opencode-ship 0.2.1 → 0.4.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 +69 -16
- package/README.md +26 -12
- package/THIRD_PARTY_NOTICES.md +12 -0
- package/dist/cli.js +1014 -469
- package/dist/core.js +1 -1
- package/dist/plugin.js +104 -10
- package/docs/adr/0001-opencode-ship-redesign.md +1 -1
- package/package.json +6 -1
- package/schema/project-adapter.schema.json +1 -1
- package/schema/ship-config.schema.json +6 -1
- package/schema/ship-lock.schema.json +7 -3
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// opencode-ship CLI v0.
|
|
2
|
+
// opencode-ship CLI v0.4.0
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
5
|
var __esm = (fn, res) => function __init() {
|
|
@@ -10,109 +10,6 @@ var __export = (target, all) => {
|
|
|
10
10
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
// src/installer/catalog.js
|
|
14
|
-
import { fileURLToPath } from "node:url";
|
|
15
|
-
import { dirname, resolve } from "node:path";
|
|
16
|
-
function getTemplateSet() {
|
|
17
|
-
return TEMPLATE_SET;
|
|
18
|
-
}
|
|
19
|
-
var packageRoot, TEMPLATE_SET, CATALOG, POINTER_ENTRIES;
|
|
20
|
-
var init_catalog = __esm({
|
|
21
|
-
"src/installer/catalog.js"() {
|
|
22
|
-
packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
23
|
-
TEMPLATE_SET = "v0.2.0";
|
|
24
|
-
CATALOG = [
|
|
25
|
-
{
|
|
26
|
-
kind: "plugin",
|
|
27
|
-
path: ".opencode/plugin/opencode-ship.js",
|
|
28
|
-
source: resolve(packageRoot, "dist/plugin.js"),
|
|
29
|
-
mode: 420
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
kind: "agent",
|
|
33
|
-
path: ".opencode/agents/delivery-reviewer.md",
|
|
34
|
-
source: resolve(packageRoot, "assets/agents/delivery-reviewer.md"),
|
|
35
|
-
mode: 420
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
kind: "agent",
|
|
39
|
-
path: ".opencode/agents/delivery-verifier.md",
|
|
40
|
-
source: resolve(packageRoot, "assets/agents/delivery-verifier.md"),
|
|
41
|
-
mode: 420
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
kind: "skill",
|
|
45
|
-
path: ".opencode/skills/delivery-workflow/SKILL.md",
|
|
46
|
-
source: resolve(packageRoot, "assets/skills/delivery-workflow/SKILL.md"),
|
|
47
|
-
mode: 420
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
kind: "skill",
|
|
51
|
-
path: ".opencode/skills/planning-research-checkpoint/SKILL.md",
|
|
52
|
-
source: resolve(packageRoot, "assets/skills/planning-research-checkpoint/SKILL.md"),
|
|
53
|
-
mode: 420
|
|
54
|
-
}
|
|
55
|
-
];
|
|
56
|
-
POINTER_ENTRIES = [
|
|
57
|
-
{
|
|
58
|
-
pointer: "/agent/build/permission/delivery_inspect",
|
|
59
|
-
strategy: "value",
|
|
60
|
-
value: "allow"
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
pointer: "/agent/build/permission/delivery_issue",
|
|
64
|
-
strategy: "value",
|
|
65
|
-
value: "allow"
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
pointer: "/agent/build/permission/delivery_worktree",
|
|
69
|
-
strategy: "value",
|
|
70
|
-
value: "allow"
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
pointer: "/agent/build/permission/delivery_verify",
|
|
74
|
-
strategy: "value",
|
|
75
|
-
value: "deny"
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
pointer: "/agent/build/permission/delivery_review",
|
|
79
|
-
strategy: "value",
|
|
80
|
-
value: "deny"
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
pointer: "/agent/build/permission/delivery_pr",
|
|
84
|
-
strategy: "value",
|
|
85
|
-
value: "allow"
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
pointer: "/agent/build/permission/delivery_ready",
|
|
89
|
-
strategy: "value",
|
|
90
|
-
value: "allow"
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
pointer: "/agent/build/permission/delivery_merge",
|
|
94
|
-
strategy: "value",
|
|
95
|
-
value: "ask"
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
pointer: "/agent/build/permission/delivery_cleanup",
|
|
99
|
-
strategy: "value",
|
|
100
|
-
value: "allow"
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
pointer: "/agent/build/permission/task/delivery-reviewer",
|
|
104
|
-
strategy: "value",
|
|
105
|
-
value: "allow"
|
|
106
|
-
},
|
|
107
|
-
{
|
|
108
|
-
pointer: "/agent/build/permission/task/delivery-verifier",
|
|
109
|
-
strategy: "value",
|
|
110
|
-
value: "allow"
|
|
111
|
-
}
|
|
112
|
-
];
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
|
|
116
13
|
// src/installer/json-pointer.js
|
|
117
14
|
var json_pointer_exports = {};
|
|
118
15
|
__export(json_pointer_exports, {
|
|
@@ -216,6 +113,7 @@ var init_hash = __esm({
|
|
|
216
113
|
// src/installer/root-config.js
|
|
217
114
|
var root_config_exports = {};
|
|
218
115
|
__export(root_config_exports, {
|
|
116
|
+
POINTER_ENTRIES: () => POINTER_ENTRIES,
|
|
219
117
|
applyOwnedPointers: () => applyOwnedPointers,
|
|
220
118
|
defaultRootConfigPath: () => defaultRootConfigPath,
|
|
221
119
|
findRootConfig: () => findRootConfig,
|
|
@@ -225,24 +123,24 @@ __export(root_config_exports, {
|
|
|
225
123
|
readRootConfig: () => readRootConfig,
|
|
226
124
|
synthesizeDefaultRootConfig: () => synthesizeDefaultRootConfig
|
|
227
125
|
});
|
|
228
|
-
import { existsSync as
|
|
126
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
|
|
229
127
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
230
|
-
import { resolve as
|
|
128
|
+
import { resolve as resolve5 } from "node:path";
|
|
231
129
|
function findRootConfig(repoRoot) {
|
|
232
130
|
for (const rel of ROOT_PATH_CANDIDATES) {
|
|
233
|
-
const abs =
|
|
234
|
-
if (
|
|
131
|
+
const abs = resolve5(repoRoot, rel);
|
|
132
|
+
if (existsSync5(abs)) return { path: abs, relative: rel, format: rel.endsWith(".jsonc") ? "jsonc" : "json" };
|
|
235
133
|
}
|
|
236
134
|
return { path: null, relative: ROOT_PATH_CANDIDATES[0], format: "json" };
|
|
237
135
|
}
|
|
238
136
|
function defaultRootConfigPath(repoRoot) {
|
|
239
|
-
return
|
|
137
|
+
return resolve5(repoRoot, ROOT_PATH_CANDIDATES[0]);
|
|
240
138
|
}
|
|
241
139
|
function readRootConfig(absPath) {
|
|
242
|
-
if (!
|
|
140
|
+
if (!existsSync5(absPath)) {
|
|
243
141
|
return { ok: false, error: { kind: "missing", path: absPath } };
|
|
244
142
|
}
|
|
245
|
-
const raw =
|
|
143
|
+
const raw = readFileSync3(absPath, "utf8");
|
|
246
144
|
const stripped = stripJsonc(raw);
|
|
247
145
|
try {
|
|
248
146
|
const value = JSON.parse(stripped);
|
|
@@ -382,6 +280,9 @@ function formatRootConfigPreserving(value) {
|
|
|
382
280
|
return formatRootConfig(value);
|
|
383
281
|
}
|
|
384
282
|
function parseRootConfigPreservingOrder(text) {
|
|
283
|
+
if (typeof text !== "string" || text.length === 0) {
|
|
284
|
+
return { value: {}, format: "json" };
|
|
285
|
+
}
|
|
385
286
|
const parser = new RootConfigParser(text);
|
|
386
287
|
const value = parser.parseValue(
|
|
387
288
|
0,
|
|
@@ -391,12 +292,68 @@ function parseRootConfigPreservingOrder(text) {
|
|
|
391
292
|
const isJsonc = text.includes("//") || text.includes("/*");
|
|
392
293
|
return { value, format: isJsonc ? "jsonc" : "json" };
|
|
393
294
|
}
|
|
394
|
-
var ROOT_PATH_CANDIDATES, RootConfigParser;
|
|
295
|
+
var POINTER_ENTRIES, ROOT_PATH_CANDIDATES, RootConfigParser;
|
|
395
296
|
var init_root_config = __esm({
|
|
396
297
|
"src/installer/root-config.js"() {
|
|
397
298
|
init_json_pointer();
|
|
398
299
|
init_hash();
|
|
399
|
-
|
|
300
|
+
POINTER_ENTRIES = [
|
|
301
|
+
{
|
|
302
|
+
pointer: "/agent/build/permission/delivery_inspect",
|
|
303
|
+
strategy: "value",
|
|
304
|
+
value: "allow"
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
pointer: "/agent/build/permission/delivery_issue",
|
|
308
|
+
strategy: "value",
|
|
309
|
+
value: "allow"
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
pointer: "/agent/build/permission/delivery_worktree",
|
|
313
|
+
strategy: "value",
|
|
314
|
+
value: "allow"
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
pointer: "/agent/build/permission/delivery_verify",
|
|
318
|
+
strategy: "value",
|
|
319
|
+
value: "deny"
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
pointer: "/agent/build/permission/delivery_review",
|
|
323
|
+
strategy: "value",
|
|
324
|
+
value: "deny"
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
pointer: "/agent/build/permission/delivery_pr",
|
|
328
|
+
strategy: "value",
|
|
329
|
+
value: "allow"
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
pointer: "/agent/build/permission/delivery_ready",
|
|
333
|
+
strategy: "value",
|
|
334
|
+
value: "allow"
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
pointer: "/agent/build/permission/delivery_merge",
|
|
338
|
+
strategy: "value",
|
|
339
|
+
value: "ask"
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
pointer: "/agent/build/permission/delivery_cleanup",
|
|
343
|
+
strategy: "value",
|
|
344
|
+
value: "allow"
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
pointer: "/agent/build/permission/task/delivery-reviewer",
|
|
348
|
+
strategy: "value",
|
|
349
|
+
value: "allow"
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
pointer: "/agent/build/permission/task/delivery-verifier",
|
|
353
|
+
strategy: "value",
|
|
354
|
+
value: "allow"
|
|
355
|
+
}
|
|
356
|
+
];
|
|
400
357
|
ROOT_PATH_CANDIDATES = ["opencode.json", "opencode.jsonc"];
|
|
401
358
|
RootConfigParser = class {
|
|
402
359
|
constructor(text) {
|
|
@@ -527,82 +484,45 @@ var init_root_config = __esm({
|
|
|
527
484
|
}
|
|
528
485
|
});
|
|
529
486
|
|
|
530
|
-
// src/
|
|
531
|
-
var
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
if (
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
await rename2(tmp, path);
|
|
567
|
-
return path;
|
|
568
|
-
}
|
|
569
|
-
function computeIntegrity(lock) {
|
|
570
|
-
const { integrity: _ignored, ...without } = lock ?? {};
|
|
571
|
-
void _ignored;
|
|
572
|
-
return {
|
|
573
|
-
lockSha256: bytesHashString(stableStringify(without))
|
|
574
|
-
};
|
|
575
|
-
}
|
|
576
|
-
async function validateIntegrity(lock) {
|
|
577
|
-
if (!lock?.integrity?.lockSha256) return false;
|
|
578
|
-
const expected = computeIntegrity(lock).lockSha256;
|
|
579
|
-
return expected === lock.integrity.lockSha256;
|
|
580
|
-
}
|
|
581
|
-
async function migrateLegacyLock(repoRoot) {
|
|
582
|
-
const legacy = resolve4(repoRoot, ".opencode", "delivery.lock.json");
|
|
583
|
-
if (!existsSync4(legacy)) return null;
|
|
584
|
-
try {
|
|
585
|
-
const raw = await readFile4(legacy, "utf8");
|
|
586
|
-
const parsed = JSON.parse(raw);
|
|
587
|
-
if (parsed.contractVersion !== 1 || typeof parsed.adapterSha256 !== "string") return null;
|
|
588
|
-
return {
|
|
589
|
-
kind: "legacy-lock",
|
|
590
|
-
sourcePath: legacy,
|
|
591
|
-
payload: { contractVersion: 1, adapterSha256: parsed.adapterSha256, writtenAt: parsed.writtenAt ?? null },
|
|
592
|
-
sha256: bytesHashString(raw)
|
|
593
|
-
};
|
|
594
|
-
} catch {
|
|
595
|
-
return null;
|
|
487
|
+
// src/profile.js
|
|
488
|
+
var PROFILES = Object.freeze(["core", "engineering"]);
|
|
489
|
+
var DEFAULT_PROFILE = "core";
|
|
490
|
+
function isValidProfile(name) {
|
|
491
|
+
return typeof name === "string" && PROFILES.includes(name);
|
|
492
|
+
}
|
|
493
|
+
function normalizeProfile(name) {
|
|
494
|
+
if (name === void 0 || name === null) return DEFAULT_PROFILE;
|
|
495
|
+
if (!isValidProfile(name)) return null;
|
|
496
|
+
return name;
|
|
497
|
+
}
|
|
498
|
+
function resolveProfile({ cli = null, config = null, lock = null } = {}) {
|
|
499
|
+
if (cli !== null && cli !== void 0) {
|
|
500
|
+
const v = normalizeProfile(cli);
|
|
501
|
+
if (v === null) {
|
|
502
|
+
throw new Error(`unknown CLI profile '${cli}' (expected one of: ${PROFILES.join(", ")})`);
|
|
503
|
+
}
|
|
504
|
+
return { profile: v, source: "cli" };
|
|
505
|
+
}
|
|
506
|
+
if (config && typeof config === "object" && config.profile !== void 0 && config.profile !== null) {
|
|
507
|
+
const v = normalizeProfile(config.profile);
|
|
508
|
+
if (v === null) {
|
|
509
|
+
throw new Error(
|
|
510
|
+
`unknown ship.config.json profile '${config.profile}' (expected one of: ${PROFILES.join(", ")})`
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
return { profile: v, source: "config" };
|
|
514
|
+
}
|
|
515
|
+
if (lock && typeof lock === "object" && lock.manager && lock.manager.profile !== void 0) {
|
|
516
|
+
const v = normalizeProfile(lock.manager.profile);
|
|
517
|
+
if (v === null) {
|
|
518
|
+
throw new Error(
|
|
519
|
+
`unknown lock manager.profile '${lock.manager.profile}' (expected one of: ${PROFILES.join(", ")})`
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
return { profile: v, source: "lock" };
|
|
596
523
|
}
|
|
524
|
+
return { profile: DEFAULT_PROFILE, source: "default" };
|
|
597
525
|
}
|
|
598
|
-
var CURRENT_LOCK_SCHEMA;
|
|
599
|
-
var init_lock = __esm({
|
|
600
|
-
"src/installer/lock.js"() {
|
|
601
|
-
init_hash();
|
|
602
|
-
init_json_pointer();
|
|
603
|
-
CURRENT_LOCK_SCHEMA = 1;
|
|
604
|
-
}
|
|
605
|
-
});
|
|
606
526
|
|
|
607
527
|
// src/installer/cli-args.js
|
|
608
528
|
var USAGE = `opencode-ship <command> [options]
|
|
@@ -618,7 +538,7 @@ Commands:
|
|
|
618
538
|
|
|
619
539
|
Options:
|
|
620
540
|
--root <path> Project root (defaults to cwd).
|
|
621
|
-
--
|
|
541
|
+
--profile <name> Override active profile: ${PROFILES.join(", ")}.
|
|
622
542
|
--force-config Rewrite the user config from detection (init only).
|
|
623
543
|
--force-root-config Create opencode.json when absent (init only).
|
|
624
544
|
--strict-doctor Fail init when doctor reports unhealthy checks.
|
|
@@ -629,7 +549,7 @@ Options:
|
|
|
629
549
|
function parseFlags(argv) {
|
|
630
550
|
const options = {
|
|
631
551
|
rootPath: null,
|
|
632
|
-
|
|
552
|
+
profile: null,
|
|
633
553
|
json: false,
|
|
634
554
|
replaceManaged: false,
|
|
635
555
|
purgeConfig: false,
|
|
@@ -646,8 +566,16 @@ function parseFlags(argv) {
|
|
|
646
566
|
else if (arg === "--force-root-config") options.forceRootConfig = true;
|
|
647
567
|
else if (arg === "--strict-doctor") options.strictDoctor = true;
|
|
648
568
|
else if (arg === "--root") options.rootPath = argv[++i];
|
|
649
|
-
else if (arg === "--
|
|
650
|
-
|
|
569
|
+
else if (arg === "--profile") {
|
|
570
|
+
const value = argv[++i];
|
|
571
|
+
if (value === void 0) {
|
|
572
|
+
return { error: "--profile requires a value" };
|
|
573
|
+
}
|
|
574
|
+
if (!isValidProfile(value)) {
|
|
575
|
+
return { error: `unknown profile '${value}' (expected one of: ${PROFILES.join(", ")})` };
|
|
576
|
+
}
|
|
577
|
+
options.profile = value;
|
|
578
|
+
} else if (arg === "-h" || arg === "--help") return { help: true };
|
|
651
579
|
else if (arg === "-v" || arg === "--version") return { version: true };
|
|
652
580
|
else return { error: `unknown flag ${arg}` };
|
|
653
581
|
}
|
|
@@ -680,32 +608,194 @@ function helpText() {
|
|
|
680
608
|
}
|
|
681
609
|
|
|
682
610
|
// src/installer/executor.js
|
|
683
|
-
|
|
684
|
-
import { existsSync as existsSync8 } from "node:fs";
|
|
611
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
685
612
|
import { mkdir as mkdir4, readFile as readFile7, rename as rename4, unlink as unlink2, writeFile as writeFile4 } from "node:fs/promises";
|
|
686
|
-
import { dirname as
|
|
613
|
+
import { dirname as dirname6, resolve as resolve10 } from "node:path";
|
|
614
|
+
|
|
615
|
+
// src/installer/catalog.js
|
|
616
|
+
import { resolve as resolve3, relative, sep } from "node:path";
|
|
617
|
+
import { existsSync as existsSync3, statSync } from "node:fs";
|
|
618
|
+
|
|
619
|
+
// src/installer/package-root.js
|
|
620
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
621
|
+
import { fileURLToPath } from "node:url";
|
|
622
|
+
import { dirname, resolve } from "node:path";
|
|
623
|
+
var PACKAGE_NAME = "opencode-ship";
|
|
624
|
+
function resolvePackageRoot(startUrl) {
|
|
625
|
+
let candidate = dirname(fileURLToPath(startUrl ?? import.meta.url));
|
|
626
|
+
while (candidate && candidate !== "/") {
|
|
627
|
+
const pkgPath = resolve(candidate, "package.json");
|
|
628
|
+
if (existsSync(pkgPath)) {
|
|
629
|
+
try {
|
|
630
|
+
const raw = readFileSync(pkgPath, "utf8");
|
|
631
|
+
const pkg = JSON.parse(raw);
|
|
632
|
+
if (pkg && pkg.name === PACKAGE_NAME) return candidate;
|
|
633
|
+
} catch {
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
candidate = dirname(candidate);
|
|
637
|
+
}
|
|
638
|
+
throw new Error(`opencode-ship package root not found from ${startUrl ?? import.meta.url}`);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// src/version.js
|
|
642
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "node:fs";
|
|
643
|
+
import { dirname as dirname2, resolve as resolve2 } from "node:path";
|
|
644
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
645
|
+
var PACKAGE_VERSION = "0.4.0";
|
|
646
|
+
var TEMPLATE_SET = `v${PACKAGE_VERSION}`;
|
|
647
|
+
|
|
648
|
+
// src/installer/catalog.js
|
|
649
|
+
var TEMPLATE_SET_ID = TEMPLATE_SET;
|
|
650
|
+
var packageRoot = resolvePackageRoot(import.meta.url);
|
|
651
|
+
var CATALOG = [
|
|
652
|
+
{
|
|
653
|
+
id: "plugin:opencode-ship",
|
|
654
|
+
kind: "plugin",
|
|
655
|
+
path: ".opencode/plugins/opencode-ship.js",
|
|
656
|
+
source: resolve3(packageRoot, "dist/plugin.js"),
|
|
657
|
+
mode: 420,
|
|
658
|
+
profiles: ["core", "engineering"]
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
id: "agent:delivery-reviewer",
|
|
662
|
+
kind: "agent",
|
|
663
|
+
path: ".opencode/agents/delivery-reviewer.md",
|
|
664
|
+
source: resolve3(packageRoot, "assets/agents/delivery-reviewer.md"),
|
|
665
|
+
mode: 420,
|
|
666
|
+
profiles: ["core", "engineering"]
|
|
667
|
+
},
|
|
668
|
+
{
|
|
669
|
+
id: "agent:delivery-verifier",
|
|
670
|
+
kind: "agent",
|
|
671
|
+
path: ".opencode/agents/delivery-verifier.md",
|
|
672
|
+
source: resolve3(packageRoot, "assets/agents/delivery-verifier.md"),
|
|
673
|
+
mode: 420,
|
|
674
|
+
profiles: ["core", "engineering"]
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
id: "skill:delivery-workflow",
|
|
678
|
+
kind: "skill",
|
|
679
|
+
path: ".opencode/skills/delivery-workflow/SKILL.md",
|
|
680
|
+
source: resolve3(packageRoot, "assets/skills/delivery-workflow/SKILL.md"),
|
|
681
|
+
mode: 420,
|
|
682
|
+
profiles: ["core", "engineering"]
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
id: "skill:planning-research-checkpoint",
|
|
686
|
+
kind: "skill",
|
|
687
|
+
path: ".opencode/skills/planning-research-checkpoint/SKILL.md",
|
|
688
|
+
source: resolve3(packageRoot, "assets/skills/planning-research-checkpoint/SKILL.md"),
|
|
689
|
+
mode: 420,
|
|
690
|
+
profiles: ["core", "engineering"]
|
|
691
|
+
}
|
|
692
|
+
];
|
|
693
|
+
function filterCatalogByProfile(catalog, profile) {
|
|
694
|
+
const effective = profile === void 0 || profile === null ? DEFAULT_PROFILE : profile;
|
|
695
|
+
if (!isValidProfile(effective)) {
|
|
696
|
+
throw new Error(
|
|
697
|
+
`filterCatalogByProfile: unknown profile '${profile}' (expected one of: ${PROFILES.join(", ")})`
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
return catalog.filter((entry) => Array.isArray(entry.profiles) && entry.profiles.includes(effective));
|
|
701
|
+
}
|
|
702
|
+
var ALLOWED_KINDS = /* @__PURE__ */ new Set(["plugin", "agent", "skill", "support"]);
|
|
703
|
+
function validateCatalog({ catalog = CATALOG } = {}) {
|
|
704
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
705
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
706
|
+
const issues = [];
|
|
707
|
+
for (const entry of catalog) {
|
|
708
|
+
if (!entry || typeof entry !== "object") {
|
|
709
|
+
issues.push({ id: null, kind: "shape", message: "catalog entry is not an object" });
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
const { id, kind, path, source, mode } = entry;
|
|
713
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
714
|
+
issues.push({ id: null, kind: "id", message: `entry id missing: ${JSON.stringify(entry)}` });
|
|
715
|
+
} else if (seenIds.has(id)) {
|
|
716
|
+
issues.push({ id, kind: "duplicate-id", message: `duplicate catalog id: ${id}` });
|
|
717
|
+
} else {
|
|
718
|
+
seenIds.add(id);
|
|
719
|
+
}
|
|
720
|
+
if (typeof path !== "string" || !path.startsWith(".opencode" + sep)) {
|
|
721
|
+
issues.push({ id, kind: "path", message: `path must be rooted under .opencode/: ${path}` });
|
|
722
|
+
}
|
|
723
|
+
if (seenPaths.has(path)) {
|
|
724
|
+
issues.push({ id, kind: "duplicate-path", message: `duplicate target path: ${path}` });
|
|
725
|
+
} else {
|
|
726
|
+
seenPaths.add(path);
|
|
727
|
+
}
|
|
728
|
+
if (!ALLOWED_KINDS.has(kind)) {
|
|
729
|
+
issues.push({ id, kind: "kind", message: `unsupported entry kind: ${kind}` });
|
|
730
|
+
}
|
|
731
|
+
if (typeof source !== "string" || source.length === 0) {
|
|
732
|
+
issues.push({ id, kind: "source", message: `source path missing: ${id}` });
|
|
733
|
+
} else if (!existsSync3(source)) {
|
|
734
|
+
issues.push({ id, kind: "source-missing", message: `source file not found: ${source}` });
|
|
735
|
+
} else {
|
|
736
|
+
try {
|
|
737
|
+
const stats = statSync(source);
|
|
738
|
+
if (!stats.isFile()) {
|
|
739
|
+
issues.push({ id, kind: "source-not-file", message: `source is not a regular file: ${source}` });
|
|
740
|
+
} else if (stats.size === 0) {
|
|
741
|
+
issues.push({ id, kind: "source-empty", message: `source file is empty: ${source}` });
|
|
742
|
+
}
|
|
743
|
+
} catch (e) {
|
|
744
|
+
issues.push({ id, kind: "source-stat", message: `unable to stat source: ${e?.message ?? e}` });
|
|
745
|
+
}
|
|
746
|
+
const rel = relative(packageRoot, source);
|
|
747
|
+
if (rel.startsWith("..")) {
|
|
748
|
+
issues.push({ id, kind: "source-out-of-package", message: `source escapes package root: ${source}` });
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
if (mode !== 420) {
|
|
752
|
+
issues.push({ id, kind: "mode", message: `mode must be 0o644: ${id}` });
|
|
753
|
+
}
|
|
754
|
+
if (!Array.isArray(entry.profiles) || entry.profiles.length === 0) {
|
|
755
|
+
issues.push({ id, kind: "profiles", message: `profiles must be a non-empty array: ${id}` });
|
|
756
|
+
} else {
|
|
757
|
+
for (const p of entry.profiles) {
|
|
758
|
+
if (!isValidProfile(p)) {
|
|
759
|
+
issues.push({ id, kind: "profiles", message: `unknown profile in profiles[${entry.profiles.indexOf(p)}]: ${p} (expected one of: ${PROFILES.join(", ")})` });
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
if (issues.length > 0) {
|
|
765
|
+
const summary = issues.map((i) => i.message).join("; ");
|
|
766
|
+
const err = new Error(`opencode-ship catalog validation failed: ${summary}`);
|
|
767
|
+
err.issues = issues;
|
|
768
|
+
err.catalogValidation = true;
|
|
769
|
+
throw err;
|
|
770
|
+
}
|
|
771
|
+
return catalog;
|
|
772
|
+
}
|
|
687
773
|
|
|
688
774
|
// src/installer/planner.js
|
|
689
|
-
|
|
690
|
-
init_hash();
|
|
691
|
-
import { existsSync as existsSync3 } from "node:fs";
|
|
775
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
692
776
|
import { readFile as readFile3, stat } from "node:fs/promises";
|
|
777
|
+
init_hash();
|
|
693
778
|
|
|
694
779
|
// src/installer/config.js
|
|
695
780
|
import { readFile, writeFile, rename, mkdir } from "node:fs/promises";
|
|
696
|
-
import { existsSync } from "node:fs";
|
|
697
|
-
import { dirname as
|
|
781
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
782
|
+
import { dirname as dirname3, resolve as resolve4 } from "node:path";
|
|
698
783
|
|
|
699
784
|
// schema/ship-config.schema.json
|
|
700
785
|
var ship_config_schema_default = {
|
|
701
786
|
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
702
|
-
$id: "https://github.com/Viktorxyz/opencode-
|
|
787
|
+
$id: "https://github.com/Viktorxyz/opencode-ship/schema/ship-config.schema.json",
|
|
703
788
|
title: "opencode-ship user config",
|
|
704
789
|
type: "object",
|
|
705
790
|
required: ["schemaVersion"],
|
|
706
791
|
additionalProperties: false,
|
|
707
792
|
properties: {
|
|
708
793
|
schemaVersion: { const: 1 },
|
|
794
|
+
profile: {
|
|
795
|
+
type: "string",
|
|
796
|
+
enum: ["core", "engineering"],
|
|
797
|
+
description: "Active profile (precedence layer 2: ship.config > lock > core default)."
|
|
798
|
+
},
|
|
709
799
|
owner: {
|
|
710
800
|
type: "string",
|
|
711
801
|
description: "Optional override for the issue/manifest owner field. Defaults to the agent's local user.name."
|
|
@@ -927,11 +1017,11 @@ function validateSchema(value, schema) {
|
|
|
927
1017
|
init_json_pointer();
|
|
928
1018
|
init_hash();
|
|
929
1019
|
function configPath(repoRoot) {
|
|
930
|
-
return
|
|
1020
|
+
return resolve4(repoRoot, ".opencode", "ship.config.json");
|
|
931
1021
|
}
|
|
932
1022
|
async function loadConfig(repoRoot) {
|
|
933
1023
|
const path = configPath(repoRoot);
|
|
934
|
-
if (!
|
|
1024
|
+
if (!existsSync4(path)) return null;
|
|
935
1025
|
const raw = await readFile(path, "utf8");
|
|
936
1026
|
let parsed;
|
|
937
1027
|
try {
|
|
@@ -952,15 +1042,6 @@ async function loadConfig(repoRoot) {
|
|
|
952
1042
|
value: parsed
|
|
953
1043
|
};
|
|
954
1044
|
}
|
|
955
|
-
async function writeConfig(repoRoot, value) {
|
|
956
|
-
const path = configPath(repoRoot);
|
|
957
|
-
await mkdir(dirname2(path), { recursive: true });
|
|
958
|
-
const raw = JSON.stringify(value, null, 2) + "\n";
|
|
959
|
-
const tmp = `${path}.tmp`;
|
|
960
|
-
await writeFile(tmp, raw, "utf8");
|
|
961
|
-
await rename(tmp, path);
|
|
962
|
-
return { path, raw, sha256: bytesHashString(raw) };
|
|
963
|
-
}
|
|
964
1045
|
function renderDefaultConfig(detection, overrides = {}) {
|
|
965
1046
|
const pm = detection?.packageManager ?? "npm";
|
|
966
1047
|
const safeBootstrap = Array.isArray(detection?.worktreeBootstrap) && detection.worktreeBootstrap.length ? detection.worktreeBootstrap : [["npm", "install"]];
|
|
@@ -1002,17 +1083,16 @@ function renderDefaultConfig(detection, overrides = {}) {
|
|
|
1002
1083
|
|
|
1003
1084
|
// src/installer/planner.js
|
|
1004
1085
|
init_json_pointer();
|
|
1005
|
-
init_catalog();
|
|
1006
1086
|
init_root_config();
|
|
1007
1087
|
init_root_config();
|
|
1008
1088
|
async function readBytes(path) {
|
|
1009
|
-
if (!
|
|
1089
|
+
if (!existsSync6(path)) return null;
|
|
1010
1090
|
const buf = await readFile3(path);
|
|
1011
1091
|
const fileStat = await stat(path);
|
|
1012
1092
|
return { bytes: buf, hash: bytesHashString(buf.toString("utf8")), mode: fileStat.mode & 511 };
|
|
1013
1093
|
}
|
|
1014
1094
|
async function readDesiredBytes(source) {
|
|
1015
|
-
if (!source || !
|
|
1095
|
+
if (!source || !existsSync6(source)) return null;
|
|
1016
1096
|
const buf = await readFile3(source);
|
|
1017
1097
|
return { bytes: buf, hash: bytesHashString(buf.toString("utf8")) };
|
|
1018
1098
|
}
|
|
@@ -1085,13 +1165,40 @@ async function planManagedFile({ entry, repoRoot, lock, allowUnowned }) {
|
|
|
1085
1165
|
reason: locked?.sha256 == null ? "managed file already exists; bytes differ from upstream" : "managed file is locally modified"
|
|
1086
1166
|
};
|
|
1087
1167
|
}
|
|
1088
|
-
async function planFileInstall({ repoRoot, lock, allowUnowned = false }) {
|
|
1168
|
+
async function planFileInstall({ repoRoot, lock, allowUnowned = false, catalog = CATALOG }) {
|
|
1089
1169
|
const plan = [];
|
|
1090
|
-
for (const entry of
|
|
1170
|
+
for (const entry of catalog) {
|
|
1091
1171
|
plan.push(await planManagedFile({ entry, repoRoot, lock, allowUnowned }));
|
|
1092
1172
|
}
|
|
1093
1173
|
return plan;
|
|
1094
1174
|
}
|
|
1175
|
+
async function planMigrationCleanup({ repoRoot, lock, migrationReport, allowUnowned = false }) {
|
|
1176
|
+
const action = migrationReport?.actions?.find((entry) => entry.kind === "candidate-remove-legacy-plugin-path");
|
|
1177
|
+
if (!action) return [];
|
|
1178
|
+
const relPath = ".opencode/plugin/opencode-ship.js";
|
|
1179
|
+
const target = `${repoRoot}/${relPath}`;
|
|
1180
|
+
const current = await readBytes(target);
|
|
1181
|
+
if (!current) return [];
|
|
1182
|
+
const locked = lookupLockedFile(lock, relPath);
|
|
1183
|
+
if (locked?.sha256 === current.hash || allowUnowned) {
|
|
1184
|
+
return [{
|
|
1185
|
+
kind: "delete",
|
|
1186
|
+
op: "file",
|
|
1187
|
+
target,
|
|
1188
|
+
relPath,
|
|
1189
|
+
reason: "remove the lock-owned v0.2 singular plugin path"
|
|
1190
|
+
}];
|
|
1191
|
+
}
|
|
1192
|
+
return [{
|
|
1193
|
+
kind: "conflict",
|
|
1194
|
+
op: "file",
|
|
1195
|
+
target,
|
|
1196
|
+
relPath,
|
|
1197
|
+
currentSha: current.hash,
|
|
1198
|
+
previousSha: locked?.sha256 ?? null,
|
|
1199
|
+
reason: "legacy singular plugin is unowned or locally modified"
|
|
1200
|
+
}];
|
|
1201
|
+
}
|
|
1095
1202
|
async function planUninstall({ repoRoot, lock }) {
|
|
1096
1203
|
if (!lock) return [];
|
|
1097
1204
|
const plan = [];
|
|
@@ -1113,7 +1220,7 @@ async function planUninstall({ repoRoot, lock }) {
|
|
|
1113
1220
|
}
|
|
1114
1221
|
return plan;
|
|
1115
1222
|
}
|
|
1116
|
-
async function planConfigSynthesis({ repoRoot, detection, lock, forceOverwrite }) {
|
|
1223
|
+
async function planConfigSynthesis({ repoRoot, detection, lock, forceOverwrite, migrationSeed = null }) {
|
|
1117
1224
|
const existing = await loadConfig(repoRoot);
|
|
1118
1225
|
if (existing?.ok && !forceOverwrite) {
|
|
1119
1226
|
return {
|
|
@@ -1127,10 +1234,11 @@ async function planConfigSynthesis({ repoRoot, detection, lock, forceOverwrite }
|
|
|
1127
1234
|
reason: "user config already present"
|
|
1128
1235
|
};
|
|
1129
1236
|
}
|
|
1130
|
-
const desiredValue = renderDefaultConfig(detection);
|
|
1237
|
+
const desiredValue = migrationSeed ?? renderDefaultConfig(detection);
|
|
1131
1238
|
const desiredJson = JSON.stringify(desiredValue, null, 2) + "\n";
|
|
1132
1239
|
const desiredSha = bytesHashString(desiredJson);
|
|
1133
1240
|
const kind = existing?.ok && forceOverwrite ? "update" : "create";
|
|
1241
|
+
const reason = existing?.ok ? "user config overwritten via --force-config" : migrationSeed ? "synthesising a default config from legacy adapter migration" : "synthesising a default config from detection";
|
|
1134
1242
|
return {
|
|
1135
1243
|
kind,
|
|
1136
1244
|
op: "config",
|
|
@@ -1140,22 +1248,29 @@ async function planConfigSynthesis({ repoRoot, detection, lock, forceOverwrite }
|
|
|
1140
1248
|
desiredSha,
|
|
1141
1249
|
bytes: Buffer.from(desiredJson, "utf8"),
|
|
1142
1250
|
configValue: desiredValue,
|
|
1143
|
-
reason
|
|
1251
|
+
reason
|
|
1144
1252
|
};
|
|
1145
1253
|
}
|
|
1146
1254
|
async function planRootConfigApply({ repoRoot, lock, forceRepair }) {
|
|
1147
1255
|
const detected = findRootConfig(repoRoot);
|
|
1148
1256
|
const target = detected.path ?? defaultRootConfigPath(repoRoot);
|
|
1149
1257
|
const previous = lock?.manager?.rootDocuments?.find((d) => d.path === detected.relative);
|
|
1150
|
-
const fileMissing = !
|
|
1258
|
+
const fileMissing = !existsSync6(target);
|
|
1151
1259
|
if (fileMissing && !forceRepair) {
|
|
1260
|
+
const seededRecords = previous?.pointers && previous.pointers.length > 0 ? previous.pointers : POINTER_ENTRIES.map((entry) => ({
|
|
1261
|
+
pointer: entry.pointer,
|
|
1262
|
+
strategy: entry.strategy,
|
|
1263
|
+
installedSha256: bytesHashString(stableStringify(entry.value)),
|
|
1264
|
+
previous: { existed: false }
|
|
1265
|
+
}));
|
|
1152
1266
|
return {
|
|
1153
1267
|
kind: "noop",
|
|
1154
1268
|
op: "root-config",
|
|
1155
1269
|
target,
|
|
1156
1270
|
relPath: detected.relative,
|
|
1157
1271
|
reason: "no root opencode.json present",
|
|
1158
|
-
edits: []
|
|
1272
|
+
edits: [],
|
|
1273
|
+
pointerRecords: seededRecords
|
|
1159
1274
|
};
|
|
1160
1275
|
}
|
|
1161
1276
|
if (fileMissing && forceRepair) {
|
|
@@ -1163,7 +1278,12 @@ async function planRootConfigApply({ repoRoot, lock, forceRepair }) {
|
|
|
1163
1278
|
const doc = synthesizeDefaultRootConfig2();
|
|
1164
1279
|
const bytes2 = Buffer.from(formatRootConfig3(doc), "utf8");
|
|
1165
1280
|
const newSha2 = bytesHashString(stableStringify(doc));
|
|
1166
|
-
const installedPointers2 =
|
|
1281
|
+
const installedPointers2 = POINTER_ENTRIES.map((entry) => ({
|
|
1282
|
+
pointer: entry.pointer,
|
|
1283
|
+
strategy: entry.strategy,
|
|
1284
|
+
installedSha256: bytesHashString(stableStringify(entry.value)),
|
|
1285
|
+
previous: { existed: false }
|
|
1286
|
+
}));
|
|
1167
1287
|
return {
|
|
1168
1288
|
kind: "create",
|
|
1169
1289
|
op: "root-config",
|
|
@@ -1172,7 +1292,7 @@ async function planRootConfigApply({ repoRoot, lock, forceRepair }) {
|
|
|
1172
1292
|
bytes: bytes2,
|
|
1173
1293
|
desiredSha: newSha2,
|
|
1174
1294
|
currentSha: null,
|
|
1175
|
-
edits:
|
|
1295
|
+
edits: installedPointers2.map((p) => ({ kind: "create", pointer: p.pointer, value: "(synthesized)" })),
|
|
1176
1296
|
pointerRecords: installedPointers2,
|
|
1177
1297
|
format: "json",
|
|
1178
1298
|
document: doc,
|
|
@@ -1187,11 +1307,36 @@ async function planRootConfigApply({ repoRoot, lock, forceRepair }) {
|
|
|
1187
1307
|
target,
|
|
1188
1308
|
relPath: detected.relative,
|
|
1189
1309
|
reason: `root config ${docResult.error.kind}`,
|
|
1190
|
-
edits: []
|
|
1310
|
+
edits: [],
|
|
1311
|
+
pointerRecords: previous?.pointers ?? []
|
|
1191
1312
|
};
|
|
1192
1313
|
}
|
|
1193
1314
|
const previousPointers = previous?.pointers ?? [];
|
|
1194
1315
|
const result = applyOwnedPointers(docResult.value, { allowEqualValues: true });
|
|
1316
|
+
if (result.applied.length === 0 && result.skipped.every((s) => s.reason === "already equal")) {
|
|
1317
|
+
const ownedPointers = previousPointers.length > 0 ? previousPointers : POINTER_ENTRIES.map((entry) => {
|
|
1318
|
+
const existing = getPointer(docResult.value, entry.pointer);
|
|
1319
|
+
return {
|
|
1320
|
+
pointer: entry.pointer,
|
|
1321
|
+
strategy: entry.strategy,
|
|
1322
|
+
installedSha256: bytesHashString(stableStringify(existing ?? entry.value)),
|
|
1323
|
+
previous: existing === void 0 ? { existed: false } : { existed: true, value: existing }
|
|
1324
|
+
};
|
|
1325
|
+
});
|
|
1326
|
+
return {
|
|
1327
|
+
kind: "noop",
|
|
1328
|
+
op: "root-config",
|
|
1329
|
+
target,
|
|
1330
|
+
relPath: detected.relative,
|
|
1331
|
+
reason: "no installer-owned entries missing",
|
|
1332
|
+
edits: [],
|
|
1333
|
+
pointerRecords: ownedPointers,
|
|
1334
|
+
format: docResult.format,
|
|
1335
|
+
document: docResult.value,
|
|
1336
|
+
currentSha: docResult.sha256 ?? null,
|
|
1337
|
+
desiredSha: docResult.sha256 ?? null
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1195
1340
|
const edits = result.applied.map((a) => ({
|
|
1196
1341
|
kind: "create",
|
|
1197
1342
|
pointer: a.pointer,
|
|
@@ -1207,8 +1352,8 @@ async function planRootConfigApply({ repoRoot, lock, forceRepair }) {
|
|
|
1207
1352
|
let bytes;
|
|
1208
1353
|
let docForWrite = result.doc;
|
|
1209
1354
|
try {
|
|
1210
|
-
const { value: sourceValue, format: sourceFormat } = parseRootConfigPreservingOrder2(docResult.raw);
|
|
1211
|
-
docForWrite = sourceValue;
|
|
1355
|
+
const { value: sourceValue, format: sourceFormat } = parseRootConfigPreservingOrder2(docResult.raw ?? "");
|
|
1356
|
+
docForWrite = sourceValue && typeof sourceValue === "object" ? sourceValue : result.doc;
|
|
1212
1357
|
for (const entry of result.applied) {
|
|
1213
1358
|
docForWrite = setPointer2(docForWrite, entry.pointer, entry.value);
|
|
1214
1359
|
}
|
|
@@ -1231,6 +1376,17 @@ async function planRootConfigApply({ repoRoot, lock, forceRepair }) {
|
|
|
1231
1376
|
if (idx >= 0) installedPointers[idx] = next;
|
|
1232
1377
|
else installedPointers.push(next);
|
|
1233
1378
|
}
|
|
1379
|
+
for (const e of result.skipped) {
|
|
1380
|
+
if (e.reason !== "already equal") continue;
|
|
1381
|
+
const idx = installedPointers.findIndex((p) => p.pointer === e.pointer);
|
|
1382
|
+
if (idx >= 0) continue;
|
|
1383
|
+
installedPointers.push({
|
|
1384
|
+
pointer: e.pointer,
|
|
1385
|
+
strategy: "value",
|
|
1386
|
+
installedSha256: bytesHashString(stableStringify(e.existing ?? null)),
|
|
1387
|
+
previous: { existed: true, value: e.existing }
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1234
1390
|
return {
|
|
1235
1391
|
kind: edits.some((e) => e.kind === "conflict") ? "conflict" : edits.length ? "update" : "noop",
|
|
1236
1392
|
op: "root-config",
|
|
@@ -1247,15 +1403,97 @@ async function planRootConfigApply({ repoRoot, lock, forceRepair }) {
|
|
|
1247
1403
|
};
|
|
1248
1404
|
}
|
|
1249
1405
|
|
|
1406
|
+
// src/installer/lock.js
|
|
1407
|
+
init_hash();
|
|
1408
|
+
init_json_pointer();
|
|
1409
|
+
import { readFile as readFile4, writeFile as writeFile2, rename as rename2, mkdir as mkdir2 } from "node:fs/promises";
|
|
1410
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
1411
|
+
import { dirname as dirname4, resolve as resolve6 } from "node:path";
|
|
1412
|
+
var CURRENT_LOCK_SCHEMA = 2;
|
|
1413
|
+
function lockPath(repoRoot) {
|
|
1414
|
+
return resolve6(repoRoot, ".opencode", "ship.lock.json");
|
|
1415
|
+
}
|
|
1416
|
+
function computeIntegrity(lock) {
|
|
1417
|
+
const { integrity: _ignored, ...without } = lock ?? {};
|
|
1418
|
+
void _ignored;
|
|
1419
|
+
return {
|
|
1420
|
+
lockSha256: bytesHashString(stableStringify(without))
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
function validateLock(rawLock) {
|
|
1424
|
+
if (rawLock === null || rawLock === void 0) {
|
|
1425
|
+
return { ok: true, kind: "missing", issues: [] };
|
|
1426
|
+
}
|
|
1427
|
+
if (typeof rawLock !== "object" || Array.isArray(rawLock)) {
|
|
1428
|
+
return { ok: false, kind: "shape", issues: ["lock root must be an object"] };
|
|
1429
|
+
}
|
|
1430
|
+
const issues = [];
|
|
1431
|
+
let kind = "ok";
|
|
1432
|
+
if (rawLock.contractVersion !== CURRENT_LOCK_SCHEMA && rawLock.contractVersion !== 1) {
|
|
1433
|
+
issues.push(`unsupported contractVersion: ${JSON.stringify(rawLock.contractVersion)} (expected ${CURRENT_LOCK_SCHEMA} or 1)`);
|
|
1434
|
+
kind = "schema";
|
|
1435
|
+
}
|
|
1436
|
+
const manager = rawLock.manager;
|
|
1437
|
+
if (manager === void 0) {
|
|
1438
|
+
issues.push("manager section missing");
|
|
1439
|
+
kind = kind === "ok" ? "shape" : kind;
|
|
1440
|
+
} else if (typeof manager !== "object" || manager === null) {
|
|
1441
|
+
issues.push("manager section must be an object");
|
|
1442
|
+
kind = kind === "ok" ? "shape" : kind;
|
|
1443
|
+
} else if (manager.schemaVersion !== CURRENT_LOCK_SCHEMA && manager.schemaVersion !== 1) {
|
|
1444
|
+
issues.push(`unsupported manager.schemaVersion: ${JSON.stringify(manager.schemaVersion)} (expected ${CURRENT_LOCK_SCHEMA} or 1)`);
|
|
1445
|
+
kind = "schema";
|
|
1446
|
+
} else if (manager.name !== "opencode-ship") {
|
|
1447
|
+
issues.push(`unknown manager.name: ${JSON.stringify(manager.name)}`);
|
|
1448
|
+
kind = "shape";
|
|
1449
|
+
} else if (rawLock.contractVersion === CURRENT_LOCK_SCHEMA && manager.schemaVersion === CURRENT_LOCK_SCHEMA && manager.profile !== void 0 && !isValidProfile(manager.profile)) {
|
|
1450
|
+
issues.push(`invalid manager.profile: ${JSON.stringify(manager.profile)} (expected one of: core, engineering)`);
|
|
1451
|
+
kind = "shape";
|
|
1452
|
+
}
|
|
1453
|
+
if (!rawLock.files || !Array.isArray(rawLock.files)) {
|
|
1454
|
+
issues.push("files must be an array");
|
|
1455
|
+
kind = kind === "ok" ? "shape" : kind;
|
|
1456
|
+
}
|
|
1457
|
+
if (!rawLock.integrity || typeof rawLock.integrity !== "object") {
|
|
1458
|
+
issues.push("integrity section missing");
|
|
1459
|
+
kind = kind === "ok" ? "shape" : kind;
|
|
1460
|
+
} else {
|
|
1461
|
+
const expected = computeIntegrity(rawLock).lockSha256;
|
|
1462
|
+
if (expected !== rawLock.integrity.lockSha256) {
|
|
1463
|
+
issues.push(`integrity mismatch: stored ${rawLock.integrity.lockSha256} != computed ${expected}`);
|
|
1464
|
+
kind = "integrity";
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
return { ok: issues.length === 0, kind, issues };
|
|
1468
|
+
}
|
|
1469
|
+
async function readValidatedLock(repoRoot) {
|
|
1470
|
+
const path = lockPath(repoRoot);
|
|
1471
|
+
if (!existsSync7(path)) {
|
|
1472
|
+
return { kind: "missing", lock: null, issues: [] };
|
|
1473
|
+
}
|
|
1474
|
+
let raw;
|
|
1475
|
+
try {
|
|
1476
|
+
const text = await readFile4(path, "utf8");
|
|
1477
|
+
raw = JSON.parse(text);
|
|
1478
|
+
} catch (e) {
|
|
1479
|
+
return {
|
|
1480
|
+
kind: "integrity",
|
|
1481
|
+
lock: null,
|
|
1482
|
+
issues: [`unable to parse lock JSON: ${e?.message ?? String(e)}`]
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
const validation = validateLock(raw);
|
|
1486
|
+
return { kind: validation.kind, lock: validation.ok ? raw : null, issues: validation.issues };
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1250
1489
|
// src/installer/executor.js
|
|
1251
|
-
init_lock();
|
|
1252
1490
|
init_hash();
|
|
1253
1491
|
init_json_pointer();
|
|
1254
1492
|
|
|
1255
1493
|
// src/installer/detection/project.js
|
|
1256
1494
|
import { spawnSync } from "node:child_process";
|
|
1257
|
-
import { existsSync as
|
|
1258
|
-
import { resolve as
|
|
1495
|
+
import { existsSync as existsSync8, readFileSync as readFileSync4 } from "node:fs";
|
|
1496
|
+
import { resolve as resolve7, join } from "node:path";
|
|
1259
1497
|
function runGit(cwd, args) {
|
|
1260
1498
|
const r = spawnSync("git", ["-C", cwd, ...args], {
|
|
1261
1499
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -1264,17 +1502,17 @@ function runGit(cwd, args) {
|
|
|
1264
1502
|
return { status: r.status ?? -1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
1265
1503
|
}
|
|
1266
1504
|
function detectPackageManager(repoRoot) {
|
|
1267
|
-
if (
|
|
1268
|
-
if (
|
|
1269
|
-
if (
|
|
1270
|
-
if (
|
|
1505
|
+
if (existsSync8(join(repoRoot, "pnpm-lock.yaml"))) return "pnpm";
|
|
1506
|
+
if (existsSync8(join(repoRoot, "yarn.lock"))) return "yarn";
|
|
1507
|
+
if (existsSync8(join(repoRoot, "bun.lockb"))) return "bun";
|
|
1508
|
+
if (existsSync8(join(repoRoot, "package-lock.json"))) return "npm";
|
|
1271
1509
|
return null;
|
|
1272
1510
|
}
|
|
1273
1511
|
function readPackageJson(repoRoot) {
|
|
1274
1512
|
const path = join(repoRoot, "package.json");
|
|
1275
|
-
if (!
|
|
1513
|
+
if (!existsSync8(path)) return null;
|
|
1276
1514
|
try {
|
|
1277
|
-
return JSON.parse(
|
|
1515
|
+
return JSON.parse(readFileSync4(path, "utf8"));
|
|
1278
1516
|
} catch {
|
|
1279
1517
|
return null;
|
|
1280
1518
|
}
|
|
@@ -1349,7 +1587,7 @@ function detectOwner(repoRoot) {
|
|
|
1349
1587
|
}
|
|
1350
1588
|
function detectProject(repoRoot = process.cwd()) {
|
|
1351
1589
|
const errors = [];
|
|
1352
|
-
const cwd =
|
|
1590
|
+
const cwd = resolve7(repoRoot);
|
|
1353
1591
|
const inside = runGit(cwd, ["rev-parse", "--show-toplevel"]);
|
|
1354
1592
|
if (inside.status !== 0) {
|
|
1355
1593
|
errors.push({ kind: "not-a-git-repo", path: cwd, detail: inside.stderr.trim() });
|
|
@@ -1397,7 +1635,6 @@ function detectProject(repoRoot = process.cwd()) {
|
|
|
1397
1635
|
|
|
1398
1636
|
// src/installer/executor.js
|
|
1399
1637
|
init_root_config();
|
|
1400
|
-
init_lock();
|
|
1401
1638
|
|
|
1402
1639
|
// src/installer/transaction.js
|
|
1403
1640
|
import {
|
|
@@ -1409,34 +1646,62 @@ import {
|
|
|
1409
1646
|
stat as stat2,
|
|
1410
1647
|
open
|
|
1411
1648
|
} from "node:fs/promises";
|
|
1412
|
-
import { existsSync as
|
|
1413
|
-
import { dirname as
|
|
1649
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
1650
|
+
import { dirname as dirname5, resolve as resolve8 } from "node:path";
|
|
1414
1651
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
1652
|
+
init_hash();
|
|
1415
1653
|
function gitDir(repoRoot) {
|
|
1416
1654
|
const r = spawnSync2("git", ["-C", repoRoot, "rev-parse", "--path-format=absolute", "--git-dir"], {
|
|
1417
1655
|
encoding: "utf8"
|
|
1418
1656
|
});
|
|
1419
1657
|
if (r.status !== 0) return null;
|
|
1420
1658
|
const trimmed = r.stdout.trim();
|
|
1421
|
-
return trimmed ?
|
|
1659
|
+
return trimmed ? resolve8(repoRoot, trimmed) : null;
|
|
1422
1660
|
}
|
|
1423
1661
|
function lockDir(repoRoot) {
|
|
1424
|
-
return
|
|
1662
|
+
return resolve8(gitDir(repoRoot) ?? repoRoot, "opencode-ship");
|
|
1425
1663
|
}
|
|
1426
1664
|
function transactionLockPath(repoRoot) {
|
|
1427
|
-
return
|
|
1665
|
+
return resolve8(lockDir(repoRoot), ".txn.lock");
|
|
1428
1666
|
}
|
|
1429
1667
|
function journalPath(repoRoot, txnId) {
|
|
1430
|
-
return
|
|
1668
|
+
return resolve8(lockDir(repoRoot), `.txn-${txnId}.journal`);
|
|
1669
|
+
}
|
|
1670
|
+
function backupPath(target, token) {
|
|
1671
|
+
return `${target}.txn-${token}-backup`;
|
|
1672
|
+
}
|
|
1673
|
+
function stagedPath(target, token) {
|
|
1674
|
+
return `${target}.txn-${token}-staged`;
|
|
1675
|
+
}
|
|
1676
|
+
async function mkdirp(path) {
|
|
1677
|
+
await mkdir3(path, { recursive: true });
|
|
1431
1678
|
}
|
|
1432
1679
|
async function acquireLock(repoRoot, txnId) {
|
|
1433
1680
|
const dir = lockDir(repoRoot);
|
|
1434
|
-
|
|
1435
|
-
await
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1681
|
+
const path = transactionLockPath(repoRoot);
|
|
1682
|
+
await mkdirp(dir);
|
|
1683
|
+
let created = false;
|
|
1684
|
+
try {
|
|
1685
|
+
const handle = await open(path, "wx", 384);
|
|
1686
|
+
created = true;
|
|
1687
|
+
try {
|
|
1688
|
+
await handle.writeFile(JSON.stringify({
|
|
1689
|
+
pid: process.pid,
|
|
1690
|
+
txnId,
|
|
1691
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1692
|
+
}));
|
|
1693
|
+
await handle.sync();
|
|
1694
|
+
} finally {
|
|
1695
|
+
await handle.close();
|
|
1696
|
+
}
|
|
1697
|
+
await fsyncDir(dir);
|
|
1698
|
+
} catch (error) {
|
|
1699
|
+
if (created) {
|
|
1700
|
+
await unlink(path).catch(() => null);
|
|
1701
|
+
await fsyncDir(dir);
|
|
1702
|
+
}
|
|
1703
|
+
throw error;
|
|
1704
|
+
}
|
|
1440
1705
|
}
|
|
1441
1706
|
async function releaseLock(repoRoot) {
|
|
1442
1707
|
try {
|
|
@@ -1444,6 +1709,18 @@ async function releaseLock(repoRoot) {
|
|
|
1444
1709
|
} catch {
|
|
1445
1710
|
}
|
|
1446
1711
|
}
|
|
1712
|
+
async function liveLockOwner(repoRoot) {
|
|
1713
|
+
const path = transactionLockPath(repoRoot);
|
|
1714
|
+
if (!existsSync9(path)) return false;
|
|
1715
|
+
try {
|
|
1716
|
+
const lock = JSON.parse(await readFile5(path, "utf8"));
|
|
1717
|
+
if (!Number.isInteger(lock?.pid) || lock.pid <= 0) return false;
|
|
1718
|
+
process.kill(lock.pid, 0);
|
|
1719
|
+
return true;
|
|
1720
|
+
} catch (error) {
|
|
1721
|
+
return error?.code === "EPERM";
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1447
1724
|
async function fsyncDir(path) {
|
|
1448
1725
|
try {
|
|
1449
1726
|
const handle = await open(path, "r");
|
|
@@ -1455,21 +1732,32 @@ async function fsyncDir(path) {
|
|
|
1455
1732
|
function randomToken() {
|
|
1456
1733
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1457
1734
|
}
|
|
1458
|
-
async function captureOriginal(target) {
|
|
1459
|
-
if (!existsSync6(target)) return null;
|
|
1460
|
-
try {
|
|
1461
|
-
const bytes = await readFile5(target);
|
|
1462
|
-
const fileStat = await stat2(target);
|
|
1463
|
-
return { bytes, mode: fileStat.mode & 511 };
|
|
1464
|
-
} catch {
|
|
1465
|
-
return null;
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
1735
|
function asMode(mode) {
|
|
1469
1736
|
return typeof mode === "number" ? mode : 420;
|
|
1470
1737
|
}
|
|
1471
1738
|
async function writeJournal(repoRoot, txnId, journal) {
|
|
1472
|
-
|
|
1739
|
+
const { entries, ...header } = journal;
|
|
1740
|
+
const json = { ...header, ledger: entries.map((entry) => ({
|
|
1741
|
+
op: entry.op,
|
|
1742
|
+
target: entry.target,
|
|
1743
|
+
backup: entry.backup ?? null,
|
|
1744
|
+
staged: entry.staged ?? null,
|
|
1745
|
+
hadOriginal: entry.hadOriginal ?? null,
|
|
1746
|
+
commitMarker: entry.commitMarker ?? false,
|
|
1747
|
+
installedSha256: entry.installedSha256 ?? null,
|
|
1748
|
+
mode: entry.mode ?? null
|
|
1749
|
+
})) };
|
|
1750
|
+
const target = journalPath(repoRoot, txnId);
|
|
1751
|
+
const staged = `${target}.tmp`;
|
|
1752
|
+
const handle = await open(staged, "w", 384);
|
|
1753
|
+
try {
|
|
1754
|
+
await handle.writeFile(JSON.stringify(json, null, 2), "utf8");
|
|
1755
|
+
await handle.sync();
|
|
1756
|
+
} finally {
|
|
1757
|
+
await handle.close();
|
|
1758
|
+
}
|
|
1759
|
+
await rename3(staged, target);
|
|
1760
|
+
await fsyncDir(dirname5(target));
|
|
1473
1761
|
}
|
|
1474
1762
|
async function clearJournal(repoRoot, txnId) {
|
|
1475
1763
|
if (!txnId) return;
|
|
@@ -1478,50 +1766,104 @@ async function clearJournal(repoRoot, txnId) {
|
|
|
1478
1766
|
} catch {
|
|
1479
1767
|
}
|
|
1480
1768
|
}
|
|
1769
|
+
async function readJournal(repoRoot, name) {
|
|
1770
|
+
const path = resolve8(lockDir(repoRoot), name);
|
|
1771
|
+
try {
|
|
1772
|
+
const raw = await readFile5(path, "utf8");
|
|
1773
|
+
return JSON.parse(raw);
|
|
1774
|
+
} catch {
|
|
1775
|
+
return null;
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
async function isCommitted(journal) {
|
|
1779
|
+
if (journal.committed) return true;
|
|
1780
|
+
const marker = journal.ledger?.find((entry) => entry.commitMarker);
|
|
1781
|
+
if (!marker?.target || !marker.installedSha256 || !existsSync9(marker.target)) return false;
|
|
1782
|
+
try {
|
|
1783
|
+
return bytesHashString(await readFile5(marker.target, "utf8")) === marker.installedSha256;
|
|
1784
|
+
} catch {
|
|
1785
|
+
return false;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
async function restoreEntry(entry) {
|
|
1789
|
+
if (entry.op === "write") {
|
|
1790
|
+
if (entry.backup && existsSync9(entry.backup)) {
|
|
1791
|
+
await rename3(entry.backup, entry.target);
|
|
1792
|
+
} else if (entry.hadOriginal === false && existsSync9(entry.target)) {
|
|
1793
|
+
await unlink(entry.target);
|
|
1794
|
+
}
|
|
1795
|
+
if (entry.staged && existsSync9(entry.staged)) await unlink(entry.staged);
|
|
1796
|
+
} else if (entry.op === "delete" && entry.backup && existsSync9(entry.backup)) {
|
|
1797
|
+
await rename3(entry.backup, entry.target);
|
|
1798
|
+
}
|
|
1799
|
+
await fsyncDir(dirname5(entry.target));
|
|
1800
|
+
}
|
|
1801
|
+
async function recoverJournal(repoRoot, dir, name) {
|
|
1802
|
+
const journal = await readJournal(repoRoot, name);
|
|
1803
|
+
if (!journal) {
|
|
1804
|
+
await unlink(resolve8(dir, name)).catch(() => null);
|
|
1805
|
+
return true;
|
|
1806
|
+
}
|
|
1807
|
+
const committed = await isCommitted(journal);
|
|
1808
|
+
const entries = committed ? journal.ledger ?? [] : [...journal.ledger ?? []].reverse();
|
|
1809
|
+
let complete = true;
|
|
1810
|
+
for (const entry of entries) {
|
|
1811
|
+
try {
|
|
1812
|
+
if (committed) await commitEntry(entry);
|
|
1813
|
+
else await restoreEntry(entry);
|
|
1814
|
+
} catch {
|
|
1815
|
+
complete = false;
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
if (complete) await unlink(resolve8(dir, name)).catch(() => null);
|
|
1819
|
+
return complete;
|
|
1820
|
+
}
|
|
1481
1821
|
async function recover(repoRoot) {
|
|
1482
1822
|
const dir = lockDir(repoRoot);
|
|
1483
|
-
if (!
|
|
1823
|
+
if (!existsSync9(dir)) return { recovered: false, recoveredCount: 0 };
|
|
1824
|
+
if (await liveLockOwner(repoRoot)) {
|
|
1825
|
+
return { recovered: false, recoveredCount: 0, blocked: true };
|
|
1826
|
+
}
|
|
1484
1827
|
const { readdir } = await import("node:fs/promises");
|
|
1485
1828
|
const names = await readdir(dir).catch(() => []);
|
|
1486
1829
|
const journals = names.filter((n) => n.startsWith(".txn-") && n.endsWith(".journal"));
|
|
1487
|
-
if (!journals.length)
|
|
1830
|
+
if (!journals.length) {
|
|
1831
|
+
await releaseLock(repoRoot);
|
|
1832
|
+
return { recovered: false, recoveredCount: 0 };
|
|
1833
|
+
}
|
|
1488
1834
|
let totalRecovered = 0;
|
|
1835
|
+
let recoveryFailed = false;
|
|
1489
1836
|
for (const name of journals) {
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
const raw = await readFile5(resolve6(dir, name), "utf8");
|
|
1493
|
-
payload = JSON.parse(raw);
|
|
1494
|
-
} catch {
|
|
1495
|
-
await unlink(resolve6(dir, name)).catch(() => null);
|
|
1496
|
-
continue;
|
|
1497
|
-
}
|
|
1498
|
-
const journal = payload;
|
|
1499
|
-
for (const entry of [...journal.entries ?? []].reverse()) {
|
|
1500
|
-
if (entry.op !== "promote") continue;
|
|
1501
|
-
if (entry.original) {
|
|
1502
|
-
try {
|
|
1503
|
-
await writeFile3(entry.target, entry.original.bytes, { mode: entry.original.mode ?? 420 });
|
|
1504
|
-
} catch {
|
|
1505
|
-
}
|
|
1506
|
-
} else if (entry.staged) {
|
|
1507
|
-
try {
|
|
1508
|
-
await rename3(entry.staged, entry.target);
|
|
1509
|
-
} catch {
|
|
1510
|
-
try {
|
|
1511
|
-
await unlink(entry.staged);
|
|
1512
|
-
} catch {
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1517
|
-
await unlink(resolve6(dir, name)).catch(() => null);
|
|
1518
|
-
totalRecovered += 1;
|
|
1837
|
+
if (await recoverJournal(repoRoot, dir, name)) totalRecovered += 1;
|
|
1838
|
+
else recoveryFailed = true;
|
|
1519
1839
|
}
|
|
1520
1840
|
await releaseLock(repoRoot);
|
|
1521
|
-
return {
|
|
1841
|
+
return {
|
|
1842
|
+
recovered: totalRecovered > 0,
|
|
1843
|
+
recoveredCount: totalRecovered,
|
|
1844
|
+
blocked: recoveryFailed,
|
|
1845
|
+
reason: recoveryFailed ? "recovery-failed" : null
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1848
|
+
async function commitEntry(entry) {
|
|
1849
|
+
let changed = false;
|
|
1850
|
+
if (entry.backup && existsSync9(entry.backup)) {
|
|
1851
|
+
await unlink(entry.backup);
|
|
1852
|
+
changed = true;
|
|
1853
|
+
}
|
|
1854
|
+
if (entry.staged && existsSync9(entry.staged)) {
|
|
1855
|
+
await unlink(entry.staged);
|
|
1856
|
+
changed = true;
|
|
1857
|
+
}
|
|
1858
|
+
if (changed) await fsyncDir(dirname5(entry.target));
|
|
1522
1859
|
}
|
|
1523
1860
|
async function executePlan({ repoRoot, plan, newLockBuilder }) {
|
|
1524
1861
|
const recovered = await recover(repoRoot);
|
|
1862
|
+
if (recovered.blocked) {
|
|
1863
|
+
const kind = recovered.reason ?? "lock-held";
|
|
1864
|
+
const message = kind === "lock-held" ? "another opencode-ship transaction is active" : "a previous opencode-ship transaction could not be recovered";
|
|
1865
|
+
return { ok: false, error: { kind, message } };
|
|
1866
|
+
}
|
|
1525
1867
|
const txnId = `txn-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
1526
1868
|
try {
|
|
1527
1869
|
await acquireLock(repoRoot, txnId);
|
|
@@ -1529,58 +1871,94 @@ async function executePlan({ repoRoot, plan, newLockBuilder }) {
|
|
|
1529
1871
|
return { ok: false, error: { kind: "lock-held", message: e?.message ?? String(e) } };
|
|
1530
1872
|
}
|
|
1531
1873
|
const journal = { repoRoot, txnId, startedAt: (/* @__PURE__ */ new Date()).toISOString(), entries: [] };
|
|
1532
|
-
await writeJournal(repoRoot, txnId, journal);
|
|
1533
1874
|
try {
|
|
1875
|
+
await writeJournal(repoRoot, txnId, journal);
|
|
1534
1876
|
for (const op of plan) {
|
|
1535
1877
|
if (op.op !== "file") continue;
|
|
1536
1878
|
if (op.kind === "conflict" || op.kind === "noop" || op.kind === "converge") continue;
|
|
1537
|
-
const
|
|
1879
|
+
const token = randomToken();
|
|
1880
|
+
const backup = backupPath(op.target, token);
|
|
1881
|
+
const staged = stagedPath(op.target, token);
|
|
1538
1882
|
if (op.kind === "delete") {
|
|
1539
|
-
if (!
|
|
1540
|
-
const backup = `${op.target}.txn-${randomToken()}-backup`;
|
|
1541
|
-
await rename3(op.target, backup);
|
|
1883
|
+
if (!existsSync9(op.target)) continue;
|
|
1542
1884
|
journal.entries.push({
|
|
1543
|
-
op: "
|
|
1885
|
+
op: "delete",
|
|
1544
1886
|
target: op.target,
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1887
|
+
backup,
|
|
1888
|
+
staged: null,
|
|
1889
|
+
hadOriginal: true,
|
|
1890
|
+
mode: asMode(op.mode)
|
|
1548
1891
|
});
|
|
1892
|
+
await writeJournal(repoRoot, txnId, journal);
|
|
1893
|
+
await rename3(op.target, backup);
|
|
1549
1894
|
} else {
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
await writeFile3(tempPath, op.bytes ?? Buffer.alloc(0), { mode: asMode(op.mode) });
|
|
1553
|
-
const h = await open(tempPath, "r+");
|
|
1554
|
-
await h.sync();
|
|
1555
|
-
await h.close();
|
|
1556
|
-
await rename3(tempPath, op.target);
|
|
1895
|
+
await mkdirp(dirname5(op.target));
|
|
1896
|
+
const hadOriginal = existsSync9(op.target);
|
|
1557
1897
|
journal.entries.push({
|
|
1558
|
-
op: "
|
|
1898
|
+
op: "write",
|
|
1559
1899
|
target: op.target,
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1900
|
+
backup: hadOriginal ? backup : null,
|
|
1901
|
+
staged,
|
|
1902
|
+
hadOriginal,
|
|
1903
|
+
mode: asMode(op.mode)
|
|
1563
1904
|
});
|
|
1905
|
+
await writeJournal(repoRoot, txnId, journal);
|
|
1906
|
+
await writeFile3(staged, op.bytes ?? Buffer.alloc(0), { mode: asMode(op.mode) });
|
|
1907
|
+
const h = await open(staged, "r+");
|
|
1908
|
+
await h.sync();
|
|
1909
|
+
await h.close();
|
|
1910
|
+
if (hadOriginal) await rename3(op.target, backup);
|
|
1911
|
+
await rename3(staged, op.target);
|
|
1564
1912
|
}
|
|
1565
|
-
await fsyncDir(
|
|
1566
|
-
await writeJournal(repoRoot, txnId, journal);
|
|
1913
|
+
await fsyncDir(dirname5(op.target));
|
|
1567
1914
|
}
|
|
1568
1915
|
if (newLockBuilder) {
|
|
1569
1916
|
const lockValue = await newLockBuilder();
|
|
1570
|
-
const { writeLock: writer } = await Promise.resolve().then(() => (init_lock(), lock_exports));
|
|
1571
1917
|
const lockPathTarget = `${repoRoot}/.opencode/ship.lock.json`;
|
|
1918
|
+
await mkdirp(dirname5(lockPathTarget));
|
|
1919
|
+
const token = randomToken();
|
|
1920
|
+
const backup = backupPath(lockPathTarget, token);
|
|
1921
|
+
const staged = stagedPath(lockPathTarget, token);
|
|
1922
|
+
const hadOriginal = existsSync9(lockPathTarget);
|
|
1923
|
+
const finalLock = { ...lockValue, integrity: computeIntegrity(lockValue) };
|
|
1924
|
+
const lockBytes = JSON.stringify(finalLock, null, 2) + "\n";
|
|
1572
1925
|
journal.entries.push({
|
|
1573
|
-
op: "
|
|
1926
|
+
op: "write",
|
|
1574
1927
|
target: lockPathTarget,
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1928
|
+
backup: hadOriginal ? backup : null,
|
|
1929
|
+
staged,
|
|
1930
|
+
hadOriginal,
|
|
1931
|
+
commitMarker: true,
|
|
1932
|
+
installedSha256: bytesHashString(lockBytes),
|
|
1933
|
+
mode: 420
|
|
1578
1934
|
});
|
|
1579
|
-
await writer(repoRoot, lockValue);
|
|
1580
1935
|
await writeJournal(repoRoot, txnId, journal);
|
|
1936
|
+
await writeFile3(staged, lockBytes, { mode: 420 });
|
|
1937
|
+
const handle = await open(staged, "r+");
|
|
1938
|
+
await handle.sync();
|
|
1939
|
+
await handle.close();
|
|
1940
|
+
if (hadOriginal) await rename3(lockPathTarget, backup);
|
|
1941
|
+
await rename3(staged, lockPathTarget);
|
|
1942
|
+
await fsyncDir(dirname5(lockPathTarget));
|
|
1943
|
+
}
|
|
1944
|
+
journal.committed = true;
|
|
1945
|
+
await writeJournal(repoRoot, txnId, journal);
|
|
1946
|
+
let cleanupComplete = true;
|
|
1947
|
+
for (const entry of journal.entries) {
|
|
1948
|
+
try {
|
|
1949
|
+
await commitEntry(entry);
|
|
1950
|
+
} catch {
|
|
1951
|
+
cleanupComplete = false;
|
|
1952
|
+
}
|
|
1581
1953
|
}
|
|
1582
|
-
await clearJournal(repoRoot, txnId);
|
|
1583
|
-
return {
|
|
1954
|
+
if (cleanupComplete) await clearJournal(repoRoot, txnId);
|
|
1955
|
+
return {
|
|
1956
|
+
ok: true,
|
|
1957
|
+
txnId,
|
|
1958
|
+
recovered: recovered.recovered ?? false,
|
|
1959
|
+
recoveredCount: recovered.recoveredCount ?? 0,
|
|
1960
|
+
cleanupPending: !cleanupComplete
|
|
1961
|
+
};
|
|
1584
1962
|
} catch (e) {
|
|
1585
1963
|
await rollback(repoRoot, journal);
|
|
1586
1964
|
return { ok: false, error: { kind: "transaction-failed", message: e?.message ?? String(e) } };
|
|
@@ -1589,75 +1967,51 @@ async function executePlan({ repoRoot, plan, newLockBuilder }) {
|
|
|
1589
1967
|
}
|
|
1590
1968
|
}
|
|
1591
1969
|
async function rollback(repoRoot, journal) {
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
} else if (entry.staged) {
|
|
1600
|
-
try {
|
|
1601
|
-
await rename3(entry.staged, entry.target);
|
|
1602
|
-
} catch {
|
|
1603
|
-
try {
|
|
1604
|
-
await unlink(entry.staged);
|
|
1605
|
-
} catch {
|
|
1606
|
-
}
|
|
1607
|
-
}
|
|
1608
|
-
} else {
|
|
1609
|
-
try {
|
|
1610
|
-
await unlink(entry.target);
|
|
1611
|
-
} catch {
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
if (entry.staged && entry.staged !== entry.target && existsSync6(entry.staged)) {
|
|
1615
|
-
try {
|
|
1616
|
-
await unlink(entry.staged);
|
|
1617
|
-
} catch {
|
|
1618
|
-
}
|
|
1970
|
+
const entries = journal.entries ?? journal.ledger ?? [];
|
|
1971
|
+
let complete = true;
|
|
1972
|
+
for (const entry of [...entries].reverse()) {
|
|
1973
|
+
try {
|
|
1974
|
+
await restoreEntry(entry);
|
|
1975
|
+
} catch {
|
|
1976
|
+
complete = false;
|
|
1619
1977
|
}
|
|
1620
1978
|
}
|
|
1621
|
-
if (journal.txnId) await clearJournal(repoRoot, journal.txnId);
|
|
1979
|
+
if (complete && journal.txnId) await clearJournal(repoRoot, journal.txnId);
|
|
1622
1980
|
}
|
|
1623
1981
|
|
|
1624
1982
|
// src/installer/migration.js
|
|
1625
|
-
init_lock();
|
|
1626
1983
|
import { readFile as readFile6 } from "node:fs/promises";
|
|
1627
|
-
import { existsSync as
|
|
1628
|
-
import { resolve as
|
|
1629
|
-
|
|
1630
|
-
// src/installer/ship-adapter.js
|
|
1631
|
-
import { resolve as resolve7 } from "node:path";
|
|
1632
|
-
|
|
1633
|
-
// src/installer/migration.js
|
|
1984
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
1985
|
+
import { resolve as resolve9 } from "node:path";
|
|
1634
1986
|
function legacyAdapterPath(repoRoot) {
|
|
1635
|
-
return
|
|
1987
|
+
return resolve9(repoRoot, ".opencode", "delivery.json");
|
|
1636
1988
|
}
|
|
1637
1989
|
function legacyLockPath(repoRoot) {
|
|
1638
|
-
return
|
|
1990
|
+
return resolve9(repoRoot, ".opencode", "delivery.lock.json");
|
|
1639
1991
|
}
|
|
1640
1992
|
function legacyPluginPath(repoRoot) {
|
|
1641
|
-
return
|
|
1993
|
+
return resolve9(repoRoot, ".opencode", "plugin", "delivery.ts");
|
|
1642
1994
|
}
|
|
1643
1995
|
async function detectLegacyShapes(repoRoot) {
|
|
1644
1996
|
const out = {
|
|
1645
1997
|
adapter: false,
|
|
1646
1998
|
legacyLock: false,
|
|
1647
1999
|
plugin: false,
|
|
2000
|
+
pluginOld: false,
|
|
1648
2001
|
reviewer: false,
|
|
1649
2002
|
verifier: false
|
|
1650
2003
|
};
|
|
1651
|
-
if (
|
|
1652
|
-
if (
|
|
1653
|
-
if (
|
|
1654
|
-
if (
|
|
1655
|
-
if (
|
|
2004
|
+
if (existsSync10(legacyAdapterPath(repoRoot))) out.adapter = true;
|
|
2005
|
+
if (existsSync10(legacyLockPath(repoRoot))) out.legacyLock = true;
|
|
2006
|
+
if (existsSync10(legacyPluginPath(repoRoot))) out.plugin = true;
|
|
2007
|
+
if (existsSync10(resolve9(repoRoot, ".opencode/plugin/opencode-ship.js"))) out.pluginOld = true;
|
|
2008
|
+
if (existsSync10(resolve9(repoRoot, ".opencode/agents/delivery-reviewer.md"))) out.reviewer = true;
|
|
2009
|
+
if (existsSync10(resolve9(repoRoot, ".opencode/agents/delivery-verifier.md"))) out.verifier = true;
|
|
1656
2010
|
return out;
|
|
1657
2011
|
}
|
|
1658
2012
|
async function readLegacyAdapter(repoRoot) {
|
|
1659
2013
|
const path = legacyAdapterPath(repoRoot);
|
|
1660
|
-
if (!
|
|
2014
|
+
if (!existsSync10(path)) return null;
|
|
1661
2015
|
try {
|
|
1662
2016
|
const raw = await readFile6(path, "utf8");
|
|
1663
2017
|
return { path, raw, value: JSON.parse(raw) };
|
|
@@ -1665,44 +2019,49 @@ async function readLegacyAdapter(repoRoot) {
|
|
|
1665
2019
|
return null;
|
|
1666
2020
|
}
|
|
1667
2021
|
}
|
|
1668
|
-
async function migration({ repoRoot, lock, forceRepair }) {
|
|
2022
|
+
async function migration({ repoRoot, lock, forceRepair, detection = null }) {
|
|
1669
2023
|
const shapes = await detectLegacyShapes(repoRoot);
|
|
1670
2024
|
const legacy = await readLegacyAdapter(repoRoot);
|
|
1671
2025
|
const config = await loadConfig(repoRoot);
|
|
1672
2026
|
const actions = [];
|
|
2027
|
+
let proposedConfigSeed = null;
|
|
1673
2028
|
if (legacy && !config?.ok) {
|
|
1674
|
-
|
|
1675
|
-
actions.push({ kind: "
|
|
2029
|
+
proposedConfigSeed = legacyToShipConfig(legacy.value, detection);
|
|
2030
|
+
actions.push({ kind: "candidate-seed-config", from: legacy.path });
|
|
1676
2031
|
}
|
|
1677
2032
|
if (legacy && shapes.legacyLock && !lock?.manager) {
|
|
1678
2033
|
actions.push({ kind: "kept-legacy-lock", path: legacyLockPath(repoRoot) });
|
|
1679
2034
|
}
|
|
1680
|
-
if (shapes.plugin &&
|
|
2035
|
+
if (shapes.plugin && existsSync10(resolve9(repoRoot, ".opencode/plugins/opencode-ship.js"))) {
|
|
1681
2036
|
if (!forceRepair) {
|
|
1682
2037
|
actions.push({ kind: "candidate-remove-legacy-plugin", path: legacyPluginPath(repoRoot) });
|
|
1683
2038
|
}
|
|
1684
2039
|
}
|
|
1685
|
-
|
|
2040
|
+
if (shapes.pluginOld) {
|
|
2041
|
+
actions.push({ kind: "candidate-remove-legacy-plugin-path", path: resolve9(repoRoot, ".opencode/plugin/opencode-ship.js") });
|
|
2042
|
+
}
|
|
2043
|
+
return { shapes, actions, legacyPresent: Boolean(legacy), proposedConfigSeed };
|
|
1686
2044
|
}
|
|
1687
|
-
function legacyToShipConfig(legacy) {
|
|
1688
|
-
if (!legacy || typeof legacy !== "object") return renderDefaultConfig({});
|
|
2045
|
+
function legacyToShipConfig(legacy, detection = null) {
|
|
2046
|
+
if (!legacy || typeof legacy !== "object") return renderDefaultConfig(detection ?? {});
|
|
2047
|
+
const repoSlug = typeof legacy.repository?.repoSlug === "string" ? legacy.repository.repoSlug : detection?.repository ?? "owner/repo";
|
|
1689
2048
|
return {
|
|
1690
2049
|
schemaVersion: 1,
|
|
1691
2050
|
project: {
|
|
1692
|
-
remote: legacy.repository?.remote ?? "origin",
|
|
1693
|
-
repository:
|
|
1694
|
-
defaultBranch: legacy.repository?.defaultBranch?.name ?? "main",
|
|
1695
|
-
packageManager: "
|
|
2051
|
+
remote: legacy.repository?.remote ?? detection?.remote ?? "origin",
|
|
2052
|
+
repository: repoSlug,
|
|
2053
|
+
defaultBranch: legacy.repository?.defaultBranch?.name ?? detection?.defaultBranch ?? "main",
|
|
2054
|
+
packageManager: detection?.packageManager ?? "pnpm",
|
|
1696
2055
|
detectOverrides: false
|
|
1697
2056
|
},
|
|
1698
2057
|
delivery: {
|
|
1699
2058
|
worktree: {
|
|
1700
2059
|
root: legacy.worktree?.root ?? ".worktrees",
|
|
1701
2060
|
branchTemplate: legacy.worktree?.branchTemplate ?? "{actor}/{slug}",
|
|
1702
|
-
bootstrap: Array.isArray(legacy.worktree?.bootstrap) ? legacy.worktree.bootstrap : [["
|
|
2061
|
+
bootstrap: Array.isArray(legacy.worktree?.bootstrap) && legacy.worktree.bootstrap.length ? legacy.worktree.bootstrap : [["pnpm", "install", "--frozen-lockfile"]]
|
|
1703
2062
|
},
|
|
1704
2063
|
verification: {
|
|
1705
|
-
commands: legacy.verification?.commands
|
|
2064
|
+
commands: Array.isArray(legacy.verification?.commands) && legacy.verification.commands.length ? legacy.verification.commands : [{ id: "canonical", argv: ["pnpm", "verify:workspace"], timeoutMs: 18e5 }],
|
|
1706
2065
|
requireCleanDiffAfter: legacy.verification?.requireCleanDiffAfter ?? true,
|
|
1707
2066
|
invalidateOnHeadChange: legacy.verification?.invalidateOnHeadChange ?? true
|
|
1708
2067
|
},
|
|
@@ -1715,37 +2074,69 @@ function legacyToShipConfig(legacy) {
|
|
|
1715
2074
|
},
|
|
1716
2075
|
ready: legacy.ready ?? { requires: ["review", "local-verification", "remote-ci"], stopAfterReady: true },
|
|
1717
2076
|
merge: legacy.merge ?? { strategy: "squash", policy: "explicit-user-request-only", requireFreshGates: true },
|
|
1718
|
-
cleanup: legacy.cleanup
|
|
2077
|
+
cleanup: legacy.cleanup && typeof legacy.cleanup === "object" && "when" in legacy.cleanup ? legacy.cleanup : { when: "next-task", requireUnpublishedGuard: true }
|
|
1719
2078
|
}
|
|
1720
2079
|
};
|
|
1721
2080
|
}
|
|
1722
2081
|
|
|
1723
2082
|
// src/installer/executor.js
|
|
1724
2083
|
async function readCurrentBytes(targetPath) {
|
|
1725
|
-
if (!
|
|
2084
|
+
if (!existsSync11(targetPath)) return null;
|
|
1726
2085
|
const buf = await readFile7(targetPath);
|
|
1727
2086
|
return { bytes: buf, hash: bytesHashString(buf.toString("utf8")) };
|
|
1728
2087
|
}
|
|
1729
|
-
async function previewInstall({ rootPath, replaceManaged, forceConfig, forceRootConfig }) {
|
|
2088
|
+
async function previewInstall({ rootPath, profile = null, replaceManaged, forceConfig, forceRootConfig }) {
|
|
1730
2089
|
const detection = detectProject(rootPath ?? process.cwd());
|
|
1731
2090
|
if (detection.errors.some((e) => e.kind === "not-a-git-repo")) {
|
|
1732
2091
|
return { ok: false, error: { kind: "invalid-project", errors: detection.errors } };
|
|
1733
2092
|
}
|
|
1734
2093
|
const repoRoot = detection.repoRoot;
|
|
1735
|
-
const
|
|
1736
|
-
|
|
2094
|
+
const validatedLock = await readValidatedLock(repoRoot);
|
|
2095
|
+
if (validatedLock.kind === "schema") {
|
|
2096
|
+
return { ok: false, error: { kind: "unsupported-lock-schema", issues: validatedLock.issues } };
|
|
2097
|
+
}
|
|
2098
|
+
if (validatedLock.kind === "integrity" || validatedLock.kind === "shape") {
|
|
2099
|
+
return { ok: false, error: { kind: "lock-invalid", issues: validatedLock.issues } };
|
|
2100
|
+
}
|
|
2101
|
+
const lock = validatedLock.lock;
|
|
2102
|
+
const migrationReport = await migration({ repoRoot, lock, forceRepair: false, detection });
|
|
2103
|
+
const configResult = await loadConfig(repoRoot);
|
|
2104
|
+
const configValue = configResult?.ok ? configResult.value : null;
|
|
2105
|
+
const resolved = resolveProfile({
|
|
2106
|
+
cli: profile,
|
|
2107
|
+
config: configValue,
|
|
2108
|
+
lock
|
|
2109
|
+
});
|
|
2110
|
+
const activeCatalog = filterCatalogByProfile(CATALOG, resolved.profile);
|
|
1737
2111
|
const configPlan = await planConfigSynthesis({
|
|
1738
2112
|
repoRoot,
|
|
1739
2113
|
detection,
|
|
1740
2114
|
lock,
|
|
1741
|
-
forceOverwrite: Boolean(forceConfig)
|
|
2115
|
+
forceOverwrite: Boolean(forceConfig),
|
|
2116
|
+
migrationSeed: migrationReport?.proposedConfigSeed ?? null
|
|
2117
|
+
});
|
|
2118
|
+
const filePlan = await planFileInstall({ repoRoot, lock, allowUnowned: Boolean(replaceManaged), catalog: activeCatalog });
|
|
2119
|
+
const migrationPlan = await planMigrationCleanup({
|
|
2120
|
+
repoRoot,
|
|
2121
|
+
lock,
|
|
2122
|
+
migrationReport,
|
|
2123
|
+
allowUnowned: Boolean(replaceManaged)
|
|
1742
2124
|
});
|
|
1743
|
-
const filePlan = await planFileInstall({ repoRoot, lock, allowUnowned: Boolean(replaceManaged) });
|
|
1744
2125
|
const rootPlan = await planRootConfigApply({ repoRoot, lock, forceRepair: Boolean(forceRootConfig) });
|
|
1745
|
-
const plan = [...filePlan ?? [], configPlan, rootPlan];
|
|
2126
|
+
const plan = [...filePlan ?? [], ...migrationPlan, configPlan, rootPlan];
|
|
1746
2127
|
const conflicts = plan.filter((p) => p && p.kind === "conflict");
|
|
1747
2128
|
const summary = summarise(plan);
|
|
1748
|
-
return {
|
|
2129
|
+
return {
|
|
2130
|
+
ok: true,
|
|
2131
|
+
repoRoot,
|
|
2132
|
+
detection,
|
|
2133
|
+
lock,
|
|
2134
|
+
profile: resolved,
|
|
2135
|
+
plan,
|
|
2136
|
+
conflicts,
|
|
2137
|
+
summary,
|
|
2138
|
+
migrationReport
|
|
2139
|
+
};
|
|
1749
2140
|
}
|
|
1750
2141
|
async function previewUninstall({ rootPath }) {
|
|
1751
2142
|
const detection = detectProject(rootPath ?? process.cwd());
|
|
@@ -1753,9 +2144,16 @@ async function previewUninstall({ rootPath }) {
|
|
|
1753
2144
|
return { ok: false, error: { kind: "invalid-project" } };
|
|
1754
2145
|
}
|
|
1755
2146
|
const repoRoot = detection.repoRoot;
|
|
1756
|
-
const
|
|
2147
|
+
const validatedLock = await readValidatedLock(repoRoot);
|
|
2148
|
+
if (validatedLock.kind === "schema") {
|
|
2149
|
+
return { ok: false, error: { kind: "unsupported-lock-schema", issues: validatedLock.issues } };
|
|
2150
|
+
}
|
|
2151
|
+
if (validatedLock.kind === "integrity" || validatedLock.kind === "shape") {
|
|
2152
|
+
return { ok: false, error: { kind: "lock-invalid", issues: validatedLock.issues } };
|
|
2153
|
+
}
|
|
2154
|
+
const lock = validatedLock.lock;
|
|
1757
2155
|
if (!lock) {
|
|
1758
|
-
return { ok: true, repoRoot, lock, plan: [], conflicts: [], summary: summarise([]) };
|
|
2156
|
+
return { ok: true, repoRoot, lock: null, plan: [], conflicts: [], summary: summarise([]) };
|
|
1759
2157
|
}
|
|
1760
2158
|
const plan = await planUninstall({ repoRoot, lock });
|
|
1761
2159
|
const conflicts = plan.filter((p) => p.kind === "conflict");
|
|
@@ -1772,7 +2170,7 @@ function summarise(plan) {
|
|
|
1772
2170
|
}
|
|
1773
2171
|
return counts;
|
|
1774
2172
|
}
|
|
1775
|
-
async function assembleLock({ repoRoot, plan, lock, configPlan, rootPlan }) {
|
|
2173
|
+
async function assembleLock({ repoRoot, plan, lock, configPlan, rootPlan, profile = null }) {
|
|
1776
2174
|
const files = [];
|
|
1777
2175
|
const remain = lock?.files?.filter((f) => !plan.some((op) => op?.relPath === f.path)) ?? [];
|
|
1778
2176
|
for (const op of plan) {
|
|
@@ -1798,24 +2196,30 @@ async function assembleLock({ repoRoot, plan, lock, configPlan, rootPlan }) {
|
|
|
1798
2196
|
}
|
|
1799
2197
|
const configSha = configPlan?.kind === "create" || configPlan?.kind === "update" ? configPlan.desiredSha : configPlan?.kind === "noop" ? configPlan.currentSha : null;
|
|
1800
2198
|
const rootPointers = rootPlan?.pointerRecords ?? lock?.manager?.rootDocuments?.[0]?.pointers ?? [];
|
|
2199
|
+
const hasRootPlan = Boolean(rootPlan?.target || rootPlan?.pointerRecords && rootPlan.pointerRecords.length > 0);
|
|
2200
|
+
const hasRootDocuments = rootPlan?.pointerRecords && rootPlan.pointerRecords.length > 0 || lock?.manager?.rootDocuments && lock.manager.rootDocuments.length > 0;
|
|
1801
2201
|
return {
|
|
1802
|
-
contractVersion:
|
|
2202
|
+
contractVersion: CURRENT_LOCK_SCHEMA,
|
|
1803
2203
|
manager: {
|
|
1804
|
-
schemaVersion:
|
|
2204
|
+
schemaVersion: CURRENT_LOCK_SCHEMA,
|
|
1805
2205
|
name: "opencode-ship",
|
|
1806
|
-
version: "0.
|
|
1807
|
-
templateSet:
|
|
2206
|
+
version: "0.4.0",
|
|
2207
|
+
templateSet: TEMPLATE_SET_ID,
|
|
2208
|
+
// Newly written locks always carry the resolved profile so
|
|
2209
|
+
// future CLI invocations without --profile still resolve to
|
|
2210
|
+
// the same choice through the lock-precedence layer.
|
|
2211
|
+
profile: profile ?? lock?.manager?.profile,
|
|
1808
2212
|
appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1809
2213
|
config: {
|
|
1810
2214
|
path: ".opencode/ship.config.json",
|
|
1811
2215
|
sha256: configSha ?? lock?.manager?.config?.sha256 ?? "",
|
|
1812
2216
|
existed: Boolean(lock?.manager?.config?.existed)
|
|
1813
2217
|
},
|
|
1814
|
-
rootDocuments:
|
|
1815
|
-
path: rootPlan.
|
|
1816
|
-
format: rootPlan
|
|
1817
|
-
pointers:
|
|
1818
|
-
}] :
|
|
2218
|
+
rootDocuments: hasRootDocuments && (hasRootPlan || (lock?.manager?.rootDocuments?.length ?? 0) > 0) ? [{
|
|
2219
|
+
path: rootPlan?.relPath ?? lock?.manager?.rootDocuments?.[0]?.path ?? "opencode.json",
|
|
2220
|
+
format: rootPlan?.format ?? lock?.manager?.rootDocuments?.[0]?.format ?? "json",
|
|
2221
|
+
pointers: rootPlan?.pointerRecords && rootPlan.pointerRecords.length > 0 ? rootPlan.pointerRecords : lock?.manager?.rootDocuments?.[0]?.pointers ?? []
|
|
2222
|
+
}] : []
|
|
1819
2223
|
},
|
|
1820
2224
|
files,
|
|
1821
2225
|
cleanupPending: lock?.cleanupPending ?? []
|
|
@@ -1856,7 +2260,8 @@ async function commitInstall(preview, { json, command }) {
|
|
|
1856
2260
|
plan: fileOnly,
|
|
1857
2261
|
lock: preview.lock,
|
|
1858
2262
|
configPlan,
|
|
1859
|
-
rootPlan
|
|
2263
|
+
rootPlan,
|
|
2264
|
+
profile: preview.profile?.profile
|
|
1860
2265
|
});
|
|
1861
2266
|
const txPlan = await stageFiles(fileOnly, repoRoot);
|
|
1862
2267
|
if (configPlan && (configPlan.kind === "create" || configPlan.kind === "update")) {
|
|
@@ -1908,7 +2313,16 @@ async function commitInstall(preview, { json, command }) {
|
|
|
1908
2313
|
async function stageFiles(filePlan, repoRoot) {
|
|
1909
2314
|
const out = [];
|
|
1910
2315
|
for (const op of filePlan) {
|
|
1911
|
-
if (op.kind === "conflict" || op.kind === "noop" || op.kind === "converge"
|
|
2316
|
+
if (op.kind === "conflict" || op.kind === "noop" || op.kind === "converge") continue;
|
|
2317
|
+
if (op.kind === "delete") {
|
|
2318
|
+
out.push({
|
|
2319
|
+
op: "file",
|
|
2320
|
+
kind: "delete",
|
|
2321
|
+
target: op.target,
|
|
2322
|
+
relPath: op.relPath
|
|
2323
|
+
});
|
|
2324
|
+
continue;
|
|
2325
|
+
}
|
|
1912
2326
|
out.push({
|
|
1913
2327
|
op: "file",
|
|
1914
2328
|
kind: op.kind,
|
|
@@ -1937,12 +2351,11 @@ function relativeTemplate(source) {
|
|
|
1937
2351
|
}
|
|
1938
2352
|
|
|
1939
2353
|
// src/installer/commands/doctor.js
|
|
1940
|
-
|
|
1941
|
-
import { existsSync as existsSync9, readFileSync as readFileSync3 } from "node:fs";
|
|
2354
|
+
import { existsSync as existsSync12, readFileSync as readFileSync5 } from "node:fs";
|
|
1942
2355
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
1943
2356
|
init_hash();
|
|
2357
|
+
import { resolve as resolve11 } from "node:path";
|
|
1944
2358
|
init_root_config();
|
|
1945
|
-
import { resolve as resolve10 } from "node:path";
|
|
1946
2359
|
|
|
1947
2360
|
// src/installer/report.js
|
|
1948
2361
|
var REPORT_VERSION = 1;
|
|
@@ -2015,14 +2428,77 @@ function checkGhAuth() {
|
|
|
2015
2428
|
detail: r.status === 0 ? "authenticated (token)" : (r.stderr || r.stdout || "").trim() || "no session"
|
|
2016
2429
|
};
|
|
2017
2430
|
}
|
|
2431
|
+
function checkPackageIntegrity() {
|
|
2432
|
+
try {
|
|
2433
|
+
validateCatalog();
|
|
2434
|
+
return { name: "package integrity", ok: true, detail: `${CATALOG.length} catalog entries` };
|
|
2435
|
+
} catch (e) {
|
|
2436
|
+
return {
|
|
2437
|
+
name: "package integrity",
|
|
2438
|
+
ok: false,
|
|
2439
|
+
detail: `${e?.message ?? e}: ${(e?.issues ?? []).map((i) => i.message).join("; ")}`
|
|
2440
|
+
};
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
function buildSourceHashIndex() {
|
|
2444
|
+
const idx = /* @__PURE__ */ new Map();
|
|
2445
|
+
for (const entry of CATALOG) {
|
|
2446
|
+
if (!existsSync12(entry.source)) continue;
|
|
2447
|
+
try {
|
|
2448
|
+
const buf = readFileSync5(entry.source, "utf8");
|
|
2449
|
+
idx.set(entry.source, bytesHashString(buf));
|
|
2450
|
+
} catch {
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
return idx;
|
|
2454
|
+
}
|
|
2455
|
+
function checkCatalogInstall(repoRoot, sourceHashes) {
|
|
2456
|
+
const rows = [];
|
|
2457
|
+
for (const entry of CATALOG) {
|
|
2458
|
+
const target = resolve11(repoRoot, entry.path);
|
|
2459
|
+
if (!existsSync12(target)) {
|
|
2460
|
+
rows.push(`${entry.id}: missing`);
|
|
2461
|
+
continue;
|
|
2462
|
+
}
|
|
2463
|
+
try {
|
|
2464
|
+
const buf = readFileSync5(target, "utf8");
|
|
2465
|
+
const actual = bytesHashString(buf);
|
|
2466
|
+
const expected = sourceHashes.get(entry.source);
|
|
2467
|
+
if (expected && expected !== actual) {
|
|
2468
|
+
rows.push(`${entry.id}: drift`);
|
|
2469
|
+
} else {
|
|
2470
|
+
rows.push(`${entry.id}: ok`);
|
|
2471
|
+
}
|
|
2472
|
+
} catch (e) {
|
|
2473
|
+
rows.push(`${entry.id}: ${e?.message ?? e}`);
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
const allOk = rows.length === 0 || rows.every((r) => r.endsWith("ok"));
|
|
2477
|
+
return {
|
|
2478
|
+
name: "catalog assets present",
|
|
2479
|
+
ok: allOk,
|
|
2480
|
+
detail: rows.join(",")
|
|
2481
|
+
};
|
|
2482
|
+
}
|
|
2018
2483
|
async function checkLock(repoRoot) {
|
|
2019
|
-
const
|
|
2020
|
-
if (
|
|
2021
|
-
|
|
2484
|
+
const result = await readValidatedLock(repoRoot);
|
|
2485
|
+
if (result.kind === "missing") {
|
|
2486
|
+
return { name: "lock present", ok: false, detail: "no lock" };
|
|
2487
|
+
}
|
|
2488
|
+
if (result.kind === "schema") {
|
|
2489
|
+
return { name: "lock present", ok: false, detail: `unsupported schema: ${result.issues.join("; ")}` };
|
|
2490
|
+
}
|
|
2491
|
+
if (result.kind === "integrity") {
|
|
2492
|
+
return { name: "lock present", ok: false, detail: `integrity: ${result.issues.join("; ")}` };
|
|
2493
|
+
}
|
|
2494
|
+
if (result.kind === "shape") {
|
|
2495
|
+
return { name: "lock present", ok: false, detail: `malformed: ${result.issues.join("; ")}` };
|
|
2496
|
+
}
|
|
2497
|
+
const lock = result.lock;
|
|
2022
2498
|
return {
|
|
2023
2499
|
name: "lock present",
|
|
2024
|
-
ok:
|
|
2025
|
-
detail:
|
|
2500
|
+
ok: true,
|
|
2501
|
+
detail: `manager@${lock.manager?.version ?? "?"} schema=${lock.manager?.schemaVersion ?? "?"}`
|
|
2026
2502
|
};
|
|
2027
2503
|
}
|
|
2028
2504
|
async function checkConfig(repoRoot) {
|
|
@@ -2033,50 +2509,41 @@ async function checkConfig(repoRoot) {
|
|
|
2033
2509
|
detail: r?.ok ? "loaded" : r?.error?.kind ?? "missing"
|
|
2034
2510
|
};
|
|
2035
2511
|
}
|
|
2036
|
-
function
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
try {
|
|
2040
|
-
const buf = readFileSync3(path, "utf8");
|
|
2041
|
-
return { name: "plugin", ok: buf.includes("opencode-ship"), detail: "loaded" };
|
|
2042
|
-
} catch (e) {
|
|
2043
|
-
return { name: "plugin", ok: false, detail: e.message };
|
|
2512
|
+
async function checkManagedHashes(repoRoot, validatedLock) {
|
|
2513
|
+
if (validatedLock.kind !== "ok" || !validatedLock.lock) {
|
|
2514
|
+
return { name: "managed hashes", ok: false, detail: "no usable lock" };
|
|
2044
2515
|
}
|
|
2045
|
-
}
|
|
2046
|
-
function checkAgents(repoRoot) {
|
|
2047
|
-
const reviewer = resolve10(repoRoot, ".opencode/agents/delivery-reviewer.md");
|
|
2048
|
-
const verifier = resolve10(repoRoot, ".opencode/agents/delivery-verifier.md");
|
|
2049
|
-
return {
|
|
2050
|
-
name: "canonical agents",
|
|
2051
|
-
ok: existsSync9(reviewer) && existsSync9(verifier),
|
|
2052
|
-
detail: existsSync9(reviewer) && existsSync9(verifier) ? "loaded" : "missing"
|
|
2053
|
-
};
|
|
2054
|
-
}
|
|
2055
|
-
function checkSkills(repoRoot) {
|
|
2056
|
-
const a = resolve10(repoRoot, ".opencode/skills/delivery-workflow/SKILL.md");
|
|
2057
|
-
const b = resolve10(repoRoot, ".opencode/skills/planning-research-checkpoint/SKILL.md");
|
|
2058
|
-
return {
|
|
2059
|
-
name: "canonical skills",
|
|
2060
|
-
ok: existsSync9(a) && existsSync9(b),
|
|
2061
|
-
detail: existsSync9(a) && existsSync9(b) ? "loaded" : "missing"
|
|
2062
|
-
};
|
|
2063
|
-
}
|
|
2064
|
-
async function checkManagedHashes(repoRoot) {
|
|
2065
|
-
const lock = await readLock(repoRoot);
|
|
2066
|
-
if (!lock) return { name: "managed hashes", ok: false, detail: "no lock" };
|
|
2067
2516
|
const drift = [];
|
|
2068
|
-
for (const entry of lock.files ?? []) {
|
|
2069
|
-
const p =
|
|
2070
|
-
if (!
|
|
2517
|
+
for (const entry of validatedLock.lock.files ?? []) {
|
|
2518
|
+
const p = resolve11(repoRoot, entry.path);
|
|
2519
|
+
if (!existsSync12(p)) {
|
|
2071
2520
|
drift.push(`missing:${entry.path}`);
|
|
2072
2521
|
continue;
|
|
2073
2522
|
}
|
|
2074
|
-
const buf =
|
|
2523
|
+
const buf = readFileSync5(p, "utf8");
|
|
2075
2524
|
const actual = bytesHashString(buf);
|
|
2076
2525
|
if (actual !== entry.sha256) drift.push(`drift:${entry.path}`);
|
|
2077
2526
|
}
|
|
2078
2527
|
return { name: "managed hashes", ok: drift.length === 0, detail: drift.length ? drift.join(",") : "match" };
|
|
2079
2528
|
}
|
|
2529
|
+
async function checkActiveProfileFootprint(repoRoot, validatedLock, profile) {
|
|
2530
|
+
if (validatedLock.kind !== "ok" || !validatedLock.lock) {
|
|
2531
|
+
return { name: "profile footprint", ok: true, detail: "no lock; n/a" };
|
|
2532
|
+
}
|
|
2533
|
+
if (!profile) {
|
|
2534
|
+
return { name: "profile footprint", ok: true, detail: "no profile; n/a" };
|
|
2535
|
+
}
|
|
2536
|
+
const expectedPaths = new Set(
|
|
2537
|
+
filterCatalogByProfile(CATALOG, profile).map((e) => e.path)
|
|
2538
|
+
);
|
|
2539
|
+
const present = (validatedLock.lock.files ?? []).filter((f) => expectedPaths.has(f.path)).map((f) => f.path);
|
|
2540
|
+
const missing = [...expectedPaths].filter((p) => !present.includes(p));
|
|
2541
|
+
return {
|
|
2542
|
+
name: "profile footprint",
|
|
2543
|
+
ok: missing.length === 0,
|
|
2544
|
+
detail: missing.length ? `missing profile assets: ${missing.join(",")}` : `${present.length}/${expectedPaths.size} present`
|
|
2545
|
+
};
|
|
2546
|
+
}
|
|
2080
2547
|
async function checkRootConfig(repoRoot) {
|
|
2081
2548
|
const { findRootConfig: findRootConfig2 } = await Promise.resolve().then(() => (init_root_config(), root_config_exports));
|
|
2082
2549
|
const candidate = findRootConfig2(repoRoot);
|
|
@@ -2099,7 +2566,7 @@ function writeEnvelope({ command, plan, summary, diagnostics, json, exitCode })
|
|
|
2099
2566
|
process.stdout.write(renderHuman({ command, plan, conflicts, summary, diagnostics }) + "\n");
|
|
2100
2567
|
}
|
|
2101
2568
|
}
|
|
2102
|
-
async function runDoctor({ rootPath, json, writeOutput = true }) {
|
|
2569
|
+
async function runDoctor({ rootPath, profile, json, writeOutput = true }) {
|
|
2103
2570
|
const detection = detectProject(rootPath ?? process.cwd());
|
|
2104
2571
|
if (detection.errors.some((e) => e.kind === "not-a-git-repo")) {
|
|
2105
2572
|
const issues2 = ["not in a git repository"];
|
|
@@ -2117,17 +2584,27 @@ async function runDoctor({ rootPath, json, writeOutput = true }) {
|
|
|
2117
2584
|
return { issues: issues2, exitCode: 2, plan: plan2, checks: checks2 };
|
|
2118
2585
|
}
|
|
2119
2586
|
const repoRoot = detection.repoRoot;
|
|
2587
|
+
const sourceHashes = buildSourceHashIndex();
|
|
2588
|
+
const validatedLock = await readValidatedLock(repoRoot);
|
|
2589
|
+
const packageIntegrity = checkPackageIntegrity();
|
|
2590
|
+
const configResult = await loadConfig(repoRoot);
|
|
2591
|
+
const configValue = configResult?.ok ? configResult.value : null;
|
|
2592
|
+
const resolved = resolveProfile({
|
|
2593
|
+
cli: profile,
|
|
2594
|
+
config: configValue,
|
|
2595
|
+
lock: validatedLock.lock
|
|
2596
|
+
});
|
|
2120
2597
|
const checks = [
|
|
2121
2598
|
checkNode(),
|
|
2122
2599
|
checkGit(),
|
|
2123
2600
|
checkGh(),
|
|
2124
2601
|
checkGhAuth(),
|
|
2602
|
+
packageIntegrity,
|
|
2603
|
+
checkCatalogInstall(repoRoot, sourceHashes),
|
|
2125
2604
|
await checkLock(repoRoot),
|
|
2126
2605
|
await checkConfig(repoRoot),
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
checkSkills(repoRoot),
|
|
2130
|
-
await checkManagedHashes(repoRoot),
|
|
2606
|
+
await checkManagedHashes(repoRoot, validatedLock),
|
|
2607
|
+
await checkActiveProfileFootprint(repoRoot, validatedLock, resolved.profile),
|
|
2131
2608
|
await checkRootConfig(repoRoot)
|
|
2132
2609
|
];
|
|
2133
2610
|
const issues = checks.filter((c) => !c.ok).map((c) => `${c.name}: ${c.detail}`);
|
|
@@ -2139,21 +2616,39 @@ async function runDoctor({ rootPath, json, writeOutput = true }) {
|
|
|
2139
2616
|
reason: c.detail
|
|
2140
2617
|
}));
|
|
2141
2618
|
const summary = summarise2(plan);
|
|
2142
|
-
|
|
2619
|
+
let exitCode = 1;
|
|
2620
|
+
if (issues.length === 0) exitCode = 0;
|
|
2621
|
+
if (!packageIntegrity.ok) exitCode = 4;
|
|
2622
|
+
if (validatedLock.kind === "schema") exitCode = 5;
|
|
2143
2623
|
if (writeOutput) writeEnvelope({ command: "doctor", plan, summary, diagnostics: issues, json, exitCode });
|
|
2144
2624
|
process.exitCode = exitCode;
|
|
2145
|
-
return { issues, exitCode, plan, checks };
|
|
2625
|
+
return { issues, exitCode, plan, checks, profile: resolved };
|
|
2146
2626
|
}
|
|
2147
2627
|
|
|
2148
2628
|
// src/installer/commands/init.js
|
|
2149
2629
|
async function runInit(options) {
|
|
2630
|
+
try {
|
|
2631
|
+
validateCatalog();
|
|
2632
|
+
} catch (e) {
|
|
2633
|
+
if (e?.catalogValidation) {
|
|
2634
|
+
return emitFailure(4, `catalog validation failed: ${e.message}`, options.json, "init");
|
|
2635
|
+
}
|
|
2636
|
+
throw e;
|
|
2637
|
+
}
|
|
2150
2638
|
const preview = await previewInstall({
|
|
2151
2639
|
rootPath: options.rootPath ?? null,
|
|
2640
|
+
profile: options.profile ?? null,
|
|
2152
2641
|
replaceManaged: false,
|
|
2153
2642
|
forceConfig: Boolean(options.forceConfig),
|
|
2154
2643
|
forceRootConfig: Boolean(options.forceRootConfig)
|
|
2155
2644
|
});
|
|
2156
2645
|
if (!preview.ok) {
|
|
2646
|
+
if (preview.error?.kind === "unsupported-lock-schema") {
|
|
2647
|
+
return emitFailure(5, `unsupported lock schema: ${(preview.error.issues ?? []).join("; ")}`, options.json, "init");
|
|
2648
|
+
}
|
|
2649
|
+
if (preview.error?.kind === "lock-invalid") {
|
|
2650
|
+
return emitFailure(3, `lock invalid: ${(preview.error.issues ?? []).join("; ")}`, options.json, "init");
|
|
2651
|
+
}
|
|
2157
2652
|
return emitFailure(2, preview.error?.kind ?? "invalid-project", options.json, "init");
|
|
2158
2653
|
}
|
|
2159
2654
|
const committed = await commitInstall(preview, { json: options.json, command: "init" });
|
|
@@ -2166,6 +2661,7 @@ async function runInit(options) {
|
|
|
2166
2661
|
}
|
|
2167
2662
|
const doctor = await runDoctor({
|
|
2168
2663
|
rootPath: options.rootPath ?? null,
|
|
2664
|
+
profile: options.profile ?? null,
|
|
2169
2665
|
json: Boolean(options.json),
|
|
2170
2666
|
writeOutput: false
|
|
2171
2667
|
});
|
|
@@ -2245,13 +2741,40 @@ function serializePlan2(plan) {
|
|
|
2245
2741
|
});
|
|
2246
2742
|
}
|
|
2247
2743
|
async function runDiff(options) {
|
|
2744
|
+
try {
|
|
2745
|
+
validateCatalog();
|
|
2746
|
+
} catch (e) {
|
|
2747
|
+
if (e?.catalogValidation) {
|
|
2748
|
+
if (options.json) {
|
|
2749
|
+
process.stdout.write(JSON.stringify({
|
|
2750
|
+
reportVersion: 1,
|
|
2751
|
+
command: "diff",
|
|
2752
|
+
status: "error",
|
|
2753
|
+
plan: [],
|
|
2754
|
+
conflicts: [],
|
|
2755
|
+
summary: { create: 0, update: 0, noop: 0, delete: 0, conflict: 0, converge: 0 },
|
|
2756
|
+
diagnostics: [`catalog validation failed: ${e.message}`],
|
|
2757
|
+
exitCode: 4
|
|
2758
|
+
}, null, 2) + "\n");
|
|
2759
|
+
} else {
|
|
2760
|
+
process.stdout.write(`opencode-ship: catalog validation failed: ${e.message}
|
|
2761
|
+
`);
|
|
2762
|
+
}
|
|
2763
|
+
process.exitCode = 4;
|
|
2764
|
+
return { ok: false, exitCode: 4 };
|
|
2765
|
+
}
|
|
2766
|
+
throw e;
|
|
2767
|
+
}
|
|
2248
2768
|
const preview = await previewInstall({
|
|
2249
2769
|
rootPath: options.rootPath ?? null,
|
|
2770
|
+
profile: options.profile ?? null,
|
|
2250
2771
|
replaceManaged: false,
|
|
2251
2772
|
forceConfig: false,
|
|
2252
2773
|
forceRootConfig: false
|
|
2253
2774
|
});
|
|
2254
2775
|
if (!preview.ok) {
|
|
2776
|
+
const exitCode = preview.error?.kind === "unsupported-lock-schema" ? 5 : preview.error?.kind === "lock-invalid" ? 3 : 2;
|
|
2777
|
+
const message = preview.error?.kind === "lock-invalid" ? `lock invalid: ${(preview.error.issues ?? []).join("; ")}` : preview.error?.kind === "unsupported-lock-schema" ? `unsupported lock schema: ${(preview.error.issues ?? []).join("; ")}` : preview.error?.kind ?? "invalid-project";
|
|
2255
2778
|
if (options.json) {
|
|
2256
2779
|
process.stdout.write(JSON.stringify({
|
|
2257
2780
|
reportVersion: 1,
|
|
@@ -2260,15 +2783,15 @@ async function runDiff(options) {
|
|
|
2260
2783
|
plan: [],
|
|
2261
2784
|
conflicts: [],
|
|
2262
2785
|
summary: { create: 0, update: 0, noop: 0, delete: 0, conflict: 0, converge: 0 },
|
|
2263
|
-
diagnostics: [
|
|
2264
|
-
exitCode
|
|
2786
|
+
diagnostics: [message],
|
|
2787
|
+
exitCode
|
|
2265
2788
|
}, null, 2) + "\n");
|
|
2266
2789
|
} else {
|
|
2267
|
-
process.stdout.write(`opencode-ship: ${
|
|
2790
|
+
process.stdout.write(`opencode-ship: ${message}
|
|
2268
2791
|
`);
|
|
2269
2792
|
}
|
|
2270
|
-
process.exitCode =
|
|
2271
|
-
return { ok: false, exitCode
|
|
2793
|
+
process.exitCode = exitCode;
|
|
2794
|
+
return { ok: false, exitCode };
|
|
2272
2795
|
}
|
|
2273
2796
|
const { plan, conflicts, migrationReport } = preview;
|
|
2274
2797
|
const summary = summarise3(plan);
|
|
@@ -2305,13 +2828,28 @@ async function runDiff(options) {
|
|
|
2305
2828
|
|
|
2306
2829
|
// src/installer/commands/update.js
|
|
2307
2830
|
async function runUpdate(options) {
|
|
2831
|
+
try {
|
|
2832
|
+
validateCatalog();
|
|
2833
|
+
} catch (e) {
|
|
2834
|
+
if (e?.catalogValidation) {
|
|
2835
|
+
return emitFailure2(4, `catalog validation failed: ${e.message}`, options.json, "update");
|
|
2836
|
+
}
|
|
2837
|
+
throw e;
|
|
2838
|
+
}
|
|
2308
2839
|
const preview = await previewInstall({
|
|
2309
2840
|
rootPath: options.rootPath,
|
|
2841
|
+
profile: options.profile ?? null,
|
|
2310
2842
|
replaceManaged: options.replaceManaged,
|
|
2311
2843
|
forceConfig: options.forceConfig,
|
|
2312
2844
|
forceRootConfig: options.forceRootConfig
|
|
2313
2845
|
});
|
|
2314
2846
|
if (!preview.ok) {
|
|
2847
|
+
if (preview.error?.kind === "unsupported-lock-schema") {
|
|
2848
|
+
return emitFailure2(5, `unsupported lock schema: ${(preview.error.issues ?? []).join("; ")}`, options.json, "update");
|
|
2849
|
+
}
|
|
2850
|
+
if (preview.error?.kind === "lock-invalid") {
|
|
2851
|
+
return emitFailure2(3, `lock invalid: ${(preview.error.issues ?? []).join("; ")}`, options.json, "update");
|
|
2852
|
+
}
|
|
2315
2853
|
return emitFailure2(2, preview.error?.kind ?? "invalid-project", options.json, "update");
|
|
2316
2854
|
}
|
|
2317
2855
|
if (preview.conflicts.length > 0 && !options.replaceManaged) {
|
|
@@ -2359,11 +2897,17 @@ function emitFailure2(code, message, json, command) {
|
|
|
2359
2897
|
|
|
2360
2898
|
// src/installer/commands/uninstall.js
|
|
2361
2899
|
import { unlink as unlink3 } from "node:fs/promises";
|
|
2362
|
-
import { existsSync as
|
|
2363
|
-
import { resolve as
|
|
2900
|
+
import { existsSync as existsSync13 } from "node:fs";
|
|
2901
|
+
import { resolve as resolve12 } from "node:path";
|
|
2364
2902
|
async function runUninstall(options) {
|
|
2365
|
-
const preview = await previewUninstall({ rootPath: options.rootPath });
|
|
2903
|
+
const preview = await previewUninstall({ rootPath: options.rootPath, profile: options.profile ?? null });
|
|
2366
2904
|
if (!preview.ok) {
|
|
2905
|
+
if (preview.error?.kind === "unsupported-lock-schema") {
|
|
2906
|
+
return emitFailure3(5, `unsupported lock schema: ${(preview.error.issues ?? []).join("; ")}`, options.json);
|
|
2907
|
+
}
|
|
2908
|
+
if (preview.error?.kind === "lock-invalid") {
|
|
2909
|
+
return emitFailure3(3, `lock invalid: ${(preview.error.issues ?? []).join("; ")}`, options.json);
|
|
2910
|
+
}
|
|
2367
2911
|
return emitFailure3(2, preview.error?.kind ?? "invalid-project", options.json);
|
|
2368
2912
|
}
|
|
2369
2913
|
const { repoRoot, plan, conflicts, summary } = preview;
|
|
@@ -2374,8 +2918,8 @@ async function runUninstall(options) {
|
|
|
2374
2918
|
if (!tx.ok) {
|
|
2375
2919
|
return emitFailure3(4, tx.error?.message ?? "transaction failure", options.json);
|
|
2376
2920
|
}
|
|
2377
|
-
const lockPath2 =
|
|
2378
|
-
if (
|
|
2921
|
+
const lockPath2 = resolve12(repoRoot, ".opencode", "ship.lock.json");
|
|
2922
|
+
if (existsSync13(lockPath2)) await unlink3(lockPath2).catch(() => null);
|
|
2379
2923
|
if (options.purgeConfig) {
|
|
2380
2924
|
await unlink3(configPath(repoRoot)).catch(() => null);
|
|
2381
2925
|
}
|
|
@@ -2432,7 +2976,7 @@ function emitReport(plan, conflicts, summary, json, exitCode, diagnostics) {
|
|
|
2432
2976
|
}
|
|
2433
2977
|
|
|
2434
2978
|
// src/cli.js
|
|
2435
|
-
var VERSION =
|
|
2979
|
+
var VERSION = PACKAGE_VERSION;
|
|
2436
2980
|
async function main() {
|
|
2437
2981
|
const parsed = parseCommand(process.argv.slice(2));
|
|
2438
2982
|
if (parsed.command === "help") {
|
|
@@ -2447,7 +2991,7 @@ async function main() {
|
|
|
2447
2991
|
return;
|
|
2448
2992
|
}
|
|
2449
2993
|
if ("error" in parsed) {
|
|
2450
|
-
process.
|
|
2994
|
+
process.stderr.write(`opencode-ship: ${parsed.error}
|
|
2451
2995
|
|
|
2452
2996
|
${helpText()}`);
|
|
2453
2997
|
process.exitCode = 2;
|
|
@@ -2455,21 +2999,22 @@ ${helpText()}`);
|
|
|
2455
2999
|
}
|
|
2456
3000
|
const options = parsed.options ?? {};
|
|
2457
3001
|
const opts = options;
|
|
3002
|
+
const profile = opts.profile ?? null;
|
|
2458
3003
|
switch (parsed.command) {
|
|
2459
3004
|
case "init":
|
|
2460
|
-
await runInit({ json: !!opts.json, rootPath: opts.rootPath, forceConfig: !!opts.forceConfig, forceRootConfig: !!opts.forceRootConfig, strictDoctor: !!opts.strictDoctor });
|
|
3005
|
+
await runInit({ json: !!opts.json, rootPath: opts.rootPath, profile, forceConfig: !!opts.forceConfig, forceRootConfig: !!opts.forceRootConfig, strictDoctor: !!opts.strictDoctor });
|
|
2461
3006
|
return;
|
|
2462
3007
|
case "diff":
|
|
2463
|
-
await runDiff({ json: !!opts.json, rootPath: opts.rootPath });
|
|
3008
|
+
await runDiff({ json: !!opts.json, rootPath: opts.rootPath, profile });
|
|
2464
3009
|
return;
|
|
2465
3010
|
case "update":
|
|
2466
|
-
await runUpdate({ json: !!opts.json, rootPath: opts.rootPath, replaceManaged: !!opts.replaceManaged, forceConfig: !!opts.forceConfig, forceRootConfig: !!opts.forceRootConfig });
|
|
3011
|
+
await runUpdate({ json: !!opts.json, rootPath: opts.rootPath, profile, replaceManaged: !!opts.replaceManaged, forceConfig: !!opts.forceConfig, forceRootConfig: !!opts.forceRootConfig });
|
|
2467
3012
|
return;
|
|
2468
3013
|
case "doctor":
|
|
2469
|
-
await runDoctor({ json: !!opts.json, rootPath: opts.rootPath });
|
|
3014
|
+
await runDoctor({ json: !!opts.json, rootPath: opts.rootPath, profile });
|
|
2470
3015
|
return;
|
|
2471
3016
|
case "uninstall":
|
|
2472
|
-
await runUninstall({ json: !!opts.json, rootPath: opts.rootPath, purgeConfig: !!opts.purgeConfig });
|
|
3017
|
+
await runUninstall({ json: !!opts.json, rootPath: opts.rootPath, profile, purgeConfig: !!opts.purgeConfig });
|
|
2473
3018
|
return;
|
|
2474
3019
|
default:
|
|
2475
3020
|
process.stdout.write(helpText() + "\n");
|