@shawnstack/quickforge 1.7.10 → 1.7.12

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 (124) hide show
  1. package/dist/assets/{AgentProfilesPage-Dw9orPNt.js → AgentProfilesPage-CApCpsDd.js} +1 -1
  2. package/dist/assets/ChatPanelHost-COTO7swS.js +48 -0
  3. package/dist/assets/CloudAccountSettingsPage-Be5r3dtv.js +1 -0
  4. package/dist/assets/{PluginsPage-Dj1bmsEf.js → PluginsPage-BIbIDfWY.js} +1 -1
  5. package/dist/assets/ScheduledTasksPage-BOqmBNCx.js +2 -0
  6. package/dist/assets/{SettingsWorkspacePage-CFuvSATi.js → SettingsWorkspacePage-BCRtycRN.js} +304 -304
  7. package/dist/assets/ShareLinksSettingsPage-BrxQacJn.js +1 -0
  8. package/dist/assets/{SharedConversationPage-DLo05j7c.js → SharedConversationPage-DJa0M0El.js} +1 -1
  9. package/dist/assets/TerminalDock-Cqz_9mTC.js +2 -0
  10. package/dist/assets/WorkspaceInspector-C-GOK81D.js +36 -0
  11. package/dist/assets/{abnfDiagram-VRR7QNED-CMhVoYUh.js → abnfDiagram-VRR7QNED-DGklo3PO.js} +1 -1
  12. package/dist/assets/{architectureDiagram-ZJ3FMSHR-C52qL6Hg.js → architectureDiagram-ZJ3FMSHR-CioWLVTp.js} +1 -1
  13. package/dist/assets/{blockDiagram-677ZJIJ3-CM3XbLol.js → blockDiagram-677ZJIJ3-BJWBq5e9.js} +1 -1
  14. package/dist/assets/{c4Diagram-LMCZKHZV-D7Bh1ui1.js → c4Diagram-LMCZKHZV-BacRvqFK.js} +1 -1
  15. package/dist/assets/channel-CeXY-wZo.js +1 -0
  16. package/dist/assets/{chunk-32BRIVSS-xgrtbT8e.js → chunk-32BRIVSS-B53BNE2C.js} +1 -1
  17. package/dist/assets/{chunk-52WLFC77-CDbaHw80.js → chunk-52WLFC77-CYq4hT4f.js} +1 -1
  18. package/dist/assets/{chunk-C7G6YPKG-ZMLUvfLy.js → chunk-C7G6YPKG-KFaGpP_Z.js} +1 -1
  19. package/dist/assets/{chunk-EX3LRPZG-BcQhaqt7.js → chunk-EX3LRPZG-DqVd_ihG.js} +1 -1
  20. package/dist/assets/{chunk-FWX5IMBZ-DrnrdrSu.js → chunk-FWX5IMBZ-BxRjZkOP.js} +2 -2
  21. package/dist/assets/{chunk-HOUHSVGY-tdc-Z9eW.js → chunk-HOUHSVGY-C9QO8omv.js} +1 -1
  22. package/dist/assets/{chunk-ICXQ74PX-Cd7igVGt.js → chunk-ICXQ74PX-ClbGPgLV.js} +1 -1
  23. package/dist/assets/{chunk-MOJQB5TN-CLTjV5ov.js → chunk-MOJQB5TN-Ba4wlF1J.js} +1 -1
  24. package/dist/assets/{chunk-OGEWGWER-TgvkXkFX.js → chunk-OGEWGWER-DczhQmcx.js} +1 -1
  25. package/dist/assets/{chunk-PUDLZKDR-C5GMVYup.js → chunk-PUDLZKDR-CPWpcKUF.js} +1 -1
  26. package/dist/assets/{chunk-Q4XR5HBZ-fo5-HsqY.js → chunk-Q4XR5HBZ-CiF6T--X.js} +1 -1
  27. package/dist/assets/{chunk-V7JOEXUC-CEDazV6w.js → chunk-V7JOEXUC-Dxv0zwmw.js} +1 -1
  28. package/dist/assets/{chunk-VAUOI2AC-CWYjg8zE.js → chunk-VAUOI2AC-D3J5bw1P.js} +1 -1
  29. package/dist/assets/{chunk-VR4S4FIN-C_-eEj-9.js → chunk-VR4S4FIN-BXQSwwII.js} +1 -1
  30. package/dist/assets/{chunk-WYO6CB5R-NF-MsJJR.js → chunk-WYO6CB5R-C0azl-fk.js} +2 -2
  31. package/dist/assets/{chunk-ZGVPDNZ5-CRrAYi4g.js → chunk-ZGVPDNZ5-D_8F4Akx.js} +1 -1
  32. package/dist/assets/classDiagram-OUVF2IWQ-B8M6sf49.js +1 -0
  33. package/dist/assets/classDiagram-v2-EOCWNBFH-BY7fzBDH.js +1 -0
  34. package/dist/assets/{cynefinDiagram-TSTJHNR4-D4sZ7LYI.js → cynefinDiagram-TSTJHNR4-BFlsWz0B.js} +1 -1
  35. package/dist/assets/{dagre-VKFMJZFB-BlsnSh-Q.js → dagre-VKFMJZFB-DXhpjlEs.js} +1 -1
  36. package/dist/assets/{diagram-FQU43EPY-DW1yQiRO.js → diagram-FQU43EPY-DRVVOD9k.js} +1 -1
  37. package/dist/assets/{diagram-G47NLZAW-BTUAsFjb.js → diagram-G47NLZAW-BV9Gw1tL.js} +1 -1
  38. package/dist/assets/{diagram-NH7WQ7WH-BcaomqSd.js → diagram-NH7WQ7WH-CrUkXfRY.js} +1 -1
  39. package/dist/assets/{diagram-OA4YK3LP-DDLX7Xwr.js → diagram-OA4YK3LP-CuILjn7v.js} +1 -1
  40. package/dist/assets/{diagram-WEI45ONY-CIave0An.js → diagram-WEI45ONY-nlNN3tWB.js} +1 -1
  41. package/dist/assets/{ebnfDiagram-CCIWWBDH-Cr1Y6lPu.js → ebnfDiagram-CCIWWBDH-BuDgZt9n.js} +1 -1
  42. package/dist/assets/{erDiagram-Q63AITRT-D3EgiVVH.js → erDiagram-Q63AITRT-MEwxjmtP.js} +1 -1
  43. package/dist/assets/flowDiagram-23GEKE2U-BiUP-Scq.js +1 -0
  44. package/dist/assets/{ganttDiagram-NO4QXBWP-B0oyDIqK.js → ganttDiagram-NO4QXBWP-DNw5qghE.js} +1 -1
  45. package/dist/assets/{gitGraphDiagram-IHSO6WYX-uQtr6IqK.js → gitGraphDiagram-IHSO6WYX-CaTcakAt.js} +1 -1
  46. package/dist/assets/index-BWf6y3Kh.css +3 -0
  47. package/dist/assets/index-D3GWkXJR.js +66 -0
  48. package/dist/assets/{infoDiagram-FWYZ7A6U-B_GzlLIM.js → infoDiagram-FWYZ7A6U-DwfyP7dU.js} +1 -1
  49. package/dist/assets/{ishikawaDiagram-FXEZZL3T-CKQIsSdU.js → ishikawaDiagram-FXEZZL3T-CmrzbOCV.js} +1 -1
  50. package/dist/assets/{journeyDiagram-5HDEW3XC-DrrvzZL1.js → journeyDiagram-5HDEW3XC-CYqe3sgr.js} +1 -1
  51. package/dist/assets/{kanban-definition-HUTT4EX6-CJQRTdXz.js → kanban-definition-HUTT4EX6-BMDO_mv0.js} +1 -1
  52. package/dist/assets/{line-B4DxX7tC.js → line-SbxJR8ZT.js} +1 -1
  53. package/dist/assets/lit-vendor-CinchGmI.js +2 -0
  54. package/dist/assets/local-tools-DY0yEU3T.js +350 -0
  55. package/dist/assets/{mcp-servers-dialog-CwHp0K4q.js → mcp-servers-dialog-7Xwu2RVW.js} +2 -2
  56. package/dist/assets/{mermaid.core-DWpShXCs.js → mermaid.core-CwXTzYjs.js} +3 -3
  57. package/dist/assets/{mindmap-definition-LN4V7U3C-CEVwZD78.js → mindmap-definition-LN4V7U3C-CLyJ78mO.js} +1 -1
  58. package/dist/assets/{pegDiagram-2B236MQR-CBLkBdnQ.js → pegDiagram-2B236MQR-4qFznrSw.js} +1 -1
  59. package/dist/assets/{pi-web-ui-DV5jKjSg.js → pi-web-ui-BNuvqKDT.js} +289 -289
  60. package/dist/assets/{pieDiagram-ENE6RG2P-8tCnwtWS.js → pieDiagram-ENE6RG2P-141sGxVe.js} +1 -1
  61. package/dist/assets/{quadrantDiagram-ABIIQ3AL-CWd28cHF.js → quadrantDiagram-ABIIQ3AL-SUI44pAX.js} +1 -1
  62. package/dist/assets/{railroadDiagram-RFXS5EU6-CI4ouOBs.js → railroadDiagram-RFXS5EU6-1oiZuYBB.js} +1 -1
  63. package/dist/assets/{requirementDiagram-TGXJPOKE-DtFz7sw4.js → requirementDiagram-TGXJPOKE-CfVishLi.js} +1 -1
  64. package/dist/assets/{sankeyDiagram-HTMAVEWB--VeOwwk9.js → sankeyDiagram-HTMAVEWB-DVu7J_Z9.js} +1 -1
  65. package/dist/assets/{sequenceDiagram-DBY2YBRQ-B8AHk5Oe.js → sequenceDiagram-DBY2YBRQ-D-Ip2yiS.js} +1 -1
  66. package/dist/assets/{skills-dialog-k9ycekIU.js → skills-dialog-C_JvWbAa.js} +3 -3
  67. package/dist/assets/{stateDiagram-2N3HPSRC-etXnJ8X8.js → stateDiagram-2N3HPSRC-Cg0hWwrp.js} +1 -1
  68. package/dist/assets/stateDiagram-v2-6OUMAXLB-DhmzNQd9.js +1 -0
  69. package/dist/assets/{swimlanes-5IMT3BWC-pabIKAmn.js → swimlanes-5IMT3BWC-Co5D-cGU.js} +1 -1
  70. package/dist/assets/swimlanesDiagram-G3AALYLV-BEvomQNa.js +8 -0
  71. package/dist/assets/{timeline-definition-FHXFAJF6-C55MKm3L.js → timeline-definition-FHXFAJF6-CBk4c_Ua.js} +1 -1
  72. package/dist/assets/{vennDiagram-L72KCM5P-CgNf1Q9W.js → vennDiagram-L72KCM5P-BII7wdRx.js} +1 -1
  73. package/dist/assets/{wardleyDiagram-EHGQE667-Drt5ytec.js → wardleyDiagram-EHGQE667-CkyJJIXP.js} +1 -1
  74. package/dist/assets/{xychartDiagram-FW5EYKEG-hhoEo_Ar.js → xychartDiagram-FW5EYKEG-B1A66Ks0.js} +1 -1
  75. package/dist/index.html +6 -6
  76. package/package.json +1 -1
  77. package/server/acp/server.mjs +3 -6
  78. package/server/agent-manager.mjs +192 -48
  79. package/server/approval-store.mjs +1 -0
  80. package/server/ask-store.mjs +78 -0
  81. package/server/auto-archive.mjs +55 -8
  82. package/server/index.mjs +172 -51
  83. package/server/lan-access-cutover.mjs +21 -4
  84. package/server/maintenance/downgrade-session-state-v1.mjs +46 -85
  85. package/server/maintenance/export-session-state-v1.mjs +1 -11
  86. package/server/routes/agent.mjs +9 -0
  87. package/server/routes/backup.mjs +1 -1
  88. package/server/routes/storage.mjs +63 -49
  89. package/server/scheduled-runs-cutover.mjs +15 -1
  90. package/server/session-index-service.mjs +57 -391
  91. package/server/session-state-backup.mjs +56 -5
  92. package/server/session-state-import.mjs +174 -0
  93. package/server/session-state-maintenance.mjs +126 -0
  94. package/server/session-state-service.mjs +128 -210
  95. package/server/share-cutover.mjs +21 -4
  96. package/server/sqlite/database.mjs +142 -7
  97. package/server/sqlite/lan-access-repository.mjs +6 -4
  98. package/server/sqlite/migrations.mjs +109 -0
  99. package/server/sqlite/scheduled-task-runs-repository.mjs +6 -3
  100. package/server/sqlite/session-index-repository.mjs +59 -208
  101. package/server/sqlite/session-state-repository.mjs +337 -387
  102. package/server/sqlite/share-repository.mjs +56 -45
  103. package/server/startup-state.mjs +107 -0
  104. package/server/storage.mjs +218 -454
  105. package/server/tools/definitions.mjs +18 -0
  106. package/server/tools/index.mjs +14 -2
  107. package/server/utils/logger.mjs +15 -4
  108. package/dist/assets/ChatPanelHost-CW3-MQyL.js +0 -48
  109. package/dist/assets/CloudAccountSettingsPage-CDkKN1Sz.js +0 -1
  110. package/dist/assets/ScheduledTasksPage-C9lOTi75.js +0 -2
  111. package/dist/assets/ShareLinksSettingsPage-CH_t33A3.js +0 -1
  112. package/dist/assets/TerminalDock-CSOVub-m.js +0 -2
  113. package/dist/assets/WorkspaceInspector-DPXgLIum.js +0 -36
  114. package/dist/assets/channel-Duu1CuH9.js +0 -1
  115. package/dist/assets/classDiagram-OUVF2IWQ-Cc63fLWP.js +0 -1
  116. package/dist/assets/classDiagram-v2-EOCWNBFH-CoXU0qT6.js +0 -1
  117. package/dist/assets/flowDiagram-23GEKE2U-BS7KXcqn.js +0 -1
  118. package/dist/assets/index-DQBOm9gt.js +0 -66
  119. package/dist/assets/index-ps85guTs.css +0 -3
  120. package/dist/assets/lit-vendor-DvaQX_UQ.js +0 -2
  121. package/dist/assets/local-tools-BxfQeIUQ.js +0 -270
  122. package/dist/assets/stateDiagram-v2-6OUMAXLB-Dms3pICj.js +0 -1
  123. package/dist/assets/swimlanesDiagram-G3AALYLV-B14aWjv6.js +0 -8
  124. package/server/session-state-cutover.mjs +0 -563
