gemiterm 2.0.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.
- package/.env +2 -0
- package/.github/workflows/manual-release.yml +34 -0
- package/.github/workflows/release.yml +198 -0
- package/.github/workflows/test.yml +29 -0
- package/.opencode/commands/opsx-apply.md +152 -0
- package/.opencode/commands/opsx-archive.md +157 -0
- package/.opencode/commands/opsx-explore.md +169 -0
- package/.opencode/commands/opsx-propose.md +104 -0
- package/.opencode/commands/opsx-sync.md +140 -0
- package/.opencode/commands/search-intelligently.md +9 -0
- package/.opencode/opencode.json +20 -0
- package/.opencode/skills/intellisearch/.version +1 -0
- package/.opencode/skills/intellisearch/SKILL.md +84 -0
- package/.opencode/skills/intellisearch/references/brave-search.md +67 -0
- package/.opencode/skills/intellisearch/references/ddg-search.md +79 -0
- package/.opencode/skills/intellisearch/references/deepwiki-tools.md +246 -0
- package/.opencode/skills/intellisearch/references/examples.md +97 -0
- package/.opencode/skills/intellisearch/references/gh-cli.md +143 -0
- package/.opencode/skills/intellisearch/references/google-search.md +40 -0
- package/.opencode/skills/intellisearch/references/search-workflow.md +143 -0
- package/.opencode/skills/intellisearch/references/workflow.md +115 -0
- package/.opencode/skills/openspec-apply-change/SKILL.md +159 -0
- package/.opencode/skills/openspec-archive-change/SKILL.md +117 -0
- package/.opencode/skills/openspec-explore/SKILL.md +287 -0
- package/.opencode/skills/openspec-propose/SKILL.md +111 -0
- package/.opencode/skills/openspec-sync-specs/SKILL.md +147 -0
- package/.serena/project.yml +133 -0
- package/AGENTS.md +136 -0
- package/CHANGELOG.md +3 -0
- package/README.md +245 -0
- package/bun.lock +105 -0
- package/docs/INSTALL.md +185 -0
- package/docs/PLAYWRIGHT_CLI_API.md +306 -0
- package/docs/python-release-for-reference.md +175 -0
- package/examples/sample_json_export.json +32 -0
- package/examples/sample_markdown_export.md +221 -0
- package/examples/usage_examples.sh +222 -0
- package/install.ps1 +264 -0
- package/install.sh +275 -0
- package/openspec/MAESTRO_MIGRATION.md +180 -0
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/design.md +194 -0
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/proposal.md +52 -0
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/specs/multi-profile-conversations/spec.md +133 -0
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/tasks.md +95 -0
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/design.md +28 -0
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/proposal.md +22 -0
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/specs/interactive-prompt-loop/spec.md +35 -0
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/tasks.md +28 -0
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/design.md +323 -0
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/proposal.md +122 -0
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/specs/no-capability-changes/spec.md +29 -0
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/tasks.md +172 -0
- package/openspec/changes/archive/2026-06-09-v2-install-migration/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-06-09-v2-install-migration/design.md +241 -0
- package/openspec/changes/archive/2026-06-09-v2-install-migration/proposal.md +41 -0
- package/openspec/changes/archive/2026-06-09-v2-install-migration/specs/v2-installer/spec.md +205 -0
- package/openspec/changes/archive/2026-06-09-v2-install-migration/tasks.md +97 -0
- package/openspec/changes/commander-cli-parser/.openspec.yaml +2 -0
- package/openspec/changes/commander-cli-parser/design.md +107 -0
- package/openspec/changes/commander-cli-parser/proposal.md +31 -0
- package/openspec/changes/commander-cli-parser/specs/cli/spec.md +48 -0
- package/openspec/changes/commander-cli-parser/tasks.md +33 -0
- package/openspec/changes/cross-platform-build-and-ci/.openspec.yaml +2 -0
- package/openspec/changes/cross-platform-build-and-ci/design.md +213 -0
- package/openspec/changes/cross-platform-build-and-ci/proposal.md +50 -0
- package/openspec/changes/cross-platform-build-and-ci/specs/path-and-file-mediation/spec.md +335 -0
- package/openspec/changes/cross-platform-build-and-ci/specs/release-pipeline/spec.md +95 -0
- package/openspec/changes/cross-platform-build-and-ci/tasks.md +164 -0
- package/openspec/config.yaml +20 -0
- package/openspec/specs/auth/spec.md +394 -0
- package/openspec/specs/cli/spec.md +108 -0
- package/openspec/specs/commands/spec.md +409 -0
- package/openspec/specs/configuration/spec.md +130 -0
- package/openspec/specs/conversations/spec.md +139 -0
- package/openspec/specs/domain-model/spec.md +128 -0
- package/openspec/specs/formatters/spec.md +158 -0
- package/openspec/specs/install-browser/spec.md +146 -0
- package/openspec/specs/interactive-prompt-loop/spec.md +41 -0
- package/openspec/specs/logger/spec.md +71 -0
- package/openspec/specs/mediator/spec.md +72 -0
- package/openspec/specs/multi-profile-conversations/spec.md +145 -0
- package/openspec/specs/path-utils/spec.md +101 -0
- package/openspec/specs/profiles/spec.md +94 -0
- package/openspec/specs/storage/spec.md +198 -0
- package/openspec/specs/testing/spec.md +237 -0
- package/openspec/specs/v2-installer/spec.md +205 -0
- package/openspec/specs/validators/spec.md +65 -0
- package/package.json +45 -0
- package/scripts/build.ts +88 -0
- package/scripts/clean-build.ps1 +4 -0
- package/scripts/clean-build.sh +5 -0
- package/scripts/install-browser.ps1 +5 -0
- package/scripts/install-browser.sh +6 -0
- package/scripts/lint-path-mediation.ps1 +65 -0
- package/scripts/lint-path-mediation.sh +40 -0
- package/src/cli/.gitkeep +0 -0
- package/src/cli/command-registry.ts +62 -0
- package/src/cli/commands/auth-command.ts +179 -0
- package/src/cli/commands/continue-command.ts +158 -0
- package/src/cli/commands/delete-command.ts +163 -0
- package/src/cli/commands/export-all-command.ts +243 -0
- package/src/cli/commands/export-command.ts +156 -0
- package/src/cli/commands/fetch-command.ts +176 -0
- package/src/cli/commands/help.ts +45 -0
- package/src/cli/commands/install-browser-command.ts +33 -0
- package/src/cli/commands/list-command.ts +226 -0
- package/src/cli/commands/new-command.ts +147 -0
- package/src/cli/commands/profile-command.ts +226 -0
- package/src/cli/commands/status-command.ts +59 -0
- package/src/cli/index.ts +213 -0
- package/src/cli/utils/interactive-prompt.ts +64 -0
- package/src/core/.gitkeep +0 -0
- package/src/core/command-handlers.ts +240 -0
- package/src/core/errors.ts +41 -0
- package/src/core/mediator.ts +58 -0
- package/src/core/query-handlers.ts +163 -0
- package/src/core/types.ts +43 -0
- package/src/infrastructure/.gitkeep +0 -0
- package/src/infrastructure/cli-table.ts +45 -0
- package/src/infrastructure/config.ts +52 -0
- package/src/infrastructure/formatters.ts +130 -0
- package/src/infrastructure/io.ts +161 -0
- package/src/infrastructure/logger.ts +41 -0
- package/src/infrastructure/path-utils.ts +157 -0
- package/src/infrastructure/storage.ts +209 -0
- package/src/infrastructure/validators.ts +28 -0
- package/src/services/.gitkeep +0 -0
- package/src/services/auth-service.ts +135 -0
- package/src/services/cookie-monitor.ts +159 -0
- package/src/services/cookie-storage-service.ts +74 -0
- package/src/services/gemini-client-wrapper.ts +239 -0
- package/src/services/install-browser-service.ts +205 -0
- package/src/services/playwright-cli-driver.ts +269 -0
- package/src/services/profile-auth-manager.ts +65 -0
- package/src/services/profile-service.ts +121 -0
- package/tests/cli/auth-command.test.ts +220 -0
- package/tests/cli/command-registry.test.ts +67 -0
- package/tests/cli/continue-command.test.ts +100 -0
- package/tests/cli/delete-command.test.ts +128 -0
- package/tests/cli/export-all-command.test.ts +375 -0
- package/tests/cli/install-browser-command.test.ts +48 -0
- package/tests/cli/list-command.test.ts +179 -0
- package/tests/cli/new-command.test.ts +139 -0
- package/tests/cli/status-command.test.ts +106 -0
- package/tests/core/query-handlers.test.ts +187 -0
- package/tests/fixtures/auth-fixtures.ts +98 -0
- package/tests/fixtures/chat-fixtures.ts +87 -0
- package/tests/infrastructure/cli-table.test.ts +107 -0
- package/tests/infrastructure/formatters.test.ts +202 -0
- package/tests/infrastructure/io.test.ts +197 -0
- package/tests/infrastructure/logger.test.ts +74 -0
- package/tests/infrastructure/storage.test.ts +335 -0
- package/tests/infrastructure/validators.test.ts +67 -0
- package/tests/integration/commands/auth.test.ts +186 -0
- package/tests/integration/commands/continue.test.ts +130 -0
- package/tests/integration/commands/delete.test.ts +140 -0
- package/tests/integration/commands/export.test.ts +266 -0
- package/tests/integration/commands/fetch.test.ts +238 -0
- package/tests/integration/commands/list.test.ts +281 -0
- package/tests/integration/commands/profile.test.ts +258 -0
- package/tests/integration/commands/status.test.ts +223 -0
- package/tests/integration/installer-script-shape.test.ts +67 -0
- package/tests/parity/compare-outputs.ts +359 -0
- package/tests/parity/test-commands-parity.ps1 +82 -0
- package/tests/parity/test-commands-parity.sh +61 -0
- package/tests/services/auth-service.test.ts +315 -0
- package/tests/services/cookie-monitor.test.ts +229 -0
- package/tests/services/cookie-storage-service.test.ts +230 -0
- package/tests/services/gemini-client-wrapper.test.ts +688 -0
- package/tests/services/install-browser-service.test.ts +83 -0
- package/tests/services/playwright-cli-driver.test.ts +468 -0
- package/tests/services/profile-auth-manager.test.ts +334 -0
- package/tests/services/profile-service.test.ts +224 -0
- package/tests/setup.ts +95 -0
- package/tests/smoke/smoke.test.ts +52 -0
- package/tests/unit/config.test.ts +159 -0
- package/tests/unit/errors.test.ts +119 -0
- package/tests/unit/formatters.test.ts +316 -0
- package/tests/unit/mediator.test.ts +152 -0
- package/tests/unit/path-utils.test.ts +289 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
## ADDED Requirements
|
|
2
|
+
|
|
3
|
+
### Requirement: PowerShell Installer Ships With Every v2.0.0+ Release
|
|
4
|
+
The system MUST attach a `install.ps1` script to every GitHub release tagged `v*` that targets the v2.0.0+ series. The `install.ps1` script MUST be a PowerShell Core 7+ script that, when invoked as `irm https://github.com/expert-vision-software/GemiTerm/releases/latest/download/install.ps1 | iex`, downloads the latest `GemiTerm.exe` release asset, installs it to `$env:LOCALAPPDATA\GemiTerm\GemiTerm.exe`, runs `gemiterm install-browser`, verifies Chromium is present, and augments the user `PATH` idempotently.
|
|
5
|
+
|
|
6
|
+
#### Scenario: One-liner install on a fresh Windows machine
|
|
7
|
+
- **WHEN** a user with no prior GemiTerm install runs `irm https://github.com/expert-vision-software/GemiTerm/releases/latest/download/install.ps1 | iex` in PowerShell Core 7+
|
|
8
|
+
- **THEN** the latest `GemiTerm.exe` is downloaded to `$env:LOCALAPPDATA\GemiTerm\GemiTerm.exe`, `gemiterm install-browser` is invoked to install Chromium, the user `PATH` is augmented to include `$env:LOCALAPPDATA\GemiTerm`, and the script prints a success message
|
|
9
|
+
|
|
10
|
+
#### Scenario: Fresh install places binary at the v1.4.1-equivalent path
|
|
11
|
+
- **WHEN** `install.ps1` runs on a system with no prior install
|
|
12
|
+
- **THEN** the binary is placed at `$env:LOCALAPPDATA\GemiTerm\GemiTerm.exe` (note capital `G` in `GemiTerm`, matching the v1.4.1 install path) and a fresh `%APPDATA%\gemiterm\` config directory is created only if the user later runs `gemiterm` for the first time
|
|
13
|
+
|
|
14
|
+
#### Scenario: `gemiterm --version` reports the installed version
|
|
15
|
+
- **WHEN** a user runs `gemiterm --version` after a successful install
|
|
16
|
+
- **THEN** the command exits 0 and prints a version string of the form `gemiterm v2.x.y`
|
|
17
|
+
|
|
18
|
+
### Requirement: POSIX Installer Ships With Every v2.0.0+ Release
|
|
19
|
+
The system MUST attach a `install.sh` script to every GitHub release tagged `v*` that targets the v2.0.0+ series. The `install.sh` script MUST be a POSIX bash 3.2+ script that, when invoked as `curl -fsSL https://github.com/expert-vision-software/GemiTerm/releases/latest/download/install.sh | bash`, downloads the latest `GemiTerm` release asset, installs it to `~/.local/bin/gemiterm`, runs `gemiterm install-browser`, verifies Chromium is present, and writes a `source`-able snippet to `~/gemiterm/env.sh` that is appended to `~/.bashrc` (and `~/.zshrc` if it exists) for `PATH` discovery.
|
|
20
|
+
|
|
21
|
+
#### Scenario: One-liner install on a fresh Linux machine
|
|
22
|
+
- **WHEN** a user with no prior GemiTerm install runs `curl -fsSL https://github.com/expert-vision-software/GemiTerm/releases/latest/download/install.sh | bash` in bash
|
|
23
|
+
- **THEN** the latest `GemiTerm` binary is downloaded to `~/.local/bin/gemiterm` (creating the directory if it does not exist), `chmod +x` is applied, `gemiterm install-browser` is invoked, and a `source` line for `~/gemiterm/env.sh` is appended to `~/.bashrc` if not already present
|
|
24
|
+
|
|
25
|
+
#### Scenario: `gemiterm --version` reports the installed version on POSIX
|
|
26
|
+
- **WHEN** a user runs `gemiterm --version` after a successful POSIX install (in a new shell, or after `source ~/gemiterm/env.sh`)
|
|
27
|
+
- **THEN** the command exits 0 and prints a version string of the form `gemiterm v2.x.y`
|
|
28
|
+
|
|
29
|
+
#### Scenario: POSIX install creates `~/.local/bin` if missing
|
|
30
|
+
- **WHEN** `install.sh` runs on a system where `~/.local/bin` does not exist
|
|
31
|
+
- **THEN** the directory is created with `mkdir -p` and the binary is placed inside; a message is printed if the directory was created
|
|
32
|
+
|
|
33
|
+
### Requirement: Upgrade From v1.4.1 Preserves User Data
|
|
34
|
+
The system MUST upgrade an existing v1.4.1 installation in place without modifying the user's profile data. The installer MUST detect an existing install by the presence of `GemiTerm.exe` at `$env:LOCALAPPDATA\GemiTerm\GemiTerm.exe` (Windows) or `gemiterm` at `~/.local/bin/gemiterm` (POSIX), MUST replace the binary atomically, and MUST NOT delete, move, or rewrite anything under `%APPDATA%\gemiterm\` (Windows) or `~/gemiterm/` (POSIX).
|
|
35
|
+
|
|
36
|
+
#### Scenario: Binary replaced in place, config dir untouched on Windows upgrade
|
|
37
|
+
- **WHEN** a user with v1.4.1 already installed runs the v2.0.0 `install.ps1`
|
|
38
|
+
- **THEN** the binary at `$env:LOCALAPPDATA\GemiTerm\GemiTerm.exe` is overwritten, the installer prints `"Detected existing install at …; upgrading in place."`, and every file under `%APPDATA%\gemiterm\profiles\<name>\storage_state.json` is byte-identical to its pre-upgrade content (verified by hash)
|
|
39
|
+
|
|
40
|
+
#### Scenario: `.default` profile marker preserved on upgrade
|
|
41
|
+
- **WHEN** a user with v1.4.1 has a `.default` marker file under `%APPDATA%\gemiterm\profiles\` pointing to profile `work` and runs the v2.0.0 installer
|
|
42
|
+
- **THEN** after the upgrade, `%APPDATA%\gemiterm\profiles\.default` still exists, still contains `work` (or the prior content), and `gemiterm status` still lists `work` as the default profile
|
|
43
|
+
|
|
44
|
+
#### Scenario: `gemiterm status` shows the same profile list after upgrade
|
|
45
|
+
- **WHEN** a user runs `gemiterm status` before and after the v1.4.1 → v2.0.0 upgrade
|
|
46
|
+
- **THEN** both invocations print the same set of profile names and the same authentication-state column for each profile
|
|
47
|
+
|
|
48
|
+
#### Scenario: PowerShell installer detects Python v1.4.1 and prompts uninstall without touching config
|
|
49
|
+
- **WHEN** `install.ps1` runs and `pip show gemiterm` (or `gemiterm --version`) indicates a Python v1.4.1 install is present
|
|
50
|
+
- **THEN** the installer prints `"WARNING: Python v1.4.1 detected. Your config data at %APPDATA%\gemiterm\ is safe and will be preserved. Please run 'pip uninstall gemiterm' before re-running this installer to complete the v2.0.0 installation."` and exits non-zero; the existing binary is NOT replaced and no config data is modified
|
|
51
|
+
|
|
52
|
+
#### Scenario: POSIX installer detects Python v1.4.1 and prompts uninstall without touching config
|
|
53
|
+
- **WHEN** `install.sh` runs and `pip show gemiterm` (or `gemiterm --version`) indicates a Python v1.4.1 install is present
|
|
54
|
+
- **THEN** the installer prints `"WARNING: Python v1.4.1 detected. Your config data at ~/gemiterm/ is safe and will be preserved. Please run 'pip uninstall gemiterm' before re-running this installer to complete the v2.0.0 installation."` and exits non-zero; the existing binary is NOT replaced and no config data is modified
|
|
55
|
+
|
|
56
|
+
### Requirement: `--uninstall` Removes Binary And PATH Entry, Preserves Config Dir
|
|
57
|
+
The system MUST support a `--uninstall` flag on both `install.ps1` and `install.sh`. The uninstall flow MUST delete the binary, remove the install dir from the persistent user `PATH` (or remove the `~/gemiterm/env.sh` snippet and the `source` line from `~/.bashrc` / `~/.zshrc` on POSIX), and MUST NOT delete or modify the user's config dir at `%APPDATA%\gemiterm\` (Windows) or `~/gemiterm/` (POSIX). After uninstall, a re-install MUST restore the binary and find the same profiles as before the uninstall.
|
|
58
|
+
|
|
59
|
+
#### Scenario: PowerShell uninstall removes binary and PATH entry, leaves config dir intact
|
|
60
|
+
- **WHEN** a user runs `pwsh -File install.ps1 --uninstall`
|
|
61
|
+
- **THEN** `$env:LOCALAPPDATA\GemiTerm\GemiTerm.exe` is deleted, `$env:LOCALAPPDATA\GemiTerm` is removed from the user `PATH` (persistent registry value), and `%APPDATA%\gemiterm\` is **NOT** deleted; the script prints `"GemiTerm uninstalled successfully."` and exits 0
|
|
62
|
+
|
|
63
|
+
#### Scenario: POSIX uninstall removes binary, env snippet, and bashrc source line, leaves config dir intact
|
|
64
|
+
- **WHEN** a user runs `bash install.sh --uninstall`
|
|
65
|
+
- **THEN** `~/.local/bin/gemiterm` is deleted, `~/gemiterm/env.sh` is deleted, the `source ~/gemiterm/env.sh` line is removed from `~/.bashrc` (and `~/.zshrc` if it exists), and `~/gemiterm/profiles/` is **NOT** deleted
|
|
66
|
+
|
|
67
|
+
#### Scenario: Re-install after uninstall restores the binary and profiles
|
|
68
|
+
- **WHEN** a user runs `install.ps1` (or `install.sh`) after a prior `--uninstall`
|
|
69
|
+
- **THEN** the binary is re-installed at the same path and `gemiterm status` lists the same profiles that existed before the uninstall (proving the config dir was preserved across the uninstall/reinstall cycle)
|
|
70
|
+
|
|
71
|
+
### Requirement: Tag Override For Canary And Release-Candidate Installs
|
|
72
|
+
The system MUST accept a tag override on both installers. On PowerShell, the `-Tag <version>` parameter overrides the default "latest" release resolution. On POSIX, the `GEMITERM_TAG=<version>` environment variable overrides the default. When a tag is provided, the installer MUST fetch the named release (e.g. `v2.0.0-rc.1`) via `https://api.github.com/repos/expert-vision-software/GemiTerm/releases/tags/$Tag` and download the asset from `https://github.com/expert-vision-software/GemiTerm/releases/download/$Tag/`.
|
|
73
|
+
|
|
74
|
+
#### Scenario: PowerShell `-Tag` parameter downloads a specific release
|
|
75
|
+
- **WHEN** a user runs `pwsh -File install.ps1 -Tag v2.0.0-rc.1`
|
|
76
|
+
- **THEN** the installer fetches the `v2.0.0-rc.1` release from the GitHub API, downloads `GemiTerm.exe` from the `v2.0.0-rc.1` release assets, and installs it; `gemiterm --version` after install prints `gemiterm v2.0.0-rc.1` (or the equivalent)
|
|
77
|
+
|
|
78
|
+
#### Scenario: POSIX `GEMITERM_TAG` env var downloads a specific release
|
|
79
|
+
- **WHEN** a user runs `GEMITERM_TAG=v2.0.0-rc.1 bash install.sh`
|
|
80
|
+
- **THEN** the installer fetches the `v2.0.0-rc.1` release, downloads `GemiTerm` from the `v2.0.0-rc.1` release assets, installs it, and `gemiterm --version` prints the rc.1 version
|
|
81
|
+
|
|
82
|
+
#### Scenario: Default tag is "latest"
|
|
83
|
+
- **WHEN** a user runs the install one-liner with no `-Tag` and no `GEMITERM_TAG` env var
|
|
84
|
+
- **THEN** the installer fetches `https://api.github.com/repos/expert-vision-software/GemiTerm/releases/latest` and downloads the asset from the resolved tag
|
|
85
|
+
|
|
86
|
+
### Requirement: PATH Augmentation Is Idempotent
|
|
87
|
+
The system MUST add the install dir to the user `PATH` on a fresh install and MUST NOT add a duplicate entry on a re-install. On Windows, the comparison MUST be case-insensitive (Windows file paths are case-insensitive). On POSIX, the comparison MUST be case-sensitive (POSIX filesystems are case-sensitive by default). The current shell's `PATH` MUST also be updated so the user does not have to open a new terminal.
|
|
88
|
+
|
|
89
|
+
#### Scenario: Fresh install adds install dir to user PATH
|
|
90
|
+
- **WHEN** `install.ps1` runs and `$env:LOCALAPPDATA\GemiTerm` is not in the user `PATH`
|
|
91
|
+
- **THEN** the install dir is appended to the persistent user `PATH` (via `[Environment]::SetEnvironmentVariable('Path', ..., 'User')`) and to the current session's `$env:Path`
|
|
92
|
+
|
|
93
|
+
#### Scenario: Re-install does not duplicate the PATH entry
|
|
94
|
+
- **WHEN** `install.ps1` runs a second time and `$env:LOCALAPPDATA\GemiTerm` is already in the user `PATH` (case-insensitive match)
|
|
95
|
+
- **THEN** the user `PATH` is unchanged (no duplicate entry) and the script prints no "PATH updated" message
|
|
96
|
+
|
|
97
|
+
#### Scenario: POSIX install writes env.sh and appends source line to ~/.bashrc idempotently
|
|
98
|
+
- **WHEN** `install.sh` runs and `~/gemiterm/env.sh` is written and the `source` line is appended to `~/.bashrc` only if not already present (case-insensitive substring check)
|
|
99
|
+
- **THEN** a second `install.sh` run does not append a second `source` line and does not duplicate the `export PATH=...` line inside `env.sh`
|
|
100
|
+
|
|
101
|
+
### Requirement: Browser Install Step Runs As Part Of Install And Is Verified
|
|
102
|
+
The system MUST invoke `bunx @playwright/cli install chromium` directly (not via the `gemiterm install-browser` command) as part of the install flow, MUST verify that Chromium is on disk after the invocation, and MUST exit non-zero with a clear message if the verification fails. The verification check is a glob over `$env:LOCALAPPDATA\ms-playwright\chromium-*\chrome.exe` (Windows) or `~/.cache/ms-playwright/chromium-*/chrome-linux/chrome` (POSIX).
|
|
103
|
+
|
|
104
|
+
#### Scenario: Fresh install downloads Chromium via playwright-cli
|
|
105
|
+
- **WHEN** `install.ps1` (or `install.sh`) runs on a system without Chromium and `bun` is in PATH
|
|
106
|
+
- **THEN** `bunx @playwright/cli install chromium` is invoked, which downloads Chromium via Playwright, and the installer verifies the Chromium binary is present under the appropriate path before exiting 0
|
|
107
|
+
|
|
108
|
+
#### Scenario: Bun not present on system — installer bootstraps Bun first
|
|
109
|
+
- **WHEN** `install.ps1` (or `install.sh`) runs and `bun` is not in PATH
|
|
110
|
+
- **THEN** the installer downloads and installs Bun via the official installer (`irm https://bun.sh/install.ps1 | iex` on Windows, `curl -fsSL https://bun.sh/install | bash` on POSIX) before running `bunx @playwright/cli install chromium`
|
|
111
|
+
|
|
112
|
+
#### Scenario: Bun bootstrap failure fails the installer
|
|
113
|
+
- **WHEN** the Bun bootstrap step fails (network error)
|
|
114
|
+
- **THEN** the installer prints `"Bun installation failed. Install Bun manually from https://bun.sh and re-run this installer."` and exits non-zero; the existing binary (if upgrading) is left in place
|
|
115
|
+
|
|
116
|
+
#### Scenario: playwright-cli install failure fails the installer
|
|
117
|
+
- **WHEN** `bunx @playwright/cli install chromium` exits non-zero (e.g. network failure during the Chromium download)
|
|
118
|
+
- **THEN** the installer prints `"Chromium installation verification failed. Re-run the installer after fixing the network, or run 'bunx @playwright/cli install chromium' manually."` and exits non-zero; the existing binary (if upgrading) is left in place
|
|
119
|
+
|
|
120
|
+
#### Scenario: Re-install on a system with Chromium is a no-op for the browser step
|
|
121
|
+
- **WHEN** `install.ps1` (or `install.sh`) runs on a system that already has Chromium
|
|
122
|
+
- **THEN** `bunx @playwright/cli install chromium` returns quickly (no re-download), the verification step confirms Chromium is present, and the installer exits 0
|
|
123
|
+
|
|
124
|
+
### Requirement: Network Failure Is Handled Gracefully
|
|
125
|
+
The system MUST detect when the GitHub API or the release download is unreachable and MUST exit non-zero with a clear remediation message. The installer MUST NOT replace the existing binary on a failed download (idempotency of the upgrade flow).
|
|
126
|
+
|
|
127
|
+
#### Scenario: api.github.com unreachable
|
|
128
|
+
- **WHEN** `install.ps1` (or `install.sh`) runs on a system where `https://api.github.com/repos/expert-vision-software/GemiTerm/releases/latest` returns a network error
|
|
129
|
+
- **THEN** the installer prints `"Cannot reach GitHub releases. Check your network connection or use the 'build from source' instructions in docs/INSTALL.md."` and exits non-zero; the existing binary (if any) is **NOT** modified
|
|
130
|
+
|
|
131
|
+
#### Scenario: Asset download fails mid-stream
|
|
132
|
+
- **WHEN** the download of `GemiTerm.exe` (or `GemiTerm`) starts but fails partway (e.g. connection reset)
|
|
133
|
+
- **THEN** the installer exits non-zero, the partial file is deleted (or, on Windows, not yet renamed into place), and the existing binary (if upgrading) is **NOT** replaced
|
|
134
|
+
|
|
135
|
+
#### Scenario: Existing binary is untouched on a failed install
|
|
136
|
+
- **WHEN** a fresh install fails partway and the user had an existing v1.4.1 binary
|
|
137
|
+
- **THEN** `gemiterm --version` after the failure still reports the v1.4.1 version (the original binary is byte-identical)
|
|
138
|
+
|
|
139
|
+
### Requirement: No Breaking Changes To User Data Or Config Paths
|
|
140
|
+
The system MUST NOT change the on-disk layout of the user config dir relative to v1.4.1. The Windows config dir MUST remain `%APPDATA%\gemiterm\`, the POSIX config dir MUST be `~/gemiterm/`, and the `GEMITERM_CONFIG_DIR` env-var override MUST be honored by the installed binary. The v1.4.1 source config dir at `~/.config/gemiterm/` is the **only** dir the installer may copy from; it MUST NOT be read, written, or deleted by v2.0.0 itself.
|
|
141
|
+
|
|
142
|
+
#### Scenario: Windows config path is unchanged
|
|
143
|
+
- **WHEN** a user inspects the config dir after install
|
|
144
|
+
- **THEN** profiles, cookies, and the `.default` marker live at `%APPDATA%\gemiterm\profiles\<name>\storage_state.json` and `%APPDATA%\gemiterm\profiles\.default` exactly as in v1.4.1
|
|
145
|
+
|
|
146
|
+
#### Scenario: POSIX config path is unchanged
|
|
147
|
+
- **WHEN** a user inspects the config dir after a POSIX install
|
|
148
|
+
- **THEN** profiles, cookies, and the `.default` marker live at `~/gemiterm/profiles/<name>/storage_state.json` and `~/gemiterm/profiles/.default` exactly as the v2.0.0 binary's path-utils expects
|
|
149
|
+
|
|
150
|
+
#### Scenario: GEMITERM_CONFIG_DIR override is honored
|
|
151
|
+
- **WHEN** a user sets `GEMITERM_CONFIG_DIR=/custom/path` and runs `gemiterm status`
|
|
152
|
+
- **THEN** the installed binary reads from `/custom/path` (overriding the platform default), as verified by `src/infrastructure/path-utils.ts:8-18`
|
|
153
|
+
|
|
154
|
+
### Requirement: v1.4.1 Config Dir Is Migrated Forward On First v2.0.0 Install
|
|
155
|
+
The system MUST detect a v1.4.1 config dir at `~/.config/gemiterm/` (the path v1.4.1 used on every platform; see `src/gemiterm/config.py:14` in the v1.4.1 reference at `C:\dev\projects\github\webgemini-cli`) and, on the first v2.0.0 install where the v2.0.0 target dir does not yet exist, MUST copy the v1.4.1 tree to the v2.0.0 location. The Windows target is `%APPDATA%\gemiterm\`; the POSIX target is `~/gemiterm/`. The v1.4.1 source dir MUST be left in place as a safety net. The copy is a one-time operation: if the v2.0.0 target dir already exists (e.g. from a prior install or manual setup), the copy MUST be skipped.
|
|
156
|
+
|
|
157
|
+
#### Scenario: Windows user with v1.4.1 Python config upgrades to v2.0.0
|
|
158
|
+
- **WHEN** a user with `~/.config/gemiterm/profiles/work/storage_state.json` and `%APPDATA%\gemiterm\` either missing or empty runs `install.ps1`
|
|
159
|
+
- **THEN** the installer copies the contents of `~/.config/gemiterm/` to `%APPDATA%\gemiterm\` (preserving `profiles/`, the `.default` marker, and any `storage_state.json` files byte-for-byte), prints `"v1.4.1 config copied to %APPDATA%\gemiterm\. The original at ... is left in place as a backup."`, and `gemiterm status` after install lists the same profile names that existed in the v1.4.1 dir
|
|
160
|
+
|
|
161
|
+
#### Scenario: POSIX user with v1.4.1 Python config upgrades to v2.0.0
|
|
162
|
+
- **WHEN** a user with `~/.config/gemiterm/profiles/work/storage_state.json` and `~/gemiterm/` either missing or empty runs `install.sh`
|
|
163
|
+
- **THEN** the installer copies the contents of `~/.config/gemiterm/` to `~/gemiterm/`, prints the equivalent copy message, and `gemiterm status` lists the same profile names as the v1.4.1 dir
|
|
164
|
+
|
|
165
|
+
#### Scenario: v2.0.0 target dir already exists — copy is skipped
|
|
166
|
+
- **WHEN** a user with `%APPDATA%\gemiterm\profiles\work\storage_state.json` (v2.0.0) and `~/.config/gemiterm\profiles\old\storage_state.json` (v1.4.1) runs `install.ps1`
|
|
167
|
+
- **THEN** the installer prints no copy message, `%APPDATA%\gemiterm\profiles\work\storage_state.json` is byte-identical to its pre-install hash, and `gemiterm status` lists `work` (not `old`) as the active profile
|
|
168
|
+
|
|
169
|
+
#### Scenario: Neither v1.4.1 nor v2.0.0 config exists — installer proceeds without copy
|
|
170
|
+
- **WHEN** a fresh-install user has neither `~/.config/gemiterm/` nor `%APPDATA%\gemiterm\` (or `~/gemiterm/`) and runs the installer
|
|
171
|
+
- **THEN** the installer does not print a copy message and does not create a config dir; the config dir is created lazily on the first `gemiterm auth` / `gemiterm list` invocation
|
|
172
|
+
|
|
173
|
+
### Requirement: Installer Prompts For Package-Manager Install When Bun Or Npm Is On PATH
|
|
174
|
+
The system MUST detect whether `bun` or `npm` is on `PATH` at install time. If either is present and stdin is a TTY, the installer MUST prompt the user with `"It is recommended to install via bun or npm package manager. Are you sure you want to continue with binary install? [y/N]"` and MUST default to N. The user MUST type `y` or `yes` to proceed with the binary install. If the user declines, the installer MUST print `"Aborted. Install via: bun i -g gemiterm"` (or `npm i -g gemiterm`, whichever is on PATH) and exit 0. The prompt MUST be suppressed when stdin is not a TTY (e.g. the `irm | iex` and `curl | bash` one-liner flows), so unattended installs proceed without blocking.
|
|
175
|
+
|
|
176
|
+
#### Scenario: User with bun on PATH answers the prompt with y
|
|
177
|
+
- **WHEN** `install.sh` runs in a TTY, `bun` is on PATH, and the user types `y` at the prompt
|
|
178
|
+
- **THEN** the installer proceeds with the binary download, install-browser, and PATH-augmentation steps, and exits 0
|
|
179
|
+
|
|
180
|
+
#### Scenario: User with npm on PATH answers the prompt with N (default)
|
|
181
|
+
- **WHEN** `install.ps1` runs in a TTY, `npm` is on PATH, and the user presses Enter (or types anything other than `y`/`yes`) at the prompt
|
|
182
|
+
- **THEN** the installer prints `"Aborted. Install via: npm i -g gemiterm"` and exits 0; no binary is downloaded and no `PATH` entry is added
|
|
183
|
+
|
|
184
|
+
#### Scenario: Unattended one-liner install with npm on PATH skips the prompt
|
|
185
|
+
- **WHEN** a user runs `irm ... | iex` or `curl -fsSL ... | bash` (stdin is not a TTY) and `bun`/`npm` is on PATH
|
|
186
|
+
- **THEN** the installer does not prompt and proceeds with the binary install
|
|
187
|
+
|
|
188
|
+
#### Scenario: Neither bun nor npm is on PATH — no prompt
|
|
189
|
+
- **WHEN** the installer runs and neither `bun` nor `npm` is on PATH
|
|
190
|
+
- **THEN** the installer does not print the prompt and proceeds with the binary install (the package-manager install is not an option for this user)
|
|
191
|
+
|
|
192
|
+
### Requirement: Installer Scripts Are Syntax-Validated In CI
|
|
193
|
+
The system MUST provide a CI gate that verifies both `install.ps1` and `install.sh` parse without syntax errors. The gate MUST be a single test file at `tests/integration/installer-script-shape.test.ts` that invokes the PowerShell parser API and `bash -n` on the scripts and fails the CI run if either reports an error.
|
|
194
|
+
|
|
195
|
+
#### Scenario: PowerShell parser accepts install.ps1
|
|
196
|
+
- **WHEN** CI runs `pwsh -NoProfile -Command "& { [System.Management.Automation.Language.Parser]::ParseFile('install.ps1', [ref]$null, [ref]$null) }"`
|
|
197
|
+
- **THEN** the command exits 0 and reports no parse errors
|
|
198
|
+
|
|
199
|
+
#### Scenario: Bash parser accepts install.sh
|
|
200
|
+
- **WHEN** CI runs `bash -n install.sh`
|
|
201
|
+
- **THEN** the command exits 0 and reports no syntax errors
|
|
202
|
+
|
|
203
|
+
#### Scenario: A malformed install script fails the shape test
|
|
204
|
+
- **WHEN** `install.sh` is edited to contain a syntax error (e.g. an unclosed brace)
|
|
205
|
+
- **THEN** `tests/integration/installer-script-shape.test.ts` fails in CI, blocking the merge; the failure message includes the `bash` error output for debugging
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
## Purpose
|
|
2
|
+
|
|
3
|
+
Input validation helpers used by command handlers and services to enforce invariants on user-supplied data: conversation IDs, ISO date strings, and profile names. All helpers throw a `GemitermError` (the project's base domain error) on invalid input so the calling layer can treat validation failures uniformly.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
### Requirement: validateConversationId
|
|
8
|
+
The system MUST export a `validateConversationId(id: string): void` function. The function MUST throw a `GemitermError` whose message starts with `Conversation ID must not be empty` when the supplied id is falsy, an empty string, or a whitespace-only string (i.e. `id.trim().length === 0`). The function MUST return `void` and MUST NOT throw for any other string.
|
|
9
|
+
|
|
10
|
+
#### Scenario: Non-empty id passes
|
|
11
|
+
- **WHEN** `validateConversationId("abc123")` is called
|
|
12
|
+
- **THEN** the function does not throw
|
|
13
|
+
|
|
14
|
+
#### Scenario: Empty string throws
|
|
15
|
+
- **WHEN** `validateConversationId("")` is called
|
|
16
|
+
- **THEN** the function throws a `GemitermError` whose message includes `Conversation ID must not be empty`
|
|
17
|
+
|
|
18
|
+
#### Scenario: Whitespace-only id throws
|
|
19
|
+
- **WHEN** `validateConversationId(" ")` is called
|
|
20
|
+
- **THEN** the function throws a `GemitermError`
|
|
21
|
+
|
|
22
|
+
### Requirement: parseIsoDate
|
|
23
|
+
The system MUST export a `parseIsoDate(dateStr: string, fieldName: string): number` function. The function MUST parse the supplied string with `Date.parse`. If parsing yields a `NaN` value, the function MUST throw a `GemitermError` whose message names the supplied `fieldName` and the invalid input. If parsing succeeds, the function MUST return the resulting timestamp in milliseconds (a `number`).
|
|
24
|
+
|
|
25
|
+
#### Scenario: Valid ISO date returns timestamp
|
|
26
|
+
- **WHEN** `parseIsoDate("2025-01-15T10:30:00.000Z", "createdAt")` is called
|
|
27
|
+
- **THEN** the function returns a `number` equal to `new Date("2025-01-15T10:30:00.000Z").getTime()`
|
|
28
|
+
|
|
29
|
+
#### Scenario: Invalid date string throws
|
|
30
|
+
- **WHEN** `parseIsoDate("not-a-date", "field")` is called
|
|
31
|
+
- **THEN** the function throws a `GemitermError` whose message contains `Invalid ISO date for 'field'`
|
|
32
|
+
|
|
33
|
+
#### Scenario: Field name appears in error
|
|
34
|
+
- **WHEN** `parseIsoDate("bad", "updatedAt")` is called
|
|
35
|
+
- **THEN** the thrown error's message contains the literal string `'updatedAt'`
|
|
36
|
+
|
|
37
|
+
### Requirement: validateProfileName Allowed Character Set
|
|
38
|
+
The system MUST export a `validateProfileName(name: string): void` function. The function MUST treat a name as valid iff (a) the name is non-empty after trimming, and (b) the un-trimmed name matches the regular expression `/^[a-zA-Z0-9_-]+$/` — i.e. the name contains only ASCII letters, ASCII digits, hyphens, and underscores, and is at least one character long. Any other input MUST cause a `GemitermError` to be thrown.
|
|
39
|
+
|
|
40
|
+
#### Scenario: Valid names pass
|
|
41
|
+
- **WHEN** `validateProfileName` is called with `"default"`, `"my-profile"`, `"profile_2"`, or `"ABC123"`
|
|
42
|
+
- **THEN** the function does not throw
|
|
43
|
+
|
|
44
|
+
#### Scenario: Empty string throws
|
|
45
|
+
- **WHEN** `validateProfileName("")` is called
|
|
46
|
+
- **THEN** the function throws a `GemitermError` whose message includes `Profile name must not be empty`
|
|
47
|
+
|
|
48
|
+
#### Scenario: Whitespace-only name throws
|
|
49
|
+
- **WHEN** `validateProfileName(" ")` is called
|
|
50
|
+
- **THEN** the function throws a `GemitermError`
|
|
51
|
+
|
|
52
|
+
#### Scenario: Names with spaces throw
|
|
53
|
+
- **WHEN** `validateProfileName("my profile")` is called
|
|
54
|
+
- **THEN** the function throws a `GemitermError`
|
|
55
|
+
|
|
56
|
+
#### Scenario: Names with disallowed special characters throw
|
|
57
|
+
- **WHEN** `validateProfileName` is called with `"profile@bad"`, `"pro/ject"`, or `"pro.file"`
|
|
58
|
+
- **THEN** the function throws a `GemitermError`
|
|
59
|
+
|
|
60
|
+
### Requirement: Validation Errors Use GemitermError
|
|
61
|
+
All three validators (`validateConversationId`, `parseIsoDate`, `validateProfileName`) MUST throw instances of the project's base `GemitermError` class on invalid input. Validation errors therefore MUST be catchable as `GemitermError` (or as `Error`) by upstream layers.
|
|
62
|
+
|
|
63
|
+
#### Scenario: Validation errors are GemitermErrors
|
|
64
|
+
- **WHEN** any of the three validators is called with invalid input
|
|
65
|
+
- **THEN** the thrown value is an instance of `GemitermError`
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gemiterm",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"module": "index.ts",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Expert-Vision-Software/gemiterm.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/Expert-Vision-Software/gemiterm#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/Expert-Vision-Software/gemiterm/issues"
|
|
13
|
+
},
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/bun": "^1.3.14"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"start": "bun run src/cli/index.ts",
|
|
22
|
+
"dev": "bun run src/cli/index.ts",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"test": "bun test",
|
|
25
|
+
"test:unit": "bun test tests/unit",
|
|
26
|
+
"test:integration": "bun test tests/integration",
|
|
27
|
+
"test:parity": "bun test tests/parity",
|
|
28
|
+
"test:smoke": "bun test tests/smoke",
|
|
29
|
+
"test:all": "bun test",
|
|
30
|
+
"lint:mediation": "bash scripts/lint-path-mediation.sh",
|
|
31
|
+
"lint:mediation:ps": "pwsh scripts/lint-path-mediation.ps1",
|
|
32
|
+
"build": "bun run scripts/build.ts",
|
|
33
|
+
"build:linux": "bun run scripts/build.ts linux",
|
|
34
|
+
"build:windows": "bun run scripts/build.ts windows",
|
|
35
|
+
"build:release": "bun run scripts/build.ts release"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"typescript": "^6.0.3"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"chalk": "^5.6.2",
|
|
42
|
+
"cli-table3": "^0.6.5",
|
|
43
|
+
"gemini-reverse": "^1.0.12"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/scripts/build.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { mkdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
type Target = "bun-linux-x64" | "bun-windows-x64" | "bun-darwin-x64" | "bun-darwin-arm64";
|
|
6
|
+
|
|
7
|
+
interface Profile {
|
|
8
|
+
target: Target | "host";
|
|
9
|
+
outfile: string;
|
|
10
|
+
minify: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function hostTarget(): Target {
|
|
14
|
+
switch (process.platform) {
|
|
15
|
+
case "win32":
|
|
16
|
+
return "bun-windows-x64";
|
|
17
|
+
case "darwin":
|
|
18
|
+
return process.arch === "arm64" ? "bun-darwin-arm64" : "bun-darwin-x64";
|
|
19
|
+
case "linux":
|
|
20
|
+
default:
|
|
21
|
+
return "bun-linux-x64";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const PROFILES: Record<string, Profile> = {
|
|
26
|
+
default: { target: "host", outfile: "dist/gemiterm", minify: false },
|
|
27
|
+
linux: { target: "bun-linux-x64", outfile: "dist/gemiterm", minify: false },
|
|
28
|
+
windows: { target: "bun-windows-x64", outfile: "dist/gemiterm.exe", minify: false },
|
|
29
|
+
release: { target: "host", outfile: "dist/gemiterm", minify: true },
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function loadPackageJson(): { name: string; version: string } {
|
|
33
|
+
const raw = readFileSync(resolve(import.meta.dir, "..", "package.json"), "utf-8");
|
|
34
|
+
const parsed = JSON.parse(raw) as { name?: unknown; version?: unknown };
|
|
35
|
+
if (typeof parsed.name !== "string" || typeof parsed.version !== "string") {
|
|
36
|
+
throw new Error("scripts/build.ts: package.json is missing name/version strings");
|
|
37
|
+
}
|
|
38
|
+
return { name: parsed.name, version: parsed.version };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function pickProfile(): Profile {
|
|
42
|
+
const requested = process.argv[2];
|
|
43
|
+
if (!requested) return PROFILES.default!;
|
|
44
|
+
const profile = PROFILES[requested];
|
|
45
|
+
if (!profile) {
|
|
46
|
+
const known = Object.keys(PROFILES).join(", ");
|
|
47
|
+
throw new Error(`scripts/build.ts: unknown profile "${requested}". Known: ${known}`);
|
|
48
|
+
}
|
|
49
|
+
return profile;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function ensureWindowsExeSuffix(path: string, target: Target): string {
|
|
53
|
+
if (target !== "bun-windows-x64") return path;
|
|
54
|
+
return path.toLowerCase().endsWith(".exe") ? path : `${path}.exe`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function main(): Promise<void> {
|
|
58
|
+
const profile = pickProfile();
|
|
59
|
+
const pkg = loadPackageJson();
|
|
60
|
+
const target = profile.target === "host" ? hostTarget() : profile.target;
|
|
61
|
+
|
|
62
|
+
const projectRoot = resolve(import.meta.dir, "..");
|
|
63
|
+
const outfile = resolve(projectRoot, ensureWindowsExeSuffix(profile.outfile, target));
|
|
64
|
+
mkdirSync(dirname(outfile), { recursive: true });
|
|
65
|
+
|
|
66
|
+
const result = await Bun.build({
|
|
67
|
+
entrypoints: [resolve(projectRoot, "src/cli/index.ts")],
|
|
68
|
+
target: "bun",
|
|
69
|
+
compile: {
|
|
70
|
+
target,
|
|
71
|
+
outfile,
|
|
72
|
+
},
|
|
73
|
+
minify: profile.minify,
|
|
74
|
+
define: {
|
|
75
|
+
__GEMITERM_VERSION__: JSON.stringify(pkg.version),
|
|
76
|
+
__GEMITERM_NAME__: JSON.stringify(pkg.name),
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (!result.success) {
|
|
81
|
+
for (const log of result.logs) console.error(log);
|
|
82
|
+
throw new Error(`scripts/build.ts: build failed for profile ${JSON.stringify(profile)}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log(`built ${profile.outfile} (target=${target}, minify=${profile.minify}, version=${pkg.version})`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
await main();
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# scripts/lint-path-mediation.ps1
|
|
2
|
+
#
|
|
3
|
+
# PowerShell Core port of scripts/lint-path-mediation.sh.
|
|
4
|
+
# Enforce the path-and-file mediation rule: no file in src/ outside the
|
|
5
|
+
# allowed exemptions may import from node:fs, node:path, or node:os.
|
|
6
|
+
#
|
|
7
|
+
# Allowed exemptions:
|
|
8
|
+
# - src/infrastructure/path-utils.ts (canonical home)
|
|
9
|
+
# - src/services/install-browser-service.ts (WSL mount parser)
|
|
10
|
+
#
|
|
11
|
+
# Exit 0 when the rule is satisfied, non-zero with a clear message otherwise.
|
|
12
|
+
# This script is called by the CI test.yml workflow as the last step.
|
|
13
|
+
|
|
14
|
+
$ErrorActionPreference = "Stop"
|
|
15
|
+
|
|
16
|
+
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
17
|
+
$srcDir = Join-Path $scriptDir "..\src"
|
|
18
|
+
if (-not (Test-Path $srcDir)) {
|
|
19
|
+
Write-Error "src/ directory not found at $srcDir"
|
|
20
|
+
exit 1
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
$tsFiles = Get-ChildItem -Path $srcDir -Recurse -Filter "*.ts" -File
|
|
24
|
+
$forbidden = @()
|
|
25
|
+
foreach ($file in $tsFiles) {
|
|
26
|
+
$matches_found = Select-String -Path $file.FullName -Pattern 'from\s+["''](node:fs|node:path|node:os)["'']'
|
|
27
|
+
if ($matches_found) {
|
|
28
|
+
$isExempt = $false
|
|
29
|
+
$normalized = $file.FullName -replace "\\", "/"
|
|
30
|
+
$normalized = $normalized -replace "^.*/gemiterm-bun-rewrite/", ""
|
|
31
|
+
if ($normalized -eq "src/infrastructure/path-utils.ts") { $isExempt = $true }
|
|
32
|
+
if ($normalized -eq "src/infrastructure/io.ts") { $isExempt = $true }
|
|
33
|
+
if ($normalized -eq "src/services/install-browser-service.ts") { $isExempt = $true }
|
|
34
|
+
if (-not $isExempt) {
|
|
35
|
+
foreach ($m in $matches_found) {
|
|
36
|
+
$forbidden += [PSCustomObject]@{
|
|
37
|
+
Path = $normalized
|
|
38
|
+
LineNumber = $m.LineNumber
|
|
39
|
+
Line = $m.Line.Trim()
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if ($forbidden.Count -gt 0) {
|
|
47
|
+
Write-Host "ERROR: forbidden direct imports of node:fs / node:path / node:os in src/." -ForegroundColor Red
|
|
48
|
+
Write-Host ""
|
|
49
|
+
Write-Host "All file-system and path operations must route through:"
|
|
50
|
+
Write-Host " - src/infrastructure/path-utils.ts (path values)"
|
|
51
|
+
Write-Host " - src/infrastructure/io.ts (file-system side effects)"
|
|
52
|
+
Write-Host ""
|
|
53
|
+
Write-Host "Offending imports:"
|
|
54
|
+
foreach ($f in $forbidden) {
|
|
55
|
+
Write-Host " $($f.Path):$($f.LineNumber): $($f.Line)"
|
|
56
|
+
}
|
|
57
|
+
Write-Host ""
|
|
58
|
+
Write-Host "If you have a legitimate need for a direct import, add the file to"
|
|
59
|
+
Write-Host "the exemption list in this script and the CI workflow with a comment"
|
|
60
|
+
Write-Host "explaining why."
|
|
61
|
+
exit 1
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
Write-Host "OK: no forbidden node:fs / node:path / node:os imports in src/"
|
|
65
|
+
exit 0
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/lint-path-mediation.sh
|
|
3
|
+
#
|
|
4
|
+
# Enforce the path-and-file mediation rule: no file in src/ outside the
|
|
5
|
+
# allowed exemptions may import from node:fs, node:path, or node:os.
|
|
6
|
+
#
|
|
7
|
+
# Allowed exemptions:
|
|
8
|
+
# - src/infrastructure/path-utils.ts (canonical home for paths)
|
|
9
|
+
# - src/infrastructure/io.ts (canonical home for file ops)
|
|
10
|
+
# - src/services/install-browser-service.ts (WSL mount parser)
|
|
11
|
+
#
|
|
12
|
+
# Exit 0 when the rule is satisfied, non-zero with a clear message otherwise.
|
|
13
|
+
# This script is called by the CI test.yml workflow as the last step.
|
|
14
|
+
|
|
15
|
+
set -euo pipefail
|
|
16
|
+
|
|
17
|
+
FORBIDDEN=$(grep -rn --include='*.ts' "from \"node:" src/ \
|
|
18
|
+
| grep -E "from \"node:(fs|path|os)\"" \
|
|
19
|
+
| grep -v "src/infrastructure/path-utils.ts" \
|
|
20
|
+
| grep -v "src/infrastructure/io.ts" \
|
|
21
|
+
| grep -v "src/services/install-browser-service.ts" \
|
|
22
|
+
|| true)
|
|
23
|
+
|
|
24
|
+
if [ -n "$FORBIDDEN" ]; then
|
|
25
|
+
echo "ERROR: forbidden direct imports of node:fs / node:path / node:os in src/." >&2
|
|
26
|
+
echo "" >&2
|
|
27
|
+
echo "All file-system and path operations must route through:" >&2
|
|
28
|
+
echo " - src/infrastructure/path-utils.ts (path values)" >&2
|
|
29
|
+
echo " - src/infrastructure/io.ts (file-system side effects)" >&2
|
|
30
|
+
echo "" >&2
|
|
31
|
+
echo "Offending imports:" >&2
|
|
32
|
+
echo "$FORBIDDEN" >&2
|
|
33
|
+
echo "" >&2
|
|
34
|
+
echo "If you have a legitimate need for a direct import, add the file to" >&2
|
|
35
|
+
echo "the exemption list in this script and the CI workflow with a comment" >&2
|
|
36
|
+
echo "explaining why." >&2
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
echo "OK: no forbidden node:fs / node:path / node:os imports in src/"
|
package/src/cli/.gitkeep
ADDED
|
File without changes
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Mediator } from "../core/mediator.ts";
|
|
2
|
+
import type { ProfileAuthManager } from "../services/profile-auth-manager.ts";
|
|
3
|
+
import { AuthCommand } from "./commands/auth-command.ts";
|
|
4
|
+
import { ProfileCommand } from "./commands/profile-command.ts";
|
|
5
|
+
import { StatusCommand } from "./commands/status-command.ts";
|
|
6
|
+
import { ListCommand } from "./commands/list-command.ts";
|
|
7
|
+
import { FetchCommand } from "./commands/fetch-command.ts";
|
|
8
|
+
import { ContinueCommand } from "./commands/continue-command.ts";
|
|
9
|
+
import { NewCommand } from "./commands/new-command.ts";
|
|
10
|
+
import { DeleteCommand } from "./commands/delete-command.ts";
|
|
11
|
+
import { ExportCommand } from "./commands/export-command.ts";
|
|
12
|
+
import { ExportAllCommand } from "./commands/export-all-command.ts";
|
|
13
|
+
import { InstallBrowserCommand } from "./commands/install-browser-command.ts";
|
|
14
|
+
|
|
15
|
+
export interface CliCommandContext {
|
|
16
|
+
verbose: boolean;
|
|
17
|
+
mediator: Mediator;
|
|
18
|
+
profileAuthManager: ProfileAuthManager;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface CliCommand {
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly description: string;
|
|
24
|
+
execute(args: string[], context: CliCommandContext): Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class CommandRegistry {
|
|
28
|
+
private handlers = new Map<string, CliCommand>();
|
|
29
|
+
|
|
30
|
+
register(commandName: string, handler: CliCommand): void {
|
|
31
|
+
if (this.handlers.has(commandName)) {
|
|
32
|
+
throw new Error(`Command already registered: ${commandName}`);
|
|
33
|
+
}
|
|
34
|
+
this.handlers.set(commandName, handler);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getHandler(commandName: string): CliCommand | undefined {
|
|
38
|
+
return this.handlers.get(commandName);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
has(commandName: string): boolean {
|
|
42
|
+
return this.handlers.has(commandName);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getRegisteredNames(): string[] {
|
|
46
|
+
return [...this.handlers.keys()];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
registerAllCommands(): void {
|
|
50
|
+
this.register("auth", new AuthCommand());
|
|
51
|
+
this.register("profile", new ProfileCommand());
|
|
52
|
+
this.register("status", new StatusCommand());
|
|
53
|
+
this.register("list", new ListCommand());
|
|
54
|
+
this.register("fetch", new FetchCommand());
|
|
55
|
+
this.register("continue", new ContinueCommand());
|
|
56
|
+
this.register("new", new NewCommand());
|
|
57
|
+
this.register("delete", new DeleteCommand());
|
|
58
|
+
this.register("export", new ExportCommand());
|
|
59
|
+
this.register("export-all", new ExportAllCommand());
|
|
60
|
+
this.register("install-browser", new InstallBrowserCommand());
|
|
61
|
+
}
|
|
62
|
+
}
|