@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,1145 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { mkdir, readFile, readdir, readlink, rename, rm, writeFile, } from 'node:fs/promises';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { createDiagnostic } from '../schemas/diagnostics.js';
|
|
8
|
+
import { diagnosticCodes } from '../schemas/errors.js';
|
|
9
|
+
import { installJournalSchema, runtimeManifestSchema, runtimePackageManifestSchema, runtimeSourceSchema, } from '../schemas/install.js';
|
|
10
|
+
import { isPathInside } from '../schemas/paths.js';
|
|
11
|
+
import { errorMessage } from '../schemas/outcome.js';
|
|
12
|
+
import { observePath, readFileBounded, readTextFileBounded, resolveRealpath, sha256Digest, } from '../infra/fs.js';
|
|
13
|
+
import { acquireInstallLock, readJsonDocument, releaseInstallLock, scanJournalDirectory, writeInstallJournal, } from './ownership.js';
|
|
14
|
+
export function resolveUserDataDir(environment = process.env, platform = process.platform, home = homedir()) {
|
|
15
|
+
if (environment['PAPPRA_DATA_HOME'] !== undefined) {
|
|
16
|
+
return resolve(environment['PAPPRA_DATA_HOME']);
|
|
17
|
+
}
|
|
18
|
+
if (platform === 'darwin') {
|
|
19
|
+
return join(home, 'Library', 'Application Support', 'Pappra');
|
|
20
|
+
}
|
|
21
|
+
if (platform === 'win32') {
|
|
22
|
+
return join(environment['LOCALAPPDATA'] ?? home, 'Pappra');
|
|
23
|
+
}
|
|
24
|
+
return join(environment['XDG_DATA_HOME'] ?? join(home, '.local', 'share'), 'pappra');
|
|
25
|
+
}
|
|
26
|
+
function validVersion(version) {
|
|
27
|
+
return /^[0-9A-Za-z][0-9A-Za-z0-9._+-]*$/.test(version);
|
|
28
|
+
}
|
|
29
|
+
export function runtimePaths(version, dataDir = resolveUserDataDir()) {
|
|
30
|
+
const runtimes = join(dataDir, 'runtimes');
|
|
31
|
+
return {
|
|
32
|
+
dataDir,
|
|
33
|
+
runtimeRoot: join(runtimes, version),
|
|
34
|
+
lockPath: join(runtimes, '.locks', `${version}.lock`),
|
|
35
|
+
stagingRoot: join(runtimes, '.staging'),
|
|
36
|
+
journalsDir: join(dataDir, 'journals'),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function diagnosticFailure(code, message, hint, source) {
|
|
40
|
+
return createDiagnostic({
|
|
41
|
+
code,
|
|
42
|
+
severity: 'error',
|
|
43
|
+
message,
|
|
44
|
+
source,
|
|
45
|
+
hint,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function diagnosticCode(name) {
|
|
49
|
+
return diagnosticCodes[name];
|
|
50
|
+
}
|
|
51
|
+
function failureResult(status, message, options) {
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
status,
|
|
55
|
+
version: options.version,
|
|
56
|
+
runtimeRoot: options.runtimeRoot ?? null,
|
|
57
|
+
manifest: options.manifest ?? null,
|
|
58
|
+
journal: options.journal ?? null,
|
|
59
|
+
diagnostics: [
|
|
60
|
+
diagnosticFailure(diagnosticCodes.initApplyFailed, message, options.hint ?? 'Inspect the runtime state; Pappra did not auto-retry.', options.source ?? 'runtime install'),
|
|
61
|
+
],
|
|
62
|
+
residualPaths: options.residualPaths ?? [],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function isoNow() {
|
|
66
|
+
return new Date().toISOString();
|
|
67
|
+
}
|
|
68
|
+
async function hashPackageDirectory(directory) {
|
|
69
|
+
const parts = [];
|
|
70
|
+
const visit = async (current) => {
|
|
71
|
+
const entries = (await readdir(current, { withFileTypes: true })).sort((left, right) => left.name.localeCompare(right.name));
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
const path = join(current, entry.name);
|
|
74
|
+
const rel = relative(directory, path).split(sep).join('/');
|
|
75
|
+
if (entry.isSymbolicLink()) {
|
|
76
|
+
const target = await readlink(path).catch(() => '');
|
|
77
|
+
parts.push(`symlink:${rel}:${target}`);
|
|
78
|
+
}
|
|
79
|
+
else if (entry.isDirectory()) {
|
|
80
|
+
parts.push(`dir:${rel}`);
|
|
81
|
+
await visit(path);
|
|
82
|
+
}
|
|
83
|
+
else if (entry.isFile()) {
|
|
84
|
+
const file = await readFileBounded(path, 64 * 1024 * 1024);
|
|
85
|
+
if (!file.ok) {
|
|
86
|
+
throw new Error(`Cannot hash runtime package file ${path}: ${file.code}`);
|
|
87
|
+
}
|
|
88
|
+
parts.push(`file:${rel}:${file.value.digest}`);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
parts.push(`other:${rel}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
await visit(directory);
|
|
96
|
+
return sha256Digest(parts.join('\n'));
|
|
97
|
+
}
|
|
98
|
+
async function packageDigest(directory) {
|
|
99
|
+
return hashPackageDirectory(directory);
|
|
100
|
+
}
|
|
101
|
+
async function runCommand(executable, args, options) {
|
|
102
|
+
return new Promise((resolvePromise) => {
|
|
103
|
+
const child = spawn(executable, [...args], {
|
|
104
|
+
cwd: options.cwd,
|
|
105
|
+
shell: false,
|
|
106
|
+
env: { ...process.env, npm_config_update_notifier: 'false' },
|
|
107
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
108
|
+
});
|
|
109
|
+
let stdout = '';
|
|
110
|
+
let stderr = '';
|
|
111
|
+
let timedOut = false;
|
|
112
|
+
const timer = setTimeout(() => {
|
|
113
|
+
timedOut = true;
|
|
114
|
+
child.kill('SIGKILL');
|
|
115
|
+
}, options.timeoutMs);
|
|
116
|
+
child.stdout.on('data', (chunk) => {
|
|
117
|
+
if (stdout.length < 4 * 1024 * 1024)
|
|
118
|
+
stdout += chunk.toString('utf8');
|
|
119
|
+
});
|
|
120
|
+
child.stderr.on('data', (chunk) => {
|
|
121
|
+
if (stderr.length < 4 * 1024 * 1024)
|
|
122
|
+
stderr += chunk.toString('utf8');
|
|
123
|
+
});
|
|
124
|
+
child.on('error', (error) => {
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
resolvePromise({
|
|
127
|
+
code: null,
|
|
128
|
+
stdout,
|
|
129
|
+
stderr: `${stderr}${errorMessage(error)}`,
|
|
130
|
+
timedOut,
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
child.on('close', (code) => {
|
|
134
|
+
clearTimeout(timer);
|
|
135
|
+
resolvePromise({ code, stdout, stderr, timedOut });
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
function npmCommand() {
|
|
140
|
+
const execpath = process.env['npm_execpath'];
|
|
141
|
+
if (execpath !== undefined && /npm-cli\.js$/.test(execpath)) {
|
|
142
|
+
return { executable: process.execPath, args: [execpath] };
|
|
143
|
+
}
|
|
144
|
+
const bin = dirname(process.execPath);
|
|
145
|
+
const candidates = [
|
|
146
|
+
resolve(bin, '../lib/node_modules/npm/bin/npm-cli.js'),
|
|
147
|
+
resolve(bin, 'node_modules/npm/bin/npm-cli.js'),
|
|
148
|
+
];
|
|
149
|
+
for (const candidate of candidates) {
|
|
150
|
+
if (candidate.endsWith('npm-cli.js') && existsSync(candidate)) {
|
|
151
|
+
return { executable: process.execPath, args: [candidate] };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
executable: process.platform === 'win32' ? 'npm.cmd' : 'npm',
|
|
156
|
+
args: [],
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
async function readPackageManifest(directory, boundaryRoot) {
|
|
160
|
+
const manifestPath = join(directory, 'package.json');
|
|
161
|
+
const real = await resolveRealpath(manifestPath);
|
|
162
|
+
if (real === null || !isPathInside(boundaryRoot, real))
|
|
163
|
+
return null;
|
|
164
|
+
const read = await readTextFileBounded(manifestPath);
|
|
165
|
+
if (!read.ok)
|
|
166
|
+
return null;
|
|
167
|
+
try {
|
|
168
|
+
const raw = JSON.parse(read.value.text);
|
|
169
|
+
const parsed = runtimePackageManifestSchema.safeParse(raw);
|
|
170
|
+
return parsed.success ? parsed.data : null;
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function binPath(packageManifest, packageName) {
|
|
177
|
+
if (packageManifest.bin === undefined)
|
|
178
|
+
return null;
|
|
179
|
+
if (typeof packageManifest.bin === 'string')
|
|
180
|
+
return packageManifest.bin;
|
|
181
|
+
return (packageManifest.bin[packageName] ?? packageManifest.bin['pappra'] ?? null);
|
|
182
|
+
}
|
|
183
|
+
async function verifyInstalledRuntime(runtimeRoot, expectedVersion, expectedPackageName, timeoutMs) {
|
|
184
|
+
const packageRoot = join(runtimeRoot, 'node_modules', ...expectedPackageName.split('/'));
|
|
185
|
+
const runtimeReal = await resolveRealpath(runtimeRoot);
|
|
186
|
+
const packageManifest = runtimeReal === null
|
|
187
|
+
? null
|
|
188
|
+
: await readPackageManifest(packageRoot, runtimeReal);
|
|
189
|
+
if (packageManifest === null ||
|
|
190
|
+
packageManifest.name !== expectedPackageName ||
|
|
191
|
+
packageManifest.version !== expectedVersion) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
const bin = binPath(packageManifest, expectedPackageName);
|
|
195
|
+
if (bin === null)
|
|
196
|
+
return null;
|
|
197
|
+
const executablePath = resolve(packageRoot, bin);
|
|
198
|
+
if (!isPathInside(packageRoot, executablePath))
|
|
199
|
+
return null;
|
|
200
|
+
const observation = await observePath(executablePath);
|
|
201
|
+
if (observation.kind !== 'file')
|
|
202
|
+
return null;
|
|
203
|
+
const command = await runCommand(process.execPath, [executablePath, '--version', '--json'], { cwd: packageRoot, timeoutMs });
|
|
204
|
+
if (command.timedOut || command.code !== 0)
|
|
205
|
+
return null;
|
|
206
|
+
let parsed;
|
|
207
|
+
try {
|
|
208
|
+
parsed = JSON.parse(command.stdout.trim());
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
if (typeof parsed !== 'object' ||
|
|
214
|
+
parsed === null ||
|
|
215
|
+
!('version' in parsed) ||
|
|
216
|
+
parsed.version !== expectedVersion) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
manifest: packageManifest,
|
|
221
|
+
digest: await packageDigest(packageRoot),
|
|
222
|
+
executableRelative: relative(runtimeRoot, executablePath)
|
|
223
|
+
.split(sep)
|
|
224
|
+
.join('/'),
|
|
225
|
+
smoke: {
|
|
226
|
+
argv: ['--version', '--json'],
|
|
227
|
+
exitCode: command.code,
|
|
228
|
+
stdoutDigest: sha256Digest(command.stdout),
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
async function readRuntimeManifest(runtimeRoot) {
|
|
233
|
+
const read = await readJsonDocument(join(runtimeRoot, 'install-manifest.json'), runtimeManifestSchema);
|
|
234
|
+
return read.value;
|
|
235
|
+
}
|
|
236
|
+
async function scanRuntimeJournals(version, paths) {
|
|
237
|
+
const directory = await scanJournalDirectory(paths.journalsDir);
|
|
238
|
+
if (directory.kind === 'missing') {
|
|
239
|
+
return { journal: null, unreadable: [], directoryUnreadable: false };
|
|
240
|
+
}
|
|
241
|
+
if (directory.kind === 'unreadable') {
|
|
242
|
+
return {
|
|
243
|
+
journal: null,
|
|
244
|
+
unreadable: [paths.journalsDir],
|
|
245
|
+
directoryUnreadable: true,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
const unreadable = [...directory.nonRegular];
|
|
249
|
+
let journal = null;
|
|
250
|
+
for (const path of directory.files) {
|
|
251
|
+
const read = await readJsonDocument(path, installJournalSchema);
|
|
252
|
+
if (read.value === null) {
|
|
253
|
+
unreadable.push(path);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (journal === null &&
|
|
257
|
+
read.value.runtimeVersion === version &&
|
|
258
|
+
(read.value.status === 'prepared' ||
|
|
259
|
+
read.value.status === 'applying' ||
|
|
260
|
+
read.value.status === 'applied' ||
|
|
261
|
+
read.value.status === 'rollback-required')) {
|
|
262
|
+
journal = read.value;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return { journal, unreadable, directoryUnreadable: false };
|
|
266
|
+
}
|
|
267
|
+
async function completeExistingRuntime(version, paths, packageName, timeoutMs) {
|
|
268
|
+
const observation = await observePath(paths.runtimeRoot);
|
|
269
|
+
if (observation.kind === 'missing')
|
|
270
|
+
return null;
|
|
271
|
+
if (observation.kind !== 'directory') {
|
|
272
|
+
return failureResult('corrupt', `Runtime path is not a directory: ${paths.runtimeRoot}`, {
|
|
273
|
+
version,
|
|
274
|
+
runtimeRoot: paths.runtimeRoot,
|
|
275
|
+
hint: 'Inspect or remove the incomplete runtime explicitly.',
|
|
276
|
+
source: paths.runtimeRoot,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
const manifest = await readRuntimeManifest(paths.runtimeRoot);
|
|
280
|
+
if (manifest === null) {
|
|
281
|
+
return failureResult('corrupt', `Runtime has no valid manifest: ${paths.runtimeRoot}`, {
|
|
282
|
+
version,
|
|
283
|
+
runtimeRoot: paths.runtimeRoot,
|
|
284
|
+
hint: 'Inspect the partial runtime; Pappra does not overwrite it.',
|
|
285
|
+
source: paths.runtimeRoot,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
if (manifest.runtimeVersion !== version ||
|
|
289
|
+
manifest.packageName !== packageName) {
|
|
290
|
+
return failureResult('corrupt', `Runtime manifest does not match ${version}.`, {
|
|
291
|
+
version,
|
|
292
|
+
runtimeRoot: paths.runtimeRoot,
|
|
293
|
+
manifest,
|
|
294
|
+
hint: 'Inspect or remove the unrelated runtime explicitly.',
|
|
295
|
+
source: paths.runtimeRoot,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
const packageRoot = join(paths.runtimeRoot, 'node_modules', ...packageName.split('/'));
|
|
299
|
+
const digest = await packageDigest(packageRoot).catch(() => null);
|
|
300
|
+
const executableObservation = await observePath(manifest.executable);
|
|
301
|
+
if (digest === null ||
|
|
302
|
+
digest !== manifest.packageDigest ||
|
|
303
|
+
executableObservation.kind !== 'file') {
|
|
304
|
+
return failureResult('corrupt', `Runtime package content no longer matches its manifest: ${paths.runtimeRoot}`, {
|
|
305
|
+
version,
|
|
306
|
+
runtimeRoot: paths.runtimeRoot,
|
|
307
|
+
manifest,
|
|
308
|
+
hint: 'Inspect the runtime; Pappra will not silently reinstall or overwrite it.',
|
|
309
|
+
source: paths.runtimeRoot,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
const verified = await verifyInstalledRuntime(paths.runtimeRoot, version, packageName, timeoutMs);
|
|
313
|
+
if (verified === null) {
|
|
314
|
+
return failureResult('corrupt', `Runtime smoke check failed: ${paths.runtimeRoot}`, {
|
|
315
|
+
version,
|
|
316
|
+
runtimeRoot: paths.runtimeRoot,
|
|
317
|
+
manifest,
|
|
318
|
+
hint: 'Run doctor diagnostics; Pappra will not replace a failed runtime automatically.',
|
|
319
|
+
source: paths.runtimeRoot,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
ok: true,
|
|
324
|
+
status: 'reused',
|
|
325
|
+
version,
|
|
326
|
+
runtimeRoot: paths.runtimeRoot,
|
|
327
|
+
manifest,
|
|
328
|
+
journal: null,
|
|
329
|
+
diagnostics: [],
|
|
330
|
+
residualPaths: [],
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function updateJournal(journal, status, step, detail, failed = false) {
|
|
334
|
+
return {
|
|
335
|
+
...journal,
|
|
336
|
+
status,
|
|
337
|
+
updatedAt: isoNow(),
|
|
338
|
+
steps: [
|
|
339
|
+
...journal.steps,
|
|
340
|
+
{ name: step, status: failed ? 'failed' : 'ok', at: isoNow(), detail },
|
|
341
|
+
],
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
export async function installRuntime(options) {
|
|
345
|
+
const version = options.version;
|
|
346
|
+
const packageName = options.packageName ?? 'pappra';
|
|
347
|
+
if (!validVersion(version)) {
|
|
348
|
+
return failureResult('failed', `Invalid runtime version: ${version}`, {
|
|
349
|
+
version,
|
|
350
|
+
hint: 'Use a plain exact version (no path separators).',
|
|
351
|
+
source: 'runtime install',
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
if (!/^[A-Za-z0-9@][A-Za-z0-9._/@-]*$/.test(packageName) ||
|
|
355
|
+
packageName.includes('..')) {
|
|
356
|
+
return failureResult('failed', `Invalid runtime package name: ${packageName}`, {
|
|
357
|
+
version,
|
|
358
|
+
hint: 'Use a normal npm package name.',
|
|
359
|
+
source: 'runtime install',
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
const timeoutMs = options.timeoutMs ?? 120_000;
|
|
363
|
+
const dataDir = resolve(options.dataDir ?? resolveUserDataDir());
|
|
364
|
+
const paths = runtimePaths(version, dataDir);
|
|
365
|
+
const journalScan = await scanRuntimeJournals(version, paths);
|
|
366
|
+
if (journalScan.unreadable.length > 0) {
|
|
367
|
+
return failureResult(journalScan.directoryUnreadable ? 'failed' : 'partial', `Runtime journal state is unreadable or incompatible: ${journalScan.unreadable.join(', ')}`, {
|
|
368
|
+
version,
|
|
369
|
+
runtimeRoot: paths.runtimeRoot,
|
|
370
|
+
residualPaths: journalScan.unreadable,
|
|
371
|
+
hint: 'Inspect the listed journals; Pappra does not ignore incompatible install state.',
|
|
372
|
+
source: paths.journalsDir,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
if (journalScan.journal !== null) {
|
|
376
|
+
return failureResult('partial', `A previous runtime installation is incomplete: ${join(paths.journalsDir, `${journalScan.journal.installId}.json`)}`, {
|
|
377
|
+
version,
|
|
378
|
+
runtimeRoot: paths.runtimeRoot,
|
|
379
|
+
journal: journalScan.journal,
|
|
380
|
+
hint: 'Inspect the journal and runtime directory; Pappra does not auto-retry an interrupted install.',
|
|
381
|
+
source: paths.runtimeRoot,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
const existingLock = await observePath(paths.lockPath);
|
|
385
|
+
if (existingLock.kind !== 'missing') {
|
|
386
|
+
return failureResult('locked', 'Runtime installation is locked.', {
|
|
387
|
+
version,
|
|
388
|
+
runtimeRoot: paths.runtimeRoot,
|
|
389
|
+
residualPaths: [paths.lockPath],
|
|
390
|
+
hint: 'Inspect the existing lock; Pappra does not reuse a runtime while its install state is locked.',
|
|
391
|
+
source: paths.lockPath,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
const existing = await completeExistingRuntime(version, paths, packageName, timeoutMs);
|
|
395
|
+
if (existing !== null)
|
|
396
|
+
return existing;
|
|
397
|
+
const lock = await acquireInstallLock(paths.lockPath, 'install-runtime', paths.runtimeRoot, dataDir);
|
|
398
|
+
if (!lock.ok) {
|
|
399
|
+
return failureResult('locked', lock.message ?? 'Runtime installation is locked.', {
|
|
400
|
+
version,
|
|
401
|
+
runtimeRoot: paths.runtimeRoot,
|
|
402
|
+
hint: 'Another install owns this version lock; Pappra will not reclaim it by age.',
|
|
403
|
+
source: paths.lockPath,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
const afterLock = await completeExistingRuntime(version, paths, packageName, timeoutMs);
|
|
407
|
+
if (afterLock !== null) {
|
|
408
|
+
await releaseInstallLock(paths.lockPath, lock.lock.token);
|
|
409
|
+
return afterLock;
|
|
410
|
+
}
|
|
411
|
+
let source;
|
|
412
|
+
try {
|
|
413
|
+
if (options.source.kind === 'tarball') {
|
|
414
|
+
const tarballPath = resolve(options.source.path ?? '');
|
|
415
|
+
const observation = await observePath(tarballPath);
|
|
416
|
+
if (observation.kind !== 'file' || observation.stats === null) {
|
|
417
|
+
throw new Error(`Runtime tarball is not a regular file: ${tarballPath}`);
|
|
418
|
+
}
|
|
419
|
+
const bytes = await readFile(tarballPath);
|
|
420
|
+
source = {
|
|
421
|
+
kind: 'tarball',
|
|
422
|
+
packageName,
|
|
423
|
+
version,
|
|
424
|
+
path: tarballPath,
|
|
425
|
+
digest: sha256Digest(bytes),
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
source = {
|
|
430
|
+
kind: 'version',
|
|
431
|
+
packageName,
|
|
432
|
+
version,
|
|
433
|
+
ref: `${packageName}@${version}`,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
runtimeSourceSchema.parse(source);
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
await releaseInstallLock(paths.lockPath, lock.lock.token);
|
|
440
|
+
return failureResult('failed', `Invalid runtime source: ${errorMessage(error)}`, {
|
|
441
|
+
version,
|
|
442
|
+
runtimeRoot: paths.runtimeRoot,
|
|
443
|
+
hint: 'Provide a readable local tarball or an exact package version.',
|
|
444
|
+
source: paths.runtimeRoot,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
const installId = randomUUID();
|
|
448
|
+
const stage = join(paths.stagingRoot, installId);
|
|
449
|
+
const journalFile = join(paths.journalsDir, `${installId}.json`);
|
|
450
|
+
let journal = {
|
|
451
|
+
schema: 'pappra/install-journal/1',
|
|
452
|
+
installId,
|
|
453
|
+
operation: 'install-runtime',
|
|
454
|
+
target: paths.runtimeRoot,
|
|
455
|
+
workspaceRoot: null,
|
|
456
|
+
runtimeVersion: version,
|
|
457
|
+
status: 'prepared',
|
|
458
|
+
createdAt: isoNow(),
|
|
459
|
+
updatedAt: isoNow(),
|
|
460
|
+
steps: [
|
|
461
|
+
{
|
|
462
|
+
name: 'lock-acquired',
|
|
463
|
+
status: 'ok',
|
|
464
|
+
at: isoNow(),
|
|
465
|
+
detail: paths.lockPath,
|
|
466
|
+
},
|
|
467
|
+
],
|
|
468
|
+
entries: [],
|
|
469
|
+
residuals: [],
|
|
470
|
+
manifestRef: join(paths.runtimeRoot, 'install-manifest.json'),
|
|
471
|
+
};
|
|
472
|
+
const writeJournal = async () => {
|
|
473
|
+
const written = await writeInstallJournal(journalFile, journal, dataDir);
|
|
474
|
+
return written.ok;
|
|
475
|
+
};
|
|
476
|
+
if (!(await writeJournal())) {
|
|
477
|
+
await releaseInstallLock(paths.lockPath, lock.lock.token);
|
|
478
|
+
return failureResult('failed', 'Cannot write runtime install journal.', {
|
|
479
|
+
version,
|
|
480
|
+
runtimeRoot: paths.runtimeRoot,
|
|
481
|
+
journal,
|
|
482
|
+
hint: 'Inspect data directory permissions before retrying.',
|
|
483
|
+
source: journalFile,
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
let published = false;
|
|
487
|
+
const fail = async (message, stageResidual) => {
|
|
488
|
+
let residual = [];
|
|
489
|
+
if (stageResidual) {
|
|
490
|
+
try {
|
|
491
|
+
await rm(stage, { recursive: true, force: true });
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
residual = [stage];
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
journal = updateJournal(journal, residual.length === 0 ? 'rolled-back' : 'rollback-required', 'failed', message, true);
|
|
498
|
+
journal = { ...journal, residuals: residual };
|
|
499
|
+
await writeJournal();
|
|
500
|
+
const lockRelease = await releaseInstallLock(paths.lockPath, lock.lock.token);
|
|
501
|
+
if (!lockRelease.ok)
|
|
502
|
+
residual = [...residual, paths.lockPath];
|
|
503
|
+
return failureResult('failed', message, {
|
|
504
|
+
version,
|
|
505
|
+
runtimeRoot: paths.runtimeRoot,
|
|
506
|
+
journal,
|
|
507
|
+
residualPaths: residual,
|
|
508
|
+
hint: 'Inspect the journal and staging/runtime paths; no automatic retry was performed.',
|
|
509
|
+
source: journalFile,
|
|
510
|
+
});
|
|
511
|
+
};
|
|
512
|
+
try {
|
|
513
|
+
await mkdir(stage, { recursive: true });
|
|
514
|
+
await writeFile(join(stage, 'package.json'), `${JSON.stringify({ name: 'pappra-runtime-stage', private: true, version: '0.0.0-private' }, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
515
|
+
journal = updateJournal(journal, 'applying', 'staging-created', stage);
|
|
516
|
+
if (!(await writeJournal())) {
|
|
517
|
+
throw new Error(`Cannot update install journal: ${journalFile}`);
|
|
518
|
+
}
|
|
519
|
+
const npm = npmCommand();
|
|
520
|
+
const installArgs = [
|
|
521
|
+
...npm.args,
|
|
522
|
+
'install',
|
|
523
|
+
'--prefix',
|
|
524
|
+
stage,
|
|
525
|
+
'--ignore-scripts',
|
|
526
|
+
'--no-audit',
|
|
527
|
+
'--no-fund',
|
|
528
|
+
'--no-package-lock',
|
|
529
|
+
'--save-exact',
|
|
530
|
+
...(options.offline === true ? ['--offline'] : []),
|
|
531
|
+
source.kind === 'tarball'
|
|
532
|
+
? source.path
|
|
533
|
+
: `${source.packageName}@${source.version}`,
|
|
534
|
+
];
|
|
535
|
+
const installed = await runCommand(npm.executable, installArgs, {
|
|
536
|
+
cwd: stage,
|
|
537
|
+
timeoutMs,
|
|
538
|
+
});
|
|
539
|
+
if (installed.timedOut || installed.code !== 0) {
|
|
540
|
+
throw new Error(`Package manager install failed (${installed.timedOut ? 'timeout' : `exit ${String(installed.code)}`}): ${installed.stderr.slice(0, 1000)}`);
|
|
541
|
+
}
|
|
542
|
+
journal = updateJournal(journal, 'applying', 'package-installed', null);
|
|
543
|
+
if (!(await writeJournal()))
|
|
544
|
+
throw new Error('Cannot update install journal.');
|
|
545
|
+
const verification = await verifyInstalledRuntime(stage, version, packageName, timeoutMs);
|
|
546
|
+
if (verification === null) {
|
|
547
|
+
throw new Error(`Installed runtime failed version/content/smoke verification for ${version}.`);
|
|
548
|
+
}
|
|
549
|
+
journal = updateJournal(journal, 'applying', 'runtime-verified', null);
|
|
550
|
+
if (!(await writeJournal()))
|
|
551
|
+
throw new Error('Cannot update install journal.');
|
|
552
|
+
const manifest = {
|
|
553
|
+
schema: 'pappra/runtime-manifest/1',
|
|
554
|
+
installId,
|
|
555
|
+
packageName,
|
|
556
|
+
runtimeVersion: version,
|
|
557
|
+
runtimeRoot: paths.runtimeRoot,
|
|
558
|
+
executable: join(paths.runtimeRoot, ...verification.executableRelative.split('/')),
|
|
559
|
+
source,
|
|
560
|
+
packageDigest: verification.digest,
|
|
561
|
+
smoke: verification.smoke,
|
|
562
|
+
verifiedAt: isoNow(),
|
|
563
|
+
};
|
|
564
|
+
const parsedManifest = runtimeManifestSchema.safeParse(manifest);
|
|
565
|
+
if (!parsedManifest.success) {
|
|
566
|
+
throw new Error(`Runtime manifest is invalid: ${parsedManifest.error.issues[0]?.message ?? 'unknown'}`);
|
|
567
|
+
}
|
|
568
|
+
await writeFile(join(stage, 'install-manifest.json'), `${JSON.stringify(parsedManifest.data, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
569
|
+
journal = updateJournal(journal, 'applied', 'manifest-written', null);
|
|
570
|
+
if (!(await writeJournal()))
|
|
571
|
+
throw new Error('Cannot update install journal.');
|
|
572
|
+
const finalObservation = await observePath(paths.runtimeRoot);
|
|
573
|
+
if (finalObservation.kind !== 'missing') {
|
|
574
|
+
throw new Error(`Runtime version directory already exists: ${paths.runtimeRoot}`);
|
|
575
|
+
}
|
|
576
|
+
await rename(stage, paths.runtimeRoot);
|
|
577
|
+
published = true;
|
|
578
|
+
const finalManifest = await readRuntimeManifest(paths.runtimeRoot);
|
|
579
|
+
if (finalManifest === null) {
|
|
580
|
+
throw new Error('Runtime manifest is unreadable after publish.');
|
|
581
|
+
}
|
|
582
|
+
journal = updateJournal(journal, 'verified', 'published', paths.runtimeRoot);
|
|
583
|
+
journal = {
|
|
584
|
+
...journal,
|
|
585
|
+
manifestRef: join(paths.runtimeRoot, 'install-manifest.json'),
|
|
586
|
+
};
|
|
587
|
+
await options.beforeVerifiedJournalPersist?.();
|
|
588
|
+
const verifiedJournalWritten = await writeJournal();
|
|
589
|
+
const released = await releaseInstallLock(paths.lockPath, lock.lock.token);
|
|
590
|
+
const residualPaths = [];
|
|
591
|
+
if (!verifiedJournalWritten)
|
|
592
|
+
residualPaths.push(journalFile);
|
|
593
|
+
if (!released.ok)
|
|
594
|
+
residualPaths.push(paths.lockPath);
|
|
595
|
+
if (residualPaths.length > 0) {
|
|
596
|
+
return {
|
|
597
|
+
ok: false,
|
|
598
|
+
status: 'partial',
|
|
599
|
+
version,
|
|
600
|
+
runtimeRoot: paths.runtimeRoot,
|
|
601
|
+
manifest: finalManifest,
|
|
602
|
+
journal,
|
|
603
|
+
diagnostics: residualPaths.map((path) => createDiagnostic({
|
|
604
|
+
code: diagnosticCodes.initApplyResidual,
|
|
605
|
+
severity: 'error',
|
|
606
|
+
message: `Runtime is verified but install state is not final: ${path}`,
|
|
607
|
+
source: path,
|
|
608
|
+
hint: 'Do not treat initialization as complete. Inspect the journal/lock and retry before continuing.',
|
|
609
|
+
})),
|
|
610
|
+
residualPaths,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
return {
|
|
614
|
+
ok: true,
|
|
615
|
+
status: 'installed',
|
|
616
|
+
version,
|
|
617
|
+
runtimeRoot: paths.runtimeRoot,
|
|
618
|
+
manifest: finalManifest,
|
|
619
|
+
journal,
|
|
620
|
+
diagnostics: [],
|
|
621
|
+
residualPaths,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
catch (error) {
|
|
625
|
+
if (published) {
|
|
626
|
+
journal = updateJournal(journal, 'rollback-required', 'published-but-failed', errorMessage(error), true);
|
|
627
|
+
journal = { ...journal, residuals: [paths.runtimeRoot] };
|
|
628
|
+
await writeJournal();
|
|
629
|
+
const released = await releaseInstallLock(paths.lockPath, lock.lock.token);
|
|
630
|
+
const residual = [paths.runtimeRoot];
|
|
631
|
+
if (!released.ok)
|
|
632
|
+
residual.push(paths.lockPath);
|
|
633
|
+
return failureResult('failed', errorMessage(error), {
|
|
634
|
+
version,
|
|
635
|
+
runtimeRoot: paths.runtimeRoot,
|
|
636
|
+
manifest: null,
|
|
637
|
+
journal,
|
|
638
|
+
residualPaths: residual,
|
|
639
|
+
hint: 'Runtime was published but post-publish validation/state failed. Inspect it before retrying; Pappra did not delete it automatically.',
|
|
640
|
+
source: paths.runtimeRoot,
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
return fail(errorMessage(error), true);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
export async function inspectRuntimeState(version, dataDir = resolveUserDataDir()) {
|
|
647
|
+
const paths = runtimePaths(version, dataDir);
|
|
648
|
+
if (!validVersion(version)) {
|
|
649
|
+
return {
|
|
650
|
+
version,
|
|
651
|
+
status: 'corrupt',
|
|
652
|
+
runtimeRoot: paths.runtimeRoot,
|
|
653
|
+
manifest: null,
|
|
654
|
+
journal: null,
|
|
655
|
+
diagnostics: [
|
|
656
|
+
createDiagnostic({
|
|
657
|
+
code: diagnosticCodes.initApplyFailed,
|
|
658
|
+
severity: 'error',
|
|
659
|
+
message: `Invalid runtime version: ${version}`,
|
|
660
|
+
source: paths.runtimeRoot,
|
|
661
|
+
hint: 'Use a plain exact version without path separators.',
|
|
662
|
+
}),
|
|
663
|
+
],
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
const lockObservation = await observePath(paths.lockPath);
|
|
667
|
+
const journalScan = await scanRuntimeJournals(version, paths);
|
|
668
|
+
if (journalScan.unreadable.length > 0) {
|
|
669
|
+
return {
|
|
670
|
+
version,
|
|
671
|
+
status: 'partial',
|
|
672
|
+
runtimeRoot: paths.runtimeRoot,
|
|
673
|
+
manifest: null,
|
|
674
|
+
journal: null,
|
|
675
|
+
diagnostics: [
|
|
676
|
+
createDiagnostic({
|
|
677
|
+
code: diagnosticCodes.initApplyResidual,
|
|
678
|
+
severity: 'warning',
|
|
679
|
+
message: `Runtime journals are unreadable or incompatible: ${journalScan.unreadable.join(', ')}`,
|
|
680
|
+
source: paths.journalsDir,
|
|
681
|
+
hint: 'Inspect the listed journals; no automatic retry or cleanup is performed.',
|
|
682
|
+
}),
|
|
683
|
+
],
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
const journal = journalScan.journal;
|
|
687
|
+
if (journal !== null) {
|
|
688
|
+
return {
|
|
689
|
+
version,
|
|
690
|
+
status: 'partial',
|
|
691
|
+
runtimeRoot: paths.runtimeRoot,
|
|
692
|
+
manifest: null,
|
|
693
|
+
journal,
|
|
694
|
+
diagnostics: [
|
|
695
|
+
createDiagnostic({
|
|
696
|
+
code: diagnosticCodes.initApplyResidual,
|
|
697
|
+
severity: 'warning',
|
|
698
|
+
message: `Runtime installation is incomplete; journal: ${join(paths.journalsDir, `${journal.installId}.json`)}`,
|
|
699
|
+
source: paths.runtimeRoot,
|
|
700
|
+
hint: 'Inspect the journal and staging directory; no automatic retry or cleanup is performed.',
|
|
701
|
+
}),
|
|
702
|
+
],
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
if (lockObservation.kind !== 'missing') {
|
|
706
|
+
return {
|
|
707
|
+
version,
|
|
708
|
+
status: 'locked',
|
|
709
|
+
runtimeRoot: paths.runtimeRoot,
|
|
710
|
+
manifest: null,
|
|
711
|
+
journal: null,
|
|
712
|
+
diagnostics: [
|
|
713
|
+
createDiagnostic({
|
|
714
|
+
code: diagnosticCodes.initApplyResidual,
|
|
715
|
+
severity: 'warning',
|
|
716
|
+
message: `Runtime installation lock exists: ${paths.lockPath}`,
|
|
717
|
+
source: paths.lockPath,
|
|
718
|
+
hint: 'Inspect the lock owner before acting; locks do not expire automatically.',
|
|
719
|
+
}),
|
|
720
|
+
],
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
const complete = await completeExistingRuntime(version, paths, 'pappra', 30_000);
|
|
724
|
+
if (complete === null) {
|
|
725
|
+
return {
|
|
726
|
+
version,
|
|
727
|
+
status: 'missing',
|
|
728
|
+
runtimeRoot: paths.runtimeRoot,
|
|
729
|
+
manifest: null,
|
|
730
|
+
journal: null,
|
|
731
|
+
diagnostics: [],
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
return {
|
|
735
|
+
version,
|
|
736
|
+
status: complete.status === 'reused' ? 'complete' : 'corrupt',
|
|
737
|
+
runtimeRoot: paths.runtimeRoot,
|
|
738
|
+
manifest: complete.manifest,
|
|
739
|
+
journal: complete.journal,
|
|
740
|
+
diagnostics: complete.diagnostics,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
export async function removeRuntime(options) {
|
|
744
|
+
const dataDir = resolve(options.dataDir ?? resolveUserDataDir());
|
|
745
|
+
const paths = runtimePaths(options.version, dataDir);
|
|
746
|
+
if (!validVersion(options.version)) {
|
|
747
|
+
return {
|
|
748
|
+
ok: false,
|
|
749
|
+
status: 'corrupt',
|
|
750
|
+
runtimeRoot: paths.runtimeRoot,
|
|
751
|
+
diagnostics: [
|
|
752
|
+
createDiagnostic({
|
|
753
|
+
code: diagnosticCodes.initApplyFailed,
|
|
754
|
+
severity: 'error',
|
|
755
|
+
message: `Invalid runtime version: ${options.version}`,
|
|
756
|
+
source: paths.runtimeRoot,
|
|
757
|
+
hint: 'Use a plain exact version without path separators.',
|
|
758
|
+
}),
|
|
759
|
+
],
|
|
760
|
+
residualPaths: [],
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
const references = options.activeReferences ?? [];
|
|
764
|
+
if (references.length > 0 || options.activeReferences === undefined) {
|
|
765
|
+
return {
|
|
766
|
+
ok: false,
|
|
767
|
+
status: 'in-use',
|
|
768
|
+
runtimeRoot: paths.runtimeRoot,
|
|
769
|
+
diagnostics: [
|
|
770
|
+
createDiagnostic({
|
|
771
|
+
code: diagnosticCodes.initApplyResidual,
|
|
772
|
+
severity: 'error',
|
|
773
|
+
message: options.activeReferences === undefined
|
|
774
|
+
? 'Runtime removal requires an explicit active-reference check.'
|
|
775
|
+
: `Runtime is referenced by ${references.length} active workspace(s).`,
|
|
776
|
+
source: paths.runtimeRoot,
|
|
777
|
+
hint: 'Resolve active references and retry; Pappra does not guess that a runtime is unused.',
|
|
778
|
+
}),
|
|
779
|
+
],
|
|
780
|
+
residualPaths: [],
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
const state = await inspectRuntimeState(options.version, dataDir);
|
|
784
|
+
if (state.status === 'missing') {
|
|
785
|
+
return {
|
|
786
|
+
ok: true,
|
|
787
|
+
status: 'missing',
|
|
788
|
+
runtimeRoot: paths.runtimeRoot,
|
|
789
|
+
diagnostics: [],
|
|
790
|
+
residualPaths: [],
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
if (state.status === 'locked' || state.status === 'partial') {
|
|
794
|
+
return {
|
|
795
|
+
ok: false,
|
|
796
|
+
status: state.status === 'locked' ? 'locked' : 'corrupt',
|
|
797
|
+
runtimeRoot: paths.runtimeRoot,
|
|
798
|
+
diagnostics: state.diagnostics,
|
|
799
|
+
residualPaths: [],
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
if (state.status !== 'complete' || state.manifest === null) {
|
|
803
|
+
return {
|
|
804
|
+
ok: false,
|
|
805
|
+
status: 'corrupt',
|
|
806
|
+
runtimeRoot: paths.runtimeRoot,
|
|
807
|
+
diagnostics: state.diagnostics,
|
|
808
|
+
residualPaths: [],
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
try {
|
|
812
|
+
await rm(paths.runtimeRoot, { recursive: true, force: false });
|
|
813
|
+
return {
|
|
814
|
+
ok: true,
|
|
815
|
+
status: 'removed',
|
|
816
|
+
runtimeRoot: paths.runtimeRoot,
|
|
817
|
+
diagnostics: [],
|
|
818
|
+
residualPaths: [],
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
catch (error) {
|
|
822
|
+
return {
|
|
823
|
+
ok: false,
|
|
824
|
+
status: 'failed',
|
|
825
|
+
runtimeRoot: paths.runtimeRoot,
|
|
826
|
+
diagnostics: [
|
|
827
|
+
createDiagnostic({
|
|
828
|
+
code: diagnosticCodes.initApplyResidual,
|
|
829
|
+
severity: 'error',
|
|
830
|
+
message: `Runtime removal failed: ${errorMessage(error)}`,
|
|
831
|
+
source: paths.runtimeRoot,
|
|
832
|
+
hint: 'Inspect the runtime directory and permissions; it was not reported as removed.',
|
|
833
|
+
}),
|
|
834
|
+
],
|
|
835
|
+
residualPaths: [paths.runtimeRoot],
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
/** Read-only runtime state inspection. It never executes the installed CLI. */
|
|
840
|
+
export async function inspectRuntimeStatePassive(version, dataDir = resolveUserDataDir()) {
|
|
841
|
+
const paths = runtimePaths(version, dataDir);
|
|
842
|
+
if (!validVersion(version)) {
|
|
843
|
+
return {
|
|
844
|
+
version,
|
|
845
|
+
status: 'unreadable',
|
|
846
|
+
runtimeRoot: paths.runtimeRoot,
|
|
847
|
+
manifest: null,
|
|
848
|
+
journal: null,
|
|
849
|
+
lockPath: paths.lockPath,
|
|
850
|
+
lockPresent: false,
|
|
851
|
+
packageDigestMatch: false,
|
|
852
|
+
executableExists: false,
|
|
853
|
+
diagnostics: [
|
|
854
|
+
createDiagnostic({
|
|
855
|
+
code: diagnosticCodes.initApplyFailed,
|
|
856
|
+
severity: 'error',
|
|
857
|
+
message: `Invalid runtime version: ${version}`,
|
|
858
|
+
source: paths.runtimeRoot,
|
|
859
|
+
hint: 'Inspect the workspace runtimeVersion; it is not a safe exact version.',
|
|
860
|
+
}),
|
|
861
|
+
],
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
const lock = await observePath(paths.lockPath);
|
|
865
|
+
const lockPresent = lock.kind !== 'missing';
|
|
866
|
+
const journalScan = await scanRuntimeJournals(version, paths);
|
|
867
|
+
if (journalScan.unreadable.length > 0) {
|
|
868
|
+
return {
|
|
869
|
+
version,
|
|
870
|
+
status: 'unreadable',
|
|
871
|
+
runtimeRoot: paths.runtimeRoot,
|
|
872
|
+
manifest: null,
|
|
873
|
+
journal: null,
|
|
874
|
+
lockPath: paths.lockPath,
|
|
875
|
+
lockPresent,
|
|
876
|
+
packageDigestMatch: false,
|
|
877
|
+
executableExists: false,
|
|
878
|
+
diagnostics: [
|
|
879
|
+
createDiagnostic({
|
|
880
|
+
code: diagnosticCodes.initApplyResidual,
|
|
881
|
+
severity: 'error',
|
|
882
|
+
message: `Runtime journals are unreadable or incompatible: ${journalScan.unreadable.join(', ')}`,
|
|
883
|
+
source: paths.journalsDir,
|
|
884
|
+
hint: 'Inspect the listed journals; doctor treats them as unknown state and does not ignore them.',
|
|
885
|
+
}),
|
|
886
|
+
],
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
const journal = journalScan.journal;
|
|
890
|
+
if (lockPresent) {
|
|
891
|
+
return {
|
|
892
|
+
version,
|
|
893
|
+
status: 'locked',
|
|
894
|
+
runtimeRoot: paths.runtimeRoot,
|
|
895
|
+
manifest: null,
|
|
896
|
+
journal,
|
|
897
|
+
lockPath: paths.lockPath,
|
|
898
|
+
lockPresent,
|
|
899
|
+
packageDigestMatch: false,
|
|
900
|
+
executableExists: false,
|
|
901
|
+
diagnostics: [
|
|
902
|
+
createDiagnostic({
|
|
903
|
+
code: diagnosticCodes.initApplyResidual,
|
|
904
|
+
severity: 'warning',
|
|
905
|
+
message: `Runtime installation lock exists: ${paths.lockPath}`,
|
|
906
|
+
source: paths.lockPath,
|
|
907
|
+
hint: 'Inspect the lock owner; Pappra does not reclaim it by age or through doctor.',
|
|
908
|
+
}),
|
|
909
|
+
],
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
if (journal !== null) {
|
|
913
|
+
return {
|
|
914
|
+
version,
|
|
915
|
+
status: 'partial',
|
|
916
|
+
runtimeRoot: paths.runtimeRoot,
|
|
917
|
+
manifest: null,
|
|
918
|
+
journal,
|
|
919
|
+
lockPath: paths.lockPath,
|
|
920
|
+
lockPresent,
|
|
921
|
+
packageDigestMatch: false,
|
|
922
|
+
executableExists: false,
|
|
923
|
+
diagnostics: [
|
|
924
|
+
createDiagnostic({
|
|
925
|
+
code: diagnosticCodes.initApplyResidual,
|
|
926
|
+
severity: 'warning',
|
|
927
|
+
message: `Runtime installation journal is not complete: ${join(paths.journalsDir, `${journal.installId}.json`)}`,
|
|
928
|
+
source: paths.runtimeRoot,
|
|
929
|
+
hint: 'Inspect the journal; no automatic retry or cleanup is performed.',
|
|
930
|
+
}),
|
|
931
|
+
],
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
const runtimeObservation = await observePath(paths.runtimeRoot);
|
|
935
|
+
if (runtimeObservation.kind === 'missing') {
|
|
936
|
+
return {
|
|
937
|
+
version,
|
|
938
|
+
status: 'missing',
|
|
939
|
+
runtimeRoot: paths.runtimeRoot,
|
|
940
|
+
manifest: null,
|
|
941
|
+
journal: null,
|
|
942
|
+
lockPath: paths.lockPath,
|
|
943
|
+
lockPresent,
|
|
944
|
+
packageDigestMatch: false,
|
|
945
|
+
executableExists: false,
|
|
946
|
+
diagnostics: [],
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
if (runtimeObservation.kind !== 'directory') {
|
|
950
|
+
return {
|
|
951
|
+
version,
|
|
952
|
+
status: 'corrupt',
|
|
953
|
+
runtimeRoot: paths.runtimeRoot,
|
|
954
|
+
manifest: null,
|
|
955
|
+
journal: null,
|
|
956
|
+
lockPath: paths.lockPath,
|
|
957
|
+
lockPresent,
|
|
958
|
+
packageDigestMatch: false,
|
|
959
|
+
executableExists: false,
|
|
960
|
+
diagnostics: [
|
|
961
|
+
createDiagnostic({
|
|
962
|
+
code: diagnosticCodes.initApplyFailed,
|
|
963
|
+
severity: 'error',
|
|
964
|
+
message: `Runtime path is not a directory: ${paths.runtimeRoot}`,
|
|
965
|
+
source: paths.runtimeRoot,
|
|
966
|
+
hint: 'Inspect or remove the path explicitly; doctor does not delete it.',
|
|
967
|
+
}),
|
|
968
|
+
],
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
const runtimeReal = await resolveRealpath(paths.runtimeRoot);
|
|
972
|
+
const manifestPath = join(paths.runtimeRoot, 'install-manifest.json');
|
|
973
|
+
const manifestReal = await resolveRealpath(manifestPath);
|
|
974
|
+
if (runtimeReal === null ||
|
|
975
|
+
(manifestReal !== null && !isPathInside(runtimeReal, manifestReal))) {
|
|
976
|
+
return {
|
|
977
|
+
version,
|
|
978
|
+
status: 'corrupt',
|
|
979
|
+
runtimeRoot: paths.runtimeRoot,
|
|
980
|
+
manifest: null,
|
|
981
|
+
journal: null,
|
|
982
|
+
lockPath: paths.lockPath,
|
|
983
|
+
lockPresent,
|
|
984
|
+
packageDigestMatch: false,
|
|
985
|
+
executableExists: false,
|
|
986
|
+
diagnostics: [
|
|
987
|
+
createDiagnostic({
|
|
988
|
+
code: diagnosticCodes.initApplyFailed,
|
|
989
|
+
severity: 'error',
|
|
990
|
+
message: 'Runtime root or manifest path escapes the runtime directory.',
|
|
991
|
+
source: manifestPath,
|
|
992
|
+
hint: 'Inspect the runtime; doctor does not read through symlinked state paths.',
|
|
993
|
+
}),
|
|
994
|
+
],
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
const manifestRead = await readJsonDocument(manifestPath, runtimeManifestSchema);
|
|
998
|
+
if (!manifestRead.exists || manifestRead.value === null) {
|
|
999
|
+
return {
|
|
1000
|
+
version,
|
|
1001
|
+
status: manifestRead.exists ? 'unreadable' : 'corrupt',
|
|
1002
|
+
runtimeRoot: paths.runtimeRoot,
|
|
1003
|
+
manifest: null,
|
|
1004
|
+
journal: null,
|
|
1005
|
+
lockPath: paths.lockPath,
|
|
1006
|
+
lockPresent,
|
|
1007
|
+
packageDigestMatch: false,
|
|
1008
|
+
executableExists: false,
|
|
1009
|
+
diagnostics: manifestRead.diagnostics.length > 0
|
|
1010
|
+
? manifestRead.diagnostics
|
|
1011
|
+
: [
|
|
1012
|
+
createDiagnostic({
|
|
1013
|
+
code: diagnosticCodes.initApplyFailed,
|
|
1014
|
+
severity: 'error',
|
|
1015
|
+
message: `Runtime manifest is missing: ${paths.runtimeRoot}`,
|
|
1016
|
+
source: paths.runtimeRoot,
|
|
1017
|
+
hint: 'Inspect the partial runtime; doctor does not overwrite it.',
|
|
1018
|
+
}),
|
|
1019
|
+
],
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
const manifest = manifestRead.value;
|
|
1023
|
+
if (manifest.runtimeVersion !== version) {
|
|
1024
|
+
return {
|
|
1025
|
+
version,
|
|
1026
|
+
status: 'version-mismatch',
|
|
1027
|
+
runtimeRoot: paths.runtimeRoot,
|
|
1028
|
+
manifest,
|
|
1029
|
+
journal: null,
|
|
1030
|
+
lockPath: paths.lockPath,
|
|
1031
|
+
lockPresent,
|
|
1032
|
+
packageDigestMatch: false,
|
|
1033
|
+
executableExists: false,
|
|
1034
|
+
diagnostics: [
|
|
1035
|
+
createDiagnostic({
|
|
1036
|
+
code: diagnosticCodes.initApplyFailed,
|
|
1037
|
+
severity: 'error',
|
|
1038
|
+
message: `Runtime manifest reports ${manifest.runtimeVersion}, expected ${version}.`,
|
|
1039
|
+
source: paths.runtimeRoot,
|
|
1040
|
+
hint: 'Inspect the runtime version and workspace target; doctor does not change either.',
|
|
1041
|
+
}),
|
|
1042
|
+
],
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
if (manifest.packageName !== 'pappra' ||
|
|
1046
|
+
manifest.runtimeRoot !== paths.runtimeRoot) {
|
|
1047
|
+
return {
|
|
1048
|
+
version,
|
|
1049
|
+
status: 'corrupt',
|
|
1050
|
+
runtimeRoot: paths.runtimeRoot,
|
|
1051
|
+
manifest,
|
|
1052
|
+
journal: null,
|
|
1053
|
+
lockPath: paths.lockPath,
|
|
1054
|
+
lockPresent,
|
|
1055
|
+
packageDigestMatch: false,
|
|
1056
|
+
executableExists: false,
|
|
1057
|
+
diagnostics: [
|
|
1058
|
+
createDiagnostic({
|
|
1059
|
+
code: diagnosticCodes.initApplyFailed,
|
|
1060
|
+
severity: 'error',
|
|
1061
|
+
message: 'Runtime manifest identity does not match the requested runtime.',
|
|
1062
|
+
source: paths.runtimeRoot,
|
|
1063
|
+
hint: 'Inspect the runtime; doctor does not adopt a mismatched manifest.',
|
|
1064
|
+
}),
|
|
1065
|
+
],
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
const packageRoot = join(paths.runtimeRoot, 'node_modules', 'pappra');
|
|
1069
|
+
const packageObservation = await observePath(packageRoot);
|
|
1070
|
+
const packageReal = packageObservation.kind === 'directory'
|
|
1071
|
+
? await resolveRealpath(packageRoot)
|
|
1072
|
+
: null;
|
|
1073
|
+
if (runtimeReal === null ||
|
|
1074
|
+
packageReal === null ||
|
|
1075
|
+
!isPathInside(runtimeReal, packageReal)) {
|
|
1076
|
+
return {
|
|
1077
|
+
version,
|
|
1078
|
+
status: 'corrupt',
|
|
1079
|
+
runtimeRoot: paths.runtimeRoot,
|
|
1080
|
+
manifest,
|
|
1081
|
+
journal: null,
|
|
1082
|
+
lockPath: paths.lockPath,
|
|
1083
|
+
lockPresent,
|
|
1084
|
+
packageDigestMatch: false,
|
|
1085
|
+
executableExists: false,
|
|
1086
|
+
diagnostics: [
|
|
1087
|
+
createDiagnostic({
|
|
1088
|
+
code: diagnosticCodes.initApplyFailed,
|
|
1089
|
+
severity: 'error',
|
|
1090
|
+
message: 'Runtime package path is missing or escapes the runtime directory.',
|
|
1091
|
+
source: packageRoot,
|
|
1092
|
+
hint: 'Inspect the runtime; doctor does not read through symlinked package paths.',
|
|
1093
|
+
}),
|
|
1094
|
+
],
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
const executableObservation = await observePath(manifest.executable);
|
|
1098
|
+
const executableReal = await resolveRealpath(manifest.executable);
|
|
1099
|
+
const executableInsideRuntime = executableReal !== null &&
|
|
1100
|
+
isPathInside(runtimeReal, executableReal) &&
|
|
1101
|
+
isPathInside(paths.runtimeRoot, manifest.executable);
|
|
1102
|
+
const packageManifest = await readPackageManifest(packageRoot, runtimeReal);
|
|
1103
|
+
const digest = await packageDigest(packageRoot).catch(() => null);
|
|
1104
|
+
const packageDigestMatch = digest !== null &&
|
|
1105
|
+
digest === manifest.packageDigest &&
|
|
1106
|
+
packageObservation.kind === 'directory' &&
|
|
1107
|
+
packageManifest !== null &&
|
|
1108
|
+
packageManifest.name === 'pappra' &&
|
|
1109
|
+
packageManifest.version === version;
|
|
1110
|
+
const executableExists = executableInsideRuntime && executableObservation.kind === 'file';
|
|
1111
|
+
if (!packageDigestMatch || !executableExists) {
|
|
1112
|
+
return {
|
|
1113
|
+
version,
|
|
1114
|
+
status: 'corrupt',
|
|
1115
|
+
runtimeRoot: paths.runtimeRoot,
|
|
1116
|
+
manifest,
|
|
1117
|
+
journal: null,
|
|
1118
|
+
lockPath: paths.lockPath,
|
|
1119
|
+
lockPresent,
|
|
1120
|
+
packageDigestMatch,
|
|
1121
|
+
executableExists,
|
|
1122
|
+
diagnostics: [
|
|
1123
|
+
createDiagnostic({
|
|
1124
|
+
code: diagnosticCodes.initApplyFailed,
|
|
1125
|
+
severity: 'error',
|
|
1126
|
+
message: 'Runtime content or executable no longer matches its manifest.',
|
|
1127
|
+
source: paths.runtimeRoot,
|
|
1128
|
+
hint: 'Inspect the runtime; doctor does not reinstall or overwrite it.',
|
|
1129
|
+
}),
|
|
1130
|
+
],
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
return {
|
|
1134
|
+
version,
|
|
1135
|
+
status: 'content-match',
|
|
1136
|
+
runtimeRoot: paths.runtimeRoot,
|
|
1137
|
+
manifest,
|
|
1138
|
+
journal: null,
|
|
1139
|
+
lockPath: paths.lockPath,
|
|
1140
|
+
lockPresent,
|
|
1141
|
+
packageDigestMatch,
|
|
1142
|
+
executableExists,
|
|
1143
|
+
diagnostics: [],
|
|
1144
|
+
};
|
|
1145
|
+
}
|