gemiterm 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -9
- package/package.json +13 -4
- package/src/cli/command-registry.ts +66 -62
- package/src/cli/commands/install-skills-command.ts +35 -0
- package/src/cli/index.ts +19 -44
- package/src/infrastructure/cli-parser.ts +77 -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,220 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, mock, beforeEach, afterEach, spyOn } from "bun:test";
|
|
2
|
-
import { AuthCommand } from "../../src/cli/commands/auth-command.ts";
|
|
3
|
-
import { Mediator } from "../../src/core/mediator.ts";
|
|
4
|
-
import type { CliCommandContext } from "../../src/cli/command-registry.ts";
|
|
5
|
-
import * as configModule from "../../src/infrastructure/config.ts";
|
|
6
|
-
|
|
7
|
-
describe("AuthCommand", () => {
|
|
8
|
-
let command: AuthCommand;
|
|
9
|
-
let context: CliCommandContext;
|
|
10
|
-
let listSpy: ReturnType<typeof spyOn>;
|
|
11
|
-
let defaultProfileSpy: ReturnType<typeof spyOn>;
|
|
12
|
-
|
|
13
|
-
beforeEach(() => {
|
|
14
|
-
command = new AuthCommand();
|
|
15
|
-
context = { verbose: false, mediator: new Mediator() };
|
|
16
|
-
listSpy = spyOn(configModule, "listProfiles").mockReturnValue([]);
|
|
17
|
-
defaultProfileSpy = spyOn(configModule, "getDefaultProfileName").mockReturnValue("default");
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
afterEach(() => {
|
|
21
|
-
mock.restore();
|
|
22
|
-
listSpy.mockRestore();
|
|
23
|
-
defaultProfileSpy.mockRestore();
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test("has correct name and description", () => {
|
|
27
|
-
expect(command.name).toBe("auth");
|
|
28
|
-
expect(command.description).toBe("Authenticate with Google Gemini");
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
describe("execute", () => {
|
|
32
|
-
test("authenticates with default profile when no profiles exist", async () => {
|
|
33
|
-
listSpy.mockReturnValue([]);
|
|
34
|
-
|
|
35
|
-
const authSpy = spyOn(command as any, "authenticateWithProfile").mockResolvedValue(undefined);
|
|
36
|
-
await command.execute([], context);
|
|
37
|
-
|
|
38
|
-
expect(authSpy).toHaveBeenCalledWith(
|
|
39
|
-
expect.anything(),
|
|
40
|
-
"default",
|
|
41
|
-
expect.anything(),
|
|
42
|
-
true,
|
|
43
|
-
);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("authenticates directly when only one profile exists", async () => {
|
|
47
|
-
listSpy.mockReturnValue(["my-profile"]);
|
|
48
|
-
|
|
49
|
-
const authSpy = spyOn(command as any, "authenticateWithProfile").mockResolvedValue(undefined);
|
|
50
|
-
await command.execute([], context);
|
|
51
|
-
|
|
52
|
-
expect(authSpy).toHaveBeenCalledWith(
|
|
53
|
-
expect.anything(),
|
|
54
|
-
"my-profile",
|
|
55
|
-
expect.anything(),
|
|
56
|
-
false,
|
|
57
|
-
);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test("shows profile menu when multiple profiles exist", async () => {
|
|
61
|
-
listSpy.mockReturnValue(["p1", "p2"]);
|
|
62
|
-
|
|
63
|
-
const menuSpy = spyOn(command as any, "showProfileMenu").mockResolvedValue(null);
|
|
64
|
-
await command.execute([], context);
|
|
65
|
-
|
|
66
|
-
expect(menuSpy).toHaveBeenCalled();
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test("authenticates selected profile from menu", async () => {
|
|
70
|
-
listSpy.mockReturnValue(["p1", "p2"]);
|
|
71
|
-
|
|
72
|
-
const menuSpy = spyOn(command as any, "showProfileMenu").mockResolvedValue({
|
|
73
|
-
type: "auth",
|
|
74
|
-
profileName: "p2",
|
|
75
|
-
});
|
|
76
|
-
const authSpy = spyOn(command as any, "authenticateWithProfile").mockResolvedValue(undefined);
|
|
77
|
-
|
|
78
|
-
await command.execute([], context);
|
|
79
|
-
|
|
80
|
-
expect(authSpy).toHaveBeenCalledWith(
|
|
81
|
-
expect.anything(),
|
|
82
|
-
"p2",
|
|
83
|
-
expect.anything(),
|
|
84
|
-
false,
|
|
85
|
-
);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("exits when X is selected in profile menu", async () => {
|
|
89
|
-
listSpy.mockReturnValue(["p1", "p2"]);
|
|
90
|
-
|
|
91
|
-
const promptSpy = spyOn(command as any, "promptInput").mockResolvedValue("X");
|
|
92
|
-
const logSpy = spyOn(console, "log").mockImplementation(() => {});
|
|
93
|
-
|
|
94
|
-
await command.execute([], context);
|
|
95
|
-
|
|
96
|
-
expect(promptSpy).toHaveBeenCalledWith("Select an option");
|
|
97
|
-
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("Continuing with current default"));
|
|
98
|
-
logSpy.mockRestore();
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
test("throws on invalid profile name when adding", async () => {
|
|
102
|
-
listSpy.mockReturnValue(["p1", "p2"]);
|
|
103
|
-
|
|
104
|
-
const promptSpy = spyOn(command as any, "promptInput").mockImplementation(
|
|
105
|
-
(prompt: string) => {
|
|
106
|
-
if (prompt.includes("Select")) return Promise.resolve("A");
|
|
107
|
-
if (prompt.includes("Enter profile name")) return Promise.resolve("bad name!!");
|
|
108
|
-
return Promise.resolve("");
|
|
109
|
-
},
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
await expect(command.execute([], context)).rejects.toThrow("invalid characters");
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test("throws when deleting nonexistent profile", async () => {
|
|
116
|
-
listSpy.mockReturnValue(["p1", "p2"]);
|
|
117
|
-
|
|
118
|
-
const promptSpy = spyOn(command as any, "promptInput").mockImplementation(
|
|
119
|
-
(prompt: string) => {
|
|
120
|
-
if (prompt.includes("Select")) return Promise.resolve("D");
|
|
121
|
-
if (prompt.includes("Enter profile name to delete")) return Promise.resolve("ghost");
|
|
122
|
-
return Promise.resolve("");
|
|
123
|
-
},
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
await expect(command.execute([], context)).rejects.toThrow("does not exist");
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
test("cancels deletion when user answers no", async () => {
|
|
130
|
-
listSpy.mockReturnValue(["p1", "p2"]);
|
|
131
|
-
|
|
132
|
-
const promptSpy = spyOn(command as any, "promptInput").mockImplementation(
|
|
133
|
-
(prompt: string) => {
|
|
134
|
-
if (prompt.includes("Select")) return Promise.resolve("D");
|
|
135
|
-
if (prompt.includes("Enter profile name to delete")) return Promise.resolve("p1");
|
|
136
|
-
if (prompt.includes("Delete profile")) return Promise.resolve("n");
|
|
137
|
-
return Promise.resolve("");
|
|
138
|
-
},
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
await command.execute([], context);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
test("renames profile and authenticates with new name", async () => {
|
|
145
|
-
listSpy.mockReturnValue(["p1", "p2"]);
|
|
146
|
-
|
|
147
|
-
const authSpy = spyOn(command as any, "authenticateWithProfile").mockResolvedValue(undefined);
|
|
148
|
-
const mockRename = mock(() => {});
|
|
149
|
-
|
|
150
|
-
const { ProfileManager } = await import("../../src/infrastructure/storage.ts");
|
|
151
|
-
const origProto = ProfileManager.prototype.rename;
|
|
152
|
-
ProfileManager.prototype.rename = mockRename;
|
|
153
|
-
|
|
154
|
-
try {
|
|
155
|
-
const promptSpy = spyOn(command as any, "promptInput").mockImplementation(
|
|
156
|
-
(prompt: string) => {
|
|
157
|
-
if (prompt.includes("Select")) return Promise.resolve("R");
|
|
158
|
-
if (prompt.includes("current profile name")) return Promise.resolve("p1");
|
|
159
|
-
if (prompt.includes("new profile name")) return Promise.resolve("p1-new");
|
|
160
|
-
return Promise.resolve("");
|
|
161
|
-
},
|
|
162
|
-
);
|
|
163
|
-
|
|
164
|
-
await command.execute([], context);
|
|
165
|
-
|
|
166
|
-
expect(mockRename).toHaveBeenCalledWith("p1", "p1-new");
|
|
167
|
-
expect(authSpy).toHaveBeenCalledWith(
|
|
168
|
-
expect.anything(),
|
|
169
|
-
"p1-new",
|
|
170
|
-
expect.anything(),
|
|
171
|
-
false,
|
|
172
|
-
);
|
|
173
|
-
} finally {
|
|
174
|
-
ProfileManager.prototype.rename = origProto;
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
test("set default for a profile", async () => {
|
|
179
|
-
listSpy.mockReturnValue(["p1", "p2"]);
|
|
180
|
-
|
|
181
|
-
const logSpy = spyOn(console, "log").mockImplementation(() => {});
|
|
182
|
-
const setDefaultSpy = spyOn(configModule, "setDefaultProfileName").mockImplementation(() => {});
|
|
183
|
-
const mockSetDefault = mock(() => {});
|
|
184
|
-
|
|
185
|
-
const { ProfileManager } = await import("../../src/infrastructure/storage.ts");
|
|
186
|
-
const origProto = ProfileManager.prototype.setDefault;
|
|
187
|
-
ProfileManager.prototype.setDefault = mockSetDefault;
|
|
188
|
-
|
|
189
|
-
try {
|
|
190
|
-
const promptSpy = spyOn(command as any, "promptInput").mockImplementation(
|
|
191
|
-
(prompt: string) => {
|
|
192
|
-
if (prompt.includes("Select")) return Promise.resolve("S");
|
|
193
|
-
if (prompt.includes("Enter profile name to set as default")) return Promise.resolve("p2");
|
|
194
|
-
return Promise.resolve("");
|
|
195
|
-
},
|
|
196
|
-
);
|
|
197
|
-
|
|
198
|
-
await command.execute([], context);
|
|
199
|
-
|
|
200
|
-
expect(mockSetDefault).toHaveBeenCalledWith("p2");
|
|
201
|
-
expect(setDefaultSpy).toHaveBeenCalledWith("p2");
|
|
202
|
-
} finally {
|
|
203
|
-
ProfileManager.prototype.setDefault = origProto;
|
|
204
|
-
logSpy.mockRestore();
|
|
205
|
-
}
|
|
206
|
-
});
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
describe("showProfileMenu", () => {
|
|
210
|
-
test("returns null for unknown option", async () => {
|
|
211
|
-
const promptSpy = spyOn(command as any, "promptInput").mockResolvedValue("Z");
|
|
212
|
-
|
|
213
|
-
const result = await (command as any).showProfileMenu(["p1"], {
|
|
214
|
-
getStatus: () => ({ name: "p1", exists: true, isActive: true, expiresAt: null, isDefault: true }),
|
|
215
|
-
} as any);
|
|
216
|
-
|
|
217
|
-
expect(result).toBeNull();
|
|
218
|
-
});
|
|
219
|
-
});
|
|
220
|
-
});
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, mock, beforeEach } from "bun:test";
|
|
2
|
-
import { CommandRegistry } from "../../src/cli/command-registry.ts";
|
|
3
|
-
import { Mediator } from "../../src/core/mediator.ts";
|
|
4
|
-
import type { CliCommand, CliCommandContext } from "../../src/cli/command-registry.ts";
|
|
5
|
-
|
|
6
|
-
class FakeCommand implements CliCommand {
|
|
7
|
-
constructor(
|
|
8
|
-
public readonly name: string,
|
|
9
|
-
public readonly description: string,
|
|
10
|
-
private readonly executeFn: (args: string[], ctx: CliCommandContext) => Promise<void>,
|
|
11
|
-
) {}
|
|
12
|
-
|
|
13
|
-
async execute(args: string[], ctx: CliCommandContext): Promise<void> {
|
|
14
|
-
return this.executeFn(args, ctx);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe("CommandRegistry", () => {
|
|
19
|
-
let registry: CommandRegistry;
|
|
20
|
-
|
|
21
|
-
beforeEach(() => {
|
|
22
|
-
registry = new CommandRegistry();
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
test("registers and retrieves a command handler", () => {
|
|
26
|
-
const handler = new FakeCommand("test", "A test command", async () => {});
|
|
27
|
-
registry.register("test", handler);
|
|
28
|
-
expect(registry.getHandler("test")).toBe(handler);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test("returns undefined for unregistered command", () => {
|
|
32
|
-
expect(registry.getHandler("nonexistent")).toBeUndefined();
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test("throws when registering duplicate command", () => {
|
|
36
|
-
const handler = new FakeCommand("dup", "desc", async () => {});
|
|
37
|
-
registry.register("dup", handler);
|
|
38
|
-
expect(() => registry.register("dup", handler)).toThrow("Command already registered: dup");
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test("has() returns correct boolean", () => {
|
|
42
|
-
registry.register("exists", new FakeCommand("exists", "desc", async () => {}));
|
|
43
|
-
expect(registry.has("exists")).toBe(true);
|
|
44
|
-
expect(registry.has("nope")).toBe(false);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test("getRegisteredNames() returns all registered names", () => {
|
|
48
|
-
registry.register("login", new FakeCommand("login", "Login", async () => {}));
|
|
49
|
-
registry.register("list", new FakeCommand("list", "List", async () => {}));
|
|
50
|
-
const names = registry.getRegisteredNames();
|
|
51
|
-
expect(names).toContain("login");
|
|
52
|
-
expect(names).toContain("list");
|
|
53
|
-
expect(names).toHaveLength(2);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test("execute dispatches to correct handler", async () => {
|
|
57
|
-
const executeMock = mock(() => Promise.resolve());
|
|
58
|
-
const handler = new FakeCommand("greet", "Greet", executeMock);
|
|
59
|
-
registry.register("greet", handler);
|
|
60
|
-
|
|
61
|
-
const ctx: CliCommandContext = { verbose: false, mediator: new Mediator() };
|
|
62
|
-
await registry.getHandler("greet")!.execute(["--name", "World"], ctx);
|
|
63
|
-
|
|
64
|
-
expect(executeMock).toHaveBeenCalledWith(["--name", "World"], ctx);
|
|
65
|
-
expect(executeMock).toHaveBeenCalledTimes(1);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, mock, beforeEach, afterEach, spyOn } from "bun:test";
|
|
2
|
-
import { ContinueCommand } from "../../src/cli/commands/continue-command.ts";
|
|
3
|
-
import { Mediator } from "../../src/core/mediator.ts";
|
|
4
|
-
import type { CliCommandContext } from "../../src/cli/command-registry.ts";
|
|
5
|
-
import { COMMAND_TYPES } from "../../src/core/command-handlers.ts";
|
|
6
|
-
import { QUERY_TYPES } from "../../src/core/query-handlers.ts";
|
|
7
|
-
|
|
8
|
-
describe("ContinueCommand", () => {
|
|
9
|
-
let command: ContinueCommand;
|
|
10
|
-
let mediator: Mediator;
|
|
11
|
-
let context: CliCommandContext;
|
|
12
|
-
let logSpy: ReturnType<typeof spyOn>;
|
|
13
|
-
|
|
14
|
-
beforeEach(() => {
|
|
15
|
-
command = new ContinueCommand();
|
|
16
|
-
mediator = new Mediator();
|
|
17
|
-
context = {
|
|
18
|
-
verbose: false,
|
|
19
|
-
mediator,
|
|
20
|
-
profileAuthManager: {
|
|
21
|
-
getActiveProfiles: mock(() => ["default"]),
|
|
22
|
-
findProfileForConversation: mock(() => null),
|
|
23
|
-
ensureAuthenticated: mock(() => ({ secure_1psid: "", secure_1psidts: null })),
|
|
24
|
-
} as unknown as CliCommandContext["profileAuthManager"],
|
|
25
|
-
};
|
|
26
|
-
logSpy = spyOn(console, "log").mockImplementation(() => {});
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
afterEach(() => {
|
|
30
|
-
mock.restore();
|
|
31
|
-
logSpy.mockRestore();
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("has correct name and description", () => {
|
|
35
|
-
expect(command.name).toBe("continue");
|
|
36
|
-
expect(command.description).toBe("Continue a conversation");
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test("shows help with --help flag", async () => {
|
|
40
|
-
await command.execute(["--help"], context);
|
|
41
|
-
const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
42
|
-
expect(output).toContain("Usage: gemiterm continue");
|
|
43
|
-
expect(output).toContain("conversation_id");
|
|
44
|
-
expect(output).toContain("/exit");
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test("shows help with -h flag", async () => {
|
|
48
|
-
await command.execute(["-h"], context);
|
|
49
|
-
const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
50
|
-
expect(output).toContain("Usage: gemiterm continue");
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test("sends message in non-interactive mode with conversation_id and message", async () => {
|
|
54
|
-
const mockHandler = {
|
|
55
|
-
commandType: COMMAND_TYPES.SEND_MESSAGE,
|
|
56
|
-
handle: mock(async () => ({ response: "Hello from Gemini!" })),
|
|
57
|
-
};
|
|
58
|
-
mediator.registerCommandHandler(mockHandler as any);
|
|
59
|
-
|
|
60
|
-
await command.execute(["conv123", "Hello there"], context);
|
|
61
|
-
|
|
62
|
-
const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
63
|
-
expect(output).toContain("Model:");
|
|
64
|
-
expect(output).toContain("Hello from Gemini!");
|
|
65
|
-
expect(mockHandler.handle).toHaveBeenCalledWith(
|
|
66
|
-
expect.objectContaining({
|
|
67
|
-
type: COMMAND_TYPES.SEND_MESSAGE,
|
|
68
|
-
payload: expect.objectContaining({
|
|
69
|
-
conversationId: "conv123",
|
|
70
|
-
message: "Hello there",
|
|
71
|
-
}),
|
|
72
|
-
}),
|
|
73
|
-
);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test("sends correct command type via mediator", async () => {
|
|
77
|
-
const mockHandler = {
|
|
78
|
-
commandType: COMMAND_TYPES.SEND_MESSAGE,
|
|
79
|
-
handle: mock(async () => ({ response: "ok" })),
|
|
80
|
-
};
|
|
81
|
-
mediator.registerCommandHandler(mockHandler as any);
|
|
82
|
-
|
|
83
|
-
await command.execute(["conv-id", "test message"], context);
|
|
84
|
-
|
|
85
|
-
expect(mockHandler.handle).toHaveBeenCalledTimes(1);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("invokes list command when no conversation_id provided", async () => {
|
|
89
|
-
const mockListHandler = {
|
|
90
|
-
queryType: QUERY_TYPES.LIST_CHATS,
|
|
91
|
-
handle: mock(async () => ({ chats: [] })),
|
|
92
|
-
};
|
|
93
|
-
mediator.registerQueryHandler(mockListHandler as any);
|
|
94
|
-
|
|
95
|
-
await command.execute([], context);
|
|
96
|
-
|
|
97
|
-
const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
98
|
-
expect(output).toContain("Listing conversations");
|
|
99
|
-
});
|
|
100
|
-
});
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, mock, beforeEach, afterEach, spyOn } from "bun:test";
|
|
2
|
-
import { DeleteCommand } from "../../src/cli/commands/delete-command.ts";
|
|
3
|
-
import { Mediator } from "../../src/core/mediator.ts";
|
|
4
|
-
import type { CliCommandContext } from "../../src/cli/command-registry.ts";
|
|
5
|
-
import { COMMAND_TYPES } from "../../src/core/command-handlers.ts";
|
|
6
|
-
|
|
7
|
-
describe("DeleteCommand", () => {
|
|
8
|
-
let command: DeleteCommand;
|
|
9
|
-
let mediator: Mediator;
|
|
10
|
-
let context: CliCommandContext;
|
|
11
|
-
let logSpy: ReturnType<typeof spyOn>;
|
|
12
|
-
let errorSpy: ReturnType<typeof spyOn>;
|
|
13
|
-
let exitSpy: ReturnType<typeof spyOn>;
|
|
14
|
-
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
command = new DeleteCommand();
|
|
17
|
-
mediator = new Mediator();
|
|
18
|
-
context = {
|
|
19
|
-
verbose: false,
|
|
20
|
-
mediator,
|
|
21
|
-
profileAuthManager: {
|
|
22
|
-
getActiveProfiles: mock(() => ["default"]),
|
|
23
|
-
findProfileForConversation: mock(() => null),
|
|
24
|
-
ensureAuthenticated: mock(() => ({ secure_1psid: "", secure_1psidts: null })),
|
|
25
|
-
} as unknown as CliCommandContext["profileAuthManager"],
|
|
26
|
-
};
|
|
27
|
-
logSpy = spyOn(console, "log").mockImplementation(() => {});
|
|
28
|
-
errorSpy = spyOn(console, "error").mockImplementation(() => {});
|
|
29
|
-
exitSpy = spyOn(process, "exit").mockImplementation(() => {
|
|
30
|
-
throw new Error("process.exit called");
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
afterEach(() => {
|
|
35
|
-
mock.restore();
|
|
36
|
-
logSpy.mockRestore();
|
|
37
|
-
errorSpy.mockRestore();
|
|
38
|
-
exitSpy.mockRestore();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test("has correct name and description", () => {
|
|
42
|
-
expect(command.name).toBe("delete");
|
|
43
|
-
expect(command.description).toBe("Delete a conversation");
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("shows help with --help flag", async () => {
|
|
47
|
-
await command.execute(["--help"], context);
|
|
48
|
-
const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
49
|
-
expect(output).toContain("Usage: gemiterm delete");
|
|
50
|
-
expect(output).toContain("--force");
|
|
51
|
-
expect(output).toContain("--help");
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test("shows error when no conversation ID provided", async () => {
|
|
55
|
-
await expect(command.execute([], context)).rejects.toThrow("process.exit called");
|
|
56
|
-
|
|
57
|
-
const errorOutput = errorSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
58
|
-
expect(errorOutput).toContain("conversation ID is required");
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("sends delete-conversation command with --force flag", async () => {
|
|
62
|
-
const mockHandler = {
|
|
63
|
-
commandType: COMMAND_TYPES.DELETE_CONVERSATION,
|
|
64
|
-
handle: mock(async () => ({ success: true })),
|
|
65
|
-
};
|
|
66
|
-
mediator.registerCommandHandler(mockHandler as any);
|
|
67
|
-
|
|
68
|
-
await command.execute(["abc123", "--force"], context);
|
|
69
|
-
|
|
70
|
-
expect(mockHandler.handle).toHaveBeenCalledWith(
|
|
71
|
-
expect.objectContaining({
|
|
72
|
-
payload: expect.objectContaining({ conversationId: "abc123" }),
|
|
73
|
-
}),
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
77
|
-
expect(output).toContain("abc123");
|
|
78
|
-
expect(output).toContain("deleted");
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test("sends delete-conversation command with -f short flag", async () => {
|
|
82
|
-
const mockHandler = {
|
|
83
|
-
commandType: COMMAND_TYPES.DELETE_CONVERSATION,
|
|
84
|
-
handle: mock(async () => ({ success: true })),
|
|
85
|
-
};
|
|
86
|
-
mediator.registerCommandHandler(mockHandler as any);
|
|
87
|
-
|
|
88
|
-
await command.execute(["abc123", "-f"], context);
|
|
89
|
-
|
|
90
|
-
expect(mockHandler.handle).toHaveBeenCalledWith(
|
|
91
|
-
expect.objectContaining({
|
|
92
|
-
payload: expect.objectContaining({ conversationId: "abc123" }),
|
|
93
|
-
}),
|
|
94
|
-
);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test("handles failed deletion", async () => {
|
|
98
|
-
const mockHandler = {
|
|
99
|
-
commandType: COMMAND_TYPES.DELETE_CONVERSATION,
|
|
100
|
-
handle: mock(async () => ({ success: false })),
|
|
101
|
-
};
|
|
102
|
-
mediator.registerCommandHandler(mockHandler as any);
|
|
103
|
-
|
|
104
|
-
await expect(command.execute(["abc123", "--force"], context)).rejects.toThrow(
|
|
105
|
-
"process.exit called",
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
const errorOutput = errorSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
109
|
-
expect(errorOutput).toContain("Failed to delete conversation");
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test("handles error from handler", async () => {
|
|
113
|
-
const mockHandler = {
|
|
114
|
-
commandType: COMMAND_TYPES.DELETE_CONVERSATION,
|
|
115
|
-
handle: mock(async () => {
|
|
116
|
-
throw new Error("Network error");
|
|
117
|
-
}),
|
|
118
|
-
};
|
|
119
|
-
mediator.registerCommandHandler(mockHandler as any);
|
|
120
|
-
|
|
121
|
-
await expect(command.execute(["abc123", "--force"], context)).rejects.toThrow(
|
|
122
|
-
"process.exit called",
|
|
123
|
-
);
|
|
124
|
-
|
|
125
|
-
const errorOutput = errorSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
126
|
-
expect(errorOutput).toContain("Network error");
|
|
127
|
-
});
|
|
128
|
-
});
|