@blackbelt-technology/pi-agent-dashboard 0.2.8 → 0.3.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 +114 -9
- package/README.md +218 -97
- package/docs/architecture.md +107 -7
- package/package.json +9 -4
- package/packages/extension/package.json +1 -1
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +300 -3
- package/packages/extension/src/__tests__/fork-entryid-timing.test.ts +100 -0
- package/packages/extension/src/ask-user-tool.ts +289 -20
- package/packages/extension/src/bridge.ts +38 -4
- package/packages/extension/src/command-handler.ts +34 -39
- package/packages/extension/src/prompt-expander.ts +25 -4
- package/packages/server/package.json +2 -1
- package/packages/server/src/__tests__/auto-attach.test.ts +10 -1
- package/packages/server/src/__tests__/auto-shutdown.test.ts +8 -2
- package/packages/server/src/__tests__/browse-endpoint.test.ts +229 -10
- package/packages/server/src/__tests__/browser-gateway-handler-errors.test.ts +129 -0
- package/packages/server/src/__tests__/cors.test.ts +34 -2
- package/packages/server/src/__tests__/editor-manager-pid-registry.test.ts +168 -0
- package/packages/server/src/__tests__/editor-manager.test.ts +33 -0
- package/packages/server/src/__tests__/editor-pid-registry.test.ts +191 -0
- package/packages/server/src/__tests__/editor-registry.test.ts +3 -2
- package/packages/server/src/__tests__/fix-pty-permissions.test.ts +59 -0
- package/packages/server/src/__tests__/git-operations.test.ts +9 -7
- package/packages/server/src/__tests__/health-endpoint.test.ts +11 -13
- package/packages/server/src/__tests__/openspec-tasks-parser.test.ts +178 -0
- package/packages/server/src/__tests__/openspec-tasks-routes.test.ts +180 -0
- package/packages/server/src/__tests__/package-manager-wrapper-resolve.test.ts +122 -0
- package/packages/server/src/__tests__/pi-core-checker.test.ts +195 -0
- package/packages/server/src/__tests__/pi-core-routes.test.ts +184 -0
- package/packages/server/src/__tests__/pi-core-updater.test.ts +214 -0
- package/packages/server/src/__tests__/provider-auth-routes.test.ts +13 -3
- package/packages/server/src/__tests__/recommended-routes.test.ts +389 -0
- package/packages/server/src/__tests__/session-file-dedup.test.ts +10 -10
- package/packages/server/src/__tests__/session-lifecycle-logging.test.ts +8 -2
- package/packages/server/src/__tests__/sleep-aware-heartbeat.test.ts +3 -1
- package/packages/server/src/__tests__/smoke-integration.test.ts +10 -10
- package/packages/server/src/__tests__/test-server-canary.test.ts +31 -0
- package/packages/server/src/__tests__/tunnel.test.ts +91 -0
- package/packages/server/src/__tests__/ws-ping-pong.test.ts +10 -2
- package/packages/server/src/browse.ts +100 -6
- package/packages/server/src/browser-gateway.ts +16 -3
- package/packages/server/src/editor-manager.ts +20 -1
- package/packages/server/src/editor-pid-registry.ts +198 -0
- package/packages/server/src/fix-pty-permissions.ts +44 -0
- package/packages/server/src/headless-pid-registry.ts +9 -0
- package/packages/server/src/npm-search-proxy.ts +71 -0
- package/packages/server/src/openspec-tasks.ts +158 -0
- package/packages/server/src/package-manager-wrapper.ts +31 -0
- package/packages/server/src/pi-core-checker.ts +290 -0
- package/packages/server/src/pi-core-updater.ts +166 -0
- package/packages/server/src/pi-gateway.ts +7 -0
- package/packages/server/src/process-manager.ts +1 -1
- package/packages/server/src/routes/file-routes.ts +30 -3
- package/packages/server/src/routes/openspec-routes.ts +83 -1
- package/packages/server/src/routes/pi-core-routes.ts +117 -0
- package/packages/server/src/routes/provider-auth-routes.ts +4 -2
- package/packages/server/src/routes/provider-routes.ts +12 -2
- package/packages/server/src/routes/recommended-routes.ts +227 -0
- package/packages/server/src/routes/system-routes.ts +10 -1
- package/packages/server/src/server.ts +151 -15
- package/packages/server/src/terminal-manager.ts +4 -0
- package/packages/server/src/test-env-guard.ts +26 -0
- package/packages/server/src/test-support/test-server.ts +63 -0
- package/packages/server/src/tunnel.ts +132 -8
- package/packages/shared/package.json +1 -1
- package/packages/shared/src/__tests__/config.test.ts +3 -3
- package/packages/shared/src/__tests__/openspec-poller.test.ts +44 -0
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +123 -0
- package/packages/shared/src/__tests__/source-matching.test.ts +143 -0
- package/packages/shared/src/browser-protocol.ts +23 -1
- package/packages/shared/src/openspec-poller.ts +8 -3
- package/packages/shared/src/recommended-extensions.ts +180 -0
- package/packages/shared/src/rest-api.ts +71 -0
- package/packages/shared/src/source-matching.ts +126 -0
- package/packages/shared/src/test-support/setup-home.ts +74 -0
- package/packages/shared/src/types.ts +7 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recommended pi extensions for pi-agent-dashboard.
|
|
3
|
+
*
|
|
4
|
+
* The dashboard has custom UI and wiring for a small set of pi extensions
|
|
5
|
+
* it was built to work with. This manifest enumerates them so the dashboard
|
|
6
|
+
* can surface installation status, offer one-click installs in the Packages
|
|
7
|
+
* tab, walk users through setup in the first-launch wizard, and warn when
|
|
8
|
+
* a `required` entry is missing.
|
|
9
|
+
*
|
|
10
|
+
* This list is intentionally curated (not auto-discovered from npm). Each
|
|
11
|
+
* entry lives and dies by explicit PR review — the dashboard team owns the
|
|
12
|
+
* decision of which extensions are promoted.
|
|
13
|
+
*
|
|
14
|
+
* Descriptions in `fallbackDescription` are shipped inline. At runtime the
|
|
15
|
+
* dashboard server optionally enriches them with live descriptions fetched
|
|
16
|
+
* from the npm registry or GitHub (see `/api/packages/recommended`).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Relative importance of a recommended extension. */
|
|
20
|
+
export type RecommendedExtensionStatus =
|
|
21
|
+
| "required" // dashboard features or provider paths break without it
|
|
22
|
+
| "strongly-suggested" // dashboard has UI that depends on this
|
|
23
|
+
| "optional"; // nice-to-have
|
|
24
|
+
|
|
25
|
+
/** Static manifest entry. Enriched at runtime via the recommended route. */
|
|
26
|
+
export interface RecommendedExtension {
|
|
27
|
+
/** Stable kebab-case identifier. Used for skip/persist state and IPC. */
|
|
28
|
+
id: string;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* pi install source. Any form parseable by pi's DefaultPackageManager:
|
|
32
|
+
* - `npm:<name>`
|
|
33
|
+
* - `git:<host>/<path>`
|
|
34
|
+
* - `git@<host>:<path>.git`
|
|
35
|
+
* - `https://<host>/<path>.git`
|
|
36
|
+
* - local path
|
|
37
|
+
*/
|
|
38
|
+
source: string;
|
|
39
|
+
|
|
40
|
+
/** Human-readable package name for the UI. */
|
|
41
|
+
displayName: string;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Fallback description. Used when npm/GitHub is unreachable. Kept
|
|
45
|
+
* short (one or two sentences).
|
|
46
|
+
*/
|
|
47
|
+
fallbackDescription: string;
|
|
48
|
+
|
|
49
|
+
/** Relative importance. */
|
|
50
|
+
status: RecommendedExtensionStatus;
|
|
51
|
+
|
|
52
|
+
/** Which dashboard features light up when this is installed. */
|
|
53
|
+
unlocks: string[];
|
|
54
|
+
|
|
55
|
+
/** Tool names this extension registers (for diagnostics / UI hinting). */
|
|
56
|
+
toolsRegistered?: string[];
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* True when the extension self-wires into pi / dashboard without
|
|
60
|
+
* additional configuration — installing it is sufficient for it to
|
|
61
|
+
* start working.
|
|
62
|
+
*/
|
|
63
|
+
autowired?: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Enriched manifest entry returned by GET /api/packages/recommended. */
|
|
67
|
+
export interface EnrichedRecommendedExtension extends RecommendedExtension {
|
|
68
|
+
/** Live description (falls back to `fallbackDescription` on fetch failure). */
|
|
69
|
+
description: string;
|
|
70
|
+
/** Current upstream version, if available. */
|
|
71
|
+
version?: string;
|
|
72
|
+
/**
|
|
73
|
+
* Install status by scope. `null` means not present on disk in any scope.
|
|
74
|
+
*/
|
|
75
|
+
installed: { scope: "global" | "local" | null };
|
|
76
|
+
/** True iff the source is currently listed in `~/.pi/agent/settings.json` `packages[]`. */
|
|
77
|
+
activeInPi: boolean;
|
|
78
|
+
/** True iff a newer version is available upstream. */
|
|
79
|
+
updateAvailable: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const RECOMMENDED_EXTENSIONS: readonly RecommendedExtension[] = [
|
|
83
|
+
{
|
|
84
|
+
id: "pi-anthropic-messages",
|
|
85
|
+
source: "git@github.com:BlackBeltTechnology/pi-anthropic-messages.git",
|
|
86
|
+
displayName: "pi-anthropic-messages",
|
|
87
|
+
fallbackDescription:
|
|
88
|
+
"Protocol bridge that makes pi's custom tools work with any " +
|
|
89
|
+
"anthropic-messages endpoint for Claude models (direct Anthropic " +
|
|
90
|
+
"OAuth/API key, 9Router cc/claude-*, pi-model-proxy, any Claude " +
|
|
91
|
+
"Code-flavored proxy). Required whenever a provider has " +
|
|
92
|
+
'api: "anthropic-messages" with a Claude model — without it, ' +
|
|
93
|
+
"tool calls fall back to Claude Code's built-in bash_ide sandbox.",
|
|
94
|
+
status: "required",
|
|
95
|
+
unlocks: ["Tool calls on Anthropic OAuth / 9Router cc/* / proxy providers"],
|
|
96
|
+
autowired: true,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: "tintinweb-pi-subagents",
|
|
100
|
+
source: "npm:@tintinweb/pi-subagents",
|
|
101
|
+
displayName: "@tintinweb/pi-subagents",
|
|
102
|
+
fallbackDescription:
|
|
103
|
+
"Claude Code-style autonomous sub-agents for pi. Registers " +
|
|
104
|
+
"the Agent tool and its companions. The dashboard has custom " +
|
|
105
|
+
"card UI for it.",
|
|
106
|
+
status: "strongly-suggested",
|
|
107
|
+
unlocks: [
|
|
108
|
+
"Agent tool card UI",
|
|
109
|
+
"Subagent activity badge",
|
|
110
|
+
"get_subagent_result / steer_subagent renderers",
|
|
111
|
+
],
|
|
112
|
+
toolsRegistered: ["Agent", "get_subagent_result", "steer_subagent"],
|
|
113
|
+
autowired: true,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: "pi-flows",
|
|
117
|
+
source: "git@github.com:BlackBeltTechnology/pi-flows.git",
|
|
118
|
+
displayName: "pi-flows",
|
|
119
|
+
fallbackDescription:
|
|
120
|
+
"Flow engine, dashboard, and orchestration extensions for pi. " +
|
|
121
|
+
"Powers the dashboard's Flow view, role aliases, and multi-agent " +
|
|
122
|
+
"orchestration tools.",
|
|
123
|
+
status: "strongly-suggested",
|
|
124
|
+
unlocks: [
|
|
125
|
+
"Flow dashboard",
|
|
126
|
+
"Role aliases (@planning, @coding, …)",
|
|
127
|
+
"subagent / flow_write / flow_results / agent_write / ask_user / skill_read / finish tools",
|
|
128
|
+
],
|
|
129
|
+
toolsRegistered: [
|
|
130
|
+
"subagent",
|
|
131
|
+
"agent_catalog",
|
|
132
|
+
"agent_write",
|
|
133
|
+
"flow_write",
|
|
134
|
+
"flow_results",
|
|
135
|
+
"skill_read",
|
|
136
|
+
"ask_user",
|
|
137
|
+
"finish",
|
|
138
|
+
],
|
|
139
|
+
autowired: true,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: "pi-web-access",
|
|
143
|
+
source: "npm:pi-web-access",
|
|
144
|
+
displayName: "pi-web-access",
|
|
145
|
+
fallbackDescription:
|
|
146
|
+
"Web search, URL fetching, GitHub repo cloning, PDF extraction, " +
|
|
147
|
+
"and YouTube / local video analysis for pi.",
|
|
148
|
+
status: "strongly-suggested",
|
|
149
|
+
unlocks: ["web_search", "code_search", "fetch_content", "get_search_content"],
|
|
150
|
+
toolsRegistered: [
|
|
151
|
+
"web_search",
|
|
152
|
+
"code_search",
|
|
153
|
+
"fetch_content",
|
|
154
|
+
"get_search_content",
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: "pi-agent-browser",
|
|
159
|
+
source: "npm:pi-agent-browser",
|
|
160
|
+
displayName: "pi-agent-browser",
|
|
161
|
+
fallbackDescription:
|
|
162
|
+
"Browser automation (open, snapshot, click, fill, screenshot) " +
|
|
163
|
+
"via the agent-browser CLI.",
|
|
164
|
+
status: "optional",
|
|
165
|
+
unlocks: ["browser tool (open, snapshot, click, screenshot)"],
|
|
166
|
+
toolsRegistered: ["browser"],
|
|
167
|
+
},
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
/** Retrieve a recommended entry by id, or `undefined`. */
|
|
171
|
+
export function getRecommendedExtension(id: string): RecommendedExtension | undefined {
|
|
172
|
+
return RECOMMENDED_EXTENSIONS.find((e) => e.id === id);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Retrieve all entries with the given status. */
|
|
176
|
+
export function getRecommendedByStatus(
|
|
177
|
+
status: RecommendedExtensionStatus,
|
|
178
|
+
): readonly RecommendedExtension[] {
|
|
179
|
+
return RECOMMENDED_EXTENSIONS.filter((e) => e.status === status);
|
|
180
|
+
}
|
|
@@ -7,6 +7,11 @@ import type {
|
|
|
7
7
|
ApiResponse,
|
|
8
8
|
} from "./types.js";
|
|
9
9
|
|
|
10
|
+
export type { ApiResponse } from "./types.js";
|
|
11
|
+
import type { EnrichedRecommendedExtension } from "./recommended-extensions.js";
|
|
12
|
+
|
|
13
|
+
export type { EnrichedRecommendedExtension } from "./recommended-extensions.js";
|
|
14
|
+
|
|
10
15
|
// ── Sessions ────────────────────────────────────────────────────────
|
|
11
16
|
|
|
12
17
|
export interface ListSessionsQuery {
|
|
@@ -63,6 +68,14 @@ export interface BrowseEntry {
|
|
|
63
68
|
isPi: boolean;
|
|
64
69
|
}
|
|
65
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Response shape for `GET /api/browse?path=<dir>&q=<query>`.
|
|
73
|
+
*
|
|
74
|
+
* The optional `q` query parameter, when present and non-empty, causes the
|
|
75
|
+
* server to filter entries by case-insensitive substring on `name` and rank
|
|
76
|
+
* them (exact → prefix → word-boundary → substring) before the 200-entry cap.
|
|
77
|
+
* When omitted or whitespace-only, entries are sorted alphabetically.
|
|
78
|
+
*/
|
|
66
79
|
export interface BrowseResult {
|
|
67
80
|
entries: BrowseEntry[];
|
|
68
81
|
parent: string | null;
|
|
@@ -71,6 +84,18 @@ export interface BrowseResult {
|
|
|
71
84
|
|
|
72
85
|
export type BrowseResponse = ApiResponse<BrowseResult>;
|
|
73
86
|
|
|
87
|
+
/** Request body for `POST /api/browse/mkdir`. */
|
|
88
|
+
export interface MkdirRequest {
|
|
89
|
+
parent: string;
|
|
90
|
+
name: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface MkdirResult {
|
|
94
|
+
path: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type MkdirResponse = ApiResponse<MkdirResult>;
|
|
98
|
+
|
|
74
99
|
// ── Tunnel Status ───────────────────────────────────────────────────
|
|
75
100
|
|
|
76
101
|
export type TunnelStatus =
|
|
@@ -246,6 +271,46 @@ export interface PackageUpdateInfo {
|
|
|
246
271
|
|
|
247
272
|
export type CheckUpdatesResponse = ApiResponse<PackageUpdateInfo[]>;
|
|
248
273
|
|
|
274
|
+
// ── Pi core version check ────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
/** A core pi ecosystem CLI package (not managed by pi's PackageManager). */
|
|
277
|
+
export interface PiCorePackage {
|
|
278
|
+
name: string;
|
|
279
|
+
displayName: string;
|
|
280
|
+
currentVersion: string;
|
|
281
|
+
latestVersion: string | null;
|
|
282
|
+
updateAvailable: boolean;
|
|
283
|
+
installSource: "global" | "managed";
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export interface PiCoreStatus {
|
|
287
|
+
packages: PiCorePackage[];
|
|
288
|
+
updatesAvailable: number;
|
|
289
|
+
lastChecked: string;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export type PiCoreVersionsResponse = ApiResponse<PiCoreStatus>;
|
|
293
|
+
|
|
294
|
+
/** Request body for POST /api/pi-core/update. Empty packages = update all. */
|
|
295
|
+
export interface PiCoreUpdateRequest {
|
|
296
|
+
packages?: string[];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Result of a single package update. */
|
|
300
|
+
export interface PiCoreUpdateResult {
|
|
301
|
+
name: string;
|
|
302
|
+
success: boolean;
|
|
303
|
+
error?: string;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Response from POST /api/pi-core/update (completes synchronously). */
|
|
307
|
+
export interface PiCoreUpdateResponse {
|
|
308
|
+
results: PiCoreUpdateResult[];
|
|
309
|
+
sessionsReloaded: number;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export type PiCoreUpdateApiResponse = ApiResponse<PiCoreUpdateResponse>;
|
|
313
|
+
|
|
249
314
|
// ── Known Servers ─────────────────────────────────────────────
|
|
250
315
|
|
|
251
316
|
import type { KnownServer } from "./config.js";
|
|
@@ -281,3 +346,9 @@ export interface NetworkInterface {
|
|
|
281
346
|
netmask: string;
|
|
282
347
|
cidr: string;
|
|
283
348
|
}
|
|
349
|
+
|
|
350
|
+
// ── Recommended extensions ───────────────────────────
|
|
351
|
+
|
|
352
|
+
export type ListRecommendedExtensionsResponse = ApiResponse<{
|
|
353
|
+
recommended: EnrichedRecommendedExtension[];
|
|
354
|
+
}>;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// source-matching — canonical "two source strings refer to the same package"
|
|
3
|
+
// predicate, shared between the server's /api/packages/recommended route
|
|
4
|
+
// and the Electron wizard's bootstrap enricher.
|
|
5
|
+
//
|
|
6
|
+
// Pure string logic, no fs / no pi SDK dependency. Safe to import from any
|
|
7
|
+
// package (shared, server, client, electron).
|
|
8
|
+
//
|
|
9
|
+
// Input sources take one of these forms:
|
|
10
|
+
//
|
|
11
|
+
// npm:<name>[@<version>]
|
|
12
|
+
// e.g. "npm:pi-web-access", "npm:@tintinweb/pi-subagents@0.5.2"
|
|
13
|
+
//
|
|
14
|
+
// git@<host>:<owner>/<repo>[.git]
|
|
15
|
+
// e.g. "git@github.com:BlackBeltTechnology/pi-flows.git"
|
|
16
|
+
//
|
|
17
|
+
// https://<host>/<owner>/<repo>[.git][#ref]
|
|
18
|
+
// e.g. "https://github.com/BlackBeltTechnology/pi-flows.git"
|
|
19
|
+
//
|
|
20
|
+
// git:<host>/<owner>/<repo>[#ref]
|
|
21
|
+
// e.g. "git:github.com/BlackBeltTechnology/pi-flows#main"
|
|
22
|
+
//
|
|
23
|
+
// any other string (absolute path, relative path, unrecognized URL)
|
|
24
|
+
// → parsed as kind:"raw" with the literal preserved
|
|
25
|
+
//
|
|
26
|
+
// Matching rules:
|
|
27
|
+
// - Same kind: exact comparison of the semantically-meaningful parts.
|
|
28
|
+
// - Cross-kind (git ↔ raw): the raw source's basename (last path
|
|
29
|
+
// segment, stripped of trailing slash and trailing .git) must equal
|
|
30
|
+
// the git repo name, case-insensitive. This handles the common case
|
|
31
|
+
// where a user registered the package via `pi install -l <path>`
|
|
32
|
+
// instead of by URL and the basename is the repo name.
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
export type SourceKey =
|
|
36
|
+
| { kind: "npm"; name: string }
|
|
37
|
+
| { kind: "git"; host: string; owner: string; repo: string }
|
|
38
|
+
| { kind: "raw"; source: string };
|
|
39
|
+
|
|
40
|
+
export function parseSourceKey(source: string): SourceKey {
|
|
41
|
+
const trimmed = source.trim();
|
|
42
|
+
|
|
43
|
+
if (trimmed.startsWith("npm:")) {
|
|
44
|
+
const spec = trimmed.slice(4).trim();
|
|
45
|
+
// Strip a trailing @version but preserve the scope @ in @scope/name.
|
|
46
|
+
// If spec starts with @, the SECOND @ (if any) delimits version.
|
|
47
|
+
let name = spec;
|
|
48
|
+
if (spec.startsWith("@")) {
|
|
49
|
+
const idx = spec.indexOf("@", 1);
|
|
50
|
+
if (idx > 0) name = spec.slice(0, idx);
|
|
51
|
+
} else {
|
|
52
|
+
const idx = spec.indexOf("@");
|
|
53
|
+
if (idx > 0) name = spec.slice(0, idx);
|
|
54
|
+
}
|
|
55
|
+
return { kind: "npm", name };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const sshMatch = trimmed.match(/^git@([^:]+):([^/]+)\/([^/.]+)(?:\.git)?$/);
|
|
59
|
+
if (sshMatch) {
|
|
60
|
+
return { kind: "git", host: sshMatch[1], owner: sshMatch[2], repo: sshMatch[3] };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const httpsMatch = trimmed.match(/^https?:\/\/([^/]+)\/([^/]+)\/([^/#.]+)(?:\.git)?(?:#.+)?$/);
|
|
64
|
+
if (httpsMatch) {
|
|
65
|
+
return {
|
|
66
|
+
kind: "git",
|
|
67
|
+
host: httpsMatch[1],
|
|
68
|
+
owner: httpsMatch[2],
|
|
69
|
+
repo: httpsMatch[3],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const gitPrefixMatch = trimmed.match(/^git:([^/]+)\/([^/]+)\/([^/#]+?)(?:\.git)?(?:#.+)?$/);
|
|
74
|
+
if (gitPrefixMatch) {
|
|
75
|
+
return {
|
|
76
|
+
kind: "git",
|
|
77
|
+
host: gitPrefixMatch[1],
|
|
78
|
+
owner: gitPrefixMatch[2],
|
|
79
|
+
repo: gitPrefixMatch[3],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { kind: "raw", source: trimmed };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Extract the basename (last path segment, .git-stripped) from a raw
|
|
88
|
+
* source string. Returns lowercase or null.
|
|
89
|
+
*/
|
|
90
|
+
function localPathBasename(src: string): string | null {
|
|
91
|
+
const stripped = src.replace(/\/+$/, "").replace(/\.git$/, "");
|
|
92
|
+
const segments = stripped.split(/[\\/]/);
|
|
93
|
+
const tail = segments[segments.length - 1];
|
|
94
|
+
return tail ? tail.toLowerCase() : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* True iff two source strings refer to the same package. See module
|
|
99
|
+
* header for the full matching rules and rationale.
|
|
100
|
+
*/
|
|
101
|
+
export function sourcesMatch(a: string, b: string): boolean {
|
|
102
|
+
const ka = parseSourceKey(a);
|
|
103
|
+
const kb = parseSourceKey(b);
|
|
104
|
+
|
|
105
|
+
if (ka.kind === kb.kind) {
|
|
106
|
+
if (ka.kind === "npm" && kb.kind === "npm") return ka.name === kb.name;
|
|
107
|
+
if (ka.kind === "git" && kb.kind === "git") {
|
|
108
|
+
return (
|
|
109
|
+
ka.host.toLowerCase() === kb.host.toLowerCase() &&
|
|
110
|
+
ka.owner.toLowerCase() === kb.owner.toLowerCase() &&
|
|
111
|
+
ka.repo.toLowerCase() === kb.repo.toLowerCase()
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
if (ka.kind === "raw" && kb.kind === "raw") return ka.source === kb.source;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Cross-kind: git ↔ raw (local path). Match on repo basename.
|
|
118
|
+
const gitKey = ka.kind === "git" ? ka : kb.kind === "git" ? kb : null;
|
|
119
|
+
const rawKey = ka.kind === "raw" ? ka : kb.kind === "raw" ? kb : null;
|
|
120
|
+
if (gitKey && rawKey) {
|
|
121
|
+
const basename = localPathBasename(rawKey.source);
|
|
122
|
+
if (basename && basename === gitKey.repo.toLowerCase()) return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vitest globalSetup: tripwire + directory bootstrap.
|
|
3
|
+
*
|
|
4
|
+
* Wired via `test.globalSetup` in each package's `vitest.config.ts`. Runs ONCE
|
|
5
|
+
* at vitest boot, before any test file is loaded.
|
|
6
|
+
*
|
|
7
|
+
* Responsibilities:
|
|
8
|
+
* 1. Tripwire — throws if `process.env.HOME` still points at the developer's
|
|
9
|
+
* real user home (meaning the root `npm test` script wasn't used and HOME
|
|
10
|
+
* wasn't overridden). Aborts the entire run before any destructive code
|
|
11
|
+
* can touch real ~/.pi/.
|
|
12
|
+
* 2. Pre-create `<HOME>/.pi/agent/sessions/` and `<HOME>/.pi/dashboard/` so
|
|
13
|
+
* production code that reads those paths finds empty but well-formed
|
|
14
|
+
* directories.
|
|
15
|
+
*
|
|
16
|
+
* Why globalSetup (not setupFiles):
|
|
17
|
+
* setupFiles' `beforeAll` can run AFTER a test file's top-level imports
|
|
18
|
+
* execute destructive module-level code. globalSetup runs strictly before
|
|
19
|
+
* ANY test file is loaded. Combined with the `npm test` process-level HOME
|
|
20
|
+
* override, there is zero window in which code can see real HOME.
|
|
21
|
+
*
|
|
22
|
+
* The process-level HOME override in package.json is the primary isolation
|
|
23
|
+
* layer; this module is the second-line tripwire that catches regressions.
|
|
24
|
+
*/
|
|
25
|
+
import { mkdirSync } from "node:fs";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
import os from "node:os";
|
|
28
|
+
|
|
29
|
+
/** Vitest globalSetup default export. Returns a teardown function. */
|
|
30
|
+
export default function setup() {
|
|
31
|
+
const currentHome = process.env.HOME ?? "";
|
|
32
|
+
const realHome = os.userInfo().homedir;
|
|
33
|
+
|
|
34
|
+
if (!currentHome) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
"[test-isolation] process.env.HOME is empty. " +
|
|
37
|
+
"Run tests via `npm test` (which sets HOME to a tmp dir) " +
|
|
38
|
+
"or prefix manually: `HOME=$(mktemp -d) npx vitest run`.",
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (currentHome === realHome) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`[test-isolation] process.env.HOME (${currentHome}) equals the real user home ` +
|
|
45
|
+
`(${realHome}). This would let tests read and mutate ~/.pi/, potentially killing ` +
|
|
46
|
+
`live pi sessions. Run tests via \`npm test\` — it sets HOME to an ephemeral tmp dir. ` +
|
|
47
|
+
`If you invoked vitest directly, prefix with \`HOME=$(mktemp -d)\`.`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!currentHome.startsWith(os.tmpdir())) {
|
|
52
|
+
// Not strictly fatal — developer may have pointed HOME at a custom scratch dir —
|
|
53
|
+
// but warn loudly because it's unusual.
|
|
54
|
+
// eslint-disable-next-line no-console
|
|
55
|
+
console.warn(
|
|
56
|
+
`[test-isolation] HOME (${currentHome}) is not under os.tmpdir() (${os.tmpdir()}). ` +
|
|
57
|
+
`Tests will still run but this layout is unusual.`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Pre-create expected .pi subdirectories so code that reads them finds empty dirs.
|
|
62
|
+
mkdirSync(join(currentHome, ".pi", "agent", "sessions"), { recursive: true });
|
|
63
|
+
mkdirSync(join(currentHome, ".pi", "dashboard"), { recursive: true });
|
|
64
|
+
|
|
65
|
+
// eslint-disable-next-line no-console
|
|
66
|
+
console.log(`[test-isolation] HOME=${currentHome} (real=${realHome})`);
|
|
67
|
+
|
|
68
|
+
// Teardown: nothing to do. The tmp HOME was created by the npm script's
|
|
69
|
+
// `$(mktemp -d)`; leaving the dir on disk is fine (OS cleans tmpdir).
|
|
70
|
+
// Tests that need per-file isolation continue to use their own mkdtemp.
|
|
71
|
+
return () => {
|
|
72
|
+
/* no-op */
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -140,6 +140,13 @@ export interface OpenSpecChange {
|
|
|
140
140
|
completedTasks: number;
|
|
141
141
|
totalTasks: number;
|
|
142
142
|
artifacts: OpenSpecArtifact[];
|
|
143
|
+
/**
|
|
144
|
+
* Artifact-authoring completeness reported by `openspec status --change <name> --json`.
|
|
145
|
+
* `true` when all required artifacts for the change's workflow are present/done.
|
|
146
|
+
* Orthogonal to task-tally completeness; used by the dashboard to surface an
|
|
147
|
+
* "Archive anyway" escape hatch when artifacts are authored but tasks remain unchecked.
|
|
148
|
+
*/
|
|
149
|
+
isComplete?: boolean;
|
|
143
150
|
}
|
|
144
151
|
|
|
145
152
|
/** Lifecycle state of an OpenSpec change, derived from artifacts + task status */
|