@shawnstack/quickforge 1.3.17 → 1.3.19

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.
Files changed (133) hide show
  1. package/README.md +10 -10
  2. package/bin/quickforge.mjs +258 -49
  3. package/dist/assets/anthropic-Bj3HAZgj.js +39 -0
  4. package/dist/assets/azure-openai-responses-IdZZrSrI.js +1 -0
  5. package/dist/assets/github-copilot-headers-CMb2BbzT.js +1 -0
  6. package/dist/assets/google-Brt_lS1J.js +1 -0
  7. package/dist/assets/{google-shared-XhYUKiGZ.js → google-shared-CLc4ziON.js} +3 -3
  8. package/dist/assets/google-vertex-B6HsoZ34.js +1 -0
  9. package/dist/assets/{index-Dm7aEWvT.js → index-D0CVLdX_.js} +525 -489
  10. package/dist/assets/index-D0W9hAl_.css +3 -0
  11. package/dist/assets/{mistral-DxhS4Wkn.js → mistral-CenXqwPz.js} +3 -3
  12. package/dist/assets/openai-codex-responses-D9ffGwbj.js +7 -0
  13. package/dist/assets/openai-completions-eWdeSGBG.js +5 -0
  14. package/dist/assets/openai-responses-Cavpmjeu.js +1 -0
  15. package/dist/assets/{openai-responses-shared-f_P3e1nz.js → openai-responses-shared-DF3ZGaUx.js} +5 -3
  16. package/dist/assets/transform-messages-CmnxG9RB.js +1 -0
  17. package/dist/index.html +2 -2
  18. package/node_modules/@anthropic-ai/sdk/CHANGELOG.md +34 -0
  19. package/node_modules/@anthropic-ai/sdk/bin/migration-config.json +185 -0
  20. package/node_modules/@anthropic-ai/sdk/package.json +1 -1
  21. package/node_modules/@anthropic-ai/sdk/resources/beta/beta.js +4 -0
  22. package/node_modules/@anthropic-ai/sdk/resources/beta/beta.mjs +4 -0
  23. package/node_modules/@anthropic-ai/sdk/resources/beta/files.js +5 -5
  24. package/node_modules/@anthropic-ai/sdk/resources/beta/files.mjs +5 -5
  25. package/node_modules/@anthropic-ai/sdk/resources/beta/index.js +11 -9
  26. package/node_modules/@anthropic-ai/sdk/resources/beta/index.mjs +1 -0
  27. package/node_modules/@anthropic-ai/sdk/resources/beta/memory-stores/index.js +11 -0
  28. package/node_modules/@anthropic-ai/sdk/resources/beta/memory-stores/index.mjs +5 -0
  29. package/node_modules/@anthropic-ai/sdk/resources/beta/memory-stores/memories.js +130 -0
  30. package/node_modules/@anthropic-ai/sdk/resources/beta/memory-stores/memories.mjs +126 -0
  31. package/node_modules/@anthropic-ai/sdk/resources/beta/memory-stores/memory-stores.js +145 -0
  32. package/node_modules/@anthropic-ai/sdk/resources/beta/memory-stores/memory-stores.mjs +140 -0
  33. package/node_modules/@anthropic-ai/sdk/resources/beta/memory-stores/memory-versions.js +81 -0
  34. package/node_modules/@anthropic-ai/sdk/resources/beta/memory-stores/memory-versions.mjs +77 -0
  35. package/node_modules/@anthropic-ai/sdk/resources/beta/memory-stores.js +6 -0
  36. package/node_modules/@anthropic-ai/sdk/resources/beta/memory-stores.mjs +3 -0
  37. package/node_modules/@anthropic-ai/sdk/tools/memory/node.js +12 -5
  38. package/node_modules/@anthropic-ai/sdk/tools/memory/node.mjs +12 -5
  39. package/node_modules/@anthropic-ai/sdk/version.js +1 -1
  40. package/node_modules/@anthropic-ai/sdk/version.mjs +1 -1
  41. package/node_modules/@aws-sdk/client-bedrock-runtime/package.json +5 -5
  42. package/node_modules/@aws-sdk/core/package.json +2 -2
  43. package/node_modules/@aws-sdk/credential-provider-env/package.json +2 -2
  44. package/node_modules/@aws-sdk/credential-provider-http/dist-cjs/fromHttp/fromHttp.js +12 -6
  45. package/node_modules/@aws-sdk/credential-provider-http/dist-es/fromHttp/fromHttp.js +12 -6
  46. package/node_modules/@aws-sdk/credential-provider-http/package.json +3 -2
  47. package/node_modules/@aws-sdk/credential-provider-ini/package.json +9 -9
  48. package/node_modules/@aws-sdk/credential-provider-login/package.json +3 -3
  49. package/node_modules/@aws-sdk/credential-provider-node/package.json +7 -7
  50. package/node_modules/@aws-sdk/credential-provider-process/package.json +2 -2
  51. package/node_modules/@aws-sdk/credential-provider-sso/package.json +4 -4
  52. package/node_modules/@aws-sdk/credential-provider-web-identity/package.json +3 -3
  53. package/node_modules/@aws-sdk/middleware-websocket/package.json +2 -2
  54. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/cognito-identity/index.js +1 -1
  55. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/signin/index.js +1 -1
  56. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sso/index.js +1 -1
  57. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sso-oidc/index.js +1 -1
  58. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sts/index.js +1 -1
  59. package/node_modules/@aws-sdk/nested-clients/package.json +3 -3
  60. package/node_modules/@aws-sdk/signature-v4-multi-region/package.json +1 -2
  61. package/node_modules/@aws-sdk/token-providers/package.json +3 -3
  62. package/node_modules/@aws-sdk/xml-builder/package.json +2 -2
  63. package/node_modules/@mariozechner/pi-agent-core/README.md +14 -0
  64. package/node_modules/@mariozechner/pi-agent-core/dist/agent-loop.js +9 -0
  65. package/node_modules/@mariozechner/pi-agent-core/dist/agent.js +1 -1
  66. package/node_modules/@mariozechner/pi-agent-core/package.json +2 -2
  67. package/node_modules/@mariozechner/pi-ai/README.md +20 -31
  68. package/node_modules/@mariozechner/pi-ai/dist/env-api-keys.js +7 -0
  69. package/node_modules/@mariozechner/pi-ai/dist/index.js +2 -0
  70. package/node_modules/@mariozechner/pi-ai/dist/models.generated.js +2420 -1213
  71. package/node_modules/@mariozechner/pi-ai/dist/models.js +28 -20
  72. package/node_modules/@mariozechner/pi-ai/dist/providers/amazon-bedrock.js +11 -11
  73. package/node_modules/@mariozechner/pi-ai/dist/providers/anthropic.js +43 -26
  74. package/node_modules/@mariozechner/pi-ai/dist/providers/azure-openai-responses.js +12 -6
  75. package/node_modules/@mariozechner/pi-ai/dist/providers/cloudflare.js +10 -3
  76. package/node_modules/@mariozechner/pi-ai/dist/providers/google-shared.js +4 -13
  77. package/node_modules/@mariozechner/pi-ai/dist/providers/google-vertex.js +4 -3
  78. package/node_modules/@mariozechner/pi-ai/dist/providers/google.js +4 -3
  79. package/node_modules/@mariozechner/pi-ai/dist/providers/mistral.js +8 -7
  80. package/node_modules/@mariozechner/pi-ai/dist/providers/openai-codex-responses.js +296 -41
  81. package/node_modules/@mariozechner/pi-ai/dist/providers/openai-completions.js +169 -153
  82. package/node_modules/@mariozechner/pi-ai/dist/providers/openai-responses-shared.js +14 -1
  83. package/node_modules/@mariozechner/pi-ai/dist/providers/openai-responses.js +22 -8
  84. package/node_modules/@mariozechner/pi-ai/dist/providers/register-builtins.js +0 -18
  85. package/node_modules/@mariozechner/pi-ai/dist/providers/simple-options.js +1 -0
  86. package/node_modules/@mariozechner/pi-ai/dist/session-resources.js +22 -0
  87. package/node_modules/@mariozechner/pi-ai/dist/utils/diagnostics.js +25 -0
  88. package/node_modules/@mariozechner/pi-ai/dist/utils/oauth/index.js +0 -10
  89. package/node_modules/@mariozechner/pi-ai/dist/utils/oauth/openai-codex.js +25 -14
  90. package/node_modules/@mariozechner/pi-ai/dist/utils/overflow.js +14 -0
  91. package/node_modules/@mariozechner/pi-ai/package.json +2 -6
  92. package/package.json +3 -3
  93. package/server/agent-manager.mjs +279 -12
  94. package/server/auto-compaction.mjs +1 -2
  95. package/server/conversation-compaction.mjs +0 -5
  96. package/server/index.mjs +1 -0
  97. package/server/routes/static.mjs +1 -0
  98. package/server/routes/tools.mjs +3 -1
  99. package/server/session-utils.mjs +6 -1
  100. package/server/share-store.mjs +27 -4
  101. package/server/subagents.mjs +101 -0
  102. package/server/system-prompt.mjs +30 -1
  103. package/server/tools/definitions.mjs +20 -0
  104. package/server/tools/index.mjs +956 -726
  105. package/dist/assets/anthropic-Ck2DxOfr.js +0 -39
  106. package/dist/assets/azure-openai-responses-DIoz5q4Z.js +0 -1
  107. package/dist/assets/github-copilot-headers-CrI0CIJ7.js +0 -1
  108. package/dist/assets/google-Dau-4ve_.js +0 -1
  109. package/dist/assets/google-gemini-cli-DttMmbGb.js +0 -2
  110. package/dist/assets/google-vertex-BeukMl44.js +0 -1
  111. package/dist/assets/index-DgJVElbv.css +0 -3
  112. package/dist/assets/openai-codex-responses-X3sTzNAa.js +0 -7
  113. package/dist/assets/openai-completions-CRB9Vm0w.js +0 -5
  114. package/dist/assets/openai-responses-DXluu3oi.js +0 -1
  115. package/dist/assets/transform-messages-CV4kCtBB.js +0 -1
  116. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/LICENSE +0 -201
  117. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/README.md +0 -62
  118. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-cjs/index.js +0 -156
  119. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/constants.js +0 -2
  120. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/fromEnvSigningName.js +0 -16
  121. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/fromSso.js +0 -80
  122. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/fromStatic.js +0 -8
  123. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/getNewSsoOidcToken.js +0 -11
  124. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/getSsoOidcClient.js +0 -10
  125. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/index.js +0 -4
  126. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/nodeProvider.js +0 -5
  127. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/validateTokenExpiry.js +0 -7
  128. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/validateTokenKey.js +0 -7
  129. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/dist-es/writeSSOTokenToFile.js +0 -8
  130. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/package.json +0 -69
  131. package/node_modules/@mariozechner/pi-ai/dist/providers/google-gemini-cli.js +0 -779
  132. package/node_modules/@mariozechner/pi-ai/dist/utils/oauth/google-antigravity.js +0 -377
  133. package/node_modules/@mariozechner/pi-ai/dist/utils/oauth/google-gemini-cli.js +0 -482
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # 速构 QuickForge
2
2
 
