singularity-forge/src/web/export-service.ts
Tom Boucher de9ba8aeb7 fix: add windowsHide to all web-mode subprocess spawns (#2628) (#3046)
On Windows, child_process.spawn() and execFile() open a visible console
window by default. The web server spawn, RPC bridge, browser opener, and
all 15 web service subprocess calls were missing windowsHide: true,
causing constant console window flashing when running gsd --web.

Closes #2628

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 14:50:13 -06:00

97 lines
3.6 KiB
TypeScript

import { execFile } from "node:child_process"
import { existsSync } from "node:fs"
import { join } from "node:path"
import { pathToFileURL } from "node:url"
import { resolveBridgeRuntimeConfig } from "./bridge-service.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 resolveTsLoaderPath(packageRoot: string): string {
return join(packageRoot, "src", "resources", "extensions", "gsd", "tests", "resolve-ts.mjs")
}
/**
* Generates an export file via a child process and returns its content.
* The child calls writeExportFile() which creates a timestamped file in .gsd/,
* then reads its content back for browser display.
*/
export async function collectExportData(
format: "markdown" | "json" = "markdown",
projectCwdOverride?: string,
): Promise<ExportResult> {
const config = resolveBridgeRuntimeConfig(undefined, projectCwdOverride)
const { packageRoot, projectCwd } = config
const resolveTsLoader = resolveTsLoaderPath(packageRoot)
const moduleResolution = resolveSubprocessModule(packageRoot, "resources/extensions/gsd/export.ts")
const exportModulePath = moduleResolution.modulePath
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");',
`const mod = await import(pathToFileURL(process.env.${EXPORT_MODULE_ENV}).href);`,
'const format = process.env.GSD_EXPORT_FORMAT || "markdown";',
'const basePath = process.env.GSD_EXPORT_BASE;',
'const filePath = mod.writeExportFile(basePath, format);',
'if (filePath) {',
' const { readFileSync } = await import("node:fs");',
' const { basename } = await import("node:path");',
' const content = readFileSync(filePath, "utf-8");',
' process.stdout.write(JSON.stringify({ content, format, filename: basename(filePath) }));',
'} else {',
' process.stdout.write(JSON.stringify({ content: "No metrics data available for export.", format, filename: "export." + (format === "json" ? "json" : "md") }));',
'}',
].join(" ")
const prefixArgs = buildSubprocessPrefixArgs(packageRoot, moduleResolution, pathToFileURL(resolveTsLoader).href)
return await new Promise<ExportResult>((resolveResult, reject) => {
execFile(
process.execPath,
[
...prefixArgs,
"--eval",
script,
],
{
cwd: packageRoot,
env: {
...process.env,
[EXPORT_MODULE_ENV]: exportModulePath,
GSD_EXPORT_BASE: projectCwd,
GSD_EXPORT_FORMAT: format,
},
maxBuffer: EXPORT_MAX_BUFFER,
windowsHide: true,
},
(error, stdout, stderr) => {
if (error) {
reject(new Error(`export data subprocess failed: ${stderr || error.message}`))
return
}
try {
resolveResult(JSON.parse(stdout) as ExportResult)
} catch (parseError) {
reject(
new Error(
`export data subprocess returned invalid JSON: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
),
)
}
},
)
})
}