@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,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared configuration module for PI Dashboard.
|
|
3
|
+
* Used by both the server CLI and bridge extension.
|
|
4
|
+
*/
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
|
|
9
|
+
export const CONFIG_DIR = path.join(os.homedir(), ".pi", "dashboard");
|
|
10
|
+
export const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
11
|
+
|
|
12
|
+
export type SpawnStrategy = "tmux" | "headless";
|
|
13
|
+
|
|
14
|
+
export interface AuthProviderConfig {
|
|
15
|
+
clientId: string;
|
|
16
|
+
clientSecret: string;
|
|
17
|
+
issuerUrl?: string;
|
|
18
|
+
name?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface AuthConfig {
|
|
22
|
+
secret: string;
|
|
23
|
+
providers: Record<string, AuthProviderConfig>;
|
|
24
|
+
allowedUsers?: string[];
|
|
25
|
+
bypassUrls?: string[];
|
|
26
|
+
bypassHosts?: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface MemoryLimitsConfig {
|
|
30
|
+
/** Max events stored per session (0 = unlimited). Default: 200 */
|
|
31
|
+
maxEventsPerSession: number;
|
|
32
|
+
/** Max chars before truncating string fields in events (0 = no truncation). Default: 0 (disabled) */
|
|
33
|
+
maxStringFieldSize: number;
|
|
34
|
+
/** Max bytes in browser WebSocket send buffer before dropping messages (0 = no limit). Default: 4194304 (4MB) */
|
|
35
|
+
maxWsBufferBytes: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const DEFAULT_MEMORY_LIMITS: MemoryLimitsConfig = {
|
|
39
|
+
maxEventsPerSession: 5000,
|
|
40
|
+
maxStringFieldSize: 0,
|
|
41
|
+
maxWsBufferBytes: 4 * 1024 * 1024,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export interface EditorConfig {
|
|
45
|
+
/** Override path to code-server binary */
|
|
46
|
+
binary?: string;
|
|
47
|
+
/** Minutes before idle instance is killed (default: 10) */
|
|
48
|
+
idleTimeoutMinutes: number;
|
|
49
|
+
/** Maximum concurrent code-server instances (default: 3) */
|
|
50
|
+
maxInstances: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const DEFAULT_EDITOR_CONFIG: EditorConfig = {
|
|
54
|
+
idleTimeoutMinutes: 10,
|
|
55
|
+
maxInstances: 3,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export interface DashboardConfig {
|
|
59
|
+
port: number;
|
|
60
|
+
piPort: number;
|
|
61
|
+
autoStart: boolean;
|
|
62
|
+
autoShutdown: boolean;
|
|
63
|
+
shutdownIdleSeconds: number;
|
|
64
|
+
spawnStrategy: SpawnStrategy;
|
|
65
|
+
tunnel: { enabled: boolean; reservedToken?: string };
|
|
66
|
+
devBuildOnReload: boolean;
|
|
67
|
+
auth?: AuthConfig;
|
|
68
|
+
defaultModel: string;
|
|
69
|
+
memoryLimits: MemoryLimitsConfig;
|
|
70
|
+
editor: EditorConfig;
|
|
71
|
+
/** Networks trusted for full access without authentication (CIDR, wildcard, exact IP) */
|
|
72
|
+
trustedNetworks: string[];
|
|
73
|
+
/** Merged trustedNetworks + auth.bypassHosts (deduplicated). Computed at load time. */
|
|
74
|
+
resolvedTrustedNetworks: string[];
|
|
75
|
+
/** CORS allowed origins for cross-origin client hosting */
|
|
76
|
+
cors: CorsConfig;
|
|
77
|
+
/** Last-used server address (host:port) for reconnection */
|
|
78
|
+
lastServer?: string;
|
|
79
|
+
/** Whether the server was launched by the Electron app */
|
|
80
|
+
electronMode: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface CorsConfig {
|
|
84
|
+
/** Additional origins allowed for cross-origin requests */
|
|
85
|
+
allowedOrigins: string[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const VALID_SPAWN_STRATEGIES: SpawnStrategy[] = ["tmux", "headless"];
|
|
89
|
+
|
|
90
|
+
const DEFAULTS: DashboardConfig = {
|
|
91
|
+
port: 8000,
|
|
92
|
+
piPort: 9999,
|
|
93
|
+
autoStart: true,
|
|
94
|
+
autoShutdown: false,
|
|
95
|
+
shutdownIdleSeconds: 300,
|
|
96
|
+
spawnStrategy: "headless",
|
|
97
|
+
tunnel: { enabled: true },
|
|
98
|
+
devBuildOnReload: false,
|
|
99
|
+
defaultModel: "",
|
|
100
|
+
memoryLimits: { ...DEFAULT_MEMORY_LIMITS },
|
|
101
|
+
editor: { ...DEFAULT_EDITOR_CONFIG },
|
|
102
|
+
trustedNetworks: [],
|
|
103
|
+
resolvedTrustedNetworks: [],
|
|
104
|
+
cors: { allowedOrigins: [] },
|
|
105
|
+
electronMode: false,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Parse and validate the auth config section.
|
|
110
|
+
* Returns undefined if auth is not configured or has no providers.
|
|
111
|
+
*/
|
|
112
|
+
function parseAuthConfig(raw: any): AuthConfig | undefined {
|
|
113
|
+
if (!raw || typeof raw !== "object") return undefined;
|
|
114
|
+
const providers = raw.providers;
|
|
115
|
+
if (!providers || typeof providers !== "object" || Object.keys(providers).length === 0) {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
// Validate each provider has at least clientId and clientSecret
|
|
119
|
+
const validProviders: Record<string, AuthProviderConfig> = {};
|
|
120
|
+
for (const [key, value] of Object.entries(providers)) {
|
|
121
|
+
const p = value as any;
|
|
122
|
+
if (p && typeof p === "object" && p.clientId && p.clientSecret) {
|
|
123
|
+
validProviders[key] = {
|
|
124
|
+
clientId: p.clientId,
|
|
125
|
+
clientSecret: p.clientSecret,
|
|
126
|
+
...(p.issuerUrl ? { issuerUrl: p.issuerUrl } : {}),
|
|
127
|
+
...(p.name ? { name: p.name } : {}),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (Object.keys(validProviders).length === 0) return undefined;
|
|
132
|
+
return {
|
|
133
|
+
secret: raw.secret ?? "",
|
|
134
|
+
providers: validProviders,
|
|
135
|
+
...(Array.isArray(raw.allowedUsers) ? { allowedUsers: raw.allowedUsers } : Array.isArray(raw.allowedEmails) ? { allowedUsers: raw.allowedEmails } : {}),
|
|
136
|
+
bypassUrls: Array.isArray(raw.bypassUrls) ? raw.bypassUrls.filter((u: unknown) => typeof u === "string") : [],
|
|
137
|
+
bypassHosts: Array.isArray(raw.bypassHosts) ? raw.bypassHosts.filter((u: unknown) => typeof u === "string") : [],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function parseEditorConfig(raw: any): EditorConfig {
|
|
142
|
+
if (!raw || typeof raw !== "object") return { ...DEFAULT_EDITOR_CONFIG };
|
|
143
|
+
return {
|
|
144
|
+
...(typeof raw.binary === "string" ? { binary: raw.binary } : {}),
|
|
145
|
+
idleTimeoutMinutes: typeof raw.idleTimeoutMinutes === "number" ? raw.idleTimeoutMinutes : DEFAULT_EDITOR_CONFIG.idleTimeoutMinutes,
|
|
146
|
+
maxInstances: typeof raw.maxInstances === "number" ? raw.maxInstances : DEFAULT_EDITOR_CONFIG.maxInstances,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function parseMemoryLimits(raw: any): MemoryLimitsConfig {
|
|
151
|
+
if (!raw || typeof raw !== "object") return { ...DEFAULT_MEMORY_LIMITS };
|
|
152
|
+
return {
|
|
153
|
+
maxEventsPerSession: typeof raw.maxEventsPerSession === "number" ? raw.maxEventsPerSession : DEFAULT_MEMORY_LIMITS.maxEventsPerSession,
|
|
154
|
+
maxStringFieldSize: typeof raw.maxStringFieldSize === "number" ? raw.maxStringFieldSize : DEFAULT_MEMORY_LIMITS.maxStringFieldSize,
|
|
155
|
+
maxWsBufferBytes: typeof raw.maxWsBufferBytes === "number" ? raw.maxWsBufferBytes : DEFAULT_MEMORY_LIMITS.maxWsBufferBytes,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function parseTrustedNetworks(raw: any): string[] {
|
|
160
|
+
if (!Array.isArray(raw)) return [];
|
|
161
|
+
return raw.filter((entry: unknown) => typeof entry === "string" && entry.length > 0);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Load configuration from ~/.pi/dashboard/config.json.
|
|
166
|
+
* Returns defaults for missing fields, malformed JSON, or missing file.
|
|
167
|
+
*/
|
|
168
|
+
export function loadConfig(): DashboardConfig {
|
|
169
|
+
const configDir = path.join(os.homedir(), ".pi", "dashboard");
|
|
170
|
+
const configFile = path.join(configDir, "config.json");
|
|
171
|
+
const defaults: DashboardConfig = { ...DEFAULTS };
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
if (!fs.existsSync(configFile)) return defaults;
|
|
175
|
+
const raw = fs.readFileSync(configFile, "utf-8");
|
|
176
|
+
if (!raw.trim()) return defaults;
|
|
177
|
+
const parsed = JSON.parse(raw);
|
|
178
|
+
const rawStrategy = parsed.spawnStrategy;
|
|
179
|
+
const spawnStrategy: SpawnStrategy =
|
|
180
|
+
VALID_SPAWN_STRATEGIES.includes(rawStrategy) ? rawStrategy : defaults.spawnStrategy;
|
|
181
|
+
|
|
182
|
+
const result: DashboardConfig = {
|
|
183
|
+
port: parsed.port ?? defaults.port,
|
|
184
|
+
piPort: parsed.piPort ?? defaults.piPort,
|
|
185
|
+
autoStart: parsed.autoStart ?? defaults.autoStart,
|
|
186
|
+
autoShutdown: parsed.autoShutdown ?? defaults.autoShutdown,
|
|
187
|
+
shutdownIdleSeconds: parsed.shutdownIdleSeconds ?? defaults.shutdownIdleSeconds,
|
|
188
|
+
spawnStrategy,
|
|
189
|
+
tunnel: {
|
|
190
|
+
enabled: parsed.tunnel?.enabled ?? defaults.tunnel.enabled,
|
|
191
|
+
...(parsed.tunnel?.reservedToken ? { reservedToken: parsed.tunnel.reservedToken } : {}),
|
|
192
|
+
},
|
|
193
|
+
devBuildOnReload: parsed.devBuildOnReload ?? defaults.devBuildOnReload,
|
|
194
|
+
defaultModel: typeof parsed.defaultModel === "string" ? parsed.defaultModel : defaults.defaultModel,
|
|
195
|
+
auth: parseAuthConfig(parsed.auth),
|
|
196
|
+
memoryLimits: parseMemoryLimits(parsed.memoryLimits),
|
|
197
|
+
editor: parseEditorConfig(parsed.editor),
|
|
198
|
+
trustedNetworks: parseTrustedNetworks(parsed.trustedNetworks),
|
|
199
|
+
resolvedTrustedNetworks: [],
|
|
200
|
+
cors: {
|
|
201
|
+
allowedOrigins: Array.isArray(parsed.cors?.allowedOrigins)
|
|
202
|
+
? parsed.cors.allowedOrigins.filter((o: unknown) => typeof o === "string")
|
|
203
|
+
: defaults.cors.allowedOrigins,
|
|
204
|
+
},
|
|
205
|
+
...(typeof parsed.lastServer === "string" ? { lastServer: parsed.lastServer } : {}),
|
|
206
|
+
electronMode: parsed.electronMode === true,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// Compute resolvedTrustedNetworks: merge trustedNetworks + auth.bypassHosts
|
|
210
|
+
const merged = new Set(result.trustedNetworks);
|
|
211
|
+
if (result.auth?.bypassHosts) {
|
|
212
|
+
for (const h of result.auth.bypassHosts) merged.add(h);
|
|
213
|
+
}
|
|
214
|
+
result.resolvedTrustedNetworks = Array.from(merged);
|
|
215
|
+
return result;
|
|
216
|
+
} catch {
|
|
217
|
+
return defaults;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Create ~/.pi/dashboard/config.json with defaults if it doesn't exist.
|
|
223
|
+
* Creates the directory recursively if needed.
|
|
224
|
+
*/
|
|
225
|
+
export function ensureConfig(): void {
|
|
226
|
+
const configDir = path.join(os.homedir(), ".pi", "dashboard");
|
|
227
|
+
const configFile = path.join(configDir, "config.json");
|
|
228
|
+
|
|
229
|
+
if (fs.existsSync(configFile)) return;
|
|
230
|
+
|
|
231
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
232
|
+
|
|
233
|
+
const defaults = {
|
|
234
|
+
port: DEFAULTS.port,
|
|
235
|
+
piPort: DEFAULTS.piPort,
|
|
236
|
+
autoStart: DEFAULTS.autoStart,
|
|
237
|
+
autoShutdown: DEFAULTS.autoShutdown,
|
|
238
|
+
shutdownIdleSeconds: DEFAULTS.shutdownIdleSeconds,
|
|
239
|
+
spawnStrategy: DEFAULTS.spawnStrategy,
|
|
240
|
+
tunnel: DEFAULTS.tunnel,
|
|
241
|
+
devBuildOnReload: DEFAULTS.devBuildOnReload,
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
fs.writeFileSync(configFile, JSON.stringify(defaults, null, 2) + "\n");
|
|
245
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the session file diff API.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** A single edit operation (oldText → newText replacement) */
|
|
6
|
+
export interface EditOperation {
|
|
7
|
+
oldText: string;
|
|
8
|
+
newText: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** An individual file change event extracted from session events */
|
|
12
|
+
export interface FileChangeEvent {
|
|
13
|
+
/** "edit" or "write" */
|
|
14
|
+
type: "edit" | "write";
|
|
15
|
+
/** Event timestamp (ms since epoch) */
|
|
16
|
+
timestamp: number;
|
|
17
|
+
/** Truncated assistant message preceding this change (reason/context) */
|
|
18
|
+
message?: string;
|
|
19
|
+
/** Edit operations (only for type "edit") */
|
|
20
|
+
edits?: EditOperation[];
|
|
21
|
+
/** Written content (only for type "write") */
|
|
22
|
+
content?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A file entry with all its change events */
|
|
26
|
+
export interface FileDiffEntry {
|
|
27
|
+
/** File path relative to cwd */
|
|
28
|
+
path: string;
|
|
29
|
+
/** Individual change events, ordered by timestamp */
|
|
30
|
+
changes: FileChangeEvent[];
|
|
31
|
+
/** Aggregate git diff (unified format) when available */
|
|
32
|
+
gitDiff?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Response from GET /api/session-diff */
|
|
36
|
+
export interface SessionDiffResponse {
|
|
37
|
+
/** Changed files with their change events */
|
|
38
|
+
files: FileDiffEntry[];
|
|
39
|
+
/** Whether the session cwd is a git repository */
|
|
40
|
+
isGitRepo: boolean;
|
|
41
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the browser-based editor (code-server) feature.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type EditorInstanceStatus = "starting" | "ready" | "stopped";
|
|
6
|
+
|
|
7
|
+
export interface EditorInstance {
|
|
8
|
+
id: string;
|
|
9
|
+
cwd: string;
|
|
10
|
+
port: number;
|
|
11
|
+
status: EditorInstanceStatus;
|
|
12
|
+
proxyPath: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface EditorDetectionResult {
|
|
16
|
+
available: boolean;
|
|
17
|
+
binary?: string;
|
|
18
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mDNS discovery module for pi-dashboard.
|
|
3
|
+
* Advertises and discovers `_pi-dashboard._tcp` services on the local network.
|
|
4
|
+
*/
|
|
5
|
+
import { Bonjour, type Service, type Browser } from "bonjour-service";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import { EventEmitter } from "node:events";
|
|
8
|
+
import { isDashboardRunning } from "./server-identity.js";
|
|
9
|
+
|
|
10
|
+
const SERVICE_TYPE = "pi-dashboard";
|
|
11
|
+
|
|
12
|
+
export interface DiscoveredServer {
|
|
13
|
+
/** Hostname of the machine running the server */
|
|
14
|
+
host: string;
|
|
15
|
+
/** HTTP port */
|
|
16
|
+
port: number;
|
|
17
|
+
/** Pi gateway WebSocket port */
|
|
18
|
+
piPort: number;
|
|
19
|
+
/** Dashboard version */
|
|
20
|
+
version: string;
|
|
21
|
+
/** Server process PID */
|
|
22
|
+
pid: number;
|
|
23
|
+
/** Whether the server is on this machine */
|
|
24
|
+
isLocal: boolean;
|
|
25
|
+
/** How the server was discovered: "mdns" or "fallback" */
|
|
26
|
+
source: "mdns" | "fallback";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let bonjourInstance: Bonjour | null = null;
|
|
30
|
+
let publishedService: Service | null = null;
|
|
31
|
+
|
|
32
|
+
function getBonjour(): Bonjour {
|
|
33
|
+
if (!bonjourInstance) {
|
|
34
|
+
bonjourInstance = new Bonjour();
|
|
35
|
+
}
|
|
36
|
+
return bonjourInstance;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Advertise this dashboard server on mDNS.
|
|
41
|
+
*/
|
|
42
|
+
export function advertiseDashboard(port: number, piPort: number): void {
|
|
43
|
+
const bonjour = getBonjour();
|
|
44
|
+
const pkg = { version: "0.0.0" }; // Will be replaced by actual version
|
|
45
|
+
try {
|
|
46
|
+
const pkgJson = require("../../package.json");
|
|
47
|
+
pkg.version = pkgJson.version ?? "0.0.0";
|
|
48
|
+
} catch { /* ignore */ }
|
|
49
|
+
|
|
50
|
+
publishedService = bonjour.publish({
|
|
51
|
+
name: `pi-dashboard-${os.hostname()}-${port}`,
|
|
52
|
+
type: SERVICE_TYPE,
|
|
53
|
+
port,
|
|
54
|
+
txt: {
|
|
55
|
+
version: pkg.version,
|
|
56
|
+
pid: String(process.pid),
|
|
57
|
+
piPort: String(piPort),
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Stop advertising this dashboard server.
|
|
64
|
+
*/
|
|
65
|
+
export function stopAdvertising(): void {
|
|
66
|
+
if (publishedService) {
|
|
67
|
+
publishedService.stop(() => {});
|
|
68
|
+
publishedService = null;
|
|
69
|
+
}
|
|
70
|
+
if (bonjourInstance) {
|
|
71
|
+
bonjourInstance.destroy();
|
|
72
|
+
bonjourInstance = null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if a discovered service is running on the local machine.
|
|
78
|
+
*/
|
|
79
|
+
export function isLocalService(service: Service): boolean {
|
|
80
|
+
const hostname = os.hostname();
|
|
81
|
+
const host = service.host ?? "";
|
|
82
|
+
|
|
83
|
+
// Direct hostname match
|
|
84
|
+
if (host === hostname || host === `${hostname}.local` || host === "localhost") {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check against local network addresses
|
|
89
|
+
const localAddresses = getLocalAddresses();
|
|
90
|
+
const serviceAddresses = service.addresses ?? [];
|
|
91
|
+
return serviceAddresses.some(addr => localAddresses.has(addr));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getLocalAddresses(): Set<string> {
|
|
95
|
+
const addresses = new Set<string>(["127.0.0.1", "::1"]);
|
|
96
|
+
const interfaces = os.networkInterfaces();
|
|
97
|
+
for (const iface of Object.values(interfaces)) {
|
|
98
|
+
if (!iface) continue;
|
|
99
|
+
for (const info of iface) {
|
|
100
|
+
addresses.add(info.address);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return addresses;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function serviceToServer(service: Service, isLocal: boolean): DiscoveredServer {
|
|
107
|
+
const txt = service.txt as Record<string, string> | undefined;
|
|
108
|
+
return {
|
|
109
|
+
host: service.host ?? "unknown",
|
|
110
|
+
port: service.port,
|
|
111
|
+
piPort: parseInt(txt?.piPort ?? "9999", 10),
|
|
112
|
+
version: txt?.version ?? "unknown",
|
|
113
|
+
pid: parseInt(txt?.pid ?? "0", 10),
|
|
114
|
+
isLocal,
|
|
115
|
+
source: "mdns",
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* One-shot discovery: browse for dashboard servers with timeout.
|
|
121
|
+
* Returns localhost servers first, then remote.
|
|
122
|
+
*/
|
|
123
|
+
export async function discoverDashboard(timeout = 2000): Promise<DiscoveredServer[]> {
|
|
124
|
+
return new Promise((resolve) => {
|
|
125
|
+
const servers: DiscoveredServer[] = [];
|
|
126
|
+
// Use a fresh Bonjour instance to avoid conflicts with the
|
|
127
|
+
// singleton used by advertiseDashboard()
|
|
128
|
+
const bonjour = new Bonjour();
|
|
129
|
+
let resolved = false;
|
|
130
|
+
|
|
131
|
+
const finish = () => {
|
|
132
|
+
if (resolved) return;
|
|
133
|
+
resolved = true;
|
|
134
|
+
browser.stop();
|
|
135
|
+
bonjour.destroy();
|
|
136
|
+
// Sort: local first
|
|
137
|
+
servers.sort((a, b) => (a.isLocal === b.isLocal ? 0 : a.isLocal ? -1 : 1));
|
|
138
|
+
resolve(servers);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const timer = setTimeout(finish, timeout);
|
|
142
|
+
|
|
143
|
+
const browser = bonjour.find({ type: SERVICE_TYPE });
|
|
144
|
+
browser.on("up", (service: Service) => {
|
|
145
|
+
const isLocal = isLocalService(service);
|
|
146
|
+
servers.push(serviceToServer(service, isLocal));
|
|
147
|
+
// Resolve early once a local server is found
|
|
148
|
+
if (isLocal) {
|
|
149
|
+
clearTimeout(timer);
|
|
150
|
+
// Small delay to collect any other simultaneous responses
|
|
151
|
+
setTimeout(finish, 100);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Fallback discovery: probe localhost via health check when mDNS finds nothing.
|
|
159
|
+
*/
|
|
160
|
+
export async function discoverFallback(port: number): Promise<DiscoveredServer | null> {
|
|
161
|
+
const status = await isDashboardRunning(port);
|
|
162
|
+
if (!status.running) return null;
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
host: "localhost",
|
|
166
|
+
port,
|
|
167
|
+
piPort: 9999, // Default — we can't know the actual piPort from health check alone
|
|
168
|
+
version: "unknown",
|
|
169
|
+
pid: status.pid ?? 0,
|
|
170
|
+
isLocal: true,
|
|
171
|
+
source: "fallback",
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Full discovery: mDNS first, fallback to health check.
|
|
177
|
+
*/
|
|
178
|
+
export async function discoverDashboardWithFallback(
|
|
179
|
+
configPort: number,
|
|
180
|
+
mdnsTimeout = 2000,
|
|
181
|
+
): Promise<{ servers: DiscoveredServer[]; portConflict: boolean }> {
|
|
182
|
+
const servers = await discoverDashboard(mdnsTimeout);
|
|
183
|
+
if (servers.length > 0) {
|
|
184
|
+
return { servers, portConflict: false };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// mDNS found nothing — try health check fallback
|
|
188
|
+
const status = await isDashboardRunning(configPort);
|
|
189
|
+
if (status.running) {
|
|
190
|
+
return {
|
|
191
|
+
servers: [{
|
|
192
|
+
host: "localhost",
|
|
193
|
+
port: configPort,
|
|
194
|
+
piPort: 9999,
|
|
195
|
+
version: "unknown",
|
|
196
|
+
pid: status.pid ?? 0,
|
|
197
|
+
isLocal: true,
|
|
198
|
+
source: "fallback",
|
|
199
|
+
}],
|
|
200
|
+
portConflict: false,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return { servers: [], portConflict: status.portConflict ?? false };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Continuous browser that emits events when servers appear/disappear.
|
|
209
|
+
*/
|
|
210
|
+
export interface DashboardBrowser extends EventEmitter {
|
|
211
|
+
/** Currently known servers */
|
|
212
|
+
servers: Map<string, DiscoveredServer>;
|
|
213
|
+
/** Stop browsing */
|
|
214
|
+
stop(): void;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function createBrowser(): DashboardBrowser {
|
|
218
|
+
const emitter = new EventEmitter() as DashboardBrowser;
|
|
219
|
+
emitter.servers = new Map();
|
|
220
|
+
|
|
221
|
+
const bonjour = new Bonjour();
|
|
222
|
+
const browser: Browser = bonjour.find({ type: SERVICE_TYPE });
|
|
223
|
+
|
|
224
|
+
browser.on("up", (service: Service) => {
|
|
225
|
+
const isLocal = isLocalService(service);
|
|
226
|
+
const server = serviceToServer(service, isLocal);
|
|
227
|
+
const key = `${server.host}:${server.port}`;
|
|
228
|
+
emitter.servers.set(key, server);
|
|
229
|
+
emitter.emit("server-up", server);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
browser.on("down", (service: Service) => {
|
|
233
|
+
const host = service.host ?? "unknown";
|
|
234
|
+
const key = `${host}:${service.port}`;
|
|
235
|
+
const server = emitter.servers.get(key);
|
|
236
|
+
if (server) {
|
|
237
|
+
emitter.servers.delete(key);
|
|
238
|
+
emitter.emit("server-down", server);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
emitter.stop = () => {
|
|
243
|
+
browser.stop();
|
|
244
|
+
bonjour.destroy();
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
return emitter;
|
|
248
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects OpenSpec activity from tool execution events.
|
|
3
|
+
* Returns partial activity info (phase and/or changeName) or null if not openspec-related.
|
|
4
|
+
*/
|
|
5
|
+
import type { OpenSpecPhase } from "./types.js";
|
|
6
|
+
|
|
7
|
+
export interface DetectedActivity {
|
|
8
|
+
phase?: OpenSpecPhase;
|
|
9
|
+
changeName?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Map from skill directory name suffix to phase */
|
|
13
|
+
const SKILL_PHASE_MAP: Record<string, OpenSpecPhase> = {
|
|
14
|
+
"apply-change": "apply",
|
|
15
|
+
"archive-change": "archive",
|
|
16
|
+
"bulk-archive-change": "archive",
|
|
17
|
+
"continue-change": "continue",
|
|
18
|
+
"explore": "explore",
|
|
19
|
+
"ff-change": "ff",
|
|
20
|
+
"new-change": "new",
|
|
21
|
+
"onboard": "onboard",
|
|
22
|
+
"sync-specs": "sync-specs",
|
|
23
|
+
"verify-change": "verify",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Regex to match openspec skill SKILL.md reads */
|
|
27
|
+
const SKILL_PATH_RE = /\.pi\/skills\/openspec-([^/]+)\/SKILL\.md$/;
|
|
28
|
+
|
|
29
|
+
/** Regex to match openspec change file reads */
|
|
30
|
+
const CHANGE_PATH_RE = /openspec\/changes\/([^/]+)\//;
|
|
31
|
+
|
|
32
|
+
/** Regex to match --change "name" or --change name in CLI commands */
|
|
33
|
+
const CLI_CHANGE_FLAG_RE = /openspec\s+\S+.*--change\s+["']?([^\s"']+)["']?/;
|
|
34
|
+
|
|
35
|
+
/** Regex to match openspec archive <name> */
|
|
36
|
+
const CLI_ARCHIVE_RE = /openspec\s+archive\s+["']?([^\s"']+)["']?/;
|
|
37
|
+
|
|
38
|
+
/** Regex to match openspec new change "name" (positional arg) */
|
|
39
|
+
const CLI_NEW_CHANGE_RE = /openspec\s+new\s+change\s+["']?([^\s"']+)["']?/;
|
|
40
|
+
|
|
41
|
+
export function detectOpenSpecActivity(
|
|
42
|
+
toolName: string,
|
|
43
|
+
args: Record<string, unknown> | undefined,
|
|
44
|
+
): DetectedActivity | null {
|
|
45
|
+
if (!args) return null;
|
|
46
|
+
|
|
47
|
+
const tool = toolName.toLowerCase();
|
|
48
|
+
|
|
49
|
+
if (tool === "read") {
|
|
50
|
+
const path = args.path as string | undefined;
|
|
51
|
+
if (!path) return null;
|
|
52
|
+
|
|
53
|
+
// Check for skill file read → phase detection
|
|
54
|
+
const skillMatch = path.match(SKILL_PATH_RE);
|
|
55
|
+
if (skillMatch) {
|
|
56
|
+
const suffix = skillMatch[1];
|
|
57
|
+
const phase = SKILL_PHASE_MAP[suffix];
|
|
58
|
+
if (phase) return { phase };
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Check for openspec change file read → change name detection
|
|
63
|
+
const changeMatch = path.match(CHANGE_PATH_RE);
|
|
64
|
+
if (changeMatch) {
|
|
65
|
+
return { changeName: changeMatch[1] };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (tool === "write") {
|
|
72
|
+
const path = args.path as string | undefined;
|
|
73
|
+
if (!path) return null;
|
|
74
|
+
|
|
75
|
+
const changeMatch = path.match(CHANGE_PATH_RE);
|
|
76
|
+
if (changeMatch) {
|
|
77
|
+
return { changeName: changeMatch[1] };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (tool === "bash") {
|
|
84
|
+
const command = args.command as string | undefined;
|
|
85
|
+
if (!command || !command.includes("openspec")) return null;
|
|
86
|
+
|
|
87
|
+
// Check for --change flag
|
|
88
|
+
const flagMatch = command.match(CLI_CHANGE_FLAG_RE);
|
|
89
|
+
if (flagMatch) {
|
|
90
|
+
return { changeName: flagMatch[1] };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check for openspec archive <name>
|
|
94
|
+
const archiveMatch = command.match(CLI_ARCHIVE_RE);
|
|
95
|
+
if (archiveMatch) {
|
|
96
|
+
return { changeName: archiveMatch[1] };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check for openspec new change "name"
|
|
100
|
+
const newChangeMatch = command.match(CLI_NEW_CHANGE_RE);
|
|
101
|
+
if (newChangeMatch) {
|
|
102
|
+
return { changeName: newChangeMatch[1] };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return null;
|
|
109
|
+
}
|