3
3
  <p align="center">
4
- <img alt="Version" src="https://img.shields.io/badge/version-1.3.17-blue" />
4
+ <img alt="Version" src="https://img.shields.io/badge/version-1.3.19-blue" />
5
5
  <img alt="License" src="https://img.shields.io/badge/license-MIT-green" />
6
6
  <img alt="Node" src="https://img.shields.io/badge/node-%3E%3D20-brightgreen" />
7
7
  <img alt="React" src="https://img.shields.io/badge/react-19-61DAFB?logo=react" />
@@ -65,7 +65,7 @@ QuickForge 的工具能力很直接,因此也需要谨慎使用:
65
65
  #### 从 npm 安装
66
66
 
67
67
  ```bash
68
- npm install -g @shawnstack/quickforge@1.3.17
68
+ npm install -g @shawnstack/quickforge@1.3.19
69
69
  qf
70
70
 
71
71
  # CLI 工具
@@ -79,17 +79,17 @@ qf update
79
79
  当前版本的离线包:
80
80
 
81
81
  ```text
82
- package-offline/shawnstack-quickforge-1.3.17.tgz
82
+ package-offline/shawnstack-quickforge-1.3.19.tgz
83
83
  ```
84
84
 
85
85
  在安装了 Node.js 20+ 和 npm 的机器上执行:
86
86
 
87
87
  ```bash
