gemiterm 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/.env +2 -0
  2. package/.github/workflows/manual-release.yml +34 -0
  3. package/.github/workflows/release.yml +198 -0
  4. package/.github/workflows/test.yml +29 -0
  5. package/.opencode/commands/opsx-apply.md +152 -0
  6. package/.opencode/commands/opsx-archive.md +157 -0
  7. package/.opencode/commands/opsx-explore.md +169 -0
  8. package/.opencode/commands/opsx-propose.md +104 -0
  9. package/.opencode/commands/opsx-sync.md +140 -0
  10. package/.opencode/commands/search-intelligently.md +9 -0
  11. package/.opencode/opencode.json +20 -0
  12. package/.opencode/skills/intellisearch/.version +1 -0
  13. package/.opencode/skills/intellisearch/SKILL.md +84 -0
  14. package/.opencode/skills/intellisearch/references/brave-search.md +67 -0
  15. package/.opencode/skills/intellisearch/references/ddg-search.md +79 -0
  16. package/.opencode/skills/intellisearch/references/deepwiki-tools.md +246 -0
  17. package/.opencode/skills/intellisearch/references/examples.md +97 -0
  18. package/.opencode/skills/intellisearch/references/gh-cli.md +143 -0
  19. package/.opencode/skills/intellisearch/references/google-search.md +40 -0
  20. package/.opencode/skills/intellisearch/references/search-workflow.md +143 -0
  21. package/.opencode/skills/intellisearch/references/workflow.md +115 -0
  22. package/.opencode/skills/openspec-apply-change/SKILL.md +159 -0
  23. package/.opencode/skills/openspec-archive-change/SKILL.md +117 -0
  24. package/.opencode/skills/openspec-explore/SKILL.md +287 -0
  25. package/.opencode/skills/openspec-propose/SKILL.md +111 -0
  26. package/.opencode/skills/openspec-sync-specs/SKILL.md +147 -0
  27. package/.serena/project.yml +133 -0
  28. package/AGENTS.md +136 -0
  29. package/CHANGELOG.md +3 -0
  30. package/README.md +245 -0
  31. package/bun.lock +105 -0
  32. package/docs/INSTALL.md +185 -0
  33. package/docs/PLAYWRIGHT_CLI_API.md +306 -0
  34. package/docs/python-release-for-reference.md +175 -0
  35. package/examples/sample_json_export.json +32 -0
  36. package/examples/sample_markdown_export.md +221 -0
  37. package/examples/usage_examples.sh +222 -0
  38. package/install.ps1 +264 -0
  39. package/install.sh +275 -0
  40. package/openspec/MAESTRO_MIGRATION.md +180 -0
  41. package/openspec/changes/archive/2026-06-08-command-spec-conformance/.openspec.yaml +2 -0
  42. package/openspec/changes/archive/2026-06-08-command-spec-conformance/design.md +194 -0
  43. package/openspec/changes/archive/2026-06-08-command-spec-conformance/proposal.md +52 -0
  44. package/openspec/changes/archive/2026-06-08-command-spec-conformance/specs/multi-profile-conversations/spec.md +133 -0
  45. package/openspec/changes/archive/2026-06-08-command-spec-conformance/tasks.md +95 -0
  46. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/.openspec.yaml +2 -0
  47. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/design.md +28 -0
  48. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/proposal.md +22 -0
  49. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/specs/interactive-prompt-loop/spec.md +35 -0
  50. package/openspec/changes/archive/2026-06-09-refactor-interactive-prompt-loop/tasks.md +28 -0
  51. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/.openspec.yaml +2 -0
  52. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/design.md +323 -0
  53. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/proposal.md +122 -0
  54. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/specs/no-capability-changes/spec.md +29 -0
  55. package/openspec/changes/archive/2026-06-09-replace-gemini-api-placeholder-with-gemini-reverse/tasks.md +172 -0
  56. package/openspec/changes/archive/2026-06-09-v2-install-migration/.openspec.yaml +2 -0
  57. package/openspec/changes/archive/2026-06-09-v2-install-migration/design.md +241 -0
  58. package/openspec/changes/archive/2026-06-09-v2-install-migration/proposal.md +41 -0
  59. package/openspec/changes/archive/2026-06-09-v2-install-migration/specs/v2-installer/spec.md +205 -0
  60. package/openspec/changes/archive/2026-06-09-v2-install-migration/tasks.md +97 -0
  61. package/openspec/changes/commander-cli-parser/.openspec.yaml +2 -0
  62. package/openspec/changes/commander-cli-parser/design.md +107 -0
  63. package/openspec/changes/commander-cli-parser/proposal.md +31 -0
  64. package/openspec/changes/commander-cli-parser/specs/cli/spec.md +48 -0
  65. package/openspec/changes/commander-cli-parser/tasks.md +33 -0
  66. package/openspec/changes/cross-platform-build-and-ci/.openspec.yaml +2 -0
  67. package/openspec/changes/cross-platform-build-and-ci/design.md +213 -0
  68. package/openspec/changes/cross-platform-build-and-ci/proposal.md +50 -0
  69. package/openspec/changes/cross-platform-build-and-ci/specs/path-and-file-mediation/spec.md +335 -0
  70. package/openspec/changes/cross-platform-build-and-ci/specs/release-pipeline/spec.md +95 -0
  71. package/openspec/changes/cross-platform-build-and-ci/tasks.md +164 -0
  72. package/openspec/config.yaml +20 -0
  73. package/openspec/specs/auth/spec.md +394 -0
  74. package/openspec/specs/cli/spec.md +108 -0
  75. package/openspec/specs/commands/spec.md +409 -0
  76. package/openspec/specs/configuration/spec.md +130 -0
  77. package/openspec/specs/conversations/spec.md +139 -0
  78. package/openspec/specs/domain-model/spec.md +128 -0
  79. package/openspec/specs/formatters/spec.md +158 -0
  80. package/openspec/specs/install-browser/spec.md +146 -0
  81. package/openspec/specs/interactive-prompt-loop/spec.md +41 -0
  82. package/openspec/specs/logger/spec.md +71 -0
  83. package/openspec/specs/mediator/spec.md +72 -0
  84. package/openspec/specs/multi-profile-conversations/spec.md +145 -0
  85. package/openspec/specs/path-utils/spec.md +101 -0
  86. package/openspec/specs/profiles/spec.md +94 -0
  87. package/openspec/specs/storage/spec.md +198 -0
  88. package/openspec/specs/testing/spec.md +237 -0
  89. package/openspec/specs/v2-installer/spec.md +205 -0
  90. package/openspec/specs/validators/spec.md +65 -0
  91. package/package.json +45 -0
  92. package/scripts/build.ts +88 -0
  93. package/scripts/clean-build.ps1 +4 -0
  94. package/scripts/clean-build.sh +5 -0
  95. package/scripts/install-browser.ps1 +5 -0
  96. package/scripts/install-browser.sh +6 -0
  97. package/scripts/lint-path-mediation.ps1 +65 -0
  98. package/scripts/lint-path-mediation.sh +40 -0
  99. package/src/cli/.gitkeep +0 -0
  100. package/src/cli/command-registry.ts +62 -0
  101. package/src/cli/commands/auth-command.ts +179 -0
  102. package/src/cli/commands/continue-command.ts +158 -0
  103. package/src/cli/commands/delete-command.ts +163 -0
  104. package/src/cli/commands/export-all-command.ts +243 -0
  105. package/src/cli/commands/export-command.ts +156 -0
  106. package/src/cli/commands/fetch-command.ts +176 -0
  107. package/src/cli/commands/help.ts +45 -0
  108. package/src/cli/commands/install-browser-command.ts +33 -0
  109. package/src/cli/commands/list-command.ts +226 -0
  110. package/src/cli/commands/new-command.ts +147 -0
  111. package/src/cli/commands/profile-command.ts +226 -0
  112. package/src/cli/commands/status-command.ts +59 -0
  113. package/src/cli/index.ts +213 -0
  114. package/src/cli/utils/interactive-prompt.ts +64 -0
  115. package/src/core/.gitkeep +0 -0
  116. package/src/core/command-handlers.ts +240 -0
  117. package/src/core/errors.ts +41 -0
  118. package/src/core/mediator.ts +58 -0
  119. package/src/core/query-handlers.ts +163 -0
  120. package/src/core/types.ts +43 -0
  121. package/src/infrastructure/.gitkeep +0 -0
  122. package/src/infrastructure/cli-table.ts +45 -0
  123. package/src/infrastructure/config.ts +52 -0
  124. package/src/infrastructure/formatters.ts +130 -0
  125. package/src/infrastructure/io.ts +161 -0
  126. package/src/infrastructure/logger.ts +41 -0
  127. package/src/infrastructure/path-utils.ts +157 -0
  128. package/src/infrastructure/storage.ts +209 -0
  129. package/src/infrastructure/validators.ts +28 -0
  130. package/src/services/.gitkeep +0 -0
  131. package/src/services/auth-service.ts +135 -0
  132. package/src/services/cookie-monitor.ts +159 -0
  133. package/src/services/cookie-storage-service.ts +74 -0
  134. package/src/services/gemini-client-wrapper.ts +239 -0
  135. package/src/services/install-browser-service.ts +205 -0
  136. package/src/services/playwright-cli-driver.ts +269 -0
  137. package/src/services/profile-auth-manager.ts +65 -0
  138. package/src/services/profile-service.ts +121 -0
  139. package/tests/cli/auth-command.test.ts +220 -0
  140. package/tests/cli/command-registry.test.ts +67 -0
  141. package/tests/cli/continue-command.test.ts +100 -0
  142. package/tests/cli/delete-command.test.ts +128 -0
  143. package/tests/cli/export-all-command.test.ts +375 -0
  144. package/tests/cli/install-browser-command.test.ts +48 -0
  145. package/tests/cli/list-command.test.ts +179 -0
  146. package/tests/cli/new-command.test.ts +139 -0
  147. package/tests/cli/status-command.test.ts +106 -0
  148. package/tests/core/query-handlers.test.ts +187 -0
  149. package/tests/fixtures/auth-fixtures.ts +98 -0
  150. package/tests/fixtures/chat-fixtures.ts +87 -0
  151. package/tests/infrastructure/cli-table.test.ts +107 -0
  152. package/tests/infrastructure/formatters.test.ts +202 -0
  153. package/tests/infrastructure/io.test.ts +197 -0
  154. package/tests/infrastructure/logger.test.ts +74 -0
  155. package/tests/infrastructure/storage.test.ts +335 -0
  156. package/tests/infrastructure/validators.test.ts +67 -0
  157. package/tests/integration/commands/auth.test.ts +186 -0
  158. package/tests/integration/commands/continue.test.ts +130 -0
  159. package/tests/integration/commands/delete.test.ts +140 -0
  160. package/tests/integration/commands/export.test.ts +266 -0
  161. package/tests/integration/commands/fetch.test.ts +238 -0
  162. package/tests/integration/commands/list.test.ts +281 -0
  163. package/tests/integration/commands/profile.test.ts +258 -0
  164. package/tests/integration/commands/status.test.ts +223 -0
  165. package/tests/integration/installer-script-shape.test.ts +67 -0
  166. package/tests/parity/compare-outputs.ts +359 -0
  167. package/tests/parity/test-commands-parity.ps1 +82 -0
  168. package/tests/parity/test-commands-parity.sh +61 -0
  169. package/tests/services/auth-service.test.ts +315 -0
  170. package/tests/services/cookie-monitor.test.ts +229 -0
  171. package/tests/services/cookie-storage-service.test.ts +230 -0
  172. package/tests/services/gemini-client-wrapper.test.ts +688 -0
  173. package/tests/services/install-browser-service.test.ts +83 -0
  174. package/tests/services/playwright-cli-driver.test.ts +468 -0
  175. package/tests/services/profile-auth-manager.test.ts +334 -0
  176. package/tests/services/profile-service.test.ts +224 -0
  177. package/tests/setup.ts +95 -0
  178. package/tests/smoke/smoke.test.ts +52 -0
  179. package/tests/unit/config.test.ts +159 -0
  180. package/tests/unit/errors.test.ts +119 -0
  181. package/tests/unit/formatters.test.ts +316 -0
  182. package/tests/unit/mediator.test.ts +152 -0
  183. package/tests/unit/path-utils.test.ts +289 -0
  184. package/tsconfig.json +16 -0
