gemiterm 2.0.0 → 2.1.0

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 (150) 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/install-skills-command.ts +35 -0
  5. package/src/cli/index.ts +19 -44
  6. package/src/infrastructure/cli-parser.ts +77 -0
  7. package/src/services/skills-cli-driver.ts +58 -0
  8. package/src/services/skills-service.ts +52 -0
  9. package/.env +0 -2
  10. package/.github/workflows/manual-release.yml +0 -34
  11. package/.github/workflows/release.yml +0 -198
  12. package/.github/workflows/test.yml +0 -29
  13. package/.opencode/commands/opsx-apply.md +0 -152
  14. package/.opencode/commands/opsx-archive.md +0 -157
  15. package/.opencode/commands/opsx-explore.md +0 -169
  16. package/.opencode/commands/opsx-propose.md +0 -104
  17. package/.opencode/commands/opsx-sync.md +0 -140
  18. package/.opencode/commands/search-intelligently.md +0 -9
  19. package/.opencode/opencode.json +0 -20
  20. package/.opencode/skills/intellisearch/.version +0 -1
  21. package/.opencode/skills/intellisearch/SKILL.md +0 -84
  22. package/.opencode/skills/intellisearch/references/brave-search.md +0 -67
  23. package/.opencode/skills/intellisearch/references/ddg-search.md +0 -79
  24. package/.opencode/skills/intellisearch/references/deepwiki-tools.md +0 -246
  25. package/.opencode/skills/intellisearch/references/examples.md +0 -97
  26. package/.opencode/skills/intellisearch/references/gh-cli.md +0 -143
  27. package/.opencode/skills/intellisearch/references/google-search.md +0 -40
  28. package/.opencode/skills/intellisearch/references/search-workflow.md +0 -143
  29. package/.opencode/skills/intellisearch/references/workflow.md +0 -115
  30. package/.opencode/skills/openspec-apply-change/SKILL.md +0 -159
  31. package/.opencode/skills/openspec-archive-change/SKILL.md +0 -117
  32. package/.opencode/skills/openspec-explore/SKILL.md +0 -287
  33. package/.opencode/skills/openspec-propose/SKILL.md +0 -111
  34. package/.opencode/skills/openspec-sync-specs/SKILL.md +0 -147
  35. package/.serena/project.yml +0 -133
  36. package/AGENTS.md +0 -136
  37. package/CHANGELOG.md +0 -3
  38. package/bun.lock +0 -105
  39. package/docs/INSTALL.md +0 -185
  40. package/docs/PLAYWRIGHT_CLI_API.md +0 -306
  41. package/docs/python-release-for-reference.md +0 -175
  42. package/examples/sample_json_export.json +0 -32
  43. package/examples/sample_markdown_export.md +0 -221
  44. package/examples/usage_examples.sh +0 -222
  45. package/install.ps1 +0 -264
  46. package/install.sh +0 -275
  47. package/openspec/MAESTRO_MIGRATION.md +0 -180
  48. package/openspec/changes/archive/2026-06-08-command-spec-conformance/.openspec.yaml +0 -2
  49. package/openspec/changes/archive/2026-06-08-command-spec-conformance/design.md +0 -194
  50. package/openspec/changes/archive/2026-06-08-command-spec-conformance/proposal.md +0 -52
  51. package/openspec/changes/archive/2026-06-08-command-spec-conformance/specs/multi-profile-conversations/spec.md +0 -133
  52. package/openspec/changes/archive/2026-06-08-command-spec-conformance/tasks.md +0 -95
  53. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/.openspec.yaml +0 -2
  54. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/design.md +0 -28
  55. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/proposal.md +0 -22
  56. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/specs/interactive-prompt-loop/spec.md +0 -35
  57. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/tasks.md +0 -28
  58. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/.openspec.yaml +0 -2
  59. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/design.md +0 -323
  60. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/proposal.md +0 -122
  61. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/specs/no-capability-changes/spec.md +0 -29
  62. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/tasks.md +0 -172
  63. package/openspec/changes/archive/2026-06-09-v2-install-migration/.openspec.yaml +0 -2
  64. package/openspec/changes/archive/2026-06-09-v2-install-migration/design.md +0 -241
  65. package/openspec/changes/archive/2026-06-09-v2-install-migration/proposal.md +0 -41
  66. package/openspec/changes/archive/2026-06-09-v2-install-migration/specs/v2-installer/spec.md +0 -205
  67. package/openspec/changes/archive/2026-06-09-v2-install-migration/tasks.md +0 -97
  68. package/openspec/changes/commander-cli-parser/.openspec.yaml +0 -2
  69. package/openspec/changes/commander-cli-parser/design.md +0 -107
  70. package/openspec/changes/commander-cli-parser/proposal.md +0 -31
  71. package/openspec/changes/commander-cli-parser/specs/cli/spec.md +0 -48
  72. package/openspec/changes/commander-cli-parser/tasks.md +0 -33
  73. package/openspec/changes/cross-platform-build-and-ci/.openspec.yaml +0 -2
  74. package/openspec/changes/cross-platform-build-and-ci/design.md +0 -213
  75. package/openspec/changes/cross-platform-build-and-ci/proposal.md +0 -50
  76. package/openspec/changes/cross-platform-build-and-ci/specs/path-and-file-mediation/spec.md +0 -335
  77. package/openspec/changes/cross-platform-build-and-ci/specs/release-pipeline/spec.md +0 -95
  78. package/openspec/changes/cross-platform-build-and-ci/tasks.md +0 -164
  79. package/openspec/config.yaml +0 -20
  80. package/openspec/specs/auth/spec.md +0 -394
  81. package/openspec/specs/cli/spec.md +0 -108
  82. package/openspec/specs/commands/spec.md +0 -409
  83. package/openspec/specs/configuration/spec.md +0 -130
  84. package/openspec/specs/conversations/spec.md +0 -139
  85. package/openspec/specs/domain-model/spec.md +0 -128
  86. package/openspec/specs/formatters/spec.md +0 -158
  87. package/openspec/specs/install-browser/spec.md +0 -146
  88. package/openspec/specs/interactive-prompt-loop/spec.md +0 -41
  89. package/openspec/specs/logger/spec.md +0 -71
  90. package/openspec/specs/mediator/spec.md +0 -72
  91. package/openspec/specs/multi-profile-conversations/spec.md +0 -145
  92. package/openspec/specs/path-utils/spec.md +0 -101
  93. package/openspec/specs/profiles/spec.md +0 -94
  94. package/openspec/specs/storage/spec.md +0 -198
  95. package/openspec/specs/testing/spec.md +0 -237
  96. package/openspec/specs/v2-installer/spec.md +0 -205
  97. package/openspec/specs/validators/spec.md +0 -65
  98. package/scripts/build.ts +0 -88
  99. package/scripts/clean-build.ps1 +0 -4
  100. package/scripts/clean-build.sh +0 -5
  101. package/scripts/install-browser.ps1 +0 -5
  102. package/scripts/install-browser.sh +0 -6
  103. package/scripts/lint-path-mediation.ps1 +0 -65
  104. package/scripts/lint-path-mediation.sh +0 -40
  105. package/tests/cli/auth-command.test.ts +0 -220
  106. package/tests/cli/command-registry.test.ts +0 -67
  107. package/tests/cli/continue-command.test.ts +0 -100
  108. package/tests/cli/delete-command.test.ts +0 -128
  109. package/tests/cli/export-all-command.test.ts +0 -375
  110. package/tests/cli/install-browser-command.test.ts +0 -48
  111. package/tests/cli/list-command.test.ts +0 -179
  112. package/tests/cli/new-command.test.ts +0 -139
  113. package/tests/cli/status-command.test.ts +0 -106
  114. package/tests/core/query-handlers.test.ts +0 -187
  115. package/tests/fixtures/auth-fixtures.ts +0 -98
  116. package/tests/fixtures/chat-fixtures.ts +0 -87
  117. package/tests/infrastructure/cli-table.test.ts +0 -107
  118. package/tests/infrastructure/formatters.test.ts +0 -202
  119. package/tests/infrastructure/io.test.ts +0 -197
  120. package/tests/infrastructure/logger.test.ts +0 -74
  121. package/tests/infrastructure/storage.test.ts +0 -335
  122. package/tests/infrastructure/validators.test.ts +0 -67
  123. package/tests/integration/commands/auth.test.ts +0 -186
  124. package/tests/integration/commands/continue.test.ts +0 -130
  125. package/tests/integration/commands/delete.test.ts +0 -140
  126. package/tests/integration/commands/export.test.ts +0 -266
  127. package/tests/integration/commands/fetch.test.ts +0 -238
  128. package/tests/integration/commands/list.test.ts +0 -281
  129. package/tests/integration/commands/profile.test.ts +0 -258
  130. package/tests/integration/commands/status.test.ts +0 -223
  131. package/tests/integration/installer-script-shape.test.ts +0 -67
  132. package/tests/parity/compare-outputs.ts +0 -359
  133. package/tests/parity/test-commands-parity.ps1 +0 -82
  134. package/tests/parity/test-commands-parity.sh +0 -61
  135. package/tests/services/auth-service.test.ts +0 -315
  136. package/tests/services/cookie-monitor.test.ts +0 -229
  137. package/tests/services/cookie-storage-service.test.ts +0 -230
  138. package/tests/services/gemini-client-wrapper.test.ts +0 -688
  139. package/tests/services/install-browser-service.test.ts +0 -83
  140. package/tests/services/playwright-cli-driver.test.ts +0 -468
  141. package/tests/services/profile-auth-manager.test.ts +0 -334
  142. package/tests/services/profile-service.test.ts +0 -224
  143. package/tests/setup.ts +0 -95
  144. package/tests/smoke/smoke.test.ts +0 -52
  145. package/tests/unit/config.test.ts +0 -159
  146. package/tests/unit/errors.test.ts +0 -119
  147. package/tests/unit/formatters.test.ts +0 -316
  148. package/tests/unit/mediator.test.ts +0 -152
  149. package/tests/unit/path-utils.test.ts +0 -289
  150. package/tsconfig.json +0 -16