88
- npm install -g ./package-offline/shawnstack-quickforge-1.3.17.tgz
88
+ npm install -g ./package-offline/shawnstack-quickforge-1.3.19.tgz
89
89
  qf
90
90
  ```
91
91
 
92
- 该包由 `v1.3.17` 标签生成,包含离线安装所需的运行时依赖。
92
+ 该包由 `v1.3.19` 标签生成,包含离线安装所需的运行时依赖。
93
93
 
94
94
  ### 本地开发
95
95
 
@@ -228,7 +228,7 @@ QuickForge intentionally exposes powerful local capabilities, so the boundaries
228
228
  #### npm
229
229
 
230
230
  ```bash
231
- npm install -g @shawnstack/quickforge@1.3.17
231
+ npm install -g @shawnstack/quickforge@1.3.19
232
232
  qf
233
233
 
234
234
  # CLI utilities
@@ -239,20 +239,20 @@ qf update
239
239
 
240
240
  #### Offline tarball
241
241
 
242
- The offline release package for `v1.3.17` is:
242
+ The offline release package for `v1.3.19` is:
243
243
 
244
244
  ```text
245
- package-offline/shawnstack-quickforge-1.3.17.tgz
245
+ package-offline/shawnstack-quickforge-1.3.19.tgz
246
246
  ```