@@ -0,0 +1,152 @@
1
+ import { describe, test, expect, beforeEach } from "bun:test";
2
+ import {
3
+ Mediator,
4
+ type QueryHandler,
5
+ type CommandHandler,
6
+ type Query,
7
+ type Command,
8
+ } from "../../src/core/mediator.ts";
9
+
10
+ describe("Mediator", () => {
11
+ let mediator: Mediator;
12
+
13
+ beforeEach(() => {
14
+ mediator = new Mediator();
15
+ });
16
+
17
+ describe("registerQueryHandler and send (queries)", () => {
18
+ test("resolves a query via a registered handler", async () => {
19
+ const handler: QueryHandler<string, number> = {
20
+ queryType: "GetLength",
21
+ handle(query: Query<string>) {
22
+ return Promise.resolve(query.payload.length);
23
+ },
24
+ };
25
+
26
+ mediator.registerQueryHandler(handler);
27
+
28
+ const result = await mediator.send<number>({
29
+ type: "GetLength",
30
+ payload: "hello",
31
+ });
32
+
33
+ expect(result).toBe(5);
34
+ });
35
+
36
+ test("throws if two handlers are registered for the same query type", () => {
37
+ const handlerA: QueryHandler<void, void> = {
38
+ queryType: "Duplicate",
39
+ handle() {
40
+ return Promise.resolve();
41
+ },
42
+ };
43
+ const handlerB: QueryHandler<void, void> = {
44
+ queryType: "Duplicate",
45
+ handle() {
46
+ return Promise.resolve();
47
+ },
48
+ };
49
+
50
+ mediator.registerQueryHandler(handlerA);
51
+
52
+ expect(() => mediator.registerQueryHandler(handlerB)).toThrow(
53
+ "Query handler already registered for type: Duplicate",
54
+ );
55
+ });
56
+
57
+ test("passes the full query object to the handler", async () => {
58
+ let received: Query<{ key: string }> | undefined;
59
+ const handler: QueryHandler<{ key: string }, string> = {
60
+ queryType: "Inspect",
61
+ handle(query) {
62
+ received = query;
63
+ return Promise.resolve("ok");
64
+ },
65
+ };
66
+
67
+ mediator.registerQueryHandler(handler);
68
+
69
+ await mediator.send({ type: "Inspect", payload: { key: "val" } });
70
+
71
+ expect(received).toBeDefined();
72
+ expect(received!.type).toBe("Inspect");
73
+ expect(received!.payload).toEqual({ key: "val" });
74
+ });
75
+ });
76
+
77
+ describe("registerCommandHandler and send (commands)", () => {
78
+ test("resolves a command via a registered handler", async () => {
79
+ const handler: CommandHandler<string, string> = {
80
+ commandType: "Echo",
81
+ handle(cmd: Command<string>) {
82
+ return Promise.resolve(cmd.payload);
83
+ },
84
+ };
85
+
86
+ mediator.registerCommandHandler(handler);
87
+
88
+ const result = await mediator.send<string>({
89
+ type: "Echo",
90
+ payload: "hello world",
91
+ });
92
+
93
+ expect(result).toBe("hello world");
94
+ });
95
+
96
+ test("throws if two handlers are registered for the same command type", () => {
97
+ const handlerA: CommandHandler<void, void> = {
98
+ commandType: "Duplicate",
99
+ handle() {
100
+ return Promise.resolve();
101
+ },
102
+ };
103
+ const handlerB: CommandHandler<void, void> = {
104
+ commandType: "Duplicate",
105
+ handle() {
106
+ return Promise.resolve();
107
+ },
108
+ };
109
+
110
+ mediator.registerCommandHandler(handlerA);
111
+
112
+ expect(() => mediator.registerCommandHandler(handlerB)).toThrow(
113
+ "Command handler already registered for type: Duplicate",
114
+ );
115
+ });
116
+ });
117
+
118
+ describe("unknown message type", () => {
119
+ test("throws when sending a message with no registered handler", async () => {
120
+ await expect(
121
+ mediator.send({ type: "NobodyHandlesMe", payload: null }),
122
+ ).rejects.toThrow(
123
+ "No handler registered for message type: NobodyHandlesMe",
124
+ );
125
+ });
126
+
127
+ test("does not mix query and command handlers", async () => {
128
+ mediator.registerQueryHandler<void, string>({
129
+ queryType: "OnlyQuery",
130
+ handle() {
131
+ return Promise.resolve("query-result");
132
+ },
133
+ });
134
+
135
+ await expect(
136
+ mediator.send({ type: "OnlyQuery", payload: undefined }),
137
+ ).resolves.toBe("query-result");
138
+
139
+ const fresh = new Mediator();
140
+ fresh.registerCommandHandler<void, string>({
141
+ commandType: "OnlyCommand",
142
+ handle() {
143
+ return Promise.resolve("command-result");
144
+ },
145
+ });
146
+
147
+ await expect(
148
+ fresh.send({ type: "OnlyCommand", payload: undefined }),
149
+ ).resolves.toBe("command-result");
150
+ });
151
+ });
152
+ });
@@ -0,0 +1,289 @@
1
+ import { describe, test, expect, beforeEach, afterEach, spyOn } from "bun:test";
2
+ import { join, resolve } from "node:path";
3
+ import * as os from "node:os";
4
+ import * as fs from "node:fs";
5
+ import {
6
+ resolvePath,
7
+ getConfigDir,
8
+ getProfilesDir,
9
+ getProfilePath,
10
+ getProfileDir,
11
+ getDefaultProfileMarkerPath,
12
+ isWSL,
13
+ getProjectRoot,
14
+ getPackageJson,
15
+ STORAGE_STATE_FILE,
16
+ PROFILES_DIR,
17
+ DEFAULT_PROFILE_MARKER,
18
+ } from "../../src/infrastructure/path-utils.ts";
19
+
20
+ describe("path-utils", () => {
21
+ describe("constants", () => {
22
+ test("STORAGE_STATE_FILE is storage_state.json", () => {
23
+ expect(STORAGE_STATE_FILE).toBe("storage_state.json");
24
+ });
25
+
26
+ test("PROFILES_DIR is profiles", () => {
27
+ expect(PROFILES_DIR).toBe("profiles");
28
+ });
29
+
30
+ test("DEFAULT_PROFILE_MARKER is .default", () => {
31
+ expect(DEFAULT_PROFILE_MARKER).toBe(".default");
32
+ });
33
+ });
34
+
35
+ describe("resolvePath", () => {
36
+ test("joins and resolves path segments", () => {
37
+ const result = resolvePath("foo", "bar", "baz.txt");
38
+ expect(result).toBe(resolve(join("foo", "bar", "baz.txt")));
39
+ });
40
+
41
+ test("resolves relative paths to absolute", () => {
42
+ const result = resolvePath("a", "b");
43
+ expect(result).toBe(resolve(join("a", "b")));
44
+ });
45
+
46
+ test("handles single segment", () => {
47
+ const result = resolvePath("single");
48
+ expect(result).toBe(resolve("single"));
49
+ });
50
+
51
+ test("normalizes paths with .. and .", () => {
52
+ const result = resolvePath("foo", "..", "bar");
53
+ expect(result).toBe(resolve(join("foo", "..", "bar")));
54
+ });
55
+
56
+ test("handles empty input", () => {
57
+ const result = resolvePath();
58
+ expect(result).toBe(resolve(join()));
59
+ });
60
+ });
61
+
62
+ describe("getConfigDir", () => {
63
+ let originalConfigDir: string | undefined;
64
+ let originalAppData: string | undefined;
65
+
66
+ beforeEach(() => {
67
+ originalConfigDir = process.env.GEMITERM_CONFIG_DIR;
68
+ originalAppData = process.env.APPDATA;
69
+ delete process.env.GEMITERM_CONFIG_DIR;
70
+ });
71
+
72
+ afterEach(() => {
73
+ if (originalConfigDir !== undefined) {
74
+ process.env.GEMITERM_CONFIG_DIR = originalConfigDir;
75
+ } else {
76
+ delete process.env.GEMITERM_CONFIG_DIR;
77
+ }
78
+ if (originalAppData !== undefined) {
79
+ process.env.APPDATA = originalAppData;
80
+ } else {
81
+ delete process.env.APPDATA;
82
+ }
83
+ });
84
+
85
+ test("returns GEMITERM_CONFIG_DIR env override when set", () => {
86
+ process.env.GEMITERM_CONFIG_DIR = "/tmp/my-custom-config";
87
+ expect(getConfigDir()).toBe("/tmp/my-custom-config");
88
+ });
89
+
90
+ test("on win32 with APPDATA, returns APPDATA/gemiterm", () => {
91
+ const platformSpy = spyOn(os, "platform").mockReturnValue("win32");
92
+ process.env.APPDATA = "C:\\Users\\test\\AppData\\Roaming";
93
+ expect(getConfigDir()).toBe(join("C:\\Users\\test\\AppData\\Roaming", "gemiterm"));
94
+ platformSpy.mockRestore();
95
+ });
96
+
97
+ test("on win32 without APPDATA, falls back to homedir/gemiterm", () => {
98
+ const platformSpy = spyOn(os, "platform").mockReturnValue("win32");
99
+ delete process.env.APPDATA;
100
+ expect(getConfigDir()).toBe(join(os.homedir(), "gemiterm"));
101
+ platformSpy.mockRestore();
102
+ });
103
+
104
+ test("on linux, returns homedir/gemiterm", () => {
105
+ const platformSpy = spyOn(os, "platform").mockReturnValue("linux");
106
+ delete process.env.APPDATA;
107
+ expect(getConfigDir()).toBe(join(os.homedir(), "gemiterm"));
108
+ platformSpy.mockRestore();
109
+ });
110
+
111
+ test("on darwin, returns homedir/gemiterm", () => {
112
+ const platformSpy = spyOn(os, "platform").mockReturnValue("darwin");
113
+ expect(getConfigDir()).toBe(join(os.homedir(), "gemiterm"));
114
+ platformSpy.mockRestore();
115
+ });
116
+
117
+ test("env override takes precedence over platform-specific path", () => {
118
+ const platformSpy = spyOn(os, "platform").mockReturnValue("win32");
119
+ process.env.GEMITERM_CONFIG_DIR = "/override/path";
120
+ process.env.APPDATA = "C:\\AppData";
121
+ expect(getConfigDir()).toBe("/override/path");
122
+ platformSpy.mockRestore();
123
+ });
124
+ });
125
+
126
+ describe("getProfilesDir", () => {
127
+ let originalConfigDir: string | undefined;
128
+
129
+ beforeEach(() => {
130
+ originalConfigDir = process.env.GEMITERM_CONFIG_DIR;
131
+ });
132
+
133
+ afterEach(() => {
134
+ if (originalConfigDir !== undefined) {
135
+ process.env.GEMITERM_CONFIG_DIR = originalConfigDir;
136
+ } else {
137
+ delete process.env.GEMITERM_CONFIG_DIR;
138
+ }
139
+ });
140
+
141
+ test("returns config dir + profiles", () => {
142
+ process.env.GEMITERM_CONFIG_DIR = "/tmp/gemiterm";
143
+ expect(getProfilesDir()).toBe(join("/tmp/gemiterm", PROFILES_DIR));
144
+ });
145
+ });
146
+
147
+ describe("getProfilePath", () => {
148
+ let originalConfigDir: string | undefined;
149
+
150
+ beforeEach(() => {
151
+ originalConfigDir = process.env.GEMITERM_CONFIG_DIR;
152
+ });
153
+
154
+ afterEach(() => {
155
+ if (originalConfigDir !== undefined) {
156
+ process.env.GEMITERM_CONFIG_DIR = originalConfigDir;
157
+ } else {
158
+ delete process.env.GEMITERM_CONFIG_DIR;
159
+ }
160
+ });
161
+
162
+ test("returns profiles/<name>/storage_state.json", () => {
163
+ process.env.GEMITERM_CONFIG_DIR = "/tmp/gemiterm";
164
+ expect(getProfilePath("default")).toBe(
165
+ join("/tmp/gemiterm", PROFILES_DIR, "default", STORAGE_STATE_FILE),
166
+ );
167
+ });
168
+
169
+ test("handles profile names with special characters", () => {
170
+ process.env.GEMITERM_CONFIG_DIR = "/tmp/gemiterm";
171
+ expect(getProfilePath("my-profile")).toBe(
172
+ join("/tmp/gemiterm", PROFILES_DIR, "my-profile", STORAGE_STATE_FILE),
173
+ );
174
+ });
175
+ });
176
+
177
+ describe("getProfileDir", () => {
178
+ let originalConfigDir: string | undefined;
179
+
180
+ beforeEach(() => {
181
+ originalConfigDir = process.env.GEMITERM_CONFIG_DIR;
182
+ });
183
+
184
+ afterEach(() => {
185
+ if (originalConfigDir !== undefined) {
186
+ process.env.GEMITERM_CONFIG_DIR = originalConfigDir;
187
+ } else {
188
+ delete process.env.GEMITERM_CONFIG_DIR;
189
+ }
190
+ });
191
+
192
+ test("returns profiles/<name>", () => {
193
+ process.env.GEMITERM_CONFIG_DIR = "/tmp/gemiterm";
194
+ expect(getProfileDir("work")).toBe(join("/tmp/gemiterm", PROFILES_DIR, "work"));
195
+ });
196
+ });
197
+
198
+ describe("getDefaultProfileMarkerPath", () => {
199
+ let originalConfigDir: string | undefined;
200
+
201
+ beforeEach(() => {
202
+ originalConfigDir = process.env.GEMITERM_CONFIG_DIR;
203
+ });
204
+
205
+ afterEach(() => {
206
+ if (originalConfigDir !== undefined) {
207
+ process.env.GEMITERM_CONFIG_DIR = originalConfigDir;
208
+ } else {
209
+ delete process.env.GEMITERM_CONFIG_DIR;
210
+ }
211
+ });
212
+
213
+ test("returns profiles/.default", () => {
214
+ process.env.GEMITERM_CONFIG_DIR = "/tmp/gemiterm";
215
+ expect(getDefaultProfileMarkerPath()).toBe(
216
+ join("/tmp/gemiterm", PROFILES_DIR, DEFAULT_PROFILE_MARKER),
217
+ );
218
+ });
219
+ });
220
+
221
+ describe("isWSL", () => {
222
+ let originalPlatform: NodeJS.Platform;
223
+ let originalWslDistro: string | undefined;
224
+
225
+ beforeEach(() => {
226
+ originalPlatform = process.platform;
227
+ originalWslDistro = process.env.WSL_DISTRO_NAME;
228
+ });
229
+
230
+ afterEach(() => {
231
+ Object.defineProperty(process, "platform", { value: originalPlatform, configurable: true });
232
+ if (originalWslDistro !== undefined) {
233
+ process.env.WSL_DISTRO_NAME = originalWslDistro;
234
+ } else {
235
+ delete process.env.WSL_DISTRO_NAME;
236
+ }
237
+ });
238
+
239
+ test("returns false on Windows", () => {
240
+ Object.defineProperty(process, "platform", { value: "win32", configurable: true });
241
+ delete process.env.WSL_DISTRO_NAME;
242
+ expect(isWSL()).toBe(false);
243
+ });
244
+
245
+ test("returns false on macOS", () => {
246
+ Object.defineProperty(process, "platform", { value: "darwin", configurable: true });
247
+ delete process.env.WSL_DISTRO_NAME;
248
+ expect(isWSL()).toBe(false);
249
+ });
250
+
251
+ test("returns true on Linux when WSL_DISTRO_NAME is set and non-empty", () => {
252
+ Object.defineProperty(process, "platform", { value: "linux", configurable: true });
253
+ process.env.WSL_DISTRO_NAME = "Ubuntu";
254
+ expect(isWSL()).toBe(true);
255
+ });
256
+
257
+ test("returns false on Linux when WSL_DISTRO_NAME is unset and no /proc/version marker", () => {
258
+ Object.defineProperty(process, "platform", { value: "linux", configurable: true });
259
+ delete process.env.WSL_DISTRO_NAME;
260
+ const existsSpy = spyOn(fs, "existsSync").mockReturnValue(false);
261
+ try {
262
+ expect(isWSL()).toBe(false);
263
+ } finally {
264
+ existsSpy.mockRestore();
265
+ }
266
+ });
267
+ });
268
+
269
+ describe("getProjectRoot", () => {
270
+ test("returns the repo root when called from a file under the repo", () => {
271
+ const root = getProjectRoot(import.meta.url);
272
+ expect(root).toBeTruthy();
273
+ expect(fs.existsSync(join(root, "package.json"))).toBe(true);
274
+ });
275
+
276
+ test("is idempotent — multiple calls return the same path", () => {
277
+ const a = getProjectRoot(import.meta.url);
278
+ const b = getProjectRoot(import.meta.url);
279
+ expect(a).toBe(b);
280
+ });
281
+ });
282
+
283
+ describe("getPackageJson", () => {
284
+ test("returns the parsed package.json with name and version", () => {
285
+ const pkg = getPackageJson(import.meta.url);
286
+ expect(pkg.name).toBe("gemiterm");
287
+ });
288
+ });
289
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "noEmit": true,
10
+ "allowImportingTsExtensions": true,
11
+ "types": ["bun"],
12
+ "outDir": "dist"
13
+ },
14
+ "include": ["src/**/*.ts"],
15
+ "exclude": ["node_modules", "dist", "**/*.test.ts"]
16
+ }