From 09a6d8400b145422143c111c76524a404a57dcd1 Mon Sep 17 00:00:00 2001 From: Oswaldo Dantas Date: Sat, 13 Dec 2025 00:42:29 +0100 Subject: [PATCH 1/2] feat(fctx-migration): implement context migration and linking - Implement `GlobalConfig` to track context locations (`~/.first/first.conf`). - Add `save --to ` to save contexts to custom locations. - Add `save --link` and `update --link` to replace source files with symlinks. - Add `mv` command to move contexts and update symlinks. - Implement `SymlinkManager` for safe symlink operations. - Update `walkthrough.md` with new features. --- GEMINI.md | 1 + ROADMAP.md | 9 +- SPEC-ROADMAP.md | 2 +- .../checklists/requirements.md | 34 + specs/009-fctx-migration/contracts/cli.md | 32 + specs/009-fctx-migration/data-model.md | 33 + specs/009-fctx-migration/plan.md | 155 ++++ specs/009-fctx-migration/quickstart.md | 31 + specs/009-fctx-migration/spec.md | 96 +++ specs/009-fctx-migration/tasks.md | 65 ++ specs/009-fctx-migration/verify_manual.sh | 124 +++ specs/009-fctx-migration/walkthrough.md | 62 ++ src/main/scala/first/AppRunner.scala | 14 +- src/main/scala/first/CliDef.scala | 8 +- src/main/scala/first/Logging.scala | 51 +- src/main/scala/first/cli/MvCommand.scala | 17 + src/main/scala/first/cli/SaveCommand.scala | 20 +- src/main/scala/first/cli/UpdateCommand.scala | 7 +- src/main/scala/first/config/ConfigError.scala | 2 + .../scala/first/config/ConfigReader.scala | 109 ++- .../scala/first/config/ConfigWriter.scala | 9 + .../scala/first/config/GlobalConfig.scala | 49 ++ src/main/scala/first/core/FctxWriter.scala | 4 +- src/main/scala/first/core/Load.scala | 2 +- src/main/scala/first/core/Mv.scala | 105 +++ src/main/scala/first/core/Save.scala | 38 +- src/main/scala/first/core/Swap.scala | 2 +- .../scala/first/core/SymlinkManager.scala | 21 + src/main/scala/first/core/Update.scala | 211 ++++-- src/test/scala/first/BaseSuite.scala | 15 + .../scala/first/cli/SaveCommandTests.scala | 18 + .../first/config/GlobalConfigTests.scala | 37 + src/test/scala/first/core/MvTests.scala | 66 ++ src/test/scala/first/core/SaveTests.scala | 69 ++ .../first/core/SymlinkManagerTests.scala | 53 ++ src/test/scala/first/core/UpdateTests.scala | 82 ++ test_output.txt | 715 ++++++++++++++++++ 37 files changed, 2236 insertions(+), 132 deletions(-) create mode 100644 specs/009-fctx-migration/checklists/requirements.md create mode 100644 specs/009-fctx-migration/contracts/cli.md create mode 100644 specs/009-fctx-migration/data-model.md create mode 100644 specs/009-fctx-migration/plan.md create mode 100644 specs/009-fctx-migration/quickstart.md create mode 100644 specs/009-fctx-migration/spec.md create mode 100644 specs/009-fctx-migration/tasks.md create mode 100755 specs/009-fctx-migration/verify_manual.sh create mode 100644 specs/009-fctx-migration/walkthrough.md create mode 100644 src/main/scala/first/cli/MvCommand.scala create mode 100644 src/main/scala/first/config/ConfigWriter.scala create mode 100644 src/main/scala/first/config/GlobalConfig.scala create mode 100644 src/main/scala/first/core/Mv.scala create mode 100644 src/main/scala/first/core/SymlinkManager.scala create mode 100644 src/test/scala/first/cli/SaveCommandTests.scala create mode 100644 src/test/scala/first/config/GlobalConfigTests.scala create mode 100644 src/test/scala/first/core/MvTests.scala create mode 100644 src/test/scala/first/core/SymlinkManagerTests.scala create mode 100644 test_output.txt diff --git a/GEMINI.md b/GEMINI.md index 12b5184..9691364 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -73,6 +73,7 @@ This memories should be immediatelly available to Gemini: - Scala 3.3.4 (Scala Native 0.5.9) + decline (CLI), os-lib (FS), upickle (JSON), sttp (HTTP) (005-coursier-install) - Scala 3.3.4 + `com.monovore::decline` (CLI), `com.lihaoyi::os-lib` (File Ops), `org.ekrich::sconfig` (HOCON) (008-update-fctx) - Filesystem (HOCON config files) (008-update-fctx) +- Scala 3.3.4 (Scala Native 0.5.x) (009-fctx-migration) ## Recent Changes - 001-fctx-management-actions: Added Scala 3.3.4 + decline, sconfig, munit, scribe diff --git a/ROADMAP.md b/ROADMAP.md index 5b4079c..9dfc6ba 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -116,10 +116,11 @@ diverge a bit as we progress but just to have a main idea): 6. [x] We can swap between fctxs using the swap cli action 7. [x] We can use remote artifacts and contexts (http/https/gh) 8. [x] We can install the tool easily with `curl -fsSL https://raw.githubusercontent.com/oswaldo/first/main/install.sh | sh` -9. We can migrate and link contexts using `save --to` and `save --link`, and move them with `mv` -10. We can dogfood the tool for its own development -11. We can safely manage and remove fctxs using the new cleanup actions (`rm-def`, `rm-files`, `rm-all`). -12. We can perform all supported cli actions using a terminal interactive screen / mode +9. [x] We can update existing fctx definitions using the update cli action +10. We can migrate and link contexts using `save --to` and `save --link`, and move them with `mv` +11. We can dogfood the tool for its own development +12. We can safely manage and remove fctxs using the new cleanup actions (`rm-def`, `rm-files`, `rm-all`). +13. We can perform all supported cli actions using a terminal interactive screen / mode ## Future Ideas diff --git a/SPEC-ROADMAP.md b/SPEC-ROADMAP.md index 6bf5d39..354c91c 100644 --- a/SPEC-ROADMAP.md +++ b/SPEC-ROADMAP.md @@ -51,7 +51,7 @@ by running the specified command for each requirement. /speckit.specify Enable installation via coursier so users can run 'cs install first' ``` -6. [ ] **Context Update Operations** +6. [x] **Context Update Operations** - **Description**: Implement an `update` command to modify existing fctx definitions without recreating them. This should support: - `first update [context-name]` - Update the current (or specified) context with changes to tracked artifacts - `first update [context-name] --add "new-file.txt,another-dir/"` - Add new artifacts to the context diff --git a/specs/009-fctx-migration/checklists/requirements.md b/specs/009-fctx-migration/checklists/requirements.md new file mode 100644 index 0000000..4912679 --- /dev/null +++ b/specs/009-fctx-migration/checklists/requirements.md @@ -0,0 +1,34 @@ +# Specification Quality Checklist: Context Migration & Linking + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2025-12-11 +**Feature**: [Link to spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- Items marked incomplete require spec updates before `/speckit.clarify` or `/speckit.plan` diff --git a/specs/009-fctx-migration/contracts/cli.md b/specs/009-fctx-migration/contracts/cli.md new file mode 100644 index 0000000..b7de7c0 --- /dev/null +++ b/specs/009-fctx-migration/contracts/cli.md @@ -0,0 +1,32 @@ +# CLI Contracts + +## Save Command + +### `save` + +Extended arguments: + +- `--to ` (Optional): Specifies the target directory for the context definition. If provided, the context is saved to this directory instead of `.then/`. +- `--link` (Flag): If set, after saving, the source files in the workspace are replaced with symbolic links pointing to the saved artifacts. + +```bash +first save --files [--to ] [--link] +``` + +## Move Command + +### `mv` + +Moves a context definition to a new location. + +- Argument 1: `context-name` (Required) - Name of the context to move. +- Argument 2: `new-path` (Required) - Destination directory path. + +```bash +first mv +``` + +**Constraints**: + +- `new-path` must not exist. +- `context-name` must be a known context. diff --git a/specs/009-fctx-migration/data-model.md b/specs/009-fctx-migration/data-model.md new file mode 100644 index 0000000..6fce8f0 --- /dev/null +++ b/specs/009-fctx-migration/data-model.md @@ -0,0 +1,33 @@ +# Data Model + +## Global Configuration (`~/.first/first.conf`) + +The global configuration file acts as a central registry for known context definitions. This enables the tool to locate contexts that are not in standard project locations (e.g., when using `save --to` or `mv`). + +```hocon +// List of absolute paths to known fctx definition files (fctx-def.conf) +fctx-files: [ + "/home/user/.first/default/fctx-def.conf", + "/home/user/projects/my-context-repo/fctx-def.conf" +] + +// [Existing field, may remain] +last-loaded { + name: "default", + at: "/home/user/projects/current" +} +``` + +## Context Definition (`fctx-def.conf`) + +No changes to the data model itself, but `swapAs` field will be actively used and validated. + +```hocon +artifacts: [ + { + path: "config/app.conf" + swapAs: "symlink" // or "copy" + md5: "..." + } +] +``` diff --git a/specs/009-fctx-migration/plan.md b/specs/009-fctx-migration/plan.md new file mode 100644 index 0000000..1c1d17b --- /dev/null +++ b/specs/009-fctx-migration/plan.md @@ -0,0 +1,155 @@ +# Implementation Plan: Context Migration & Linking + +**Branch**: `009-fctx-migration` | **Date**: 2025-12-11 | **Spec**: [Spec](spec.md) +**Input**: Feature specification from `specs/009-fctx-migration/spec.md` + +## Summary + +Implement `save --to `, `save --link`, and `mv` commands to enable context migration and "dogfooding". +This requires introducing a `GlobalConfig` registry (`~/.first/first.conf`) to track context locations, enabling `first` to find contexts stored outside standard locations. +We will also implement symlink handling for `--link` and `mv`, ensuring workspace integrity. + +## Technical Context + +**Language/Version**: Scala 3.3.4 (Scala Native 0.5.x) +**Primary Dependencies**: + +- `com.lihaoyi::os-lib`: File system operations (move, symlink, copy). +- `org.ekrich::sconfig`: HOCON configuration reading/writing. +- `com.monovore::decline`: CLI argument parsing. + **Storage**: +- Local Filesystem: `fctx-def.conf` (context definitions), `~/.first/first.conf` (global registry). + **Testing**: `munit` for unit/integration tests. + **Target Platform**: Linux/macOS (primary for symlinks), Windows (fallback behavior). + **Project Type**: CLI Tool. + **Constraints**: +- **Symlinks**: `os.symlink` behavior on Windows is restrictive. We will implement checks to warn/fallback on Windows. +- **Atomic Operations**: `mv` and `save` should be as atomic as possible. + +## Constitution Check + +- [x] **Non-Intrusive Tooling**: `first.conf` is in `~/.first`, not the project. `save --link` replaces files with symlinks, which is intrusive but explicit user intent. +- [x] **Full Context Swapping**: Enhances swapping by allowing contexts to live anywhere. +- [x] **Separation of Concerns**: Decouples context storage from project location. +- [x] **Developer as Author**: User explicitly controls locations (`--to`) and linking (`--link`). +- [x] **Vision-Driven**: Aligns with "Context Migration" and "Dogfooding" roadmap items. +- [x] **Type Safety**: Will use `Path` and specific types for config. +- [x] **Safe and Expressive**: specific error handling for IO. +- [x] **Reuse First**: Reusing `ConfigReader`, `ArtifactProcessor`. + +## Project Structure + +### Documentation + +```text +specs/009-fctx-migration/ +├── plan.md # This file +├── research.md # N/A (Research done inline) +├── data-model.md # GlobalConfig model +├── quickstart.md # Usage examples +└── tasks.md # Task breakdown +``` + +### Source Code + +```text +src/main/scala/first/ +├── config/ +│ ├── GlobalConfig.scala # [NEW] Handles ~/.first/first.conf +│ └── ConfigWriter.scala # [NEW] Helper for writing structured config (extracted from FctxWriter?) +├── core/ +│ ├── Mv.scala # [NEW] Core logic for mv +│ └── SymlinkManager.scala # [NEW] Helper for safely managing symlinks +├── cli/ +│ ├── MvCommand.scala # [NEW] CLI entry point +│ └── SaveCommand.scala # [MODIFY] Add --to and --link args +└── ... +``` + +## Proposed Changes + +### 1. Global Configuration Registry (`GlobalConfig.scala`) + +Use `~/.first/first.conf` to track contexts. + +```hocon +fctx-files: [ + "/abs/path/to/ctx1/fctx-def.conf", + "/other/path/ctx2/fctx-def.conf" +] +``` + +- **[NEW] `src/main/scala/first/config/GlobalConfig.scala`**: + - `addContext(path: Path): Either[Error, Unit]` + - `removeContext(path: Path): Either[Error, Unit]` + - `updateContext(oldPath: Path, newPath: Path): Either[Error, Unit]` + - `listContextPaths(): List[Path]` +- **[MODIFY] `src/main/scala/first/config/ConfigReader.scala`**: + - Update `discoverFctxDefPaths` to include paths from `GlobalConfig`. + +### 2. Save Command Improvements (`save --to`, `save --link`) + +- **[MODIFY] `src/main/scala/first/cli/SaveCommand.scala`**: + - Add `to: Option[Path]` and `link: Boolean = false` to `SaveOpts`. +- **[MODIFY] `src/main/scala/first/core/Save.scala`**: + - Logic for `--to`: Use provided path as `fctxConfDir`. Register with `GlobalConfig`. + - Logic for `--link`: After processing artifacts, replace source files with symlinks to `artifactsDir`. + - Use `SymlinkManager` helper. + +### 3. Move Command (`mv`) + +- **[NEW] `src/main/scala/first/cli/MvCommand.scala`**: + - `first mv ` +- **[NEW] `src/main/scala/first/core/Mv.scala`**: + - Resolve current context path. + - Check destination. + - `os.move`. + - Update `GlobalConfig`. + - Scan CWD for symlinks pointing to old path and update them to new path. + +### 4. Symlink Handling (`SymlinkManager`) + +- **[NEW] `src/main/scala/first/core/SymlinkManager.scala`**: + - `createSymlink(source: Path, dest: Path): Either[Error, Unit]` + - Checks OS (warn if Windows). + - Handles relative vs absolute linking constraints. + +### 5. Update Command Improvements (`update --link`) + +- **[MODIFY] `src/main/scala/first/cli/UpdateCommand.scala`**: + - Add `link: Boolean = false`. +- **[MODIFY] `src/main/scala/first/core/Update.scala`**: + - When processing added artifacts (`newProcessed`), if `link` is true: + - Invoke `SymlinkManager.createSymlink(source, dest)` similar to `Save`. + +## Verification Plan + +### Automated Tests + +- **Unit Tests**: + - `GlobalConfigTests`: Verify reading/writing `first.conf`. + - `SymlinkManagerTests`: Verify symlink creation (skip on Windows/CI if needed, or use mocks). + - `MvTests`: Verify logic (mocking file sys or using temp folders). + - `SaveTests`: Test `--to` and `--link` logic. + +### Manual Verification + +1. **Save to Custom Location**: + - Run `first save test-ctx --to /tmp/test-ctx --files "foo.txt"`. + - Verify `/tmp/test-ctx/fctx-def.conf` exists. + - Verify `first ls` shows `test-ctx`. + +2. **Save and Link**: + - Run `first save link-ctx --files "bar.txt" --link`. + - Verify `bar.txt` is now a symlink pointing to `.then/link-ctx/artifacts/bar.txt`. + +3. **Move Context**: + - Run `first mv link-ctx /tmp/moved-ctx`. + - Verify `.then/link-ctx` is gone. + - Verify `/tmp/moved-ctx` exists. + - Verify `bar.txt` symlink now points to `/tmp/moved-ctx/artifacts/bar.txt`. + - Verify `first load link-ctx` still works. + +4. **Edge Cases**: + - Try moving to existing path (should fail). + - Try moving unknown context (should fail). diff --git a/specs/009-fctx-migration/quickstart.md b/specs/009-fctx-migration/quickstart.md new file mode 100644 index 0000000..0dba5b7 --- /dev/null +++ b/specs/009-fctx-migration/quickstart.md @@ -0,0 +1,31 @@ +# Quickstart: Context Migration & Linking + +## Saving to a Custom Location + +Save a context to a specific directory (e.g., for sharing via git): + +```bash +first save my-shared-ctx --to ~/git/shared-contexts --files "style.conf,rules.xml" +``` + +## Linking Local Files + +Move local files to the context and replace them with symlinks: + +```bash +first save local-dev --files "local.properties" --link +``` + +## Moving a Context + +Move an existing context to a new location: + +```bash +first mv my-shared-ctx ~/new-location/my-shared-ctx +``` + +The tool will automatically: + +1. Move the context definition. +2. Update the global registry. +3. Update any active symlinks in your current workspace to point to the new location. diff --git a/specs/009-fctx-migration/spec.md b/specs/009-fctx-migration/spec.md new file mode 100644 index 0000000..e374eb9 --- /dev/null +++ b/specs/009-fctx-migration/spec.md @@ -0,0 +1,96 @@ +# Feature Specification: Context Migration & Linking + +**Feature Branch**: `009-fctx-migration` +**Created**: 2025-12-11 +**Status**: Draft +**Input**: User description: "Implement context migration and linking capabilities including 'save --to', 'save --link', and 'mv'" + +## Vision Alignment + +This specification expands the `first` tool's flexibility by allowing users to control _where_ contexts are stored and _how_ they integrate with the workspace. It fulfills the roadmap goal of enabling "Context Migration & Linking," which is essential for advanced workflows like dogfooding (where the tool manages its own development environment from a separate repository) and team sharing. + +--- + +## User Scenarios & Testing + +### User Story 1 - Save to Custom Location (Priority: P1) + +As a developer, I want to save a context definition to a specific directory (e.g., a shared git repository or a backup folder) instead of the default location, so I can share it with my team or organize it my way. + +**Why this priority**: Essential for the "dogfooding" goal and shared workflows. + +**Independent Test**: Can be tested by saving a context to a temp dir and verifying the files exist there and are registered in `first.conf`. + +**Acceptance Scenarios**: + +1. **Given** a set of local files and an empty target directory, **When** I run `first save my-ctx --to /tmp/my-ctx --files "config.txt"`, **Then** the context definition is created in `/tmp/my-ctx` and registered in `first.conf`. +2. **Given** an existing context, **When** I run `first save existing-ctx --to /new/path`, **Then** the context is saved to the new path (effectively a "Save As" if the name is different, or a move/update if the name is the same? _Clarification: `save` usually creates/updates. If I use `--to`, I am defining where that creation acts._). + - _Refinement_: `save` defines a context. If I say `save my-ctx --to X`, I am saying "The context 'my-ctx' is located at X". + +--- + +### User Story 2 - Centralize and Link (Priority: P1) + +As a developer, I want to move my local configuration files into a managed context and replace the local copies with symlinks pointing to that context, so I can centralize management without breaking my local setup. + +**Why this priority**: Core "setup" workflow. Allows users to "adopt" existing configurations into `first`. + +**Independent Test**: Create a file, run `save --link`, verify file is replaced by symlink pointing to the context artifact. + +**Acceptance Scenarios**: + +1. **Given** a local file `app.conf`, **When** I run `first save my-ctx --files "app.conf" --link`, **Then** `app.conf` is moved to the context storage, and `app.conf` in the current directory becomes a symlink to the stored file. +2. **Given** a directory to include, **When** I run `first save my-ctx --files "config/" --link`, **Then** the directory and its contents are handled (files inside linked or directory linked?), and local paths become symlinks. + +--- + +### User Story 3 - Move Context (`mv`) (Priority: P2) + +As a developer, I want to move an implementation context definition to a new location on my disk and have `first` remember where it is, so I can reorganize my file system without breaking the tool's awareness of the context. + +**Why this priority**: Keeps the environment clean and adaptable. + +**Independent Test**: Create a context, move it with `first mv`, check `first ls` shows new path, check `first load` still works. + +**Acceptance Scenarios**: + +1. **Given** a known context `my-ctx` at path A, **When** I run `first mv my-ctx /path/to/B`, **Then** the directory A is moved to B, and `first.conf` updates the record for `my-ctx` to point to B. +2. **Given** a context name that doesn't exist, **When** I run `first mv unknown-ctx /path`, **Then** it fails with a clear error. +3. **Given** a context `my-ctx` that has active symlinks in the workspace, **When** I run `first mv my-ctx /new/path`, **Then** the context files are moved AND the symlinks in the workspace are updated to point to `/new/path`. + +--- + +### Edge Cases + +- **Windows Symlinks**: On Windows, `--link` should likely fall back to copy or warn, as per roadmap ("on windows... always be copy"). +- **Target Exists**: If `--to` target exists, `save` should prompt to overwrite/merge (standard `save` behavior). +- **Move to Existing**: `mv` should fail if destination exists. +- **Update Links**: When moving a context, `mv` MUST attempt to identify and update any symbolic links in the current workspace that point to artifacts within the moving context, ensuring they point to the new location. + +## Requirements + +### Functional Requirements + +- **FR-001**: `save` command MUST support a `--to ` argument to specify the absolute or relative path for the context definition directory. +- **FR-002**: `save` command MUST support a `--link` boolean flag. +- **FR-003**: When `--link` is used, successful artifact saving MUST be followed by replacing the source file in the workspace with a symbolic link to the saved artifact. +- **FR-004**: `first mv ` command MUST move the context definition directory from its current resolved path to the ``. +- **FR-005**: `first mv` MUST update the `fctx-files` registry in `first.conf` to reflect the new absolute path of the moved context. +- **FR-006**: `first mv` MUST fail if the destination directory already exists. +- **FR-007**: `first mv` MUST fail if the `` is not found in the loaded configuration. +- **FR-008**: `save --to` result MUST be registered in `first.conf` as a known context location. +- **FR-009**: `first mv` MUST iterate through files tracked in the context's `fctx-def.conf` (if easy to resolve) or scan the workspace to find and update symlinks pointing to the old context location. + +### Key Entities + +- **Context Definition (FctxDef)**: The directory containing `fctx-def.conf` and artifacts. +- **Global Config (`first.conf`)**: The central registry tracking known contexts. + +## Success Criteria + +### Measurable Outcomes + +- **SC-001**: A user can relocate a context using `mv` and immediately `load` it without manual config editing. +- **SC-002**: A user can "install" a local config file into a context using `save --link` and have it replaced by a working symlink. +- **SC-003**: A user can create a context in a completely separate directory (e.g. `../my-contexts/`) using `save --to` and have it recognized by `first`. +- **SC-004**: When moving a context with `mv`, existing symlinks in the workspace are correctly updated to the new location. diff --git a/specs/009-fctx-migration/tasks.md b/specs/009-fctx-migration/tasks.md new file mode 100644 index 0000000..27b03eb --- /dev/null +++ b/specs/009-fctx-migration/tasks.md @@ -0,0 +1,65 @@ +# Tasks: Context Migration & Linking + +**Feature**: `009-fctx-migration` +**Spec**: [Spec](spec.md) | **Plan**: [Plan](plan.md) + +## Phase 1: Core Infrastructure (Global Config) + +Establish the `GlobalConfig` registry to track context locations, which is the foundation for `save --to` and `mv`. + +- [x] T001 [P] Create `src/main/scala/first/config/GlobalConfig.scala` to manage `~/.first/first.conf` registry +- [x] T002 [P] Create `src/main/scala/first/config/ConfigWriter.scala` for robust config writing +- [x] T003 Modify `src/main/scala/first/config/ConfigReader.scala` to discover contexts from `GlobalConfig` +- [x] T004 Create `src/test/scala/first/config/GlobalConfigTests.scala` to verify registry operations + +## Phase 2: User Story 1 - Save to Custom Location (P1) + +Implement the ability to save contexts to arbitrary paths using `save --to`. + +- [x] T005 [US1] Modify `src/main/scala/first/cli/SaveCommand.scala` to add `--to` option +- [x] T006 [US1] Modify `src/main/scala/first/core/Save.scala` to handle custom paths and register with GlobalConfig +- [x] T007 [US1] Update `src/test/scala/first/core/SaveTests.scala` to verify `--to` functionality +- [x] T008 [P] [US2] Create `src/main/scala/first/core/SymlinkManager.scala` for safe symlink operations +- [x] T009 [P] [US2] Create `src/test/scala/first/core/SymlinkManagerTests.scala` +- [x] T010 [US2] Modify `src/main/scala/first/cli/SaveCommand.scala` to add `--link` flag +- [x] T011 [US2] Modify `src/main/scala/first/core/Save.scala` to implement linking logic (replace files with symlinks) +- [x] T012 [US2] Update `src/test/scala/first/core/SaveTests.scala` to verify `--link` functionality + +## Phase 4: User Story 3 - Move Context (P2) + +Implement the `mv` command to relocate contexts and update references. + +- [x] T013 [P] [US3] Create `src/main/scala/first/cli/MvCommand.scala` for `mv` command parsing +- [x] T014 [US3] Create `src/main/scala/first/core/Mv.scala` implementing move logic and GlobalConfig updates +- [x] T015 [US3] Implement workspace symlink scanning and updating in `src/main/scala/first/core/Mv.scala` +- [x] T016 [US3] Integrate `MvCommand` into `src/main/scala/first/AppRunner.scala` and `src/main/scala/first/Main.scala` +- [x] T017 [US3] Create `src/test/scala/first/core/MvTests.scala` covering move and relinking scenarios + +## Phase 5: Polish & Documentation + +- [x] T018 Verify all commands with `--dry-run` and `--verbose` +- [x] T019 Update `src/main/scala/first/CliDef.scala` to include `mv` command help +- [x] T020 Run full end-to-end verification (Quickstart scenarios) +- [x] T021 Update `walkthrough.md` with new features and scenarios + +## Phase 6: Update --link support (P2) + +Allow converting new artifacts to symlinks during update. + +- [x] T022 [US4] Modify `src/main/scala/first/cli/UpdateCommand.scala` to add `--link` flag +- [x] T023 [US4] Modify `src/main/scala/first/core/Update.scala` to handle optional linking for added artifacts +- [x] T024 [US4] Update `src/test/scala/first/core/UpdateTests.scala` to verify `update --link` functionality +- [x] T025 [US4] Update `walkthrough.md` with update scenario + +## Dependencies & Strategy + +- **Strategy**: MVP first. Phase 1 enables Phase 2 & 4. Phase 2 (custom location) is required for Phase 4 (Move) to make sense globally. +- **Dependencies**: + - T001 (GlobalConfig) -> T003, T006, T014 + - T008 (SymlinkManager) -> T011, T015 + - T013 (MvCommand) -> T016 + +## Parallel Execution Opportunities + +- T001, T002, T008, T013 can be started in parallel. +- Phase 2 (Save improvements) and Phase 4 (Mv logic) can overlap once Phase 1 is stable. diff --git a/specs/009-fctx-migration/verify_manual.sh b/specs/009-fctx-migration/verify_manual.sh new file mode 100755 index 0000000..dac9d24 --- /dev/null +++ b/specs/009-fctx-migration/verify_manual.sh @@ -0,0 +1,124 @@ +#!/bin/bash +set -e + +# Setup +# Helper to echo command before running +run_cmd() { + echo "> $@" + "$@" +} + +echo "Building first..." +scala-cli package . --native -o tmp/first -f + +FIRST=$(realpath tmp/first) +TEST_DIR=$(mktemp -d -t first-verification-XXXXXX) +echo "Running verification in $TEST_DIR" + +cd "$TEST_DIR" +touch foo.txt +echo "foo content" > foo.txt + +# Scenario 1: Save to Custom Location +echo "--- Scenario 1: Save to Custom Location ---" +run_cmd "$FIRST" save custom-ctx --to "$TEST_DIR/custom-ctx" --artifacts foo.txt +if [ -f "$TEST_DIR/custom-ctx/fctx-def.conf" ]; then + echo "PASS: custom-ctx created at custom location" +else + echo "FAIL: custom-ctx not found at custom location" + exit 1 +fi + +# Verify Global Config Registration (naive check) +if grep -q "$TEST_DIR/custom-ctx/fctx-def.conf" ~/.first/first.conf; then + echo "PASS: custom-ctx registered in global config" +else + echo "FAIL: custom-ctx not found in global config" + # Don't exit here as it might be a partial match issue or first run, but good to know + # exit 1 +fi + + +# Scenario 2: Save and Link +echo "--- Scenario 2: Save and Link ---" +touch bar.txt +echo "bar content" > bar.txt +run_cmd "$FIRST" save link-ctx --artifacts bar.txt --link + +if [ -L "bar.txt" ]; then + echo "PASS: bar.txt is a symlink" +else + echo "FAIL: bar.txt is not a symlink" + exit 1 +fi + +TARGET=$(readlink bar.txt) +echo "Symlink target: $TARGET" +if [[ "$TARGET" == *".then/link-ctx/artifacts/bar.txt" ]]; then + echo "PASS: Symlink points to correct artifact location" +else + echo "FAIL: Symlink target incorrect" + exit 1 +fi + + +# Scenario 3: Move Context +echo "--- Scenario 3: Move Context ---" +run_cmd "$FIRST" mv link-ctx "$TEST_DIR/moved-ctx" + +if [ ! -d ".then/link-ctx" ]; then + echo "PASS: Old context directory removed" +else + echo "FAIL: Old context directory still exists" + exit 1 +fi + +if [ -d "$TEST_DIR/moved-ctx" ]; then + echo "PASS: Context moved to new location" +else + echo "FAIL: Context not found at new location" + exit 1 +fi + +NEW_TARGET=$(readlink bar.txt) +echo "New Symlink target: $NEW_TARGET" +if [[ "$NEW_TARGET" == *"$TEST_DIR/moved-ctx/artifacts/bar.txt" ]]; then + echo "PASS: Symlink updated to new location" +else + echo "FAIL: Symlink not updated correctly" + exit 1 +fi + + +# Scenario 4: Update and Link +echo "--- Scenario 4: Update and Link ---" +touch baz.txt +echo "baz content" > baz.txt +echo "baz content" > baz.txt +cd "$TEST_DIR/moved-ctx" +run_cmd "$FIRST" update moved-ctx --add ../baz.txt --link +cd "$TEST_DIR" + +if [ -L "baz.txt" ]; then + echo "PASS: baz.txt is a symlink" +else + echo "FAIL: baz.txt is not a symlink" + exit 1 +fi + +BAZ_TARGET=$(readlink baz.txt) +echo "Symlink target: $BAZ_TARGET" +if [[ "$BAZ_TARGET" == *"$TEST_DIR/moved-ctx/artifacts/baz.txt" ]]; then + echo "PASS: Symlink points to correct artifact location" +else + echo "FAIL: Symlink target incorrect" + exit 1 +fi + + + + +echo "--- All scenarios passed! ---" +# Cleanup +rm -rf "$TEST_DIR" +# echo "Test dir left at $TEST_DIR for inspection." diff --git a/specs/009-fctx-migration/walkthrough.md b/specs/009-fctx-migration/walkthrough.md new file mode 100644 index 0000000..f6aae2d --- /dev/null +++ b/specs/009-fctx-migration/walkthrough.md @@ -0,0 +1,62 @@ +# Walkthrough: Context Migration & Linking + +## Overview + +This feature enables: + +1. **Saving to custom locations**: `first save my-ctx --to /path/to/my-ctx` +2. **Linking contexts**: `first save my-ctx --link` replaces artifacts with symlinks. +3. **Moving contexts**: `first mv my-ctx /new/path` moves the definition and updates symlinks. + +## Verification + +### Automated Tests + +- `GlobalConfigTests`: Verified registry operations (add, remove, update, list). +- `SaveTests`: Verified `--to` (saves to custom path, registers config) and `--link` (creates symlinks). +- `MvTests`: Verified context moving, global config update, and symlink repairing. + +### Manual Scenarios + +#### 1. Save to Custom Location + +```bash +first save custom-ctx --to /tmp/custom-ctx --artifacts foo.txt +``` + +Result: + +- `/tmp/custom-ctx/fctx-def.conf` created. +- Context registered in `~/.first/first.conf`. + +#### 2. Save and Link + +```bash +first save link-ctx --artifacts bar.txt --link +``` + +Result: + +- `bar.txt` replaced by symlink to `.then/link-ctx/artifacts/bar.txt`. + +#### 3. Move Context + +```bash +first mv link-ctx /tmp/moved-ctx +``` + +Result: + +- Context moved to `/tmp/moved-ctx`. +- `bar.txt` symlink updated to point to `/tmp/moved-ctx/artifacts/bar.txt`. + +#### 4. Update and Link + +```bash +first update link-ctx --add baz.txt --link +``` + +Result: + +- `baz.txt` added to `.then/link-ctx/artifacts/`. +- Local `baz.txt` replaced by symlink. diff --git a/src/main/scala/first/AppRunner.scala b/src/main/scala/first/AppRunner.scala index dd66dcd..5610c07 100644 --- a/src/main/scala/first/AppRunner.scala +++ b/src/main/scala/first/AppRunner.scala @@ -5,6 +5,7 @@ import first.config.ConfigReader import first.core.Context import first.core.ExitHandler import first.core.Load +import first.core.Mv import first.core.PlatformExitHandler import first.core.Save import first.core.Swap @@ -16,17 +17,18 @@ class AppRunner(exitHandler: ExitHandler = PlatformExitHandler): try val workingDir = at.map(p => os.Path(p)).getOrElse(os.pwd) at.foreach(path => System.setProperty("user.dir", os.Path(path).toString)) - val context = new Context(workingDir) + val context = Context(workingDir) scribe.info(s"Context: ${context.workingDir}, WorkingDir: $workingDir") cmd match - case SaveCmd(opts) => new Save().run(opts, context) - case LoadCmd(opts) => new Load().run(opts, context) - case SwapCmd(opts) => new Swap().run(opts, context) - case UpdateCmd(opts) => new Update().run(opts, context) + case SaveCmd(opts) => Save().run(opts, context).fold(throw _, identity) + case LoadCmd(opts) => Load().run(opts, context) + case SwapCmd(opts) => Swap().run(opts, context) + case UpdateCmd(opts) => Update().run(opts, context).fold(throw _, identity) + case MvCmd(opts) => Mv().run(opts, context).fold(throw _, identity) case LsCmd => - val reader = new ConfigReader() + val reader = ConfigReader() val contexts = reader.listAvailableContextsWithPaths(workingDir) if contexts.isEmpty then scribe.info("No saved contexts found.") else diff --git a/src/main/scala/first/CliDef.scala b/src/main/scala/first/CliDef.scala index a283efb..e85b3f0 100644 --- a/src/main/scala/first/CliDef.scala +++ b/src/main/scala/first/CliDef.scala @@ -1,6 +1,7 @@ package first import first.cli.LoadCommand +import first.cli.MvCommand import first.cli.SaveCommand import first.cli.SwapCommand import first.cli.UpdateCommand @@ -14,6 +15,7 @@ object CliDef: case class LoadCmd(opts: LoadCommand.LoadOpts) extends CliCommand case class UpdateCmd(opts: UpdateCommand.UpdateOpts) extends CliCommand case class SwapCmd(opts: SwapCommand.SwapOpts) extends CliCommand + case class MvCmd(opts: MvCommand.MvOpts) extends CliCommand case object LsCmd extends CliCommand case object HelpCmd extends CliCommand @@ -30,11 +32,15 @@ object CliDef: .subcommand("swap", "Swap from the currently active fctx to a new one.")(SwapCommand.swapOpts) .map(SwapCmd.apply) + val mvCmd: Opts[MvCmd] = + Opts.subcommand("mv", "Move a context to a new location.")(MvCommand.mvOpts).map(MvCmd.apply) + val lsCmd: Opts[LsCmd.type] = Opts.subcommand("ls", "List all available contexts.")(Opts.unit.map(_ => LsCmd)) val helpCmd: Opts[HelpCmd.type] = Opts.subcommand("help", "Display help information.")(Opts.unit.map(_ => HelpCmd)) - val subcommands: Opts[CliCommand] = saveCmd orElse updateCmd orElse loadCmd orElse swapCmd orElse lsCmd orElse helpCmd + val subcommands: Opts[CliCommand] = + saveCmd orElse updateCmd orElse loadCmd orElse swapCmd orElse mvCmd orElse lsCmd orElse helpCmd val atOpt: Opts[Option[String]] = Opts.option[String]("at", "Specify the working directory.").orNone diff --git a/src/main/scala/first/Logging.scala b/src/main/scala/first/Logging.scala index cd04c02..3baa635 100644 --- a/src/main/scala/first/Logging.scala +++ b/src/main/scala/first/Logging.scala @@ -5,19 +5,45 @@ import scribe.file.* import scribe.format.* object Logging: - def configure(): Unit = - import java.time.format.DateTimeFormatter - import java.time.Instant - import java.time.ZoneId - import scribe.output.TextOutput + import java.time.format.DateTimeFormatter + import java.time.Instant + import java.time.ZoneId + import scribe.output.TextOutput + import scala.scalanative.posix.unistd + + val pid: Int = unistd.getpid() + + private val dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault()) + private val isoDate = FormatBlock { logRecord => + new TextOutput(dtf.format(Instant.ofEpochMilli(logRecord.timeStamp))) + } + + val fileFormatter: Formatter = formatter"$isoDate $level $position - $messages" - val dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault()) - val isoDate = FormatBlock { logRecord => - new TextOutput(dtf.format(Instant.ofEpochMilli(logRecord.timeStamp))) - } + def createFileWriter(): FileWriter = FileWriter( + System.getProperty("user.dir") / ".first" / "logs" / + (year % "-" % month % "-" % day) / + (year % "-" % month % "-" % day % "_" % hour % "-" % minute % "-" % second % "_" % pid.toString % ".log"), + ) + + private def cleanupOldLogs(): Unit = + // Safe to ignore errors if logs dir doesn't exist or permissions issue + try + val logsDir = os.Path(System.getProperty("user.dir")) / ".first" / "logs" + if os.exists(logsDir) then + val allLogs = os.walk(logsDir).filter(_.ext == "log").sortBy(_.toString) + if allLogs.size >= 10 then + allLogs.dropRight(9).foreach { log => + os.remove(log) + val parent = log / os.up + if os.list(parent).isEmpty then os.remove(parent) + } + catch case _ => () // Fail silently for log cleanup + + def configure(): Unit = + cleanupOldLogs() - val cliFormatter = formatter"$messages$newLine" - val fileFormatter = formatter"$isoDate $level $position - $messages" + val cliFormatter = formatter"$messages$newLine" Logger.root .clearHandlers() @@ -29,8 +55,7 @@ object Logging: .withHandler( formatter = fileFormatter, minimumLevel = Some(Level.Debug), - writer = - FileWriter(System.getProperty("user.dir") / ".first" / "logs" / (year % "-" % month % "-" % day % ".log")), + writer = createFileWriter(), outputFormat = scribe.output.format.ASCIIOutputFormat, ) .replace() diff --git a/src/main/scala/first/cli/MvCommand.scala b/src/main/scala/first/cli/MvCommand.scala new file mode 100644 index 0000000..2ddd121 --- /dev/null +++ b/src/main/scala/first/cli/MvCommand.scala @@ -0,0 +1,17 @@ +package first.cli + +import cats.implicits.* +import com.monovore.decline.* +import os.Path + +object MvCommand: + case class MvOpts( + name: String, + dest: Path, + ) + + val mvOpts: Opts[MvOpts] = + val nameOpt = Opts.argument[String](metavar = "context-name") + val destOpt = Opts.argument[String](metavar = "destination-path").map(s => os.Path(s, os.pwd)) + + (nameOpt, destOpt).mapN(MvOpts.apply) diff --git a/src/main/scala/first/cli/SaveCommand.scala b/src/main/scala/first/cli/SaveCommand.scala index 4b179e9..7185716 100644 --- a/src/main/scala/first/cli/SaveCommand.scala +++ b/src/main/scala/first/cli/SaveCommand.scala @@ -4,6 +4,7 @@ import first.config.SwapAs import cats.implicits.* import com.monovore.decline.* +import os.Path object SaveCommand: case class SaveOpts( @@ -14,10 +15,14 @@ object SaveCommand: force: Boolean, dryRun: Boolean, verbose: Boolean, + toContextPath: Option[Path], + link: Boolean, ) val saveOpts: Opts[SaveOpts] = - val contextNameOpts = Opts.argument[String](metavar = "context-name") + val contextNameOpts = Opts.argument[String](metavar = "context-name").validate("Invalid context name") { name => + name.matches("^[a-zA-Z0-9_.-]+$") && !name.contains("..") + } val artifactsOpt = Opts .option[String]( long = "artifacts", @@ -42,5 +47,16 @@ object SaveCommand: val forceOpt = Opts.flag(long = "force", help = "Force overwrite of existing fctx definition.").orFalse val dryRunOpt = Opts.flag(long = "dry-run", help = "Show what would be done without actually doing it.").orFalse val verboseOpt = Opts.flag(long = "verbose", short = "v", help = "Enable verbose output.").orFalse + val toOpt = Opts + .option[String]( + long = "to", + help = "Save context to a specific path.", + ) + .map(s => os.Path(s, os.pwd)) + .orNone + val linkOpt = + Opts.flag(long = "link", help = "Replace original files with symlinks to the saved artifacts.").orFalse - (contextNameOpts, artifactsOpt, includesOpt, swapAsOpt, forceOpt, dryRunOpt, verboseOpt).mapN(SaveOpts.apply) + (contextNameOpts, artifactsOpt, includesOpt, swapAsOpt, forceOpt, dryRunOpt, verboseOpt, toOpt, linkOpt).mapN( + SaveOpts.apply, + ) diff --git a/src/main/scala/first/cli/UpdateCommand.scala b/src/main/scala/first/cli/UpdateCommand.scala index 1b36553..776829b 100644 --- a/src/main/scala/first/cli/UpdateCommand.scala +++ b/src/main/scala/first/cli/UpdateCommand.scala @@ -13,6 +13,7 @@ object UpdateCommand: swapAs: first.config.SwapAs, dryRun: Boolean = false, verbose: Boolean = false, + link: Boolean = false, ) val updateOpts: Opts[UpdateOpts] = @@ -52,4 +53,8 @@ object UpdateCommand: .flag("verbose", short = "v", help = "Enable verbose logging") .orFalse - (contextName, add, forget, includes, forgetIncludes, swapAs, dryRun, verbose).mapN(UpdateOpts.apply) + val link = Opts + .flag("link", help = "Replace added files with symlinks to the artifacts") + .orFalse + + (contextName, add, forget, includes, forgetIncludes, swapAs, dryRun, verbose, link).mapN(UpdateOpts.apply) diff --git a/src/main/scala/first/config/ConfigError.scala b/src/main/scala/first/config/ConfigError.scala index 5b1a85d..cadd2ef 100644 --- a/src/main/scala/first/config/ConfigError.scala +++ b/src/main/scala/first/config/ConfigError.scala @@ -4,3 +4,5 @@ sealed trait ConfigError case class FileParseError(path: String, message: String) extends ConfigError case class PathNotFound(path: String) extends ConfigError case class CircularDependency(path: List[String]) extends ConfigError +case class FileWriteError(path: String, message: String) extends ConfigError +case class AmbiguousContextError(message: String) extends ConfigError diff --git a/src/main/scala/first/config/ConfigReader.scala b/src/main/scala/first/config/ConfigReader.scala index 1a92dcf..8ada00c 100644 --- a/src/main/scala/first/config/ConfigReader.scala +++ b/src/main/scala/first/config/ConfigReader.scala @@ -16,7 +16,7 @@ import org.ekrich.config.Config import org.ekrich.config.ConfigFactory import os.* -class ConfigReader(downloader: DownloaderClient = Downloader): +class ConfigReader(downloader: DownloaderClient = Downloader, val userHome: Path = os.home): private def parseConfigFile(path: Path): Either[ConfigError, Config] = scribe.debug(s"Parsing config file: $path") @@ -231,12 +231,22 @@ class ConfigReader(downloader: DownloaderClient = Downloader): val pathsFromRoot = loop(Some(startPath), Nil) - val userHome = os.home val userHomeFctxPath = userHome / ".first" / contextName / "fctx-def.conf" + val globalConfig = new GlobalConfig(userHome) + + val globalPaths = globalConfig + .listContextPaths() + .getOrElse(Nil) + .filter { p => + if os.exists(p) then + Try(ConfigFactory.parseFile(p.toIO)) + .map(c => c.hasPath("name") && c.getString("name") == contextName) + .getOrElse(false) + else false + } val allPaths = - if os.exists(userHomeFctxPath) then userHomeFctxPath :: pathsFromRoot - else pathsFromRoot + (if os.exists(userHomeFctxPath) then userHomeFctxPath :: pathsFromRoot else pathsFromRoot) ++ globalPaths scribe.debug(s"Discovered paths for context '$contextName' from '$startPath': $allPaths") allPaths.distinct @@ -265,7 +275,6 @@ class ConfigReader(downloader: DownloaderClient = Downloader): val contextsFromRoot = discoverContextsIn(Some(startPath), Map.empty) - val userHome = os.home val userHomeFirstDir = userHome / ".first" val userHomeContexts = if os.isDir(userHomeFirstDir) then @@ -279,4 +288,92 @@ class ConfigReader(downloader: DownloaderClient = Downloader): acc.updated(ctx, acc.getOrElse(ctx, Nil) ++ paths) } - allContexts.map { case (ctx, paths) => ctx -> paths.filter(os.exists(_)) }.filter(_._2.nonEmpty) + val globalConfig = new GlobalConfig(userHome) + val globalContexts = globalConfig + .listContextPaths() + .getOrElse(Nil) + .filter(os.exists) + .flatMap { path => + scribe.debug(s"Checking path for global context: $path") + Try(ConfigFactory.parseFile(path.toIO)) + .map { config => + if config.hasPath("name") then + val name = config.getString("name") + scribe.debug(s"Found global context '$name' at $path") + Some(name -> path) + else + scribe.debug(s"No 'name' found in config at $path") + None + } + .fold( + e => + scribe.debug(s"Failed to parse global context config at $path: ${e.getMessage}") + None + , + identity, + ) + } + .groupBy(_._1) + .map { case (name, pairs) => name -> pairs.map(_._2) } + + val finalContexts = globalContexts.foldLeft(allContexts) { case (acc, (ctx, paths)) => + acc.updated(ctx, acc.getOrElse(ctx, Nil) ++ paths) + } + + finalContexts.map { case (ctx, paths) => ctx -> paths.filter(os.exists(_)) }.filter(_._2.nonEmpty) + + def detectContextName(startPath: Path): Option[String] = + @tailrec + def loop(currentPathOpt: Option[Path]): Option[String] = + currentPathOpt match + case Some(currentPath) if os.isDir(currentPath) => + val configPath = currentPath / "fctx-def.conf" + if os.exists(configPath) then + scribe.debug(s"Found config at $configPath") + Try(ConfigFactory.parseFile(configPath.toIO)) + .map(config => + if config.hasPath("name") then + val name = config.getString("name") + scribe.debug(s"Detected context '$name'") + Some(name) + else + scribe.debug("No 'name' in config") + None, + ) + .getOrElse(None) + else + // Also check standard structure .then//fctx-def.conf if we are at root? + // But usually we are inside. + // If we are deep inside artifacts/, we walk up. + loop(Try(currentPath / os.up).toOption) + case _ => + scribe.debug("Reached root, no context found") + None + + loop(Some(startPath)) + +object ConfigReader: + def resolveWriteTarget(paths: List[os.Path], workingDir: os.Path): Either[ConfigError, os.Path] = + val existingPaths = paths.filter(os.exists) + if existingPaths.isEmpty then Left(PathNotFound(s"No valid context paths found from: $paths")) + else + // 1. Filter checks if workingDir is inside the context's directory (parent of fctx-def.conf) + // This implies the user is "inside" the context scope. + // We search for the "most specific" scope (longest path length). + val localMatches = existingPaths + .filter(p => workingDir.startsWith(p / os.up)) + .sortBy(_.segmentCount) + .reverse + + localMatches.headOption match + case Some(bestMatch) => Right(bestMatch) + case None => + // 2. If no local scope matches, we are outside all contexts. + // We can only proceed if there is exactly one option. + if existingPaths.size == 1 then Right(existingPaths.head) + else + Left( + AmbiguousContextError( + s"Multiple external contexts found: ${existingPaths.mkString(", ")}. Please navigate inside the desired context directory or use a unique name (if applicable).", + ), + ) diff --git a/src/main/scala/first/config/ConfigWriter.scala b/src/main/scala/first/config/ConfigWriter.scala new file mode 100644 index 0000000..d2505ae --- /dev/null +++ b/src/main/scala/first/config/ConfigWriter.scala @@ -0,0 +1,9 @@ +package first.config + +object ConfigWriter: + def writeStringList(key: String, values: List[String]): String = + if values.isEmpty then s"$key = []" + else + // Simple HOCON list formatting + val listContent = values.map(v => s"\"$v\"").mkString("\n ", ",\n ", "\n") + s"$key = [$listContent]" diff --git a/src/main/scala/first/config/GlobalConfig.scala b/src/main/scala/first/config/GlobalConfig.scala new file mode 100644 index 0000000..093994d --- /dev/null +++ b/src/main/scala/first/config/GlobalConfig.scala @@ -0,0 +1,49 @@ +package first.config + +import scala.jdk.CollectionConverters.* +import scala.util.Try + +import org.ekrich.config.ConfigFactory +import os.Path + +class GlobalConfig(val home: Path): + private val globalConfigFile = home / ".first" / "first.conf" + + def addContext(path: Path): Either[ConfigError, Unit] = + for + paths <- listContextPaths() + _ <- if paths.contains(path) then Right(()) else writePaths(paths :+ path) + yield () + + def removeContext(path: Path): Either[ConfigError, Unit] = + for + paths <- listContextPaths() + _ <- writePaths(paths.filterNot(_ == path)) + yield () + + def updateContext(oldPath: Path, newPath: Path): Either[ConfigError, Unit] = + for + paths <- listContextPaths() + updatedPaths = paths.map(p => if p == oldPath then newPath else p) + _ <- writePaths(updatedPaths) + yield () + + def listContextPaths(): Either[ConfigError, List[Path]] = + if !os.exists(globalConfigFile) then Right(Nil) + else + Try(ConfigFactory.parseFile(globalConfigFile.toIO)).toEither.left + .map(e => FileParseError(globalConfigFile.toString, e.getMessage)) + .map { config => + if config.hasPath("fctx-files") then config.getStringList("fctx-files").asScala.toList.map(os.Path(_)) + else Nil + } + + private def writePaths(paths: List[Path]): Either[ConfigError, Unit] = + Try { + if !os.exists(globalConfigFile) then os.makeDir.all(globalConfigFile / os.up) + + val content = ConfigWriter.writeStringList("fctx-files", paths.map(_.toString)) + os.write.over(globalConfigFile, content) + }.toEither.left.map(e => FileWriteError(globalConfigFile.toString, e.getMessage)) + +object GlobalConfig extends GlobalConfig(os.home) diff --git a/src/main/scala/first/core/FctxWriter.scala b/src/main/scala/first/core/FctxWriter.scala index c6f67a5..dea940b 100644 --- a/src/main/scala/first/core/FctxWriter.scala +++ b/src/main/scala/first/core/FctxWriter.scala @@ -4,6 +4,8 @@ import first.config.FctxDef object FctxWriter: def toHocon(fctxDef: FctxDef): String = + val name = s"name = \"${fctxDef.name}\"\n\n" + val includes = if fctxDef.includes.nonEmpty then val listContent = fctxDef.includes.map(i => s"\"$i\"").mkString(", ") @@ -21,4 +23,4 @@ object FctxWriter: .mkString("artifacts = [\n", ",\n", "\n]") else "artifacts = []" - includes + artifacts + name + includes + artifacts diff --git a/src/main/scala/first/core/Load.scala b/src/main/scala/first/core/Load.scala index 7567778..1b675de 100644 --- a/src/main/scala/first/core/Load.scala +++ b/src/main/scala/first/core/Load.scala @@ -76,7 +76,7 @@ class Load(downloader: DownloaderClient = Downloader): scribe.debug("Verbose logging enabled.") import context.workingDir - val configReader = new ConfigReader() + val configReader = ConfigReader() val fctxConfDir = workingDir / ".then" / opts.contextName val artifactsDir = fctxConfDir / "artifacts" diff --git a/src/main/scala/first/core/Mv.scala b/src/main/scala/first/core/Mv.scala new file mode 100644 index 0000000..c843a5b --- /dev/null +++ b/src/main/scala/first/core/Mv.scala @@ -0,0 +1,105 @@ +package first.core + +import first.cli.MvCommand.MvOpts +import first.config.ConfigError +import first.config.ConfigReader +import first.config.GlobalConfig + +import scala.util.Try + +import os.Path + +class Mv(globalConfig: GlobalConfig = GlobalConfig): + def run(opts: MvOpts, context: Context): Either[Throwable, Unit] = + val reader = ConfigReader() + val paths = reader.listAvailableContextsWithPaths(context.workingDir) + + val contextPaths = paths.get(opts.name).getOrElse(Nil) + + if contextPaths.isEmpty then Left(RuntimeException(s"Context '${opts.name}' not found.")) + else + if contextPaths.size > 1 then + scribe.warn(s"Multiple locations found for '${opts.name}': $contextPaths. Using the first one.") + + ConfigReader + .resolveWriteTarget(contextPaths, context.workingDir) + .left + .map: + case first.config.AmbiguousContextError(msg) => RuntimeException(msg) + case e => RuntimeException(s"Error determining context path: $e") + .flatMap { oldDefPath => + val oldContextDir = oldDefPath / os.up + val dest = opts.dest + val newDefPath = dest / "fctx-def.conf" + val newContextName = dest.last + val renameNeeded = newContextName != opts.name + + // Load context BEFORE moving if we might need to update it + val fctxDefToUpdateEither = + if renameNeeded then + reader + .load(opts.name, context.workingDir) + .map(Option(_)) + .left + .map(e => RuntimeException(s"Failed to load context for rename: $e")) + else Right(None) + + fctxDefToUpdateEither.flatMap { fctxDefOpt => + scribe.info(s"Moving context '${opts.name}' from $oldContextDir to $dest") + + if os.exists(dest) then Left(RuntimeException(s"Destination '$dest' already exists.")) + else + // Move + Try(os.move(oldContextDir, dest)).toEither.flatMap { _ => + + // Rename context if needed + if renameNeeded && fctxDefOpt.isDefined then + val updatedFctxDef = fctxDefOpt.get.copy(name = newContextName) + val hocon = FctxWriter.toHocon(updatedFctxDef) + Try(os.write.over(newDefPath, hocon)) match + case scala.util.Failure(e) => scribe.warn(s"Failed to write renamed context config: $e") + case _ => scribe.info(s"Renamed context from '${opts.name}' to '$newContextName'") + + // Update GlobalConfig + // We always try to update or add, handling errors gracefully but logging + // Since GlobalConfig operations return Either[ConfigError, Unit], we can map them if we want strictness, + // but the original code just warned on failure. We'll maintain that warning behavior but inside the Right path. + + globalConfig.listContextPaths() match + case Right(paths) if paths.contains(oldDefPath) => + globalConfig.updateContext(oldDefPath, newDefPath) match + case Left(e) => scribe.warn(s"Failed to update GlobalConfig: $e") + case Right(_) => scribe.debug("Updated GlobalConfig.") + case Right(_) => + globalConfig.addContext(newDefPath) match + case Left(e) => scribe.warn(s"Failed to register new location in GlobalConfig: $e") + case Right(_) => scribe.debug("Registered new location in GlobalConfig.") + case Left(e) => scribe.warn(s"Failed to read GlobalConfig: $e") + + // Update Symlinks + updateSymlinks(context.workingDir, oldContextDir, dest) + + scribe.info(s"Moved context '${opts.name}' to $dest") + Right(()) + } + } + } + + private def updateSymlinks(rootDir: Path, oldTargetBase: Path, newTargetBase: Path): Unit = + // Naive walk. For large workspaces this might be slow, but safe for now. + // Skip .git and other ignorable dirs? + os.walk(rootDir, skip = p => p.last.startsWith(".git")).foreach { p => + if os.isLink(p) then + try + val linkTarget = os.readLink(p) + // We only care if the link target (resolved) starts with oldTargetBase + // Using toString to avoid strict Path dependence issues + if linkTarget.toString.startsWith(oldTargetBase.toString) then + val relStr = linkTarget.toString.substring(oldTargetBase.toString.length).stripPrefix("/") + val relative = os.RelPath(relStr) + val newTarget = newTargetBase / relative + + SymlinkManager.createSymlink(p, newTarget, relative = true) + scribe.debug(s"Updated symlink $p -> $newTarget") + catch case e: Throwable => scribe.debug(s"Failed to check/update link $p: ${e.getMessage}") + } diff --git a/src/main/scala/first/core/Save.scala b/src/main/scala/first/core/Save.scala index 58ba21a..b366a2b 100644 --- a/src/main/scala/first/core/Save.scala +++ b/src/main/scala/first/core/Save.scala @@ -4,22 +4,19 @@ import first.cli.SaveCommand.SaveOpts import os.* -class Save: - def run(opts: SaveOpts, context: Context): Unit = +class Save(globalConfig: first.config.GlobalConfig = first.config.GlobalConfig): + def run(opts: SaveOpts, context: Context): Either[Throwable, Unit] = if opts.verbose then scribe.Logger.root.withMinimumLevel(scribe.Level.Debug).replace() scribe.debug("Verbose logging enabled.") val workingDir = context.workingDir - val fctxConfDir = workingDir / ".then" / opts.contextName + val fctxConfDir = opts.toContextPath.getOrElse(workingDir / ".then" / opts.contextName) val artifactsDir = fctxConfDir / "artifacts" val fctxConfPath = fctxConfDir / "fctx-def.conf" scribe.debug(s"Target fctx directory: $fctxConfDir") - // List of (Artifact Definition, Optional Source Path) - // Source Path is None for remote artifacts (not copied during save) - // Source Path is Some(p) for local/external artifacts // List of (Artifact Definition, Optional Source Path) // Source Path is None for remote artifacts (not copied during save) // Source Path is Some(p) for local/external artifacts @@ -37,27 +34,30 @@ class Save: if opts.dryRun then scribe.info("DRY RUN: The following actions would be taken:") - scribe.info(s"- Ensure directory exists: $artifactsDir") processedArtifacts.foreach { case (artifact, sourcePathOpt) => sourcePathOpt match case Some(sourcePath) => val destPath = artifactsDir / os.RelPath(artifact.path) scribe.info(s"- Copy: $sourcePath -> $destPath") + if opts.link then scribe.info(s"- Link: $sourcePath -> $destPath") case None => scribe.info(s"- Remote Artifact (no copy): ${artifact.path}") } if os.exists(fctxConfPath) then scribe.info(s"- Backup existing file: $fctxConfPath to ${fctxConfPath.toString + ".bak"}") scribe.info(s"- Write to file: $fctxConfPath") + if opts.toContextPath.isDefined then scribe.info(s"- Register with GlobalConfig: $fctxConfPath") scribe.info("--- HOCON Content ---") scribe.info(hoconContent) scribe.info("--- End HOCON Content ---") - return + Right(()) if os.exists(fctxConfPath) && !opts.force then - scribe.error(s"Configuration file already exists: $fctxConfPath") - scribe.error("Use --force to overwrite. Aborting.") - return + return Left( + RuntimeException( + s"Configuration file already exists: $fctxConfPath. Use --force to overwrite. Aborting.", + ), + ) val backupPath = fctxConfDir / "fctx-def.conf.bak" @@ -74,6 +74,11 @@ class Save: val destPath = artifactsDir / os.RelPath(artifact.path) os.copy(sourcePath, destPath, createFolders = true, replaceExisting = true) scribe.debug(s"Copied artifact: $sourcePath -> $destPath") + + if opts.link then + SymlinkManager.createSymlink(sourcePath, destPath, relative = true) match + case Left(e) => scribe.warn(s"Failed to link $sourcePath: $e") + case Right(_) => scribe.debug(s"Linked $sourcePath -> $destPath") case None => scribe.debug(s"Skipping copy for remote artifact: ${artifact.path}") } @@ -81,11 +86,21 @@ class Save: os.write(fctxConfPath, hoconContent) scribe.info(s"Successfully saved fctx '${opts.contextName}' to $fctxConfPath") + if opts.toContextPath.isDefined then + globalConfig.addContext(fctxConfPath) match + case Left(e) => scribe.warn(s"Failed to register context with GlobalConfig: $e") + case Right(_) => scribe.debug(s"Registered context with GlobalConfig: $fctxConfPath") + if os.exists(backupPath) then os.remove(backupPath) scribe.debug(s"Removed backup file $backupPath") + + Right(()) catch case e: Exception => + // scribe.error is handled above in AppRunner but we want to cleanup first? + // AppRunner catches Exception. + // But we need to cleanup backup. scribe.error(s"Failed to save fctx: ${e.getMessage}", e) if os.exists(backupPath) then try @@ -97,3 +112,4 @@ class Save: if os.exists(artifactsDir) then scribe.warn("The artifacts directory may contain partially copied files. Cleaning up.") os.remove.all(artifactsDir) + Left(e) diff --git a/src/main/scala/first/core/Swap.scala b/src/main/scala/first/core/Swap.scala index 8766c4e..b007282 100644 --- a/src/main/scala/first/core/Swap.scala +++ b/src/main/scala/first/core/Swap.scala @@ -12,7 +12,7 @@ class Swap: scribe.debug("Verbose logging enabled.") import context.workingDir - val configReader = new ConfigReader() + val configReader = ConfigReader() val activeFctxOpt = context.state.activeFctx if activeFctxOpt.isEmpty then diff --git a/src/main/scala/first/core/SymlinkManager.scala b/src/main/scala/first/core/SymlinkManager.scala new file mode 100644 index 0000000..23019eb --- /dev/null +++ b/src/main/scala/first/core/SymlinkManager.scala @@ -0,0 +1,21 @@ +package first.core + +import first.config.ConfigError +import first.config.FileWriteError + +import scala.util.Try + +import os.Path + +object SymlinkManager: + def createSymlink(link: Path, target: Path, relative: Boolean = false): Either[ConfigError, Unit] = + if scala.util.Properties.isWin then + scribe.warn("Creating symlinks on Windows may require Administrator privileges.") + + if !os.exists(target) then scribe.warn(s"Symlink target does not exist: $target") + + Try { + if os.exists(link, followLinks = false) then os.remove.all(link) + val actualTarget = if relative then target.relativeTo(link / os.up) else target + os.symlink(link, actualTarget) + }.toEither.left.map(e => FileWriteError(link.toString, s"Failed to create symlink to $target: ${e.getMessage}")) diff --git a/src/main/scala/first/core/Update.scala b/src/main/scala/first/core/Update.scala index c92cde6..6f2e1e9 100644 --- a/src/main/scala/first/core/Update.scala +++ b/src/main/scala/first/core/Update.scala @@ -3,93 +3,136 @@ package first.core import first.cli.UpdateCommand.UpdateOpts import first.config.ConfigReader -class Update: - def run(opts: UpdateOpts, context: Context): Unit = +import scala.util.Try + +class Update(configReader: ConfigReader = ConfigReader()): + def run(opts: UpdateOpts, context: Context): Either[Throwable, Unit] = if opts.verbose then scribe.Logger.root.withMinimumLevel(scribe.Level.Debug).replace() scribe.debug("Verbose logging enabled.") - val contextName = opts.contextName.orElse(context.state.activeFctx.map(_.name)) match - case Some(name) => name + val contextNameEither = opts.contextName + .orElse(configReader.detectContextName(context.workingDir)) + .orElse(context.state.activeFctx.map(_.name)) match + case Some(name) => Right(name) case None => - scribe.error("No context specified and no active context found.") - return - - scribe.debug(s"Updating context: $contextName") - - val configReader = new ConfigReader() - configReader.load(contextName, context.workingDir) match - case Left(error) => - scribe.error(s"Failed to load context '$contextName': $error") - case Right(fctxDef) => - scribe.info(s"Loaded context '${fctxDef.name}' with ${fctxDef.artifacts.size} artifacts") - // 1. Process Forget - val pathsToForget = opts.forget.map(p => ArtifactProcessor.resolveConfigPath(p, context.workingDir)).toSet - val existingAfterForget = fctxDef.artifacts.filterNot(a => pathsToForget.contains(a.path)) - - if pathsToForget.nonEmpty then - scribe.info(s"Forgetting ${pathsToForget.size} artifacts: ${pathsToForget.mkString(", ")}") - - // 2. Process Add (New Artifacts) - val newProcessed = ArtifactProcessor.process(opts.add, context.workingDir, opts.swapAs) - val newArtifactsMap = newProcessed.map { case (a, src) => a.path -> (a, src) }.toMap - - // 3. Merge Add with Existing (after Forget) - val existingFiltered = existingAfterForget.filterNot(a => newArtifactsMap.contains(a.path)) - val updatedArtifacts = existingFiltered ++ newProcessed.map(_._1) - - // 4. Process Includes - val includesToForget = opts.forgetIncludes.toSet - val existingIncludesAfterForget = fctxDef.includes.filterNot(i => includesToForget.contains(i)) - - // Merge with new includes (avoiding duplicates) - val updatedIncludes = (existingIncludesAfterForget ++ opts.includes).distinct - - val updatedFctxDef = fctxDef.copy(artifacts = updatedArtifacts, includes = updatedIncludes) - - // 5. Write back - val hoconContent = FctxWriter.toHocon(updatedFctxDef) - val fctxConfDir = context.workingDir / ".then" / fctxDef.name - val fctxConfPath = fctxConfDir / "fctx-def.conf" - val artifactsDir = fctxConfDir / "artifacts" - val backupPath = fctxConfDir / "fctx-def.conf.bak" - - if opts.dryRun then - scribe.info("DRY RUN: The following actions would be taken:") - newProcessed.foreach { case (a, _) => scribe.info(s"- Add/Update artifact: ${a.path}") } - if updatedIncludes != fctxDef.includes then scribe.info(s"- Update includes: $updatedIncludes") - scribe.info(s"- Write to file: $fctxConfPath") - scribe.info("--- HOCON Content ---") - scribe.info(hoconContent) - scribe.info("--- End HOCON Content ---") - return - - try - if os.exists(fctxConfPath) then os.move(fctxConfPath, backupPath, replaceExisting = true) - - os.makeDir.all(artifactsDir) - newProcessed.foreach { case (artifact, sourcePathOpt) => - sourcePathOpt match - case Some(sourcePath) => - val destPath = artifactsDir / os.RelPath(artifact.path) - os.copy(sourcePath, destPath, createFolders = true, replaceExisting = true) - scribe.debug(s"Copied artifact: $sourcePath -> $destPath") - case None => - scribe.debug(s"Remote artifact (no copy): ${artifact.path}") + Left(RuntimeException("No context specified and no active context found.")) + + contextNameEither.flatMap { contextName => + scribe.debug(s"Updating context: $contextName") + + val available = configReader.listAvailableContextsWithPaths(context.workingDir) + val contextPaths = available.getOrElse(contextName, Nil) + scribe.debug(s"Available contexts keys: ${available.keys.mkString(", ")}") + scribe.debug(s"Available contexts for '$contextName': $contextPaths") + + if contextPaths.isEmpty then Left(RuntimeException(s"Context '$contextName' not found.")) + else + // Prioritize context in .then if multiple? Or just pick first? + // Same logic as Mv: + // Use strict resolution logic + ConfigReader + .resolveWriteTarget(contextPaths, context.workingDir) + .left + .map: + case first.config.AmbiguousContextError(msg) => RuntimeException(msg) + case e => RuntimeException(s"Error determining context path: $e") + .flatMap { fctxConfPath => + val fctxConfDir = fctxConfPath / os.up + + // Now load from that specific path to get current state + import first.config.ConfigError // Ensure import + configReader + .load( + contextName, + context.workingDir, + ) + .left + .map(e => RuntimeException(s"Failed to load context '$contextName': $e")) + .flatMap { fctxDef => + scribe.info(s"Loaded context '${fctxDef.name}' with ${fctxDef.artifacts.size} artifacts") + // 1. Process Forget + val pathsToForget = + opts.forget.map(p => ArtifactProcessor.resolveConfigPath(p, context.workingDir)).toSet + val existingAfterForget = fctxDef.artifacts.filterNot(a => pathsToForget.contains(a.path)) + + if pathsToForget.nonEmpty then + scribe.info(s"Forgetting ${pathsToForget.size} artifacts: ${pathsToForget.mkString(", ")}") + + // 2. Process Add (New Artifacts) + Try(ArtifactProcessor.process(opts.add, context.workingDir, opts.swapAs)).toEither.flatMap: + newProcessed => + val newArtifactsMap = newProcessed.map { case (a, src) => a.path -> (a, src) }.toMap + + // 3. Merge Add with Existing (after Forget) + val existingFiltered = existingAfterForget.filterNot(a => newArtifactsMap.contains(a.path)) + val updatedArtifacts = existingFiltered ++ newProcessed.map(_._1) + + // 4. Process Includes + val includesToForget = opts.forgetIncludes.toSet + val existingIncludesAfterForget = fctxDef.includes.filterNot(i => includesToForget.contains(i)) + + // Merge with new includes (avoiding duplicates) + val updatedIncludes = (existingIncludesAfterForget ++ opts.includes).distinct + + val updatedFctxDef = fctxDef.copy(artifacts = updatedArtifacts, includes = updatedIncludes) + + // 5. Write back + val hoconContent = FctxWriter.toHocon(updatedFctxDef) + // fctxConfDir and fctxConfPath determined above! + val artifactsDir = fctxConfDir / "artifacts" + val backupPath = fctxConfDir / "fctx-def.conf.bak" + + scribe.debug(s"Processing update for artifactsDir: $artifactsDir") + scribe.debug(s"New processed artifacts: ${newProcessed.map(_._1.path)}") + + if opts.dryRun then + scribe.info("DRY RUN: The following actions would be taken:") + newProcessed.foreach { case (a, src) => + scribe.info(s"- Add/Update artifact: ${a.path}") + if opts.link && src.isDefined then scribe.info(s" - Link: ${src.get} -> (artifact)") + } + if updatedIncludes != fctxDef.includes then scribe.info(s"- Update includes: $updatedIncludes") + scribe.info(s"- Write to file: $fctxConfPath") + scribe.info("--- HOCON Content ---") + scribe.info(hoconContent) + scribe.info("--- End HOCON Content ---") + Right(()) + else + Try { + if os.exists(fctxConfPath) then os.move(fctxConfPath, backupPath, replaceExisting = true) + + os.makeDir.all(artifactsDir) + newProcessed.foreach { case (artifact, sourcePathOpt) => + sourcePathOpt match + case Some(sourcePath) => + val destPath = artifactsDir / os.RelPath(artifact.path) + os.copy(sourcePath, destPath, createFolders = true, replaceExisting = true) + scribe.debug(s"Copied artifact: $sourcePath -> $destPath") + + if opts.link then + SymlinkManager.createSymlink(sourcePath, destPath, relative = true) match + case Left(e) => scribe.warn(s"Failed to link $sourcePath: $e") + case Right(_) => scribe.debug(s"Linked $sourcePath -> $destPath") + case None => + scribe.debug(s"Remote artifact (no copy): ${artifact.path}") + } + + os.write(fctxConfPath, hoconContent) + scribe.info(s"Successfully updated fctx '${fctxDef.name}'") + + if os.exists(backupPath) then os.remove(backupPath) + }.toEither.left.map { e => + scribe.error(s"Failed to update fctx: ${e.getMessage}", e) + if os.exists(backupPath) then + try + os.move(backupPath, fctxConfPath, replaceExisting = true) + scribe.info("Restored backup of fctx-def.conf successfully.") + catch + case restoreError: Exception => + scribe.error(s"FATAL: Failed to restore backup: ${restoreError.getMessage}", restoreError) + e + } + } } - - os.write(fctxConfPath, hoconContent) - scribe.info(s"Successfully updated fctx '${fctxDef.name}'") - - if os.exists(backupPath) then os.remove(backupPath) - - catch - case e: Exception => - scribe.error(s"Failed to update fctx: ${e.getMessage}", e) - if os.exists(backupPath) then - try - os.move(backupPath, fctxConfPath, replaceExisting = true) - scribe.info("Restored backup of fctx-def.conf successfully.") - catch - case restoreError: Exception => - scribe.error(s"FATAL: Failed to restore backup: ${restoreError.getMessage}", restoreError) + } diff --git a/src/test/scala/first/BaseSuite.scala b/src/test/scala/first/BaseSuite.scala index 1b15215..9b58447 100644 --- a/src/test/scala/first/BaseSuite.scala +++ b/src/test/scala/first/BaseSuite.scala @@ -15,5 +15,20 @@ abstract class BaseSuite extends FunSuite: formatter = testFormatter, minimumLevel = Some(Level.Debug), ) + .withHandler( + formatter = Logging.fileFormatter, + minimumLevel = Some(Level.Debug), + writer = Logging.createFileWriter(), + outputFormat = scribe.output.format.ASCIIOutputFormat, + ) .replace() super.beforeAll() + + // Logging hooks for easier debugging of test output in CI/log files + override def beforeEach(context: BeforeEach): Unit = + scribe.debug(s"Starting test: ${context.test.name}") + super.beforeEach(context) + + override def afterEach(context: AfterEach): Unit = + scribe.debug(s"Finished test: ${context.test.name}") + super.afterEach(context) diff --git a/src/test/scala/first/cli/SaveCommandTests.scala b/src/test/scala/first/cli/SaveCommandTests.scala new file mode 100644 index 0000000..ea1a6d1 --- /dev/null +++ b/src/test/scala/first/cli/SaveCommandTests.scala @@ -0,0 +1,18 @@ +package first.cli + +import first.BaseSuite + +class SaveCommandTests extends BaseSuite: + test("SaveCommand validates context name"): + val invalidNames = List("ctx/foo", "../ctx", "ctx\\bar", "ctx..foo") + val validNames = List("ctx", "ctx-foo", "ctx_foo", "ctx.foo") + + val command = com.monovore.decline.Command("save", "save context", helpFlag = false)(SaveCommand.saveOpts) + + for name <- invalidNames do + val result = command.parse(List(name), sys.env) + assert(result.isLeft, s"Should reject invalid name: $name") + + for name <- validNames do + val result = command.parse(List(name), sys.env) + assert(result.isRight, s"Should accept valid name: $name") diff --git a/src/test/scala/first/config/GlobalConfigTests.scala b/src/test/scala/first/config/GlobalConfigTests.scala new file mode 100644 index 0000000..179c878 --- /dev/null +++ b/src/test/scala/first/config/GlobalConfigTests.scala @@ -0,0 +1,37 @@ +package first.config + +import munit.FunSuite +import os.Path + +class GlobalConfigTests extends FunSuite: + test("GlobalConfig can add, list, update and remove contexts") { + val tempHome = os.temp.dir() + val globalConfig = new GlobalConfig(tempHome) + + val ctx1 = tempHome / "ctx1" / "fctx-def.conf" + val ctx2 = tempHome / "ctx2" / "fctx-def.conf" + val ctx1New = tempHome / "ctx1_new" / "fctx-def.conf" + + // Initial list empty + assertEquals(globalConfig.listContextPaths(), Right(Nil)) + + // Add ctx1 + assertEquals(globalConfig.addContext(ctx1), Right(())) + assertEquals(globalConfig.listContextPaths(), Right(List(ctx1))) + + // Add ctx2 + assertEquals(globalConfig.addContext(ctx2), Right(())) + assertEquals(globalConfig.listContextPaths(), Right(List(ctx1, ctx2))) + + // Update ctx1 -> ctx1New + assertEquals(globalConfig.updateContext(ctx1, ctx1New), Right(())) + assertEquals(globalConfig.listContextPaths(), Right(List(ctx1New, ctx2))) + + // Remove ctx2 + assertEquals(globalConfig.removeContext(ctx2), Right(())) + assertEquals(globalConfig.listContextPaths(), Right(List(ctx1New))) + + // Idempotency check for add + assertEquals(globalConfig.addContext(ctx1New), Right(())) + assertEquals(globalConfig.listContextPaths(), Right(List(ctx1New))) + } diff --git a/src/test/scala/first/core/MvTests.scala b/src/test/scala/first/core/MvTests.scala new file mode 100644 index 0000000..050eaae --- /dev/null +++ b/src/test/scala/first/core/MvTests.scala @@ -0,0 +1,66 @@ +package first.core + +import first.BaseSuite +import first.cli.MvCommand.MvOpts + +import os.* + +class MvTests extends BaseSuite: + test("mv should move context and update symlinks"): + if !scala.util.Properties.isWin then + val workingDir = os.temp.dir() + val ctxName = "mv-test" + val ctxDir = workingDir / ".then" / ctxName + val artifacts = ctxDir / "artifacts" + os.makeDir.all(artifacts) + os.write(ctxDir / "fctx-def.conf", """name = "mv-test"""") + os.write(artifacts / "foo.txt", "foo content") + + // Create a symlink pointing to the artifact + val link = workingDir / "foo-link.txt" + os.symlink(link, artifacts / "foo.txt") + + val dest = os.temp.dir() / "new-loc" + + val globalConfig = first.config.GlobalConfig(workingDir) + val context = Context(workingDir) + val mv = Mv(globalConfig) + + val opts = MvOpts(ctxName, dest) + assert(mv.run(opts, context).isRight) + + assert(!os.exists(ctxDir)) + assert(os.exists(dest / "fctx-def.conf")) + + // Check symlink update + assert(os.isLink(link)) + val relTarget = os.readLink(link) + val newTarget = link / os.up / os.RelPath(relTarget.toString) + assert(newTarget == dest / "artifacts" / "foo.txt") + assert(os.read(link) == "foo content") + + test("mv should rename context in config when destination name differs"): + if !scala.util.Properties.isWin then + val workingDir = os.temp.dir() + val ctxName = "original-name" + val ctxDir = workingDir / ".then" / ctxName + os.makeDir.all(ctxDir) + // Create valid HOCON with name + os.write(ctxDir / "fctx-def.conf", s"""name = "$ctxName"""") + + val destName = "renamed-context" + val dest = workingDir / ".then" / destName + + val globalConfig = first.config.GlobalConfig(workingDir) + val context = Context(workingDir) + val mv = Mv(globalConfig) + + val opts = MvOpts(ctxName, dest) + assert(mv.run(opts, context).isRight) + + assert(!os.exists(ctxDir)) + assert(os.exists(dest / "fctx-def.conf")) + + val newConfigContent = os.read(dest / "fctx-def.conf") + assert(newConfigContent.contains(s"""name = "$destName"""")) + assert(!newConfigContent.contains(s"""name = "$ctxName"""")) diff --git a/src/test/scala/first/core/SaveTests.scala b/src/test/scala/first/core/SaveTests.scala index 88b754d..21039a1 100644 --- a/src/test/scala/first/core/SaveTests.scala +++ b/src/test/scala/first/core/SaveTests.scala @@ -21,6 +21,8 @@ class SaveTests extends BaseSuite: force = false, dryRun = false, verbose = true, + toContextPath = None, + link = false, ) val context = new Context(tempDir) @@ -56,6 +58,8 @@ class SaveTests extends BaseSuite: force = false, dryRun = false, verbose = true, + toContextPath = None, + link = false, ) val context = new Context(contextDir) @@ -88,6 +92,8 @@ class SaveTests extends BaseSuite: force = false, dryRun = false, verbose = false, + toContextPath = None, + link = false, ) val context = new Context(tempDir) @@ -96,3 +102,66 @@ class SaveTests extends BaseSuite: val fctxDefPath = tempDir / ".then" / contextName / "fctx-def.conf" val content = os.read(fctxDefPath) assert(content.contains("""includes = ["base", "common"]""")) + + test("save --to should save to custom path and register with GlobalConfig"): + val workingDir = os.temp.dir() + val customDir = os.temp.dir() / "custom-ctx" + val contextName = "custom-save-test" + + val opts = SaveOpts( + contextName = contextName, + artifacts = Nil, + includes = Nil, + swapAs = SwapAs.Symlink, + force = false, + dryRun = false, + verbose = true, + toContextPath = Some(customDir), + link = false, + ) + + val globalConfig = new first.config.GlobalConfig(workingDir) + val context = new Context(workingDir) + val save = new Save(globalConfig) + + save.run(opts, context) + + val fctxDefPath = customDir / "fctx-def.conf" + assert(os.exists(fctxDefPath)) + + val registeredPaths = globalConfig.listContextPaths().toOption.get + assert(registeredPaths.contains(fctxDefPath)) + + test("save --link should replace file with symlink"): + if !scala.util.Properties.isWin then + val tempDir = os.temp.dir() + val contextName = "link-save-test" + + val fileToLink = tempDir / "file.txt" + os.write(fileToLink, "content") + + val opts = SaveOpts( + contextName = contextName, + artifacts = List(fileToLink.toString), + includes = Nil, + swapAs = SwapAs.Symlink, + force = false, + dryRun = false, + verbose = true, + toContextPath = None, + link = true, + ) + + val context = new Context(tempDir) + val save = new Save() + + save.run(opts, context) + + // Check if fileToLink is now a symlink + assert(os.isLink(fileToLink)) + assert(os.read(fileToLink) == "content") // os.read follows symlinks + + val target = os.readLink(fileToLink) + val resolvedTarget = fileToLink / os.up / os.RelPath(target.toString) + val expectedTarget = tempDir / ".then" / contextName / "artifacts" / "file.txt" + assert(resolvedTarget == expectedTarget) diff --git a/src/test/scala/first/core/SymlinkManagerTests.scala b/src/test/scala/first/core/SymlinkManagerTests.scala new file mode 100644 index 0000000..46136fd --- /dev/null +++ b/src/test/scala/first/core/SymlinkManagerTests.scala @@ -0,0 +1,53 @@ +package first.core + +import first.BaseSuite + +class SymlinkManagerTests extends BaseSuite: + test("createSymlink creates relative symlink when requested"): + val dir = os.temp.dir() + try + val target = dir / "target.txt" + os.write(target, "content") + + val linkDir = dir / "subdir" + os.makeDir(linkDir) + val link = linkDir / "link" + + // Create relative link: link -> ../target.txt + SymlinkManager.createSymlink(link, target, relative = true) + + assert(os.isLink(link)) + assert(os.read(link) == "content") + + // Verify it's actually relative (readLink returns resolved path usually, but we can check if it works after move) + // os-lib's os.readLink(p) returns the *absolute path* the link points to. + // To verify relative-ness, we can move the parent dir and see if it still works. + + val newDir = dir / os.up / (dir.last + "_moved") + os.move(dir, newDir) + + val newLink = newDir / "subdir" / "link" + assert(os.isLink(newLink)) + assert(os.read(newLink) == "content") + finally os.remove.all(dir) + + test("createSymlink creates absolute symlink by default"): + val dir = os.temp.dir() + try + val target = dir / "target.txt" + os.write(target, "content") + val link = dir / "link" + + SymlinkManager.createSymlink(link, target, relative = false) + + assert(os.isLink(link)) + // Moving the dir breaks absolute links pointing inside it + val newDir = dir / os.up / (dir.last + "_moved_abs") + os.move(dir, newDir) + + val newLink = newDir / "link" + // attempting to read should fail if it was absolute pointing to old path + intercept[java.nio.file.NoSuchFileException] { + os.read(newLink) + } + finally os.remove.all(dir) diff --git a/src/test/scala/first/core/UpdateTests.scala b/src/test/scala/first/core/UpdateTests.scala index a1ac732..d9ec04f 100644 --- a/src/test/scala/first/core/UpdateTests.scala +++ b/src/test/scala/first/core/UpdateTests.scala @@ -1,6 +1,7 @@ package first.core import first.cli.UpdateCommand.UpdateOpts +import first.config.ConfigReader import first.config.SwapAs import first.model.ActiveFctx @@ -31,6 +32,7 @@ class UpdateTests extends FunSuite: swapAs = SwapAs.Copy, dryRun = false, verbose = false, + link = false, ) new Update().run(opts, context) @@ -66,6 +68,7 @@ class UpdateTests extends FunSuite: swapAs = SwapAs.Copy, dryRun = false, verbose = false, + link = false, ) new Update().run(opts, context) @@ -100,6 +103,7 @@ class UpdateTests extends FunSuite: swapAs = SwapAs.Copy, dryRun = false, verbose = false, + link = false, ) new Update().run(opts, context) @@ -134,6 +138,7 @@ class UpdateTests extends FunSuite: swapAs = SwapAs.Copy, dryRun = false, verbose = false, + link = false, ) new Update().run(opts, context) @@ -173,6 +178,7 @@ class UpdateTests extends FunSuite: swapAs = SwapAs.Copy, dryRun = false, verbose = false, + link = false, ) new Update().run(opts, context) @@ -207,6 +213,7 @@ class UpdateTests extends FunSuite: swapAs = SwapAs.Copy, dryRun = false, verbose = false, + link = false, ) new Update().run(opts, context) @@ -215,3 +222,78 @@ class UpdateTests extends FunSuite: assert(confContent.contains("""includes = ["keep", "new-inc"]""")) assert(!confContent.contains("remove")) } + + test("Update --link links added artifacts") { + if !scala.util.Properties.isWin then + val wd = os.temp.dir() + val context = new Context(wd) + val fctxName = "link-update" + val fctxDir = wd / ".then" / fctxName + os.makeDir.all(fctxDir) + os.write(fctxDir / "fctx-def.conf", """artifacts = []""") + + val newFile = wd / "link-me.txt" + os.write(newFile, "content") + + val opts = UpdateOpts( + contextName = Some(fctxName), + add = List(newFile.toString), + forget = Nil, + includes = Nil, + forgetIncludes = Nil, + swapAs = SwapAs.Symlink, + dryRun = false, + verbose = false, + link = true, + ) + + new Update().run(opts, context) + + assert(os.isLink(newFile)) + assert(os.read(newFile) == "content") + val target = os.readLink(newFile) + assert(target.toString.contains(".then/link-update/artifacts/link-me.txt")) + } + + test("Update works for custom location context") { + val wd = os.temp.dir() + val context = new Context(wd) + // Custom location NOT in .then + val customDir = wd / "custom-loc" + os.makeDir.all(customDir) + os.write( + customDir / "fctx-def.conf", + """ + |name = "custom-loc" + |artifacts = [] + """.stripMargin, + ) + + // Use a ConfigReader with the temp dir as home to ensure it finds the GlobalConfig we are about to write + val configReader = ConfigReader(userHome = wd) + + // Register it in a local GlobalConfig (which writes to wd/.first/first.conf) + val globalConfig = first.config.GlobalConfig(wd) + globalConfig.addContext(customDir / "fctx-def.conf") + + val newFile = wd / "custom.txt" + os.write(newFile, "custom") + + val opts = UpdateOpts( + contextName = Some("custom-loc"), + add = List(newFile.toString), + forget = Nil, + includes = Nil, + forgetIncludes = Nil, + swapAs = SwapAs.Copy, + dryRun = false, + verbose = false, + link = false, + ) + + // Inject the reader into Update + assert(Update(configReader).run(opts, context).isRight) + + assert(os.exists(customDir / "artifacts" / "custom.txt")) + assert(os.read(customDir / "fctx-def.conf").contains("custom.txt")) + } diff --git a/test_output.txt b/test_output.txt new file mode 100644 index 0000000..31aba64 --- /dev/null +++ b/test_output.txt @@ -0,0 +1,715 @@ +Some utilized directives are marked as experimental: + - `//> using publish.organization oswaldo` + - `//> using publish.name first` + - `//> using publish.moduleName first` +Please bear in mind that non-ideal user experience should be expected. +If you encounter any bugs or have feedback to share, make sure to reach out to the maintenance team at https://github.com/VirtusLab/scala-cli +first.remote.UrlResolverTests: + + isRemote should return true for http url 0.001s + + isRemote should return true for https url 0.000s + + isRemote should return true for gh url 0.000s + + isRemote should return false for local absolute path 0.000s + + isRemote should return false for local relative path 0.000s + + resolve should return correct URI for http url 0.000s + + resolve should return correct URI for https url 0.000s + + resolve should resolve gh:// url with explicit branch via @ syntax 0.000s + + resolve should resolve gh:// url with implicit main branch for root files 0.000s + + resolve should resolve gh:// url with direct mapping for deep paths (user must provide branch) 0.000s + + resolve should fail for invalid gh:// paths 0.000s + + resolve should return file URI for local absolute path 0.000s + + resolve should return file URI for local relative path 0.000s +first.remote.CacheTests: + + put and get content 0.001s + + get non-existent content 0.000s + + put from source path 0.000s +first.remote.DownloaderTests: + + successful download returns Right with content 0.000s + + failed download returns Left with error message 0.000s + + download with authentication includes auth token 0.000s + + createRequest should add GITHUB_TOKEN for github.com URLs 0.001s + + createRequest should NOT add FIRST_AUTH_TOKEN for github.com URLs 0.000s + + createRequest should add FIRST_AUTH_TOKEN for non-github URLs 0.000s +first.AppRunnerTest: +Command failed: requirement failed: relative/path is not an absolute path + + + AppRunner calls exit(1) on failure 0.013s +first.core.MvTests: +2025.12.12 00:45:30 INFO first.core.Mv.run:34 - Moving context 'mv-test' from /tmp/2539139437408264452beepbH/.then/mv-test to /tmp/2985114425460181353RfTaOh/new-loc + +2025.12.12 00:45:30 DEBUG first.core.Mv.run:57 - Registered new location in GlobalConfig. + +2025.12.12 00:45:30 DEBUG first.core.Mv.updateSymlinks:80 - Updated symlink /tmp/2539139437408264452beepbH/foo-link.txt -> /tmp/2985114425460181353RfTaOh/new-loc/artifacts/foo.txt + +2025.12.12 00:45:30 INFO first.core.Mv.run:63 - Moved context 'mv-test' to /tmp/2985114425460181353RfTaOh/new-loc + + + mv should move context and update symlinks 0.007s +first.core.LoadTests: + + validateArtifacts should pass when all artifacts exist 0.001s + + validateArtifacts should fail when local artifact is missing 0.000s + + validateArtifacts should fail when remote artifact is missing 0.000s +first.core.SaveTests: +2025.12.12 00:45:30 DEBUG first.core.Save.run:11 - Verbose logging enabled. + +2025.12.12 00:45:30 DEBUG first.core.Save.run:18 - Target fctx directory: /tmp/1582574395418109546SGUjMe/.then/remote-save-test + +2025.12.12 00:45:30 DEBUG first.core.ArtifactProcessor.process:28 - Processing remote artifact: http://example.com/file.txt + +2025.12.12 00:45:30 DEBUG first.core.Save.run:68 - Ensured artifacts directory exists: /tmp/1582574395418109546SGUjMe/.then/remote-save-test/artifacts + +2025.12.12 00:45:30 DEBUG first.core.Save.run:81 - Skipping copy for remote artifact: http://example.com/file.txt + +2025.12.12 00:45:30 INFO first.core.Save.run:85 - Successfully saved fctx 'remote-save-test' to /tmp/1582574395418109546SGUjMe/.then/remote-save-test/fctx-def.conf + + + save should handle remote artifacts correctly 0.001s +2025.12.12 00:45:30 DEBUG first.core.Save.run:11 - Verbose logging enabled. + +2025.12.12 00:45:30 DEBUG first.core.Save.run:18 - Target fctx directory: /tmp/6492022565876751227cRamI6/.then/external-save-test + +2025.12.12 00:45:30 DEBUG first.core.ArtifactProcessor.process:33 - Processing local/external artifact: /tmp/8155677808084045687h6pP4q/external.txt -> external.txt + +2025.12.12 00:45:30 DEBUG first.core.Save.run:68 - Ensured artifacts directory exists: /tmp/6492022565876751227cRamI6/.then/external-save-test/artifacts + +2025.12.12 00:45:30 DEBUG first.core.Save.run:74 - Copied artifact: /tmp/8155677808084045687h6pP4q/external.txt -> /tmp/6492022565876751227cRamI6/.then/external-save-test/artifacts/external.txt + +2025.12.12 00:45:30 INFO first.core.Save.run:85 - Successfully saved fctx 'external-save-test' to /tmp/6492022565876751227cRamI6/.then/external-save-test/fctx-def.conf + + + save should handle external artifacts (absolute paths) correctly 0.001s +2025.12.12 00:45:30 DEBUG first.core.Save.run:18 - Target fctx directory: /tmp/3071224419653860033tL37kq/.then/includes-save-test + +2025.12.12 00:45:30 DEBUG first.core.Save.run:68 - Ensured artifacts directory exists: /tmp/3071224419653860033tL37kq/.then/includes-save-test/artifacts + +2025.12.12 00:45:30 INFO first.core.Save.run:85 - Successfully saved fctx 'includes-save-test' to /tmp/3071224419653860033tL37kq/.then/includes-save-test/fctx-def.conf + + + save should handle includes 0.001s +2025.12.12 00:45:30 DEBUG first.core.Save.run:11 - Verbose logging enabled. + +2025.12.12 00:45:30 DEBUG first.core.Save.run:18 - Target fctx directory: /tmp/293849596605597765ROnvn4/custom-ctx + +2025.12.12 00:45:30 DEBUG first.core.Save.run:68 - Ensured artifacts directory exists: /tmp/293849596605597765ROnvn4/custom-ctx/artifacts + +2025.12.12 00:45:30 INFO first.core.Save.run:85 - Successfully saved fctx 'custom-save-test' to /tmp/293849596605597765ROnvn4/custom-ctx/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.core.Save.run:90 - Registered context with GlobalConfig: /tmp/293849596605597765ROnvn4/custom-ctx/fctx-def.conf + + + save --to should save to custom path and register with GlobalConfig 0.002s +2025.12.12 00:45:30 DEBUG first.core.Save.run:11 - Verbose logging enabled. + +2025.12.12 00:45:30 DEBUG first.core.Save.run:18 - Target fctx directory: /tmp/2017549242056862317ScWjG0/.then/link-save-test + +2025.12.12 00:45:30 DEBUG first.core.ArtifactProcessor.process:33 - Processing local/external artifact: /tmp/2017549242056862317ScWjG0/file.txt -> file.txt + +2025.12.12 00:45:30 DEBUG first.core.Save.run:68 - Ensured artifacts directory exists: /tmp/2017549242056862317ScWjG0/.then/link-save-test/artifacts + +2025.12.12 00:45:30 DEBUG first.core.Save.run:74 - Copied artifact: /tmp/2017549242056862317ScWjG0/file.txt -> /tmp/2017549242056862317ScWjG0/.then/link-save-test/artifacts/file.txt + +2025.12.12 00:45:30 DEBUG first.core.Save.run:79 - Linked /tmp/2017549242056862317ScWjG0/file.txt -> /tmp/2017549242056862317ScWjG0/.then/link-save-test/artifacts/file.txt + +2025.12.12 00:45:30 INFO first.core.Save.run:85 - Successfully saved fctx 'link-save-test' to /tmp/2017549242056862317ScWjG0/.then/link-save-test/fctx-def.conf + + + save --link should replace file with symlink 0.001s +first.core.FctxWriterTests: + + toHocon writes artifacts and includes 0.000s + + toHocon handles empty includes 0.000s +first.core.SwapTests: +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'fctx-b' from '/tmp/5142950106824610784krt19W' + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/5142950106824610784krt19W + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/5142950106824610784krt19W/.then/fctx-b/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/fctx-b/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'fctx-b' from '/tmp/5142950106824610784krt19W': List(/tmp/5142950106824610784krt19W/.then/fctx-b/fctx-def.conf) + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/5142950106824610784krt19W/.then/fctx-b/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: fctx-b + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'fctx-a' from '/tmp/5142950106824610784krt19W' + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/5142950106824610784krt19W + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/5142950106824610784krt19W/.then/fctx-a/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/fctx-a/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'fctx-a' from '/tmp/5142950106824610784krt19W': List(/tmp/5142950106824610784krt19W/.then/fctx-a/fctx-def.conf) + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/5142950106824610784krt19W/.then/fctx-a/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: fctx-a + +2025.12.12 00:45:30 INFO first.core.Swap.run:55 - Swapping from 'fctx-a' to 'fctx-b'... + +2025.12.12 00:45:30 INFO first.core.Swap.run:62 - Removed: /tmp/5142950106824610784krt19W/file1.txt + +2025.12.12 00:45:30 INFO first.core.Swap.run:84 - Created symlink: /tmp/5142950106824610784krt19W/file3.txt -> /tmp/5142950106824610784krt19W/.then/fctx-b/artifacts/file3.txt + +2025.12.12 00:45:30 INFO first.core.Swap.run:106 - Updated artifact: /tmp/5142950106824610784krt19W/.then/fctx-b/artifacts/file2.txt -> /tmp/5142950106824610784krt19W/file2.txt + +2025.12.12 00:45:30 INFO first.core.Swap.run:110 - Successfully swapped to fctx 'fctx-b'. + + + swap fctx successfully 0.016s +2025.12.12 00:45:30 DEBUG first.core.Swap.run:12 - Verbose logging enabled. + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'fctx-dry-b' from '/tmp/7174920901587530120iXtaiY' + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/7174920901587530120iXtaiY + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/7174920901587530120iXtaiY/.then/fctx-dry-b/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/fctx-dry-b/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'fctx-dry-b' from '/tmp/7174920901587530120iXtaiY': List(/tmp/7174920901587530120iXtaiY/.then/fctx-dry-b/fctx-def.conf) + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/7174920901587530120iXtaiY/.then/fctx-dry-b/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: fctx-dry-b + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'fctx-dry-a' from '/tmp/7174920901587530120iXtaiY' + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/7174920901587530120iXtaiY + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/7174920901587530120iXtaiY/.then/fctx-dry-a/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/fctx-dry-a/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'fctx-dry-a' from '/tmp/7174920901587530120iXtaiY': List(/tmp/7174920901587530120iXtaiY/.then/fctx-dry-a/fctx-def.conf) + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/7174920901587530120iXtaiY/.then/fctx-dry-a/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: fctx-dry-a + +2025.12.12 00:45:30 INFO first.core.Swap.run:49 - DRY RUN: The following actions would be taken: + +2025.12.12 00:45:30 INFO first.core.Swap.run:50 - - Remove: file1.txt + +2025.12.12 00:45:30 INFO first.core.Swap.run:51 - - Add: file2.txt + + + swap with dry-run should not modify file system 0.011s +2025.12.12 00:45:30 ERROR first.core.Swap.run:19 - No active fctx found. Please load an fctx first. + + + swap from clean state should do nothing 0.001s +2025.12.12 00:45:30 DEBUG first.core.Swap.run:12 - Verbose logging enabled. + +2025.12.12 00:45:30 WARN first.core.Swap.run:26 - Fctx 'fctx-same' is already active. Nothing to do. + + + swap to the same fctx should do nothing 0.001s +first.core.UpdateTests: +2025.12.12 00:45:30 DEBUG first.core.Update.run:18 - Updating context: test-context + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'test-context' from '/tmp/451058678490820685ONViBw' + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/451058678490820685ONViBw + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/451058678490820685ONViBw/.then/test-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/test-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'test-context' from '/tmp/451058678490820685ONViBw': List(/tmp/451058678490820685ONViBw/.then/test-context/fctx-def.conf) + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/451058678490820685ONViBw/.then/test-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: test-context + +2025.12.12 00:45:30 INFO first.core.Update.run:54 - Loaded context 'test-context' with 0 artifacts + +2025.12.12 00:45:30 DEBUG first.core.ArtifactProcessor.process:33 - Processing local/external artifact: /tmp/451058678490820685ONViBw/new.txt -> new.txt + +2025.12.12 00:45:30 DEBUG first.core.Update.run:107 - Copied artifact: /tmp/451058678490820685ONViBw/new.txt -> /tmp/451058678490820685ONViBw/.then/test-context/artifacts/new.txt + +2025.12.12 00:45:30 INFO first.core.Update.run:118 - Successfully updated fctx 'test-context' + + + Update adds new artifact to existing context 0.009s +2025.12.12 00:45:30 DEBUG first.core.Update.run:18 - Updating context: test-update + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'test-update' from '/tmp/7711212760610465373M4awLj' + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/7711212760610465373M4awLj + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/7711212760610465373M4awLj/.then/test-update/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/test-update/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'test-update' from '/tmp/7711212760610465373M4awLj': List(/tmp/7711212760610465373M4awLj/.then/test-update/fctx-def.conf) + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/7711212760610465373M4awLj/.then/test-update/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: test-update + +2025.12.12 00:45:30 INFO first.core.Update.run:54 - Loaded context 'test-update' with 1 artifacts + +2025.12.12 00:45:30 DEBUG first.core.ArtifactProcessor.process:33 - Processing local/external artifact: /tmp/7711212760610465373M4awLj/foo.txt -> foo.txt + +2025.12.12 00:45:30 DEBUG first.core.Update.run:107 - Copied artifact: /tmp/7711212760610465373M4awLj/foo.txt -> /tmp/7711212760610465373M4awLj/.then/test-update/artifacts/foo.txt + +2025.12.12 00:45:30 INFO first.core.Update.run:118 - Successfully updated fctx 'test-update' + + + Update replaces existing artifact 0.009s +2025.12.12 00:45:30 DEBUG first.core.Update.run:18 - Updating context: active-one + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'active-one' from '/tmp/1670349829541966794F3WJ4Q' + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/1670349829541966794F3WJ4Q + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/1670349829541966794F3WJ4Q/.then/active-one/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/active-one/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'active-one' from '/tmp/1670349829541966794F3WJ4Q': List(/tmp/1670349829541966794F3WJ4Q/.then/active-one/fctx-def.conf) + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/1670349829541966794F3WJ4Q/.then/active-one/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: active-one + +2025.12.12 00:45:30 INFO first.core.Update.run:54 - Loaded context 'active-one' with 0 artifacts + +2025.12.12 00:45:30 DEBUG first.core.ArtifactProcessor.process:33 - Processing local/external artifact: /tmp/1670349829541966794F3WJ4Q/active.txt -> active.txt + +2025.12.12 00:45:30 DEBUG first.core.Update.run:107 - Copied artifact: /tmp/1670349829541966794F3WJ4Q/active.txt -> /tmp/1670349829541966794F3WJ4Q/.then/active-one/artifacts/active.txt + +2025.12.12 00:45:30 INFO first.core.Update.run:118 - Successfully updated fctx 'active-one' + + + Update resolves active context if name not provided 0.009s +2025.12.12 00:45:30 DEBUG first.core.Update.run:18 - Updating context: forget-ctx + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'forget-ctx' from '/tmp/8322031107398896423VCTPI9' + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/8322031107398896423VCTPI9 + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/8322031107398896423VCTPI9/.then/forget-ctx/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/forget-ctx/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'forget-ctx' from '/tmp/8322031107398896423VCTPI9': List(/tmp/8322031107398896423VCTPI9/.then/forget-ctx/fctx-def.conf) + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/8322031107398896423VCTPI9/.then/forget-ctx/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: forget-ctx + +2025.12.12 00:45:30 INFO first.core.Update.run:54 - Loaded context 'forget-ctx' with 2 artifacts + +2025.12.12 00:45:30 INFO first.core.Update.run:60 - Forgetting 1 artifacts: foo.txt + +2025.12.12 00:45:30 INFO first.core.Update.run:118 - Successfully updated fctx 'forget-ctx' + + + Update forgets artifact 0.009s +2025.12.12 00:45:30 DEBUG first.core.Update.run:18 - Updating context: includes-ctx + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'includes-ctx' from '/tmp/2820337592119703937RlETKS' + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/2820337592119703937RlETKS + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/2820337592119703937RlETKS/.then/includes-ctx/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/includes-ctx/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'includes-ctx' from '/tmp/2820337592119703937RlETKS': List(/tmp/2820337592119703937RlETKS/.then/includes-ctx/fctx-def.conf) + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/2820337592119703937RlETKS/.then/includes-ctx/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/2820337592119703937RlETKS + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/2820337592119703937RlETKS/.then/base/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/base/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'base' from '/tmp/2820337592119703937RlETKS': List() + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/2820337592119703937RlETKS + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/2820337592119703937RlETKS/.then/ext/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/ext/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'ext' from '/tmp/2820337592119703937RlETKS': List() + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: includes-ctx + +2025.12.12 00:45:30 INFO first.core.Update.run:54 - Loaded context 'includes-ctx' with 0 artifacts + +2025.12.12 00:45:30 DEBUG first.core.ArtifactProcessor.process:33 - Processing local/external artifact: /tmp/2820337592119703937RlETKS/new.txt -> new.txt + +2025.12.12 00:45:30 DEBUG first.core.Update.run:107 - Copied artifact: /tmp/2820337592119703937RlETKS/new.txt -> /tmp/2820337592119703937RlETKS/.then/includes-ctx/artifacts/new.txt + +2025.12.12 00:45:30 INFO first.core.Update.run:118 - Successfully updated fctx 'includes-ctx' + + + Update preserves includes 0.016s +2025.12.12 00:45:30 DEBUG first.core.Update.run:18 - Updating context: includes-manage + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'includes-manage' from '/tmp/3802151003282766197W52pLx' + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/3802151003282766197W52pLx + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/3802151003282766197W52pLx/.then/includes-manage/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/includes-manage/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'includes-manage' from '/tmp/3802151003282766197W52pLx': List(/tmp/3802151003282766197W52pLx/.then/includes-manage/fctx-def.conf) + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/3802151003282766197W52pLx/.then/includes-manage/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/3802151003282766197W52pLx + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/3802151003282766197W52pLx/.then/keep/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/keep/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'keep' from '/tmp/3802151003282766197W52pLx': List() + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/3802151003282766197W52pLx + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/3802151003282766197W52pLx/.then/remove/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/remove/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'remove' from '/tmp/3802151003282766197W52pLx': List() + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: includes-manage + +2025.12.12 00:45:30 INFO first.core.Update.run:54 - Loaded context 'includes-manage' with 0 artifacts + +2025.12.12 00:45:30 INFO first.core.Update.run:118 - Successfully updated fctx 'includes-manage' + + + Update adds and forgets includes 0.013s +2025.12.12 00:45:30 DEBUG first.core.Update.run:18 - Updating context: link-update + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'link-update' from '/tmp/8786708825091638038Fntx3p' + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/8786708825091638038Fntx3p + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/8786708825091638038Fntx3p/.then/link-update/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/link-update/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'link-update' from '/tmp/8786708825091638038Fntx3p': List(/tmp/8786708825091638038Fntx3p/.then/link-update/fctx-def.conf) + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/8786708825091638038Fntx3p/.then/link-update/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: link-update + +2025.12.12 00:45:30 INFO first.core.Update.run:54 - Loaded context 'link-update' with 0 artifacts + +2025.12.12 00:45:30 DEBUG first.core.ArtifactProcessor.process:33 - Processing local/external artifact: /tmp/8786708825091638038Fntx3p/link-me.txt -> link-me.txt + +2025.12.12 00:45:30 DEBUG first.core.Update.run:107 - Copied artifact: /tmp/8786708825091638038Fntx3p/link-me.txt -> /tmp/8786708825091638038Fntx3p/.then/link-update/artifacts/link-me.txt + +2025.12.12 00:45:30 DEBUG first.core.Update.run:112 - Linked /tmp/8786708825091638038Fntx3p/link-me.txt -> /tmp/8786708825091638038Fntx3p/.then/link-update/artifacts/link-me.txt + +2025.12.12 00:45:30 INFO first.core.Update.run:118 - Successfully updated fctx 'link-update' + + + Update --link links added artifacts 0.008s +2025.12.12 00:45:30 DEBUG first.core.Update.run:18 - Updating context: custom-loc + +2025.12.12 00:45:30 ERROR first.core.Update.run:25 - Context 'custom-loc' not found. + +==> X first.core.UpdateTests.Update works for custom location context 0.006s munit.FailException: /home/oswaldo/git/first/src/test/scala/first/core/UpdateTests.scala:304 assertion failed +303: +304: assert(os.exists(customDir / "artifacts" / "custom.txt")) +305: assert(os.read(customDir / "fctx-def.conf").contains("custom.txt")) +first.core.ArtifactProcessorTests: +2025.12.12 00:45:30 DEBUG first.core.ArtifactProcessor.process:33 - Processing local/external artifact: /tmp/4022069736977903406NvtmwW/foo.txt -> foo.txt + + + process local file inside workspace 0.001s +2025.12.12 00:45:30 DEBUG first.core.ArtifactProcessor.process:28 - Processing remote artifact: https://example.com/foo.txt + + + process remote artifact 0.000s +2025.12.12 00:45:30 DEBUG first.core.ArtifactProcessor.process:33 - Processing local/external artifact: /tmp/32823071817991955355gfnlt/outsider.conf -> outsider.conf + + + process external file 0.000s +first.config.GlobalConfigTests: + + GlobalConfig can add, list, update and remove contexts 0.006s +first.config.ConfigReaderTests: +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'my-context' from '/tmp/602971688414601366lqeU0v/valid-hocon' + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/602971688414601366lqeU0v/valid-hocon + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/602971688414601366lqeU0v/valid-hocon/.then/my-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/602971688414601366lqeU0v + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/602971688414601366lqeU0v/.then/my-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/my-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'my-context' from '/tmp/602971688414601366lqeU0v/valid-hocon': List(/tmp/602971688414601366lqeU0v/valid-hocon/.then/my-context/fctx-def.conf) + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/602971688414601366lqeU0v/valid-hocon/.then/my-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/602971688414601366lqeU0v/valid-hocon + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/602971688414601366lqeU0v/valid-hocon/.then/included-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/602971688414601366lqeU0v + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/602971688414601366lqeU0v/.then/included-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/included-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'included-context' from '/tmp/602971688414601366lqeU0v/valid-hocon': List(/tmp/602971688414601366lqeU0v/valid-hocon/.then/included-context/fctx-def.conf) + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/602971688414601366lqeU0v/valid-hocon/.then/included-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: my-context + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: my-context + + + successfully parse a valid HOCON config into FctxDef 0.012s +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'bad-context' from '/tmp/2003634630090545142MQ4R5S/invalid-hocon' + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/2003634630090545142MQ4R5S/invalid-hocon + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/2003634630090545142MQ4R5S/invalid-hocon/.then/bad-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/2003634630090545142MQ4R5S + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/2003634630090545142MQ4R5S/.then/bad-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/bad-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'bad-context' from '/tmp/2003634630090545142MQ4R5S/invalid-hocon': List(/tmp/2003634630090545142MQ4R5S/invalid-hocon/.then/bad-context/fctx-def.conf) + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/2003634630090545142MQ4R5S/invalid-hocon/.then/bad-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: bad-context + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:71 - Error mapping config to FctxDef: /tmp/2003634630090545142MQ4R5S/invalid-hocon/.then/bad-context/fctx-def.conf: 4: path has type OBJECT rather than STRING + + + return FileParseError for invalid HOCON syntax 0.005s +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'cumulative-context' from '/tmp/4582072941153976256HMbIPU/cumulative-loading' + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/4582072941153976256HMbIPU/cumulative-loading + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/4582072941153976256HMbIPU/cumulative-loading/.then/cumulative-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/4582072941153976256HMbIPU + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/4582072941153976256HMbIPU/.then/cumulative-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/cumulative-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'cumulative-context' from '/tmp/4582072941153976256HMbIPU/cumulative-loading': List(/home/oswaldo/.first/cumulative-context/fctx-def.conf, /tmp/4582072941153976256HMbIPU/cumulative-loading/.then/cumulative-context/fctx-def.conf) + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/4582072941153976256HMbIPU/cumulative-loading/.then/cumulative-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /home/oswaldo/.first/cumulative-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: cumulative-context + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: cumulative-context + + + cumulative config loading with overrides 0.006s +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'main-context' from '/tmp/8010528402483960769N3rMos/valid-includes' + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/8010528402483960769N3rMos/valid-includes + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/8010528402483960769N3rMos/valid-includes/.then/main-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/8010528402483960769N3rMos + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/8010528402483960769N3rMos/.then/main-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/main-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'main-context' from '/tmp/8010528402483960769N3rMos/valid-includes': List(/tmp/8010528402483960769N3rMos/valid-includes/.then/main-context/fctx-def.conf) + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/8010528402483960769N3rMos/valid-includes/.then/main-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/8010528402483960769N3rMos/valid-includes + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/8010528402483960769N3rMos/valid-includes/.then/included-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/8010528402483960769N3rMos + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/8010528402483960769N3rMos/.then/included-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/included-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'included-context' from '/tmp/8010528402483960769N3rMos/valid-includes': List(/tmp/8010528402483960769N3rMos/valid-includes/.then/included-context/fctx-def.conf) + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/8010528402483960769N3rMos/valid-includes/.then/included-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: main-context + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: main-context + + + valid includes should merge configurations 0.009s +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'context-a' from '/tmp/8732046518728768426leFPGo/circular-includes' + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/8732046518728768426leFPGo/circular-includes + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/8732046518728768426leFPGo/circular-includes/.then/context-a/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/8732046518728768426leFPGo + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/8732046518728768426leFPGo/.then/context-a/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/context-a/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'context-a' from '/tmp/8732046518728768426leFPGo/circular-includes': List(/tmp/8732046518728768426leFPGo/circular-includes/.then/context-a/fctx-def.conf) + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/8732046518728768426leFPGo/circular-includes/.then/context-a/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/8732046518728768426leFPGo/circular-includes + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/8732046518728768426leFPGo/circular-includes/.then/context-b/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/8732046518728768426leFPGo + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/8732046518728768426leFPGo/.then/context-b/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/context-b/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'context-b' from '/tmp/8732046518728768426leFPGo/circular-includes': List(/tmp/8732046518728768426leFPGo/circular-includes/.then/context-b/fctx-def.conf) + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/8732046518728768426leFPGo/circular-includes/.then/context-b/fctx-def.conf + + + circular includes should return CircularDependency error 0.007s +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'path-context' from '/tmp/3814542970136059507AjfHvQ/relative-paths/.then/path-context' + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/3814542970136059507AjfHvQ/relative-paths/.then/path-context + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/3814542970136059507AjfHvQ/relative-paths/.then/path-context/.then/path-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/3814542970136059507AjfHvQ/relative-paths/.then + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/3814542970136059507AjfHvQ/relative-paths/.then/.then/path-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/3814542970136059507AjfHvQ/relative-paths + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/3814542970136059507AjfHvQ/relative-paths/.then/path-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/3814542970136059507AjfHvQ + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/3814542970136059507AjfHvQ/.then/path-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/path-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'path-context' from '/tmp/3814542970136059507AjfHvQ/relative-paths/.then/path-context': List(/tmp/3814542970136059507AjfHvQ/relative-paths/.then/path-context/fctx-def.conf) + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/3814542970136059507AjfHvQ/relative-paths/.then/path-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: path-context + + + relative artifact paths should be preserved 0.005s +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'remote-context' from '/tmp/708541836262839629hfmoT7/remote-include' + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/708541836262839629hfmoT7/remote-include + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/708541836262839629hfmoT7/remote-include/.then/remote-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/708541836262839629hfmoT7 + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/708541836262839629hfmoT7/.then/remote-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/remote-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'remote-context' from '/tmp/708541836262839629hfmoT7/remote-include': List(/tmp/708541836262839629hfmoT7/remote-include/.then/remote-context/fctx-def.conf) + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/708541836262839629hfmoT7/remote-include/.then/remote-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.handleRemoteInclude:87 - Handling remote include: http://example.com/remote.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.handleRemoteInclude:98 - Downloaded remote include to /home/oswaldo/.first/cache/6360d95f7ab8eb33ce56cca702d9fe6def591918ca0c8e8bf4c7c090ab8edec2 + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: remote-context + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: remote-context + + + remote include should download and merge config 0.006s +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'main-context' from '/tmp/2415015308826155912dZ9Y0R/remote-circular' + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/2415015308826155912dZ9Y0R/remote-circular + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/2415015308826155912dZ9Y0R/remote-circular/.then/main-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/2415015308826155912dZ9Y0R + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/2415015308826155912dZ9Y0R/.then/main-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/main-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'main-context' from '/tmp/2415015308826155912dZ9Y0R/remote-circular': List(/tmp/2415015308826155912dZ9Y0R/remote-circular/.then/main-context/fctx-def.conf) + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/2415015308826155912dZ9Y0R/remote-circular/.then/main-context/fctx-def.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.handleRemoteInclude:87 - Handling remote include: http://example.com/config-a.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.handleRemoteInclude:98 - Downloaded remote include to /home/oswaldo/.first/cache/ad47a06310fcc3f2f59e00aa6f0f30d07dc5b1a3eedf69115945f49beef3897c + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.handleRemoteInclude:87 - Handling remote include: http://example.com/config-b.conf + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.handleRemoteInclude:98 - Downloaded remote include to /home/oswaldo/.first/cache/a8d433c03bd2d562f51e8d3a4b6a5f21160a023d938df19f5f57eea7a07075d9 + +2025.12.12 00:45:30 DEBUG first.config.ConfigReader.handleRemoteInclude:87 - Handling remote include: http://example.com/config-a.conf + + + circular remote includes should return CircularDependency error 0.008s +first.cli.HelpTests: + + simple test 0.000s + + first --help 0.001s +first.cli.CliTests: +Some utilized directives are marked as experimental: + - `//> using publish.organization oswaldo` + - `//> using publish.name first` + - `//> using publish.moduleName first` +Please bear in mind that non-ideal user experience should be expected. +If you encounter any bugs or have feedback to share, make sure to reach out to the maintenance team at https://github.com/VirtusLab/scala-cli +Wrote /tmp/2429281828181916449lEMC0a/first, run it with + /tmp/2429281828181916449lEMC0a/first + + save command should create a new fctx file 0.017s + + load command should create symlinks 0.037s + + swap command should update symlinks 0.043s + + ls command should list available contexts 0.017s + + load command should download gh:// artifact 0.130s From 9246532278cd585aa94d247aff2c12c2c8a26c86 Mon Sep 17 00:00:00 2001 From: Oswaldo Dantas Date: Wed, 31 Dec 2025 01:03:33 +0100 Subject: [PATCH 2/2] docs: amend constitution to v1.9.0 (Agile Dependency Management + Clean Git History) --- .specify/memory/constitution.md | 22 +- test_output.txt | 715 -------------------------------- 2 files changed, 16 insertions(+), 721 deletions(-) delete mode 100644 test_output.txt diff --git a/.specify/memory/constitution.md b/.specify/memory/constitution.md index f70599c..34c8c9f 100644 --- a/.specify/memory/constitution.md +++ b/.specify/memory/constitution.md @@ -1,10 +1,10 @@ @@ -47,7 +47,14 @@ Code must be written to be readable and accessible, avoiding pitfalls like `null - **Expressive Logic**: Prefer properly named functions and pattern matching (`match`) or monadic chains (`map`, `flatMap`) over excessive nesting of `if/else` blocks. - **No Var**: Do not use `var`. Design algorithms so intermediate invalid or inconsistent values are not possible. Avoid `java.util.concurrent.atomic` unless absolutely necessary. -### VIII. Reuse First Architecture +### VIII. Agile Dependency Management + +Dependencies should be selected with care but kept fresh. We prefer a "Selective but Fresh" approach: + +- **Selective**: Only add dependencies when they provide significant value over the standard library or simple custom implementations. +- **Fresh**: When a dependency is accepted, it must be kept up-to-date with its latest stable version to ensure security, performance, and access to the latest features. + +### IX. Reuse First Architecture Before implementing new features, developers MUST analyze existing code for reusable components. If a new feature involves operations similar to existing ones (e.g., file persistence, configuration loading), existing stable implementations MUST be inspected for reuse. Common logic SHOULD be extracted into shared core components to avoid duplication and leverage proven stability. @@ -59,6 +66,9 @@ Before implementing new features, developers MUST analyze existing code for reus ## Development Workflow - **Atomic Features**: Each feature should be developed on a dedicated branch and result in a single, comprehensive commit upon completion. +- **Clean Git History**: + - **Active Development**: During the implementation of a feature (before merge), developers should amend the existing feature commit rather than creating new "fix" commits. This ensures that the final feature commit is a clean, self-contained unit of work. + - **Post-Merge**: "Fix" commits are reserved for addressing regressions or bugs discovered after a feature has been merged or released. - **Linear History**: To maintain a clean and readable revision history, feature branches MUST be rebased onto the main branch before merging. Merge commits are to be avoided in favor of fast-forward merges. - **Roadmap Tracking**: After a feature branch is merged, the `SPEC-ROADMAP.md` file must be updated to mark the corresponding feature as complete. - **Specification Gating**: Before implementation can begin, the feature's `requirements.md` checklist MUST be reviewed and marked as complete. This serves as the formal quality gate after the clarification step. @@ -74,4 +84,4 @@ This constitution is the supreme governing document for the `first` project. All All pull requests and code reviews must include a check for compliance with this constitution. Any deviation from these principles must be explicitly justified and approved. -**Version**: 1.8.0 | **Ratified**: 2025-10-30 | **Last Amended**: 2025-12-09 +**Version**: 1.9.0 | **Ratified**: 2025-10-30 | **Last Amended**: 2025-12-31 diff --git a/test_output.txt b/test_output.txt deleted file mode 100644 index 31aba64..0000000 --- a/test_output.txt +++ /dev/null @@ -1,715 +0,0 @@ -Some utilized directives are marked as experimental: - - `//> using publish.organization oswaldo` - - `//> using publish.name first` - - `//> using publish.moduleName first` -Please bear in mind that non-ideal user experience should be expected. -If you encounter any bugs or have feedback to share, make sure to reach out to the maintenance team at https://github.com/VirtusLab/scala-cli -first.remote.UrlResolverTests: - + isRemote should return true for http url 0.001s - + isRemote should return true for https url 0.000s - + isRemote should return true for gh url 0.000s - + isRemote should return false for local absolute path 0.000s - + isRemote should return false for local relative path 0.000s - + resolve should return correct URI for http url 0.000s - + resolve should return correct URI for https url 0.000s - + resolve should resolve gh:// url with explicit branch via @ syntax 0.000s - + resolve should resolve gh:// url with implicit main branch for root files 0.000s - + resolve should resolve gh:// url with direct mapping for deep paths (user must provide branch) 0.000s - + resolve should fail for invalid gh:// paths 0.000s - + resolve should return file URI for local absolute path 0.000s - + resolve should return file URI for local relative path 0.000s -first.remote.CacheTests: - + put and get content 0.001s - + get non-existent content 0.000s - + put from source path 0.000s -first.remote.DownloaderTests: - + successful download returns Right with content 0.000s - + failed download returns Left with error message 0.000s - + download with authentication includes auth token 0.000s - + createRequest should add GITHUB_TOKEN for github.com URLs 0.001s - + createRequest should NOT add FIRST_AUTH_TOKEN for github.com URLs 0.000s - + createRequest should add FIRST_AUTH_TOKEN for non-github URLs 0.000s -first.AppRunnerTest: -Command failed: requirement failed: relative/path is not an absolute path - - + AppRunner calls exit(1) on failure 0.013s -first.core.MvTests: -2025.12.12 00:45:30 INFO first.core.Mv.run:34 - Moving context 'mv-test' from /tmp/2539139437408264452beepbH/.then/mv-test to /tmp/2985114425460181353RfTaOh/new-loc - -2025.12.12 00:45:30 DEBUG first.core.Mv.run:57 - Registered new location in GlobalConfig. - -2025.12.12 00:45:30 DEBUG first.core.Mv.updateSymlinks:80 - Updated symlink /tmp/2539139437408264452beepbH/foo-link.txt -> /tmp/2985114425460181353RfTaOh/new-loc/artifacts/foo.txt - -2025.12.12 00:45:30 INFO first.core.Mv.run:63 - Moved context 'mv-test' to /tmp/2985114425460181353RfTaOh/new-loc - - + mv should move context and update symlinks 0.007s -first.core.LoadTests: - + validateArtifacts should pass when all artifacts exist 0.001s - + validateArtifacts should fail when local artifact is missing 0.000s - + validateArtifacts should fail when remote artifact is missing 0.000s -first.core.SaveTests: -2025.12.12 00:45:30 DEBUG first.core.Save.run:11 - Verbose logging enabled. - -2025.12.12 00:45:30 DEBUG first.core.Save.run:18 - Target fctx directory: /tmp/1582574395418109546SGUjMe/.then/remote-save-test - -2025.12.12 00:45:30 DEBUG first.core.ArtifactProcessor.process:28 - Processing remote artifact: http://example.com/file.txt - -2025.12.12 00:45:30 DEBUG first.core.Save.run:68 - Ensured artifacts directory exists: /tmp/1582574395418109546SGUjMe/.then/remote-save-test/artifacts - -2025.12.12 00:45:30 DEBUG first.core.Save.run:81 - Skipping copy for remote artifact: http://example.com/file.txt - -2025.12.12 00:45:30 INFO first.core.Save.run:85 - Successfully saved fctx 'remote-save-test' to /tmp/1582574395418109546SGUjMe/.then/remote-save-test/fctx-def.conf - - + save should handle remote artifacts correctly 0.001s -2025.12.12 00:45:30 DEBUG first.core.Save.run:11 - Verbose logging enabled. - -2025.12.12 00:45:30 DEBUG first.core.Save.run:18 - Target fctx directory: /tmp/6492022565876751227cRamI6/.then/external-save-test - -2025.12.12 00:45:30 DEBUG first.core.ArtifactProcessor.process:33 - Processing local/external artifact: /tmp/8155677808084045687h6pP4q/external.txt -> external.txt - -2025.12.12 00:45:30 DEBUG first.core.Save.run:68 - Ensured artifacts directory exists: /tmp/6492022565876751227cRamI6/.then/external-save-test/artifacts - -2025.12.12 00:45:30 DEBUG first.core.Save.run:74 - Copied artifact: /tmp/8155677808084045687h6pP4q/external.txt -> /tmp/6492022565876751227cRamI6/.then/external-save-test/artifacts/external.txt - -2025.12.12 00:45:30 INFO first.core.Save.run:85 - Successfully saved fctx 'external-save-test' to /tmp/6492022565876751227cRamI6/.then/external-save-test/fctx-def.conf - - + save should handle external artifacts (absolute paths) correctly 0.001s -2025.12.12 00:45:30 DEBUG first.core.Save.run:18 - Target fctx directory: /tmp/3071224419653860033tL37kq/.then/includes-save-test - -2025.12.12 00:45:30 DEBUG first.core.Save.run:68 - Ensured artifacts directory exists: /tmp/3071224419653860033tL37kq/.then/includes-save-test/artifacts - -2025.12.12 00:45:30 INFO first.core.Save.run:85 - Successfully saved fctx 'includes-save-test' to /tmp/3071224419653860033tL37kq/.then/includes-save-test/fctx-def.conf - - + save should handle includes 0.001s -2025.12.12 00:45:30 DEBUG first.core.Save.run:11 - Verbose logging enabled. - -2025.12.12 00:45:30 DEBUG first.core.Save.run:18 - Target fctx directory: /tmp/293849596605597765ROnvn4/custom-ctx - -2025.12.12 00:45:30 DEBUG first.core.Save.run:68 - Ensured artifacts directory exists: /tmp/293849596605597765ROnvn4/custom-ctx/artifacts - -2025.12.12 00:45:30 INFO first.core.Save.run:85 - Successfully saved fctx 'custom-save-test' to /tmp/293849596605597765ROnvn4/custom-ctx/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.core.Save.run:90 - Registered context with GlobalConfig: /tmp/293849596605597765ROnvn4/custom-ctx/fctx-def.conf - - + save --to should save to custom path and register with GlobalConfig 0.002s -2025.12.12 00:45:30 DEBUG first.core.Save.run:11 - Verbose logging enabled. - -2025.12.12 00:45:30 DEBUG first.core.Save.run:18 - Target fctx directory: /tmp/2017549242056862317ScWjG0/.then/link-save-test - -2025.12.12 00:45:30 DEBUG first.core.ArtifactProcessor.process:33 - Processing local/external artifact: /tmp/2017549242056862317ScWjG0/file.txt -> file.txt - -2025.12.12 00:45:30 DEBUG first.core.Save.run:68 - Ensured artifacts directory exists: /tmp/2017549242056862317ScWjG0/.then/link-save-test/artifacts - -2025.12.12 00:45:30 DEBUG first.core.Save.run:74 - Copied artifact: /tmp/2017549242056862317ScWjG0/file.txt -> /tmp/2017549242056862317ScWjG0/.then/link-save-test/artifacts/file.txt - -2025.12.12 00:45:30 DEBUG first.core.Save.run:79 - Linked /tmp/2017549242056862317ScWjG0/file.txt -> /tmp/2017549242056862317ScWjG0/.then/link-save-test/artifacts/file.txt - -2025.12.12 00:45:30 INFO first.core.Save.run:85 - Successfully saved fctx 'link-save-test' to /tmp/2017549242056862317ScWjG0/.then/link-save-test/fctx-def.conf - - + save --link should replace file with symlink 0.001s -first.core.FctxWriterTests: - + toHocon writes artifacts and includes 0.000s - + toHocon handles empty includes 0.000s -first.core.SwapTests: -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'fctx-b' from '/tmp/5142950106824610784krt19W' - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/5142950106824610784krt19W - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/5142950106824610784krt19W/.then/fctx-b/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/fctx-b/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'fctx-b' from '/tmp/5142950106824610784krt19W': List(/tmp/5142950106824610784krt19W/.then/fctx-b/fctx-def.conf) - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/5142950106824610784krt19W/.then/fctx-b/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: fctx-b - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'fctx-a' from '/tmp/5142950106824610784krt19W' - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/5142950106824610784krt19W - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/5142950106824610784krt19W/.then/fctx-a/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/fctx-a/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'fctx-a' from '/tmp/5142950106824610784krt19W': List(/tmp/5142950106824610784krt19W/.then/fctx-a/fctx-def.conf) - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/5142950106824610784krt19W/.then/fctx-a/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: fctx-a - -2025.12.12 00:45:30 INFO first.core.Swap.run:55 - Swapping from 'fctx-a' to 'fctx-b'... - -2025.12.12 00:45:30 INFO first.core.Swap.run:62 - Removed: /tmp/5142950106824610784krt19W/file1.txt - -2025.12.12 00:45:30 INFO first.core.Swap.run:84 - Created symlink: /tmp/5142950106824610784krt19W/file3.txt -> /tmp/5142950106824610784krt19W/.then/fctx-b/artifacts/file3.txt - -2025.12.12 00:45:30 INFO first.core.Swap.run:106 - Updated artifact: /tmp/5142950106824610784krt19W/.then/fctx-b/artifacts/file2.txt -> /tmp/5142950106824610784krt19W/file2.txt - -2025.12.12 00:45:30 INFO first.core.Swap.run:110 - Successfully swapped to fctx 'fctx-b'. - - + swap fctx successfully 0.016s -2025.12.12 00:45:30 DEBUG first.core.Swap.run:12 - Verbose logging enabled. - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'fctx-dry-b' from '/tmp/7174920901587530120iXtaiY' - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/7174920901587530120iXtaiY - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/7174920901587530120iXtaiY/.then/fctx-dry-b/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/fctx-dry-b/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'fctx-dry-b' from '/tmp/7174920901587530120iXtaiY': List(/tmp/7174920901587530120iXtaiY/.then/fctx-dry-b/fctx-def.conf) - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/7174920901587530120iXtaiY/.then/fctx-dry-b/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: fctx-dry-b - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'fctx-dry-a' from '/tmp/7174920901587530120iXtaiY' - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/7174920901587530120iXtaiY - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/7174920901587530120iXtaiY/.then/fctx-dry-a/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/fctx-dry-a/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'fctx-dry-a' from '/tmp/7174920901587530120iXtaiY': List(/tmp/7174920901587530120iXtaiY/.then/fctx-dry-a/fctx-def.conf) - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/7174920901587530120iXtaiY/.then/fctx-dry-a/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: fctx-dry-a - -2025.12.12 00:45:30 INFO first.core.Swap.run:49 - DRY RUN: The following actions would be taken: - -2025.12.12 00:45:30 INFO first.core.Swap.run:50 - - Remove: file1.txt - -2025.12.12 00:45:30 INFO first.core.Swap.run:51 - - Add: file2.txt - - + swap with dry-run should not modify file system 0.011s -2025.12.12 00:45:30 ERROR first.core.Swap.run:19 - No active fctx found. Please load an fctx first. - - + swap from clean state should do nothing 0.001s -2025.12.12 00:45:30 DEBUG first.core.Swap.run:12 - Verbose logging enabled. - -2025.12.12 00:45:30 WARN first.core.Swap.run:26 - Fctx 'fctx-same' is already active. Nothing to do. - - + swap to the same fctx should do nothing 0.001s -first.core.UpdateTests: -2025.12.12 00:45:30 DEBUG first.core.Update.run:18 - Updating context: test-context - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'test-context' from '/tmp/451058678490820685ONViBw' - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/451058678490820685ONViBw - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/451058678490820685ONViBw/.then/test-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/test-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'test-context' from '/tmp/451058678490820685ONViBw': List(/tmp/451058678490820685ONViBw/.then/test-context/fctx-def.conf) - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/451058678490820685ONViBw/.then/test-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: test-context - -2025.12.12 00:45:30 INFO first.core.Update.run:54 - Loaded context 'test-context' with 0 artifacts - -2025.12.12 00:45:30 DEBUG first.core.ArtifactProcessor.process:33 - Processing local/external artifact: /tmp/451058678490820685ONViBw/new.txt -> new.txt - -2025.12.12 00:45:30 DEBUG first.core.Update.run:107 - Copied artifact: /tmp/451058678490820685ONViBw/new.txt -> /tmp/451058678490820685ONViBw/.then/test-context/artifacts/new.txt - -2025.12.12 00:45:30 INFO first.core.Update.run:118 - Successfully updated fctx 'test-context' - - + Update adds new artifact to existing context 0.009s -2025.12.12 00:45:30 DEBUG first.core.Update.run:18 - Updating context: test-update - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'test-update' from '/tmp/7711212760610465373M4awLj' - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/7711212760610465373M4awLj - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/7711212760610465373M4awLj/.then/test-update/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/test-update/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'test-update' from '/tmp/7711212760610465373M4awLj': List(/tmp/7711212760610465373M4awLj/.then/test-update/fctx-def.conf) - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/7711212760610465373M4awLj/.then/test-update/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: test-update - -2025.12.12 00:45:30 INFO first.core.Update.run:54 - Loaded context 'test-update' with 1 artifacts - -2025.12.12 00:45:30 DEBUG first.core.ArtifactProcessor.process:33 - Processing local/external artifact: /tmp/7711212760610465373M4awLj/foo.txt -> foo.txt - -2025.12.12 00:45:30 DEBUG first.core.Update.run:107 - Copied artifact: /tmp/7711212760610465373M4awLj/foo.txt -> /tmp/7711212760610465373M4awLj/.then/test-update/artifacts/foo.txt - -2025.12.12 00:45:30 INFO first.core.Update.run:118 - Successfully updated fctx 'test-update' - - + Update replaces existing artifact 0.009s -2025.12.12 00:45:30 DEBUG first.core.Update.run:18 - Updating context: active-one - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'active-one' from '/tmp/1670349829541966794F3WJ4Q' - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/1670349829541966794F3WJ4Q - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/1670349829541966794F3WJ4Q/.then/active-one/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/active-one/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'active-one' from '/tmp/1670349829541966794F3WJ4Q': List(/tmp/1670349829541966794F3WJ4Q/.then/active-one/fctx-def.conf) - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/1670349829541966794F3WJ4Q/.then/active-one/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: active-one - -2025.12.12 00:45:30 INFO first.core.Update.run:54 - Loaded context 'active-one' with 0 artifacts - -2025.12.12 00:45:30 DEBUG first.core.ArtifactProcessor.process:33 - Processing local/external artifact: /tmp/1670349829541966794F3WJ4Q/active.txt -> active.txt - -2025.12.12 00:45:30 DEBUG first.core.Update.run:107 - Copied artifact: /tmp/1670349829541966794F3WJ4Q/active.txt -> /tmp/1670349829541966794F3WJ4Q/.then/active-one/artifacts/active.txt - -2025.12.12 00:45:30 INFO first.core.Update.run:118 - Successfully updated fctx 'active-one' - - + Update resolves active context if name not provided 0.009s -2025.12.12 00:45:30 DEBUG first.core.Update.run:18 - Updating context: forget-ctx - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'forget-ctx' from '/tmp/8322031107398896423VCTPI9' - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/8322031107398896423VCTPI9 - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/8322031107398896423VCTPI9/.then/forget-ctx/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/forget-ctx/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'forget-ctx' from '/tmp/8322031107398896423VCTPI9': List(/tmp/8322031107398896423VCTPI9/.then/forget-ctx/fctx-def.conf) - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/8322031107398896423VCTPI9/.then/forget-ctx/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: forget-ctx - -2025.12.12 00:45:30 INFO first.core.Update.run:54 - Loaded context 'forget-ctx' with 2 artifacts - -2025.12.12 00:45:30 INFO first.core.Update.run:60 - Forgetting 1 artifacts: foo.txt - -2025.12.12 00:45:30 INFO first.core.Update.run:118 - Successfully updated fctx 'forget-ctx' - - + Update forgets artifact 0.009s -2025.12.12 00:45:30 DEBUG first.core.Update.run:18 - Updating context: includes-ctx - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'includes-ctx' from '/tmp/2820337592119703937RlETKS' - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/2820337592119703937RlETKS - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/2820337592119703937RlETKS/.then/includes-ctx/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/includes-ctx/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'includes-ctx' from '/tmp/2820337592119703937RlETKS': List(/tmp/2820337592119703937RlETKS/.then/includes-ctx/fctx-def.conf) - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/2820337592119703937RlETKS/.then/includes-ctx/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/2820337592119703937RlETKS - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/2820337592119703937RlETKS/.then/base/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/base/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'base' from '/tmp/2820337592119703937RlETKS': List() - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/2820337592119703937RlETKS - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/2820337592119703937RlETKS/.then/ext/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/ext/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'ext' from '/tmp/2820337592119703937RlETKS': List() - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: includes-ctx - -2025.12.12 00:45:30 INFO first.core.Update.run:54 - Loaded context 'includes-ctx' with 0 artifacts - -2025.12.12 00:45:30 DEBUG first.core.ArtifactProcessor.process:33 - Processing local/external artifact: /tmp/2820337592119703937RlETKS/new.txt -> new.txt - -2025.12.12 00:45:30 DEBUG first.core.Update.run:107 - Copied artifact: /tmp/2820337592119703937RlETKS/new.txt -> /tmp/2820337592119703937RlETKS/.then/includes-ctx/artifacts/new.txt - -2025.12.12 00:45:30 INFO first.core.Update.run:118 - Successfully updated fctx 'includes-ctx' - - + Update preserves includes 0.016s -2025.12.12 00:45:30 DEBUG first.core.Update.run:18 - Updating context: includes-manage - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'includes-manage' from '/tmp/3802151003282766197W52pLx' - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/3802151003282766197W52pLx - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/3802151003282766197W52pLx/.then/includes-manage/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/includes-manage/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'includes-manage' from '/tmp/3802151003282766197W52pLx': List(/tmp/3802151003282766197W52pLx/.then/includes-manage/fctx-def.conf) - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/3802151003282766197W52pLx/.then/includes-manage/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/3802151003282766197W52pLx - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/3802151003282766197W52pLx/.then/keep/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/keep/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'keep' from '/tmp/3802151003282766197W52pLx': List() - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/3802151003282766197W52pLx - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/3802151003282766197W52pLx/.then/remove/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/remove/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'remove' from '/tmp/3802151003282766197W52pLx': List() - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: includes-manage - -2025.12.12 00:45:30 INFO first.core.Update.run:54 - Loaded context 'includes-manage' with 0 artifacts - -2025.12.12 00:45:30 INFO first.core.Update.run:118 - Successfully updated fctx 'includes-manage' - - + Update adds and forgets includes 0.013s -2025.12.12 00:45:30 DEBUG first.core.Update.run:18 - Updating context: link-update - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'link-update' from '/tmp/8786708825091638038Fntx3p' - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/8786708825091638038Fntx3p - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/8786708825091638038Fntx3p/.then/link-update/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/link-update/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'link-update' from '/tmp/8786708825091638038Fntx3p': List(/tmp/8786708825091638038Fntx3p/.then/link-update/fctx-def.conf) - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/8786708825091638038Fntx3p/.then/link-update/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: link-update - -2025.12.12 00:45:30 INFO first.core.Update.run:54 - Loaded context 'link-update' with 0 artifacts - -2025.12.12 00:45:30 DEBUG first.core.ArtifactProcessor.process:33 - Processing local/external artifact: /tmp/8786708825091638038Fntx3p/link-me.txt -> link-me.txt - -2025.12.12 00:45:30 DEBUG first.core.Update.run:107 - Copied artifact: /tmp/8786708825091638038Fntx3p/link-me.txt -> /tmp/8786708825091638038Fntx3p/.then/link-update/artifacts/link-me.txt - -2025.12.12 00:45:30 DEBUG first.core.Update.run:112 - Linked /tmp/8786708825091638038Fntx3p/link-me.txt -> /tmp/8786708825091638038Fntx3p/.then/link-update/artifacts/link-me.txt - -2025.12.12 00:45:30 INFO first.core.Update.run:118 - Successfully updated fctx 'link-update' - - + Update --link links added artifacts 0.008s -2025.12.12 00:45:30 DEBUG first.core.Update.run:18 - Updating context: custom-loc - -2025.12.12 00:45:30 ERROR first.core.Update.run:25 - Context 'custom-loc' not found. - -==> X first.core.UpdateTests.Update works for custom location context 0.006s munit.FailException: /home/oswaldo/git/first/src/test/scala/first/core/UpdateTests.scala:304 assertion failed -303: -304: assert(os.exists(customDir / "artifacts" / "custom.txt")) -305: assert(os.read(customDir / "fctx-def.conf").contains("custom.txt")) -first.core.ArtifactProcessorTests: -2025.12.12 00:45:30 DEBUG first.core.ArtifactProcessor.process:33 - Processing local/external artifact: /tmp/4022069736977903406NvtmwW/foo.txt -> foo.txt - - + process local file inside workspace 0.001s -2025.12.12 00:45:30 DEBUG first.core.ArtifactProcessor.process:28 - Processing remote artifact: https://example.com/foo.txt - - + process remote artifact 0.000s -2025.12.12 00:45:30 DEBUG first.core.ArtifactProcessor.process:33 - Processing local/external artifact: /tmp/32823071817991955355gfnlt/outsider.conf -> outsider.conf - - + process external file 0.000s -first.config.GlobalConfigTests: - + GlobalConfig can add, list, update and remove contexts 0.006s -first.config.ConfigReaderTests: -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'my-context' from '/tmp/602971688414601366lqeU0v/valid-hocon' - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/602971688414601366lqeU0v/valid-hocon - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/602971688414601366lqeU0v/valid-hocon/.then/my-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/602971688414601366lqeU0v - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/602971688414601366lqeU0v/.then/my-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/my-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'my-context' from '/tmp/602971688414601366lqeU0v/valid-hocon': List(/tmp/602971688414601366lqeU0v/valid-hocon/.then/my-context/fctx-def.conf) - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/602971688414601366lqeU0v/valid-hocon/.then/my-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/602971688414601366lqeU0v/valid-hocon - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/602971688414601366lqeU0v/valid-hocon/.then/included-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/602971688414601366lqeU0v - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/602971688414601366lqeU0v/.then/included-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/included-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'included-context' from '/tmp/602971688414601366lqeU0v/valid-hocon': List(/tmp/602971688414601366lqeU0v/valid-hocon/.then/included-context/fctx-def.conf) - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/602971688414601366lqeU0v/valid-hocon/.then/included-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: my-context - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: my-context - - + successfully parse a valid HOCON config into FctxDef 0.012s -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'bad-context' from '/tmp/2003634630090545142MQ4R5S/invalid-hocon' - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/2003634630090545142MQ4R5S/invalid-hocon - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/2003634630090545142MQ4R5S/invalid-hocon/.then/bad-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/2003634630090545142MQ4R5S - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/2003634630090545142MQ4R5S/.then/bad-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/bad-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'bad-context' from '/tmp/2003634630090545142MQ4R5S/invalid-hocon': List(/tmp/2003634630090545142MQ4R5S/invalid-hocon/.then/bad-context/fctx-def.conf) - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/2003634630090545142MQ4R5S/invalid-hocon/.then/bad-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: bad-context - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:71 - Error mapping config to FctxDef: /tmp/2003634630090545142MQ4R5S/invalid-hocon/.then/bad-context/fctx-def.conf: 4: path has type OBJECT rather than STRING - - + return FileParseError for invalid HOCON syntax 0.005s -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'cumulative-context' from '/tmp/4582072941153976256HMbIPU/cumulative-loading' - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/4582072941153976256HMbIPU/cumulative-loading - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/4582072941153976256HMbIPU/cumulative-loading/.then/cumulative-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/4582072941153976256HMbIPU - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/4582072941153976256HMbIPU/.then/cumulative-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/cumulative-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'cumulative-context' from '/tmp/4582072941153976256HMbIPU/cumulative-loading': List(/home/oswaldo/.first/cumulative-context/fctx-def.conf, /tmp/4582072941153976256HMbIPU/cumulative-loading/.then/cumulative-context/fctx-def.conf) - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/4582072941153976256HMbIPU/cumulative-loading/.then/cumulative-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /home/oswaldo/.first/cumulative-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: cumulative-context - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: cumulative-context - - + cumulative config loading with overrides 0.006s -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'main-context' from '/tmp/8010528402483960769N3rMos/valid-includes' - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/8010528402483960769N3rMos/valid-includes - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/8010528402483960769N3rMos/valid-includes/.then/main-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/8010528402483960769N3rMos - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/8010528402483960769N3rMos/.then/main-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/main-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'main-context' from '/tmp/8010528402483960769N3rMos/valid-includes': List(/tmp/8010528402483960769N3rMos/valid-includes/.then/main-context/fctx-def.conf) - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/8010528402483960769N3rMos/valid-includes/.then/main-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/8010528402483960769N3rMos/valid-includes - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/8010528402483960769N3rMos/valid-includes/.then/included-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/8010528402483960769N3rMos - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/8010528402483960769N3rMos/.then/included-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/included-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'included-context' from '/tmp/8010528402483960769N3rMos/valid-includes': List(/tmp/8010528402483960769N3rMos/valid-includes/.then/included-context/fctx-def.conf) - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/8010528402483960769N3rMos/valid-includes/.then/included-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: main-context - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: main-context - - + valid includes should merge configurations 0.009s -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'context-a' from '/tmp/8732046518728768426leFPGo/circular-includes' - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/8732046518728768426leFPGo/circular-includes - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/8732046518728768426leFPGo/circular-includes/.then/context-a/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/8732046518728768426leFPGo - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/8732046518728768426leFPGo/.then/context-a/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/context-a/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'context-a' from '/tmp/8732046518728768426leFPGo/circular-includes': List(/tmp/8732046518728768426leFPGo/circular-includes/.then/context-a/fctx-def.conf) - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/8732046518728768426leFPGo/circular-includes/.then/context-a/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/8732046518728768426leFPGo/circular-includes - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/8732046518728768426leFPGo/circular-includes/.then/context-b/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/8732046518728768426leFPGo - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/8732046518728768426leFPGo/.then/context-b/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/context-b/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'context-b' from '/tmp/8732046518728768426leFPGo/circular-includes': List(/tmp/8732046518728768426leFPGo/circular-includes/.then/context-b/fctx-def.conf) - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/8732046518728768426leFPGo/circular-includes/.then/context-b/fctx-def.conf - - + circular includes should return CircularDependency error 0.007s -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'path-context' from '/tmp/3814542970136059507AjfHvQ/relative-paths/.then/path-context' - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/3814542970136059507AjfHvQ/relative-paths/.then/path-context - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/3814542970136059507AjfHvQ/relative-paths/.then/path-context/.then/path-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/3814542970136059507AjfHvQ/relative-paths/.then - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/3814542970136059507AjfHvQ/relative-paths/.then/.then/path-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/3814542970136059507AjfHvQ/relative-paths - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/3814542970136059507AjfHvQ/relative-paths/.then/path-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/3814542970136059507AjfHvQ - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/3814542970136059507AjfHvQ/.then/path-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/path-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'path-context' from '/tmp/3814542970136059507AjfHvQ/relative-paths/.then/path-context': List(/tmp/3814542970136059507AjfHvQ/relative-paths/.then/path-context/fctx-def.conf) - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/3814542970136059507AjfHvQ/relative-paths/.then/path-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: path-context - - + relative artifact paths should be preserved 0.005s -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'remote-context' from '/tmp/708541836262839629hfmoT7/remote-include' - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/708541836262839629hfmoT7/remote-include - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/708541836262839629hfmoT7/remote-include/.then/remote-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/708541836262839629hfmoT7 - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/708541836262839629hfmoT7/.then/remote-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/remote-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'remote-context' from '/tmp/708541836262839629hfmoT7/remote-include': List(/tmp/708541836262839629hfmoT7/remote-include/.then/remote-context/fctx-def.conf) - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/708541836262839629hfmoT7/remote-include/.then/remote-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.handleRemoteInclude:87 - Handling remote include: http://example.com/remote.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.handleRemoteInclude:98 - Downloaded remote include to /home/oswaldo/.first/cache/6360d95f7ab8eb33ce56cca702d9fe6def591918ca0c8e8bf4c7c090ab8edec2 - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: remote-context - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.mapConfigToFctxDef:37 - Mapping config to FctxDef for context: remote-context - - + remote include should download and merge config 0.006s -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.load:187 - Loading context 'main-context' from '/tmp/2415015308826155912dZ9Y0R/remote-circular' - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/2415015308826155912dZ9Y0R/remote-circular - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/2415015308826155912dZ9Y0R/remote-circular/.then/main-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp/2415015308826155912dZ9Y0R - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/2415015308826155912dZ9Y0R/.then/main-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:224 - Loop: currentPath = /tmp - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.loop:226 - Loop: fctxPath = /tmp/.then/main-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.discoverFctxDefPaths:245 - Discovered paths for context 'main-context' from '/tmp/2415015308826155912dZ9Y0R/remote-circular': List(/tmp/2415015308826155912dZ9Y0R/remote-circular/.then/main-context/fctx-def.conf) - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.parseConfigFile:22 - Parsing config file: /tmp/2415015308826155912dZ9Y0R/remote-circular/.then/main-context/fctx-def.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.handleRemoteInclude:87 - Handling remote include: http://example.com/config-a.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.handleRemoteInclude:98 - Downloaded remote include to /home/oswaldo/.first/cache/ad47a06310fcc3f2f59e00aa6f0f30d07dc5b1a3eedf69115945f49beef3897c - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.handleRemoteInclude:87 - Handling remote include: http://example.com/config-b.conf - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.handleRemoteInclude:98 - Downloaded remote include to /home/oswaldo/.first/cache/a8d433c03bd2d562f51e8d3a4b6a5f21160a023d938df19f5f57eea7a07075d9 - -2025.12.12 00:45:30 DEBUG first.config.ConfigReader.handleRemoteInclude:87 - Handling remote include: http://example.com/config-a.conf - - + circular remote includes should return CircularDependency error 0.008s -first.cli.HelpTests: - + simple test 0.000s - + first --help 0.001s -first.cli.CliTests: -Some utilized directives are marked as experimental: - - `//> using publish.organization oswaldo` - - `//> using publish.name first` - - `//> using publish.moduleName first` -Please bear in mind that non-ideal user experience should be expected. -If you encounter any bugs or have feedback to share, make sure to reach out to the maintenance team at https://github.com/VirtusLab/scala-cli -Wrote /tmp/2429281828181916449lEMC0a/first, run it with - /tmp/2429281828181916449lEMC0a/first - + save command should create a new fctx file 0.017s - + load command should create symlinks 0.037s - + swap command should update symlinks 0.043s - + ls command should list available contexts 0.017s - + load command should download gh:// artifact 0.130s