gemiterm 2.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.md +32 -9
  2. package/package.json +13 -4
  3. package/src/cli/command-registry.ts +66 -62
  4. package/src/cli/commands/continue-command.ts +75 -1
  5. package/src/cli/commands/install-skills-command.ts +35 -0
  6. package/src/cli/commands/new-command.ts +62 -0
  7. package/src/cli/index.ts +19 -44
  8. package/src/cli/utils/long-arg-guard.ts +28 -0
  9. package/src/cli/utils/prompt-file.ts +26 -0
  10. package/src/infrastructure/cli-parser.ts +77 -0
  11. package/src/infrastructure/io.ts +9 -0
  12. package/src/services/skills-cli-driver.ts +58 -0
  13. package/src/services/skills-service.ts +52 -0
  14. package/.env +0 -2
  15. package/.github/workflows/manual-release.yml +0 -34
  16. package/.github/workflows/release.yml +0 -198
  17. package/.github/workflows/test.yml +0 -29
  18. package/.opencode/commands/opsx-apply.md +0 -152
  19. package/.opencode/commands/opsx-archive.md +0 -157
  20. package/.opencode/commands/opsx-explore.md +0 -169
  21. package/.opencode/commands/opsx-propose.md +0 -104
  22. package/.opencode/commands/opsx-sync.md +0 -140
  23. package/.opencode/commands/search-intelligently.md +0 -9
  24. package/.opencode/opencode.json +0 -20
  25. package/.opencode/skills/intellisearch/.version +0 -1
  26. package/.opencode/skills/intellisearch/SKILL.md +0 -84
  27. package/.opencode/skills/intellisearch/references/brave-search.md +0 -67
  28. package/.opencode/skills/intellisearch/references/ddg-search.md +0 -79
  29. package/.opencode/skills/intellisearch/references/deepwiki-tools.md +0 -246
  30. package/.opencode/skills/intellisearch/references/examples.md +0 -97
  31. package/.opencode/skills/intellisearch/references/gh-cli.md +0 -143
  32. package/.opencode/skills/intellisearch/references/google-search.md +0 -40
  33. package/.opencode/skills/intellisearch/references/search-workflow.md +0 -143
  34. package/.opencode/skills/intellisearch/references/workflow.md +0 -115
  35. package/.opencode/skills/openspec-apply-change/SKILL.md +0 -159
  36. package/.opencode/skills/openspec-archive-change/SKILL.md +0 -117
  37. package/.opencode/skills/openspec-explore/SKILL.md +0 -287
  38. package/.opencode/skills/openspec-propose/SKILL.md +0 -111
  39. package/.opencode/skills/openspec-sync-specs/SKILL.md +0 -147
  40. package/.serena/project.yml +0 -133
  41. package/AGENTS.md +0 -136
  42. package/CHANGELOG.md +0 -3
  43. package/bun.lock +0 -105
  44. package/docs/INSTALL.md +0 -185
  45. package/docs/PLAYWRIGHT_CLI_API.md +0 -306
  46. package/docs/python-release-for-reference.md +0 -175
  47. package/examples/sample_json_export.json +0 -32
  48. package/examples/sample_markdown_export.md +0 -221
  49. package/examples/usage_examples.sh +0 -222
  50. package/install.ps1 +0 -264
  51. package/install.sh +0 -275
  52. package/openspec/MAESTRO_MIGRATION.md +0 -180
  53. package/openspec/changes/archive/2026-06-08-command-spec-conformance/.openspec.yaml +0 -2
  54. package/openspec/changes/archive/2026-06-08-command-spec-conformance/design.md +0 -194
  55. package/openspec/changes/archive/2026-06-08-command-spec-conformance/proposal.md +0 -52
  56. package/openspec/changes/archive/2026-06-08-command-spec-conformance/specs/multi-profile-conversations/spec.md +0 -133
  57. package/openspec/changes/archive/2026-06-08-command-spec-conformance/tasks.md +0 -95
  58. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/.openspec.yaml +0 -2
  59. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/design.md +0 -28
  60. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/proposal.md +0 -22
  61. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/specs/interactive-prompt-loop/spec.md +0 -35
  62. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/tasks.md +0 -28
  63. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/.openspec.yaml +0 -2
  64. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/design.md +0 -323
  65. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/proposal.md +0 -122
  66. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/specs/no-capability-changes/spec.md +0 -29
  67. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/tasks.md +0 -172
  68. package/openspec/changes/archive/2026-06-09-v2-install-migration/.openspec.yaml +0 -2
  69. package/openspec/changes/archive/2026-06-09-v2-install-migration/design.md +0 -241
  70. package/openspec/changes/archive/2026-06-09-v2-install-migration/proposal.md +0 -41
  71. package/openspec/changes/archive/2026-06-09-v2-install-migration/specs/v2-installer/spec.md +0 -205
  72. package/openspec/changes/archive/2026-06-09-v2-install-migration/tasks.md +0 -97
  73. package/openspec/changes/commander-cli-parser/.openspec.yaml +0 -2
  74. package/openspec/changes/commander-cli-parser/design.md +0 -107
  75. package/openspec/changes/commander-cli-parser/proposal.md +0 -31
  76. package/openspec/changes/commander-cli-parser/specs/cli/spec.md +0 -48
  77. package/openspec/changes/commander-cli-parser/tasks.md +0 -33
  78. package/openspec/changes/cross-platform-build-and-ci/.openspec.yaml +0 -2
  79. package/openspec/changes/cross-platform-build-and-ci/design.md +0 -213
  80. package/openspec/changes/cross-platform-build-and-ci/proposal.md +0 -50
  81. package/openspec/changes/cross-platform-build-and-ci/specs/path-and-file-mediation/spec.md +0 -335
  82. package/openspec/changes/cross-platform-build-and-ci/specs/release-pipeline/spec.md +0 -95
  83. package/openspec/changes/cross-platform-build-and-ci/tasks.md +0 -164
  84. package/openspec/config.yaml +0 -20
  85. package/openspec/specs/auth/spec.md +0 -394
  86. package/openspec/specs/cli/spec.md +0 -108
  87. package/openspec/specs/commands/spec.md +0 -409
  88. package/openspec/specs/configuration/spec.md +0 -130
  89. package/openspec/specs/conversations/spec.md +0 -139
  90. package/openspec/specs/domain-model/spec.md +0 -128
  91. package/openspec/specs/formatters/spec.md +0 -158
  92. package/openspec/specs/install-browser/spec.md +0 -146
  93. package/openspec/specs/interactive-prompt-loop/spec.md +0 -41
  94. package/openspec/specs/logger/spec.md +0 -71
  95. package/openspec/specs/mediator/spec.md +0 -72
  96. package/openspec/specs/multi-profile-conversations/spec.md +0 -145
  97. package/openspec/specs/path-utils/spec.md +0 -101
  98. package/openspec/specs/profiles/spec.md +0 -94
  99. package/openspec/specs/storage/spec.md +0 -198
  100. package/openspec/specs/testing/spec.md +0 -237
  101. package/openspec/specs/v2-installer/spec.md +0 -205
  102. package/openspec/specs/validators/spec.md +0 -65
  103. package/scripts/build.ts +0 -88
  104. package/scripts/clean-build.ps1 +0 -4
  105. package/scripts/clean-build.sh +0 -5
  106. package/scripts/install-browser.ps1 +0 -5
  107. package/scripts/install-browser.sh +0 -6
  108. package/scripts/lint-path-mediation.ps1 +0 -65
  109. package/scripts/lint-path-mediation.sh +0 -40
  110. package/tests/cli/auth-command.test.ts +0 -220
  111. package/tests/cli/command-registry.test.ts +0 -67
  112. package/tests/cli/continue-command.test.ts +0 -100
  113. package/tests/cli/delete-command.test.ts +0 -128
  114. package/tests/cli/export-all-command.test.ts +0 -375
  115. package/tests/cli/install-browser-command.test.ts +0 -48
  116. package/tests/cli/list-command.test.ts +0 -179
  117. package/tests/cli/new-command.test.ts +0 -139
  118. package/tests/cli/status-command.test.ts +0 -106
  119. package/tests/core/query-handlers.test.ts +0 -187
  120. package/tests/fixtures/auth-fixtures.ts +0 -98
  121. package/tests/fixtures/chat-fixtures.ts +0 -87
  122. package/tests/infrastructure/cli-table.test.ts +0 -107
  123. package/tests/infrastructure/formatters.test.ts +0 -202
  124. package/tests/infrastructure/io.test.ts +0 -197
  125. package/tests/infrastructure/logger.test.ts +0 -74
  126. package/tests/infrastructure/storage.test.ts +0 -335
  127. package/tests/infrastructure/validators.test.ts +0 -67
  128. package/tests/integration/commands/auth.test.ts +0 -186
  129. package/tests/integration/commands/continue.test.ts +0 -130
  130. package/tests/integration/commands/delete.test.ts +0 -140
  131. package/tests/integration/commands/export.test.ts +0 -266
  132. package/tests/integration/commands/fetch.test.ts +0 -238
  133. package/tests/integration/commands/list.test.ts +0 -281
  134. package/tests/integration/commands/profile.test.ts +0 -258
  135. package/tests/integration/commands/status.test.ts +0 -223
  136. package/tests/integration/installer-script-shape.test.ts +0 -67
  137. package/tests/parity/compare-outputs.ts +0 -359
  138. package/tests/parity/test-commands-parity.ps1 +0 -82
  139. package/tests/parity/test-commands-parity.sh +0 -61
  140. package/tests/services/auth-service.test.ts +0 -315
  141. package/tests/services/cookie-monitor.test.ts +0 -229
  142. package/tests/services/cookie-storage-service.test.ts +0 -230
  143. package/tests/services/gemini-client-wrapper.test.ts +0 -688
  144. package/tests/services/install-browser-service.test.ts +0 -83
  145. package/tests/services/playwright-cli-driver.test.ts +0 -468
  146. package/tests/services/profile-auth-manager.test.ts +0 -334
  147. package/tests/services/profile-service.test.ts +0 -224
  148. package/tests/setup.ts +0 -95
  149. package/tests/smoke/smoke.test.ts +0 -52
  150. package/tests/unit/config.test.ts +0 -159
  151. package/tests/unit/errors.test.ts +0 -119
  152. package/tests/unit/formatters.test.ts +0 -316
  153. package/tests/unit/mediator.test.ts +0 -152
  154. package/tests/unit/path-utils.test.ts +0 -289
  155. package/tsconfig.json +0 -16
