@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.
- package/AGENTS.md +342 -0
- package/README.md +619 -0
- package/docs/architecture.md +646 -0
- package/package.json +92 -0
- package/packages/extension/package.json +33 -0
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +85 -0
- package/packages/extension/src/__tests__/command-handler.test.ts +712 -0
- package/packages/extension/src/__tests__/connection.test.ts +344 -0
- package/packages/extension/src/__tests__/credentials-updated.test.ts +26 -0
- package/packages/extension/src/__tests__/dev-build.test.ts +79 -0
- package/packages/extension/src/__tests__/event-forwarder.test.ts +89 -0
- package/packages/extension/src/__tests__/git-info.test.ts +112 -0
- package/packages/extension/src/__tests__/git-link-builder.test.ts +102 -0
- package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +232 -0
- package/packages/extension/src/__tests__/openspec-poller.test.ts +119 -0
- package/packages/extension/src/__tests__/process-metrics.test.ts +47 -0
- package/packages/extension/src/__tests__/process-scanner.test.ts +202 -0
- package/packages/extension/src/__tests__/prompt-expander.test.ts +54 -0
- package/packages/extension/src/__tests__/server-auto-start.test.ts +167 -0
- package/packages/extension/src/__tests__/server-launcher.test.ts +44 -0
- package/packages/extension/src/__tests__/server-probe.test.ts +25 -0
- package/packages/extension/src/__tests__/session-switch.test.ts +139 -0
- package/packages/extension/src/__tests__/session-sync.test.ts +55 -0
- package/packages/extension/src/__tests__/source-detector.test.ts +73 -0
- package/packages/extension/src/__tests__/stats-extractor.test.ts +92 -0
- package/packages/extension/src/__tests__/ui-proxy.test.ts +583 -0
- package/packages/extension/src/__tests__/watchdog.test.ts +161 -0
- package/packages/extension/src/ask-user-tool.ts +63 -0
- package/packages/extension/src/bridge-context.ts +64 -0
- package/packages/extension/src/bridge.ts +926 -0
- package/packages/extension/src/command-handler.ts +538 -0
- package/packages/extension/src/connection.ts +204 -0
- package/packages/extension/src/dev-build.ts +39 -0
- package/packages/extension/src/event-forwarder.ts +40 -0
- package/packages/extension/src/flow-event-wiring.ts +102 -0
- package/packages/extension/src/git-info.ts +65 -0
- package/packages/extension/src/git-link-builder.ts +112 -0
- package/packages/extension/src/model-tracker.ts +56 -0
- package/packages/extension/src/pi-env.d.ts +23 -0
- package/packages/extension/src/process-metrics.ts +70 -0
- package/packages/extension/src/process-scanner.ts +396 -0
- package/packages/extension/src/prompt-expander.ts +87 -0
- package/packages/extension/src/provider-register.ts +276 -0
- package/packages/extension/src/server-auto-start.ts +87 -0
- package/packages/extension/src/server-launcher.ts +82 -0
- package/packages/extension/src/server-probe.ts +33 -0
- package/packages/extension/src/session-sync.ts +154 -0
- package/packages/extension/src/source-detector.ts +26 -0
- package/packages/extension/src/ui-proxy.ts +269 -0
- package/packages/extension/tsconfig.json +11 -0
- package/packages/server/package.json +37 -0
- package/packages/server/src/__tests__/auth-plugin.test.ts +117 -0
- package/packages/server/src/__tests__/auth.test.ts +224 -0
- package/packages/server/src/__tests__/auto-attach.test.ts +246 -0
- package/packages/server/src/__tests__/auto-resume.test.ts +135 -0
- package/packages/server/src/__tests__/auto-shutdown.test.ts +136 -0
- package/packages/server/src/__tests__/browse-endpoint.test.ts +104 -0
- package/packages/server/src/__tests__/bulk-archive-handler.test.ts +15 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +73 -0
- package/packages/server/src/__tests__/client-discovery.test.ts +39 -0
- package/packages/server/src/__tests__/config-api.test.ts +104 -0
- package/packages/server/src/__tests__/cors.test.ts +48 -0
- package/packages/server/src/__tests__/directory-service.test.ts +240 -0
- package/packages/server/src/__tests__/editor-detection.test.ts +60 -0
- package/packages/server/src/__tests__/editor-endpoints.test.ts +26 -0
- package/packages/server/src/__tests__/editor-manager.test.ts +73 -0
- package/packages/server/src/__tests__/editor-registry.test.ts +151 -0
- package/packages/server/src/__tests__/event-status-extraction-flow.test.ts +55 -0
- package/packages/server/src/__tests__/event-status-extraction.test.ts +58 -0
- package/packages/server/src/__tests__/extension-register.test.ts +61 -0
- package/packages/server/src/__tests__/file-endpoint.test.ts +49 -0
- package/packages/server/src/__tests__/force-kill-handler.test.ts +109 -0
- package/packages/server/src/__tests__/git-operations.test.ts +251 -0
- package/packages/server/src/__tests__/headless-pid-registry.test.ts +233 -0
- package/packages/server/src/__tests__/headless-shutdown-fallback.test.ts +109 -0
- package/packages/server/src/__tests__/health-endpoint.test.ts +35 -0
- package/packages/server/src/__tests__/heartbeat-ack.test.ts +63 -0
- package/packages/server/src/__tests__/json-store.test.ts +70 -0
- package/packages/server/src/__tests__/localhost-guard.test.ts +149 -0
- package/packages/server/src/__tests__/memory-event-store.test.ts +260 -0
- package/packages/server/src/__tests__/memory-session-manager.test.ts +80 -0
- package/packages/server/src/__tests__/meta-persistence.test.ts +107 -0
- package/packages/server/src/__tests__/migrate-persistence.test.ts +180 -0
- package/packages/server/src/__tests__/npm-search-proxy.test.ts +153 -0
- package/packages/server/src/__tests__/oauth-callback-server.test.ts +165 -0
- package/packages/server/src/__tests__/openspec-archive.test.ts +87 -0
- package/packages/server/src/__tests__/package-manager-wrapper.test.ts +163 -0
- package/packages/server/src/__tests__/package-routes.test.ts +172 -0
- package/packages/server/src/__tests__/pending-fork-registry.test.ts +69 -0
- package/packages/server/src/__tests__/pending-load-manager.test.ts +144 -0
- package/packages/server/src/__tests__/pending-resume-registry.test.ts +130 -0
- package/packages/server/src/__tests__/pi-resource-scanner.test.ts +235 -0
- package/packages/server/src/__tests__/preferences-store.test.ts +108 -0
- package/packages/server/src/__tests__/process-manager.test.ts +184 -0
- package/packages/server/src/__tests__/provider-auth-handlers.test.ts +93 -0
- package/packages/server/src/__tests__/provider-auth-routes.test.ts +143 -0
- package/packages/server/src/__tests__/provider-auth-storage.test.ts +114 -0
- package/packages/server/src/__tests__/resolve-path.test.ts +38 -0
- package/packages/server/src/__tests__/ring-buffer.test.ts +45 -0
- package/packages/server/src/__tests__/server-pid.test.ts +89 -0
- package/packages/server/src/__tests__/session-api.test.ts +244 -0
- package/packages/server/src/__tests__/session-diff.test.ts +138 -0
- package/packages/server/src/__tests__/session-file-dedup.test.ts +102 -0
- package/packages/server/src/__tests__/session-file-reader.test.ts +85 -0
- package/packages/server/src/__tests__/session-lifecycle-logging.test.ts +138 -0
- package/packages/server/src/__tests__/session-order-manager.test.ts +135 -0
- package/packages/server/src/__tests__/session-ordering-integration.test.ts +102 -0
- package/packages/server/src/__tests__/session-scanner.test.ts +199 -0
- package/packages/server/src/__tests__/shutdown-endpoint.test.ts +42 -0
- package/packages/server/src/__tests__/skip-wipe.test.ts +123 -0
- package/packages/server/src/__tests__/sleep-aware-heartbeat.test.ts +126 -0
- package/packages/server/src/__tests__/smoke-integration.test.ts +175 -0
- package/packages/server/src/__tests__/spa-fallback.test.ts +68 -0
- package/packages/server/src/__tests__/subscription-handler.test.ts +155 -0
- package/packages/server/src/__tests__/terminal-gateway.test.ts +61 -0
- package/packages/server/src/__tests__/terminal-manager.test.ts +257 -0
- package/packages/server/src/__tests__/trusted-networks-config.test.ts +84 -0
- package/packages/server/src/__tests__/tunnel.test.ts +206 -0
- package/packages/server/src/__tests__/ws-ping-pong.test.ts +112 -0
- package/packages/server/src/auth-plugin.ts +302 -0
- package/packages/server/src/auth.ts +323 -0
- package/packages/server/src/browse.ts +55 -0
- package/packages/server/src/browser-gateway.ts +495 -0
- package/packages/server/src/browser-handlers/directory-handler.ts +137 -0
- package/packages/server/src/browser-handlers/handler-context.ts +45 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +271 -0
- package/packages/server/src/browser-handlers/session-meta-handler.ts +95 -0
- package/packages/server/src/browser-handlers/subscription-handler.ts +154 -0
- package/packages/server/src/browser-handlers/terminal-handler.ts +37 -0
- package/packages/server/src/cli.ts +347 -0
- package/packages/server/src/config-api.ts +130 -0
- package/packages/server/src/directory-service.ts +162 -0
- package/packages/server/src/editor-detection.ts +60 -0
- package/packages/server/src/editor-manager.ts +352 -0
- package/packages/server/src/editor-proxy.ts +134 -0
- package/packages/server/src/editor-registry.ts +108 -0
- package/packages/server/src/event-status-extraction.ts +131 -0
- package/packages/server/src/event-wiring.ts +589 -0
- package/packages/server/src/extension-register.ts +92 -0
- package/packages/server/src/git-operations.ts +200 -0
- package/packages/server/src/headless-pid-registry.ts +207 -0
- package/packages/server/src/idle-timer.ts +61 -0
- package/packages/server/src/json-store.ts +32 -0
- package/packages/server/src/localhost-guard.ts +117 -0
- package/packages/server/src/memory-event-store.ts +193 -0
- package/packages/server/src/memory-session-manager.ts +123 -0
- package/packages/server/src/meta-persistence.ts +64 -0
- package/packages/server/src/migrate-persistence.ts +195 -0
- package/packages/server/src/npm-search-proxy.ts +143 -0
- package/packages/server/src/oauth-callback-server.ts +177 -0
- package/packages/server/src/openspec-archive.ts +60 -0
- package/packages/server/src/package-manager-wrapper.ts +200 -0
- package/packages/server/src/pending-fork-registry.ts +53 -0
- package/packages/server/src/pending-load-manager.ts +110 -0
- package/packages/server/src/pending-resume-registry.ts +69 -0
- package/packages/server/src/pi-gateway.ts +419 -0
- package/packages/server/src/pi-resource-scanner.ts +369 -0
- package/packages/server/src/preferences-store.ts +116 -0
- package/packages/server/src/process-manager.ts +311 -0
- package/packages/server/src/provider-auth-handlers.ts +438 -0
- package/packages/server/src/provider-auth-storage.ts +200 -0
- package/packages/server/src/resolve-path.ts +12 -0
- package/packages/server/src/routes/editor-routes.ts +86 -0
- package/packages/server/src/routes/file-routes.ts +116 -0
- package/packages/server/src/routes/git-routes.ts +89 -0
- package/packages/server/src/routes/openspec-routes.ts +99 -0
- package/packages/server/src/routes/package-routes.ts +172 -0
- package/packages/server/src/routes/provider-auth-routes.ts +244 -0
- package/packages/server/src/routes/provider-routes.ts +101 -0
- package/packages/server/src/routes/route-deps.ts +23 -0
- package/packages/server/src/routes/session-routes.ts +91 -0
- package/packages/server/src/routes/system-routes.ts +271 -0
- package/packages/server/src/server-pid.ts +84 -0
- package/packages/server/src/server.ts +554 -0
- package/packages/server/src/session-api.ts +330 -0
- package/packages/server/src/session-bootstrap.ts +80 -0
- package/packages/server/src/session-diff.ts +178 -0
- package/packages/server/src/session-discovery.ts +134 -0
- package/packages/server/src/session-file-reader.ts +135 -0
- package/packages/server/src/session-order-manager.ts +73 -0
- package/packages/server/src/session-scanner.ts +233 -0
- package/packages/server/src/session-stats-reader.ts +99 -0
- package/packages/server/src/terminal-gateway.ts +51 -0
- package/packages/server/src/terminal-manager.ts +241 -0
- package/packages/server/src/tunnel.ts +329 -0
- package/packages/server/tsconfig.json +11 -0
- package/packages/shared/package.json +15 -0
- package/packages/shared/src/__tests__/config.test.ts +358 -0
- package/packages/shared/src/__tests__/deriveChangeState.test.ts +95 -0
- package/packages/shared/src/__tests__/mdns-discovery.test.ts +80 -0
- package/packages/shared/src/__tests__/protocol.test.ts +243 -0
- package/packages/shared/src/__tests__/resolve-jiti.test.ts +17 -0
- package/packages/shared/src/__tests__/server-identity.test.ts +73 -0
- package/packages/shared/src/__tests__/session-meta.test.ts +125 -0
- package/packages/shared/src/archive-types.ts +11 -0
- package/packages/shared/src/browser-protocol.ts +534 -0
- package/packages/shared/src/config.ts +245 -0
- package/packages/shared/src/diff-types.ts +41 -0
- package/packages/shared/src/editor-types.ts +18 -0
- package/packages/shared/src/mdns-discovery.ts +248 -0
- package/packages/shared/src/openspec-activity-detector.ts +109 -0
- package/packages/shared/src/openspec-poller.ts +96 -0
- package/packages/shared/src/protocol.ts +369 -0
- package/packages/shared/src/resolve-jiti.ts +43 -0
- package/packages/shared/src/rest-api.ts +255 -0
- package/packages/shared/src/server-identity.ts +51 -0
- package/packages/shared/src/session-meta.ts +86 -0
- package/packages/shared/src/state-replay.ts +174 -0
- package/packages/shared/src/stats-extractor.ts +54 -0
- package/packages/shared/src/terminal-types.ts +18 -0
- package/packages/shared/src/types.ts +351 -0
- package/packages/shared/tsconfig.json +8 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi Resource Scanner — discovers extensions, skills, and prompts
|
|
3
|
+
* from local (.pi/), global (~/.pi/agent/), and installed packages.
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import * as os from "node:os";
|
|
8
|
+
import { execSync } from "node:child_process";
|
|
9
|
+
import type { PiResource, PiResourceScope, PiPackageInfo, PiResourcesResult } from "@blackbelt-technology/pi-dashboard-shared/rest-api.js";
|
|
10
|
+
|
|
11
|
+
// ── Frontmatter Parsing ─────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
export function parseFrontmatter(
|
|
14
|
+
content: string,
|
|
15
|
+
fallbackFirstLine = false,
|
|
16
|
+
): { name?: string; description?: string } {
|
|
17
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
18
|
+
if (!match) {
|
|
19
|
+
if (fallbackFirstLine) {
|
|
20
|
+
const firstLine = content.split(/\r?\n/).find((l) => l.trim().length > 0);
|
|
21
|
+
return { description: firstLine?.trim() };
|
|
22
|
+
}
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const yaml = match[1];
|
|
27
|
+
const name = yaml.match(/^name:\s*(.+)$/m)?.[1]?.trim();
|
|
28
|
+
|
|
29
|
+
// Handle both single-line and multi-line (>) description
|
|
30
|
+
let description: string | undefined;
|
|
31
|
+
// Check for multi-line (> or |) first, then single-line
|
|
32
|
+
const multiMatch = yaml.match(/^description:\s*[>|]-?\s*\r?\n((?:[ \t]+.+(?:\r?\n|$))*)/m);
|
|
33
|
+
if (multiMatch) {
|
|
34
|
+
description = multiMatch[1]
|
|
35
|
+
.split(/\r?\n/)
|
|
36
|
+
.map((l) => l.trim())
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
.join(" ");
|
|
39
|
+
} else {
|
|
40
|
+
const singleLine = yaml.match(/^description:\s*(.+)$/m);
|
|
41
|
+
if (singleLine) {
|
|
42
|
+
description = singleLine[1].trim();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { name, description };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Directory Scanning Helpers ──────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
function safeReaddir(dir: string): string[] {
|
|
52
|
+
try {
|
|
53
|
+
return fs.readdirSync(dir);
|
|
54
|
+
} catch {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function safeReadFile(filePath: string): string | undefined {
|
|
60
|
+
try {
|
|
61
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
62
|
+
} catch {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function safeIsDirectory(p: string): boolean {
|
|
68
|
+
try {
|
|
69
|
+
return fs.statSync(p).isDirectory();
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function discoverSkills(skillsDir: string): PiResource[] {
|
|
76
|
+
const skills: PiResource[] = [];
|
|
77
|
+
for (const entry of safeReaddir(skillsDir)) {
|
|
78
|
+
const entryPath = path.join(skillsDir, entry);
|
|
79
|
+
if (safeIsDirectory(entryPath)) {
|
|
80
|
+
// Directory with SKILL.md
|
|
81
|
+
const skillFile = path.join(entryPath, "SKILL.md");
|
|
82
|
+
const content = safeReadFile(skillFile);
|
|
83
|
+
if (content) {
|
|
84
|
+
const fm = parseFrontmatter(content);
|
|
85
|
+
skills.push({
|
|
86
|
+
name: fm.name ?? entry,
|
|
87
|
+
description: fm.description,
|
|
88
|
+
filePath: skillFile,
|
|
89
|
+
type: "skill",
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
} else if (entry.endsWith(".md")) {
|
|
93
|
+
// Root .md file as single skill
|
|
94
|
+
const content = safeReadFile(entryPath);
|
|
95
|
+
const fm = content ? parseFrontmatter(content) : {};
|
|
96
|
+
skills.push({
|
|
97
|
+
name: fm.name ?? entry.replace(/\.md$/, ""),
|
|
98
|
+
description: fm.description,
|
|
99
|
+
filePath: entryPath,
|
|
100
|
+
type: "skill",
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return skills;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function discoverExtensions(extDir: string): PiResource[] {
|
|
108
|
+
const extensions: PiResource[] = [];
|
|
109
|
+
for (const entry of safeReaddir(extDir)) {
|
|
110
|
+
const entryPath = path.join(extDir, entry);
|
|
111
|
+
if (entry.endsWith(".ts") || entry.endsWith(".js")) {
|
|
112
|
+
extensions.push({
|
|
113
|
+
name: entry.replace(/\.(ts|js)$/, ""),
|
|
114
|
+
filePath: entryPath,
|
|
115
|
+
type: "extension",
|
|
116
|
+
});
|
|
117
|
+
} else if (safeIsDirectory(entryPath)) {
|
|
118
|
+
const indexTs = path.join(entryPath, "index.ts");
|
|
119
|
+
const indexJs = path.join(entryPath, "index.js");
|
|
120
|
+
const indexFile = fs.existsSync(indexTs) ? indexTs : fs.existsSync(indexJs) ? indexJs : null;
|
|
121
|
+
if (indexFile) {
|
|
122
|
+
extensions.push({
|
|
123
|
+
name: entry,
|
|
124
|
+
filePath: indexFile,
|
|
125
|
+
type: "extension",
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return extensions;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function discoverPrompts(promptsDir: string): PiResource[] {
|
|
134
|
+
const prompts: PiResource[] = [];
|
|
135
|
+
for (const entry of safeReaddir(promptsDir)) {
|
|
136
|
+
if (!entry.endsWith(".md")) continue;
|
|
137
|
+
const entryPath = path.join(promptsDir, entry);
|
|
138
|
+
if (safeIsDirectory(entryPath)) continue;
|
|
139
|
+
const content = safeReadFile(entryPath);
|
|
140
|
+
const fm = content ? parseFrontmatter(content, true) : {};
|
|
141
|
+
prompts.push({
|
|
142
|
+
name: entry.replace(/\.md$/, ""),
|
|
143
|
+
description: fm.description,
|
|
144
|
+
filePath: entryPath,
|
|
145
|
+
type: "prompt",
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return prompts;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function emptyScope(): PiResourceScope {
|
|
152
|
+
return { extensions: [], skills: [], prompts: [] };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── Scope Scanners ──────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
export function scanLocalResources(cwd: string): PiResourceScope {
|
|
158
|
+
const piDir = path.join(cwd, ".pi");
|
|
159
|
+
if (!fs.existsSync(piDir)) return emptyScope();
|
|
160
|
+
return {
|
|
161
|
+
extensions: discoverExtensions(path.join(piDir, "extensions")),
|
|
162
|
+
skills: discoverSkills(path.join(piDir, "skills")),
|
|
163
|
+
prompts: discoverPrompts(path.join(piDir, "prompts")),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function scanGlobalResources(globalDir: string): PiResourceScope {
|
|
168
|
+
if (!fs.existsSync(globalDir)) return emptyScope();
|
|
169
|
+
return {
|
|
170
|
+
extensions: discoverExtensions(path.join(globalDir, "extensions")),
|
|
171
|
+
skills: discoverSkills(path.join(globalDir, "skills")),
|
|
172
|
+
prompts: discoverPrompts(path.join(globalDir, "prompts")),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── Package Resolution ──────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
let cachedNpmGlobalRoot: string | null = null;
|
|
179
|
+
|
|
180
|
+
function getNpmGlobalRoot(): string | null {
|
|
181
|
+
if (cachedNpmGlobalRoot !== null) return cachedNpmGlobalRoot;
|
|
182
|
+
try {
|
|
183
|
+
cachedNpmGlobalRoot = execSync("npm root -g", { encoding: "utf-8", timeout: 10_000 }).trim();
|
|
184
|
+
return cachedNpmGlobalRoot;
|
|
185
|
+
} catch {
|
|
186
|
+
cachedNpmGlobalRoot = "";
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Visible for testing — reset cached npm root */
|
|
192
|
+
export function _resetNpmRootCache() {
|
|
193
|
+
cachedNpmGlobalRoot = null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function resolvePackagePath(entry: string, settingsDir: string, scope: "local" | "global", cwd?: string): { resolved: string; source: string } | null {
|
|
197
|
+
if (typeof entry === "object") {
|
|
198
|
+
// Object-form package with source key
|
|
199
|
+
entry = (entry as any).source ?? "";
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (entry.startsWith("npm:")) {
|
|
203
|
+
const pkgName = entry.slice(4).replace(/@[^/]*$/, ""); // strip version
|
|
204
|
+
const npmRoot = getNpmGlobalRoot();
|
|
205
|
+
if (!npmRoot) return null;
|
|
206
|
+
return { resolved: path.join(npmRoot, pkgName), source: entry };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (entry.startsWith("git:") || entry.startsWith("https://") || entry.startsWith("ssh://") || entry.startsWith("http://")) {
|
|
210
|
+
// Extract host/path from git URL
|
|
211
|
+
let url = entry.replace(/^git:/, "");
|
|
212
|
+
// Handle git@host:path format
|
|
213
|
+
url = url.replace(/^git@([^:]+):/, "$1/");
|
|
214
|
+
// Strip protocol
|
|
215
|
+
url = url.replace(/^(https?|ssh|git):\/\//, "");
|
|
216
|
+
// Strip auth
|
|
217
|
+
url = url.replace(/^[^@]+@/, "");
|
|
218
|
+
// Strip .git suffix and version ref
|
|
219
|
+
url = url.replace(/\.git$/, "").replace(/@[^/]*$/, "");
|
|
220
|
+
|
|
221
|
+
const baseDir = scope === "local" && cwd
|
|
222
|
+
? path.join(cwd, ".pi", "git")
|
|
223
|
+
: path.join(os.homedir(), ".pi", "agent", "git");
|
|
224
|
+
return { resolved: path.join(baseDir, url), source: entry };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Local path (relative or absolute)
|
|
228
|
+
if (path.isAbsolute(entry)) {
|
|
229
|
+
return { resolved: entry, source: entry };
|
|
230
|
+
}
|
|
231
|
+
return { resolved: path.resolve(settingsDir, entry), source: entry };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function scanPackageDir(pkgDir: string): PiResourceScope {
|
|
235
|
+
// Try pi manifest from package.json
|
|
236
|
+
const pkgJsonPath = path.join(pkgDir, "package.json");
|
|
237
|
+
const pkgJsonStr = safeReadFile(pkgJsonPath);
|
|
238
|
+
if (pkgJsonStr) {
|
|
239
|
+
try {
|
|
240
|
+
const pkgJson = JSON.parse(pkgJsonStr);
|
|
241
|
+
if (pkgJson.pi) {
|
|
242
|
+
const scope = emptyScope();
|
|
243
|
+
if (Array.isArray(pkgJson.pi.extensions)) {
|
|
244
|
+
for (const extPath of pkgJson.pi.extensions) {
|
|
245
|
+
const resolved = path.resolve(pkgDir, extPath);
|
|
246
|
+
if (fs.existsSync(resolved)) {
|
|
247
|
+
if (safeIsDirectory(resolved)) {
|
|
248
|
+
scope.extensions.push(...discoverExtensions(resolved));
|
|
249
|
+
} else {
|
|
250
|
+
const name = path.basename(resolved).replace(/\.(ts|js)$/, "");
|
|
251
|
+
scope.extensions.push({ name, filePath: resolved, type: "extension" });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (Array.isArray(pkgJson.pi.skills)) {
|
|
257
|
+
for (const skillPath of pkgJson.pi.skills) {
|
|
258
|
+
const resolved = path.resolve(pkgDir, skillPath);
|
|
259
|
+
if (safeIsDirectory(resolved)) {
|
|
260
|
+
scope.skills.push(...discoverSkills(resolved));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (Array.isArray(pkgJson.pi.prompts)) {
|
|
265
|
+
for (const promptPath of pkgJson.pi.prompts) {
|
|
266
|
+
const resolved = path.resolve(pkgDir, promptPath);
|
|
267
|
+
if (safeIsDirectory(resolved)) {
|
|
268
|
+
scope.prompts.push(...discoverPrompts(resolved));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return scope;
|
|
273
|
+
}
|
|
274
|
+
} catch {
|
|
275
|
+
// Invalid JSON, fall through to conventional
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Conventional directory discovery
|
|
280
|
+
return {
|
|
281
|
+
extensions: discoverExtensions(path.join(pkgDir, "extensions")),
|
|
282
|
+
skills: discoverSkills(path.join(pkgDir, "skills")),
|
|
283
|
+
prompts: discoverPrompts(path.join(pkgDir, "prompts")),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function readSettingsPackages(settingsPath: string): string[] {
|
|
288
|
+
const content = safeReadFile(settingsPath);
|
|
289
|
+
if (!content) return [];
|
|
290
|
+
try {
|
|
291
|
+
const settings = JSON.parse(content);
|
|
292
|
+
if (!Array.isArray(settings.packages)) return [];
|
|
293
|
+
return settings.packages.map((p: string | { source: string }) =>
|
|
294
|
+
typeof p === "string" ? p : p.source,
|
|
295
|
+
);
|
|
296
|
+
} catch {
|
|
297
|
+
return [];
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function resolvePackages(
|
|
302
|
+
entries: string[],
|
|
303
|
+
settingsDir: string,
|
|
304
|
+
scope: "local" | "global" = "local",
|
|
305
|
+
cwd?: string,
|
|
306
|
+
): PiPackageInfo[] {
|
|
307
|
+
const packages: PiPackageInfo[] = [];
|
|
308
|
+
const seen = new Set<string>();
|
|
309
|
+
|
|
310
|
+
for (const entry of entries) {
|
|
311
|
+
const resolved = resolvePackagePath(entry, settingsDir, scope, cwd);
|
|
312
|
+
if (!resolved || !fs.existsSync(resolved.resolved)) continue;
|
|
313
|
+
|
|
314
|
+
const realDir = resolved.resolved;
|
|
315
|
+
if (seen.has(realDir)) continue;
|
|
316
|
+
seen.add(realDir);
|
|
317
|
+
|
|
318
|
+
// Read package.json for metadata
|
|
319
|
+
const pkgJsonStr = safeReadFile(path.join(realDir, "package.json"));
|
|
320
|
+
let name = path.basename(realDir);
|
|
321
|
+
let description: string | undefined;
|
|
322
|
+
if (pkgJsonStr) {
|
|
323
|
+
try {
|
|
324
|
+
const pkgJson = JSON.parse(pkgJsonStr);
|
|
325
|
+
name = pkgJson.name ?? name;
|
|
326
|
+
description = pkgJson.description;
|
|
327
|
+
} catch { /* ignore */ }
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const resources = scanPackageDir(realDir);
|
|
331
|
+
packages.push({ name, description, source: resolved.source, resources, scope });
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return packages;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ── Main Entry Point ────────────────────────────────────────────────
|
|
338
|
+
|
|
339
|
+
export interface ScanOptions {
|
|
340
|
+
globalDir?: string;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export async function scanPiResources(cwd: string, options?: ScanOptions): Promise<PiResourcesResult> {
|
|
344
|
+
const globalDir = options?.globalDir ?? path.join(os.homedir(), ".pi", "agent");
|
|
345
|
+
|
|
346
|
+
const local = scanLocalResources(cwd);
|
|
347
|
+
const global = scanGlobalResources(globalDir);
|
|
348
|
+
|
|
349
|
+
// Collect package entries from both settings files
|
|
350
|
+
const localSettingsPath = path.join(cwd, ".pi", "settings.json");
|
|
351
|
+
const globalSettingsPath = path.join(globalDir, "settings.json");
|
|
352
|
+
|
|
353
|
+
const localPackageEntries = readSettingsPackages(localSettingsPath);
|
|
354
|
+
const globalPackageEntries = readSettingsPackages(globalSettingsPath);
|
|
355
|
+
|
|
356
|
+
// Local packages first (they win on dedup)
|
|
357
|
+
const localPackages = resolvePackages(localPackageEntries, path.dirname(localSettingsPath), "local", cwd);
|
|
358
|
+
const globalPackages = resolvePackages(globalPackageEntries, path.dirname(globalSettingsPath), "global");
|
|
359
|
+
|
|
360
|
+
// Deduplicate: local wins
|
|
361
|
+
const localNames = new Set(localPackages.map((p) => p.name));
|
|
362
|
+
const dedupedGlobal = globalPackages.filter((p) => !localNames.has(p.name));
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
local,
|
|
366
|
+
global,
|
|
367
|
+
packages: [...localPackages, ...dedupedGlobal],
|
|
368
|
+
};
|
|
369
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global UI preferences store — JSON-backed with debounced writes.
|
|
3
|
+
* Stores cross-session state: pinned directories and session ordering.
|
|
4
|
+
* Replaces `state-store.ts` (hidden state moved to per-session `.meta.json`).
|
|
5
|
+
*/
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { CONFIG_DIR } from "@blackbelt-technology/pi-dashboard-shared/config.js";
|
|
8
|
+
import { readJsonFile, writeJsonFile } from "./json-store.js";
|
|
9
|
+
import { safeRealpathSync } from "./resolve-path.js";
|
|
10
|
+
|
|
11
|
+
export const PREFERENCES_FILE = path.join(CONFIG_DIR, "preferences.json");
|
|
12
|
+
|
|
13
|
+
interface PreferencesData {
|
|
14
|
+
sessionOrder: Record<string, string[]>;
|
|
15
|
+
pinnedDirectories: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PreferencesStore {
|
|
19
|
+
getSessionOrder(): Record<string, string[]>;
|
|
20
|
+
setSessionOrder(order: Record<string, string[]>): void;
|
|
21
|
+
getPinnedDirectories(): string[];
|
|
22
|
+
setPinnedDirectories(dirs: string[]): void;
|
|
23
|
+
pinDirectory(dirPath: string): void;
|
|
24
|
+
unpinDirectory(dirPath: string): void;
|
|
25
|
+
reorderPinnedDirs(dirs: string[]): void;
|
|
26
|
+
flush(): void;
|
|
27
|
+
dispose(): void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const DEBOUNCE_MS = 1000;
|
|
31
|
+
|
|
32
|
+
export function createPreferencesStore(filePath: string = PREFERENCES_FILE): PreferencesStore {
|
|
33
|
+
const data: PreferencesData = readJsonFile<PreferencesData>(filePath, { sessionOrder: {}, pinnedDirectories: [] });
|
|
34
|
+
let sessionOrder: Record<string, string[]> = data.sessionOrder ?? {};
|
|
35
|
+
// Resolve symlinks in stored pinned paths on load
|
|
36
|
+
const rawPinned = data.pinnedDirectories ?? [];
|
|
37
|
+
let pinnedDirectories: string[] = rawPinned.map(safeRealpathSync);
|
|
38
|
+
// Deduplicate in case symlinks resolved to the same path
|
|
39
|
+
pinnedDirectories = [...new Set(pinnedDirectories)];
|
|
40
|
+
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
41
|
+
let dirty = pinnedDirectories.length !== rawPinned.length || pinnedDirectories.some((p, i) => p !== rawPinned[i]);
|
|
42
|
+
|
|
43
|
+
function scheduleSave(): void {
|
|
44
|
+
dirty = true;
|
|
45
|
+
if (debounceTimer) return;
|
|
46
|
+
debounceTimer = setTimeout(() => {
|
|
47
|
+
debounceTimer = null;
|
|
48
|
+
if (dirty) {
|
|
49
|
+
dirty = false;
|
|
50
|
+
writeJsonFile(filePath, { sessionOrder, pinnedDirectories } satisfies PreferencesData);
|
|
51
|
+
}
|
|
52
|
+
}, DEBOUNCE_MS);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function flushNow(): void {
|
|
56
|
+
if (debounceTimer) {
|
|
57
|
+
clearTimeout(debounceTimer);
|
|
58
|
+
debounceTimer = null;
|
|
59
|
+
}
|
|
60
|
+
if (dirty) {
|
|
61
|
+
dirty = false;
|
|
62
|
+
writeJsonFile(filePath, { sessionOrder, pinnedDirectories } satisfies PreferencesData);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (dirty) scheduleSave();
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
getSessionOrder(): Record<string, string[]> {
|
|
70
|
+
return sessionOrder;
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
setSessionOrder(order: Record<string, string[]>): void {
|
|
74
|
+
sessionOrder = order;
|
|
75
|
+
scheduleSave();
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
getPinnedDirectories(): string[] {
|
|
79
|
+
return [...pinnedDirectories];
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
setPinnedDirectories(dirs: string[]): void {
|
|
83
|
+
pinnedDirectories = [...dirs];
|
|
84
|
+
scheduleSave();
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
pinDirectory(dirPath: string): void {
|
|
88
|
+
if (pinnedDirectories.includes(dirPath)) return;
|
|
89
|
+
pinnedDirectories.push(dirPath);
|
|
90
|
+
scheduleSave();
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
unpinDirectory(dirPath: string): void {
|
|
94
|
+
const idx = pinnedDirectories.indexOf(dirPath);
|
|
95
|
+
if (idx === -1) return;
|
|
96
|
+
pinnedDirectories.splice(idx, 1);
|
|
97
|
+
scheduleSave();
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
reorderPinnedDirs(dirs: string[]): void {
|
|
101
|
+
pinnedDirectories = [...dirs];
|
|
102
|
+
scheduleSave();
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
flush(): void {
|
|
106
|
+
flushNow();
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
dispose(): void {
|
|
110
|
+
if (debounceTimer) {
|
|
111
|
+
clearTimeout(debounceTimer);
|
|
112
|
+
debounceTimer = null;
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|