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,409 @@
|
|
|
1
|
+
## Purpose
|
|
2
|
+
|
|
3
|
+
The CLI command layer of the `gemiterm` application. It defines the 11 top-level user-facing commands (auth, profile, status, list, fetch, continue, new, delete, export, export-all, install-browser), each implemented as a `CliCommand` class registered by the `CommandRegistry`. The command layer is responsible for argument parsing, mediator dispatch (queries `ListChatsQuery` / `FetchChatQuery` and commands `SendMessageCommand` / `StartNewChatCommand` / `DeleteConversationCommand`), interactive REPL I/O for `new` and `continue`, and human-readable / JSON output formatting. This spec documents the as-built behavior of the command layer; any future conformance fixes are tracked separately in the `command-spec-conformance` change.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
### Requirement: ListCommand
|
|
8
|
+
|
|
9
|
+
The system MUST provide a `list` command implemented by `ListCommand` in `src/cli/commands/list-command.ts`. The command MUST be registered under the name `list` and MUST send a `ListChatsQuery` to the mediator with a payload of shape `{ limit?, offset?, search?, allProfiles }`. The command MUST support the flags `--limit/-n <N>` (default 10), `--offset <N>` (default 0), `--all` (no limit), `--all-profiles`, `--sort <recent|oldest|alpha>` (default `recent`), `--search/-s <query>`, `--after <date>`, `--before <date>`, `--format/-f <text|json>` (default `text`), and `--path/-p <path>`. When `--all` is not set, the command MUST additionally slice the result set to `[offset, offset + limit)`. The `--all-profiles` flag MUST be propagated into the mediator payload as `allProfiles: true`.
|
|
10
|
+
|
|
11
|
+
#### Scenario: List with no flags sends a default query
|
|
12
|
+
- **WHEN** the user runs `gemiterm list`
|
|
13
|
+
- **THEN** the command sends a `ListChatsQuery` to the mediator with `limit: 10`, `offset: 0`, no `search`, and `allProfiles: false`, and renders the result as a 4-column text table (ID / TITLE / DATE / PIN)
|
|
14
|
+
|
|
15
|
+
#### Scenario: List with --limit
|
|
16
|
+
- **WHEN** the user runs `gemiterm list --limit 5`
|
|
17
|
+
- **THEN** the mediator payload carries `limit: 5` and at most 5 chats are displayed
|
|
18
|
+
|
|
19
|
+
#### Scenario: List with --all disables the slice
|
|
20
|
+
- **WHEN** the user runs `gemiterm list --all`
|
|
21
|
+
- **THEN** the mediator payload carries `limit: undefined` and the slice step is skipped (all returned chats are shown)
|
|
22
|
+
|
|
23
|
+
#### Scenario: List with --all-profiles propagates to mediator
|
|
24
|
+
- **WHEN** the user runs `gemiterm list --all-profiles`
|
|
25
|
+
- **THEN** the mediator payload carries `allProfiles: true`
|
|
26
|
+
|
|
27
|
+
#### Scenario: List with --sort alpha sorts ascending by title
|
|
28
|
+
- **WHEN** the user runs `gemiterm list --sort alpha`
|
|
29
|
+
- **THEN** the displayed chats are sorted by `title` ascending using `localeCompare`
|
|
30
|
+
|
|
31
|
+
#### Scenario: List with --search forwards the search term
|
|
32
|
+
- **WHEN** the user runs `gemiterm list --search "Bun"`
|
|
33
|
+
- **THEN** the mediator payload carries `search: "Bun"`
|
|
34
|
+
|
|
35
|
+
#### Scenario: List with --format json
|
|
36
|
+
- **WHEN** the user runs `gemiterm list --format json`
|
|
37
|
+
- **THEN** the output is a JSON document with shape `{ chats: ChatInfo[] }` and nothing else is written to stdout
|
|
38
|
+
|
|
39
|
+
#### Scenario: List with --path writes the rendered output to the given file
|
|
40
|
+
- **WHEN** the user runs `gemiterm list --path ./out.txt`
|
|
41
|
+
- **THEN** the rendered text or JSON content is written to `./out.txt` and a confirmation line `Output written to: <resolved>` is printed
|
|
42
|
+
|
|
43
|
+
#### Scenario: List with --after and --before filters chats by date
|
|
44
|
+
- **WHEN** the user runs `gemiterm list --after 2024-01-01 --before 2024-12-31`
|
|
45
|
+
- **THEN** chats with `timestamp` outside the inclusive range are removed before display
|
|
46
|
+
|
|
47
|
+
#### Scenario: List with no conversations prints the empty message
|
|
48
|
+
- **WHEN** the mediator returns an empty `chats` array
|
|
49
|
+
- **THEN** the output contains the message `No conversations found.`
|
|
50
|
+
|
|
51
|
+
#### Scenario: List --help shows usage
|
|
52
|
+
- **WHEN** the user runs `gemiterm list --help`
|
|
53
|
+
- **THEN** the output contains `Usage: gemiterm list` and documents every flag above
|
|
54
|
+
|
|
55
|
+
### Requirement: ListCommand Text Output Table
|
|
56
|
+
|
|
57
|
+
The `list` command's default text output MUST be a 4-column table with headers `ID`, `TITLE`, `DATE`, `PIN` (in that order). The table MUST be produced by `formatChatList` in `src/infrastructure/formatters.ts`. When `--all-profiles` is set, the command MUST call `formatChatList(chats, { includeProfileColumn: true })` and the rendered text output MUST include a `PROFILE` column as the 5th column.
|
|
58
|
+
|
|
59
|
+
#### Scenario: Default text output has 4 columns
|
|
60
|
+
- **WHEN** the user runs `gemiterm list` (no flags)
|
|
61
|
+
- **THEN** the rendered table contains header columns `ID`, `TITLE`, `DATE`, and `PIN` only
|
|
62
|
+
|
|
63
|
+
#### Scenario: --all-profiles adds a Profile column to text output
|
|
64
|
+
- **WHEN** the user runs `gemiterm list --all-profiles`
|
|
65
|
+
- **THEN** the rendered text output table contains 5 columns `ID`, `TITLE`, `DATE`, `PIN`, `PROFILE` and each row shows the owning profile name
|
|
66
|
+
|
|
67
|
+
### Requirement: FetchCommand
|
|
68
|
+
|
|
69
|
+
The system MUST provide a `fetch` command implemented by `FetchCommand` in `src/cli/commands/fetch-command.ts`. The command MUST accept a single optional positional `<conversation_id>` argument and MUST support `--format/-f <text|json>` (default `text`) and `--path/-p <path>`. When a conversation id is provided, the command MUST send a `FetchChatQuery` to the mediator with payload `{ conversationId }`. When no conversation id is provided, the command MUST invoke the `list` command via the `CommandRegistry` and return without sending a fetch query. Text output MUST include a header line `Conversation: <id>` and label each message with `User:` or `Model:` depending on role. JSON output MUST be `{ conversationId, messages }`.
|
|
70
|
+
|
|
71
|
+
#### Scenario: Fetch with conversation id sends a FetchChatQuery
|
|
72
|
+
- **WHEN** the user runs `gemiterm fetch conv-abc123`
|
|
73
|
+
- **THEN** the mediator receives a `FetchChatQuery` with `payload.conversationId === "conv-abc123"`
|
|
74
|
+
|
|
75
|
+
#### Scenario: Fetch with no id invokes list
|
|
76
|
+
- **WHEN** the user runs `gemiterm fetch` with no positional argument
|
|
77
|
+
- **THEN** no `FetchChatQuery` is sent and the `list` command is executed against the same context (after printing a "No conversation ID specified" notice)
|
|
78
|
+
|
|
79
|
+
#### Scenario: Fetch with --format json
|
|
80
|
+
- **WHEN** the user runs `gemiterm fetch conv-abc123 --format json`
|
|
81
|
+
- **THEN** the output is a JSON document with shape `{ conversationId: "conv-abc123", messages: Message[] }`
|
|
82
|
+
|
|
83
|
+
#### Scenario: Fetch with --path writes the rendered output to the given file
|
|
84
|
+
- **WHEN** the user runs `gemiterm fetch conv-abc123 --path ./out.txt`
|
|
85
|
+
- **THEN** the rendered text or JSON content is written to `./out.txt` and a confirmation line `Output written to: <resolved>` is printed
|
|
86
|
+
|
|
87
|
+
#### Scenario: Fetch with empty messages prints "No messages found"
|
|
88
|
+
- **WHEN** the mediator returns `messages: []`
|
|
89
|
+
- **THEN** the rendered text output contains `No messages found.`
|
|
90
|
+
|
|
91
|
+
#### Scenario: Fetch --help shows usage
|
|
92
|
+
- **WHEN** the user runs `gemiterm fetch --help`
|
|
93
|
+
- **THEN** the output contains `Usage: gemiterm fetch [conversation_id] [options]` and documents `--format`, `--path`, and `--help`
|
|
94
|
+
|
|
95
|
+
### Requirement: ContinueCommand
|
|
96
|
+
|
|
97
|
+
The system MUST provide a `continue` command implemented by `ContinueCommand` in `src/cli/commands/continue-command.ts`. The command MUST accept an optional positional `<conversation_id>` and an optional positional `<message>`, plus `--help/-h`. When `<conversation_id>` is missing, the command MUST invoke the `list` command via the `CommandRegistry` and return. When both `<conversation_id>` and `<message>` are present, the command MUST look up the owning profile via `ProfileAuthManager.findProfileForConversation(conversationId)` and send a `SendMessageCommand` to the mediator with payload `{ conversationId, message, profileName? }`; when the lookup returns a profile name, `profileName` MUST be set on the payload so the handler routes to that profile's `GeminiClientService`. When no profile owns the conversation, the command MUST throw `AuthenticationError` with a remediation message and exit non-zero. When `<conversation_id>` is present and `<message>` is absent, the command MUST start an interactive REPL that reads lines from stdin and sends each non-empty line as a `SendMessageCommand` using the same profile-lookup logic; the REPL MUST exit on `/exit` or `/quit` and MUST ignore empty lines. In a single-profile setup the behavior MUST be unchanged: the default profile is used without a lookup.
|
|
98
|
+
|
|
99
|
+
#### Scenario: Continue with id and message sends a SendMessageCommand
|
|
100
|
+
- **WHEN** the user runs `gemiterm continue conv-abc123 "Hello there"`
|
|
101
|
+
- **THEN** the mediator receives a `SendMessageCommand` with `payload.conversationId === "conv-abc123"` and `payload.message === "Hello there"`, and the response text is printed after a `Model:` label
|
|
102
|
+
|
|
103
|
+
#### Scenario: Continue with no id invokes list
|
|
104
|
+
- **WHEN** the user runs `gemiterm continue` with no positional argument
|
|
105
|
+
- **THEN** the `list` command is executed and no `SendMessageCommand` is sent
|
|
106
|
+
|
|
107
|
+
#### Scenario: Continue with id but no message starts an interactive REPL
|
|
108
|
+
- **WHEN** the user runs `gemiterm continue conv-abc123` and types a line into stdin
|
|
109
|
+
- **THEN** a `SendMessageCommand` is sent for that line and the model response is printed after a `Model:` label
|
|
110
|
+
|
|
111
|
+
#### Scenario: Continue REPL exits on /exit
|
|
112
|
+
- **WHEN** the user types `/exit` or `/quit` in the continue REPL
|
|
113
|
+
- **THEN** the readline interface closes and the command returns
|
|
114
|
+
|
|
115
|
+
#### Scenario: Continue REPL ignores empty lines
|
|
116
|
+
- **WHEN** the user enters a blank line in the continue REPL
|
|
117
|
+
- **THEN** no `SendMessageCommand` is sent and the REPL continues prompting
|
|
118
|
+
|
|
119
|
+
#### Scenario: Continue --help shows usage
|
|
120
|
+
- **WHEN** the user runs `gemiterm continue --help`
|
|
121
|
+
- **THEN** the output contains `Usage: gemiterm continue [conversation_id] [message] [options]` and documents `/exit`, `/quit`, and `--help`
|
|
122
|
+
|
|
123
|
+
### Requirement: NewCommand
|
|
124
|
+
|
|
125
|
+
The system MUST provide a `new` command implemented by `NewCommand` in `src/cli/commands/new-command.ts`. The command MUST accept an optional positional `<message>` and MUST support `--profile/-p <name>` and `--help/-h`. When `<message>` is present, the command MUST send a `StartNewChatCommand` to the mediator with payload `{ message, profileName? }` and print the new conversation id and the model response. When `<message>` is absent, the command MUST start an interactive REPL that starts a new chat on the first non-empty line and continues with `SendMessageCommand` against the resulting `conversationId` for subsequent lines; the REPL MUST exit on `/exit` or `/quit`.
|
|
126
|
+
|
|
127
|
+
#### Scenario: New with message sends StartNewChatCommand and prints the conversation id
|
|
128
|
+
- **WHEN** the user runs `gemiterm new "Hello Gemini"`
|
|
129
|
+
- **THEN** the mediator receives a `StartNewChatCommand` with `payload.message === "Hello Gemini"`, the output contains `Conversation ID: <id>`, and the model response is printed after a `Model:` label
|
|
130
|
+
|
|
131
|
+
#### Scenario: New with --profile includes profileName in the payload
|
|
132
|
+
- **WHEN** the user runs `gemiterm new "Hi" --profile work`
|
|
133
|
+
- **THEN** the mediator payload carries `profileName: "work"`
|
|
134
|
+
|
|
135
|
+
#### Scenario: New with no message starts an interactive REPL
|
|
136
|
+
- **WHEN** the user runs `gemiterm new` with no message
|
|
137
|
+
- **THEN** the command enters a REPL; the first non-empty line is sent as a `StartNewChatCommand` and subsequent lines are sent as `SendMessageCommand` against the resulting conversation id
|
|
138
|
+
|
|
139
|
+
#### Scenario: New REPL exits on /exit or /quit
|
|
140
|
+
- **WHEN** the user types `/exit` or `/quit` in the new-command REPL
|
|
141
|
+
- **THEN** the readline interface closes and the command returns
|
|
142
|
+
|
|
143
|
+
#### Scenario: New --help shows usage
|
|
144
|
+
- **WHEN** the user runs `gemiterm new --help`
|
|
145
|
+
- **THEN** the output contains `Usage: gemiterm new [message] [options]` and documents `--profile`, `--help`, and the `/exit` / `/quit` REPL commands
|
|
146
|
+
|
|
147
|
+
### Requirement: DeleteCommand
|
|
148
|
+
|
|
149
|
+
The system MUST provide a `delete` command implemented by `DeleteCommand` in `src/cli/commands/delete-command.ts`. The command MUST accept a single positional `<conversation_id>` argument and MUST support `--force/-f` and `--help/-h`. When `<conversation_id>` is missing, the command MUST print `Error: conversation ID is required.` and exit with code 1. When `<conversation_id>` is present, the command MUST validate the id via `validateConversationId`. When `--force` is not set, the command MUST prompt the user with `Delete conversation '<id>'? (yes/no):` via readline and MUST treat any answer starting with `y` (case-insensitive) as confirmation; on confirmation it MUST look up the owning profile via `ProfileAuthManager.findProfileForConversation(conversationId)` and send a `DeleteConversationCommand` to the mediator with payload `{ conversationId, profileName? }`; when the lookup returns a profile name, `profileName` MUST be set on the payload so the handler routes to that profile's `GeminiClientService`. When no profile owns the conversation, the command MUST throw `AuthenticationError` with a remediation message and exit non-zero. The command MUST print `Conversation '<id>' deleted.` on success and MUST exit with code 1 on a failed result or handler error. In a single-profile setup the behavior MUST be unchanged: the default profile is used without a lookup.
|
|
150
|
+
|
|
151
|
+
#### Scenario: Delete with --force sends DeleteConversationCommand
|
|
152
|
+
- **WHEN** the user runs `gemiterm delete conv-abc123 --force`
|
|
153
|
+
- **THEN** the mediator receives a `DeleteConversationCommand` with `payload.conversationId === "conv-abc123"`, no readline prompt is shown, and on `result.success === true` the output contains `deleted.`
|
|
154
|
+
|
|
155
|
+
#### Scenario: Delete without --force prompts for confirmation
|
|
156
|
+
- **WHEN** the user runs `gemiterm delete conv-abc123` and answers `yes` to the prompt
|
|
157
|
+
- **THEN** the mediator receives a `DeleteConversationCommand` with `payload.conversationId === "conv-abc123"`
|
|
158
|
+
|
|
159
|
+
#### Scenario: Delete without --force and no confirmation aborts
|
|
160
|
+
- **WHEN** the user runs `gemiterm delete conv-abc123` and answers `no` to the prompt
|
|
161
|
+
- **THEN** no `DeleteConversationCommand` is sent and the output contains `Cancelled.`
|
|
162
|
+
|
|
163
|
+
#### Scenario: Delete with no id errors and exits 1
|
|
164
|
+
- **WHEN** the user runs `gemiterm delete`
|
|
165
|
+
- **THEN** the output contains `Error: conversation ID is required.` and the process exits with code 1
|
|
166
|
+
|
|
167
|
+
#### Scenario: Delete with -f short flag
|
|
168
|
+
- **WHEN** the user runs `gemiterm delete conv-abc123 -f`
|
|
169
|
+
- **THEN** the command is equivalent to `--force` (no prompt, mediator call proceeds)
|
|
170
|
+
|
|
171
|
+
#### Scenario: Delete with failed result exits 1
|
|
172
|
+
- **WHEN** the mediator handler returns `{ success: false }`
|
|
173
|
+
- **THEN** the output contains `Failed to delete conversation.` and the process exits with code 1
|
|
174
|
+
|
|
175
|
+
#### Scenario: Delete --help shows usage
|
|
176
|
+
- **WHEN** the user runs `gemiterm delete --help`
|
|
177
|
+
- **THEN** the output contains `Usage: gemiterm delete <conversation_id> [options]` and documents `--force` and `--help`
|
|
178
|
+
|
|
179
|
+
### Requirement: ExportCommand
|
|
180
|
+
|
|
181
|
+
The system MUST provide an `export` command implemented by `ExportCommand` in `src/cli/commands/export-command.ts`. The command MUST accept a single positional `<conversation_id>` argument and MUST support `--output/-o <path>`, `--format/-f <markdown|json>` (default `markdown`), `--include-metadata`, and `--help/-h`. The command MUST send a `FetchChatQuery` to the mediator with `payload.conversationId` and MUST write the formatted output to the path supplied by `--output` or, when `--output` is not set, to a default file in the current working directory named `gemini-chat-<conversation_id>-<YYYY-MM-DD>.<ext>` (where `ext` is `md` for markdown and `json` for json). The command MUST create the output directory (and any parents) with `mkdirSync(..., { recursive: true })` before writing. Markdown output MUST be produced by `formatChatAsMarkdown` and JSON output MUST be produced by `formatChatAsJson`. When `<conversation_id>` is missing or invalid, the command MUST print an error and exit with code 1.
|
|
182
|
+
|
|
183
|
+
#### Scenario: Export with --output writes to the supplied path
|
|
184
|
+
- **WHEN** the user runs `gemiterm export conv-abc123 --output ./out.md`
|
|
185
|
+
- **THEN** the file `./out.md` is created (its parent directory is created if missing) and contains the markdown export of the conversation
|
|
186
|
+
|
|
187
|
+
#### Scenario: Export with default path
|
|
188
|
+
- **WHEN** the user runs `gemiterm export conv-abc123` with no `--output`
|
|
189
|
+
- **THEN** a file named `gemini-chat-conv-abc123-<YYYY-MM-DD>.md` is created in the current working directory
|
|
190
|
+
|
|
191
|
+
#### Scenario: Export with --format json
|
|
192
|
+
- **WHEN** the user runs `gemiterm export conv-abc123 --format json`
|
|
193
|
+
- **THEN** the default output filename has a `.json` extension and the file content is the JSON export of the conversation
|
|
194
|
+
|
|
195
|
+
#### Scenario: Export with --include-metadata
|
|
196
|
+
- **WHEN** the user runs `gemiterm export conv-abc123 --include-metadata`
|
|
197
|
+
- **THEN** `formatChatAsMarkdown` is called with `includeMetadata=true` and the resulting file contains a metadata header
|
|
198
|
+
|
|
199
|
+
#### Scenario: Export with no id errors and exits 1
|
|
200
|
+
- **WHEN** the user runs `gemiterm export`
|
|
201
|
+
- **THEN** the output contains `Error: conversation ID is required.` and the process exits with code 1
|
|
202
|
+
|
|
203
|
+
#### Scenario: Export --help shows usage
|
|
204
|
+
- **WHEN** the user runs `gemiterm export --help`
|
|
205
|
+
- **THEN** the output contains `Usage: gemiterm export <conversation_id> [options]` and documents `--output`, `--format`, `--include-metadata`, and `--help`
|
|
206
|
+
|
|
207
|
+
### Requirement: ExportAllCommand
|
|
208
|
+
|
|
209
|
+
The system MUST provide an `export-all` command implemented by `ExportAllCommand` in `src/cli/commands/export-all-command.ts`. The command MUST support `--output-dir/-o <dir>` (default `./exports`), `--since <date>`, `--include-metadata`, `--all-profiles/-a`, and `--help/-h`. The command MUST send a `ListChatsQuery` to the mediator with payload `{ allProfiles }`, MUST filter the resulting chats to those whose `timestamp` is on or after the `--since` date (when supplied), and MUST iterate over the remaining chats sending a `FetchChatQuery` for each and writing a markdown file per chat under the output directory using `formatChatAsMarkdown` with sanitized filenames of the form `gemini-chat-<sanitized-title>-<YYYY-MM-DD>.md`. The command MUST create the output directory with `mkdirSync(..., { recursive: true })` and MUST write an `index.md` file in the output directory that lists each successfully exported conversation as a markdown link and, when present, a `## Failed Exports` section listing failed exports with their error message. The command MUST print progress to stdout as ` [i/total] Exporting <id>...` followed by `OK` or `FAILED` on the same line, and MUST print a final summary including `Exported: <n>`, optional `Failed: <n>`, `Output: <dir>`, and `Index: <dir>/index.md`. When `--include-metadata` is set the index MUST include a `> Successful: <n> | Failed: <n>` line.
|
|
210
|
+
|
|
211
|
+
#### Scenario: Export-all writes per-chat files and an index
|
|
212
|
+
- **WHEN** the user runs `gemiterm export-all --output-dir ./exports` against a mediator returning two chats
|
|
213
|
+
- **THEN** exactly two markdown files appear under `./exports` and `./exports/index.md` contains a `## Conversations` section linking to both filenames
|
|
214
|
+
|
|
215
|
+
#### Scenario: Export-all default output directory is ./exports
|
|
216
|
+
- **WHEN** the user runs `gemiterm export-all` with no `--output-dir`
|
|
217
|
+
- **THEN** the output directory is `./exports` (resolved against the current working directory)
|
|
218
|
+
|
|
219
|
+
#### Scenario: Export-all with --all-profiles propagates to mediator
|
|
220
|
+
- **WHEN** the user runs `gemiterm export-all --all-profiles`
|
|
221
|
+
- **THEN** the `ListChatsQuery` payload carries `allProfiles: true`
|
|
222
|
+
|
|
223
|
+
#### Scenario: Export-all with --since filters by date
|
|
224
|
+
- **WHEN** the user runs `gemiterm export-all --since 2024-01-01`
|
|
225
|
+
- **THEN** chats with `timestamp < 2024-01-01` are excluded from the iteration and from the index
|
|
226
|
+
|
|
227
|
+
#### Scenario: Export-all reports failed exports
|
|
228
|
+
- **WHEN** the mediator's `FetchChatQuery` handler throws for one chat
|
|
229
|
+
- **THEN** the failing chat appears in `index.md` under a `## Failed Exports` section and the printed summary contains `Failed: 1` (two spaces before the count, matching `formatReportSummary`)
|
|
230
|
+
|
|
231
|
+
#### Scenario: Export-all shows progress lines
|
|
232
|
+
- **WHEN** the user runs `gemiterm export-all` against a mediator returning N chats
|
|
233
|
+
- **THEN** exactly N progress lines of the form `[i/N]` are written to stdout, each followed by `OK` or `FAILED`
|
|
234
|
+
|
|
235
|
+
#### Scenario: Export-all with no conversations prints the empty message
|
|
236
|
+
- **WHEN** the mediator returns an empty `chats` array
|
|
237
|
+
- **THEN** the output contains `No conversations found to export.` and no `index.md` is written
|
|
238
|
+
|
|
239
|
+
#### Scenario: Export-all --include-metadata adds success/failure counts to the index
|
|
240
|
+
- **WHEN** the user runs `gemiterm export-all --include-metadata`
|
|
241
|
+
- **THEN** `index.md` contains a line `> Successful: <n> | Failed: <n>`
|
|
242
|
+
|
|
243
|
+
#### Scenario: Export-all --help shows usage
|
|
244
|
+
- **WHEN** the user runs `gemiterm export-all --help`
|
|
245
|
+
- **THEN** the output contains `Usage: gemiterm export-all [options]` and documents `--output-dir`, `--since`, `--include-metadata`, `--all-profiles`, and `--help`
|
|
246
|
+
|
|
247
|
+
### Requirement: AuthCommand
|
|
248
|
+
|
|
249
|
+
The system MUST provide an `auth` command implemented by `AuthCommand` in `src/cli/commands/auth-command.ts`. The command MUST be registered under the name `auth` (NOT `login`) and MUST take no positional arguments. The command MUST delegate the actual browser-driven authentication to `AuthService.authenticate(profileName)`. When zero profiles exist, the command MUST create the default profile and authenticate against it. When exactly one profile exists, the command MUST authenticate against that profile directly. When more than one profile exists, the command MUST display a profile management menu using `formatProfileTable` and the options `[A] Add new profile`, `[D] Delete profile`, `[S] Set default`, `[R] Rename profile`, `[X] Exit and continue with current default`. The `A` and `R` options MUST trigger authentication against the resulting profile; `D`, `S`, and `X` MUST NOT trigger authentication. The `D` option MUST require a `[y/N]` confirmation before deletion. The `S` option MUST call both `ProfileManager.setDefault` and `setDefaultProfileName`. Profile names MUST be validated via `validateProfileName`.
|
|
250
|
+
|
|
251
|
+
#### Scenario: Auth with no profiles creates and authenticates the default profile
|
|
252
|
+
- **WHEN** the user runs `gemiterm auth` and no profiles exist
|
|
253
|
+
- **THEN** the default profile is created and `AuthService.authenticate` is invoked against it
|
|
254
|
+
|
|
255
|
+
#### Scenario: Auth with one profile authenticates that profile directly
|
|
256
|
+
- **WHEN** the user runs `gemiterm auth` and exactly one profile exists
|
|
257
|
+
- **THEN** `AuthService.authenticate` is invoked against that profile and no menu is shown
|
|
258
|
+
|
|
259
|
+
#### Scenario: Auth with multiple profiles shows the menu
|
|
260
|
+
- **WHEN** the user runs `gemiterm auth` and more than one profile exists
|
|
261
|
+
- **THEN** the profile management menu is printed and the user is prompted with `Select an option:`
|
|
262
|
+
|
|
263
|
+
#### Scenario: Auth menu option A creates and authenticates
|
|
264
|
+
- **WHEN** the user selects `A` and enters a valid new profile name
|
|
265
|
+
- **THEN** the profile is created and `AuthService.authenticate` is invoked against the new name
|
|
266
|
+
|
|
267
|
+
#### Scenario: Auth menu option D requires confirmation
|
|
268
|
+
- **WHEN** the user selects `D`, enters an existing profile name, and answers the `Delete profile '<name>'? [y/N]` prompt with `y`
|
|
269
|
+
- **THEN** the profile is removed via `ProfileManager.delete`
|
|
270
|
+
|
|
271
|
+
#### Scenario: Auth menu option S sets the default profile
|
|
272
|
+
- **WHEN** the user selects `S` and enters an existing profile name
|
|
273
|
+
- **THEN** `ProfileManager.setDefault(name)` and `setDefaultProfileName(name)` are both called
|
|
274
|
+
|
|
275
|
+
#### Scenario: Auth menu option R renames and authenticates
|
|
276
|
+
- **WHEN** the user selects `R` and enters an existing name and a new valid name
|
|
277
|
+
- **THEN** `ProfileManager.rename(old, new)` is called and `AuthService.authenticate` is invoked against the new name
|
|
278
|
+
|
|
279
|
+
#### Scenario: Auth menu option X exits without authenticating
|
|
280
|
+
- **WHEN** the user selects `X` (or any other unhandled option) in the menu
|
|
281
|
+
- **THEN** no `AuthService.authenticate` is invoked and the output contains `Continuing with current default profile.`
|
|
282
|
+
|
|
283
|
+
#### Scenario: Auth rejects invalid profile names
|
|
284
|
+
- **WHEN** the user enters an invalid profile name in response to an `A` or `R` prompt
|
|
285
|
+
- **THEN** `validateProfileName` throws and the command fails with the validator's error message
|
|
286
|
+
|
|
287
|
+
#### Scenario: Auth --help shows usage
|
|
288
|
+
- **WHEN** the user runs `gemiterm auth --help`
|
|
289
|
+
- **THEN** the output contains `Usage: gemiterm auth` and documents `-h, --help`
|
|
290
|
+
|
|
291
|
+
### Requirement: ProfileCommand
|
|
292
|
+
|
|
293
|
+
The system MUST provide a `profile` command implemented by `ProfileCommand` in `src/cli/commands/profile-command.ts`. The command MUST be a subcommand-style dispatcher with actions `add <name>`, `delete <name>`, `rename <old> <new>`, `default <name>`, and `list`. The first positional argument MUST select the action; the action name MUST be one of the five above, otherwise the command MUST throw a `GemitermError` whose message includes the substring `Unknown action` and lists the valid actions. When no action is provided, the command MUST print usage. The `add` action MUST validate the new name via `validateProfileName`, MUST throw `Profile '<name>' already exists.` if the name is taken, MUST create the profile via `ProfileManager.create`, and MUST then call `AuthService.authenticate(profileName)`. The `delete` action MUST require a `Delete profile '<name>'? [y/N]` confirmation before calling `ProfileManager.delete`. The `rename` action MUST validate the new name, MUST call `ProfileManager.rename(old, new)`, and MUST throw if `new` already exists. The `default` action MUST call both `ProfileManager.setDefault` and `setDefaultProfileName`. The `list` action MUST print `No profiles found.` when the profile list is empty (with a hint to run `gemiterm login`); otherwise it MUST print a profile table via `formatProfileTable` with the `* = default profile` legend.
|
|
294
|
+
|
|
295
|
+
#### Scenario: Profile with no arguments prints usage
|
|
296
|
+
- **WHEN** the user runs `gemiterm profile`
|
|
297
|
+
- **THEN** the output contains `Usage: gemiterm profile <action> [args]` and lists all five actions with descriptions
|
|
298
|
+
|
|
299
|
+
#### Scenario: Profile add creates a profile and authenticates
|
|
300
|
+
- **WHEN** the user runs `gemiterm profile add new-name`
|
|
301
|
+
- **THEN** `ProfileManager.create("new-name")` is called and `AuthService.authenticate("new-name")` is invoked
|
|
302
|
+
|
|
303
|
+
#### Scenario: Profile add rejects duplicate name
|
|
304
|
+
- **WHEN** the user runs `gemiterm profile add existing`
|
|
305
|
+
- **THEN** the command throws `Profile 'existing' already exists.`
|
|
306
|
+
|
|
307
|
+
#### Scenario: Profile delete requires confirmation
|
|
308
|
+
- **WHEN** the user runs `gemiterm profile delete my-profile` and answers `n` to the confirmation prompt
|
|
309
|
+
- **THEN** no `ProfileManager.delete` is called and the output contains `Cancelled.`
|
|
310
|
+
|
|
311
|
+
#### Scenario: Profile rename calls ProfileManager.rename
|
|
312
|
+
- **WHEN** the user runs `gemiterm profile rename old new`
|
|
313
|
+
- **THEN** `ProfileManager.rename("old", "new")` is called
|
|
314
|
+
|
|
315
|
+
#### Scenario: Profile default sets the default profile
|
|
316
|
+
- **WHEN** the user runs `gemiterm profile default work`
|
|
317
|
+
- **THEN** `ProfileManager.setDefault("work")` and `setDefaultProfileName("work")` are both called
|
|
318
|
+
|
|
319
|
+
#### Scenario: Profile list prints the profile table
|
|
320
|
+
- **WHEN** the user runs `gemiterm profile list` and profiles exist
|
|
321
|
+
- **THEN** the output contains `Profiles` and a profile table produced by `formatProfileTable`
|
|
322
|
+
|
|
323
|
+
#### Scenario: Profile list with no profiles prints the empty message
|
|
324
|
+
- **WHEN** the user runs `gemiterm profile list` and no profiles exist
|
|
325
|
+
- **THEN** the output contains `No profiles found.`
|
|
326
|
+
|
|
327
|
+
#### Scenario: Profile with unknown action throws
|
|
328
|
+
- **WHEN** the user runs `gemiterm profile bogus`
|
|
329
|
+
- **THEN** the command throws a `GemitermError` whose message contains `Unknown action 'bogus'`
|
|
330
|
+
|
|
331
|
+
#### Scenario: Profile --help shows usage
|
|
332
|
+
- **WHEN** the user runs `gemiterm profile --help`
|
|
333
|
+
- **THEN** the output contains `Usage: gemiterm profile <action> [args]` and documents all five actions
|
|
334
|
+
|
|
335
|
+
### Requirement: StatusCommand
|
|
336
|
+
|
|
337
|
+
The system MUST provide a `status` command implemented by `StatusCommand` in `src/cli/commands/status-command.ts`. The command MUST take no arguments (other than `--help/-h`). The command MUST call `ensureConfigDir()`, MUST print a `Configuration` section containing `Directory: <configDir>` (the value from `getConfigDir()`), and MUST then print a `Profiles` section using `formatProfileTable`. When no profiles exist, the command MUST print `No profiles found. Run 'gemiterm login' to create one.` and MUST exit with code 2. When profiles exist, the command MUST additionally log a status line with the count of active profiles via `Logger.info`.
|
|
338
|
+
|
|
339
|
+
#### Scenario: Status with profiles shows the directory and the profile table
|
|
340
|
+
- **WHEN** the user runs `gemiterm status` and at least one profile exists
|
|
341
|
+
- **THEN** the output contains `Configuration`, `Directory: <configDir>`, `Profiles`, and a profile table with columns `NAME`, `ACTIVE`, `EXPIRES`, `DEFAULT`
|
|
342
|
+
|
|
343
|
+
#### Scenario: Status with no profiles exits with code 2
|
|
344
|
+
- **WHEN** the user runs `gemiterm status` and no profiles exist
|
|
345
|
+
- **THEN** the output contains `No profiles found.` and the process exits with code 2
|
|
346
|
+
|
|
347
|
+
#### Scenario: Status --help shows usage
|
|
348
|
+
- **WHEN** the user runs `gemiterm status --help`
|
|
349
|
+
- **THEN** the output contains `Usage: gemiterm status` and documents `-h, --help`
|
|
350
|
+
|
|
351
|
+
### Requirement: InstallBrowserCommand
|
|
352
|
+
|
|
353
|
+
The system MUST provide an `install-browser` command implemented by `InstallBrowserCommand` in `src/cli/commands/install-browser-command.ts`. The command MUST be marked as hidden (its description starts with "Install Chromium browser for Playwright (hidden command)"). The command MUST print `Checking browser installation...` (dim) before invoking `InstallBrowserService.install()`. On success the command MUST print `Browser ready.` (green). On `InstallBrowserError` the command MUST log the error and cause, MUST print `Failed to install browser.` (red) and the hint `You may need to run: bunx @playwright/cli install chromium` (dim), and MUST exit with code 1.
|
|
354
|
+
|
|
355
|
+
#### Scenario: Install-browser prints the success lines on a clean install
|
|
356
|
+
- **WHEN** the user runs `gemiterm install-browser` and `InstallBrowserService.install()` resolves
|
|
357
|
+
- **THEN** the output contains `Checking browser installation...` and `Browser ready.`
|
|
358
|
+
|
|
359
|
+
#### Scenario: Install-browser exits 1 on InstallBrowserError
|
|
360
|
+
- **WHEN** the user runs `gemiterm install-browser` and `InstallBrowserService.install()` throws an `InstallBrowserError`
|
|
361
|
+
- **THEN** the output contains `Failed to install browser.` and the hint about `bunx @playwright/cli install chromium`, and the process exits with code 1
|
|
362
|
+
|
|
363
|
+
### Requirement: CommandRegistry
|
|
364
|
+
|
|
365
|
+
The system MUST provide a `CommandRegistry` class in `src/cli/command-registry.ts` that stores `CliCommand` instances keyed by command name. The `register(name, handler)` method MUST throw `Command already registered: <name>` when the same name is registered twice. The `getHandler(name)` method MUST return the handler for the name or `undefined` if not present. The `has(name)` method MUST return a boolean. The `getRegisteredNames()` method MUST return an array of all registered names. The `registerAllCommands()` method MUST register all 11 commands by name: `auth`, `profile`, `status`, `list`, `fetch`, `continue`, `new`, `delete`, `export`, `export-all`, and `install-browser`. The `CliCommandContext` interface MUST carry `{ verbose: boolean, mediator: Mediator, profileAuthManager: ProfileAuthManager }`. The `CliCommand` interface MUST require `name: string`, `description: string`, and `execute(args, context): Promise<void>`.
|
|
366
|
+
|
|
367
|
+
#### Scenario: Registering the same name twice throws
|
|
368
|
+
- **WHEN** `register("dup", handlerA)` is called and then `register("dup", handlerB)`
|
|
369
|
+
- **THEN** the second call throws `Command already registered: dup`
|
|
370
|
+
|
|
371
|
+
#### Scenario: getHandler returns the registered handler
|
|
372
|
+
- **WHEN** `register("list", handler)` is called
|
|
373
|
+
- **THEN** `getHandler("list")` returns the same handler instance
|
|
374
|
+
|
|
375
|
+
#### Scenario: getHandler returns undefined for unknown names
|
|
376
|
+
- **WHEN** no handler is registered for `nope`
|
|
377
|
+
- **THEN** `getHandler("nope")` returns `undefined` and `has("nope")` returns `false`
|
|
378
|
+
|
|
379
|
+
#### Scenario: registerAllCommands registers all 11 commands
|
|
380
|
+
- **WHEN** `registerAllCommands()` is called
|
|
381
|
+
- **THEN** `getRegisteredNames()` returns an array that includes `auth`, `profile`, `status`, `list`, `fetch`, `continue`, `new`, `delete`, `export`, `export-all`, and `install-browser` (11 entries total)
|
|
382
|
+
|
|
383
|
+
### Requirement: Command Help Output
|
|
384
|
+
|
|
385
|
+
Every command in the registry MUST support `--help` and `-h`. When `--help` or `-h` is supplied, the command MUST print a usage block starting with `Usage: gemiterm <command> ...` and MUST NOT perform its primary mediator action. Each command's usage block MUST list that command's flags and positional arguments.
|
|
386
|
+
|
|
387
|
+
#### Scenario: Every command has a --help that starts with Usage
|
|
388
|
+
- **WHEN** any of `gemiterm <cmd> --help` or `gemiterm <cmd> -h` is invoked for `cmd` in `{list, fetch, continue, new, delete, export, export-all, status, auth, profile}`
|
|
389
|
+
- **THEN** the first line of the output is `Usage: gemiterm <cmd> ...`
|
|
390
|
+
|
|
391
|
+
#### Scenario: --help does not perform the command's primary action
|
|
392
|
+
- **WHEN** `gemiterm <cmd> --help` is invoked
|
|
393
|
+
- **THEN** the command's primary mediator action is not executed (no `ListChatsQuery`, `FetchChatQuery`, `SendMessageCommand`, `StartNewChatCommand`, or `DeleteConversationCommand` is sent)
|
|
394
|
+
|
|
395
|
+
### Requirement: Global Help and Version
|
|
396
|
+
|
|
397
|
+
The system MUST print a global help block via `showHelp(registry)` in `src/cli/commands/help.ts` when `gemiterm` is invoked with no arguments, when `gemiterm --help` or `gemiterm -h` is invoked, or when an unknown subcommand is supplied. The global help block MUST list the `Commands:` section using the names from the registry, the `Global Options:` section (`--version`, `--help, -h`, `--verbose, -v`), and a footer hinting at `gemiterm <command> --help`. When `--version` is supplied the system MUST print `gemiterm v<version>` (where `<version>` is the value of `package.json:version`) and exit with code 0. When an unknown subcommand is supplied, the system MUST print `Unknown command: '<name>'` to stderr and MUST additionally print a `Did you mean one of: <list>?` hint when the registry has registered commands, otherwise a `Run 'gemiterm --help' for available commands.` hint, and the process MUST exit with code 1.
|
|
398
|
+
|
|
399
|
+
#### Scenario: No arguments prints global help
|
|
400
|
+
- **WHEN** the user runs `gemiterm` with no arguments
|
|
401
|
+
- **THEN** the output contains `GemiTerm - Google Gemini Terminal Client`, a `Commands:` section, and a `Global Options:` section; the process exits with code 0
|
|
402
|
+
|
|
403
|
+
#### Scenario: --version prints the package version
|
|
404
|
+
- **WHEN** the user runs `gemiterm --version`
|
|
405
|
+
- **THEN** the output is `gemiterm v<package.json:version>` and the process exits with code 0
|
|
406
|
+
|
|
407
|
+
#### Scenario: Unknown subcommand prints an error and a hint
|
|
408
|
+
- **WHEN** the user runs `gemiterm bogus` and the registry has registered commands
|
|
409
|
+
- **THEN** stderr contains `Unknown command: 'bogus'` and a `Did you mean one of:` line listing the registered names, and the process exits with code 1
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
## Purpose
|
|
2
|
+
|
|
3
|
+
The configuration subsystem resolves the on-disk config directory, manages the default-profile marker file, lists and mutates profile directories, and ensures the config tree exists. It encapsulates the platform-specific location of the user's gemiterm data and the `profiles/<name>/storage_state.json` layout used by storage.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
### Requirement: Config Directory Resolution With Env Override
|
|
8
|
+
The system MUST resolve the config directory by checking the `GEMITERM_CONFIG_DIR` environment variable first. If that variable is set to a non-empty string, the config directory MUST be exactly that string. If the env var is unset or empty, the system MUST fall back to a platform default.
|
|
9
|
+
|
|
10
|
+
#### Scenario: Env override takes precedence
|
|
11
|
+
- **WHEN** `GEMITERM_CONFIG_DIR` is set to `/custom/gemiterm`
|
|
12
|
+
- **THEN** `getConfigDir()` returns `/custom/gemiterm` regardless of the current platform
|
|
13
|
+
|
|
14
|
+
#### Scenario: Env var empty falls back to platform default
|
|
15
|
+
- **WHEN** `GEMITERM_CONFIG_DIR` is unset or empty
|
|
16
|
+
- **THEN** `getConfigDir()` returns the platform default
|
|
17
|
+
|
|
18
|
+
### Requirement: Windows Config Path
|
|
19
|
+
On Windows (`process.platform === "win32"`), when the env override is not set, the system MUST use `%APPDATA%\gemiterm` (i.e. the value of the `APPDATA` environment variable joined with `"gemiterm"`).
|
|
20
|
+
|
|
21
|
+
#### Scenario: Windows with APPDATA
|
|
22
|
+
- **WHEN** `process.platform === "win32"` and `APPDATA` is set to `C:\Users\test\AppData\Roaming`
|
|
23
|
+
- **THEN** `getConfigDir()` returns `C:\Users\test\AppData\Roaming\gemiterm`
|
|
24
|
+
|
|
25
|
+
#### Scenario: Windows without APPDATA falls back to homedir
|
|
26
|
+
- **WHEN** `process.platform === "win32"` and `APPDATA` is not set
|
|
27
|
+
- **THEN** `getConfigDir()` returns `<homedir>/gemiterm` (the Linux/macOS-style default)
|
|
28
|
+
|
|
29
|
+
### Requirement: Linux and macOS Config Path
|
|
30
|
+
On Linux and macOS (`process.platform` is `"linux"` or `"darwin"`), when the env override is not set, the system MUST use `~/gemiterm` (i.e. `<homedir>/gemiterm`; no `.config` intermediary).
|
|
31
|
+
|
|
32
|
+
#### Scenario: Linux platform
|
|
33
|
+
- **WHEN** `process.platform === "linux"` and the env override is not set
|
|
34
|
+
- **THEN** `getConfigDir()` returns `<homedir>/gemiterm`
|
|
35
|
+
|
|
36
|
+
#### Scenario: macOS platform
|
|
37
|
+
- **WHEN** `process.platform === "darwin"` and the env override is not set
|
|
38
|
+
- **THEN** `getConfigDir()` returns `<homedir>/gemiterm`
|
|
39
|
+
|
|
40
|
+
### Requirement: Profiles Subdirectory Layout
|
|
41
|
+
The system MUST expose a `getProfilesDir()` function that returns `<configDir>/profiles`. The profiles subdirectory is the parent of every per-profile directory.
|
|
42
|
+
|
|
43
|
+
#### Scenario: Profiles dir under config
|
|
44
|
+
- **WHEN** the config dir is `/x/gemiterm`
|
|
45
|
+
- **THEN** `getProfilesDir()` returns `/x/gemiterm/profiles`
|
|
46
|
+
|
|
47
|
+
### Requirement: Storage State File Path
|
|
48
|
+
The system MUST expose a `getProfilePath(name)` function that returns `<profilesDir>/<name>/storage_state.json`. The filename `storage_state.json` is the canonical file inside each profile directory that holds the cookie JSON.
|
|
49
|
+
|
|
50
|
+
#### Scenario: Profile path for a single profile
|
|
51
|
+
- **WHEN** the config dir is `/x/gemiterm`
|
|
52
|
+
- **THEN** `getProfilePath("work")` returns `/x/gemiterm/profiles/work/storage_state.json`
|
|
53
|
+
|
|
54
|
+
### Requirement: Default Profile Marker File
|
|
55
|
+
The system MUST use the file name `.default` (the value of the exported `DEFAULT_PROFILE_MARKER` constant) inside the profiles directory to record the current default profile name. The full marker path is `<profilesDir>/.default` and is exposed via `getDefaultProfileMarkerPath()`. The file content MUST be the default profile name as a plain text string.
|
|
56
|
+
|
|
57
|
+
#### Scenario: Marker path
|
|
58
|
+
- **WHEN** the config dir is `/x/gemiterm`
|
|
59
|
+
- **THEN** `getDefaultProfileMarkerPath()` returns `/x/gemiterm/profiles/.default`
|
|
60
|
+
|
|
61
|
+
### Requirement: Default Profile Name Resolution
|
|
62
|
+
The system MUST expose a `getDefaultProfileName()` function that returns the contents of the marker file (trimmed) when the file exists, and MUST return the literal string `"default"` when the marker file does not exist.
|
|
63
|
+
|
|
64
|
+
#### Scenario: No marker file returns 'default'
|
|
65
|
+
- **WHEN** the marker file does not exist on disk
|
|
66
|
+
- **THEN** `getDefaultProfileName()` returns `"default"`
|
|
67
|
+
|
|
68
|
+
#### Scenario: Marker file present returns its content
|
|
69
|
+
- **WHEN** the marker file contains the trimmed text `work`
|
|
70
|
+
- **THEN** `getDefaultProfileName()` returns `"work"`
|
|
71
|
+
|
|
72
|
+
#### Scenario: Marker file whitespace is trimmed
|
|
73
|
+
- **WHEN** the marker file contains `" my-profile \n"`
|
|
74
|
+
- **THEN** `getDefaultProfileName()` returns `"my-profile"`
|
|
75
|
+
|
|
76
|
+
### Requirement: Set Default Profile Name
|
|
77
|
+
The system MUST expose a `setDefaultProfileName(name)` function that creates the profiles directory if missing and writes the supplied `name` to the marker file, overwriting any prior value.
|
|
78
|
+
|
|
79
|
+
#### Scenario: Set a new default
|
|
80
|
+
- **WHEN** `setDefaultProfileName("personal")` is called
|
|
81
|
+
- **THEN** the marker file exists at `<profilesDir>/.default` and its content is the string `"personal"`
|
|
82
|
+
|
|
83
|
+
#### Scenario: Set creates the profiles directory
|
|
84
|
+
- **WHEN** `setDefaultProfileName("work")` is called and `<configDir>/profiles` does not exist
|
|
85
|
+
- **THEN** the function creates the profiles directory as a side effect
|
|
86
|
+
|
|
87
|
+
#### Scenario: Set overwrites the previous default
|
|
88
|
+
- **WHEN** the marker already contains `"first"` and `setDefaultProfileName("second")` is called
|
|
89
|
+
- **THEN** `getDefaultProfileName()` returns `"second"`
|
|
90
|
+
|
|
91
|
+
### Requirement: List Profiles
|
|
92
|
+
The system MUST expose a `listProfiles()` function that returns the names of all profile directories in the profiles directory, sorted alphabetically. The function MUST exclude the `.default` marker file and MUST exclude any non-directory entry. If the profiles directory does not exist, the function MUST return an empty array.
|
|
93
|
+
|
|
94
|
+
#### Scenario: Empty when profiles dir is missing
|
|
95
|
+
- **WHEN** the profiles directory does not exist
|
|
96
|
+
- **THEN** `listProfiles()` returns `[]`
|
|
97
|
+
|
|
98
|
+
#### Scenario: Empty when profiles dir is empty
|
|
99
|
+
- **WHEN** the profiles directory exists but is empty
|
|
100
|
+
- **THEN** `listProfiles()` returns `[]`
|
|
101
|
+
|
|
102
|
+
#### Scenario: Sorted list of profile directories
|
|
103
|
+
- **WHEN** the profiles directory contains subdirectories `charlie`, `alpha`, and `bravo`
|
|
104
|
+
- **THEN** `listProfiles()` returns `["alpha", "bravo", "charlie"]`
|
|
105
|
+
|
|
106
|
+
#### Scenario: Excludes the .default marker
|
|
107
|
+
- **WHEN** the profiles directory contains `work`, `personal`, and a `.default` file
|
|
108
|
+
- **THEN** `listProfiles()` returns `["personal", "work"]`
|
|
109
|
+
|
|
110
|
+
#### Scenario: Excludes non-directory entries
|
|
111
|
+
- **WHEN** the profiles directory contains `valid-profile` (directory) and `not-a-profile.txt` (file)
|
|
112
|
+
- **THEN** `listProfiles()` returns `["valid-profile"]`
|
|
113
|
+
|
|
114
|
+
### Requirement: Ensure Config Directory
|
|
115
|
+
The system MUST expose an `ensureConfigDir()` function that creates the config directory and the profiles subdirectory (both with `recursive: true`) and returns the config directory path. The function MUST be idempotent — calling it when the directories already exist MUST NOT throw.
|
|
116
|
+
|
|
117
|
+
#### Scenario: Creates both directories
|
|
118
|
+
- **WHEN** neither the config dir nor the profiles dir exist
|
|
119
|
+
- **THEN** calling `ensureConfigDir()` creates both and returns the config directory path
|
|
120
|
+
|
|
121
|
+
#### Scenario: Idempotent
|
|
122
|
+
- **WHEN** the config dir and profiles dir already exist
|
|
123
|
+
- **THEN** calling `ensureConfigDir()` does not throw and returns the config directory path
|
|
124
|
+
|
|
125
|
+
### Requirement: Exported Configuration API Surface
|
|
126
|
+
The system MUST export the following functions from `src/infrastructure/config.ts`: `getConfigDir`, `getProfilesDir`, `getProfilePath`, `getDefaultProfileName`, `setDefaultProfileName`, `listProfiles`, and `ensureConfigDir`. These functions form the complete public surface for profile marker management and config directory handling at this layer.
|
|
127
|
+
|
|
128
|
+
#### Scenario: All functions importable
|
|
129
|
+
- **WHEN** a consumer imports any of the seven exported function names from `src/infrastructure/config.ts`
|
|
130
|
+
- **THEN** the import resolves to a callable function
|