247
247
 
248
248
  Install it on a machine with Node.js 20+ and npm:
249
249
 
250
250
  ```bash
251
- npm install -g ./package-offline/shawnstack-quickforge-1.3.17.tgz
251
+ npm install -g ./package-offline/shawnstack-quickforge-1.3.19.tgz
252
252
  qf
253
253
  ```
254
254
 
255
- The package was generated from tag `v1.3.17` and includes bundled runtime dependencies for offline installation.
255
+ The package was generated from tag `v1.3.19` and includes bundled runtime dependencies for offline installation.
256
256
 
257
257
  ### Local development
258
258
 
@@ -266,33 +266,178 @@ function getLogFile() {
266
266
  return path.join(getDataDir(), 'logs', `server-${date}.log`)
267
267
  }
268
268
 
269
- async function cmdStop() {
270
- const pid = await readPid()
271
- if (!pid) {
272
- console.log('QuickForge is not running (no PID file found).')
273
- return
269
+ function sleep(ms) {
270
+ return new Promise((resolve) => setTimeout(resolve, ms))
271
+ }
272
+
273
+ function getPort() {
274
+ return String(process.env.QUICKFORGE_PORT || '5176')
275
+ }
276
+
277
+ function getDisplayHost() {
278
+ const host = process.env.QUICKFORGE_HOST || '0.0.0.0'
279
+ return host === '0.0.0.0' ? '<LAN-IP>' : host
280
+ }
281
+
282
+ function getProbeHost() {
283
+ const host = process.env.QUICKFORGE_HOST || '127.0.0.1'
284
+ if (host === '0.0.0.0' || host === '::') return '127.0.0.1'
285
+ return host
286
+ }
287
+
288
+ function getHealthUrl() {
289
+ return `http://${getProbeHost()}:${getPort()}/api/health`
290
+ }
291
+
292
+ function getServiceUrl() {
293
+ return `http://${getDisplayHost()}:${getPort()}`
294
+ }
295
+
296
+ function formatHealth(health) {
297
+ if (!health) return 'unavailable'
298
+ const parts = []
299
+ if (health.pid) parts.push(`PID ${health.pid}`)
300
+ if (health.bootId) parts.push(`bootId ${health.bootId}`)
301
+ if (health.startedAt) parts.push(`started ${health.startedAt}`)
302
+ if (health.mode) parts.push(`mode ${health.mode}`)
303
+ return parts.join(', ') || 'available'
304
+ }
305
+
306
+ async function fetchHealth(timeoutMs = 800) {
307
+ const controller = new AbortController()
308
+ const timeout = setTimeout(() => controller.abort(), timeoutMs)
309
+ timeout.unref?.()
310
+
311
+ try {
312
+ const response = await fetch(getHealthUrl(), {
313
+ headers: { accept: 'application/json' },
314
+ signal: controller.signal,
315
+ })
316
+ if (!response.ok) return null
317
+ const payload = await response.json()
318
+ if (!payload || payload.ok !== true || !payload.pid) return null
319
+ return payload
320
+ } catch {
321
+ return null
322
+ } finally {
323
+ clearTimeout(timeout)
324
+ }
325
+ }
326
+
327
+ async function waitForHealth({ expectedPid = null, previousBootId = null, requireChanged = false, timeoutMs = 15000 } = {}) {
328
+ const deadline = Date.now() + timeoutMs
329
+ while (Date.now() < deadline) {
330
+ const health = await fetchHealth()
331
+ if (health) {
332
+ const pidMatches = !expectedPid || Number(health.pid) === Number(expectedPid)
333
+ const bootChanged = !requireChanged || !previousBootId || health.bootId !== previousBootId
334
+ if (pidMatches && bootChanged) return health
335
+ }
336
+ await sleep(300)
337
+ }
338
+ return null
339
+ }
340
+
341
+ async function waitForProcessExit(pid, timeoutMs = 10000) {
342
+ const deadline = Date.now() + timeoutMs
343
+ while (Date.now() < deadline) {
344
+ if (!isProcessRunning(pid)) return true
345
+ await sleep(250)
274
346
  }
347
+ return !isProcessRunning(pid)
348
+ }
275
349
 
276
- if (!isProcessRunning(pid)) {
277
- console.log(`QuickForge PID ${pid} is not running. Cleaning up PID file.`)
350
+ async function resolveRunningService() {
351
+ const pidFilePid = await readPid()
352
+ const pidFileAlive = pidFilePid ? isProcessRunning(pidFilePid) : false
353
+ if (pidFilePid && !pidFileAlive) {
354
+ console.log(`Found stale PID file (${pidFilePid}); cleaning it up.`)
278
355
  await removePid()
279
- return
280
356
  }
281
357
 
282
- console.log(`Stopping QuickForge (PID ${pid})...`)
358
+ const health = await fetchHealth()
359
+ if (health?.pid && isProcessRunning(Number(health.pid))) {
360
+ return {
361
+ pid: Number(health.pid),
362
+ source: 'health',
363
+ health,
364
+ pidFilePid,
365
+ pidFileAlive,
366
+ }
367
+ }
368
+
369
+ if (pidFileAlive) {
370
+ return {
371
+ pid: pidFilePid,
372
+ source: 'pid-file',
373
+ health: null,
374
+ pidFilePid,
375
+ pidFileAlive,
376
+ }
377
+ }
378
+
379
+ return {
380
+ pid: null,
381
+ source: 'none',
382
+ health: null,
383
+ pidFilePid,
384
+ pidFileAlive,
385
+ }
386
+ }
387
+
388
+ async function terminateProcess(pid) {
389
+ if (!pid || !isProcessRunning(pid)) return true
390
+
283
391
  try {
284
392
  process.kill(pid, 'SIGTERM')
285
393
  } catch {
286
- // force kill on Windows
287
- try {
288
- process.kill(pid, 'SIGKILL')
289
- } catch {
290
- // ignore
291
- }
394
+ // The process may have already exited.
395
+ }
396
+
397
+ if (await waitForProcessExit(pid, 10000)) return true
398
+
399
+ console.log(`PID ${pid} did not exit after SIGTERM; forcing stop...`)
400
+ try {
401
+ process.kill(pid, 'SIGKILL')
402
+ } catch {
403
+ // The process may have already exited.
292
404
  }
293
405
 
406
+ return waitForProcessExit(pid, 5000)
407
+ }
408
+
409
+ async function stopResolvedService(service) {
410
+ if (!service?.pid) {
411
+ console.log('QuickForge is not running.')
412
+ return false
413
+ }
414
+
415
+ console.log(`Stopping QuickForge (PID ${service.pid}, source: ${service.source})...`)
416
+ if (service.health) console.log(`Current service: ${formatHealth(service.health)}`)
417
+ if (service.pidFilePid && service.pidFilePid !== service.pid) {
418
+ console.log(`PID file points to ${service.pidFilePid}, but active service is PID ${service.pid}; using active service.`)
419
+ }
420
+
421
+ const stopped = await terminateProcess(service.pid)
294
422
  await removePid()
295
- console.log('QuickForge stopped.')
423
+
424
+ if (!stopped) {
425
+ throw new Error(`Timed out stopping QuickForge PID ${service.pid}.`)
426
+ }
427
+
428
+ const remaining = await fetchHealth()
429
+ if (remaining?.pid) {
430
+ console.log(`Warning: /api/health still responds after stop: ${formatHealth(remaining)}`)
431
+ } else {
432
+ console.log('QuickForge stopped.')
433
+ }
434
+
435
+ return true
436
+ }
437
+
438
+ async function cmdStop() {
439
+ const service = await resolveRunningService()
440
+ await stopResolvedService(service)
296
441
  }
297
442
 
298
443
  function lanModeEnabled() {
@@ -309,23 +454,26 @@ function prepareEnvForCommand() {
309
454
  return env
310
455
  }
311
456
 
312
- function getServiceUrl() {
313
- const host = process.env.QUICKFORGE_HOST || '0.0.0.0'
314
- const displayHost = host === '0.0.0.0' ? '<LAN-IP>' : host
315
- const port = process.env.QUICKFORGE_PORT || '5176'
316
- return `http://${displayHost}:${port}`
317
- }
318
-
319
- async function cmdStart() {
320
- const existingPid = await readPid()
321
- if (existingPid && isProcessRunning(existingPid)) {
322
- console.log(`QuickForge is already running (PID ${existingPid}).`)
457
+ async function startService({ previousBootId = null } = {}) {
458
+ const existing = await resolveRunningService()
459
+ if (existing.pid) {
460
+ console.log(`QuickForge is already running (PID ${existing.pid}).`)
461
+ if (existing.health) console.log(`Current service: ${formatHealth(existing.health)}`)
462
+ if (existing.health?.pid && existing.pidFilePid !== existing.health.pid) {
463
+ await writePid(existing.health.pid)
464
+ console.log(`PID file updated: ${getPidFile()}`)
465
+ }
323
466
  console.log('Use "quickforge stop" to stop it first, or "quickforge restart".')
324
- return
467
+ return existing.health
325
468
  }
326
469
 
327
- // Clean up stale PID file
328
- if (existingPid) await removePid()
470
+ const serviceUrl = getServiceUrl()
471
+ const healthUrl = getHealthUrl()
472
+ const dataDir = getDataDir()
473
+ const logFile = getLogFile()
474
+
475
+ console.log(`Starting QuickForge on ${serviceUrl}...`)
476
+ console.log(`Health check: ${healthUrl}`)
329
477
 
330
478
  const child = spawn(process.execPath, [serverScript], {
331
479
  detached: true,
@@ -334,13 +482,37 @@ async function cmdStart() {
334
482
  env: prepareEnvForCommand(),
335
483
  })
336
484
 
337
- await writePid(child.pid)
485
+ let exitInfo = null
486
+ child.once('exit', (code, signal) => {
487
+ exitInfo = { code, signal }
488
+ })
489
+
490
+ await new Promise((resolve, reject) => {
491
+ child.once('spawn', resolve)
492
+ child.once('error', reject)
493
+ })
494
+
495
+ console.log(`Spawned server process (PID ${child.pid}). Waiting for service readiness...`)
496
+ const health = await waitForHealth({ expectedPid: child.pid, previousBootId, requireChanged: Boolean(previousBootId) })
497
+
498
+ if (!health) {
499
+ const exitReason = exitInfo
500
+ ? `process exited early (code ${exitInfo.code ?? 'null'}, signal ${exitInfo.signal ?? 'null'})`
501
+ : 'health check timed out'
502
+
503
+ if (!exitInfo && isProcessRunning(child.pid)) {
504
+ console.log(`Startup ${exitReason}; stopping spawned PID ${child.pid}...`)
505
+ await terminateProcess(child.pid)
506
+ }
507
+
508
+ await removePid()
509
+ throw new Error(`QuickForge failed to start: ${exitReason}. Check log: ${logFile}`)
510
+ }
511
+
512
+ await writePid(health.pid)
338
513
  child.unref()
339
514
 
340
- const serviceUrl = getServiceUrl()
341
- const dataDir = getDataDir()
342
- const logFile = getLogFile()
343
- console.log(`QuickForge started (PID ${child.pid}).`)
515
+ console.log(`QuickForge started and verified (${formatHealth(health)}).`)
344
516
  console.log(`Open: ${serviceUrl}`)
345
517
  console.log(`Data: ${dataDir}`)
346
518
  console.log(`Config: ${path.join(dataDir, 'config', 'config.json')}`)
@@ -354,31 +526,68 @@ async function cmdStart() {
354
526
  console.log(' quickforge restart Restart the background service')
355
527
  console.log(' quickforge status Check if the service is running')
356
528
  console.log(' quickforge logs Watch today\'s server log')
529
+
530
+ return health
531
+ }
532
+
533
+ async function cmdStart() {
534
+ await startService()
357
535
  }
358
536
 
359
537
  async function cmdRestart() {
360
- await cmdStop()
361
- // Small delay to let the port free up
362
- await new Promise((resolve) => setTimeout(resolve, 500))
363
- await cmdStart()
538
+ console.log('Restarting QuickForge...')
539
+ const before = await fetchHealth()
540
+ if (before) {
541
+ console.log(`Before restart: ${formatHealth(before)}`)
542
+ } else {
543
+ console.log('Before restart: no healthy service responded.')
544
+ }
545
+
546
+ const service = await resolveRunningService()
547
+ if (service.pid) {
548
+ await stopResolvedService(service)
549
+ await sleep(500)
550
+ } else {
551
+ console.log('No running QuickForge service found; starting a new one.')
552
+ }
553
+
554
+ const after = await startService({ previousBootId: before?.bootId || null })
555
+ if (!after) return
556
+
557
+ const pidChanged = !before?.pid || Number(after.pid) !== Number(before.pid)
558
+ const bootChanged = !before?.bootId || after.bootId !== before.bootId
559
+
560
+ if (pidChanged || bootChanged) {
561
+ console.log(`Restart verified: ${formatHealth(after)}`)
562
+ } else {
563
+ console.log('Warning: service responded after restart, but PID/bootId did not change.')
564
+ process.exitCode = 1
565
+ }
364
566
  }
365
567
 
366
568
  async function cmdStatus() {
367
- const pid = await readPid()
368
- if (!pid) {
369
- console.log('QuickForge is not running.')
569
+ const service = await resolveRunningService()
570
+ if (service.health) {
571
+ console.log(`QuickForge is running (${formatHealth(service.health)}).`)
572
+ console.log(`URL: ${getServiceUrl()}`)
573
+ console.log(`Health: ${getHealthUrl()}`)
574
+ console.log(`Log: ${getLogFile()}`)
575
+ if (service.pidFilePid !== service.health.pid) {
576
+ await writePid(service.health.pid)
577
+ console.log(`PID file repaired: ${getPidFile()}`)
578
+ }
579
+ console.log('Watch: quickforge logs')
370
580
  return
371
581
  }
372
582
 
373
- if (isProcessRunning(pid)) {
374
- console.log(`QuickForge is running (PID ${pid}).`)
375
- console.log(`URL: ${getServiceUrl()}`)
583
+ if (service.pid) {
584
+ console.log(`QuickForge process is running (PID ${service.pid}), but /api/health is not reachable.`)
585
+ console.log(`Health: ${getHealthUrl()}`)
376
586
  console.log(`Log: ${getLogFile()}`)
377
- console.log('Watch: quickforge logs')
378
- } else {
379
- console.log(`QuickForge PID ${pid} is stale (not running).`)
380
- await removePid()
587
+ return
381
588
  }
589
+
590
+ console.log('QuickForge is not running.')
382
591
  }
383
592
 
384
593
  async function cmdLogs() {