spexcode 0.5.2 → 0.5.4
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/README.md +1 -1
- package/package.json +1 -1
- package/spec-cli/bin/spex.mjs +25 -1
- package/spec-cli/hooks/dispatch.sh +24 -1
- package/spec-cli/src/anchors.ts +192 -58
- package/spec-cli/src/attach.ts +11 -8
- package/spec-cli/src/claude-headless.ts +13 -52
- package/spec-cli/src/cli.ts +209 -71
- package/spec-cli/src/client.ts +143 -5
- package/spec-cli/src/gateway-hub.ts +2 -6
- package/spec-cli/src/gateway.ts +96 -11
- package/spec-cli/src/git.ts +1395 -96
- package/spec-cli/src/graph.ts +21 -17
- package/spec-cli/src/graphCache.ts +405 -61
- package/spec-cli/src/graphStream.ts +581 -182
- package/spec-cli/src/guide.ts +15 -4
- package/spec-cli/src/harness.ts +1133 -71
- package/spec-cli/src/headless-controller.ts +63 -0
- package/spec-cli/src/help.ts +120 -64
- package/spec-cli/src/host-resources.ts +698 -0
- package/spec-cli/src/index.ts +95 -21
- package/spec-cli/src/init.ts +73 -15
- package/spec-cli/src/issues.ts +36 -3
- package/spec-cli/src/layout.ts +189 -41
- package/spec-cli/src/lint.ts +152 -27
- package/spec-cli/src/localIssues.ts +5 -1
- package/spec-cli/src/maintenance-wrapper.ts +276 -0
- package/spec-cli/src/materialize.ts +6 -7
- package/spec-cli/src/opencode-headless.ts +4 -8
- package/spec-cli/src/pi-headless.ts +17 -60
- package/spec-cli/src/process-identity.ts +174 -0
- package/spec-cli/src/project-store.ts +17 -0
- package/spec-cli/src/pty-bridge.ts +6 -3
- package/spec-cli/src/reviews.ts +17 -13
- package/spec-cli/src/runtime-ownership.ts +88 -0
- package/spec-cli/src/session-maintenance.ts +750 -0
- package/spec-cli/src/session-timeline.ts +6 -6
- package/spec-cli/src/sessions.ts +1251 -208
- package/spec-cli/src/sh.ts +3 -0
- package/spec-cli/src/source-files.ts +27 -2
- package/spec-cli/src/specs.ts +58 -25
- package/spec-cli/src/supervise.ts +31 -11
- package/spec-cli/src/uninstall.ts +7 -6
- package/spec-cli/templates/hooks/commit-msg +14 -0
- package/spec-cli/templates/hooks/pre-commit +29 -15
- package/spec-cli/templates/hooks/prepare-commit-msg +20 -48
- package/spec-cli/templates/hooks/reference-transaction +105 -0
- package/spec-cli/templates/spec/project/.plugins/commands/supervisor/spec.md +1 -1
- package/spec-cli/templates/spec/project/.plugins/core/mark-active/mark-active.sh +23 -21
- package/spec-cli/templates/spec/project/.plugins/core/spec.md +12 -10
- package/spec-cli/templates/spec/project/.plugins/core/stop-gate/stop-gate.sh +15 -6
- package/spec-cli/templates/spec/project/.plugins/prompts/atomic-landing/spec.md +32 -0
- package/spec-dashboard/dist/assets/App-C5vbTw8Q.js +2 -0
- package/spec-dashboard/dist/assets/Dashboard-u8RIS3NY.js +27 -0
- package/spec-dashboard/dist/assets/EvalsPage-BS7ITcNo.js +2 -0
- package/spec-dashboard/dist/assets/IssuesPage-DXbqQFW_.js +1 -0
- package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/spec-dashboard/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/spec-dashboard/dist/assets/MobileApp-DVLnk9hz.js +2 -0
- package/spec-dashboard/dist/assets/Modal-6mHq6fbZ.js +1 -0
- package/spec-dashboard/dist/assets/{PageScroll-CadAKuSy.js → PageScroll-CAY4S4g4.js} +1 -1
- package/spec-dashboard/dist/assets/ProjectsPage-UQyzsTWN.js +1 -0
- package/spec-dashboard/dist/assets/SessionInterface-DKU4c1Z-.js +39 -0
- package/spec-dashboard/dist/assets/SessionWindow-zGwJaGbR.js +1 -0
- package/spec-dashboard/dist/assets/Settings-igR17pns.js +1 -0
- package/spec-dashboard/dist/assets/Thread-B-ZUarN1.js +13 -0
- package/spec-dashboard/dist/assets/TimelineChat-Cp579UoJ.css +1 -0
- package/spec-dashboard/dist/assets/TimelineChat-sc49Qj5d.js +276 -0
- package/spec-dashboard/dist/assets/data-B1ot4PF0.js +1 -0
- package/spec-dashboard/dist/assets/index-BqBNCa1V.js +41 -0
- package/spec-dashboard/dist/assets/index-CzutlTDf.css +1 -0
- package/spec-dashboard/dist/assets/{terminalFont-DmTPNdbU.js → terminalFont-C6oGNKrf.js} +1 -1
- package/spec-dashboard/dist/index.html +5 -4
- package/spec-dashboard/src/reviewFilters.js +46 -8
- package/spec-dashboard/src/session.js +64 -9
- package/spec-eval/src/cli.ts +144 -32
- package/spec-eval/src/evaltab.ts +16 -9
- package/spec-eval/src/freshness.ts +336 -54
- package/spec-eval/src/scenariofresh.ts +51 -1
- package/spec-eval/src/scenarios.ts +324 -25
- package/spec-eval/src/sessioneval.ts +969 -173
- package/spec-cli/src/message-stream.ts +0 -147
- package/spec-dashboard/dist/assets/Dashboard-CTAuTyZ3.js +0 -27
- package/spec-dashboard/dist/assets/EvalsPage-KbMMownG.js +0 -2
- package/spec-dashboard/dist/assets/IssuesPage-DmyLb9Rj.js +0 -1
- package/spec-dashboard/dist/assets/MobileApp-D2RZGt4Z.js +0 -2
- package/spec-dashboard/dist/assets/Modal-3brXUhM0.js +0 -1
- package/spec-dashboard/dist/assets/ProjectsPage-DU3x4Y8l.js +0 -1
- package/spec-dashboard/dist/assets/SessionInterface-BtrzlOPs.js +0 -39
- package/spec-dashboard/dist/assets/SessionWindow-BWH5O0jh.js +0 -13
- package/spec-dashboard/dist/assets/Settings-COgdKTJB.js +0 -1
- package/spec-dashboard/dist/assets/TimelineChat-DQ21GSJK.js +0 -1
- package/spec-dashboard/dist/assets/index-D6HBvKkJ.js +0 -41
- package/spec-dashboard/dist/assets/index-DFdlYy4H.css +0 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { execFile, spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
3
|
import { promisify } from 'node:util'
|
|
4
|
-
import { alive } from './sessions.js'
|
|
4
|
+
import { alive, withSessionInputLock } from './sessions.js'
|
|
5
|
+
import { runSessionOperationSync } from './session-maintenance.js'
|
|
5
6
|
|
|
6
7
|
const pexec = promisify(execFile)
|
|
7
8
|
const TMUX_SOCK = process.env.SPEXCODE_TMUX || 'spexcode'
|
|
@@ -362,8 +363,10 @@ const MAX_INPUT_BYTES = 64 * 1024
|
|
|
362
363
|
export function forwardInput(id: string, viewer: Viewer, data: string): boolean {
|
|
363
364
|
const subscription = currentSubscription(id, viewer)
|
|
364
365
|
if (!subscription?.visible || !subscription.bridge || !data || Buffer.byteLength(data, 'utf8') > MAX_INPUT_BYTES) return false
|
|
365
|
-
|
|
366
|
-
|
|
366
|
+
return runSessionOperationSync({ op: 'terminal-input', sessionId: id }, () => withSessionInputLock(id, () => {
|
|
367
|
+
sendControl(subscription.bridge!, { t: 'input', data })
|
|
368
|
+
return true
|
|
369
|
+
}) ?? false)
|
|
367
370
|
}
|
|
368
371
|
|
|
369
372
|
async function restoreBridge(id: string, viewer: Viewer, subscription: Subscription): Promise<void> {
|
package/spec-cli/src/reviews.ts
CHANGED
|
@@ -15,6 +15,11 @@ import { EVAL_QUERY_DEFAULT, ISSUE_QUERY_DEFAULT, readToken } from '../../spec-d
|
|
|
15
15
|
export const REVIEW_PER_PAGE = 25
|
|
16
16
|
|
|
17
17
|
type ReviewItem = Record<string, unknown>
|
|
18
|
+
// A section count is one number, or — when the domain adapter splits that section — its named buckets,
|
|
19
|
+
// which re-add to the same whole population ([[review-filters]] owns the split; Evals splits its measured
|
|
20
|
+
// verdicts into {fresh,stale} so the remeasurement debt travels with the count instead of being recomputed
|
|
21
|
+
// per surface). The fold happens ONCE, here on the server, over the complete filtered population.
|
|
22
|
+
export type ReviewCount = number | Record<string, number>
|
|
18
23
|
type ReviewOption = { value: string; label?: string; count?: number }
|
|
19
24
|
type ReviewFacet = { key: string; label?: string; value: string; meaningful?: boolean; options: ReviewOption[] }
|
|
20
25
|
type EvalNeighbor = { node: string; scenario: string; state: string }
|
|
@@ -29,7 +34,7 @@ export type PagedReview<T extends ReviewItem = ReviewItem> = {
|
|
|
29
34
|
prev: number | null
|
|
30
35
|
next: number | null
|
|
31
36
|
revision: string
|
|
32
|
-
counts: Record<string,
|
|
37
|
+
counts: Record<string, ReviewCount>
|
|
33
38
|
facets: Record<string, ReviewFacet>
|
|
34
39
|
section: { key: string; value: string; options: ReviewOption[] } | null
|
|
35
40
|
}
|
|
@@ -129,12 +134,12 @@ export async function issuesReview(query: string | undefined, requestedPage: unk
|
|
|
129
134
|
}
|
|
130
135
|
}
|
|
131
136
|
|
|
132
|
-
const byNewest = (a: any, b: any): number =>
|
|
137
|
+
const byNewest = (a: any, b: any): number => Number(b.filterKind === EVAL_FILTER_KIND.RESULT) - Number(a.filterKind === EVAL_FILTER_KIND.RESULT)
|
|
138
|
+
|| String(b.ts ?? '').localeCompare(String(a.ts ?? ''))
|
|
133
139
|
|| String(a.node ?? '').localeCompare(String(b.node ?? ''))
|
|
134
140
|
|| String(a.scenario ?? '').localeCompare(String(b.scenario ?? ''))
|
|
135
141
|
|
|
136
142
|
export function trunkEvalReviewItems(nodes: any[]): ReviewItem[] {
|
|
137
|
-
const blind: any[] = []
|
|
138
143
|
const items: any[] = []
|
|
139
144
|
for (const node of nodes ?? []) {
|
|
140
145
|
const latest = new Map<string, any>()
|
|
@@ -142,7 +147,7 @@ export function trunkEvalReviewItems(nodes: any[]): ReviewItem[] {
|
|
|
142
147
|
for (const scenario of node.scenarios ?? []) {
|
|
143
148
|
const reading = latest.get(scenario.name)
|
|
144
149
|
if (!reading) {
|
|
145
|
-
|
|
150
|
+
items.push({
|
|
146
151
|
scenario: scenario.name,
|
|
147
152
|
expected: scenario.expected,
|
|
148
153
|
tags: scenario.tags,
|
|
@@ -163,21 +168,18 @@ export function trunkEvalReviewItems(nodes: any[]): ReviewItem[] {
|
|
|
163
168
|
})
|
|
164
169
|
}
|
|
165
170
|
}
|
|
166
|
-
|
|
167
|
-
return [...blind, ...items.sort(byNewest)]
|
|
171
|
+
return items.sort(byNewest)
|
|
168
172
|
}
|
|
169
173
|
|
|
170
174
|
export function scopedEvalReviewItems(model: SessionEvals): ReviewItem[] {
|
|
171
|
-
const
|
|
172
|
-
const own: any[] = []
|
|
173
|
-
const inherited: any[] = []
|
|
175
|
+
const items: any[] = []
|
|
174
176
|
for (const node of model.nodes ?? []) {
|
|
175
177
|
const latest = new Map<string, any>()
|
|
176
178
|
for (const reading of node.evals ?? []) if (!latest.has(reading.scenario)) latest.set(reading.scenario, reading)
|
|
177
179
|
for (const scenario of node.scenarios ?? []) {
|
|
178
180
|
const reading = latest.get(scenario.name)
|
|
179
181
|
if (!reading) {
|
|
180
|
-
|
|
182
|
+
items.push({
|
|
181
183
|
scenario: scenario.name,
|
|
182
184
|
expected: scenario.expected,
|
|
183
185
|
tags: scenario.tags,
|
|
@@ -192,15 +194,16 @@ export function scopedEvalReviewItems(model: SessionEvals): ReviewItem[] {
|
|
|
192
194
|
...reading,
|
|
193
195
|
expected: scenario.expected ?? reading.expected,
|
|
194
196
|
tags: scenario.tags,
|
|
197
|
+
impact: scenario.impact,
|
|
195
198
|
state: evalReviewState(reading),
|
|
196
199
|
node: node.id,
|
|
197
200
|
hue: node.hue,
|
|
198
201
|
filterKind: EVAL_FILTER_KIND.RESULT,
|
|
199
202
|
}
|
|
200
|
-
|
|
203
|
+
items.push(item)
|
|
201
204
|
}
|
|
202
205
|
}
|
|
203
|
-
return
|
|
206
|
+
return items.sort(byNewest)
|
|
204
207
|
}
|
|
205
208
|
|
|
206
209
|
const evalItemKey = (item: any): string => `${String(item?.node ?? '')}\0${String(item?.scenario ?? '')}`
|
|
@@ -327,9 +330,10 @@ export async function evalsReview(query: string | undefined, requestedPage: unkn
|
|
|
327
330
|
unknown: model.nodes.reduce((count, node) => count + (node.unknownCoverage?.length ?? 0), 0),
|
|
328
331
|
summary: model.summary,
|
|
329
332
|
evalRevision: model.evalRevision,
|
|
333
|
+
impact: model.impact,
|
|
330
334
|
...paginateReview(items, filtered.shown, filtered, requestedPage, {
|
|
331
335
|
domain: 'evals', scope, items, gates: model.gates, summary: model.summary,
|
|
332
|
-
evalRevision: model.evalRevision, sessions: sessions.map((session) => session.id),
|
|
336
|
+
evalRevision: model.evalRevision, impact: model.impact, sessions: sessions.map((session) => session.id),
|
|
333
337
|
}),
|
|
334
338
|
}
|
|
335
339
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import { spawn } from 'node:child_process'
|
|
3
|
+
import { closeSync, mkdirSync, openSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { dirname, join } from 'node:path'
|
|
5
|
+
import { repoRoot } from './git.js'
|
|
6
|
+
import { readJsonConfig, runtimeRoot } from './layout.js'
|
|
7
|
+
import { processStartToken, writeDetachedRuntimeReceipt, type ProcessIdentity } from './process-identity.js'
|
|
8
|
+
|
|
9
|
+
export type BackendInstanceRecord = {
|
|
10
|
+
version: 1
|
|
11
|
+
instanceId: string
|
|
12
|
+
pid: number
|
|
13
|
+
startToken: string
|
|
14
|
+
projectRoot: string
|
|
15
|
+
startedAt: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const backendInstancesDir = () => join(runtimeRoot(), 'backend-instances')
|
|
19
|
+
const backendInstancePath = (instanceId: string) => join(backendInstancesDir(), `${instanceId}.json`)
|
|
20
|
+
|
|
21
|
+
const atomicJson = (path: string, value: unknown) => {
|
|
22
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
23
|
+
const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`
|
|
24
|
+
writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 })
|
|
25
|
+
renameSync(tmp, path)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function readBackendInstanceRecords(root = repoRoot()): BackendInstanceRecord[] {
|
|
29
|
+
let names: string[]
|
|
30
|
+
try { names = readdirSync(backendInstancesDir()) } catch { return [] }
|
|
31
|
+
return names.flatMap((name) => {
|
|
32
|
+
try {
|
|
33
|
+
const rec = readJsonConfig(join(backendInstancesDir(), name)) as BackendInstanceRecord
|
|
34
|
+
return rec?.version === 1 && rec.projectRoot === root ? [rec] : []
|
|
35
|
+
} catch { return [] }
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function registerBackendInstance(instanceId: string, pid = process.pid, root = repoRoot()): BackendInstanceRecord {
|
|
40
|
+
const startToken = processStartToken(pid)
|
|
41
|
+
if (!startToken) throw new Error(`cannot identify backend supervisor PID ${pid}`)
|
|
42
|
+
const record: BackendInstanceRecord = { version: 1, instanceId, pid, startToken, projectRoot: root, startedAt: new Date().toISOString() }
|
|
43
|
+
atomicJson(backendInstancePath(instanceId), record)
|
|
44
|
+
return record
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function unregisterBackendInstance(instanceId: string, pid = process.pid): void {
|
|
48
|
+
try {
|
|
49
|
+
const record = readJsonConfig(backendInstancePath(instanceId)) as BackendInstanceRecord
|
|
50
|
+
if (record.instanceId === instanceId && record.pid === pid && record.startToken === processStartToken(pid)) rmSync(backendInstancePath(instanceId), { force: true })
|
|
51
|
+
} catch { /* not ours / already removed */ }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function spawnDetachedRuntime(opts: {
|
|
55
|
+
cwd: string
|
|
56
|
+
logFile: string
|
|
57
|
+
pidFile: string
|
|
58
|
+
receiptFile: string
|
|
59
|
+
command: string
|
|
60
|
+
args: string[]
|
|
61
|
+
env?: NodeJS.ProcessEnv
|
|
62
|
+
}): ProcessIdentity {
|
|
63
|
+
mkdirSync(dirname(opts.logFile), { recursive: true })
|
|
64
|
+
mkdirSync(dirname(opts.pidFile), { recursive: true })
|
|
65
|
+
const log = openSync(opts.logFile, 'a', 0o600)
|
|
66
|
+
let child: ReturnType<typeof spawn>
|
|
67
|
+
try {
|
|
68
|
+
child = spawn(opts.command, opts.args, {
|
|
69
|
+
cwd: opts.cwd,
|
|
70
|
+
env: opts.env ?? process.env,
|
|
71
|
+
detached: true,
|
|
72
|
+
stdio: ['ignore', log, log],
|
|
73
|
+
})
|
|
74
|
+
} finally { closeSync(log) }
|
|
75
|
+
child.once('error', () => {})
|
|
76
|
+
if (!child.pid) throw new Error(`could not spawn detached shared runtime: ${opts.command}`)
|
|
77
|
+
child.unref()
|
|
78
|
+
try {
|
|
79
|
+
const identity = writeDetachedRuntimeReceipt(child.pid, opts.receiptFile)
|
|
80
|
+
writeFileSync(opts.pidFile, `${child.pid}\n`, { mode: 0o600 })
|
|
81
|
+
return { pid: identity.pid, startToken: identity.startToken }
|
|
82
|
+
} catch (error) {
|
|
83
|
+
try { child.kill('SIGTERM') } catch { /* exact just-spawned child already exited */ }
|
|
84
|
+
rmSync(opts.pidFile, { force: true })
|
|
85
|
+
rmSync(opts.receiptFile, { force: true })
|
|
86
|
+
throw error
|
|
87
|
+
}
|
|
88
|
+
}
|