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.
Files changed (150) hide show
  1. package/README.md +32 -9
  2. package/package.json +13 -4
  3. package/src/cli/command-registry.ts +66 -62
  4. package/src/cli/commands/install-skills-command.ts +35 -0
  5. package/src/cli/index.ts +19 -44
  6. package/src/infrastructure/cli-parser.ts +77 -0
  7. package/src/services/skills-cli-driver.ts +58 -0
  8. package/src/services/skills-service.ts +52 -0
  9. package/.env +0 -2
  10. package/.github/workflows/manual-release.yml +0 -34
  11. package/.github/workflows/release.yml +0 -198
  12. package/.github/workflows/test.yml +0 -29
  13. package/.opencode/commands/opsx-apply.md +0 -152
  14. package/.opencode/commands/opsx-archive.md +0 -157
  15. package/.opencode/commands/opsx-explore.md +0 -169
  16. package/.opencode/commands/opsx-propose.md +0 -104
  17. package/.opencode/commands/opsx-sync.md +0 -140
  18. package/.opencode/commands/search-intelligently.md +0 -9
  19. package/.opencode/opencode.json +0 -20
  20. package/.opencode/skills/intellisearch/.version +0 -1
  21. package/.opencode/skills/intellisearch/SKILL.md +0 -84
  22. package/.opencode/skills/intellisearch/references/brave-search.md +0 -67
  23. package/.opencode/skills/intellisearch/references/ddg-search.md +0 -79
  24. package/.opencode/skills/intellisearch/references/deepwiki-tools.md +0 -246
  25. package/.opencode/skills/intellisearch/references/examples.md +0 -97
  26. package/.opencode/skills/intellisearch/references/gh-cli.md +0 -143
  27. package/.opencode/skills/intellisearch/references/google-search.md +0 -40
  28. package/.opencode/skills/intellisearch/references/search-workflow.md +0 -143
  29. package/.opencode/skills/intellisearch/references/workflow.md +0 -115
  30. package/.opencode/skills/openspec-apply-change/SKILL.md +0 -159
  31. package/.opencode/skills/openspec-archive-change/SKILL.md +0 -117
  32. package/.opencode/skills/openspec-explore/SKILL.md +0 -287
  33. package/.opencode/skills/openspec-propose/SKILL.md +0 -111
  34. package/.opencode/skills/openspec-sync-specs/SKILL.md +0 -147
  35. package/.serena/project.yml +0 -133
  36. package/AGENTS.md +0 -136
  37. package/CHANGELOG.md +0 -3
  38. package/bun.lock +0 -105
  39. package/docs/INSTALL.md +0 -185
  40. package/docs/PLAYWRIGHT_CLI_API.md +0 -306
  41. package/docs/python-release-for-reference.md +0 -175
  42. package/examples/sample_json_export.json +0 -32
  43. package/examples/sample_markdown_export.md +0 -221
  44. package/examples/usage_examples.sh +0 -222
  45. package/install.ps1 +0 -264
  46. package/install.sh +0 -275
  47. package/openspec/MAESTRO_MIGRATION.md +0 -180
  48. package/openspec/changes/archive/2026-06-08-command-spec-conformance/.openspec.yaml +0 -2
  49. package/openspec/changes/archive/2026-06-08-command-spec-conformance/design.md +0 -194
  50. package/openspec/changes/archive/2026-06-08-command-spec-conformance/proposal.md +0 -52
  51. package/openspec/changes/archive/2026-06-08-command-spec-conformance/specs/multi-profile-conversations/spec.md +0 -133
  52. package/openspec/changes/archive/2026-06-08-command-spec-conformance/tasks.md +0 -95
  53. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/.openspec.yaml +0 -2
  54. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/design.md +0 -28
  55. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/proposal.md +0 -22
  56. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/specs/interactive-prompt-loop/spec.md +0 -35
  57. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/tasks.md +0 -28
  58. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/.openspec.yaml +0 -2
  59. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/design.md +0 -323
  60. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/proposal.md +0 -122
  61. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/specs/no-capability-changes/spec.md +0 -29
  62. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/tasks.md +0 -172
  63. package/openspec/changes/archive/2026-06-09-v2-install-migration/.openspec.yaml +0 -2
  64. package/openspec/changes/archive/2026-06-09-v2-install-migration/design.md +0 -241
  65. package/openspec/changes/archive/2026-06-09-v2-install-migration/proposal.md +0 -41
  66. package/openspec/changes/archive/2026-06-09-v2-install-migration/specs/v2-installer/spec.md +0 -205
  67. package/openspec/changes/archive/2026-06-09-v2-install-migration/tasks.md +0 -97
  68. package/openspec/changes/commander-cli-parser/.openspec.yaml +0 -2
  69. package/openspec/changes/commander-cli-parser/design.md +0 -107
  70. package/openspec/changes/commander-cli-parser/proposal.md +0 -31
  71. package/openspec/changes/commander-cli-parser/specs/cli/spec.md +0 -48
  72. package/openspec/changes/commander-cli-parser/tasks.md +0 -33
  73. package/openspec/changes/cross-platform-build-and-ci/.openspec.yaml +0 -2
  74. package/openspec/changes/cross-platform-build-and-ci/design.md +0 -213
  75. package/openspec/changes/cross-platform-build-and-ci/proposal.md +0 -50
  76. package/openspec/changes/cross-platform-build-and-ci/specs/path-and-file-mediation/spec.md +0 -335
  77. package/openspec/changes/cross-platform-build-and-ci/specs/release-pipeline/spec.md +0 -95
  78. package/openspec/changes/cross-platform-build-and-ci/tasks.md +0 -164
  79. package/openspec/config.yaml +0 -20
  80. package/openspec/specs/auth/spec.md +0 -394
  81. package/openspec/specs/cli/spec.md +0 -108
  82. package/openspec/specs/commands/spec.md +0 -409
  83. package/openspec/specs/configuration/spec.md +0 -130
  84. package/openspec/specs/conversations/spec.md +0 -139
  85. package/openspec/specs/domain-model/spec.md +0 -128
  86. package/openspec/specs/formatters/spec.md +0 -158
  87. package/openspec/specs/install-browser/spec.md +0 -146
  88. package/openspec/specs/interactive-prompt-loop/spec.md +0 -41
  89. package/openspec/specs/logger/spec.md +0 -71
  90. package/openspec/specs/mediator/spec.md +0 -72
  91. package/openspec/specs/multi-profile-conversations/spec.md +0 -145
  92. package/openspec/specs/path-utils/spec.md +0 -101
  93. package/openspec/specs/profiles/spec.md +0 -94
  94. package/openspec/specs/storage/spec.md +0 -198
  95. package/openspec/specs/testing/spec.md +0 -237
  96. package/openspec/specs/v2-installer/spec.md +0 -205
  97. package/openspec/specs/validators/spec.md +0 -65
  98. package/scripts/build.ts +0 -88
  99. package/scripts/clean-build.ps1 +0 -4
  100. package/scripts/clean-build.sh +0 -5
  101. package/scripts/install-browser.ps1 +0 -5
  102. package/scripts/install-browser.sh +0 -6
  103. package/scripts/lint-path-mediation.ps1 +0 -65
  104. package/scripts/lint-path-mediation.sh +0 -40
  105. package/tests/cli/auth-command.test.ts +0 -220
  106. package/tests/cli/command-registry.test.ts +0 -67
  107. package/tests/cli/continue-command.test.ts +0 -100
  108. package/tests/cli/delete-command.test.ts +0 -128
  109. package/tests/cli/export-all-command.test.ts +0 -375
  110. package/tests/cli/install-browser-command.test.ts +0 -48
  111. package/tests/cli/list-command.test.ts +0 -179
  112. package/tests/cli/new-command.test.ts +0 -139
  113. package/tests/cli/status-command.test.ts +0 -106
  114. package/tests/core/query-handlers.test.ts +0 -187
  115. package/tests/fixtures/auth-fixtures.ts +0 -98
  116. package/tests/fixtures/chat-fixtures.ts +0 -87
  117. package/tests/infrastructure/cli-table.test.ts +0 -107
  118. package/tests/infrastructure/formatters.test.ts +0 -202
  119. package/tests/infrastructure/io.test.ts +0 -197
  120. package/tests/infrastructure/logger.test.ts +0 -74
  121. package/tests/infrastructure/storage.test.ts +0 -335
  122. package/tests/infrastructure/validators.test.ts +0 -67
  123. package/tests/integration/commands/auth.test.ts +0 -186
  124. package/tests/integration/commands/continue.test.ts +0 -130
  125. package/tests/integration/commands/delete.test.ts +0 -140
  126. package/tests/integration/commands/export.test.ts +0 -266
  127. package/tests/integration/commands/fetch.test.ts +0 -238
  128. package/tests/integration/commands/list.test.ts +0 -281
  129. package/tests/integration/commands/profile.test.ts +0 -258
  130. package/tests/integration/commands/status.test.ts +0 -223
  131. package/tests/integration/installer-script-shape.test.ts +0 -67
  132. package/tests/parity/compare-outputs.ts +0 -359
  133. package/tests/parity/test-commands-parity.ps1 +0 -82
  134. package/tests/parity/test-commands-parity.sh +0 -61
  135. package/tests/services/auth-service.test.ts +0 -315
  136. package/tests/services/cookie-monitor.test.ts +0 -229
  137. package/tests/services/cookie-storage-service.test.ts +0 -230
  138. package/tests/services/gemini-client-wrapper.test.ts +0 -688
  139. package/tests/services/install-browser-service.test.ts +0 -83
  140. package/tests/services/playwright-cli-driver.test.ts +0 -468
  141. package/tests/services/profile-auth-manager.test.ts +0 -334
  142. package/tests/services/profile-service.test.ts +0 -224
  143. package/tests/setup.ts +0 -95
  144. package/tests/smoke/smoke.test.ts +0 -52
  145. package/tests/unit/config.test.ts +0 -159
  146. package/tests/unit/errors.test.ts +0 -119
  147. package/tests/unit/formatters.test.ts +0 -316
  148. package/tests/unit/mediator.test.ts +0 -152
  149. package/tests/unit/path-utils.test.ts +0 -289
  150. package/tsconfig.json +0 -16
