@blackbelt-technology/pi-agent-dashboard 0.2.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 (212) hide show
  1. package/AGENTS.md +342 -0
  2. package/README.md +619 -0
  3. package/docs/architecture.md +646 -0
  4. package/package.json +92 -0
  5. package/packages/extension/package.json +33 -0
  6. package/packages/extension/src/__tests__/ask-user-tool.test.ts +85 -0
  7. package/packages/extension/src/__tests__/command-handler.test.ts +712 -0
  8. package/packages/extension/src/__tests__/connection.test.ts +344 -0
  9. package/packages/extension/src/__tests__/credentials-updated.test.ts +26 -0
  10. package/packages/extension/src/__tests__/dev-build.test.ts +79 -0
  11. package/packages/extension/src/__tests__/event-forwarder.test.ts +89 -0
  12. package/packages/extension/src/__tests__/git-info.test.ts +112 -0
  13. package/packages/extension/src/__tests__/git-link-builder.test.ts +102 -0
  14. package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +232 -0
  15. package/packages/extension/src/__tests__/openspec-poller.test.ts +119 -0
  16. package/packages/extension/src/__tests__/process-metrics.test.ts +47 -0
  17. package/packages/extension/src/__tests__/process-scanner.test.ts +202 -0
  18. package/packages/extension/src/__tests__/prompt-expander.test.ts +54 -0
  19. package/packages/extension/src/__tests__/server-auto-start.test.ts +167 -0
  20. package/packages/extension/src/__tests__/server-launcher.test.ts +44 -0
  21. package/packages/extension/src/__tests__/server-probe.test.ts +25 -0
  22. package/packages/extension/src/__tests__/session-switch.test.ts +139 -0
  23. package/packages/extension/src/__tests__/session-sync.test.ts +55 -0
  24. package/packages/extension/src/__tests__/source-detector.test.ts +73 -0
  25. package/packages/extension/src/__tests__/stats-extractor.test.ts +92 -0
  26. package/packages/extension/src/__tests__/ui-proxy.test.ts +583 -0
  27. package/packages/extension/src/__tests__/watchdog.test.ts +161 -0
  28. package/packages/extension/src/ask-user-tool.ts +63 -0
  29. package/packages/extension/src/bridge-context.ts +64 -0
  30. package/packages/extension/src/bridge.ts +926 -0
  31. package/packages/extension/src/command-handler.ts +538 -0
  32. package/packages/extension/src/connection.ts +204 -0
  33. package/packages/extension/src/dev-build.ts +39 -0
  34. package/packages/extension/src/event-forwarder.ts +40 -0
  35. package/packages/extension/src/flow-event-wiring.ts +102 -0
  36. package/packages/extension/src/git-info.ts +65 -0
  37. package/packages/extension/src/git-link-builder.ts +112 -0
  38. package/packages/extension/src/model-tracker.ts +56 -0
  39. package/packages/extension/src/pi-env.d.ts +23 -0
  40. package/packages/extension/src/process-metrics.ts +70 -0
  41. package/packages/extension/src/process-scanner.ts +396 -0
  42. package/packages/extension/src/prompt-expander.ts +87 -0
  43. package/packages/extension/src/provider-register.ts +276 -0
  44. package/packages/extension/src/server-auto-start.ts +87 -0
  45. package/packages/extension/src/server-launcher.ts +82 -0
  46. package/packages/extension/src/server-probe.ts +33 -0
  47. package/packages/extension/src/session-sync.ts +154 -0
  48. package/packages/extension/src/source-detector.ts +26 -0
  49. package/packages/extension/src/ui-proxy.ts +269 -0
  50. package/packages/extension/tsconfig.json +11 -0
  51. package/packages/server/package.json +37 -0
  52. package/packages/server/src/__tests__/auth-plugin.test.ts +117 -0
  53. package/packages/server/src/__tests__/auth.test.ts +224 -0
  54. package/packages/server/src/__tests__/auto-attach.test.ts +246 -0
  55. package/packages/server/src/__tests__/auto-resume.test.ts +135 -0
  56. package/packages/server/src/__tests__/auto-shutdown.test.ts +136 -0
  57. package/packages/server/src/__tests__/browse-endpoint.test.ts +104 -0
  58. package/packages/server/src/__tests__/bulk-archive-handler.test.ts +15 -0
  59. package/packages/server/src/__tests__/cli-parse.test.ts +73 -0
  60. package/packages/server/src/__tests__/client-discovery.test.ts +39 -0
  61. package/packages/server/src/__tests__/config-api.test.ts +104 -0
  62. package/packages/server/src/__tests__/cors.test.ts +48 -0
  63. package/packages/server/src/__tests__/directory-service.test.ts +240 -0
  64. package/packages/server/src/__tests__/editor-detection.test.ts +60 -0
  65. package/packages/server/src/__tests__/editor-endpoints.test.ts +26 -0
  66. package/packages/server/src/__tests__/editor-manager.test.ts +73 -0
  67. package/packages/server/src/__tests__/editor-registry.test.ts +151 -0
  68. package/packages/server/src/__tests__/event-status-extraction-flow.test.ts +55 -0
  69. package/packages/server/src/__tests__/event-status-extraction.test.ts +58 -0
  70. package/packages/server/src/__tests__/extension-register.test.ts +61 -0
  71. package/packages/server/src/__tests__/file-endpoint.test.ts +49 -0
  72. package/packages/server/src/__tests__/force-kill-handler.test.ts +109 -0
  73. package/packages/server/src/__tests__/git-operations.test.ts +251 -0
  74. package/packages/server/src/__tests__/headless-pid-registry.test.ts +233 -0
  75. package/packages/server/src/__tests__/headless-shutdown-fallback.test.ts +109 -0
  76. package/packages/server/src/__tests__/health-endpoint.test.ts +35 -0
  77. package/packages/server/src/__tests__/heartbeat-ack.test.ts +63 -0
  78. package/packages/server/src/__tests__/json-store.test.ts +70 -0
  79. package/packages/server/src/__tests__/localhost-guard.test.ts +149 -0
  80. package/packages/server/src/__tests__/memory-event-store.test.ts +260 -0
  81. package/packages/server/src/__tests__/memory-session-manager.test.ts +80 -0
  82. package/packages/server/src/__tests__/meta-persistence.test.ts +107 -0
  83. package/packages/server/src/__tests__/migrate-persistence.test.ts +180 -0
  84. package/packages/server/src/__tests__/npm-search-proxy.test.ts +153 -0
  85. package/packages/server/src/__tests__/oauth-callback-server.test.ts +165 -0
  86. package/packages/server/src/__tests__/openspec-archive.test.ts +87 -0
  87. package/packages/server/src/__tests__/package-manager-wrapper.test.ts +163 -0
  88. package/packages/server/src/__tests__/package-routes.test.ts +172 -0
  89. package/packages/server/src/__tests__/pending-fork-registry.test.ts +69 -0
  90. package/packages/server/src/__tests__/pending-load-manager.test.ts +144 -0
  91. package/packages/server/src/__tests__/pending-resume-registry.test.ts +130 -0
  92. package/packages/server/src/__tests__/pi-resource-scanner.test.ts +235 -0
  93. package/packages/server/src/__tests__/preferences-store.test.ts +108 -0
  94. package/packages/server/src/__tests__/process-manager.test.ts +184 -0
  95. package/packages/server/src/__tests__/provider-auth-handlers.test.ts +93 -0
  96. package/packages/server/src/__tests__/provider-auth-routes.test.ts +143 -0
  97. package/packages/server/src/__tests__/provider-auth-storage.test.ts +114 -0
  98. package/packages/server/src/__tests__/resolve-path.test.ts +38 -0
  99. package/packages/server/src/__tests__/ring-buffer.test.ts +45 -0
  100. package/packages/server/src/__tests__/server-pid.test.ts +89 -0
  101. package/packages/server/src/__tests__/session-api.test.ts +244 -0
  102. package/packages/server/src/__tests__/session-diff.test.ts +138 -0
  103. package/packages/server/src/__tests__/session-file-dedup.test.ts +102 -0
  104. package/packages/server/src/__tests__/session-file-reader.test.ts +85 -0
  105. package/packages/server/src/__tests__/session-lifecycle-logging.test.ts +138 -0
  106. package/packages/server/src/__tests__/session-order-manager.test.ts +135 -0
  107. package/packages/server/src/__tests__/session-ordering-integration.test.ts +102 -0
  108. package/packages/server/src/__tests__/session-scanner.test.ts +199 -0
  109. package/packages/server/src/__tests__/shutdown-endpoint.test.ts +42 -0
  110. package/packages/server/src/__tests__/skip-wipe.test.ts +123 -0
  111. package/packages/server/src/__tests__/sleep-aware-heartbeat.test.ts +126 -0
  112. package/packages/server/src/__tests__/smoke-integration.test.ts +175 -0
  113. package/packages/server/src/__tests__/spa-fallback.test.ts +68 -0
  114. package/packages/server/src/__tests__/subscription-handler.test.ts +155 -0
  115. package/packages/server/src/__tests__/terminal-gateway.test.ts +61 -0
  116. package/packages/server/src/__tests__/terminal-manager.test.ts +257 -0
  117. package/packages/server/src/__tests__/trusted-networks-config.test.ts +84 -0
  118. package/packages/server/src/__tests__/tunnel.test.ts +206 -0
  119. package/packages/server/src/__tests__/ws-ping-pong.test.ts +112 -0
  120. package/packages/server/src/auth-plugin.ts +302 -0
  121. package/packages/server/src/auth.ts +323 -0
  122. package/packages/server/src/browse.ts +55 -0
  123. package/packages/server/src/browser-gateway.ts +495 -0
  124. package/packages/server/src/browser-handlers/directory-handler.ts +137 -0
  125. package/packages/server/src/browser-handlers/handler-context.ts +45 -0
  126. package/packages/server/src/browser-handlers/session-action-handler.ts +271 -0
  127. package/packages/server/src/browser-handlers/session-meta-handler.ts +95 -0
  128. package/packages/server/src/browser-handlers/subscription-handler.ts +154 -0
  129. package/packages/server/src/browser-handlers/terminal-handler.ts +37 -0
  130. package/packages/server/src/cli.ts +347 -0
  131. package/packages/server/src/config-api.ts +130 -0
  132. package/packages/server/src/directory-service.ts +162 -0
  133. package/packages/server/src/editor-detection.ts +60 -0
  134. package/packages/server/src/editor-manager.ts +352 -0
  135. package/packages/server/src/editor-proxy.ts +134 -0
  136. package/packages/server/src/editor-registry.ts +108 -0
  137. package/packages/server/src/event-status-extraction.ts +131 -0
  138. package/packages/server/src/event-wiring.ts +589 -0
  139. package/packages/server/src/extension-register.ts +92 -0
  140. package/packages/server/src/git-operations.ts +200 -0
  141. package/packages/server/src/headless-pid-registry.ts +207 -0
  142. package/packages/server/src/idle-timer.ts +61 -0
  143. package/packages/server/src/json-store.ts +32 -0
  144. package/packages/server/src/localhost-guard.ts +117 -0
  145. package/packages/server/src/memory-event-store.ts +193 -0
  146. package/packages/server/src/memory-session-manager.ts +123 -0
  147. package/packages/server/src/meta-persistence.ts +64 -0
  148. package/packages/server/src/migrate-persistence.ts +195 -0
  149. package/packages/server/src/npm-search-proxy.ts +143 -0
  150. package/packages/server/src/oauth-callback-server.ts +177 -0
  151. package/packages/server/src/openspec-archive.ts +60 -0
  152. package/packages/server/src/package-manager-wrapper.ts +200 -0
  153. package/packages/server/src/pending-fork-registry.ts +53 -0
  154. package/packages/server/src/pending-load-manager.ts +110 -0
  155. package/packages/server/src/pending-resume-registry.ts +69 -0
  156. package/packages/server/src/pi-gateway.ts +419 -0
  157. package/packages/server/src/pi-resource-scanner.ts +369 -0
  158. package/packages/server/src/preferences-store.ts +116 -0
  159. package/packages/server/src/process-manager.ts +311 -0
  160. package/packages/server/src/provider-auth-handlers.ts +438 -0
  161. package/packages/server/src/provider-auth-storage.ts +200 -0
  162. package/packages/server/src/resolve-path.ts +12 -0
  163. package/packages/server/src/routes/editor-routes.ts +86 -0
  164. package/packages/server/src/routes/file-routes.ts +116 -0
  165. package/packages/server/src/routes/git-routes.ts +89 -0
  166. package/packages/server/src/routes/openspec-routes.ts +99 -0
  167. package/packages/server/src/routes/package-routes.ts +172 -0
  168. package/packages/server/src/routes/provider-auth-routes.ts +244 -0
  169. package/packages/server/src/routes/provider-routes.ts +101 -0
  170. package/packages/server/src/routes/route-deps.ts +23 -0
  171. package/packages/server/src/routes/session-routes.ts +91 -0
  172. package/packages/server/src/routes/system-routes.ts +271 -0
  173. package/packages/server/src/server-pid.ts +84 -0
  174. package/packages/server/src/server.ts +554 -0
  175. package/packages/server/src/session-api.ts +330 -0
  176. package/packages/server/src/session-bootstrap.ts +80 -0
  177. package/packages/server/src/session-diff.ts +178 -0
  178. package/packages/server/src/session-discovery.ts +134 -0
  179. package/packages/server/src/session-file-reader.ts +135 -0
  180. package/packages/server/src/session-order-manager.ts +73 -0
  181. package/packages/server/src/session-scanner.ts +233 -0
  182. package/packages/server/src/session-stats-reader.ts +99 -0
  183. package/packages/server/src/terminal-gateway.ts +51 -0
  184. package/packages/server/src/terminal-manager.ts +241 -0
  185. package/packages/server/src/tunnel.ts +329 -0
  186. package/packages/server/tsconfig.json +11 -0
  187. package/packages/shared/package.json +15 -0
  188. package/packages/shared/src/__tests__/config.test.ts +358 -0
  189. package/packages/shared/src/__tests__/deriveChangeState.test.ts +95 -0
  190. package/packages/shared/src/__tests__/mdns-discovery.test.ts +80 -0
  191. package/packages/shared/src/__tests__/protocol.test.ts +243 -0
  192. package/packages/shared/src/__tests__/resolve-jiti.test.ts +17 -0
  193. package/packages/shared/src/__tests__/server-identity.test.ts +73 -0
  194. package/packages/shared/src/__tests__/session-meta.test.ts +125 -0
  195. package/packages/shared/src/archive-types.ts +11 -0
  196. package/packages/shared/src/browser-protocol.ts +534 -0
  197. package/packages/shared/src/config.ts +245 -0
  198. package/packages/shared/src/diff-types.ts +41 -0
  199. package/packages/shared/src/editor-types.ts +18 -0
  200. package/packages/shared/src/mdns-discovery.ts +248 -0
  201. package/packages/shared/src/openspec-activity-detector.ts +109 -0
  202. package/packages/shared/src/openspec-poller.ts +96 -0
  203. package/packages/shared/src/protocol.ts +369 -0
  204. package/packages/shared/src/resolve-jiti.ts +43 -0
  205. package/packages/shared/src/rest-api.ts +255 -0
  206. package/packages/shared/src/server-identity.ts +51 -0
  207. package/packages/shared/src/session-meta.ts +86 -0
  208. package/packages/shared/src/state-replay.ts +174 -0
  209. package/packages/shared/src/stats-extractor.ts +54 -0
  210. package/packages/shared/src/terminal-types.ts +18 -0
  211. package/packages/shared/src/types.ts +351 -0
  212. package/packages/shared/tsconfig.json +8 -0