@@ -1,5 +1,5 @@
1
1
  import { createHash, randomBytes } from 'node:crypto'
2
- import { getSqliteStorage } from './database.mjs'
2
+ import { getSqliteStorage, runSharedSqliteQuickCheck } from './database.mjs'
3
3
 
4
4
  // F10 share storage: `share_sessions` (strictly mapped columns + `extra_json`
5
5
  // for opaque/unknown fields) and `share_tokens` (independent token table with
@@ -301,7 +301,36 @@ function attachTokens(records, tokenRows) {
301
301
  }))
302
302
  }
303
303
 
304
- function insertShareRow(database, record, revision, createdAt, updatedAt) {
304
+ const SHARE_SESSION_UPSERT_SQL = `
305
+ INSERT INTO share_sessions (${SHARE_COLUMNS}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
306
+ ON CONFLICT(share_id) DO UPDATE SET
307
+ session_id = excluded.session_id,
308
+ permission = excluded.permission,
309
+ title_snapshot = excluded.title_snapshot,
310
+ scope = excluded.scope,
311
+ project_id = excluded.project_id,
312
+ password_hash = excluded.password_hash,
313
+ password_salt = excluded.password_salt,
314
+ password_version = excluded.password_version,
315
+ auth_version = excluded.auth_version,
316
+ allow_cloud_usage = excluded.allow_cloud_usage,
317
+ created_at = excluded.created_at,
318
+ updated_at = excluded.updated_at,
319
+ expires_at = excluded.expires_at,
320
+ revoked_at = excluded.revoked_at,
321
+ superseded_at = excluded.superseded_at,
322
+ access_count = excluded.access_count,
323
+ last_accessed_at = excluded.last_accessed_at,
324
+ created_from_host = excluded.created_from_host,
325
+ last_updated_from_host = excluded.last_updated_from_host,
326
+ revision = excluded.revision,
327
+ record_digest = excluded.record_digest,
328
+ deleted_at = excluded.deleted_at,
329
+ extra_json = excluded.extra_json
330
+ `
331
+
332
+ // The optional statement lets the cutover replaceAll reuse one precompiled upsert.
333
+ function insertShareRow(database, record, revision, createdAt, updatedAt, statement = null) {
305
334
  const extra = {}
306
335
  for (const [key, value] of Object.entries(record)) {
307
336
  if (!KNOWN_FIELDS.has(key) && !['revision', 'deletedAt'].includes(key)) extra[key] = value
@@ -317,33 +346,8 @@ function insertShareRow(database, record, revision, createdAt, updatedAt) {
317
346
  record.createdFromHost ?? null, record.lastUpdatedFromHost ?? null,
318
347
  revision, shareRecordDigest(record), record.deletedAt ?? null, JSON.stringify(extra),
319
348
  ]
320
- database.prepare(`
321
- INSERT INTO share_sessions (${SHARE_COLUMNS}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
322
- ON CONFLICT(share_id) DO UPDATE SET
323
- session_id = excluded.session_id,
324
- permission = excluded.permission,
325
- title_snapshot = excluded.title_snapshot,
326
- scope = excluded.scope,
327
- project_id = excluded.project_id,
328
- password_hash = excluded.password_hash,
329
- password_salt = excluded.password_salt,
330
- password_version = excluded.password_version,
331
- auth_version = excluded.auth_version,
332
- allow_cloud_usage = excluded.allow_cloud_usage,
333
- created_at = excluded.created_at,
334
- updated_at = excluded.updated_at,
335
- expires_at = excluded.expires_at,
336
- revoked_at = excluded.revoked_at,
337
- superseded_at = excluded.superseded_at,
338
- access_count = excluded.access_count,
339
- last_accessed_at = excluded.last_accessed_at,
340
- created_from_host = excluded.created_from_host,
341
- last_updated_from_host = excluded.last_updated_from_host,
342
- revision = excluded.revision,
343
- record_digest = excluded.record_digest,
344
- deleted_at = excluded.deleted_at,
345
- extra_json = excluded.extra_json
346
- `).run(...values)
349
+ const insert = statement ?? database.prepare(SHARE_SESSION_UPSERT_SQL)
350
+ insert.run(...values)
347
351
  }
348
352
 
349
353
  function replaceTokens(database, shareId, tokens) {
@@ -354,18 +358,21 @@ function replaceTokens(database, shareId, tokens) {
354
358
  }
355
359
  }
356
360
 
357
- function enqueueShareMirror(database, record, operation, updatedAt) {
361
+ const ENQUEUE_SHARE_MIRROR_SQL = `
362
+ INSERT INTO share_json_mirror_queue (share_id, operation, share_json, attempts, last_error, updated_at)
363
+ VALUES (?, ?, ?, 0, NULL, ?)
364
+ ON CONFLICT(share_id) DO UPDATE SET
365
+ operation = excluded.operation,
366
+ share_json = excluded.share_json,
367
+ attempts = 0,
368
+ last_error = NULL,
369
+ updated_at = excluded.updated_at
370
+ `
371
+
372
+ function enqueueShareMirror(database, record, operation, updatedAt, statement = null) {
358
373
  const stored = record ? { ...record, revision: undefined, deletedAt: undefined } : null
359
- database.prepare(`
360
- INSERT INTO share_json_mirror_queue (share_id, operation, share_json, attempts, last_error, updated_at)
361
- VALUES (?, ?, ?, 0, NULL, ?)
362
- ON CONFLICT(share_id) DO UPDATE SET
363
- operation = excluded.operation,
364
- share_json = excluded.share_json,
365
- attempts = 0,
366
- last_error = NULL,
367
- updated_at = excluded.updated_at
368
- `).run(
374
+ const insert = statement ?? database.prepare(ENQUEUE_SHARE_MIRROR_SQL)
375
+ insert.run(
369
376
  record?.id || record,
370
377
  operation,
371
378
  operation === 'upsert' ? (stored ? recordJson(stored) : null) : null,
@@ -716,10 +723,12 @@ export function createShareRepository(storageHandle, { now = () => new Date().to
716
723
  const timestamp = now()
717
724
  return storage.transaction((database) => {
718
725
  database.exec('DELETE FROM share_sessions; DELETE FROM share_tokens; DELETE FROM share_json_mirror_queue;')
726
+ const insertSession = database.prepare(SHARE_SESSION_UPSERT_SQL)
727
+ const insertMirror = database.prepare(ENQUEUE_SHARE_MIRROR_SQL)
719
728
  for (const record of records) {
720
- insertShareRow(database, { ...record, updatedAt: record.updatedAt || timestamp }, 1, record.createdAt || timestamp, record.updatedAt || timestamp)
729
+ insertShareRow(database, { ...record, updatedAt: record.updatedAt || timestamp }, 1, record.createdAt || timestamp, record.updatedAt || timestamp, insertSession)
721
730
  replaceTokens(database, record.id, record.tokens)
722
- enqueueShareMirror(database, { ...record, revision: 1 }, 'upsert', timestamp)
731
+ enqueueShareMirror(database, { ...record, revision: 1 }, 'upsert', timestamp, insertMirror)
723
732
  }
724
733
  const digest = shareSnapshotDigest(records)
725
734
  if (expectedCount !== undefined && records.length !== expectedCount) throw new Error('Share replace count verification failed')
@@ -750,10 +759,12 @@ export function createShareRepository(storageHandle, { now = () => new Date().to
750
759
  ))
751
760
  }
752
761
 
753
- function verifyIntegrity({ quickCheck = false } = {}) {
762
+ function verifyIntegrity({ quickCheck = false, forceQuickCheck = false } = {}) {
754
763
  if (quickCheck) {
755
- const result = storage.prepare('PRAGMA quick_check').all().map((row) => row.quick_check)
756
- if (result.length !== 1 || result[0] !== 'ok') throw new Error(`SQLite quick_check failed: ${result.join(', ')}`)
764
+ // Shared quick_check gate (process cache + marker cadence) see
765
+ // runSharedSqliteQuickCheck in database.mjs; `forceQuickCheck` is the
766
+ // manual-maintenance escape hatch that always runs a real scan.
767
+ runSharedSqliteQuickCheck(storage, { force: forceQuickCheck === true })
757
768
  }
758
769
  const rows = storage.prepare(`SELECT ${SHARE_COLUMNS} FROM share_sessions`).all()
759
770
  let invalidRecords = 0
@@ -0,0 +1,107 @@
1
+ import { readScheduledRunsState } from './scheduled-runs-cutover.mjs'
2
+ import { readSessionStorageState } from './session-state-service.mjs'
3
+ import { readShareStorageState } from './share-service.mjs'
4
+ import { readLanAccessStorageState } from './lan-access-service.mjs'
5
+
6
+ // P1 startup maintenance window: the HTTP server listens before the storage
7
+ // migration/initialization chain completes. This module owns the process-wide
8
+ // startup state shared by server/index.mjs (maintenance gate, /api/health and
9
+ // /api/migration-status) and its tests.
10
+
11
+ export const STARTUP_STATES = Object.freeze({
12
+ MIGRATING: 'migrating',
13
+ READY: 'ready',
14
+ FAILED: 'failed',
15
+ })
16
+
17
+ // Read-only endpoints that stay reachable while the maintenance gate refuses
18
+ // every other /api/* route, so CLI/Desktop clients can observe progress.
19
+ export const MAINTENANCE_API_WHITELIST = Object.freeze([
20
+ '/api/health',
21
+ '/api/migration-status',
22
+ ])
23
+
24
+ // The server process starts in 'migrating' at module evaluation; the startup
25
+ // chain moves it to 'ready' (all domains initialized) or 'failed'.
26
+ let startupState = STARTUP_STATES.MIGRATING
27
+ let startupError = null
28
+
29
+ export function getStartupState() {
30
+ return startupState
31
+ }
32
+
33
+ export function getStartupError() {
34
+ return startupError
35
+ }
36
+
37
+ export function setStartupState(state, errorMessage = null) {
38
+ startupState = state
39
+ startupError = state === STARTUP_STATES.FAILED
40
+ ? (errorMessage || 'Startup failed')
41
+ : null
42
+ }
43
+
44
+ // Actionable recovery guidance appended to startup errors (fail-closed
45
+ // startup, review §5.1). The original exception text stays intact; the
46
+ // guidance is an additional multi-line paragraph shown verbatim by the
47
+ // frontend failed page (whitespace-pre-wrap).
48
+ export const STARTUP_RECOVERY_GUIDANCE = [
49
+ 'Recovery guidance:',
50
+ '1. Stop all QuickForge processes so the data files are not locked.',
51
+ '2. Diagnose offline: node server/maintenance/downgrade-session-state-v1.mjs --dry-run',
52
+ '3. Export a backup: node server/maintenance/export-session-state-v1.mjs <output-path>',
53
+ '4. Follow the recovery runbook: docs/architecture/session-storage-recovery-runbook.zh-CN.md',
54
+ ].join('\n')
55
+
56
+ export function withStartupRecoveryGuidance(errorMessage) {
57
+ return `${errorMessage || 'Startup failed'}\n\n${STARTUP_RECOVERY_GUIDANCE}`
58
+ }
59
+
60
+ // Test helper: restore the module-evaluation defaults.
61
+ export function resetStartupState() {
62
+ startupState = STARTUP_STATES.MIGRATING
63
+ startupError = null
64
+ }
65
+
66
+ // Maintenance gate for /api/* routes. Returns null when the request may be
67
+ // dispatched, or the maintenance refusal payload ({status, state}) otherwise.
68
+ export function resolveMaintenanceGate(method, pathname) {
69
+ const state = getStartupState()
70
+ if (state === STARTUP_STATES.READY) return null
71
+ if (!pathname || !pathname.startsWith('/api/')) return null
72
+ if (method === 'GET' && MAINTENANCE_API_WHITELIST.includes(pathname)) return null
73
+ return { status: 503, state }
74
+ }
75
+
76
+ function readDomain(read) {
77
+ try {
78
+ return read()
79
+ } catch (error) {
80
+ // A domain whose storage is not initialized yet (or whose state table
81
+ // cannot be read) must not fail the whole status endpoint; report the
82
+ // error name so the UI can still render the remaining domains.
83
+ return { phase: 'unknown', error: error?.name || 'Error' }
84
+ }
85
+ }
86
+
87
+ // Aggregated migration status across the four storage domains. `storage` is
88
+ // optional (scheduled-runs accepts an injected handle; the other domains read
89
+ // the process-wide SQLite storage internally).
90
+ export function readMigrationStatus(storage) {
91
+ const state = getStartupState()
92
+ const status = {
93
+ ok: true,
94
+ state,
95
+ domains: {
96
+ scheduledRuns: readDomain(() => readScheduledRunsState(storage)),
97
+ // Storage v2: the session domain is SQLite-authoritative by
98
+ // construction; readSessionStorageState reports the constant
99
+ // authoritative phase with a live session count.
100
+ sessionState: readDomain(() => readSessionStorageState()),
101
+ share: readDomain(() => readShareStorageState()),
102
+ lanAccess: readDomain(() => readLanAccessStorageState()),
103
+ },
104
+ }
105
+ if (state === STARTUP_STATES.FAILED) status.startupError = getStartupError()
106
+ return status
107
+ }