@blackbelt-technology/pi-agent-dashboard 0.5.3 → 0.5.4
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 +19 -30
- package/README.md +69 -1
- package/docs/architecture.md +89 -165
- package/package.json +10 -7
- package/packages/extension/package.json +2 -2
- package/packages/extension/src/__tests__/bridge-default-model-gate.test.ts +47 -0
- package/packages/extension/src/__tests__/bridge-followup-chat-order.test.ts +215 -0
- package/packages/extension/src/__tests__/bridge-followup-multi-entry.test.ts +202 -0
- package/packages/extension/src/__tests__/bridge-queue-update-forward.test.ts +77 -0
- package/packages/extension/src/__tests__/bridge-retry-ordering.test.ts +148 -0
- package/packages/extension/src/__tests__/bridge-shadow-queue-drain.test.ts +221 -0
- package/packages/extension/src/__tests__/bridge-shadow-queue-gate.test.ts +299 -0
- package/packages/extension/src/__tests__/bridge-shutdown-reset.test.ts +238 -0
- package/packages/extension/src/__tests__/bridge-slash-command-routing.test.ts +127 -31
- package/packages/extension/src/__tests__/command-handler.test.ts +105 -3
- package/packages/extension/src/__tests__/fixtures/usage-limit-error-strings.ts +127 -0
- package/packages/extension/src/__tests__/source-detector.test.ts +15 -0
- package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +12 -0
- package/packages/extension/src/bridge-default-model-gate.ts +32 -0
- package/packages/extension/src/bridge.ts +299 -20
- package/packages/extension/src/command-handler.ts +53 -7
- package/packages/extension/src/dashboard-default-adapter.ts +5 -0
- package/packages/extension/src/prompt-bus.ts +15 -0
- package/packages/extension/src/slash-dispatch.ts +30 -15
- package/packages/extension/src/source-detector.ts +13 -5
- package/packages/extension/src/usage-limit-orderer.ts +18 -1
- package/packages/server/bin/pi-dashboard.mjs +62 -14
- package/packages/server/package.json +9 -5
- package/packages/server/src/__tests__/browser-gateway-register-handler.test.ts +69 -0
- package/packages/server/src/__tests__/cli-env-no-clobber.test.ts +46 -0
- package/packages/server/src/__tests__/cli-no-bootstrap-references.test.ts +69 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +9 -10
- package/packages/server/src/__tests__/cli-version.test.ts +151 -0
- package/packages/server/src/__tests__/directory-service-openspec-enabled.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service-toctou.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service.test.ts +9 -0
- package/packages/server/src/__tests__/doctor-route.test.ts +53 -0
- package/packages/server/src/__tests__/event-wiring-queue-state.test.ts +156 -0
- package/packages/server/src/__tests__/event-wiring-resume-clear.test.ts +105 -0
- package/packages/server/src/__tests__/health-shape.test.ts +35 -12
- package/packages/server/src/__tests__/installed-package-enricher.test.ts +12 -12
- package/packages/server/src/__tests__/is-activity-event.test.ts +4 -7
- package/packages/server/src/__tests__/package-routes.test.ts +6 -2
- package/packages/server/src/__tests__/pi-changelog-routes.test.ts +10 -13
- package/packages/server/src/__tests__/pi-core-checker.test.ts +2 -2
- package/packages/server/src/__tests__/pi-version-skew.test.ts +3 -2
- package/packages/server/src/__tests__/plugin-activation-routes.test.ts +267 -0
- package/packages/server/src/__tests__/plugin-intent-cache.test.ts +75 -0
- package/packages/server/src/__tests__/preferences-store.test.ts +196 -0
- package/packages/server/src/__tests__/reattach-placement.test.ts +9 -0
- package/packages/server/src/__tests__/recommended-routes.test.ts +2 -2
- package/packages/server/src/__tests__/recovery-server.test.ts +203 -0
- package/packages/server/src/__tests__/session-action-handler-clear-queue.test.ts +153 -0
- package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +43 -0
- package/packages/server/src/__tests__/session-order-manager.test.ts +9 -0
- package/packages/server/src/__tests__/session-order-reboot.test.ts +9 -0
- package/packages/server/src/__tests__/session-ordering-integration.test.ts +9 -0
- package/packages/server/src/browser-gateway.ts +83 -5
- package/packages/server/src/browser-handlers/directory-handler.ts +69 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +89 -0
- package/packages/server/src/browser-handlers/subscription-handler.ts +23 -0
- package/packages/server/src/changelog-parser.ts +1 -1
- package/packages/server/src/cli.ts +68 -250
- package/packages/server/src/event-status-extraction.ts +14 -62
- package/packages/server/src/event-wiring.ts +23 -10
- package/packages/server/src/memory-session-manager.ts +4 -0
- package/packages/server/src/pi-core-checker.ts +1 -1
- package/packages/server/src/pi-dev-version-check.ts +1 -1
- package/packages/server/src/pi-version-skew.ts +24 -46
- package/packages/server/src/plugin-intent-cache.ts +67 -0
- package/packages/server/src/preferences-store.ts +199 -13
- package/packages/server/src/recovery-server.ts +366 -0
- package/packages/server/src/routes/__tests__/manifest-route.test.ts +138 -0
- package/packages/server/src/routes/doctor-routes.ts +26 -21
- package/packages/server/src/routes/manifest-route.ts +162 -0
- package/packages/server/src/routes/openspec-routes.ts +4 -25
- package/packages/server/src/routes/pi-changelog-routes.ts +5 -24
- package/packages/server/src/routes/pi-core-routes.ts +3 -23
- package/packages/server/src/routes/plugin-activation-routes.ts +193 -0
- package/packages/server/src/routes/recommended-routes.ts +21 -0
- package/packages/server/src/routes/system-routes.ts +73 -11
- package/packages/server/src/server.ts +105 -307
- package/packages/server/src/session-api.ts +5 -63
- package/packages/shared/package.json +1 -1
- package/packages/shared/src/__tests__/binary-lookup-resolveJiti.test.ts +28 -0
- package/packages/shared/src/__tests__/binary-lookup-spawn-env.test.ts +61 -0
- package/packages/shared/src/__tests__/binary-lookup.test.ts +16 -0
- package/packages/shared/src/__tests__/bridge-register.test.ts +67 -0
- package/packages/shared/src/__tests__/ci-electron-no-side-effects.test.ts +129 -0
- package/packages/shared/src/__tests__/config.test.ts +40 -0
- package/packages/shared/src/__tests__/dashboard-paths.test.ts +81 -0
- package/packages/shared/src/__tests__/ensure-windows-path.test.ts +112 -0
- package/packages/shared/src/__tests__/intent-types.test.ts +120 -0
- package/packages/shared/src/__tests__/jiti-packages-parity.test.ts +85 -0
- package/packages/shared/src/__tests__/legacy-managed-dir.test.ts +59 -0
- package/packages/shared/src/__tests__/no-direct-child-process.test.ts +12 -0
- package/packages/shared/src/__tests__/no-electron-execpath-spawn.test.ts +149 -0
- package/packages/shared/src/__tests__/no-flow-command-route-claims.test.ts +71 -0
- package/packages/shared/src/__tests__/no-flow-references-in-shell.test.ts +221 -0
- package/packages/shared/src/__tests__/no-managed-dir-reference.test.ts +134 -0
- package/packages/shared/src/__tests__/no-pi-dashboard-version-jiti-gate.test.ts +41 -0
- package/packages/shared/src/__tests__/no-primitive-direct-import.test.ts +235 -0
- package/packages/shared/src/__tests__/no-server-imports-in-resolver.test.ts +53 -0
- package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +54 -101
- package/packages/shared/src/__tests__/node-spawn.test.ts +29 -13
- package/packages/shared/src/__tests__/pi-package-resolver.test.ts +300 -0
- package/packages/shared/src/__tests__/plugin-activation-contracts.test.ts +74 -0
- package/packages/shared/src/__tests__/plugin-bridge-classify-source.test.ts +73 -0
- package/packages/shared/src/__tests__/plugin-bridge-register-extended.test.ts +17 -5
- package/packages/shared/src/__tests__/plugin-bridge-register-packages.test.ts +233 -0
- package/packages/shared/src/__tests__/plugin-bridge-register.test.ts +19 -9
- package/packages/shared/src/__tests__/publish-workflow-contract.test.ts +154 -15
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +28 -10
- package/packages/shared/src/__tests__/resolver-parity-with-scanner.test.ts +76 -0
- package/packages/shared/src/__tests__/server-identity.test.ts +127 -0
- package/packages/shared/src/__tests__/server-launcher.test.ts +35 -0
- package/packages/shared/src/__tests__/source-matching.test.ts +5 -5
- package/packages/shared/src/__tests__/sync-versions-spec.test.ts +76 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +50 -2
- package/packages/shared/src/bridge-register.ts +35 -2
- package/packages/shared/src/browser-protocol.ts +176 -2
- package/packages/shared/src/config.ts +12 -0
- package/packages/shared/src/dashboard-paths.ts +69 -0
- package/packages/shared/src/dashboard-plugin/index.ts +2 -0
- package/packages/shared/src/dashboard-plugin/intent-types.ts +93 -0
- package/packages/shared/src/dashboard-plugin/manifest-types.ts +55 -1
- package/packages/shared/src/dashboard-plugin/plugin-status.ts +82 -0
- package/packages/shared/src/dashboard-plugin/slot-props.ts +11 -0
- package/packages/shared/src/dashboard-plugin/slot-types.ts +16 -2
- package/packages/shared/src/dashboard-plugin/ui-primitives.ts +287 -0
- package/packages/shared/src/dashboard-starter.ts +22 -0
- package/packages/shared/src/doctor-core.ts +49 -27
- package/packages/shared/src/launch-source-types.ts +9 -9
- package/packages/shared/src/legacy-managed-dir.ts +97 -0
- package/packages/shared/src/mdns-discovery.ts +4 -1
- package/packages/shared/src/pi-package-resolver.ts +388 -0
- package/packages/shared/src/platform/binary-lookup.ts +27 -3
- package/packages/shared/src/platform/ensure-windows-path.ts +95 -0
- package/packages/shared/src/platform/exec.ts +22 -0
- package/packages/shared/src/platform/node-spawn.ts +42 -41
- package/packages/shared/src/plugin-bridge-register.ts +275 -18
- package/packages/shared/src/protocol.ts +94 -2
- package/packages/shared/src/recommended-extensions.ts +34 -10
- package/packages/shared/src/server-identity.ts +74 -5
- package/packages/shared/src/server-launcher.ts +20 -0
- package/packages/shared/src/source-matching.ts +1 -1
- package/packages/shared/src/tool-registry/__tests__/node-script-toargv-fallback.test.ts +84 -0
- package/packages/shared/src/tool-registry/definitions.ts +91 -7
- package/packages/shared/src/types.ts +12 -8
- package/scripts/maybe-patch-package.cjs +44 -0
- package/packages/server/src/__tests__/bootstrap-install-from-list.test.ts +0 -263
- package/packages/server/src/__tests__/bootstrap-queue.test.ts +0 -120
- package/packages/server/src/__tests__/bootstrap-routes.test.ts +0 -125
- package/packages/server/src/__tests__/bootstrap-state.test.ts +0 -119
- package/packages/server/src/__tests__/cli-bootstrap.test.ts +0 -36
- package/packages/server/src/__tests__/event-status-extraction-flow.test.ts +0 -55
- package/packages/server/src/__tests__/legacy-pi-cleanup.test.ts +0 -149
- package/packages/server/src/__tests__/post-install-openspec-refresh.test.ts +0 -180
- package/packages/server/src/__tests__/post-install-rescan.test.ts +0 -134
- package/packages/server/src/__tests__/system-routes-reextract.test.ts +0 -91
- package/packages/server/src/bootstrap-install-from-list.ts +0 -232
- package/packages/server/src/bootstrap-queue.ts +0 -130
- package/packages/server/src/bootstrap-state.ts +0 -159
- package/packages/server/src/legacy-pi-cleanup.ts +0 -151
- package/packages/server/src/routes/bootstrap-routes.ts +0 -125
- package/packages/shared/src/__tests__/bootstrap/README.md +0 -133
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +0 -378
- package/packages/shared/src/__tests__/bootstrap/assertions.ts +0 -136
- package/packages/shared/src/__tests__/bootstrap/cube.test.ts +0 -47
- package/packages/shared/src/__tests__/bootstrap/cube.ts +0 -66
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +0 -84
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +0 -90
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +0 -34
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +0 -20
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +0 -62
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +0 -34
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +0 -49
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +0 -12
- package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +0 -156
- package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +0 -157
- package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +0 -102
- package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +0 -76
- package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +0 -94
- package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +0 -87
- package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +0 -143
- package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +0 -64
- package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +0 -77
- package/packages/shared/src/__tests__/bootstrap/families/index.ts +0 -19
- package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +0 -61
- package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +0 -50
- package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +0 -272
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +0 -58
- package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +0 -84
- package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +0 -9
- package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +0 -85
- package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +0 -122
- package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +0 -36
- package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +0 -39
- package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +0 -220
- package/packages/shared/src/__tests__/bootstrap/harness.ts +0 -413
- package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +0 -125
- package/packages/shared/src/__tests__/bootstrap/scenarios.ts +0 -132
- package/packages/shared/src/__tests__/bootstrap-install-resolve-npm.test.ts +0 -72
- package/packages/shared/src/__tests__/install-managed-node-bootstrap-order.test.ts +0 -68
- package/packages/shared/src/__tests__/install-managed-node.test.ts +0 -192
- package/packages/shared/src/__tests__/installable-list.test.ts +0 -130
- package/packages/shared/src/__tests__/no-installable-list-in-bridge.test.ts +0 -52
- package/packages/shared/src/bootstrap-install.ts +0 -406
- package/packages/shared/src/installable-list.ts +0 -152
- package/packages/shared/src/launch-source-flag.ts +0 -14
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { mergeInstallableList, readInstallableList, writeInstallableList } from "../installable-list.js";
|
|
3
|
-
import type { InstallableList } from "../installable-list.js";
|
|
4
|
-
import fs from "node:fs";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import os from "node:os";
|
|
7
|
-
|
|
8
|
-
// ── mergeInstallableList tests ──────────────────────────────────────────────
|
|
9
|
-
|
|
10
|
-
describe("mergeInstallableList", () => {
|
|
11
|
-
it("keep-user-pin: user version wins when it differs from bundled", () => {
|
|
12
|
-
const existing: InstallableList = {
|
|
13
|
-
version: "1",
|
|
14
|
-
packages: [{ name: "tsx", version: "^4.0.0", required: true, kind: "npm" }],
|
|
15
|
-
};
|
|
16
|
-
const bundled: InstallableList = {
|
|
17
|
-
version: "2",
|
|
18
|
-
packages: [{ name: "tsx", version: "^5.0.0", required: true, kind: "npm" }],
|
|
19
|
-
};
|
|
20
|
-
const { list, warnings } = mergeInstallableList(existing, bundled);
|
|
21
|
-
const found = list.packages.find((p) => p.name === "tsx");
|
|
22
|
-
expect(found?.version).toBe("^4.0.0");
|
|
23
|
-
expect(warnings.length).toBeGreaterThan(0);
|
|
24
|
-
expect(warnings[0]).toContain("tsx");
|
|
25
|
-
expect(warnings[0]).toContain("^4.0.0");
|
|
26
|
-
expect(warnings[0]).toContain("^5.0.0");
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("drop-pin-warn: package in existing but not in bundled gets deprecated=true + warning", () => {
|
|
30
|
-
const existing: InstallableList = {
|
|
31
|
-
version: "1",
|
|
32
|
-
packages: [{ name: "old-tool", version: "^1.0.0", required: false, kind: "npm" }],
|
|
33
|
-
};
|
|
34
|
-
const bundled: InstallableList = { version: "2", packages: [] };
|
|
35
|
-
const { list, warnings } = mergeInstallableList(existing, bundled);
|
|
36
|
-
const found = list.packages.find((p) => p.name === "old-tool");
|
|
37
|
-
expect(found?.deprecated).toBe(true);
|
|
38
|
-
expect(warnings.length).toBeGreaterThan(0);
|
|
39
|
-
expect(warnings[0]).toContain("old-tool");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("add-new-required: new required package from bundled is added as-is", () => {
|
|
43
|
-
const existing: InstallableList = { version: "1", packages: [] };
|
|
44
|
-
const bundled: InstallableList = {
|
|
45
|
-
version: "2",
|
|
46
|
-
packages: [{ name: "pi", version: "*", required: true, kind: "npm" }],
|
|
47
|
-
};
|
|
48
|
-
const { list, warnings } = mergeInstallableList(existing, bundled);
|
|
49
|
-
const found = list.packages.find((p) => p.name === "pi");
|
|
50
|
-
expect(found).toBeDefined();
|
|
51
|
-
expect(found?.required).toBe(true);
|
|
52
|
-
expect(found?.defaultOff).toBeFalsy();
|
|
53
|
-
expect(warnings).toHaveLength(0);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it("add-new-optional: new optional package from bundled is added with defaultOff=true", () => {
|
|
57
|
-
const existing: InstallableList = { version: "1", packages: [] };
|
|
58
|
-
const bundled: InstallableList = {
|
|
59
|
-
version: "2",
|
|
60
|
-
packages: [{ name: "openspec", version: "*", required: false, kind: "npm" }],
|
|
61
|
-
};
|
|
62
|
-
const { list, warnings } = mergeInstallableList(existing, bundled);
|
|
63
|
-
const found = list.packages.find((p) => p.name === "openspec");
|
|
64
|
-
expect(found).toBeDefined();
|
|
65
|
-
expect(found?.defaultOff).toBe(true);
|
|
66
|
-
expect(warnings).toHaveLength(0);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("version marker in result comes from bundled", () => {
|
|
70
|
-
const existing: InstallableList = { version: "1", packages: [] };
|
|
71
|
-
const bundled: InstallableList = { version: "42", packages: [] };
|
|
72
|
-
const { list } = mergeInstallableList(existing, bundled);
|
|
73
|
-
expect(list.version).toBe("42");
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// ── readInstallableList tests ───────────────────────────────────────────────
|
|
78
|
-
// Use a real temp directory (HOME is already ephemeral in the test runner).
|
|
79
|
-
|
|
80
|
-
describe("readInstallableList", () => {
|
|
81
|
-
let tmpDir: string;
|
|
82
|
-
|
|
83
|
-
beforeEach(() => {
|
|
84
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "installable-test-"));
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
afterEach(() => {
|
|
88
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
89
|
-
vi.restoreAllMocks();
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("returns null when file is absent", async () => {
|
|
93
|
-
const result = await readInstallableList(tmpDir);
|
|
94
|
-
expect(result).toBeNull();
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("drops entries with invalid kind and warns", async () => {
|
|
98
|
-
const list: InstallableList = {
|
|
99
|
-
version: "1",
|
|
100
|
-
packages: [
|
|
101
|
-
{ name: "good-pkg", version: "*", required: true, kind: "npm" },
|
|
102
|
-
{ name: "bad-pkg", version: "*", required: true, kind: "unknown-kind" as any },
|
|
103
|
-
],
|
|
104
|
-
};
|
|
105
|
-
// Write via writeInstallableList (bypasses the drop-invalid-kind guard).
|
|
106
|
-
const filePath = path.join(tmpDir, "installable.json");
|
|
107
|
-
fs.writeFileSync(filePath, JSON.stringify(list), "utf-8");
|
|
108
|
-
|
|
109
|
-
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
110
|
-
const result = await readInstallableList(tmpDir);
|
|
111
|
-
|
|
112
|
-
expect(result).not.toBeNull();
|
|
113
|
-
expect(result!.packages.map((p) => p.name)).toEqual(["good-pkg"]);
|
|
114
|
-
expect(warnSpy).toHaveBeenCalledOnce();
|
|
115
|
-
expect(warnSpy.mock.calls[0]![0]).toContain("bad-pkg");
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("reads a valid file and returns the list", async () => {
|
|
119
|
-
const list: InstallableList = {
|
|
120
|
-
version: "3",
|
|
121
|
-
packages: [{ name: "tsx", version: "^5.0.0", required: true, kind: "npm" }],
|
|
122
|
-
};
|
|
123
|
-
await writeInstallableList(list, tmpDir);
|
|
124
|
-
const result = await readInstallableList(tmpDir);
|
|
125
|
-
expect(result).not.toBeNull();
|
|
126
|
-
expect(result!.version).toBe("3");
|
|
127
|
-
expect(result!.packages).toHaveLength(1);
|
|
128
|
-
expect(result!.packages[0]!.name).toBe("tsx");
|
|
129
|
-
});
|
|
130
|
-
});
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Contract test: the bridge auto-spawn code path (server-launcher.ts) must
|
|
3
|
-
* NOT import from `installable-list`. Only Electron seeds `installable.json`
|
|
4
|
-
* on first run; Bridge and Standalone starters must not produce or consume
|
|
5
|
-
* that file.
|
|
6
|
-
*
|
|
7
|
-
* This is a static source scan — no runtime execution. If this test fails,
|
|
8
|
-
* a dependency on installable-list was accidentally added to the bridge
|
|
9
|
-
* launcher which would break the "file-absent is a no-op" contract on
|
|
10
|
-
* Bridge/Standalone bootstraps.
|
|
11
|
-
*
|
|
12
|
-
* See change: simplify-electron-bootstrap-derived-state (task 5.7).
|
|
13
|
-
*/
|
|
14
|
-
import { describe, expect, it } from "vitest";
|
|
15
|
-
import fs from "node:fs";
|
|
16
|
-
import path from "node:path";
|
|
17
|
-
import url from "node:url";
|
|
18
|
-
|
|
19
|
-
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
|
20
|
-
const repoRoot = path.resolve(here, "..", "..", "..", "..");
|
|
21
|
-
|
|
22
|
-
/** Files that form the bridge auto-spawn contract. */
|
|
23
|
-
const BRIDGE_SPAWN_FILES: readonly string[] = [
|
|
24
|
-
"packages/extension/src/server-launcher.ts",
|
|
25
|
-
"packages/extension/src/server-auto-start.ts",
|
|
26
|
-
"packages/extension/src/connection.ts",
|
|
27
|
-
];
|
|
28
|
-
|
|
29
|
-
describe("bridge auto-spawn does not reference installable-list", () => {
|
|
30
|
-
for (const rel of BRIDGE_SPAWN_FILES) {
|
|
31
|
-
it(`${rel} does not import from installable-list`, () => {
|
|
32
|
-
const file = path.resolve(repoRoot, rel);
|
|
33
|
-
if (!fs.existsSync(file)) {
|
|
34
|
-
// File absent (optional extension file) — contract trivially satisfied.
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
const source = fs.readFileSync(file, "utf-8");
|
|
38
|
-
|
|
39
|
-
// Strip line comments before checking so a commented-out import
|
|
40
|
-
// does not trigger a false positive.
|
|
41
|
-
const stripped = source
|
|
42
|
-
.replace(/\/\/[^\n]*/g, "")
|
|
43
|
-
.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
44
|
-
|
|
45
|
-
expect(
|
|
46
|
-
stripped,
|
|
47
|
-
`${rel} must not import from "installable-list" — only Electron seeds installable.json. ` +
|
|
48
|
-
`Bridge/Standalone starters must not produce or consume that file.`,
|
|
49
|
-
).not.toMatch(/installable-list/);
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
});
|
|
@@ -1,406 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared bootstrap installer — single entry point for installing pi,
|
|
3
|
-
* openspec, tsx, and recommended packages into the managed directory
|
|
4
|
-
* (~/.pi-dashboard/). Callable from any entry point: Electron wizard,
|
|
5
|
-
* `pi-dashboard` CLI first-run, `pi-dashboard upgrade-pi` subcommand,
|
|
6
|
-
* and the `POST /api/bootstrap/upgrade-pi` REST handler.
|
|
7
|
-
*
|
|
8
|
-
* This module is deliberately free of Electron-specific concerns
|
|
9
|
-
* (bundled-node, offline-bundle cacache, resourcesPath). Those remain
|
|
10
|
-
* in `packages/electron/src/lib/dependency-installer.ts` which now
|
|
11
|
-
* delegates its "install from npm registry" step to this function.
|
|
12
|
-
*
|
|
13
|
-
* See change: unified-bootstrap-install.
|
|
14
|
-
*/
|
|
15
|
-
import { spawn as cpSpawn, spawnSync as cpSpawnSync } from "./platform/exec.js";
|
|
16
|
-
import {
|
|
17
|
-
cpSync,
|
|
18
|
-
existsSync,
|
|
19
|
-
mkdirSync,
|
|
20
|
-
readFileSync,
|
|
21
|
-
rmSync,
|
|
22
|
-
writeFileSync,
|
|
23
|
-
} from "node:fs";
|
|
24
|
-
import path from "node:path";
|
|
25
|
-
import { getManagedDir } from "./managed-paths.js";
|
|
26
|
-
import { getDefaultRegistry, type ToolRegistry } from "./tool-registry/index.js";
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Per-package progress tick. Mirrors the Electron `InstallProgress`
|
|
30
|
-
* shape so existing wizard UI code needs no changes.
|
|
31
|
-
*/
|
|
32
|
-
export interface InstallProgress {
|
|
33
|
-
step: string;
|
|
34
|
-
status: "pending" | "running" | "done" | "error";
|
|
35
|
-
error?: string;
|
|
36
|
-
/** Last line of npm output (for streaming progress). */
|
|
37
|
-
output?: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export type ProgressCallback = (progress: InstallProgress) => void;
|
|
41
|
-
|
|
42
|
-
export interface BootstrapInstallOptions {
|
|
43
|
-
/** Packages to install via `npm install <pkg>` (registry fetch). */
|
|
44
|
-
packages: string[];
|
|
45
|
-
/** Root of the managed install. Defaults to `getManagedDir()`. */
|
|
46
|
-
managedDir?: string;
|
|
47
|
-
/** Called on every progress tick (pending/running/done/error). */
|
|
48
|
-
progress?: ProgressCallback;
|
|
49
|
-
/**
|
|
50
|
-
* Optional override of the npm invocation. By default the function
|
|
51
|
-
* resolves the `npm` tool via `ToolRegistry.resolve("npm")` and
|
|
52
|
-
* falls back to the plain `npm` / `npm.cmd` binary on PATH. When
|
|
53
|
-
* Electron wants to steer the install to bundled Node + npm-cli.js,
|
|
54
|
-
* it passes the full argv prefix (e.g. `["<path>/node", "<path>/npm-cli.js"]`).
|
|
55
|
-
*/
|
|
56
|
-
npmArgv?: string[];
|
|
57
|
-
/**
|
|
58
|
-
* Optional environment overrides merged into the child process env.
|
|
59
|
-
* Electron uses this to put bundled Node on PATH for postinstall
|
|
60
|
-
* scripts.
|
|
61
|
-
*/
|
|
62
|
-
env?: NodeJS.ProcessEnv;
|
|
63
|
-
/**
|
|
64
|
-
* Inject a tool registry (tests). Defaults to `getDefaultRegistry()`.
|
|
65
|
-
*/
|
|
66
|
-
registry?: ToolRegistry;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export interface BootstrapInstallSuccess {
|
|
70
|
-
ok: true;
|
|
71
|
-
installed: string[];
|
|
72
|
-
managedDir: string;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export interface BootstrapInstallFailure {
|
|
76
|
-
ok: false;
|
|
77
|
-
error: string;
|
|
78
|
-
installed: string[];
|
|
79
|
-
managedDir: string;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export type BootstrapInstallResult = BootstrapInstallSuccess | BootstrapInstallFailure;
|
|
83
|
-
|
|
84
|
-
/** Ensure the managed directory exists with a package.json. */
|
|
85
|
-
export function ensureManagedDir(managedDir: string): void {
|
|
86
|
-
mkdirSync(managedDir, { recursive: true });
|
|
87
|
-
const pkgPath = path.join(managedDir, "package.json");
|
|
88
|
-
if (!existsSync(pkgPath)) {
|
|
89
|
-
writeFileSync(
|
|
90
|
-
pkgPath,
|
|
91
|
-
JSON.stringify({ name: "pi-dashboard-managed", private: true, type: "module" }, null, 2),
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Resolve the npm invocation used for bootstrap installs.
|
|
98
|
-
*
|
|
99
|
-
* Order:
|
|
100
|
-
* 1. Explicit `npmArgv` override (Electron bundled-node case).
|
|
101
|
-
* 2. `ToolRegistry.resolve("npm")`.
|
|
102
|
-
* 3. Plain `npm` (Unix) or `npm.cmd` (Windows) on PATH.
|
|
103
|
-
*
|
|
104
|
-
* Returns the argv list that will have `install <packages...>` appended.
|
|
105
|
-
*/
|
|
106
|
-
export function resolveNpmArgv(
|
|
107
|
-
opts: Pick<BootstrapInstallOptions, "npmArgv" | "registry">,
|
|
108
|
-
): string[] {
|
|
109
|
-
if (opts.npmArgv && opts.npmArgv.length > 0) return [...opts.npmArgv];
|
|
110
|
-
|
|
111
|
-
const registry = opts.registry ?? getDefaultRegistry();
|
|
112
|
-
if (registry.has("npm")) {
|
|
113
|
-
const res = registry.resolve("npm");
|
|
114
|
-
if (res.ok && res.path) return [res.path];
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Last resort: rely on PATH. On Windows the .cmd shim is required
|
|
118
|
-
// because spawn doesn't auto-append extensions.
|
|
119
|
-
const npmBin = process.platform === "win32" ? "npm.cmd" : "npm"; // platform-branch-ok
|
|
120
|
-
return [npmBin];
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** Internal: spawn npm with a given argv + packages; stream progress. */
|
|
124
|
-
function runNpmOnce(
|
|
125
|
-
argvBase: string[],
|
|
126
|
-
packages: string[],
|
|
127
|
-
cwd: string,
|
|
128
|
-
env: NodeJS.ProcessEnv,
|
|
129
|
-
onOutput?: (line: string) => void,
|
|
130
|
-
): Promise<void> {
|
|
131
|
-
return new Promise((resolve, reject) => {
|
|
132
|
-
const [cmd, ...baseArgs] = argvBase;
|
|
133
|
-
if (!cmd) {
|
|
134
|
-
reject(new Error("resolveNpmArgv returned an empty argv"));
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
const args = [...baseArgs, "install", ...packages];
|
|
138
|
-
|
|
139
|
-
const child = cpSpawn(cmd, args, {
|
|
140
|
-
cwd,
|
|
141
|
-
env,
|
|
142
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
143
|
-
timeout: 300_000,
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
let tail = "";
|
|
147
|
-
|
|
148
|
-
const handleData = (data: Buffer): void => {
|
|
149
|
-
const text = data.toString();
|
|
150
|
-
tail += text;
|
|
151
|
-
if (tail.length > 4096) tail = tail.slice(-4096);
|
|
152
|
-
const lines = text.split("\n").filter((l) => l.trim());
|
|
153
|
-
const last = lines[lines.length - 1];
|
|
154
|
-
if (last && onOutput) onOutput(last.trim().substring(0, 120));
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
child.stdout?.on("data", handleData);
|
|
158
|
-
child.stderr?.on("data", handleData);
|
|
159
|
-
|
|
160
|
-
child.on("error", (err) => reject(new Error(err.message)));
|
|
161
|
-
child.on("close", (code) => {
|
|
162
|
-
if (code !== 0) {
|
|
163
|
-
reject(new Error(tail.slice(-500) || `npm install exited with code ${code}`));
|
|
164
|
-
} else {
|
|
165
|
-
resolve();
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Install the given packages into the managed directory.
|
|
173
|
-
*
|
|
174
|
-
* Per-package progress is reported via `progress`. Installation is
|
|
175
|
-
* sequential (not concurrent) so a failure stops the chain — matching
|
|
176
|
-
* the behavior of the Electron wizard today. The return value reports
|
|
177
|
-
* which packages completed successfully before any failure.
|
|
178
|
-
*/
|
|
179
|
-
export async function bootstrapInstall(
|
|
180
|
-
opts: BootstrapInstallOptions,
|
|
181
|
-
): Promise<BootstrapInstallResult> {
|
|
182
|
-
const managedDir = opts.managedDir ?? getManagedDir();
|
|
183
|
-
ensureManagedDir(managedDir);
|
|
184
|
-
|
|
185
|
-
const argvBase = resolveNpmArgv(opts);
|
|
186
|
-
const env = { ...process.env, ...(opts.env ?? {}) };
|
|
187
|
-
|
|
188
|
-
const installed: string[] = [];
|
|
189
|
-
for (const pkg of opts.packages) {
|
|
190
|
-
const step = pkg.split("/").pop() || pkg;
|
|
191
|
-
opts.progress?.({ step, status: "running" });
|
|
192
|
-
try {
|
|
193
|
-
await runNpmOnce(argvBase, [pkg], managedDir, env, (output) => {
|
|
194
|
-
opts.progress?.({ step, status: "running", output });
|
|
195
|
-
});
|
|
196
|
-
opts.progress?.({ step, status: "done" });
|
|
197
|
-
installed.push(pkg);
|
|
198
|
-
} catch (err) {
|
|
199
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
200
|
-
opts.progress?.({ step, status: "error", error: message });
|
|
201
|
-
return { ok: false, error: message, installed, managedDir };
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return { ok: true, installed, managedDir };
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Convenience wrapper: install pi, openspec, tsx into the default
|
|
210
|
-
* managed directory. Used by the CLI degraded-mode first-run path.
|
|
211
|
-
*/
|
|
212
|
-
export async function bootstrapInstallDefaults(
|
|
213
|
-
progress?: ProgressCallback,
|
|
214
|
-
): Promise<BootstrapInstallResult> {
|
|
215
|
-
return bootstrapInstall({
|
|
216
|
-
packages: ["@earendil-works/pi-coding-agent", "@fission-ai/openspec"],
|
|
217
|
-
progress,
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// ── Managed Node runtime install ───────────────────────────────────────
|
|
222
|
-
//
|
|
223
|
-
// See change: embed-managed-node-runtime (spec: managed-node-runtime).
|
|
224
|
-
//
|
|
225
|
-
// `installManagedNode` copies a bundled Node distribution into
|
|
226
|
-
// `<managedDir>/node/` and writes a `<managedDir>/node/.version` marker.
|
|
227
|
-
// Idempotent: skip when marker matches the bundled version, replace on
|
|
228
|
-
// mismatch, no-op when the bundled source isn't available (standalone
|
|
229
|
-
// CLI install with no Electron resources).
|
|
230
|
-
|
|
231
|
-
export interface InstallManagedNodeOptions {
|
|
232
|
-
/**
|
|
233
|
-
* Source directory containing the bundled Node distribution.
|
|
234
|
-
* Layout matches the upstream Node zip/tar:
|
|
235
|
-
* Windows: `<dir>/node.exe`, `<dir>/npm.cmd`, `<dir>/npx.cmd`
|
|
236
|
-
* Unix: `<dir>/bin/node`, `<dir>/bin/npm`, `<dir>/bin/npx`
|
|
237
|
-
*
|
|
238
|
-
* Caller resolves this (Electron uses `path.dirname(getBundledNodePath())`
|
|
239
|
-
* after stripping the platform-specific suffix). Pass `null` /
|
|
240
|
-
* `undefined` for the standalone CLI install case — the function
|
|
241
|
-
* no-ops without error.
|
|
242
|
-
*/
|
|
243
|
-
bundledNodeDir?: string | null;
|
|
244
|
-
/** Root of the managed install. Defaults to `getManagedDir()`. */
|
|
245
|
-
managedDir?: string;
|
|
246
|
-
/** Called on every progress tick. Mirrors `bootstrapInstall` shape. */
|
|
247
|
-
progress?: ProgressCallback;
|
|
248
|
-
/**
|
|
249
|
-
* Test seam: override how the bundled Node version is read. Default
|
|
250
|
-
* spawns `<bundledNodeDir>/bin/node --version` (or `node.exe` on
|
|
251
|
-
* Windows) and trims stdout. Tests inject a fake to avoid real spawns.
|
|
252
|
-
*/
|
|
253
|
-
_readVersion?: (sourceBinary: string) => string | null;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
export interface InstallManagedNodeResult {
|
|
257
|
-
/** True iff the operation succeeded (including the no-op cases). */
|
|
258
|
-
ok: boolean;
|
|
259
|
-
/** Did we actually copy files? false when no-op or skipped. */
|
|
260
|
-
copied: boolean;
|
|
261
|
-
/** Resolved managed Node directory (`<managedDir>/node`). */
|
|
262
|
-
managedNodeDir: string;
|
|
263
|
-
/** Bundled Node version (e.g. `v22.12.0`) when known. */
|
|
264
|
-
version?: string;
|
|
265
|
-
/** Reason for skip / failure. Always set when `copied === false`. */
|
|
266
|
-
reason?: string;
|
|
267
|
-
/** Error message when `ok === false`. */
|
|
268
|
-
error?: string;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/** Path to the source `node` / `node.exe` binary inside `bundledNodeDir`. */
|
|
272
|
-
function sourceNodeBinary(bundledNodeDir: string): string {
|
|
273
|
-
return process.platform === "win32" // platform-branch-ok: Node distribution layout differs Windows vs Unix
|
|
274
|
-
? path.join(bundledNodeDir, "node.exe")
|
|
275
|
-
: path.join(bundledNodeDir, "bin", "node");
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Spawn `<nodeBinary> --version` and return the trimmed stdout (e.g.
|
|
280
|
-
* `v22.12.0`). Returns null on failure or when the binary is missing.
|
|
281
|
-
* Synchronous because bootstrap is naturally serial.
|
|
282
|
-
*/
|
|
283
|
-
function readNodeVersion(nodeBinary: string): string | null {
|
|
284
|
-
if (!existsSync(nodeBinary)) return null;
|
|
285
|
-
try {
|
|
286
|
-
const r = cpSpawnSync(nodeBinary, ["--version"], {
|
|
287
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
288
|
-
timeout: 5_000,
|
|
289
|
-
encoding: "utf-8",
|
|
290
|
-
});
|
|
291
|
-
if (r.status !== 0) return null;
|
|
292
|
-
const out = (r.stdout ?? "").toString().trim();
|
|
293
|
-
return out || null;
|
|
294
|
-
} catch {
|
|
295
|
-
return null;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/** Read the `<managedNodeDir>/.version` marker if present, else null. */
|
|
300
|
-
function readManagedMarker(managedNodeDir: string): string | null {
|
|
301
|
-
const markerPath = path.join(managedNodeDir, ".version");
|
|
302
|
-
if (!existsSync(markerPath)) return null;
|
|
303
|
-
try {
|
|
304
|
-
return readFileSync(markerPath, "utf-8").trim() || null;
|
|
305
|
-
} catch {
|
|
306
|
-
return null;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Idempotently copy the bundled Node distribution into `<managedDir>/node/`.
|
|
312
|
-
*
|
|
313
|
-
* - First-run: full recursive copy + write `.version` marker.
|
|
314
|
-
* - Re-run with matching marker: no-op.
|
|
315
|
-
* - Mismatched marker (or missing marker with dir present): replace +
|
|
316
|
-
* rewrite marker.
|
|
317
|
-
* - Bundled source absent or `bundledNodeDir == null`: no-op.
|
|
318
|
-
* - Failed copy mid-flight: marker NOT written, so next call retries.
|
|
319
|
-
*/
|
|
320
|
-
export async function installManagedNode(
|
|
321
|
-
opts: InstallManagedNodeOptions = {},
|
|
322
|
-
): Promise<InstallManagedNodeResult> {
|
|
323
|
-
const managedDir = opts.managedDir ?? getManagedDir();
|
|
324
|
-
const managedNodeDir = path.join(managedDir, "node");
|
|
325
|
-
const step = "node-runtime";
|
|
326
|
-
|
|
327
|
-
const bundledDir = opts.bundledNodeDir ?? null;
|
|
328
|
-
if (!bundledDir) {
|
|
329
|
-
return {
|
|
330
|
-
ok: true,
|
|
331
|
-
copied: false,
|
|
332
|
-
managedNodeDir,
|
|
333
|
-
reason: "no bundled source",
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const sourceBinary = sourceNodeBinary(bundledDir);
|
|
338
|
-
const sourceVersion = (opts._readVersion ?? readNodeVersion)(sourceBinary);
|
|
339
|
-
if (!sourceVersion) {
|
|
340
|
-
return {
|
|
341
|
-
ok: true,
|
|
342
|
-
copied: false,
|
|
343
|
-
managedNodeDir,
|
|
344
|
-
reason: `bundled node binary missing or unreadable: ${sourceBinary}`,
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const existingMarker = readManagedMarker(managedNodeDir);
|
|
349
|
-
if (existingMarker === sourceVersion) {
|
|
350
|
-
return {
|
|
351
|
-
ok: true,
|
|
352
|
-
copied: false,
|
|
353
|
-
managedNodeDir,
|
|
354
|
-
version: sourceVersion,
|
|
355
|
-
reason: "version matches bundled — no copy needed",
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
opts.progress?.({
|
|
360
|
-
step,
|
|
361
|
-
status: "running",
|
|
362
|
-
output: `Installing Node ${sourceVersion} runtime`,
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
try {
|
|
366
|
-
// Replace any existing dir (handles the mismatch + missing-marker
|
|
367
|
-
// cases) so the copy is from a clean slate.
|
|
368
|
-
if (existsSync(managedNodeDir)) {
|
|
369
|
-
rmSync(managedNodeDir, { recursive: true, force: true });
|
|
370
|
-
}
|
|
371
|
-
mkdirSync(path.dirname(managedNodeDir), { recursive: true });
|
|
372
|
-
|
|
373
|
-
cpSync(bundledDir, managedNodeDir, {
|
|
374
|
-
recursive: true,
|
|
375
|
-
force: true,
|
|
376
|
-
// dereference: false keeps symlinks-as-symlinks (Unix npm shim).
|
|
377
|
-
// verbatimSymlinks would also work in newer Node; default is fine.
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
// Marker last — partial copy on failure leaves no marker, so the
|
|
381
|
-
// next invocation treats the dir as missing and retries.
|
|
382
|
-
writeFileSync(
|
|
383
|
-
path.join(managedNodeDir, ".version"),
|
|
384
|
-
sourceVersion + "\n",
|
|
385
|
-
"utf-8",
|
|
386
|
-
);
|
|
387
|
-
|
|
388
|
-
opts.progress?.({ step, status: "done", output: `Node ${sourceVersion}` });
|
|
389
|
-
return {
|
|
390
|
-
ok: true,
|
|
391
|
-
copied: true,
|
|
392
|
-
managedNodeDir,
|
|
393
|
-
version: sourceVersion,
|
|
394
|
-
};
|
|
395
|
-
} catch (err) {
|
|
396
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
397
|
-
opts.progress?.({ step, status: "error", error: message });
|
|
398
|
-
return {
|
|
399
|
-
ok: false,
|
|
400
|
-
copied: false,
|
|
401
|
-
managedNodeDir,
|
|
402
|
-
version: sourceVersion,
|
|
403
|
-
error: message,
|
|
404
|
-
};
|
|
405
|
-
}
|
|
406
|
-
}
|