gemiterm 2.0.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -9
- package/package.json +13 -4
- package/src/cli/command-registry.ts +66 -62
- package/src/cli/commands/continue-command.ts +75 -1
- package/src/cli/commands/install-skills-command.ts +35 -0
- package/src/cli/commands/new-command.ts +62 -0
- package/src/cli/index.ts +19 -44
- package/src/cli/utils/long-arg-guard.ts +28 -0
- package/src/cli/utils/prompt-file.ts +26 -0
- package/src/infrastructure/cli-parser.ts +77 -0
- package/src/infrastructure/io.ts +9 -0
- package/src/services/skills-cli-driver.ts +58 -0
- package/src/services/skills-service.ts +52 -0
- package/.env +0 -2
- package/.github/workflows/manual-release.yml +0 -34
- package/.github/workflows/release.yml +0 -198
- package/.github/workflows/test.yml +0 -29
- package/.opencode/commands/opsx-apply.md +0 -152
- package/.opencode/commands/opsx-archive.md +0 -157
- package/.opencode/commands/opsx-explore.md +0 -169
- package/.opencode/commands/opsx-propose.md +0 -104
- package/.opencode/commands/opsx-sync.md +0 -140
- package/.opencode/commands/search-intelligently.md +0 -9
- package/.opencode/opencode.json +0 -20
- package/.opencode/skills/intellisearch/.version +0 -1
- package/.opencode/skills/intellisearch/SKILL.md +0 -84
- package/.opencode/skills/intellisearch/references/brave-search.md +0 -67
- package/.opencode/skills/intellisearch/references/ddg-search.md +0 -79
- package/.opencode/skills/intellisearch/references/deepwiki-tools.md +0 -246
- package/.opencode/skills/intellisearch/references/examples.md +0 -97
- package/.opencode/skills/intellisearch/references/gh-cli.md +0 -143
- package/.opencode/skills/intellisearch/references/google-search.md +0 -40
- package/.opencode/skills/intellisearch/references/search-workflow.md +0 -143
- package/.opencode/skills/intellisearch/references/workflow.md +0 -115
- package/.opencode/skills/openspec-apply-change/SKILL.md +0 -159
- package/.opencode/skills/openspec-archive-change/SKILL.md +0 -117
- package/.opencode/skills/openspec-explore/SKILL.md +0 -287
- package/.opencode/skills/openspec-propose/SKILL.md +0 -111
- package/.opencode/skills/openspec-sync-specs/SKILL.md +0 -147
- package/.serena/project.yml +0 -133
- package/AGENTS.md +0 -136
- package/CHANGELOG.md +0 -3
- package/bun.lock +0 -105
- package/docs/INSTALL.md +0 -185
- package/docs/PLAYWRIGHT_CLI_API.md +0 -306
- package/docs/python-release-for-reference.md +0 -175
- package/examples/sample_json_export.json +0 -32
- package/examples/sample_markdown_export.md +0 -221
- package/examples/usage_examples.sh +0 -222
- package/install.ps1 +0 -264
- package/install.sh +0 -275
- package/openspec/MAESTRO_MIGRATION.md +0 -180
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/.openspec.yaml +0 -2
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/design.md +0 -194
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/proposal.md +0 -52
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/specs/multi-profile-conversations/spec.md +0 -133
- package/openspec/changes/archive/2026-06-08-command-spec-conformance/tasks.md +0 -95
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/.openspec.yaml +0 -2
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/design.md +0 -28
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/proposal.md +0 -22
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/specs/interactive-prompt-loop/spec.md +0 -35
- package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/tasks.md +0 -28
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/.openspec.yaml +0 -2
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/design.md +0 -323
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/proposal.md +0 -122
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/specs/no-capability-changes/spec.md +0 -29
- package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/tasks.md +0 -172
- package/openspec/changes/archive/2026-06-09-v2-install-migration/.openspec.yaml +0 -2
- package/openspec/changes/archive/2026-06-09-v2-install-migration/design.md +0 -241
- package/openspec/changes/archive/2026-06-09-v2-install-migration/proposal.md +0 -41
- package/openspec/changes/archive/2026-06-09-v2-install-migration/specs/v2-installer/spec.md +0 -205
- package/openspec/changes/archive/2026-06-09-v2-install-migration/tasks.md +0 -97
- package/openspec/changes/commander-cli-parser/.openspec.yaml +0 -2
- package/openspec/changes/commander-cli-parser/design.md +0 -107
- package/openspec/changes/commander-cli-parser/proposal.md +0 -31
- package/openspec/changes/commander-cli-parser/specs/cli/spec.md +0 -48
- package/openspec/changes/commander-cli-parser/tasks.md +0 -33
- package/openspec/changes/cross-platform-build-and-ci/.openspec.yaml +0 -2
- package/openspec/changes/cross-platform-build-and-ci/design.md +0 -213
- package/openspec/changes/cross-platform-build-and-ci/proposal.md +0 -50
- package/openspec/changes/cross-platform-build-and-ci/specs/path-and-file-mediation/spec.md +0 -335
- package/openspec/changes/cross-platform-build-and-ci/specs/release-pipeline/spec.md +0 -95
- package/openspec/changes/cross-platform-build-and-ci/tasks.md +0 -164
- package/openspec/config.yaml +0 -20
- package/openspec/specs/auth/spec.md +0 -394
- package/openspec/specs/cli/spec.md +0 -108
- package/openspec/specs/commands/spec.md +0 -409
- package/openspec/specs/configuration/spec.md +0 -130
- package/openspec/specs/conversations/spec.md +0 -139
- package/openspec/specs/domain-model/spec.md +0 -128
- package/openspec/specs/formatters/spec.md +0 -158
- package/openspec/specs/install-browser/spec.md +0 -146
- package/openspec/specs/interactive-prompt-loop/spec.md +0 -41
- package/openspec/specs/logger/spec.md +0 -71
- package/openspec/specs/mediator/spec.md +0 -72
- package/openspec/specs/multi-profile-conversations/spec.md +0 -145
- package/openspec/specs/path-utils/spec.md +0 -101
- package/openspec/specs/profiles/spec.md +0 -94
- package/openspec/specs/storage/spec.md +0 -198
- package/openspec/specs/testing/spec.md +0 -237
- package/openspec/specs/v2-installer/spec.md +0 -205
- package/openspec/specs/validators/spec.md +0 -65
- package/scripts/build.ts +0 -88
- package/scripts/clean-build.ps1 +0 -4
- package/scripts/clean-build.sh +0 -5
- package/scripts/install-browser.ps1 +0 -5
- package/scripts/install-browser.sh +0 -6
- package/scripts/lint-path-mediation.ps1 +0 -65
- package/scripts/lint-path-mediation.sh +0 -40
- package/tests/cli/auth-command.test.ts +0 -220
- package/tests/cli/command-registry.test.ts +0 -67
- package/tests/cli/continue-command.test.ts +0 -100
- package/tests/cli/delete-command.test.ts +0 -128
- package/tests/cli/export-all-command.test.ts +0 -375
- package/tests/cli/install-browser-command.test.ts +0 -48
- package/tests/cli/list-command.test.ts +0 -179
- package/tests/cli/new-command.test.ts +0 -139
- package/tests/cli/status-command.test.ts +0 -106
- package/tests/core/query-handlers.test.ts +0 -187
- package/tests/fixtures/auth-fixtures.ts +0 -98
- package/tests/fixtures/chat-fixtures.ts +0 -87
- package/tests/infrastructure/cli-table.test.ts +0 -107
- package/tests/infrastructure/formatters.test.ts +0 -202
- package/tests/infrastructure/io.test.ts +0 -197
- package/tests/infrastructure/logger.test.ts +0 -74
- package/tests/infrastructure/storage.test.ts +0 -335
- package/tests/infrastructure/validators.test.ts +0 -67
- package/tests/integration/commands/auth.test.ts +0 -186
- package/tests/integration/commands/continue.test.ts +0 -130
- package/tests/integration/commands/delete.test.ts +0 -140
- package/tests/integration/commands/export.test.ts +0 -266
- package/tests/integration/commands/fetch.test.ts +0 -238
- package/tests/integration/commands/list.test.ts +0 -281
- package/tests/integration/commands/profile.test.ts +0 -258
- package/tests/integration/commands/status.test.ts +0 -223
- package/tests/integration/installer-script-shape.test.ts +0 -67
- package/tests/parity/compare-outputs.ts +0 -359
- package/tests/parity/test-commands-parity.ps1 +0 -82
- package/tests/parity/test-commands-parity.sh +0 -61
- package/tests/services/auth-service.test.ts +0 -315
- package/tests/services/cookie-monitor.test.ts +0 -229
- package/tests/services/cookie-storage-service.test.ts +0 -230
- package/tests/services/gemini-client-wrapper.test.ts +0 -688
- package/tests/services/install-browser-service.test.ts +0 -83
- package/tests/services/playwright-cli-driver.test.ts +0 -468
- package/tests/services/profile-auth-manager.test.ts +0 -334
- package/tests/services/profile-service.test.ts +0 -224
- package/tests/setup.ts +0 -95
- package/tests/smoke/smoke.test.ts +0 -52
- package/tests/unit/config.test.ts +0 -159
- package/tests/unit/errors.test.ts +0 -119
- package/tests/unit/formatters.test.ts +0 -316
- package/tests/unit/mediator.test.ts +0 -152
- package/tests/unit/path-utils.test.ts +0 -289
- package/tsconfig.json +0 -16
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
## Purpose
|
|
2
|
-
|
|
3
|
-
The application's logging facility. It provides a single `Logger` class that emits timestamped, leveled, module-tagged log lines to `process.stderr`. Verbosity is controlled globally via a static flag, and each `Logger` instance carries a per-instance module tag that is rendered into every line it produces.
|
|
4
|
-
|
|
5
|
-
## Requirements
|
|
6
|
-
|
|
7
|
-
### Requirement: Logger Class With Leveled Methods
|
|
8
|
-
The system MUST export a `Logger` class with four instance methods: `debug(message, ...args)`, `info(message, ...args)`, `warn(message, ...args)`, and `error(message, ...args)`. Each method MUST format the supplied message (and any trailing args) into a single line on `process.stderr` with a level tag of `DEBUG`, `INFO`, `WARN`, or `ERROR` respectively.
|
|
9
|
-
|
|
10
|
-
#### Scenario: info writes an INFO line
|
|
11
|
-
- **WHEN** a `Logger` instance has `info("hello world")` called
|
|
12
|
-
- **THEN** `process.stderr.write` is invoked once with a string containing `[INFO]` and the message
|
|
13
|
-
|
|
14
|
-
#### Scenario: warn writes a WARN line
|
|
15
|
-
- **WHEN** `warn("something happened")` is called
|
|
16
|
-
- **THEN** the emitted line contains `[WARN]`
|
|
17
|
-
|
|
18
|
-
#### Scenario: error writes an ERROR line
|
|
19
|
-
- **WHEN** `error("failed")` is called
|
|
20
|
-
- **THEN** the emitted line contains `[ERROR]`
|
|
21
|
-
|
|
22
|
-
### Requirement: Verbose Toggle For Debug Output
|
|
23
|
-
The `Logger` class MUST expose a static `setVerbose(enabled: boolean)` method that sets a process-global verbose flag, and a static `isVerbose(): boolean` method that returns the current flag value. When the flag is `false` (the default), `debug()` MUST NOT write anything. When the flag is `true`, `debug()` MUST write a `DEBUG` line identically to the other levels.
|
|
24
|
-
|
|
25
|
-
#### Scenario: debug is silent by default
|
|
26
|
-
- **WHEN** `setVerbose(false)` is the current state
|
|
27
|
-
- **THEN** calling `debug("msg")` does not invoke `process.stderr.write`
|
|
28
|
-
|
|
29
|
-
#### Scenario: setVerbose(true) enables debug
|
|
30
|
-
- **WHEN** `setVerbose(true)` is called and then `debug("verbose message")` is invoked
|
|
31
|
-
- **THEN** `process.stderr.write` is invoked once with a string containing `[DEBUG]`
|
|
32
|
-
|
|
33
|
-
#### Scenario: isVerbose reflects current state
|
|
34
|
-
- **WHEN** the verbose flag is toggled
|
|
35
|
-
- **THEN** `isVerbose()` returns the new value
|
|
36
|
-
|
|
37
|
-
### Requirement: Standard Log Line Format
|
|
38
|
-
Every emitted log line MUST have the exact structure `[<ISO_TIMESTAMP>] [<LEVEL>] [<module>] <message>`, where `<ISO_TIMESTAMP>` is the result of `new Date().toISOString()`, `<LEVEL>` is one of `DEBUG`, `INFO`, `WARN`, `ERROR`, and `<module>` is the per-instance tag supplied at construction time. The line MUST end with a single newline character.
|
|
39
|
-
|
|
40
|
-
#### Scenario: ISO timestamp prefix
|
|
41
|
-
- **WHEN** any log method is called
|
|
42
|
-
- **THEN** the emitted line starts with `[` followed by an ISO-8601 timestamp that matches `YYYY-MM-DDTHH:MM:SS.sssZ`
|
|
43
|
-
|
|
44
|
-
#### Scenario: Module tag in the line
|
|
45
|
-
- **WHEN** a `Logger` is constructed with module tag `"test-module"` and `info("hi")` is called
|
|
46
|
-
- **THEN** the emitted line contains `[test-module]`
|
|
47
|
-
|
|
48
|
-
### Requirement: Output To Stderr Only
|
|
49
|
-
The `Logger` MUST write exclusively to `process.stderr` and MUST NOT write to `process.stdout`. This separation guarantees that logger output does not pollute structured stdout payloads (such as JSON command results) that consumers may parse.
|
|
50
|
-
|
|
51
|
-
#### Scenario: Logs do not touch stdout
|
|
52
|
-
- **WHEN** any log method is called
|
|
53
|
-
- **THEN** the only `process.<stream>.write` invocation triggered is on `process.stderr` (no writes to `process.stdout`)
|
|
54
|
-
|
|
55
|
-
### Requirement: Per-Instance Module Tag
|
|
56
|
-
Each `Logger` instance MUST carry a single, immutable `module` tag set via the constructor. The tag MUST appear in every line emitted by that instance. Different `Logger` instances with different module tags MUST produce lines with different module fields.
|
|
57
|
-
|
|
58
|
-
#### Scenario: Two instances, two different module tags
|
|
59
|
-
- **WHEN** `new Logger("cli").info("a")` and `new Logger("auth").info("b")` are both called
|
|
60
|
-
- **THEN** one emitted line contains `[cli]` and the other contains `[auth]`
|
|
61
|
-
|
|
62
|
-
### Requirement: Variadic Args Appended As Space-Separated
|
|
63
|
-
When extra arguments are passed to any log method after the message, the system MUST stringify each argument (via `String(arg)`), join them with single spaces, and append the result to the message portion of the line (after a single space separator). When no extra args are supplied, the message is emitted unchanged.
|
|
64
|
-
|
|
65
|
-
#### Scenario: Extra args appended
|
|
66
|
-
- **WHEN** `info("result", 42, "extra")` is called on a logger with module tag `"svc"`
|
|
67
|
-
- **THEN** the emitted line contains `[svc] result 42 extra`
|
|
68
|
-
|
|
69
|
-
#### Scenario: No extra args
|
|
70
|
-
- **WHEN** `info("plain")` is called
|
|
71
|
-
- **THEN** the emitted line contains the message verbatim and no trailing `undefined` token
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
## Purpose
|
|
2
|
-
|
|
3
|
-
A lightweight, in-process CQRS mediator that decouples request senders (CLI command handlers, services) from request handlers (query and command handlers). The mediator maintains two separate handler maps keyed by message `type`, enforces single-handler-per-type registration, and routes `send()` calls to the correct handler.
|
|
4
|
-
|
|
5
|
-
## Requirements
|
|
6
|
-
|
|
7
|
-
### Requirement: Message Types
|
|
8
|
-
The system MUST export a `Query<TPayload = unknown>` interface and a `Command<TPayload = unknown>` interface. Each interface MUST have a readonly `type: string` field and a readonly `payload: TPayload` field. `Query` and `Command` MUST be structurally distinct interfaces (the same `{ type, payload }` shape can be sent as either, but each handler type is registered separately).
|
|
9
|
-
|
|
10
|
-
#### Scenario: Constructing a query
|
|
11
|
-
- **WHEN** a caller creates `{ type: "GetX", payload: { id: 1 } }` typed as `Query<{ id: number }>`
|
|
12
|
-
- **THEN** the resulting object has `type === "GetX"` and `payload.id === 1`
|
|
13
|
-
|
|
14
|
-
#### Scenario: Constructing a command
|
|
15
|
-
- **WHEN** a caller creates `{ type: "DoX", payload: "data" }` typed as `Command<string>`
|
|
16
|
-
- **THEN** the resulting object has `type === "DoX"` and `payload === "data"`
|
|
17
|
-
|
|
18
|
-
### Requirement: Handler Interfaces
|
|
19
|
-
The system MUST export a `QueryHandler<TPayload, TResult>` interface and a `CommandHandler<TPayload, TResult>` interface. `QueryHandler` MUST expose a readonly `queryType: string` and an async `handle(query: Query<TPayload>): Promise<TResult>` method. `CommandHandler` MUST expose a readonly `commandType: string` and an async `handle(command: Command<TPayload>): Promise<TResult>` method.
|
|
20
|
-
|
|
21
|
-
#### Scenario: Defining a query handler
|
|
22
|
-
- **WHEN** an object is declared as `QueryHandler<string, number>` with `queryType: "Len"` and a `handle` that returns the payload length
|
|
23
|
-
- **THEN** the handler object satisfies the `QueryHandler` shape and is accepted by `registerQueryHandler`
|
|
24
|
-
|
|
25
|
-
#### Scenario: Defining a command handler
|
|
26
|
-
- **WHEN** an object is declared as `CommandHandler<string, string>` with `commandType: "Echo"` and a `handle` that returns the payload
|
|
27
|
-
- **THEN** the handler object satisfies the `CommandHandler` shape and is accepted by `registerCommandHandler`
|
|
28
|
-
|
|
29
|
-
### Requirement: Mediator Handler Registration
|
|
30
|
-
The `Mediator` class MUST provide `registerQueryHandler(handler)` and `registerCommandHandler(handler)` methods. Each method MUST store the handler in a separate internal map keyed by `queryType` / `commandType`. If a handler is already registered for the same `type`, the registration MUST throw an `Error` whose message names the duplicate type.
|
|
31
|
-
|
|
32
|
-
#### Scenario: Registering a query handler
|
|
33
|
-
- **WHEN** `registerQueryHandler(handler)` is called with `handler.queryType === "GetLength"`
|
|
34
|
-
- **THEN** the mediator stores the handler and a subsequent `send` of `{ type: "GetLength", payload: ... }` resolves through this handler
|
|
35
|
-
|
|
36
|
-
#### Scenario: Duplicate query registration throws
|
|
37
|
-
- **WHEN** two `QueryHandler` objects with the same `queryType` are both passed to `registerQueryHandler`
|
|
38
|
-
- **THEN** the second call throws an error whose message is `Query handler already registered for type: <type>`
|
|
39
|
-
|
|
40
|
-
#### Scenario: Duplicate command registration throws
|
|
41
|
-
- **WHEN** two `CommandHandler` objects with the same `commandType` are both passed to `registerCommandHandler`
|
|
42
|
-
- **THEN** the second call throws an error whose message is `Command handler already registered for type: <type>`
|
|
43
|
-
|
|
44
|
-
### Requirement: Send Dispatches by Type
|
|
45
|
-
`Mediator.send<TResult>(message)` MUST look up the registered handler whose `type` matches `message.type` (query map first, then command map) and MUST invoke that handler's `handle()` method with the full `message` object. The returned promise MUST resolve to the handler's result. If no handler is registered for the given `type`, `send()` MUST reject with an `Error` whose message is `No handler registered for message type: <type>`.
|
|
46
|
-
|
|
47
|
-
#### Scenario: Query send dispatches to query handler
|
|
48
|
-
- **WHEN** a query handler for `"GetLength"` returns `payload.length` and `send({ type: "GetLength", payload: "hello" })` is called
|
|
49
|
-
- **THEN** the returned promise resolves to `5`
|
|
50
|
-
|
|
51
|
-
#### Scenario: Command send dispatches to command handler
|
|
52
|
-
- **WHEN** a command handler for `"Echo"` returns its payload and `send({ type: "Echo", payload: "hi" })` is called
|
|
53
|
-
- **THEN** the returned promise resolves to `"hi"`
|
|
54
|
-
|
|
55
|
-
#### Scenario: Send with no registered handler rejects
|
|
56
|
-
- **WHEN** `send({ type: "Nobody", payload: null })` is called on a mediator with no handler for that type
|
|
57
|
-
- **THEN** the returned promise rejects with an `Error` whose message is `No handler registered for message type: Nobody`
|
|
58
|
-
|
|
59
|
-
#### Scenario: Full message object is forwarded to the handler
|
|
60
|
-
- **WHEN** a query handler captures the `query` argument and `send({ type: "Inspect", payload: { key: "val" } })` is called
|
|
61
|
-
- **THEN** the handler observes `received.type === "Inspect"` and `received.payload === { key: "val" }`
|
|
62
|
-
|
|
63
|
-
### Requirement: Query and Command Maps Are Independent
|
|
64
|
-
The mediator MUST maintain two separate internal maps: one for query handlers, one for command handlers. A handler registered as a query MUST NOT satisfy a `send()` call for a type that is only registered as a command, and vice versa.
|
|
65
|
-
|
|
66
|
-
#### Scenario: Query handler is not reachable via command map
|
|
67
|
-
- **WHEN** a query handler is registered for `"Q"` and no command handler is registered for `"Q"`
|
|
68
|
-
- **THEN** `send({ type: "Q", payload: undefined })` is dispatched to the query handler and resolves with the query's result (the mediator also accepts the call as falling through to the command map, but the query handler — being first — handles it)
|
|
69
|
-
|
|
70
|
-
#### Scenario: Command handler is not visible to query registration
|
|
71
|
-
- **WHEN** a command handler is registered for `"C"` on a fresh mediator and a query handler is registered for a different type
|
|
72
|
-
- **THEN** `send({ type: "C", payload: undefined })` resolves to the command handler's result and is unaffected by the presence of the unrelated query handler
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
## Purpose
|
|
2
|
-
|
|
3
|
-
Multi-profile conversation ownership and routing. This capability enables the CLI to correctly identify which authenticated profile owns a given conversation, route `continue` and `delete` commands to the owning profile, and display profile ownership in `list --all-profiles` output.
|
|
4
|
-
|
|
5
|
-
## Requirements
|
|
6
|
-
|
|
7
|
-
### Requirement: findProfileForConversation returns the profile that owns a conversation
|
|
8
|
-
|
|
9
|
-
The system MUST return the name of the profile whose server-side chat list contains the given conversation ID. The `conversationId` argument MUST be passed to the per-profile lookup helper; it MUST NOT be ignored. When no profile owns the conversation, the system MUST return `null`. When multiple profiles report ownership of the same conversation ID (an inconsistent state the user is responsible for), the system MUST return the first profile in `profileManager.list()` order that reports ownership.
|
|
10
|
-
|
|
11
|
-
#### Scenario: Conversation exists in one profile
|
|
12
|
-
- **WHEN** `findProfileForConversation("abc-123")` is called and conversation `abc-123` exists in profile `work` but not in profile `personal`
|
|
13
|
-
- **THEN** the method returns the string `"work"`
|
|
14
|
-
|
|
15
|
-
#### Scenario: Conversation exists in no profile
|
|
16
|
-
- **WHEN** `findProfileForConversation("abc-123")` is called and conversation `abc-123` does not exist in any active profile's chat list
|
|
17
|
-
- **THEN** the method returns `null`
|
|
18
|
-
|
|
19
|
-
#### Scenario: Conversation exists in multiple profiles (inconsistent state)
|
|
20
|
-
- **WHEN** `findProfileForConversation("abc-123")` is called and conversation `abc-123` exists in both profile `work` and profile `personal`, and `profileManager.list()` returns `["work", "personal"]` in that order
|
|
21
|
-
- **THEN** the method returns the string `"work"` (the first profile in list order that reports ownership)
|
|
22
|
-
|
|
23
|
-
#### Scenario: The conversationId argument is actually used
|
|
24
|
-
- **WHEN** a unit test calls `findProfileForConversation("abc-123")` and the `profileHasConversation` helper is mocked
|
|
25
|
-
- **THEN** the mocked helper MUST have been invoked with the exact string `"abc-123"` (verified via `expect(mock).toHaveBeenCalledWith("work", "abc-123")` or an equivalent assertion); the method MUST NOT return early with a first-active-wins fallback
|
|
26
|
-
|
|
27
|
-
### Requirement: continue-command targets the profile that owns the conversation
|
|
28
|
-
|
|
29
|
-
The system MUST look up the profile that owns the conversation via `ProfileAuthManager.findProfileForConversation` before sending the `SendMessageCommand`. The system MUST route the message to that profile's `GeminiClientService` instance. When no profile owns the conversation, the system MUST throw `AuthenticationError` with a remediation message and exit non-zero. In a single-profile setup (only the default profile is active), the behavior MUST be unchanged: the default profile is used.
|
|
30
|
-
|
|
31
|
-
#### Scenario: Multi-profile continue routes to the owning profile
|
|
32
|
-
- **WHEN** a user with active profiles `work` and `personal` runs `gemiterm continue abc-123 "hello"` and conversation `abc-123` is owned by `work`
|
|
33
|
-
- **THEN** the `SendMessageCommand` is sent via the `work` profile's `GeminiClientService` (verified by the response content being scoped to the `work` account); the `personal` profile's `GeminiClientService` is NOT invoked for this send
|
|
34
|
-
|
|
35
|
-
#### Scenario: Continue on an unknown conversation ID exits non-zero
|
|
36
|
-
- **WHEN** a user runs `gemiterm continue unknown-id "hello"` and no profile owns conversation `unknown-id`
|
|
37
|
-
- **THEN** the command throws `AuthenticationError` whose message contains `"Could not find a profile that owns conversation 'unknown-id'"` and the suggested remediation `"gemiterm list --all-profiles"`, and the process exits with a non-zero exit code
|
|
38
|
-
|
|
39
|
-
#### Scenario: Single-profile continue is unchanged
|
|
40
|
-
- **WHEN** a user with only the default profile active runs `gemiterm continue abc-123 "hello"`
|
|
41
|
-
- **THEN** the behavior is identical to before the change: the default profile is used, no `findProfileForConversation` call is required, and the response is returned normally
|
|
42
|
-
|
|
43
|
-
### Requirement: delete-command targets the profile that owns the conversation
|
|
44
|
-
|
|
45
|
-
The system MUST look up the profile that owns the conversation via `ProfileAuthManager.findProfileForConversation` before sending the `DeleteConversationCommand`. The system MUST route the delete to that profile's `GeminiClientService` instance. When no profile owns the conversation, the system MUST throw `AuthenticationError` with the same remediation message used by `continue` and exit non-zero. In a single-profile setup, the behavior MUST be unchanged.
|
|
46
|
-
|
|
47
|
-
#### Scenario: Multi-profile delete routes to the owning profile
|
|
48
|
-
- **WHEN** a user with active profiles `work` and `personal` runs `gemiterm delete abc-123 --force` and conversation `abc-123` is owned by `work`
|
|
49
|
-
- **THEN** the `DeleteConversationCommand` is sent via the `work` profile's `GeminiClientService`; the `personal` profile's `GeminiClientService` is NOT invoked for this delete
|
|
50
|
-
|
|
51
|
-
#### Scenario: Delete on an unknown conversation ID exits non-zero
|
|
52
|
-
- **WHEN** a user runs `gemiterm delete unknown-id --force` and no profile owns conversation `unknown-id`
|
|
53
|
-
- **THEN** the command throws `AuthenticationError` whose message contains `"Could not find a profile that owns conversation 'unknown-id'"` and the process exits with a non-zero exit code
|
|
54
|
-
|
|
55
|
-
#### Scenario: Single-profile delete is unchanged
|
|
56
|
-
- **WHEN** a user with only the default profile active runs `gemiterm delete abc-123 --force`
|
|
57
|
-
- **THEN** the behavior is identical to before the change: the default profile is used, no `findProfileForConversation` call is required, and the conversation is deleted
|
|
58
|
-
|
|
59
|
-
### Requirement: list --all-profiles renders a Profile column in the text table
|
|
60
|
-
|
|
61
|
-
The system MUST render a `PROFILE` column in the text table output of `gemiterm list --all-profiles` and MUST NOT render it in any other mode. Each row MUST show the name of the profile that owns the conversation in that row. The column width MUST be 14 characters, matching the visual style of the other columns.
|
|
62
|
-
|
|
63
|
-
#### Scenario: list --all-profiles shows 5 columns
|
|
64
|
-
- **WHEN** a user with active profiles `work` and `personal` runs `gemiterm list --all-profiles`
|
|
65
|
-
- **THEN** the text table header contains the columns `ID, TITLE, DATE, PIN, PROFILE` in that order, and each row's last cell contains the owning profile name (e.g. `work` or `personal`)
|
|
66
|
-
|
|
67
|
-
#### Scenario: list without --all-profiles shows 4 columns
|
|
68
|
-
- **WHEN** a user runs `gemiterm list` (no `--all-profiles` flag)
|
|
69
|
-
- **THEN** the text table header contains the columns `ID, TITLE, DATE, PIN` only; the `PROFILE` column is absent and the chat output matches the pre-change format byte-for-byte
|
|
70
|
-
|
|
71
|
-
#### Scenario: list --all-profiles --format json includes profile field
|
|
72
|
-
- **WHEN** a user runs `gemiterm list --all-profiles --format json`
|
|
73
|
-
- **THEN** the JSON output's `chats` array contains a `profile` field on every chat; each `profile` value is the name of the profile that owns that chat
|
|
74
|
-
|
|
75
|
-
#### Scenario: list without --all-profiles --format json omits profile field
|
|
76
|
-
- **WHEN** a user runs `gemiterm list --format json` (no `--all-profiles` flag)
|
|
77
|
-
- **THEN** the JSON output's `chats` array does NOT contain a `profile` field on any chat; the JSON shape is byte-compatible with the pre-change output
|
|
78
|
-
|
|
79
|
-
### Requirement: ChatInfo carries an optional profile field
|
|
80
|
-
|
|
81
|
-
The `ChatInfo` type in `src/core/types.ts` MUST carry an optional `profile?: string` field. The field MUST be unset (or `undefined`) for chats returned by `list` without `--all-profiles` and MUST be the owning profile name for chats returned by `list --all-profiles`. Existing serialized JSON output for the non-`--all-profiles` path MUST be byte-compatible with the pre-change shape (the field is omitted, not set to `null` or `""`).
|
|
82
|
-
|
|
83
|
-
#### Scenario: ChatInfo type allows optional profile field
|
|
84
|
-
- **WHEN** a `ChatInfo` object is constructed with `{ id: "abc-123", title: "T", isPinned: false, timestamp: 0, profile: "work" }`
|
|
85
|
-
- **THEN** the object is type-valid and the `profile` field is the string `"work"`
|
|
86
|
-
|
|
87
|
-
#### Scenario: ChatInfo type allows absence of profile field
|
|
88
|
-
- **WHEN** a `ChatInfo` object is constructed with `{ id: "abc-123", title: "T", isPinned: false, timestamp: 0 }` (no `profile` key)
|
|
89
|
-
- **THEN** the object is type-valid and the `profile` field is `undefined`
|
|
90
|
-
|
|
91
|
-
#### Scenario: ChatInfo serialized to JSON omits profile when unset
|
|
92
|
-
- **WHEN** a `ChatInfo` object with no `profile` field is serialized via `JSON.stringify`
|
|
93
|
-
- **THEN** the resulting JSON does NOT contain a `profile` key (matches the pre-change byte layout)
|
|
94
|
-
|
|
95
|
-
### Requirement: formatChatList accepts an includeProfileColumn flag
|
|
96
|
-
|
|
97
|
-
The `formatChatList` function in `src/infrastructure/formatters.ts` MUST accept an optional second argument `options?: { includeProfileColumn?: boolean }`. When `options.includeProfileColumn` is `true`, the rendered table MUST have 5 columns including `PROFILE`. When the flag is `false` or omitted, the rendered table MUST have the original 4 columns (`ID, TITLE, DATE, PIN`) and the output MUST be byte-compatible with the pre-change format.
|
|
98
|
-
|
|
99
|
-
#### Scenario: formatChatList with includeProfileColumn: true renders 5 columns
|
|
100
|
-
- **WHEN** `formatChatList(chats, { includeProfileColumn: true })` is called with chats that each have a `profile` field
|
|
101
|
-
- **THEN** the rendered output contains the header `ID TITLE DATE PIN PROFILE` (with the column-padding and divider convention used elsewhere) and each row's last cell shows the chat's `profile` value
|
|
102
|
-
|
|
103
|
-
#### Scenario: formatChatList without flag renders 4 columns (backward compat)
|
|
104
|
-
- **WHEN** `formatChatList(chats)` is called with no second argument
|
|
105
|
-
- **THEN** the rendered output contains the header `ID TITLE DATE PIN` and the output is byte-compatible with the pre-change format for the same input (no `PROFILE` column)
|
|
106
|
-
|
|
107
|
-
#### Scenario: Existing formatChatList callers continue to pass without modification
|
|
108
|
-
- **WHEN** an existing test calls `formatChatList(chats)` (the pre-change call shape) after the change lands
|
|
109
|
-
- **THEN** the test continues to pass without modification (regression gate for the formatter; the optional second argument is non-breaking)
|
|
110
|
-
|
|
111
|
-
### Requirement: GeminiClientService exposes a profileHasConversation helper
|
|
112
|
-
|
|
113
|
-
The `GeminiClientService` class in `src/services/gemini-client-wrapper.ts` MUST expose a `profileHasConversation(profileName: string, conversationId: string): Promise<boolean>` method. The method MUST return `true` if the given conversation ID appears in the named profile's chat list, and `false` otherwise. The `IGeminiClientService` interface in `src/core/command-handlers.ts` MUST declare the method so handlers and tests can use it without downcasting. The method MUST NOT mutate the calling instance's cookies or session state.
|
|
114
|
-
|
|
115
|
-
#### Scenario: profileHasConversation returns true for owning profile
|
|
116
|
-
- **WHEN** `await geminiClient.profileHasConversation("work", "abc-123")` is called and conversation `abc-123` exists in `work`'s chat list
|
|
117
|
-
- **THEN** the method returns `true`
|
|
118
|
-
|
|
119
|
-
#### Scenario: profileHasConversation returns false for non-owning profile
|
|
120
|
-
- **WHEN** `await geminiClient.profileHasConversation("personal", "abc-123")` is called and conversation `abc-123` does NOT exist in `personal`'s chat list
|
|
121
|
-
- **THEN** the method returns `false`
|
|
122
|
-
|
|
123
|
-
#### Scenario: profileHasConversation is declared on the IGeminiClientService interface
|
|
124
|
-
- **WHEN** a handler is typed against `IGeminiClientService` and calls `geminiClient.profileHasConversation(name, id)`
|
|
125
|
-
- **THEN** the call type-checks (the method is part of the interface, not a concrete-class-only method)
|
|
126
|
-
|
|
127
|
-
#### Scenario: profileHasConversation does not mutate the calling instance
|
|
128
|
-
- **WHEN** `geminiClient.profileHasConversation("work", "abc-123")` is called on an instance configured for the default profile
|
|
129
|
-
- **THEN** the calling instance's cookie config and `authenticated` flag are unchanged after the call returns (verified by reading the instance fields in a test)
|
|
130
|
-
|
|
131
|
-
### Requirement: Regression test update for profile-auth-manager documents the bug fix
|
|
132
|
-
|
|
133
|
-
The 8 existing unit tests in the `describe("findProfileForConversation")` block of `tests/services/profile-auth-manager.test.ts` MUST be updated to assert the correct per-profile-lookup behavior (not the previous first-active-wins behavior). The test count for that describe block MUST increase (8 → 11+) after the change. A leading comment in the test file MUST explain that the 8 changed tests previously documented the bug and the change is the fix.
|
|
134
|
-
|
|
135
|
-
#### Scenario: The 8 existing tests are updated to mock profileHasConversation
|
|
136
|
-
- **WHEN** `tests/services/profile-auth-manager.test.ts` is read after the change
|
|
137
|
-
- **THEN** the existing tests in the `findProfileForConversation` block are rewritten to mock `GeminiClientService.profileHasConversation` and assert the new behavior; the new behavior is the first profile in list order whose `profileHasConversation` returns `true`
|
|
138
|
-
|
|
139
|
-
#### Scenario: The test count for profile-auth-manager increases
|
|
140
|
-
- **WHEN** `bun test tests/services/profile-auth-manager.test.ts` is run after the change
|
|
141
|
-
- **THEN** the test count is at least 11 (8 original tests updated plus 3-4 new tests covering: "conversation found in second profile", "conversation not in any profile", "throws when no active profiles")
|
|
142
|
-
|
|
143
|
-
#### Scenario: The test file's leading comment documents the test changes
|
|
144
|
-
- **WHEN** `tests/services/profile-auth-manager.test.ts` is read after the change
|
|
145
|
-
- **THEN** the file contains a leading comment block stating: "The 8 tests in `describe('findProfileForConversation')` previously asserted the BUGGY 'first active profile' behavior; they have been updated to assert the CORRECT per-profile-lookup behavior. See `openspec/changes/command-spec-conformance/proposal.md` for context."
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
## Purpose
|
|
2
|
-
|
|
3
|
-
Cross-platform path resolution helpers used throughout the gemiterm infrastructure. This module centralizes how the application locates its config directory, profiles directory, per-profile storage file, and default-profile marker file, and exposes a generic `resolvePath` helper for joining and normalizing paths in an OS-correct way.
|
|
4
|
-
|
|
5
|
-
## Requirements
|
|
6
|
-
|
|
7
|
-
### Requirement: resolvePath Joins and Normalizes
|
|
8
|
-
The system MUST export a `resolvePath(...parts: string[])` function that joins all supplied path segments with the OS-native separator and resolves the result to an absolute path (via `path.resolve`). The function MUST correctly handle zero, one, or many segments and MUST normalize `..` and `.` segments.
|
|
9
|
-
|
|
10
|
-
#### Scenario: Multiple segments
|
|
11
|
-
- **WHEN** `resolvePath("foo", "bar", "baz.txt")` is called
|
|
12
|
-
- **THEN** the result is `path.resolve(path.join("foo", "bar", "baz.txt"))` — the joined, resolved path
|
|
13
|
-
|
|
14
|
-
#### Scenario: Single segment
|
|
15
|
-
- **WHEN** `resolvePath("single")` is called
|
|
16
|
-
- **THEN** the result is `path.resolve("single")`
|
|
17
|
-
|
|
18
|
-
#### Scenario: No segments
|
|
19
|
-
- **WHEN** `resolvePath()` is called with no arguments
|
|
20
|
-
- **THEN** the result is `path.resolve(path.join())` (i.e. resolves the current working directory)
|
|
21
|
-
|
|
22
|
-
#### Scenario: Path with .. and .
|
|
23
|
-
- **WHEN** `resolvePath("foo", "..", "bar")` is called
|
|
24
|
-
- **THEN** the result is the OS-normalized absolute path equivalent to `path.resolve(path.join("foo", "..", "bar"))`
|
|
25
|
-
|
|
26
|
-
### Requirement: getConfigDir Honors Env Override
|
|
27
|
-
The system MUST export a `getConfigDir()` function. The function MUST first check the `GEMITERM_CONFIG_DIR` environment variable. If the variable is set to a non-empty string, the function MUST return that string verbatim and MUST NOT consult any platform-specific code path. If the variable is unset or empty, the function MUST fall back to a platform default.
|
|
28
|
-
|
|
29
|
-
#### Scenario: Env override wins on any platform
|
|
30
|
-
- **WHEN** `GEMITERM_CONFIG_DIR=/override/path` and `process.platform === "win32"`
|
|
31
|
-
- **THEN** `getConfigDir()` returns `"/override/path"`
|
|
32
|
-
|
|
33
|
-
#### Scenario: Env override wins on Linux/macOS
|
|
34
|
-
- **WHEN** `GEMITERM_CONFIG_DIR=/override/path` and `process.platform === "linux"`
|
|
35
|
-
- **THEN** `getConfigDir()` returns `"/override/path"`
|
|
36
|
-
|
|
37
|
-
### Requirement: getConfigDir Platform Default
|
|
38
|
-
When `GEMITERM_CONFIG_DIR` is unset, `getConfigDir()` MUST return a platform-appropriate default: on `process.platform === "win32"` with `APPDATA` set it MUST return `<APPDATA>/gemiterm`; on Windows without `APPDATA` and on `linux` / `darwin` it MUST return `<homedir>/gemiterm` (no `.config` intermediary).
|
|
39
|
-
|
|
40
|
-
#### Scenario: Windows with APPDATA
|
|
41
|
-
- **WHEN** the env override is unset, `process.platform === "win32"`, and `APPDATA` is set
|
|
42
|
-
- **THEN** `getConfigDir()` returns `<APPDATA>/gemiterm`
|
|
43
|
-
|
|
44
|
-
#### Scenario: Windows without APPDATA
|
|
45
|
-
- **WHEN** the env override is unset, `process.platform === "win32"`, and `APPDATA` is not set
|
|
46
|
-
- **THEN** `getConfigDir()` returns `<homedir>/gemiterm`
|
|
47
|
-
|
|
48
|
-
#### Scenario: Linux
|
|
49
|
-
- **WHEN** the env override is unset and `process.platform === "linux"`
|
|
50
|
-
- **THEN** `getConfigDir()` returns `<homedir>/gemiterm`
|
|
51
|
-
|
|
52
|
-
#### Scenario: macOS
|
|
53
|
-
- **WHEN** the env override is unset and `process.platform === "darwin"`
|
|
54
|
-
- **THEN** `getConfigDir()` returns `<homedir>/gemiterm`
|
|
55
|
-
|
|
56
|
-
### Requirement: getProfilesDir
|
|
57
|
-
The system MUST export a `getProfilesDir()` function that returns `<configDir>/profiles`, where `configDir` is the result of `getConfigDir()`. The function MUST always return the same result on a given invocation as `path.join(getConfigDir(), "profiles")`.
|
|
58
|
-
|
|
59
|
-
#### Scenario: Profiles dir is config + /profiles
|
|
60
|
-
- **WHEN** `GEMITERM_CONFIG_DIR=/tmp/gemiterm`
|
|
61
|
-
- **THEN** `getProfilesDir()` returns `path.join("/tmp/gemiterm", "profiles")`
|
|
62
|
-
|
|
63
|
-
### Requirement: getProfilePath
|
|
64
|
-
The system MUST export a `getProfilePath(name: string)` function that returns `<configDir>/profiles/<name>/storage_state.json`. The function MUST interpolate the supplied name directly into the path with no additional validation.
|
|
65
|
-
|
|
66
|
-
#### Scenario: Single profile
|
|
67
|
-
- **WHEN** `GEMITERM_CONFIG_DIR=/tmp/gemiterm`
|
|
68
|
-
- **THEN** `getProfilePath("default")` returns `<configDir>/profiles/default/storage_state.json`
|
|
69
|
-
|
|
70
|
-
#### Scenario: Names with special characters
|
|
71
|
-
- **WHEN** `GEMITERM_CONFIG_DIR=/tmp/gemiterm`
|
|
72
|
-
- **THEN** `getProfilePath("my-profile")` returns `<configDir>/profiles/my-profile/storage_state.json`
|
|
73
|
-
|
|
74
|
-
### Requirement: getProfileDir
|
|
75
|
-
The system MUST export a `getProfileDir(name: string)` function that returns `<configDir>/profiles/<name>`. The function MUST NOT include any storage file in the returned path.
|
|
76
|
-
|
|
77
|
-
#### Scenario: Profile directory for a name
|
|
78
|
-
- **WHEN** `GEMITERM_CONFIG_DIR=/tmp/gemiterm`
|
|
79
|
-
- **THEN** `getProfileDir("work")` returns `<configDir>/profiles/work`
|
|
80
|
-
|
|
81
|
-
### Requirement: getDefaultProfileMarkerPath
|
|
82
|
-
The system MUST export a `getDefaultProfileMarkerPath()` function that returns `<configDir>/profiles/.default` — the path of the marker file used to record the current default profile name.
|
|
83
|
-
|
|
84
|
-
#### Scenario: Marker path
|
|
85
|
-
- **WHEN** `GEMITERM_CONFIG_DIR=/tmp/gemiterm`
|
|
86
|
-
- **THEN** `getDefaultProfileMarkerPath()` returns `<configDir>/profiles/.default`
|
|
87
|
-
|
|
88
|
-
### Requirement: Path Constants
|
|
89
|
-
The system MUST export three string constants: `STORAGE_STATE_FILE` (value `"storage_state.json"`), `PROFILES_DIR` (value `"profiles"`), and `DEFAULT_PROFILE_MARKER` (value `".default"`). These constants are the canonical values used by the path helpers and other modules.
|
|
90
|
-
|
|
91
|
-
#### Scenario: STORAGE_STATE_FILE
|
|
92
|
-
- **WHEN** the module is loaded
|
|
93
|
-
- **THEN** `STORAGE_STATE_FILE === "storage_state.json"`
|
|
94
|
-
|
|
95
|
-
#### Scenario: PROFILES_DIR
|
|
96
|
-
- **WHEN** the module is loaded
|
|
97
|
-
- **THEN** `PROFILES_DIR === "profiles"`
|
|
98
|
-
|
|
99
|
-
#### Scenario: DEFAULT_PROFILE_MARKER
|
|
100
|
-
- **WHEN** the module is loaded
|
|
101
|
-
- **THEN** `DEFAULT_PROFILE_MARKER === ".default"`
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
## Purpose
|
|
2
|
-
|
|
3
|
-
The profile CRUD business logic. It owns the `ProfileService` class that wraps the storage layer for higher-level operations used by the CLI and command handlers: authenticating against an existing profile, listing profile statuses, deleting, renaming, and setting the default profile. It also owns the `IProfileService` and `IProfileQueryService` interfaces that the command/query handlers depend on.
|
|
4
|
-
|
|
5
|
-
## Requirements
|
|
6
|
-
|
|
7
|
-
### Requirement: ProfileService.authenticate returns an AuthResult for a valid profile
|
|
8
|
-
The `ProfileService.authenticate(profileName?)` method MUST resolve the profile name (provided value, or the configured default), validate it via `validateProfileName`, and ensure the config directory exists. If the named profile does not exist in the `ProfileManager`, the method MUST call `profileManager.create(name)` first. The method MUST then verify that the profile has valid cookies via `profileManager.hasValidCookies(name)`; if not, it MUST throw an `AuthenticationError` whose message contains `No valid session for profile '<name>'` and the substring `gemiterm login`. On success, the method MUST return an `AuthResult` with `cookies: Cookie[]` (rebuilt from the stored `secure1psid` and `secure1psidts` values) and `expiresAt: Date | null` (derived from the profile's `expiresAt` ISO string, or `null` when no expiry is known).
|
|
9
|
-
|
|
10
|
-
#### Scenario: Authenticating a profile with valid cookies returns an AuthResult
|
|
11
|
-
- **WHEN** `authenticate()` is called and the default profile has fresh cookies
|
|
12
|
-
- **THEN** the method resolves with an `AuthResult` whose `cookies` array has length 2 (entries for `__Secure-1PSID` and `__Secure-1PSIDTS`), and whose `expiresAt` is a `Date` instance
|
|
13
|
-
|
|
14
|
-
#### Scenario: Throws AuthenticationError when no valid cookies exist
|
|
15
|
-
- **WHEN** `authenticate("default")` is called and the profile has no cookies
|
|
16
|
-
- **THEN** the method rejects with an `AuthenticationError` whose message contains `No valid session`
|
|
17
|
-
|
|
18
|
-
#### Scenario: Creates profile if it does not exist and authenticates
|
|
19
|
-
- **WHEN** `authenticate("default")` is called and no profile by that name exists, but valid cookies are present in the storage
|
|
20
|
-
- **THEN** the method calls `profileManager.create("default")` and resolves with the `AuthResult` for those cookies
|
|
21
|
-
|
|
22
|
-
#### Scenario: Throws on invalid profile name
|
|
23
|
-
- **WHEN** `authenticate("bad name!")` is called
|
|
24
|
-
- **THEN** the method rejects with an error whose message contains `invalid characters`
|
|
25
|
-
|
|
26
|
-
### Requirement: ProfileService.getProfileStatuses returns all profile statuses
|
|
27
|
-
The `ProfileService.getProfileStatuses()` method MUST ensure the config directory exists and MUST return the result of `profileManager.getAllStatuses()`. The returned array MUST be a `ProfileStatus[]` (the same type defined in the domain model), and MUST be empty when no profiles are configured.
|
|
28
|
-
|
|
29
|
-
#### Scenario: Returns statuses for all existing profiles
|
|
30
|
-
- **WHEN** profiles have been created and have cookies saved
|
|
31
|
-
- **THEN** `getProfileStatuses()` returns an array containing a `ProfileStatus` for each configured profile, with `isActive` reflecting the cookie validity
|
|
32
|
-
|
|
33
|
-
#### Scenario: Returns empty array when no profiles exist
|
|
34
|
-
- **WHEN** no profiles have been created
|
|
35
|
-
- **THEN** `getProfileStatuses()` returns `[]`
|
|
36
|
-
|
|
37
|
-
### Requirement: ProfileService.getAuthStatus reports default profile authentication
|
|
38
|
-
The `ProfileService.getAuthStatus()` method MUST return an object with two fields: `authenticated: boolean` and `profileName: string | null`. The method MUST check whether the configured default profile exists and has valid cookies; if so, it MUST return `{ authenticated: true, profileName: <default> }`. If the default profile name is unset, the method MUST return `{ authenticated: false, profileName: null }`. If the default profile exists but does not have valid cookies, the method MUST also return `{ authenticated: false, profileName: null }`.
|
|
39
|
-
|
|
40
|
-
#### Scenario: Returns authenticated when default profile has valid cookies
|
|
41
|
-
- **WHEN** `getAuthStatus()` is called and the default profile has fresh cookies
|
|
42
|
-
- **THEN** it returns `{ authenticated: true, profileName: "default" }`
|
|
43
|
-
|
|
44
|
-
#### Scenario: Returns not authenticated when default has no cookies
|
|
45
|
-
- **WHEN** `getAuthStatus()` is called and the default profile has no valid cookies
|
|
46
|
-
- **THEN** it returns `{ authenticated: false, profileName: null }`
|
|
47
|
-
|
|
48
|
-
### Requirement: ProfileService.deleteProfile removes a profile
|
|
49
|
-
The `ProfileService.deleteProfile(name)` method MUST validate the name, throw a `GemitermError` whose message contains `does not exist` when the profile is not in `profileManager.list()`, and otherwise call `profileManager.delete(name)`.
|
|
50
|
-
|
|
51
|
-
#### Scenario: Deletes an existing profile
|
|
52
|
-
- **WHEN** `deleteProfile("to-delete")` is called and the profile exists
|
|
53
|
-
- **THEN** `profileManager.list()` no longer contains the deleted profile name
|
|
54
|
-
|
|
55
|
-
#### Scenario: Throws on non-existent profile
|
|
56
|
-
- **WHEN** `deleteProfile("ghost")` is called and the profile does not exist
|
|
57
|
-
- **THEN** the method rejects with a `GemitermError` whose message contains `does not exist`
|
|
58
|
-
|
|
59
|
-
#### Scenario: Throws on invalid profile name
|
|
60
|
-
- **WHEN** `deleteProfile("bad name!")` is called
|
|
61
|
-
- **THEN** the method rejects with an error whose message contains `invalid characters`
|
|
62
|
-
|
|
63
|
-
### Requirement: ProfileService.renameProfile renames a profile directory
|
|
64
|
-
The `ProfileService.renameProfile(oldName, newName)` method MUST validate both names and call `profileManager.rename(oldName, newName)`. The underlying rename MUST move the profile directory on disk and MUST update the default profile marker when the renamed profile was the default.
|
|
65
|
-
|
|
66
|
-
#### Scenario: Renaming an existing profile moves the directory
|
|
67
|
-
- **WHEN** `renameProfile("old-name", "new-name")` is called and the old profile exists
|
|
68
|
-
- **THEN** `profileManager.list()` contains `"new-name"` and no longer contains `"old-name"`
|
|
69
|
-
|
|
70
|
-
#### Scenario: Throws when either name is invalid
|
|
71
|
-
- **WHEN** `renameProfile("old", "new!")` is called
|
|
72
|
-
- **THEN** the method rejects with an error whose message contains `invalid characters`
|
|
73
|
-
|
|
74
|
-
### Requirement: ProfileService.setDefaultProfile writes the default marker
|
|
75
|
-
The `ProfileService.setDefaultProfile(name)` method MUST validate the name and call `profileManager.setDefault(name)`. The underlying call MUST write the default marker file (or update it) so that subsequent `profileManager.getDefault()` reads return the new name.
|
|
76
|
-
|
|
77
|
-
#### Scenario: Setting default updates the marker
|
|
78
|
-
- **WHEN** `setDefaultProfile("p2")` is called and profiles `p1` and `p2` exist
|
|
79
|
-
- **THEN** `profileManager.getDefault()` returns `"p2"`
|
|
80
|
-
|
|
81
|
-
#### Scenario: Throws on invalid profile name
|
|
82
|
-
- **WHEN** `setDefaultProfile("bad!")` is called
|
|
83
|
-
- **THEN** the method rejects with an error whose message contains `invalid characters`
|
|
84
|
-
|
|
85
|
-
### Requirement: ProfileService is exposed via the IProfileService and IProfileQueryService interfaces
|
|
86
|
-
The `ProfileService` class MUST satisfy the contract of the `IProfileService` interface defined in the command handlers module, which requires `authenticate(profileName?): Promise<AuthResult>`, `deleteProfile(name): Promise<void>`, `renameProfile(oldName, newName): Promise<void>`, and `setDefaultProfile(name): Promise<void>`. It MUST additionally satisfy the `IProfileQueryService` interface defined in the query handlers module, which requires `getProfileStatuses(): Promise<ProfileStatus[]>` and `getAuthStatus(): Promise<{ authenticated: boolean; profileName: string | null }>`. The class MUST be the canonical implementation wired into both the command-handler and query-handler layers.
|
|
87
|
-
|
|
88
|
-
#### Scenario: IProfileService methods are all callable
|
|
89
|
-
- **WHEN** an `IProfileService` reference is used to call `authenticate`, `deleteProfile`, `renameProfile`, and `setDefaultProfile`
|
|
90
|
-
- **THEN** each call dispatches to the corresponding `ProfileService` method
|
|
91
|
-
|
|
92
|
-
#### Scenario: IProfileQueryService methods are all callable
|
|
93
|
-
- **WHEN** an `IProfileQueryService` reference is used to call `getProfileStatuses` and `getAuthStatus`
|
|
94
|
-
- **THEN** each call dispatches to the corresponding `ProfileService` method
|