@@ -1,172 +0,0 @@
1
- ## 1. Dependency setup
2
-
3
- - [x] 1.1 Run `npm view gemini-reverse version` (or
4
- `bun pm view gemini-reverse version` if available) and pin the latest
5
- stable in `package.json` `dependencies` (placeholder in design: `^0.5.0`).
6
- - [x] 1.2 Run `bun install` and confirm `node_modules/gemini-reverse/`
7
- resolves and `package.json` `dependencies` lists the package.
8
- - [x] 1.3 Open `node_modules/gemini-reverse/index.d.ts` and confirm the
9
- exported types match the names used in `design.md` (`GeminiClient`,
10
- `ChatInfo`, `ChatSession`, `ModelOutput`, `AvailableModel`, `ChatHistory`,
11
- `AuthError`, `APIError`, `TimeoutError`, `UsageLimitExceeded`,
12
- `TemporarilyBlocked`, `ModelInvalid`, `GeminiError`). Update
13
- `design.md` (open question Q1) and the import list in
14
- `src/services/gemini-client-wrapper.ts` if any name differs.
15
- - [x] 1.4 Run `bun run typecheck` and confirm the existing 432-test
16
- baseline project still type-checks (no behavior change yet — package
17
- is installed but not imported anywhere).
18
-
19
- ## 2. Wrapper rewrite — composition
20
-
21
- - [x] 2.1 In `src/services/gemini-client-wrapper.ts`, replace the import
22
- block: add `import { GeminiClient } from "gemini-reverse";` and the
23
- named error classes (`AuthError`, `APIError`, `TimeoutError`,
24
- `UsageLimitExceeded`, `TemporarilyBlocked`, `ModelInvalid`,
25
- `GeminiError`) per `design.md` Decision 5. Keep the existing imports
26
- for our domain types and our error classes.
27
- - [x] 2.2 Replace the `GeminiClientConfig` interface to mirror the
28
- upstream constructor's option shape:
29
- `interface GeminiClientConfig { secure1psid: string; secure1psidts?: string | null }`.
30
- Keep the field names `secure1psid` / `secure1psidts` (matching
31
- `src/cli/index.ts:98,110` and `cookie-storage-service.ts`) — the
32
- wrapper adapts them to upstream's `secure_1psid` / `secure_1psidts`
33
- at construction time.
34
- - [x] 2.3 Add a private field `private readonly client: GeminiClient;`
35
- and construct it in the constructor as
36
- `new GeminiClient({ secure_1psid: config.secure1psid, secure_1psidts:
37
- config.secure1psidts ?? null })`. The `client` is **not** initialized
38
- in the constructor — see task 2.5.
39
- - [x] 2.4 Replace the placeholder fields `authenticated` and the
40
- methods `buildHeaders`, `buildCookieHeader`, `requestApi`,
41
- `ensureAuthenticated` with the upstream-backed implementation.
42
- - [x] 2.5 Add an async public method `init(): Promise<void>` that
43
- awaits `this.client.init({ timeout: 300_000, autoClose: false,
44
- autoRefresh: true, refreshInterval: 540_000 })`. Guard with
45
- `this.initialized` so re-entry is a no-op.
46
- - [x] 2.6 Make `listChats`, `fetchChat`, `sendMessage`, `startNewChat`,
47
- `deleteChat`, `listModels` all async-call into `this.client.*` and
48
- wrap each call in a `try { … } catch (e) { throw translateError(e); }`
49
- (translateError defined in task 2.8). Ensure `init()` is awaited
50
- implicitly on first call (a per-instance `initPromise: Promise<void>`
51
- field that `init()` populates; methods `await this.initPromise` before
52
- any I/O — keeps the contract simple and avoids races).
53
- - [x] 2.7 Keep `forProfile(profileName)` returning a new
54
- `GeminiClientService` bound to the profile's cookies (per
55
- `design.md` Decision 2). The per-profile wrapper is constructed with
56
- its own `GeminiClient` instance; `init()` is called on the new
57
- instance (not the parent) the first time it is used.
58
- - [x] 2.8 Implement the private `translateError(e: unknown):
59
- GemitermError` per `design.md` Decision 5's table. Use
60
- `instanceof` against the upstream error classes. Always set
61
- `cause = e` (when `e` is an `Error`) on the returned
62
- `GemitermError` so the existing `logger.debug(\`${op} failed: ${error}\`)`
63
- call sites in command/query handlers see the underlying message via
64
- `error.cause?.message`.
65
- - [x] 2.9 Implement the private mapping helpers
66
- (`toDomainChatInfo`, `toDomainMessages`, `toDomainModelName`) per
67
- `design.md` Decision 3. Apply post-filter (`search` / `limit` /
68
- `offset`) in `listChats` exactly as the placeholder does at
69
- `gemini-client-wrapper.ts:148-160`.
70
- - [x] 2.10 Preserve the public class fields and methods consumed by
71
- callers: `logger`, `cookieStorageService`, `profileName`, and
72
- `isAuthenticated()`. `isAuthenticated()` returns `true` once
73
- `init()` has resolved and `this.client` is ready, `false`
74
- otherwise (mirrors the existing `authenticated` flag).
75
-
76
- ## 3. Call-site wiring (CLI startup)
77
-
78
- - [x] 3.1 In `src/cli/index.ts`, after the `factoryClient` is
79
- constructed (around line 110), add a single `await
80
- factoryClient.init()` before the command registry is built. The
81
- factory instance has empty cookies; this should resolve without
82
- error (upstream `init()` tolerates empty cookies — confirm via the
83
- test in task 5.4).
84
- - [x] 3.2 Confirm `src/cli/index.ts:89-104` (`getGeminiClient()`) and
85
- the per-profile `forProfile()` chain at lines 116-153 need **no
86
- other edits** — the wrapper's signature is unchanged. If
87
- `getGeminiClient()` caches the singleton, add an `await init()`
88
- on the cached value the first time it is requested.
89
- - [x] 3.3 Run `bun run typecheck` and confirm `tsc --noEmit` passes
90
- with the new wrapper.
91
- - [x] 3.4 Confirm no other file under `src/` imports
92
- `gemini-client-wrapper.ts` *or* `gemini-reverse` outside of the
93
- wrapper itself. `AGENTS.md` path-mediation lint should remain
94
- clean (the wrapper does not import `node:fs` / `node:path` /
95
- `node:os`).
96
-
97
- ## 4. Test setup
98
-
99
- - [x] 4.1 Create `tests/services/gemini-client-wrapper.test.ts` using
100
- `bun:test`. Use `mock.module("gemini-reverse", …)` to install a
101
- fake `GeminiClient` class before the wrapper is imported. Mirror
102
- the style of `tests/services/cookie-storage-service.test.ts` (no
103
- external network, deterministic fixtures).
104
- - [x] 4.2 Add fixtures for the upstream types: a `mockChatInfo` with
105
- `cid` / `title` / `is_pinned` / `timestamp`, a `mockChatHistory`
106
- with a couple of `turns` (one `user`, one `model`, plus a turn that
107
- uses `parts[].text` to exercise the fallback in
108
- `toDomainMessages`), a `mockAvailableModel` with `display_name` and
109
- one without (to exercise the `display_name || model_name ||
110
- model_id` fallback).
111
- - [x] 4.3 Cover the happy path: `listChats` filters by `search`,
112
- respects `limit` / `offset`, sorts by `timestamp` desc, attaches
113
- `profile` when `forProfile` was used; `fetchChat` flattens
114
- `parts[].text` correctly; `sendMessage` returns `output.text`;
115
- `startNewChat` returns `output.text` and the `ChatSession.cid`;
116
- `deleteChat` calls `client.deleteChat(cid)`; `listModels` returns
117
- display names with the fallback chain.
118
- - [x] 4.4 Cover the error translations: instantiate
119
- `new AuthError(…)`, `new TimeoutError(…)`,
120
- `new UsageLimitExceeded(…)`, `new TemporarilyBlocked(…)`,
121
- `new ModelInvalid(…)`, `new APIError(…)`, plus a plain
122
- `Error("boom")` and assert each one is translated to the
123
- expected `GemitermError` subclass with the expected message.
124
- - [x] 4.5 Cover `forProfile`: assert a brand-new `GeminiClient` is
125
- constructed (counter on the mock constructor) and the new
126
- instance's `listChats` is scoped to the profile's cookies
127
- (assert the constructor received the loaded cookies for the named
128
- profile).
129
- - [x] 4.6 Cover `init()` idempotency: call `init()` twice on the
130
- same wrapper and assert the underlying `GeminiClient.init` was
131
- called exactly once.
132
- - [x] 4.7 Cover the empty-cookies factory case: construct with
133
- `secure1psid: ""` and call `init()` — assert it resolves without
134
- throwing and `isAuthenticated()` returns `true` after init.
135
-
136
- ## 5. Validation
137
-
138
- - [x] 5.1 Run `bun run test` and confirm the existing 432 tests all
139
- pass (no regression) plus the new `gemini-client-wrapper.test.ts`
140
- cases (target: ~25 new cases; total ≥ 432 + 25).
141
- - [x] 5.2 Run `bun run typecheck` (`tsc --noEmit`) and confirm zero
142
- errors.
143
- - [x] 5.3 Run `bun run lint:mediation` and confirm the
144
- path-mediation lint is still clean (the wrapper does not import
145
- `node:fs` / `node:path` / `node:os`).
146
- - [x] 5.4 Manual smoke: run `bun run src/cli/index.ts list --help` (or
147
- another command that exercises the wrapper constructor) and
148
- confirm it starts without crashing — no real network call needed,
149
- this just exercises the `new GeminiClientService(…)` + `init()`
150
- path with empty cookies.
151
- - [x] 5.5 (Optional, only if a Google account is available locally)
152
- Run `bun run src/cli/index.ts auth --profile smoke` and then
153
- `bun run src/cli/index.ts list --profile smoke` against a real
154
- account, confirm the `gemini-reverse`-backed `listChats` returns
155
- real chats. Not a CI gate — the unit tests in section 4 are the
156
- CI gate.
157
-
158
- ## 6. Commit & document
159
-
160
- - [x] 6.1 Stage only the intended files: `package.json`,
161
- `bun.lockb` (or `bun.lock`), `src/services/gemini-client-wrapper.ts`,
162
- `src/cli/index.ts` (if task 3.1 added an `await init()`), and the
163
- new `tests/services/gemini-client-wrapper.test.ts`. Confirm with
164
- `git status` and `git diff --staged` before committing.
165
- - [x] 6.2 Commit with a conventional-commit message, e.g.
166
- `feat(gemini): replace placeholder HTTP client with gemini-reverse`.
167
- Reference this OpenSpec change in the body:
168
- `Replaces src/services/gemini-client-wrapper.ts placeholder fetch()
169
- client with the gemini-reverse npm library. Preserves the public
170
- IGeminiClientService / IGeminiClientQueryService contract and all
171
- 11 CLI commands. See openspec/changes/replace-gemini-api-placeholder-with-gemini-reverse/`.
172
- - [x] 6.3 Do **not** push (per `AGENTS.md`).
@@ -1,2 +0,0 @@
1
- schema: spec-driven
2
- created: 2026-06-08
@@ -1,241 +0,0 @@
1
- ## Context
2
-
3
- GemiTerm v1.4.1 shipped a PowerShell installer (`install.ps1` at the repo root) that placed the Python-built binary at `$env:LOCALAPPDATA\GemiTerm\GemiTerm.exe`, installed Chromium, and added the directory to the user `PATH`. That installer was deleted in commit `4bdefa8` ("remove all python implementation") and the new Bun-built v2.0.0 binary has **no installer at all** — no `install.ps1`, no `install.sh`, no `winget` manifest. End users upgrading from v1.4.1 have no supported migration path, and new users have no supported install path. The v2.0.0 binary produced by the sibling `cross-platform-build-and-ci` change cannot reach users without a delivery script.
4
-
5
- The v1.4.1 reference installer at `C:\dev\projects\github\webgemini-cli\install.ps1` (read-only) establishes the v1.4.1 contract: it does a `Invoke-RestMethod` against `api.github.com/repos/expert-vision-software/GemiTerm/releases/latest`, downloads `GemiTerm.exe` from the matched release asset, runs `& $exePath install-browser`, verifies Chromium at `$env:LOCALAPPDATA\ms-playwright\chromium-*\chrome.exe`, and idempotently augments user `PATH` with `$env:LOCALAPPDATA\GemiTerm` (note: **capital G** for the install dir, matching the v1.4.1 directory). Its `--uninstall` path deletes the binary and removes the `PATH` entry but leaves the user's `%APPDATA%\gemiterm\` config dir intact.
6
-
7
- Verified by reading `src/infrastructure/path-utils.ts:8-18`: the v2.0.0 binary reads its config from the same `%APPDATA%\gemiterm\` (Windows) and `~/gemiterm/` (POSIX) locations, with the same `GEMITERM_CONFIG_DIR` env-var override. The `.default` marker file and `profiles/<name>/storage_state.json` shape (`path-utils.ts:4-6`) are unchanged.
8
-
9
- **Path-contract clarification (amended 2026-06-09).** The v1.4.1 Python installer wrote its config to `~/.config/gemiterm/` on every platform (Python `src/gemiterm/config.py:14` returns `Path.home() / ".config/gemiterm"` and has no Windows branch). v2.0.0 reads from `%APPDATA%\gemiterm\` on Windows and `~/gemiterm/` on POSIX — neither of which matches the v1.4.1 path on Windows. The v1.4.1 → v2.0.0 upgrade is therefore **not** a pure in-place upgrade on Windows (and would not be on POSIX either, since the new path drops the `.config` intermediary). The installer performs a **one-time copy-forward** of the v1.4.1 tree to the v2.0.0 location; the v1.4.1 directory is left in place as a safety net. This is the migration promise the installer makes. `install-browser` is idempotent (the 5 unit tests in `tests/services/install-browser-service.test.ts` and 3 in `tests/cli/install-browser-command.test.ts` are the regression gate).
10
-
11
- Constraints:
12
- - **PowerShell Core 7+** for `install.ps1`; the v1.4.1 script was Windows PowerShell 5-only syntax. The v2 script must use PowerShell Core syntax (e.g. `Split-Path -Parent $PSCommandPath` for self-locating) because GitHub Actions Windows runners ship only `pwsh`.
13
- - **Bash 3.2+** for `install.sh` to support macOS's ancient default. No `bashisms` beyond `local`, `[[ ]]`, and `$( )` (all POSIX-safe).
14
- - **No network in CI**: the install scripts themselves are not exercised end-to-end in CI (the GitHub Actions runners are isolated and the test suite is hermetic). A syntax-shape test (`pwsh -NoProfile -Command "[Parser]::ParseFile(...)"` and `bash -n`) is the only in-CI gate.
15
- - **SENSITIVE AREA**: the install-browser step that runs `gemiterm install-browser` touches the playwright-cli/Chromium subsystem. No code in `src/services/playwright-cli-driver.ts`, `src/services/cookie-monitor.ts`, or `src/services/auth-service.ts` is touched by this change; the 5+3 existing tests are the regression gate.
16
- - **TLS-only downloads**: `Invoke-WebRequest` and `curl -fSL` enforce HTTPS; the scripts MUST NOT follow HTTP redirects to non-HTTPS.
17
- - **No code-signing**: the Windows binary is unsigned, so the installer cannot verify a signature. Mitigation: pin the release tag and match asset names exactly.
18
-
19
- Stakeholders: end users (install/upgrade/uninstall), maintainer (releases), security reviewers (TLS, no arbitrary code execution).
20
-
21
- ## Goals / Non-Goals
22
-
23
- **Goals:**
24
- - Ship `install.ps1` (PowerShell Core) and `install.sh` (POSIX bash) at the repo root that install, upgrade, and uninstall GemiTerm v2.0.0.
25
- - Mirror the v1.4.1 contract for the **directory and asset names** (`$env:LOCALAPPDATA\GemiTerm\GemiTerm.exe`, `~/.local/bin/gemiterm`) so existing `PATH` entries and shell history keep working across the upgrade.
26
- - Preserve v1.4.1 user data on upgrade: `%APPDATA%\gemiterm\` (Windows) and `~/gemiterm/` (POSIX) are never deleted by the installer. The v1.4.1 source dir (`~/.config/gemiterm/`) is copied forward on first v2.0.0 install and left in place as a safety net.
27
- - Make `PATH` augmentation **idempotent** on both platforms (no duplicate entries, case-insensitive comparison on Windows).
28
- - Support tag override for canary/R installs (`-Tag v2.0.0-rc.1` for PowerShell, `GEMITERM_TAG=v2.0.0-rc.1` for bash).
29
- - Run `gemiterm install-browser` as part of install; fail loud if Chromium is not verified after the install-browser step.
30
- - Document the offline / build-from-source fallback in `docs/INSTALL.md` because the installer is the only **online** install path on Windows (no `winget` manifest, no Chocolatey package).
31
-
32
- **Non-Goals:**
33
- - Modifying `src/services/playwright-cli-driver.ts`, `src/services/cookie-monitor.ts`, or `src/services/auth-service.ts` (sensitive area; the install-browser path is well-tested already).
34
- - Adding `winget` / `scoop` / Homebrew / Linux-package-manager manifests. The release-attach contract for the existing assets is the deliverable; package-manager registrations are a separate effort.
35
- - Code-signing the Windows binary or installer (deferred; no cert available).
36
- - Cross-compiling the installer scripts from one shell to the other (e.g. `powershell → bash` transpiler). Each script is hand-written for its platform.
37
- - Adding macOS-specific installer logic (the v1.4.1 `install.ps1` is Windows-only; `install.sh` will work on macOS for completeness but is not tested or documented as a primary install path).
38
- - Auto-elevating to admin / using sudo for `~/.local/bin` (XDG user dir is per-user and writable without elevation; `/usr/local/bin` is explicitly out of scope).
39
-
40
- ## Decisions
41
-
42
- ### D1. Two scripts (`.ps1` and `.sh`) — one per platform family, not one cross-platform script
43
-
44
- Windows shell and POSIX shell are not a single language. A "portable" script (e.g. one that branches on `$PSVersionTable` vs `$BASH_VERSION`) becomes unreadable and is hostile to anyone trying to copy-paste a one-liner. The two ecosystems have different conventions:
45
- - Windows: `irm ... | iex` (PowerShell Core `Invoke-RestMethod` + `Invoke-Expression`) is the conventional install bootstrap. The user does **not** save the script first; it streams into the shell.
46
- - POSIX: `curl -fsSL ... | bash` is the conventional install bootstrap. The script is usually self-contained (no need to save separately) but a multi-line snippet is fine.
47
-
48
- Each script is hand-written for its target shell. They share the **contract** (what flags, what files, what `PATH` behavior, what uninstall behavior) but not the code. The GitHub release assets are named `install.ps1` and `install.sh` to match what the one-liner in `docs/INSTALL.md` downloads.
49
-
50
- **Alternative considered:** One Python installer invoked by both `python3 -c "..."` and `py -3 -c "..."`. Rejected: requires Python on the user's machine (the binary's whole point is that it does not need Python) and breaks the `irm | iex` Windows convention.
51
- **Alternative considered:** Bun as the installer runtime (`bun run install.ts`). Rejected: forces the user to install Bun before installing GemiTerm — defeats the purpose of a Bun-built binary that has no runtime dep.
52
-
53
- ### D2. `~/.local/bin/gemiterm` on Linux/WSL, not `/usr/local/bin` or `~/bin`
54
-
55
- `~/.local/bin` is the XDG user-dir standard (`XDG_BIN_HOME` per the XDG Base Directory Specification). It is writable without `sudo` on every modern Linux distro, and is on `PATH` by default on Debian 12+, Ubuntu 23.04+, Fedora 38+, and Arch. On distros where it isn't, the user gets a clear remediation (`export PATH="$HOME/.local/bin:$PATH"` written to `~/gemiterm/env.sh`).
56
-
57
- - **`/usr/local/bin`**: requires `sudo`, breaks the per-user install model, and surprises users when they uninstall and the file persists.
58
- - **`~/.bin`**: non-standard; not on `PATH` by default on any major distro.
59
- - **`~/bin`**: only on `PATH` if it exists and the user's shell has the right snippet in `.profile` (Debian does, but only if the dir exists; macOS does not). Non-portable.
60
-
61
- The install path is `~/.local/bin/gemiterm` (no extension; the Bun binary has no `.exe` suffix on POSIX). This is **different** from the Windows install path of `$env:LOCALAPPDATA\GemiTerm\GemiTerm.exe` — the difference is intentional and is documented in `docs/INSTALL.md` as a platform difference, not an inconsistency.
62
-
63
- **Alternative considered:** `/opt/gemiterm/gemiterm`. Rejected: requires `sudo` and is unusual for a per-user CLI tool.
64
- **Alternative considered:** Symlink the binary into `~/.local/bin` from a versioned install dir at `~/.local/share/gemiterm/`. Rejected: complicates the upgrade flow (atomic replace of the binary, vs. a symlink that needs `ln -sf`); a single binary at `~/.local/bin/gemiterm` is the v1.4.1-equivalent shape.
65
-
66
- ### D3. Shell snippet at `~/gemiterm/env.sh` sourced from `~/.bashrc`, not a symlink in `PATH`
67
-
68
- After installing to `~/.local/bin/gemiterm`, the binary is on `PATH` for **new shell sessions** but not for the **current** shell. Two ways to make it discoverable in the current shell:
69
-
70
- 1. **Symlink** approach: `install.sh` would put the binary in `~/.local/bin/gemiterm` and the user's shell would pick it up the next time they open a terminal. No current-shell update.
71
- 2. **Shell snippet** approach: `install.sh` writes a 1-line `export PATH="$HOME/.local/bin:$PATH"` to `~/gemiterm/env.sh` and appends `[[ -f ~/gemiterm/env.sh ]] && source ~/gemiterm/env.sh` to `~/.bashrc` (and `~/.zshrc` if present). The user can `source ~/gemiterm/env.sh` in the current shell for an immediate update.
72
-
73
- The shell-snippet approach is chosen because:
74
- - **Discoverability**: `~/gemiterm/env.sh` is owned by GemiTerm; the user can `cat` it and see exactly what GemiTerm added to their shell environment. A symlink in `PATH` is invisible.
75
- - **Idempotency**: appending a `source` line to `~/.bashrc` is a one-liner that's easy to check for duplication. A symlink farm is harder to audit.
76
- - **Per-user isolation**: `~/gemiterm/` is the same root the v2.0.0 binary uses for its own data (profiles, cookies, `.default` marker). Co-locating the env snippet in the config dir means `gemiterm config-dir` (if added later) prints a path the user already understands.
77
-
78
- The snippet is **only** appended if the line is not already present (case-insensitive substring check). The uninstall flow removes the snippet and the `source` line.
79
-
80
- **Alternative considered:** Print "Add `~/.local/bin` to your PATH" as a manual instruction and don't touch `~/.bashrc`. Rejected: every Linux install script for the last 10 years has done the `~/.bashrc` append (Rustup, nvm, fnm, deno). The user's expectation is set; the snippet is the standard pattern.
81
- **Alternative considered:** Use `~/.profile` instead of `~/.bashrc`. Rejected: `~/.profile` is sourced by login shells only, not by interactive non-login shells (the default for most terminal emulators on Ubuntu/Fedora). The user would have to log out and back in, which is a worse UX than reopening a terminal.
82
-
83
- ### D4. Upgrade flow is "overwrite in place" — never "uninstall then install"
84
-
85
- The v1.4.1 → v2.0.0 upgrade must not touch the v2.0.0 config dir. The binary at `$env:LOCALAPPDATA\GemiTerm\GemiTerm.exe` (Windows) or `~/.local/bin/gemiterm` (POSIX) is replaced atomically (write to a `.new` sibling, then `Move-Item -Force` / `mv` to overwrite). The v2.0.0 config dir at `%APPDATA%\gemiterm\` (Windows) or `~/gemiterm/` (POSIX) is left untouched. **If the v1.4.1 config dir at `~/.config/gemiterm/` exists and the v2.0.0 dir does not, the installer copies the v1.4.1 tree forward to the v2.0.0 location and leaves the v1.4.1 dir in place as a safety net.** `install-browser` is idempotent: re-running it on a system that already has Chromium is a no-op (verified by reading the 5 unit tests in `tests/services/install-browser-service.test.ts`).
86
-
87
- The "Detected existing install at …; upgrading in place." message tells the user this is a v1.4.1 → v2.0.0 (or v2.x → v2.y) upgrade, **not** a fresh install, and that their data is preserved. This matches the v1.4.1 installer's behavior of always overwriting in place (the v1.4.1 script has no uninstall step on upgrade — it just downloads and overwrites).
88
-
89
- **Alternative considered:** Detect v1.4.1 vs v2.x by reading the binary's `--version` output before downloading. Rejected: requires executing the existing binary, which is brittle (the v1.4.1 binary may not respond to `--version` the same way as the v2.0.0 one). The presence of `GemiTerm.exe` at the install path is sufficient signal.
90
- **Alternative considered:** Use a side-by-side install (v1.4.1 stays at `GemiTerm-v1.exe`, v2.0.0 installs as `GemiTerm.exe`). Rejected: doubles disk usage, complicates uninstall, and the contract for v2.0.0 is "this replaces v1.4.1".
91
-
92
- ### D5. Download fallback is "build from source", not "pip install"
93
-
94
- The v1.4.1 installer's fallback when GitHub is unreachable is "run `pip install gemiterm`" — the v1.4.1 binary was a Python wheel. The v2.0.0 binary is a Bun single-file executable with no `pip` equivalent. The v2.0.0 fallback is:
95
- 1. `git clone https://github.com/expert-vision-software/GemiTerm.git`
96
- 2. `cd GemiTerm && bun install && bun run build`
97
- 3. The resulting `dist/gemiterm` (or `dist/gemiterm.exe`) is the user-built binary, moved to the install path.
98
-
99
- This is documented in `docs/INSTALL.md` under "Troubleshooting → Cannot reach GitHub releases". The script's network-failure message points the user to that doc, not to `pip`.
100
-
101
- **Alternative considered:** Bundle a fallback mirror. Rejected: no mirror exists; creating one is out of scope for this change.
102
- **Alternative considered:** Use a CDN mirror (e.g. `objects.githubusercontent.com`) as a fallback when `api.github.com` is down. Rejected: `objects.githubusercontent.com` is rate-limited and the failure modes are not well-documented; the build-from-source path is sufficient for a CLI tool that is also buildable from source.
103
-
104
- ### D6. Tag override: `-Tag` (PowerShell) and `GEMITERM_TAG` (POSIX), both default to "latest"
105
-
106
- The release resolution is a two-step process:
107
- 1. If `-Tag` / `GEMITERM_TAG` is set, fetch `https://api.github.com/repos/expert-vision-software/GemiTerm/releases/tags/$Tag` to resolve the tag object (which includes the asset list).
108
- 2. Otherwise, fetch `https://api.github.com/repos/expert-vision-software/GemiTerm/releases/latest` (which 302-redirects to the most recent published release).
109
-
110
- The tag is interpolated into the download URL: `https://github.com/expert-vision-software/GemiTerm/releases/download/$Tag/GemiTerm.exe` (or `GemiTerm` on POSIX). Defaulting to "latest" means the user does not have to know the current version number — the one-liner in the README is enough.
111
-
112
- **Alternative considered:** Use a `LATEST` symlink URL (`/releases/latest/download/GemiTerm.exe`). Rejected: GitHub does not support `latest` in the `download/` path; only the `api/repos/.../releases/latest` endpoint supports it. The two-step is the only correct approach.
113
- **Alternative considered:** Pin a specific version in the script (e.g. `v2.0.0`). Rejected: the script becomes a one-shot for that version; canary installs become a code change. The tag-override parameter is the right shape.
114
-
115
- ### D7. `PATH` augmentation: check before append, never duplicate, case-insensitive on Windows
116
-
117
- Both scripts must:
118
- - Read the current user `PATH` (PowerShell: `[Environment]::GetEnvironmentVariable('Path', 'User')`; POSIX: `echo "$PATH"`).
119
- - Check whether the install dir is **already** present. On Windows, the check is case-insensitive (`$userPath -notlike "*$exeDir*"` after lowercasing both sides, or `Split-Path -Leaf` per segment). On POSIX, the check is case-sensitive (POSIX filesystems are case-sensitive by default).
120
- - Append only if not present. The append uses the platform's path separator (`;` on Windows, `:` on POSIX).
121
- - Update both the **persistent** `PATH` (registry on Windows, `~/.bashrc` snippet on POSIX) and the **current shell's** `PATH` (`$env:Path = "$env:Path;$exeDir"` for the current PowerShell session; `export PATH` after sourcing the snippet on POSIX).
122
-
123
- Idempotency is critical: re-running the installer 10 times must produce a `PATH` with exactly one entry for `GemiTerm`. The check-before-append is the entire mechanism — there is no dedup pass.
124
-
125
- **Alternative considered:** Always append, then dedup in a second pass. Rejected: a second pass that mutates `PATH` is fragile (it can mangle other entries if the dedup logic has a bug). The check-before-append is one line and is provably correct by inspection.
126
-
127
- ### D8. Cross-change coordination: `release.yml` must attach both scripts
128
-
129
- The `cross-platform-build-and-ci` change owns `.github/workflows/release.yml` and the `release-pipeline` capability. The `release-pipeline` spec at `openspec/changes/cross-platform-build-and-ci/specs/release-pipeline/spec.md:38` already mandates attaching `install.sh` and `install.ps1` as release assets. The install scripts in this change are **inert** until the sibling change ships — without `release.yml` attaching them, there is no asset at the `releases/latest/download/install.ps1` URL that the one-liner in the README points to.
130
-
131
- This is a **blocking dependency**: the install scripts in this change should not be merged (or at minimum, should not be advertised in the README) until the sibling change is merged and a `v2.0.0` tag has been pushed to verify the assets are reachable. The `tasks.md` group "8. Cross-change coordination" makes this explicit.
132
-
133
- **Alternative considered:** Have this change own `release.yml`. Rejected: that workflow file is the sibling change's scope; this change would either duplicate it or do an end-run around the other proposal. The right move is to coordinate and add a release-attach scenario to the sibling's spec (already done at `release-pipeline/spec.md:42`).
134
- **Alternative considered:** Have this change own only the `install.*` scripts and let the sibling change pick them up via a hardcoded `files:` list. This is the chosen approach; no code change in `release.yml` is needed because the sibling change's spec already lists the files.
135
-
136
- ### D9. Idempotent `install-browser` verification: glob + `Test-Path` / `test -x`
137
-
138
- After `gemiterm install-browser` returns, the installer verifies Chromium is on disk. The v1.4.1 installer uses `Get-ChildItem "$env:LOCALAPPDATA\ms-playwright\chromium-*" -ErrorAction SilentlyContinue` — this is kept for the v2.0.0 installer because the Playwright cache directory layout (`ms-playwright/chromium-<rev>/chrome.exe`) is owned by Playwright and is stable across versions.
139
-
140
- On POSIX, the equivalent is `~/.cache/ms-playwright/chromium-*/chrome-linux/chrome` (the `chrome-linux` subdir name is part of the Playwright convention and is stable). The verification step is:
141
- - PowerShell: `Get-ChildItem "$env:LOCALAPPDATA\ms-playwright\chromium-*\chrome.exe" -Recurse | Select-Object -First 1` — exits 0 if any match, else fails with "Chromium installation verification failed".
142
- - POSIX: `find ~/.cache/ms-playwright/chromium-* -name chrome -type f -executable 2>/dev/null | head -n 1` — exits 0 if any match, else fails with the same message.
143
-
144
- The `install-browser` step itself is non-deterministic in duration (~100 MB Chromium download on a fresh install, near-instant if Chromium is already present). The installer prints progress (`Downloading GemiTerm…`, `Installing Chromium browser for Playwright…`) so the user is not staring at a silent terminal.
145
-
146
- **Alternative considered:** Skip the verification and trust `install-browser`'s exit code. Rejected: `install-browser` can exit 0 on a partial install (e.g. network blip during download) and the user discovers the failure on first `gemiterm auth`. The glob check is cheap (a single directory traversal) and gives a clear "Chromium not found" error.
147
- **Alternative considered:** Bundle Chromium with the binary. Rejected: the v1.4.1 model (download on first install via Playwright) is established and works; bundling doubles the release asset size and breaks the Playwright upgrade path.
148
-
149
- ### D10. v1.4.1 config copy-forward (Windows + POSIX)
150
-
151
- The v1.4.1 Python installer wrote its config to `~/.config/gemiterm/` on every platform. v2.0.0 reads from `%APPDATA%\gemiterm\` (Windows) or `~/gemiterm/` (POSIX). On Windows these are two different directories (`%USERPROFILE%\.config\gemiterm\` vs `%APPDATA%\gemiterm\`); on POSIX the new path drops the `.config` intermediary. A naive "overwrite in place" upgrade would leave the user with cookies in the old path and a v2.0.0 binary that does not see them.
152
-
153
- The installer therefore performs a **one-time copy-forward**: if `~/.config/gemiterm/` exists and the v2.0.0 target dir does not, copy the tree forward. The v1.4.1 source dir is left in place as a safety net. The copy is non-destructive on both sides:
154
- - The v1.4.1 dir is never read again by v2.0.0; the user can `rm -rf` it after verifying `gemiterm status` shows the expected profiles.
155
- - The v2.0.0 dir is never overwritten by the copy; if the user has already created v2.0.0 profiles manually, those are preserved.
156
-
157
- **Alternative considered:** Symbolic link from the v2.0.0 location to the v1.4.1 location. Rejected: a symlink in the user's config dir is a one-off hack that does not generalize (e.g. a v2.0.0 → v2.0.0-rc.1 test would write to the symlink target, polluting the v1.4.1 dir). A copy is the cleanest, and the v1.4.1 dir is small (< 50 KB per profile).
158
- **Alternative considered:** Print a "your v1.4.1 config is at X, please run `gemiterm migrate` to copy it" message and require the user to run a second command. Rejected: the user already has to run the installer; making them run a second command is hostile. The copy is idempotent and re-runnable; if the v2.0.0 dir already exists, the copy is a no-op.
159
-
160
- ### D11. Package-manager install prompt (added 2026-06-09)
161
-
162
- Both `install.ps1` and `install.sh` detect whether `bun` or `npm` is on `PATH`. If either is present, the installer prints:
163
-
164
- ```
165
- It is recommended to install via bun or npm package manager.
166
- Are you sure you want to continue with binary install? [y/N]
167
- ```
168
-
169
- The default is **N**. The user must type `y` or `yes` to proceed with the binary drop. If the user declines, the installer prints `Aborted. Install via: bun i -g gemiterm` (or `npm i -g gemiterm`, whichever is on PATH) and exits 0. If stdin is not a TTY (the `irm | iex` and `curl | bash` one-liners are unattended), the prompt is suppressed and the installer proceeds.
170
-
171
- The rationale: gemiterm will be published to npm as `gemiterm`; the package-manager install is the cleaner long-term path because it gives the user a normal `npm update -g gemiterm` / `bun update -g gemiterm` upgrade story. The binary-drop installer is the bootstrap path for users who don't yet have Node.js / Bun. The prompt nudges the user toward the package manager without blocking them.
172
-
173
- **Alternative considered:** Hard-fail if a package manager is on PATH. Rejected: too aggressive. The binary install is legitimate for air-gapped installs, CI environments with no Node.js, and users who explicitly want the binary. A prompt is the right balance.
174
- **Alternative considered:** Always print the package-manager hint as informational text and proceed with the binary. Rejected: passive text is easy to ignore; a `y/N` prompt forces a conscious decision.
175
- **Alternative considered:** Detect `winget` / `brew` / `apt` and recommend those instead. Rejected: the maintainer does not ship to those channels in v2.0.0 (per the design's "Non-Goals" list); bun/npm is the only first-party package manager install.
176
-
177
- ## Risks / Trade-offs
178
-
179
- - **Risk:** The installer reaches the network (`api.github.com`, `objects.githubusercontent.com`); an offline install is impossible. → **Mitigation:** document the "build from source" fallback in `docs/INSTALL.md`. The fallback uses `git clone` + `bun run build` and produces the same `GemiTerm.exe` / `GemiTerm` artifact. The installer also prints a clear "Cannot reach GitHub releases. Check your network connection or use the 'build from source' instructions in docs/INSTALL.md." message on network failure.
180
-
181
- - **Risk:** The installer's `install-browser` step is non-deterministic in duration (~100 MB Chromium download on a fresh install). → **Mitigation:** progress messages (`Downloading GemiTerm…`, `Installing Chromium browser for Playwright…`); non-zero exit on failure. The user can see the install is not hung.
182
-
183
- - **Risk:** The installer runs as the invoking user. If the user lacks write to `$env:LOCALAPPDATA` (extremely rare on Windows) or `~/.local/bin` (also rare on Linux; can happen on locked-down corporate distros), the install fails partway. → **Mitigation:** clear error message at the point of failure ("Cannot write to $env:LOCALAPPDATA\GemiTerm. Check your permissions."); document that `LOCALAPPDATA` is per-user and the install path can be overridden via a `-InstallDir` parameter (PowerShell) or `GEMITERM_INSTALL_DIR` env var (POSIX).
184
-
185
- - **Risk:** The installer scripts are large for shell scripts (~200 lines each). Hard to read; easy to introduce syntax errors. → **Mitigation:** keep them readable (one step per `function` / `function`-like block, named steps); add a header comment with usage (`# Usage: pwsh -File install.ps1 [-Tag v2.0.0] [-Uninstall] [-InstallDir <path>]`); lint with `pwsh -NoProfile -Command "[System.Management.Automation.Language.Parser]::ParseFile('install.ps1', [ref]$null, [ref]$null)"` and `bash -n install.sh` in CI.
186
-
187
- - **Risk:** The Windows binary is unsigned. A MITM on `releases/download/...` could substitute a malicious binary. → **Mitigation:** TLS only (`Invoke-WebRequest` / `curl -fSL` enforce HTTPS); pin the release tag (not a floating "latest" URL on the download step — only the API step resolves "latest"). Future: code-signing is in the deferred work list (see `cross-platform-build-and-ci/design.md:41`).
188
-
189
- - **Risk:** A v1.4.1 user with a corrupted `%APPDATA%\gemiterm\` (or `~/gemiterm/`) may want to start fresh, but the installer never deletes the config dir. → **Mitigation:** `docs/INSTALL.md` documents the manual cleanup path ("`rmdir /s /q %APPDATA%\gemiterm`" on Windows, "`rm -rf ~/gemiterm`" on POSIX). The installer itself is intentionally non-destructive to the v2.0.0 config dir. The v1.4.1 source dir at `~/.config/gemiterm/` is left in place as a safety net; the user can `rm -rf` it after verifying the migration.
190
-
191
- - **Risk:** The `~/.bashrc` append is bash-only; fish and nushell users won't pick up the `PATH` change. → **Mitigation:** the `docs/INSTALL.md` troubleshooting section calls out "fish/nushell: add `set -gx PATH ~/.local/bin $PATH` to your config.fish / `~/.config/nushell/config.nu`". Out of scope to write per-shell snippets.
192
-
193
- - **Risk:** The 5 install-browser-service tests and 3 install-browser-command tests cover the `gemiterm install-browser` subcommand's logic but not its invocation from the installer. → **Mitigation:** the installer's `install-browser` step is a thin wrapper (`& $exePath install-browser`); the existing test coverage is the regression gate. A manual smoke test in `tasks.md` group 7 verifies the full chain locally.
194
-
195
- - **Risk:** The WSL install path (`~/.local/bin/gemiterm` on the WSL distro's filesystem) means the Chromium download lives on the WSL filesystem, not the Windows host. This is a behavior difference from v1.4.1 (which downloaded to `%LOCALAPPDATA%` on the Windows host, accessible to both). → **Mitigation:** documented in `docs/INSTALL.md` as "WSL: Chromium is downloaded to the WSL distro, not Windows. The auth flow runs inside WSL." This matches the existing `findWslBrowser` detection in `src/services/install-browser-service.ts:124-141` — Chromium is WSL-local.
196
-
197
- - **Trade-off:** Two scripts = two sets of test surface = two lint rules in CI. → **Accepted:** the scripts are independent and the alternative (one cross-platform script) is harder to read and maintain. The shape test (`tests/integration/installer-script-shape.test.ts`) covers both.
198
-
199
- - **Trade-off:** The install path on Windows (`$env:LOCALAPPDATA\GemiTerm\`) is **capital G** but the config path (`%APPDATA%\gemiterm\`) is **lowercase**. → **Accepted:** this is the v1.4.1 convention and changing it would break the upgrade-in-place promise. Documented in `docs/INSTALL.md` to avoid user confusion.
200
-
201
- ## Migration Plan
202
-
203
- The change is purely additive — new files at the repo root, one new doc, one README update, one new test. No code in `src/` is touched. The migration is "users running the new install scripts", not "users migrating their data".
204
-
205
- **Deploy steps** (all done in this PR):
206
- 1. Land `install.ps1` and `install.sh` at the repo root.
207
- 2. Land `docs/INSTALL.md` covering both scripts, the upgrade flow, the uninstall flow, the data-paths overview, and the build-from-source fallback.
208
- 3. Update `README.md` to point to `docs/INSTALL.md` and add an "Upgrading from v1.4.1" callout.
209
- 4. Add `tests/integration/installer-script-shape.test.ts` that runs the PowerShell parser and `bash -n` checks.
210
- 5. **Coordinate with the `cross-platform-build-and-ci` change**: confirm that `release.yml` attaches `install.ps1` and `install.sh` to every release. The sibling change's `release-pipeline` spec at `openspec/changes/cross-platform-build-and-ci/specs/release-pipeline/spec.md:38` already mandates this. Verify by reading the spec before merging.
211
- 6. Push a `v2.0.0-rc.1` tag to verify the release pipeline attaches both scripts. Open the GitHub Release and confirm `install.ps1` and `install.sh` are downloadable from `https://github.com/expert-vision-software/GemiTerm/releases/download/v2.0.0-rc.1/`. Delete the tag and the draft release after verification.
212
- 7. Push `v2.0.0` to ship.
213
-
214
- **v1.4.1 → v2.0.0 user data migration:**
215
- - Windows: zero action required by the user. The installer overwrites `$env:LOCALAPPDATA\GemiTerm\GemiTerm.exe` in place; `%APPDATA%\gemiterm\` is untouched. The user's profiles, cookies, and `.default` marker are preserved automatically.
216
- - Linux/WSL: there is no v1.4.1 install path on Linux (the v1.4.1 Python binary was Windows-only). No user-data migration needed.
217
-
218
- **Rollback strategy:**
219
- - The install scripts are inert until a `v*` tag is pushed and the release is published. Rolling back this change after merge but before tag push: revert the commit.
220
- - Rolling back after a `v2.0.0` tag is published: the maintainer can re-tag with `v2.0.1` after fixing the script, or delete the GitHub Release. The v2.0.0 binary already on a user's machine keeps working (the installer's only job is to drop the binary; the binary itself has its own update-via-`gemiterm update` path if added later, or the user re-runs the installer).
221
- - The `install-browser` step's regression gate is the 5+3 existing tests. If a future change to those files breaks the install flow, the change to those files is the one to revert, not the installer.
222
-
223
- ## Open Questions
224
-
225
- - **Q1:** Should the PowerShell installer accept `-InstallDir <path>` to override the default `$env:LOCALAPPDATA\GemiTerm` location (e.g. for users who want a portable install on a USB drive)? The proposal's wording says no (hard-coded path), but a parameter is a small addition. **Decision:** add a `-InstallDir` parameter on PowerShell and a `GEMITERM_INSTALL_DIR` env var on POSIX, both defaulting to the platform standard. This handles the rare user who needs to override.
226
-
227
- - **Q2:** Should `install.sh` create the `~/.local/bin` directory if it doesn't exist (XDG conventions say it should exist on first use, but Debian and Ubuntu do not create it by default)? **Decision:** yes, `mkdir -p` is idempotent and the install path is meaningless without it. The install prints a message if the dir was created.
228
-
229
- - **Q3:** Should the installer verify a checksum of the downloaded binary? The v1.4.1 installer did not. **Decision:** not in this change. SHA256 verification is a follow-up that requires a `SHA256SUMS` file in the release (which the sibling `cross-platform-build-and-ci` change would also need to attach). Documented as a future improvement.
230
-
231
- - **Q4:** Should `install.sh` also `chmod +x` the binary explicitly, or trust the `mv` from the temp file to preserve the executable bit? **Decision:** explicit `chmod +x`. The temp-file write may not preserve the bit (depends on the `umask`), and the user-facing command is `gemiterm`, not `./gemiterm` — a non-executable binary is a confusing failure.
232
-
233
- - **Q5:** Should the uninstall flow also remove the `~/gemiterm/env.sh` snippet, or leave it for the user to clean up? **Decision:** remove it. The env snippet is owned by the install (not by the user), and leaving it on uninstall is a half-state that confuses re-installs. The config dir (`profiles/`, `.default`) is **not** removed.
234
-
235
- - **Q6:** Should the PowerShell uninstall also revoke the persistent `PATH` change in the Windows registry (`HKCU\Environment`), or only the current session's `PATH`? **Decision:** revoke the persistent change via `[Environment]::SetEnvironmentVariable('Path', $newPath, 'User')` — this is the registry write. The v1.4.1 uninstall did this and the v2.0.0 uninstall must match.
236
-
237
- - **Q7:** What is the minimum supported Windows version? The v1.4.1 installer required Windows PowerShell 5.1 (shipped with Windows 7+); the v2.0.0 installer uses PowerShell Core 7+ (which is on GitHub Actions runners but not pre-installed on most Windows desktops). **Decision:** document "PowerShell 7 or later" in `docs/INSTALL.md` and link to the install instructions (`winget install Microsoft.PowerShell`). The one-liner `irm ... | iex` works on PowerShell 5.1 for the `Install-Module`-style of one-liner, but the v2.0.0 script uses Core-only syntax (`Split-Path -Parent $PSCommandPath`). A user with only Windows PowerShell 5.1 will need to upgrade.
238
-
239
- - **Q8:** Should the README mention the `v2.0.0-rc.1` tag-override flag, or keep it as "advanced" in `docs/INSTALL.md`? **Decision:** keep it in `docs/INSTALL.md` only. The README's one-liner is the 95% path; canary installs are a 5% path and should not clutter the quick-start.
240
-
241
- - **Q9:** Should the install script also support installing from a local file (e.g. `pwsh -File install.ps1 -FromFile /path/to/GemiTerm.exe`) for testing or air-gapped installs? **Decision:** not in this change. The "build from source" path in `docs/INSTALL.md` covers the air-gapped case.
@@ -1,41 +0,0 @@
1
- ## Why
2
-
3
- GemiTerm v1.4.1 shipped with a PowerShell installer (`install.ps1` at the repo root) that downloaded the Python-built Windows binary from GitHub Releases into `$env:LOCALAPPDATA\GemiTerm\`, installed Chromium, and added the directory to the user `PATH`. That installer was **deleted in commit `4bdefa8`** ("remove all python implementation") when the Python codebase was removed. The new Bun-built v2.0.0 binary has **no installer at all** — there is no `install.ps1`, no `install.sh`, no `winget` manifest. End users who upgrade from v1.4.1 have no supported migration path, and new users have no supported install path. This change recreates the installer for v2.0.0, with explicit support for preserving the existing v1.4.1 user data (profiles, cookies, default profile marker) on upgrade.
4
-
5
- ## What Changes
6
-
7
- - **Add `install.ps1` at the repo root** — PowerShell installer for Windows. Mirrors the v1.4.1 contract so existing user `PATH` entries and `%APPDATA%\gemiterm\` config keep working. Stages:
8
- 1. If `--uninstall` is passed, run uninstall flow (delete binary, remove from `PATH`, leave config dir alone so a re-install keeps the user's data) and exit 0.
9
- 2. If `GemiTerm.exe` already exists at `$env:LOCALAPPDATA\GemiTerm\GemiTerm.exe`, treat this as an **upgrade** from v1.4.1 → v2.0.0 (or a v2 → v2.0.x upgrade). Print "Detected existing install at …; upgrading in place." Do NOT delete the existing config dir.
10
- 3. Resolve the GitHub release tag: by default use the latest release (`https://api.github.com/repos/expert-vision-software/GemiTerm/releases/latest`); honor an optional `-Tag v2.0.0` argument for canary/R installs.
11
- 4. Download `GemiTerm.exe` (Windows asset) and `install.ps1` (this script) to `$env:LOCALAPPDATA\GemiTerm\`.
12
- 5. Run `& $exePath install-browser` to install Chromium via playwright-cli.
13
- 6. Verify Chromium is present under `$env:LOCALAPPDATA\ms-playwright\chromium-*\chrome.exe`. Fail loud if not.
14
- 7. Add `$env:LOCALAPPDATA\GemiTerm` to user `PATH` **idempotently** (only append if not already present).
15
- 8. Print final success: `"GemiTerm v2.0.0 installed. Run 'gemiterm status' to verify, then 'gemiterm auth' to authenticate."`.
16
- - **Add `install.sh` at the repo root** — POSIX installer for Linux and WSL. Mirror of the Windows flow for `~/.local/bin/gemiterm` (XDG-style) and `~/gemiterm/` (config) and `~/.cache/ms-playwright/` (Chromium). Supports `--uninstall`, upgrade-in-place, tag override via `GEMITERM_TAG` env var, idempotent `PATH` augmentation (writes a `~/gemiterm/env.sh` snippet to be sourced from `~/.bashrc` / `~/.zshrc`).
17
- - **Add `docs/INSTALL.md`** — End-user install guide covering both scripts, the upgrade flow, the uninstall flow, troubleshooting (e.g. "PATH did not refresh in current shell" → `refreshenv` / new shell), and the data-paths overview (binary vs config vs Chromium).
18
- - **Update `README.md`** — Replace any v1.4.1 install instructions with a 1-line "see `docs/INSTALL.md`" pointer. Add a "Upgrading from v1.4.1" callout.
19
- - **CI integration** — The release-pipeline workflow (`release.yml`, owned by the `cross-platform-build-and-ci` change) must attach `install.ps1` and `install.sh` to every GitHub release. Document this contract here as a cross-change dependency; the actual workflow file is owned by the other change.
20
-
21
- **v1.4.1 → v2.0.0 data preservation:** The v1.4.1 Python installer wrote its config to `~/.config/gemiterm/` on every platform (verified by reading `C:\dev\projects\github\webgemini-cli\src\gemiterm\config.py:14`, which has no Windows branch and always returns `Path.home() / ".config/gemiterm"`). The v2.0.0 Bun installer reads from `%APPDATA%\gemiterm\` on Windows and `~/gemiterm/` on POSIX — neither of which matches the v1.4.1 path. The v1.4.1 → v2.0.0 upgrade therefore performs a **one-time copy-forward**: if the v1.4.1 dir exists and the v2.0.0 dir does not, the installer copies the v1.4.1 tree forward and leaves the v1.4.1 dir in place as a safety net. Profile directories, `storage_state.json` cookies, and the `.default` marker are preserved across the upgrade with no transformation of the cookie data itself; the v2.0.0 `cookie-storage-service` drops the `origins` array on first re-save (no current spec requires `origins`).
22
-
23
- **No breaking changes** to the CLI user surface. The installer is an addition; the CLI binary behaves identically.
24
-
25
- ## Capabilities
26
-
27
- ### New Capabilities
28
- - `v2-installer`: The `install.ps1` and `install.sh` scripts that download, install, upgrade, and uninstall GemiTerm v2.0.0 across Windows, Linux, and WSL, with explicit v1.4.1 → v2.0.0 in-place upgrade support. Creates `openspec/changes/v2-install-migration/specs/v2-installer/spec.md`.
29
-
30
- ### Modified Capabilities
31
- - (none) — no existing capability has a spec-level requirement change. The CLI behavior, config paths, and storage format are unchanged. The installer is a brand-new delivery surface.
32
-
33
- ## Impact
34
-
35
- - **New files:** `install.ps1` (PowerShell), `install.sh` (POSIX shell), `docs/INSTALL.md`.
36
- - **Modified files:** `README.md` (replace install section with pointer to `docs/INSTALL.md`; add upgrade callout).
37
- - **No code changes** to `src/`. The installer scripts are external to the binary.
38
- - **Cross-change dependency:** the `cross-platform-build-and-ci` change owns `.github/workflows/release.yml` and must attach both `install.ps1` and `install.sh` to each release. Coordinate via the `release-pipeline` capability's release-attach scenario. (If the other change has not landed first, the installer scripts in this repo are inert — they're only invoked by the install workflow.)
39
- - **Tests:** New smoke test `tests/integration/installer-script-shape.test.ts` (or just a unit test) that validates the install scripts parse (PowerShell syntax check via `pwsh -NoProfile -Command "$null = [System.Management.Automation.PSParser]::Tokenize((Get-Content -Raw install.ps1), [ref]$null)"` and Bash syntax check via `bash -n install.sh`). The scripts themselves are not exercised end-to-end in CI (no network, no real install).
40
- - **Security:** the installer must verify the downloaded asset matches a known name (`GemiTerm.exe`, `install.ps1`) and use TLS via `Invoke-WebRequest` / `curl -fSL`. It must NOT execute arbitrary downloaded content beyond the named binary.
41
- - **SENSITIVE AREA:** the installer scripts run the new `gemiterm install-browser` command during install, which touches the playwright-cli/Chromium subsystem. The user's troubleshooting history with cookie capture / subprocess spawning is NOT relevant here (the install-browser path is well-tested by `tests/services/install-browser-service.test.ts` and `tests/cli/install-browser-command.test.ts`). No code in `src/services/playwright-cli-driver.ts`, `cookie-monitor.ts`, or `auth-service.ts` is touched.