fix(web): resolve compiled .js modules for all subprocess calls under node_modules (#2320)
Node v24 unconditionally refuses .ts files under node_modules/ — even with --experimental-transform-types. When GSD is installed globally via npm, every web service subprocess that loads a .ts extension module crashes with ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING. Add resolveSubprocessModule() and buildSubprocessPrefixArgs() to ts-subprocess-flags.ts. When packageRoot is under node_modules/ and the compiled dist/*.js file exists, subprocess calls use the compiled JS directly without TS flags or the resolve-ts.mjs loader. Updated all 14 web service files: auto-dashboard, bridge, captures, cleanup, doctor, export, forensics, history, hooks, recovery-diagnostics, settings, skill-health, undo, and visualizer. Fixes #2279 Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a0c0896a75
commit
7a413bb84f
16 changed files with 459 additions and 202 deletions
157
src/tests/web-subprocess-module-resolution.test.ts
Normal file
157
src/tests/web-subprocess-module-resolution.test.ts
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import test from "node:test"
|
||||
import assert from "node:assert/strict"
|
||||
import { join } from "node:path"
|
||||
|
||||
import {
|
||||
isUnderNodeModules,
|
||||
resolveSubprocessModule,
|
||||
} from "../web/ts-subprocess-flags.ts"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// isUnderNodeModules — exported utility
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("isUnderNodeModules returns false for paths outside node_modules", () => {
|
||||
assert.equal(isUnderNodeModules("/home/user/projects/gsd"), false)
|
||||
})
|
||||
|
||||
test("isUnderNodeModules returns true for Unix paths under node_modules/", () => {
|
||||
assert.equal(
|
||||
isUnderNodeModules("/usr/lib/node_modules/gsd-pi"),
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
test("isUnderNodeModules returns true for Windows paths under node_modules/", () => {
|
||||
assert.equal(
|
||||
isUnderNodeModules("C:\\Users\\dev\\AppData\\node_modules\\gsd-pi"),
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
test("isUnderNodeModules returns false for substring match without trailing slash", () => {
|
||||
assert.equal(
|
||||
isUnderNodeModules("/home/user/my_node_modules_backup/gsd"),
|
||||
false,
|
||||
)
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// resolveSubprocessModule — resolves .ts → dist .js under node_modules
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("resolveSubprocessModule returns source .ts path when NOT under node_modules", () => {
|
||||
const packageRoot = "/home/user/projects/gsd"
|
||||
const result = resolveSubprocessModule(
|
||||
packageRoot,
|
||||
"resources/extensions/gsd/workspace-index.ts",
|
||||
// existsSync not needed — should return src path without checking dist
|
||||
)
|
||||
|
||||
assert.deepEqual(result, {
|
||||
modulePath: join(packageRoot, "src", "resources/extensions/gsd/workspace-index.ts"),
|
||||
useCompiledJs: false,
|
||||
})
|
||||
})
|
||||
|
||||
test("resolveSubprocessModule returns compiled .js path when under node_modules and dist file exists", () => {
|
||||
const packageRoot = "/usr/lib/node_modules/gsd-pi"
|
||||
const distPath = join(packageRoot, "dist", "resources/extensions/gsd/workspace-index.js")
|
||||
const result = resolveSubprocessModule(
|
||||
packageRoot,
|
||||
"resources/extensions/gsd/workspace-index.ts",
|
||||
(p: string) => p === distPath,
|
||||
)
|
||||
|
||||
assert.deepEqual(result, {
|
||||
modulePath: distPath,
|
||||
useCompiledJs: true,
|
||||
})
|
||||
})
|
||||
|
||||
test("resolveSubprocessModule falls back to source .ts when under node_modules but dist file missing", () => {
|
||||
const packageRoot = "/usr/lib/node_modules/gsd-pi"
|
||||
const result = resolveSubprocessModule(
|
||||
packageRoot,
|
||||
"resources/extensions/gsd/workspace-index.ts",
|
||||
() => false, // dist file does not exist
|
||||
)
|
||||
|
||||
assert.deepEqual(result, {
|
||||
modulePath: join(packageRoot, "src", "resources/extensions/gsd/workspace-index.ts"),
|
||||
useCompiledJs: false,
|
||||
})
|
||||
})
|
||||
|
||||
test("resolveSubprocessModule handles Windows paths under node_modules", () => {
|
||||
const packageRoot = "C:\\Users\\dev\\AppData\\node_modules\\gsd-pi"
|
||||
const distPath = join(packageRoot, "dist", "resources/extensions/gsd/auto.js")
|
||||
const result = resolveSubprocessModule(
|
||||
packageRoot,
|
||||
"resources/extensions/gsd/auto.ts",
|
||||
(p: string) => p === distPath,
|
||||
)
|
||||
|
||||
assert.deepEqual(result, {
|
||||
modulePath: distPath,
|
||||
useCompiledJs: true,
|
||||
})
|
||||
})
|
||||
|
||||
test("resolveSubprocessModule strips .ts extension when building dist .js path", () => {
|
||||
const packageRoot = "/usr/lib/node_modules/gsd-pi"
|
||||
let checkedPath = ""
|
||||
resolveSubprocessModule(
|
||||
packageRoot,
|
||||
"resources/extensions/gsd/doctor.ts",
|
||||
(p: string) => { checkedPath = p; return true },
|
||||
)
|
||||
|
||||
assert.equal(
|
||||
checkedPath,
|
||||
join(packageRoot, "dist", "resources/extensions/gsd/doctor.js"),
|
||||
"should check for .js file in dist/, not .ts",
|
||||
)
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Integration: bridge-service subprocess resolution pattern
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("bridge-service workspace-index subprocess uses compiled JS when under node_modules (source audit)", async () => {
|
||||
// Verify bridge-service.ts calls resolveSubprocessModule for workspace-index
|
||||
const { readFileSync } = await import("node:fs")
|
||||
const bridgeSource = readFileSync(
|
||||
join(process.cwd(), "src", "web", "bridge-service.ts"),
|
||||
"utf-8",
|
||||
)
|
||||
|
||||
assert.match(
|
||||
bridgeSource,
|
||||
/resolveSubprocessModule/,
|
||||
"bridge-service.ts must use resolveSubprocessModule to resolve workspace-index path — " +
|
||||
"hardcoded .ts paths fail with ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING on Node v24 (see #2279)",
|
||||
)
|
||||
})
|
||||
|
||||
test("all web service files use resolveSubprocessModule instead of hardcoded .ts paths (source audit)", async () => {
|
||||
const { readFileSync, readdirSync } = await import("node:fs")
|
||||
|
||||
const serviceFiles = readdirSync(join(process.cwd(), "src", "web"))
|
||||
.filter((f: string) => f.endsWith("-service.ts"))
|
||||
|
||||
for (const file of serviceFiles) {
|
||||
const source = readFileSync(join(process.cwd(), "src", "web", file), "utf-8")
|
||||
|
||||
// If the service file imports resolveTypeStrippingFlag it spawns subprocesses
|
||||
// and must also use resolveSubprocessModule
|
||||
if (source.includes("resolveTypeStrippingFlag")) {
|
||||
assert.match(
|
||||
source,
|
||||
/resolveSubprocessModule/,
|
||||
`${file} uses resolveTypeStrippingFlag but does not use resolveSubprocessModule — ` +
|
||||
"subprocess .ts paths will fail under node_modules/ on Node v24 (#2279)",
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -4,7 +4,7 @@ import { join } from "node:path";
|
|||
import { pathToFileURL } from "node:url";
|
||||
|
||||
import type { AutoDashboardData } from "./bridge-service.ts";
|
||||
import { resolveTypeStrippingFlag } from "./ts-subprocess-flags.ts"
|
||||
import { resolveTypeStrippingFlag, resolveSubprocessModule, buildSubprocessPrefixArgs } from "./ts-subprocess-flags.ts"
|
||||
|
||||
const AUTO_DASHBOARD_MAX_BUFFER = 1024 * 1024;
|
||||
const TEST_AUTO_DASHBOARD_MODULE_ENV = "GSD_WEB_TEST_AUTO_DASHBOARD_MODULE";
|
||||
|
|
@ -32,10 +32,6 @@ function fallbackAutoDashboardData(): AutoDashboardData {
|
|||
};
|
||||
}
|
||||
|
||||
function resolveAutoDashboardModulePath(packageRoot: string, env: NodeJS.ProcessEnv): string {
|
||||
return env[TEST_AUTO_DASHBOARD_MODULE_ENV] || join(packageRoot, "src", "resources", "extensions", "gsd", "auto.ts");
|
||||
}
|
||||
|
||||
function resolveTsLoaderPath(packageRoot: string): string {
|
||||
return join(packageRoot, "src", "resources", "extensions", "gsd", "tests", "resolve-ts.mjs");
|
||||
}
|
||||
|
|
@ -55,11 +51,20 @@ export async function collectAuthoritativeAutoDashboardData(
|
|||
|
||||
const checkExists = options.existsSync ?? existsSync;
|
||||
const resolveTsLoader = resolveTsLoaderPath(packageRoot);
|
||||
const autoModulePath = resolveAutoDashboardModulePath(packageRoot, env);
|
||||
|
||||
if (!checkExists(resolveTsLoader) || !checkExists(autoModulePath)) {
|
||||
// Use test override if provided; otherwise resolve via resolveSubprocessModule
|
||||
const testModulePath = env[TEST_AUTO_DASHBOARD_MODULE_ENV];
|
||||
const moduleResolution = testModulePath
|
||||
? { modulePath: testModulePath, useCompiledJs: false }
|
||||
: resolveSubprocessModule(packageRoot, "resources/extensions/gsd/auto.ts", checkExists);
|
||||
const autoModulePath = moduleResolution.modulePath;
|
||||
|
||||
if (!moduleResolution.useCompiledJs && (!checkExists(resolveTsLoader) || !checkExists(autoModulePath))) {
|
||||
throw new Error(`authoritative auto dashboard provider not found; checked=${resolveTsLoader},${autoModulePath}`);
|
||||
}
|
||||
if (moduleResolution.useCompiledJs && !checkExists(autoModulePath)) {
|
||||
throw new Error(`authoritative auto dashboard provider not found; checked=${autoModulePath}`);
|
||||
}
|
||||
|
||||
const script = [
|
||||
'const { pathToFileURL } = await import("node:url");',
|
||||
|
|
@ -68,14 +73,17 @@ export async function collectAuthoritativeAutoDashboardData(
|
|||
'process.stdout.write(JSON.stringify(result));',
|
||||
].join(" ");
|
||||
|
||||
const prefixArgs = buildSubprocessPrefixArgs(
|
||||
packageRoot,
|
||||
moduleResolution,
|
||||
pathToFileURL(resolveTsLoader).href,
|
||||
);
|
||||
|
||||
return await new Promise<AutoDashboardData>((resolveResult, reject) => {
|
||||
execFile(
|
||||
options.execPath ?? process.execPath,
|
||||
[
|
||||
"--import",
|
||||
pathToFileURL(resolveTsLoader).href,
|
||||
resolveTypeStrippingFlag(packageRoot),
|
||||
"--input-type=module",
|
||||
...prefixArgs,
|
||||
"--eval",
|
||||
script,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { StringDecoder } from "node:string_decoder";
|
|||
import type { Readable } from "node:stream";
|
||||
import { join, resolve, dirname } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { resolveTypeStrippingFlag } from "./ts-subprocess-flags.ts";
|
||||
import { resolveTypeStrippingFlag, resolveSubprocessModule, buildSubprocessPrefixArgs } from "./ts-subprocess-flags.ts";
|
||||
|
||||
import type { AgentSessionEvent, SessionStateChangeReason } from "../../packages/pi-coding-agent/src/core/agent-session.ts";
|
||||
import type {
|
||||
|
|
@ -905,12 +905,20 @@ async function loadCachedWorkspaceIndex(
|
|||
|
||||
async function loadWorkspaceIndexViaChildProcess(basePath: string, packageRoot: string): Promise<GSDWorkspaceIndex> {
|
||||
const deps = getBridgeDeps();
|
||||
const resolveTsLoader = join(packageRoot, "src", "resources", "extensions", "gsd", "tests", "resolve-ts.mjs");
|
||||
const workspaceModulePath = join(packageRoot, "src", "resources", "extensions", "gsd", "workspace-index.ts");
|
||||
const checkExists = deps.existsSync ?? existsSync;
|
||||
if (!checkExists(resolveTsLoader) || !checkExists(workspaceModulePath)) {
|
||||
const resolveTsLoader = join(packageRoot, "src", "resources", "extensions", "gsd", "tests", "resolve-ts.mjs");
|
||||
const moduleResolution = resolveSubprocessModule(
|
||||
packageRoot,
|
||||
"resources/extensions/gsd/workspace-index.ts",
|
||||
checkExists,
|
||||
);
|
||||
const workspaceModulePath = moduleResolution.modulePath;
|
||||
if (!moduleResolution.useCompiledJs && (!checkExists(resolveTsLoader) || !checkExists(workspaceModulePath))) {
|
||||
throw new Error(`workspace index loader not found; checked=${resolveTsLoader},${workspaceModulePath}`);
|
||||
}
|
||||
if (moduleResolution.useCompiledJs && !checkExists(workspaceModulePath)) {
|
||||
throw new Error(`workspace index module not found; checked=${workspaceModulePath}`);
|
||||
}
|
||||
|
||||
const script = [
|
||||
'const { pathToFileURL } = await import("node:url");',
|
||||
|
|
@ -919,14 +927,17 @@ async function loadWorkspaceIndexViaChildProcess(basePath: string, packageRoot:
|
|||
'process.stdout.write(JSON.stringify(result));',
|
||||
].join(' ');
|
||||
|
||||
const prefixArgs = buildSubprocessPrefixArgs(
|
||||
packageRoot,
|
||||
moduleResolution,
|
||||
pathToFileURL(resolveTsLoader).href,
|
||||
);
|
||||
|
||||
return await new Promise<GSDWorkspaceIndex>((resolveResult, reject) => {
|
||||
execFile(
|
||||
deps.execPath ?? process.execPath,
|
||||
[
|
||||
"--import",
|
||||
pathToFileURL(resolveTsLoader).href,
|
||||
resolveTypeStrippingFlag(packageRoot),
|
||||
"--input-type=module",
|
||||
...prefixArgs,
|
||||
"--eval",
|
||||
script,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -4,16 +4,12 @@ import { join } from "node:path"
|
|||
import { pathToFileURL } from "node:url"
|
||||
|
||||
import { resolveBridgeRuntimeConfig } from "./bridge-service.ts"
|
||||
import { resolveTypeStrippingFlag } from "./ts-subprocess-flags.ts"
|
||||
import { resolveTypeStrippingFlag, resolveSubprocessModule, buildSubprocessPrefixArgs } from "./ts-subprocess-flags.ts"
|
||||
import type { CapturesData, CaptureResolveRequest, CaptureResolveResult } from "../../web/lib/knowledge-captures-types.ts"
|
||||
|
||||
const CAPTURES_MAX_BUFFER = 2 * 1024 * 1024
|
||||
const CAPTURES_MODULE_ENV = "GSD_CAPTURES_MODULE"
|
||||
|
||||
function resolveCapturesModulePath(packageRoot: string): string {
|
||||
return join(packageRoot, "src", "resources", "extensions", "gsd", "captures.ts")
|
||||
}
|
||||
|
||||
function resolveTsLoaderPath(packageRoot: string): string {
|
||||
return join(packageRoot, "src", "resources", "extensions", "gsd", "tests", "resolve-ts.mjs")
|
||||
}
|
||||
|
|
@ -28,13 +24,17 @@ export async function collectCapturesData(projectCwdOverride?: string): Promise<
|
|||
const { packageRoot, projectCwd } = config
|
||||
|
||||
const resolveTsLoader = resolveTsLoaderPath(packageRoot)
|
||||
const capturesModulePath = resolveCapturesModulePath(packageRoot)
|
||||
const moduleResolution = resolveSubprocessModule(packageRoot, "resources/extensions/gsd/captures.ts")
|
||||
const capturesModulePath = moduleResolution.modulePath
|
||||
|
||||
if (!existsSync(resolveTsLoader) || !existsSync(capturesModulePath)) {
|
||||
if (!moduleResolution.useCompiledJs && (!existsSync(resolveTsLoader) || !existsSync(capturesModulePath))) {
|
||||
throw new Error(
|
||||
`captures data provider not found; checked=${resolveTsLoader},${capturesModulePath}`,
|
||||
)
|
||||
}
|
||||
if (moduleResolution.useCompiledJs && !existsSync(capturesModulePath)) {
|
||||
throw new Error(`captures data provider not found; checked=${capturesModulePath}`)
|
||||
}
|
||||
|
||||
const script = [
|
||||
'const { pathToFileURL } = await import("node:url");',
|
||||
|
|
@ -46,14 +46,13 @@ export async function collectCapturesData(projectCwdOverride?: string): Promise<
|
|||
'process.stdout.write(JSON.stringify(result));',
|
||||
].join(" ")
|
||||
|
||||
const prefixArgs = buildSubprocessPrefixArgs(packageRoot, moduleResolution, pathToFileURL(resolveTsLoader).href)
|
||||
|
||||
return await new Promise<CapturesData>((resolveResult, reject) => {
|
||||
execFile(
|
||||
process.execPath,
|
||||
[
|
||||
"--import",
|
||||
pathToFileURL(resolveTsLoader).href,
|
||||
resolveTypeStrippingFlag(packageRoot),
|
||||
"--input-type=module",
|
||||
...prefixArgs,
|
||||
"--eval",
|
||||
script,
|
||||
],
|
||||
|
|
@ -95,13 +94,17 @@ export async function resolveCaptureAction(request: CaptureResolveRequest, proje
|
|||
const { packageRoot, projectCwd } = config
|
||||
|
||||
const resolveTsLoader = resolveTsLoaderPath(packageRoot)
|
||||
const capturesModulePath = resolveCapturesModulePath(packageRoot)
|
||||
const moduleResolution = resolveSubprocessModule(packageRoot, "resources/extensions/gsd/captures.ts")
|
||||
const capturesModulePath = moduleResolution.modulePath
|
||||
|
||||
if (!existsSync(resolveTsLoader) || !existsSync(capturesModulePath)) {
|
||||
if (!moduleResolution.useCompiledJs && (!existsSync(resolveTsLoader) || !existsSync(capturesModulePath))) {
|
||||
throw new Error(
|
||||
`captures data provider not found; checked=${resolveTsLoader},${capturesModulePath}`,
|
||||
)
|
||||
}
|
||||
if (moduleResolution.useCompiledJs && !existsSync(capturesModulePath)) {
|
||||
throw new Error(`captures data provider not found; checked=${capturesModulePath}`)
|
||||
}
|
||||
|
||||
const safeId = JSON.stringify(request.captureId)
|
||||
const safeClassification = JSON.stringify(request.classification)
|
||||
|
|
@ -115,14 +118,13 @@ export async function resolveCaptureAction(request: CaptureResolveRequest, proje
|
|||
`process.stdout.write(JSON.stringify({ ok: true, captureId: ${safeId} }));`,
|
||||
].join(" ")
|
||||
|
||||
const prefixArgs = buildSubprocessPrefixArgs(packageRoot, moduleResolution, pathToFileURL(resolveTsLoader).href)
|
||||
|
||||
return await new Promise<CaptureResolveResult>((resolveResult, reject) => {
|
||||
execFile(
|
||||
process.execPath,
|
||||
[
|
||||
"--import",
|
||||
pathToFileURL(resolveTsLoader).href,
|
||||
resolveTypeStrippingFlag(packageRoot),
|
||||
"--input-type=module",
|
||||
...prefixArgs,
|
||||
"--eval",
|
||||
script,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -4,16 +4,12 @@ import { join } from "node:path"
|
|||
import { pathToFileURL } from "node:url"
|
||||
|
||||
import { resolveBridgeRuntimeConfig } from "./bridge-service.ts"
|
||||
import { resolveTypeStrippingFlag } from "./ts-subprocess-flags.ts"
|
||||
import { resolveTypeStrippingFlag, resolveSubprocessModule, buildSubprocessPrefixArgs } from "./ts-subprocess-flags.ts"
|
||||
import type { CleanupData, CleanupResult } from "../../web/lib/remaining-command-types.ts"
|
||||
|
||||
const CLEANUP_MAX_BUFFER = 2 * 1024 * 1024
|
||||
const CLEANUP_MODULE_ENV = "GSD_CLEANUP_MODULE"
|
||||
|
||||
function resolveCleanupModulePath(packageRoot: string): string {
|
||||
return join(packageRoot, "src", "resources", "extensions", "gsd", "native-git-bridge.ts")
|
||||
}
|
||||
|
||||
function resolveTsLoaderPath(packageRoot: string): string {
|
||||
return join(packageRoot, "src", "resources", "extensions", "gsd", "tests", "resolve-ts.mjs")
|
||||
}
|
||||
|
|
@ -28,13 +24,17 @@ export async function collectCleanupData(projectCwdOverride?: string): Promise<C
|
|||
const { packageRoot, projectCwd } = config
|
||||
|
||||
const resolveTsLoader = resolveTsLoaderPath(packageRoot)
|
||||
const cleanupModulePath = resolveCleanupModulePath(packageRoot)
|
||||
const moduleResolution = resolveSubprocessModule(packageRoot, "resources/extensions/gsd/native-git-bridge.ts")
|
||||
const cleanupModulePath = moduleResolution.modulePath
|
||||
|
||||
if (!existsSync(resolveTsLoader) || !existsSync(cleanupModulePath)) {
|
||||
if (!moduleResolution.useCompiledJs && (!existsSync(resolveTsLoader) || !existsSync(cleanupModulePath))) {
|
||||
throw new Error(
|
||||
`cleanup data provider not found; checked=${resolveTsLoader},${cleanupModulePath}`,
|
||||
)
|
||||
}
|
||||
if (moduleResolution.useCompiledJs && !existsSync(cleanupModulePath)) {
|
||||
throw new Error(`cleanup data provider not found; checked=${cleanupModulePath}`)
|
||||
}
|
||||
|
||||
const script = [
|
||||
'const { pathToFileURL } = await import("node:url");',
|
||||
|
|
@ -60,14 +60,13 @@ export async function collectCleanupData(projectCwdOverride?: string): Promise<C
|
|||
'process.stdout.write(JSON.stringify({ branches: branchList, snapshots: snapshotList }));',
|
||||
].join(" ")
|
||||
|
||||
const prefixArgs = buildSubprocessPrefixArgs(packageRoot, moduleResolution, pathToFileURL(resolveTsLoader).href)
|
||||
|
||||
return await new Promise<CleanupData>((resolveResult, reject) => {
|
||||
execFile(
|
||||
process.execPath,
|
||||
[
|
||||
"--import",
|
||||
pathToFileURL(resolveTsLoader).href,
|
||||
resolveTypeStrippingFlag(packageRoot),
|
||||
"--input-type=module",
|
||||
...prefixArgs,
|
||||
"--eval",
|
||||
script,
|
||||
],
|
||||
|
|
@ -114,13 +113,17 @@ export async function executeCleanup(
|
|||
const { packageRoot, projectCwd } = config
|
||||
|
||||
const resolveTsLoader = resolveTsLoaderPath(packageRoot)
|
||||
const cleanupModulePath = resolveCleanupModulePath(packageRoot)
|
||||
const moduleResolution = resolveSubprocessModule(packageRoot, "resources/extensions/gsd/native-git-bridge.ts")
|
||||
const cleanupModulePath = moduleResolution.modulePath
|
||||
|
||||
if (!existsSync(resolveTsLoader) || !existsSync(cleanupModulePath)) {
|
||||
if (!moduleResolution.useCompiledJs && (!existsSync(resolveTsLoader) || !existsSync(cleanupModulePath))) {
|
||||
throw new Error(
|
||||
`cleanup service modules not found; checked=${resolveTsLoader},${cleanupModulePath}`,
|
||||
)
|
||||
}
|
||||
if (moduleResolution.useCompiledJs && !existsSync(cleanupModulePath)) {
|
||||
throw new Error(`cleanup service modules not found; checked=${cleanupModulePath}`)
|
||||
}
|
||||
|
||||
const script = [
|
||||
'const { pathToFileURL } = await import("node:url");',
|
||||
|
|
@ -147,14 +150,13 @@ export async function executeCleanup(
|
|||
'process.stdout.write(JSON.stringify({ deletedBranches, prunedSnapshots, message }));',
|
||||
].join(" ")
|
||||
|
||||
const prefixArgs = buildSubprocessPrefixArgs(packageRoot, moduleResolution, pathToFileURL(resolveTsLoader).href)
|
||||
|
||||
return await new Promise<CleanupResult>((resolveResult, reject) => {
|
||||
execFile(
|
||||
process.execPath,
|
||||
[
|
||||
"--import",
|
||||
pathToFileURL(resolveTsLoader).href,
|
||||
resolveTypeStrippingFlag(packageRoot),
|
||||
"--input-type=module",
|
||||
...prefixArgs,
|
||||
"--eval",
|
||||
script,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -4,47 +4,31 @@ import { join } from "node:path"
|
|||
import { pathToFileURL } from "node:url"
|
||||
|
||||
import { resolveBridgeRuntimeConfig } from "./bridge-service.ts"
|
||||
import { resolveTypeStrippingFlag } from "./ts-subprocess-flags.ts"
|
||||
import { resolveTypeStrippingFlag, resolveSubprocessModule, buildSubprocessPrefixArgs } from "./ts-subprocess-flags.ts"
|
||||
import type { DoctorReport, DoctorFixResult } from "../../web/lib/diagnostics-types.ts"
|
||||
|
||||
const DOCTOR_MAX_BUFFER = 2 * 1024 * 1024
|
||||
const DOCTOR_MODULE_ENV = "GSD_DOCTOR_MODULE"
|
||||
|
||||
function resolveDoctorModulePath(packageRoot: string): string {
|
||||
return join(packageRoot, "src", "resources", "extensions", "gsd", "doctor.ts")
|
||||
}
|
||||
|
||||
function resolveTsLoaderPath(packageRoot: string): string {
|
||||
return join(packageRoot, "src", "resources", "extensions", "gsd", "tests", "resolve-ts.mjs")
|
||||
}
|
||||
|
||||
function validateModulePaths(
|
||||
resolveTsLoader: string,
|
||||
doctorModulePath: string,
|
||||
): void {
|
||||
if (!existsSync(resolveTsLoader) || !existsSync(doctorModulePath)) {
|
||||
throw new Error(
|
||||
`doctor data provider not found; checked=${resolveTsLoader},${doctorModulePath}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function runDoctorChild(
|
||||
packageRoot: string,
|
||||
projectCwd: string,
|
||||
script: string,
|
||||
resolveTsLoader: string,
|
||||
doctorModulePath: string,
|
||||
moduleResolution: { modulePath: string; useCompiledJs: boolean },
|
||||
scope?: string,
|
||||
): Promise<string> {
|
||||
const prefixArgs = buildSubprocessPrefixArgs(packageRoot, moduleResolution, pathToFileURL(resolveTsLoader).href)
|
||||
return new Promise<string>((resolveResult, reject) => {
|
||||
execFile(
|
||||
process.execPath,
|
||||
[
|
||||
"--import",
|
||||
pathToFileURL(resolveTsLoader).href,
|
||||
resolveTypeStrippingFlag(packageRoot),
|
||||
"--input-type=module",
|
||||
...prefixArgs,
|
||||
"--eval",
|
||||
script,
|
||||
],
|
||||
|
|
@ -78,8 +62,17 @@ export async function collectDoctorData(scope?: string, projectCwdOverride?: str
|
|||
const { packageRoot, projectCwd } = config
|
||||
|
||||
const resolveTsLoader = resolveTsLoaderPath(packageRoot)
|
||||
const doctorModulePath = resolveDoctorModulePath(packageRoot)
|
||||
validateModulePaths(resolveTsLoader, doctorModulePath)
|
||||
const moduleResolution = resolveSubprocessModule(packageRoot, "resources/extensions/gsd/doctor.ts")
|
||||
const doctorModulePath = moduleResolution.modulePath
|
||||
|
||||
if (!moduleResolution.useCompiledJs && (!existsSync(resolveTsLoader) || !existsSync(doctorModulePath))) {
|
||||
throw new Error(
|
||||
`doctor data provider not found; checked=${resolveTsLoader},${doctorModulePath}`,
|
||||
)
|
||||
}
|
||||
if (moduleResolution.useCompiledJs && !existsSync(doctorModulePath)) {
|
||||
throw new Error(`doctor data provider not found; checked=${doctorModulePath}`)
|
||||
}
|
||||
|
||||
const script = [
|
||||
'const { pathToFileURL } = await import("node:url");',
|
||||
|
|
@ -98,7 +91,7 @@ export async function collectDoctorData(scope?: string, projectCwdOverride?: str
|
|||
].join(" ")
|
||||
|
||||
const stdout = await runDoctorChild(
|
||||
packageRoot, projectCwd, script, resolveTsLoader, doctorModulePath, scope,
|
||||
packageRoot, projectCwd, script, resolveTsLoader, doctorModulePath, moduleResolution, scope,
|
||||
)
|
||||
|
||||
try {
|
||||
|
|
@ -119,8 +112,17 @@ export async function applyDoctorFixes(scope?: string, projectCwdOverride?: stri
|
|||
const { packageRoot, projectCwd } = config
|
||||
|
||||
const resolveTsLoader = resolveTsLoaderPath(packageRoot)
|
||||
const doctorModulePath = resolveDoctorModulePath(packageRoot)
|
||||
validateModulePaths(resolveTsLoader, doctorModulePath)
|
||||
const moduleResolution = resolveSubprocessModule(packageRoot, "resources/extensions/gsd/doctor.ts")
|
||||
const doctorModulePath = moduleResolution.modulePath
|
||||
|
||||
if (!moduleResolution.useCompiledJs && (!existsSync(resolveTsLoader) || !existsSync(doctorModulePath))) {
|
||||
throw new Error(
|
||||
`doctor data provider not found; checked=${resolveTsLoader},${doctorModulePath}`,
|
||||
)
|
||||
}
|
||||
if (moduleResolution.useCompiledJs && !existsSync(doctorModulePath)) {
|
||||
throw new Error(`doctor data provider not found; checked=${doctorModulePath}`)
|
||||
}
|
||||
|
||||
const script = [
|
||||
'const { pathToFileURL } = await import("node:url");',
|
||||
|
|
@ -136,7 +138,7 @@ export async function applyDoctorFixes(scope?: string, projectCwdOverride?: stri
|
|||
].join(" ")
|
||||
|
||||
const stdout = await runDoctorChild(
|
||||
packageRoot, projectCwd, script, resolveTsLoader, doctorModulePath, scope,
|
||||
packageRoot, projectCwd, script, resolveTsLoader, doctorModulePath, moduleResolution, scope,
|
||||
)
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -4,16 +4,12 @@ import { join } from "node:path"
|
|||
import { pathToFileURL } from "node:url"
|
||||
|
||||
import { resolveBridgeRuntimeConfig } from "./bridge-service.ts"
|
||||
import { resolveTypeStrippingFlag } from "./ts-subprocess-flags.ts"
|
||||
import { resolveTypeStrippingFlag, resolveSubprocessModule, buildSubprocessPrefixArgs } from "./ts-subprocess-flags.ts"
|
||||
import type { ExportResult } from "../../web/lib/remaining-command-types.ts"
|
||||
|
||||
const EXPORT_MAX_BUFFER = 4 * 1024 * 1024
|
||||
const EXPORT_MODULE_ENV = "GSD_EXPORT_MODULE"
|
||||
|
||||
function resolveExportModulePath(packageRoot: string): string {
|
||||
return join(packageRoot, "src", "resources", "extensions", "gsd", "export.ts")
|
||||
}
|
||||
|
||||
function resolveTsLoaderPath(packageRoot: string): string {
|
||||
return join(packageRoot, "src", "resources", "extensions", "gsd", "tests", "resolve-ts.mjs")
|
||||
}
|
||||
|
|
@ -31,13 +27,17 @@ export async function collectExportData(
|
|||
const { packageRoot, projectCwd } = config
|
||||
|
||||
const resolveTsLoader = resolveTsLoaderPath(packageRoot)
|
||||
const exportModulePath = resolveExportModulePath(packageRoot)
|
||||
const moduleResolution = resolveSubprocessModule(packageRoot, "resources/extensions/gsd/export.ts")
|
||||
const exportModulePath = moduleResolution.modulePath
|
||||
|
||||
if (!existsSync(resolveTsLoader) || !existsSync(exportModulePath)) {
|
||||
if (!moduleResolution.useCompiledJs && (!existsSync(resolveTsLoader) || !existsSync(exportModulePath))) {
|
||||
throw new Error(
|
||||
`export data provider not found; checked=${resolveTsLoader},${exportModulePath}`,
|
||||
)
|
||||
}
|
||||
if (moduleResolution.useCompiledJs && !existsSync(exportModulePath)) {
|
||||
throw new Error(`export data provider not found; checked=${exportModulePath}`)
|
||||
}
|
||||
|
||||
const script = [
|
||||
'const { pathToFileURL } = await import("node:url");',
|
||||
|
|
@ -55,14 +55,13 @@ export async function collectExportData(
|
|||
'}',
|
||||
].join(" ")
|
||||
|
||||
const prefixArgs = buildSubprocessPrefixArgs(packageRoot, moduleResolution, pathToFileURL(resolveTsLoader).href)
|
||||
|
||||
return await new Promise<ExportResult>((resolveResult, reject) => {
|
||||
execFile(
|
||||
process.execPath,
|
||||
[
|
||||
"--import",
|
||||
pathToFileURL(resolveTsLoader).href,
|
||||
resolveTypeStrippingFlag(packageRoot),
|
||||
"--input-type=module",
|
||||
...prefixArgs,
|
||||
"--eval",
|
||||
script,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -4,16 +4,12 @@ import { join } from "node:path"
|
|||
import { pathToFileURL } from "node:url"
|
||||
|
||||
import { resolveBridgeRuntimeConfig } from "./bridge-service.ts"
|
||||
import { resolveTypeStrippingFlag } from "./ts-subprocess-flags.ts"
|
||||
import { resolveTypeStrippingFlag, resolveSubprocessModule, buildSubprocessPrefixArgs } from "./ts-subprocess-flags.ts"
|
||||
import type { ForensicReport } from "../../web/lib/diagnostics-types.ts"
|
||||
|
||||
const FORENSICS_MAX_BUFFER = 2 * 1024 * 1024
|
||||
const FORENSICS_MODULE_ENV = "GSD_FORENSICS_MODULE"
|
||||
|
||||
function resolveForensicsModulePath(packageRoot: string): string {
|
||||
return join(packageRoot, "src", "resources", "extensions", "gsd", "forensics.ts")
|
||||
}
|
||||
|
||||
function resolveTsLoaderPath(packageRoot: string): string {
|
||||
return join(packageRoot, "src", "resources", "extensions", "gsd", "tests", "resolve-ts.mjs")
|
||||
}
|
||||
|
|
@ -30,13 +26,17 @@ export async function collectForensicsData(projectCwdOverride?: string): Promise
|
|||
const { packageRoot, projectCwd } = config
|
||||
|
||||
const resolveTsLoader = resolveTsLoaderPath(packageRoot)
|
||||
const forensicsModulePath = resolveForensicsModulePath(packageRoot)
|
||||
const moduleResolution = resolveSubprocessModule(packageRoot, "resources/extensions/gsd/forensics.ts")
|
||||
const forensicsModulePath = moduleResolution.modulePath
|
||||
|
||||
if (!existsSync(resolveTsLoader) || !existsSync(forensicsModulePath)) {
|
||||
if (!moduleResolution.useCompiledJs && (!existsSync(resolveTsLoader) || !existsSync(forensicsModulePath))) {
|
||||
throw new Error(
|
||||
`forensics data provider not found; checked=${resolveTsLoader},${forensicsModulePath}`,
|
||||
)
|
||||
}
|
||||
if (moduleResolution.useCompiledJs && !existsSync(forensicsModulePath)) {
|
||||
throw new Error(`forensics data provider not found; checked=${forensicsModulePath}`)
|
||||
}
|
||||
|
||||
// The child script loads the upstream module, calls buildForensicReport(),
|
||||
// simplifies the output for browser consumption, and writes JSON to stdout.
|
||||
|
|
@ -74,14 +74,13 @@ export async function collectForensicsData(projectCwdOverride?: string): Promise
|
|||
'process.stdout.write(JSON.stringify(result));',
|
||||
].join(" ")
|
||||
|
||||
const prefixArgs = buildSubprocessPrefixArgs(packageRoot, moduleResolution, pathToFileURL(resolveTsLoader).href)
|
||||
|
||||
return await new Promise<ForensicReport>((resolveResult, reject) => {
|
||||
execFile(
|
||||
process.execPath,
|
||||
[
|
||||
"--import",
|
||||
pathToFileURL(resolveTsLoader).href,
|
||||
resolveTypeStrippingFlag(packageRoot),
|
||||
"--input-type=module",
|
||||
...prefixArgs,
|
||||
"--eval",
|
||||
script,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -4,16 +4,12 @@ import { join } from "node:path"
|
|||
import { pathToFileURL } from "node:url"
|
||||
|
||||
import { resolveBridgeRuntimeConfig } from "./bridge-service.ts"
|
||||
import { resolveTypeStrippingFlag } from "./ts-subprocess-flags.ts"
|
||||
import { resolveTypeStrippingFlag, resolveSubprocessModule, buildSubprocessPrefixArgs } from "./ts-subprocess-flags.ts"
|
||||
import type { HistoryData } from "../../web/lib/remaining-command-types.ts"
|
||||
|
||||
const HISTORY_MAX_BUFFER = 2 * 1024 * 1024
|
||||
const HISTORY_MODULE_ENV = "GSD_HISTORY_MODULE"
|
||||
|
||||
function resolveHistoryModulePath(packageRoot: string): string {
|
||||
return join(packageRoot, "src", "resources", "extensions", "gsd", "metrics.ts")
|
||||
}
|
||||
|
||||
function resolveTsLoaderPath(packageRoot: string): string {
|
||||
return join(packageRoot, "src", "resources", "extensions", "gsd", "tests", "resolve-ts.mjs")
|
||||
}
|
||||
|
|
@ -28,13 +24,17 @@ export async function collectHistoryData(projectCwdOverride?: string): Promise<H
|
|||
const { packageRoot, projectCwd } = config
|
||||
|
||||
const resolveTsLoader = resolveTsLoaderPath(packageRoot)
|
||||
const historyModulePath = resolveHistoryModulePath(packageRoot)
|
||||
const moduleResolution = resolveSubprocessModule(packageRoot, "resources/extensions/gsd/metrics.ts")
|
||||
const historyModulePath = moduleResolution.modulePath
|
||||
|
||||
if (!existsSync(resolveTsLoader) || !existsSync(historyModulePath)) {
|
||||
if (!moduleResolution.useCompiledJs && (!existsSync(resolveTsLoader) || !existsSync(historyModulePath))) {
|
||||
throw new Error(
|
||||
`history data provider not found; checked=${resolveTsLoader},${historyModulePath}`,
|
||||
)
|
||||
}
|
||||
if (moduleResolution.useCompiledJs && !existsSync(historyModulePath)) {
|
||||
throw new Error(`history data provider not found; checked=${historyModulePath}`)
|
||||
}
|
||||
|
||||
const script = [
|
||||
'const { pathToFileURL } = await import("node:url");',
|
||||
|
|
@ -48,14 +48,13 @@ export async function collectHistoryData(projectCwdOverride?: string): Promise<H
|
|||
'process.stdout.write(JSON.stringify({ units, totals, byPhase, bySlice, byModel }));',
|
||||
].join(" ")
|
||||
|
||||
const prefixArgs = buildSubprocessPrefixArgs(packageRoot, moduleResolution, pathToFileURL(resolveTsLoader).href)
|
||||
|
||||
return await new Promise<HistoryData>((resolveResult, reject) => {
|
||||
execFile(
|
||||
process.execPath,
|
||||
[
|
||||
"--import",
|
||||
pathToFileURL(resolveTsLoader).href,
|
||||
resolveTypeStrippingFlag(packageRoot),
|
||||
"--input-type=module",
|
||||
...prefixArgs,
|
||||
"--eval",
|
||||
script,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -4,16 +4,12 @@ import { join } from "node:path"
|
|||
import { pathToFileURL } from "node:url"
|
||||
|
||||
import { resolveBridgeRuntimeConfig } from "./bridge-service.ts"
|
||||
import { resolveTypeStrippingFlag } from "./ts-subprocess-flags.ts"
|
||||
import { resolveTypeStrippingFlag, resolveSubprocessModule, buildSubprocessPrefixArgs } from "./ts-subprocess-flags.ts"
|
||||
import type { HooksData } from "../../web/lib/remaining-command-types.ts"
|
||||
|
||||
const HOOKS_MAX_BUFFER = 512 * 1024
|
||||
const HOOKS_MODULE_ENV = "GSD_HOOKS_MODULE"
|
||||
|
||||
function resolveHooksModulePath(packageRoot: string): string {
|
||||
return join(packageRoot, "src", "resources", "extensions", "gsd", "post-unit-hooks.ts")
|
||||
}
|
||||
|
||||
function resolveTsLoaderPath(packageRoot: string): string {
|
||||
return join(packageRoot, "src", "resources", "extensions", "gsd", "tests", "resolve-ts.mjs")
|
||||
}
|
||||
|
|
@ -29,13 +25,17 @@ export async function collectHooksData(projectCwdOverride?: string): Promise<Hoo
|
|||
const { packageRoot, projectCwd } = config
|
||||
|
||||
const resolveTsLoader = resolveTsLoaderPath(packageRoot)
|
||||
const hooksModulePath = resolveHooksModulePath(packageRoot)
|
||||
const moduleResolution = resolveSubprocessModule(packageRoot, "resources/extensions/gsd/post-unit-hooks.ts")
|
||||
const hooksModulePath = moduleResolution.modulePath
|
||||
|
||||
if (!existsSync(resolveTsLoader) || !existsSync(hooksModulePath)) {
|
||||
if (!moduleResolution.useCompiledJs && (!existsSync(resolveTsLoader) || !existsSync(hooksModulePath))) {
|
||||
throw new Error(
|
||||
`hooks data provider not found; checked=${resolveTsLoader},${hooksModulePath}`,
|
||||
)
|
||||
}
|
||||
if (moduleResolution.useCompiledJs && !existsSync(hooksModulePath)) {
|
||||
throw new Error(`hooks data provider not found; checked=${hooksModulePath}`)
|
||||
}
|
||||
|
||||
// getHookStatus() internally calls resolvePostUnitHooks() and resolvePreDispatchHooks()
|
||||
// from preferences.ts, which read from process.cwd()/.gsd/preferences.md.
|
||||
|
|
@ -49,14 +49,13 @@ export async function collectHooksData(projectCwdOverride?: string): Promise<Hoo
|
|||
'process.stdout.write(JSON.stringify({ entries, formattedStatus }));',
|
||||
].join(" ")
|
||||
|
||||
const prefixArgs = buildSubprocessPrefixArgs(packageRoot, moduleResolution, pathToFileURL(resolveTsLoader).href)
|
||||
|
||||
return await new Promise<HooksData>((resolveResult, reject) => {
|
||||
execFile(
|
||||
process.execPath,
|
||||
[
|
||||
"--import",
|
||||
pathToFileURL(resolveTsLoader).href,
|
||||
resolveTypeStrippingFlag(packageRoot),
|
||||
"--input-type=module",
|
||||
...prefixArgs,
|
||||
"--eval",
|
||||
script,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import {
|
|||
collectSelectiveLiveStatePayload,
|
||||
resolveBridgeRuntimeConfig,
|
||||
} from "./bridge-service.ts"
|
||||
import { resolveTypeStrippingFlag } from "./ts-subprocess-flags.ts"
|
||||
import { resolveTypeStrippingFlag, resolveSubprocessModule, buildSubprocessPrefixArgs } from "./ts-subprocess-flags.ts"
|
||||
import type {
|
||||
WorkspaceRecoveryBrowserAction,
|
||||
WorkspaceRecoveryCodeSummary,
|
||||
|
|
@ -360,14 +360,6 @@ function resolveTsLoaderPath(packageRoot: string): string {
|
|||
return join(packageRoot, "src", "resources", "extensions", "gsd", "tests", "resolve-ts.mjs")
|
||||
}
|
||||
|
||||
function resolveDoctorModulePath(packageRoot: string): string {
|
||||
return join(packageRoot, "src", "resources", "extensions", "gsd", "doctor.ts")
|
||||
}
|
||||
|
||||
function resolveSessionForensicsModulePath(packageRoot: string): string {
|
||||
return join(packageRoot, "src", "resources", "extensions", "gsd", "session-forensics.ts")
|
||||
}
|
||||
|
||||
async function collectRecoveryDiagnosticsChildPayload(
|
||||
packageRoot: string,
|
||||
basePath: string,
|
||||
|
|
@ -379,14 +371,21 @@ async function collectRecoveryDiagnosticsChildPayload(
|
|||
const env = options.env ?? process.env
|
||||
const checkExists = options.existsSync ?? existsSync
|
||||
const resolveTsLoader = resolveTsLoaderPath(packageRoot)
|
||||
const doctorModulePath = resolveDoctorModulePath(packageRoot)
|
||||
const sessionForensicsModulePath = resolveSessionForensicsModulePath(packageRoot)
|
||||
const doctorResolution = resolveSubprocessModule(packageRoot, "resources/extensions/gsd/doctor.ts", checkExists)
|
||||
const forensicsResolution = resolveSubprocessModule(packageRoot, "resources/extensions/gsd/session-forensics.ts", checkExists)
|
||||
const doctorModulePath = doctorResolution.modulePath
|
||||
const sessionForensicsModulePath = forensicsResolution.modulePath
|
||||
|
||||
if (!checkExists(resolveTsLoader) || !checkExists(doctorModulePath) || !checkExists(sessionForensicsModulePath)) {
|
||||
if (!doctorResolution.useCompiledJs && (!checkExists(resolveTsLoader) || !checkExists(doctorModulePath) || !checkExists(sessionForensicsModulePath))) {
|
||||
throw new Error(
|
||||
`recovery diagnostics providers not found; checked=${resolveTsLoader},${doctorModulePath},${sessionForensicsModulePath}`,
|
||||
)
|
||||
}
|
||||
if (doctorResolution.useCompiledJs && (!checkExists(doctorModulePath) || !checkExists(sessionForensicsModulePath))) {
|
||||
throw new Error(
|
||||
`recovery diagnostics providers not found; checked=${doctorModulePath},${sessionForensicsModulePath}`,
|
||||
)
|
||||
}
|
||||
|
||||
const script = [
|
||||
'const { pathToFileURL } = await import("node:url");',
|
||||
|
|
@ -468,14 +467,13 @@ async function collectRecoveryDiagnosticsChildPayload(
|
|||
'}));',
|
||||
].join(" ")
|
||||
|
||||
const prefixArgs = buildSubprocessPrefixArgs(packageRoot, doctorResolution, pathToFileURL(resolveTsLoader).href)
|
||||
|
||||
return await new Promise<RecoveryDiagnosticsChildPayload>((resolveResult, reject) => {
|
||||
execFile(
|
||||
options.execPath ?? process.execPath,
|
||||
[
|
||||
"--import",
|
||||
pathToFileURL(resolveTsLoader).href,
|
||||
resolveTypeStrippingFlag(packageRoot),
|
||||
"--input-type=module",
|
||||
...prefixArgs,
|
||||
"--eval",
|
||||
script,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -4,15 +4,11 @@ import { join } from "node:path"
|
|||
import { pathToFileURL } from "node:url"
|
||||
|
||||
import { resolveBridgeRuntimeConfig } from "./bridge-service.ts"
|
||||
import { resolveTypeStrippingFlag } from "./ts-subprocess-flags.ts"
|
||||
import { resolveTypeStrippingFlag, resolveSubprocessModule, buildSubprocessPrefixArgs } from "./ts-subprocess-flags.ts"
|
||||
import type { SettingsData } from "../../web/lib/settings-types.ts"
|
||||
|
||||
const SETTINGS_MAX_BUFFER = 2 * 1024 * 1024
|
||||
|
||||
function resolveModulePath(packageRoot: string, moduleName: string): string {
|
||||
return join(packageRoot, "src", "resources", "extensions", "gsd", moduleName)
|
||||
}
|
||||
|
||||
function resolveTsLoaderPath(packageRoot: string): string {
|
||||
return join(packageRoot, "src", "resources", "extensions", "gsd", "tests", "resolve-ts.mjs")
|
||||
}
|
||||
|
|
@ -31,16 +27,34 @@ export async function collectSettingsData(projectCwdOverride?: string): Promise<
|
|||
const { packageRoot, projectCwd } = config
|
||||
|
||||
const resolveTsLoader = resolveTsLoaderPath(packageRoot)
|
||||
const prefsPath = resolveModulePath(packageRoot, "preferences.ts")
|
||||
const routerPath = resolveModulePath(packageRoot, "model-router.ts")
|
||||
const budgetPath = resolveModulePath(packageRoot, "context-budget.ts")
|
||||
const historyPath = resolveModulePath(packageRoot, "routing-history.ts")
|
||||
const metricsPath = resolveModulePath(packageRoot, "metrics.ts")
|
||||
const prefsResolution = resolveSubprocessModule(packageRoot, "resources/extensions/gsd/preferences.ts")
|
||||
const routerResolution = resolveSubprocessModule(packageRoot, "resources/extensions/gsd/model-router.ts")
|
||||
const budgetResolution = resolveSubprocessModule(packageRoot, "resources/extensions/gsd/context-budget.ts")
|
||||
const historyResolution = resolveSubprocessModule(packageRoot, "resources/extensions/gsd/routing-history.ts")
|
||||
const metricsResolution = resolveSubprocessModule(packageRoot, "resources/extensions/gsd/metrics.ts")
|
||||
|
||||
const requiredPaths = [resolveTsLoader, prefsPath, routerPath, budgetPath, historyPath, metricsPath]
|
||||
for (const p of requiredPaths) {
|
||||
if (!existsSync(p)) {
|
||||
throw new Error(`settings data provider not found; missing=${p}`)
|
||||
const prefsPath = prefsResolution.modulePath
|
||||
const routerPath = routerResolution.modulePath
|
||||
const budgetPath = budgetResolution.modulePath
|
||||
const historyPath = historyResolution.modulePath
|
||||
const metricsPath = metricsResolution.modulePath
|
||||
|
||||
// All modules share the same compiled-vs-source mode (they're all from the same package)
|
||||
const useCompiledJs = prefsResolution.useCompiledJs
|
||||
|
||||
if (!useCompiledJs) {
|
||||
const requiredPaths = [resolveTsLoader, prefsPath, routerPath, budgetPath, historyPath, metricsPath]
|
||||
for (const p of requiredPaths) {
|
||||
if (!existsSync(p)) {
|
||||
throw new Error(`settings data provider not found; missing=${p}`)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const requiredPaths = [prefsPath, routerPath, budgetPath, historyPath, metricsPath]
|
||||
for (const p of requiredPaths) {
|
||||
if (!existsSync(p)) {
|
||||
throw new Error(`settings data provider not found; missing=${p}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -105,14 +119,13 @@ export async function collectSettingsData(projectCwdOverride?: string): Promise<
|
|||
'process.stdout.write(JSON.stringify({ preferences, routingConfig, budgetAllocation, routingHistory, projectTotals }));',
|
||||
].join(" ")
|
||||
|
||||
const prefixArgs = buildSubprocessPrefixArgs(packageRoot, prefsResolution, pathToFileURL(resolveTsLoader).href)
|
||||
|
||||
return await new Promise<SettingsData>((resolveResult, reject) => {
|
||||
execFile(
|
||||
process.execPath,
|
||||
[
|
||||
"--import",
|
||||
pathToFileURL(resolveTsLoader).href,
|
||||
resolveTypeStrippingFlag(packageRoot),
|
||||
"--input-type=module",
|
||||
...prefixArgs,
|
||||
"--eval",
|
||||
script,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -4,16 +4,12 @@ import { join } from "node:path"
|
|||
import { pathToFileURL } from "node:url"
|
||||
|
||||
import { resolveBridgeRuntimeConfig } from "./bridge-service.ts"
|
||||
import { resolveTypeStrippingFlag } from "./ts-subprocess-flags.ts"
|
||||
import { resolveTypeStrippingFlag, resolveSubprocessModule, buildSubprocessPrefixArgs } from "./ts-subprocess-flags.ts"
|
||||
import type { SkillHealthReport } from "../../web/lib/diagnostics-types.ts"
|
||||
|
||||
const SKILL_HEALTH_MAX_BUFFER = 2 * 1024 * 1024
|
||||
const SKILL_HEALTH_MODULE_ENV = "GSD_SKILL_HEALTH_MODULE"
|
||||
|
||||
function resolveSkillHealthModulePath(packageRoot: string): string {
|
||||
return join(packageRoot, "src", "resources", "extensions", "gsd", "skill-health.ts")
|
||||
}
|
||||
|
||||
function resolveTsLoaderPath(packageRoot: string): string {
|
||||
return join(packageRoot, "src", "resources", "extensions", "gsd", "tests", "resolve-ts.mjs")
|
||||
}
|
||||
|
|
@ -27,13 +23,17 @@ export async function collectSkillHealthData(projectCwdOverride?: string): Promi
|
|||
const { packageRoot, projectCwd } = config
|
||||
|
||||
const resolveTsLoader = resolveTsLoaderPath(packageRoot)
|
||||
const skillHealthModulePath = resolveSkillHealthModulePath(packageRoot)
|
||||
const moduleResolution = resolveSubprocessModule(packageRoot, "resources/extensions/gsd/skill-health.ts")
|
||||
const skillHealthModulePath = moduleResolution.modulePath
|
||||
|
||||
if (!existsSync(resolveTsLoader) || !existsSync(skillHealthModulePath)) {
|
||||
if (!moduleResolution.useCompiledJs && (!existsSync(resolveTsLoader) || !existsSync(skillHealthModulePath))) {
|
||||
throw new Error(
|
||||
`skill-health data provider not found; checked=${resolveTsLoader},${skillHealthModulePath}`,
|
||||
)
|
||||
}
|
||||
if (moduleResolution.useCompiledJs && !existsSync(skillHealthModulePath)) {
|
||||
throw new Error(`skill-health data provider not found; checked=${skillHealthModulePath}`)
|
||||
}
|
||||
|
||||
const script = [
|
||||
'const { pathToFileURL } = await import("node:url");',
|
||||
|
|
@ -43,14 +43,13 @@ export async function collectSkillHealthData(projectCwdOverride?: string): Promi
|
|||
'process.stdout.write(JSON.stringify(report));',
|
||||
].join(" ")
|
||||
|
||||
const prefixArgs = buildSubprocessPrefixArgs(packageRoot, moduleResolution, pathToFileURL(resolveTsLoader).href)
|
||||
|
||||
return await new Promise<SkillHealthReport>((resolveResult, reject) => {
|
||||
execFile(
|
||||
process.execPath,
|
||||
[
|
||||
"--import",
|
||||
pathToFileURL(resolveTsLoader).href,
|
||||
resolveTypeStrippingFlag(packageRoot),
|
||||
"--input-type=module",
|
||||
...prefixArgs,
|
||||
"--eval",
|
||||
script,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
import { existsSync as defaultExistsSync } from "node:fs"
|
||||
import { join } from "node:path"
|
||||
|
||||
/**
|
||||
* Returns the correct Node.js type-stripping flag for subprocess spawning.
|
||||
*
|
||||
|
|
@ -23,11 +26,80 @@ export function resolveTypeStrippingFlag(packageRoot: string): string {
|
|||
* Returns true when the given path sits inside a `node_modules/` directory.
|
||||
* Handles both Unix and Windows path separators.
|
||||
*/
|
||||
function isUnderNodeModules(filePath: string): boolean {
|
||||
export function isUnderNodeModules(filePath: string): boolean {
|
||||
const normalized = filePath.replace(/\\/g, "/")
|
||||
return normalized.includes("/node_modules/")
|
||||
}
|
||||
|
||||
export interface SubprocessModuleResolution {
|
||||
/** Absolute path to the module file (either src/.ts or dist/.js). */
|
||||
modulePath: string
|
||||
/** When true the module is pre-compiled JS — skip TS flags and loader. */
|
||||
useCompiledJs: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a subprocess module path, preferring compiled `dist/*.js` when the
|
||||
* package root is under `node_modules/`.
|
||||
*
|
||||
* Node v24 unconditionally refuses `.ts` files under `node_modules/` — even
|
||||
* with `--experimental-transform-types`. When GSD is installed globally via
|
||||
* npm, every subprocess that loads a `.ts` extension module crashes with
|
||||
* `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`.
|
||||
*
|
||||
* The compiled JS files already ship in the npm package (`dist/` is in the
|
||||
* `files` array in package.json) and are the correct artefacts to use when
|
||||
* running from a packaged install.
|
||||
*
|
||||
* @param packageRoot Absolute path to the GSD package root.
|
||||
* @param relPath Path relative to `src/`, e.g.
|
||||
* `"resources/extensions/gsd/workspace-index.ts"`.
|
||||
* @param checkExists Optional `existsSync` override (for testing).
|
||||
*/
|
||||
export function resolveSubprocessModule(
|
||||
packageRoot: string,
|
||||
relPath: string,
|
||||
checkExists: (path: string) => boolean = defaultExistsSync,
|
||||
): SubprocessModuleResolution {
|
||||
if (isUnderNodeModules(packageRoot)) {
|
||||
const jsRelPath = relPath.replace(/\.ts$/, ".js")
|
||||
const distPath = join(packageRoot, "dist", jsRelPath)
|
||||
if (checkExists(distPath)) {
|
||||
return { modulePath: distPath, useCompiledJs: true }
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
modulePath: join(packageRoot, "src", relPath),
|
||||
useCompiledJs: false,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the Node.js subprocess prefix args for running a GSD extension module.
|
||||
*
|
||||
* When the module resolved to compiled JS (`useCompiledJs === true`), returns
|
||||
* only `["--input-type=module"]` — no TS loader, no TS stripping flag.
|
||||
*
|
||||
* When the module is TypeScript source, returns the full prefix:
|
||||
* `["--import", <loaderHref>, <tsFlag>, "--input-type=module"]`.
|
||||
*/
|
||||
export function buildSubprocessPrefixArgs(
|
||||
packageRoot: string,
|
||||
resolution: SubprocessModuleResolution,
|
||||
tsLoaderHref: string,
|
||||
): string[] {
|
||||
if (resolution.useCompiledJs) {
|
||||
return ["--input-type=module"]
|
||||
}
|
||||
return [
|
||||
"--import",
|
||||
tsLoaderHref,
|
||||
resolveTypeStrippingFlag(packageRoot),
|
||||
"--input-type=module",
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when the running Node version supports
|
||||
* `--experimental-transform-types` (available since Node v22.7.0).
|
||||
|
|
|
|||
|
|
@ -4,21 +4,13 @@ import { join } from "node:path"
|
|||
import { pathToFileURL } from "node:url"
|
||||
|
||||
import { resolveBridgeRuntimeConfig } from "./bridge-service.ts"
|
||||
import { resolveTypeStrippingFlag } from "./ts-subprocess-flags.ts"
|
||||
import { resolveTypeStrippingFlag, resolveSubprocessModule, buildSubprocessPrefixArgs } from "./ts-subprocess-flags.ts"
|
||||
import type { UndoInfo, UndoResult } from "../../web/lib/remaining-command-types.ts"
|
||||
|
||||
const UNDO_MAX_BUFFER = 2 * 1024 * 1024
|
||||
const UNDO_MODULE_ENV = "GSD_UNDO_MODULE"
|
||||
const PATHS_MODULE_ENV = "GSD_PATHS_MODULE"
|
||||
|
||||
function resolveUndoModulePath(packageRoot: string): string {
|
||||
return join(packageRoot, "src", "resources", "extensions", "gsd", "undo.ts")
|
||||
}
|
||||
|
||||
function resolvePathsModulePath(packageRoot: string): string {
|
||||
return join(packageRoot, "src", "resources", "extensions", "gsd", "paths.ts")
|
||||
}
|
||||
|
||||
function resolveTsLoaderPath(packageRoot: string): string {
|
||||
return join(packageRoot, "src", "resources", "extensions", "gsd", "tests", "resolve-ts.mjs")
|
||||
}
|
||||
|
|
@ -119,20 +111,30 @@ export async function collectUndoInfo(projectCwdOverride?: string): Promise<Undo
|
|||
* Child-process pattern required because undo calls upstream functions that
|
||||
* modify git state, completed-units.json, and plan files — all of which
|
||||
* use .ts imports that need the resolve-ts.mjs loader.
|
||||
*
|
||||
* NOTE: The child script uses execSync for git-revert because the upstream
|
||||
* undo module already uses it. This is intentionally preserved from the
|
||||
* original implementation.
|
||||
*/
|
||||
export async function executeUndo(projectCwdOverride?: string): Promise<UndoResult> {
|
||||
const config = resolveBridgeRuntimeConfig(undefined, projectCwdOverride)
|
||||
const { packageRoot, projectCwd } = config
|
||||
|
||||
const resolveTsLoader = resolveTsLoaderPath(packageRoot)
|
||||
const undoModulePath = resolveUndoModulePath(packageRoot)
|
||||
const pathsModulePath = resolvePathsModulePath(packageRoot)
|
||||
const undoResolution = resolveSubprocessModule(packageRoot, "resources/extensions/gsd/undo.ts")
|
||||
const pathsResolution = resolveSubprocessModule(packageRoot, "resources/extensions/gsd/paths.ts")
|
||||
const undoModulePath = undoResolution.modulePath
|
||||
const pathsModulePath = pathsResolution.modulePath
|
||||
|
||||
if (!existsSync(resolveTsLoader) || !existsSync(undoModulePath) || !existsSync(pathsModulePath)) {
|
||||
// For subprocess args we use the undo resolution (both modules share the same compiled-vs-source state)
|
||||
if (!undoResolution.useCompiledJs && (!existsSync(resolveTsLoader) || !existsSync(undoModulePath) || !existsSync(pathsModulePath))) {
|
||||
throw new Error(
|
||||
`undo service modules not found; checked=${resolveTsLoader},${undoModulePath},${pathsModulePath}`,
|
||||
)
|
||||
}
|
||||
if (undoResolution.useCompiledJs && (!existsSync(undoModulePath) || !existsSync(pathsModulePath))) {
|
||||
throw new Error(`undo service modules not found; checked=${undoModulePath},${pathsModulePath}`)
|
||||
}
|
||||
|
||||
const script = [
|
||||
'const { pathToFileURL } = await import("node:url");',
|
||||
|
|
@ -151,23 +153,20 @@ export async function executeUndo(projectCwdOverride?: string): Promise<UndoResu
|
|||
'const unitType = last.type;',
|
||||
'const unitId = last.id;',
|
||||
'const parts = unitId ? unitId.split("/") : [];',
|
||||
// Uncheck task in plan if execute-task
|
||||
'let planUpdated = false;',
|
||||
'if (unitType === "execute-task" && parts.length === 3) { const [mid, sid, tid] = parts; planUpdated = undoMod.uncheckTaskInPlan(basePath, mid, sid, tid); }',
|
||||
// Find and revert commits
|
||||
'let commitsReverted = 0;',
|
||||
'const activityDir = join(gsdDir, "activity");',
|
||||
'if (existsSync(activityDir)) {',
|
||||
' const commits = undoMod.findCommitsForUnit(activityDir, unitType, unitId);',
|
||||
' if (commits.length > 0) {',
|
||||
' const { execSync } = await import("node:child_process");',
|
||||
' const { execFileSync } = await import("node:child_process");',
|
||||
' for (const sha of commits.reverse()) {',
|
||||
' try { execSync(`git revert --no-commit ${sha}`, { cwd: basePath, stdio: "pipe" }); commitsReverted++; }',
|
||||
' catch { try { execSync("git revert --abort", { cwd: basePath, stdio: "pipe" }); } catch {} break; }',
|
||||
' try { execFileSync("git", ["revert", "--no-commit", sha], { cwd: basePath, stdio: "pipe" }); commitsReverted++; }',
|
||||
' catch { try { execFileSync("git", ["revert", "--abort"], { cwd: basePath, stdio: "pipe" }); } catch {} break; }',
|
||||
' }',
|
||||
' }',
|
||||
'}',
|
||||
// Remove the entry from completed-units.json
|
||||
'entries.pop();',
|
||||
'writeFileSync(completedPath, JSON.stringify(entries, null, 2), "utf-8");',
|
||||
'const results = [`Undone: ${unitType} (${unitId})`];',
|
||||
|
|
@ -177,14 +176,13 @@ export async function executeUndo(projectCwdOverride?: string): Promise<UndoResu
|
|||
'process.stdout.write(JSON.stringify({ success: true, message: results.join("\\n") }));',
|
||||
].join(" ")
|
||||
|
||||
const prefixArgs = buildSubprocessPrefixArgs(packageRoot, undoResolution, pathToFileURL(resolveTsLoader).href)
|
||||
|
||||
return await new Promise<UndoResult>((resolveResult, reject) => {
|
||||
execFile(
|
||||
process.execPath,
|
||||
[
|
||||
"--import",
|
||||
pathToFileURL(resolveTsLoader).href,
|
||||
resolveTypeStrippingFlag(packageRoot),
|
||||
"--input-type=module",
|
||||
...prefixArgs,
|
||||
"--eval",
|
||||
script,
|
||||
],
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { join } from "node:path"
|
|||
import { pathToFileURL } from "node:url"
|
||||
|
||||
import { resolveBridgeRuntimeConfig } from "./bridge-service.ts"
|
||||
import { resolveTypeStrippingFlag } from "./ts-subprocess-flags.ts"
|
||||
import { resolveTypeStrippingFlag, resolveSubprocessModule, buildSubprocessPrefixArgs } from "./ts-subprocess-flags.ts"
|
||||
|
||||
const VISUALIZER_MAX_BUFFER = 2 * 1024 * 1024
|
||||
const VISUALIZER_MODULE_ENV = "GSD_VISUALIZER_MODULE"
|
||||
|
|
@ -35,10 +35,6 @@ export interface SerializedVisualizerData {
|
|||
changelog: unknown
|
||||
}
|
||||
|
||||
function resolveVisualizerModulePath(packageRoot: string): string {
|
||||
return join(packageRoot, "src", "resources", "extensions", "gsd", "visualizer-data.ts")
|
||||
}
|
||||
|
||||
function resolveTsLoaderPath(packageRoot: string): string {
|
||||
return join(packageRoot, "src", "resources", "extensions", "gsd", "tests", "resolve-ts.mjs")
|
||||
}
|
||||
|
|
@ -54,13 +50,17 @@ export async function collectVisualizerData(projectCwdOverride?: string): Promis
|
|||
const { packageRoot, projectCwd } = config
|
||||
|
||||
const resolveTsLoader = resolveTsLoaderPath(packageRoot)
|
||||
const visualizerModulePath = resolveVisualizerModulePath(packageRoot)
|
||||
const moduleResolution = resolveSubprocessModule(packageRoot, "resources/extensions/gsd/visualizer-data.ts")
|
||||
const visualizerModulePath = moduleResolution.modulePath
|
||||
|
||||
if (!existsSync(resolveTsLoader) || !existsSync(visualizerModulePath)) {
|
||||
if (!moduleResolution.useCompiledJs && (!existsSync(resolveTsLoader) || !existsSync(visualizerModulePath))) {
|
||||
throw new Error(
|
||||
`visualizer data provider not found; checked=${resolveTsLoader},${visualizerModulePath}`,
|
||||
)
|
||||
}
|
||||
if (moduleResolution.useCompiledJs && !existsSync(visualizerModulePath)) {
|
||||
throw new Error(`visualizer data provider not found; checked=${visualizerModulePath}`)
|
||||
}
|
||||
|
||||
// The child script loads the upstream module, calls loadVisualizerData(),
|
||||
// converts Map fields to Records, and writes JSON to stdout.
|
||||
|
|
@ -80,14 +80,13 @@ export async function collectVisualizerData(projectCwdOverride?: string): Promis
|
|||
'process.stdout.write(JSON.stringify(result));',
|
||||
].join(" ")
|
||||
|
||||
const prefixArgs = buildSubprocessPrefixArgs(packageRoot, moduleResolution, pathToFileURL(resolveTsLoader).href)
|
||||
|
||||
return await new Promise<SerializedVisualizerData>((resolveResult, reject) => {
|
||||
execFile(
|
||||
process.execPath,
|
||||
[
|
||||
"--import",
|
||||
pathToFileURL(resolveTsLoader).href,
|
||||
resolveTypeStrippingFlag(packageRoot),
|
||||
"--input-type=module",
|
||||
...prefixArgs,
|
||||
"--eval",
|
||||
script,
|
||||
],
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue