@blackbelt-technology/pi-agent-dashboard 0.4.5 → 0.5.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 -267
- package/README.md +51 -2
- package/docs/architecture.md +266 -25
- package/package.json +14 -4
- package/packages/extension/package.json +2 -2
- package/packages/extension/src/__tests__/build-provider-catalogue.test.ts +176 -0
- package/packages/extension/src/__tests__/markdown-image-inliner.test.ts +355 -0
- package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +68 -0
- package/packages/extension/src/__tests__/prompt-bus.test.ts +44 -0
- package/packages/extension/src/__tests__/prompt-expander.test.ts +45 -0
- package/packages/extension/src/__tests__/server-launcher.test.ts +24 -1
- package/packages/extension/src/__tests__/vcs-info-jj.test.ts +145 -0
- package/packages/extension/src/__tests__/{git-info.test.ts → vcs-info.test.ts} +6 -6
- package/packages/extension/src/bridge-context.ts +7 -0
- package/packages/extension/src/bridge.ts +142 -4
- package/packages/extension/src/command-handler.ts +6 -0
- package/packages/extension/src/markdown-image-inliner.ts +268 -0
- package/packages/extension/src/model-tracker.ts +35 -1
- package/packages/extension/src/prompt-bus.ts +4 -3
- package/packages/extension/src/prompt-expander.ts +50 -2
- package/packages/extension/src/provider-register.ts +117 -0
- package/packages/extension/src/server-launcher.ts +18 -1
- package/packages/extension/src/session-sync.ts +6 -1
- package/packages/extension/src/vcs-info.ts +184 -0
- package/packages/server/package.json +4 -4
- package/packages/server/src/__tests__/auto-attach-slug-defense.test.ts +104 -0
- package/packages/server/src/__tests__/bootstrap-install-from-list.test.ts +263 -0
- package/packages/server/src/__tests__/browser-gateway-snapshot-on-connect.test.ts +143 -0
- package/packages/server/src/__tests__/build-auth-status.test.ts +190 -0
- package/packages/server/src/__tests__/cold-boot-openspec-broadcast.test.ts +161 -0
- package/packages/server/src/__tests__/doctor-route.test.ts +132 -0
- package/packages/server/src/__tests__/event-wiring-providers-list.test.ts +87 -0
- package/packages/server/src/__tests__/has-openspec-dir.test.ts +64 -0
- package/packages/server/src/__tests__/health-shape.test.ts +43 -0
- package/packages/server/src/__tests__/idle-timer-respects-terminals.test.ts +115 -0
- package/packages/server/src/__tests__/is-unread-trigger.test.ts +4 -2
- package/packages/server/src/__tests__/jj-routes.test.ts +93 -0
- package/packages/server/src/__tests__/openspec-connect-snapshot.test.ts +92 -0
- package/packages/server/src/__tests__/openspec-tasks-parser.test.ts +114 -0
- package/packages/server/src/__tests__/pi-core-updater-managed-path.test.ts +177 -0
- package/packages/server/src/__tests__/process-manager-codes.test.ts +80 -0
- package/packages/server/src/__tests__/process-manager-managed-path.test.ts +73 -0
- package/packages/server/src/__tests__/provider-auth-storage.test.ts +42 -11
- package/packages/server/src/__tests__/provider-catalogue-cache.test.ts +54 -0
- package/packages/server/src/__tests__/session-action-handler-spawn-error.test.ts +17 -2
- package/packages/server/src/__tests__/session-action-handler-spawn.test.ts +150 -0
- package/packages/server/src/__tests__/session-diff-vcs.test.ts +61 -0
- package/packages/server/src/__tests__/session-discovery-skill-firstmessage.test.ts +95 -0
- package/packages/server/src/__tests__/spawn-failure-log.test.ts +118 -0
- package/packages/server/src/__tests__/spawn-preflight.test.ts +91 -0
- package/packages/server/src/__tests__/spawn-register-watchdog.test.ts +166 -0
- package/packages/server/src/__tests__/subscription-handler.test.ts +98 -6
- package/packages/server/src/__tests__/system-routes-reextract.test.ts +91 -0
- package/packages/server/src/__tests__/system-routes-restart.test.ts +4 -4
- package/packages/server/src/__tests__/system-routes-spawn-failures.test.ts +84 -0
- package/packages/server/src/__tests__/terminal-manager.test.ts +45 -0
- package/packages/server/src/bootstrap-install-from-list.ts +232 -0
- package/packages/server/src/bootstrap-state.ts +18 -0
- package/packages/server/src/browser-gateway.ts +58 -21
- package/packages/server/src/browser-handlers/directory-handler.ts +4 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +60 -2
- package/packages/server/src/browser-handlers/subscription-handler.ts +50 -3
- package/packages/server/src/cli.ts +22 -0
- package/packages/server/src/directory-service.ts +31 -0
- package/packages/server/src/event-wiring.ts +57 -2
- package/packages/server/src/home-lock.d.ts +124 -0
- package/packages/server/src/home-lock.js +330 -0
- package/packages/server/src/home-lock.js.map +1 -0
- package/packages/server/src/idle-timer.ts +15 -1
- package/packages/server/src/openspec-tasks.ts +50 -19
- package/packages/server/src/pi-core-updater.ts +65 -9
- package/packages/server/src/pi-gateway.ts +6 -0
- package/packages/server/src/process-manager.ts +62 -11
- package/packages/server/src/provider-auth-handlers.ts +9 -0
- package/packages/server/src/provider-auth-storage.ts +83 -51
- package/packages/server/src/provider-catalogue-cache.ts +41 -0
- package/packages/server/src/routes/doctor-routes.ts +140 -0
- package/packages/server/src/routes/jj-routes.ts +386 -0
- package/packages/server/src/routes/provider-auth-routes.ts +9 -0
- package/packages/server/src/routes/session-routes.ts +12 -3
- package/packages/server/src/routes/system-routes.ts +38 -1
- package/packages/server/src/server.ts +16 -9
- package/packages/server/src/session-bootstrap.ts +27 -12
- package/packages/server/src/session-diff.ts +118 -1
- package/packages/server/src/session-discovery.ts +10 -3
- package/packages/server/src/session-scanner.ts +4 -2
- package/packages/server/src/spawn-failure-log.ts +130 -0
- package/packages/server/src/spawn-preflight.ts +82 -0
- package/packages/server/src/spawn-register-watchdog.ts +236 -0
- package/packages/server/src/terminal-manager.ts +12 -1
- package/packages/shared/package.json +1 -1
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +1 -0
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +1 -0
- package/packages/shared/src/__tests__/bootstrap-install-resolve-npm.test.ts +72 -0
- package/packages/shared/src/__tests__/browser-protocol-types.test.ts +47 -1
- package/packages/shared/src/__tests__/config.test.ts +48 -0
- package/packages/shared/src/__tests__/dashboard-starter.test.ts +40 -0
- package/packages/shared/src/__tests__/detached-spawn.test.ts +24 -0
- package/packages/shared/src/__tests__/doctor-core.test.ts +134 -0
- package/packages/shared/src/__tests__/doctor-fault-tolerance.test.ts +218 -0
- package/packages/shared/src/__tests__/doctor-format.test.ts +121 -0
- package/packages/shared/src/__tests__/install-managed-node-bootstrap-order.test.ts +68 -0
- package/packages/shared/src/__tests__/install-managed-node.test.ts +192 -0
- package/packages/shared/src/__tests__/installable-list.test.ts +130 -0
- package/packages/shared/src/__tests__/managed-node-path.test.ts +122 -0
- package/packages/shared/src/__tests__/managed-runtime-strategy.test.ts +74 -0
- package/packages/shared/src/__tests__/no-installable-list-in-bridge.test.ts +52 -0
- package/packages/shared/src/__tests__/no-raw-openspec-status-in-skills.test.ts +6 -1
- package/packages/shared/src/__tests__/platform-jj.test.ts +339 -0
- package/packages/shared/src/__tests__/skill-block-parser.test.ts +153 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +18 -2
- package/packages/shared/src/bootstrap-install.ts +196 -2
- package/packages/shared/src/browser-protocol.ts +112 -1
- package/packages/shared/src/config.ts +29 -0
- package/packages/shared/src/dashboard-starter.ts +33 -0
- package/packages/shared/src/diff-types.ts +17 -0
- package/packages/shared/src/doctor-core.ts +821 -0
- package/packages/shared/src/index.ts +9 -0
- package/packages/shared/src/installable-list.ts +152 -0
- package/packages/shared/src/launch-source-flag.ts +14 -0
- package/packages/shared/src/launch-source-types.ts +18 -0
- package/packages/shared/src/openspec-activity-detector.ts +25 -7
- package/packages/shared/src/platform/detached-spawn.ts +13 -2
- package/packages/shared/src/platform/jj.ts +405 -0
- package/packages/shared/src/platform/managed-node-path.ts +77 -0
- package/packages/shared/src/protocol.ts +60 -2
- package/packages/shared/src/rest-api.ts +4 -0
- package/packages/shared/src/skill-block-parser.ts +115 -0
- package/packages/shared/src/tool-registry/__tests__/managed-runtime-strategy.test.ts +166 -0
- package/packages/shared/src/tool-registry/definitions.ts +19 -5
- package/packages/shared/src/tool-registry/strategies.ts +42 -0
- package/packages/shared/src/types.ts +91 -0
- package/packages/extension/src/git-info.ts +0 -55
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Barrel re-exports for selected shared symbols. Most consumers import
|
|
3
|
+
* directly from per-file paths (`@blackbelt-technology/pi-dashboard-shared/<file>.js`)
|
|
4
|
+
* via the package's `exports` map. This barrel exists for symbols that
|
|
5
|
+
* would otherwise be cumbersome to wire — currently the doctor core.
|
|
6
|
+
*
|
|
7
|
+
* Added by change: doctor-rich-output.
|
|
8
|
+
*/
|
|
9
|
+
export * from "./doctor-core.js";
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* installable-list.ts — types and helpers for ~/.pi/dashboard/installable.json.
|
|
3
|
+
*
|
|
4
|
+
* This file describes the set of packages the dashboard needs installed.
|
|
5
|
+
* Electron seeds the file on first run; Bridge / Standalone ignore it (file-absent
|
|
6
|
+
* path is a no-op in the server bootstrap). The server reads it during bootstrap.
|
|
7
|
+
*/
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
|
|
12
|
+
export type InstallableKind = "npm" | "pi-extension";
|
|
13
|
+
|
|
14
|
+
export interface InstallablePackage {
|
|
15
|
+
name: string;
|
|
16
|
+
/** Semver range or "*". */
|
|
17
|
+
version: string;
|
|
18
|
+
required: boolean;
|
|
19
|
+
kind: InstallableKind;
|
|
20
|
+
deprecated?: boolean;
|
|
21
|
+
defaultOff?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface InstallableList {
|
|
25
|
+
version: string;
|
|
26
|
+
packages: InstallablePackage[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface MergeResult {
|
|
30
|
+
list: InstallableList;
|
|
31
|
+
warnings: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const VALID_KINDS: ReadonlySet<string> = new Set<InstallableKind>(["npm", "pi-extension"]);
|
|
35
|
+
|
|
36
|
+
function defaultConfigDir(): string {
|
|
37
|
+
return path.join(os.homedir(), ".pi", "dashboard");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function installablePath(configDir: string): string {
|
|
41
|
+
return path.join(configDir, "installable.json");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Read `~/.pi/dashboard/installable.json` (or `configDir/installable.json`).
|
|
46
|
+
*
|
|
47
|
+
* Returns `null` when the file is absent. Logs a warning and drops entries
|
|
48
|
+
* with an invalid `kind` field. Does NOT create the file.
|
|
49
|
+
*/
|
|
50
|
+
export async function readInstallableList(
|
|
51
|
+
configDir?: string,
|
|
52
|
+
): Promise<InstallableList | null> {
|
|
53
|
+
const dir = configDir ?? defaultConfigDir();
|
|
54
|
+
const filePath = installablePath(dir);
|
|
55
|
+
|
|
56
|
+
let raw: string;
|
|
57
|
+
try {
|
|
58
|
+
raw = await fs.promises.readFile(filePath, "utf-8");
|
|
59
|
+
} catch (err: any) {
|
|
60
|
+
if (err.code === "ENOENT") return null;
|
|
61
|
+
throw err;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const parsed = JSON.parse(raw) as InstallableList;
|
|
65
|
+
|
|
66
|
+
const validPackages: InstallablePackage[] = [];
|
|
67
|
+
for (const pkg of parsed.packages ?? []) {
|
|
68
|
+
if (!VALID_KINDS.has(pkg.kind)) {
|
|
69
|
+
console.warn(
|
|
70
|
+
`[installable-list] Dropping entry "${pkg.name}" with unknown kind "${pkg.kind}".`,
|
|
71
|
+
);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
validPackages.push(pkg);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { version: parsed.version, packages: validPackages };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Atomically write `list` to `configDir/installable.json`.
|
|
82
|
+
* Writes a temp file then renames so readers never see a partial write.
|
|
83
|
+
*/
|
|
84
|
+
export async function writeInstallableList(
|
|
85
|
+
list: InstallableList,
|
|
86
|
+
configDir?: string,
|
|
87
|
+
): Promise<void> {
|
|
88
|
+
const dir = configDir ?? defaultConfigDir();
|
|
89
|
+
const filePath = installablePath(dir);
|
|
90
|
+
const tmpPath = filePath + ".tmp." + process.pid;
|
|
91
|
+
|
|
92
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
93
|
+
await fs.promises.writeFile(tmpPath, JSON.stringify(list, null, 2), "utf-8");
|
|
94
|
+
await fs.promises.rename(tmpPath, filePath);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Pure merge: reconcile a user's `existing` list against the `bundled` defaults.
|
|
99
|
+
*
|
|
100
|
+
* Rules:
|
|
101
|
+
* - Package in both: keep user's pinned version (user wins); emit a warning when
|
|
102
|
+
* the bundled default differs.
|
|
103
|
+
* - Package in existing but NOT in bundled: keep it, set `deprecated: true`, emit warning.
|
|
104
|
+
* - New `required: true` package in bundled: add it.
|
|
105
|
+
* - New `required: false` package in bundled: add it with `defaultOff: true`.
|
|
106
|
+
* - Result version marker comes from `bundled.version`.
|
|
107
|
+
*/
|
|
108
|
+
export function mergeInstallableList(
|
|
109
|
+
existing: InstallableList,
|
|
110
|
+
bundled: InstallableList,
|
|
111
|
+
): MergeResult {
|
|
112
|
+
const warnings: string[] = [];
|
|
113
|
+
const bundledMap = new Map(bundled.packages.map((p) => [p.name, p]));
|
|
114
|
+
const existingMap = new Map(existing.packages.map((p) => [p.name, p]));
|
|
115
|
+
|
|
116
|
+
const merged: InstallablePackage[] = [];
|
|
117
|
+
|
|
118
|
+
// Walk existing packages.
|
|
119
|
+
for (const pkg of existing.packages) {
|
|
120
|
+
const bundledPkg = bundledMap.get(pkg.name);
|
|
121
|
+
if (!bundledPkg) {
|
|
122
|
+
// Dropped in bundled → deprecate.
|
|
123
|
+
warnings.push(
|
|
124
|
+
`Package "${pkg.name}" is no longer in the bundled list and has been marked deprecated.`,
|
|
125
|
+
);
|
|
126
|
+
merged.push({ ...pkg, deprecated: true });
|
|
127
|
+
} else {
|
|
128
|
+
// Present in both → user version wins.
|
|
129
|
+
if (pkg.version !== bundledPkg.version) {
|
|
130
|
+
warnings.push(
|
|
131
|
+
`Package "${pkg.name}" is pinned at "${pkg.version}" (bundled default: "${bundledPkg.version}"). User pin preserved.`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
merged.push({ ...pkg });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Walk bundled packages not yet in existing.
|
|
139
|
+
for (const bundledPkg of bundled.packages) {
|
|
140
|
+
if (existingMap.has(bundledPkg.name)) continue;
|
|
141
|
+
if (!bundledPkg.required) {
|
|
142
|
+
merged.push({ ...bundledPkg, defaultOff: true });
|
|
143
|
+
} else {
|
|
144
|
+
merged.push({ ...bundledPkg });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
list: { version: bundled.version, packages: merged },
|
|
150
|
+
warnings,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature flag for the LaunchSource V2 resolver.
|
|
3
|
+
* Default: true (Phase C cutover). Set LAUNCH_SOURCE_V2=false to revert to
|
|
4
|
+
* the legacy wizard + mode.json path for debugging.
|
|
5
|
+
*
|
|
6
|
+
* TODO: remove LAUNCH_SOURCE_V2 flag in follow-up change after Phase C ships
|
|
7
|
+
* without regressions for one release cycle. The flag and its CI matrix
|
|
8
|
+
* entry should be deleted at that point.
|
|
9
|
+
*/
|
|
10
|
+
export function isLaunchSourceV2Enabled(env: Record<string, string | undefined>): boolean {
|
|
11
|
+
const val = env["LAUNCH_SOURCE_V2"];
|
|
12
|
+
if (val === undefined) return true; // default ON in Phase C
|
|
13
|
+
return val === "true" || val === "1";
|
|
14
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LaunchSource — discriminated union describing how the dashboard server was (or should be) started.
|
|
3
|
+
*
|
|
4
|
+
* "attach" — a server is already running; Electron attaches to it.
|
|
5
|
+
* "devMonorepo" — running from a checked-out monorepo (dev workflow).
|
|
6
|
+
* "piExtension" — the pi bridge extension owns a server package in node_modules.
|
|
7
|
+
* "npmGlobal" — `pi-dashboard` is installed globally via npm.
|
|
8
|
+
* "extracted" — bundled Electron resources provide the server (managed install).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type SourceKind = "attach" | "devMonorepo" | "piExtension" | "npmGlobal" | "extracted";
|
|
12
|
+
|
|
13
|
+
export type LaunchSource =
|
|
14
|
+
| { kind: "attach"; url: string; starter: "Bridge" | "Standalone" | "Electron" }
|
|
15
|
+
| { kind: "devMonorepo"; cliPath: string; cwd: string }
|
|
16
|
+
| { kind: "piExtension"; cliPath: string; cwd: string }
|
|
17
|
+
| { kind: "npmGlobal"; cliPath: string; cwd: string }
|
|
18
|
+
| { kind: "extracted"; cliPath: string; cwd: string; didExtract?: boolean };
|
|
@@ -40,6 +40,22 @@ const CLI_ARCHIVE_RE = /openspec\s+archive\s+["']?([^\s"']+)["']?/;
|
|
|
40
40
|
/** Regex to match openspec new change "name" (positional arg) */
|
|
41
41
|
const CLI_NEW_CHANGE_RE = /openspec\s+new\s+change\s+["']?([^\s"']+)["']?/;
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* OpenSpec change-slug shape: lowercase kebab-case, must start with a letter,
|
|
45
|
+
* max 64 characters. Mirrors the validation enforced by `openspec new change`.
|
|
46
|
+
*
|
|
47
|
+
* Single source of truth for any code that needs to gate a captured token
|
|
48
|
+
* before treating it as an OpenSpec change name (detector + auto-attach
|
|
49
|
+
* defense-in-depth in event-wiring.ts).
|
|
50
|
+
*
|
|
51
|
+
* See change: fix-uuid-rename-bug.
|
|
52
|
+
*/
|
|
53
|
+
const OPENSPEC_CHANGE_SLUG_RE = /^[a-z][a-z0-9-]{0,63}$/;
|
|
54
|
+
|
|
55
|
+
export function isValidOpenSpecChangeSlug(name: string): boolean {
|
|
56
|
+
return OPENSPEC_CHANGE_SLUG_RE.test(name);
|
|
57
|
+
}
|
|
58
|
+
|
|
43
59
|
export function detectOpenSpecActivity(
|
|
44
60
|
toolName: string,
|
|
45
61
|
args: Record<string, unknown> | undefined,
|
|
@@ -63,7 +79,7 @@ export function detectOpenSpecActivity(
|
|
|
63
79
|
|
|
64
80
|
// Check for openspec change file read → change name detection (passive)
|
|
65
81
|
const changeMatch = path.match(CHANGE_PATH_RE);
|
|
66
|
-
if (changeMatch) {
|
|
82
|
+
if (changeMatch && isValidOpenSpecChangeSlug(changeMatch[1])) {
|
|
67
83
|
return { changeName: changeMatch[1], isActive: false };
|
|
68
84
|
}
|
|
69
85
|
|
|
@@ -75,7 +91,7 @@ export function detectOpenSpecActivity(
|
|
|
75
91
|
if (!path) return null;
|
|
76
92
|
|
|
77
93
|
const changeMatch = path.match(CHANGE_PATH_RE);
|
|
78
|
-
if (changeMatch) {
|
|
94
|
+
if (changeMatch && isValidOpenSpecChangeSlug(changeMatch[1])) {
|
|
79
95
|
return { changeName: changeMatch[1], isActive: true };
|
|
80
96
|
}
|
|
81
97
|
|
|
@@ -94,11 +110,13 @@ export function detectOpenSpecActivity(
|
|
|
94
110
|
if (!match) return null;
|
|
95
111
|
|
|
96
112
|
const name = match[1];
|
|
97
|
-
// Reject
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
|
|
113
|
+
// Reject any token that is not a valid OpenSpec change slug. Subsumes the
|
|
114
|
+
// earlier `-`-prefix guard (a leading `-` fails the `[a-z]` first-char
|
|
115
|
+
// class) and additionally rejects UUIDs, mixed-case, underscored, or
|
|
116
|
+
// overlong tokens that the CLI regexes' `[^\s"']+` capture group would
|
|
117
|
+
// otherwise pass through into auto-attach + auto-rename.
|
|
118
|
+
// See changes: fix-openspec-flag-rename-bug, fix-uuid-rename-bug.
|
|
119
|
+
if (!isValidOpenSpecChangeSlug(name)) return null;
|
|
102
120
|
|
|
103
121
|
return { changeName: name, isActive: true };
|
|
104
122
|
}
|
|
@@ -47,10 +47,18 @@ export interface SpawnDetachedOptions {
|
|
|
47
47
|
/** Environment for the child. Defaults to `process.env` via Node. */
|
|
48
48
|
env?: NodeJS.ProcessEnv;
|
|
49
49
|
/**
|
|
50
|
-
* Optional file descriptor for stderr. When omitted,
|
|
50
|
+
* Optional file descriptor for combined stdout + stderr. When omitted,
|
|
51
|
+
* BOTH stdout and stderr are "ignore".
|
|
52
|
+
*
|
|
51
53
|
* Caller is responsible for `fs.openSync(logPath, "a")` and closing the
|
|
52
54
|
* parent's copy after spawn (the child retains its dup via stdio
|
|
53
55
|
* inheritance). File fds survive parent death; pipes do not.
|
|
56
|
+
*
|
|
57
|
+
* Routing both streams to a single fd matches what the standalone CLI
|
|
58
|
+
* (`packages/server/src/cli.ts`) does directly with raw `spawn()`. Prior
|
|
59
|
+
* to change `fix-electron-extracted-jiti-and-stdio-capture`, only stderr
|
|
60
|
+
* was captured and clean-startup `console.log` output was silently
|
|
61
|
+
* dropped (resulting in 0-byte server.log files).
|
|
54
62
|
*/
|
|
55
63
|
logFd?: number;
|
|
56
64
|
/**
|
|
@@ -123,7 +131,10 @@ export interface SpawnDetachedResult {
|
|
|
123
131
|
*/
|
|
124
132
|
export async function spawnDetached(opts: SpawnDetachedOptions): Promise<SpawnDetachedResult> {
|
|
125
133
|
const stdioIn: "ignore" | "pipe" = opts.stdinMode ?? "ignore";
|
|
126
|
-
|
|
134
|
+
// logFd applies to BOTH stdout and stderr. See JSDoc on logFd above and
|
|
135
|
+
// change: fix-electron-extracted-jiti-and-stdio-capture.
|
|
136
|
+
const outFd: "ignore" | number = opts.logFd ?? "ignore";
|
|
137
|
+
const stdio: ("ignore" | "pipe" | number)[] = [stdioIn, outFd, outFd];
|
|
127
138
|
|
|
128
139
|
let child: ChildProcess;
|
|
129
140
|
let spawnError: string | null = null;
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jujutsu (jj) tool module — Recipe-based API for jj operations the
|
|
3
|
+
* dashboard runs from multiple call sites (bridge VCS probe, jj-plugin
|
|
4
|
+
* server routes, session-diff regime-aware enrichment).
|
|
5
|
+
*
|
|
6
|
+
* Mirror of `platform/git.ts`: every function is a thin wrapper over
|
|
7
|
+
* `run(recipe, input)`. No `child_process` imports, no `process.platform`
|
|
8
|
+
* branches, no inline shell-escape logic.
|
|
9
|
+
*
|
|
10
|
+
* **Minimum jj version**: target `>= 0.18.0` for `workspace add -r`,
|
|
11
|
+
* `op restore`, `fork_point()`, and `--no-pager`. The version is
|
|
12
|
+
* captured in tool-registry metadata only; no runtime gate yet.
|
|
13
|
+
*
|
|
14
|
+
* **Output parsing strategy**: `jj` does not have a stable `--json` flag
|
|
15
|
+
* across the commands we use. Where parsing is required (`workspaceList`,
|
|
16
|
+
* `workspaceRoot`, `version`), we parse the human-readable output with
|
|
17
|
+
* defensive regexes. Mutation commands (`workspaceAdd`, `bookmarkCreate`,
|
|
18
|
+
* etc.) just check exit codes.
|
|
19
|
+
*
|
|
20
|
+
* See change: add-jj-workspace-plugin.
|
|
21
|
+
*/
|
|
22
|
+
import { run, unwrap, type Recipe, type Result } from "./runner.js";
|
|
23
|
+
|
|
24
|
+
// ── Recipes (pure data) ─────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
const JJ_TIMEOUT = 15_000;
|
|
27
|
+
|
|
28
|
+
interface WithCwd {
|
|
29
|
+
cwd: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** `jj --version` → semver string (e.g. "jj 0.18.0"). */
|
|
33
|
+
export const JJ_VERSION: Recipe<{}, string | undefined> = {
|
|
34
|
+
argv: () => ["jj", "--version"],
|
|
35
|
+
parse: (out) => {
|
|
36
|
+
const m = out.match(/jj\s+([0-9]+\.[0-9]+\.[0-9]+)/);
|
|
37
|
+
return m ? m[1] : out.trim() || undefined;
|
|
38
|
+
},
|
|
39
|
+
timeout: JJ_TIMEOUT,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* `jj workspace root` → absolute path of the current workspace's root.
|
|
44
|
+
* Errors when cwd is not inside a jj repo.
|
|
45
|
+
*/
|
|
46
|
+
export const JJ_WORKSPACE_ROOT: Recipe<WithCwd, string | undefined> = {
|
|
47
|
+
argv: () => ["jj", "workspace", "root"],
|
|
48
|
+
parse: (out) => out.trim() || undefined,
|
|
49
|
+
timeout: JJ_TIMEOUT,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* `jj workspace list` → raw output, one workspace per line.
|
|
54
|
+
* Format (jj 0.18+): `<name>: <change-id-short> <commit-id-short> (...) <desc>`
|
|
55
|
+
* Caller parses via `parseWorkspaceList`.
|
|
56
|
+
*/
|
|
57
|
+
export const JJ_WORKSPACE_LIST: Recipe<WithCwd, string> = {
|
|
58
|
+
argv: () => ["jj", "workspace", "list", "--no-pager"],
|
|
59
|
+
parse: (out) => out,
|
|
60
|
+
timeout: JJ_TIMEOUT,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* `jj workspace add <abs-path> [-r <rev>]` — non-destructive on the
|
|
65
|
+
* source workspace; creates a new working-copy commit on top of `rev`.
|
|
66
|
+
*/
|
|
67
|
+
export const JJ_WORKSPACE_ADD: Recipe<
|
|
68
|
+
WithCwd & { destPath: string; baseRev?: string },
|
|
69
|
+
void
|
|
70
|
+
> = {
|
|
71
|
+
argv: ({ destPath, baseRev }) => {
|
|
72
|
+
const argv: string[] = ["jj", "workspace", "add", destPath];
|
|
73
|
+
if (baseRev) argv.push("-r", baseRev);
|
|
74
|
+
return argv;
|
|
75
|
+
},
|
|
76
|
+
parse: () => undefined,
|
|
77
|
+
timeout: JJ_TIMEOUT,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** `jj workspace forget <name>` — detaches without deleting files on disk. */
|
|
81
|
+
export const JJ_WORKSPACE_FORGET: Recipe<WithCwd & { name: string }, void> = {
|
|
82
|
+
argv: ({ name }) => ["jj", "workspace", "forget", name],
|
|
83
|
+
parse: () => undefined,
|
|
84
|
+
timeout: JJ_TIMEOUT,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** `jj bookmark create <name> -r <rev>`. */
|
|
88
|
+
export const JJ_BOOKMARK_CREATE: Recipe<
|
|
89
|
+
WithCwd & { name: string; rev: string },
|
|
90
|
+
void
|
|
91
|
+
> = {
|
|
92
|
+
argv: ({ name, rev }) => ["jj", "bookmark", "create", name, "-r", rev],
|
|
93
|
+
parse: () => undefined,
|
|
94
|
+
timeout: JJ_TIMEOUT,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* `jj bookmark list -T 'name ++ "\n"'` — list bookmark names, one per line.
|
|
99
|
+
* Used by fold-back to check whether a bookmark name already exists.
|
|
100
|
+
*/
|
|
101
|
+
export const JJ_BOOKMARK_LIST: Recipe<WithCwd, string> = {
|
|
102
|
+
argv: () => ["jj", "bookmark", "list", "-T", 'name ++ "\\n"', "--no-pager"],
|
|
103
|
+
parse: (out) => out,
|
|
104
|
+
timeout: JJ_TIMEOUT,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/** `jj git init --colocate` — converts a plain-git cwd into a jj-colocated repo. */
|
|
108
|
+
export const JJ_GIT_INIT_COLOCATE: Recipe<WithCwd, void> = {
|
|
109
|
+
argv: () => ["jj", "git", "init", "--colocate"],
|
|
110
|
+
parse: () => undefined,
|
|
111
|
+
timeout: JJ_TIMEOUT,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/** `jj git push --bookmark <name>` — translates jj history to git refs. */
|
|
115
|
+
export const JJ_GIT_PUSH: Recipe<WithCwd & { bookmark: string }, void> = {
|
|
116
|
+
argv: ({ bookmark }) => ["jj", "git", "push", "--bookmark", bookmark],
|
|
117
|
+
parse: () => undefined,
|
|
118
|
+
timeout: JJ_TIMEOUT,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* `jj diff [--from R1] [--to R2] [-- <path>]` — unified diff output.
|
|
123
|
+
* Default invocation diffs the working copy (`@`) against its parent (`@-`).
|
|
124
|
+
*/
|
|
125
|
+
export const JJ_DIFF: Recipe<
|
|
126
|
+
WithCwd & { fromRev?: string; toRev?: string; path?: string },
|
|
127
|
+
string
|
|
128
|
+
> = {
|
|
129
|
+
argv: ({ fromRev, toRev, path }) => {
|
|
130
|
+
const argv: string[] = ["jj", "diff", "--no-pager"];
|
|
131
|
+
if (fromRev) argv.push("--from", fromRev);
|
|
132
|
+
if (toRev) argv.push("--to", toRev);
|
|
133
|
+
if (path) argv.push("--", path);
|
|
134
|
+
return argv;
|
|
135
|
+
},
|
|
136
|
+
parse: (out) => out,
|
|
137
|
+
timeout: JJ_TIMEOUT,
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* `jj resolve --list` — newline-separated list of files with conflicts.
|
|
142
|
+
* Empty output means no conflicts; tolerated exit 1 for "nothing to resolve".
|
|
143
|
+
*/
|
|
144
|
+
export const JJ_RESOLVE_LIST: Recipe<WithCwd, string> = {
|
|
145
|
+
argv: () => ["jj", "resolve", "--list", "--no-pager"],
|
|
146
|
+
parse: (out) => out,
|
|
147
|
+
timeout: JJ_TIMEOUT,
|
|
148
|
+
tolerate: [1],
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* `jj op log -T 'id.short() ++ "\n"' --limit 1` — current op id (short).
|
|
153
|
+
* Used by fold-back to capture pre-rebase state for `op restore`.
|
|
154
|
+
*/
|
|
155
|
+
export const JJ_OP_LOG_HEAD: Recipe<WithCwd, string | undefined> = {
|
|
156
|
+
argv: () => [
|
|
157
|
+
"jj", "op", "log",
|
|
158
|
+
"-T", 'id.short() ++ "\\n"',
|
|
159
|
+
"--limit", "1",
|
|
160
|
+
"--no-pager",
|
|
161
|
+
],
|
|
162
|
+
parse: (out) => out.trim().split("\n")[0]?.trim() || undefined,
|
|
163
|
+
timeout: JJ_TIMEOUT,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/** `jj op restore <op-id>` — undo back to the given operation. */
|
|
167
|
+
export const JJ_OP_RESTORE: Recipe<WithCwd & { opId: string }, void> = {
|
|
168
|
+
argv: ({ opId }) => ["jj", "op", "restore", opId],
|
|
169
|
+
parse: () => undefined,
|
|
170
|
+
timeout: JJ_TIMEOUT,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/** `jj rebase -d <dest> -s <src>` — rebase src and descendants onto dest. */
|
|
174
|
+
export const JJ_REBASE: Recipe<
|
|
175
|
+
WithCwd & { dest: string; src: string },
|
|
176
|
+
void
|
|
177
|
+
> = {
|
|
178
|
+
argv: ({ dest, src }) => ["jj", "rebase", "-d", dest, "-s", src],
|
|
179
|
+
parse: () => undefined,
|
|
180
|
+
timeout: JJ_TIMEOUT,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* `jj log -r '<revset>' -T 'change_id.short() ++ "\n"'` —
|
|
185
|
+
* list change ids matching a revset, one per line.
|
|
186
|
+
* Used to check for unfolded commits and resolve `fork_point()`.
|
|
187
|
+
*/
|
|
188
|
+
export const JJ_LOG_REVSET: Recipe<
|
|
189
|
+
WithCwd & { revset: string; template?: string },
|
|
190
|
+
string
|
|
191
|
+
> = {
|
|
192
|
+
argv: ({ revset, template }) => [
|
|
193
|
+
"jj", "log",
|
|
194
|
+
"-r", revset,
|
|
195
|
+
"-T", template ?? 'change_id.short() ++ "\\n"',
|
|
196
|
+
"--no-pager",
|
|
197
|
+
"--no-graph",
|
|
198
|
+
],
|
|
199
|
+
parse: (out) => out,
|
|
200
|
+
timeout: JJ_TIMEOUT,
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// ── Registry ────────────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
export const JJ_RECIPES = {
|
|
206
|
+
JJ_VERSION,
|
|
207
|
+
JJ_WORKSPACE_ROOT,
|
|
208
|
+
JJ_WORKSPACE_LIST,
|
|
209
|
+
JJ_WORKSPACE_ADD,
|
|
210
|
+
JJ_WORKSPACE_FORGET,
|
|
211
|
+
JJ_BOOKMARK_CREATE,
|
|
212
|
+
JJ_BOOKMARK_LIST,
|
|
213
|
+
JJ_GIT_INIT_COLOCATE,
|
|
214
|
+
JJ_GIT_PUSH,
|
|
215
|
+
JJ_DIFF,
|
|
216
|
+
JJ_RESOLVE_LIST,
|
|
217
|
+
JJ_OP_LOG_HEAD,
|
|
218
|
+
JJ_OP_RESTORE,
|
|
219
|
+
JJ_REBASE,
|
|
220
|
+
JJ_LOG_REVSET,
|
|
221
|
+
} as const;
|
|
222
|
+
|
|
223
|
+
// ── Public typed API ────────────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
export function version(): Result<string | undefined> {
|
|
226
|
+
return run(JJ_VERSION, {}, {});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function workspaceRoot(input: WithCwd): Result<string | undefined> {
|
|
230
|
+
return run(JJ_WORKSPACE_ROOT, input, { cwd: input.cwd });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function workspaceList(input: WithCwd): Result<string> {
|
|
234
|
+
return run(JJ_WORKSPACE_LIST, input, { cwd: input.cwd });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function workspaceAdd(
|
|
238
|
+
input: WithCwd & { destPath: string; baseRev?: string },
|
|
239
|
+
): Result<void> {
|
|
240
|
+
return run(JJ_WORKSPACE_ADD, input, { cwd: input.cwd });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function workspaceForget(
|
|
244
|
+
input: WithCwd & { name: string },
|
|
245
|
+
): Result<void> {
|
|
246
|
+
return run(JJ_WORKSPACE_FORGET, input, { cwd: input.cwd });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function bookmarkCreate(
|
|
250
|
+
input: WithCwd & { name: string; rev: string },
|
|
251
|
+
): Result<void> {
|
|
252
|
+
return run(JJ_BOOKMARK_CREATE, input, { cwd: input.cwd });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function bookmarkList(input: WithCwd): Result<string> {
|
|
256
|
+
return run(JJ_BOOKMARK_LIST, input, { cwd: input.cwd });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function gitInitColocate(input: WithCwd): Result<void> {
|
|
260
|
+
return run(JJ_GIT_INIT_COLOCATE, input, { cwd: input.cwd });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function gitPush(
|
|
264
|
+
input: WithCwd & { bookmark: string },
|
|
265
|
+
): Result<void> {
|
|
266
|
+
return run(JJ_GIT_PUSH, input, { cwd: input.cwd });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function diff(
|
|
270
|
+
input: WithCwd & { fromRev?: string; toRev?: string; path?: string },
|
|
271
|
+
): Result<string> {
|
|
272
|
+
return run(JJ_DIFF, input, { cwd: input.cwd });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function resolveList(input: WithCwd): Result<string> {
|
|
276
|
+
return run(JJ_RESOLVE_LIST, input, { cwd: input.cwd });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function opLogHead(input: WithCwd): Result<string | undefined> {
|
|
280
|
+
return run(JJ_OP_LOG_HEAD, input, { cwd: input.cwd });
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function opRestore(
|
|
284
|
+
input: WithCwd & { opId: string },
|
|
285
|
+
): Result<void> {
|
|
286
|
+
return run(JJ_OP_RESTORE, input, { cwd: input.cwd });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function rebase(
|
|
290
|
+
input: WithCwd & { dest: string; src: string },
|
|
291
|
+
): Result<void> {
|
|
292
|
+
return run(JJ_REBASE, input, { cwd: input.cwd });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function logRevset(
|
|
296
|
+
input: WithCwd & { revset: string; template?: string },
|
|
297
|
+
): Result<string> {
|
|
298
|
+
return run(JJ_LOG_REVSET, input, { cwd: input.cwd });
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ── Best-effort wrappers ────────────────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
export function versionOr(fallback?: string): string | undefined {
|
|
304
|
+
return unwrap(version(), fallback);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function workspaceRootOr(
|
|
308
|
+
input: WithCwd,
|
|
309
|
+
fallback?: string,
|
|
310
|
+
): string | undefined {
|
|
311
|
+
return unwrap(workspaceRoot(input), fallback);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function workspaceListOr(input: WithCwd, fallback = ""): string {
|
|
315
|
+
return unwrap(workspaceList(input), fallback);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function diffOr(
|
|
319
|
+
input: WithCwd & { fromRev?: string; toRev?: string; path?: string },
|
|
320
|
+
fallback = "",
|
|
321
|
+
): string {
|
|
322
|
+
return unwrap(diff(input), fallback);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function resolveListOr(input: WithCwd, fallback = ""): string {
|
|
326
|
+
return unwrap(resolveList(input), fallback);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function opLogHeadOr(
|
|
330
|
+
input: WithCwd,
|
|
331
|
+
fallback?: string,
|
|
332
|
+
): string | undefined {
|
|
333
|
+
return unwrap(opLogHead(input), fallback);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function bookmarkListOr(input: WithCwd, fallback = ""): string {
|
|
337
|
+
return unwrap(bookmarkList(input), fallback);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ── Pure parsers (separate from I/O for unit testability) ───────────────────
|
|
341
|
+
|
|
342
|
+
export interface JjWorkspaceEntry {
|
|
343
|
+
/** Workspace name (e.g. "default", "agent-1"). */
|
|
344
|
+
name: string;
|
|
345
|
+
/** Short change id of the workspace's working-copy commit. */
|
|
346
|
+
changeIdShort?: string;
|
|
347
|
+
/** Short commit id (the underlying git commit when colocated). */
|
|
348
|
+
commitIdShort?: string;
|
|
349
|
+
/** Working-copy description, if any. */
|
|
350
|
+
description?: string;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Parse `jj workspace list` output into structured entries.
|
|
355
|
+
* Format: `<name>: <change-id-short> <commit-id-short> [(empty)] [(no description set) | <desc>]`
|
|
356
|
+
*
|
|
357
|
+
* Defensive: skips lines that don't match the expected shape.
|
|
358
|
+
*/
|
|
359
|
+
export function parseWorkspaceList(raw: string): JjWorkspaceEntry[] {
|
|
360
|
+
const entries: JjWorkspaceEntry[] = [];
|
|
361
|
+
for (const line of raw.split("\n")) {
|
|
362
|
+
const trimmed = line.trim();
|
|
363
|
+
if (!trimmed) continue;
|
|
364
|
+
const colonIdx = trimmed.indexOf(":");
|
|
365
|
+
if (colonIdx <= 0) continue;
|
|
366
|
+
const name = trimmed.slice(0, colonIdx).trim();
|
|
367
|
+
const rest = trimmed.slice(colonIdx + 1).trim();
|
|
368
|
+
if (!name) continue;
|
|
369
|
+
// The remainder typically starts with two short ids separated by space.
|
|
370
|
+
const idMatch = rest.match(/^([0-9a-z]+)\s+([0-9a-f]+)/i);
|
|
371
|
+
const entry: JjWorkspaceEntry = { name };
|
|
372
|
+
if (idMatch) {
|
|
373
|
+
entry.changeIdShort = idMatch[1];
|
|
374
|
+
entry.commitIdShort = idMatch[2];
|
|
375
|
+
// Strip jj's parenthesized markers ((empty), (no description set),
|
|
376
|
+
// (conflict), etc.) and only keep what's left as the description.
|
|
377
|
+
let after = rest.slice(idMatch[0].length).trim();
|
|
378
|
+
while (/^\([^)]*\)/.test(after)) {
|
|
379
|
+
after = after.replace(/^\([^)]*\)\s*/, "");
|
|
380
|
+
}
|
|
381
|
+
if (after) {
|
|
382
|
+
entry.description = after;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
entries.push(entry);
|
|
386
|
+
}
|
|
387
|
+
return entries;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Given a workspace root absolute path and the parsed workspace list, find
|
|
392
|
+
* the workspace name whose working copy lives at that path. Returns
|
|
393
|
+
* `undefined` if no entry matches.
|
|
394
|
+
*
|
|
395
|
+
* Note: `jj workspace list` does not include the workspace path; resolution
|
|
396
|
+
* by name happens via the bridge probe checking `<workspace-name>@` revsets
|
|
397
|
+
* separately. For the bridge's purposes, we ALSO read `.jj/repo/working_copy/`
|
|
398
|
+
* filesystem layout — this parser is a structural fallback only.
|
|
399
|
+
*/
|
|
400
|
+
export function findWorkspaceByName(
|
|
401
|
+
entries: readonly JjWorkspaceEntry[],
|
|
402
|
+
name: string,
|
|
403
|
+
): JjWorkspaceEntry | undefined {
|
|
404
|
+
return entries.find((e) => e.name === name);
|
|
405
|
+
}
|