@phyzess/pappra 0.0.0-stage → 0.1.0-beta.2
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/LICENSE +21 -0
- package/README.md +80 -2
- package/THIRD_PARTY_NOTICES.md +16 -0
- package/assets/integrations/skills/pappra/SKILL.md +68 -0
- package/assets/integrations/skills/pappra/references/hosts.md +30 -0
- package/assets/integrations/skills/pappra/references/lifecycle.md +37 -0
- package/assets/integrations/skills/pappra/references/operations.md +14 -0
- package/assets/manifest.json +4 -0
- package/dist/app/collaboration.d.ts +170 -0
- package/dist/app/collaboration.js +1650 -0
- package/dist/app/doctor.d.ts +9 -0
- package/dist/app/doctor.js +1030 -0
- package/dist/app/init.d.ts +143 -0
- package/dist/app/init.js +1111 -0
- package/dist/cli/commands/code.d.ts +16 -0
- package/dist/cli/commands/code.js +127 -0
- package/dist/cli/commands/collaboration.d.ts +13 -0
- package/dist/cli/commands/collaboration.js +186 -0
- package/dist/cli/commands/continuity.d.ts +29 -0
- package/dist/cli/commands/continuity.js +133 -0
- package/dist/cli/commands/docs.d.ts +11 -0
- package/dist/cli/commands/docs.js +133 -0
- package/dist/cli/commands/doctor.d.ts +14 -0
- package/dist/cli/commands/doctor.js +57 -0
- package/dist/cli/commands/host.d.ts +13 -0
- package/dist/cli/commands/host.js +125 -0
- package/dist/cli/commands/init.d.ts +51 -0
- package/dist/cli/commands/init.js +668 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +10 -0
- package/dist/cli/output.d.ts +4 -0
- package/dist/cli/output.js +1 -0
- package/dist/cli/run.d.ts +2 -0
- package/dist/cli/run.js +597 -0
- package/dist/code/budget.d.ts +14 -0
- package/dist/code/budget.js +39 -0
- package/dist/code/exec.d.ts +23 -0
- package/dist/code/exec.js +133 -0
- package/dist/code/identity.d.ts +13 -0
- package/dist/code/identity.js +214 -0
- package/dist/code/index.d.ts +6 -0
- package/dist/code/index.js +3 -0
- package/dist/code/providers/codegraph.d.ts +59 -0
- package/dist/code/providers/codegraph.js +567 -0
- package/dist/code/registration.d.ts +9 -0
- package/dist/code/registration.js +293 -0
- package/dist/code/service.d.ts +53 -0
- package/dist/code/service.js +633 -0
- package/dist/code/text.d.ts +3 -0
- package/dist/code/text.js +202 -0
- package/dist/collab/admission.d.ts +450 -0
- package/dist/collab/admission.js +797 -0
- package/dist/collab/canonical.d.ts +26 -0
- package/dist/collab/canonical.js +136 -0
- package/dist/collab/contracts.d.ts +73 -0
- package/dist/collab/contracts.js +76 -0
- package/dist/collab/envelope.d.ts +52 -0
- package/dist/collab/envelope.js +241 -0
- package/dist/collab/fingerprint.d.ts +7 -0
- package/dist/collab/fingerprint.js +58 -0
- package/dist/collab/handoff.d.ts +350 -0
- package/dist/collab/handoff.js +513 -0
- package/dist/collab/index.d.ts +9 -0
- package/dist/collab/index.js +8 -0
- package/dist/collab/lifecycle.d.ts +449 -0
- package/dist/collab/lifecycle.js +534 -0
- package/dist/collab/messages.d.ts +148 -0
- package/dist/collab/messages.js +583 -0
- package/dist/collab/notify.d.ts +47 -0
- package/dist/collab/notify.js +140 -0
- package/dist/collab/preflight.d.ts +31 -0
- package/dist/collab/preflight.js +271 -0
- package/dist/collab/resources.d.ts +119 -0
- package/dist/collab/resources.js +451 -0
- package/dist/collab/store.d.ts +143 -0
- package/dist/collab/store.js +606 -0
- package/dist/collab/types.d.ts +354 -0
- package/dist/collab/types.js +1 -0
- package/dist/collab/validation.d.ts +10 -0
- package/dist/collab/validation.js +303 -0
- package/dist/continuity/context.d.ts +24 -0
- package/dist/continuity/context.js +179 -0
- package/dist/continuity/index.d.ts +3 -0
- package/dist/continuity/index.js +1 -0
- package/dist/continuity/paths.d.ts +37 -0
- package/dist/continuity/paths.js +258 -0
- package/dist/continuity/service.d.ts +59 -0
- package/dist/continuity/service.js +1258 -0
- package/dist/hosts/claude/native.d.ts +79 -0
- package/dist/hosts/claude/native.js +853 -0
- package/dist/hosts/codex/native.d.ts +345 -0
- package/dist/hosts/codex/native.js +1528 -0
- package/dist/hosts/codex/protocol.d.ts +51 -0
- package/dist/hosts/codex/protocol.js +42 -0
- package/dist/hosts/detect.d.ts +28 -0
- package/dist/hosts/detect.js +73 -0
- package/dist/hosts/index.d.ts +10 -0
- package/dist/hosts/index.js +10 -0
- package/dist/hosts/integration/bundle.d.ts +16 -0
- package/dist/hosts/integration/bundle.js +81 -0
- package/dist/hosts/integration/hook-entry.d.ts +1 -0
- package/dist/hosts/integration/hook-entry.js +68 -0
- package/dist/hosts/integration/hook.d.ts +32 -0
- package/dist/hosts/integration/hook.js +781 -0
- package/dist/hosts/integration/pi-extension-runtime.d.ts +18 -0
- package/dist/hosts/integration/pi-extension-runtime.js +89 -0
- package/dist/hosts/integration/pi-template.d.ts +3 -0
- package/dist/hosts/integration/pi-template.js +5 -0
- package/dist/hosts/integration/service.d.ts +41 -0
- package/dist/hosts/integration/service.js +1381 -0
- package/dist/hosts/integration/support.d.ts +36 -0
- package/dist/hosts/integration/support.js +41 -0
- package/dist/hosts/integration/templates.d.ts +22 -0
- package/dist/hosts/integration/templates.js +70 -0
- package/dist/hosts/pi/protocol.d.ts +22 -0
- package/dist/hosts/pi/protocol.js +28 -0
- package/dist/hosts/pi/query-result.d.ts +22 -0
- package/dist/hosts/pi/query-result.js +710 -0
- package/dist/hosts/pi/rpc.d.ts +937 -0
- package/dist/hosts/pi/rpc.js +1621 -0
- package/dist/hosts/pilot.d.ts +485 -0
- package/dist/hosts/pilot.js +593 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/infra/fs.d.ts +27 -0
- package/dist/infra/fs.js +130 -0
- package/dist/infra/git.d.ts +60 -0
- package/dist/infra/git.js +358 -0
- package/dist/infra/owned-process-group.d.ts +173 -0
- package/dist/infra/owned-process-group.js +571 -0
- package/dist/install/apply.d.ts +65 -0
- package/dist/install/apply.js +663 -0
- package/dist/install/index.d.ts +5 -0
- package/dist/install/index.js +5 -0
- package/dist/install/ownership.d.ts +85 -0
- package/dist/install/ownership.js +430 -0
- package/dist/install/plan.d.ts +39 -0
- package/dist/install/plan.js +97 -0
- package/dist/install/rollback.d.ts +18 -0
- package/dist/install/rollback.js +544 -0
- package/dist/install/runtime.d.ts +76 -0
- package/dist/install/runtime.js +1145 -0
- package/dist/knowledge/catalog.d.ts +47 -0
- package/dist/knowledge/catalog.js +308 -0
- package/dist/knowledge/context.d.ts +28 -0
- package/dist/knowledge/context.js +61 -0
- package/dist/knowledge/index.d.ts +3 -0
- package/dist/knowledge/index.js +1 -0
- package/dist/knowledge/journal.d.ts +16 -0
- package/dist/knowledge/journal.js +109 -0
- package/dist/knowledge/lifecycle.d.ts +8 -0
- package/dist/knowledge/lifecycle.js +1001 -0
- package/dist/knowledge/paths.d.ts +37 -0
- package/dist/knowledge/paths.js +236 -0
- package/dist/knowledge/refs.d.ts +36 -0
- package/dist/knowledge/refs.js +267 -0
- package/dist/knowledge/service.d.ts +21 -0
- package/dist/knowledge/service.js +393 -0
- package/dist/knowledge/types.d.ts +171 -0
- package/dist/knowledge/types.js +1 -0
- package/dist/knowledge/write.d.ts +8 -0
- package/dist/knowledge/write.js +1223 -0
- package/dist/package-info.d.ts +6 -0
- package/dist/package-info.js +19 -0
- package/dist/schemas/code.d.ts +377 -0
- package/dist/schemas/code.js +145 -0
- package/dist/schemas/collaboration.d.ts +115 -0
- package/dist/schemas/collaboration.js +106 -0
- package/dist/schemas/continuity.d.ts +315 -0
- package/dist/schemas/continuity.js +199 -0
- package/dist/schemas/diagnostics.d.ts +28 -0
- package/dist/schemas/diagnostics.js +27 -0
- package/dist/schemas/doctor.d.ts +157 -0
- package/dist/schemas/doctor.js +75 -0
- package/dist/schemas/document.d.ts +644 -0
- package/dist/schemas/document.js +296 -0
- package/dist/schemas/errors.d.ts +177 -0
- package/dist/schemas/errors.js +176 -0
- package/dist/schemas/evidence.d.ts +24 -0
- package/dist/schemas/evidence.js +16 -0
- package/dist/schemas/host.d.ts +436 -0
- package/dist/schemas/host.js +201 -0
- package/dist/schemas/index.d.ts +15 -0
- package/dist/schemas/index.js +15 -0
- package/dist/schemas/init.d.ts +121 -0
- package/dist/schemas/init.js +94 -0
- package/dist/schemas/install.d.ts +213 -0
- package/dist/schemas/install.js +139 -0
- package/dist/schemas/machine.d.ts +43 -0
- package/dist/schemas/machine.js +96 -0
- package/dist/schemas/outcome.d.ts +23 -0
- package/dist/schemas/outcome.js +17 -0
- package/dist/schemas/paths.d.ts +19 -0
- package/dist/schemas/paths.js +63 -0
- package/dist/schemas/workspace.d.ts +120 -0
- package/dist/schemas/workspace.js +201 -0
- package/dist/workspace/config-file.d.ts +36 -0
- package/dist/workspace/config-file.js +237 -0
- package/dist/workspace/config.d.ts +28 -0
- package/dist/workspace/config.js +79 -0
- package/dist/workspace/discover.d.ts +34 -0
- package/dist/workspace/discover.js +195 -0
- package/dist/workspace/documents.d.ts +21 -0
- package/dist/workspace/documents.js +395 -0
- package/dist/workspace/ignore.d.ts +10 -0
- package/dist/workspace/ignore.js +48 -0
- package/dist/workspace/index.d.ts +7 -0
- package/dist/workspace/index.js +3 -0
- package/dist/workspace/repositories.d.ts +24 -0
- package/dist/workspace/repositories.js +411 -0
- package/dist/workspace/resolve.d.ts +24 -0
- package/dist/workspace/resolve.js +268 -0
- package/dist/workspace/types.d.ts +71 -0
- package/dist/workspace/types.js +1 -0
- package/dist/workspace/validation.d.ts +18 -0
- package/dist/workspace/validation.js +197 -0
- package/package.json +105 -4
- package/skills/pappra/SKILL.md +68 -0
- package/skills/pappra/references/hosts.md +30 -0
- package/skills/pappra/references/lifecycle.md +37 -0
- package/skills/pappra/references/operations.md +14 -0
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { createDiagnostic } from '../schemas/diagnostics.js';
|
|
5
|
+
import { diagnosticCodes } from '../schemas/errors.js';
|
|
6
|
+
import { sha256Digest } from '../infra/fs.js';
|
|
7
|
+
import { observePath } from '../infra/fs.js';
|
|
8
|
+
import { acquireInstallLock, ensureStatePathBoundary, ensureWorkspaceStateBoundary, inspectOwnedFile, journalPath, readInstallJournal, readWorkspaceManifest, releaseInstallLock, removeOwnedFile, rootIdentityOf, workspaceInstallPaths, writeBytesAtomic, writeInstallJournal, writeWorkspaceManifest, } from './ownership.js';
|
|
9
|
+
function diagnostic(code, message, hint, source) {
|
|
10
|
+
return createDiagnostic({
|
|
11
|
+
code,
|
|
12
|
+
severity: 'error',
|
|
13
|
+
message,
|
|
14
|
+
source,
|
|
15
|
+
hint,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function now() {
|
|
19
|
+
return new Date().toISOString();
|
|
20
|
+
}
|
|
21
|
+
async function removeManifestEntries(root, manifest, entries) {
|
|
22
|
+
const removed = [];
|
|
23
|
+
const residual = [];
|
|
24
|
+
const keep = [];
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
const result = await removeOwnedFile(root, entry);
|
|
27
|
+
if (result.ok) {
|
|
28
|
+
removed.push(entry.path);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
residual.push(entry.path);
|
|
32
|
+
keep.push(entry);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const removedSet = new Set(removed);
|
|
36
|
+
const remaining = manifest.entries.filter((entry) => !entries.some((target) => target.path === entry.path && removedSet.has(target.path)));
|
|
37
|
+
// Entries that disappeared from disk are also no longer owned.
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
if (removedSet.has(entry.path))
|
|
40
|
+
continue;
|
|
41
|
+
const state = await inspectOwnedFile(root, entry);
|
|
42
|
+
if (state.status === 'missing') {
|
|
43
|
+
const index = remaining.findIndex((item) => item.path === entry.path);
|
|
44
|
+
if (index >= 0) {
|
|
45
|
+
remaining.splice(index, 1);
|
|
46
|
+
removed.push(entry.path);
|
|
47
|
+
removedSet.add(entry.path);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const filtered = remaining.filter((entry) => !removedSet.has(entry.path) ||
|
|
52
|
+
keep.some((item) => item.path === entry.path));
|
|
53
|
+
return { removed, residual, remaining: filtered };
|
|
54
|
+
}
|
|
55
|
+
function journalFor(operation, target, root) {
|
|
56
|
+
const installId = randomUUID();
|
|
57
|
+
return {
|
|
58
|
+
journal: {
|
|
59
|
+
schema: 'pappra/install-journal/1',
|
|
60
|
+
installId,
|
|
61
|
+
operation,
|
|
62
|
+
target,
|
|
63
|
+
workspaceRoot: root,
|
|
64
|
+
runtimeVersion: null,
|
|
65
|
+
status: 'prepared',
|
|
66
|
+
createdAt: now(),
|
|
67
|
+
updatedAt: now(),
|
|
68
|
+
steps: [],
|
|
69
|
+
entries: [],
|
|
70
|
+
residuals: [],
|
|
71
|
+
manifestRef: workspaceInstallPaths(root).manifestPath,
|
|
72
|
+
},
|
|
73
|
+
file: journalPath(root, installId),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function withStep(journal, name, detail, failed = false) {
|
|
77
|
+
return {
|
|
78
|
+
...journal,
|
|
79
|
+
updatedAt: now(),
|
|
80
|
+
steps: [
|
|
81
|
+
...journal.steps,
|
|
82
|
+
{ name, status: failed ? 'failed' : 'ok', at: now(), detail },
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async function acquireForRemoval(root, operation, target) {
|
|
87
|
+
const stateBoundary = await ensureWorkspaceStateBoundary(root);
|
|
88
|
+
if (!stateBoundary.ok) {
|
|
89
|
+
return {
|
|
90
|
+
ok: false,
|
|
91
|
+
result: {
|
|
92
|
+
ok: false,
|
|
93
|
+
status: 'failed',
|
|
94
|
+
removedPaths: [],
|
|
95
|
+
residualPaths: [],
|
|
96
|
+
diagnostics: [
|
|
97
|
+
diagnostic(diagnosticCodes.initTargetUnsafe, stateBoundary.message, stateBoundary.nextAction ??
|
|
98
|
+
'Inspect symlinked Pappra state paths; nothing was removed.', workspaceInstallPaths(root).localDir),
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const lock = await acquireInstallLock(workspaceInstallPaths(root).lockPath, operation, target, root);
|
|
104
|
+
if (!lock.ok || lock.lock === null) {
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
result: {
|
|
108
|
+
ok: false,
|
|
109
|
+
status: 'locked',
|
|
110
|
+
removedPaths: [],
|
|
111
|
+
residualPaths: [],
|
|
112
|
+
diagnostics: [
|
|
113
|
+
diagnostic(diagnosticCodes.initApplyFailed, lock.message ?? 'Workspace lock is held.', 'Another managed operation owns this workspace; inspect the lock manually.', lock.path),
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return { ok: true, token: lock.lock.token, path: lock.path };
|
|
119
|
+
}
|
|
120
|
+
async function finishJournal(file, journal, residual, boundaryRoot) {
|
|
121
|
+
return writeInstallJournal(file, {
|
|
122
|
+
...journal,
|
|
123
|
+
status: residual.length === 0 ? 'rolled-back' : 'rollback-required',
|
|
124
|
+
residuals: [...residual],
|
|
125
|
+
updatedAt: now(),
|
|
126
|
+
}, boundaryRoot).then((written) => written.ok
|
|
127
|
+
? { ok: true, value: null }
|
|
128
|
+
: {
|
|
129
|
+
ok: false,
|
|
130
|
+
code: diagnosticCodes.initApplyResidual,
|
|
131
|
+
message: written.message,
|
|
132
|
+
retryable: false,
|
|
133
|
+
nextAction: written.nextAction,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
async function rollbackMachineRuntimeUpdate(root, manifest, journal, updateInstallId) {
|
|
137
|
+
const path = journal.entries[0]?.path ?? journal.target;
|
|
138
|
+
const journalEntry = journal.entries.find((entry) => entry.path === path);
|
|
139
|
+
if (journalEntry === undefined ||
|
|
140
|
+
journalEntry.identity === null ||
|
|
141
|
+
journalEntry.backupRef === null ||
|
|
142
|
+
journalEntry.backupDigest === null) {
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
status: 'partial',
|
|
146
|
+
removedPaths: [],
|
|
147
|
+
residualPaths: [path],
|
|
148
|
+
diagnostics: [
|
|
149
|
+
diagnostic(diagnosticCodes.initApplyResidual, 'Runtime update journal has no bound after-state/backup record.', 'Inspect the machine file manually; Pappra cannot prove this update.', journalPath(root, updateInstallId)),
|
|
150
|
+
],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const index = manifest.entries.findIndex((entry) => entry.path === path);
|
|
154
|
+
if (index < 0) {
|
|
155
|
+
return {
|
|
156
|
+
ok: false,
|
|
157
|
+
status: 'unmanaged',
|
|
158
|
+
removedPaths: [],
|
|
159
|
+
residualPaths: [path],
|
|
160
|
+
diagnostics: [
|
|
161
|
+
diagnostic(diagnosticCodes.initApplyUnsupported, 'Runtime update has no matching ownership entry.', 'Inspect install-manifest.json; Pappra cannot restore the update automatically.', workspaceInstallPaths(root).manifestPath),
|
|
162
|
+
],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const entry = manifest.entries[index];
|
|
166
|
+
if (entry.afterDigest !== journalEntry.digest ||
|
|
167
|
+
entry.identity.dev !== journalEntry.identity.dev ||
|
|
168
|
+
entry.identity.ino !== journalEntry.identity.ino) {
|
|
169
|
+
return {
|
|
170
|
+
ok: false,
|
|
171
|
+
status: 'partial',
|
|
172
|
+
removedPaths: [],
|
|
173
|
+
residualPaths: [path],
|
|
174
|
+
diagnostics: [
|
|
175
|
+
diagnostic(diagnosticCodes.initApplyResidual, 'The requested runtime update was superseded or ownership changed.', 'Preserve the current machine.json and inspect the latest update journal.', path),
|
|
176
|
+
],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const expectedEntry = {
|
|
180
|
+
...entry,
|
|
181
|
+
afterDigest: journalEntry.digest,
|
|
182
|
+
identity: journalEntry.identity,
|
|
183
|
+
backupRef: journalEntry.backupRef,
|
|
184
|
+
backupDigest: journalEntry.backupDigest,
|
|
185
|
+
};
|
|
186
|
+
const current = await inspectOwnedFile(root, expectedEntry);
|
|
187
|
+
if (current.status !== 'match') {
|
|
188
|
+
return {
|
|
189
|
+
ok: false,
|
|
190
|
+
status: 'partial',
|
|
191
|
+
removedPaths: [],
|
|
192
|
+
residualPaths: [path],
|
|
193
|
+
diagnostics: [
|
|
194
|
+
diagnostic(diagnosticCodes.initApplyResidual, `machine.json changed after the runtime update: ${current.status}`, 'Preserve the current file; Pappra does not overwrite user changes.', path),
|
|
195
|
+
],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const backup = join(root, ...journalEntry.backupRef.split('/'));
|
|
199
|
+
const boundary = await ensureStatePathBoundary(root, backup);
|
|
200
|
+
if (!boundary.ok) {
|
|
201
|
+
return {
|
|
202
|
+
ok: false,
|
|
203
|
+
status: 'failed',
|
|
204
|
+
removedPaths: [],
|
|
205
|
+
residualPaths: [path],
|
|
206
|
+
diagnostics: [
|
|
207
|
+
diagnostic(diagnosticCodes.initTargetUnsafe, boundary.message, boundary.nextAction ?? 'Inspect the backup path; it was not read.', backup),
|
|
208
|
+
],
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
let backupBytes;
|
|
212
|
+
try {
|
|
213
|
+
backupBytes = await readFile(backup);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return {
|
|
217
|
+
ok: false,
|
|
218
|
+
status: 'partial',
|
|
219
|
+
removedPaths: [],
|
|
220
|
+
residualPaths: [path],
|
|
221
|
+
diagnostics: [
|
|
222
|
+
diagnostic(diagnosticCodes.initApplyResidual, 'Runtime update backup is missing or unreadable.', 'Restore from another backup; machine.json was not changed.', backup),
|
|
223
|
+
],
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
if (sha256Digest(backupBytes) !== journalEntry.backupDigest) {
|
|
227
|
+
return {
|
|
228
|
+
ok: false,
|
|
229
|
+
status: 'partial',
|
|
230
|
+
removedPaths: [],
|
|
231
|
+
residualPaths: [path],
|
|
232
|
+
diagnostics: [
|
|
233
|
+
diagnostic(diagnosticCodes.initApplyResidual, 'Runtime update backup no longer matches its recorded digest.', 'Machine.json was not changed. Inspect/replace the backup before retrying rollback.', backup),
|
|
234
|
+
],
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
const lock = await acquireForRemoval(root, 'rollback-workspace-files', updateInstallId);
|
|
238
|
+
if (!lock.ok)
|
|
239
|
+
return lock.result;
|
|
240
|
+
const { journal: rollbackJournal, file } = journalFor('rollback-workspace-files', updateInstallId, root);
|
|
241
|
+
let currentJournal = withStep({ ...rollbackJournal, status: 'applying', updatedAt: now() }, 'lock-acquired', lock.path);
|
|
242
|
+
currentJournal = withStep(currentJournal, 'recovered-from-update-journal', updateInstallId);
|
|
243
|
+
await writeInstallJournal(file, currentJournal, root);
|
|
244
|
+
const restoreWrite = await writeBytesAtomic(join(root, path), backupBytes, root);
|
|
245
|
+
if (!restoreWrite.ok) {
|
|
246
|
+
const residual = [path, file];
|
|
247
|
+
const failedJournal = await finishJournal(file, currentJournal, residual, root).catch(() => ({ ok: false }));
|
|
248
|
+
const released = await releaseInstallLock(lock.path, lock.token);
|
|
249
|
+
if (!failedJournal.ok)
|
|
250
|
+
residual.push(file);
|
|
251
|
+
if (!released.ok)
|
|
252
|
+
residual.push(lock.path);
|
|
253
|
+
return {
|
|
254
|
+
ok: false,
|
|
255
|
+
status: 'partial',
|
|
256
|
+
removedPaths: [],
|
|
257
|
+
residualPaths: residual,
|
|
258
|
+
diagnostics: [
|
|
259
|
+
diagnostic(diagnosticCodes.initApplyResidual, `Cannot restore machine.json from backup: ${restoreWrite.message}`, 'Inspect the backup and file; Pappra did not claim rollback success.', path),
|
|
260
|
+
],
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
const restored = await observePath(join(root, path));
|
|
264
|
+
const afterDigest = sha256Digest(backupBytes);
|
|
265
|
+
const updatedEntry = {
|
|
266
|
+
...entry,
|
|
267
|
+
afterDigest,
|
|
268
|
+
backupRef: null,
|
|
269
|
+
backupDigest: null,
|
|
270
|
+
identity: restored.kind === 'file' && restored.stats !== null
|
|
271
|
+
? { dev: restored.stats.dev, ino: restored.stats.ino }
|
|
272
|
+
: entry.identity,
|
|
273
|
+
verifiedAt: now(),
|
|
274
|
+
};
|
|
275
|
+
const updatedEntries = [...manifest.entries];
|
|
276
|
+
updatedEntries[index] = updatedEntry;
|
|
277
|
+
const manifestWritten = await writeWorkspaceManifest(root, {
|
|
278
|
+
...manifest,
|
|
279
|
+
entries: updatedEntries,
|
|
280
|
+
updatedAt: now(),
|
|
281
|
+
});
|
|
282
|
+
const residual = manifestWritten.ok
|
|
283
|
+
? []
|
|
284
|
+
: [workspaceInstallPaths(root).manifestPath];
|
|
285
|
+
const finished = await finishJournal(file, {
|
|
286
|
+
...currentJournal,
|
|
287
|
+
entries: [
|
|
288
|
+
{
|
|
289
|
+
path,
|
|
290
|
+
status: residual.length === 0 ? 'verified' : 'residual',
|
|
291
|
+
digest: afterDigest,
|
|
292
|
+
identity: updatedEntry.identity,
|
|
293
|
+
backupRef: null,
|
|
294
|
+
backupDigest: null,
|
|
295
|
+
detail: null,
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
residuals: residual,
|
|
299
|
+
}, residual, root);
|
|
300
|
+
if (!finished.ok)
|
|
301
|
+
residual.push(file);
|
|
302
|
+
const released = await releaseInstallLock(lock.path, lock.token);
|
|
303
|
+
if (!released.ok)
|
|
304
|
+
residual.push(lock.path);
|
|
305
|
+
return {
|
|
306
|
+
ok: residual.length === 0,
|
|
307
|
+
status: residual.length === 0 ? 'rolled-back' : 'partial',
|
|
308
|
+
removedPaths: residual.length === 0 ? [path] : [],
|
|
309
|
+
residualPaths: residual,
|
|
310
|
+
diagnostics: residual.map((residualPath) => createDiagnostic({
|
|
311
|
+
code: diagnosticCodes.initApplyResidual,
|
|
312
|
+
severity: 'warning',
|
|
313
|
+
message: `Runtime update rollback left a residual path: ${residualPath}`,
|
|
314
|
+
source: residualPath,
|
|
315
|
+
hint: 'Inspect manually; Pappra did not claim a complete restoration.',
|
|
316
|
+
})),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
export async function rollbackWorkspaceInstall(options) {
|
|
320
|
+
const manifestRead = await readWorkspaceManifest(options.root);
|
|
321
|
+
let manifest = manifestRead.value;
|
|
322
|
+
let entries = manifest?.entries.filter((entry) => entry.installId === options.installId) ?? [];
|
|
323
|
+
let journalRecovered = null;
|
|
324
|
+
let pendingResidual = [];
|
|
325
|
+
if (entries.length === 0) {
|
|
326
|
+
const journalRead = await readInstallJournal(journalPath(options.root, options.installId));
|
|
327
|
+
const journal = journalRead.value;
|
|
328
|
+
if (journal !== null &&
|
|
329
|
+
journal.operation === 'update-machine-runtime' &&
|
|
330
|
+
manifest !== null &&
|
|
331
|
+
(journal.status === 'applied' ||
|
|
332
|
+
journal.status === 'verified' ||
|
|
333
|
+
journal.status === 'rollback-required')) {
|
|
334
|
+
return rollbackMachineRuntimeUpdate(options.root, manifest, journal, options.installId);
|
|
335
|
+
}
|
|
336
|
+
if (journal !== null &&
|
|
337
|
+
journal.operation === 'apply-workspace-files' &&
|
|
338
|
+
journal.workspaceRoot === options.root &&
|
|
339
|
+
(journal.status === 'applying' ||
|
|
340
|
+
journal.status === 'applied' ||
|
|
341
|
+
journal.status === 'verified' ||
|
|
342
|
+
journal.status === 'rollback-required')) {
|
|
343
|
+
const recovered = journal.entries
|
|
344
|
+
.filter((entry) => (entry.status === 'created' || entry.status === 'verified') &&
|
|
345
|
+
entry.identity !== null)
|
|
346
|
+
.map((entry) => ({
|
|
347
|
+
path: entry.path,
|
|
348
|
+
installId: options.installId,
|
|
349
|
+
operation: 'create',
|
|
350
|
+
beforeDigest: null,
|
|
351
|
+
afterDigest: entry.digest,
|
|
352
|
+
backupRef: null,
|
|
353
|
+
backupDigest: null,
|
|
354
|
+
identity: entry.identity,
|
|
355
|
+
owner: 'pappra',
|
|
356
|
+
verifiedAt: journal.updatedAt,
|
|
357
|
+
}));
|
|
358
|
+
pendingResidual = journal.entries
|
|
359
|
+
.filter((entry) => entry.status === 'pending')
|
|
360
|
+
.map((entry) => entry.path);
|
|
361
|
+
if (recovered.length > 0) {
|
|
362
|
+
entries = recovered;
|
|
363
|
+
journalRecovered = journal;
|
|
364
|
+
}
|
|
365
|
+
else if (pendingResidual.length > 0) {
|
|
366
|
+
return {
|
|
367
|
+
ok: false,
|
|
368
|
+
status: 'partial',
|
|
369
|
+
removedPaths: [],
|
|
370
|
+
residualPaths: pendingResidual,
|
|
371
|
+
diagnostics: [
|
|
372
|
+
diagnostic(diagnosticCodes.initApplyResidual, 'Interrupted apply journal has pending entries without proven file identity.', 'Inspect the listed paths manually; Pappra cannot prove ownership.', journalPath(options.root, options.installId)),
|
|
373
|
+
],
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (entries.length === 0) {
|
|
379
|
+
return {
|
|
380
|
+
ok: false,
|
|
381
|
+
status: 'unmanaged',
|
|
382
|
+
removedPaths: [],
|
|
383
|
+
residualPaths: [],
|
|
384
|
+
diagnostics: manifestRead.diagnostics.length > 0
|
|
385
|
+
? manifestRead.diagnostics
|
|
386
|
+
: [
|
|
387
|
+
diagnostic(diagnosticCodes.initApplyUnsupported, 'Workspace has no managed ownership entry for this install.', 'There is nothing with proven ownership to roll back.', workspaceInstallPaths(options.root).manifestPath),
|
|
388
|
+
],
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
if (manifest !== null) {
|
|
392
|
+
const currentIdentity = await rootIdentityOf(options.root);
|
|
393
|
+
if (currentIdentity === null ||
|
|
394
|
+
currentIdentity.dev !== manifest.rootIdentity.dev ||
|
|
395
|
+
currentIdentity.ino !== manifest.rootIdentity.ino) {
|
|
396
|
+
return {
|
|
397
|
+
ok: false,
|
|
398
|
+
status: 'failed',
|
|
399
|
+
removedPaths: [],
|
|
400
|
+
residualPaths: [],
|
|
401
|
+
diagnostics: [
|
|
402
|
+
diagnostic(diagnosticCodes.initPlanStale, 'Workspace root identity does not match the managed manifest.', 'Inspect the workspace; Pappra does not roll back another root.', options.root),
|
|
403
|
+
],
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const lock = await acquireForRemoval(options.root, 'rollback-workspace-files', options.installId);
|
|
408
|
+
if (!lock.ok)
|
|
409
|
+
return lock.result;
|
|
410
|
+
const { journal, file } = journalFor('rollback-workspace-files', options.installId, options.root);
|
|
411
|
+
let current = withStep({ ...journal, status: 'applying', updatedAt: now() }, 'lock-acquired', lock.path);
|
|
412
|
+
if (journalRecovered !== null) {
|
|
413
|
+
current = withStep(current, 'recovered-from-apply-journal', journalRecovered.installId);
|
|
414
|
+
}
|
|
415
|
+
await writeInstallJournal(file, current, options.root);
|
|
416
|
+
const baseline = manifest ?? {
|
|
417
|
+
schema: 'pappra/install-manifest/1',
|
|
418
|
+
workspaceRoot: options.root,
|
|
419
|
+
rootIdentity: { dev: 0, ino: 0 },
|
|
420
|
+
entries: [],
|
|
421
|
+
createdAt: now(),
|
|
422
|
+
updatedAt: now(),
|
|
423
|
+
};
|
|
424
|
+
const removal = await removeManifestEntries(options.root, baseline, entries);
|
|
425
|
+
let residual = [...removal.residual, ...pendingResidual];
|
|
426
|
+
if (manifest !== null) {
|
|
427
|
+
const remainingManifest = {
|
|
428
|
+
...manifest,
|
|
429
|
+
entries: [...removal.remaining],
|
|
430
|
+
updatedAt: now(),
|
|
431
|
+
};
|
|
432
|
+
const manifestWritten = await writeWorkspaceManifest(options.root, remainingManifest);
|
|
433
|
+
if (!manifestWritten.ok) {
|
|
434
|
+
residual.push(workspaceInstallPaths(options.root).manifestPath);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
current = {
|
|
438
|
+
...withStep(current, 'files-removed', removal.removed.join(', ')),
|
|
439
|
+
entries: entries.map((entry) => ({
|
|
440
|
+
path: entry.path,
|
|
441
|
+
status: residual.includes(entry.path) ? 'residual' : 'removed',
|
|
442
|
+
digest: entry.afterDigest,
|
|
443
|
+
identity: entry.identity,
|
|
444
|
+
backupRef: entry.backupRef,
|
|
445
|
+
backupDigest: entry.backupDigest,
|
|
446
|
+
detail: null,
|
|
447
|
+
})),
|
|
448
|
+
residuals: residual,
|
|
449
|
+
};
|
|
450
|
+
const journalWritten = await finishJournal(file, current, residual, options.root);
|
|
451
|
+
if (!journalWritten.ok)
|
|
452
|
+
residual.push(file);
|
|
453
|
+
const lockRelease = await releaseInstallLock(lock.path, lock.token);
|
|
454
|
+
if (!lockRelease.ok)
|
|
455
|
+
residual.push(lock.path);
|
|
456
|
+
return {
|
|
457
|
+
ok: residual.length === 0,
|
|
458
|
+
status: residual.length === 0 ? 'rolled-back' : 'partial',
|
|
459
|
+
removedPaths: removal.removed,
|
|
460
|
+
residualPaths: residual,
|
|
461
|
+
diagnostics: residual.map((path) => createDiagnostic({
|
|
462
|
+
code: diagnosticCodes.initApplyResidual,
|
|
463
|
+
severity: 'warning',
|
|
464
|
+
message: `Rollback left a residual path: ${path}`,
|
|
465
|
+
source: path,
|
|
466
|
+
hint: 'Inspect manually; Pappra preserves files whose ownership or content changed.',
|
|
467
|
+
})),
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
export async function uninstallWorkspaceManagedFiles(options) {
|
|
471
|
+
const manifestRead = await readWorkspaceManifest(options.root);
|
|
472
|
+
if (manifestRead.value === null) {
|
|
473
|
+
return {
|
|
474
|
+
ok: true,
|
|
475
|
+
status: 'missing',
|
|
476
|
+
removedPaths: [],
|
|
477
|
+
residualPaths: [],
|
|
478
|
+
diagnostics: [],
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
const manifest = manifestRead.value;
|
|
482
|
+
const currentIdentity = await rootIdentityOf(options.root);
|
|
483
|
+
if (currentIdentity === null ||
|
|
484
|
+
currentIdentity.dev !== manifest.rootIdentity.dev ||
|
|
485
|
+
currentIdentity.ino !== manifest.rootIdentity.ino) {
|
|
486
|
+
return {
|
|
487
|
+
ok: false,
|
|
488
|
+
status: 'failed',
|
|
489
|
+
removedPaths: [],
|
|
490
|
+
residualPaths: [],
|
|
491
|
+
diagnostics: [
|
|
492
|
+
diagnostic(diagnosticCodes.initPlanStale, 'Workspace root identity does not match the managed manifest.', 'Inspect the workspace; Pappra does not uninstall another root.', options.root),
|
|
493
|
+
],
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
const lock = await acquireForRemoval(options.root, 'uninstall-workspace-files', options.root);
|
|
497
|
+
if (!lock.ok)
|
|
498
|
+
return lock.result;
|
|
499
|
+
const { journal, file } = journalFor('uninstall-workspace-files', options.root, options.root);
|
|
500
|
+
let current = withStep({ ...journal, status: 'applying', updatedAt: now() }, 'lock-acquired', lock.path);
|
|
501
|
+
await writeInstallJournal(file, current, options.root);
|
|
502
|
+
const removal = await removeManifestEntries(options.root, manifest, manifest.entries);
|
|
503
|
+
const remainingManifest = {
|
|
504
|
+
...manifest,
|
|
505
|
+
entries: [...removal.remaining],
|
|
506
|
+
updatedAt: now(),
|
|
507
|
+
};
|
|
508
|
+
const manifestWritten = await writeWorkspaceManifest(options.root, remainingManifest);
|
|
509
|
+
const residual = [...removal.residual];
|
|
510
|
+
if (!manifestWritten.ok)
|
|
511
|
+
residual.push(workspaceInstallPaths(options.root).manifestPath);
|
|
512
|
+
current = {
|
|
513
|
+
...withStep(current, 'files-removed', removal.removed.join(', ')),
|
|
514
|
+
entries: manifest.entries.map((entry) => ({
|
|
515
|
+
path: entry.path,
|
|
516
|
+
status: residual.includes(entry.path) ? 'residual' : 'removed',
|
|
517
|
+
digest: entry.afterDigest,
|
|
518
|
+
identity: entry.identity,
|
|
519
|
+
backupRef: entry.backupRef,
|
|
520
|
+
backupDigest: entry.backupDigest,
|
|
521
|
+
detail: null,
|
|
522
|
+
})),
|
|
523
|
+
residuals: residual,
|
|
524
|
+
};
|
|
525
|
+
const journalWritten = await finishJournal(file, current, residual, options.root);
|
|
526
|
+
if (!journalWritten.ok)
|
|
527
|
+
residual.push(file);
|
|
528
|
+
const lockRelease = await releaseInstallLock(lock.path, lock.token);
|
|
529
|
+
if (!lockRelease.ok)
|
|
530
|
+
residual.push(lock.path);
|
|
531
|
+
return {
|
|
532
|
+
ok: residual.length === 0,
|
|
533
|
+
status: residual.length === 0 ? 'uninstalled' : 'partial',
|
|
534
|
+
removedPaths: removal.removed,
|
|
535
|
+
residualPaths: residual,
|
|
536
|
+
diagnostics: residual.map((path) => createDiagnostic({
|
|
537
|
+
code: diagnosticCodes.initApplyResidual,
|
|
538
|
+
severity: 'warning',
|
|
539
|
+
message: `Uninstall preserved a residual path: ${path}`,
|
|
540
|
+
source: path,
|
|
541
|
+
hint: 'User-modified or uncertain files are preserved; inspect manually.',
|
|
542
|
+
})),
|
|
543
|
+
};
|
|
544
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Diagnostic } from '../schemas/diagnostics.js';
|
|
2
|
+
import type { InstallJournal, RuntimeManifest } from '../schemas/install.js';
|
|
3
|
+
export type RuntimeInstallStatus = 'installed' | 'reused' | 'locked' | 'corrupt' | 'partial' | 'failed';
|
|
4
|
+
export interface RuntimeInstallResult {
|
|
5
|
+
readonly ok: boolean;
|
|
6
|
+
readonly status: RuntimeInstallStatus;
|
|
7
|
+
readonly version: string;
|
|
8
|
+
readonly runtimeRoot: string | null;
|
|
9
|
+
readonly manifest: RuntimeManifest | null;
|
|
10
|
+
readonly journal: InstallJournal | null;
|
|
11
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
12
|
+
readonly residualPaths: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
export interface RuntimeInstallSource {
|
|
15
|
+
readonly kind: 'version' | 'tarball';
|
|
16
|
+
/** Required for tarball sources. */
|
|
17
|
+
readonly path?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface InstallRuntimeOptions {
|
|
20
|
+
readonly version: string;
|
|
21
|
+
readonly source: RuntimeInstallSource;
|
|
22
|
+
readonly dataDir?: string;
|
|
23
|
+
readonly packageName?: string;
|
|
24
|
+
readonly offline?: boolean;
|
|
25
|
+
readonly timeoutMs?: number;
|
|
26
|
+
/** Fault-injection seam for tests; the CLI never supplies this hook. */
|
|
27
|
+
readonly beforeVerifiedJournalPersist?: () => Promise<void> | void;
|
|
28
|
+
}
|
|
29
|
+
export interface RuntimePaths {
|
|
30
|
+
readonly dataDir: string;
|
|
31
|
+
readonly runtimeRoot: string;
|
|
32
|
+
readonly lockPath: string;
|
|
33
|
+
readonly stagingRoot: string;
|
|
34
|
+
readonly journalsDir: string;
|
|
35
|
+
}
|
|
36
|
+
export declare function resolveUserDataDir(environment?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, home?: string): string;
|
|
37
|
+
export declare function runtimePaths(version: string, dataDir?: string): RuntimePaths;
|
|
38
|
+
export declare function installRuntime(options: InstallRuntimeOptions): Promise<RuntimeInstallResult>;
|
|
39
|
+
export interface RuntimeStateReport {
|
|
40
|
+
readonly version: string;
|
|
41
|
+
readonly status: 'missing' | 'complete' | 'partial' | 'corrupt' | 'locked';
|
|
42
|
+
readonly runtimeRoot: string;
|
|
43
|
+
readonly manifest: RuntimeManifest | null;
|
|
44
|
+
readonly journal: InstallJournal | null;
|
|
45
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
46
|
+
}
|
|
47
|
+
export declare function inspectRuntimeState(version: string, dataDir?: string): Promise<RuntimeStateReport>;
|
|
48
|
+
export interface RemoveRuntimeOptions {
|
|
49
|
+
readonly version: string;
|
|
50
|
+
readonly dataDir?: string;
|
|
51
|
+
/** Caller-provided live workspace/runtime references. Unknown is not empty. */
|
|
52
|
+
readonly activeReferences?: readonly string[];
|
|
53
|
+
}
|
|
54
|
+
export interface RemoveRuntimeResult {
|
|
55
|
+
readonly ok: boolean;
|
|
56
|
+
readonly status: 'removed' | 'missing' | 'in-use' | 'locked' | 'corrupt' | 'failed';
|
|
57
|
+
readonly runtimeRoot: string;
|
|
58
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
59
|
+
readonly residualPaths: readonly string[];
|
|
60
|
+
}
|
|
61
|
+
export declare function removeRuntime(options: RemoveRuntimeOptions): Promise<RemoveRuntimeResult>;
|
|
62
|
+
export type RuntimePassiveStatus = 'missing' | 'content-match' | 'version-mismatch' | 'corrupt' | 'locked' | 'partial' | 'unreadable';
|
|
63
|
+
export interface PassiveRuntimeState {
|
|
64
|
+
readonly version: string;
|
|
65
|
+
readonly status: RuntimePassiveStatus;
|
|
66
|
+
readonly runtimeRoot: string;
|
|
67
|
+
readonly manifest: RuntimeManifest | null;
|
|
68
|
+
readonly journal: InstallJournal | null;
|
|
69
|
+
readonly lockPath: string;
|
|
70
|
+
readonly lockPresent: boolean;
|
|
71
|
+
readonly packageDigestMatch: boolean;
|
|
72
|
+
readonly executableExists: boolean;
|
|
73
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
74
|
+
}
|
|
75
|
+
/** Read-only runtime state inspection. It never executes the installed CLI. */
|
|
76
|
+
export declare function inspectRuntimeStatePassive(version: string, dataDir?: string): Promise<PassiveRuntimeState>;
|