forge-workflow 0.1.0-beta.4 → 0.1.0-beta.6
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/AGENTS.md +18 -7
- package/CHANGELOG.md +79 -1
- package/CLAUDE.md +0 -12
- package/CODING_STANDARDS.md +72 -0
- package/README.md +6 -2
- package/bin/forge-cmd.js +20 -0
- package/bin/forge.js +28 -375
- package/docs/INDEX.md +1 -1
- package/docs/guides/BEADS_GITHUB_SYNC.md +2 -31
- package/docs/guides/MIGRATION.md +4 -4
- package/docs/guides/SETUP.md +16 -16
- package/docs/reference/COMMANDS.md +8 -5
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
- package/docs/reference/INSIGHTS_RECAP.md +9 -20
- package/docs/reference/INSTALL.md +4 -0
- package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
- package/docs/reference/RELEASE.md +5 -3
- package/docs/reference/TOOLCHAIN.md +8 -0
- package/docs/reference/github-accounts.md +134 -0
- package/docs/reference/protected-state-surfaces.md +4 -4
- package/docs/reference/shepherd.md +114 -35
- package/lefthook.yml +12 -0
- package/lib/activation/ensure-forge-home.js +33 -15
- package/lib/adapters/pr-state-adapter.js +359 -144
- package/lib/audit-evidence.js +71 -110
- package/lib/base-remote.js +138 -0
- package/lib/beta5-compatibility-evidence.js +1093 -0
- package/lib/bun-lockfile-proof.js +413 -0
- package/lib/bun-workflow-pins.js +461 -0
- package/lib/capabilities/index.js +9 -0
- package/lib/capabilities/model.js +141 -0
- package/lib/capabilities/probes.js +347 -0
- package/lib/capped-jsonl-log.js +236 -0
- package/lib/codex-skills.js +2 -2
- package/lib/commands/_manifest.js +1 -0
- package/lib/commands/_registry.js +50 -20
- package/lib/commands/clean.js +252 -32
- package/lib/commands/dev.js +4 -33
- package/lib/commands/doctor.js +37 -6
- package/lib/commands/gate.js +197 -27
- package/lib/commands/github.js +215 -0
- package/lib/commands/hooks.js +276 -30
- package/lib/commands/insights.js +8 -3
- package/lib/commands/memory.js +66 -2
- package/lib/commands/merge.js +1265 -58
- package/lib/commands/plan.js +33 -2
- package/lib/commands/pr.js +3 -1
- package/lib/commands/preflight.js +21 -4
- package/lib/commands/prime.js +21 -8
- package/lib/commands/push.js +146 -54
- package/lib/commands/recall.js +127 -49
- package/lib/commands/recap.js +6 -1
- package/lib/commands/release.js +39 -3
- package/lib/commands/remember.js +28 -4
- package/lib/commands/serve.js +26 -9
- package/lib/commands/setup.js +323 -98
- package/lib/commands/shepherd.js +591 -73
- package/lib/commands/ship.js +36 -91
- package/lib/commands/skill.js +127 -11
- package/lib/commands/status.js +17 -1
- package/lib/commands/team.js +47 -8
- package/lib/commands/test.js +187 -38
- package/lib/commands/validate.js +65 -21
- package/lib/commands/worktree.js +359 -45
- package/lib/core/runtime-graph.js +1 -1
- package/lib/doc-assertions.js +297 -0
- package/lib/existing-tdd-gate.js +253 -0
- package/lib/fixtures/beta5-corpus/v1/README.md +9 -0
- package/lib/fixtures/beta5-corpus/v1/contract/command-contract.json +26 -0
- package/lib/fixtures/beta5-corpus/v1/contract/package-contract.json +13 -0
- package/lib/fixtures/beta5-corpus/v1/contract/workflow-stage-matrix.json +8 -0
- package/lib/fixtures/beta5-corpus/v1/manifest.json +25 -0
- package/lib/fixtures/beta5-corpus/v1/state/comments.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/config.yaml +6 -0
- package/lib/fixtures/beta5-corpus/v1/state/dependencies.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/issues.jsonl +2 -0
- package/lib/fixtures/beta5-corpus/v1/state/kernel.sql +20 -0
- package/lib/forge-context.js +1 -4
- package/lib/forge-issues.js +134 -32
- package/lib/gate-events.js +98 -10
- package/lib/git-defaults.js +56 -0
- package/lib/github-context.js +308 -0
- package/lib/global-flags.js +1 -0
- package/lib/harness-capability-matrix.js +3 -3
- package/lib/hook-renderer.js +122 -5
- package/lib/insights.js +96 -80
- package/lib/issue-render.js +19 -0
- package/lib/kernel/backing-issue.js +14 -2
- package/lib/kernel/broker.js +739 -31
- package/lib/kernel/claim-reconciler.js +238 -0
- package/lib/kernel/cli-broker-factory.js +12 -1
- package/lib/kernel/close-on-merge.js +154 -0
- package/lib/kernel/fs-class.js +42 -25
- package/lib/kernel/lease-enforcer.js +9 -4
- package/lib/kernel/legacy-claim-repair.js +442 -0
- package/lib/kernel/live-claim-projection.js +26 -0
- package/lib/kernel/migrations.js +118 -3
- package/lib/kernel/readiness-model.js +184 -12
- package/lib/kernel/schema.js +49 -1
- package/lib/kernel/sqlite-driver.js +3435 -172
- package/lib/kernel/taxonomy-validator.js +4 -1
- package/lib/kernel/windows-private-acl.js +239 -0
- package/lib/lefthook-wiring.js +21 -1
- package/lib/memory/hygiene.js +191 -0
- package/lib/memory/router.js +110 -28
- package/lib/memory/usage-evidence.js +4 -0
- package/lib/memory-digest.js +106 -15
- package/lib/memory-recall-events.js +145 -0
- package/lib/memory-recall.js +71 -10
- package/lib/merge-rules.js +143 -21
- package/lib/npm-publish-workflow.js +465 -0
- package/lib/orientation.js +68 -43
- package/lib/package-root.js +2 -0
- package/lib/plugin-catalog.js +14 -4
- package/lib/pr-bundle.js +5 -6
- package/lib/pr-monitor/auto-actions.js +169 -28
- package/lib/pr-monitor/differ.js +110 -4
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/flow-monitor.js +1424 -0
- package/lib/pr-monitor/gather.js +251 -44
- package/lib/pr-monitor/journal.js +18 -39
- package/lib/pr-monitor/monitor.js +117 -10
- package/lib/pr-monitor/process-identity.js +117 -0
- package/lib/pr-monitor/reconcile-executor.js +1129 -470
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-summary.js +293 -0
- package/lib/pr-monitor/review-preflight.js +269 -0
- package/lib/pr-monitor/shepherd-lease.js +38 -20
- package/lib/pr-monitor/verdict.js +438 -0
- package/lib/pr-monitor/watch-lifecycle.js +145 -27
- package/lib/pr-monitor/watch-owner.js +1414 -0
- package/lib/pr-monitor/watch.js +129 -58
- package/lib/pr-pull.js +33 -14
- package/lib/pr-shepherd.js +51 -11
- package/lib/preflight/gates.js +65 -18
- package/lib/preflight/runner.js +5 -0
- package/lib/project-memory.js +178 -4
- package/lib/protected-state-authority.js +1100 -0
- package/lib/protected-state-surfaces.js +243 -45
- package/lib/release-readiness.js +53 -7
- package/lib/review-adapter.js +65 -0
- package/lib/shell-utils.js +1 -1
- package/lib/skills-sync.js +71 -35
- package/lib/smart-merge.js +28 -4
- package/lib/symlink-utils.js +74 -26
- package/lib/upgrade-safety.js +39 -0
- package/lib/using-forge.js +19 -6
- package/lib/validation/risk-manifest.js +339 -0
- package/lib/workflow/enforce-stage.js +44 -0
- package/lib/workflow/plan-authority.js +225 -0
- package/package.json +12 -9
- package/scripts/commitlint.js +13 -15
- package/scripts/doc-asserting-tests.js +158 -0
- package/scripts/generate-risk-manifest.js +91 -0
- package/scripts/github-context-bridge.sh +10 -0
- package/scripts/legacy-claim-repair.js +145 -0
- package/scripts/lib/behavioral-eval-runner.js +310 -0
- package/scripts/lib/behavioral-eval-runtime.js +457 -0
- package/scripts/lib/eval-evidence.js +328 -0
- package/scripts/lib/eval-runner.js +81 -41
- package/scripts/lib/immutable-eval-corpus.js +309 -0
- package/scripts/lib/promotion-evidence-loader.js +94 -0
- package/scripts/lib/promotion-scorecard.js +314 -0
- package/scripts/npm-release-receipt.js +134 -0
- package/scripts/process-tree.js +773 -0
- package/scripts/protected-state-check.js +479 -31
- package/scripts/run-command-eval.js +29 -1
- package/scripts/sync-agent-skills.js +333 -34
- package/scripts/sync-d20-audit.js +172 -0
- package/scripts/test-full-suite.js +935 -37
- package/scripts/test-profile.js +13 -3
- package/scripts/test.js +271 -57
- package/skills/coverage.json +1 -0
- package/skills/review/SKILL.md +6 -11
- package/skills/review/evals/scorecard.json +4 -4
- package/skills/rollback/SKILL.md +4 -11
- package/skills/rollback/evals/scorecard.json +3 -3
- package/skills/setup/SKILL.md +18 -0
- package/skills/setup/evals/scorecard.json +3 -3
- package/skills/shepherd/SKILL.md +39 -16
- package/skills/shepherd/evals/scorecard.json +4 -4
- package/skills/ship/SKILL.md +4 -12
- package/skills/ship/evals/scorecard.json +3 -3
- package/skills/validate/SKILL.md +3 -0
- package/skills/validate/evals/scorecard.json +1 -1
- package/skills/worktree/SKILL.md +6 -1
- package/skills/worktree/evals/scorecard.json +2 -2
- package/lib/beads-setup.js +0 -538
- package/lib/beads-sync-scaffold.js +0 -189
- package/lib/pat-setup.js +0 -207
- package/lib/pr-monitor/render-sticky.js +0 -206
- package/lib/pr-monitor/upsert-sticky.js +0 -169
- package/scripts/beads-context.sh +0 -577
- package/scripts/beads-migrate-to-dolt.sh +0 -7
- package/scripts/beads-upgrade-smoke.sh +0 -284
- package/scripts/lib/beads-migrate-to-dolt.mjs +0 -503
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { randomUUID } = require('node:crypto');
|
|
7
|
+
const { spawnSync: defaultSpawnSync } = require('node:child_process');
|
|
8
|
+
|
|
9
|
+
const MANIFEST_ENV = 'FORGE_TEST_PROCESS_MANIFEST';
|
|
10
|
+
const TOKEN_ENV = 'FORGE_TEST_PROCESS_TOKEN';
|
|
11
|
+
const INSTANCE_ENV = 'FORGE_TEST_PROCESS_INSTANCE';
|
|
12
|
+
const SESSION_ENV = 'FORGE_SESSION_ID';
|
|
13
|
+
const MANIFEST_VERSION = 1;
|
|
14
|
+
const DEFAULT_MANIFEST_DIR = path.join(os.tmpdir(), 'forge-test-processes');
|
|
15
|
+
|
|
16
|
+
function nowIso() {
|
|
17
|
+
return new Date().toISOString();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizePid(value) {
|
|
21
|
+
const pid = Number(value);
|
|
22
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function nonEmptyString(value) {
|
|
26
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeIdentity(value) {
|
|
30
|
+
return nonEmptyString(typeof value === 'string' ? value.trim() : value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isUsableIdentity(value) {
|
|
34
|
+
const identity = normalizeIdentity(value);
|
|
35
|
+
return identity && !identity.startsWith('unverified:') ? identity : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isValidManifestEntry(entry) {
|
|
39
|
+
if (!entry || typeof entry !== 'object') return false;
|
|
40
|
+
if (!nonEmptyString(entry.id) || !nonEmptyString(entry.token)) return false;
|
|
41
|
+
if (!['reserved', 'running'].includes(entry.status)) return false;
|
|
42
|
+
if (!nonEmptyString(entry.startedAt)) return false;
|
|
43
|
+
if (entry.instanceId != null && !nonEmptyString(entry.instanceId)) return false;
|
|
44
|
+
if (entry.status === 'reserved') {
|
|
45
|
+
if (entry.pid == null) return entry.identity == null;
|
|
46
|
+
return normalizePid(entry.pid) === entry.pid
|
|
47
|
+
&& (entry.identity == null || normalizeIdentity(entry.identity) !== null);
|
|
48
|
+
}
|
|
49
|
+
return normalizePid(entry.pid) === entry.pid && normalizeIdentity(entry.identity) !== null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isValidManifest(manifest) {
|
|
53
|
+
return Boolean(
|
|
54
|
+
manifest
|
|
55
|
+
&& manifest.version === MANIFEST_VERSION
|
|
56
|
+
&& nonEmptyString(manifest.token)
|
|
57
|
+
&& (manifest.instanceId == null || nonEmptyString(manifest.instanceId))
|
|
58
|
+
&& manifest.owner
|
|
59
|
+
&& typeof manifest.owner === 'object'
|
|
60
|
+
&& normalizePid(manifest.owner.pid) === manifest.owner.pid
|
|
61
|
+
&& nonEmptyString(manifest.owner.startedAt)
|
|
62
|
+
&& (manifest.owner.identity == null || normalizeIdentity(manifest.owner.identity) !== null)
|
|
63
|
+
&& Array.isArray(manifest.children)
|
|
64
|
+
&& manifest.children.every(isValidManifestEntry),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function inspectResource(resourcePath, fsApi = fs) {
|
|
69
|
+
const stat = fsApi.lstatSync || fsApi.statSync;
|
|
70
|
+
if (typeof stat !== 'function') {
|
|
71
|
+
try {
|
|
72
|
+
return {
|
|
73
|
+
exists: typeof fsApi.existsSync === 'function' ? fsApi.existsSync(resourcePath) : true,
|
|
74
|
+
metadata: null,
|
|
75
|
+
};
|
|
76
|
+
} catch {
|
|
77
|
+
return { exists: false, metadata: null };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
return { exists: true, metadata: stat.call(fsApi, resourcePath) };
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') {
|
|
84
|
+
return { exists: false, metadata: null };
|
|
85
|
+
}
|
|
86
|
+
return { exists: null, metadata: null };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isOwnedMetadata(metadata, processApi = process, {
|
|
91
|
+
allowSharedAncestor = false,
|
|
92
|
+
allowTrustedSymlink = false,
|
|
93
|
+
} = {}) {
|
|
94
|
+
if (!allowTrustedSymlink
|
|
95
|
+
&& metadata && typeof metadata.isSymbolicLink === 'function' && metadata.isSymbolicLink()) return false;
|
|
96
|
+
if (allowTrustedSymlink) return true;
|
|
97
|
+
const currentUid = typeof processApi.getuid === 'function' ? processApi.getuid() : null;
|
|
98
|
+
if (currentUid == null || typeof metadata?.uid !== 'number' || metadata.uid === currentUid) return true;
|
|
99
|
+
if (!allowSharedAncestor) return false;
|
|
100
|
+
const mode = metadata?.mode;
|
|
101
|
+
if (typeof mode !== 'number') return false;
|
|
102
|
+
const writableByOthers = (mode & 0o022) !== 0;
|
|
103
|
+
const sticky = (mode & 0o1000) !== 0;
|
|
104
|
+
return !writableByOthers || sticky;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isTrustedSystemSymlink(resourcePath, metadata, fsApi = fs) {
|
|
108
|
+
if (path.sep !== '/' || metadata?.uid !== 0
|
|
109
|
+
|| typeof metadata?.isSymbolicLink !== 'function' || !metadata.isSymbolicLink()
|
|
110
|
+
|| typeof fsApi.readlinkSync !== 'function') return false;
|
|
111
|
+
const normalizedPath = path.posix.normalize(resourcePath);
|
|
112
|
+
if (!['/var', '/tmp'].includes(normalizedPath)) return false;
|
|
113
|
+
let target;
|
|
114
|
+
try {
|
|
115
|
+
target = fsApi.readlinkSync(resourcePath);
|
|
116
|
+
} catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
return path.posix.resolve(path.posix.dirname(normalizedPath), target)
|
|
120
|
+
=== `/private${normalizedPath}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function pathComponents(resourcePath) {
|
|
124
|
+
const absolutePath = path.resolve(resourcePath);
|
|
125
|
+
const parsed = path.parse(absolutePath);
|
|
126
|
+
const components = [parsed.root];
|
|
127
|
+
let current = parsed.root;
|
|
128
|
+
for (const segment of absolutePath.slice(parsed.root.length).split(path.sep)) {
|
|
129
|
+
if (!segment) continue;
|
|
130
|
+
current = path.join(current, segment);
|
|
131
|
+
components.push(current);
|
|
132
|
+
}
|
|
133
|
+
return components;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isOwnedPath(resourcePath, fsApi = fs, processApi = process) {
|
|
137
|
+
if (typeof resourcePath !== 'string' || resourcePath.length === 0) return false;
|
|
138
|
+
const stat = fsApi.lstatSync || fsApi.statSync;
|
|
139
|
+
if (typeof stat !== 'function') return true;
|
|
140
|
+
let missing = false;
|
|
141
|
+
const components = pathComponents(resourcePath);
|
|
142
|
+
for (const [index, component] of components.entries()) {
|
|
143
|
+
const inspected = inspectResource(component, fsApi);
|
|
144
|
+
if (inspected.exists === false) {
|
|
145
|
+
missing = true;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const allowSharedAncestor = index < components.length - 1;
|
|
149
|
+
const trustedSystemSymlink = allowSharedAncestor
|
|
150
|
+
&& isTrustedSystemSymlink(component, inspected.metadata, fsApi);
|
|
151
|
+
if (inspected.exists !== true || missing || !isOwnedMetadata(inspected.metadata, processApi, {
|
|
152
|
+
allowSharedAncestor,
|
|
153
|
+
allowTrustedSymlink: trustedSystemSymlink,
|
|
154
|
+
})) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function isOwnedManifestPath(manifestPath, fsApi = fs, processApi = process) {
|
|
162
|
+
return isOwnedPath(path.dirname(manifestPath), fsApi, processApi)
|
|
163
|
+
&& isOwnedPath(manifestPath, fsApi, processApi);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function readProcessManifest(manifestPath, fsApi = fs, processApi = process) {
|
|
167
|
+
if (typeof manifestPath !== 'string' || manifestPath.length === 0) return null;
|
|
168
|
+
if (!isOwnedManifestPath(manifestPath, fsApi, processApi)) return null;
|
|
169
|
+
try {
|
|
170
|
+
const manifest = JSON.parse(fsApi.readFileSync(manifestPath, 'utf8'));
|
|
171
|
+
return isValidManifest(manifest) ? manifest : null;
|
|
172
|
+
} catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function writeProcessManifest(manifestPath, manifest, fsApi = fs, processApi = process) {
|
|
178
|
+
const manifestDir = path.dirname(manifestPath);
|
|
179
|
+
if (!isOwnedPath(manifestPath, fsApi, processApi)) {
|
|
180
|
+
throw new Error('process manifest path is not owned by the current user');
|
|
181
|
+
}
|
|
182
|
+
let directory = inspectResource(manifestDir, fsApi);
|
|
183
|
+
if (directory.exists === null) throw new Error('process manifest directory cannot be inspected');
|
|
184
|
+
if (directory.exists === false) {
|
|
185
|
+
fsApi.mkdirSync(manifestDir, { recursive: true, mode: 0o700 });
|
|
186
|
+
directory = inspectResource(manifestDir, fsApi);
|
|
187
|
+
}
|
|
188
|
+
if (directory.exists !== true
|
|
189
|
+
|| !isOwnedPath(manifestDir, fsApi, processApi)
|
|
190
|
+
|| (typeof directory.metadata?.isDirectory === 'function' && !directory.metadata.isDirectory())) {
|
|
191
|
+
throw new Error('process manifest directory is not a user-owned directory');
|
|
192
|
+
}
|
|
193
|
+
if (typeof fsApi.chmodSync === 'function') {
|
|
194
|
+
if (!isOwnedPath(manifestDir, fsApi, processApi)) {
|
|
195
|
+
throw new Error('process manifest directory is not a user-owned directory');
|
|
196
|
+
}
|
|
197
|
+
fsApi.chmodSync(manifestDir, 0o700);
|
|
198
|
+
}
|
|
199
|
+
if (!isOwnedPath(manifestPath, fsApi, processApi)) {
|
|
200
|
+
throw new Error('process manifest path is not owned by the current user');
|
|
201
|
+
}
|
|
202
|
+
fsApi.writeFileSync(manifestPath, JSON.stringify(manifest), 'utf8');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function pathExists(manifestPath, fsApi = fs) {
|
|
206
|
+
try {
|
|
207
|
+
if (typeof fsApi.existsSync === 'function') return fsApi.existsSync(manifestPath);
|
|
208
|
+
fsApi.accessSync(manifestPath);
|
|
209
|
+
return true;
|
|
210
|
+
} catch {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function defaultIsAlive(pid, processApi) {
|
|
216
|
+
try {
|
|
217
|
+
processApi.kill(pid, 0);
|
|
218
|
+
return true;
|
|
219
|
+
} catch (error) {
|
|
220
|
+
return error && error.code === 'EPERM';
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function readProcIdentity(pid, fsApi) {
|
|
225
|
+
try {
|
|
226
|
+
const stat = fsApi.readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
227
|
+
const closeParen = stat.lastIndexOf(')');
|
|
228
|
+
if (closeParen >= 0) {
|
|
229
|
+
const startTime = nonEmptyString(stat.slice(closeParen + 2).trim().split(/\s+/)[19]);
|
|
230
|
+
if (startTime) return `proc:${startTime}`;
|
|
231
|
+
}
|
|
232
|
+
} catch {
|
|
233
|
+
// Fall through to ps on platforms without procfs.
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function readPsIdentity(pid, spawnSync) {
|
|
239
|
+
try {
|
|
240
|
+
const result = spawnSync('ps', ['-o', 'lstart=', '-p', String(pid)], {
|
|
241
|
+
shell: false, stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8', windowsHide: true,
|
|
242
|
+
});
|
|
243
|
+
const value = result && !result.error && result.status === 0
|
|
244
|
+
? normalizeIdentity(result.stdout) : null;
|
|
245
|
+
return value ? `ps:${value}` : null;
|
|
246
|
+
} catch {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function readWindowsIdentity(pid, spawnSync) {
|
|
252
|
+
try {
|
|
253
|
+
const result = spawnSync('powershell.exe', [
|
|
254
|
+
'-NoLogo', '-NoProfile', '-NonInteractive', '-Command',
|
|
255
|
+
`(Get-Process -Id ${String(pid)} -ErrorAction Stop).StartTime.ToUniversalTime().Ticks`,
|
|
256
|
+
], { shell: false, stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8', windowsHide: true });
|
|
257
|
+
const value = result && !result.error && result.status === 0
|
|
258
|
+
? normalizeIdentity(result.stdout) : null;
|
|
259
|
+
return value ? `windows:${value}` : null;
|
|
260
|
+
} catch {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function defaultGetProcessIdentity(pid, options = {}) {
|
|
266
|
+
const normalizedPid = normalizePid(pid);
|
|
267
|
+
if (!normalizedPid) return null;
|
|
268
|
+
const platform = options.platform || process.platform;
|
|
269
|
+
const fsApi = options.fsApi || fs;
|
|
270
|
+
const spawnSync = options.spawnSync || defaultSpawnSync;
|
|
271
|
+
if (platform !== 'win32') return readProcIdentity(normalizedPid, fsApi)
|
|
272
|
+
|| readPsIdentity(normalizedPid, spawnSync);
|
|
273
|
+
return readWindowsIdentity(normalizedPid, spawnSync);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function signalExitCode(signal) {
|
|
277
|
+
if (signal === 'SIGINT') return 130;
|
|
278
|
+
if (signal === 'SIGTERM') return 143;
|
|
279
|
+
return 1;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function createInitialManifest(token, processApi, getProcessIdentity, instanceId) {
|
|
283
|
+
const ownerPid = normalizePid(processApi.pid);
|
|
284
|
+
let ownerIdentity;
|
|
285
|
+
try {
|
|
286
|
+
ownerIdentity = normalizeIdentity(getProcessIdentity(ownerPid));
|
|
287
|
+
} catch {
|
|
288
|
+
ownerIdentity = null;
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
version: MANIFEST_VERSION,
|
|
292
|
+
token,
|
|
293
|
+
instanceId,
|
|
294
|
+
owner: {
|
|
295
|
+
pid: ownerPid,
|
|
296
|
+
identity: ownerIdentity,
|
|
297
|
+
startedAt: nowIso(),
|
|
298
|
+
},
|
|
299
|
+
children: [],
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function createProcessTree(options = {}) {
|
|
304
|
+
const fsApi = options.fsApi || fs;
|
|
305
|
+
const processApi = options.processApi || process;
|
|
306
|
+
const platform = options.platform || process.platform;
|
|
307
|
+
const spawnSync = options.spawnSync || defaultSpawnSync;
|
|
308
|
+
const env = options.env || process.env;
|
|
309
|
+
const allInstances = options.allInstances === true;
|
|
310
|
+
const instanceId = allInstances ? null : String(options.instanceId || randomUUID());
|
|
311
|
+
const hasCustomIdentity = typeof options.getProcessIdentity === 'function';
|
|
312
|
+
const getProcessIdentity = options.getProcessIdentity || ((pid) => defaultGetProcessIdentity(pid, {
|
|
313
|
+
platform,
|
|
314
|
+
fsApi,
|
|
315
|
+
spawnSync,
|
|
316
|
+
}));
|
|
317
|
+
const captureIdentity = (pid) => {
|
|
318
|
+
let identity;
|
|
319
|
+
try {
|
|
320
|
+
identity = normalizeIdentity(getProcessIdentity(pid));
|
|
321
|
+
} catch {
|
|
322
|
+
identity = null;
|
|
323
|
+
}
|
|
324
|
+
// Synthetic child handles may not be visible to the host process table.
|
|
325
|
+
// Keep their reservation, but never treat this marker as kill
|
|
326
|
+
// authorization if no stable evidence was available.
|
|
327
|
+
if (!identity && !hasCustomIdentity) return `unverified:${pid}`;
|
|
328
|
+
return identity;
|
|
329
|
+
};
|
|
330
|
+
// A Forge claim records FORGE_SESSION_ID. Use that same stable runtime token
|
|
331
|
+
// for the root manifest, while preserving an inherited manifest token for
|
|
332
|
+
// nested process trees that are already attached to their parent run.
|
|
333
|
+
const inheritedToken = nonEmptyString(options.token) || nonEmptyString(env[TOKEN_ENV]);
|
|
334
|
+
const token = String(inheritedToken || env[SESSION_ENV] || randomUUID());
|
|
335
|
+
const explicitManifestPath = nonEmptyString(options.manifestPath);
|
|
336
|
+
const environmentManifestPath = nonEmptyString(env[MANIFEST_ENV]);
|
|
337
|
+
const manifestDir = options.manifestDir || DEFAULT_MANIFEST_DIR;
|
|
338
|
+
|
|
339
|
+
if (!explicitManifestPath && !environmentManifestPath && options.reconcile !== false) {
|
|
340
|
+
reconcileProcessManifests({
|
|
341
|
+
...options,
|
|
342
|
+
manifestDir,
|
|
343
|
+
fsApi,
|
|
344
|
+
platform,
|
|
345
|
+
processApi,
|
|
346
|
+
spawnSync,
|
|
347
|
+
getProcessIdentity,
|
|
348
|
+
allInstances: true,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const manifestPath = explicitManifestPath
|
|
353
|
+
|| environmentManifestPath
|
|
354
|
+
|| path.join(manifestDir, `run-${normalizePid(processApi.pid) || process.pid}-${token}.json`);
|
|
355
|
+
const markerExists = pathExists(manifestPath, fsApi);
|
|
356
|
+
const existing = readProcessManifest(manifestPath, fsApi, processApi);
|
|
357
|
+
// An environment-provided path may belong to another run. Treat that as
|
|
358
|
+
// unverifiable and fail closed; never overwrite or reap another run's marker.
|
|
359
|
+
const inheritedManifestVerified = explicitManifestPath
|
|
360
|
+
|| !environmentManifestPath
|
|
361
|
+
|| Boolean(inheritedToken && existing?.token === inheritedToken);
|
|
362
|
+
const usable = inheritedManifestVerified
|
|
363
|
+
&& (!markerExists || (existing && existing.token === token));
|
|
364
|
+
let manifest = existing && existing.token === token
|
|
365
|
+
? existing
|
|
366
|
+
: (usable ? createInitialManifest(token, processApi, getProcessIdentity, instanceId) : null);
|
|
367
|
+
let sequence = manifest ? manifest.children.length : 0;
|
|
368
|
+
let cleanupSignal = null;
|
|
369
|
+
let cleanupComplete = false;
|
|
370
|
+
|
|
371
|
+
function ownsEntry(entry, latest = manifest) {
|
|
372
|
+
return Boolean(allInstances || (entry && entry.instanceId === instanceId)
|
|
373
|
+
|| (entry && entry.instanceId == null && latest?.instanceId === instanceId));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (usable) {
|
|
377
|
+
try {
|
|
378
|
+
writeProcessManifest(manifestPath, manifest, fsApi, processApi);
|
|
379
|
+
} catch {
|
|
380
|
+
// A missing/unwritable marker makes ownership unverifiable. All cleanup
|
|
381
|
+
// operations remain fail-closed, and the caller still receives its normal
|
|
382
|
+
// child-process status.
|
|
383
|
+
manifest = null;
|
|
384
|
+
}
|
|
385
|
+
} else {
|
|
386
|
+
manifest = null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function currentManifest() {
|
|
390
|
+
if (!usable || !manifest) return null;
|
|
391
|
+
const latest = readProcessManifest(manifestPath, fsApi, processApi);
|
|
392
|
+
return latest && latest.token === token ? latest : null;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function persist(next) {
|
|
396
|
+
if (!next || !usable || !manifest) return false;
|
|
397
|
+
try {
|
|
398
|
+
writeProcessManifest(manifestPath, next, fsApi, processApi);
|
|
399
|
+
manifest = next;
|
|
400
|
+
return true;
|
|
401
|
+
} catch {
|
|
402
|
+
manifest = null;
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function reserveChild(metadata = {}) {
|
|
408
|
+
const latest = currentManifest();
|
|
409
|
+
if (!latest) return null;
|
|
410
|
+
const entry = {
|
|
411
|
+
id: `${token}:${sequence++}:${randomUUID()}`,
|
|
412
|
+
token,
|
|
413
|
+
instanceId,
|
|
414
|
+
kind: metadata.kind || 'child',
|
|
415
|
+
label: metadata.label || null,
|
|
416
|
+
command: metadata.command || null,
|
|
417
|
+
pid: null,
|
|
418
|
+
status: 'reserved',
|
|
419
|
+
startedAt: nowIso(),
|
|
420
|
+
registeredAt: null,
|
|
421
|
+
identity: null,
|
|
422
|
+
};
|
|
423
|
+
latest.children.push(entry);
|
|
424
|
+
return persist(latest) ? { id: entry.id } : null;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function registerChild(reservation, child) {
|
|
428
|
+
const latest = currentManifest();
|
|
429
|
+
const id = reservation && reservation.id;
|
|
430
|
+
const pid = normalizePid(child && child.pid);
|
|
431
|
+
if (!latest || typeof id !== 'string' || !pid) return null;
|
|
432
|
+
const entry = latest.children.find((candidate) => candidate.id === id);
|
|
433
|
+
if (!entry || !ownsEntry(entry, latest) || entry.token !== token || entry.status !== 'reserved') return null;
|
|
434
|
+
const identity = captureIdentity(pid);
|
|
435
|
+
if (!identity) return null;
|
|
436
|
+
entry.pid = pid;
|
|
437
|
+
entry.identity = identity;
|
|
438
|
+
entry.status = 'running';
|
|
439
|
+
entry.registeredAt = nowIso();
|
|
440
|
+
return persist(latest) ? { ...entry } : null;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function unregisterChild(reservation) {
|
|
444
|
+
const latest = currentManifest();
|
|
445
|
+
const id = reservation && reservation.id;
|
|
446
|
+
if (!latest || typeof id !== 'string') return false;
|
|
447
|
+
const entry = latest.children.find((candidate) => candidate.id === id);
|
|
448
|
+
if (!ownsEntry(entry, latest)) return false;
|
|
449
|
+
const next = { ...latest, children: latest.children.filter((candidate) => candidate.id !== id) };
|
|
450
|
+
return persist(next);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function envFor(childEnv = env) {
|
|
454
|
+
if (!usable || !manifest) {
|
|
455
|
+
const stripped = { ...childEnv };
|
|
456
|
+
delete stripped[MANIFEST_ENV];
|
|
457
|
+
delete stripped[TOKEN_ENV];
|
|
458
|
+
delete stripped[INSTANCE_ENV];
|
|
459
|
+
return stripped;
|
|
460
|
+
}
|
|
461
|
+
return {
|
|
462
|
+
...childEnv,
|
|
463
|
+
[MANIFEST_ENV]: manifestPath,
|
|
464
|
+
[TOKEN_ENV]: token,
|
|
465
|
+
[INSTANCE_ENV]: instanceId,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function isVerifiableEntry(latest, entry) {
|
|
470
|
+
if (!(
|
|
471
|
+
latest
|
|
472
|
+
&& latest.version === MANIFEST_VERSION
|
|
473
|
+
&& latest.token === token
|
|
474
|
+
&& entry
|
|
475
|
+
&& entry.token === token
|
|
476
|
+
&& ownsEntry(entry, latest)
|
|
477
|
+
&& typeof entry.id === 'string'
|
|
478
|
+
&& entry.status === 'running'
|
|
479
|
+
&& normalizePid(entry.pid) === entry.pid
|
|
480
|
+
&& typeof entry.startedAt === 'string'
|
|
481
|
+
&& entry.startedAt.length > 0
|
|
482
|
+
)) return false;
|
|
483
|
+
|
|
484
|
+
const storedIdentity = isUsableIdentity(entry.identity);
|
|
485
|
+
if (!storedIdentity) return false;
|
|
486
|
+
let currentIdentity;
|
|
487
|
+
try {
|
|
488
|
+
currentIdentity = isUsableIdentity(getProcessIdentity(entry.pid));
|
|
489
|
+
} catch {
|
|
490
|
+
currentIdentity = null;
|
|
491
|
+
}
|
|
492
|
+
return currentIdentity !== null && currentIdentity === storedIdentity;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function isEntryAlive(entry) {
|
|
496
|
+
try {
|
|
497
|
+
return (options.isAlive || defaultIsAlive)(entry.pid, processApi);
|
|
498
|
+
} catch {
|
|
499
|
+
return true;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function killOwnedEntry(entry, signal) {
|
|
504
|
+
const pid = normalizePid(entry && entry.pid);
|
|
505
|
+
if (!pid || pid === normalizePid(processApi.pid) || !isVerifiableEntry(currentManifest(), entry)) {
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
if (!(options.isAlive || defaultIsAlive)(pid, processApi)) return false;
|
|
509
|
+
|
|
510
|
+
try {
|
|
511
|
+
if (platform === 'win32') {
|
|
512
|
+
const result = spawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], {
|
|
513
|
+
shell: false,
|
|
514
|
+
stdio: 'ignore',
|
|
515
|
+
windowsHide: true,
|
|
516
|
+
});
|
|
517
|
+
return Boolean(result && !result.error && result.status === 0);
|
|
518
|
+
}
|
|
519
|
+
processApi.kill(-pid, signal);
|
|
520
|
+
return true;
|
|
521
|
+
} catch {
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function cleanup(signal = 'SIGTERM') {
|
|
527
|
+
const force = signal === 'SIGKILL';
|
|
528
|
+
if (cleanupComplete || (cleanupSignal === signal && !force)) {
|
|
529
|
+
return { killed: [] };
|
|
530
|
+
}
|
|
531
|
+
cleanupSignal = signal;
|
|
532
|
+
const latest = currentManifest();
|
|
533
|
+
if (!latest) return { killed: [] };
|
|
534
|
+
|
|
535
|
+
const killed = [];
|
|
536
|
+
for (const entry of latest.children) {
|
|
537
|
+
if (killOwnedEntry(entry, signal)) killed.push(entry.pid);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const verifiedCache = new Map();
|
|
541
|
+
const aliveCache = new Map();
|
|
542
|
+
const verifiedOnce = (entry, manifestForEntry) => {
|
|
543
|
+
if (!verifiedCache.has(entry.id)) {
|
|
544
|
+
verifiedCache.set(entry.id, isVerifiableEntry(manifestForEntry, entry));
|
|
545
|
+
}
|
|
546
|
+
return verifiedCache.get(entry.id);
|
|
547
|
+
};
|
|
548
|
+
const aliveOnce = (entry) => {
|
|
549
|
+
if (!aliveCache.has(entry.id)) aliveCache.set(entry.id, isEntryAlive(entry));
|
|
550
|
+
return aliveCache.get(entry.id);
|
|
551
|
+
};
|
|
552
|
+
let finalManifest = currentManifest();
|
|
553
|
+
if (finalManifest) {
|
|
554
|
+
const liveEntries = finalManifest.children.filter((entry) => !(
|
|
555
|
+
entry.status === 'running'
|
|
556
|
+
&& !verifiedOnce(entry, finalManifest)
|
|
557
|
+
&& !aliveOnce(entry)
|
|
558
|
+
));
|
|
559
|
+
if (liveEntries.length !== finalManifest.children.length) {
|
|
560
|
+
persist({ ...finalManifest, children: liveEntries });
|
|
561
|
+
finalManifest = currentManifest();
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
const hasUnverifiableLiveEntry = finalManifest?.children.some(
|
|
565
|
+
(entry) => entry.status === 'running' && !verifiedOnce(entry, finalManifest)
|
|
566
|
+
&& aliveOnce(entry),
|
|
567
|
+
);
|
|
568
|
+
const hasLiveRunningEntry = finalManifest?.children.some(
|
|
569
|
+
(entry) => entry.status === 'running'
|
|
570
|
+
&& verifiedOnce(entry, finalManifest)
|
|
571
|
+
&& aliveOnce(entry),
|
|
572
|
+
);
|
|
573
|
+
const hasOutstandingReservation = finalManifest?.children.some(
|
|
574
|
+
(entry) => entry.status === 'reserved',
|
|
575
|
+
);
|
|
576
|
+
const hasFailedLiveEntry = finalManifest?.children.some(
|
|
577
|
+
(entry) => entry.status === 'running'
|
|
578
|
+
&& verifiedOnce(entry, finalManifest)
|
|
579
|
+
&& aliveOnce(entry)
|
|
580
|
+
&& !killed.includes(entry.pid),
|
|
581
|
+
);
|
|
582
|
+
const safeToRemove = finalManifest
|
|
583
|
+
&& !hasUnverifiableLiveEntry
|
|
584
|
+
&& !hasOutstandingReservation
|
|
585
|
+
&& !hasFailedLiveEntry
|
|
586
|
+
&& (force || !hasLiveRunningEntry);
|
|
587
|
+
if (safeToRemove && finalManifest.token === token) {
|
|
588
|
+
try {
|
|
589
|
+
fsApi.rmSync(manifestPath, { force: true });
|
|
590
|
+
cleanupComplete = !pathExists(manifestPath, fsApi);
|
|
591
|
+
} catch {
|
|
592
|
+
// A stale marker is safe to leave behind; its token still prevents a
|
|
593
|
+
// later run from touching this process tree.
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return { killed };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function abortChild(reservation, child, signal = 'SIGKILL') {
|
|
600
|
+
const pid = normalizePid(child && child.pid);
|
|
601
|
+
const retainReservation = (identityOverride = null) => {
|
|
602
|
+
const latest = currentManifest();
|
|
603
|
+
const id = reservation && reservation.id;
|
|
604
|
+
const entry = latest?.children.find((candidate) => candidate.id === id);
|
|
605
|
+
if (!latest || !entry || !ownsEntry(entry, latest) || entry.token !== token || entry.status !== 'reserved') {
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
entry.pid = pid;
|
|
609
|
+
entry.identity = identityOverride || captureIdentity(pid) || `unverified:${pid}`;
|
|
610
|
+
entry.status = 'running';
|
|
611
|
+
entry.registeredAt = nowIso();
|
|
612
|
+
persist(latest);
|
|
613
|
+
return true;
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
// Capture stable identity before direct kill so the process-group/tree kill
|
|
617
|
+
// can reap descendants while the reservation is still verifiable.
|
|
618
|
+
const identity = pid ? captureIdentity(pid) : null;
|
|
619
|
+
const retained = pid && identity ? retainReservation(identity) : false;
|
|
620
|
+
const finalizeAbort = (result) => {
|
|
621
|
+
if (pid && !retained) retainReservation(identity);
|
|
622
|
+
cleanup('SIGKILL');
|
|
623
|
+
unregisterChild(reservation);
|
|
624
|
+
cleanup('SIGKILL');
|
|
625
|
+
return result;
|
|
626
|
+
};
|
|
627
|
+
const groupResult = retained ? cleanup(signal) : { killed: [] };
|
|
628
|
+
if (pid && groupResult.killed.includes(pid)) {
|
|
629
|
+
unregisterChild(reservation);
|
|
630
|
+
cleanup('SIGKILL');
|
|
631
|
+
return true;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
let killSucceeded = false;
|
|
635
|
+
try {
|
|
636
|
+
if (child && typeof child.kill === 'function') {
|
|
637
|
+
killSucceeded = child.kill(signal) === true;
|
|
638
|
+
}
|
|
639
|
+
} catch {
|
|
640
|
+
killSucceeded = false;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (killSucceeded || !pid) return finalizeAbort(killSucceeded);
|
|
644
|
+
|
|
645
|
+
const alive = (() => {
|
|
646
|
+
try {
|
|
647
|
+
return (options.isAlive || defaultIsAlive)(normalizePid(child.pid), processApi);
|
|
648
|
+
} catch {
|
|
649
|
+
return true;
|
|
650
|
+
}
|
|
651
|
+
})();
|
|
652
|
+
if (!alive) return finalizeAbort(killSucceeded);
|
|
653
|
+
|
|
654
|
+
if (!retained) retainReservation(identity);
|
|
655
|
+
if (currentManifest()?.children.some((entry) => entry.pid === pid && entry.status === 'running')) {
|
|
656
|
+
// If identity evidence was available, cleanup can attempt a verified
|
|
657
|
+
// tree kill. Otherwise retain the reservation for later reconciliation.
|
|
658
|
+
cleanup('SIGKILL');
|
|
659
|
+
}
|
|
660
|
+
return killSucceeded;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function installSignalHandlers(onSignal) {
|
|
664
|
+
if (!processApi || typeof processApi.on !== 'function') return () => {};
|
|
665
|
+
const handlers = new Map();
|
|
666
|
+
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
667
|
+
const handler = () => {
|
|
668
|
+
cleanup(signal === 'SIGINT' ? 'SIGTERM' : signal);
|
|
669
|
+
if (typeof onSignal === 'function') onSignal(signal);
|
|
670
|
+
else processApi.exitCode = signalExitCode(signal);
|
|
671
|
+
};
|
|
672
|
+
handlers.set(signal, handler);
|
|
673
|
+
processApi.on(signal, handler);
|
|
674
|
+
}
|
|
675
|
+
return () => {
|
|
676
|
+
if (typeof processApi.removeListener !== 'function') return;
|
|
677
|
+
for (const [signal, handler] of handlers) processApi.removeListener(signal, handler);
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return {
|
|
682
|
+
cleanup,
|
|
683
|
+
envFor,
|
|
684
|
+
installSignalHandlers,
|
|
685
|
+
registerChild,
|
|
686
|
+
reserveChild,
|
|
687
|
+
unregisterChild,
|
|
688
|
+
abortChild,
|
|
689
|
+
manifestPath,
|
|
690
|
+
instanceId,
|
|
691
|
+
token,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function reapProcessManifest(manifestPath, options = {}) {
|
|
696
|
+
const fsApi = options.fsApi || fs;
|
|
697
|
+
const processApi = options.processApi || process;
|
|
698
|
+
const existing = readProcessManifest(manifestPath, fsApi, processApi);
|
|
699
|
+
if (!existing) return { killed: [], reaped: false };
|
|
700
|
+
|
|
701
|
+
const ownerPid = normalizePid(existing.owner.pid);
|
|
702
|
+
const currentPid = normalizePid(processApi.pid);
|
|
703
|
+
if (!ownerPid || ownerPid === currentPid) return { killed: [], reaped: false };
|
|
704
|
+
|
|
705
|
+
const isAlive = options.isAlive || defaultIsAlive;
|
|
706
|
+
const ownerAlive = (() => {
|
|
707
|
+
try {
|
|
708
|
+
return isAlive(ownerPid, processApi);
|
|
709
|
+
} catch {
|
|
710
|
+
return true;
|
|
711
|
+
}
|
|
712
|
+
})();
|
|
713
|
+
if (ownerAlive) {
|
|
714
|
+
// A live owner is not an orphan. Treat identity uncertainty as live too;
|
|
715
|
+
// this avoids killing a process that reused the owner's PID.
|
|
716
|
+
return { killed: [], reaped: false };
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const tree = createProcessTree({
|
|
720
|
+
...options,
|
|
721
|
+
manifestPath,
|
|
722
|
+
token: existing.token,
|
|
723
|
+
allInstances: true,
|
|
724
|
+
reconcile: false,
|
|
725
|
+
});
|
|
726
|
+
const result = tree.cleanup(options.signal || 'SIGKILL');
|
|
727
|
+
return { ...result, reaped: !pathExists(manifestPath, fsApi) };
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function reconcileProcessManifests(options = {}) {
|
|
731
|
+
const fsApi = options.fsApi || fs;
|
|
732
|
+
const processApi = options.processApi || process;
|
|
733
|
+
const manifestDir = options.manifestDir || DEFAULT_MANIFEST_DIR;
|
|
734
|
+
if (!isOwnedPath(manifestDir, fsApi, processApi)) return { reaped: 0 };
|
|
735
|
+
let entries;
|
|
736
|
+
try {
|
|
737
|
+
entries = fsApi.readdirSync(manifestDir, { withFileTypes: true });
|
|
738
|
+
} catch {
|
|
739
|
+
return { reaped: 0 };
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
let reaped = 0;
|
|
743
|
+
for (const entry of entries) {
|
|
744
|
+
const name = typeof entry === 'string' ? entry : entry?.name;
|
|
745
|
+
if (!name || !name.endsWith('.json')) continue;
|
|
746
|
+
if (typeof entry !== 'string' && entry.isFile && !entry.isFile()) continue;
|
|
747
|
+
const manifestPath = path.join(manifestDir, name);
|
|
748
|
+
const result = reapProcessManifest(manifestPath, {
|
|
749
|
+
...options,
|
|
750
|
+
fsApi,
|
|
751
|
+
processApi,
|
|
752
|
+
manifestDir,
|
|
753
|
+
reconcile: false,
|
|
754
|
+
});
|
|
755
|
+
if (result.reaped) reaped += 1;
|
|
756
|
+
}
|
|
757
|
+
return { reaped };
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
module.exports = {
|
|
761
|
+
DEFAULT_MANIFEST_DIR,
|
|
762
|
+
INSTANCE_ENV,
|
|
763
|
+
MANIFEST_ENV,
|
|
764
|
+
MANIFEST_VERSION,
|
|
765
|
+
TOKEN_ENV,
|
|
766
|
+
createProcessTree,
|
|
767
|
+
defaultGetProcessIdentity,
|
|
768
|
+
isValidManifest,
|
|
769
|
+
readProcessManifest,
|
|
770
|
+
reapProcessManifest,
|
|
771
|
+
reconcileProcessManifests,
|
|
772
|
+
signalExitCode,
|
|
773
|
+
};
|