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,258 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, mock, beforeEach, afterEach, spyOn } from "bun:test";
|
|
2
|
-
import { ProfileCommand } from "../../../src/cli/commands/profile-command.ts";
|
|
3
|
-
import { Mediator } from "../../../src/core/mediator.ts";
|
|
4
|
-
import type { CliCommandContext } from "../../../src/cli/command-registry.ts";
|
|
5
|
-
import { setupTestConfig, teardownTestConfig } from "../../setup.ts";
|
|
6
|
-
import * as configModule from "../../../src/infrastructure/config.ts";
|
|
7
|
-
|
|
8
|
-
describe("profile command integration", () => {
|
|
9
|
-
let command: ProfileCommand;
|
|
10
|
-
let context: CliCommandContext;
|
|
11
|
-
let logSpy: ReturnType<typeof spyOn>;
|
|
12
|
-
let originalEnv: Record<string, string | undefined>;
|
|
13
|
-
|
|
14
|
-
beforeEach(() => {
|
|
15
|
-
command = new ProfileCommand();
|
|
16
|
-
context = { verbose: false, mediator: new Mediator() };
|
|
17
|
-
logSpy = spyOn(console, "log").mockImplementation(() => {});
|
|
18
|
-
originalEnv = {
|
|
19
|
-
GEMITERM_CONFIG_DIR: process.env.GEMITERM_CONFIG_DIR,
|
|
20
|
-
};
|
|
21
|
-
const configDir = setupTestConfig("profile-integration");
|
|
22
|
-
spyOn(configModule, "getConfigDir").mockReturnValue(configDir);
|
|
23
|
-
spyOn(configModule, "ensureConfigDir").mockImplementation(() => {});
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
afterEach(() => {
|
|
27
|
-
mock.restore();
|
|
28
|
-
logSpy.mockRestore();
|
|
29
|
-
teardownTestConfig(originalEnv);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
describe("command metadata", () => {
|
|
33
|
-
test("has correct name and description", () => {
|
|
34
|
-
expect(command.name).toBe("profile");
|
|
35
|
-
expect(command.description).toBe("Manage authentication profiles");
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
describe("--help flag", () => {
|
|
40
|
-
test("no arguments shows usage information", async () => {
|
|
41
|
-
await command.execute([], context);
|
|
42
|
-
|
|
43
|
-
const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
44
|
-
expect(output).toContain("Usage: gemiterm profile <action>");
|
|
45
|
-
expect(output).toContain("profile add");
|
|
46
|
-
expect(output).toContain("profile delete");
|
|
47
|
-
expect(output).toContain("profile rename");
|
|
48
|
-
expect(output).toContain("profile default");
|
|
49
|
-
expect(output).toContain("profile list");
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test("usage shows all actions with descriptions", async () => {
|
|
53
|
-
await command.execute([], context);
|
|
54
|
-
|
|
55
|
-
const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
56
|
-
expect(output).toContain("Create new profile and authenticate");
|
|
57
|
-
expect(output).toContain("Delete a profile");
|
|
58
|
-
expect(output).toContain("Rename a profile");
|
|
59
|
-
expect(output).toContain("Set default profile");
|
|
60
|
-
expect(output).toContain("List all profiles with status");
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
describe("unknown action", () => {
|
|
65
|
-
test("throws error for unknown action", async () => {
|
|
66
|
-
await expect(command.execute(["invalid"], context)).rejects.toThrow(
|
|
67
|
-
/Unknown action 'invalid'/,
|
|
68
|
-
);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("error message includes valid actions", async () => {
|
|
72
|
-
try {
|
|
73
|
-
await command.execute(["bogus"], context);
|
|
74
|
-
expect.unreachable("Should have thrown");
|
|
75
|
-
} catch (err: any) {
|
|
76
|
-
expect(err.message).toContain("add");
|
|
77
|
-
expect(err.message).toContain("delete");
|
|
78
|
-
expect(err.message).toContain("rename");
|
|
79
|
-
expect(err.message).toContain("default");
|
|
80
|
-
expect(err.message).toContain("list");
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
describe("profile list", () => {
|
|
86
|
-
test("shows no profiles message when empty", async () => {
|
|
87
|
-
spyOn(configModule, "listProfiles").mockReturnValue([]);
|
|
88
|
-
|
|
89
|
-
await command.execute(["list"], context);
|
|
90
|
-
|
|
91
|
-
const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
92
|
-
expect(output).toContain("No profiles found");
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test("displays profile table with profiles", async () => {
|
|
96
|
-
const profiles = ["work", "personal"];
|
|
97
|
-
spyOn(configModule, "listProfiles").mockReturnValue(profiles);
|
|
98
|
-
spyOn(configModule, "getDefaultProfileName").mockReturnValue("work");
|
|
99
|
-
|
|
100
|
-
const mockGetStatus = mock((name: string) => ({
|
|
101
|
-
name,
|
|
102
|
-
exists: true,
|
|
103
|
-
isActive: name === "work",
|
|
104
|
-
expiresAt: name === "work" ? "2099-12-31T00:00:00.000Z" : null,
|
|
105
|
-
isDefault: name === "work",
|
|
106
|
-
}));
|
|
107
|
-
|
|
108
|
-
const mockCookieStorage = { load: mock(() => []), save: mock(() => {}), delete: mock(() => {}), list: mock(() => profiles) };
|
|
109
|
-
const mockProfileManager = { getStatus: mockGetStatus, create: mock(() => {}), delete: mock(() => {}), rename: mock(() => {}), setDefault: mock(() => {}), getDefault: mock(() => "work"), list: mock(() => profiles), getAllStatuses: mock(() => []), hasValidCookies: mock(() => false), loadCookiesForApi: mock(() => ({ secure1psid: "", secure1psidts: null })) };
|
|
110
|
-
|
|
111
|
-
spyOn(command as any, "promptInput").mockResolvedValue("");
|
|
112
|
-
const origCookieStorage = (await import("../../../src/infrastructure/storage.ts")).CookieStorage;
|
|
113
|
-
spyOn(origCookieStorage.prototype, "load").mockReturnValue([
|
|
114
|
-
{ name: "__Secure-1PSID", value: "test", domain: ".google.com", path: "/", expires: Date.now() / 1000 + 999999, httpOnly: true, secure: true, sameSite: "None" },
|
|
115
|
-
{ name: "__Secure-1PSIDTS", value: "test", domain: ".google.com", path: "/", expires: Date.now() / 1000 + 999999, httpOnly: true, secure: true, sameSite: "None" },
|
|
116
|
-
]);
|
|
117
|
-
|
|
118
|
-
await command.execute(["list"], context);
|
|
119
|
-
|
|
120
|
-
const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
121
|
-
expect(output).toContain("Profiles");
|
|
122
|
-
expect(output).toContain("NAME");
|
|
123
|
-
expect(output).toContain("ACTIVE");
|
|
124
|
-
expect(output).toContain("work");
|
|
125
|
-
expect(output).toContain("personal");
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
describe("profile add", () => {
|
|
130
|
-
test("throws error when no name provided", async () => {
|
|
131
|
-
await expect(command.execute(["add"], context)).rejects.toThrow(
|
|
132
|
-
"Usage: profile add <name>",
|
|
133
|
-
);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test("throws error for invalid profile name", async () => {
|
|
137
|
-
await expect(command.execute(["add", "bad name!"], context)).rejects.toThrow();
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
test("throws error when profile already exists", async () => {
|
|
141
|
-
spyOn(configModule, "listProfiles").mockReturnValue(["existing"]);
|
|
142
|
-
|
|
143
|
-
await expect(command.execute(["add", "existing"], context)).rejects.toThrow(
|
|
144
|
-
"Profile 'existing' already exists",
|
|
145
|
-
);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
test("creates profile and authenticates for new profile", async () => {
|
|
149
|
-
spyOn(configModule, "listProfiles").mockReturnValue([]);
|
|
150
|
-
|
|
151
|
-
const mockProfileManager = {
|
|
152
|
-
create: mock((name: string) => {}),
|
|
153
|
-
delete: mock(() => {}),
|
|
154
|
-
rename: mock(() => {}),
|
|
155
|
-
setDefault: mock(() => {}),
|
|
156
|
-
};
|
|
157
|
-
spyOn(command as any, "addProfile").mockResolvedValue(undefined);
|
|
158
|
-
|
|
159
|
-
await command.execute(["add", "newprofile"], context);
|
|
160
|
-
|
|
161
|
-
const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
describe("profile delete", () => {
|
|
166
|
-
test("throws error when no name provided", async () => {
|
|
167
|
-
await expect(command.execute(["delete"], context)).rejects.toThrow(
|
|
168
|
-
"Usage: profile delete <name>",
|
|
169
|
-
);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
test("throws error when profile does not exist", async () => {
|
|
173
|
-
spyOn(configModule, "listProfiles").mockReturnValue([]);
|
|
174
|
-
|
|
175
|
-
await expect(command.execute(["delete", "nonexistent"], context)).rejects.toThrow(
|
|
176
|
-
"Profile 'nonexistent' does not exist",
|
|
177
|
-
);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
test("cancels deletion when user does not confirm", async () => {
|
|
181
|
-
spyOn(configModule, "listProfiles").mockReturnValue(["target"]);
|
|
182
|
-
|
|
183
|
-
spyOn(command as any, "promptInput").mockResolvedValue("n");
|
|
184
|
-
|
|
185
|
-
await command.execute(["delete", "target"], context);
|
|
186
|
-
|
|
187
|
-
const output = logSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
188
|
-
expect(output).toContain("Cancelled");
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
test("deletes profile when user confirms", async () => {
|
|
192
|
-
spyOn(configModule, "listProfiles").mockReturnValue(["target"]);
|
|
193
|
-
|
|
194
|
-
spyOn(command as any, "promptInput").mockResolvedValue("y");
|
|
195
|
-
const deleteSpy = mock(() => {});
|
|
196
|
-
spyOn(command as any, "deleteProfile").mockResolvedValue(undefined);
|
|
197
|
-
|
|
198
|
-
await command.execute(["delete", "target"], context);
|
|
199
|
-
});
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
describe("profile rename", () => {
|
|
203
|
-
test("throws error when missing arguments", async () => {
|
|
204
|
-
await expect(command.execute(["rename"], context)).rejects.toThrow(
|
|
205
|
-
"Usage: profile rename <name> <newName>",
|
|
206
|
-
);
|
|
207
|
-
await expect(command.execute(["rename", "old"], context)).rejects.toThrow(
|
|
208
|
-
"Usage: profile rename <name> <newName>",
|
|
209
|
-
);
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
test("throws error when profile does not exist", async () => {
|
|
213
|
-
spyOn(configModule, "listProfiles").mockReturnValue([]);
|
|
214
|
-
|
|
215
|
-
await expect(command.execute(["rename", "ghost", "newname"], context)).rejects.toThrow(
|
|
216
|
-
"Profile 'ghost' does not exist",
|
|
217
|
-
);
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
test("throws error for invalid new name", async () => {
|
|
221
|
-
spyOn(configModule, "listProfiles").mockReturnValue(["old"]);
|
|
222
|
-
|
|
223
|
-
await expect(command.execute(["rename", "old", "bad name!"], context)).rejects.toThrow();
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
test("throws error when new name already exists", async () => {
|
|
227
|
-
spyOn(configModule, "listProfiles").mockReturnValue(["old", "taken"]);
|
|
228
|
-
|
|
229
|
-
await expect(command.execute(["rename", "old", "taken"], context)).rejects.toThrow(
|
|
230
|
-
"Profile 'taken' already exists",
|
|
231
|
-
);
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
describe("profile default", () => {
|
|
236
|
-
test("throws error when no name provided", async () => {
|
|
237
|
-
await expect(command.execute(["default"], context)).rejects.toThrow(
|
|
238
|
-
"Usage: profile default <name>",
|
|
239
|
-
);
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
test("throws error when profile does not exist", async () => {
|
|
243
|
-
spyOn(configModule, "listProfiles").mockReturnValue([]);
|
|
244
|
-
|
|
245
|
-
await expect(command.execute(["default", "nonexistent"], context)).rejects.toThrow(
|
|
246
|
-
"Profile 'nonexistent' does not exist",
|
|
247
|
-
);
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
test("sets default profile successfully", async () => {
|
|
251
|
-
spyOn(configModule, "listProfiles").mockReturnValue(["target"]);
|
|
252
|
-
|
|
253
|
-
spyOn(command as any, "setDefaultProfile").mockResolvedValue(undefined);
|
|
254
|
-
|
|
255
|
-
await command.execute(["default", "target"], context);
|
|
256
|
-
});
|
|
257
|
-
});
|
|
258
|
-
});
|
|
@@ -1,223 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, mock, beforeEach, afterEach, spyOn } from "bun:test";
|
|
2
|
-
import { StatusCommand } from "../../../src/cli/commands/status-command.ts";
|
|
3
|
-
import { Mediator } from "../../../src/core/mediator.ts";
|
|
4
|
-
import { ProfileManager } from "../../../src/infrastructure/storage.ts";
|
|
5
|
-
import type { CliCommandContext } from "../../../src/cli/command-registry.ts";
|
|
6
|
-
import { setupTestConfig, teardownTestConfig } from "../../setup.ts";
|
|
7
|
-
import * as configModule from "../../../src/infrastructure/config.ts";
|
|
8
|
-
import type { ProfileStatus } from "../../../src/core/types.ts";
|
|
9
|
-
|
|
10
|
-
function getOutput(logSpy: ReturnType<typeof spyOn>): string {
|
|
11
|
-
return logSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const freshCookies = [
|
|
15
|
-
{ name: "__Secure-1PSID", value: "test", domain: ".google.com", path: "/", expires: Math.floor(Date.now() / 1000) + 86400 * 30, httpOnly: true, secure: true, sameSite: "None" as const },
|
|
16
|
-
{ name: "__Secure-1PSIDTS", value: "test", domain: ".google.com", path: "/", expires: Math.floor(Date.now() / 1000) + 86400 * 30, httpOnly: true, secure: true, sameSite: "None" as const },
|
|
17
|
-
];
|
|
18
|
-
|
|
19
|
-
const expiredCookies = [
|
|
20
|
-
{ name: "__Secure-1PSID", value: "test", domain: ".google.com", path: "/", expires: -1, httpOnly: true, secure: true, sameSite: "None" as const },
|
|
21
|
-
{ name: "__Secure-1PSIDTS", value: "test", domain: ".google.com", path: "/", expires: -1, httpOnly: true, secure: true, sameSite: "None" as const },
|
|
22
|
-
];
|
|
23
|
-
|
|
24
|
-
describe("status command integration", () => {
|
|
25
|
-
let command: StatusCommand;
|
|
26
|
-
let context: CliCommandContext;
|
|
27
|
-
let logSpy: ReturnType<typeof spyOn>;
|
|
28
|
-
let stderrSpy: ReturnType<typeof spyOn>;
|
|
29
|
-
let originalEnv: Record<string, string | undefined>;
|
|
30
|
-
let exitSpy: ReturnType<typeof spyOn>;
|
|
31
|
-
|
|
32
|
-
beforeEach(() => {
|
|
33
|
-
command = new StatusCommand();
|
|
34
|
-
context = { verbose: false, mediator: new Mediator() };
|
|
35
|
-
logSpy = spyOn(console, "log").mockImplementation(() => {});
|
|
36
|
-
stderrSpy = spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
37
|
-
exitSpy = spyOn(process, "exit").mockImplementation(() => {});
|
|
38
|
-
originalEnv = {
|
|
39
|
-
GEMITERM_CONFIG_DIR: process.env.GEMITERM_CONFIG_DIR,
|
|
40
|
-
};
|
|
41
|
-
const configDir = setupTestConfig("status-integration");
|
|
42
|
-
spyOn(configModule, "getConfigDir").mockReturnValue(configDir);
|
|
43
|
-
spyOn(configModule, "ensureConfigDir").mockImplementation(() => {});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
afterEach(() => {
|
|
47
|
-
mock.restore();
|
|
48
|
-
logSpy.mockRestore();
|
|
49
|
-
teardownTestConfig(originalEnv);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
describe("command metadata", () => {
|
|
53
|
-
test("has correct name and description", () => {
|
|
54
|
-
expect(command.name).toBe("status");
|
|
55
|
-
expect(command.description).toBe("Show configuration and profile status");
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
describe("--help flag", () => {
|
|
60
|
-
test("--help shows usage information", async () => {
|
|
61
|
-
await command.execute(["--help"], context);
|
|
62
|
-
|
|
63
|
-
const output = getOutput(logSpy);
|
|
64
|
-
expect(output).toContain("Usage: gemiterm status");
|
|
65
|
-
expect(output).toContain("Options:");
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test("-h shows usage information", async () => {
|
|
69
|
-
await command.execute(["-h"], context);
|
|
70
|
-
|
|
71
|
-
const output = getOutput(logSpy);
|
|
72
|
-
expect(output).toContain("Usage: gemiterm status");
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test("help does not access config or profiles", async () => {
|
|
76
|
-
const listSpy = spyOn(configModule, "listProfiles").mockReturnValue([]);
|
|
77
|
-
const ensureSpy = spyOn(configModule, "ensureConfigDir").mockImplementation(() => {});
|
|
78
|
-
|
|
79
|
-
await command.execute(["--help"], context);
|
|
80
|
-
|
|
81
|
-
expect(listSpy).not.toHaveBeenCalled();
|
|
82
|
-
expect(ensureSpy).not.toHaveBeenCalled();
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
describe("no profiles", () => {
|
|
87
|
-
test("displays message when no profiles exist", async () => {
|
|
88
|
-
spyOn(configModule, "listProfiles").mockReturnValue([]);
|
|
89
|
-
|
|
90
|
-
await command.execute([], context);
|
|
91
|
-
|
|
92
|
-
const output = getOutput(logSpy);
|
|
93
|
-
expect(output).toContain("No profiles found");
|
|
94
|
-
expect(output).toContain("gemiterm login");
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test("exits with code 2 when no profiles exist", async () => {
|
|
98
|
-
spyOn(configModule, "listProfiles").mockReturnValue([]);
|
|
99
|
-
|
|
100
|
-
await command.execute([], context);
|
|
101
|
-
|
|
102
|
-
expect(exitSpy).toHaveBeenCalledWith(2);
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
describe("profile table display", () => {
|
|
107
|
-
function setupProfilesWithStatus(statuses: ProfileStatus[]): void {
|
|
108
|
-
spyOn(configModule, "listProfiles").mockReturnValue(statuses.map((s) => s.name));
|
|
109
|
-
spyOn(configModule, "getDefaultProfileName").mockReturnValue(
|
|
110
|
-
statuses.find((s) => s.isDefault)?.name ?? statuses[0]?.name ?? "default",
|
|
111
|
-
);
|
|
112
|
-
spyOn(ProfileManager.prototype, "getStatus").mockImplementation(function (this: ProfileManager, name: string) {
|
|
113
|
-
const found = statuses.find((s) => s.name === name);
|
|
114
|
-
if (!found) {
|
|
115
|
-
return { name, exists: false, isActive: false, expiresAt: null, isDefault: false };
|
|
116
|
-
}
|
|
117
|
-
return found;
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
test("displays configuration header and directory", async () => {
|
|
122
|
-
setupProfilesWithStatus([
|
|
123
|
-
{ name: "default", exists: true, isActive: true, expiresAt: "2099-12-31T00:00:00.000Z", isDefault: true },
|
|
124
|
-
]);
|
|
125
|
-
|
|
126
|
-
await command.execute([], context);
|
|
127
|
-
|
|
128
|
-
const output = getOutput(logSpy);
|
|
129
|
-
expect(output).toContain("Configuration");
|
|
130
|
-
expect(output).toContain("Directory:");
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test("displays profile table with column headers", async () => {
|
|
134
|
-
setupProfilesWithStatus([
|
|
135
|
-
{ name: "work", exists: true, isActive: true, expiresAt: "2099-12-31T00:00:00.000Z", isDefault: true },
|
|
136
|
-
{ name: "personal", exists: true, isActive: true, expiresAt: "2099-12-31T00:00:00.000Z", isDefault: false },
|
|
137
|
-
]);
|
|
138
|
-
|
|
139
|
-
await command.execute([], context);
|
|
140
|
-
|
|
141
|
-
const output = getOutput(logSpy);
|
|
142
|
-
expect(output).toContain("NAME");
|
|
143
|
-
expect(output).toContain("ACTIVE");
|
|
144
|
-
expect(output).toContain("EXPIRES");
|
|
145
|
-
expect(output).toContain("DEFAULT");
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
test("displays profile names in table output", async () => {
|
|
149
|
-
const profiles = ["alpha", "beta"];
|
|
150
|
-
setupProfilesWithStatus([
|
|
151
|
-
{ name: "alpha", exists: true, isActive: true, expiresAt: "2099-12-31T00:00:00.000Z", isDefault: true },
|
|
152
|
-
{ name: "beta", exists: true, isActive: true, expiresAt: "2099-12-31T00:00:00.000Z", isDefault: false },
|
|
153
|
-
]);
|
|
154
|
-
|
|
155
|
-
await command.execute([], context);
|
|
156
|
-
|
|
157
|
-
const output = getOutput(logSpy);
|
|
158
|
-
for (const name of profiles) {
|
|
159
|
-
expect(output).toContain(name);
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
test("marks default profile with asterisk", async () => {
|
|
164
|
-
setupProfilesWithStatus([
|
|
165
|
-
{ name: "default", exists: true, isActive: true, expiresAt: "2099-12-31T00:00:00.000Z", isDefault: true },
|
|
166
|
-
]);
|
|
167
|
-
|
|
168
|
-
await command.execute([], context);
|
|
169
|
-
|
|
170
|
-
const output = getOutput(logSpy);
|
|
171
|
-
expect(output).toContain("* = default profile");
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
test("shows active count message on stderr", async () => {
|
|
175
|
-
setupProfilesWithStatus([
|
|
176
|
-
{ name: "work", exists: true, isActive: true, expiresAt: "2099-12-31T00:00:00.000Z", isDefault: true },
|
|
177
|
-
{ name: "personal", exists: true, isActive: true, expiresAt: "2099-12-31T00:00:00.000Z", isDefault: false },
|
|
178
|
-
]);
|
|
179
|
-
|
|
180
|
-
await command.execute([], context);
|
|
181
|
-
|
|
182
|
-
const stderrOutput = stderrSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
183
|
-
expect(stderrOutput).toContain("2 of 2 profile(s) active");
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
describe("inactive profiles", () => {
|
|
188
|
-
test("shows inactive indicator in profile table", async () => {
|
|
189
|
-
spyOn(configModule, "listProfiles").mockReturnValue(["expired"]);
|
|
190
|
-
spyOn(configModule, "getDefaultProfileName").mockReturnValue("expired");
|
|
191
|
-
spyOn(ProfileManager.prototype, "getStatus").mockReturnValue({
|
|
192
|
-
name: "expired",
|
|
193
|
-
exists: true,
|
|
194
|
-
isActive: false,
|
|
195
|
-
expiresAt: null,
|
|
196
|
-
isDefault: true,
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
await command.execute([], context);
|
|
200
|
-
|
|
201
|
-
const output = getOutput(logSpy);
|
|
202
|
-
expect(output).toContain("expired");
|
|
203
|
-
expect(output).toContain("N/A");
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
test("logs no valid sessions message on stderr", async () => {
|
|
207
|
-
spyOn(configModule, "listProfiles").mockReturnValue(["expired"]);
|
|
208
|
-
spyOn(configModule, "getDefaultProfileName").mockReturnValue("expired");
|
|
209
|
-
spyOn(ProfileManager.prototype, "getStatus").mockReturnValue({
|
|
210
|
-
name: "expired",
|
|
211
|
-
exists: true,
|
|
212
|
-
isActive: false,
|
|
213
|
-
expiresAt: null,
|
|
214
|
-
isDefault: true,
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
await command.execute([], context);
|
|
218
|
-
|
|
219
|
-
const stderrOutput = stderrSpy.mock.calls.map((c) => c[0]).join("\n");
|
|
220
|
-
expect(stderrOutput).toContain("No profiles have valid sessions");
|
|
221
|
-
});
|
|
222
|
-
});
|
|
223
|
-
});
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "bun:test";
|
|
2
|
-
import { spawnSync } from "node:child_process";
|
|
3
|
-
import { resolve } from "node:path";
|
|
4
|
-
|
|
5
|
-
const root = resolve(import.meta.dir, "../..");
|
|
6
|
-
const ps1Path = resolve(root, "install.ps1");
|
|
7
|
-
const shPath = resolve(root, "install.sh");
|
|
8
|
-
|
|
9
|
-
describe("installer script shape", () => {
|
|
10
|
-
test("install.ps1 parses without errors", () => {
|
|
11
|
-
let hasPwsh;
|
|
12
|
-
try {
|
|
13
|
-
hasPwsh = spawnSync("pwsh", ["-NoProfile", "-Command", "exit 0"], {
|
|
14
|
-
timeout: 5000,
|
|
15
|
-
});
|
|
16
|
-
} catch (err) {
|
|
17
|
-
if (err.code === "ENOENT") {
|
|
18
|
-
console.log("Skipping install.ps1 parse test: pwsh not found on PATH");
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
throw err;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (hasPwsh.status !== 0) {
|
|
25
|
-
console.log("Skipping install.ps1 parse test: pwsh not found on PATH");
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const result = spawnSync(
|
|
30
|
-
"pwsh",
|
|
31
|
-
[
|
|
32
|
-
"-NoProfile",
|
|
33
|
-
"-Command",
|
|
34
|
-
`& { [System.Management.Automation.Language.Parser]::ParseFile('${ps1Path}', [ref]$null, [ref]$null) }`,
|
|
35
|
-
],
|
|
36
|
-
{ timeout: 10000 },
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
expect(result.status).toBe(0);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test("install.sh parses without syntax errors", () => {
|
|
43
|
-
const hasBash = spawnSync("bash", ["-c", "exit 0"], { timeout: 5000 });
|
|
44
|
-
|
|
45
|
-
if (hasBash.status !== 0) {
|
|
46
|
-
console.log("Skipping install.sh parse test: bash not found on PATH");
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const result = spawnSync("bash", ["-n", shPath], { timeout: 10000 });
|
|
51
|
-
|
|
52
|
-
expect(result.status).toBe(0);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test("both scripts contain the migration promise comment", async () => {
|
|
56
|
-
const ps1Content = await Bun.file(ps1Path).text();
|
|
57
|
-
const shContent = await Bun.file(shPath).text();
|
|
58
|
-
|
|
59
|
-
const hasPs1Promise =
|
|
60
|
-
ps1Content.includes("in place") && ps1Content.includes("NEVER deleted");
|
|
61
|
-
const hasShPromise =
|
|
62
|
-
shContent.includes("in place") && shContent.includes("NEVER deleted");
|
|
63
|
-
|
|
64
|
-
expect(hasPs1Promise).toBe(true);
|
|
65
|
-
expect(hasShPromise).toBe(true);
|
|
66
|
-
});
|
|
67
|
-
});
|