@xiongxianfei/rigorloop 0.1.4
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 +5 -0
- package/dist/bin/rigorloop.js +1499 -0
- package/dist/lib/command-result.js +33 -0
- package/dist/lib/lockfile.js +303 -0
- package/dist/lib/new-change-filesystem.js +154 -0
- package/dist/lib/new-change.js +226 -0
- package/dist/lib/official-archive-url.js +42 -0
- package/dist/metadata/adapter-artifacts-v0.1.4.json +30 -0
- package/dist/metadata/releases.json +11 -0
- package/package.json +19 -0
|
@@ -0,0 +1,1499 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { inflateRawSync } from "node:zlib";
|
|
6
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
import { EXIT, exitCodeForResult } from "../lib/command-result.js";
|
|
10
|
+
import { parseLockfile, serializeLockfile, sha256NormalizedText } from "../lib/lockfile.js";
|
|
11
|
+
import { buildNewChangeDraft, parseNewChangeArgs } from "../lib/new-change.js";
|
|
12
|
+
import { runNewChangePlan } from "../lib/new-change-filesystem.js";
|
|
13
|
+
import { validateOfficialArchiveUrl } from "../lib/official-archive-url.js";
|
|
14
|
+
|
|
15
|
+
const ADAPTER = "codex";
|
|
16
|
+
const AGENTS_ROOT = ".agents";
|
|
17
|
+
const INSTALL_ROOT = ".agents/skills";
|
|
18
|
+
const LOCKFILE_PATH = "rigorloop.lock";
|
|
19
|
+
const DIRECTORY_PLAN = [AGENTS_ROOT, INSTALL_ROOT];
|
|
20
|
+
|
|
21
|
+
function packageInfo() {
|
|
22
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const packageJsonPath = join(here, "..", "..", "package.json");
|
|
24
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
25
|
+
if (!packageJson.name || !packageJson.version) {
|
|
26
|
+
throw new Error("Package name or version is missing.");
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
name: packageJson.name,
|
|
30
|
+
version: packageJson.version,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseFlags(args) {
|
|
35
|
+
const flags = {
|
|
36
|
+
json: false,
|
|
37
|
+
quiet: false,
|
|
38
|
+
debug: false,
|
|
39
|
+
noColor: Boolean(process.env.NO_COLOR),
|
|
40
|
+
dryRun: false,
|
|
41
|
+
adapter: undefined,
|
|
42
|
+
fromArchiveProvided: false,
|
|
43
|
+
fromArchive: undefined,
|
|
44
|
+
force: false,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const positional = [];
|
|
48
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
49
|
+
const arg = args[index];
|
|
50
|
+
if (arg === "--json") {
|
|
51
|
+
flags.json = true;
|
|
52
|
+
} else if (arg === "--quiet") {
|
|
53
|
+
flags.quiet = true;
|
|
54
|
+
} else if (arg === "--debug") {
|
|
55
|
+
flags.debug = true;
|
|
56
|
+
} else if (arg === "--no-color") {
|
|
57
|
+
flags.noColor = true;
|
|
58
|
+
} else if (arg === "--dry-run") {
|
|
59
|
+
flags.dryRun = true;
|
|
60
|
+
} else if (arg === "--adapter") {
|
|
61
|
+
if (args[index + 1] && !args[index + 1].startsWith("--")) {
|
|
62
|
+
flags.adapter = args[index + 1];
|
|
63
|
+
index += 1;
|
|
64
|
+
}
|
|
65
|
+
} else if (arg === "--from-archive") {
|
|
66
|
+
flags.fromArchiveProvided = true;
|
|
67
|
+
if (args[index + 1] && !args[index + 1].startsWith("--")) {
|
|
68
|
+
flags.fromArchive = args[index + 1];
|
|
69
|
+
index += 1;
|
|
70
|
+
}
|
|
71
|
+
} else if (arg === "--force") {
|
|
72
|
+
flags.force = true;
|
|
73
|
+
} else {
|
|
74
|
+
positional.push(arg);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { flags, positional };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function envelope(command, flags, overrides = {}) {
|
|
82
|
+
const diagnostics = flags.debug ? { debug: true } : {};
|
|
83
|
+
return {
|
|
84
|
+
schema_version: 1,
|
|
85
|
+
command,
|
|
86
|
+
package: packageInfo(),
|
|
87
|
+
cwd: process.cwd(),
|
|
88
|
+
status: "success",
|
|
89
|
+
summary: "",
|
|
90
|
+
actions: [],
|
|
91
|
+
artifacts: [],
|
|
92
|
+
blockers: [],
|
|
93
|
+
warnings: [],
|
|
94
|
+
errors: [],
|
|
95
|
+
diagnostics,
|
|
96
|
+
...overrides,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function writeJson(result) {
|
|
101
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function writeHuman(message, flags) {
|
|
105
|
+
if (!flags.quiet) {
|
|
106
|
+
process.stdout.write(message);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function usage() {
|
|
111
|
+
return `RigorLoop CLI
|
|
112
|
+
|
|
113
|
+
Usage:
|
|
114
|
+
rigorloop --help
|
|
115
|
+
rigorloop version
|
|
116
|
+
rigorloop init --adapter codex [--dry-run] [--json]
|
|
117
|
+
rigorloop new-change <change-id> --title <title> [--dry-run] [--json]
|
|
118
|
+
|
|
119
|
+
Commands:
|
|
120
|
+
version Print package name and version.
|
|
121
|
+
init --adapter codex Initialize the first-slice Codex adapter plan.
|
|
122
|
+
new-change Plan a change metadata scaffold.
|
|
123
|
+
`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function releaseForPackage(version) {
|
|
127
|
+
return `v${version}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function sourceForFlags(flags, info) {
|
|
131
|
+
if (flags.fromArchiveProvided) {
|
|
132
|
+
return {
|
|
133
|
+
type: "local-archive",
|
|
134
|
+
archive: flags.fromArchive,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
type: "release-archive",
|
|
140
|
+
release: releaseForPackage(info.version),
|
|
141
|
+
archive: `rigorloop-adapter-codex-${releaseForPackage(info.version)}.zip`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function manifestContent(info, source) {
|
|
146
|
+
const sourceLines =
|
|
147
|
+
source.type === "local-archive"
|
|
148
|
+
? [` type: local-archive`, ` archive: "${source.archive}"`]
|
|
149
|
+
: [` type: release-archive`, ` release: "${source.release}"`];
|
|
150
|
+
|
|
151
|
+
return `schema_version: 1
|
|
152
|
+
rigorloop:
|
|
153
|
+
package: "${info.name}"
|
|
154
|
+
package_version: "${info.version}"
|
|
155
|
+
adapters:
|
|
156
|
+
- name: codex
|
|
157
|
+
install_root: "${INSTALL_ROOT}"
|
|
158
|
+
source:
|
|
159
|
+
${sourceLines.join("\n")}
|
|
160
|
+
`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function plannedLockfile(info, source, manifest) {
|
|
164
|
+
const artifact = source.artifact;
|
|
165
|
+
return {
|
|
166
|
+
schema_version: 1,
|
|
167
|
+
rigorloop: {
|
|
168
|
+
package: info.name,
|
|
169
|
+
version: info.version,
|
|
170
|
+
},
|
|
171
|
+
manifest: {
|
|
172
|
+
path: "rigorloop.yaml",
|
|
173
|
+
sha256: sha256NormalizedText(manifest),
|
|
174
|
+
},
|
|
175
|
+
generated: {
|
|
176
|
+
adapters: [
|
|
177
|
+
{
|
|
178
|
+
adapter: ADAPTER,
|
|
179
|
+
release: releaseForPackage(info.version),
|
|
180
|
+
source: source.type,
|
|
181
|
+
archive: source.type === "local-archive" ? basename(source.archive) : source.archive,
|
|
182
|
+
archive_sha256: artifact?.sha256 ?? "<planned>",
|
|
183
|
+
installed_root: INSTALL_ROOT,
|
|
184
|
+
tree_hash_algorithm: "rigorloop-tree-hash-v1",
|
|
185
|
+
tree_sha256: artifact?.tree_sha256 ?? "<planned-after-install>",
|
|
186
|
+
file_count: "<planned-after-install>",
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function lockfileForVerifiedInstall(info, source, manifest, artifact, treeHash, fileCount) {
|
|
194
|
+
return {
|
|
195
|
+
schema_version: 1,
|
|
196
|
+
rigorloop: {
|
|
197
|
+
package: info.name,
|
|
198
|
+
version: info.version,
|
|
199
|
+
},
|
|
200
|
+
manifest: {
|
|
201
|
+
path: "rigorloop.yaml",
|
|
202
|
+
sha256: sha256NormalizedText(manifest),
|
|
203
|
+
},
|
|
204
|
+
generated: {
|
|
205
|
+
adapters: [
|
|
206
|
+
{
|
|
207
|
+
adapter: ADAPTER,
|
|
208
|
+
release: releaseForPackage(info.version),
|
|
209
|
+
source: source.type,
|
|
210
|
+
archive: source.type === "local-archive" ? basename(source.archive) : source.archive,
|
|
211
|
+
archive_sha256: artifact.sha256,
|
|
212
|
+
installed_root: INSTALL_ROOT,
|
|
213
|
+
tree_hash_algorithm: "rigorloop-tree-hash-v1",
|
|
214
|
+
tree_sha256: treeHash,
|
|
215
|
+
file_count: fileCount,
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function compatibleManifest(content) {
|
|
223
|
+
return (
|
|
224
|
+
content.includes("schema_version: 1") &&
|
|
225
|
+
content.includes("name: codex") &&
|
|
226
|
+
content.includes(`install_root: "${INSTALL_ROOT}"`)
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function pathState(path) {
|
|
231
|
+
if (!existsSync(path)) {
|
|
232
|
+
return "absent";
|
|
233
|
+
}
|
|
234
|
+
return statSync(path).isDirectory() ? "directory" : "file";
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function directoryKind(path) {
|
|
238
|
+
return path === AGENTS_ROOT ? "codex-agent-root" : "codex-install-root";
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function sha256(bytes) {
|
|
242
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function normalizeText(bytes) {
|
|
246
|
+
let text = bytes.toString("utf8");
|
|
247
|
+
if (text.charCodeAt(0) === 0xfeff) {
|
|
248
|
+
text = text.slice(1);
|
|
249
|
+
}
|
|
250
|
+
return Buffer.from(text.replace(/\r\n?/g, "\n"), "utf8");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function metadataDirectory() {
|
|
254
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
255
|
+
return join(here, "..", "metadata");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function releaseIndexPath() {
|
|
259
|
+
return join(metadataDirectory(), "releases.json");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function loadReleaseDescriptor(info) {
|
|
263
|
+
const release = releaseForPackage(info.version);
|
|
264
|
+
let index;
|
|
265
|
+
try {
|
|
266
|
+
index = loadJsonFile(releaseIndexPath());
|
|
267
|
+
} catch {
|
|
268
|
+
return {
|
|
269
|
+
blocker: metadataBlocker(
|
|
270
|
+
"metadata-trust-root-unavailable",
|
|
271
|
+
"Bundled release metadata index is unavailable.",
|
|
272
|
+
"releases.json",
|
|
273
|
+
"Use a CLI package version that bundles release metadata for this adapter release.",
|
|
274
|
+
),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const descriptor = index?.schema_version === 1 ? index.releases?.[release] : undefined;
|
|
279
|
+
if (
|
|
280
|
+
!descriptor ||
|
|
281
|
+
descriptor.source_repository !== "xiongxianfei/rigorloop" ||
|
|
282
|
+
descriptor.release_tag !== release ||
|
|
283
|
+
!isNonEmptyString(descriptor.bundled_metadata)
|
|
284
|
+
) {
|
|
285
|
+
return {
|
|
286
|
+
blocker: metadataBlocker(
|
|
287
|
+
"metadata-trust-root-unavailable",
|
|
288
|
+
`Bundled release metadata index does not define a trusted Codex metadata source for ${release}.`,
|
|
289
|
+
"releases.json",
|
|
290
|
+
"Use a CLI package version that bundles release metadata for this adapter release.",
|
|
291
|
+
),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
return { descriptor };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function loadVerifiedBundledMetadata(info) {
|
|
298
|
+
const release = loadReleaseDescriptor(info);
|
|
299
|
+
const releaseTag = releaseForPackage(info.version);
|
|
300
|
+
if (release.blocker) {
|
|
301
|
+
return release;
|
|
302
|
+
}
|
|
303
|
+
const descriptor = release.descriptor;
|
|
304
|
+
if (!isSha256(descriptor.bundled_metadata_sha256)) {
|
|
305
|
+
return {
|
|
306
|
+
blocker: metadataBlocker(
|
|
307
|
+
"metadata-trust-root-unavailable",
|
|
308
|
+
"Bundled release metadata index is missing a trusted bundled metadata SHA-256.",
|
|
309
|
+
"releases.json",
|
|
310
|
+
"Use a CLI package version that bundles a trusted adapter metadata hash.",
|
|
311
|
+
),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
let metadataBytes;
|
|
315
|
+
try {
|
|
316
|
+
metadataBytes = readFileSync(join(metadataDirectory(), descriptor.bundled_metadata));
|
|
317
|
+
} catch {
|
|
318
|
+
return {
|
|
319
|
+
blocker: metadataBlocker(
|
|
320
|
+
"metadata-unavailable",
|
|
321
|
+
`Bundled adapter metadata is unavailable for Codex ${releaseTag}.`,
|
|
322
|
+
descriptor.bundled_metadata,
|
|
323
|
+
"Use a CLI package version that bundles metadata for this adapter release.",
|
|
324
|
+
),
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
const verifiedMetadata = parseVerifiedMetadataBytes(metadataBytes, descriptor.bundled_metadata_sha256);
|
|
328
|
+
if (verifiedMetadata.error) {
|
|
329
|
+
return { error: verifiedMetadata.error };
|
|
330
|
+
}
|
|
331
|
+
return { metadata: verifiedMetadata.metadata, descriptor };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function loadJsonFile(path) {
|
|
335
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function fetchBytes(url) {
|
|
339
|
+
const response = await fetch(url);
|
|
340
|
+
if (!response.ok) {
|
|
341
|
+
throw new Error(`HTTP ${response.status}`);
|
|
342
|
+
}
|
|
343
|
+
return Buffer.from(await response.arrayBuffer());
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function parseVerifiedMetadataBytes(bytes, expectedSha256) {
|
|
347
|
+
const actualSha256 = sha256(bytes);
|
|
348
|
+
if (actualSha256 !== expectedSha256) {
|
|
349
|
+
return {
|
|
350
|
+
error: {
|
|
351
|
+
code: "metadata-sha256-mismatch",
|
|
352
|
+
message: "Bundled adapter metadata SHA-256 does not match the bundled release index.",
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
try {
|
|
357
|
+
return { metadata: JSON.parse(bytes.toString("utf8")) };
|
|
358
|
+
} catch (error) {
|
|
359
|
+
return {
|
|
360
|
+
error: {
|
|
361
|
+
code: "metadata-invalid",
|
|
362
|
+
message: `Release metadata JSON is invalid: ${error.message}`,
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function metadataBlocker(code, message, path, nextAction = "Use a compatible verified Codex adapter archive.") {
|
|
369
|
+
return {
|
|
370
|
+
code,
|
|
371
|
+
message,
|
|
372
|
+
path,
|
|
373
|
+
next_action: nextAction,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function isNonEmptyString(value) {
|
|
378
|
+
return typeof value === "string" && value.length > 0;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function isSha256(value) {
|
|
382
|
+
return typeof value === "string" && /^[0-9a-f]{64}$/i.test(value);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function validateMetadata(metadata, info) {
|
|
386
|
+
const release = releaseForPackage(info.version);
|
|
387
|
+
if (!metadata || metadata.schema_version !== 1) {
|
|
388
|
+
return { error: { code: "metadata-invalid", message: "Adapter metadata schema_version must be 1." } };
|
|
389
|
+
}
|
|
390
|
+
if (metadata.release?.version !== release || metadata.release?.release_tag !== release) {
|
|
391
|
+
return {
|
|
392
|
+
blocker: metadataBlocker("release-version-incompatible", `Adapter metadata is not compatible with ${release}.`),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
if (metadata.release?.source_repository !== "xiongxianfei/rigorloop") {
|
|
396
|
+
return { error: { code: "metadata-invalid", message: "Adapter metadata source repository is not trusted." } };
|
|
397
|
+
}
|
|
398
|
+
if (!isNonEmptyString(metadata.release?.source_commit) || !isNonEmptyString(metadata.release?.published_at)) {
|
|
399
|
+
return { error: { code: "metadata-invalid", message: "Adapter metadata release identity is incomplete." } };
|
|
400
|
+
}
|
|
401
|
+
if (!isNonEmptyString(metadata.metadata?.url) || !isSha256(metadata.metadata?.sha256)) {
|
|
402
|
+
return { error: { code: "metadata-invalid", message: "Adapter metadata URL or SHA-256 is missing or invalid." } };
|
|
403
|
+
}
|
|
404
|
+
if (metadata.validation?.result !== "pass") {
|
|
405
|
+
return { error: { code: "metadata-invalid", message: "Adapter metadata validation result is not pass." } };
|
|
406
|
+
}
|
|
407
|
+
if (!isNonEmptyString(metadata.validation?.command)) {
|
|
408
|
+
return { error: { code: "metadata-invalid", message: "Adapter metadata validation command is missing." } };
|
|
409
|
+
}
|
|
410
|
+
const artifact = metadata.artifacts?.find((entry) => entry.adapter === ADAPTER);
|
|
411
|
+
if (!artifact) {
|
|
412
|
+
return { blocker: metadataBlocker("adapter-unknown", "Adapter metadata does not include Codex.") };
|
|
413
|
+
}
|
|
414
|
+
if (
|
|
415
|
+
!isNonEmptyString(artifact.archive) ||
|
|
416
|
+
!isNonEmptyString(artifact.url) ||
|
|
417
|
+
!isSha256(artifact.sha256) ||
|
|
418
|
+
!Number.isInteger(artifact.size_bytes) ||
|
|
419
|
+
artifact.size_bytes < 0 ||
|
|
420
|
+
!isSha256(artifact.tree_sha256)
|
|
421
|
+
) {
|
|
422
|
+
return { error: { code: "metadata-invalid", message: "Codex adapter artifact metadata is incomplete." } };
|
|
423
|
+
}
|
|
424
|
+
if ((artifact.install_root ?? "").replace(/\/$/, "") !== INSTALL_ROOT) {
|
|
425
|
+
return { error: { code: "metadata-invalid", message: "Codex adapter install root is not .agents/skills." } };
|
|
426
|
+
}
|
|
427
|
+
if (artifact.tree_hash_algorithm && artifact.tree_hash_algorithm !== "rigorloop-tree-hash-v1") {
|
|
428
|
+
return { error: { code: "metadata-invalid", message: "Unsupported tree hash algorithm in adapter metadata." } };
|
|
429
|
+
}
|
|
430
|
+
return { artifact };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function readUInt16(buffer, offset) {
|
|
434
|
+
return buffer.readUInt16LE(offset);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function readUInt32(buffer, offset) {
|
|
438
|
+
return buffer.readUInt32LE(offset);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function findEndOfCentralDirectory(buffer) {
|
|
442
|
+
for (let offset = buffer.length - 22; offset >= 0; offset -= 1) {
|
|
443
|
+
if (readUInt32(buffer, offset) === 0x06054b50) {
|
|
444
|
+
return offset;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
throw Object.assign(new Error("Archive is not a valid ZIP file."), { code: "archive-invalid" });
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function parseZipEntries(buffer) {
|
|
451
|
+
const eocd = findEndOfCentralDirectory(buffer);
|
|
452
|
+
const entryCount = readUInt16(buffer, eocd + 10);
|
|
453
|
+
const centralOffset = readUInt32(buffer, eocd + 16);
|
|
454
|
+
let offset = centralOffset;
|
|
455
|
+
const entries = [];
|
|
456
|
+
|
|
457
|
+
for (let index = 0; index < entryCount; index += 1) {
|
|
458
|
+
if (readUInt32(buffer, offset) !== 0x02014b50) {
|
|
459
|
+
throw Object.assign(new Error("Archive central directory is invalid."), { code: "archive-invalid" });
|
|
460
|
+
}
|
|
461
|
+
const method = readUInt16(buffer, offset + 10);
|
|
462
|
+
const compressedSize = readUInt32(buffer, offset + 20);
|
|
463
|
+
const uncompressedSize = readUInt32(buffer, offset + 24);
|
|
464
|
+
const nameLength = readUInt16(buffer, offset + 28);
|
|
465
|
+
const extraLength = readUInt16(buffer, offset + 30);
|
|
466
|
+
const commentLength = readUInt16(buffer, offset + 32);
|
|
467
|
+
const externalAttributes = readUInt32(buffer, offset + 38);
|
|
468
|
+
const localOffset = readUInt32(buffer, offset + 42);
|
|
469
|
+
const name = buffer.subarray(offset + 46, offset + 46 + nameLength).toString("utf8");
|
|
470
|
+
|
|
471
|
+
if (readUInt32(buffer, localOffset) !== 0x04034b50) {
|
|
472
|
+
throw Object.assign(new Error("Archive local header is invalid."), { code: "archive-invalid" });
|
|
473
|
+
}
|
|
474
|
+
const localNameLength = readUInt16(buffer, localOffset + 26);
|
|
475
|
+
const localExtraLength = readUInt16(buffer, localOffset + 28);
|
|
476
|
+
const dataStart = localOffset + 30 + localNameLength + localExtraLength;
|
|
477
|
+
const compressed = buffer.subarray(dataStart, dataStart + compressedSize);
|
|
478
|
+
let bytes;
|
|
479
|
+
if (method === 0) {
|
|
480
|
+
bytes = compressed;
|
|
481
|
+
} else if (method === 8) {
|
|
482
|
+
bytes = inflateRawSync(compressed);
|
|
483
|
+
} else {
|
|
484
|
+
throw Object.assign(new Error("Archive uses unsupported compression."), { code: "archive-unsupported-compression" });
|
|
485
|
+
}
|
|
486
|
+
if (bytes.length !== uncompressedSize) {
|
|
487
|
+
throw Object.assign(new Error("Archive entry size is invalid."), { code: "archive-invalid" });
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const unixMode = (externalAttributes >>> 16) & 0xffff;
|
|
491
|
+
entries.push({
|
|
492
|
+
name,
|
|
493
|
+
bytes,
|
|
494
|
+
directory: name.endsWith("/"),
|
|
495
|
+
symlink: (unixMode & 0o170000) === 0o120000,
|
|
496
|
+
});
|
|
497
|
+
offset += 46 + nameLength + extraLength + commentLength;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return entries;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function unsafePathCode(name) {
|
|
504
|
+
if (!name || name.startsWith("/") || name.startsWith("\\") || /^[A-Za-z]:/.test(name) || name.includes("\\")) {
|
|
505
|
+
return "archive-path-traversal";
|
|
506
|
+
}
|
|
507
|
+
if (name.split("/").some((part) => part === ".." || part === "")) {
|
|
508
|
+
return "archive-path-traversal";
|
|
509
|
+
}
|
|
510
|
+
if (!name.startsWith(`${INSTALL_ROOT}/`)) {
|
|
511
|
+
return "archive-install-root-invalid";
|
|
512
|
+
}
|
|
513
|
+
return undefined;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function isArchiveSupportEntry(name) {
|
|
517
|
+
return name === "AGENTS.md";
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function fileRowsForTree(entries) {
|
|
521
|
+
return entries
|
|
522
|
+
.filter((entry) => !entry.directory)
|
|
523
|
+
.map((entry) => {
|
|
524
|
+
const relativePath = entry.name.slice(`${INSTALL_ROOT}/`.length);
|
|
525
|
+
const bytes = relativePath.endsWith(".md") ? normalizeText(entry.bytes) : entry.bytes;
|
|
526
|
+
return [relativePath, sha256(bytes)];
|
|
527
|
+
})
|
|
528
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function treeHashForEntries(entries) {
|
|
532
|
+
return treeHashForRows(fileRowsForTree(entries));
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function treeHashForRows(rows) {
|
|
536
|
+
const manifest = `rigorloop-tree-hash-v1\n${rows.map(([path, hash]) => `${path}\t${hash}`).join("\n")}\n`;
|
|
537
|
+
return sha256(Buffer.from(manifest, "utf8"));
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function rowsEqual(left, right) {
|
|
541
|
+
if (left.length !== right.length) {
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
return left.every(([path, hash], index) => path === right[index][0] && hash === right[index][1]);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function fileRowsForFilesystem(root) {
|
|
548
|
+
const rows = [];
|
|
549
|
+
function visit(relativeDirectory) {
|
|
550
|
+
const absoluteDirectory = resolve(process.cwd(), root, relativeDirectory);
|
|
551
|
+
for (const name of readdirSync(absoluteDirectory).sort()) {
|
|
552
|
+
const relativePath = relativeDirectory ? `${relativeDirectory}/${name}` : name;
|
|
553
|
+
const absolutePath = resolve(process.cwd(), root, relativePath);
|
|
554
|
+
const stat = statSync(absolutePath);
|
|
555
|
+
if (stat.isDirectory()) {
|
|
556
|
+
visit(relativePath);
|
|
557
|
+
} else if (stat.isFile()) {
|
|
558
|
+
const bytes = relativePath.endsWith(".md") ? normalizeText(readFileSync(absolutePath)) : readFileSync(absolutePath);
|
|
559
|
+
rows.push([relativePath, sha256(bytes)]);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
visit("");
|
|
564
|
+
return rows.sort(([left], [right]) => left.localeCompare(right));
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function treeHashForFilesystem(root) {
|
|
568
|
+
const rows = fileRowsForFilesystem(root);
|
|
569
|
+
return {
|
|
570
|
+
rows,
|
|
571
|
+
treeHash: treeHashForRows(rows),
|
|
572
|
+
fileCount: rows.length,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function currentCodexLockfileEntry() {
|
|
577
|
+
const lockfileAbsolutePath = resolve(process.cwd(), LOCKFILE_PATH);
|
|
578
|
+
if (!existsSync(lockfileAbsolutePath)) {
|
|
579
|
+
return undefined;
|
|
580
|
+
}
|
|
581
|
+
const parsed = parseLockfile(readFileSync(lockfileAbsolutePath, "utf8"));
|
|
582
|
+
if (!parsed.ok) {
|
|
583
|
+
return undefined;
|
|
584
|
+
}
|
|
585
|
+
return parsed.lockfile.generated.adapters.find((entry) => entry.adapter === ADAPTER && entry.installed_root === INSTALL_ROOT);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function installedTreeMismatchError(actualTree, expectedTreeHash, expectedFileCount) {
|
|
589
|
+
return {
|
|
590
|
+
code: "installed-tree-mismatch",
|
|
591
|
+
message: "Installed Codex adapter tree does not match trusted metadata.",
|
|
592
|
+
expected_tree_sha256: expectedTreeHash,
|
|
593
|
+
actual_tree_sha256: actualTree.treeHash,
|
|
594
|
+
expected_file_count: expectedFileCount,
|
|
595
|
+
actual_file_count: actualTree.fileCount,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function verifyInstalledTree(entries, artifact, { allowMissingOrEmpty = false } = {}) {
|
|
600
|
+
const expectedRows = fileRowsForTree(entries);
|
|
601
|
+
const expectedTreeHash = artifact.tree_sha256;
|
|
602
|
+
const expectedFileCount = expectedRows.length;
|
|
603
|
+
|
|
604
|
+
if (!existsSync(resolve(process.cwd(), INSTALL_ROOT))) {
|
|
605
|
+
return allowMissingOrEmpty ? { ok: true, expectedRows, expectedFileCount } : { error: installedTreeMismatchError({ treeHash: "<missing>", fileCount: 0 }, expectedTreeHash, expectedFileCount) };
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const actualTree = treeHashForFilesystem(INSTALL_ROOT);
|
|
609
|
+
if (allowMissingOrEmpty && actualTree.fileCount === 0) {
|
|
610
|
+
return { ok: true, expectedRows, expectedFileCount };
|
|
611
|
+
}
|
|
612
|
+
if (!rowsEqual(actualTree.rows, expectedRows) || actualTree.treeHash !== expectedTreeHash) {
|
|
613
|
+
return { error: installedTreeMismatchError(actualTree, expectedTreeHash, expectedFileCount) };
|
|
614
|
+
}
|
|
615
|
+
return { ok: true, expectedRows, expectedFileCount, treeHash: actualTree.treeHash };
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function generatedOutputConflictBlocker(entries) {
|
|
619
|
+
const directories = new Set();
|
|
620
|
+
for (const entry of entries) {
|
|
621
|
+
const parts = entry.name.split("/");
|
|
622
|
+
parts.pop();
|
|
623
|
+
while (parts.length > 2) {
|
|
624
|
+
directories.add(parts.join("/"));
|
|
625
|
+
parts.pop();
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
for (const directory of [...directories].sort()) {
|
|
630
|
+
if (pathState(resolve(process.cwd(), directory)) === "file") {
|
|
631
|
+
return {
|
|
632
|
+
code: "overwrite-refused",
|
|
633
|
+
message: `${directory} exists and is not a directory.`,
|
|
634
|
+
path: directory,
|
|
635
|
+
next_action: "Move the existing file before running init.",
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
for (const entry of entries) {
|
|
641
|
+
if (pathState(resolve(process.cwd(), entry.name)) === "directory") {
|
|
642
|
+
return {
|
|
643
|
+
code: "overwrite-refused",
|
|
644
|
+
message: `${entry.name} exists and is not a file.`,
|
|
645
|
+
path: entry.name,
|
|
646
|
+
next_action: "Move the existing directory before running init.",
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return undefined;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function lockfileDriftBlocker(lockfileEntry) {
|
|
655
|
+
if (!lockfileEntry) {
|
|
656
|
+
return undefined;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const rootState = pathState(resolve(process.cwd(), lockfileEntry.installed_root));
|
|
660
|
+
if (rootState === "absent") {
|
|
661
|
+
return {
|
|
662
|
+
code: "generated-output-missing",
|
|
663
|
+
message: "Codex generated output recorded in rigorloop.lock is missing.",
|
|
664
|
+
adapter: lockfileEntry.adapter,
|
|
665
|
+
installed_root: lockfileEntry.installed_root,
|
|
666
|
+
expected_tree_sha256: lockfileEntry.tree_sha256,
|
|
667
|
+
actual_tree_sha256: null,
|
|
668
|
+
next_action: "Restore the recorded generated output or resolve drift before running init.",
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
if (rootState !== "directory") {
|
|
672
|
+
return {
|
|
673
|
+
code: "overwrite-refused",
|
|
674
|
+
message: `${lockfileEntry.installed_root} exists and is not a directory.`,
|
|
675
|
+
path: lockfileEntry.installed_root,
|
|
676
|
+
next_action: "Move the existing file before running init.",
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const actualTree = treeHashForFilesystem(lockfileEntry.installed_root);
|
|
681
|
+
if (actualTree.treeHash !== lockfileEntry.tree_sha256 || actualTree.fileCount !== lockfileEntry.file_count) {
|
|
682
|
+
return {
|
|
683
|
+
code: "generated-output-drift",
|
|
684
|
+
message: "Codex generated output differs from rigorloop.lock.",
|
|
685
|
+
adapter: lockfileEntry.adapter,
|
|
686
|
+
installed_root: lockfileEntry.installed_root,
|
|
687
|
+
expected_tree_sha256: lockfileEntry.tree_sha256,
|
|
688
|
+
actual_tree_sha256: actualTree.treeHash,
|
|
689
|
+
expected_file_count: lockfileEntry.file_count,
|
|
690
|
+
actual_file_count: actualTree.fileCount,
|
|
691
|
+
next_action: "Resolve generated output drift before running init.",
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return undefined;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function inspectArchive(archiveBytes, artifact) {
|
|
699
|
+
if (artifact.size_bytes !== undefined && archiveBytes.length !== artifact.size_bytes) {
|
|
700
|
+
return { error: { code: "archive-size-mismatch", message: "Archive size does not match metadata." } };
|
|
701
|
+
}
|
|
702
|
+
const archiveHash = sha256(archiveBytes);
|
|
703
|
+
if (artifact.sha256 && archiveHash !== artifact.sha256) {
|
|
704
|
+
return { error: { code: "archive-sha-mismatch", message: "Archive SHA-256 does not match metadata." } };
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
let entries;
|
|
708
|
+
try {
|
|
709
|
+
entries = parseZipEntries(archiveBytes);
|
|
710
|
+
} catch (error) {
|
|
711
|
+
return { error: { code: error.code ?? "archive-invalid", message: error.message } };
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const installEntries = [];
|
|
715
|
+
for (const entry of entries) {
|
|
716
|
+
if (isArchiveSupportEntry(entry.name)) {
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
const pathCode = unsafePathCode(entry.name);
|
|
720
|
+
if (pathCode) {
|
|
721
|
+
return { error: { code: pathCode, message: `Archive entry is not allowed: ${entry.name}`, path: entry.name } };
|
|
722
|
+
}
|
|
723
|
+
if (entry.symlink) {
|
|
724
|
+
return { error: { code: "archive-symlink-entry", message: `Archive symlink entry is not allowed: ${entry.name}`, path: entry.name } };
|
|
725
|
+
}
|
|
726
|
+
installEntries.push(entry);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const files = installEntries.filter((entry) => !entry.directory);
|
|
730
|
+
const treeHash = treeHashForEntries(files);
|
|
731
|
+
if (artifact.tree_sha256 && treeHash !== artifact.tree_sha256) {
|
|
732
|
+
return { error: { code: "tree-hash-mismatch", message: "Installed tree hash does not match metadata." } };
|
|
733
|
+
}
|
|
734
|
+
return { entries: files, archiveHash, treeHash, fileCount: files.length };
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function addArchiveActions(plan, entries) {
|
|
738
|
+
const directories = new Set();
|
|
739
|
+
for (const entry of entries) {
|
|
740
|
+
const parts = entry.name.split("/");
|
|
741
|
+
parts.pop();
|
|
742
|
+
while (parts.length > 2) {
|
|
743
|
+
directories.add(parts.join("/"));
|
|
744
|
+
parts.pop();
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
for (const directory of [...directories].sort()) {
|
|
748
|
+
const state = pathState(resolve(process.cwd(), directory));
|
|
749
|
+
plan.actions.push({
|
|
750
|
+
type: "create-dir",
|
|
751
|
+
path: directory,
|
|
752
|
+
status: state === "absent" ? "pending" : state === "directory" ? "skipped" : "blocked",
|
|
753
|
+
reason: state === "absent" ? `Create ${directory}.` : state === "directory" ? `${directory} already exists.` : `${directory} exists and is not a directory.`,
|
|
754
|
+
});
|
|
755
|
+
plan.artifacts.push({
|
|
756
|
+
path: directory,
|
|
757
|
+
kind: "adapter-directory",
|
|
758
|
+
status: state === "absent" ? "pending" : state === "directory" ? "existing" : "blocked",
|
|
759
|
+
});
|
|
760
|
+
if (state === "file") {
|
|
761
|
+
plan.blockers.push({
|
|
762
|
+
code: "overwrite-refused",
|
|
763
|
+
message: `${directory} exists and is not a directory.`,
|
|
764
|
+
path: directory,
|
|
765
|
+
next_action: "Move the existing file before running init.",
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
for (const entry of entries) {
|
|
770
|
+
const state = pathState(resolve(process.cwd(), entry.name));
|
|
771
|
+
let existingMatches = false;
|
|
772
|
+
if (state === "file") {
|
|
773
|
+
const relativePath = entry.name.slice(`${INSTALL_ROOT}/`.length);
|
|
774
|
+
const existingBytes = relativePath.endsWith(".md")
|
|
775
|
+
? normalizeText(readFileSync(resolve(process.cwd(), entry.name)))
|
|
776
|
+
: readFileSync(resolve(process.cwd(), entry.name));
|
|
777
|
+
const entryBytes = relativePath.endsWith(".md") ? normalizeText(entry.bytes) : entry.bytes;
|
|
778
|
+
existingMatches = Buffer.compare(existingBytes, entryBytes) === 0;
|
|
779
|
+
}
|
|
780
|
+
plan.actions.push({
|
|
781
|
+
type: "copy",
|
|
782
|
+
path: entry.name,
|
|
783
|
+
status: state === "absent" ? "pending" : existingMatches ? "skipped" : "blocked",
|
|
784
|
+
reason:
|
|
785
|
+
state === "absent"
|
|
786
|
+
? "Install verified Codex adapter file."
|
|
787
|
+
: existingMatches
|
|
788
|
+
? `${entry.name} already matches verified Codex adapter content.`
|
|
789
|
+
: `${entry.name} already exists.`,
|
|
790
|
+
});
|
|
791
|
+
plan.artifacts.push({
|
|
792
|
+
path: entry.name,
|
|
793
|
+
kind: "adapter-file",
|
|
794
|
+
status: state === "absent" ? "pending" : existingMatches ? "existing" : "blocked",
|
|
795
|
+
});
|
|
796
|
+
if (state !== "absent" && !existingMatches) {
|
|
797
|
+
plan.blockers.push({
|
|
798
|
+
code: "overwrite-refused",
|
|
799
|
+
message: `${entry.name} already exists.`,
|
|
800
|
+
path: entry.name,
|
|
801
|
+
next_action: "Move the existing file before running init.",
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function writeArchiveEntries(entries) {
|
|
808
|
+
for (const entry of entries) {
|
|
809
|
+
const outputPath = resolve(process.cwd(), entry.name);
|
|
810
|
+
mkdirSync(dirname(outputPath), { recursive: true });
|
|
811
|
+
const relativePath = entry.name.slice(`${INSTALL_ROOT}/`.length);
|
|
812
|
+
const bytes = relativePath.endsWith(".md") ? normalizeText(entry.bytes) : entry.bytes;
|
|
813
|
+
writeFileSync(outputPath, bytes);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function planDirectoryActions(flags) {
|
|
818
|
+
const actions = [];
|
|
819
|
+
const artifacts = [];
|
|
820
|
+
const blockers = [];
|
|
821
|
+
let parentBlocked = false;
|
|
822
|
+
|
|
823
|
+
for (const relativePath of DIRECTORY_PLAN) {
|
|
824
|
+
const state = parentBlocked ? "blocked-by-parent" : pathState(resolve(process.cwd(), relativePath));
|
|
825
|
+
if (state === "absent") {
|
|
826
|
+
actions.push({
|
|
827
|
+
type: "create-dir",
|
|
828
|
+
path: relativePath,
|
|
829
|
+
status: flags.dryRun ? "planned" : "pending",
|
|
830
|
+
reason: `Create ${relativePath}.`,
|
|
831
|
+
});
|
|
832
|
+
artifacts.push({
|
|
833
|
+
path: relativePath,
|
|
834
|
+
kind: directoryKind(relativePath),
|
|
835
|
+
status: flags.dryRun ? "planned" : "pending",
|
|
836
|
+
});
|
|
837
|
+
} else if (state === "directory") {
|
|
838
|
+
actions.push({
|
|
839
|
+
type: "create-dir",
|
|
840
|
+
path: relativePath,
|
|
841
|
+
status: "skipped",
|
|
842
|
+
reason: `${relativePath} already exists.`,
|
|
843
|
+
});
|
|
844
|
+
artifacts.push({
|
|
845
|
+
path: relativePath,
|
|
846
|
+
kind: directoryKind(relativePath),
|
|
847
|
+
status: "existing",
|
|
848
|
+
});
|
|
849
|
+
} else {
|
|
850
|
+
actions.push({
|
|
851
|
+
type: "create-dir",
|
|
852
|
+
path: relativePath,
|
|
853
|
+
status: "blocked",
|
|
854
|
+
reason:
|
|
855
|
+
state === "blocked-by-parent"
|
|
856
|
+
? `${relativePath} cannot be created because ${AGENTS_ROOT} is not a directory.`
|
|
857
|
+
: `${relativePath} exists and is not a directory.`,
|
|
858
|
+
});
|
|
859
|
+
artifacts.push({
|
|
860
|
+
path: relativePath,
|
|
861
|
+
kind: directoryKind(relativePath),
|
|
862
|
+
status: "blocked",
|
|
863
|
+
});
|
|
864
|
+
if (state !== "blocked-by-parent") {
|
|
865
|
+
blockers.push({
|
|
866
|
+
code: "overwrite-refused",
|
|
867
|
+
message: `${relativePath} exists and is not a directory.`,
|
|
868
|
+
path: relativePath,
|
|
869
|
+
next_action: `Move the existing file before running init.`,
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
if (relativePath === AGENTS_ROOT) {
|
|
873
|
+
parentBlocked = true;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
return { actions, artifacts, blockers };
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function addLockfilePlan(flags, actions, artifacts, blockers, errors) {
|
|
882
|
+
const lockfileAbsolutePath = resolve(process.cwd(), LOCKFILE_PATH);
|
|
883
|
+
if (!existsSync(lockfileAbsolutePath)) {
|
|
884
|
+
actions.push({
|
|
885
|
+
type: "write",
|
|
886
|
+
path: LOCKFILE_PATH,
|
|
887
|
+
status: flags.dryRun ? "planned" : "pending",
|
|
888
|
+
reason: flags.dryRun
|
|
889
|
+
? "Plan durable lockfile content."
|
|
890
|
+
: "Write durable lockfile after verified Codex adapter install.",
|
|
891
|
+
});
|
|
892
|
+
artifacts.push({
|
|
893
|
+
path: LOCKFILE_PATH,
|
|
894
|
+
kind: "project-lockfile",
|
|
895
|
+
status: flags.dryRun ? "planned" : "pending",
|
|
896
|
+
});
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
const parsed = parseLockfile(readFileSync(lockfileAbsolutePath, "utf8"));
|
|
901
|
+
if (parsed.ok) {
|
|
902
|
+
actions.push({
|
|
903
|
+
type: "write",
|
|
904
|
+
path: LOCKFILE_PATH,
|
|
905
|
+
status: flags.dryRun ? "planned" : "pending",
|
|
906
|
+
reason: flags.dryRun
|
|
907
|
+
? "Plan update to supported rigorloop.lock."
|
|
908
|
+
: "Update supported rigorloop.lock after verified Codex adapter install.",
|
|
909
|
+
});
|
|
910
|
+
artifacts.push({
|
|
911
|
+
path: LOCKFILE_PATH,
|
|
912
|
+
kind: "project-lockfile",
|
|
913
|
+
status: flags.dryRun ? "planned" : "pending",
|
|
914
|
+
});
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
actions.push({
|
|
919
|
+
type: "write",
|
|
920
|
+
path: LOCKFILE_PATH,
|
|
921
|
+
status: "blocked",
|
|
922
|
+
reason: parsed.message,
|
|
923
|
+
});
|
|
924
|
+
artifacts.push({
|
|
925
|
+
path: LOCKFILE_PATH,
|
|
926
|
+
kind: "project-lockfile",
|
|
927
|
+
status: "blocked",
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
if (parsed.kind === "unsupported") {
|
|
931
|
+
blockers.push({
|
|
932
|
+
code: parsed.code,
|
|
933
|
+
message: parsed.message,
|
|
934
|
+
path: LOCKFILE_PATH,
|
|
935
|
+
next_action: "Use a compatible CLI version or resolve the unsupported lockfile shape.",
|
|
936
|
+
});
|
|
937
|
+
} else {
|
|
938
|
+
errors.push({
|
|
939
|
+
code: parsed.code,
|
|
940
|
+
message: parsed.message,
|
|
941
|
+
path: LOCKFILE_PATH,
|
|
942
|
+
next_action: "Repair or move rigorloop.lock before running init.",
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function buildInitPlan(flags, artifact) {
|
|
948
|
+
const info = packageInfo();
|
|
949
|
+
const source = sourceForFlags(flags, info);
|
|
950
|
+
if (artifact) {
|
|
951
|
+
source.artifact = artifact;
|
|
952
|
+
}
|
|
953
|
+
const manifestPath = "rigorloop.yaml";
|
|
954
|
+
const manifestAbsolutePath = resolve(process.cwd(), manifestPath);
|
|
955
|
+
const manifest = manifestContent(info, source);
|
|
956
|
+
const actions = [];
|
|
957
|
+
const artifacts = [];
|
|
958
|
+
const blockers = [];
|
|
959
|
+
const errors = [];
|
|
960
|
+
|
|
961
|
+
if (flags.fromArchiveProvided && (!flags.fromArchive || flags.fromArchive.startsWith("--"))) {
|
|
962
|
+
errors.push({
|
|
963
|
+
code: "invalid-archive-path",
|
|
964
|
+
message: "Missing required value for --from-archive.",
|
|
965
|
+
path: "--from-archive",
|
|
966
|
+
next_action: "Provide an existing Codex adapter archive path or omit --from-archive.",
|
|
967
|
+
});
|
|
968
|
+
} else if (flags.fromArchiveProvided && !existsSync(resolve(process.cwd(), flags.fromArchive))) {
|
|
969
|
+
errors.push({
|
|
970
|
+
code: "invalid-archive-path",
|
|
971
|
+
message: `Local archive path does not exist: ${flags.fromArchive}`,
|
|
972
|
+
path: flags.fromArchive,
|
|
973
|
+
next_action: "Provide an existing Codex adapter archive path or omit --from-archive.",
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const directoryPlan = planDirectoryActions(flags);
|
|
978
|
+
actions.push(...directoryPlan.actions);
|
|
979
|
+
artifacts.push(...directoryPlan.artifacts);
|
|
980
|
+
blockers.push(...directoryPlan.blockers);
|
|
981
|
+
|
|
982
|
+
if (existsSync(manifestAbsolutePath)) {
|
|
983
|
+
const existingManifest = readFileSync(manifestAbsolutePath, "utf8");
|
|
984
|
+
if (compatibleManifest(existingManifest)) {
|
|
985
|
+
actions.push({
|
|
986
|
+
type: "write",
|
|
987
|
+
path: manifestPath,
|
|
988
|
+
status: flags.dryRun ? "planned" : "skipped",
|
|
989
|
+
reason: "Compatible rigorloop.yaml already exists.",
|
|
990
|
+
});
|
|
991
|
+
artifacts.push({
|
|
992
|
+
path: manifestPath,
|
|
993
|
+
kind: "project-manifest",
|
|
994
|
+
status: "existing",
|
|
995
|
+
});
|
|
996
|
+
} else {
|
|
997
|
+
errors.push({
|
|
998
|
+
code: "invalid-config",
|
|
999
|
+
message: "Existing rigorloop.yaml is not compatible with the first-slice Codex init contract.",
|
|
1000
|
+
path: manifestPath,
|
|
1001
|
+
next_action: "Review or move the existing file before running init.",
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
} else {
|
|
1005
|
+
actions.push({
|
|
1006
|
+
type: "write",
|
|
1007
|
+
path: manifestPath,
|
|
1008
|
+
status: flags.dryRun ? "planned" : "pending",
|
|
1009
|
+
reason: "Create first-slice RigorLoop project manifest.",
|
|
1010
|
+
});
|
|
1011
|
+
artifacts.push({
|
|
1012
|
+
path: manifestPath,
|
|
1013
|
+
kind: "project-manifest",
|
|
1014
|
+
status: flags.dryRun ? "planned" : "pending",
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
addLockfilePlan(flags, actions, artifacts, blockers, errors);
|
|
1019
|
+
|
|
1020
|
+
return {
|
|
1021
|
+
info,
|
|
1022
|
+
source,
|
|
1023
|
+
manifest,
|
|
1024
|
+
actions,
|
|
1025
|
+
artifacts,
|
|
1026
|
+
blockers,
|
|
1027
|
+
errors,
|
|
1028
|
+
planned_lockfile: plannedLockfile(info, source, manifest),
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
function handleHelp(flags) {
|
|
1033
|
+
writeHuman(usage(), flags);
|
|
1034
|
+
return EXIT.success;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function handleVersion(flags) {
|
|
1038
|
+
const info = packageInfo();
|
|
1039
|
+
writeHuman(`${info.name} ${info.version}\n`, flags);
|
|
1040
|
+
return EXIT.success;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function commandError(command, message, flags, error) {
|
|
1044
|
+
if (flags.json) {
|
|
1045
|
+
writeJson(
|
|
1046
|
+
envelope(command, flags, {
|
|
1047
|
+
status: "error",
|
|
1048
|
+
summary: message,
|
|
1049
|
+
errors: [error],
|
|
1050
|
+
}),
|
|
1051
|
+
);
|
|
1052
|
+
} else {
|
|
1053
|
+
process.stderr.write(`${message}\n${error.next_action ?? "Run rigorloop --help."}\n`);
|
|
1054
|
+
}
|
|
1055
|
+
return exitCodeForResult({ status: "error", exit_class: "invalid_usage" });
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function invalidUsage(message, flags, command = "unknown") {
|
|
1059
|
+
return commandError(command, message, flags, {
|
|
1060
|
+
code: "invalid-usage",
|
|
1061
|
+
message,
|
|
1062
|
+
next_action: "Run rigorloop --help.",
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function newChangeUsageError(error, flags) {
|
|
1067
|
+
return commandError("new-change", error.message, flags, {
|
|
1068
|
+
code: error.code,
|
|
1069
|
+
message: error.message,
|
|
1070
|
+
next_action: "Run rigorloop new-change <change-id> --title <title>.",
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
function handleNewChange(rawArgs) {
|
|
1075
|
+
const parsed = parseNewChangeArgs(rawArgs, process.env);
|
|
1076
|
+
if (parsed.error) {
|
|
1077
|
+
return newChangeUsageError(parsed.error, parsed.flags);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
const draft = buildNewChangeDraft(parsed.value);
|
|
1081
|
+
const execution = runNewChangePlan({
|
|
1082
|
+
cwd: process.cwd(),
|
|
1083
|
+
draft,
|
|
1084
|
+
flags: parsed.flags,
|
|
1085
|
+
profile: parsed.value.profile,
|
|
1086
|
+
});
|
|
1087
|
+
const result = envelope("new-change", parsed.flags, {
|
|
1088
|
+
...execution.result,
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
if (parsed.flags.json) {
|
|
1092
|
+
writeJson(result);
|
|
1093
|
+
} else if (result.status === "blocked") {
|
|
1094
|
+
process.stderr.write(`${result.summary}\n${result.blockers[0].message}\n`);
|
|
1095
|
+
} else if (result.status === "error") {
|
|
1096
|
+
process.stderr.write(`${result.summary}\n${result.errors[0].message}\n`);
|
|
1097
|
+
} else if (parsed.flags.dryRun) {
|
|
1098
|
+
writeHuman(`RigorLoop new-change dry run completed.\n${draft.planned_change_metadata.path}\n`, parsed.flags);
|
|
1099
|
+
} else {
|
|
1100
|
+
writeHuman(`RigorLoop change metadata scaffold created.\n${draft.change.root}\n${draft.change.metadata_path}\n`, parsed.flags);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
return exitCodeForResult({
|
|
1104
|
+
status: result.status,
|
|
1105
|
+
exit_class: execution.exit_class,
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function invalidArchivePath(message, flags) {
|
|
1110
|
+
return commandError("init", message, flags, {
|
|
1111
|
+
code: "invalid-archive-path",
|
|
1112
|
+
message,
|
|
1113
|
+
path: flags.fromArchive,
|
|
1114
|
+
next_action: "Provide an existing Codex adapter archive path or omit --from-archive.",
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function unsupportedAdapter(adapter, flags) {
|
|
1119
|
+
const result = envelope("init", flags, {
|
|
1120
|
+
status: "blocked",
|
|
1121
|
+
summary: `Adapter '${adapter}' is not supported in this slice.`,
|
|
1122
|
+
blockers: [
|
|
1123
|
+
{
|
|
1124
|
+
code: "adapter-unsupported",
|
|
1125
|
+
message: `Adapter '${adapter}' is not supported in this slice.`,
|
|
1126
|
+
next_action: "Use --adapter codex.",
|
|
1127
|
+
},
|
|
1128
|
+
],
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
if (flags.json) {
|
|
1132
|
+
writeJson(result);
|
|
1133
|
+
} else {
|
|
1134
|
+
process.stderr.write(`${result.summary}\nUse --adapter codex.\n`);
|
|
1135
|
+
}
|
|
1136
|
+
return exitCodeForResult({ ...result, exit_class: "blocked" });
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function writeBlockedResult(flags, plan, summary, blockers, exitClass = "blocked") {
|
|
1140
|
+
for (const action of plan.actions) {
|
|
1141
|
+
if (action.status === "pending") {
|
|
1142
|
+
action.status = "blocked";
|
|
1143
|
+
action.reason = "Blocked before mutation.";
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
for (const artifact of plan.artifacts) {
|
|
1147
|
+
if (artifact.status === "pending") {
|
|
1148
|
+
artifact.status = "blocked";
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
const result = envelope("init", flags, {
|
|
1152
|
+
status: "blocked",
|
|
1153
|
+
summary,
|
|
1154
|
+
actions: plan.actions,
|
|
1155
|
+
artifacts: plan.artifacts,
|
|
1156
|
+
blockers,
|
|
1157
|
+
planned_manifest: {
|
|
1158
|
+
path: "rigorloop.yaml",
|
|
1159
|
+
content: plan.manifest,
|
|
1160
|
+
},
|
|
1161
|
+
planned_lockfile: plan.planned_lockfile,
|
|
1162
|
+
});
|
|
1163
|
+
if (flags.json) {
|
|
1164
|
+
writeJson(result);
|
|
1165
|
+
} else {
|
|
1166
|
+
process.stderr.write(`${result.summary}\n${blockers[0]?.next_action ?? "Resolve the blocker before running init."}\n`);
|
|
1167
|
+
}
|
|
1168
|
+
return exitCodeForResult({ ...result, exit_class: exitClass });
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function exitClassForBlockers(blockers) {
|
|
1172
|
+
return blockers.some((blocker) => blocker.code === "overwrite-refused") ? "mutation_conflict" : "blocked";
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function writeValidationErrorResult(flags, plan, error) {
|
|
1176
|
+
for (const action of plan.actions) {
|
|
1177
|
+
if (action.status === "pending") {
|
|
1178
|
+
action.status = "blocked";
|
|
1179
|
+
action.reason = "Blocked by archive verification failure.";
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
for (const artifact of plan.artifacts) {
|
|
1183
|
+
if (artifact.status === "pending") {
|
|
1184
|
+
artifact.status = "blocked";
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
const result = envelope("init", flags, {
|
|
1188
|
+
status: "error",
|
|
1189
|
+
summary: error.message,
|
|
1190
|
+
actions: plan.actions,
|
|
1191
|
+
artifacts: plan.artifacts,
|
|
1192
|
+
errors: [error],
|
|
1193
|
+
planned_manifest: {
|
|
1194
|
+
path: "rigorloop.yaml",
|
|
1195
|
+
content: plan.manifest,
|
|
1196
|
+
},
|
|
1197
|
+
planned_lockfile: plan.planned_lockfile,
|
|
1198
|
+
});
|
|
1199
|
+
if (flags.json) {
|
|
1200
|
+
writeJson(result);
|
|
1201
|
+
} else {
|
|
1202
|
+
process.stderr.write(`${result.summary}\n`);
|
|
1203
|
+
}
|
|
1204
|
+
return exitCodeForResult({ ...result, exit_class: "validation_failed" });
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
async function archiveWorkForInit(flags, info) {
|
|
1208
|
+
if (flags.dryRun) {
|
|
1209
|
+
return {};
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
const bundledMetadata = loadVerifiedBundledMetadata(info);
|
|
1213
|
+
if (bundledMetadata.blocker || bundledMetadata.error) {
|
|
1214
|
+
return bundledMetadata;
|
|
1215
|
+
}
|
|
1216
|
+
const metadata = bundledMetadata.metadata;
|
|
1217
|
+
const validation = validateMetadata(metadata, info);
|
|
1218
|
+
if (validation.blocker || validation.error) {
|
|
1219
|
+
return validation;
|
|
1220
|
+
}
|
|
1221
|
+
const artifact = validation.artifact;
|
|
1222
|
+
|
|
1223
|
+
if (flags.fromArchiveProvided) {
|
|
1224
|
+
const archiveName = basename(flags.fromArchive);
|
|
1225
|
+
if (archiveName !== artifact.archive || !archiveName.includes(metadata.release.version)) {
|
|
1226
|
+
return {
|
|
1227
|
+
blocker: metadataBlocker(
|
|
1228
|
+
"release-version-incompatible",
|
|
1229
|
+
`Local archive ${archiveName} is not compatible with ${metadata.release.version}.`,
|
|
1230
|
+
flags.fromArchive,
|
|
1231
|
+
"Use the Codex adapter archive matching the installed CLI package version.",
|
|
1232
|
+
),
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
const archiveBytes = readFileSync(resolve(process.cwd(), flags.fromArchive));
|
|
1236
|
+
const inspected = inspectArchive(archiveBytes, artifact);
|
|
1237
|
+
if (inspected.error) {
|
|
1238
|
+
return { error: inspected.error, artifact };
|
|
1239
|
+
}
|
|
1240
|
+
return { artifact, entries: inspected.entries, archiveHash: inspected.archiveHash, treeHash: inspected.treeHash };
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
let archiveBytes;
|
|
1244
|
+
const urlValidation = validateOfficialArchiveUrl({
|
|
1245
|
+
url: artifact.url,
|
|
1246
|
+
releaseTag: metadata.release.version,
|
|
1247
|
+
archive: artifact.archive,
|
|
1248
|
+
});
|
|
1249
|
+
if (!urlValidation.ok) {
|
|
1250
|
+
return {
|
|
1251
|
+
error: {
|
|
1252
|
+
code: urlValidation.code,
|
|
1253
|
+
message: urlValidation.message,
|
|
1254
|
+
path: urlValidation.path ?? "metadata.artifacts[codex].url",
|
|
1255
|
+
},
|
|
1256
|
+
artifact,
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
try {
|
|
1260
|
+
archiveBytes = await fetchBytes(artifact.url);
|
|
1261
|
+
} catch {
|
|
1262
|
+
return {
|
|
1263
|
+
blocker: metadataBlocker(
|
|
1264
|
+
"release-unavailable",
|
|
1265
|
+
"Official Codex adapter archive is unavailable.",
|
|
1266
|
+
artifact.url,
|
|
1267
|
+
"Retry later or use --from-archive with a compatible local archive.",
|
|
1268
|
+
),
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
const inspected = inspectArchive(archiveBytes, artifact);
|
|
1272
|
+
if (inspected.error) {
|
|
1273
|
+
return { error: inspected.error, artifact };
|
|
1274
|
+
}
|
|
1275
|
+
return { artifact, entries: inspected.entries, archiveHash: inspected.archiveHash, treeHash: inspected.treeHash };
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
async function handleInit(flags) {
|
|
1279
|
+
if (!flags.adapter) {
|
|
1280
|
+
return invalidUsage("Missing required option: --adapter codex.", flags, "init");
|
|
1281
|
+
}
|
|
1282
|
+
if (flags.adapter !== "codex") {
|
|
1283
|
+
return unsupportedAdapter(flags.adapter, flags);
|
|
1284
|
+
}
|
|
1285
|
+
if (flags.fromArchiveProvided && (!flags.fromArchive || flags.fromArchive.startsWith("--"))) {
|
|
1286
|
+
return invalidArchivePath("Missing required value for --from-archive.", flags);
|
|
1287
|
+
}
|
|
1288
|
+
if (flags.fromArchiveProvided && !existsSync(resolve(process.cwd(), flags.fromArchive))) {
|
|
1289
|
+
return invalidArchivePath(`Local archive path does not exist: ${flags.fromArchive}`, flags);
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
const info = packageInfo();
|
|
1293
|
+
const plan = buildInitPlan(flags);
|
|
1294
|
+
if (plan.errors.length > 0) {
|
|
1295
|
+
const result = envelope("init", flags, {
|
|
1296
|
+
status: "error",
|
|
1297
|
+
summary: plan.errors[0].message,
|
|
1298
|
+
actions: plan.actions,
|
|
1299
|
+
artifacts: plan.artifacts,
|
|
1300
|
+
errors: plan.errors,
|
|
1301
|
+
planned_manifest: {
|
|
1302
|
+
path: "rigorloop.yaml",
|
|
1303
|
+
content: plan.manifest,
|
|
1304
|
+
},
|
|
1305
|
+
planned_lockfile: plan.planned_lockfile,
|
|
1306
|
+
});
|
|
1307
|
+
if (flags.json) {
|
|
1308
|
+
writeJson(result);
|
|
1309
|
+
} else {
|
|
1310
|
+
process.stderr.write(`${result.summary}\n${plan.errors[0].next_action}\n`);
|
|
1311
|
+
}
|
|
1312
|
+
return exitCodeForResult({ ...result, exit_class: "invalid_usage" });
|
|
1313
|
+
}
|
|
1314
|
+
if (plan.blockers.length > 0) {
|
|
1315
|
+
return writeBlockedResult(flags, plan, plan.blockers[0].message, plan.blockers, exitClassForBlockers(plan.blockers));
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
const archiveWork = await archiveWorkForInit(flags, info);
|
|
1319
|
+
if (archiveWork.artifact) {
|
|
1320
|
+
plan.source.artifact = archiveWork.artifact;
|
|
1321
|
+
plan.planned_lockfile = plannedLockfile(plan.info, plan.source, plan.manifest);
|
|
1322
|
+
}
|
|
1323
|
+
if (archiveWork.entries) {
|
|
1324
|
+
const conflict = generatedOutputConflictBlocker(archiveWork.entries);
|
|
1325
|
+
if (conflict) {
|
|
1326
|
+
return writeBlockedResult(flags, plan, conflict.message, [conflict], "mutation_conflict");
|
|
1327
|
+
}
|
|
1328
|
+
const drift = lockfileDriftBlocker(currentCodexLockfileEntry());
|
|
1329
|
+
if (drift) {
|
|
1330
|
+
return writeBlockedResult(
|
|
1331
|
+
flags,
|
|
1332
|
+
plan,
|
|
1333
|
+
drift.message,
|
|
1334
|
+
[drift],
|
|
1335
|
+
drift.code === "overwrite-refused" ? "mutation_conflict" : "blocked",
|
|
1336
|
+
);
|
|
1337
|
+
}
|
|
1338
|
+
const installedTree = verifyInstalledTree(archiveWork.entries, archiveWork.artifact, { allowMissingOrEmpty: true });
|
|
1339
|
+
if (installedTree.error) {
|
|
1340
|
+
return writeValidationErrorResult(flags, plan, installedTree.error);
|
|
1341
|
+
}
|
|
1342
|
+
addArchiveActions(plan, archiveWork.entries);
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
if (archiveWork.blocker) {
|
|
1346
|
+
return writeBlockedResult(flags, plan, archiveWork.blocker.message, [archiveWork.blocker]);
|
|
1347
|
+
}
|
|
1348
|
+
if (archiveWork.error) {
|
|
1349
|
+
return writeValidationErrorResult(flags, plan, archiveWork.error);
|
|
1350
|
+
}
|
|
1351
|
+
if (plan.blockers.length > 0) {
|
|
1352
|
+
return writeBlockedResult(flags, plan, plan.blockers[0].message, plan.blockers, exitClassForBlockers(plan.blockers));
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
if (!flags.dryRun) {
|
|
1356
|
+
const manifestAction = plan.actions.find((action) => action.path === "rigorloop.yaml");
|
|
1357
|
+
const directoryActions = plan.actions.filter((action) => action.type === "create-dir" && action.status === "pending");
|
|
1358
|
+
for (const directoryAction of directoryActions) {
|
|
1359
|
+
mkdirSync(resolve(process.cwd(), directoryAction.path));
|
|
1360
|
+
directoryAction.status = "done";
|
|
1361
|
+
plan.artifacts.find((artifact) => artifact.path === directoryAction.path).status = "created";
|
|
1362
|
+
}
|
|
1363
|
+
if (manifestAction?.status === "pending") {
|
|
1364
|
+
writeFileSync(resolve(process.cwd(), "rigorloop.yaml"), plan.manifest, "utf8");
|
|
1365
|
+
manifestAction.status = "done";
|
|
1366
|
+
plan.artifacts.find((artifact) => artifact.path === "rigorloop.yaml").status = "created";
|
|
1367
|
+
}
|
|
1368
|
+
if (archiveWork.entries) {
|
|
1369
|
+
try {
|
|
1370
|
+
const pendingCopyPaths = new Set(
|
|
1371
|
+
plan.actions.filter((action) => action.type === "copy" && action.status === "pending").map((action) => action.path),
|
|
1372
|
+
);
|
|
1373
|
+
writeArchiveEntries(archiveWork.entries.filter((entry) => pendingCopyPaths.has(entry.name)));
|
|
1374
|
+
for (const action of plan.actions.filter((action) => action.type === "copy" && action.status === "pending")) {
|
|
1375
|
+
action.status = "done";
|
|
1376
|
+
plan.artifacts.find((artifact) => artifact.path === action.path).status = "created";
|
|
1377
|
+
}
|
|
1378
|
+
} catch (error) {
|
|
1379
|
+
const result = envelope("init", flags, {
|
|
1380
|
+
status: "error",
|
|
1381
|
+
summary: "Adapter installation failed after scaffold writes.",
|
|
1382
|
+
actions: plan.actions,
|
|
1383
|
+
artifacts: plan.artifacts,
|
|
1384
|
+
errors: [
|
|
1385
|
+
{
|
|
1386
|
+
code: "partial-installation-failed",
|
|
1387
|
+
message: error.message,
|
|
1388
|
+
partial_state: "scaffold files may have been written; adapter files may be incomplete.",
|
|
1389
|
+
},
|
|
1390
|
+
],
|
|
1391
|
+
planned_manifest: {
|
|
1392
|
+
path: "rigorloop.yaml",
|
|
1393
|
+
content: plan.manifest,
|
|
1394
|
+
},
|
|
1395
|
+
planned_lockfile: plan.planned_lockfile,
|
|
1396
|
+
});
|
|
1397
|
+
if (flags.json) {
|
|
1398
|
+
writeJson(result);
|
|
1399
|
+
} else {
|
|
1400
|
+
process.stderr.write(`${result.summary}\n${error.message}\n`);
|
|
1401
|
+
}
|
|
1402
|
+
return exitCodeForResult({ ...result, exit_class: "internal" });
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
if (archiveWork.entries) {
|
|
1406
|
+
const lockfileAction = plan.actions.find((action) => action.path === LOCKFILE_PATH);
|
|
1407
|
+
const lockfileArtifact = plan.artifacts.find((artifact) => artifact.path === LOCKFILE_PATH);
|
|
1408
|
+
if (lockfileAction?.status === "pending") {
|
|
1409
|
+
const lockfilePreviouslyExists = existsSync(resolve(process.cwd(), LOCKFILE_PATH));
|
|
1410
|
+
const verifiedInstalledTree = verifyInstalledTree(archiveWork.entries, archiveWork.artifact);
|
|
1411
|
+
if (verifiedInstalledTree.error) {
|
|
1412
|
+
return writeValidationErrorResult(flags, plan, verifiedInstalledTree.error);
|
|
1413
|
+
}
|
|
1414
|
+
const lockfile = lockfileForVerifiedInstall(
|
|
1415
|
+
plan.info,
|
|
1416
|
+
plan.source,
|
|
1417
|
+
plan.manifest,
|
|
1418
|
+
archiveWork.artifact,
|
|
1419
|
+
archiveWork.artifact.tree_sha256,
|
|
1420
|
+
verifiedInstalledTree.expectedFileCount,
|
|
1421
|
+
);
|
|
1422
|
+
writeFileSync(resolve(process.cwd(), LOCKFILE_PATH), serializeLockfile(lockfile), "utf8");
|
|
1423
|
+
plan.planned_lockfile = lockfile;
|
|
1424
|
+
lockfileAction.status = "done";
|
|
1425
|
+
lockfileAction.reason = lockfilePreviouslyExists
|
|
1426
|
+
? "Updated durable lockfile for verified Codex adapter install."
|
|
1427
|
+
: "Wrote durable lockfile for verified Codex adapter install.";
|
|
1428
|
+
if (lockfileArtifact) {
|
|
1429
|
+
lockfileArtifact.status = lockfilePreviouslyExists ? "updated" : "created";
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
const warnings = [];
|
|
1436
|
+
const result = envelope("init", flags, {
|
|
1437
|
+
status: warnings.length > 0 ? "warning" : "success",
|
|
1438
|
+
summary: flags.dryRun
|
|
1439
|
+
? "RigorLoop init dry run completed. No files were written."
|
|
1440
|
+
: archiveWork.entries
|
|
1441
|
+
? "RigorLoop initialized with verified Codex adapter files."
|
|
1442
|
+
: "RigorLoop initialized with Codex scaffold.",
|
|
1443
|
+
actions: plan.actions,
|
|
1444
|
+
artifacts: plan.artifacts,
|
|
1445
|
+
warnings,
|
|
1446
|
+
planned_manifest: {
|
|
1447
|
+
path: "rigorloop.yaml",
|
|
1448
|
+
content: plan.manifest,
|
|
1449
|
+
},
|
|
1450
|
+
planned_lockfile: plan.planned_lockfile,
|
|
1451
|
+
});
|
|
1452
|
+
|
|
1453
|
+
if (flags.json) {
|
|
1454
|
+
writeJson(result);
|
|
1455
|
+
} else {
|
|
1456
|
+
const lines = flags.dryRun
|
|
1457
|
+
? ["RigorLoop init dry run completed.", "No files were written."]
|
|
1458
|
+
: [
|
|
1459
|
+
archiveWork.entries
|
|
1460
|
+
? "RigorLoop initialized with verified Codex adapter files."
|
|
1461
|
+
: "RigorLoop initialized with Codex scaffold.",
|
|
1462
|
+
archiveWork.entries ? "rigorloop.lock was written." : "No adapter files were installed.",
|
|
1463
|
+
];
|
|
1464
|
+
writeHuman(`${lines.join("\n")}\n`, flags);
|
|
1465
|
+
}
|
|
1466
|
+
return exitCodeForResult({ ...result, exit_class: "success" });
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
async function main() {
|
|
1470
|
+
try {
|
|
1471
|
+
const rawArgs = process.argv.slice(2);
|
|
1472
|
+
if (rawArgs[0] === "new-change") {
|
|
1473
|
+
return handleNewChange(rawArgs.slice(1));
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
const { flags, positional } = parseFlags(rawArgs);
|
|
1477
|
+
const [command] = positional;
|
|
1478
|
+
|
|
1479
|
+
if (!command || command === "--help" || command === "-h") {
|
|
1480
|
+
return handleHelp(flags);
|
|
1481
|
+
}
|
|
1482
|
+
if (command === "version") {
|
|
1483
|
+
return handleVersion(flags);
|
|
1484
|
+
}
|
|
1485
|
+
if (command === "init") {
|
|
1486
|
+
return handleInit(flags);
|
|
1487
|
+
}
|
|
1488
|
+
if (command === "new-change") {
|
|
1489
|
+
return handleNewChange(rawArgs.slice(rawArgs.indexOf("new-change") + 1));
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
return invalidUsage(`Unknown command: ${command}`, flags);
|
|
1493
|
+
} catch (error) {
|
|
1494
|
+
process.stderr.write(`Unexpected error: ${error.message}\n`);
|
|
1495
|
+
return exitCodeForResult({ status: "error", exit_class: "internal" });
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
process.exitCode = await main();
|