@blackbelt-technology/pi-agent-dashboard 0.4.0 → 0.4.2
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 +104 -35
- package/README.md +390 -494
- package/docs/architecture.md +423 -20
- package/package.json +11 -8
- package/packages/extension/package.json +11 -4
- package/packages/extension/src/__tests__/ask-user-schema-discriminator.test.ts +141 -0
- package/packages/extension/src/__tests__/ask-user-tool.test.ts +91 -15
- package/packages/extension/src/__tests__/bridge-entry-id-pi-070.test.ts +174 -0
- package/packages/extension/src/__tests__/event-forwarder.test.ts +30 -0
- package/packages/extension/src/__tests__/fork-entryid-timing.test.ts +64 -76
- package/packages/extension/src/__tests__/multiselect-dashboard-routing.test.ts +203 -0
- package/packages/extension/src/__tests__/multiselect-list.test.ts +137 -0
- package/packages/extension/src/__tests__/multiselect-polyfill.test.ts +92 -0
- package/packages/extension/src/__tests__/no-session-replacement-calls.test.ts +99 -0
- package/packages/extension/src/__tests__/no-tui-multiselect-arm-regression.test.ts +81 -0
- package/packages/extension/src/__tests__/openspec-activity-detector.test.ts +37 -0
- package/packages/extension/src/__tests__/ui-decorators.test.ts +309 -0
- package/packages/extension/src/__tests__/ui-modules.test.ts +293 -0
- package/packages/extension/src/ask-user-tool.ts +170 -61
- package/packages/extension/src/bridge.ts +199 -19
- package/packages/extension/src/multiselect-decode.ts +40 -0
- package/packages/extension/src/multiselect-list.ts +146 -0
- package/packages/extension/src/multiselect-polyfill.ts +73 -0
- package/packages/extension/src/server-launcher.ts +15 -3
- package/packages/extension/src/ui-modules.ts +272 -0
- package/packages/server/package.json +11 -5
- package/packages/server/src/__tests__/auto-attach.test.ts +61 -8
- package/packages/server/src/__tests__/browse-endpoint.test.ts +295 -19
- package/packages/server/src/__tests__/cli-bootstrap.test.ts +36 -0
- package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +163 -0
- package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +315 -0
- package/packages/server/src/__tests__/directory-service-toctou.test.ts +303 -0
- package/packages/server/src/__tests__/directory-service.test.ts +174 -0
- package/packages/server/src/__tests__/fixtures/fork-jsonl-roundtrip.jsonl +8 -0
- package/packages/server/src/__tests__/fork-jsonl-roundtrip.test.ts +49 -0
- package/packages/server/src/__tests__/installed-package-enricher.test.ts +225 -0
- package/packages/server/src/__tests__/package-manager-wrapper-move.test.ts +414 -0
- package/packages/server/src/__tests__/package-routes.test.ts +136 -3
- package/packages/server/src/__tests__/package-source-helpers.test.ts +101 -0
- package/packages/server/src/__tests__/pending-attach-registry.test.ts +123 -0
- package/packages/server/src/__tests__/pending-resume-intent-registry.test.ts +138 -0
- package/packages/server/src/__tests__/pi-core-checker.test.ts +73 -30
- package/packages/server/src/__tests__/pi-gateway-consume-pending-attach.test.ts +112 -0
- package/packages/server/src/__tests__/pi-version-skew.test.ts +72 -0
- package/packages/server/src/__tests__/post-install-openspec-refresh.test.ts +180 -0
- package/packages/server/src/__tests__/post-install-rescan.test.ts +134 -0
- package/packages/server/src/__tests__/proposal-attach-naming.test.ts +79 -0
- package/packages/server/src/__tests__/restart-helper.test.ts +34 -6
- package/packages/server/src/__tests__/session-action-handler-spawn-with-attach.test.ts +108 -0
- package/packages/server/src/__tests__/session-order-manager.test.ts +55 -0
- package/packages/server/src/__tests__/session-order-reboot.test.ts +242 -0
- package/packages/server/src/__tests__/session-scanner.test.ts +44 -0
- package/packages/server/src/__tests__/subscription-handler.test.ts +40 -0
- package/packages/server/src/__tests__/translate-path-source.test.ts +77 -0
- package/packages/server/src/__tests__/ui-decorators-replay.test.ts +209 -0
- package/packages/server/src/__tests__/ui-modules-replay.test.ts +221 -0
- package/packages/server/src/browse.ts +118 -13
- package/packages/server/src/browser-gateway.ts +19 -0
- package/packages/server/src/browser-handlers/__tests__/session-meta-handler.test.ts +183 -0
- package/packages/server/src/browser-handlers/directory-handler.ts +7 -1
- package/packages/server/src/browser-handlers/handler-context.ts +15 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +29 -3
- package/packages/server/src/browser-handlers/session-meta-handler.ts +46 -12
- package/packages/server/src/browser-handlers/subscription-handler.ts +46 -1
- package/packages/server/src/cli.ts +61 -15
- package/packages/server/src/directory-service.ts +156 -15
- package/packages/server/src/event-wiring.ts +111 -10
- package/packages/server/src/installed-package-enricher.ts +143 -0
- package/packages/server/src/package-manager-wrapper.ts +305 -8
- package/packages/server/src/package-source-helpers.ts +104 -0
- package/packages/server/src/pending-attach-registry.ts +112 -0
- package/packages/server/src/pending-resume-intent-registry.ts +107 -0
- package/packages/server/src/pi-core-checker.ts +9 -14
- package/packages/server/src/pi-gateway.ts +14 -0
- package/packages/server/src/pi-version-skew.ts +12 -1
- package/packages/server/src/proposal-attach-naming.ts +47 -0
- package/packages/server/src/restart-helper.ts +13 -2
- package/packages/server/src/routes/file-routes.ts +29 -3
- package/packages/server/src/routes/package-routes.ts +72 -3
- package/packages/server/src/routes/plugin-config-routes.ts +129 -0
- package/packages/server/src/routes/system-routes.ts +2 -0
- package/packages/server/src/server.ts +339 -10
- package/packages/server/src/session-api.ts +30 -5
- package/packages/server/src/session-order-manager.ts +22 -0
- package/packages/server/src/session-scanner.ts +10 -1
- package/packages/shared/package.json +9 -2
- package/packages/shared/src/__tests__/browser-protocol-types.test.ts +59 -0
- package/packages/shared/src/__tests__/config-plugins.test.ts +68 -0
- package/packages/shared/src/__tests__/extension-ui-module-shape.test.ts +265 -0
- package/packages/shared/src/__tests__/no-hardcoded-node-modules-paths.test.ts +176 -0
- package/packages/shared/src/__tests__/no-raw-node-import.test.ts +146 -0
- package/packages/shared/src/__tests__/no-raw-openspec-status-in-skills.test.ts +81 -0
- package/packages/shared/src/__tests__/node-spawn.test.ts +210 -0
- package/packages/shared/src/__tests__/openspec-design-evidence.test.ts +288 -0
- package/packages/shared/src/__tests__/openspec-effective-status-script.test.ts +174 -0
- package/packages/shared/src/__tests__/openspec-poller-design-override.test.ts +225 -0
- package/packages/shared/src/__tests__/openspec-poller-specs-override.test.ts +284 -0
- package/packages/shared/src/__tests__/openspec-specs-evidence.test.ts +144 -0
- package/packages/shared/src/__tests__/platform/is-appimage-self-hit.test.ts +164 -0
- package/packages/shared/src/__tests__/plugin-bridge-register-extended.test.ts +72 -0
- package/packages/shared/src/__tests__/plugin-bridge-register.test.ts +113 -0
- package/packages/shared/src/__tests__/plugin-config-update-protocol.test.ts +41 -0
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +5 -1
- package/packages/shared/src/__tests__/resolve-tool-cli.test.ts +105 -0
- package/packages/shared/src/__tests__/spawn-session-attach-proposal.test.ts +47 -0
- package/packages/shared/src/__tests__/state-replay-entry-id.test.ts +69 -0
- package/packages/shared/src/__tests__/tool-registry-strategies-appimage.test.ts +118 -0
- package/packages/shared/src/browser-protocol.ts +110 -4
- package/packages/shared/src/config.ts +45 -0
- package/packages/shared/src/dashboard-plugin/index.ts +11 -0
- package/packages/shared/src/dashboard-plugin/manifest-types.ts +58 -0
- package/packages/shared/src/dashboard-plugin/plugin-status.ts +26 -0
- package/packages/shared/src/dashboard-plugin/slot-props.ts +92 -0
- package/packages/shared/src/dashboard-plugin/slot-types.ts +151 -0
- package/packages/shared/src/openspec-activity-detector.ts +18 -22
- package/packages/shared/src/openspec-design-evidence.ts +109 -0
- package/packages/shared/src/openspec-poller.ts +117 -3
- package/packages/shared/src/openspec-specs-evidence.ts +79 -0
- package/packages/shared/src/platform/binary-lookup.ts +96 -1
- package/packages/shared/src/platform/index.ts +1 -0
- package/packages/shared/src/platform/node-spawn.ts +154 -0
- package/packages/shared/src/plugin-bridge-register.ts +139 -0
- package/packages/shared/src/protocol.ts +79 -2
- package/packages/shared/src/recommended-extensions.ts +7 -1
- package/packages/shared/src/rest-api.ts +68 -3
- package/packages/shared/src/state-replay.ts +20 -1
- package/packages/shared/src/tool-registry/definitions.ts +92 -0
- package/packages/shared/src/tool-registry/strategies.ts +17 -3
- package/packages/shared/src/types.ts +160 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo-level invariant: any source file that passes an argv to Node
|
|
3
|
+
* with `--import` or `--loader` MUST wrap the following positions
|
|
4
|
+
* (loader and entry script) in `file://` URLs via `toFileUrl(...)` or
|
|
5
|
+
* `pathToFileURL(...).href`. Raw OS paths on Windows drives whose
|
|
6
|
+
* letter collides with URL-scheme parsing (e.g. `B:\`) crash Node with
|
|
7
|
+
* `ERR_UNSUPPORTED_ESM_URL_SCHEME`.
|
|
8
|
+
*
|
|
9
|
+
* If this test fails, migrate the offending file to use
|
|
10
|
+
* `spawnNodeScript` or wrap the entry/loader with `toFileUrl` from
|
|
11
|
+
* `@blackbelt-technology/pi-dashboard-shared/platform/node-spawn.js`.
|
|
12
|
+
*
|
|
13
|
+
* See change: fix-windows-entry-script-url.
|
|
14
|
+
*/
|
|
15
|
+
import { describe, it, expect } from "vitest";
|
|
16
|
+
import fs from "node:fs/promises";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import url from "node:url";
|
|
19
|
+
|
|
20
|
+
/** Files allowed to reference --import / --loader with raw identifiers. */
|
|
21
|
+
const ALLOWLIST: readonly string[] = [
|
|
22
|
+
"packages/shared/src/platform/node-spawn.ts",
|
|
23
|
+
// resolve-jiti.ts returns a file:// URL to callers; it does not itself
|
|
24
|
+
// build a `["--import", X, Y]` argv. Allowlisted as the documented
|
|
25
|
+
// source of loader URLs referenced in server spawn call sites.
|
|
26
|
+
"packages/shared/src/resolve-jiti.ts",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/** Per-line opt-out for intentional usages (e.g. comment examples). */
|
|
30
|
+
const OPT_OUT_MARKER = "ban:raw-node-import-ok";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Detect argv arrays containing `"--import"` or `"--loader"` followed by
|
|
34
|
+
* a bare identifier (not a string literal and not a wrapped call).
|
|
35
|
+
*
|
|
36
|
+
* We match the argv-literal shape:
|
|
37
|
+
* ["--import", X, Y]
|
|
38
|
+
* args: ["--import", X, Y, ...]
|
|
39
|
+
*
|
|
40
|
+
* Then check that both X and Y are either:
|
|
41
|
+
* - a string literal starting with "file:" (already a URL)
|
|
42
|
+
* - a call expression to toFileUrl(...) or pathToFileURL(...).href
|
|
43
|
+
* - the identifier resolveJitiImport() / resolveJitiFromAnchor() (which
|
|
44
|
+
* are documented to return file:// URLs — allowlisted by name)
|
|
45
|
+
*
|
|
46
|
+
* Anything else is flagged.
|
|
47
|
+
*/
|
|
48
|
+
const IMPORT_ARGV_RE =
|
|
49
|
+
/["']--(?:import|loader)["']\s*,\s*([^,\]]+?)\s*,\s*([^,\]]+?)(?:\s*,|\s*\])/g;
|
|
50
|
+
|
|
51
|
+
const URL_LOOKING_RE =
|
|
52
|
+
/^(?:["']file:|toFileUrl\s*\(|pathToFileURL\s*\([^)]*\)\s*\.href|resolveJitiImport\s*\(|resolveJitiFromAnchor\s*\()/;
|
|
53
|
+
|
|
54
|
+
/** Recursively walk a directory, yielding .ts / .tsx files. */
|
|
55
|
+
async function* walk(dir: string): AsyncGenerator<string> {
|
|
56
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
const full = path.join(dir, entry.name);
|
|
59
|
+
if (entry.isDirectory()) {
|
|
60
|
+
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "__tests__") continue;
|
|
61
|
+
yield* walk(full);
|
|
62
|
+
} else if (entry.isFile() && /\.(ts|tsx|mts|cts)$/.test(entry.name)) {
|
|
63
|
+
yield full;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe("no raw paths passed to node --import / --loader", () => {
|
|
69
|
+
it("only URL-wrapped or allowlisted argv positions follow --import / --loader", async () => {
|
|
70
|
+
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
|
71
|
+
const repoRoot = path.resolve(here, "..", "..", "..", "..");
|
|
72
|
+
const packagesDir = path.resolve(repoRoot, "packages");
|
|
73
|
+
|
|
74
|
+
const allowSet = new Set(
|
|
75
|
+
ALLOWLIST.map((p) => path.resolve(repoRoot, p).replace(/\\/g, "/")),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const violations: Array<{ file: string; line: number; text: string }> = [];
|
|
79
|
+
|
|
80
|
+
for (const pkg of await fs.readdir(packagesDir, { withFileTypes: true })) {
|
|
81
|
+
if (!pkg.isDirectory()) continue;
|
|
82
|
+
const srcDir = path.join(packagesDir, pkg.name, "src");
|
|
83
|
+
try {
|
|
84
|
+
await fs.access(srcDir);
|
|
85
|
+
} catch {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
for await (const file of walk(srcDir)) {
|
|
89
|
+
const normalized = file.replace(/\\/g, "/");
|
|
90
|
+
if (allowSet.has(normalized)) continue;
|
|
91
|
+
|
|
92
|
+
const content = await fs.readFile(file, "utf-8");
|
|
93
|
+
const lines = content.split(/\r?\n/);
|
|
94
|
+
|
|
95
|
+
// Walk each line and check for the argv pattern. Track byte
|
|
96
|
+
// offsets so we can compute line numbers for multi-line matches.
|
|
97
|
+
let offset = 0;
|
|
98
|
+
for (let i = 0; i < lines.length; i++) {
|
|
99
|
+
const line = lines[i]!;
|
|
100
|
+
// Fast path: only inspect lines that mention --import or --loader.
|
|
101
|
+
if (!line.includes("--import") && !line.includes("--loader")) {
|
|
102
|
+
offset += line.length + 1;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (line.includes(OPT_OUT_MARKER)) {
|
|
106
|
+
offset += line.length + 1;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
// Check the current line alone (we allow argv to be on one line;
|
|
110
|
+
// multi-line argv arrays are a rare style and would still trip
|
|
111
|
+
// the quick search above).
|
|
112
|
+
IMPORT_ARGV_RE.lastIndex = 0;
|
|
113
|
+
let m: RegExpExecArray | null;
|
|
114
|
+
while ((m = IMPORT_ARGV_RE.exec(line)) !== null) {
|
|
115
|
+
const loaderArg = m[1]!.trim();
|
|
116
|
+
const entryArg = m[2]!.trim();
|
|
117
|
+
const loaderOk = URL_LOOKING_RE.test(loaderArg);
|
|
118
|
+
const entryOk = URL_LOOKING_RE.test(entryArg);
|
|
119
|
+
if (!loaderOk || !entryOk) {
|
|
120
|
+
violations.push({
|
|
121
|
+
file: path.relative(repoRoot, file),
|
|
122
|
+
line: i + 1,
|
|
123
|
+
text: line.trim(),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
offset += line.length + 1;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (violations.length > 0) {
|
|
133
|
+
const msg =
|
|
134
|
+
`Raw filesystem paths passed to node --import / --loader found.\n` +
|
|
135
|
+
`Migrate each call site to use spawnNodeScript() or wrap the\n` +
|
|
136
|
+
`loader/entry with toFileUrl(...) from:\n` +
|
|
137
|
+
` import { toFileUrl, spawnNodeScript } from\n` +
|
|
138
|
+
` "@blackbelt-technology/pi-dashboard-shared/platform/node-spawn.js";\n\n` +
|
|
139
|
+
`Offenders (${violations.length}):\n` +
|
|
140
|
+
violations
|
|
141
|
+
.map((v) => ` ${v.file}:${v.line} ${v.text}`)
|
|
142
|
+
.join("\n");
|
|
143
|
+
expect(violations, msg).toEqual([]);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo-level invariant: OpenSpec workflow skills (.pi/skills/openspec-*)
|
|
3
|
+
* MUST invoke `.pi/skills/openspec-shared/scripts/effective-status.sh`
|
|
4
|
+
* instead of calling `openspec status --json` directly. The wrapper
|
|
5
|
+
* applies the dashboard's local-design-evidence override (R1/R2/R3) so
|
|
6
|
+
* skill-driven prompts and dashboard session-card buttons cannot
|
|
7
|
+
* disagree about a change's next-ready artifact.
|
|
8
|
+
*
|
|
9
|
+
* If this test fails: replace the offending `openspec status ... --json`
|
|
10
|
+
* line with:
|
|
11
|
+
*
|
|
12
|
+
* .pi/skills/openspec-shared/scripts/effective-status.sh "<name>"
|
|
13
|
+
*
|
|
14
|
+
* See change: fix-openspec-design-detection.
|
|
15
|
+
*/
|
|
16
|
+
import { describe, it, expect } from "vitest";
|
|
17
|
+
import fs from "node:fs/promises";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import url from "node:url";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Regex catches `openspec status ... --json` invocations. We deliberately
|
|
23
|
+
* accept whitespace flexibility but reject only the `--json` flavor (the
|
|
24
|
+
* human-readable `openspec status --change "<name>"` form is allowed
|
|
25
|
+
* because it doesn't drive logic).
|
|
26
|
+
*/
|
|
27
|
+
const RAW_STATUS_RE = /\bopenspec\s+status\b[^\n]*--json\b/;
|
|
28
|
+
|
|
29
|
+
/** Per-line opt-out marker. */
|
|
30
|
+
const OPT_OUT_MARKER = "ban:openspec-status-ok";
|
|
31
|
+
|
|
32
|
+
/** Skills the wrapper exists to serve — these MUST go through it. */
|
|
33
|
+
const GOVERNED_SKILLS = [
|
|
34
|
+
"openspec-continue-change",
|
|
35
|
+
"openspec-ff-change",
|
|
36
|
+
"openspec-apply-change",
|
|
37
|
+
"openspec-verify-change",
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
describe("OpenSpec workflow skills must use effective-status.sh", () => {
|
|
41
|
+
it("no raw `openspec status --json` outside the wrapper script", async () => {
|
|
42
|
+
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
|
43
|
+
const repoRoot = path.resolve(here, "..", "..", "..", "..");
|
|
44
|
+
const skillsRoot = path.resolve(repoRoot, ".pi", "skills");
|
|
45
|
+
|
|
46
|
+
const violations: Array<{ file: string; line: number; text: string }> = [];
|
|
47
|
+
|
|
48
|
+
for (const skill of GOVERNED_SKILLS) {
|
|
49
|
+
const skillFile = path.join(skillsRoot, skill, "SKILL.md");
|
|
50
|
+
let content: string;
|
|
51
|
+
try {
|
|
52
|
+
content = await fs.readFile(skillFile, "utf-8");
|
|
53
|
+
} catch {
|
|
54
|
+
// Skill not present in this checkout — fine, skip.
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const lines = content.split(/\r?\n/);
|
|
58
|
+
lines.forEach((line, idx) => {
|
|
59
|
+
if (!RAW_STATUS_RE.test(line)) return;
|
|
60
|
+
if (line.includes(OPT_OUT_MARKER)) return;
|
|
61
|
+
violations.push({
|
|
62
|
+
file: path.relative(repoRoot, skillFile),
|
|
63
|
+
line: idx + 1,
|
|
64
|
+
text: line.trim(),
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (violations.length > 0) {
|
|
70
|
+
const msg =
|
|
71
|
+
`Raw \`openspec status --json\` invocations found in OpenSpec skills.\n` +
|
|
72
|
+
`Replace each with the wrapper that applies the dashboard's design override:\n` +
|
|
73
|
+
` .pi/skills/openspec-shared/scripts/effective-status.sh "<name>"\n\n` +
|
|
74
|
+
`Offenders (${violations.length}):\n` +
|
|
75
|
+
violations
|
|
76
|
+
.map((v) => ` ${v.file}:${v.line} ${v.text}`)
|
|
77
|
+
.join("\n");
|
|
78
|
+
expect(violations, msg).toEqual([]);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `platform/node-spawn.ts` — the canonical helper for
|
|
3
|
+
* constructing `node --import <loader> <entry>` argv.
|
|
4
|
+
*
|
|
5
|
+
* See change: fix-windows-entry-script-url.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, vi } from "vitest";
|
|
8
|
+
import { toFileUrl, spawnNodeScript, isTsxLoader, shouldUrlWrapEntry } from "../platform/node-spawn.js";
|
|
9
|
+
import * as execModule from "../platform/exec.js";
|
|
10
|
+
|
|
11
|
+
describe("toFileUrl", () => {
|
|
12
|
+
it("returns a file:// URL input unchanged (idempotent)", () => {
|
|
13
|
+
expect(toFileUrl("file:///C:/foo.ts")).toBe("file:///C:/foo.ts");
|
|
14
|
+
expect(toFileUrl("file:///usr/local/bin/cli.js")).toBe("file:///usr/local/bin/cli.js");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("wraps Windows B:\\ drive-letter paths on any host OS", () => {
|
|
18
|
+
expect(toFileUrl("B:\\Dev\\cli.ts")).toBe("file:///B:/Dev/cli.ts");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("wraps Windows C:\\ drive-letter paths on any host OS", () => {
|
|
22
|
+
expect(toFileUrl("C:\\Users\\x\\cli.ts")).toBe("file:///C:/Users/x/cli.ts");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("wraps forward-slash Windows paths", () => {
|
|
26
|
+
expect(toFileUrl("B:/Dev/cli.ts")).toBe("file:///B:/Dev/cli.ts");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("wraps POSIX absolute paths", () => {
|
|
30
|
+
expect(toFileUrl("/usr/local/bin/cli.js")).toBe("file:///usr/local/bin/cli.js");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("handles uppercase and lowercase drive letters identically", () => {
|
|
34
|
+
expect(toFileUrl("b:\\Dev\\cli.ts")).toBe("file:///b:/Dev/cli.ts");
|
|
35
|
+
expect(toFileUrl("Z:\\foo.ts")).toBe("file:///Z:/foo.ts");
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("isTsxLoader", () => {
|
|
40
|
+
it("returns true for loader URLs containing /tsx/", () => {
|
|
41
|
+
expect(isTsxLoader("file:///home/x/node_modules/tsx/dist/esm/index.mjs")).toBe(true);
|
|
42
|
+
expect(isTsxLoader("file:///C:/project/node_modules/tsx/dist/esm/index.mjs")).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("returns true for raw paths with /tsx/ segment", () => {
|
|
46
|
+
expect(isTsxLoader("/home/x/node_modules/tsx/dist/esm/index.mjs")).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("returns true for Windows raw paths with \\tsx\\ segment", () => {
|
|
50
|
+
expect(isTsxLoader("C:\\project\\node_modules\\tsx\\dist\\esm\\index.mjs")).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("returns false for jiti loader", () => {
|
|
54
|
+
expect(isTsxLoader("file:///home/x/node_modules/@mariozechner/jiti/lib/jiti-register.mjs")).toBe(false);
|
|
55
|
+
expect(isTsxLoader("/home/x/node_modules/@mariozechner/jiti/lib/jiti-register.mjs")).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("returns false for undefined / empty", () => {
|
|
59
|
+
expect(isTsxLoader(undefined)).toBe(false);
|
|
60
|
+
expect(isTsxLoader("")).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("spawnNodeScript", () => {
|
|
65
|
+
it("URL-wraps both loader and entry when loader is NOT tsx (jiti/default)", () => {
|
|
66
|
+
const spawnSpy = vi
|
|
67
|
+
.spyOn(execModule, "spawn")
|
|
68
|
+
.mockImplementation(() => ({ unref: () => {} } as unknown as ReturnType<typeof execModule.spawn>));
|
|
69
|
+
|
|
70
|
+
spawnNodeScript({
|
|
71
|
+
nodeBin: "C:\\Program Files\\nodejs\\node.exe",
|
|
72
|
+
loader: "B:\\jiti\\register.mjs",
|
|
73
|
+
entry: "B:\\Dev\\cli.ts",
|
|
74
|
+
args: ["start", "--dev"],
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(spawnSpy).toHaveBeenCalledTimes(1);
|
|
78
|
+
const [bin, argv] = spawnSpy.mock.calls[0]!;
|
|
79
|
+
expect(bin).toBe("C:\\Program Files\\nodejs\\node.exe");
|
|
80
|
+
// On Linux host: entry stays raw even with a Windows-styled path
|
|
81
|
+
// (shouldUrlWrapEntry consults process.platform, which is Linux).
|
|
82
|
+
// The Windows-wrapped branch is exercised separately via shouldUrlWrapEntry.
|
|
83
|
+
expect(argv).toEqual([
|
|
84
|
+
"--import",
|
|
85
|
+
"file:///B:/jiti/register.mjs",
|
|
86
|
+
"B:\\Dev\\cli.ts",
|
|
87
|
+
"start",
|
|
88
|
+
"--dev",
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
spawnSpy.mockRestore();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("URL-wraps loader but passes RAW entry when loader is tsx", () => {
|
|
95
|
+
const spawnSpy = vi
|
|
96
|
+
.spyOn(execModule, "spawn")
|
|
97
|
+
.mockImplementation(() => ({ unref: () => {} } as unknown as ReturnType<typeof execModule.spawn>));
|
|
98
|
+
|
|
99
|
+
spawnNodeScript({
|
|
100
|
+
nodeBin: "/usr/bin/node",
|
|
101
|
+
loader: "/home/u/node_modules/tsx/dist/esm/index.mjs",
|
|
102
|
+
entry: "/home/u/repo/packages/server/src/cli.ts",
|
|
103
|
+
args: ["start"],
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const [, argv] = spawnSpy.mock.calls[0]!;
|
|
107
|
+
expect(argv).toEqual([
|
|
108
|
+
"--import",
|
|
109
|
+
"file:///home/u/node_modules/tsx/dist/esm/index.mjs",
|
|
110
|
+
"/home/u/repo/packages/server/src/cli.ts", // RAW, not URL
|
|
111
|
+
"start",
|
|
112
|
+
]);
|
|
113
|
+
spawnSpy.mockRestore();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("defaults nodeBin to process.execPath when omitted", () => {
|
|
117
|
+
const spawnSpy = vi
|
|
118
|
+
.spyOn(execModule, "spawn")
|
|
119
|
+
.mockImplementation(() => ({ unref: () => {} } as unknown as ReturnType<typeof execModule.spawn>));
|
|
120
|
+
|
|
121
|
+
spawnNodeScript({
|
|
122
|
+
entry: "/usr/local/cli.ts",
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const [bin] = spawnSpy.mock.calls[0]!;
|
|
126
|
+
expect(bin).toBe(process.execPath);
|
|
127
|
+
spawnSpy.mockRestore();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("omits --import when no loader is provided", () => {
|
|
131
|
+
const spawnSpy = vi
|
|
132
|
+
.spyOn(execModule, "spawn")
|
|
133
|
+
.mockImplementation(() => ({ unref: () => {} } as unknown as ReturnType<typeof execModule.spawn>));
|
|
134
|
+
|
|
135
|
+
spawnNodeScript({
|
|
136
|
+
entry: "B:\\Dev\\cli.ts",
|
|
137
|
+
args: ["help"],
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const [, argv] = spawnSpy.mock.calls[0]!;
|
|
141
|
+
// No loader → shouldUrlWrapEntry returns false on Linux host → raw entry.
|
|
142
|
+
expect(argv).toEqual(["B:\\Dev\\cli.ts", "help"]);
|
|
143
|
+
spawnSpy.mockRestore();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("passes spawnOptions through to exec.spawn unchanged", () => {
|
|
147
|
+
const spawnSpy = vi
|
|
148
|
+
.spyOn(execModule, "spawn")
|
|
149
|
+
.mockImplementation(() => ({ unref: () => {} } as unknown as ReturnType<typeof execModule.spawn>));
|
|
150
|
+
|
|
151
|
+
const opts = { detached: true, stdio: ["ignore", 1, 2] as ("ignore" | number)[], env: { FOO: "bar" } };
|
|
152
|
+
spawnNodeScript({
|
|
153
|
+
entry: "/usr/local/cli.ts",
|
|
154
|
+
spawnOptions: opts,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const [, , passedOpts] = spawnSpy.mock.calls[0]!;
|
|
158
|
+
expect(passedOpts).toBe(opts);
|
|
159
|
+
spawnSpy.mockRestore();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("accepts a loader that is already a file:// URL without double-wrapping", () => {
|
|
163
|
+
const spawnSpy = vi
|
|
164
|
+
.spyOn(execModule, "spawn")
|
|
165
|
+
.mockImplementation(() => ({ unref: () => {} } as unknown as ReturnType<typeof execModule.spawn>));
|
|
166
|
+
|
|
167
|
+
spawnNodeScript({
|
|
168
|
+
loader: "file:///C:/jiti/register.mjs",
|
|
169
|
+
entry: "B:\\Dev\\cli.ts",
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const [, argv] = spawnSpy.mock.calls[0]!;
|
|
173
|
+
// On Linux host with non-tsx loader: entry stays raw.
|
|
174
|
+
expect(argv).toEqual([
|
|
175
|
+
"--import",
|
|
176
|
+
"file:///C:/jiti/register.mjs",
|
|
177
|
+
"B:\\Dev\\cli.ts",
|
|
178
|
+
]);
|
|
179
|
+
spawnSpy.mockRestore();
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("shouldUrlWrapEntry", () => {
|
|
184
|
+
it("returns false for tsx loader on any platform", () => {
|
|
185
|
+
const tsxLoader = "file:///home/u/node_modules/tsx/dist/esm/index.mjs";
|
|
186
|
+
expect(shouldUrlWrapEntry(tsxLoader, "linux")).toBe(false);
|
|
187
|
+
expect(shouldUrlWrapEntry(tsxLoader, "darwin")).toBe(false);
|
|
188
|
+
expect(shouldUrlWrapEntry(tsxLoader, "win32")).toBe(false);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("returns false for non-tsx loader on POSIX (jiti MUST get raw entry)", () => {
|
|
192
|
+
const jiti = "file:///home/u/node_modules/@mariozechner/jiti/lib/jiti-register.mjs";
|
|
193
|
+
expect(shouldUrlWrapEntry(jiti, "linux")).toBe(false);
|
|
194
|
+
expect(shouldUrlWrapEntry(jiti, "darwin")).toBe(false);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("returns true for non-tsx loader on Windows (drive letters need file://)", () => {
|
|
198
|
+
const jiti = "file:///C:/node_modules/@mariozechner/jiti/lib/jiti-register.mjs";
|
|
199
|
+
expect(shouldUrlWrapEntry(jiti, "win32")).toBe(true);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("returns false when no loader is provided, regardless of platform", () => {
|
|
203
|
+
// Without a loader, Node's default resolver handles the entry; the URL
|
|
204
|
+
// wrap was historically used for Windows drive-letter collision, but
|
|
205
|
+
// if we were to spawn without a loader we'd still default to raw on POSIX.
|
|
206
|
+
// On Windows without a loader, callers should wrap themselves.
|
|
207
|
+
expect(shouldUrlWrapEntry(undefined, "linux")).toBe(false);
|
|
208
|
+
expect(shouldUrlWrapEntry(undefined, "darwin")).toBe(false);
|
|
209
|
+
});
|
|
210
|
+
});
|