@@ -0,0 +1,235 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import * as os from "node:os";
5
+ import { scanLocalResources, scanGlobalResources, parseFrontmatter, resolvePackages, scanPiResources } from "../pi-resource-scanner.js";
6
+
7
+ let tmpDir: string;
8
+
9
+ beforeEach(() => {
10
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-res-test-"));
11
+ });
12
+
13
+ afterEach(() => {
14
+ fs.rmSync(tmpDir, { recursive: true, force: true });
15
+ });
16
+
17
+ function writeFile(relPath: string, content: string) {
18
+ const full = path.join(tmpDir, relPath);
19
+ fs.mkdirSync(path.dirname(full), { recursive: true });
20
+ fs.writeFileSync(full, content, "utf-8");
21
+ }
22
+
23
+ describe("parseFrontmatter", () => {
24
+ it("parses name and description from YAML frontmatter", () => {
25
+ const content = `---
26
+ name: code-review
27
+ description: Comprehensive code review guidance.
28
+ license: MIT
29
+ ---
30
+
31
+ # Code Review
32
+
33
+ Instructions here.`;
34
+ const result = parseFrontmatter(content);
35
+ expect(result.name).toBe("code-review");
36
+ expect(result.description).toBe("Comprehensive code review guidance.");
37
+ });
38
+
39
+ it("handles multi-line description with >", () => {
40
+ const content = `---
41
+ name: my-skill
42
+ description: >
43
+ Line one
44
+ line two.
45
+ ---
46
+
47
+ Body`;
48
+ const result = parseFrontmatter(content);
49
+ expect(result.name).toBe("my-skill");
50
+ expect(result.description).toContain("Line one");
51
+ });
52
+
53
+ it("returns empty object for no frontmatter", () => {
54
+ const result = parseFrontmatter("# Just a heading\n\nSome content.");
55
+ expect(result.name).toBeUndefined();
56
+ expect(result.description).toBeUndefined();
57
+ });
58
+
59
+ it("extracts first non-empty line as description fallback", () => {
60
+ const result = parseFrontmatter("# My Prompt\n\nDo something useful.", true);
61
+ expect(result.description).toBe("# My Prompt");
62
+ });
63
+ });
64
+
65
+ describe("scanLocalResources", () => {
66
+ it("discovers skills from SKILL.md directories", () => {
67
+ writeFile(".pi/skills/code-review/SKILL.md", `---
68
+ name: code-review
69
+ description: Review code.
70
+ ---
71
+ Instructions`);
72
+ const result = scanLocalResources(tmpDir);
73
+ expect(result.skills).toHaveLength(1);
74
+ expect(result.skills[0].name).toBe("code-review");
75
+ expect(result.skills[0].description).toBe("Review code.");
76
+ expect(result.skills[0].type).toBe("skill");
77
+ });
78
+
79
+ it("discovers skills from root .md files", () => {
80
+ writeFile(".pi/skills/quick-review.md", `---
81
+ name: quick-review
82
+ description: Quick review.
83
+ ---
84
+ Body`);
85
+ const result = scanLocalResources(tmpDir);
86
+ expect(result.skills).toHaveLength(1);
87
+ expect(result.skills[0].name).toBe("quick-review");
88
+ });
89
+
90
+ it("discovers extensions from .ts files", () => {
91
+ writeFile(".pi/extensions/my-ext.ts", "export default function() {}");
92
+ const result = scanLocalResources(tmpDir);
93
+ expect(result.extensions).toHaveLength(1);
94
+ expect(result.extensions[0].name).toBe("my-ext");
95
+ expect(result.extensions[0].type).toBe("extension");
96
+ });
97
+
98
+ it("discovers extensions from subdirectory index.ts", () => {
99
+ writeFile(".pi/extensions/my-ext/index.ts", "export default function() {}");
100
+ const result = scanLocalResources(tmpDir);
101
+ expect(result.extensions).toHaveLength(1);
102
+ expect(result.extensions[0].name).toBe("my-ext");
103
+ });
104
+
105
+ it("discovers prompts from .md files", () => {
106
+ writeFile(".pi/prompts/review.md", `---
107
+ description: Review staged changes
108
+ ---
109
+ Review the staged changes.`);
110
+ const result = scanLocalResources(tmpDir);
111
+ expect(result.prompts).toHaveLength(1);
112
+ expect(result.prompts[0].name).toBe("review");
113
+ expect(result.prompts[0].description).toBe("Review staged changes");
114
+ expect(result.prompts[0].type).toBe("prompt");
115
+ });
116
+
117
+ it("returns empty arrays when .pi/ does not exist", () => {
118
+ const result = scanLocalResources(path.join(tmpDir, "nonexistent"));
119
+ expect(result.extensions).toEqual([]);
120
+ expect(result.skills).toEqual([]);
121
+ expect(result.prompts).toEqual([]);
122
+ });
123
+ });
124
+
125
+ describe("scanGlobalResources", () => {
126
+ it("discovers resources from a global-like directory", () => {
127
+ const globalDir = path.join(tmpDir, "global-pi");
128
+ fs.mkdirSync(path.join(globalDir, "skills", "my-skill"), { recursive: true });
129
+ fs.writeFileSync(path.join(globalDir, "skills", "my-skill", "SKILL.md"), `---
130
+ name: my-skill
131
+ description: A global skill.
132
+ ---
133
+ Body`);
134
+ fs.mkdirSync(path.join(globalDir, "extensions"), { recursive: true });
135
+ fs.writeFileSync(path.join(globalDir, "extensions", "g-ext.ts"), "export default function() {}");
136
+ fs.mkdirSync(path.join(globalDir, "prompts"), { recursive: true });
137
+ fs.writeFileSync(path.join(globalDir, "prompts", "g-prompt.md"), "Do things.");
138
+
139
+ const result = scanGlobalResources(globalDir);
140
+ expect(result.skills).toHaveLength(1);
141
+ expect(result.skills[0].name).toBe("my-skill");
142
+ expect(result.extensions).toHaveLength(1);
143
+ expect(result.extensions[0].name).toBe("g-ext");
144
+ expect(result.prompts).toHaveLength(1);
145
+ expect(result.prompts[0].name).toBe("g-prompt");
146
+ });
147
+
148
+ it("returns empty when directory does not exist", () => {
149
+ const result = scanGlobalResources("/nonexistent/path");
150
+ expect(result.extensions).toEqual([]);
151
+ expect(result.skills).toEqual([]);
152
+ expect(result.prompts).toEqual([]);
153
+ });
154
+ });
155
+
156
+ describe("resolvePackages", () => {
157
+ it("resolves a local path package with pi manifest", () => {
158
+ const pkgDir = path.join(tmpDir, "my-pkg");
159
+ fs.mkdirSync(path.join(pkgDir, "src"), { recursive: true });
160
+ fs.writeFileSync(path.join(pkgDir, "src", "bridge.ts"), "export default function() {}");
161
+ fs.writeFileSync(
162
+ path.join(pkgDir, "package.json"),
163
+ JSON.stringify({
164
+ name: "my-pkg",
165
+ description: "A test package",
166
+ pi: { extensions: ["./src/bridge.ts"] },
167
+ }),
168
+ );
169
+
170
+ const result = resolvePackages([pkgDir], path.join(tmpDir, "settings-dir"));
171
+ expect(result).toHaveLength(1);
172
+ expect(result[0].name).toBe("my-pkg");
173
+ expect(result[0].description).toBe("A test package");
174
+ expect(result[0].resources.extensions).toHaveLength(1);
175
+ expect(result[0].resources.extensions[0].name).toBe("bridge");
176
+ });
177
+
178
+ it("resolves package with conventional directories (no pi manifest)", () => {
179
+ const pkgDir = path.join(tmpDir, "conv-pkg");
180
+ fs.mkdirSync(path.join(pkgDir, "extensions"), { recursive: true });
181
+ fs.writeFileSync(path.join(pkgDir, "extensions", "ext.ts"), "export default function() {}");
182
+ fs.mkdirSync(path.join(pkgDir, "skills", "my-skill"), { recursive: true });
183
+ fs.writeFileSync(path.join(pkgDir, "skills", "my-skill", "SKILL.md"), `---
184
+ name: my-skill
185
+ description: Skill from package.
186
+ ---
187
+ Body`);
188
+ fs.writeFileSync(
189
+ path.join(pkgDir, "package.json"),
190
+ JSON.stringify({ name: "conv-pkg", description: "Conventional" }),
191
+ );
192
+
193
+ const result = resolvePackages([pkgDir], path.join(tmpDir, "settings-dir"));
194
+ expect(result).toHaveLength(1);
195
+ expect(result[0].resources.extensions).toHaveLength(1);
196
+ expect(result[0].resources.skills).toHaveLength(1);
197
+ });
198
+
199
+ it("skips missing packages silently", () => {
200
+ const result = resolvePackages(["/nonexistent/package"], tmpDir);
201
+ expect(result).toEqual([]);
202
+ });
203
+
204
+ it("resolves relative path packages from settings dir", () => {
205
+ const settingsDir = path.join(tmpDir, "project", ".pi");
206
+ const pkgDir = path.join(tmpDir, "sibling-pkg");
207
+ fs.mkdirSync(pkgDir, { recursive: true });
208
+ fs.writeFileSync(
209
+ path.join(pkgDir, "package.json"),
210
+ JSON.stringify({ name: "sibling", description: "Sibling package" }),
211
+ );
212
+
213
+ const result = resolvePackages(["../../sibling-pkg"], settingsDir);
214
+ expect(result).toHaveLength(1);
215
+ expect(result[0].name).toBe("sibling");
216
+ });
217
+ });
218
+
219
+ describe("scanPiResources (integration)", () => {
220
+ it("combines local, global, and returns a full result", async () => {
221
+ writeFile(".pi/skills/local-skill/SKILL.md", `---
222
+ name: local-skill
223
+ description: A local skill.
224
+ ---
225
+ Body`);
226
+ writeFile(".pi/prompts/my-prompt.md", "Do something.");
227
+
228
+ // We pass a custom globalDir to avoid depending on ~/.pi/agent
229
+ const result = await scanPiResources(tmpDir, { globalDir: path.join(tmpDir, "nonexistent-global") });
230
+ expect(result.local.skills).toHaveLength(1);
231
+ expect(result.local.prompts).toHaveLength(1);
232
+ expect(result.global.skills).toEqual([]);
233
+ expect(result.packages).toEqual([]);
234
+ });
235
+ });
@@ -0,0 +1,108 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import os from "node:os";
5
+ import { createPreferencesStore } from "../preferences-store.js";
6
+
7
+ // Mock resolve-path to be a no-op (no symlink resolution in tests)
8
+ vi.mock("../resolve-path.js", () => ({
9
+ safeRealpathSync: (p: string) => p,
10
+ }));
11
+
12
+ describe("preferences-store", () => {
13
+ let tmpDir: string;
14
+ let filePath: string;
15
+
16
+ beforeEach(() => {
17
+ vi.useFakeTimers();
18
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pref-store-test-"));
19
+ filePath = path.join(tmpDir, "preferences.json");
20
+ });
21
+
22
+ afterEach(() => {
23
+ vi.useRealTimers();
24
+ fs.rmSync(tmpDir, { recursive: true, force: true });
25
+ });
26
+
27
+ it("should start with empty defaults when file missing", () => {
28
+ const store = createPreferencesStore(filePath);
29
+ expect(store.getPinnedDirectories()).toEqual([]);
30
+ expect(store.getSessionOrder()).toEqual({});
31
+ store.dispose();
32
+ });
33
+
34
+ it("should load existing preferences", () => {
35
+ fs.writeFileSync(filePath, JSON.stringify({
36
+ pinnedDirectories: ["/a", "/b"],
37
+ sessionOrder: { "/a": ["s1", "s2"] },
38
+ }));
39
+ const store = createPreferencesStore(filePath);
40
+ expect(store.getPinnedDirectories()).toEqual(["/a", "/b"]);
41
+ expect(store.getSessionOrder()).toEqual({ "/a": ["s1", "s2"] });
42
+ store.dispose();
43
+ });
44
+
45
+ it("should pin and unpin directories", () => {
46
+ const store = createPreferencesStore(filePath);
47
+ store.pinDirectory("/a");
48
+ store.pinDirectory("/b");
49
+ expect(store.getPinnedDirectories()).toEqual(["/a", "/b"]);
50
+ store.unpinDirectory("/a");
51
+ expect(store.getPinnedDirectories()).toEqual(["/b"]);
52
+ store.dispose();
53
+ });
54
+
55
+ it("should not duplicate pinned directories", () => {
56
+ const store = createPreferencesStore(filePath);
57
+ store.pinDirectory("/a");
58
+ store.pinDirectory("/a");
59
+ expect(store.getPinnedDirectories()).toEqual(["/a"]);
60
+ store.dispose();
61
+ });
62
+
63
+ it("should reorder pinned directories", () => {
64
+ const store = createPreferencesStore(filePath);
65
+ store.pinDirectory("/a");
66
+ store.pinDirectory("/b");
67
+ store.reorderPinnedDirs(["/b", "/a"]);
68
+ expect(store.getPinnedDirectories()).toEqual(["/b", "/a"]);
69
+ store.dispose();
70
+ });
71
+
72
+ it("should set and get session order", () => {
73
+ const store = createPreferencesStore(filePath);
74
+ store.setSessionOrder({ "/x": ["s1", "s2"] });
75
+ expect(store.getSessionOrder()).toEqual({ "/x": ["s1", "s2"] });
76
+ store.dispose();
77
+ });
78
+
79
+ it("should debounce writes", () => {
80
+ const store = createPreferencesStore(filePath);
81
+ store.pinDirectory("/a");
82
+ store.pinDirectory("/b");
83
+ // Not written yet
84
+ expect(fs.existsSync(filePath)).toBe(false);
85
+ vi.advanceTimersByTime(1000);
86
+ const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
87
+ expect(data.pinnedDirectories).toEqual(["/a", "/b"]);
88
+ store.dispose();
89
+ });
90
+
91
+ it("should flush pending writes", () => {
92
+ const store = createPreferencesStore(filePath);
93
+ store.pinDirectory("/a");
94
+ store.flush();
95
+ const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
96
+ expect(data.pinnedDirectories).toEqual(["/a"]);
97
+ store.dispose();
98
+ });
99
+
100
+ it("should not contain hiddenSessions in output", () => {
101
+ const store = createPreferencesStore(filePath);
102
+ store.pinDirectory("/a");
103
+ store.flush();
104
+ const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
105
+ expect(data.hiddenSessions).toBeUndefined();
106
+ store.dispose();
107
+ });
108
+ });
@@ -0,0 +1,184 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { detectPlatform, buildTmuxCommand, buildHeadlessArgs, shellEscape, spawnPiSession, buildSpawnEnv, type SessionOptions } from "../process-manager.js";
3
+
4
+ describe("Process Manager", () => {
5
+ describe("detectPlatform", () => {
6
+ it("should detect macOS", () => {
7
+ const result = detectPlatform("darwin");
8
+ expect(result.strategy).toBe("tmux");
9
+ });
10
+
11
+ it("should detect Linux", () => {
12
+ const result = detectPlatform("linux");
13
+ expect(result.strategy).toBe("tmux");
14
+ });
15
+
16
+ it("should detect Windows with WSL fallback", () => {
17
+ const result = detectPlatform("win32");
18
+ expect(result.strategy).toBe("wsl");
19
+ });
20
+ });
21
+
22
+ describe("buildTmuxCommand", () => {
23
+ it("should create new session when no pi-dashboard session exists", () => {
24
+ const cmd = buildTmuxCommand("/home/user/project", false);
25
+ expect(cmd).toContain("new-session");
26
+ expect(cmd).toContain("pi-dashboard");
27
+ });
28
+
29
+ it("should create new window when pi-dashboard session exists", () => {
30
+ const cmd = buildTmuxCommand("/home/user/project", true);
31
+ expect(cmd).toContain("new-window");
32
+ });
33
+
34
+ it("should not set PI_DASHBOARD_SPAWNED env var", () => {
35
+ const cmd = buildTmuxCommand("/home/user/project", false);
36
+ expect(cmd).not.toContain("PI_DASHBOARD_SPAWNED");
37
+ });
38
+
39
+ it("should shell-escape cwd with spaces", () => {
40
+ const cmd = buildTmuxCommand("/home/user/my project", false);
41
+ expect(cmd).toContain("'/home/user/my project'");
42
+ expect(cmd).not.toContain('cd /home/user/my project &&');
43
+ });
44
+
45
+ it("should shell-escape cwd with semicolons to prevent injection", () => {
46
+ const cmd = buildTmuxCommand("/tmp/test; rm -rf /", false);
47
+ expect(cmd).toContain("'/tmp/test; rm -rf /'");
48
+ });
49
+
50
+ it("should shell-escape cwd with backticks to prevent injection", () => {
51
+ const cmd = buildTmuxCommand("/tmp/`whoami`", false);
52
+ expect(cmd).toContain("'/tmp/`whoami`'");
53
+ });
54
+
55
+ it("should shell-escape sessionFile with special characters", () => {
56
+ const cmd = buildTmuxCommand("/home/user/project", true, {
57
+ sessionFile: "/path/to/my session; cat /etc/passwd",
58
+ mode: "continue",
59
+ });
60
+ expect(cmd).toContain("--session '/path/to/my session; cat /etc/passwd'");
61
+ });
62
+
63
+ it("should not double-quote safe paths", () => {
64
+ const cmd = buildTmuxCommand("/home/user/project", false);
65
+ // Safe path should not be wrapped in single quotes
66
+ expect(cmd).toContain("cd /home/user/project &&");
67
+ });
68
+
69
+ it("should include --session flag for continue mode", () => {
70
+ const cmd = buildTmuxCommand("/home/user/project", true, {
71
+ sessionFile: "/path/to/session.jsonl",
72
+ mode: "continue",
73
+ });
74
+ expect(cmd).toContain("--session /path/to/session.jsonl");
75
+ expect(cmd).not.toContain("--fork");
76
+ });
77
+
78
+ it("should include --fork flag for fork mode", () => {
79
+ const cmd = buildTmuxCommand("/home/user/project", true, {
80
+ sessionFile: "/path/to/session.jsonl",
81
+ mode: "fork",
82
+ });
83
+ expect(cmd).toContain("--fork /path/to/session.jsonl");
84
+ expect(cmd).not.toContain("--session");
85
+ });
86
+
87
+ it("should not include session flags when no options provided", () => {
88
+ const cmd = buildTmuxCommand("/home/user/project", false);
89
+ expect(cmd).not.toContain("--session");
90
+ expect(cmd).not.toContain("--fork");
91
+ });
92
+
93
+ it("should create new session for continue mode when no tmux session exists", () => {
94
+ const cmd = buildTmuxCommand("/home/user/project", false, {
95
+ sessionFile: "/path/to/session.jsonl",
96
+ mode: "continue",
97
+ });
98
+ expect(cmd).toContain("new-session");
99
+ expect(cmd).toContain("--session /path/to/session.jsonl");
100
+ });
101
+ });
102
+
103
+ describe("buildHeadlessArgs", () => {
104
+ it("should return --mode rpc for fresh session", () => {
105
+ const args = buildHeadlessArgs();
106
+ expect(args).toEqual(["--mode", "rpc"]);
107
+ });
108
+
109
+ it("should include --session for continue mode", () => {
110
+ const args = buildHeadlessArgs({
111
+ sessionFile: "/path/to/session.jsonl",
112
+ mode: "continue",
113
+ });
114
+ expect(args).toEqual(["--mode", "rpc", "--session", "/path/to/session.jsonl"]);
115
+ });
116
+
117
+ it("should include --fork for fork mode", () => {
118
+ const args = buildHeadlessArgs({
119
+ sessionFile: "/path/to/session.jsonl",
120
+ mode: "fork",
121
+ });
122
+ expect(args).toEqual(["--mode", "rpc", "--fork", "/path/to/session.jsonl"]);
123
+ });
124
+
125
+ it("should not include session flags when no options", () => {
126
+ const args = buildHeadlessArgs({});
127
+ expect(args).toEqual(["--mode", "rpc"]);
128
+ });
129
+ });
130
+
131
+ describe("spawnPiSession", () => {
132
+ it("should return error for non-existent directory", async () => {
133
+ const result = await spawnPiSession("/tmp/definitely-does-not-exist-" + Date.now());
134
+ expect(result.success).toBe(false);
135
+ expect(result.message).toContain("Directory does not exist");
136
+ });
137
+ });
138
+
139
+ describe("SessionOptions strategy field", () => {
140
+ it("should accept tmux strategy", () => {
141
+ const opts: SessionOptions = { strategy: "tmux" };
142
+ expect(opts.strategy).toBe("tmux");
143
+ });
144
+
145
+ it("should accept headless strategy", () => {
146
+ const opts: SessionOptions = { strategy: "headless" };
147
+ expect(opts.strategy).toBe("headless");
148
+ });
149
+
150
+ it("should allow strategy with session file options", () => {
151
+ const opts: SessionOptions = {
152
+ strategy: "headless",
153
+ sessionFile: "/path/to/session.jsonl",
154
+ mode: "continue",
155
+ };
156
+ const args = buildHeadlessArgs(opts);
157
+ expect(args).toEqual(["--mode", "rpc", "--session", "/path/to/session.jsonl"]);
158
+ });
159
+ });
160
+
161
+ describe("buildSpawnEnv", () => {
162
+ it("should prepend managed bin to PATH", () => {
163
+ const env = buildSpawnEnv({ PATH: "/usr/bin" });
164
+ expect(env.PATH).toMatch(/\.pi-dashboard.*node_modules.*\.bin/);
165
+ expect(env.PATH).toContain("/usr/bin");
166
+ });
167
+
168
+ it("should not duplicate managed bin if already present", () => {
169
+ const managedBin = require("path").join(require("os").homedir(), ".pi-dashboard", "node_modules", ".bin");
170
+ const env = buildSpawnEnv({ PATH: `${managedBin}:/usr/bin` });
171
+ expect(env.PATH).toBe(`${managedBin}:/usr/bin`);
172
+ });
173
+ });
174
+
175
+ describe("electronMode", () => {
176
+ it("should force headless spawn when electronMode is true", async () => {
177
+ // electronMode should bypass tmux detection and use headless directly
178
+ // We test by calling with a non-existent dir to get a quick error without spawning
179
+ const result = await spawnPiSession("/nonexistent-path-12345", { electronMode: true });
180
+ expect(result.success).toBe(false);
181
+ expect(result.message).toContain("does not exist");
182
+ });
183
+ });
184
+ });
@@ -0,0 +1,93 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ generatePKCE,
4
+ generateState,
5
+ getProviderHandler,
6
+ getAllHandlers,
7
+ type AuthCodeHandler,
8
+ } from "../provider-auth-handlers.js";
9
+
10
+ describe("provider-auth-handlers", () => {
11
+ describe("PKCE", () => {
12
+ it("generates verifier and challenge", async () => {
13
+ const pkce = await generatePKCE();
14
+ expect(pkce.verifier).toBeTruthy();
15
+ expect(pkce.challenge).toBeTruthy();
16
+ expect(pkce.verifier).not.toBe(pkce.challenge);
17
+ });
18
+
19
+ it("generates different pairs each time", async () => {
20
+ const a = await generatePKCE();
21
+ const b = await generatePKCE();
22
+ expect(a.verifier).not.toBe(b.verifier);
23
+ });
24
+ });
25
+
26
+ describe("generateState", () => {
27
+ it("returns a hex string", () => {
28
+ const state = generateState();
29
+ expect(state).toMatch(/^[0-9a-f]+$/);
30
+ expect(state.length).toBe(32);
31
+ });
32
+ });
33
+
34
+ describe("registry", () => {
35
+ it("has all 5 providers", () => {
36
+ const handlers = getAllHandlers();
37
+ expect(handlers.length).toBe(5);
38
+ });
39
+
40
+ it("gets anthropic handler", () => {
41
+ const h = getProviderHandler("anthropic");
42
+ expect(h).toBeDefined();
43
+ expect(h!.flowType).toBe("auth_code");
44
+ expect(h!.providerId).toBe("anthropic");
45
+ });
46
+
47
+ it("gets github-copilot as device_code", () => {
48
+ const h = getProviderHandler("github-copilot");
49
+ expect(h).toBeDefined();
50
+ expect(h!.flowType).toBe("device_code");
51
+ });
52
+
53
+ it("returns undefined for unknown provider", () => {
54
+ expect(getProviderHandler("unknown")).toBeUndefined();
55
+ });
56
+ });
57
+
58
+ describe("auth URL builders", () => {
59
+ const dummyPkce = { verifier: "v", challenge: "c" };
60
+
61
+ it("anthropic builds correct auth URL", () => {
62
+ const h = getProviderHandler("anthropic") as AuthCodeHandler;
63
+ const url = h.buildAuthUrl("http://localhost:9998/callback", "test-state", dummyPkce);
64
+ expect(url).toContain("claude.ai/oauth/authorize");
65
+ expect(url).toContain("code_challenge=c");
66
+ expect(url).toContain("state=test-state");
67
+ expect(url).toContain("redirect_uri=");
68
+ });
69
+
70
+ it("openai-codex builds correct auth URL", () => {
71
+ const h = getProviderHandler("openai-codex") as AuthCodeHandler;
72
+ const url = h.buildAuthUrl("http://localhost:9998/callback", "s", dummyPkce);
73
+ expect(url).toContain("auth.openai.com/oauth/authorize");
74
+ expect(url).toContain("codex_cli_simplified_flow=true");
75
+ });
76
+
77
+ it("google-gemini-cli builds Google auth URL", () => {
78
+ const h = getProviderHandler("google-gemini-cli") as AuthCodeHandler;
79
+ const url = h.buildAuthUrl("http://localhost:9998/callback", "s", dummyPkce);
80
+ expect(url).toContain("accounts.google.com");
81
+ expect(url).toContain("cloud-platform");
82
+ expect(url).toContain("access_type=offline");
83
+ });
84
+
85
+ it("google-antigravity builds Google auth URL with extra scopes", () => {
86
+ const h = getProviderHandler("google-antigravity") as AuthCodeHandler;
87
+ const url = h.buildAuthUrl("http://localhost:9998/callback", "s", dummyPkce);
88
+ expect(url).toContain("accounts.google.com");
89
+ expect(url).toContain("cclog");
90
+ expect(url).toContain("experimentsandconfigs");
91
+ });
92
+ });
93
+ });