@@ -1,213 +0,0 @@
1
- ## Context
2
-
3
- The Bun rewrite of GemiTerm is functionally complete (432/432 unit/integration/parity/smoke tests pass on Bun 1.3.13, all 11 CLI commands registered, auth flow working) but two structural pieces are missing: the **delivery pipeline** (build fix, CI, release) and the **path-and-file mediation layer** (a single source of truth for `node:fs` and `node:path` access in `src/`).
4
-
5
- The current `package.json:20-22` build scripts:
6
-
7
- ```json
8
- "build": "bun build --compile --target=bun --outdir=dist src/cli/index.ts",
9
- "build:linux": "bun build --compile --target=bun-linux-x64 --outdir=dist src/cli/index.ts",
10
- "build:windows": "bun build --compile --target=bun-windows-x64 --outdir=dist src/cli/index.ts"
11
- ```
12
-
13
- Verified failure mode on Bun 1.3.13 (current dev environment): `bun run build` exits 1 with `error: cannot use --compile with --outdir`. Bun 1.3.x removed support for combining `--compile` with `--outdir` because the resulting tree semantics are undefined for a single-file executable.
14
-
15
- The Python release workflow at `docs/python-release-for-reference.md` (used by v1.4.1) is the target shape for the new Bun release workflow, but with PyInstaller-specific steps replaced by `bun build --compile`. The Python release tagged artifacts as `GemiTerm` (Linux) and `GemiTerm.exe` (Windows); the Bun release MUST preserve those names so the v1.4.1 `install.ps1` referenced by the related `v2-install-migration` change keeps working.
16
-
17
- A read-only audit of `src/` shows the rewrite does **node:fs** and **node:path** access in 9 files, with at least 4 copy-pasted write-to-disk blocks and ~20 open-coded `mkdirSync` / `existsSync` / `renameSync` calls. The original OpenSpec change proposed a `platform-detect.ts` module to share one private `isWsl()` helper; that helper does not need a new file, and the broader duplication is the actual cross-platform work.
18
-
19
- The audit found:
20
-
21
- - **`src/cli/commands/export-command.ts:72-82`**, **`export-all-command.ts:79-80` + 96-104`**, **`list-command.ts:138-143`**, **`fetch-command.ts:121-127`** — the same "resolve path + ensure parent dir + writeFileSync UTF-8" block, copied 4 times. Two of those (`list` and `fetch`) are byte-identical 6-line `writeOutput` methods.
22
- - **`src/infrastructure/storage.ts`** — 11 inline `mkdirSync` / `writeFileSync` / `readFileSync` / `rmSync` / `renameSync` / `existsSync` calls in 6 methods.
23
- - **`src/infrastructure/config.ts`** — 64 lines, of which 30 are open-coded `node:fs` operations wrapping the `path-utils.ts` getters.
24
- - **`src/services/install-browser-service.ts`** — 11 inline `existsSync` / `readFileSync` calls; 2 near-identical `getEdgePaths` / `getChromePaths` methods; a private `isWsl()` that is the only consumer of `/proc/version` detection.
25
- - **`src/cli/index.ts:33-35`** — the only place in the rewrite that needs `__filename` / `__dirname` and is the only consumer of `fileURLToPath`; a one-liner helper would clean it up.
26
-
27
- The previous `isWsl()` is duplicated logic that will be needed by future command code, so it makes sense to lift it into `path-utils.ts`. The lift MUST be behavior-preserving; the 5 existing unit tests in `tests/services/install-browser-service.test.ts` are the regression gate. The `src/services/playwright-cli-driver.ts`, `src/services/cookie-monitor.ts`, and `src/services/auth-service.ts` files are off-limits and will not be touched by this change.
28
-
29
- Constraints:
30
-
31
- - **Bun 1.3.13** is the pinned toolchain (the env has it; `devDependencies.@types/bun: ^1.3.14`).
32
- - **GitHub Actions** is the CI/CD host (the repo is on GitHub per `docs/python-release-for-reference.md`).
33
- - **No breaking changes** to the CLI user surface; all changes are internal build/CI plus internal refactoring.
34
- - **No new runtime dependencies** are added; only removal of the unused `commander`.
35
- - **WSL** must be supported as a first-class target because GemiTerm's primary auth flow is browser-driven and WSL users invoke the Linux binary while their Chromium lives in the Windows host filesystem (handled today by `findWslBrowser` in `src/services/install-browser-service.ts:124-141`).
36
- - **The "no `node:fs` outside path-utils and io" rule is enforced by a CI lint check** in `.github/workflows/test.yml`, not by human review. The check excludes `tests/` and `tests/parity/` (a test-helper script) and the two allowed `src/` consumers (`path-utils.ts` and `install-browser-service.ts`).
37
-
38
- Stakeholders: maintainer (who ships the release), contributors (who need fast PR feedback), end users on Linux/Windows/WSL (who download the binary from GitHub Releases).
39
-
40
- ## Goals / Non-Goals
41
-
42
- **Goals:**
43
-
44
- - Fix `bun run build` / `build:linux` / `build:windows` so they produce a working single-file executable on Bun 1.3.13.
45
- - Add a `release-pipeline` capability: three GitHub Actions workflows (`test.yml`, `build.yml`, `release.yml`) plus the build/clean/install-browser shell scripts that produce v2.0.0 cross-platform artifacts.
46
- - Add a `path-and-file-mediation` capability: extend `src/infrastructure/path-utils.ts` with `isWSL()`, `getProjectRoot()`, and `getPackageJson()`; create `src/infrastructure/io.ts` with the file-ops helpers; refactor the 7 call sites that currently use `node:fs` / `node:path` directly; add a CI lint check that enforces the rule going forward.
47
- - Keep the artifact naming convention `GemiTerm` (Linux/WSL) and `GemiTerm.exe` (Windows) so the v1.4.1 `install.ps1` keeps working.
48
- - Cleanup: remove the orphan `{` file at the repo root, remove the stray `{` line in `.gitignore:23`, remove the unused `commander` dependency, decide the fate of the empty `src/commands/` placeholder.
49
- - Update README with a "Building from source" section and a "Release artifacts" section.
50
-
51
- **Non-Goals:**
52
-
53
- - Touching `src/services/playwright-cli-driver.ts`, `src/services/cookie-monitor.ts`, or `src/services/auth-service.ts` (sensitive area per the change's explicit warning).
54
- - Publishing to npm, PyPI, Homebrew, or any package registry — distribution is GitHub Releases + raw `install.sh` / `install.ps1` only.
55
- - Adding a code-signing step for Windows binaries (would require a cert and is deferred).
56
- - Adding macOS / Darwin builds to the matrix (no users on that platform have requested it; `isWSL()` is not relevant on macOS and the rewrite does not need a `isDarwin()` helper).
57
- - Migrating the existing Python `v1.4.1 install.ps1` (that lives in the related `v2-install-migration` change).
58
- - Adding new test cases for `install-browser-service` — the 5 existing tests are the regression gate and that is sufficient.
59
- - Switching the CI provider away from GitHub Actions.
60
-
61
- ## Decisions
62
-
63
- ### D1. `src/infrastructure/io.ts` is a separate file from `path-utils.ts`, not a re-export
64
-
65
- `path-utils.ts` is about **storage paths and config directories** (functions: `resolvePath`, `getConfigDir`, `getProfilesDir`, `getProfilePath`, `getProfileDir`, `getDefaultProfileMarkerPath`, plus three constants, plus the new `isWSL`, `getProjectRoot`, `getPackageJson`). `io.ts` is about **file-system operations** (`ensureDir`, `existsFile`, `readTextFile`, `writeTextFile`, `readJsonFile`, `writeJsonFile`, `removeDir`, `renameDir`, `isDirectory`, `listSubdirectories`). The two concerns are distinct: paths are values; file ops are side effects. Conflating them would create a God-module.
66
-
67
- The dependency direction is one-way: `io.ts` may import from `path-utils.ts` (for path joining and the WSL detection), but `path-utils.ts` does not import from `io.ts`. This is verifiable by `grep` and is part of the lint rule.
68
-
69
- **Alternative considered:** Put everything in `path-utils.ts`. Rejected: mixes concerns, makes the file longer, and obscures the dependency direction.
70
-
71
- **Alternative considered:** Put everything in `io.ts` and re-export from `path-utils.ts` for backward compatibility. Rejected: forces every call site to know which is the "real" home; the dependency direction is the wrong way around.
72
-
73
- ### D2. Drop `--outdir` from `bun build --compile`; use `--outfile` for deterministic artifact paths
74
-
75
- Verified failure mode on Bun 1.3.13: `bun build --compile --target=bun --outdir=dist src/cli/index.ts` exits 1 with `error: cannot use --compile with --outdir`. Bun 1.3.x removed this combination because a single-file executable has no meaningful relationship to an output directory tree.
76
-
77
- Two viable fixes in Bun 1.3.x:
78
-
79
- 1. **Drop `--outdir` entirely** → Bun places the binary in `cwd` named after the **parent directory** of the entrypoint. Fragile and surprising.
80
- 2. **Use `--outfile <path>`** → Bun places the binary at the specified path. Matches the v1.4.1 PyInstaller convention and gives CI a deterministic upload path.
81
-
82
- **Chosen: option 2** — `--outfile dist/gemiterm` for the platform-specific scripts. The resulting build scripts:
83
-
84
- ```json
85
- "build": "bun build --compile --target=bun --outfile dist/gemiterm src/cli/index.ts",
86
- "build:linux": "bun build --compile --target=bun-linux-x64 --outfile dist/gemiterm src/cli/index.ts",
87
- "build:windows": "bun build --compile --target=bun-windows-x64 --outfile dist/gemiterm src/cli/index.ts",
88
- "build:release": "bun build --compile --target=bun --outfile dist/gemiterm --minify src/cli/index.ts"
89
- ```
90
-
91
- ### D3. The `isWSL()` helper is in `path-utils.ts`, not in a separate `platform-detect.ts`
92
-
93
- The original spec proposed a 7-helper `platform-detect.ts` module (`isWindows`, `isLinux`, `isWSL`, `isDarwin`, `detectPlatform`, `getPlatformName`, `normalizePath`). A read-only audit found **zero callers** for six of those helpers; the only non-trivial check (`isWSL()`) is the one with a real consumer. Putting `isWSL()` in its own file is over-engineering; lifting it into `path-utils.ts` keeps the cross-platform concern in the same module as the other path-related code.
94
-
95
- The 6 unnecessary helpers (`isWindows`, `isLinux`, `isDarwin`, `detectPlatform`, `getPlatformName`, `normalizePath`) are **dropped**. The only `process.platform` checks in the rewrite are 3 inline in `install-browser-service.ts` (lines 58, 61, 185), all of which are clearer as inline `=== 'win32'` / `=== 'linux'` checks than as named boolean predicates.
96
-
97
- The `isWSL()` implementation is a strict superset of the old private `isWsl()`: it checks both `/proc/version` (for "microsoft" or "WSL" case-insensitive) AND the `WSL_DISTRO_NAME` env var. This is a minor improvement in detection coverage; the 5 unit tests in `tests/services/install-browser-service.test.ts` are not affected because they do not exercise WSL detection directly.
98
-
99
- ### D4. The 4-copy "write to disk" block becomes a single `writeTextFile()` helper
100
-
101
- The same 3-line pattern (`resolve path + ensure parent dir + writeFileSync UTF-8`) appears in `export-command.ts`, `export-all-command.ts`, `list-command.ts`, and `fetch-command.ts`. Two of those (`list` and `fetch`) are byte-identical 6-line `writeOutput` private methods. A single `writeTextFile(path, content)` in `io.ts` consolidates the pattern; the call sites collapse to one line each.
102
-
103
- ### D5. The CI lint check is a hard failure on PRs
104
-
105
- The rule "no `node:fs` / `node:path` / `node:os` imports in `src/` outside `path-utils.ts` and `install-browser-service.ts`" is enforced by a grep-based check in `.github/workflows/test.yml`:
106
-
107
- ```bash
108
- ! grep -rn --include='*.ts' --exclude-dir=infrastructure --exclude-dir=install-browser-service \
109
- "from \"node:\\(fs\\|path\\|os\\)\"" src/
110
- ```
111
-
112
- Wait, `--exclude-dir` in grep is for the directory the match is in, not the filename. The right invocation is:
113
-
114
- ```bash
115
- grep -rn --include='*.ts' "from \"node:\\(fs\\|path\\|os\\)\"" src/ \
116
- | grep -v "src/infrastructure/path-utils.ts" \
117
- | grep -v "src/services/install-browser-service.ts" \
118
- | (! grep .)
119
- ```
120
-
121
- The check is a hard failure: if any forbidden import is found, the test job exits non-zero. The exclusion list is `path-utils.ts` (the canonical home) and `install-browser-service.ts` (the only legitimate cross-platform `node:path` consumer, for the WSL mount parser). `tests/` is not in scope of the check (the rule is about `src/` only).
122
-
123
- **Alternative considered:** ESLint plugin with `no-restricted-imports`. Rejected: adds a new dev dependency and a config file; the grep check is one line and requires no additional tooling.
124
-
125
- ### D6. Three separate workflow files (test.yml, build.yml, release.yml) instead of one combined workflow
126
-
127
- The Python reference (`docs/python-release-for-reference.md:1-175`) uses a single `Release` workflow with all jobs inside. We will split into three because:
128
-
129
- 1. **`test.yml` runs on every PR** — it must be fast and not blocked by the slower build matrix.
130
- 2. **`build.yml` runs on tag push + manual dispatch** — it has a matrix that takes 5-10 minutes.
131
- 3. **`release.yml` is the only one that creates the GitHub Release** — splitting it means a re-publish can be done by re-running just `release.yml` after fixing a release-note mistake.
132
-
133
- This is the standard GitHub Actions pattern for "test on PR, build on tag, release on tag" and is well-supported by `actions/upload-artifact@v4` (artifacts persist 90 days by default, which is enough for `release.yml` to download them).
134
-
135
- ### D7. WSL is a separate build job, not a runtime check
136
-
137
- WSL needs its own `build-wsl` job that runs on `ubuntu-latest` and produces a Linux x64 binary. WSL itself can run any Linux ELF binary, so a regular Linux build is sufficient — but having a dedicated `build-wsl` job in the matrix makes the artifact explicit and named consistently in the GitHub Actions UI.
138
-
139
- ### D8. Artifact naming preserves v1.4.1 convention
140
-
141
- | Platform | Local build path | CI artifact name | Release asset name |
142
- |----------|------------------|------------------|--------------------|
143
- | Linux x64 | `dist/gemiterm` | `GemiTerm` | `GemiTerm` |
144
- | Windows x64 | `dist/gemiterm.exe` | `GemiTerm.exe` | `GemiTerm.exe` |
145
- | WSL (Linux x64) | `dist/gemiterm` | `GemiTerm` | (not released — only Linux) |
146
-
147
- The v1.4.1 `install.ps1` looks for `GemiTerm.exe` in the release assets.
148
-
149
- ### D9. `bunx @playwright/cli install chromium` is the canonical install command
150
-
151
- The existing `src/services/install-browser-service.ts:183` already spawns `bunx @playwright/cli install chromium`. The new user-facing `scripts/install-browser.sh` and `scripts/install-browser.ps1` will use the exact same command.
152
-
153
- ### D10. The `src/commands/` placeholder is removed
154
-
155
- The directory contains only a `.gitkeep`. Decision: remove both the directory and the `.gitkeep`. No code references it. The CLI commands live in `src/cli/commands/`.
156
-
157
- ## Risks / Trade-offs
158
-
159
- - **Risk:** `--outfile` was added in Bun 1.3.x; older Bun versions (1.2.x) don't have it. → **Mitigation:** `devDependencies.@types/bun: ^1.3.14` already pins us to 1.3.x; CI uses `oven-sh/setup-bun@v2` with `bun-version: 1.3.13` explicitly.
160
-
161
- - **Risk:** Cross-compile from Linux to Windows requires a Bun-built-for-Windows toolchain at compile time. → **Mitigation:** the `build-windows` job runs on `windows-latest`. We never cross-compile Windows FROM Linux.
162
-
163
- - **Risk:** The `isWSL()` is broader than the old private `isWsl()`. If any test or code path assumed the old narrower check, behavior could change. → **Mitigation:** the WSL branch in `findLinuxBrowser` only adds extra candidate paths; it never removes any. Worst case, more browsers are detected. The 5 unit tests in `tests/services/install-browser-service.test.ts` all pass against the new code.
164
-
165
- - **Risk:** The CI lint check uses `grep` rather than a proper linter, so it can be fooled by multi-line imports or import-string concatenation. → **Mitigation:** the rule is enforced on `src/`; all current `src/` files use single-line `import … from "node:fs"` statements. Future authors who try to obfuscate the import will fail code review.
166
-
167
- - **Risk:** A new contributor legitimately needs `node:fs` for a new feature in `src/`. → **Mitigation:** the right answer is to add a new helper to `io.ts` and use that. If the helper is genuinely impossible to write in `io.ts` (e.g. it requires direct `fs.watch` access), the contributor edits the `.github/workflows/test.yml` exclusion list with a comment explaining why.
168
-
169
- - **Risk:** `safeReadTextFile` returns `""` on any error (preserves the existing `readFileSafe` behavior in `install-browser-service.ts:173-179`). An empty return confuses "file does not exist" with "file exists but is empty". → **Mitigation:** the only current caller (`isWsl()` check on `/proc/version`) is robust to both `""` and `null`; the result is used in a `.toLowerCase().includes("microsoft")` check that returns false on `""`. A code comment in `io.ts` notes the `""` semantic and its implication for future callers.
170
-
171
- - **Risk:** The `io.ts` module grows over time and becomes its own God-module. → **Mitigation:** the module is already 10 functions focused on a single concern (file-system access). New functions should be added only if they are used in at least 2 call sites; ad-hoc single-use helpers should stay in the call site.
172
-
173
- - **Risk:** `softprops/action-gh-release@v2` is a third-party action. If it breaks, the release pipeline fails. → **Mitigation:** pin to a specific minor version (`@v2.3.x`); the Python reference already uses this action.
174
-
175
- - **Risk:** GitHub Actions matrix can fail intermittently. → **Mitigation:** no retry for v1.4.1; we match that. The maintainer re-runs on flake.
176
-
177
- - **Risk:** Removing `commander` from `package.json` and `bun.lock` requires `bun install` to regenerate the lockfile. → **Mitigation:** the verification task includes running `bun install --frozen-lockfile` to detect drift.
178
-
179
- - **Risk:** The orphan `{` file has been there long enough to be a known-good. Removing it could break a downstream tool that `cat`s the repo. → **Mitigation:** search the repo for any reference to a file named `{` (none found) before deletion. The file is untracked, so deletion is a no-op for git history.
180
-
181
- - **Trade-off:** Splitting CI into three workflows means the `release.yml` job has to re-run `test` and `build` jobs because of the `needs:` chain. → **Accepted:** with three workflows, `test.yml` and `build.yml` cache their results via `actions/upload-artifact@v4`; `release.yml` only re-runs the release step on manual re-run.
182
-
183
- - **Trade-off:** A separate `build-wsl` job doubles the matrix time vs. reusing `build-linux`. → **Accepted:** matrix runs in parallel; total wall-clock time is `max(linux, windows, wsl) ≈ 5 minutes` either way.
184
-
185
- ## Migration Plan
186
-
187
- The change is purely additive plus one CLI build fix and a refactor of 7 internal call sites. There is no user-visible migration.
188
-
189
- **Deploy steps** (all done in this PR):
190
-
191
- 1. Land the `path-utils.ts` extensions + `io.ts` module + refactor of the 7 call sites. Run `bun test` — must stay at 432/432 plus the new tests.
192
- 2. Fix `package.json:20-22` build scripts. Run `bun run build` and `bun run build:linux` locally — both must produce an executable.
193
- 3. Add the shell scripts (`scripts/clean-build.{sh,ps1}`, `scripts/install-browser.{sh,ps1}`).
194
- 4. Add the three GitHub Actions workflows, including the lint check in `test.yml`.
195
- 5. Cleanup: `.gitignore:23` (remove stray `{`), repo root (remove orphan `{`), `package.json:29` and `bun.lock` (remove `commander`), `src/commands/` (remove placeholder).
196
- 6. Update README with "Building from source" + "Release artifacts" sections.
197
- 7. After merge, push a `v2.0.0` tag. The `build.yml` matrix produces Linux + Windows + WSL binaries. The `release.yml` job attaches them to a GitHub Release with auto-generated notes.
198
-
199
- **Rollback strategy:**
200
-
201
- - The CLI build fix is not yet shipped, so there's nothing to roll back for end users.
202
- - The workflows only fire on tag push; if a `v2.0.0` tag is pushed and the release breaks, the maintainer can delete the GitHub Release or re-tag with a patch version.
203
- - The `install-browser-service` refactor has the 5 unit tests as a safety net; if any test fails post-refactor, revert the change to that file only.
204
- - The `io.ts` additions are additive; if they cause a circular import (verified not the case by inspection), revert by removing the import from the affected call site.
205
-
206
- ## Open Questions
207
-
208
- - **Q1:** Should `safeReadTextFile` return `""` (preserves the old `readFileSafe` behavior) or `null` (more honest API)? **Decision:** keep `""` for minimum diff and add a code comment noting the implication. Future contributors can add a `safeReadTextFileOrNull` if needed.
209
- - **Q2:** Should the WSL build be a separate artifact in the GitHub Release, or is it identical to the Linux artifact? **Decision:** WSL is a CI-only check; the artifact is not released. If users complain, add it later.
210
- - **Q3:** Should the `install-browser.sh` and `install-browser.ps1` scripts also be uploaded to the GitHub Release? **Decision:** yes, attach both, but the v1.4.1 `install.ps1` will be the one referenced by `v2-install-migration`.
211
- - **Q4:** Should the `test.yml` workflow also run on `windows-latest`? **Decision:** start with `ubuntu-latest` only; expand later if a Windows-only bug appears.
212
- - **Q5:** What is the minimum Bun version for the `build` scripts to work? **Decision:** Bun 1.3.13. Document in README.
213
- - **Q6:** Should the shared `isWSL()` also read `/proc/sys/kernel/osrelease` as a fallback? **Decision:** stick with `/proc/version` + `WSL_DISTRO_NAME`.
@@ -1,50 +0,0 @@
1
- ## Why
2
-
3
- The Bun rewrite is functionally complete (432/432 tests pass, all 11 commands registered, auth flow working), but the **delivery pipeline is missing**: `bun run build` is broken in Bun 1.3.x (`--compile` no longer accepts `--outdir`), there is no CI to catch regressions on PRs, no cross-platform build matrix, no GitHub release automation, and the rewrite's `src/` is full of direct `node:fs` and `node:path` imports that duplicate the same patterns in 4–8 places each (the "resolve + dirname + mkdirSync + writeFileSync" block alone appears in 4 export commands; `mkdirSync`/`existsSync`/`renameSync` are open-coded in `storage.ts` and `config.ts`).
4
-
5
- Two findings drive the change:
6
-
7
- 1. **The build/CI/release work** — fixing `bun run build`, adding `test.yml` / `build.yml` / `release.yml` workflows, and the repo-hygiene cleanup — is the original scope and stands as-is.
8
- 2. **Path and file-system access is duplicated across the rewrite** and is the actual cross-platform work. A read-only audit of `src/` finds **9 files** importing `node:fs` and `node:path` directly, with at least 4 copy-pasted "write to disk" blocks and ~20 open-coded `mkdirSync` / `existsSync` / `renameSync` calls. The original spec proposed a `platform-detect.ts` module to share one private `isWsl()` helper, but the duplication is much wider: the rewrite does not need a platform-detection module — it needs **mandatory mediation** of all file-system and path operations through `src/infrastructure/path-utils.ts` and a new `src/infrastructure/io.ts`.
9
-
10
- This change makes that mediation the rule, with a CI lint check that fails the build if any new `node:fs`/`node:path`/`node:os` import is added to `src/` outside the two allowed modules.
11
-
12
- ## What Changes
13
-
14
- - **Fix `package.json` build scripts** so `bun run build`, `bun run build:linux`, and `bun run build:windows` produce a working single-file executable on Bun 1.3.x (drop the `--outdir` flag from `--compile` invocations; use `--outfile`).
15
- - **Add `src/infrastructure/path-utils.ts` extensions**: `isWSL()`, `getProjectRoot()`, and `getPackageJson()`. The single private `isWsl()` in `install-browser-service.ts` is lifted into `path-utils.ts` (the only platform-detection helper we actually need; the rest of the proposed `platform-detect.ts` module is dropped).
16
- - **Add `src/infrastructure/io.ts`**: a new sibling module that consolidates every `node:fs` and `node:path` operation the rewrite actually uses. Exports: `ensureDir`, `existsFile`, `readTextFile`, `safeReadTextFile`, `writeTextFile`, `readJsonFile`, `writeJsonFile`, `removeDir`, `renameDir`, `isDirectory`, `listSubdirectories`. This is the **canonical home for all file-system access**; the rule is "no other file in `src/` imports from `node:fs` or `node:path` directly" (the only allowed exception is `install-browser-service.ts`, which keeps `node:path` for the WSL mount parser).
17
- - **Refactor `src/services/install-browser-service.ts`** to consume `isWSL()`, `safeReadTextFile`, `existsFile` from the new modules. The two near-identical `getEdgePaths` / `getChromePaths` methods are unified behind a `getWindowsKnownDirs()` local helper. The 5 existing unit tests in `tests/services/install-browser-service.test.ts` are the regression gate; the 432/432 baseline MUST be preserved.
18
- - **Refactor `src/infrastructure/storage.ts` and `src/infrastructure/config.ts`** to route all file-system access through `io.ts`. `config.ts` shrinks from 64 lines to ~40; `storage.ts` loses all `node:fs` imports.
19
- - **Refactor the 4 export commands** (`src/cli/commands/export-command.ts`, `export-all-command.ts`, `list-command.ts`, `fetch-command.ts`) to use `writeTextFile` and `ensureDir`. The 3-line "resolve + mkdir + write" block that appears in 4 places collapses to a single call. The two byte-identical `writeOutput` private methods in `list-command.ts` and `fetch-command.ts` each collapse to 2 lines.
20
- - **Refactor `src/cli/index.ts`** to use `getPackageJson()` from `path-utils.ts` instead of the inline `fileURLToPath` / `__dirname` / `readFileSync` / `JSON.parse` dance at lines 33–35.
21
- - **Add `.github/workflows/test.yml`**: trigger on `push` to `main` and on `pull_request`; single `test` job on `ubuntu-latest`; steps: `actions/checkout@v4`, `oven-sh/setup-bun@v2`, `bun install --frozen-lockfile`, `bun test`, `bun run typecheck`, and a final lint check that fails the build if any new `node:fs`/`node:path`/`node:os` import is added to `src/` outside the allowed files.
22
- - **Add `.github/workflows/build.yml`**: trigger on `push` of tags matching `v*` and `workflow_dispatch`; matrix jobs `build-linux`, `build-windows`, and `build-wsl`; each verifies its artifact exists, reports size, and uploads via `actions/upload-artifact@v4`.
23
- - **Add `.github/workflows/release.yml`**: trigger on `push` of tags matching `v*`; chains `test → build → release`; release job uses `softprops/action-gh-release@v2` with auto-generated notes and attaches `GemiTerm`, `GemiTerm.exe`, `install.sh`, and `install.ps1`.
24
- - **Add `scripts/clean-build.sh`** (POSIX) and **`scripts/clean-build.ps1`** (PowerShell) that remove `dist/` and any temp build artifacts.
25
- - **Add `scripts/install-browser.sh`** and **`scripts/install-browser.ps1`** as user-facing shell wrappers that run `bunx @playwright/cli install chromium` and verify the install.
26
- - **Cleanup**: remove the unused `commander` dependency from `package.json` and `bun.lock` (it is installed but never imported). Remove the stray bare `{` line from `.gitignore` and the orphan file literally named `{` at the repo root. Remove the empty `src/commands/` placeholder (contains only `.gitkeep`).
27
- - **README updates**: add a "Building from source" section and a "Release artifacts" section; update the "Development" section to mention the new `bun run typecheck` and the `scripts/install-browser.{sh,ps1}` wrappers.
28
-
29
- **No breaking changes** to the CLI user surface. The 4 export commands keep their existing flags and behavior; the rewrite is purely internal refactoring plus build/CI.
30
-
31
- ## Capabilities
32
-
33
- ### New Capabilities
34
-
35
- - `release-pipeline`: GitHub Actions workflows (test, build, release), the build/clean scripts, the install-browser shell wrappers, and the repo-hygiene cleanup. Creates `openspec/changes/cross-platform-build-and-ci/specs/release-pipeline/spec.md`.
36
- - `path-and-file-mediation`: A mandatory mediation layer that makes `src/infrastructure/path-utils.ts` and the new `src/infrastructure/io.ts` the only consumers of `node:fs`, `node:path`, and `node:os` in `src/`. Adds `isWSL()`, `getProjectRoot()`, and `getPackageJson()` to `path-utils.ts`; adds the file-ops helpers to the new `io.ts`. Refactors all 7 existing call sites to use the new helpers. Adds a CI lint check that enforces the rule on every PR. Creates `openspec/changes/cross-platform-build-and-ci/specs/path-and-file-mediation/spec.md`.
37
-
38
- ### Modified Capabilities
39
-
40
- - (none) — no existing capability has a spec-level requirement change. The `install-browser-service` refactor is an implementation detail covered by the regression test gate.
41
-
42
- ## Impact
43
-
44
- - **Code:** `src/infrastructure/path-utils.ts` (add 3 helpers), new `src/infrastructure/io.ts` (~120 lines), `src/services/install-browser-service.ts` (delegate to shared module — no behavior change), `src/infrastructure/storage.ts` (lose 6 `node:fs` imports, shrink to use `io.ts` helpers), `src/infrastructure/config.ts` (shrink from 64 to ~40 lines), `src/cli/commands/{export,export-all,list,fetch}-command.ts` (lose `node:fs`/`node:path` imports, replace 3-line write block with `writeTextFile`), `src/cli/index.ts` (lose `node:url`/`node:path`/`node:fs` imports).
45
- - **Build:** `package.json:20-22` (3 `build*` scripts) and `bun.lock` (drop `commander`).
46
- - **Tests:** Add ~15–20 tests to `tests/unit/path-utils.test.ts` for the new path-utils helpers. Add `tests/infrastructure/io.test.ts` with ~15 tests for the new `io.ts` helpers. The existing 5 `install-browser-service.test.ts` cases are the regression gate for the `install-browser-service` refactor. CI must pass with `bun test` (currently 432/432) and `bun run typecheck` (currently 0 errors).
47
- - **Repo hygiene:** `.gitignore:23` (remove stray `{`) and the untracked file named `{` at the repo root. Delete empty `src/commands/` (only `.gitkeep` inside).
48
- - **README:** new "Building from source" and "Release artifacts" sections; update "Development" section.
49
- - **Docs dependency:** the related `v2-install-migration` change will reference the release artifacts and the v2.0.0 install scripts.
50
- - **SENSITIVE AREA:** the `install-browser-service` refactor MUST be behavior-preserving. The `playwright-cli-driver` and auth flow files are NOT touched by this change. The CI lint check is the enforcement mechanism for the "no direct `node:fs`" rule; existing code is migrated by hand as part of this change.
@@ -1,335 +0,0 @@
1
- ## Purpose
2
-
3
- This spec defines the **path-and-file mediation layer** for the GemiTerm Bun rewrite. Its success criterion is:
4
-
5
- > **No file in `src/` (other than the allowed exemptions) imports from `node:fs`, `node:path`, or `node:os` directly. All file-system and path operations route through `src/infrastructure/path-utils.ts` (path values) and `src/infrastructure/io.ts` (file-system side effects).**
6
-
7
- A CI lint check (added in `release-pipeline/spec.md` §CI Test Job) enforces this rule on every PR.
8
-
9
- The only allowed `node:fs` / `node:path` / `node:os` consumers in `src/` are:
10
-
11
- - `src/infrastructure/path-utils.ts` — the canonical home for all path-related code.
12
- - `src/services/install-browser-service.ts` — keeps a `node:path` import for the WSL `9p`/`drvfs` mount parser, which is inherently Unix-specific and not a general-purpose cross-platform concern.
13
-
14
- ---
15
-
16
- ## ADDED Requirements
17
-
18
- ### Requirement: `path-utils.ts` is the only path value source in `src/`
19
-
20
- The system MUST provide `src/infrastructure/path-utils.ts` as the canonical home for all path-related functions and constants. No other source file in `src/` MAY import from `node:path` or `node:os` directly. The only allowed `node:path` consumer in `src/` is `src/services/install-browser-service.ts`, which uses `join` and `sep` for the WSL mount parser.
21
-
22
- The `path-utils.ts` module MUST export the existing 9 symbols (the 6 path functions: `resolvePath`, `getConfigDir`, `getProfilesDir`, `getProfilePath`, `getProfileDir`, `getDefaultProfileMarkerPath`; plus 3 constants: `STORAGE_STATE_FILE`, `PROFILES_DIR`, `DEFAULT_PROFILE_MARKER`).
23
-
24
- #### Scenario: A new src/ file wants to build a path
25
-
26
- - **WHEN** a developer needs to build a path inside `src/`
27
- - **THEN** they MUST use one of the exported functions from `path-utils.ts` (e.g. `join(getProfileDir(name), "subdir")` is fine, but `join` from `node:path` is not)
28
-
29
- #### Scenario: An existing src/ file is audited
30
-
31
- - **WHEN** the CI lint check runs over `src/`
32
- - **THEN** the only matches for `from "node:path"` or `from "node:os"` are `src/infrastructure/path-utils.ts` and `src/services/install-browser-service.ts`
33
-
34
- ### Requirement: `io.ts` is the only file-system operation source in `src/`
35
-
36
- The system MUST provide `src/infrastructure/io.ts` as the canonical home for all file-system operations. No other source file in `src/` MAY import from `node:fs` directly. The CI lint check enforces this rule.
37
-
38
- The `io.ts` module MUST export the following 11 functions plus one error class: `ensureDir`, `existsFile`, `readTextFile`, `safeReadTextFile`, `writeTextFile`, `readJsonFile`, `writeJsonFile`, `removeDir`, `renameDir`, `isDirectory`, `listSubdirectories`, and `IOError`.
39
-
40
- #### Scenario: A new src/ file wants to read a file
41
-
42
- - **WHEN** a developer needs to read a file inside `src/`
43
- - **THEN** they MUST use `readTextFile` or `readJsonFile` from `io.ts`, never `readFileSync` from `node:fs`
44
-
45
- #### Scenario: A new src/ file wants to write a file
46
-
47
- - **WHEN** a developer needs to write a file inside `src/`
48
- - **THEN** they MUST use `writeTextFile` or `writeJsonFile` from `io.ts`, which creates the parent directory if it does not exist, never `writeFileSync` from `node:fs`
49
-
50
- #### Scenario: A new src/ file wants to check file existence
51
-
52
- - **WHEN** a developer needs to check whether a file or directory exists
53
- - **THEN** they MUST use `existsFile` from `io.ts`, never `existsSync` from `node:fs`
54
-
55
- ### Requirement: `isWSL()` detects WSL via `/proc/version` or `WSL_DISTRO_NAME`
56
-
57
- The system MUST provide `isWSL(): boolean` in `src/infrastructure/path-utils.ts`. The function MUST return `false` on any platform other than Linux. On Linux, it MUST return `true` if either:
58
-
59
- - `/proc/version` exists and contains the substring `microsoft` or `WSL` (case-insensitive), OR
60
- - the `WSL_DISTRO_NAME` environment variable is set and non-empty.
61
-
62
- If neither condition holds, the function MUST return `false`.
63
-
64
- #### Scenario: WSL detected via `/proc/version`
65
-
66
- - **WHEN** `process.platform === "linux"` and `/proc/version` contains the substring `microsoft`
67
- - **THEN** `isWSL()` returns `true`
68
-
69
- #### Scenario: WSL detected via `WSL_DISTRO_NAME`
70
-
71
- - **WHEN** `process.platform === "linux"` and `WSL_DISTRO_NAME` is set to any non-empty string
72
- - **THEN** `isWSL()` returns `true` even if `/proc/version` does not contain the `microsoft` substring
73
-
74
- #### Scenario: Native Linux returns false
75
-
76
- - **WHEN** `process.platform === "linux"` and neither `/proc/version` contains `microsoft` or `WSL` nor `WSL_DISTRO_NAME` is set
77
- - **THEN** `isWSL()` returns `false`
78
-
79
- #### Scenario: macOS or Windows returns false
80
-
81
- - **WHEN** `process.platform` is `"darwin"` or `"win32"`
82
- - **THEN** `isWSL()` returns `false` regardless of `/proc/version` or environment variable contents
83
-
84
- ### Requirement: `getProjectRoot()` finds the repository root from `import.meta.url`
85
-
86
- The system MUST provide `getProjectRoot(importMetaUrl?: string): string` in `src/infrastructure/path-utils.ts`. The function MUST walk up from the caller (or the provided `import.meta.url`) until it finds a directory containing `package.json`, then return the absolute path. The function MUST throw `IOError` if no `package.json` is found in any parent directory.
87
-
88
- #### Scenario: Called from a file in the repo
89
-
90
- - **WHEN** `getProjectRoot()` is called from any file under the repository root
91
- - **THEN** it returns the absolute path of the repository root (the directory containing `package.json`)
92
-
93
- #### Scenario: Called with no `package.json` ancestors
94
-
95
- - **WHEN** `getProjectRoot()` is called from a file that has no `package.json` in any parent directory
96
- - **THEN** it throws an `IOError` with a message indicating the walk completed without finding `package.json`
97
-
98
- ### Requirement: `getPackageJson()` reads and parses `package.json`
99
-
100
- The system MUST provide `getPackageJson(): { name: string; version: string; [key: string]: unknown }` in `src/infrastructure/path-utils.ts`. The function MUST call `getProjectRoot(import.meta.url)`, read `<root>/package.json` as UTF-8, parse it as JSON, and return the parsed object. On any error (file missing, file unparseable, no `package.json` ancestor), the function MUST return `{ name: "gemiterm", version: "unknown" }` as a fallback.
101
-
102
- #### Scenario: Normal call
103
-
104
- - **WHEN** `getPackageJson()` is called from a file under the repository root
105
- - **THEN** it returns the parsed `package.json` object, with `name === "gemiterm"` and `version === "2.0.0"`
106
-
107
- #### Scenario: `package.json` missing or unparseable
108
-
109
- - **WHEN** the resolved `package.json` does not exist or is unparseable JSON
110
- - **THEN** `getPackageJson()` returns the fallback object `{ name: "gemiterm", version: "unknown" }`
111
-
112
- ### Requirement: `writeTextFile` consolidates the 4-copy "resolve + mkdir + write" block
113
-
114
- The system MUST provide `writeTextFile(path: string, content: string): void` in `src/infrastructure/io.ts`. The function MUST resolve `path` to an absolute path, ensure the parent directory exists (recursively), and write `content` as UTF-8. The function MUST throw `IOError` on EACCES, ENOSPC, or any other write error.
115
-
116
- The 4 CLI commands `src/cli/commands/export-command.ts`, `export-all-command.ts`, `list-command.ts`, and `fetch-command.ts` MUST use `writeTextFile` in place of their existing `resolve + dirname + mkdirSync + writeFileSync` sequences.
117
-
118
- #### Scenario: Writing a new file to an existing directory
119
-
120
- - **WHEN** `writeTextFile("/tmp/gemiterm-test/output.md", "# Hello")` is called and `/tmp/gemiterm-test` exists
121
- - **THEN** the file `/tmp/gemiterm-test/output.md` is created with the content `# Hello`
122
-
123
- #### Scenario: Writing a new file to a nested non-existent directory
124
-
125
- - **WHEN** `writeTextFile("/tmp/gemiterm-test/nested/dir/output.md", "# Hello")` is called and neither `/tmp/gemiterm-test/nested` nor `/tmp/gemiterm-test/nested/dir` exist
126
- - **THEN** both directories are created recursively, and the file is written
127
-
128
- ### Requirement: `readJsonFile` and `writeJsonFile` provide typed JSON I/O
129
-
130
- The system MUST provide `readJsonFile<T>(path: string): T` and `writeJsonFile(path: string, data: unknown): void` in `src/infrastructure/io.ts`. `readJsonFile` MUST read the file as UTF-8, parse it as JSON, and cast the result to `T`. `writeJsonFile` MUST serialize `data` with `JSON.stringify(data, null, 2)` and write it via `writeTextFile`. Both functions MUST throw `IOError` (with a `cause` field for the original error) on any failure.
131
-
132
- #### Scenario: Round-trip a JSON object
133
-
134
- - **WHEN** a developer writes an object via `writeJsonFile(path, obj)` and reads it back via `readJsonFile<typeof obj>(path)`
135
- - **THEN** the deep-equal comparison of the two values is true
136
-
137
- #### Scenario: `readJsonFile` on a missing file
138
-
139
- - **WHEN** `readJsonFile` is called with a path that does not exist
140
- - **THEN** it throws `IOError` whose `cause` is the underlying `ENOENT` error
141
-
142
- #### Scenario: `readJsonFile` on unparseable content
143
-
144
- - **WHEN** `readJsonFile` is called with a path whose content is not valid JSON
145
- - **THEN** it throws `IOError` whose `cause` is the underlying `SyntaxError`
146
-
147
- ### Requirement: `safeReadTextFile` returns `""` on any error
148
-
149
- The system MUST provide `safeReadTextFile(path: string): string` in `src/infrastructure/io.ts`. The function MUST read the file as UTF-8 and return the content; on any error (ENOENT, EACCES, EISDIR), it MUST return the empty string `""`. The function MUST include a JSDoc comment noting that the `""` return conflates "file does not exist" with "file exists but is empty" and is appropriate for callers that only need a string for `.includes()` / `.trim()` checks.
150
-
151
- #### Scenario: Reading an existing file
152
-
153
- - **WHEN** `safeReadTextFile` is called with a path that exists
154
- - **THEN** it returns the file's content
155
-
156
- #### Scenario: Reading a missing file
157
-
158
- - **WHEN** `safeReadTextFile` is called with a path that does not exist
159
- - **THEN** it returns `""` and does not throw
160
-
161
- #### Scenario: Code comment is present
162
-
163
- - **WHEN** the source of `safeReadTextFile` is inspected
164
- - **THEN** the JSDoc comment is present and explicitly notes the `""` return and its implication
165
-
166
- ### Requirement: `ensureDir`, `removeDir`, `renameDir`, `isDirectory`, `listSubdirectories` provide safe file-system access
167
-
168
- The system MUST provide the following 5 functions in `src/infrastructure/io.ts`:
169
-
170
- - `ensureDir(path: string): void` — calls `mkdirSync(path, { recursive: true })`. Throws `IOError` on EACCES, EPERM, EROFS, or any other error.
171
- - `removeDir(path: string): void` — calls `rmSync(path, { recursive: true, force: true })`. The `force: true` flag means the function is a no-op if the path does not exist. Throws `IOError` on EACCES, EBUSY, or any other error.
172
- - `renameDir(src: string, dest: string): void` — calls `renameSync`. Throws `IOError` with a clear message on ENOENT (source missing) or EEXIST (destination present).
173
- - `isDirectory(path: string): boolean` — returns `true` if `statSync(path).isDirectory()` succeeds and the result is `true`; returns `false` on any error (including ENOENT).
174
- - `listSubdirectories(path: string): string[]` — reads the directory and returns the names of immediate subdirectories only. Returns `[]` if the path does not exist. The `.default` marker file (or any other non-directory entry) is excluded.
175
-
176
- #### Scenario: `ensureDir` creates nested directories
177
-
178
- - **WHEN** `ensureDir("/tmp/a/b/c")` is called and `/tmp/a` does not exist
179
- - **THEN** `/tmp/a`, `/tmp/a/b`, and `/tmp/a/b/c` are all created
180
-
181
- #### Scenario: `removeDir` is a no-op on missing path
182
-
183
- - **WHEN** `removeDir("/tmp/nonexistent")` is called
184
- - **THEN** no error is thrown and the function returns
185
-
186
- #### Scenario: `renameDir` throws on missing source
187
-
188
- - **WHEN** `renameDir("/tmp/nope", "/tmp/dest")` is called and `/tmp/nope` does not exist
189
- - **THEN** `IOError` is thrown with a message that includes the source path
190
-
191
- #### Scenario: `listSubdirectories` excludes the marker file
192
-
193
- - **WHEN** `listSubdirectories` is called on a profile directory containing both subdirectories and a `.default` marker file
194
- - **THEN** the returned array contains only the subdirectory names; `.default` is excluded
195
-
196
- ### Requirement: `IOError` provides structured error context
197
-
198
- The system MUST export an `IOError` class from `src/infrastructure/io.ts` that extends `Error` and includes a `cause` field (of type `Error | undefined`) for the original error. All other functions in `io.ts` MUST throw `IOError` (with a populated `cause` when the original error is available) rather than raw `Error` or re-throwing the original error.
199
-
200
- #### Scenario: `IOError` carries a cause
201
-
202
- - **WHEN** a function in `io.ts` catches an `ENOENT` error from `node:fs` and re-throws as `IOError`
203
- - **THEN** the `IOError.cause` field is the original `ENOENT` error
204
-
205
- #### Scenario: Error name is set
206
-
207
- - **WHEN** an `IOError` is constructed
208
- - **THEN** its `name` field is `"IOError"` (so `error.name === "IOError"` is true)
209
-
210
- ### Requirement: `install-browser-service` is the only WSL-detection consumer
211
-
212
- The system MUST refactor `src/services/install-browser-service.ts` to:
213
-
214
- - import `isWSL` from `path-utils.ts` and delete the private `isWsl()` method.
215
- - import `existsFile` and `safeReadTextFile` from `io.ts` and delete the private `readFileSafe` helper.
216
- - unify `getEdgePaths()` and `getChromePaths()` behind a single local `getWindowsKnownDirs()` helper.
217
-
218
- The 5 unit tests in `tests/services/install-browser-service.test.ts` MUST continue to pass without modification; the 432/432 baseline MUST be maintained.
219
-
220
- #### Scenario: `isWsl()` private method is gone
221
-
222
- - **WHEN** `src/services/install-browser-service.ts` is inspected
223
- - **THEN** the file does not define a private `isWsl()` method
224
-
225
- #### Scenario: `isWSL()` is the only WSL detection source
226
-
227
- - **WHEN** the rewrite needs to check whether it is running under WSL
228
- - **THEN** the check is made via the shared `isWSL()` from `path-utils.ts`, not via inline `/proc/version` reads
229
-
230
- #### Scenario: `getEdgePaths` and `getChromePaths` are unified
231
-
232
- - **WHEN** `src/services/install-browser-service.ts` is inspected
233
- - **THEN** both methods are short (≤5 lines each) and delegate path construction to a shared `getWindowsKnownDirs()` helper
234
-
235
- ### Requirement: `storage.ts` and `config.ts` route all file access through `io.ts`
236
-
237
- The system MUST refactor `src/infrastructure/storage.ts` and `src/infrastructure/config.ts` to import from `./io.ts` instead of `node:fs`. After the refactor, neither file MUST import from `node:fs` or `node:path` directly. `config.ts` MUST shrink from 64 lines to approximately 40 lines.
238
-
239
- The 21 unit tests in `tests/infrastructure/storage.test.ts` and the 13 unit tests in `tests/unit/config.test.ts` MUST continue to pass without modification.
240
-
241
- #### Scenario: `storage.ts` has no `node:fs` import
242
-
243
- - **WHEN** `src/infrastructure/storage.ts` is inspected
244
- - **THEN** the file does not contain `from "node:fs"`
245
-
246
- #### Scenario: `config.ts` has no `node:fs` or `node:path` import
247
-
248
- - **WHEN** `src/infrastructure/config.ts` is inspected
249
- - **THEN** the file does not contain `from "node:fs"` or `from "node:path"`
250
-
251
- #### Scenario: `storage.ts` tests still pass
252
-
253
- - **WHEN** `bun test tests/infrastructure/storage.test.ts` is run after the refactor
254
- - **THEN** all 21 tests pass
255
-
256
- #### Scenario: `config.ts` tests still pass
257
-
258
- - **WHEN** `bun test tests/unit/config.test.ts` is run after the refactor
259
- - **THEN** all 13 tests pass
260
-
261
- ### Requirement: 4 export commands use `writeTextFile`
262
-
263
- The system MUST refactor `src/cli/commands/export-command.ts`, `export-all-command.ts`, `list-command.ts`, and `fetch-command.ts` so that each file uses `writeTextFile` and `ensureDir` from `io.ts` instead of inline `node:fs` operations. After the refactor, none of the 4 files MUST import from `node:fs` or `node:path` directly. The integration tests in `tests/integration/commands/{export,list,fetch}.test.ts` and `tests/cli/export-all-command.test.ts` MUST continue to pass without modification.
264
-
265
- #### Scenario: `export-command.ts` uses `writeTextFile`
266
-
267
- - **WHEN** `src/cli/commands/export-command.ts` is inspected
268
- - **THEN** the file imports `writeTextFile` from `../../infrastructure/io.ts` and does not import from `node:fs` or `node:path`
269
-
270
- #### Scenario: `export-all-command.ts` uses `writeTextFile` and `ensureDir`
271
-
272
- - **WHEN** `src/cli/commands/export-all-command.ts` is inspected
273
- - **THEN** the file imports `writeTextFile` and `ensureDir` from `../../infrastructure/io.ts` and does not import from `node:fs` or `node:path`
274
-
275
- #### Scenario: `list-command.ts` and `fetch-command.ts` have a 2-line `writeOutput`
276
-
277
- - **WHEN** `src/cli/commands/list-command.ts` and `src/cli/commands/fetch-command.ts` are inspected
278
- - **THEN** the `writeOutput` private method in each is 2 lines (a `writeTextFile` call and a `console.log`)
279
-
280
- #### Scenario: Integration tests still pass
281
-
282
- - **WHEN** `bun test tests/integration/commands/{export,list,fetch}.test.ts tests/cli/export-all-command.test.ts` is run after the refactor
283
- - **THEN** all tests pass
284
-
285
- ### Requirement: `cli/index.ts` uses `getPackageJson()`
286
-
287
- The system MUST refactor `src/cli/index.ts` to import `getPackageJson` from `src/infrastructure/path-utils.ts` and remove the inline `fileURLToPath` / `__dirname` / `readFileSync` / `JSON.parse` sequence at lines 33-35. After the refactor, `src/cli/index.ts` MUST NOT import from `node:fs`, `node:path`, or `node:url`. The `--version` flag MUST continue to print `2.0.0`; the smoke test in `tests/smoke/smoke.test.ts` is the regression gate.
288
-
289
- #### Scenario: `cli/index.ts` has no `node:fs` / `node:path` / `node:url` import
290
-
291
- - **WHEN** `src/cli/index.ts` is inspected
292
- - **THEN** the file does not contain `from "node:fs"`, `from "node:path"`, or `from "node:url"`
293
-
294
- #### Scenario: `--version` still prints `2.0.0`
295
-
296
- - **WHEN** `gemiterm --version` is run
297
- - **THEN** the output includes the string `2.0.0`
298
-
299
- ### Requirement: CI lint check fails the build on a forbidden import
300
-
301
- The system MUST add a step to `.github/workflows/test.yml` that runs the following command and fails the workflow if the command exits non-zero:
302
-
303
- ```bash
304
- ! grep -rn --include='*.ts' "from \"node:\\(fs\\|path\\|os\\)\"" src/ \
305
- | grep -v "src/infrastructure/path-utils.ts" \
306
- | grep -v "src/services/install-browser-service.ts" \
307
- | grep -q .
308
- ```
309
-
310
- The check MUST exclude `src/infrastructure/path-utils.ts` (the canonical home) and `src/services/install-browser-service.ts` (the only legitimate cross-platform `node:path` consumer). The check MUST NOT exclude any other file.
311
-
312
- #### Scenario: A new src/ file imports from `node:fs`
313
-
314
- - **WHEN** a contributor adds `import { readFileSync } from "node:fs";` to a new file `src/services/foo.ts` and opens a PR
315
- - **THEN** the `test.yml` workflow's "Enforce path-utils mediation" step exits non-zero and the PR cannot be merged
316
-
317
- #### Scenario: A new src/ file imports from `node:path`
318
-
319
- - **WHEN** a contributor adds `import { join } from "node:path";` to `src/services/foo.ts`
320
- - **THEN** the workflow fails with a clear error message naming the file
321
-
322
- #### Scenario: A new src/ file imports from `node:os`
323
-
324
- - **WHEN** a contributor adds `import { homedir } from "node:os";` to `src/services/foo.ts`
325
- - **THEN** the workflow fails
326
-
327
- #### Scenario: The two allowed files are exempt
328
-
329
- - **WHEN** the check is run and the only matches are in `src/infrastructure/path-utils.ts` and `src/services/install-browser-service.ts`
330
- - **THEN** the check exits 0 (success)
331
-
332
- #### Scenario: Tests directory is out of scope
333
-
334
- - **WHEN** the check is run and a test file in `tests/` imports from `node:fs`
335
- - **THEN** the check is unaffected (it scans `src/` only)