@@ -1,159 +0,0 @@
1
- import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2
- import { existsSync, mkdirSync, readdirSync, writeFileSync, readFileSync, rmSync } from "node:fs";
3
- import { join } from "node:path";
4
- import {
5
- getDefaultProfileName,
6
- setDefaultProfileName,
7
- listProfiles,
8
- ensureConfigDir,
9
- } from "../../src/infrastructure/config.ts";
10
- import {
11
- setupTestConfig,
12
- teardownTestConfig,
13
- } from "../setup.ts";
14
-
15
- describe("config", () => {
16
- let configDir: string;
17
- let originalConfigDir: string | undefined;
18
-
19
- beforeEach(() => {
20
- originalConfigDir = process.env.GEMITERM_CONFIG_DIR;
21
- configDir = setupTestConfig();
22
- });
23
-
24
- afterEach(() => {
25
- teardownTestConfig({ GEMITERM_CONFIG_DIR: originalConfigDir });
26
- });
27
-
28
- describe("getDefaultProfileName", () => {
29
- test("returns 'default' when no marker file exists", () => {
30
- expect(getDefaultProfileName()).toBe("default");
31
- });
32
-
33
- test("returns the name from the marker file when it exists", () => {
34
- const profilesDir = join(configDir, "profiles");
35
- mkdirSync(profilesDir, { recursive: true });
36
- setDefaultProfileName("work");
37
- expect(getDefaultProfileName()).toBe("work");
38
- });
39
-
40
- test("trims whitespace from marker file content", () => {
41
- const profilesDir = join(configDir, "profiles");
42
- mkdirSync(profilesDir, { recursive: true });
43
- const markerPath = join(profilesDir, ".default");
44
- writeFileSync(markerPath, " my-profile \n", "utf-8");
45
- expect(getDefaultProfileName()).toBe("my-profile");
46
- });
47
- });
48
-
49
- describe("setDefaultProfileName", () => {
50
- test("creates marker file with the given name", () => {
51
- const markerPath = join(configDir, "profiles", ".default");
52
- expect(existsSync(markerPath)).toBe(false);
53
-
54
- setDefaultProfileName("personal");
55
-
56
- expect(existsSync(markerPath)).toBe(true);
57
- const content = readFileSync(markerPath, "utf-8");
58
- expect(content).toBe("personal");
59
- });
60
-
61
- test("creates profiles directory if it does not exist", () => {
62
- const profilesDir = join(configDir, "profiles");
63
- expect(existsSync(profilesDir)).toBe(false);
64
-
65
- setDefaultProfileName("work");
66
-
67
- expect(existsSync(profilesDir)).toBe(true);
68
- });
69
-
70
- test("overwrites existing marker file", () => {
71
- setDefaultProfileName("first");
72
- expect(getDefaultProfileName()).toBe("first");
73
-
74
- setDefaultProfileName("second");
75
- expect(getDefaultProfileName()).toBe("second");
76
- });
77
- });
78
-
79
- describe("listProfiles", () => {
80
- test("returns empty array when profiles directory does not exist", () => {
81
- const profilesDir = join(configDir, "profiles");
82
- expect(existsSync(profilesDir)).toBe(false);
83
-
84
- const profiles = listProfiles();
85
-
86
- expect(profiles).toEqual([]);
87
- });
88
-
89
- test("returns empty array when profiles directory exists but is empty", () => {
90
- mkdirSync(join(configDir, "profiles"), { recursive: true });
91
-
92
- const profiles = listProfiles();
93
-
94
- expect(profiles).toEqual([]);
95
- });
96
-
97
- test("returns sorted list of profile directories", () => {
98
- const profilesDir = join(configDir, "profiles");
99
- mkdirSync(join(profilesDir, "charlie"), { recursive: true });
100
- mkdirSync(join(profilesDir, "alpha"), { recursive: true });
101
- mkdirSync(join(profilesDir, "bravo"), { recursive: true });
102
-
103
- const profiles = listProfiles();
104
-
105
- expect(profiles).toEqual(["alpha", "bravo", "charlie"]);
106
- });
107
-
108
- test("excludes the default marker file (.default) from results", () => {
109
- const profilesDir = join(configDir, "profiles");
110
- mkdirSync(join(profilesDir, "work"), { recursive: true });
111
- mkdirSync(join(profilesDir, "personal"), { recursive: true });
112
- writeFileSync(join(profilesDir, ".default"), "work", "utf-8");
113
-
114
- const profiles = listProfiles();
115
-
116
- expect(profiles).toEqual(["personal", "work"]);
117
- });
118
-
119
- test("excludes files (only includes directories)", () => {
120
- const profilesDir = join(configDir, "profiles");
121
- mkdirSync(join(profilesDir, "valid-profile"), { recursive: true });
122
- writeFileSync(join(profilesDir, "not-a-profile.txt"), "data", "utf-8");
123
-
124
- const profiles = listProfiles();
125
-
126
- expect(profiles).toEqual(["valid-profile"]);
127
- });
128
- });
129
-
130
- describe("ensureConfigDir", () => {
131
- test("creates config directory when it does not exist", () => {
132
- rmSync(configDir, { recursive: true, force: true });
133
-
134
- ensureConfigDir();
135
-
136
- expect(existsSync(configDir)).toBe(true);
137
- });
138
-
139
- test("creates profiles directory inside config dir", () => {
140
- const profilesDir = join(configDir, "profiles");
141
-
142
- ensureConfigDir();
143
-
144
- expect(existsSync(profilesDir)).toBe(true);
145
- });
146
-
147
- test("returns the config directory path", () => {
148
- const result = ensureConfigDir();
149
-
150
- expect(result).toBe(configDir);
151
- });
152
-
153
- test("does not throw when directories already exist", () => {
154
- mkdirSync(join(configDir, "profiles"), { recursive: true });
155
-
156
- expect(() => ensureConfigDir()).not.toThrow();
157
- });
158
- });
159
- });
@@ -1,119 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
- import {
3
- GemitermError,
4
- AuthenticationError,
5
- CookieExpiredError,
6
- GeminiAPIError,
7
- ConversationNotFoundError,
8
- ConversationPendingError,
9
- } from "../../src/core/errors.ts";
10
-
11
- describe("errors", () => {
12
- test("all error classes extend GemitermError", () => {
13
- expect(new AuthenticationError()).toBeInstanceOf(GemitermError);
14
- expect(new CookieExpiredError()).toBeInstanceOf(GemitermError);
15
- expect(new GeminiAPIError("test")).toBeInstanceOf(GemitermError);
16
- expect(new ConversationNotFoundError("abc")).toBeInstanceOf(GemitermError);
17
- expect(new ConversationPendingError()).toBeInstanceOf(GemitermError);
18
- });
19
-
20
- test("all error classes extend Error", () => {
21
- expect(new GemitermError("x")).toBeInstanceOf(Error);
22
- expect(new AuthenticationError()).toBeInstanceOf(Error);
23
- expect(new CookieExpiredError()).toBeInstanceOf(Error);
24
- expect(new GeminiAPIError("x")).toBeInstanceOf(Error);
25
- expect(new ConversationNotFoundError("x")).toBeInstanceOf(Error);
26
- expect(new ConversationPendingError()).toBeInstanceOf(Error);
27
- });
28
-
29
- describe("GemitermError", () => {
30
- test("preserves the provided message", () => {
31
- const err = new GemitermError("something went wrong");
32
- expect(err.message).toBe("something went wrong");
33
- });
34
-
35
- test("has name set to 'GemitermError'", () => {
36
- expect(new GemitermError("").name).toBe("GemitermError");
37
- });
38
- });
39
-
40
- describe("AuthenticationError", () => {
41
- test("uses default message when none provided", () => {
42
- const err = new AuthenticationError();
43
- expect(err.message).toBe(
44
- "Not authenticated. Please run 'gemiterm login' first.",
45
- );
46
- });
47
-
48
- test("preserves custom message", () => {
49
- const err = new AuthenticationError("custom auth failure");
50
- expect(err.message).toBe("custom auth failure");
51
- });
52
-
53
- test("has name set to 'AuthenticationError'", () => {
54
- expect(new AuthenticationError().name).toBe("AuthenticationError");
55
- });
56
- });
57
-
58
- describe("CookieExpiredError", () => {
59
- test("uses default message when none provided", () => {
60
- const err = new CookieExpiredError();
61
- expect(err.message).toBe(
62
- "Session has expired. Please run 'gemiterm login' again.",
63
- );
64
- });
65
-
66
- test("preserves custom message", () => {
67
- const err = new CookieExpiredError("token expired at 12:00");
68
- expect(err.message).toBe("token expired at 12:00");
69
- });
70
-
71
- test("has name set to 'CookieExpiredError'", () => {
72
- expect(new CookieExpiredError().name).toBe("CookieExpiredError");
73
- });
74
- });
75
-
76
- describe("GeminiAPIError", () => {
77
- test("preserves the provided message", () => {
78
- const err = new GeminiAPIError("rate limit exceeded");
79
- expect(err.message).toBe("rate limit exceeded");
80
- });
81
-
82
- test("has name set to 'GeminiAPIError'", () => {
83
- expect(new GeminiAPIError("").name).toBe("GeminiAPIError");
84
- });
85
- });
86
-
87
- describe("ConversationNotFoundError", () => {
88
- test("includes the conversation ID in the message", () => {
89
- const err = new ConversationNotFoundError("conv-123");
90
- expect(err.message).toBe("Conversation 'conv-123' not found.");
91
- });
92
-
93
- test("has name set to 'ConversationNotFoundError'", () => {
94
- expect(new ConversationNotFoundError("x").name).toBe(
95
- "ConversationNotFoundError",
96
- );
97
- });
98
- });
99
-
100
- describe("ConversationPendingError", () => {
101
- test("uses default message when none provided", () => {
102
- const err = new ConversationPendingError();
103
- expect(err.message).toBe(
104
- "Conversation operation is still pending.",
105
- );
106
- });
107
-
108
- test("preserves custom message", () => {
109
- const err = new ConversationPendingError("still generating");
110
- expect(err.message).toBe("still generating");
111
- });
112
-
113
- test("has name set to 'ConversationPendingError'", () => {
114
- expect(new ConversationPendingError().name).toBe(
115
- "ConversationPendingError",
116
- );
117
- });
118
- });
119
- });
@@ -1,316 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
- import {
3
- formatChatAsMarkdown,
4
- formatChatAsJson,
5
- formatProfileTable,
6
- formatChatList,
7
- } from "../../src/infrastructure/formatters.ts";
8
- import type { Message, ChatInfo, ProfileStatus } from "../../src/core/types.ts";
9
-
10
- describe("formatters", () => {
11
- describe("formatChatAsMarkdown", () => {
12
- const messages: Message[] = [
13
- { role: "user", content: "Hello Gemini" },
14
- { role: "model", content: "Hello! How can I help you?" },
15
- ];
16
-
17
- test("produces markdown with title heading", () => {
18
- const result = formatChatAsMarkdown(messages, "Test Chat");
19
- expect(result).toContain("# Test Chat");
20
- });
21
-
22
- test("separates title heading with blank line", () => {
23
- const result = formatChatAsMarkdown(messages, "Test Chat");
24
- expect(result).toContain("# Test Chat\n\n");
25
- });
26
-
27
- test("labels user messages as **You:**", () => {
28
- const result = formatChatAsMarkdown(messages, "Test Chat");
29
- expect(result).toContain("**You:**");
30
- });
31
-
32
- test("labels model messages as **Gemini:**", () => {
33
- const result = formatChatAsMarkdown(messages, "Test Chat");
34
- expect(result).toContain("**Gemini:**");
35
- });
36
-
37
- test("includes message content after label", () => {
38
- const result = formatChatAsMarkdown(messages, "Test Chat");
39
- expect(result).toContain("**You:**\n\nHello Gemini");
40
- });
41
-
42
- test("separates messages with horizontal rule", () => {
43
- const result = formatChatAsMarkdown(messages, "Test Chat");
44
- expect(result).toContain("---");
45
- });
46
-
47
- test("handles empty messages array", () => {
48
- const result = formatChatAsMarkdown([], "Empty Chat");
49
- expect(result).toContain("# Empty Chat");
50
- expect(result).not.toContain("**You:**");
51
- expect(result).not.toContain("**Gemini:**");
52
- });
53
-
54
- test("handles single message", () => {
55
- const single: Message[] = [{ role: "user", content: "Solo message" }];
56
- const result = formatChatAsMarkdown(single, "Solo");
57
- expect(result).toContain("**You:**");
58
- expect(result).toContain("Solo message");
59
- expect(result).toContain("---");
60
- });
61
-
62
- test("includes metadata when includeMetadata and conversationId are provided", () => {
63
- const result = formatChatAsMarkdown(messages, "Test", "conv-123", true);
64
- expect(result).toContain("> Conversation ID: conv-123");
65
- expect(result).toContain("> Messages: 2");
66
- expect(result).toContain("> Exported:");
67
- });
68
-
69
- test("excludes metadata when includeMetadata is false", () => {
70
- const result = formatChatAsMarkdown(messages, "Test", "conv-123", false);
71
- expect(result).not.toContain("> Conversation ID:");
72
- });
73
-
74
- test("excludes metadata when includeMetadata is undefined", () => {
75
- const result = formatChatAsMarkdown(messages, "Test", "conv-123");
76
- expect(result).not.toContain("> Conversation ID:");
77
- });
78
-
79
- test("excludes metadata when conversationId is missing", () => {
80
- const result = formatChatAsMarkdown(messages, "Test", undefined, true);
81
- expect(result).not.toContain("> Conversation ID:");
82
- });
83
- });
84
-
85
- describe("formatChatAsJson", () => {
86
- const messages: Message[] = [
87
- { role: "user", content: "Hello" },
88
- { role: "model", content: "Hi there" },
89
- ];
90
-
91
- test("produces valid JSON", () => {
92
- const result = formatChatAsJson(messages, "conv-1");
93
- expect(() => JSON.parse(result)).not.toThrow();
94
- });
95
-
96
- test("includes conversationId in output", () => {
97
- const result = formatChatAsJson(messages, "conv-1");
98
- const parsed = JSON.parse(result);
99
- expect(parsed.conversationId).toBe("conv-1");
100
- });
101
-
102
- test("includes all messages in output", () => {
103
- const result = formatChatAsJson(messages, "conv-1");
104
- const parsed = JSON.parse(result);
105
- expect(parsed.messages).toHaveLength(2);
106
- expect(parsed.messages[0].role).toBe("user");
107
- expect(parsed.messages[1].role).toBe("model");
108
- });
109
-
110
- test("preserves message content", () => {
111
- const result = formatChatAsJson(messages, "conv-1");
112
- const parsed = JSON.parse(result);
113
- expect(parsed.messages[0].content).toBe("Hello");
114
- expect(parsed.messages[1].content).toBe("Hi there");
115
- });
116
-
117
- test("pretty-prints JSON with 2-space indentation", () => {
118
- const result = formatChatAsJson(messages, "conv-1");
119
- expect(result).toContain(" \"conversationId\"");
120
- });
121
-
122
- test("handles empty messages array", () => {
123
- const result = formatChatAsJson([], "conv-empty");
124
- const parsed = JSON.parse(result);
125
- expect(parsed.messages).toEqual([]);
126
- expect(parsed.conversationId).toBe("conv-empty");
127
- });
128
- });
129
-
130
- describe("formatProfileTable", () => {
131
- test("returns 'No profiles found' message for empty array", () => {
132
- const result = formatProfileTable([]);
133
- expect(result).toContain("No profiles found");
134
- expect(result).toContain("gemiterm login");
135
- });
136
-
137
- test("includes header row with column names", () => {
138
- const statuses: ProfileStatus[] = [
139
- { name: "default", exists: true, isActive: true, expiresAt: null, isDefault: false },
140
- ];
141
- const result = formatProfileTable(statuses);
142
- expect(result).toContain("NAME");
143
- expect(result).toContain("ACTIVE");
144
- expect(result).toContain("EXPIRES");
145
- expect(result).toContain("DEFAULT");
146
- });
147
-
148
- test("includes separator row", () => {
149
- const statuses: ProfileStatus[] = [
150
- { name: "default", exists: true, isActive: true, expiresAt: null, isDefault: false },
151
- ];
152
- const result = formatProfileTable(statuses);
153
- const lines = result.split("\n");
154
- const headerIdx = lines.findIndex((l) => l.includes("NAME"));
155
- expect(headerIdx).toBeGreaterThanOrEqual(0);
156
- expect(lines[headerIdx + 1]).toMatch(/[\u2500-\u257f]/);
157
- });
158
-
159
- test("shows profile name", () => {
160
- const statuses: ProfileStatus[] = [
161
- { name: "work", exists: true, isActive: true, expiresAt: null, isDefault: false },
162
- ];
163
- const result = formatProfileTable(statuses);
164
- expect(result).toContain("work");
165
- });
166
-
167
- test("marks default profile with asterisk", () => {
168
- const statuses: ProfileStatus[] = [
169
- { name: "default", exists: true, isActive: true, expiresAt: null, isDefault: true },
170
- ];
171
- const result = formatProfileTable(statuses);
172
- expect(result).toContain("default");
173
- expect(result).toContain("Yes");
174
- expect(result).toContain("* = default profile");
175
- });
176
-
177
- test("shows 'Session' for active profile with null expiresAt (session cookie)", () => {
178
- const statuses: ProfileStatus[] = [
179
- { name: "default", exists: true, isActive: true, expiresAt: null, isDefault: false },
180
- ];
181
- const result = formatProfileTable(statuses);
182
- expect(result).toContain("Session");
183
- expect(result).not.toContain("N/A");
184
- });
185
-
186
- test("shows N/A for expiresAt when inactive and null", () => {
187
- const statuses: ProfileStatus[] = [
188
- { name: "default", exists: true, isActive: false, expiresAt: null, isDefault: false },
189
- ];
190
- const result = formatProfileTable(statuses);
191
- expect(result).toContain("N/A");
192
- });
193
-
194
- test("shows dash for profile that does not exist", () => {
195
- const statuses: ProfileStatus[] = [
196
- { name: "missing", exists: false, isActive: false, expiresAt: null, isDefault: false },
197
- ];
198
- const result = formatProfileTable(statuses);
199
- const lines = result.split("\n");
200
- const dataLine = lines.find((l) => l.includes("missing"));
201
- expect(dataLine).toBeTruthy();
202
- });
203
-
204
- test("handles multiple profiles", () => {
205
- const statuses: ProfileStatus[] = [
206
- { name: "default", exists: true, isActive: true, expiresAt: "2026-12-31T00:00:00Z", isDefault: true },
207
- { name: "work", exists: true, isActive: false, expiresAt: "2026-06-30T00:00:00Z", isDefault: false },
208
- ];
209
- const result = formatProfileTable(statuses);
210
- expect(result).toContain("default");
211
- expect(result).toContain("work");
212
- });
213
-
214
- test("formats expiration date", () => {
215
- const statuses: ProfileStatus[] = [
216
- { name: "default", exists: true, isActive: true, expiresAt: "2026-12-31T00:00:00Z", isDefault: false },
217
- ];
218
- const result = formatProfileTable(statuses);
219
- expect(result).toContain("2026");
220
- });
221
-
222
- test("does not truncate 'Yes' in DEFAULT column for default profile", () => {
223
- const statuses: ProfileStatus[] = [
224
- { name: "default", exists: true, isActive: true, expiresAt: null, isDefault: true },
225
- ];
226
- const result = formatProfileTable(statuses);
227
- expect(result).toContain("Yes");
228
- expect(result).not.toContain("Ye\u2026");
229
- });
230
- });
231
-
232
- describe("formatChatList", () => {
233
- test("returns 'No conversations found' message for empty array", () => {
234
- const result = formatChatList([]);
235
- expect(result).toContain("No conversations found");
236
- });
237
-
238
- test("includes header row with column names", () => {
239
- const chats: ChatInfo[] = [
240
- { id: "abc123", title: "Test Chat", isPinned: false, timestamp: Date.now() },
241
- ];
242
- const result = formatChatList(chats);
243
- expect(result).toContain("ID");
244
- expect(result).toContain("TITLE");
245
- expect(result).toContain("DATE");
246
- expect(result).toContain("PIN");
247
- });
248
-
249
- test("includes separator row", () => {
250
- const chats: ChatInfo[] = [
251
- { id: "abc123", title: "Test Chat", isPinned: false, timestamp: Date.now() },
252
- ];
253
- const result = formatChatList(chats);
254
- const lines = result.split("\n");
255
- const headerIdx = lines.findIndex((l) => l.includes("ID"));
256
- expect(headerIdx).toBeGreaterThanOrEqual(0);
257
- expect(lines[headerIdx + 1]).toMatch(/[\u2500-\u257f]/);
258
- });
259
-
260
- test("shows chat title", () => {
261
- const chats: ChatInfo[] = [
262
- { id: "abc123", title: "My Chat", isPinned: false, timestamp: Date.now() },
263
- ];
264
- const result = formatChatList(chats);
265
- expect(result).toContain("My Chat");
266
- });
267
-
268
- test("shows total count footer", () => {
269
- const chats: ChatInfo[] = [
270
- { id: "abc123", title: "Chat 1", isPinned: false, timestamp: Date.now() },
271
- { id: "def456", title: "Chat 2", isPinned: false, timestamp: Date.now() },
272
- ];
273
- const result = formatChatList(chats);
274
- expect(result).toContain("Total: 2 conversations");
275
- });
276
-
277
- test("uses singular 'conversation' for single item", () => {
278
- const chats: ChatInfo[] = [
279
- { id: "abc123", title: "Chat 1", isPinned: false, timestamp: Date.now() },
280
- ];
281
- const result = formatChatList(chats);
282
- expect(result).toContain("Total: 1 conversation");
283
- });
284
-
285
- test("truncates long titles", () => {
286
- const longTitle = "A".repeat(100);
287
- const chats: ChatInfo[] = [
288
- { id: "abc123", title: longTitle, isPinned: false, timestamp: Date.now() },
289
- ];
290
- const result = formatChatList(chats);
291
- expect(result).toContain("\u2026");
292
- });
293
-
294
- test("handles multiple chats", () => {
295
- const chats: ChatInfo[] = [
296
- { id: "abc123", title: "First", isPinned: false, timestamp: 1000000 },
297
- { id: "def456", title: "Second", isPinned: true, timestamp: 2000000 },
298
- { id: "ghi789", title: "Third", isPinned: false, timestamp: 3000000 },
299
- ];
300
- const result = formatChatList(chats);
301
- expect(result).toContain("First");
302
- expect(result).toContain("Second");
303
- expect(result).toContain("Third");
304
- expect(result).toContain("Total: 3 conversations");
305
- });
306
-
307
- test("shows formatted date", () => {
308
- const chats: ChatInfo[] = [
309
- { id: "abc123", title: "Test", isPinned: false, timestamp: Date.now() },
310
- ];
311
- const result = formatChatList(chats);
312
- const currentYear = new Date().getFullYear().toString();
313
- expect(result).toContain(currentYear);
314
- });
315
- });
316
- });