@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
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import type {
|
|
3
|
+
IntentNode,
|
|
4
|
+
ActionDescriptor,
|
|
5
|
+
PluginIntentsMessage,
|
|
6
|
+
PluginActionMessage,
|
|
7
|
+
} from "../dashboard-plugin/intent-types.js";
|
|
8
|
+
|
|
9
|
+
describe("intent-types — wire format", () => {
|
|
10
|
+
it("IntentNode round-trips through JSON.stringify + JSON.parse losslessly", () => {
|
|
11
|
+
const intent: IntentNode = {
|
|
12
|
+
primitive: "ui:action-list",
|
|
13
|
+
props: {
|
|
14
|
+
actions: [
|
|
15
|
+
{ label: "Run Flow X" },
|
|
16
|
+
{ label: "Run Flow Y" },
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
key: "actions-1",
|
|
20
|
+
actions: {
|
|
21
|
+
onClick: {
|
|
22
|
+
pluginId: "flows",
|
|
23
|
+
action: "flow.run",
|
|
24
|
+
payload: { flow: "X" },
|
|
25
|
+
} satisfies ActionDescriptor,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const roundTrip = JSON.parse(JSON.stringify(intent)) as IntentNode;
|
|
30
|
+
expect(roundTrip).toEqual(intent);
|
|
31
|
+
expect(roundTrip.primitive).toBe("ui:action-list");
|
|
32
|
+
expect((roundTrip.actions?.onClick as ActionDescriptor).pluginId).toBe("flows");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("nested IntentNode inside props survives round-trip", () => {
|
|
36
|
+
const inner: IntentNode = {
|
|
37
|
+
primitive: "ui:markdown",
|
|
38
|
+
props: { content: "## Hello" },
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const outer: IntentNode = {
|
|
42
|
+
primitive: "ui:agent-card",
|
|
43
|
+
props: {
|
|
44
|
+
name: "Explore",
|
|
45
|
+
status: "running",
|
|
46
|
+
body: inner,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const roundTrip = JSON.parse(JSON.stringify(outer)) as IntentNode;
|
|
51
|
+
expect(roundTrip).toEqual(outer);
|
|
52
|
+
|
|
53
|
+
// Nested IntentNode is still structurally an IntentNode after parse
|
|
54
|
+
const nestedBody = (roundTrip.props as Record<string, unknown>).body as IntentNode;
|
|
55
|
+
expect(nestedBody.primitive).toBe("ui:markdown");
|
|
56
|
+
expect((nestedBody.props as Record<string, unknown>).content).toBe("## Hello");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("PluginIntentsMessage envelope is well-typed", () => {
|
|
60
|
+
const msg: PluginIntentsMessage = {
|
|
61
|
+
type: "plugin_intents",
|
|
62
|
+
pluginId: "flows",
|
|
63
|
+
sessionId: "abc-123",
|
|
64
|
+
slot: "session-card-action-bar",
|
|
65
|
+
intent: {
|
|
66
|
+
primitive: "ui:action-list",
|
|
67
|
+
props: { actions: [] },
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const roundTrip = JSON.parse(JSON.stringify(msg)) as PluginIntentsMessage;
|
|
72
|
+
expect(roundTrip.type).toBe("plugin_intents");
|
|
73
|
+
expect(roundTrip.slot).toBe("session-card-action-bar");
|
|
74
|
+
expect(roundTrip.sessionId).toBe("abc-123");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("PluginIntentsMessage with null intent (clear semantics) round-trips", () => {
|
|
78
|
+
const msg: PluginIntentsMessage = {
|
|
79
|
+
type: "plugin_intents",
|
|
80
|
+
pluginId: "flows",
|
|
81
|
+
sessionId: "abc-123",
|
|
82
|
+
slot: "content-view",
|
|
83
|
+
intent: null,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const roundTrip = JSON.parse(JSON.stringify(msg)) as PluginIntentsMessage;
|
|
87
|
+
expect(roundTrip.intent).toBeNull();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("PluginActionMessage envelope is well-typed", () => {
|
|
91
|
+
const msg: PluginActionMessage = {
|
|
92
|
+
type: "plugin_action",
|
|
93
|
+
pluginId: "flows",
|
|
94
|
+
sessionId: "abc-123",
|
|
95
|
+
action: "flow.run",
|
|
96
|
+
payload: { flow: "X" },
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const roundTrip = JSON.parse(JSON.stringify(msg)) as PluginActionMessage;
|
|
100
|
+
expect(roundTrip.type).toBe("plugin_action");
|
|
101
|
+
expect(roundTrip.action).toBe("flow.run");
|
|
102
|
+
expect((roundTrip.payload as { flow: string }).flow).toBe("X");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("sessionId may be null for global slots", () => {
|
|
106
|
+
const msg: PluginIntentsMessage = {
|
|
107
|
+
type: "plugin_intents",
|
|
108
|
+
pluginId: "honcho",
|
|
109
|
+
sessionId: null,
|
|
110
|
+
slot: "settings-section",
|
|
111
|
+
intent: {
|
|
112
|
+
primitive: "ui:status-pill",
|
|
113
|
+
props: { state: "connected", text: "ready" },
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const roundTrip = JSON.parse(JSON.stringify(msg)) as PluginIntentsMessage;
|
|
118
|
+
expect(roundTrip.sessionId).toBeNull();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo-lint: assert `JITI_PACKAGES` is identical between the two source
|
|
3
|
+
* sites that resolve jiti at process startup.
|
|
4
|
+
*
|
|
5
|
+
* 1. `packages/shared/src/platform/binary-lookup.ts` (canonical, used
|
|
6
|
+
* by the server-launcher / electron / doctor / cli runtime path).
|
|
7
|
+
* 2. `packages/server/bin/pi-dashboard.mjs` (bin wrapper, runs before
|
|
8
|
+
* any TS loader is registered so it cannot import the canonical
|
|
9
|
+
* module and must inline the constant).
|
|
10
|
+
*
|
|
11
|
+
* If these drift, a clean-machine `npm i -g pi-dashboard && pi-dashboard`
|
|
12
|
+
* boots inconsistently: the bin wrapper might accept a jiti spec the
|
|
13
|
+
* cli.ts's daemon respawn rejects (or vice-versa). Caught us once with
|
|
14
|
+
* `@oh-my-pi/jiti` in v0.5.3.
|
|
15
|
+
*
|
|
16
|
+
* The lint is a string-parse rather than an import because the bin
|
|
17
|
+
* wrapper is ESM-not-TS and we want to fail fast even if a future move
|
|
18
|
+
* to a CJS-only environment breaks dynamic import.
|
|
19
|
+
*
|
|
20
|
+
* See change: enable-standalone-npm-install task 7.3.
|
|
21
|
+
*/
|
|
22
|
+
import { describe, it, expect } from "vitest";
|
|
23
|
+
import { readFileSync } from "node:fs";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
import url from "node:url";
|
|
26
|
+
|
|
27
|
+
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
|
28
|
+
const REPO_ROOT = path.resolve(here, "..", "..", "..", "..");
|
|
29
|
+
|
|
30
|
+
const BIN_PATH = path.join(REPO_ROOT, "packages/server/bin/pi-dashboard.mjs");
|
|
31
|
+
const SHARED_PATH = path.join(REPO_ROOT, "packages/shared/src/platform/binary-lookup.ts");
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parse a `const JITI_PACKAGES = [ ... ]` (with optional `export` /
|
|
35
|
+
* `as const`) and return the array of string literals in declared order.
|
|
36
|
+
* Returns `null` if no such declaration is found.
|
|
37
|
+
*/
|
|
38
|
+
function parseJitiPackagesArray(source: string): string[] | null {
|
|
39
|
+
// Match either:
|
|
40
|
+
// const JITI_PACKAGES = ["a", "b"];
|
|
41
|
+
// export const JITI_PACKAGES = ["a", "b"] as const;
|
|
42
|
+
const decl = source.match(
|
|
43
|
+
/(?:export\s+)?const\s+JITI_PACKAGES\s*(?::[^=]+)?=\s*\[([^\]]+)\]/,
|
|
44
|
+
);
|
|
45
|
+
if (!decl) return null;
|
|
46
|
+
const inner = decl[1]!;
|
|
47
|
+
const items: string[] = [];
|
|
48
|
+
// Pull every quoted string in order. Accepts double or single quotes.
|
|
49
|
+
for (const m of inner.matchAll(/['"]([^'"]+)['"]/g)) {
|
|
50
|
+
items.push(m[1]!);
|
|
51
|
+
}
|
|
52
|
+
return items;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe("JITI_PACKAGES parity (binary-lookup.ts ⇔ bin/pi-dashboard.mjs)", () => {
|
|
56
|
+
it("both sites declare the same array in the same order", () => {
|
|
57
|
+
const binSrc = readFileSync(BIN_PATH, "utf-8");
|
|
58
|
+
const sharedSrc = readFileSync(SHARED_PATH, "utf-8");
|
|
59
|
+
|
|
60
|
+
const binList = parseJitiPackagesArray(binSrc);
|
|
61
|
+
const sharedList = parseJitiPackagesArray(sharedSrc);
|
|
62
|
+
|
|
63
|
+
expect(binList, `JITI_PACKAGES not found in ${BIN_PATH}`).not.toBeNull();
|
|
64
|
+
expect(sharedList, `JITI_PACKAGES not found in ${SHARED_PATH}`).not.toBeNull();
|
|
65
|
+
|
|
66
|
+
expect(binList).toEqual(sharedList);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("primary lookup is bare \"jiti\" (regression for v0.5.3 fork drift)", () => {
|
|
70
|
+
const binList = parseJitiPackagesArray(readFileSync(BIN_PATH, "utf-8"));
|
|
71
|
+
const sharedList = parseJitiPackagesArray(readFileSync(SHARED_PATH, "utf-8"));
|
|
72
|
+
// Plain "jiti" is what `dependencies.jiti` in packages/server/package.json
|
|
73
|
+
// installs. It MUST be the first candidate or the bin wrapper will look up
|
|
74
|
+
// the wrong package on a clean install.
|
|
75
|
+
expect(binList?.[0]).toBe("jiti");
|
|
76
|
+
expect(sharedList?.[0]).toBe("jiti");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("does NOT contain @oh-my-pi/jiti (removed by 2026-05-08-migrate-pi-fork-to-earendil)", () => {
|
|
80
|
+
const binList = parseJitiPackagesArray(readFileSync(BIN_PATH, "utf-8"));
|
|
81
|
+
const sharedList = parseJitiPackagesArray(readFileSync(SHARED_PATH, "utf-8"));
|
|
82
|
+
expect(binList).not.toContain("@oh-my-pi/jiti");
|
|
83
|
+
expect(sharedList).not.toContain("@oh-my-pi/jiti");
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { detectLegacyManagedDir } from "../legacy-managed-dir.js";
|
|
6
|
+
|
|
7
|
+
describe("legacy-managed-dir", () => {
|
|
8
|
+
let tmpHome: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "legacy-managed-dir-test-"));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("returns present:false when directory does not exist", () => {
|
|
19
|
+
const res = detectLegacyManagedDir({ homedir: tmpHome });
|
|
20
|
+
expect(res).toEqual({ present: false });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("returns present:false when path is a file, not a directory", () => {
|
|
24
|
+
fs.writeFileSync(path.join(tmpHome, ".pi-dashboard"), "garbage");
|
|
25
|
+
const res = detectLegacyManagedDir({ homedir: tmpHome });
|
|
26
|
+
expect(res).toEqual({ present: false });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("returns present:true with zero pkgCount when dir exists but has no node_modules", () => {
|
|
30
|
+
fs.mkdirSync(path.join(tmpHome, ".pi-dashboard"), { recursive: true });
|
|
31
|
+
const res = detectLegacyManagedDir({ homedir: tmpHome });
|
|
32
|
+
expect(res.present).toBe(true);
|
|
33
|
+
if (res.present) {
|
|
34
|
+
expect(res.pkgCount).toBe(0);
|
|
35
|
+
expect(res.path).toContain(".pi-dashboard");
|
|
36
|
+
expect(res.sizeMb).toBeGreaterThanOrEqual(0);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("counts direct children under node_modules/ as pkgCount", () => {
|
|
41
|
+
const nm = path.join(tmpHome, ".pi-dashboard", "node_modules");
|
|
42
|
+
fs.mkdirSync(path.join(nm, "foo"), { recursive: true });
|
|
43
|
+
fs.mkdirSync(path.join(nm, "bar"), { recursive: true });
|
|
44
|
+
fs.mkdirSync(path.join(nm, "@scope"), { recursive: true });
|
|
45
|
+
const res = detectLegacyManagedDir({ homedir: tmpHome });
|
|
46
|
+
expect(res.present).toBe(true);
|
|
47
|
+
if (res.present) expect(res.pkgCount).toBe(3);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("computes a non-zero sizeMb when content exists", () => {
|
|
51
|
+
const dir = path.join(tmpHome, ".pi-dashboard");
|
|
52
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
53
|
+
// Write 2 MB of bytes
|
|
54
|
+
fs.writeFileSync(path.join(dir, "blob.bin"), Buffer.alloc(2 * 1024 * 1024));
|
|
55
|
+
const res = detectLegacyManagedDir({ homedir: tmpHome });
|
|
56
|
+
expect(res.present).toBe(true);
|
|
57
|
+
if (res.present) expect(res.sizeMb).toBeGreaterThanOrEqual(2);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -24,6 +24,18 @@ const ALLOWLIST: readonly string[] = [
|
|
|
24
24
|
// See change: consolidate-windows-spawn-and-platform-handlers.
|
|
25
25
|
"packages/shared/src/platform/detached-spawn.ts",
|
|
26
26
|
"packages/shared/src/platform/subprocess-adapter.ts",
|
|
27
|
+
// Legacy-pi cleanup needs a synchronous npm-root probe at server
|
|
28
|
+
// startup; predates the platform/exec wrapper. See origin commit
|
|
29
|
+
// ab711621 (feat(bootstrap): detect + one-click cleanup of legacy
|
|
30
|
+
// @mariozechner/pi-coding-agent).
|
|
31
|
+
"packages/server/src/legacy-pi-cleanup.ts",
|
|
32
|
+
// The startup recovery HTTP server runs precisely when top-level
|
|
33
|
+
// dependencies are missing (corrupted node_modules) — importing the
|
|
34
|
+
// platform/exec wrapper there would defeat the recovery flow because
|
|
35
|
+
// its transitive deps may be the very things that are missing. The
|
|
36
|
+
// file's own header explicitly mandates: "Keep it dependency-free."
|
|
37
|
+
// See change: add-startup-recovery-server (commit e606e8b0).
|
|
38
|
+
"packages/server/src/recovery-server.ts",
|
|
27
39
|
];
|
|
28
40
|
|
|
29
41
|
/**
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo-lint: Electron callers of launchDashboardServer must pass an
|
|
3
|
+
* explicit `nodeBin:` OR set `ELECTRON_RUN_AS_NODE` in the env, so the
|
|
4
|
+
* spawned process uses a real Node binary instead of the Electron GUI
|
|
5
|
+
* binary (which silently re-launches the app and exits on the
|
|
6
|
+
* single-instance lock).
|
|
7
|
+
*
|
|
8
|
+
* Also enforces that only `pick-node.ts` may reference `process.execPath`
|
|
9
|
+
* inside `packages/electron/src/lib/**`.
|
|
10
|
+
*
|
|
11
|
+
* See design D4 in openspec/changes/fix-electron-server-launch-node-bin/design.md.
|
|
12
|
+
*/
|
|
13
|
+
import { describe, it, expect } from "vitest";
|
|
14
|
+
import fs from "node:fs/promises";
|
|
15
|
+
import { Dirent } from "node:fs";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import url from "node:url";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The only file in packages/electron/src/lib/ that may reference
|
|
21
|
+
* process.execPath directly (it wraps the value inside PickNodeInput
|
|
22
|
+
* and passes it through rather than using it as a Node binary itself).
|
|
23
|
+
*/
|
|
24
|
+
const EXECPATH_ALLOWLIST = new Set(["pick-node.ts"]);
|
|
25
|
+
|
|
26
|
+
/** Walk .ts/.tsx files, excluding node_modules, dist, and __tests__. */
|
|
27
|
+
async function* walk(dir: string): AsyncGenerator<string> {
|
|
28
|
+
let entries: Dirent[];
|
|
29
|
+
try {
|
|
30
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
31
|
+
} catch {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
const full = path.join(dir, entry.name);
|
|
36
|
+
if (entry.isDirectory()) {
|
|
37
|
+
if (["node_modules", "dist", "__tests__"].includes(entry.name)) continue;
|
|
38
|
+
yield* walk(full);
|
|
39
|
+
} else if (entry.isFile() && /\.(ts|tsx|mts|cts)$/.test(entry.name)) {
|
|
40
|
+
yield full;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe("no-electron-execpath-spawn", () => {
|
|
46
|
+
it("launchDashboardServer calls in electron/lib include nodeBin or ELECTRON_RUN_AS_NODE", async () => {
|
|
47
|
+
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
|
48
|
+
const repoRoot = path.resolve(here, "..", "..", "..", "..");
|
|
49
|
+
const electronLibDir = path.join(repoRoot, "packages", "electron", "src", "lib");
|
|
50
|
+
|
|
51
|
+
const violations: Array<{ file: string; line: number; text: string }> = [];
|
|
52
|
+
|
|
53
|
+
for await (const file of walk(electronLibDir)) {
|
|
54
|
+
const content = await fs.readFile(file, "utf-8");
|
|
55
|
+
// Find all launchDashboardServer call sites. We look for the function
|
|
56
|
+
// name on a line, then scan forward for the matching closing paren to
|
|
57
|
+
// extract the options object text.
|
|
58
|
+
const lines = content.split(/\r?\n/);
|
|
59
|
+
for (let i = 0; i < lines.length; i++) {
|
|
60
|
+
const line = lines[i]!;
|
|
61
|
+
if (!line.includes("launchDashboardServer(")) continue;
|
|
62
|
+
|
|
63
|
+
// Collect the call body: from this line to the matching closing paren.
|
|
64
|
+
let depth = 0;
|
|
65
|
+
let body = "";
|
|
66
|
+
for (let j = i; j < lines.length; j++) {
|
|
67
|
+
const l = lines[j]!;
|
|
68
|
+
body += l + "\n";
|
|
69
|
+
for (const ch of l) {
|
|
70
|
+
if (ch === "(") depth++;
|
|
71
|
+
else if (ch === ")") depth--;
|
|
72
|
+
}
|
|
73
|
+
if (depth === 0 && body.includes("launchDashboardServer(")) break;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const hasNodeBin = /\bnodeBin\s*:/.test(body);
|
|
77
|
+
const hasElectronRunAsNode = /ELECTRON_RUN_AS_NODE/.test(body);
|
|
78
|
+
|
|
79
|
+
if (!hasNodeBin && !hasElectronRunAsNode) {
|
|
80
|
+
violations.push({
|
|
81
|
+
file: path.relative(repoRoot, file),
|
|
82
|
+
line: i + 1,
|
|
83
|
+
text: line.trim(),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (violations.length > 0) {
|
|
90
|
+
const msg =
|
|
91
|
+
`launchDashboardServer() called from Electron lib without nodeBin: or ELECTRON_RUN_AS_NODE.\n` +
|
|
92
|
+
`In Electron, process.execPath is the GUI binary — spawning it without ELECTRON_RUN_AS_NODE=1\n` +
|
|
93
|
+
`re-launches the app, hits the single-instance lock, and exits silently.\n\n` +
|
|
94
|
+
`Fix: call pickNodeForServer() and pass nodeBin: pick.nodeBin.\n` +
|
|
95
|
+
`See design D4: openspec/changes/fix-electron-server-launch-node-bin/design.md\n\n` +
|
|
96
|
+
`Offenders (${violations.length}):\n` +
|
|
97
|
+
violations.map((v) => ` ${v.file}:${v.line} ${v.text}`).join("\n");
|
|
98
|
+
expect(violations, msg).toHaveLength(0);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("process.execPath not used directly as a node binary in electron/lib (outside pick-node.ts)", async () => {
|
|
103
|
+
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
|
104
|
+
const repoRoot = path.resolve(here, "..", "..", "..", "..");
|
|
105
|
+
const electronLibDir = path.join(repoRoot, "packages", "electron", "src", "lib");
|
|
106
|
+
|
|
107
|
+
// Pattern: process.execPath used as a binary path assignment (not as a
|
|
108
|
+
// processExecPath: injection key which is the permitted picker-call pattern).
|
|
109
|
+
// Flagged: nodeBin = ... ?? process.execPath
|
|
110
|
+
// Flagged: nodeBin: process.execPath
|
|
111
|
+
// Allowed: processExecPath: process.execPath (picker input)
|
|
112
|
+
// Allowed: string interpolation / log messages
|
|
113
|
+
const BINARY_EXECPATH_RE = /(?:nodeBin|cmd|bin)\s*(?:=|:)[^,;\n]*\bprocess\.execPath\b|\?\?\s*process\.execPath/;
|
|
114
|
+
// Safe patterns that should not be flagged even if they match above
|
|
115
|
+
const SAFE_RE = /processExecPath\s*:\s*process\.execPath/;
|
|
116
|
+
|
|
117
|
+
const violations: Array<{ file: string; line: number; text: string }> = [];
|
|
118
|
+
|
|
119
|
+
for await (const file of walk(electronLibDir)) {
|
|
120
|
+
const basename = path.basename(file);
|
|
121
|
+
if (EXECPATH_ALLOWLIST.has(basename)) continue;
|
|
122
|
+
|
|
123
|
+
const content = await fs.readFile(file, "utf-8");
|
|
124
|
+
const lines = content.split(/\r?\n/);
|
|
125
|
+
for (let i = 0; i < lines.length; i++) {
|
|
126
|
+
const line = lines[i]!;
|
|
127
|
+
if (BINARY_EXECPATH_RE.test(line) && !SAFE_RE.test(line)) {
|
|
128
|
+
violations.push({
|
|
129
|
+
file: path.relative(repoRoot, file),
|
|
130
|
+
line: i + 1,
|
|
131
|
+
text: line.trim(),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (violations.length > 0) {
|
|
138
|
+
const msg =
|
|
139
|
+
`process.execPath used directly as a Node binary in packages/electron/src/lib.\n` +
|
|
140
|
+
`In Electron main, process.execPath is the GUI binary, not a Node interpreter.\n` +
|
|
141
|
+
`Use pickNodeForServer() from pick-node.ts to select the correct binary.\n\n` +
|
|
142
|
+
`Allowed file: ${[...EXECPATH_ALLOWLIST].join(", ")}\n` +
|
|
143
|
+
`Allowed pattern: processExecPath: process.execPath (picker injection)\n\n` +
|
|
144
|
+
`Offenders (${violations.length}):\n` +
|
|
145
|
+
violations.map((v) => ` ${v.file}:${v.line} ${v.text}`).join("\n");
|
|
146
|
+
expect(violations, msg).toHaveLength(0);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo-lint: dashboard plugins MUST NOT claim `command-route` for any
|
|
3
|
+
* `/flows*` command. Flow operations in the dashboard are button-driven
|
|
4
|
+
* (`SessionFlowActions`, `FlowDashboard` Abort, `FlowLaunchDialog`); the
|
|
5
|
+
* pi-flows extension itself still registers the slash commands for TUI.
|
|
6
|
+
*
|
|
7
|
+
* See change: fix-pi-flows-end-to-end (Group 8, task 8.5).
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect } from "vitest";
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const PACKAGES_DIR = path.resolve(__dirname, "..", "..", "..", "..");
|
|
16
|
+
|
|
17
|
+
interface PluginClaim {
|
|
18
|
+
slot?: string;
|
|
19
|
+
command?: string;
|
|
20
|
+
component?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ManifestSlice {
|
|
24
|
+
id?: string;
|
|
25
|
+
claims?: PluginClaim[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readPluginManifests(): Array<{ pkg: string; manifest: ManifestSlice }> {
|
|
29
|
+
const out: Array<{ pkg: string; manifest: ManifestSlice }> = [];
|
|
30
|
+
for (const entry of fs.readdirSync(PACKAGES_DIR, { withFileTypes: true })) {
|
|
31
|
+
if (!entry.isDirectory()) continue;
|
|
32
|
+
const pkgJsonPath = path.join(PACKAGES_DIR, entry.name, "package.json");
|
|
33
|
+
if (!fs.existsSync(pkgJsonPath)) continue;
|
|
34
|
+
let raw: string;
|
|
35
|
+
try {
|
|
36
|
+
raw = fs.readFileSync(pkgJsonPath, "utf-8");
|
|
37
|
+
} catch {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
let parsed: { "pi-dashboard-plugin"?: ManifestSlice };
|
|
41
|
+
try {
|
|
42
|
+
parsed = JSON.parse(raw);
|
|
43
|
+
} catch {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const m = parsed["pi-dashboard-plugin"];
|
|
47
|
+
if (m) out.push({ pkg: entry.name, manifest: m });
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("repo-lint: no dashboard plugin claims command-route for /flows*", () => {
|
|
53
|
+
it("every monorepo plugin manifest is free of /flows* command-route claims", () => {
|
|
54
|
+
const offenders: string[] = [];
|
|
55
|
+
for (const { pkg, manifest } of readPluginManifests()) {
|
|
56
|
+
for (const claim of manifest.claims ?? []) {
|
|
57
|
+
if (claim.slot !== "command-route") continue;
|
|
58
|
+
if (!claim.command || !claim.command.startsWith("/flows")) continue;
|
|
59
|
+
offenders.push(`${pkg}: command "${claim.command}" → component "${claim.component ?? "?"}"`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (offenders.length > 0) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Found dashboard plugin command-route claims for /flows*:\n${offenders.map((o) => " " + o).join("\n")}\n\n` +
|
|
65
|
+
"Dashboard flow operations are button-driven (SessionFlowActions / FlowDashboard / FlowLaunchDialog). " +
|
|
66
|
+
"pi-flows itself still registers /flows* commands for TUI use; the dashboard plugin manifest must not.",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
expect(offenders).toEqual([]);
|
|
70
|
+
});
|
|
71
|
+
});
|