@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
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Decode external JSON as unknown; domain validators establish usable shapes. */
|
|
2
|
+
export function object(value) {
|
|
3
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
export function text(value) {
|
|
6
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
7
|
+
}
|
|
8
|
+
export function errorCode(error) {
|
|
9
|
+
return object(error) && typeof error['code'] === 'string'
|
|
10
|
+
? error['code']
|
|
11
|
+
: undefined;
|
|
12
|
+
}
|
|
13
|
+
export function parseJson(text) {
|
|
14
|
+
return JSON.parse(text);
|
|
15
|
+
}
|
package/dist/init.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './app/init.js';
|
package/dist/init.js
ADDED
package/dist/install/apply.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Diagnostic } from '../schemas/diagnostics.js';
|
|
|
2
2
|
import { type InstallJournal, type ManagedFileEntry, type RuntimeManifest, type WorkspaceInstallManifest } from '../schemas/install.js';
|
|
3
3
|
import type { InitPlan } from '../schemas/init.js';
|
|
4
4
|
import type { PlannedFileContent } from './plan.js';
|
|
5
|
+
import { type Outcome } from '../schemas/outcome.js';
|
|
5
6
|
export interface FileIdentity {
|
|
6
7
|
readonly dev: number;
|
|
7
8
|
readonly ino: number;
|
|
@@ -28,7 +29,10 @@ export type ManagedApplyBegin = {
|
|
|
28
29
|
readonly residualPaths: readonly string[];
|
|
29
30
|
};
|
|
30
31
|
export declare function beginManagedApply(root: string, plan: InitPlan, writes: readonly PlannedFileContent[], rootIdentity: FileIdentity): Promise<ManagedApplyBegin>;
|
|
31
|
-
export declare function recordManagedFile(context: ManagedApplyContext, path: string, digest: string, identity: FileIdentity
|
|
32
|
+
export declare function recordManagedFile(context: ManagedApplyContext, path: string, digest: string, identity: FileIdentity, replacement?: {
|
|
33
|
+
readonly backupRef: string;
|
|
34
|
+
readonly backupDigest: string;
|
|
35
|
+
}): Promise<{
|
|
32
36
|
readonly ok: true;
|
|
33
37
|
} | {
|
|
34
38
|
readonly ok: false;
|
|
@@ -56,6 +60,10 @@ export interface ManagedMachineUpdateResult {
|
|
|
56
60
|
readonly updateInstallId?: string;
|
|
57
61
|
readonly journalPath?: string;
|
|
58
62
|
}
|
|
63
|
+
/** Read-only gate shared by initial planning and every apply, including no-op plans. */
|
|
64
|
+
export declare function inspectWorkspaceInstallReadiness(root: string, ownLock?: boolean): Promise<Outcome<null>>;
|
|
65
|
+
/** Serialize a zero-file apply with installation and recovery, without adding an empty journal. */
|
|
66
|
+
export declare function verifyWorkspaceInstallReady(root: string): Promise<Outcome<null>>;
|
|
59
67
|
/** Updates the managed machine.json runtime location after a verified install. */
|
|
60
68
|
export declare function updateMachineRuntime(root: string, runtime: RuntimeManifest, options?: UpdateMachineRuntimeOptions): Promise<ManagedMachineUpdateResult>;
|
|
61
69
|
export interface ManagedWorkspaceState {
|
package/dist/install/apply.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
-
import { dirname, join } from 'node:path';
|
|
3
|
+
import { dirname, join, relative, sep } from 'node:path';
|
|
4
4
|
import { createDiagnostic } from '../schemas/diagnostics.js';
|
|
5
5
|
import { diagnosticCodes } from '../schemas/errors.js';
|
|
6
6
|
import { managedFileEntrySchema, } from '../schemas/install.js';
|
|
7
7
|
import { machineConfigSchema } from '../schemas/machine.js';
|
|
8
8
|
import { acquireInstallLock, backupPath, createManifest, ensureStatePathBoundary, ensureWorkspaceStateBoundary, inspectOwnedFile, journalPath, readInstallJournal, readWorkspaceManifest, releaseInstallLock, rootIdentityOf, scanJournalDirectory, workspaceInstallPaths, writeInstallJournal, writeJsonAtomic, writeWorkspaceManifest, } from './ownership.js';
|
|
9
9
|
import { observePath, sha256Digest } from '../infra/fs.js';
|
|
10
|
-
import {
|
|
10
|
+
import { beginRuntimeReference } from './references.js';
|
|
11
|
+
import { errorMessage, failed, succeeded, } from '../schemas/outcome.js';
|
|
11
12
|
function diagnostic(code, message, hint, source) {
|
|
12
13
|
return createDiagnostic({
|
|
13
14
|
code,
|
|
@@ -71,12 +72,12 @@ export async function beginManagedApply(root, plan, writes, rootIdentity) {
|
|
|
71
72
|
};
|
|
72
73
|
}
|
|
73
74
|
if (writes.length !== plan.files.length ||
|
|
74
|
-
plan.files.some((file) => file.operation
|
|
75
|
+
plan.files.some((file) => !['create', 'replace-managed'].includes(file.operation))) {
|
|
75
76
|
return {
|
|
76
77
|
ok: false,
|
|
77
78
|
status: 'invalid',
|
|
78
79
|
diagnostics: [
|
|
79
|
-
diagnostic(diagnosticCodes.initApplyUnsupported, '
|
|
80
|
+
diagnostic(diagnosticCodes.initApplyUnsupported, 'Managed apply supports only create and managed replacement plans.', 'Regenerate the plan with supported file operations.', root),
|
|
80
81
|
],
|
|
81
82
|
residualPaths: [],
|
|
82
83
|
};
|
|
@@ -105,12 +106,12 @@ export async function beginManagedApply(root, plan, writes, rootIdentity) {
|
|
|
105
106
|
}
|
|
106
107
|
const existingPaths = new Set(manifest.entries.map((entry) => entry.path));
|
|
107
108
|
for (const file of plan.files) {
|
|
108
|
-
if (existingPaths.has(file.path)) {
|
|
109
|
+
if ((file.operation === 'create') === existingPaths.has(file.path)) {
|
|
109
110
|
return {
|
|
110
111
|
ok: false,
|
|
111
112
|
status: 'invalid',
|
|
112
113
|
diagnostics: [
|
|
113
|
-
diagnostic(diagnosticCodes.initTargetExists, `
|
|
114
|
+
diagnostic(diagnosticCodes.initTargetExists, `Managed ownership does not match the planned operation for ${file.path}.`, 'Inspect ownership state; replacements require an existing managed entry.', workspaceInstallPaths(root).manifestPath),
|
|
114
115
|
],
|
|
115
116
|
residualPaths: [],
|
|
116
117
|
};
|
|
@@ -127,8 +128,59 @@ export async function beginManagedApply(root, plan, writes, rootIdentity) {
|
|
|
127
128
|
residualPaths: [],
|
|
128
129
|
};
|
|
129
130
|
}
|
|
131
|
+
const currentManifest = await readWorkspaceManifest(root);
|
|
132
|
+
const priorJournal = await findIncompleteWorkspaceJournal(root);
|
|
133
|
+
if (currentManifest.digest !== manifestRead.digest ||
|
|
134
|
+
priorJournal.incomplete !== null ||
|
|
135
|
+
priorJournal.unreadable.length > 0) {
|
|
136
|
+
await releaseInstallLock(lock.path, lock.lock.token);
|
|
137
|
+
return {
|
|
138
|
+
ok: false,
|
|
139
|
+
status: 'invalid',
|
|
140
|
+
residualPaths: [],
|
|
141
|
+
diagnostics: [
|
|
142
|
+
diagnostic(diagnosticCodes.initPlanStale, 'Managed ownership changed or an incomplete journal requires recovery.', 'Run pappra repair to inspect interrupted installs, then regenerate the plan.', priorJournal.incomplete ?? workspaceInstallPaths(root).manifestPath),
|
|
143
|
+
],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
130
146
|
const installId = randomUUID();
|
|
131
147
|
const journalFile = journalPath(root, installId);
|
|
148
|
+
const manifestBackupSteps = [];
|
|
149
|
+
if (plan.files.some((file) => file.operation === 'replace-managed')) {
|
|
150
|
+
const backup = backupPath(root, installId, 'install-manifest.json');
|
|
151
|
+
try {
|
|
152
|
+
const boundary = await ensureStatePathBoundary(root, backup);
|
|
153
|
+
if (!boundary.ok)
|
|
154
|
+
throw new Error(boundary.message);
|
|
155
|
+
const bytes = await readFile(workspaceInstallPaths(root).manifestPath);
|
|
156
|
+
if (sha256Digest(bytes) !== manifestRead.digest)
|
|
157
|
+
throw new Error('Ownership changed before backup.');
|
|
158
|
+
await mkdir(dirname(backup), { recursive: true });
|
|
159
|
+
await writeFile(backup, bytes, { flag: 'wx', mode: 0o600 });
|
|
160
|
+
manifestBackupSteps.push({
|
|
161
|
+
name: 'manifest-backup',
|
|
162
|
+
status: 'ok',
|
|
163
|
+
at: now(),
|
|
164
|
+
detail: relative(root, backup).split(sep).join('/'),
|
|
165
|
+
}, {
|
|
166
|
+
name: 'manifest-backup-digest',
|
|
167
|
+
status: 'ok',
|
|
168
|
+
at: now(),
|
|
169
|
+
detail: sha256Digest(bytes),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
await releaseInstallLock(lock.path, lock.lock.token);
|
|
174
|
+
return {
|
|
175
|
+
ok: false,
|
|
176
|
+
status: 'failed',
|
|
177
|
+
residualPaths: [],
|
|
178
|
+
diagnostics: [
|
|
179
|
+
diagnostic(diagnosticCodes.initApplyFailed, `Cannot back up managed ownership: ${errorMessage(error)}`, 'Inspect the workspace state; no target was changed.', journalFile),
|
|
180
|
+
],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
132
184
|
let journal = {
|
|
133
185
|
schema: 'pappra/install-journal/1',
|
|
134
186
|
installId,
|
|
@@ -141,6 +193,7 @@ export async function beginManagedApply(root, plan, writes, rootIdentity) {
|
|
|
141
193
|
updatedAt: now(),
|
|
142
194
|
steps: [
|
|
143
195
|
{ name: 'lock-acquired', status: 'ok', at: now(), detail: lock.path },
|
|
196
|
+
...manifestBackupSteps,
|
|
144
197
|
...writes.map((write) => ({
|
|
145
198
|
name: `pending:${write.path}`,
|
|
146
199
|
status: 'ok',
|
|
@@ -148,13 +201,19 @@ export async function beginManagedApply(root, plan, writes, rootIdentity) {
|
|
|
148
201
|
detail: write.digest,
|
|
149
202
|
})),
|
|
150
203
|
],
|
|
151
|
-
entries: writes.map((write) => ({
|
|
204
|
+
entries: writes.map((write, index) => ({
|
|
152
205
|
path: write.path,
|
|
153
206
|
status: 'pending',
|
|
154
207
|
digest: write.digest,
|
|
155
208
|
identity: null,
|
|
156
|
-
backupRef:
|
|
157
|
-
|
|
209
|
+
backupRef: plan.files[index]?.operation === 'replace-managed'
|
|
210
|
+
? relative(root, backupPath(root, installId, write.path))
|
|
211
|
+
.split(sep)
|
|
212
|
+
.join('/')
|
|
213
|
+
: null,
|
|
214
|
+
backupDigest: plan.files[index]?.operation === 'replace-managed'
|
|
215
|
+
? plan.files[index].expectedBeforeDigest
|
|
216
|
+
: null,
|
|
158
217
|
detail: null,
|
|
159
218
|
})),
|
|
160
219
|
residuals: [],
|
|
@@ -190,7 +249,7 @@ export async function beginManagedApply(root, plan, writes, rootIdentity) {
|
|
|
190
249
|
},
|
|
191
250
|
};
|
|
192
251
|
}
|
|
193
|
-
export async function recordManagedFile(context, path, digest, identity) {
|
|
252
|
+
export async function recordManagedFile(context, path, digest, identity, replacement) {
|
|
194
253
|
if (!context.plannedPaths.includes(path)) {
|
|
195
254
|
return {
|
|
196
255
|
ok: false,
|
|
@@ -204,6 +263,8 @@ export async function recordManagedFile(context, path, digest, identity) {
|
|
|
204
263
|
...entry,
|
|
205
264
|
status: 'created',
|
|
206
265
|
identity,
|
|
266
|
+
backupRef: replacement?.backupRef ?? null,
|
|
267
|
+
backupDigest: replacement?.backupDigest ?? null,
|
|
207
268
|
detail: null,
|
|
208
269
|
}
|
|
209
270
|
: entry);
|
|
@@ -222,14 +283,28 @@ export async function recordManagedFile(context, path, digest, identity) {
|
|
|
222
283
|
};
|
|
223
284
|
}
|
|
224
285
|
context.journal = journal;
|
|
225
|
-
context.entries.push(
|
|
286
|
+
context.entries.push({
|
|
287
|
+
...manifestEntry(context.installId, path, digest, identity),
|
|
288
|
+
...(replacement === undefined
|
|
289
|
+
? {}
|
|
290
|
+
: {
|
|
291
|
+
operation: 'replace-managed',
|
|
292
|
+
beforeDigest: replacement.backupDigest,
|
|
293
|
+
backupRef: replacement.backupRef,
|
|
294
|
+
backupDigest: replacement.backupDigest,
|
|
295
|
+
}),
|
|
296
|
+
});
|
|
226
297
|
return { ok: true };
|
|
227
298
|
}
|
|
228
299
|
export async function completeManagedApply(context) {
|
|
229
300
|
const appliedJournal = appendStep({ ...context.journal, status: 'applied', updatedAt: now() }, 'files-created', context.entries.map((entry) => entry.path).join(', '));
|
|
230
301
|
await writeInstallJournal(context.journalFile, appliedJournal, context.root);
|
|
231
302
|
context.journal = appliedJournal;
|
|
232
|
-
const
|
|
303
|
+
const changedPaths = new Set(context.entries.map((entry) => entry.path));
|
|
304
|
+
const entries = [
|
|
305
|
+
...context.manifest.entries.filter((entry) => !changedPaths.has(entry.path)),
|
|
306
|
+
...context.entries,
|
|
307
|
+
];
|
|
233
308
|
const manifest = {
|
|
234
309
|
...context.manifest,
|
|
235
310
|
entries,
|
|
@@ -349,8 +424,97 @@ async function findIncompleteWorkspaceJournal(root) {
|
|
|
349
424
|
}
|
|
350
425
|
return { incomplete, unreadable };
|
|
351
426
|
}
|
|
427
|
+
/** Read-only gate shared by initial planning and every apply, including no-op plans. */
|
|
428
|
+
export async function inspectWorkspaceInstallReadiness(root, ownLock = false) {
|
|
429
|
+
const paths = workspaceInstallPaths(root);
|
|
430
|
+
const boundary = await ensureStatePathBoundary(root, paths.lockPath);
|
|
431
|
+
if (!boundary.ok)
|
|
432
|
+
return boundary;
|
|
433
|
+
if (!ownLock && (await observePath(paths.lockPath)).kind !== 'missing') {
|
|
434
|
+
return failed(diagnosticCodes.initApplyFailed, 'Workspace installation is locked.', {
|
|
435
|
+
nextAction: 'Wait for the active init, or run pappra repair to inspect an interrupted install.',
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
const scan = await findIncompleteWorkspaceJournal(root);
|
|
439
|
+
if (scan.incomplete !== null || scan.unreadable.length > 0) {
|
|
440
|
+
return failed(diagnosticCodes.initPlanStale, `Workspace installation requires recovery: ${scan.incomplete ?? scan.unreadable[0]}`, {
|
|
441
|
+
nextAction: 'Run pappra repair for this workspace, then regenerate the init plan.',
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
const manifest = await readWorkspaceManifest(root);
|
|
445
|
+
if (manifest.diagnostics.length > 0)
|
|
446
|
+
return failed(diagnosticCodes.initPlanStale, manifest.diagnostics[0].message, { nextAction: 'Inspect install-manifest.json before retrying init.' });
|
|
447
|
+
if (manifest.value !== null) {
|
|
448
|
+
const identity = await rootIdentityOf(root);
|
|
449
|
+
if (manifest.value.workspaceRoot !== root ||
|
|
450
|
+
identity === null ||
|
|
451
|
+
manifest.value.rootIdentity.dev !== identity.dev ||
|
|
452
|
+
manifest.value.rootIdentity.ino !== identity.ino) {
|
|
453
|
+
return failed(diagnosticCodes.initPlanStale, 'Workspace installation belongs to another root identity.', {
|
|
454
|
+
nextAction: 'Inspect the local install manifest; it was not adopted or rewritten.',
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return succeeded(null);
|
|
459
|
+
}
|
|
460
|
+
/** Serialize a zero-file apply with installation and recovery, without adding an empty journal. */
|
|
461
|
+
export async function verifyWorkspaceInstallReady(root) {
|
|
462
|
+
const paths = workspaceInstallPaths(root);
|
|
463
|
+
const lock = await acquireInstallLock(paths.lockPath, 'apply-workspace-files', root, root);
|
|
464
|
+
if (!lock.ok || lock.lock === null)
|
|
465
|
+
return failed(diagnosticCodes.initApplyFailed, lock.message ?? 'Workspace installation is locked.', { nextAction: 'Run pappra repair to inspect the install lock.' });
|
|
466
|
+
let result;
|
|
467
|
+
try {
|
|
468
|
+
result = await inspectWorkspaceInstallReadiness(root, true);
|
|
469
|
+
}
|
|
470
|
+
finally {
|
|
471
|
+
const released = await releaseInstallLock(lock.path, lock.lock.token);
|
|
472
|
+
if (!released.ok)
|
|
473
|
+
result = released;
|
|
474
|
+
}
|
|
475
|
+
return result;
|
|
476
|
+
}
|
|
352
477
|
/** Updates the managed machine.json runtime location after a verified install. */
|
|
353
478
|
export async function updateMachineRuntime(root, runtime, options = {}) {
|
|
479
|
+
const lease = await beginRuntimeReference(root, runtime);
|
|
480
|
+
if (!lease.ok)
|
|
481
|
+
return {
|
|
482
|
+
ok: false,
|
|
483
|
+
status: 'failed',
|
|
484
|
+
residualPaths: [],
|
|
485
|
+
diagnostics: [
|
|
486
|
+
diagnostic(lease.code, lease.message, lease.nextAction ?? 'Inspect the runtime reference state.', runtime.runtimeRoot),
|
|
487
|
+
],
|
|
488
|
+
};
|
|
489
|
+
let result;
|
|
490
|
+
try {
|
|
491
|
+
result = await updateMachineRuntimeAt(root, runtime, options);
|
|
492
|
+
}
|
|
493
|
+
catch (error) {
|
|
494
|
+
result = {
|
|
495
|
+
ok: false,
|
|
496
|
+
status: 'failed',
|
|
497
|
+
residualPaths: [],
|
|
498
|
+
diagnostics: [
|
|
499
|
+
diagnostic(diagnosticCodes.initApplyFailed, errorMessage(error), 'Inspect the workspace binding journal before retrying.', root),
|
|
500
|
+
],
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
const released = await lease.value.release();
|
|
504
|
+
return released.ok
|
|
505
|
+
? result
|
|
506
|
+
: {
|
|
507
|
+
...result,
|
|
508
|
+
ok: false,
|
|
509
|
+
status: result.ok ? 'partial' : result.status,
|
|
510
|
+
residualPaths: [...result.residualPaths, lease.value.lockPath],
|
|
511
|
+
diagnostics: [
|
|
512
|
+
...result.diagnostics,
|
|
513
|
+
diagnostic(released.code, released.message, 'Inspect the retained runtime lock before retrying.', lease.value.lockPath),
|
|
514
|
+
],
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
async function updateMachineRuntimeAt(root, runtime, options) {
|
|
354
518
|
const stateBoundary = await ensureWorkspaceStateBoundary(root);
|
|
355
519
|
if (!stateBoundary.ok) {
|
|
356
520
|
return {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface PackageDependencyManifest {
|
|
2
|
+
readonly required: readonly string[];
|
|
3
|
+
readonly optional: readonly string[];
|
|
4
|
+
}
|
|
5
|
+
/** Reads the declared dependency names of one package without guessing ranges. */
|
|
6
|
+
export declare function readPackageDependencyManifest(packageRoot: string): Promise<PackageDependencyManifest | null>;
|
|
7
|
+
/**
|
|
8
|
+
* Resolves the exact installed copy that Node would load for `name` from
|
|
9
|
+
* `packageRoot`, walking the same ancestor chain Node resolves through. Every
|
|
10
|
+
* candidate is realpath-resolved and name-checked so a symlinked store entry or
|
|
11
|
+
* a mismatched directory is never mistaken for the requested dependency.
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveInstalledDependency(packageRoot: string, name: string): Promise<string | null>;
|
|
14
|
+
export interface DependencyMaterialization {
|
|
15
|
+
readonly ok: boolean;
|
|
16
|
+
/** Destination directories created by this run, in copy order. */
|
|
17
|
+
readonly copied: readonly string[];
|
|
18
|
+
/** Destination slots satisfied by an already-materialized exact copy. */
|
|
19
|
+
readonly reused: readonly string[];
|
|
20
|
+
/** Optional dependencies that are absent from the installed tree. */
|
|
21
|
+
readonly skippedOptional: readonly string[];
|
|
22
|
+
/** Problems that make the materialized tree unsafe to publish. */
|
|
23
|
+
readonly problems: readonly string[];
|
|
24
|
+
}
|
|
25
|
+
export interface MaterializeDependenciesOptions {
|
|
26
|
+
readonly sourceRoot: string;
|
|
27
|
+
readonly targetRoot: string;
|
|
28
|
+
/** Upper bound on visited packages; guards against pathological trees. */
|
|
29
|
+
readonly maxPackages?: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Materializes the installed dependency closure of `sourceRoot` under
|
|
33
|
+
* `targetRoot/node_modules`, reusing exact resolved copies at the highest
|
|
34
|
+
* non-shadowed ancestor. Required dependencies must resolve; absent optional
|
|
35
|
+
* dependencies are skipped. The result is a plain report: callers decide whether
|
|
36
|
+
* an incomplete closure is fatal or a reason to fall back to a package manager.
|
|
37
|
+
*/
|
|
38
|
+
export declare function materializeInstalledDependencies(options: MaterializeDependenciesOptions): Promise<DependencyMaterialization>;
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { cp, mkdir } from 'node:fs/promises';
|
|
2
|
+
import { basename, dirname, join, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { observePath, readTextFileBounded, resolveRealpath, } from '../infra/fs.js';
|
|
4
|
+
import { isPathInside, samePath } from '../schemas/paths.js';
|
|
5
|
+
const NODE_MODULES = 'node_modules';
|
|
6
|
+
const DEFAULT_MAX_PACKAGES = 512;
|
|
7
|
+
function packageDependencyNames(value) {
|
|
8
|
+
if (value === undefined)
|
|
9
|
+
return [];
|
|
10
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const entries = Object.entries(value);
|
|
14
|
+
if (entries.some(([name, range]) => !/^(?:@[A-Za-z0-9._-]+\/)?[A-Za-z0-9._-]+$/.test(name) ||
|
|
15
|
+
typeof range !== 'string')) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return entries.map(([name]) => name);
|
|
19
|
+
}
|
|
20
|
+
/** Reads the declared dependency names of one package without guessing ranges. */
|
|
21
|
+
export async function readPackageDependencyManifest(packageRoot) {
|
|
22
|
+
const manifestRead = await readTextFileBounded(join(packageRoot, 'package.json'));
|
|
23
|
+
if (!manifestRead.ok)
|
|
24
|
+
return null;
|
|
25
|
+
let decoded;
|
|
26
|
+
try {
|
|
27
|
+
decoded = JSON.parse(manifestRead.value.text);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
if (typeof decoded !== 'object' || decoded === null)
|
|
33
|
+
return null;
|
|
34
|
+
const manifest = decoded;
|
|
35
|
+
const required = packageDependencyNames(manifest.dependencies);
|
|
36
|
+
const optional = packageDependencyNames(manifest.optionalDependencies);
|
|
37
|
+
if (required === null || optional === null)
|
|
38
|
+
return null;
|
|
39
|
+
return { required, optional };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Resolves the exact installed copy that Node would load for `name` from
|
|
43
|
+
* `packageRoot`, walking the same ancestor chain Node resolves through. Every
|
|
44
|
+
* candidate is realpath-resolved and name-checked so a symlinked store entry or
|
|
45
|
+
* a mismatched directory is never mistaken for the requested dependency.
|
|
46
|
+
*/
|
|
47
|
+
export async function resolveInstalledDependency(packageRoot, name) {
|
|
48
|
+
let current = packageRoot;
|
|
49
|
+
while (true) {
|
|
50
|
+
const candidate = join(current, NODE_MODULES, ...name.split('/'));
|
|
51
|
+
const realPath = await resolveRealpath(candidate);
|
|
52
|
+
if (realPath !== null &&
|
|
53
|
+
(await observePath(realPath)).kind === 'directory') {
|
|
54
|
+
const manifestRead = await readTextFileBounded(join(realPath, 'package.json'));
|
|
55
|
+
if (manifestRead.ok) {
|
|
56
|
+
try {
|
|
57
|
+
const decoded = JSON.parse(manifestRead.value.text);
|
|
58
|
+
if (typeof decoded === 'object' &&
|
|
59
|
+
decoded !== null &&
|
|
60
|
+
'name' in decoded &&
|
|
61
|
+
decoded.name === name) {
|
|
62
|
+
return realPath;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const parent = dirname(current);
|
|
71
|
+
if (parent === current)
|
|
72
|
+
return null;
|
|
73
|
+
current = parent;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Chooses where one exact dependency belongs inside the destination tree.
|
|
78
|
+
*
|
|
79
|
+
* The slot must sit at the highest ancestor that is not shadowed by a different
|
|
80
|
+
* exact source: Node resolves `node_modules/<name>` from the nearest ancestor
|
|
81
|
+
* outwards, so a nearer conflicting placement forces a nested copy, while a free
|
|
82
|
+
* chain up to the target root allows one shared copy at the highest ancestor.
|
|
83
|
+
*/
|
|
84
|
+
function choosePlacement(input) {
|
|
85
|
+
let candidate = null;
|
|
86
|
+
for (const slot of resolutionSlots(input.destination, input.targetRoot, input.name)) {
|
|
87
|
+
const reserved = input.reservations.get(slot);
|
|
88
|
+
if (reserved &&
|
|
89
|
+
[...reserved].some((source) => source !== input.dependencyRoot))
|
|
90
|
+
break;
|
|
91
|
+
const placed = input.placements.get(slot);
|
|
92
|
+
if (placed === input.dependencyRoot)
|
|
93
|
+
return { kind: 'reused', slot };
|
|
94
|
+
if (placed !== undefined)
|
|
95
|
+
break;
|
|
96
|
+
candidate = slot;
|
|
97
|
+
}
|
|
98
|
+
return candidate === null
|
|
99
|
+
? { kind: 'conflict' }
|
|
100
|
+
: { kind: 'placed', slot: candidate };
|
|
101
|
+
}
|
|
102
|
+
function resolutionSlots(destination, targetRoot, name) {
|
|
103
|
+
const slots = [];
|
|
104
|
+
let directory = destination;
|
|
105
|
+
while (true) {
|
|
106
|
+
// Node never appends `node_modules` to a directory that already is one, so
|
|
107
|
+
// such an ancestor is walked through but never offered as a placement slot.
|
|
108
|
+
if (basename(directory) !== NODE_MODULES)
|
|
109
|
+
slots.push(join(directory, NODE_MODULES, ...name.split('/')));
|
|
110
|
+
if (samePath(directory, targetRoot))
|
|
111
|
+
break;
|
|
112
|
+
const parent = dirname(directory);
|
|
113
|
+
if (parent === directory || !isPathInside(targetRoot, parent))
|
|
114
|
+
break;
|
|
115
|
+
directory = parent;
|
|
116
|
+
}
|
|
117
|
+
return slots;
|
|
118
|
+
}
|
|
119
|
+
/** Empty nearer slots must not later shadow a consumer's already chosen copy. */
|
|
120
|
+
function reserveResolution(reservations, slots, source, resolvedSlot) {
|
|
121
|
+
for (const slot of slots) {
|
|
122
|
+
const expected = reservations.get(slot) ?? new Set();
|
|
123
|
+
expected.add(source);
|
|
124
|
+
reservations.set(slot, expected);
|
|
125
|
+
if (slot === resolvedSlot)
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Payload paths never include a nested `node_modules`: that subtree belongs to
|
|
131
|
+
* another resolver and is materialized explicitly from its own exact source.
|
|
132
|
+
*/
|
|
133
|
+
function isPayloadPath(sourceRoot, candidate) {
|
|
134
|
+
const path = relative(sourceRoot, candidate);
|
|
135
|
+
if (path === '')
|
|
136
|
+
return true;
|
|
137
|
+
return !path.split(sep).includes(NODE_MODULES);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Copies a regular package payload without nested dependency trees. Published
|
|
141
|
+
* runtime manifests forbid links, so reject them before publication rather than
|
|
142
|
+
* copying a store/absolute target or silently producing an unusable runtime.
|
|
143
|
+
*/
|
|
144
|
+
async function copyPackagePayload(sourceRoot, destination) {
|
|
145
|
+
const existing = await observePath(destination);
|
|
146
|
+
if (existing.kind !== 'missing')
|
|
147
|
+
return false;
|
|
148
|
+
try {
|
|
149
|
+
await cp(sourceRoot, destination, {
|
|
150
|
+
recursive: true,
|
|
151
|
+
dereference: false,
|
|
152
|
+
verbatimSymlinks: true,
|
|
153
|
+
force: false,
|
|
154
|
+
errorOnExist: false,
|
|
155
|
+
filter: async (candidate) => {
|
|
156
|
+
if (!isPayloadPath(sourceRoot, candidate))
|
|
157
|
+
return false;
|
|
158
|
+
const entry = await observePath(candidate);
|
|
159
|
+
if (entry.kind !== 'file' && entry.kind !== 'directory')
|
|
160
|
+
throw new Error('Dependency payload contains a link or special file');
|
|
161
|
+
return true;
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Materializes the installed dependency closure of `sourceRoot` under
|
|
172
|
+
* `targetRoot/node_modules`, reusing exact resolved copies at the highest
|
|
173
|
+
* non-shadowed ancestor. Required dependencies must resolve; absent optional
|
|
174
|
+
* dependencies are skipped. The result is a plain report: callers decide whether
|
|
175
|
+
* an incomplete closure is fatal or a reason to fall back to a package manager.
|
|
176
|
+
*/
|
|
177
|
+
export async function materializeInstalledDependencies(options) {
|
|
178
|
+
const copied = [];
|
|
179
|
+
const reused = [];
|
|
180
|
+
const skippedOptional = [];
|
|
181
|
+
const problems = [];
|
|
182
|
+
const placements = new Map();
|
|
183
|
+
const reservations = new Map();
|
|
184
|
+
const sourceRoot = (await resolveRealpath(options.sourceRoot)) ?? resolve(options.sourceRoot);
|
|
185
|
+
const targetRoot = resolve(options.targetRoot);
|
|
186
|
+
const limit = options.maxPackages ?? DEFAULT_MAX_PACKAGES;
|
|
187
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 4096)
|
|
188
|
+
return {
|
|
189
|
+
ok: false,
|
|
190
|
+
copied,
|
|
191
|
+
reused,
|
|
192
|
+
skippedOptional,
|
|
193
|
+
problems: ['invalid_package_limit'],
|
|
194
|
+
};
|
|
195
|
+
const pending = [
|
|
196
|
+
{ packageRoot: sourceRoot, destination: targetRoot },
|
|
197
|
+
];
|
|
198
|
+
let index = 0;
|
|
199
|
+
while (index < pending.length) {
|
|
200
|
+
if (index >= limit) {
|
|
201
|
+
problems.push(`package_limit:${String(limit)}`);
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
const current = pending[index];
|
|
205
|
+
index += 1;
|
|
206
|
+
if (current === undefined)
|
|
207
|
+
break;
|
|
208
|
+
const manifest = await readPackageDependencyManifest(current.packageRoot);
|
|
209
|
+
if (manifest === null) {
|
|
210
|
+
problems.push(`manifest_unreadable:${current.packageRoot}`);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const declared = [];
|
|
214
|
+
for (const name of manifest.required) {
|
|
215
|
+
if (declared.some((entry) => entry.name === name))
|
|
216
|
+
continue;
|
|
217
|
+
declared.push({ name, optional: false });
|
|
218
|
+
}
|
|
219
|
+
for (const name of manifest.optional) {
|
|
220
|
+
if (declared.some((entry) => entry.name === name))
|
|
221
|
+
continue;
|
|
222
|
+
declared.push({ name, optional: true });
|
|
223
|
+
}
|
|
224
|
+
for (const entry of declared) {
|
|
225
|
+
const name = entry.name;
|
|
226
|
+
const optional = entry.optional;
|
|
227
|
+
const dependencyRoot = await resolveInstalledDependency(current.packageRoot, name);
|
|
228
|
+
if (dependencyRoot === null) {
|
|
229
|
+
const slots = resolutionSlots(current.destination, targetRoot, name);
|
|
230
|
+
if (slots.some((slot) => placements.has(slot)))
|
|
231
|
+
problems.push(`missing_dependency_shadowed:${name}`);
|
|
232
|
+
reserveResolution(reservations, slots, null);
|
|
233
|
+
if (optional) {
|
|
234
|
+
skippedOptional.push(name);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
problems.push(`dependency_missing:${name}`);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const placement = choosePlacement({
|
|
241
|
+
placements,
|
|
242
|
+
reservations,
|
|
243
|
+
targetRoot,
|
|
244
|
+
destination: current.destination,
|
|
245
|
+
name,
|
|
246
|
+
dependencyRoot,
|
|
247
|
+
});
|
|
248
|
+
if (placement.kind === 'reused') {
|
|
249
|
+
reserveResolution(reservations, resolutionSlots(current.destination, targetRoot, name), dependencyRoot, placement.slot);
|
|
250
|
+
reused.push(placement.slot);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (placement.kind === 'conflict') {
|
|
254
|
+
problems.push(`dependency_slot_conflict:${name}`);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (!isPathInside(targetRoot, placement.slot)) {
|
|
258
|
+
problems.push(`dependency_placement_escape:${name}`);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
await mkdir(dirname(placement.slot), { recursive: true });
|
|
262
|
+
if (!(await copyPackagePayload(dependencyRoot, placement.slot))) {
|
|
263
|
+
problems.push(`dependency_copy_failed:${name}`);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
placements.set(placement.slot, dependencyRoot);
|
|
267
|
+
reserveResolution(reservations, resolutionSlots(current.destination, targetRoot, name), dependencyRoot, placement.slot);
|
|
268
|
+
copied.push(placement.slot);
|
|
269
|
+
pending.push({
|
|
270
|
+
packageRoot: dependencyRoot,
|
|
271
|
+
destination: placement.slot,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
ok: problems.length === 0,
|
|
277
|
+
copied,
|
|
278
|
+
reused,
|
|
279
|
+
skippedOptional,
|
|
280
|
+
problems,
|
|
281
|
+
};
|
|
282
|
+
}
|