@phyzess/pappra 0.0.0-stage → 0.1.0-beta.1
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
package/dist/app/init.js
ADDED
|
@@ -0,0 +1,1111 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { mkdir, open, readFile, rm, rmdir } from 'node:fs/promises';
|
|
4
|
+
import { createDiagnostic, hasErrors, pointer, } from '../schemas/diagnostics.js';
|
|
5
|
+
import { diagnosticCodes } from '../schemas/errors.js';
|
|
6
|
+
import { initPlanSchema, parseInitChoices, } from '../schemas/init.js';
|
|
7
|
+
import { machineConfigSchema } from '../schemas/machine.js';
|
|
8
|
+
import { isExternalRepositoryPath, isPathInside, isPortableRelativePath, normalizePortableRelativePath, resolvePortableRelativePath, samePath, } from '../schemas/paths.js';
|
|
9
|
+
import { WORKSPACE_SCHEMA, validateWorkspaceConfigSemantics, workspaceConfigSchema, } from '../schemas/workspace.js';
|
|
10
|
+
import { getPackageInfo } from '../package-info.js';
|
|
11
|
+
import { observePath, resolveRealpath, sha256Digest } from '../infra/fs.js';
|
|
12
|
+
import { inspectMachineConfig, inspectWorkspaceConfig, } from '../workspace/config.js';
|
|
13
|
+
import { discoverWorkspace, } from '../workspace/discover.js';
|
|
14
|
+
import { resolveWorkspaceRoot } from '../workspace/resolve.js';
|
|
15
|
+
import { buildInitPlan } from '../install/plan.js';
|
|
16
|
+
import { beginManagedApply, completeManagedApply, failManagedApply, recordManagedFile, } from '../install/apply.js';
|
|
17
|
+
const workspaceConfigRelativePath = '.pappra/workspace.json';
|
|
18
|
+
const machineConfigRelativePath = '.pappra/local/machine.json';
|
|
19
|
+
function failure(status, code, message, diagnostics = [], nextAction = null) {
|
|
20
|
+
return { ok: false, status, code, message, diagnostics, nextAction };
|
|
21
|
+
}
|
|
22
|
+
function failureFromDiagnostics(status, diagnostics, fallbackCode, message) {
|
|
23
|
+
const first = diagnostics.find((entry) => entry.severity === 'error') ?? diagnostics[0];
|
|
24
|
+
return failure(status, first?.code ?? fallbackCode, first?.message ?? message, diagnostics, first?.hint ?? null);
|
|
25
|
+
}
|
|
26
|
+
function info(code, message, source, hint = null) {
|
|
27
|
+
return createDiagnostic({
|
|
28
|
+
code,
|
|
29
|
+
severity: 'info',
|
|
30
|
+
message,
|
|
31
|
+
source,
|
|
32
|
+
hint,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function jsonBytes(value) {
|
|
36
|
+
return Buffer.from(`${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
37
|
+
}
|
|
38
|
+
function dedupeDiagnostics(diagnostics) {
|
|
39
|
+
const seen = new Set();
|
|
40
|
+
const unique = [];
|
|
41
|
+
for (const entry of diagnostics) {
|
|
42
|
+
const key = [
|
|
43
|
+
entry.code,
|
|
44
|
+
entry.severity,
|
|
45
|
+
entry.source,
|
|
46
|
+
entry.pointer ?? '',
|
|
47
|
+
entry.message,
|
|
48
|
+
].join('\0');
|
|
49
|
+
if (seen.has(key))
|
|
50
|
+
continue;
|
|
51
|
+
seen.add(key);
|
|
52
|
+
unique.push(entry);
|
|
53
|
+
}
|
|
54
|
+
return unique;
|
|
55
|
+
}
|
|
56
|
+
function sanitizeIdentifier(value) {
|
|
57
|
+
const normalized = value
|
|
58
|
+
.toLowerCase()
|
|
59
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
60
|
+
.replace(/^[^a-z0-9]+/, '')
|
|
61
|
+
.replace(/[-.]+$/g, '')
|
|
62
|
+
.slice(0, 50);
|
|
63
|
+
return normalized === '' ? 'repo' : normalized;
|
|
64
|
+
}
|
|
65
|
+
function uniqueIdentifier(base, used) {
|
|
66
|
+
let candidate = base;
|
|
67
|
+
let index = 2;
|
|
68
|
+
while (used.has(candidate)) {
|
|
69
|
+
candidate = `${base}-${index}`;
|
|
70
|
+
index += 1;
|
|
71
|
+
}
|
|
72
|
+
used.add(candidate);
|
|
73
|
+
return candidate;
|
|
74
|
+
}
|
|
75
|
+
function repositoryKey(repository) {
|
|
76
|
+
if (repository.configuredPath !== null) {
|
|
77
|
+
return isExternalRepositoryPath(repository.configuredPath)
|
|
78
|
+
? repository.configuredPath
|
|
79
|
+
: normalizePortableRelativePath(repository.configuredPath);
|
|
80
|
+
}
|
|
81
|
+
return repository.relativePath === null
|
|
82
|
+
? repository.root
|
|
83
|
+
: normalizePortableRelativePath(repository.relativePath);
|
|
84
|
+
}
|
|
85
|
+
function candidateKey(value) {
|
|
86
|
+
return isExternalRepositoryPath(value)
|
|
87
|
+
? value
|
|
88
|
+
: normalizePortableRelativePath(value);
|
|
89
|
+
}
|
|
90
|
+
/** Resolves the explicit root and refuses to create a hidden nested workspace. */
|
|
91
|
+
export async function resolveInitRoot(rootPath) {
|
|
92
|
+
const requestedRoot = resolve(rootPath);
|
|
93
|
+
const resolution = await resolveWorkspaceRoot({ startPath: requestedRoot });
|
|
94
|
+
if (resolution.state === 'unavailable' || resolution.state === 'incomplete') {
|
|
95
|
+
return failureFromDiagnostics('unavailable', resolution.diagnostics, diagnosticCodes.workspaceStartInvalid, `Cannot initialize from ${requestedRoot}.`);
|
|
96
|
+
}
|
|
97
|
+
if (resolution.root !== null) {
|
|
98
|
+
const diagnostics = [...resolution.diagnostics];
|
|
99
|
+
if (!samePath(resolution.root, requestedRoot)) {
|
|
100
|
+
diagnostics.push(info(diagnosticCodes.initRegisteredRootUsed, `Using the nearest registered workspace root: ${resolution.root}`, resolution.root, 'Run init from the workspace root or select a different directory; Pappra does not create a nested workspace implicitly.'));
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
ok: true,
|
|
104
|
+
value: {
|
|
105
|
+
requestedRoot,
|
|
106
|
+
root: resolution.root,
|
|
107
|
+
registered: true,
|
|
108
|
+
diagnostics,
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (resolution.startDirectory === null) {
|
|
113
|
+
return failure('unavailable', diagnosticCodes.workspaceStartInvalid, `Cannot resolve initialization root ${requestedRoot}.`, resolution.diagnostics, 'Pass an existing readable directory.');
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
ok: true,
|
|
117
|
+
value: {
|
|
118
|
+
requestedRoot,
|
|
119
|
+
root: resolution.startDirectory,
|
|
120
|
+
registered: false,
|
|
121
|
+
diagnostics: resolution.diagnostics,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
async function observeRootIdentity(root) {
|
|
126
|
+
const observation = await observePath(root);
|
|
127
|
+
return observation.kind === 'directory' && observation.stats !== null
|
|
128
|
+
? { dev: observation.stats.dev, ino: observation.stats.ino }
|
|
129
|
+
: null;
|
|
130
|
+
}
|
|
131
|
+
function sameRootIdentity(left, right) {
|
|
132
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Loads the discovery inputs and rejects changed or invalid existing state.
|
|
136
|
+
* This function is read-only.
|
|
137
|
+
*/
|
|
138
|
+
export async function inspectInitContext(rootPath) {
|
|
139
|
+
const rootResult = await resolveInitRoot(rootPath);
|
|
140
|
+
if (!rootResult.ok)
|
|
141
|
+
return rootResult;
|
|
142
|
+
const root = rootResult.value.root;
|
|
143
|
+
const rootIdentity = await observeRootIdentity(root);
|
|
144
|
+
if (rootIdentity === null) {
|
|
145
|
+
return failure('unavailable', diagnosticCodes.workspaceRootInvalid, `Workspace root is no longer a directory: ${root}`, rootResult.value.diagnostics, 'Retry init from an existing workspace root; no plan was produced.');
|
|
146
|
+
}
|
|
147
|
+
const workspaceInspection = await inspectWorkspaceConfig(root, {
|
|
148
|
+
missing: 'info',
|
|
149
|
+
});
|
|
150
|
+
const machineInspection = await inspectMachineConfig(root);
|
|
151
|
+
const initialDiagnostics = [
|
|
152
|
+
...workspaceInspection.diagnostics,
|
|
153
|
+
...machineInspection.diagnostics,
|
|
154
|
+
];
|
|
155
|
+
if (workspaceInspection.exists && workspaceInspection.value === null) {
|
|
156
|
+
return failureFromDiagnostics('invalid', initialDiagnostics, diagnosticCodes.configSchemaInvalid, 'Existing workspace configuration is invalid.');
|
|
157
|
+
}
|
|
158
|
+
if (machineInspection.exists && machineInspection.value === null) {
|
|
159
|
+
return failureFromDiagnostics('invalid', initialDiagnostics, diagnosticCodes.configSchemaInvalid, 'Existing machine configuration is invalid.');
|
|
160
|
+
}
|
|
161
|
+
const discovery = await discoverWorkspace(root);
|
|
162
|
+
const finalWorkspace = await inspectWorkspaceConfig(root, {
|
|
163
|
+
missing: 'info',
|
|
164
|
+
});
|
|
165
|
+
const finalMachine = await inspectMachineConfig(root);
|
|
166
|
+
const finalRootIdentity = await observeRootIdentity(root);
|
|
167
|
+
const changedDuringDiscovery = workspaceInspection.digest !== finalWorkspace.digest ||
|
|
168
|
+
workspaceInspection.exists !== finalWorkspace.exists ||
|
|
169
|
+
machineInspection.digest !== finalMachine.digest ||
|
|
170
|
+
machineInspection.exists !== finalMachine.exists;
|
|
171
|
+
const finalDiagnostics = [...initialDiagnostics, ...discovery.diagnostics];
|
|
172
|
+
if (finalRootIdentity === null ||
|
|
173
|
+
!sameRootIdentity(rootIdentity, finalRootIdentity)) {
|
|
174
|
+
return failure('conflict', diagnosticCodes.initPlanStale, 'Workspace root directory was replaced while discovery was running.', finalDiagnostics, 'Retry init from the current workspace root; no plan was produced.');
|
|
175
|
+
}
|
|
176
|
+
if (changedDuringDiscovery) {
|
|
177
|
+
return failure('conflict', diagnosticCodes.initPlanStale, 'Workspace inputs changed while discovery was running.', finalDiagnostics, 'Retry init; no plan was produced.');
|
|
178
|
+
}
|
|
179
|
+
if ((workspaceInspection.value === null && discovery.configState === 'valid') ||
|
|
180
|
+
(workspaceInspection.value !== null && discovery.configState !== 'valid') ||
|
|
181
|
+
(machineInspection.value === null && discovery.machineState === 'valid') ||
|
|
182
|
+
(machineInspection.value !== null && discovery.machineState !== 'valid')) {
|
|
183
|
+
return failure('conflict', diagnosticCodes.initPlanStale, 'Workspace configuration changed while discovery was running.', finalDiagnostics, 'Retry init; no plan was produced.');
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
ok: true,
|
|
187
|
+
context: {
|
|
188
|
+
requestedRoot: rootResult.value.requestedRoot,
|
|
189
|
+
root,
|
|
190
|
+
registered: rootResult.value.registered,
|
|
191
|
+
rootIdentity,
|
|
192
|
+
workspaceInspection,
|
|
193
|
+
machineInspection,
|
|
194
|
+
discovery,
|
|
195
|
+
diagnostics: dedupeDiagnostics([
|
|
196
|
+
...rootResult.value.diagnostics,
|
|
197
|
+
...finalDiagnostics,
|
|
198
|
+
]),
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/** Lists discovered repository scope choices for a UI. */
|
|
203
|
+
export function listRepositoryCandidates(discovery) {
|
|
204
|
+
return discovery.repositories.map((repository) => ({
|
|
205
|
+
key: repositoryKey(repository),
|
|
206
|
+
id: repository.id,
|
|
207
|
+
external: repository.external,
|
|
208
|
+
access: repository.access,
|
|
209
|
+
root: repository.root,
|
|
210
|
+
kind: repository.kind,
|
|
211
|
+
dirty: repository.git.dirty,
|
|
212
|
+
configured: repository.origin === 'workspace-config',
|
|
213
|
+
}));
|
|
214
|
+
}
|
|
215
|
+
/** Lists knowledge source choices derived only from current discovery. */
|
|
216
|
+
export function listKnowledgeCandidates(discovery) {
|
|
217
|
+
const candidates = new Map();
|
|
218
|
+
for (const document of discovery.documents) {
|
|
219
|
+
if (document.workspaceRelativePath === null)
|
|
220
|
+
continue;
|
|
221
|
+
candidates.set(document.workspaceRelativePath, {
|
|
222
|
+
path: document.workspaceRelativePath,
|
|
223
|
+
kind: document.kind,
|
|
224
|
+
origin: document.origin,
|
|
225
|
+
repositoryId: document.repositoryId,
|
|
226
|
+
});
|
|
227
|
+
const segments = document.workspaceRelativePath.split('/');
|
|
228
|
+
for (let index = 1; index < segments.length; index += 1) {
|
|
229
|
+
const path = segments.slice(0, index).join('/');
|
|
230
|
+
if (path.startsWith('.'))
|
|
231
|
+
continue;
|
|
232
|
+
candidates.set(path, {
|
|
233
|
+
path,
|
|
234
|
+
kind: 'directory',
|
|
235
|
+
origin: document.origin,
|
|
236
|
+
repositoryId: document.repositoryId,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return [...candidates.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
241
|
+
}
|
|
242
|
+
function selectRepositories(choices, discovery, root) {
|
|
243
|
+
const diagnostics = [];
|
|
244
|
+
const byKey = new Map();
|
|
245
|
+
for (const repository of discovery.repositories) {
|
|
246
|
+
byKey.set(repositoryKey(repository), repository);
|
|
247
|
+
if (repository.relativePath !== null) {
|
|
248
|
+
byKey.set(normalizePortableRelativePath(repository.relativePath), repository);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const usedIds = new Set();
|
|
252
|
+
const selected = [];
|
|
253
|
+
const seenRepositories = new Set();
|
|
254
|
+
for (const [index, choice] of choices.repositories.entries()) {
|
|
255
|
+
const key = candidateKey(choice.path);
|
|
256
|
+
const repository = byKey.get(key);
|
|
257
|
+
if (repository === undefined) {
|
|
258
|
+
diagnostics.push(createDiagnostic({
|
|
259
|
+
code: diagnosticCodes.initRepositoryUnknown,
|
|
260
|
+
severity: 'error',
|
|
261
|
+
message: `Init selection references an undiscovered repository: ${choice.path}`,
|
|
262
|
+
source: 'init choices',
|
|
263
|
+
pointer: pointer('repositories', index, 'path'),
|
|
264
|
+
hint: 'Select a repository path reported by discovery; Pappra does not invent repository scopes.',
|
|
265
|
+
}));
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (seenRepositories.has(repository.root)) {
|
|
269
|
+
diagnostics.push(createDiagnostic({
|
|
270
|
+
code: diagnosticCodes.initRepositoryDuplicate,
|
|
271
|
+
severity: 'error',
|
|
272
|
+
message: `Repository selection repeats ${repository.root}.`,
|
|
273
|
+
source: 'init choices',
|
|
274
|
+
pointer: pointer('repositories', index, 'path'),
|
|
275
|
+
hint: 'Keep one selection per discovered checkout.',
|
|
276
|
+
}));
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
seenRepositories.add(repository.root);
|
|
280
|
+
const external = repository.external;
|
|
281
|
+
const derivedBase = repository.id ??
|
|
282
|
+
(repository.relativePath === null || repository.relativePath === '.'
|
|
283
|
+
? basename(root)
|
|
284
|
+
: basename(repository.relativePath));
|
|
285
|
+
const id = choice.id
|
|
286
|
+
? choice.id
|
|
287
|
+
: uniqueIdentifier(sanitizeIdentifier(derivedBase), usedIds);
|
|
288
|
+
if (choice.id !== undefined) {
|
|
289
|
+
if (usedIds.has(choice.id)) {
|
|
290
|
+
diagnostics.push(createDiagnostic({
|
|
291
|
+
code: diagnosticCodes.initRepositoryDuplicate,
|
|
292
|
+
severity: 'error',
|
|
293
|
+
message: `Repository id "${choice.id}" is selected more than once.`,
|
|
294
|
+
source: 'init choices',
|
|
295
|
+
pointer: pointer('repositories', index, 'id'),
|
|
296
|
+
hint: 'Use unique repository ids.',
|
|
297
|
+
}));
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
usedIds.add(choice.id);
|
|
301
|
+
}
|
|
302
|
+
const configPath = external
|
|
303
|
+
? repository.configuredPath
|
|
304
|
+
: (repository.relativePath ?? repository.configuredPath ?? '.');
|
|
305
|
+
if (configPath === null) {
|
|
306
|
+
diagnostics.push(createDiagnostic({
|
|
307
|
+
code: diagnosticCodes.initRepositoryUnknown,
|
|
308
|
+
severity: 'error',
|
|
309
|
+
message: `Repository ${repository.root} has no portable configuration path.`,
|
|
310
|
+
source: 'init choices',
|
|
311
|
+
pointer: pointer('repositories', index, 'path'),
|
|
312
|
+
hint: 'Declare the checkout explicitly before initialization.',
|
|
313
|
+
}));
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
selected.push({
|
|
317
|
+
repository,
|
|
318
|
+
id,
|
|
319
|
+
path: configPath,
|
|
320
|
+
access: choice.access,
|
|
321
|
+
originalId: repository.id,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
return { selected, diagnostics };
|
|
325
|
+
}
|
|
326
|
+
function selectKnowledgeSources(choices, discovery) {
|
|
327
|
+
const allowed = new Map(listKnowledgeCandidates(discovery).map((candidate) => [
|
|
328
|
+
candidateKey(candidate.path),
|
|
329
|
+
candidate.path,
|
|
330
|
+
]));
|
|
331
|
+
const diagnostics = [];
|
|
332
|
+
const sources = [];
|
|
333
|
+
const seen = new Set();
|
|
334
|
+
for (const [index, source] of choices.knowledge.sources.entries()) {
|
|
335
|
+
const key = candidateKey(source);
|
|
336
|
+
const canonical = allowed.get(key);
|
|
337
|
+
if (canonical === undefined) {
|
|
338
|
+
diagnostics.push(createDiagnostic({
|
|
339
|
+
code: diagnosticCodes.initKnowledgeUnknown,
|
|
340
|
+
severity: 'error',
|
|
341
|
+
message: `Init selection references an undiscovered knowledge source: ${source}`,
|
|
342
|
+
source: 'init choices',
|
|
343
|
+
pointer: pointer('knowledge', 'sources', index),
|
|
344
|
+
hint: 'Select a rule or document reported by discovery; Pappra does not copy or create documentation.',
|
|
345
|
+
}));
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
if (!seen.has(canonical)) {
|
|
349
|
+
seen.add(canonical);
|
|
350
|
+
sources.push(canonical);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return { sources, diagnostics };
|
|
354
|
+
}
|
|
355
|
+
function findSelectedRepositoryForCandidate(candidate, selected) {
|
|
356
|
+
for (const repository of selected) {
|
|
357
|
+
if (candidate.repositoryId !== null &&
|
|
358
|
+
(candidate.repositoryId === repository.originalId ||
|
|
359
|
+
candidate.repositoryId === repository.id)) {
|
|
360
|
+
return repository;
|
|
361
|
+
}
|
|
362
|
+
if (candidate.workingDirectory !== null &&
|
|
363
|
+
samePath(candidate.workingDirectory, repository.repository.root)) {
|
|
364
|
+
return repository;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
function selectValidationEntries(choices, discovery, selected) {
|
|
370
|
+
const byId = new Map(discovery.validationCandidates.map((candidate) => [
|
|
371
|
+
candidate.id,
|
|
372
|
+
candidate,
|
|
373
|
+
]));
|
|
374
|
+
const diagnostics = [];
|
|
375
|
+
const validation = [];
|
|
376
|
+
const usedIds = new Set();
|
|
377
|
+
const seen = new Set();
|
|
378
|
+
for (const [index, candidateId] of choices.validation.entries()) {
|
|
379
|
+
if (seen.has(candidateId))
|
|
380
|
+
continue;
|
|
381
|
+
seen.add(candidateId);
|
|
382
|
+
const candidate = byId.get(candidateId);
|
|
383
|
+
if (candidate === undefined) {
|
|
384
|
+
diagnostics.push(createDiagnostic({
|
|
385
|
+
code: diagnosticCodes.initValidationUnknown,
|
|
386
|
+
severity: 'error',
|
|
387
|
+
message: `Init selection references an undiscovered validation candidate: ${candidateId}`,
|
|
388
|
+
source: 'init choices',
|
|
389
|
+
pointer: pointer('validation', index),
|
|
390
|
+
hint: 'Select candidates reported by discovery; discovery does not prove that a command is runnable.',
|
|
391
|
+
}));
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
const repository = findSelectedRepositoryForCandidate(candidate, selected);
|
|
395
|
+
if (repository === null) {
|
|
396
|
+
diagnostics.push(createDiagnostic({
|
|
397
|
+
code: diagnosticCodes.initValidationUnbound,
|
|
398
|
+
severity: 'warning',
|
|
399
|
+
message: `Validation candidate "${candidateId}" is not bound to a selected repository.`,
|
|
400
|
+
source: 'init choices',
|
|
401
|
+
pointer: pointer('validation', index),
|
|
402
|
+
hint: 'Select its repository or omit this candidate; unbound commands are not written to workspace config.',
|
|
403
|
+
}));
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
const base = sanitizeIdentifier(candidate.id.replace(/^(?:workspace-config|package-script):/, ''));
|
|
407
|
+
validation.push({
|
|
408
|
+
id: uniqueIdentifier(base, usedIds),
|
|
409
|
+
repositoryId: repository.id,
|
|
410
|
+
executable: candidate.executable,
|
|
411
|
+
args: [...candidate.args],
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
return { validation, diagnostics };
|
|
415
|
+
}
|
|
416
|
+
function buildWorkspaceConfig(root, choices, discovery, runtimeVersion) {
|
|
417
|
+
const diagnostics = [];
|
|
418
|
+
const repositorySelection = selectRepositories(choices, discovery, root);
|
|
419
|
+
diagnostics.push(...repositorySelection.diagnostics);
|
|
420
|
+
const knowledgeSelection = selectKnowledgeSources(choices, discovery);
|
|
421
|
+
diagnostics.push(...knowledgeSelection.diagnostics);
|
|
422
|
+
const validationSelection = selectValidationEntries(choices, discovery, repositorySelection.selected);
|
|
423
|
+
diagnostics.push(...validationSelection.diagnostics);
|
|
424
|
+
if (hasErrors(diagnostics)) {
|
|
425
|
+
return {
|
|
426
|
+
config: null,
|
|
427
|
+
selected: repositorySelection.selected,
|
|
428
|
+
diagnostics,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
const name = choices.name?.trim() ?? '';
|
|
432
|
+
const config = {
|
|
433
|
+
schema: WORKSPACE_SCHEMA,
|
|
434
|
+
workspaceId: choices.workspaceId ?? randomUUID(),
|
|
435
|
+
name: name === '' ? basename(root) || 'workspace' : name,
|
|
436
|
+
runtimeVersion,
|
|
437
|
+
repositories: repositorySelection.selected.map((repository) => ({
|
|
438
|
+
id: repository.id,
|
|
439
|
+
path: repository.path,
|
|
440
|
+
access: repository.access,
|
|
441
|
+
})),
|
|
442
|
+
knowledge: {
|
|
443
|
+
sources: [...knowledgeSelection.sources],
|
|
444
|
+
newDocuments: choices.knowledge.newDocuments,
|
|
445
|
+
taskVisibility: choices.knowledge.taskVisibility,
|
|
446
|
+
},
|
|
447
|
+
code: choices.code,
|
|
448
|
+
validation: [...validationSelection.validation],
|
|
449
|
+
excludes: [...new Set(choices.excludes)],
|
|
450
|
+
extensions: {},
|
|
451
|
+
};
|
|
452
|
+
const parsed = workspaceConfigSchema.safeParse(config);
|
|
453
|
+
if (!parsed.success) {
|
|
454
|
+
for (const issue of parsed.error.issues) {
|
|
455
|
+
diagnostics.push(createDiagnostic({
|
|
456
|
+
code: diagnosticCodes.initInvalidInput,
|
|
457
|
+
severity: 'error',
|
|
458
|
+
message: issue.message,
|
|
459
|
+
source: 'effective workspace config',
|
|
460
|
+
pointer: issue.path.length === 0
|
|
461
|
+
? null
|
|
462
|
+
: pointer(...issue.path.map((item) => String(item))),
|
|
463
|
+
hint: 'Correct the init selections; Pappra does not coerce invalid configuration.',
|
|
464
|
+
}));
|
|
465
|
+
}
|
|
466
|
+
return {
|
|
467
|
+
config: null,
|
|
468
|
+
selected: repositorySelection.selected,
|
|
469
|
+
diagnostics,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
const semantic = validateWorkspaceConfigSemantics(parsed.data, {
|
|
473
|
+
configPath: join(root, workspaceConfigRelativePath),
|
|
474
|
+
});
|
|
475
|
+
diagnostics.push(...semantic);
|
|
476
|
+
if (hasErrors(semantic)) {
|
|
477
|
+
return {
|
|
478
|
+
config: null,
|
|
479
|
+
selected: repositorySelection.selected,
|
|
480
|
+
diagnostics,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
return {
|
|
484
|
+
config: parsed.data,
|
|
485
|
+
selected: repositorySelection.selected,
|
|
486
|
+
diagnostics,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
function scopeRepositories(config, discovery) {
|
|
490
|
+
const byConfiguredPath = new Map(discovery.repositories.map((repository) => [
|
|
491
|
+
repository.configuredPath,
|
|
492
|
+
repository,
|
|
493
|
+
]));
|
|
494
|
+
return config.repositories.map((repository) => {
|
|
495
|
+
const discovered = byConfiguredPath.get(repository.path);
|
|
496
|
+
return {
|
|
497
|
+
id: repository.id,
|
|
498
|
+
path: repository.path,
|
|
499
|
+
access: repository.access,
|
|
500
|
+
external: isExternalRepositoryPath(repository.path),
|
|
501
|
+
root: discovered?.root ?? null,
|
|
502
|
+
kind: discovered?.kind ?? null,
|
|
503
|
+
dirty: discovered?.git.dirty ?? null,
|
|
504
|
+
declaredBy: 'existing',
|
|
505
|
+
};
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
function planFilesFor(config, machine, needWorkspaceConfig, needMachineConfig) {
|
|
509
|
+
const workspaceBytes = needWorkspaceConfig ? jsonBytes(config) : null;
|
|
510
|
+
const machineBytes = needMachineConfig ? jsonBytes(machine) : null;
|
|
511
|
+
const files = [];
|
|
512
|
+
if (workspaceBytes !== null) {
|
|
513
|
+
files.push({
|
|
514
|
+
path: workspaceConfigRelativePath,
|
|
515
|
+
bytes: workspaceBytes,
|
|
516
|
+
beforeText: null,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
if (machineBytes !== null) {
|
|
520
|
+
files.push({
|
|
521
|
+
path: machineConfigRelativePath,
|
|
522
|
+
bytes: machineBytes,
|
|
523
|
+
beforeText: null,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
return { files, workspaceBytes, machineBytes };
|
|
527
|
+
}
|
|
528
|
+
async function pendingRuntimeActions(config, machine) {
|
|
529
|
+
if (machine.runtime !== null) {
|
|
530
|
+
const executable = await observePath(machine.runtime.executable);
|
|
531
|
+
if (machine.runtime.version === config.runtimeVersion &&
|
|
532
|
+
executable.kind === 'file') {
|
|
533
|
+
return [];
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return [
|
|
537
|
+
{
|
|
538
|
+
kind: 'install-runtime',
|
|
539
|
+
target: 'pappra',
|
|
540
|
+
version: config.runtimeVersion,
|
|
541
|
+
},
|
|
542
|
+
];
|
|
543
|
+
}
|
|
544
|
+
function existingMachineForExistingConfig(root, config, choices, discovery) {
|
|
545
|
+
if (choices === null || choices === undefined) {
|
|
546
|
+
return {
|
|
547
|
+
machine: null,
|
|
548
|
+
diagnostics: [
|
|
549
|
+
createDiagnostic({
|
|
550
|
+
code: diagnosticCodes.initInputRequired,
|
|
551
|
+
severity: 'error',
|
|
552
|
+
message: 'Host selection is required to create local machine.json.',
|
|
553
|
+
source: 'init choices',
|
|
554
|
+
hint: 'Provide --config with hosts, or run the interactive wizard.',
|
|
555
|
+
}),
|
|
556
|
+
],
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
const externalRepositoryUnknown = config.repositories.some((repository) => isExternalRepositoryPath(repository.path));
|
|
560
|
+
if (externalRepositoryUnknown) {
|
|
561
|
+
return {
|
|
562
|
+
machine: null,
|
|
563
|
+
diagnostics: [
|
|
564
|
+
createDiagnostic({
|
|
565
|
+
code: diagnosticCodes.initExternalMappingRequired,
|
|
566
|
+
severity: 'error',
|
|
567
|
+
message: 'External repository mappings are missing, so machine.json cannot be completed safely.',
|
|
568
|
+
source: join(root, workspaceConfigRelativePath),
|
|
569
|
+
hint: 'Create or repair .pappra/local/machine.json explicitly; Pappra does not guess absolute external paths.',
|
|
570
|
+
}),
|
|
571
|
+
...discovery.unresolvedRepositories.flatMap((repository) => repository.diagnostics),
|
|
572
|
+
],
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
const machine = {
|
|
576
|
+
schema: 'pappra/machine/1',
|
|
577
|
+
workspaceRealpath: root,
|
|
578
|
+
checkoutId: randomUUID(),
|
|
579
|
+
externalRepositories: [],
|
|
580
|
+
selectedHosts: [...new Set(choices.hosts)],
|
|
581
|
+
runtime: null,
|
|
582
|
+
};
|
|
583
|
+
const parsed = machineConfigSchema.safeParse(machine);
|
|
584
|
+
if (!parsed.success) {
|
|
585
|
+
return {
|
|
586
|
+
machine: null,
|
|
587
|
+
diagnostics: [
|
|
588
|
+
createDiagnostic({
|
|
589
|
+
code: diagnosticCodes.initInvalidInput,
|
|
590
|
+
severity: 'error',
|
|
591
|
+
message: parsed.error.issues[0]?.message ??
|
|
592
|
+
'Generated machine configuration is invalid.',
|
|
593
|
+
source: join(root, machineConfigRelativePath),
|
|
594
|
+
hint: 'Correct the host selection.',
|
|
595
|
+
}),
|
|
596
|
+
],
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
return { machine: parsed.data, diagnostics: [] };
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Produces a strict plan and exact in-memory writes. No target file is created
|
|
603
|
+
* or modified by this function.
|
|
604
|
+
*/
|
|
605
|
+
export async function planWorkspaceInit(options) {
|
|
606
|
+
const contextResult = options.context === undefined
|
|
607
|
+
? await inspectInitContext(options.root)
|
|
608
|
+
: { ok: true, context: options.context };
|
|
609
|
+
if (!contextResult.ok)
|
|
610
|
+
return contextResult;
|
|
611
|
+
const context = contextResult.context;
|
|
612
|
+
const contextRootIdentity = await observeRootIdentity(context.root);
|
|
613
|
+
if (contextRootIdentity === null ||
|
|
614
|
+
!sameRootIdentity(context.rootIdentity, contextRootIdentity)) {
|
|
615
|
+
return failure('conflict', diagnosticCodes.initPlanStale, 'Workspace root directory was replaced after discovery.', context.diagnostics, 'Retry init from the current workspace root; no plan was produced.');
|
|
616
|
+
}
|
|
617
|
+
let choices = options.choices ?? null;
|
|
618
|
+
if (choices !== null) {
|
|
619
|
+
const parsedChoices = parseInitChoices(choices, 'init choices');
|
|
620
|
+
if (parsedChoices.value === null) {
|
|
621
|
+
return failureFromDiagnostics('invalid', parsedChoices.diagnostics, diagnosticCodes.initInvalidInput, 'Init selections are invalid.');
|
|
622
|
+
}
|
|
623
|
+
choices = parsedChoices.value;
|
|
624
|
+
}
|
|
625
|
+
const existingConfig = context.workspaceInspection.value;
|
|
626
|
+
const existingMachine = context.machineInspection.value;
|
|
627
|
+
const diagnostics = [...context.diagnostics];
|
|
628
|
+
const runtimeVersion = options.runtimeVersion ??
|
|
629
|
+
existingConfig?.runtimeVersion ??
|
|
630
|
+
getPackageInfo().version;
|
|
631
|
+
let config;
|
|
632
|
+
let machine;
|
|
633
|
+
let needWorkspaceConfig = false;
|
|
634
|
+
let needMachineConfig = false;
|
|
635
|
+
let selected = [];
|
|
636
|
+
if (existingConfig !== null) {
|
|
637
|
+
config = existingConfig;
|
|
638
|
+
if (choices !== null) {
|
|
639
|
+
diagnostics.push(info(diagnosticCodes.initExistingConfigReused, 'Existing valid workspace configuration is reused; init selections do not overwrite it.', context.workspaceInspection.path, 'Use a future configure flow to change existing workspace policy.'));
|
|
640
|
+
}
|
|
641
|
+
if (existingMachine !== null) {
|
|
642
|
+
machine = existingMachine;
|
|
643
|
+
}
|
|
644
|
+
else {
|
|
645
|
+
const machineResult = existingMachineForExistingConfig(context.root, existingConfig, choices, context.discovery);
|
|
646
|
+
if (machineResult.machine === null) {
|
|
647
|
+
return failureFromDiagnostics('needs_input', [...diagnostics, ...machineResult.diagnostics], diagnosticCodes.initInputRequired, 'More init input is required.');
|
|
648
|
+
}
|
|
649
|
+
machine = machineResult.machine;
|
|
650
|
+
diagnostics.push(...machineResult.diagnostics);
|
|
651
|
+
needMachineConfig = true;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
else {
|
|
655
|
+
if (choices === null) {
|
|
656
|
+
return failure('needs_input', diagnosticCodes.initInputRequired, 'Workspace configuration is missing; init selections are required.', diagnostics, 'Provide --config with pappra/init-choices/1 selections, or run the interactive wizard.');
|
|
657
|
+
}
|
|
658
|
+
const built = buildWorkspaceConfig(context.root, choices, context.discovery, runtimeVersion);
|
|
659
|
+
diagnostics.push(...built.diagnostics);
|
|
660
|
+
if (built.config === null) {
|
|
661
|
+
return failureFromDiagnostics('invalid', diagnostics, diagnosticCodes.initInvalidInput, 'Init selections do not form a valid workspace configuration.');
|
|
662
|
+
}
|
|
663
|
+
config = built.config;
|
|
664
|
+
selected = built.selected;
|
|
665
|
+
needWorkspaceConfig = true;
|
|
666
|
+
if (existingMachine !== null) {
|
|
667
|
+
machine = existingMachine;
|
|
668
|
+
diagnostics.push(info(diagnosticCodes.initExistingConfigReused, 'Existing valid machine.json is reused while workspace.json is created.', context.machineInspection.path, 'Use a future configure flow to change local host or external repository mappings.'));
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
needMachineConfig = true;
|
|
672
|
+
machine = {
|
|
673
|
+
schema: 'pappra/machine/1',
|
|
674
|
+
workspaceRealpath: context.root,
|
|
675
|
+
checkoutId: randomUUID(),
|
|
676
|
+
externalRepositories: [],
|
|
677
|
+
selectedHosts: [...new Set(choices.hosts)],
|
|
678
|
+
runtime: null,
|
|
679
|
+
};
|
|
680
|
+
const parsedMachine = machineConfigSchema.safeParse(machine);
|
|
681
|
+
if (!parsedMachine.success) {
|
|
682
|
+
return failure('invalid', diagnosticCodes.initInvalidInput, parsedMachine.error.issues[0]?.message ??
|
|
683
|
+
'Generated machine configuration is invalid.', diagnostics, 'Correct the host selection.');
|
|
684
|
+
}
|
|
685
|
+
machine = parsedMachine.data;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
const filePlan = planFilesFor(config, machine, needWorkspaceConfig, needMachineConfig);
|
|
689
|
+
const configurationDigest = filePlan.workspaceBytes !== null
|
|
690
|
+
? sha256Digest(filePlan.workspaceBytes)
|
|
691
|
+
: (context.workspaceInspection.digest ??
|
|
692
|
+
sha256Digest(JSON.stringify(config)));
|
|
693
|
+
const externalActions = await pendingRuntimeActions(config, machine);
|
|
694
|
+
const built = buildInitPlan({
|
|
695
|
+
workspaceRoot: context.root,
|
|
696
|
+
configurationDigest,
|
|
697
|
+
files: filePlan.files,
|
|
698
|
+
externalActions,
|
|
699
|
+
...(options.planId === undefined ? {} : { planId: options.planId }),
|
|
700
|
+
...(options.discoveredAt === undefined
|
|
701
|
+
? {}
|
|
702
|
+
: { discoveredAt: options.discoveredAt }),
|
|
703
|
+
});
|
|
704
|
+
const finalWorkspace = await inspectWorkspaceConfig(context.root, {
|
|
705
|
+
missing: 'info',
|
|
706
|
+
});
|
|
707
|
+
const finalMachine = await inspectMachineConfig(context.root);
|
|
708
|
+
if (context.workspaceInspection.digest !== finalWorkspace.digest ||
|
|
709
|
+
context.workspaceInspection.exists !== finalWorkspace.exists ||
|
|
710
|
+
context.machineInspection.digest !== finalMachine.digest ||
|
|
711
|
+
context.machineInspection.exists !== finalMachine.exists) {
|
|
712
|
+
return failure('conflict', diagnosticCodes.initPlanStale, 'Workspace inputs changed after planning; the plan is stale.', diagnostics, 'Retry init; no target files were written.');
|
|
713
|
+
}
|
|
714
|
+
const finalRootIdentity = await observeRootIdentity(context.root);
|
|
715
|
+
if (finalRootIdentity === null ||
|
|
716
|
+
!sameRootIdentity(context.rootIdentity, finalRootIdentity)) {
|
|
717
|
+
return failure('conflict', diagnosticCodes.initPlanStale, 'Workspace root directory changed while planning.', diagnostics, 'Retry init from the current workspace root; no target files were written.');
|
|
718
|
+
}
|
|
719
|
+
const rootIdentity = context.rootIdentity;
|
|
720
|
+
const scopeRepos = existingConfig !== null
|
|
721
|
+
? scopeRepositories(config, context.discovery)
|
|
722
|
+
: selected.map((repository) => ({
|
|
723
|
+
id: repository.id,
|
|
724
|
+
path: repository.path,
|
|
725
|
+
access: repository.access,
|
|
726
|
+
external: repository.repository.external,
|
|
727
|
+
root: repository.repository.root,
|
|
728
|
+
kind: repository.repository.kind,
|
|
729
|
+
dirty: repository.repository.git.dirty,
|
|
730
|
+
declaredBy: 'planned',
|
|
731
|
+
}));
|
|
732
|
+
const scope = {
|
|
733
|
+
workspaceRoot: context.root,
|
|
734
|
+
workspaceId: config.workspaceId,
|
|
735
|
+
name: config.name,
|
|
736
|
+
runtimeVersion: config.runtimeVersion,
|
|
737
|
+
configSource: existingConfig !== null ? 'existing' : 'planned',
|
|
738
|
+
repositories: scopeRepos,
|
|
739
|
+
knowledge: config.knowledge,
|
|
740
|
+
code: config.code,
|
|
741
|
+
validation: config.validation,
|
|
742
|
+
excludes: config.excludes,
|
|
743
|
+
hosts: machine.selectedHosts,
|
|
744
|
+
boundaries: context.discovery.boundaries,
|
|
745
|
+
};
|
|
746
|
+
return {
|
|
747
|
+
ok: true,
|
|
748
|
+
bundle: {
|
|
749
|
+
plan: built.plan,
|
|
750
|
+
scope,
|
|
751
|
+
writes: built.writes,
|
|
752
|
+
diagnostics: dedupeDiagnostics(diagnostics),
|
|
753
|
+
rootIdentity,
|
|
754
|
+
},
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
function planDiagnostics(message, code) {
|
|
758
|
+
return {
|
|
759
|
+
ok: false,
|
|
760
|
+
status: 'stale',
|
|
761
|
+
appliedFiles: [],
|
|
762
|
+
pendingExternalActions: [],
|
|
763
|
+
residualPaths: [],
|
|
764
|
+
diagnostics: [
|
|
765
|
+
createDiagnostic({
|
|
766
|
+
code,
|
|
767
|
+
severity: 'error',
|
|
768
|
+
message,
|
|
769
|
+
source: 'init apply',
|
|
770
|
+
hint: 'Retry init to produce a fresh plan; no target files were written.',
|
|
771
|
+
}),
|
|
772
|
+
],
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
function applyFailure(status, code, message, hint) {
|
|
776
|
+
return {
|
|
777
|
+
ok: false,
|
|
778
|
+
status,
|
|
779
|
+
appliedFiles: [],
|
|
780
|
+
pendingExternalActions: [],
|
|
781
|
+
residualPaths: [],
|
|
782
|
+
diagnostics: [
|
|
783
|
+
createDiagnostic({
|
|
784
|
+
code,
|
|
785
|
+
severity: 'error',
|
|
786
|
+
message,
|
|
787
|
+
source: 'init apply',
|
|
788
|
+
hint,
|
|
789
|
+
}),
|
|
790
|
+
],
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
async function safeTarget(root, relativePath) {
|
|
794
|
+
if (!isPortableRelativePath(relativePath) || relativePath === '.') {
|
|
795
|
+
return null;
|
|
796
|
+
}
|
|
797
|
+
const absolutePath = resolvePortableRelativePath(root, relativePath);
|
|
798
|
+
if (!isPathInside(root, absolutePath) || samePath(root, absolutePath)) {
|
|
799
|
+
return null;
|
|
800
|
+
}
|
|
801
|
+
let ancestor = dirname(absolutePath);
|
|
802
|
+
for (;;) {
|
|
803
|
+
const observation = await observePath(ancestor);
|
|
804
|
+
if (observation.kind !== 'missing') {
|
|
805
|
+
if (observation.kind !== 'directory')
|
|
806
|
+
return null;
|
|
807
|
+
const real = await resolveRealpath(ancestor);
|
|
808
|
+
if (real === null || !isPathInside(root, real))
|
|
809
|
+
return null;
|
|
810
|
+
return { absolutePath, existingAncestor: real };
|
|
811
|
+
}
|
|
812
|
+
if (samePath(ancestor, root))
|
|
813
|
+
return null;
|
|
814
|
+
const parent = dirname(ancestor);
|
|
815
|
+
if (samePath(parent, ancestor))
|
|
816
|
+
return null;
|
|
817
|
+
ancestor = parent;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
async function verifyBundleWrites(bundle) {
|
|
821
|
+
const parsed = initPlanSchema.safeParse(bundle.plan);
|
|
822
|
+
if (!parsed.success) {
|
|
823
|
+
return [
|
|
824
|
+
createDiagnostic({
|
|
825
|
+
code: diagnosticCodes.initPlanInvalid,
|
|
826
|
+
severity: 'error',
|
|
827
|
+
message: `Init plan does not match pappra/init-plan/1: ${parsed.error.issues[0]?.message ?? 'invalid plan'}`,
|
|
828
|
+
source: 'init plan',
|
|
829
|
+
hint: 'Regenerate the plan; Pappra does not apply malformed plans.',
|
|
830
|
+
}),
|
|
831
|
+
];
|
|
832
|
+
}
|
|
833
|
+
if (bundle.writes.length !== bundle.plan.files.length) {
|
|
834
|
+
return [
|
|
835
|
+
createDiagnostic({
|
|
836
|
+
code: diagnosticCodes.initPlanInvalid,
|
|
837
|
+
severity: 'error',
|
|
838
|
+
message: 'Init plan writes do not match planned file entries.',
|
|
839
|
+
source: 'init plan',
|
|
840
|
+
hint: 'Regenerate the plan; Pappra does not apply partial in-memory content.',
|
|
841
|
+
}),
|
|
842
|
+
];
|
|
843
|
+
}
|
|
844
|
+
const diagnostics = [];
|
|
845
|
+
for (const [index, file] of bundle.plan.files.entries()) {
|
|
846
|
+
const write = bundle.writes[index];
|
|
847
|
+
if (write === undefined || write.path !== file.path) {
|
|
848
|
+
diagnostics.push(createDiagnostic({
|
|
849
|
+
code: diagnosticCodes.initPlanInvalid,
|
|
850
|
+
severity: 'error',
|
|
851
|
+
message: `Init plan write ${index} does not match its planned path.`,
|
|
852
|
+
source: 'init plan',
|
|
853
|
+
hint: 'Regenerate the plan.',
|
|
854
|
+
}));
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
857
|
+
if (write.operation !== file.operation) {
|
|
858
|
+
diagnostics.push(createDiagnostic({
|
|
859
|
+
code: diagnosticCodes.initPlanInvalid,
|
|
860
|
+
severity: 'error',
|
|
861
|
+
message: `Planned operation for ${file.path} does not match its write.`,
|
|
862
|
+
source: 'init plan',
|
|
863
|
+
hint: 'Regenerate the plan.',
|
|
864
|
+
}));
|
|
865
|
+
}
|
|
866
|
+
if (file.operation !== 'create') {
|
|
867
|
+
diagnostics.push(createDiagnostic({
|
|
868
|
+
code: diagnosticCodes.initApplyUnsupported,
|
|
869
|
+
severity: 'error',
|
|
870
|
+
message: `P03 apply supports only create operations; ${file.path} is ${file.operation}.`,
|
|
871
|
+
source: 'init plan',
|
|
872
|
+
hint: 'P04 will implement managed replacement with a journal and rollback.',
|
|
873
|
+
}));
|
|
874
|
+
}
|
|
875
|
+
if (sha256Digest(write.bytes) !== file.nextDigest) {
|
|
876
|
+
diagnostics.push(createDiagnostic({
|
|
877
|
+
code: diagnosticCodes.initPlanInvalid,
|
|
878
|
+
severity: 'error',
|
|
879
|
+
message: `Planned bytes for ${file.path} do not match nextDigest.`,
|
|
880
|
+
source: 'init plan',
|
|
881
|
+
hint: 'Regenerate the plan from the same selections.',
|
|
882
|
+
}));
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
return diagnostics;
|
|
886
|
+
}
|
|
887
|
+
async function cleanupCreatedFiles(root, created) {
|
|
888
|
+
const residual = [];
|
|
889
|
+
for (const record of [...created].reverse()) {
|
|
890
|
+
const absolute = resolvePortableRelativePath(root, record.path);
|
|
891
|
+
try {
|
|
892
|
+
const observation = await observePath(absolute);
|
|
893
|
+
if (observation.kind === 'missing')
|
|
894
|
+
continue;
|
|
895
|
+
if (observation.kind !== 'file' ||
|
|
896
|
+
observation.stats === null ||
|
|
897
|
+
observation.stats.dev !== record.dev ||
|
|
898
|
+
observation.stats.ino !== record.ino) {
|
|
899
|
+
residual.push(record.path);
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
const written = await readFile(absolute);
|
|
903
|
+
if (sha256Digest(written) !== record.digest) {
|
|
904
|
+
residual.push(record.path);
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
await rm(absolute, { force: false });
|
|
908
|
+
}
|
|
909
|
+
catch {
|
|
910
|
+
residual.push(record.path);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
return residual;
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Applies only create operations from a plan, after revalidating root,
|
|
917
|
+
* configuration digest, target absence and exact bytes. External actions are
|
|
918
|
+
* reported as pending and never executed here.
|
|
919
|
+
*/
|
|
920
|
+
export async function applyInitPlan(bundle, options = {}) {
|
|
921
|
+
const diagnostics = await verifyBundleWrites(bundle);
|
|
922
|
+
if (diagnostics.length > 0) {
|
|
923
|
+
return {
|
|
924
|
+
ok: false,
|
|
925
|
+
status: diagnostics.some((entry) => entry.code === diagnosticCodes.initApplyUnsupported)
|
|
926
|
+
? 'unsupported'
|
|
927
|
+
: 'stale',
|
|
928
|
+
appliedFiles: [],
|
|
929
|
+
pendingExternalActions: [],
|
|
930
|
+
residualPaths: [],
|
|
931
|
+
diagnostics,
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
const root = await resolveRealpath(bundle.plan.workspaceRoot);
|
|
935
|
+
const rootObservation = await observePath(bundle.plan.workspaceRoot);
|
|
936
|
+
if (root === null ||
|
|
937
|
+
!samePath(root, bundle.plan.workspaceRoot) ||
|
|
938
|
+
rootObservation.kind !== 'directory' ||
|
|
939
|
+
rootObservation.stats === null ||
|
|
940
|
+
rootObservation.stats.dev !== bundle.rootIdentity.dev ||
|
|
941
|
+
rootObservation.stats.ino !== bundle.rootIdentity.ino) {
|
|
942
|
+
return applyFailure('stale', diagnosticCodes.initPlanStale, 'Workspace root identity changed since planning.', 'Retry init from the current workspace root; no files were written.');
|
|
943
|
+
}
|
|
944
|
+
const workspaceInspection = await inspectWorkspaceConfig(root, {
|
|
945
|
+
missing: 'info',
|
|
946
|
+
});
|
|
947
|
+
const workspaceFilePlanned = bundle.plan.files.some((file) => file.path === workspaceConfigRelativePath);
|
|
948
|
+
if (workspaceFilePlanned) {
|
|
949
|
+
if (workspaceInspection.exists) {
|
|
950
|
+
return applyFailure('conflict', diagnosticCodes.initTargetExists, 'Workspace configuration appeared after planning.', 'Remove or relocate the unexpected file, then retry init.');
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
else if (!workspaceInspection.exists ||
|
|
954
|
+
workspaceInspection.value === null ||
|
|
955
|
+
workspaceInspection.digest !== bundle.plan.configurationDigest) {
|
|
956
|
+
return planDiagnostics('Workspace configuration changed since planning.', diagnosticCodes.initPlanStale);
|
|
957
|
+
}
|
|
958
|
+
for (const file of bundle.plan.files) {
|
|
959
|
+
const target = await safeTarget(root, file.path);
|
|
960
|
+
if (target === null) {
|
|
961
|
+
return applyFailure('conflict', diagnosticCodes.initTargetUnsafe, `Planned target is not a safe workspace-relative path: ${file.path}`, 'Regenerate the plan; Pappra does not follow symlinked parents outside the workspace.');
|
|
962
|
+
}
|
|
963
|
+
const observation = await observePath(target.absolutePath);
|
|
964
|
+
if (observation.kind !== 'missing') {
|
|
965
|
+
return applyFailure('conflict', diagnosticCodes.initTargetExists, `Planned target already exists: ${file.path}`, 'Remove or relocate the unexpected file, then retry init.');
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
const managedStart = await beginManagedApply(root, bundle.plan, bundle.writes, bundle.rootIdentity);
|
|
969
|
+
if (!managedStart.ok) {
|
|
970
|
+
const status = managedStart.status === 'invalid'
|
|
971
|
+
? 'stale'
|
|
972
|
+
: managedStart.status === 'locked'
|
|
973
|
+
? 'conflict'
|
|
974
|
+
: 'failed';
|
|
975
|
+
return {
|
|
976
|
+
ok: false,
|
|
977
|
+
status,
|
|
978
|
+
appliedFiles: [],
|
|
979
|
+
pendingExternalActions: [],
|
|
980
|
+
residualPaths: managedStart.residualPaths,
|
|
981
|
+
diagnostics: managedStart.diagnostics,
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
const managed = managedStart.context;
|
|
985
|
+
let created = [];
|
|
986
|
+
let createdDirectories = [];
|
|
987
|
+
const ensureDirectory = async (directory) => {
|
|
988
|
+
const missing = [];
|
|
989
|
+
let current = directory;
|
|
990
|
+
for (;;) {
|
|
991
|
+
const observation = await observePath(current);
|
|
992
|
+
if (observation.kind !== 'missing') {
|
|
993
|
+
if (observation.kind !== 'directory') {
|
|
994
|
+
throw new Error(`Parent path is not a directory: ${current}`);
|
|
995
|
+
}
|
|
996
|
+
break;
|
|
997
|
+
}
|
|
998
|
+
missing.push(current);
|
|
999
|
+
if (samePath(current, root))
|
|
1000
|
+
break;
|
|
1001
|
+
const parent = dirname(current);
|
|
1002
|
+
if (samePath(parent, current))
|
|
1003
|
+
break;
|
|
1004
|
+
current = parent;
|
|
1005
|
+
}
|
|
1006
|
+
for (const path of missing.reverse()) {
|
|
1007
|
+
await mkdir(path);
|
|
1008
|
+
createdDirectories.push(path);
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
const rootBeforeWrite = await observePath(root);
|
|
1012
|
+
if (rootBeforeWrite.kind !== 'directory' ||
|
|
1013
|
+
rootBeforeWrite.stats === null ||
|
|
1014
|
+
rootBeforeWrite.stats.dev !== bundle.rootIdentity.dev ||
|
|
1015
|
+
rootBeforeWrite.stats.ino !== bundle.rootIdentity.ino) {
|
|
1016
|
+
return applyFailure('stale', diagnosticCodes.initPlanStale, 'Workspace root identity changed before writing.', 'Retry init from the current workspace root; no files were written.');
|
|
1017
|
+
}
|
|
1018
|
+
try {
|
|
1019
|
+
for (const [index, file] of bundle.plan.files.entries()) {
|
|
1020
|
+
const write = bundle.writes[index];
|
|
1021
|
+
if (write === undefined) {
|
|
1022
|
+
throw new Error(`Missing planned bytes for ${file.path}.`);
|
|
1023
|
+
}
|
|
1024
|
+
const target = await safeTarget(root, file.path);
|
|
1025
|
+
if (target === null) {
|
|
1026
|
+
throw new Error(`Unsafe target: ${file.path}`);
|
|
1027
|
+
}
|
|
1028
|
+
await options.beforeWrite?.(file.path, index);
|
|
1029
|
+
await ensureDirectory(dirname(target.absolutePath));
|
|
1030
|
+
const handle = await open(target.absolutePath, 'wx');
|
|
1031
|
+
const record = {
|
|
1032
|
+
path: file.path,
|
|
1033
|
+
digest: file.nextDigest,
|
|
1034
|
+
dev: -1,
|
|
1035
|
+
ino: -1,
|
|
1036
|
+
};
|
|
1037
|
+
created.push(record);
|
|
1038
|
+
try {
|
|
1039
|
+
const stats = await handle.stat();
|
|
1040
|
+
record.dev = stats.dev;
|
|
1041
|
+
record.ino = stats.ino;
|
|
1042
|
+
await handle.writeFile(write.bytes);
|
|
1043
|
+
}
|
|
1044
|
+
finally {
|
|
1045
|
+
await handle.close().catch(() => undefined);
|
|
1046
|
+
}
|
|
1047
|
+
const written = await readFile(target.absolutePath);
|
|
1048
|
+
if (sha256Digest(written) !== file.nextDigest) {
|
|
1049
|
+
throw new Error(`Written digest mismatch for ${file.path}.`);
|
|
1050
|
+
}
|
|
1051
|
+
const recorded = await recordManagedFile(managed, file.path, file.nextDigest, {
|
|
1052
|
+
dev: record.dev,
|
|
1053
|
+
ino: record.ino,
|
|
1054
|
+
});
|
|
1055
|
+
if (!recorded.ok) {
|
|
1056
|
+
throw new Error(recorded.diagnostics[0]?.message ?? `Cannot record ${file.path}.`);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
const completed = await completeManagedApply(managed);
|
|
1060
|
+
if (!completed.ok) {
|
|
1061
|
+
const residual = await cleanupCreatedFiles(root, created);
|
|
1062
|
+
const failed = await failManagedApply(managed, 'Cannot finalize managed installation manifest.', residual);
|
|
1063
|
+
return {
|
|
1064
|
+
ok: false,
|
|
1065
|
+
status: 'failed',
|
|
1066
|
+
appliedFiles: [],
|
|
1067
|
+
pendingExternalActions: [],
|
|
1068
|
+
residualPaths: failed.residualPaths,
|
|
1069
|
+
diagnostics: failed.diagnostics,
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
return {
|
|
1073
|
+
ok: true,
|
|
1074
|
+
status: bundle.plan.externalActions.length > 0
|
|
1075
|
+
? 'external-actions-pending'
|
|
1076
|
+
: created.length === 0
|
|
1077
|
+
? 'no-changes'
|
|
1078
|
+
: 'applied',
|
|
1079
|
+
appliedFiles: created.map((file) => file.path),
|
|
1080
|
+
pendingExternalActions: bundle.plan.externalActions,
|
|
1081
|
+
residualPaths: completed.residualPaths,
|
|
1082
|
+
diagnostics: completed.diagnostics,
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
catch (error) {
|
|
1086
|
+
const residual = await cleanupCreatedFiles(root, created);
|
|
1087
|
+
for (const directory of createdDirectories.reverse()) {
|
|
1088
|
+
await rmdir(directory).catch(() => undefined);
|
|
1089
|
+
}
|
|
1090
|
+
const code = typeof error === 'object' &&
|
|
1091
|
+
error !== null &&
|
|
1092
|
+
'code' in error &&
|
|
1093
|
+
error.code === 'EEXIST'
|
|
1094
|
+
? diagnosticCodes.initTargetExists
|
|
1095
|
+
: diagnosticCodes.initApplyFailed;
|
|
1096
|
+
const status = code === diagnosticCodes.initTargetExists ? 'conflict' : 'failed';
|
|
1097
|
+
const base = `Init apply ${status === 'conflict' ? 'stopped because a target appeared' : 'failed'}: ${error instanceof Error ? error.message : String(error)}`;
|
|
1098
|
+
const residualNote = residual.length === 0
|
|
1099
|
+
? 'No planned file from this attempt remains.'
|
|
1100
|
+
: `Residual planned files require manual inspection: ${residual.join(', ')}`;
|
|
1101
|
+
const failed = await failManagedApply(managed, `${base}. ${residualNote}`, residual);
|
|
1102
|
+
return {
|
|
1103
|
+
ok: false,
|
|
1104
|
+
status,
|
|
1105
|
+
appliedFiles: [],
|
|
1106
|
+
pendingExternalActions: [],
|
|
1107
|
+
residualPaths: failed.residualPaths,
|
|
1108
|
+
diagnostics: failed.diagnostics,
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
}
|