accrete-kit 0.0.0-stage → 0.1.0
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/CHANGELOG.md +43 -0
- package/LICENSE +21 -0
- package/README.md +148 -2
- package/bin/accrete-kit.mjs +4 -0
- package/dist/kernel/darwin-arm64/LICENSE +21 -0
- package/dist/kernel/darwin-arm64/THIRD-PARTY-NOTICES +5780 -0
- package/dist/kernel/darwin-arm64/accrete +0 -0
- package/dist/kernel/darwin-arm64/licenses/RUST-COPYRIGHT-library.html +7038 -0
- package/dist/kernel/darwin-arm64/licenses/musl-COPYRIGHT +193 -0
- package/dist/kernel/darwin-arm64/licenses/rust/Apache-2.0.txt +73 -0
- package/dist/kernel/darwin-arm64/licenses/rust/BSD-2-Clause.txt +9 -0
- package/dist/kernel/darwin-arm64/licenses/rust/CC-BY-SA-4.0.txt +427 -0
- package/dist/kernel/darwin-arm64/licenses/rust/GCC-exception-3.1.txt +30 -0
- package/dist/kernel/darwin-arm64/licenses/rust/GPL-2.0-only.txt +133 -0
- package/dist/kernel/darwin-arm64/licenses/rust/GPL-3.0-or-later.txt +202 -0
- package/dist/kernel/darwin-arm64/licenses/rust/ISC.txt +7 -0
- package/dist/kernel/darwin-arm64/licenses/rust/LLVM-exception.txt +15 -0
- package/dist/kernel/darwin-arm64/licenses/rust/MIT.txt +9 -0
- package/dist/kernel/darwin-arm64/licenses/rust/NCSA.txt +29 -0
- package/dist/kernel/darwin-arm64/licenses/rust/OFL-1.1.txt +43 -0
- package/dist/kernel/darwin-arm64/licenses/rust/Unicode-3.0.txt +39 -0
- package/dist/kernel/darwin-x64/LICENSE +21 -0
- package/dist/kernel/darwin-x64/THIRD-PARTY-NOTICES +5780 -0
- package/dist/kernel/darwin-x64/accrete +0 -0
- package/dist/kernel/darwin-x64/licenses/RUST-COPYRIGHT-library.html +7038 -0
- package/dist/kernel/darwin-x64/licenses/musl-COPYRIGHT +193 -0
- package/dist/kernel/darwin-x64/licenses/rust/Apache-2.0.txt +73 -0
- package/dist/kernel/darwin-x64/licenses/rust/BSD-2-Clause.txt +9 -0
- package/dist/kernel/darwin-x64/licenses/rust/CC-BY-SA-4.0.txt +427 -0
- package/dist/kernel/darwin-x64/licenses/rust/GCC-exception-3.1.txt +30 -0
- package/dist/kernel/darwin-x64/licenses/rust/GPL-2.0-only.txt +133 -0
- package/dist/kernel/darwin-x64/licenses/rust/GPL-3.0-or-later.txt +202 -0
- package/dist/kernel/darwin-x64/licenses/rust/ISC.txt +7 -0
- package/dist/kernel/darwin-x64/licenses/rust/LLVM-exception.txt +15 -0
- package/dist/kernel/darwin-x64/licenses/rust/MIT.txt +9 -0
- package/dist/kernel/darwin-x64/licenses/rust/NCSA.txt +29 -0
- package/dist/kernel/darwin-x64/licenses/rust/OFL-1.1.txt +43 -0
- package/dist/kernel/darwin-x64/licenses/rust/Unicode-3.0.txt +39 -0
- package/dist/kernel/kernel.json +17 -0
- package/dist/kernel/linux-arm64/LICENSE +21 -0
- package/dist/kernel/linux-arm64/THIRD-PARTY-NOTICES +5780 -0
- package/dist/kernel/linux-arm64/accrete +0 -0
- package/dist/kernel/linux-arm64/licenses/RUST-COPYRIGHT-library.html +7038 -0
- package/dist/kernel/linux-arm64/licenses/musl-COPYRIGHT +193 -0
- package/dist/kernel/linux-arm64/licenses/rust/Apache-2.0.txt +73 -0
- package/dist/kernel/linux-arm64/licenses/rust/BSD-2-Clause.txt +9 -0
- package/dist/kernel/linux-arm64/licenses/rust/CC-BY-SA-4.0.txt +427 -0
- package/dist/kernel/linux-arm64/licenses/rust/GCC-exception-3.1.txt +30 -0
- package/dist/kernel/linux-arm64/licenses/rust/GPL-2.0-only.txt +133 -0
- package/dist/kernel/linux-arm64/licenses/rust/GPL-3.0-or-later.txt +202 -0
- package/dist/kernel/linux-arm64/licenses/rust/ISC.txt +7 -0
- package/dist/kernel/linux-arm64/licenses/rust/LLVM-exception.txt +15 -0
- package/dist/kernel/linux-arm64/licenses/rust/MIT.txt +9 -0
- package/dist/kernel/linux-arm64/licenses/rust/NCSA.txt +29 -0
- package/dist/kernel/linux-arm64/licenses/rust/OFL-1.1.txt +43 -0
- package/dist/kernel/linux-arm64/licenses/rust/Unicode-3.0.txt +39 -0
- package/dist/kernel/linux-x64/LICENSE +21 -0
- package/dist/kernel/linux-x64/THIRD-PARTY-NOTICES +5780 -0
- package/dist/kernel/linux-x64/accrete +0 -0
- package/dist/kernel/linux-x64/licenses/RUST-COPYRIGHT-library.html +7038 -0
- package/dist/kernel/linux-x64/licenses/musl-COPYRIGHT +193 -0
- package/dist/kernel/linux-x64/licenses/rust/Apache-2.0.txt +73 -0
- package/dist/kernel/linux-x64/licenses/rust/BSD-2-Clause.txt +9 -0
- package/dist/kernel/linux-x64/licenses/rust/CC-BY-SA-4.0.txt +427 -0
- package/dist/kernel/linux-x64/licenses/rust/GCC-exception-3.1.txt +30 -0
- package/dist/kernel/linux-x64/licenses/rust/GPL-2.0-only.txt +133 -0
- package/dist/kernel/linux-x64/licenses/rust/GPL-3.0-or-later.txt +202 -0
- package/dist/kernel/linux-x64/licenses/rust/ISC.txt +7 -0
- package/dist/kernel/linux-x64/licenses/rust/LLVM-exception.txt +15 -0
- package/dist/kernel/linux-x64/licenses/rust/MIT.txt +9 -0
- package/dist/kernel/linux-x64/licenses/rust/NCSA.txt +29 -0
- package/dist/kernel/linux-x64/licenses/rust/OFL-1.1.txt +43 -0
- package/dist/kernel/linux-x64/licenses/rust/Unicode-3.0.txt +39 -0
- package/dist/scaffold/assets/contract/cli-v1.json +296 -0
- package/dist/scaffold/assets/contract/example-inputs.json +38 -0
- package/dist/scaffold/assets/contract/input-drafts.schema.json +654 -0
- package/dist/scaffold/assets/generated/cli-commands.md +288 -0
- package/dist/scaffold/assets/generated/release.json +11 -0
- package/dist/scaffold/assets/skills/accrete-data/SKILL.md +73 -0
- package/dist/scaffold/assets/skills/accrete-data/references/commands.md +193 -0
- package/dist/scaffold/assets/skills/accrete-data/references/objects-and-native-work.md +123 -0
- package/dist/scaffold/assets/skills/accrete-data/references/remote-and-retention.md +98 -0
- package/dist/scaffold/assets/skills/accrete-memory/SKILL.md +61 -0
- package/dist/scaffold/assets/skills/accrete-memory/references/commands.md +124 -0
- package/dist/scaffold/assets/skills/accrete-memory/references/handovers-and-remote-work.md +78 -0
- package/dist/scaffold/assets/skills/accrete-memory/references/writing-and-continuity.md +101 -0
- package/dist/scaffold/assets/skills/accrete-models/SKILL.md +46 -0
- package/dist/scaffold/assets/skills/accrete-models/references/cli.md +117 -0
- package/dist/scaffold/assets/skills/accrete-models/references/objects.md +67 -0
- package/dist/scaffold/assets/skills/accrete-models/references/working-practice.md +48 -0
- package/dist/scaffold/assets/skills/accrete-results/SKILL.md +69 -0
- package/dist/scaffold/assets/skills/accrete-results/references/activities-and-relations.md +116 -0
- package/dist/scaffold/assets/skills/accrete-results/references/commands.md +191 -0
- package/dist/scaffold/assets/skills/accrete-results/references/scientific-practice.md +89 -0
- package/dist/scaffold/assets/skills/accrete-tools/SKILL.md +51 -0
- package/dist/scaffold/assets/skills/accrete-tools/references/cli.md +87 -0
- package/dist/scaffold/assets/skills/accrete-tools/references/objects.md +66 -0
- package/dist/scaffold/assets/skills/accrete-tools/references/working-practice.md +51 -0
- package/dist/scaffold/assets/template/.accrete/PROJECT.md +31 -0
- package/dist/scaffold/assets/template/.accrete/config-examples/local-compute.toml.example +27 -0
- package/dist/scaffold/assets/template/.accrete/config-examples/local-workstation.toml.example +21 -0
- package/dist/scaffold/assets/template/.accrete/config-examples/project.toml.example +27 -0
- package/dist/scaffold/assets/template/.accrete/docs/cli-contract.md +393 -0
- package/dist/scaffold/assets/template/.accrete/docs/configuration.md +117 -0
- package/dist/scaffold/assets/template/.accrete/docs/coordinator.md +102 -0
- package/dist/scaffold/assets/template/.accrete/docs/long-term-work.md +108 -0
- package/dist/scaffold/assets/template/.accrete/docs/overview.md +118 -0
- package/dist/scaffold/assets/template/.accrete/docs/remote-work.md +125 -0
- package/dist/scaffold/assets/template/.accrete/docs/subagents.md +78 -0
- package/dist/scaffold/assets/template/.accrete/examples/README.md +78 -0
- package/dist/scaffold/assets/template/.accrete/examples/data-register.json +19 -0
- package/dist/scaffold/assets/template/.accrete/examples/iteration-close.json +5 -0
- package/dist/scaffold/assets/template/.accrete/examples/iteration-open.json +8 -0
- package/dist/scaffold/assets/template/.accrete/examples/memory-entry.md +10 -0
- package/dist/scaffold/assets/template/.accrete/examples/model-register.json +7 -0
- package/dist/scaffold/assets/template/.accrete/examples/observation.md +9 -0
- package/dist/scaffold/assets/template/.accrete/examples/result-amend.json +5 -0
- package/dist/scaffold/assets/template/.accrete/examples/result-record.json +11 -0
- package/dist/scaffold/assets/template/.accrete/examples/tool-admit.json +8 -0
- package/dist/scaffold/assets/template/.accrete/examples/tool-retire.json +3 -0
- package/dist/scaffold/assets/template/.accrete/examples/tool-update.json +3 -0
- package/dist/scaffold/assets/template/.gitattributes +17 -0
- package/dist/scaffold/assets/template/AGENTS.md +23 -0
- package/dist/scaffold/assets/template/CLAUDE.md +5 -0
- package/dist/scaffold/assets/template/_gitignore +12 -0
- package/dist/scaffold/config/defaults.toml +20 -0
- package/dist/scaffold/manifest.json +622 -0
- package/lib/cli.mjs +188 -0
- package/lib/fsutil.mjs +135 -0
- package/lib/install.mjs +439 -0
- package/lib/kernel.mjs +76 -0
- package/lib/scaffold.mjs +129 -0
- package/lib/workspace.mjs +278 -0
- package/package.json +18 -4
package/lib/install.mjs
ADDED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
// Ownership-aware installation. Every managed unit is compared three ways:
|
|
2
|
+
// C (current content), B (installed base) and N (incoming scaffold). Each path's
|
|
3
|
+
// decision is independent of the others, so repeating an interrupted run reaches
|
|
4
|
+
// the same outcome: what was published reads as C equals N, and the inventory
|
|
5
|
+
// that records it comes last. See docs/TEMPLATE-FORMAT.md.
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { AccreteError, atomicWrite, readCurrent, refuseSymlinks, removeWithEmptyParents, sha256 } from './fsutil.mjs';
|
|
9
|
+
import { BASES, INVENTORY, UPGRADES, findBlock, within } from './scaffold.mjs';
|
|
10
|
+
|
|
11
|
+
// Decisions that leave the project's own files as they are.
|
|
12
|
+
const KEEPS_FILES = new Set(['unchanged', 'adopt', 'keep-edited', 'keep-deleted', 'keep-seed']);
|
|
13
|
+
|
|
14
|
+
export function readInventory(root) {
|
|
15
|
+
refuseSymlinks(root, INVENTORY);
|
|
16
|
+
let text;
|
|
17
|
+
try {
|
|
18
|
+
text = fs.readFileSync(path.join(root, INVENTORY), 'utf8');
|
|
19
|
+
} catch (error) {
|
|
20
|
+
if (error.code === 'ENOENT') return null;
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
let inventory;
|
|
24
|
+
try {
|
|
25
|
+
inventory = JSON.parse(text);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
throw new AccreteError('unsupported_format', `Invalid template inventory: ${error.message}`);
|
|
28
|
+
}
|
|
29
|
+
if (inventory.schema_version !== 1) throw new AccreteError('unsupported_format', 'Unsupported template inventory schema');
|
|
30
|
+
return inventory;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const currentDigest = current => current.kind === 'file' ? sha256(current.bytes) : null;
|
|
34
|
+
|
|
35
|
+
/** The managed unit in the current file: `missing`, `no-block`, `unusable`, or its bytes. */
|
|
36
|
+
function currentUnit(current, block) {
|
|
37
|
+
if (current.kind === 'missing') return { kind: 'missing' };
|
|
38
|
+
if (current.kind === 'other') return { kind: 'unusable' };
|
|
39
|
+
if (!block) return { kind: 'present', start: 0, end: current.bytes.length, bytes: current.bytes };
|
|
40
|
+
const found = findBlock(current.bytes, block.start, block.end);
|
|
41
|
+
if (found.kind === 'none') return { kind: 'no-block' };
|
|
42
|
+
if (found.kind === 'malformed') return { kind: 'unusable' };
|
|
43
|
+
return { ...found, kind: 'present', bytes: current.bytes.subarray(found.start, found.end) };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** LF scaffold text in the newline convention of `like`. */
|
|
47
|
+
function withNewlinesOf(text, like) {
|
|
48
|
+
if (!like.includes('\r\n')) return text;
|
|
49
|
+
return Buffer.from(text.toString('latin1').replaceAll('\n', '\r\n'), 'latin1');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const candidatePath = (scaffold, destination) => `${UPGRADES}/${scaffold.template_version}/${destination}.incoming`;
|
|
53
|
+
|
|
54
|
+
function decide(unit, base, currentSha, incomingSha, cleanEntry) {
|
|
55
|
+
if (unit.kind === 'missing') return base ? 'keep-deleted' : 'install';
|
|
56
|
+
if (unit.kind === 'no-block') return base ? 'keep-deleted' : 'append-block';
|
|
57
|
+
if (unit.kind === 'unusable') return 'candidate';
|
|
58
|
+
if (currentSha === incomingSha) return base === incomingSha && cleanEntry ? 'unchanged' : 'adopt';
|
|
59
|
+
if (base === currentSha) return 'update';
|
|
60
|
+
if (base === incomingSha) return 'keep-edited';
|
|
61
|
+
return 'candidate';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function planAsset(root, scaffold, asset, entry) {
|
|
65
|
+
const current = readCurrent(root, asset.destination);
|
|
66
|
+
const step = { destination: asset.destination, asset, before: currentDigest(current), base: null, currentUnit: null };
|
|
67
|
+
if (asset.ownership === 'seed-once') {
|
|
68
|
+
return { ...step, act: !entry && current.kind === 'missing' ? 'seed' : 'keep-seed', incoming: scaffold.read(asset.source) };
|
|
69
|
+
}
|
|
70
|
+
const unit = currentUnit(current, asset.block);
|
|
71
|
+
const canonical = scaffold.unit(asset);
|
|
72
|
+
let incoming = canonical;
|
|
73
|
+
if (asset.block && current.kind === 'file') incoming = withNewlinesOf(canonical, unit.bytes ?? current.bytes);
|
|
74
|
+
const incomingSha = sha256(incoming);
|
|
75
|
+
const currentSha = unit.kind === 'present' ? sha256(unit.bytes) : null;
|
|
76
|
+
const base = entry?.ownership === asset.ownership ? entry.base_sha256 : null;
|
|
77
|
+
const clean = Boolean(entry) && !entry.pending && !entry.locally_deleted;
|
|
78
|
+
return { ...step, act: decide(unit, base, currentSha, incomingSha, clean), base, currentUnit: currentSha, incoming };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A path no longer installed: withdrawn by the scaffold, or its agent was dropped. */
|
|
82
|
+
function planWithdrawal(root, entry) {
|
|
83
|
+
const current = readCurrent(root, entry.destination);
|
|
84
|
+
const unit = currentUnit(current, entry.block);
|
|
85
|
+
let act;
|
|
86
|
+
if (entry.ownership === 'seed-once' || !entry.base_sha256) act = 'forget';
|
|
87
|
+
else if (unit.kind === 'missing' || unit.kind === 'no-block') act = 'forget';
|
|
88
|
+
else if (unit.kind === 'present' && sha256(unit.bytes) === entry.base_sha256) act = entry.block ? 'remove-block' : 'remove';
|
|
89
|
+
else act = 'detach';
|
|
90
|
+
return { destination: entry.destination, act, before: currentDigest(current), base: entry.base_sha256 };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function installedEntry(scaffold, asset, digest) {
|
|
94
|
+
return {
|
|
95
|
+
destination: asset.destination,
|
|
96
|
+
profile: asset.profile,
|
|
97
|
+
ownership: asset.ownership,
|
|
98
|
+
installed_version: scaffold.template_version,
|
|
99
|
+
base_sha256: digest,
|
|
100
|
+
...(asset.block ? { block: { start: asset.block.start, end: asset.block.end } } : {}),
|
|
101
|
+
pending: null
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Records what one run changed, relative to the Workspace root. */
|
|
106
|
+
export function publisher(root) {
|
|
107
|
+
const written = [];
|
|
108
|
+
const removed = [];
|
|
109
|
+
return {
|
|
110
|
+
written,
|
|
111
|
+
removed,
|
|
112
|
+
write(relative, bytes, options) {
|
|
113
|
+
atomicWrite(path.join(root, relative), bytes, options);
|
|
114
|
+
written.push(relative);
|
|
115
|
+
},
|
|
116
|
+
remove(relative) {
|
|
117
|
+
removeWithEmptyParents(root, relative);
|
|
118
|
+
removed.push(relative);
|
|
119
|
+
},
|
|
120
|
+
// Content-addressed: an existing blob with this name already holds these bytes.
|
|
121
|
+
base(bytes) {
|
|
122
|
+
const digest = sha256(bytes);
|
|
123
|
+
if (!fs.existsSync(path.join(root, BASES, digest))) this.write(`${BASES}/${digest}`, bytes);
|
|
124
|
+
return digest;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The whole new file for a block or file unit, as this step decided. */
|
|
130
|
+
function newFileBytes(scaffold, step, current) {
|
|
131
|
+
if (step.act === 'append-block') {
|
|
132
|
+
const eol = current.bytes.includes('\r\n') ? '\r\n' : '\n';
|
|
133
|
+
const parts = [current.bytes];
|
|
134
|
+
if (current.bytes.length > 0) {
|
|
135
|
+
if (current.bytes.at(-1) !== 0x0a) parts.push(Buffer.from(eol));
|
|
136
|
+
parts.push(Buffer.from(eol));
|
|
137
|
+
}
|
|
138
|
+
return Buffer.concat([...parts, step.incoming]);
|
|
139
|
+
}
|
|
140
|
+
if (step.act === 'update' && step.asset.block) {
|
|
141
|
+
const found = findBlock(current.bytes, step.asset.block.start, step.asset.block.end);
|
|
142
|
+
if (found.kind !== 'one') throw new AccreteError('conflict', 'Managed block changed after planning');
|
|
143
|
+
return Buffer.concat([current.bytes.subarray(0, found.start), step.incoming, current.bytes.subarray(found.end)]);
|
|
144
|
+
}
|
|
145
|
+
return step.act === 'update' ? step.incoming : scaffold.read(step.asset.source);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function writeCandidate(root, scaffold, step, entries, publish) {
|
|
149
|
+
const { asset, destination } = step;
|
|
150
|
+
const canonical = scaffold.unit(asset);
|
|
151
|
+
const relative = candidatePath(scaffold, destination);
|
|
152
|
+
refuseSymlinks(root, relative);
|
|
153
|
+
const existing = readCurrent(root, relative);
|
|
154
|
+
if (existing.kind !== 'file' || !existing.bytes.equals(canonical)) publish.write(relative, canonical);
|
|
155
|
+
const previous = entries.get(destination);
|
|
156
|
+
const kept = previous?.ownership === asset.ownership ? previous : null;
|
|
157
|
+
entries.set(destination, {
|
|
158
|
+
destination,
|
|
159
|
+
profile: asset.profile,
|
|
160
|
+
ownership: asset.ownership,
|
|
161
|
+
installed_version: kept?.installed_version ?? null,
|
|
162
|
+
base_sha256: kept?.base_sha256 ?? null,
|
|
163
|
+
...(asset.block ? { block: { start: asset.block.start, end: asset.block.end } } : {}),
|
|
164
|
+
pending: {
|
|
165
|
+
version: scaffold.template_version,
|
|
166
|
+
incoming_sha256: sha256(canonical),
|
|
167
|
+
candidate_path: relative,
|
|
168
|
+
current_sha256: step.currentUnit
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Carry out one decided step. An error affects only this path. */
|
|
174
|
+
function apply(root, scaffold, step, entries, publish) {
|
|
175
|
+
const { destination, asset, act } = step;
|
|
176
|
+
const current = readCurrent(root, destination);
|
|
177
|
+
if (!KEEPS_FILES.has(act) && currentDigest(current) !== step.before) {
|
|
178
|
+
throw new AccreteError('conflict', 'Destination changed after planning; run the upgrade again');
|
|
179
|
+
}
|
|
180
|
+
switch (act) {
|
|
181
|
+
case 'install':
|
|
182
|
+
case 'append-block':
|
|
183
|
+
case 'update': {
|
|
184
|
+
const digest = publish.base(step.incoming);
|
|
185
|
+
publish.write(destination, newFileBytes(scaffold, step, current));
|
|
186
|
+
entries.set(destination, installedEntry(scaffold, asset, digest));
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
case 'unchanged':
|
|
190
|
+
case 'adopt':
|
|
191
|
+
case 'keep-edited':
|
|
192
|
+
entries.set(destination, installedEntry(scaffold, asset, publish.base(step.incoming)));
|
|
193
|
+
break;
|
|
194
|
+
case 'candidate':
|
|
195
|
+
writeCandidate(root, scaffold, step, entries, publish);
|
|
196
|
+
break;
|
|
197
|
+
case 'keep-deleted': {
|
|
198
|
+
const { pending: _, ...kept } = entries.get(destination);
|
|
199
|
+
entries.set(destination, { ...kept, pending: null, locally_deleted: true });
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
case 'seed':
|
|
203
|
+
publish.write(destination, step.incoming);
|
|
204
|
+
entries.set(destination, { ...installedEntry(scaffold, asset, null) });
|
|
205
|
+
break;
|
|
206
|
+
case 'keep-seed':
|
|
207
|
+
if (!entries.has(destination)) entries.set(destination, { ...installedEntry(scaffold, asset, null), installed_version: null });
|
|
208
|
+
break;
|
|
209
|
+
case 'remove':
|
|
210
|
+
publish.remove(destination);
|
|
211
|
+
entries.delete(destination);
|
|
212
|
+
break;
|
|
213
|
+
case 'remove-block': {
|
|
214
|
+
const { block } = entries.get(destination);
|
|
215
|
+
const found = current.kind === 'file' ? findBlock(current.bytes, block.start, block.end) : { kind: 'none' };
|
|
216
|
+
if (found.kind !== 'one') throw new AccreteError('conflict', 'Managed block changed after planning');
|
|
217
|
+
const rest = Buffer.concat([current.bytes.subarray(0, found.start), current.bytes.subarray(found.end)]);
|
|
218
|
+
if (rest.toString('latin1').trim() === '') publish.remove(destination);
|
|
219
|
+
else publish.write(destination, rest);
|
|
220
|
+
entries.delete(destination);
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
case 'detach':
|
|
224
|
+
case 'forget':
|
|
225
|
+
entries.delete(destination);
|
|
226
|
+
break;
|
|
227
|
+
default:
|
|
228
|
+
throw new Error(`Unknown step ${act}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function writeInventory(root, inventory, publish) {
|
|
233
|
+
const bytes = Buffer.from(`${JSON.stringify(inventory, null, 2)}\n`);
|
|
234
|
+
const current = readCurrent(root, INVENTORY);
|
|
235
|
+
if (current.kind !== 'file' || !current.bytes.equals(bytes)) publish.write(INVENTORY, bytes);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const inventoryOf = (scaffold, versions, selected, entries) => ({
|
|
239
|
+
schema_version: 1,
|
|
240
|
+
template_id: scaffold.template_id,
|
|
241
|
+
template_version: versions.template_version,
|
|
242
|
+
kernel_version: versions.kernel_version,
|
|
243
|
+
selected_profiles: selected,
|
|
244
|
+
files: [...entries.values()].sort((a, b) => a.destination < b.destination ? -1 : 1)
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
/** Remove bases and candidates no entry references. These names are ours. */
|
|
248
|
+
function sweep(root, inventory) {
|
|
249
|
+
const keptBases = new Set(inventory.files.map(entry => entry.base_sha256));
|
|
250
|
+
const bases = path.join(root, BASES);
|
|
251
|
+
if (fs.existsSync(bases)) {
|
|
252
|
+
for (const name of fs.readdirSync(bases)) {
|
|
253
|
+
if (/^[0-9a-f]{64}$/.test(name) && !keptBases.has(name)) fs.rmSync(path.join(bases, name));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const keptCandidates = new Set(inventory.files.map(entry => entry.pending?.candidate_path).filter(Boolean));
|
|
257
|
+
const walk = relative => {
|
|
258
|
+
for (const item of fs.readdirSync(path.join(root, relative), { withFileTypes: true })) {
|
|
259
|
+
const child = `${relative}/${item.name}`;
|
|
260
|
+
if (item.isDirectory()) walk(child);
|
|
261
|
+
else if (item.isFile() && item.name.endsWith('.incoming') && !keptCandidates.has(child)) fs.rmSync(path.join(root, child));
|
|
262
|
+
}
|
|
263
|
+
try {
|
|
264
|
+
fs.rmdirSync(path.join(root, relative));
|
|
265
|
+
} catch (error) {
|
|
266
|
+
if (error.code !== 'ENOTEMPTY' && error.code !== 'EEXIST') throw error;
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
if (fs.existsSync(path.join(root, UPGRADES))) walk(UPGRADES);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function report(step, scaffold) {
|
|
273
|
+
const item = { destination: step.destination, action: step.act };
|
|
274
|
+
if (step.act === 'candidate') {
|
|
275
|
+
item.candidate = candidatePath(scaffold, step.destination);
|
|
276
|
+
item.base_sha256 = step.base ?? null;
|
|
277
|
+
item.next = `Resolve the candidate in the live file, then run: ${acceptCommand(scaffold, step.destination)}`;
|
|
278
|
+
}
|
|
279
|
+
return item;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const acceptCommand = (scaffold, destination) => `npx accrete-kit@${scaffold.template_version} upgrade --accept ${destination}`;
|
|
283
|
+
const failure = (destination, planned, error) => ({ destination, action: 'failed', planned, error: error.message });
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* The one route by which a scaffold reaches a Workspace, for creation and upgrade.
|
|
287
|
+
* `paths` are the Workspace's configured store roots, which assets must stay out of.
|
|
288
|
+
*/
|
|
289
|
+
export function reconcile(root, scaffold, { agents = null, dryRun = false, paths = {} } = {}) {
|
|
290
|
+
for (const value of Object.values(paths)) {
|
|
291
|
+
const store = path.posix.normalize(value).replace(/\/$/, '');
|
|
292
|
+
const overlapping = scaffold.files.find(file => within(file.destination, store));
|
|
293
|
+
if (overlapping) throw new AccreteError('invalid', `Scaffold destination overlaps a configured store: ${overlapping.destination}`);
|
|
294
|
+
}
|
|
295
|
+
for (const relative of [BASES, UPGRADES]) refuseSymlinks(root, relative);
|
|
296
|
+
const existing = readInventory(root);
|
|
297
|
+
const selected = agents ?? existing?.selected_profiles ?? scaffold.default_profiles;
|
|
298
|
+
const entries = new Map((existing?.files ?? []).map(entry => [entry.destination, entry]));
|
|
299
|
+
const assets = scaffold.files.filter(file => !file.profile || selected.includes(file.profile));
|
|
300
|
+
const installed = new Set(assets.map(file => file.destination));
|
|
301
|
+
|
|
302
|
+
const steps = [];
|
|
303
|
+
const files = [];
|
|
304
|
+
const plan = (destination, make) => {
|
|
305
|
+
try {
|
|
306
|
+
steps.push(make());
|
|
307
|
+
} catch (error) {
|
|
308
|
+
files.push(failure(destination, 'read', error));
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
for (const asset of assets) plan(asset.destination, () => planAsset(root, scaffold, asset, entries.get(asset.destination)));
|
|
312
|
+
for (const entry of entries.values()) {
|
|
313
|
+
if (!installed.has(entry.destination)) plan(entry.destination, () => planWithdrawal(root, entry));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const listed = act => steps.filter(step => step.act === act).map(step => step.destination);
|
|
317
|
+
const receipt = {
|
|
318
|
+
ok: true,
|
|
319
|
+
dry_run: dryRun,
|
|
320
|
+
project_root: root,
|
|
321
|
+
template_version: scaffold.template_version,
|
|
322
|
+
kernel_version: scaffold.kernel_version,
|
|
323
|
+
selected_profiles: selected,
|
|
324
|
+
written: [],
|
|
325
|
+
removed: [],
|
|
326
|
+
pending: listed('candidate'),
|
|
327
|
+
detached: listed('detach'),
|
|
328
|
+
locally_deleted: listed('keep-deleted')
|
|
329
|
+
};
|
|
330
|
+
const finish = () => {
|
|
331
|
+
receipt.files = files;
|
|
332
|
+
receipt.failed = files.filter(item => item.action === 'failed');
|
|
333
|
+
receipt.ok = receipt.failed.length === 0;
|
|
334
|
+
if (receipt.pending.length > 0) {
|
|
335
|
+
receipt.warning = `${receipt.pending.length} path(s) ${dryRun ? 'would be' : 'were'} preserved with a candidate under ${UPGRADES}; `
|
|
336
|
+
+ `resolve each in the live file, then run: npx accrete-kit@${scaffold.template_version} upgrade --accept PATH`;
|
|
337
|
+
}
|
|
338
|
+
return receipt;
|
|
339
|
+
};
|
|
340
|
+
if (dryRun) {
|
|
341
|
+
files.push(...steps.map(step => report(step, scaffold)));
|
|
342
|
+
return finish();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const publish = publisher(root);
|
|
346
|
+
receipt.written = publish.written;
|
|
347
|
+
receipt.removed = publish.removed;
|
|
348
|
+
// A selection change is recorded before any file moves. Otherwise an agent's
|
|
349
|
+
// half-removed files would read as owner deletions on a rerun without --agents.
|
|
350
|
+
if (!existing || JSON.stringify(existing.selected_profiles) !== JSON.stringify(selected)) {
|
|
351
|
+
writeInventory(root, inventoryOf(scaffold, existing ?? scaffold, selected, entries), publish);
|
|
352
|
+
}
|
|
353
|
+
for (const step of steps) {
|
|
354
|
+
try {
|
|
355
|
+
apply(root, scaffold, step, entries, publish);
|
|
356
|
+
files.push(report(step, scaffold));
|
|
357
|
+
} catch (error) {
|
|
358
|
+
files.push(failure(step.destination, step.act, error));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
const inventory = inventoryOf(scaffold, scaffold, selected, entries);
|
|
362
|
+
try {
|
|
363
|
+
writeInventory(root, inventory, publish);
|
|
364
|
+
} catch (error) {
|
|
365
|
+
throw new AccreteError('incomplete', `Files were published, but the inventory could not be written: ${error.message}`, { inventory: path.join(root, INVENTORY) });
|
|
366
|
+
}
|
|
367
|
+
try {
|
|
368
|
+
sweep(root, inventory);
|
|
369
|
+
} catch (error) {
|
|
370
|
+
receipt.cleanup_warning = error.message;
|
|
371
|
+
}
|
|
372
|
+
return finish();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Record a resolved candidate: the current bytes stay, and the incoming unit
|
|
377
|
+
* becomes the base they were resolved against.
|
|
378
|
+
*/
|
|
379
|
+
export function accept(root, scaffold, requested, { dryRun = false } = {}) {
|
|
380
|
+
let relative = requested;
|
|
381
|
+
if (path.isAbsolute(requested)) {
|
|
382
|
+
let absolute = requested;
|
|
383
|
+
try {
|
|
384
|
+
absolute = fs.realpathSync(requested);
|
|
385
|
+
} catch {
|
|
386
|
+
// A missing path is reported below as not being a template path.
|
|
387
|
+
}
|
|
388
|
+
relative = path.relative(root, absolute);
|
|
389
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) throw new AccreteError('invalid', '--accept must name a path inside this Workspace');
|
|
390
|
+
}
|
|
391
|
+
const destination = path.normalize(relative).split(path.sep).join('/');
|
|
392
|
+
const conflict = message => new AccreteError('conflict', message);
|
|
393
|
+
const rerun = '; run npx accrete-kit@latest upgrade for a current candidate';
|
|
394
|
+
const inventory = readInventory(root);
|
|
395
|
+
if (!inventory) throw new AccreteError('not_found', 'This Workspace has no template inventory');
|
|
396
|
+
const index = inventory.files.findIndex(entry => entry.destination === destination);
|
|
397
|
+
if (index === -1) throw conflict(`${destination} is not a template path`);
|
|
398
|
+
const { pending } = inventory.files[index];
|
|
399
|
+
if (!pending) throw conflict(`${destination} has no pending candidate`);
|
|
400
|
+
const asset = scaffold.files.find(file => file.destination === destination);
|
|
401
|
+
if (!asset) throw conflict(`${destination} is not in this template version`);
|
|
402
|
+
const incoming = scaffold.unit(asset);
|
|
403
|
+
if (pending.version !== scaffold.template_version || pending.incoming_sha256 !== sha256(incoming)) {
|
|
404
|
+
throw conflict(`The candidate for ${destination} belongs to version ${pending.version}, not this one${rerun}`);
|
|
405
|
+
}
|
|
406
|
+
refuseSymlinks(root, pending.candidate_path);
|
|
407
|
+
const candidate = readCurrent(root, pending.candidate_path);
|
|
408
|
+
if (candidate.kind !== 'file') throw conflict(`Candidate ${pending.candidate_path} is unreadable${rerun}`);
|
|
409
|
+
if (sha256(candidate.bytes) !== pending.incoming_sha256) throw conflict(`Candidate ${pending.candidate_path} was changed${rerun}`);
|
|
410
|
+
const current = readCurrent(root, destination);
|
|
411
|
+
const unit = currentUnit(current, asset.block);
|
|
412
|
+
if (unit.kind !== 'present') throw conflict(`${destination} has no usable managed content to accept; resolve it first`);
|
|
413
|
+
const accepted = { destination, current_sha256: currentDigest(current), candidate: pending.candidate_path };
|
|
414
|
+
const remaining = inventory.files.filter(entry => entry.pending && entry.destination !== destination).map(entry => entry.destination);
|
|
415
|
+
const receipt = {
|
|
416
|
+
ok: true,
|
|
417
|
+
dry_run: dryRun,
|
|
418
|
+
project_root: root,
|
|
419
|
+
template_version: scaffold.template_version,
|
|
420
|
+
kernel_version: scaffold.kernel_version,
|
|
421
|
+
accepted,
|
|
422
|
+
written: [],
|
|
423
|
+
removed: [],
|
|
424
|
+
pending: dryRun ? [destination, ...remaining] : remaining,
|
|
425
|
+
failed: []
|
|
426
|
+
};
|
|
427
|
+
if (dryRun) return receipt;
|
|
428
|
+
const publish = publisher(root);
|
|
429
|
+
receipt.written = publish.written;
|
|
430
|
+
const digest = publish.base(asset.block ? withNewlinesOf(incoming, unit.bytes) : incoming);
|
|
431
|
+
inventory.files[index] = installedEntry(scaffold, asset, digest);
|
|
432
|
+
writeInventory(root, inventory, publish);
|
|
433
|
+
try {
|
|
434
|
+
sweep(root, inventory);
|
|
435
|
+
} catch (error) {
|
|
436
|
+
receipt.cleanup_warning = error.message;
|
|
437
|
+
}
|
|
438
|
+
return receipt;
|
|
439
|
+
}
|
package/lib/kernel.mjs
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// The Kernel binaries this package carries, and the copy each Workspace runs.
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { AccreteError, readCurrent, sha256 } from './fsutil.mjs';
|
|
6
|
+
|
|
7
|
+
export const BINARY = '.accrete/bin/accrete';
|
|
8
|
+
|
|
9
|
+
// Node's platform and architecture, as the Rust target accrete-cli releases for them.
|
|
10
|
+
export const PLATFORMS = {
|
|
11
|
+
'linux-x64': 'x86_64-unknown-linux-musl',
|
|
12
|
+
'linux-arm64': 'aarch64-unknown-linux-musl',
|
|
13
|
+
'darwin-x64': 'x86_64-apple-darwin',
|
|
14
|
+
'darwin-arm64': 'aarch64-apple-darwin'
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const hostPlatform = () => `${process.platform}-${process.arch}`;
|
|
18
|
+
|
|
19
|
+
export function numericVersion(text) {
|
|
20
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(-[^+]+)?(?:\+.*)?$/.exec(text);
|
|
21
|
+
return match && { numbers: match.slice(1, 4).map(Number), prerelease: match[4] ?? null };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function compareVersions(a, b) {
|
|
25
|
+
for (const index of [0, 1, 2]) {
|
|
26
|
+
if (a.numbers[index] !== b.numbers[index]) return a.numbers[index] - b.numbers[index];
|
|
27
|
+
}
|
|
28
|
+
if (a.prerelease === b.prerelease) return 0;
|
|
29
|
+
if (a.prerelease === null) return 1;
|
|
30
|
+
if (b.prerelease === null) return -1;
|
|
31
|
+
return a.prerelease < b.prerelease ? -1 : 1;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** This host's binary from the package, checked against what fetch-kernel recorded. */
|
|
35
|
+
function embeddedBinary(scaffold) {
|
|
36
|
+
const host = hostPlatform();
|
|
37
|
+
let index = null;
|
|
38
|
+
try {
|
|
39
|
+
index = JSON.parse(fs.readFileSync(path.join(scaffold.kernelDirectory, 'kernel.json'), 'utf8'));
|
|
40
|
+
} catch {
|
|
41
|
+
// No binaries at all reads the same as none for this host.
|
|
42
|
+
}
|
|
43
|
+
const entry = index?.platforms?.[host];
|
|
44
|
+
if (!Object.hasOwn(PLATFORMS, host) || !entry) {
|
|
45
|
+
throw new AccreteError('dependency', `accrete-kit ${scaffold.template_version} has no Kernel for ${host}; `
|
|
46
|
+
+ `it supports ${Object.keys(PLATFORMS).join(', ')}. Elsewhere, extract the accrete ${scaffold.kernel_version} `
|
|
47
|
+
+ 'release archive into <workspace>/.accrete/bin/');
|
|
48
|
+
}
|
|
49
|
+
if (index.version !== scaffold.kernel_version) {
|
|
50
|
+
throw new AccreteError('invalid', `This package carries Kernel ${index.version} but declares ${scaffold.kernel_version}; run scripts/fetch-kernel.mjs`);
|
|
51
|
+
}
|
|
52
|
+
const bytes = fs.readFileSync(path.join(scaffold.kernelDirectory, host, 'accrete'));
|
|
53
|
+
if (sha256(bytes) !== entry.sha256) throw new AccreteError('invalid', `The embedded Kernel for ${host} does not match its recorded digest`);
|
|
54
|
+
return bytes;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function runsAs(target, version) {
|
|
58
|
+
const result = spawnSync(target, ['--version'], { encoding: 'utf8', timeout: 10000 });
|
|
59
|
+
return result.status === 0 && result.stdout.trim() === `accrete ${version}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Put this host's Kernel at .accrete/bin/accrete. A binary already there that runs as the
|
|
64
|
+
* declared version stays, whatever its bytes. `write` publishes and records the path.
|
|
65
|
+
*/
|
|
66
|
+
export function installKernel(root, scaffold, { dryRun = false, write }) {
|
|
67
|
+
const version = scaffold.kernel_version;
|
|
68
|
+
const report = action => ({ version, platform: hostPlatform(), path: BINARY, action });
|
|
69
|
+
const current = readCurrent(root, BINARY);
|
|
70
|
+
if (current.kind === 'other') throw new AccreteError('conflict', `${BINARY} is not a regular file, or a symbolic link is above it`);
|
|
71
|
+
if (current.kind === 'file' && runsAs(path.join(root, BINARY), version)) return report('unchanged');
|
|
72
|
+
const bytes = embeddedBinary(scaffold);
|
|
73
|
+
if (dryRun) return report('would-install');
|
|
74
|
+
write(BINARY, bytes, { mode: 0o755 });
|
|
75
|
+
return report('installed');
|
|
76
|
+
}
|
package/lib/scaffold.mjs
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// The assembled scaffold shipped in this package: a manifest and its assets.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { AccreteError, checkRelative, sha256 } from './fsutil.mjs';
|
|
6
|
+
|
|
7
|
+
export const MARKER = '.accrete/project.toml';
|
|
8
|
+
export const INVENTORY = '.accrete/template-install.json';
|
|
9
|
+
export const BASES = '.accrete/template-bases';
|
|
10
|
+
export const UPGRADES = '.accrete/template-upgrades';
|
|
11
|
+
|
|
12
|
+
const TEMPLATE_ID = 'accrete-foundation';
|
|
13
|
+
// The Kernel's paths, this host's Kernel binary, and this generator's own state.
|
|
14
|
+
const PROTECTED = [
|
|
15
|
+
'.git', MARKER, '.accrete/local.toml', '.accrete/objects', '.accrete/data', '.accrete/migrations',
|
|
16
|
+
'.accrete/bin', INVENTORY, BASES, UPGRADES
|
|
17
|
+
];
|
|
18
|
+
const OWNERSHIP = ['managed-file', 'managed-block', 'seed-once'];
|
|
19
|
+
const invalid = message => new AccreteError('invalid', message);
|
|
20
|
+
|
|
21
|
+
export const within = (destination, parent) => destination === parent || destination.startsWith(`${parent}/`);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Exactly one ordered pair of full-line markers, including the end line's
|
|
25
|
+
* terminator. Returns `none`, `malformed`, or the byte range of the block.
|
|
26
|
+
*/
|
|
27
|
+
export function findBlock(bytes, start, end) {
|
|
28
|
+
const starts = [];
|
|
29
|
+
const ends = [];
|
|
30
|
+
let offset = 0;
|
|
31
|
+
while (offset < bytes.length) {
|
|
32
|
+
const newline = bytes.indexOf(0x0a, offset);
|
|
33
|
+
const next = newline === -1 ? bytes.length : newline + 1;
|
|
34
|
+
const line = bytes.toString('latin1', offset, newline === -1 ? bytes.length : newline).replace(/\r$/, '');
|
|
35
|
+
if (line === start) starts.push(offset);
|
|
36
|
+
if (line === end) ends.push(next);
|
|
37
|
+
offset = next;
|
|
38
|
+
}
|
|
39
|
+
if (starts.length === 0 && ends.length === 0) return { kind: 'none' };
|
|
40
|
+
if (starts.length === 1 && ends.length === 1 && starts[0] < ends[0]) return { kind: 'one', start: starts[0], end: ends[0] };
|
|
41
|
+
return { kind: 'malformed' };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function checkEntry(manifest, entry, files) {
|
|
45
|
+
checkRelative(entry.source);
|
|
46
|
+
checkRelative(entry.destination);
|
|
47
|
+
if (PROTECTED.some(parent => within(entry.destination, parent))) {
|
|
48
|
+
throw invalid(`Scaffold destination overlaps Workspace state: ${entry.destination}`);
|
|
49
|
+
}
|
|
50
|
+
if (entry.profile !== null && !Object.hasOwn(manifest.profiles, entry.profile)) {
|
|
51
|
+
throw invalid(`${entry.destination} names unknown profile ${entry.profile}`);
|
|
52
|
+
}
|
|
53
|
+
if (!OWNERSHIP.includes(entry.ownership)) throw invalid(`${entry.destination} has unknown ownership ${entry.ownership}`);
|
|
54
|
+
const bytes = files.get(entry.source);
|
|
55
|
+
if (bytes.length !== entry.size || sha256(bytes) !== entry.sha256) {
|
|
56
|
+
throw invalid(`Scaffold asset does not match its manifest: ${entry.source}`);
|
|
57
|
+
}
|
|
58
|
+
if ((entry.ownership === 'managed-block') !== Boolean(entry.block)) {
|
|
59
|
+
throw invalid(`Only a managed block entry has markers, and it must have them: ${entry.destination}`);
|
|
60
|
+
}
|
|
61
|
+
if (entry.block) {
|
|
62
|
+
const block = findBlock(bytes, entry.block.start, entry.block.end);
|
|
63
|
+
if (block.kind !== 'one' || sha256(bytes.subarray(block.start, block.end)) !== entry.block.sha256) {
|
|
64
|
+
throw invalid(`Scaffold block does not match its markers and digest: ${entry.source}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function loadScaffold(directory = fileURLToPath(new URL('../dist/scaffold', import.meta.url))) {
|
|
70
|
+
let manifest;
|
|
71
|
+
let defaults;
|
|
72
|
+
try {
|
|
73
|
+
manifest = JSON.parse(fs.readFileSync(path.join(directory, 'manifest.json'), 'utf8'));
|
|
74
|
+
defaults = fs.readFileSync(path.join(directory, 'config/defaults.toml'), 'utf8');
|
|
75
|
+
} catch (error) {
|
|
76
|
+
throw invalid(`Cannot read the scaffold in ${directory}; run npm run build: ${error.message}`);
|
|
77
|
+
}
|
|
78
|
+
if (manifest.schema_version !== 1 || manifest.template_id !== TEMPLATE_ID) {
|
|
79
|
+
throw new AccreteError('unsupported_format', `Unsupported scaffold ${manifest.template_id} schema ${manifest.schema_version}`);
|
|
80
|
+
}
|
|
81
|
+
const files = new Map();
|
|
82
|
+
for (const { source } of manifest.files) {
|
|
83
|
+
if (files.has(source)) continue;
|
|
84
|
+
try {
|
|
85
|
+
files.set(checkRelative(source), fs.readFileSync(path.join(directory, source)));
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (error instanceof AccreteError) throw error;
|
|
88
|
+
throw invalid(`Scaffold asset is missing: ${source}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const seen = new Set();
|
|
92
|
+
for (const entry of manifest.files) {
|
|
93
|
+
checkEntry(manifest, entry, files);
|
|
94
|
+
const folded = entry.destination.toLowerCase();
|
|
95
|
+
if (seen.has(folded)) throw invalid(`Scaffold destination repeats ignoring case: ${entry.destination}`);
|
|
96
|
+
seen.add(folded);
|
|
97
|
+
}
|
|
98
|
+
for (const a of seen) {
|
|
99
|
+
for (const b of seen) {
|
|
100
|
+
if (a !== b && within(b, a)) throw invalid(`Scaffold destinations overlap: ${a} and ${b}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
...manifest,
|
|
105
|
+
defaults,
|
|
106
|
+
kernelDirectory: path.join(directory, '..', 'kernel'),
|
|
107
|
+
read: source => files.get(source),
|
|
108
|
+
// The update unit of an asset: its managed section, or the whole file.
|
|
109
|
+
unit(asset) {
|
|
110
|
+
const bytes = files.get(asset.source);
|
|
111
|
+
if (!asset.block) return bytes;
|
|
112
|
+
const block = findBlock(bytes, asset.block.start, asset.block.end);
|
|
113
|
+
return bytes.subarray(block.start, block.end);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** `codex,claude`, `codex`, `claude` or `none`, in the manifest's order. */
|
|
119
|
+
export function parseAgents(scaffold, text) {
|
|
120
|
+
if (text === 'none') return [];
|
|
121
|
+
const wanted = new Set(text.split(','));
|
|
122
|
+
for (const name of wanted) {
|
|
123
|
+
if (!Object.hasOwn(scaffold.profiles, name)) {
|
|
124
|
+
throw invalid(`--agents takes codex,claude, codex, claude or none; unknown host ${JSON.stringify(name)}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const ordered = scaffold.default_profiles.filter(name => wanted.has(name));
|
|
128
|
+
return [...ordered, ...[...wanted].filter(name => !ordered.includes(name))];
|
|
129
|
+
}
|