@phyzess/pappra 0.1.0-beta.13 → 0.1.0-beta.18
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 +134 -15
- package/assets/integrations/skills/pappra/SKILL.md +28 -7
- package/assets/integrations/skills/pappra/references/collaboration.md +633 -0
- package/assets/integrations/skills/pappra/references/environment.md +360 -0
- package/assets/integrations/skills/pappra/references/hosts.md +20 -0
- package/assets/integrations/skills/pappra/references/operations.md +36 -5
- package/dist/adapters/pi/recovery-extension.d.ts +65 -0
- package/dist/adapters/pi/recovery-extension.js +238 -0
- package/dist/adapters/pi/session-transition-extension.d.ts +30 -0
- package/dist/adapters/pi/session-transition-extension.js +265 -0
- package/dist/app/automatic-handoff-events.d.ts +26 -0
- package/dist/app/automatic-handoff-events.js +134 -0
- package/dist/app/bounded-file.d.ts +2 -0
- package/dist/app/bounded-file.js +2 -0
- package/dist/app/capability-evidence/claude.d.ts +37 -0
- package/dist/app/capability-evidence/claude.js +206 -0
- package/dist/app/capability-evidence/codex.d.ts +35 -0
- package/dist/app/capability-evidence/codex.js +193 -0
- package/dist/app/capability-evidence/index.d.ts +33 -0
- package/dist/app/capability-evidence/index.js +117 -0
- package/dist/app/capability-evidence/pi.d.ts +73 -0
- package/dist/app/capability-evidence/pi.js +669 -0
- package/dist/app/capability-evidence/shared.d.ts +69 -0
- package/dist/app/capability-evidence/shared.js +195 -0
- package/dist/app/collab-auto-handoff.d.ts +55 -0
- package/dist/app/collab-auto-handoff.js +384 -0
- package/dist/app/collab-bindings.d.ts +57 -0
- package/dist/app/collab-bindings.js +1006 -0
- package/dist/app/collab-bootstrap-transport.d.ts +163 -0
- package/dist/app/collab-bootstrap-transport.js +769 -0
- package/dist/app/collab-bootstrap.d.ts +137 -0
- package/dist/app/collab-bootstrap.js +3455 -0
- package/dist/app/collab-handoff.d.ts +12 -0
- package/dist/app/collab-handoff.js +511 -0
- package/dist/app/collab-messages.d.ts +2 -0
- package/dist/app/collab-messages.js +2 -0
- package/dist/app/collab-notify.d.ts +156 -0
- package/dist/app/collab-notify.js +1208 -0
- package/dist/app/collab-preflight.d.ts +34 -0
- package/dist/app/collab-preflight.js +96 -0
- package/dist/app/collab-recovery-executor.d.ts +161 -0
- package/dist/app/collab-recovery-executor.js +1153 -0
- package/dist/app/collab-recovery-projection.d.ts +103 -0
- package/dist/app/collab-recovery-projection.js +659 -0
- package/dist/app/collab-recovery.d.ts +25 -0
- package/dist/app/collab-recovery.js +429 -0
- package/dist/app/collab-resources.d.ts +16 -0
- package/dist/app/collab-resources.js +278 -0
- package/dist/app/collab-role-events.d.ts +22 -0
- package/dist/app/collab-role-events.js +329 -0
- package/dist/app/collab-role-orchestration.d.ts +53 -0
- package/dist/app/collab-role-orchestration.js +601 -0
- package/dist/app/collab-role-plan.d.ts +81 -0
- package/dist/app/collab-role-plan.js +504 -0
- package/dist/app/collab-role-repair.d.ts +109 -0
- package/dist/app/collab-role-repair.js +821 -0
- package/dist/app/collab-rollover.d.ts +263 -0
- package/dist/app/collab-rollover.js +1112 -0
- package/dist/app/collab-route.d.ts +64 -0
- package/dist/app/collab-route.js +240 -0
- package/dist/app/collab-session-transition.d.ts +221 -0
- package/dist/app/collab-session-transition.js +3334 -0
- package/dist/app/collab-state.d.ts +7 -0
- package/dist/app/collab-state.js +162 -0
- package/dist/app/collab-task-store.d.ts +22 -0
- package/dist/app/collab-task-store.js +204 -0
- package/dist/app/collab-work-execution.d.ts +127 -0
- package/dist/app/collab-work-execution.js +1027 -0
- package/dist/app/collab-work-plan.d.ts +87 -0
- package/dist/app/collab-work-plan.js +437 -0
- package/dist/app/collaboration.d.ts +8 -2
- package/dist/app/collaboration.js +101 -12
- package/dist/app/doctor.js +108 -5
- package/dist/app/host-mcp.js +34 -8
- package/dist/app/host-probe.js +4 -4
- package/dist/app/init.d.ts +58 -4
- package/dist/app/init.js +768 -155
- package/dist/app/managed-pi-bootstrap-admission.d.ts +14 -0
- package/dist/app/managed-pi-bootstrap-admission.js +104 -0
- package/dist/app/managed-pi-bootstrap-refresh.d.ts +10 -0
- package/dist/app/managed-pi-bootstrap-refresh.js +78 -0
- package/dist/app/managed-pi-delivery.d.ts +8 -0
- package/dist/app/managed-pi-delivery.js +110 -0
- package/dist/app/managed-pi-handoff-receiver.d.ts +15 -0
- package/dist/app/managed-pi-handoff-receiver.js +263 -0
- package/dist/app/managed-pi-handoff-reference.d.ts +3 -0
- package/dist/app/managed-pi-handoff-reference.js +60 -0
- package/dist/app/managed-pi-herdr.d.ts +21 -0
- package/dist/app/managed-pi-herdr.js +185 -0
- package/dist/app/managed-pi-initial-admission.d.ts +28 -0
- package/dist/app/managed-pi-initial-admission.js +416 -0
- package/dist/app/managed-pi-initial-launch.d.ts +5 -0
- package/dist/app/managed-pi-initial-launch.js +61 -0
- package/dist/app/managed-pi-native-observation.d.ts +36 -0
- package/dist/app/managed-pi-native-observation.js +191 -0
- package/dist/app/managed-pi-readiness.d.ts +29 -0
- package/dist/app/managed-pi-readiness.js +288 -0
- package/dist/app/managed-pi-repair-stop.d.ts +21 -0
- package/dist/app/managed-pi-repair-stop.js +371 -0
- package/dist/app/managed-pi-resume-admission.d.ts +32 -0
- package/dist/app/managed-pi-resume-admission.js +618 -0
- package/dist/app/managed-pi-retirement.d.ts +28 -0
- package/dist/app/managed-pi-retirement.js +112 -0
- package/dist/app/managed-pi-sdk-control.d.ts +7 -0
- package/dist/app/managed-pi-sdk-control.js +169 -0
- package/dist/app/managed-pi-sdk-runtime.d.ts +3 -0
- package/dist/app/managed-pi-sdk-runtime.js +483 -0
- package/dist/app/managed-pi-source-admission.d.ts +14 -0
- package/dist/app/managed-pi-source-admission.js +323 -0
- package/dist/app/managed-pi-transition-host.d.ts +36 -0
- package/dist/app/managed-pi-transition-host.js +292 -0
- package/dist/app/managed-pi.d.ts +54 -0
- package/dist/app/managed-pi.js +248 -0
- package/dist/app/shell-readiness.d.ts +66 -0
- package/dist/app/shell-readiness.js +368 -0
- package/dist/app/task-pin.d.ts +2 -23
- package/dist/app/task-pin.js +2 -113
- package/dist/cli/commands/collaboration-binding.d.ts +13 -0
- package/dist/cli/commands/collaboration-binding.js +118 -0
- package/dist/cli/commands/collaboration-bootstrap.d.ts +10 -0
- package/dist/cli/commands/collaboration-bootstrap.js +84 -0
- package/dist/cli/commands/collaboration-capabilities.d.ts +13 -0
- package/dist/cli/commands/collaboration-capabilities.js +127 -0
- package/dist/cli/commands/collaboration-handoff.d.ts +13 -0
- package/dist/cli/commands/collaboration-handoff.js +102 -0
- package/dist/cli/commands/collaboration-message.d.ts +17 -0
- package/dist/cli/commands/collaboration-message.js +177 -0
- package/dist/cli/commands/collaboration-plan.d.ts +8 -0
- package/dist/cli/commands/collaboration-plan.js +108 -0
- package/dist/cli/commands/collaboration-preflight.d.ts +10 -0
- package/dist/cli/commands/collaboration-preflight.js +167 -0
- package/dist/cli/commands/collaboration-recovery-run.d.ts +46 -0
- package/dist/cli/commands/collaboration-recovery-run.js +224 -0
- package/dist/cli/commands/collaboration-recovery.d.ts +11 -0
- package/dist/cli/commands/collaboration-recovery.js +104 -0
- package/dist/cli/commands/collaboration-resources.d.ts +11 -0
- package/dist/cli/commands/collaboration-resources.js +96 -0
- package/dist/cli/commands/collaboration-role-plan.d.ts +9 -0
- package/dist/cli/commands/collaboration-role-plan.js +69 -0
- package/dist/cli/commands/collaboration-role-run.d.ts +11 -0
- package/dist/cli/commands/collaboration-role-run.js +128 -0
- package/dist/cli/commands/collaboration-rollover.d.ts +10 -0
- package/dist/cli/commands/collaboration-rollover.js +103 -0
- package/dist/cli/commands/collaboration-state.d.ts +10 -0
- package/dist/cli/commands/collaboration-state.js +96 -0
- package/dist/cli/commands/collaboration-transition.d.ts +10 -0
- package/dist/cli/commands/collaboration-transition.js +75 -0
- package/dist/cli/commands/collaboration-work-run.d.ts +34 -0
- package/dist/cli/commands/collaboration-work-run.js +178 -0
- package/dist/cli/commands/collaboration.d.ts +4 -0
- package/dist/cli/commands/collaboration.js +39 -4
- package/dist/cli/commands/continuity.js +2 -6
- package/dist/cli/commands/docs.d.ts +2 -0
- package/dist/cli/commands/docs.js +89 -15
- package/dist/cli/commands/host.d.ts +7 -1
- package/dist/cli/commands/host.js +122 -7
- package/dist/cli/commands/init.d.ts +20 -3
- package/dist/cli/commands/init.js +897 -232
- package/dist/cli/commands/mcp.d.ts +1 -0
- package/dist/cli/commands/mcp.js +11 -5
- package/dist/cli/commands/repair.d.ts +12 -0
- package/dist/cli/commands/repair.js +41 -0
- package/dist/cli/commands/runtime.d.ts +1 -1
- package/dist/cli/commands/runtime.js +139 -38
- package/dist/cli/commands/session.d.ts +7 -0
- package/dist/cli/commands/session.js +82 -0
- package/dist/cli/commands/task.js +19 -5
- package/dist/cli/run.js +866 -24
- package/dist/code/identity.d.ts +7 -0
- package/dist/code/identity.js +7 -3
- package/dist/code/registration.js +7 -0
- package/dist/code/service.d.ts +9 -0
- package/dist/code/service.js +16 -3
- package/dist/code/text.js +19 -7
- package/dist/collab/admission.d.ts +91 -66
- package/dist/collab/admission.js +21 -6
- package/dist/collab/canonical.d.ts +1 -1
- package/dist/collab/canonical.js +44 -3
- package/dist/collab/envelope.d.ts +2 -2
- package/dist/collab/envelope.js +12 -12
- package/dist/collab/first-work.d.ts +3 -0
- package/dist/collab/first-work.js +15 -0
- package/dist/collab/handoff.d.ts +16 -0
- package/dist/collab/handoff.js +39 -5
- package/dist/collab/index.d.ts +97 -1
- package/dist/collab/index.js +9 -1
- package/dist/collab/messages.js +126 -3
- package/dist/collab/persisted-messages.d.ts +123 -0
- package/dist/collab/persisted-messages.js +496 -0
- package/dist/collab/preflight.d.ts +1 -1
- package/dist/collab/resources.d.ts +13 -3
- package/dist/collab/resources.js +37 -6
- package/dist/collab/rollover.d.ts +311 -0
- package/dist/collab/rollover.js +1024 -0
- package/dist/collab/store.d.ts +1 -0
- package/dist/collab/store.js +1 -4
- package/dist/collab/types.d.ts +2 -354
- package/dist/collab/validation.d.ts +2 -10
- package/dist/collab/validation.js +2 -303
- package/dist/collab/work-plan.d.ts +105 -0
- package/dist/collab/work-plan.js +843 -0
- package/dist/continuity/context.d.ts +11 -2
- package/dist/continuity/context.js +100 -13
- package/dist/continuity/handoff-bundle.d.ts +296 -0
- package/dist/continuity/handoff-bundle.js +477 -0
- package/dist/continuity/index.d.ts +2 -0
- package/dist/continuity/index.js +1 -0
- package/dist/continuity/paths.js +10 -2
- package/dist/continuity/service.js +91 -15
- package/dist/continuity/task-pin.d.ts +24 -0
- package/dist/continuity/task-pin.js +590 -0
- package/dist/doctor.d.ts +1 -0
- package/dist/doctor.js +6 -0
- package/dist/experimental/index.d.ts +13 -0
- package/dist/experimental/index.js +25 -0
- package/dist/experimental/pi.d.ts +4 -0
- package/dist/experimental/pi.js +12 -0
- package/dist/hosts/claude/native.js +1 -1
- package/dist/hosts/codex/native.js +1 -1
- package/dist/hosts/codex/protocol.js +1 -1
- package/dist/hosts/integration/bootstrap-config.d.ts +58 -0
- package/dist/hosts/integration/bootstrap-config.js +254 -0
- package/dist/hosts/integration/bundle-export.d.ts +44 -0
- package/dist/hosts/integration/bundle-export.js +366 -3
- package/dist/hosts/integration/bundle.js +3 -0
- package/dist/hosts/integration/hook.js +36 -9
- package/dist/hosts/integration/pappra-bootstrap-extension.mjs +80 -0
- package/dist/hosts/integration/pappra-extension-runtime.mjs +59 -24631
- package/dist/hosts/integration/pappra-hook.mjs +61 -24749
- package/dist/hosts/integration/pappra-recovery-extension.mjs +259 -0
- package/dist/hosts/integration/pappra-session-transition-extension.mjs +276 -0
- package/dist/hosts/integration/pi-bootstrap-extension.d.ts +36 -0
- package/dist/hosts/integration/pi-bootstrap-extension.js +101 -0
- package/dist/hosts/integration/pi-bootstrap-guard.d.ts +125 -0
- package/dist/hosts/integration/pi-bootstrap-guard.js +438 -0
- package/dist/hosts/integration/pi-bootstrap-runtime.d.ts +63 -0
- package/dist/hosts/integration/pi-bootstrap-runtime.js +223 -0
- package/dist/hosts/integration/pi-native-events.d.ts +112 -0
- package/dist/hosts/integration/pi-native-events.js +348 -0
- package/dist/hosts/integration/pi-rollover-extension.d.ts +166 -0
- package/dist/hosts/integration/pi-rollover-extension.js +431 -0
- package/dist/hosts/integration/recovery-config.d.ts +48 -0
- package/dist/hosts/integration/recovery-config.js +117 -0
- package/dist/hosts/integration/service.d.ts +6 -1
- package/dist/hosts/integration/service.js +485 -35
- package/dist/hosts/integration/support.d.ts +7 -0
- package/dist/hosts/integration/support.js +9 -1
- package/dist/hosts/integration/templates.d.ts +3 -0
- package/dist/hosts/integration/templates.js +26 -3
- package/dist/hosts/pi/managed-host-identity.d.ts +2 -0
- package/dist/hosts/pi/managed-host-identity.js +40 -0
- package/dist/hosts/pi/managed-provider-diagnostics.d.ts +6 -0
- package/dist/hosts/pi/managed-provider-diagnostics.js +184 -0
- package/dist/hosts/pi/managed-sdk-session.d.ts +335 -0
- package/dist/hosts/pi/managed-sdk-session.js +1639 -0
- package/dist/hosts/pi/managed-session-extension.d.ts +20 -0
- package/dist/hosts/pi/managed-session-extension.js +84 -0
- package/dist/hosts/pi/managed-session.d.ts +177 -0
- package/dist/hosts/pi/managed-session.js +1557 -0
- package/dist/hosts/pi/protocol.js +1 -1
- package/dist/hosts/pi/query-result.js +1 -1
- package/dist/hosts/pi/resume-capability.d.ts +41 -0
- package/dist/hosts/pi/resume-capability.js +33 -0
- package/dist/hosts/pi/rpc.d.ts +182 -134
- package/dist/hosts/pi/rpc.js +230 -37
- package/dist/hosts/pilot.d.ts +97 -74
- package/dist/hosts/pilot.js +5 -1
- package/dist/hosts/version.d.ts +3 -0
- package/dist/hosts/version.js +18 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/infra/bounded-file.d.ts +19 -0
- package/dist/infra/bounded-file.js +66 -0
- package/dist/infra/fs.js +10 -2
- package/dist/infra/git.d.ts +29 -0
- package/dist/infra/git.js +45 -9
- package/dist/infra/json-property.d.ts +2 -0
- package/dist/infra/json-property.js +69 -0
- package/dist/infra/json.d.ts +4 -0
- package/dist/infra/json.js +24 -0
- package/dist/infra/owned-process-group.js +1 -1
- package/dist/infra/private-lock.d.ts +16 -0
- package/dist/infra/private-lock.js +148 -0
- package/dist/infra/private-state.d.ts +7 -0
- package/dist/infra/private-state.js +146 -0
- package/dist/infra/process-identity.d.ts +12 -0
- package/dist/infra/process-identity.js +142 -0
- package/dist/infra/validation.d.ts +5 -0
- package/dist/infra/validation.js +15 -0
- package/dist/init.d.ts +1 -0
- package/dist/init.js +6 -0
- package/dist/install/apply.d.ts +9 -1
- package/dist/install/apply.js +176 -12
- package/dist/install/dependencies.d.ts +38 -0
- package/dist/install/dependencies.js +282 -0
- package/dist/install/ownership.js +30 -1
- package/dist/install/references.d.ts +31 -0
- package/dist/install/references.js +208 -0
- package/dist/install/repair.d.ts +37 -0
- package/dist/install/repair.js +798 -0
- package/dist/install/reset-local.d.ts +16 -0
- package/dist/install/reset-local.js +304 -0
- package/dist/install/rollback.d.ts +3 -0
- package/dist/install/rollback.js +9 -3
- package/dist/install/runtime.d.ts +10 -6
- package/dist/install/runtime.js +299 -59
- package/dist/install/source-fingerprint.d.ts +2 -0
- package/dist/install/source-fingerprint.js +103 -0
- package/dist/knowledge/catalog.d.ts +1 -0
- package/dist/knowledge/catalog.js +143 -10
- package/dist/knowledge/journal.js +17 -7
- package/dist/knowledge/paths.js +2 -1
- package/dist/knowledge/refs.js +4 -0
- package/dist/knowledge/register-batch.d.ts +4 -0
- package/dist/knowledge/register-batch.js +225 -0
- package/dist/knowledge/relocate.d.ts +5 -0
- package/dist/knowledge/relocate.js +92 -0
- package/dist/knowledge/scan.js +103 -47
- package/dist/knowledge/service.d.ts +3 -0
- package/dist/knowledge/service.js +33 -1
- package/dist/knowledge/write.d.ts +4 -1
- package/dist/knowledge/write.js +1 -1
- package/dist/mcp/server.d.ts +4 -2
- package/dist/mcp/server.js +87 -136
- package/dist/schemas/collaboration-state-validation.d.ts +6 -0
- package/dist/schemas/collaboration-state-validation.js +342 -0
- package/dist/schemas/collaboration-state.d.ts +436 -0
- package/dist/schemas/collaboration-state.js +1 -0
- package/dist/schemas/collaboration.d.ts +335 -0
- package/dist/schemas/collaboration.js +272 -0
- package/dist/schemas/continuity.d.ts +8 -0
- package/dist/schemas/continuity.js +13 -3
- package/dist/schemas/doctor.d.ts +1 -0
- package/dist/schemas/doctor.js +1 -0
- package/dist/schemas/document.d.ts +61 -0
- package/dist/schemas/document.js +9 -0
- package/dist/schemas/errors.d.ts +1 -0
- package/dist/schemas/errors.js +1 -0
- package/dist/schemas/host.d.ts +24 -0
- package/dist/schemas/host.js +19 -0
- package/dist/schemas/install.d.ts +8 -2
- package/dist/schemas/install.js +11 -1
- package/dist/schemas/managed-pi-execution.d.ts +16 -0
- package/dist/schemas/managed-pi-execution.js +2 -0
- package/dist/schemas/outcome.d.ts +9 -5
- package/dist/workspace/config-file.js +1 -3
- package/dist/workspace/documents.js +12 -2
- package/dist/workspace/repositories.d.ts +7 -0
- package/dist/workspace/repositories.js +14 -5
- package/package.json +35 -35
- package/skills/pappra/SKILL.md +28 -7
- package/skills/pappra/references/collaboration.md +633 -0
- package/skills/pappra/references/environment.md +360 -0
- package/skills/pappra/references/hosts.md +20 -0
- package/skills/pappra/references/operations.md +36 -5
- package/dist/app/collab-from.d.ts +0 -35
- package/dist/app/collab-from.js +0 -84
package/dist/infra/git.js
CHANGED
|
@@ -20,6 +20,7 @@ function sanitizedGitEnvironment() {
|
|
|
20
20
|
'GIT_CEILING_DIRECTORIES',
|
|
21
21
|
'GIT_CONFIG_COUNT',
|
|
22
22
|
'GIT_CONFIG_PARAMETERS',
|
|
23
|
+
'GIT_EXTERNAL_DIFF',
|
|
23
24
|
]) {
|
|
24
25
|
delete environment[key];
|
|
25
26
|
}
|
|
@@ -281,17 +282,22 @@ export async function inspectGitRepository(candidateRoot, options = {}) {
|
|
|
281
282
|
limitations.push('branch unavailable');
|
|
282
283
|
}
|
|
283
284
|
let dirty = null;
|
|
284
|
-
|
|
285
|
-
'
|
|
286
|
-
'--porcelain=v1',
|
|
287
|
-
'-z',
|
|
288
|
-
'--untracked-files=normal',
|
|
289
|
-
]);
|
|
290
|
-
if (statusResult.ok) {
|
|
291
|
-
dirty = statusResult.value.stdout.length > 0;
|
|
285
|
+
if (options.workingTreeState === false) {
|
|
286
|
+
limitations.push('working tree state not requested');
|
|
292
287
|
}
|
|
293
288
|
else {
|
|
294
|
-
|
|
289
|
+
const statusResult = await run([
|
|
290
|
+
'status',
|
|
291
|
+
'--porcelain=v1',
|
|
292
|
+
'-z',
|
|
293
|
+
'--untracked-files=normal',
|
|
294
|
+
]);
|
|
295
|
+
if (statusResult.ok) {
|
|
296
|
+
dirty = statusResult.value.stdout.length > 0;
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
limitations.push(`working tree state unavailable: ${statusResult.message}`);
|
|
300
|
+
}
|
|
295
301
|
}
|
|
296
302
|
const kind = isBare
|
|
297
303
|
? 'bare'
|
|
@@ -356,3 +362,33 @@ export async function filterGitIgnored(repositoryRoot, candidates, options = {})
|
|
|
356
362
|
}
|
|
357
363
|
return succeeded(ignored);
|
|
358
364
|
}
|
|
365
|
+
/**
|
|
366
|
+
* Memoizes Git repository probes for exactly one operation.
|
|
367
|
+
*
|
|
368
|
+
* Repository identity (root, git dir, HEAD, branch) is immutable for the
|
|
369
|
+
* duration of a single command, so repeated inspections of the same checkout
|
|
370
|
+
* reuse the first observation instead of spawning equivalent Git processes.
|
|
371
|
+
* Working-tree state is *not* immutable: the scope therefore only lives inside
|
|
372
|
+
* one operation and must never be reused across operations or persisted. Callers
|
|
373
|
+
* that re-observe a tree after mutation (code prepare) keep using the scope for
|
|
374
|
+
* identity probes while the working-tree diff is always re-read.
|
|
375
|
+
*/
|
|
376
|
+
export function createGitObservationScope(options = {}) {
|
|
377
|
+
const defaultWorkingTreeState = options.workingTreeState ?? true;
|
|
378
|
+
const observations = new Map();
|
|
379
|
+
return {
|
|
380
|
+
repository(root, inspectOptions = {}) {
|
|
381
|
+
const workingTreeState = inspectOptions.workingTreeState ?? defaultWorkingTreeState;
|
|
382
|
+
const key = JSON.stringify([resolve(root), workingTreeState]);
|
|
383
|
+
const cached = observations.get(key);
|
|
384
|
+
if (cached !== undefined)
|
|
385
|
+
return cached;
|
|
386
|
+
const pending = inspectGitRepository(root, {
|
|
387
|
+
...inspectOptions,
|
|
388
|
+
workingTreeState,
|
|
389
|
+
});
|
|
390
|
+
observations.set(key, pending);
|
|
391
|
+
return pending;
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** Replace one top-level JSON property while retaining unrelated bytes and style. */
|
|
2
|
+
export function updateJsonProperty(existing, key, value) {
|
|
3
|
+
if (existing === null)
|
|
4
|
+
return Buffer.from(`${JSON.stringify({ [key]: value }, null, 2)}\n`);
|
|
5
|
+
const text = existing.toString('utf8');
|
|
6
|
+
const parsed = JSON.parse(text);
|
|
7
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
8
|
+
throw new Error('json_object_required');
|
|
9
|
+
const newline = text.includes('\r\n') ? '\r\n' : '\n';
|
|
10
|
+
let indent = '';
|
|
11
|
+
const skipSpace = (at) => {
|
|
12
|
+
while (/\s/.test(text[at] ?? '') && at < text.length)
|
|
13
|
+
at += 1;
|
|
14
|
+
return at;
|
|
15
|
+
};
|
|
16
|
+
const stringEnd = (start) => {
|
|
17
|
+
let at = start + 1;
|
|
18
|
+
while (at < text.length) {
|
|
19
|
+
if (text[at] === '\\')
|
|
20
|
+
at += 2;
|
|
21
|
+
else if (text[at++] === '"')
|
|
22
|
+
return at;
|
|
23
|
+
}
|
|
24
|
+
throw new Error('json_string_unterminated');
|
|
25
|
+
};
|
|
26
|
+
const render = (prefix) => {
|
|
27
|
+
const unit = indent || (text.includes('\n') ? ' ' : undefined);
|
|
28
|
+
return (JSON.stringify(value, null, unit)?.replaceAll('\n', `${newline}${prefix}`) ?? 'null');
|
|
29
|
+
};
|
|
30
|
+
let at = skipSpace(text.indexOf('{') + 1);
|
|
31
|
+
while (text[at] !== '}') {
|
|
32
|
+
const endKey = stringEnd(at);
|
|
33
|
+
const property = JSON.parse(text.slice(at, endKey));
|
|
34
|
+
const line = text.slice(text.lastIndexOf('\n', at) + 1, at);
|
|
35
|
+
if (/^[ \t]+$/.test(line) && (!indent || line.length < indent.length))
|
|
36
|
+
indent = line;
|
|
37
|
+
const startValue = skipSpace(skipSpace(endKey) + 1);
|
|
38
|
+
let endValue = startValue;
|
|
39
|
+
let depth = 0;
|
|
40
|
+
while (endValue < text.length) {
|
|
41
|
+
const character = text[endValue];
|
|
42
|
+
if (character === '"') {
|
|
43
|
+
endValue = stringEnd(endValue);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (depth === 0 && (character === ',' || character === '}'))
|
|
47
|
+
break;
|
|
48
|
+
if (character === '[' || character === '{')
|
|
49
|
+
depth += 1;
|
|
50
|
+
if (character === ']' || character === '}')
|
|
51
|
+
depth -= 1;
|
|
52
|
+
endValue += 1;
|
|
53
|
+
}
|
|
54
|
+
let contentEnd = endValue;
|
|
55
|
+
while (/\s/.test(text[contentEnd - 1] ?? '') && contentEnd > startValue)
|
|
56
|
+
contentEnd -= 1;
|
|
57
|
+
if (property === key)
|
|
58
|
+
return Buffer.from(text.slice(0, startValue) +
|
|
59
|
+
render(/^[ \t]*$/.test(line) ? line : '') +
|
|
60
|
+
text.slice(contentEnd));
|
|
61
|
+
at = skipSpace(endValue + (text[endValue] === ',' ? 1 : 0));
|
|
62
|
+
}
|
|
63
|
+
const prefix = text.slice(0, at).trimEnd();
|
|
64
|
+
const separator = Object.keys(parsed).length > 0 ? ',' : '';
|
|
65
|
+
const insertion = text.includes('\n')
|
|
66
|
+
? `${separator}${newline}${indent || ' '}${JSON.stringify(key)}: ${render(indent || ' ')}${newline}`
|
|
67
|
+
: `${separator}${JSON.stringify(key)}:${JSON.stringify(value)}`;
|
|
68
|
+
return Buffer.from(prefix + insertion + text.slice(at));
|
|
69
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Canonicalize the value JSON actually persists, including omitted object keys and array holes. */
|
|
2
|
+
export declare function canonicalJson(value: unknown): string;
|
|
3
|
+
/** Raw SHA-256 hex for contracts that pin canonical JSON rather than insertion order. */
|
|
4
|
+
export declare function canonicalJsonDigest(value: unknown): string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
/** Canonicalize the value JSON actually persists, including omitted object keys and array holes. */
|
|
3
|
+
export function canonicalJson(value) {
|
|
4
|
+
const serialized = JSON.stringify(value);
|
|
5
|
+
if (serialized === undefined)
|
|
6
|
+
throw new TypeError('Canonical JSON requires a JSON-serializable value');
|
|
7
|
+
const persisted = JSON.parse(serialized);
|
|
8
|
+
return encode(persisted);
|
|
9
|
+
}
|
|
10
|
+
function encode(value) {
|
|
11
|
+
if (value === null || typeof value !== 'object')
|
|
12
|
+
return JSON.stringify(value);
|
|
13
|
+
if (Array.isArray(value))
|
|
14
|
+
return `[${value.map(encode).join(',')}]`;
|
|
15
|
+
const record = value;
|
|
16
|
+
return `{${Object.keys(record)
|
|
17
|
+
.sort()
|
|
18
|
+
.map((key) => `${JSON.stringify(key)}:${encode(record[key])}`)
|
|
19
|
+
.join(',')}}`;
|
|
20
|
+
}
|
|
21
|
+
/** Raw SHA-256 hex for contracts that pin canonical JSON rather than insertion order. */
|
|
22
|
+
export function canonicalJsonDigest(value) {
|
|
23
|
+
return createHash('sha256').update(canonicalJson(value)).digest('hex');
|
|
24
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { errorCode } from '
|
|
1
|
+
import { errorCode } from './validation.js';
|
|
2
2
|
import { execFileSync, spawn } from 'node:child_process';
|
|
3
3
|
// Scoped Unix process-group helper. It exists to extend a stop beyond the single owned parent
|
|
4
4
|
// into the exact process group that parent created, and nothing else.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Publish only a complete, fsynced lock; a crash cannot expose an empty owner record. */
|
|
2
|
+
export declare function publishPrivateLock(file: string, bytes: Buffer, revalidate?: () => Promise<void>): Promise<void>;
|
|
3
|
+
export interface PrivateLockObservation {
|
|
4
|
+
readonly file: string;
|
|
5
|
+
readonly state: 'missing' | 'alive' | 'dead' | 'unknown';
|
|
6
|
+
readonly token: string | null;
|
|
7
|
+
readonly digest: string | null;
|
|
8
|
+
readonly identity: {
|
|
9
|
+
readonly dev: number;
|
|
10
|
+
readonly ino: number;
|
|
11
|
+
} | null;
|
|
12
|
+
}
|
|
13
|
+
/** An old PID-only lock is reclaimable only after ESRCH, never by its age. */
|
|
14
|
+
export declare function inspectPrivateLock(file: string): Promise<PrivateLockObservation>;
|
|
15
|
+
/** One immutable claim wins a stale owner boundary; interrupted claimants can be succeeded. */
|
|
16
|
+
export declare function reclaimDeadPrivateLock(file: string, revalidate: () => Promise<void>): Promise<boolean>;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { link, lstat, open, unlink } from 'node:fs/promises';
|
|
4
|
+
import { sha256Digest } from './fs.js';
|
|
5
|
+
import { captureProcessIdentity, isLocalProcessIdentity, observeProcessIdentity, } from './process-identity.js';
|
|
6
|
+
/** Publish only a complete, fsynced lock; a crash cannot expose an empty owner record. */
|
|
7
|
+
export async function publishPrivateLock(file, bytes, revalidate) {
|
|
8
|
+
const temporary = `${file}.pending-${process.pid}-${randomUUID()}`;
|
|
9
|
+
let handle;
|
|
10
|
+
try {
|
|
11
|
+
handle = await open(temporary, constants.O_WRONLY |
|
|
12
|
+
constants.O_CREAT |
|
|
13
|
+
constants.O_EXCL |
|
|
14
|
+
constants.O_NOFOLLOW, 0o600);
|
|
15
|
+
await handle.writeFile(bytes);
|
|
16
|
+
await handle.sync();
|
|
17
|
+
await revalidate?.();
|
|
18
|
+
await link(temporary, file);
|
|
19
|
+
await revalidate?.();
|
|
20
|
+
const published = await lstat(file);
|
|
21
|
+
const staged = await handle.stat();
|
|
22
|
+
if (!published.isFile() ||
|
|
23
|
+
published.dev !== staged.dev ||
|
|
24
|
+
published.ino !== staged.ino)
|
|
25
|
+
throw new Error(`Published lock identity changed: ${file}`);
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
await handle?.close();
|
|
29
|
+
await unlink(temporary).catch(() => undefined);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** An old PID-only lock is reclaimable only after ESRCH, never by its age. */
|
|
33
|
+
export async function inspectPrivateLock(file) {
|
|
34
|
+
const empty = {
|
|
35
|
+
file,
|
|
36
|
+
state: 'unknown',
|
|
37
|
+
token: null,
|
|
38
|
+
digest: null,
|
|
39
|
+
identity: null,
|
|
40
|
+
};
|
|
41
|
+
let handle;
|
|
42
|
+
try {
|
|
43
|
+
handle = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
44
|
+
const stats = await handle.stat();
|
|
45
|
+
if (!stats.isFile() || stats.size > 64 * 1024)
|
|
46
|
+
return empty;
|
|
47
|
+
const bytes = Buffer.alloc(64 * 1024 + 1);
|
|
48
|
+
const read = await handle.read(bytes, 0, bytes.length, 0);
|
|
49
|
+
if (read.bytesRead > 64 * 1024)
|
|
50
|
+
return empty;
|
|
51
|
+
const content = bytes.subarray(0, read.bytesRead);
|
|
52
|
+
let value;
|
|
53
|
+
try {
|
|
54
|
+
value = JSON.parse(content.toString('utf8'));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return empty;
|
|
58
|
+
}
|
|
59
|
+
if (typeof value !== 'object' ||
|
|
60
|
+
value === null ||
|
|
61
|
+
!('pid' in value) ||
|
|
62
|
+
!('token' in value) ||
|
|
63
|
+
typeof value.pid !== 'number' ||
|
|
64
|
+
!Number.isSafeInteger(value.pid) ||
|
|
65
|
+
value.pid <= 0 ||
|
|
66
|
+
typeof value.token !== 'string' ||
|
|
67
|
+
!value.token)
|
|
68
|
+
return empty;
|
|
69
|
+
let state = 'unknown';
|
|
70
|
+
if ('process_identity' in value && value.process_identity !== undefined) {
|
|
71
|
+
state =
|
|
72
|
+
isLocalProcessIdentity(value.process_identity) &&
|
|
73
|
+
value.process_identity.pid === value.pid
|
|
74
|
+
? observeProcessIdentity(value.process_identity)
|
|
75
|
+
: 'unknown';
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
try {
|
|
79
|
+
process.kill(value.pid, 0);
|
|
80
|
+
// PID existence alone cannot bind an imported lock to that process birth.
|
|
81
|
+
state = 'unknown';
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (error.code === 'ESRCH')
|
|
85
|
+
state = 'dead';
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
file,
|
|
90
|
+
state,
|
|
91
|
+
token: value.token,
|
|
92
|
+
digest: sha256Digest(content),
|
|
93
|
+
identity: { dev: stats.dev, ino: stats.ino },
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
return error.code === 'ENOENT'
|
|
98
|
+
? { ...empty, state: 'missing' }
|
|
99
|
+
: empty;
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
await handle?.close();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function sameLock(a, b) {
|
|
106
|
+
return (a.state === b.state &&
|
|
107
|
+
a.token === b.token &&
|
|
108
|
+
a.digest === b.digest &&
|
|
109
|
+
a.identity?.dev === b.identity?.dev &&
|
|
110
|
+
a.identity?.ino === b.identity?.ino);
|
|
111
|
+
}
|
|
112
|
+
/** One immutable claim wins a stale owner boundary; interrupted claimants can be succeeded. */
|
|
113
|
+
export async function reclaimDeadPrivateLock(file, revalidate) {
|
|
114
|
+
await revalidate();
|
|
115
|
+
const original = await inspectPrivateLock(file);
|
|
116
|
+
if (original.state === 'missing')
|
|
117
|
+
return false;
|
|
118
|
+
if (original.state !== 'dead' || original.digest === null)
|
|
119
|
+
throw new Error(`Lock owner is alive or unverified; preserve ${file}`);
|
|
120
|
+
const base = `${file}.reclaim-${original.digest.slice(7)}`;
|
|
121
|
+
let claim = base;
|
|
122
|
+
for (let hop = 0; hop < 64; hop += 1) {
|
|
123
|
+
await revalidate();
|
|
124
|
+
const before = await inspectPrivateLock(file);
|
|
125
|
+
if (before.state === 'missing')
|
|
126
|
+
return false;
|
|
127
|
+
if (!sameLock(original, before))
|
|
128
|
+
throw new Error(`Lock changed during recovery: ${file}`);
|
|
129
|
+
try {
|
|
130
|
+
await publishPrivateLock(claim, Buffer.from(`${JSON.stringify({ token: randomUUID(), pid: process.pid, process_identity: captureProcessIdentity() ?? undefined })}\n`), revalidate);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (error.code !== 'EEXIST')
|
|
134
|
+
throw error;
|
|
135
|
+
const owner = await inspectPrivateLock(claim);
|
|
136
|
+
if (owner.state !== 'dead')
|
|
137
|
+
throw new Error(`Another or unverified repair owns ${file}`);
|
|
138
|
+
claim = `${base}.retry-${sha256Digest(JSON.stringify([owner.digest, owner.identity])).slice(7)}`;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
await revalidate();
|
|
142
|
+
if (!sameLock(original, await inspectPrivateLock(file)))
|
|
143
|
+
throw new Error(`Lock changed after recovery reservation: ${file}`);
|
|
144
|
+
await unlink(file);
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
throw new Error(`Too many interrupted lock repairs; preserve ${file}`);
|
|
148
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type Outcome } from '../schemas/outcome.js';
|
|
2
|
+
/**
|
|
3
|
+
* Prepare private state before its first write, independently of init. Existing
|
|
4
|
+
* ignore rules are preserved; an appended final wildcard prevents earlier
|
|
5
|
+
* negations from exposing machine paths, checkpoints, or task metadata.
|
|
6
|
+
*/
|
|
7
|
+
export declare function ensurePrivateState(root: string, directory?: 'local' | 'local-archive'): Promise<Outcome<null>>;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { mkdir, open } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { diagnosticCodes } from '../schemas/errors.js';
|
|
5
|
+
import { errorMessage, failed, succeeded, } from '../schemas/outcome.js';
|
|
6
|
+
import { observePath, resolveRealpath } from './fs.js';
|
|
7
|
+
const MAX_IGNORE_BYTES = 64 * 1024;
|
|
8
|
+
function hasFinalIgnoreAll(text) {
|
|
9
|
+
const rules = text
|
|
10
|
+
.split('\n')
|
|
11
|
+
.map((line) => line.replace(/\r$/, ''))
|
|
12
|
+
.filter((line) => line !== '' && !line.startsWith('#'));
|
|
13
|
+
return rules.at(-1) === '*';
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Prepare private state before its first write, independently of init. Existing
|
|
17
|
+
* ignore rules are preserved; an appended final wildcard prevents earlier
|
|
18
|
+
* negations from exposing machine paths, checkpoints, or task metadata.
|
|
19
|
+
*/
|
|
20
|
+
export async function ensurePrivateState(root, directory = 'local') {
|
|
21
|
+
if (directory !== 'local' && directory !== 'local-archive')
|
|
22
|
+
return failed(diagnosticCodes.initTargetUnsafe, 'Unknown private-state namespace.', { nextAction: 'Use a supported private-state directory.' });
|
|
23
|
+
const canonical = await resolveRealpath(root);
|
|
24
|
+
if (canonical === null)
|
|
25
|
+
return failed(diagnosticCodes.initTargetUnsafe, `Cannot resolve the private-state workspace: ${root}`, { nextAction: 'Use an existing workspace root.' });
|
|
26
|
+
const pinned = [];
|
|
27
|
+
const assertCurrent = async () => {
|
|
28
|
+
for (const identity of pinned) {
|
|
29
|
+
const current = await observePath(identity.path);
|
|
30
|
+
if (current.kind !== 'directory' ||
|
|
31
|
+
current.stats?.dev !== identity.dev ||
|
|
32
|
+
current.stats.ino !== identity.ino) {
|
|
33
|
+
throw new Error(`Private-state namespace changed: ${identity.path}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const local = join(canonical, '.pappra', directory);
|
|
38
|
+
const ignore = join(local, '.gitignore');
|
|
39
|
+
try {
|
|
40
|
+
for (const path of [canonical, join(canonical, '.pappra'), local]) {
|
|
41
|
+
await assertCurrent();
|
|
42
|
+
let observation = await observePath(path);
|
|
43
|
+
if (observation.kind === 'missing' && path !== canonical) {
|
|
44
|
+
await mkdir(path, { mode: 0o700 }).catch(async (error) => {
|
|
45
|
+
if ((await observePath(path)).kind !== 'directory')
|
|
46
|
+
throw error;
|
|
47
|
+
});
|
|
48
|
+
observation = await observePath(path);
|
|
49
|
+
}
|
|
50
|
+
if (observation.kind !== 'directory' ||
|
|
51
|
+
observation.stats === null ||
|
|
52
|
+
(await resolveRealpath(path)) !== path) {
|
|
53
|
+
throw new Error(`Private-state namespace is redirected or not a directory: ${path}`);
|
|
54
|
+
}
|
|
55
|
+
pinned.push({
|
|
56
|
+
path,
|
|
57
|
+
dev: observation.stats.dev,
|
|
58
|
+
ino: observation.stats.ino,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
await assertCurrent();
|
|
62
|
+
let observation = await observePath(ignore);
|
|
63
|
+
if (observation.kind === 'missing') {
|
|
64
|
+
let created;
|
|
65
|
+
try {
|
|
66
|
+
created = await open(ignore, constants.O_CREAT |
|
|
67
|
+
constants.O_EXCL |
|
|
68
|
+
constants.O_WRONLY |
|
|
69
|
+
constants.O_APPEND |
|
|
70
|
+
constants.O_NOFOLLOW, 0o600);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if ((await observePath(ignore)).kind === 'missing')
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
if (created !== undefined) {
|
|
77
|
+
try {
|
|
78
|
+
await assertCurrent();
|
|
79
|
+
await created.writeFile('*\n');
|
|
80
|
+
await created.sync();
|
|
81
|
+
const stats = await created.stat();
|
|
82
|
+
observation = await observePath(ignore);
|
|
83
|
+
if (observation.kind !== 'file' ||
|
|
84
|
+
observation.stats?.dev !== stats.dev ||
|
|
85
|
+
observation.stats.ino !== stats.ino) {
|
|
86
|
+
throw new Error(`Private-state ignore file changed during creation: ${ignore}`);
|
|
87
|
+
}
|
|
88
|
+
await assertCurrent();
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
await created.close();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
observation = await observePath(ignore);
|
|
95
|
+
}
|
|
96
|
+
if (observation.kind !== 'file' || observation.stats === null)
|
|
97
|
+
throw new Error(`Private-state ignore path is not a regular file: ${ignore}`);
|
|
98
|
+
const handle = await open(ignore, constants.O_RDWR | constants.O_APPEND | constants.O_NOFOLLOW);
|
|
99
|
+
try {
|
|
100
|
+
const stats = await handle.stat();
|
|
101
|
+
if (!stats.isFile() ||
|
|
102
|
+
stats.dev !== observation.stats.dev ||
|
|
103
|
+
stats.ino !== observation.stats.ino) {
|
|
104
|
+
throw new Error(`Private-state ignore identity changed: ${ignore}`);
|
|
105
|
+
}
|
|
106
|
+
const readText = async () => {
|
|
107
|
+
const bytes = Buffer.alloc(MAX_IGNORE_BYTES + 1);
|
|
108
|
+
let length = 0;
|
|
109
|
+
while (length < bytes.length) {
|
|
110
|
+
const read = await handle.read(bytes, length, bytes.length - length, length);
|
|
111
|
+
if (read.bytesRead === 0)
|
|
112
|
+
break;
|
|
113
|
+
length += read.bytesRead;
|
|
114
|
+
}
|
|
115
|
+
if (length > MAX_IGNORE_BYTES)
|
|
116
|
+
throw new Error(`Private-state ignore file exceeds ${MAX_IGNORE_BYTES} bytes: ${ignore}`);
|
|
117
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(bytes.subarray(0, length));
|
|
118
|
+
};
|
|
119
|
+
const text = await readText();
|
|
120
|
+
await assertCurrent();
|
|
121
|
+
if (!hasFinalIgnoreAll(text)) {
|
|
122
|
+
// O_APPEND preserves existing bytes even if another writer extends the
|
|
123
|
+
// file after it was read; no user file is replaced or truncated.
|
|
124
|
+
await handle.writeFile(`${text.endsWith('\n') || text === '' ? '' : '\n'}*\n`);
|
|
125
|
+
await handle.sync();
|
|
126
|
+
}
|
|
127
|
+
const current = await observePath(ignore);
|
|
128
|
+
if (current.kind !== 'file' ||
|
|
129
|
+
current.stats?.dev !== stats.dev ||
|
|
130
|
+
current.stats.ino !== stats.ino ||
|
|
131
|
+
!hasFinalIgnoreAll(await readText())) {
|
|
132
|
+
throw new Error(`Private-state ignore file changed during preparation: ${ignore}`);
|
|
133
|
+
}
|
|
134
|
+
await assertCurrent();
|
|
135
|
+
return succeeded(null);
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
await handle.close();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
return failed(diagnosticCodes.initTargetUnsafe, `Cannot prepare private state: ${errorMessage(error)}`, {
|
|
143
|
+
nextAction: `Restore real .pappra/${directory} directories and a writable regular .gitignore; existing state and rules were preserved.`,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface LocalProcessIdentity {
|
|
2
|
+
readonly schema: 'pappra/local-process/1';
|
|
3
|
+
readonly pid: number;
|
|
4
|
+
readonly machine: string;
|
|
5
|
+
readonly boot: string;
|
|
6
|
+
readonly namespace: string;
|
|
7
|
+
readonly start: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function captureProcessIdentity(pid?: number): LocalProcessIdentity | null;
|
|
10
|
+
/** Identity mismatches prove replacement only on this exact physical machine. */
|
|
11
|
+
export declare function isLocalProcessIdentity(value: unknown): value is LocalProcessIdentity;
|
|
12
|
+
export declare function observeProcessIdentity(value: unknown): 'alive' | 'dead' | 'unknown';
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
let host;
|
|
5
|
+
let self;
|
|
6
|
+
const hash = (value) => `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`;
|
|
7
|
+
function command(file, args) {
|
|
8
|
+
const result = spawnSync(file, args, {
|
|
9
|
+
encoding: 'utf8',
|
|
10
|
+
env: { ...process.env, LC_ALL: 'C', LANG: 'C' },
|
|
11
|
+
timeout: 2000,
|
|
12
|
+
maxBuffer: 32 * 1024,
|
|
13
|
+
shell: false,
|
|
14
|
+
});
|
|
15
|
+
return !result.error && result.status === 0 ? result.stdout.trim() : null;
|
|
16
|
+
}
|
|
17
|
+
function hostIdentity() {
|
|
18
|
+
if (host !== undefined)
|
|
19
|
+
return host;
|
|
20
|
+
try {
|
|
21
|
+
if (process.platform === 'linux') {
|
|
22
|
+
const machine = fs.readFileSync('/etc/machine-id', 'utf8').trim();
|
|
23
|
+
const boot = fs
|
|
24
|
+
.readFileSync('/proc/sys/kernel/random/boot_id', 'utf8')
|
|
25
|
+
.trim();
|
|
26
|
+
const namespace = fs.readlinkSync('/proc/self/ns/pid');
|
|
27
|
+
if (!/^[a-f0-9]{32}$/i.test(machine) ||
|
|
28
|
+
!/^[a-f0-9-]{36}$/i.test(boot) ||
|
|
29
|
+
!/^pid:\[[0-9]+\]$/.test(namespace))
|
|
30
|
+
return (host = null);
|
|
31
|
+
return (host = {
|
|
32
|
+
machine: hash(machine.toLowerCase()),
|
|
33
|
+
boot: hash(boot.toLowerCase()),
|
|
34
|
+
namespace,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
if (process.platform === 'darwin') {
|
|
38
|
+
const boot = command('/usr/sbin/sysctl', ['-n', 'kern.bootsessionuuid']);
|
|
39
|
+
const platform = command('/usr/sbin/ioreg', [
|
|
40
|
+
'-rd1',
|
|
41
|
+
'-c',
|
|
42
|
+
'IOPlatformExpertDevice',
|
|
43
|
+
]);
|
|
44
|
+
const machine = platform?.match(/"IOPlatformUUID"\s*=\s*"([a-f0-9-]{36})"/i)?.[1];
|
|
45
|
+
if (!boot || !/^[a-f0-9-]{36}$/i.test(boot) || !machine)
|
|
46
|
+
return (host = null);
|
|
47
|
+
return (host = {
|
|
48
|
+
machine: hash(machine.toLowerCase()),
|
|
49
|
+
boot: hash(boot.toLowerCase()),
|
|
50
|
+
namespace: 'native',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
/* Missing OS identity is not evidence of a dead owner. */
|
|
56
|
+
}
|
|
57
|
+
return (host = null);
|
|
58
|
+
}
|
|
59
|
+
function processStart(pid) {
|
|
60
|
+
try {
|
|
61
|
+
if (process.platform === 'linux') {
|
|
62
|
+
const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
63
|
+
const close = stat.lastIndexOf(')');
|
|
64
|
+
const fields = stat
|
|
65
|
+
.slice(close + 2)
|
|
66
|
+
.trim()
|
|
67
|
+
.split(/\s+/);
|
|
68
|
+
const ticks = fields[19];
|
|
69
|
+
return close > 0 && ticks && /^[0-9]+$/.test(ticks) ? ticks : null;
|
|
70
|
+
}
|
|
71
|
+
if (process.platform === 'darwin') {
|
|
72
|
+
const start = command('/bin/ps', ['-p', String(pid), '-o', 'lstart=']);
|
|
73
|
+
return start &&
|
|
74
|
+
/^[A-Za-z]{3} [A-Za-z]{3}\s+[0-9]+ [0-9:]{8} [0-9]{4}$/.test(start)
|
|
75
|
+
? start
|
|
76
|
+
: null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
/* The caller distinguishes exit from unavailable observation. */
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
export function captureProcessIdentity(pid = process.pid) {
|
|
85
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
86
|
+
return null;
|
|
87
|
+
if (pid === process.pid && self !== undefined)
|
|
88
|
+
return self;
|
|
89
|
+
const currentHost = hostIdentity();
|
|
90
|
+
const start = processStart(pid);
|
|
91
|
+
const identity = currentHost && start
|
|
92
|
+
? {
|
|
93
|
+
schema: 'pappra/local-process/1',
|
|
94
|
+
pid,
|
|
95
|
+
...currentHost,
|
|
96
|
+
start,
|
|
97
|
+
}
|
|
98
|
+
: null;
|
|
99
|
+
if (pid === process.pid)
|
|
100
|
+
self = identity;
|
|
101
|
+
return identity;
|
|
102
|
+
}
|
|
103
|
+
/** Identity mismatches prove replacement only on this exact physical machine. */
|
|
104
|
+
export function isLocalProcessIdentity(value) {
|
|
105
|
+
if (typeof value !== 'object' || value === null)
|
|
106
|
+
return false;
|
|
107
|
+
const row = value;
|
|
108
|
+
return !(row.schema !== 'pappra/local-process/1' ||
|
|
109
|
+
!Number.isSafeInteger(row.pid) ||
|
|
110
|
+
Number(row.pid) <= 0 ||
|
|
111
|
+
typeof row.machine !== 'string' ||
|
|
112
|
+
typeof row.boot !== 'string' ||
|
|
113
|
+
typeof row.namespace !== 'string' ||
|
|
114
|
+
typeof row.start !== 'string' ||
|
|
115
|
+
row.start.length === 0 ||
|
|
116
|
+
row.start.length > 128 ||
|
|
117
|
+
!/^sha256:[0-9a-f]{64}$/.test(String(row.machine)) ||
|
|
118
|
+
!/^sha256:[0-9a-f]{64}$/.test(String(row.boot)) ||
|
|
119
|
+
!/^(?:native|pid:\[[0-9]+\])$/.test(String(row.namespace)));
|
|
120
|
+
}
|
|
121
|
+
export function observeProcessIdentity(value) {
|
|
122
|
+
if (!isLocalProcessIdentity(value))
|
|
123
|
+
return 'unknown';
|
|
124
|
+
const row = value;
|
|
125
|
+
const currentHost = hostIdentity();
|
|
126
|
+
if (!currentHost || row.machine !== currentHost.machine)
|
|
127
|
+
return 'unknown';
|
|
128
|
+
if (row.boot !== currentHost.boot)
|
|
129
|
+
return 'dead';
|
|
130
|
+
if (row.namespace !== currentHost.namespace)
|
|
131
|
+
return 'unknown';
|
|
132
|
+
try {
|
|
133
|
+
process.kill(Number(row.pid), 0);
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
return error.code === 'ESRCH'
|
|
137
|
+
? 'dead'
|
|
138
|
+
: 'unknown';
|
|
139
|
+
}
|
|
140
|
+
const start = processStart(Number(row.pid));
|
|
141
|
+
return start === null ? 'unknown' : start === row.start ? 'alive' : 'dead';
|
|
142
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Decode external JSON as unknown; domain validators establish usable shapes. */
|
|
2
|
+
export declare function object(value: unknown): value is Record<string, unknown>;
|
|
3
|
+
export declare function text(value: unknown): value is string;
|
|
4
|
+
export declare function errorCode(error: unknown): string | undefined;
|
|
5
|
+
export declare function parseJson(text: string): unknown;
|