reset-framework-cli 1.1.5 → 1.2.0

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/src/lib/output.js CHANGED
@@ -1,13 +1,13 @@
1
- import path from "node:path"
2
- import { cp, mkdir, readdir, rm, writeFile } from "node:fs/promises"
3
- import { existsSync } from "node:fs"
4
-
5
- import { logger } from "./logger.js"
6
- import { runCommand } from "./process.js"
7
- import {
8
- resolveFrameworkRuntimeBinary,
9
- resolveFrameworkRuntimeOutputDir
10
- } from "./project.js"
1
+ import path from "node:path"
2
+ import { cp, mkdir, readdir, rm, writeFile } from "node:fs/promises"
3
+ import { existsSync } from "node:fs"
4
+
5
+ import { logger } from "./logger.js"
6
+ import { runCommand } from "./process.js"
7
+ import {
8
+ resolveFrameworkRuntimeBinary,
9
+ resolveFrameworkRuntimeOutputDir
10
+ } from "./project.js"
11
11
 
12
12
  function escapePlistString(value) {
13
13
  return value
@@ -60,7 +60,7 @@ function serializePlistValue(value, indentLevel = 1) {
60
60
  return `${indent}<string>${escapePlistString(String(value))}</string>\n`
61
61
  }
62
62
 
63
- function buildMacOSInfoPlist(config) {
63
+ function buildMacOSInfoPlist(config) {
64
64
  const urlTypes = Array.isArray(config.protocols?.schemes)
65
65
  ? config.protocols.schemes.map((schemeConfig) => ({
66
66
  CFBundleTypeRole: schemeConfig.role ?? "Viewer",
@@ -95,39 +95,84 @@ function buildMacOSInfoPlist(config) {
95
95
  `<plist version="1.0">\n` +
96
96
  serializePlistValue(plist, 0) +
97
97
  `</plist>\n`
98
- )
99
- }
98
+ )
99
+ }
100
+
101
+ function shouldCopyWindowsRuntimeArtifact(entryName) {
102
+ const extension = path.extname(entryName).toLowerCase()
103
+
104
+ return [
105
+ ".bin",
106
+ ".dat",
107
+ ".dll",
108
+ ".exe",
109
+ ".json",
110
+ ".manifest",
111
+ ".pak",
112
+ ".pri"
113
+ ].includes(extension)
114
+ }
115
+
116
+ function escapePowerShellLiteral(value) {
117
+ return String(value).replaceAll("'", "''")
118
+ }
119
+
120
+ export async function stageMacOSAppBundle(frameworkPaths, frameworkBuildPaths, config, options = {}) {
121
+ const { dryRun = false, outputPaths, configPath, preferSource = false } = options
122
+ const runtimeSourceBundle = resolveFrameworkRuntimeOutputDir(frameworkPaths, frameworkBuildPaths, "release", {
123
+ mustExist: !dryRun,
124
+ preferSource
125
+ })
100
126
 
101
- function shouldCopyWindowsRuntimeArtifact(entryName) {
102
- const extension = path.extname(entryName).toLowerCase()
127
+ if (!outputPaths) {
128
+ throw new Error("stageMacOSAppBundle requires resolved output paths")
129
+ }
103
130
 
104
- return [
105
- ".bin",
106
- ".dat",
107
- ".dll",
108
- ".exe",
109
- ".json",
110
- ".manifest",
111
- ".pak",
112
- ".pri"
113
- ].includes(extension)
114
- }
131
+ if (!existsSync(outputPaths.frontendEntryFile) && !dryRun) {
132
+ throw new Error(
133
+ `Missing frontend build output at ${outputPaths.frontendEntryFile}. Run the frontend build first.`
134
+ )
135
+ }
136
+
137
+ logger.info(`Staging app bundle into ${outputPaths.appBundlePath}`)
115
138
 
116
- function escapePowerShellLiteral(value) {
117
- return String(value).replaceAll("'", "''")
118
- }
139
+ if (dryRun) {
140
+ logger.info(`- copy ${runtimeSourceBundle} -> ${outputPaths.appBundlePath}`)
141
+ logger.info(`- copy ${configPath} -> ${outputPaths.bundledConfigPath}`)
142
+ logger.info(`- copy ${outputPaths.frontendDistDir} -> ${outputPaths.bundledFrontendDir}`)
143
+ return outputPaths
144
+ }
119
145
 
120
- export async function stageMacOSAppBundle(frameworkBuildPaths, appPaths, config, options = {}) {
121
- const { dryRun = false, outputPaths, configPath } = options
146
+ await mkdir(outputPaths.macosDir, { recursive: true })
147
+ await rm(outputPaths.appBundlePath, { recursive: true, force: true })
148
+ await cp(runtimeSourceBundle, outputPaths.appBundlePath, { recursive: true })
122
149
 
123
- if (!outputPaths) {
124
- throw new Error("stageMacOSAppBundle requires resolved output paths")
125
- }
150
+ await mkdir(outputPaths.resourcesDir, { recursive: true })
151
+ await cp(configPath, outputPaths.bundledConfigPath, { force: true })
152
+ await rm(outputPaths.bundledFrontendDir, { recursive: true, force: true })
153
+ await cp(outputPaths.frontendDistDir, outputPaths.bundledFrontendDir, { recursive: true })
154
+ await writeFile(
155
+ path.join(outputPaths.appBundlePath, "Contents", "Info.plist"),
156
+ buildMacOSInfoPlist(config),
157
+ "utf8"
158
+ )
126
159
 
127
- if (!existsSync(frameworkBuildPaths.releaseAppTemplate) && !dryRun) {
128
- throw new Error(
129
- `Missing runtime app bundle at ${frameworkBuildPaths.releaseAppTemplate}. Run reset-framework-cli build first.`
130
- )
160
+ return outputPaths
161
+ }
162
+
163
+ export async function stageWindowsAppBundle(frameworkPaths, frameworkBuildPaths, config, options = {}) {
164
+ const { dryRun = false, outputPaths, configPath, preferSource = false } = options
165
+ const runtimeSourceBinary = resolveFrameworkRuntimeBinary(frameworkPaths, frameworkBuildPaths, "release", {
166
+ mustExist: !dryRun,
167
+ preferSource
168
+ })
169
+ const runtimeSourceDir = resolveFrameworkRuntimeOutputDir(frameworkPaths, frameworkBuildPaths, "release", {
170
+ mustExist: !dryRun,
171
+ preferSource
172
+ })
173
+
174
+ if (!outputPaths) {
175
+ throw new Error("stageWindowsAppBundle requires resolved output paths")
131
176
  }
132
177
 
133
178
  if (!existsSync(outputPaths.frontendEntryFile) && !dryRun) {
@@ -136,91 +181,46 @@ export async function stageMacOSAppBundle(frameworkBuildPaths, appPaths, config,
136
181
  )
137
182
  }
138
183
 
139
- logger.info(`Staging app bundle into ${outputPaths.appBundlePath}`)
184
+ logger.info(`Staging Windows app into ${outputPaths.appBundlePath}`)
140
185
 
141
186
  if (dryRun) {
142
- logger.info(`- copy ${frameworkBuildPaths.releaseAppTemplate} -> ${outputPaths.appBundlePath}`)
187
+ logger.info(`- copy ${runtimeSourceDir} -> ${outputPaths.appBundlePath}`)
188
+ logger.info(`- rename ${runtimeSourceBinary} -> ${outputPaths.appExecutablePath}`)
143
189
  logger.info(`- copy ${configPath} -> ${outputPaths.bundledConfigPath}`)
144
190
  logger.info(`- copy ${outputPaths.frontendDistDir} -> ${outputPaths.bundledFrontendDir}`)
145
191
  return outputPaths
146
192
  }
147
193
 
148
- await mkdir(outputPaths.macosDir, { recursive: true })
194
+ await mkdir(outputPaths.windowsDir, { recursive: true })
149
195
  await rm(outputPaths.appBundlePath, { recursive: true, force: true })
150
- await cp(frameworkBuildPaths.releaseAppTemplate, outputPaths.appBundlePath, { recursive: true })
196
+ await mkdir(outputPaths.appBundlePath, { recursive: true })
197
+ const runtimeEntries = await readdir(runtimeSourceDir, { withFileTypes: true })
198
+ const runtimeBinaryName = path.basename(runtimeSourceBinary)
199
+
200
+ for (const entry of runtimeEntries) {
201
+ if (!entry.isDirectory() && !shouldCopyWindowsRuntimeArtifact(entry.name)) {
202
+ continue
203
+ }
204
+
205
+ const sourcePath = path.join(runtimeSourceDir, entry.name)
206
+ const targetPath =
207
+ entry.name.toLowerCase() === runtimeBinaryName.toLowerCase()
208
+ ? outputPaths.appExecutablePath
209
+ : path.join(outputPaths.appBundlePath, entry.name)
210
+
211
+ await cp(sourcePath, targetPath, { recursive: true, force: true })
212
+ }
151
213
 
152
214
  await mkdir(outputPaths.resourcesDir, { recursive: true })
153
215
  await cp(configPath, outputPaths.bundledConfigPath, { force: true })
154
216
  await rm(outputPaths.bundledFrontendDir, { recursive: true, force: true })
155
217
  await cp(outputPaths.frontendDistDir, outputPaths.bundledFrontendDir, { recursive: true })
156
- await writeFile(
157
- path.join(outputPaths.appBundlePath, "Contents", "Info.plist"),
158
- buildMacOSInfoPlist(config),
159
- "utf8"
160
- )
161
218
 
162
- return outputPaths
163
- }
164
-
165
- export async function stageWindowsAppBundle(frameworkBuildPaths, appPaths, config, options = {}) {
166
- const { dryRun = false, outputPaths, configPath } = options
167
- const runtimeSourceBinary = resolveFrameworkRuntimeBinary(frameworkBuildPaths, "release", {
168
- mustExist: !dryRun
169
- })
170
- const runtimeSourceDir = resolveFrameworkRuntimeOutputDir(frameworkBuildPaths, "release", {
171
- mustExist: !dryRun
172
- })
173
-
174
- if (!outputPaths) {
175
- throw new Error("stageWindowsAppBundle requires resolved output paths")
176
- }
177
-
178
- if (!existsSync(outputPaths.frontendEntryFile) && !dryRun) {
179
- throw new Error(
180
- `Missing frontend build output at ${outputPaths.frontendEntryFile}. Run the frontend build first.`
181
- )
182
- }
183
-
184
- logger.info(`Staging Windows app into ${outputPaths.appBundlePath}`)
185
-
186
- if (dryRun) {
187
- logger.info(`- copy ${runtimeSourceDir} -> ${outputPaths.appBundlePath}`)
188
- logger.info(`- rename ${runtimeSourceBinary} -> ${outputPaths.appExecutablePath}`)
189
- logger.info(`- copy ${configPath} -> ${outputPaths.bundledConfigPath}`)
190
- logger.info(`- copy ${outputPaths.frontendDistDir} -> ${outputPaths.bundledFrontendDir}`)
191
- return outputPaths
192
- }
193
-
194
- await mkdir(outputPaths.windowsDir, { recursive: true })
195
- await rm(outputPaths.appBundlePath, { recursive: true, force: true })
196
- await mkdir(outputPaths.appBundlePath, { recursive: true })
197
- const runtimeEntries = await readdir(runtimeSourceDir, { withFileTypes: true })
198
- const runtimeBinaryName = path.basename(runtimeSourceBinary)
199
-
200
- for (const entry of runtimeEntries) {
201
- if (!entry.isDirectory() && !shouldCopyWindowsRuntimeArtifact(entry.name)) {
202
- continue
203
- }
204
-
205
- const sourcePath = path.join(runtimeSourceDir, entry.name)
206
- const targetPath =
207
- entry.name.toLowerCase() === runtimeBinaryName.toLowerCase()
208
- ? outputPaths.appExecutablePath
209
- : path.join(outputPaths.appBundlePath, entry.name)
210
-
211
- await cp(sourcePath, targetPath, { recursive: true, force: true })
212
- }
213
-
214
- await mkdir(outputPaths.resourcesDir, { recursive: true })
215
- await cp(configPath, outputPaths.bundledConfigPath, { force: true })
216
- await rm(outputPaths.bundledFrontendDir, { recursive: true, force: true })
217
- await cp(outputPaths.frontendDistDir, outputPaths.bundledFrontendDir, { recursive: true })
218
-
219
- return outputPaths
220
- }
219
+ return outputPaths
220
+ }
221
221
 
222
- export async function createMacOSZipArchive(outputPaths, options = {}) {
223
- const { dryRun = false } = options
222
+ export async function createMacOSZipArchive(outputPaths, options = {}) {
223
+ const { dryRun = false } = options
224
224
 
225
225
  logger.info(`Packaging ${outputPaths.appBundlePath} -> ${outputPaths.zipPath}`)
226
226
 
@@ -232,37 +232,37 @@ export async function createMacOSZipArchive(outputPaths, options = {}) {
232
232
  await mkdir(outputPaths.packagesDir, { recursive: true })
233
233
  }
234
234
 
235
- await runCommand(
236
- "ditto",
237
- ["-c", "-k", "--sequesterRsrc", "--keepParent", outputPaths.appBundlePath, outputPaths.zipPath],
238
- { dryRun }
239
- )
240
- }
241
-
242
- export async function createWindowsZipArchive(outputPaths, options = {}) {
243
- const { dryRun = false } = options
244
-
245
- logger.info(`Packaging ${outputPaths.appBundlePath} -> ${outputPaths.zipPath}`)
246
-
247
- if (process.platform !== "win32") {
248
- throw new Error("Packaging is only implemented for Windows and macOS right now")
249
- }
250
-
251
- if (!dryRun) {
252
- await mkdir(outputPaths.packagesDir, { recursive: true })
253
- await rm(outputPaths.zipPath, { force: true })
254
- }
255
-
256
- const literalSourcePath = escapePowerShellLiteral(outputPaths.appBundlePath)
257
- const literalDestinationPath = escapePowerShellLiteral(outputPaths.zipPath)
258
- const archiveCommand = [
259
- "$ErrorActionPreference = 'Stop'",
260
- `Compress-Archive -LiteralPath '${literalSourcePath}' -DestinationPath '${literalDestinationPath}' -CompressionLevel Optimal`
261
- ].join("; ")
262
-
263
- await runCommand(
264
- "powershell.exe",
265
- ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", archiveCommand],
266
- { dryRun }
267
- )
268
- }
235
+ await runCommand(
236
+ "ditto",
237
+ ["-c", "-k", "--sequesterRsrc", "--keepParent", outputPaths.appBundlePath, outputPaths.zipPath],
238
+ { dryRun }
239
+ )
240
+ }
241
+
242
+ export async function createWindowsZipArchive(outputPaths, options = {}) {
243
+ const { dryRun = false } = options
244
+
245
+ logger.info(`Packaging ${outputPaths.appBundlePath} -> ${outputPaths.zipPath}`)
246
+
247
+ if (process.platform !== "win32") {
248
+ throw new Error("Packaging is only implemented for Windows and macOS right now")
249
+ }
250
+
251
+ if (!dryRun) {
252
+ await mkdir(outputPaths.packagesDir, { recursive: true })
253
+ await rm(outputPaths.zipPath, { force: true })
254
+ }
255
+
256
+ const literalSourcePath = escapePowerShellLiteral(outputPaths.appBundlePath)
257
+ const literalDestinationPath = escapePowerShellLiteral(outputPaths.zipPath)
258
+ const archiveCommand = [
259
+ "$ErrorActionPreference = 'Stop'",
260
+ `Compress-Archive -LiteralPath '${literalSourcePath}' -DestinationPath '${literalDestinationPath}' -CompressionLevel Optimal`
261
+ ].join("; ")
262
+
263
+ await runCommand(
264
+ "powershell.exe",
265
+ ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", archiveCommand],
266
+ { dryRun }
267
+ )
268
+ }
@@ -1,4 +1,4 @@
1
- import { spawn, spawnSync } from "node:child_process"
1
+ import { spawn, spawnSync } from "node:child_process"
2
2
 
3
3
  import { logger } from "./logger.js"
4
4
 
@@ -44,32 +44,32 @@ export function getInstallCommandArgs(packageManager = "npm") {
44
44
  return ["install"]
45
45
  }
46
46
 
47
- export function formatCommand(command, args = []) {
48
- return [command, ...args].join(" ")
49
- }
50
-
51
- function shouldUseShell(command) {
52
- return process.platform === "win32" && typeof command === "string" && /\.(cmd|bat)$/i.test(command)
53
- }
54
-
55
- export async function runCommand(command, args = [], options = {}) {
56
- const { cwd, dryRun = false, env } = options
57
-
58
- logger.info(`$ ${formatCommand(command, args)}`)
47
+ export function formatCommand(command, args = []) {
48
+ return [command, ...args].join(" ")
49
+ }
50
+
51
+ function shouldUseShell(command) {
52
+ return process.platform === "win32" && typeof command === "string" && /\.(cmd|bat)$/i.test(command)
53
+ }
54
+
55
+ export async function runCommand(command, args = [], options = {}) {
56
+ const { cwd, dryRun = false, env } = options
57
+
58
+ logger.info(`$ ${formatCommand(command, args)}`)
59
59
 
60
60
  if (dryRun) {
61
61
  return
62
62
  }
63
63
 
64
- await new Promise((resolve, reject) => {
65
- const child = spawn(command, args, {
66
- cwd,
67
- env: env ? { ...process.env, ...env } : process.env,
68
- stdio: "inherit",
69
- shell: shouldUseShell(command)
70
- })
71
-
72
- child.once("error", reject)
64
+ await new Promise((resolve, reject) => {
65
+ const child = spawn(command, args, {
66
+ cwd,
67
+ env: env ? { ...process.env, ...env } : process.env,
68
+ stdio: "inherit",
69
+ shell: shouldUseShell(command)
70
+ })
71
+
72
+ child.once("error", reject)
73
73
  child.once("exit", (code, signal) => {
74
74
  if (code === 0) {
75
75
  resolve(undefined)
@@ -92,13 +92,13 @@ export async function captureCommandOutput(command, args = [], options = {}) {
92
92
 
93
93
  await Promise.resolve()
94
94
 
95
- return await new Promise((resolve, reject) => {
96
- const child = spawn(command, args, {
97
- cwd,
98
- env: env ? { ...process.env, ...env } : process.env,
99
- stdio: ["ignore", "pipe", "pipe"],
100
- shell: shouldUseShell(command)
101
- })
95
+ return await new Promise((resolve, reject) => {
96
+ const child = spawn(command, args, {
97
+ cwd,
98
+ env: env ? { ...process.env, ...env } : process.env,
99
+ stdio: ["ignore", "pipe", "pipe"],
100
+ shell: shouldUseShell(command)
101
+ })
102
102
 
103
103
  let stdout = ""
104
104
  let stderr = ""
@@ -129,23 +129,23 @@ export async function captureCommandOutput(command, args = [], options = {}) {
129
129
  })
130
130
  }
131
131
 
132
- export function spawnCommand(command, args = [], options = {}) {
133
- const { cwd, dryRun = false, env } = options
134
-
135
- logger.info(`$ ${formatCommand(command, args)}`)
132
+ export function spawnCommand(command, args = [], options = {}) {
133
+ const { cwd, dryRun = false, env } = options
134
+
135
+ logger.info(`$ ${formatCommand(command, args)}`)
136
136
 
137
137
  if (dryRun) {
138
138
  return null
139
139
  }
140
140
 
141
- return spawn(command, args, {
142
- cwd,
143
- env: env ? { ...process.env, ...env } : process.env,
144
- detached: process.platform !== "win32",
145
- stdio: "inherit",
146
- shell: shouldUseShell(command)
147
- })
148
- }
141
+ return spawn(command, args, {
142
+ cwd,
143
+ env: env ? { ...process.env, ...env } : process.env,
144
+ detached: process.platform !== "win32",
145
+ stdio: "inherit",
146
+ shell: shouldUseShell(command)
147
+ })
148
+ }
149
149
 
150
150
  export function waitForProcessExit(child, label) {
151
151
  return new Promise((resolve, reject) => {
@@ -167,120 +167,120 @@ export function waitForProcessExit(child, label) {
167
167
  })
168
168
  }
169
169
 
170
- export function registerChildCleanup(children) {
171
- const activeChildren = children.filter(Boolean)
172
- let cleanupPromise = null
173
-
174
- const waitForChildExit = (child, timeoutMs = 3000) =>
175
- new Promise((resolve) => {
176
- if (!child || child.exitCode !== null || child.signalCode !== null) {
177
- resolve(true)
178
- return
179
- }
180
-
181
- const handleExit = () => {
182
- clearTimeout(timeout)
183
- child.off("error", handleError)
184
- resolve(true)
185
- }
186
-
187
- const handleError = () => {
188
- clearTimeout(timeout)
189
- child.off("exit", handleExit)
190
- resolve(true)
191
- }
192
-
193
- const timeout = setTimeout(() => {
194
- child.off("exit", handleExit)
195
- child.off("error", handleError)
196
- resolve(false)
197
- }, timeoutMs)
198
-
199
- child.once("exit", handleExit)
200
- child.once("error", handleError)
201
- })
202
-
203
- const terminateChild = async (child) => {
204
- if (!child || child.pid == null || child.exitCode !== null || child.signalCode !== null) {
205
- return
206
- }
207
-
208
- if (process.platform === "win32") {
209
- const result = spawnSync("taskkill.exe", ["/pid", String(child.pid), "/t", "/f"], {
210
- stdio: "ignore",
211
- windowsHide: true
212
- })
213
-
214
- if (result.status !== 0 && child.exitCode === null && child.signalCode === null) {
215
- try {
216
- child.kill("SIGTERM")
217
- } catch {
218
- // Ignore late shutdown races.
219
- }
220
- }
221
-
222
- await waitForChildExit(child, 2000)
223
- return
224
- }
225
-
226
- try {
227
- process.kill(-child.pid, "SIGTERM")
228
- } catch (error) {
229
- if (error?.code !== "ESRCH") {
230
- try {
231
- child.kill("SIGTERM")
232
- } catch {
233
- // Ignore late shutdown races.
234
- }
235
- }
236
- }
237
-
238
- if (await waitForChildExit(child, 3000)) {
239
- return
240
- }
241
-
242
- try {
243
- process.kill(-child.pid, "SIGKILL")
244
- } catch (error) {
245
- if (error?.code !== "ESRCH") {
246
- try {
247
- child.kill("SIGKILL")
248
- } catch {
249
- // Ignore late shutdown races.
250
- }
251
- }
252
- }
253
-
254
- await waitForChildExit(child, 1000)
255
- }
256
-
257
- const cleanup = async () => {
258
- if (cleanupPromise) {
259
- return cleanupPromise
260
- }
261
-
262
- cleanupPromise = (async () => {
263
- await Promise.all(activeChildren.map((child) => terminateChild(child)))
264
- })()
265
-
266
- return cleanupPromise
267
- }
268
-
269
- const handleSignal = () => {
270
- void cleanup().finally(() => {
271
- process.exit(0)
272
- })
273
- }
274
-
275
- process.on("SIGINT", handleSignal)
276
- process.on("SIGTERM", handleSignal)
277
-
278
- return async () => {
279
- process.off("SIGINT", handleSignal)
280
- process.off("SIGTERM", handleSignal)
281
- await cleanup()
282
- }
283
- }
170
+ export function registerChildCleanup(children) {
171
+ const activeChildren = children.filter(Boolean)
172
+ let cleanupPromise = null
173
+
174
+ const waitForChildExit = (child, timeoutMs = 3000) =>
175
+ new Promise((resolve) => {
176
+ if (!child || child.exitCode !== null || child.signalCode !== null) {
177
+ resolve(true)
178
+ return
179
+ }
180
+
181
+ const handleExit = () => {
182
+ clearTimeout(timeout)
183
+ child.off("error", handleError)
184
+ resolve(true)
185
+ }
186
+
187
+ const handleError = () => {
188
+ clearTimeout(timeout)
189
+ child.off("exit", handleExit)
190
+ resolve(true)
191
+ }
192
+
193
+ const timeout = setTimeout(() => {
194
+ child.off("exit", handleExit)
195
+ child.off("error", handleError)
196
+ resolve(false)
197
+ }, timeoutMs)
198
+
199
+ child.once("exit", handleExit)
200
+ child.once("error", handleError)
201
+ })
202
+
203
+ const terminateChild = async (child) => {
204
+ if (!child || child.pid == null || child.exitCode !== null || child.signalCode !== null) {
205
+ return
206
+ }
207
+
208
+ if (process.platform === "win32") {
209
+ const result = spawnSync("taskkill.exe", ["/pid", String(child.pid), "/t", "/f"], {
210
+ stdio: "ignore",
211
+ windowsHide: true
212
+ })
213
+
214
+ if (result.status !== 0 && child.exitCode === null && child.signalCode === null) {
215
+ try {
216
+ child.kill("SIGTERM")
217
+ } catch {
218
+ // Ignore late shutdown races.
219
+ }
220
+ }
221
+
222
+ await waitForChildExit(child, 2000)
223
+ return
224
+ }
225
+
226
+ try {
227
+ process.kill(-child.pid, "SIGTERM")
228
+ } catch (error) {
229
+ if (error?.code !== "ESRCH") {
230
+ try {
231
+ child.kill("SIGTERM")
232
+ } catch {
233
+ // Ignore late shutdown races.
234
+ }
235
+ }
236
+ }
237
+
238
+ if (await waitForChildExit(child, 3000)) {
239
+ return
240
+ }
241
+
242
+ try {
243
+ process.kill(-child.pid, "SIGKILL")
244
+ } catch (error) {
245
+ if (error?.code !== "ESRCH") {
246
+ try {
247
+ child.kill("SIGKILL")
248
+ } catch {
249
+ // Ignore late shutdown races.
250
+ }
251
+ }
252
+ }
253
+
254
+ await waitForChildExit(child, 1000)
255
+ }
256
+
257
+ const cleanup = async () => {
258
+ if (cleanupPromise) {
259
+ return cleanupPromise
260
+ }
261
+
262
+ cleanupPromise = (async () => {
263
+ await Promise.all(activeChildren.map((child) => terminateChild(child)))
264
+ })()
265
+
266
+ return cleanupPromise
267
+ }
268
+
269
+ const handleSignal = () => {
270
+ void cleanup().finally(() => {
271
+ process.exit(0)
272
+ })
273
+ }
274
+
275
+ process.on("SIGINT", handleSignal)
276
+ process.on("SIGTERM", handleSignal)
277
+
278
+ return async () => {
279
+ process.off("SIGINT", handleSignal)
280
+ process.off("SIGTERM", handleSignal)
281
+ await cleanup()
282
+ }
283
+ }
284
284
 
285
285
  export async function waitForUrl(url, options = {}) {
286
286
  const { intervalMs = 250, timeoutMs = 15000 } = options