opencode-ship 1.1.7 → 1.1.9-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -1
- package/README.md +13 -13
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/assets/agents/ship-controller.md +47 -22
- package/assets/agents/ship-final-spec-reviewer.md +11 -11
- package/assets/agents/ship-final-standards-reviewer.md +11 -11
- package/assets/agents/ship-plan.md +24 -5
- package/assets/agents/ship-planner.md +46 -20
- package/assets/agents/{delivery-reviewer.md → ship-reviewer.md} +16 -16
- package/assets/agents/ship-task-builder.md +11 -11
- package/assets/agents/ship-task-reviewer.md +12 -12
- package/assets/agents/{delivery-verifier.md → ship-verifier.md} +13 -13
- package/assets/commands/setup-ship-workflow.md +6 -5
- package/assets/commands/ship-deliver.md +27 -30
- package/assets/defaults/workflow-models.history.json +7 -0
- package/assets/defaults/workflow-models.json +5 -0
- package/assets/skills/brainstorming/SKILL.md +10 -7
- package/assets/skills/dispatching-parallel-agents/SKILL.md +1 -1
- package/assets/skills/engineering-workflow/SKILL.md +10 -4
- package/assets/skills/executing-plans/SKILL.md +18 -63
- package/assets/skills/planning-research-checkpoint/SKILL.md +16 -13
- package/assets/skills/receiving-code-review/SKILL.md +1 -1
- package/assets/skills/requesting-code-review/SKILL.md +1 -1
- package/assets/skills/setup-ship-workflow/SKILL.md +27 -33
- package/assets/skills/ship-workflow/SKILL.md +83 -0
- package/assets/skills/skill-discovery/SKILL.md +17 -89
- package/assets/skills/subagent-driven-development/SKILL.md +6 -2
- package/assets/skills/systematic-debugging/SKILL.md +1 -1
- package/assets/skills/test-driven-development/SKILL.md +1 -1
- package/assets/skills/verification-before-completion/SKILL.md +1 -1
- package/assets/skills/wayfinder/SKILL.md +7 -1
- package/assets/skills/writing-plans/SKILL.md +31 -20
- package/dist/cli.js +1482 -175
- package/dist/core.d.ts +18 -0
- package/dist/core.js +528 -11
- package/dist/plugin.js +18613 -17483
- package/package.json +2 -1
- package/schema/project-adapter.example.json +2 -2
- package/schema/project-opencode-shim.json +2 -0
- package/schema/ship-lock.schema.json +23 -2
- package/tests/plugin/expected-tools.mjs +48 -5
- package/tests/plugin/plugin-load.test.mjs +27 -5
- package/vendor/sources.json +21 -21
- package/assets/skills/delivery-workflow/SKILL.md +0 -64
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// opencode-ship CLI v1.1.
|
|
2
|
+
// opencode-ship CLI v1.1.9-rc.1
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
5
|
var __esm = (fn, res) => function __init() {
|
|
@@ -110,11 +110,108 @@ import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
|
110
110
|
var PACKAGE_VERSION, TEMPLATE_SET;
|
|
111
111
|
var init_version = __esm({
|
|
112
112
|
"src/version.js"() {
|
|
113
|
-
PACKAGE_VERSION = "1.1.
|
|
113
|
+
PACKAGE_VERSION = "1.1.9-rc.1";
|
|
114
114
|
TEMPLATE_SET = `v${PACKAGE_VERSION}`;
|
|
115
115
|
}
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
+
// src/installer/workflow-models.js
|
|
119
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
120
|
+
import { resolve as resolve3 } from "node:path";
|
|
121
|
+
function failDefaults(message) {
|
|
122
|
+
const err = new Error(message);
|
|
123
|
+
err.catalogValidation = true;
|
|
124
|
+
throw err;
|
|
125
|
+
}
|
|
126
|
+
function readJson(path) {
|
|
127
|
+
if (!existsSync3(path)) {
|
|
128
|
+
failDefaults(`defaults file missing: ${path}`);
|
|
129
|
+
}
|
|
130
|
+
let raw;
|
|
131
|
+
try {
|
|
132
|
+
raw = readFileSync3(path, "utf8");
|
|
133
|
+
} catch (e) {
|
|
134
|
+
failDefaults(`defaults file unreadable: ${path}: ${e?.message ?? e}`);
|
|
135
|
+
}
|
|
136
|
+
if (raw.trim().length === 0) {
|
|
137
|
+
failDefaults(`defaults file empty: ${path}`);
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
return JSON.parse(raw);
|
|
141
|
+
} catch (e) {
|
|
142
|
+
failDefaults(`defaults file is not JSON: ${path}: ${e?.message ?? e}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function assertRoles(value, label) {
|
|
146
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
147
|
+
failDefaults(`${label} must be an object with planner, builder, finalReviewer`);
|
|
148
|
+
}
|
|
149
|
+
for (const role of MODEL_ROLES) {
|
|
150
|
+
if (typeof value[role] !== "string" || value[role].length === 0) {
|
|
151
|
+
failDefaults(`${label} missing role: ${role}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function loadWorkflowModelDefaults() {
|
|
156
|
+
const root = resolvePackageRoot(import.meta.url);
|
|
157
|
+
const current = readJson(resolve3(root, "assets/defaults/workflow-models.json"));
|
|
158
|
+
const history = readJson(resolve3(root, "assets/defaults/workflow-models.history.json"));
|
|
159
|
+
assertRoles(current, "workflow-models.json");
|
|
160
|
+
if (!Array.isArray(history)) {
|
|
161
|
+
failDefaults("workflow-models.history.json must be a JSON array");
|
|
162
|
+
}
|
|
163
|
+
for (let i = 0; i < history.length; i++) {
|
|
164
|
+
assertRoles(history[i], `workflow-models.history.json[${i}]`);
|
|
165
|
+
}
|
|
166
|
+
return { current, history };
|
|
167
|
+
}
|
|
168
|
+
function isNonEmptyString(value) {
|
|
169
|
+
return typeof value === "string" && value.length > 0;
|
|
170
|
+
}
|
|
171
|
+
function resolveWorkflowModels({ configModels, lockModels, cliModels, current, history }) {
|
|
172
|
+
const config = configModels && typeof configModels === "object" ? configModels : {};
|
|
173
|
+
const lock = lockModels && typeof lockModels === "object" ? lockModels : {};
|
|
174
|
+
const cli = cliModels && typeof cliModels === "object" ? cliModels : {};
|
|
175
|
+
const hist = Array.isArray(history) ? history : [];
|
|
176
|
+
const models = {};
|
|
177
|
+
const provenance = {};
|
|
178
|
+
const changedRoles = [];
|
|
179
|
+
for (const role of MODEL_ROLES) {
|
|
180
|
+
let id;
|
|
181
|
+
let source;
|
|
182
|
+
if (isNonEmptyString(cli[role])) {
|
|
183
|
+
source = "override";
|
|
184
|
+
id = cli[role];
|
|
185
|
+
} else if (lock[role]?.source === "override") {
|
|
186
|
+
source = "override";
|
|
187
|
+
id = isNonEmptyString(config[role]) ? config[role] : lock[role].applied;
|
|
188
|
+
} else if (lock[role]?.source === "default") {
|
|
189
|
+
source = "default";
|
|
190
|
+
id = current[role];
|
|
191
|
+
} else if (!isNonEmptyString(config[role])) {
|
|
192
|
+
source = "default";
|
|
193
|
+
id = current[role];
|
|
194
|
+
} else if (config[role] === current[role] || hist.some((entry) => entry?.[role] === config[role])) {
|
|
195
|
+
source = "default";
|
|
196
|
+
id = current[role];
|
|
197
|
+
} else {
|
|
198
|
+
source = "override";
|
|
199
|
+
id = config[role];
|
|
200
|
+
}
|
|
201
|
+
models[role] = id;
|
|
202
|
+
provenance[role] = { source, applied: id };
|
|
203
|
+
if (id !== config[role]) changedRoles.push(role);
|
|
204
|
+
}
|
|
205
|
+
return { models, provenance, changedRoles };
|
|
206
|
+
}
|
|
207
|
+
var MODEL_ROLES;
|
|
208
|
+
var init_workflow_models = __esm({
|
|
209
|
+
"src/installer/workflow-models.js"() {
|
|
210
|
+
init_package_root();
|
|
211
|
+
MODEL_ROLES = ["planner", "builder", "finalReviewer"];
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
118
215
|
// src/installer/catalog.js
|
|
119
216
|
var catalog_exports = {};
|
|
120
217
|
__export(catalog_exports, {
|
|
@@ -125,8 +222,8 @@ __export(catalog_exports, {
|
|
|
125
222
|
filterCatalogByProfile: () => filterCatalogByProfile,
|
|
126
223
|
validateCatalog: () => validateCatalog
|
|
127
224
|
});
|
|
128
|
-
import { resolve as
|
|
129
|
-
import { existsSync as
|
|
225
|
+
import { resolve as resolve4, relative, sep } from "node:path";
|
|
226
|
+
import { existsSync as existsSync4, statSync } from "node:fs";
|
|
130
227
|
function filterCatalogByProfile(catalog, profile) {
|
|
131
228
|
const effective = profile === void 0 || profile === null ? DEFAULT_PROFILE : isValidProfile(profile) ? profile : profile === "core" ? DEFAULT_PROFILE : null;
|
|
132
229
|
if (effective === null) {
|
|
@@ -166,7 +263,7 @@ function validateCatalog({ catalog = CATALOG } = {}) {
|
|
|
166
263
|
}
|
|
167
264
|
if (typeof source !== "string" || source.length === 0) {
|
|
168
265
|
issues.push({ id, kind: "source", message: `source path missing: ${id}` });
|
|
169
|
-
} else if (!
|
|
266
|
+
} else if (!existsSync4(source)) {
|
|
170
267
|
issues.push({ id, kind: "source-missing", message: `source file not found: ${source}` });
|
|
171
268
|
} else {
|
|
172
269
|
try {
|
|
@@ -204,6 +301,7 @@ function validateCatalog({ catalog = CATALOG } = {}) {
|
|
|
204
301
|
err.catalogValidation = true;
|
|
205
302
|
throw err;
|
|
206
303
|
}
|
|
304
|
+
loadWorkflowModelDefaults();
|
|
207
305
|
return catalog;
|
|
208
306
|
}
|
|
209
307
|
var TEMPLATE_SET_ID, packageRoot, PACKAGE_ROOT, MATT_SKILLS, SUPER_SKILLS, ENGINEERING_AGENTS, ENGINEERING_COMMANDS, CATALOG, ALLOWED_KINDS;
|
|
@@ -212,6 +310,7 @@ var init_catalog = __esm({
|
|
|
212
310
|
init_package_root();
|
|
213
311
|
init_version();
|
|
214
312
|
init_profile();
|
|
313
|
+
init_workflow_models();
|
|
215
314
|
TEMPLATE_SET_ID = TEMPLATE_SET;
|
|
216
315
|
packageRoot = resolvePackageRoot(import.meta.url);
|
|
217
316
|
PACKAGE_ROOT = packageRoot;
|
|
@@ -261,7 +360,23 @@ var init_catalog = __esm({
|
|
|
261
360
|
id: "plugin:opencode-ship",
|
|
262
361
|
kind: "plugin",
|
|
263
362
|
path: ".opencode/plugins/opencode-ship.js",
|
|
264
|
-
source:
|
|
363
|
+
source: resolve4(packageRoot, "dist/plugin.js"),
|
|
364
|
+
mode: 420,
|
|
365
|
+
profiles: ["engineering"]
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
id: "agent:ship-reviewer",
|
|
369
|
+
kind: "agent",
|
|
370
|
+
path: ".opencode/agents/ship-reviewer.md",
|
|
371
|
+
source: resolve4(packageRoot, "assets/agents/ship-reviewer.md"),
|
|
372
|
+
mode: 420,
|
|
373
|
+
profiles: ["engineering"]
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
id: "agent:ship-verifier",
|
|
377
|
+
kind: "agent",
|
|
378
|
+
path: ".opencode/agents/ship-verifier.md",
|
|
379
|
+
source: resolve4(packageRoot, "assets/agents/ship-verifier.md"),
|
|
265
380
|
mode: 420,
|
|
266
381
|
profiles: ["engineering"]
|
|
267
382
|
},
|
|
@@ -269,23 +384,25 @@ var init_catalog = __esm({
|
|
|
269
384
|
id: "agent:delivery-reviewer",
|
|
270
385
|
kind: "agent",
|
|
271
386
|
path: ".opencode/agents/delivery-reviewer.md",
|
|
272
|
-
source:
|
|
387
|
+
source: resolve4(packageRoot, "assets/agents/ship-reviewer.md"),
|
|
273
388
|
mode: 420,
|
|
274
|
-
profiles: ["engineering"]
|
|
389
|
+
profiles: ["engineering"],
|
|
390
|
+
legacy: true
|
|
275
391
|
},
|
|
276
392
|
{
|
|
277
393
|
id: "agent:delivery-verifier",
|
|
278
394
|
kind: "agent",
|
|
279
395
|
path: ".opencode/agents/delivery-verifier.md",
|
|
280
|
-
source:
|
|
396
|
+
source: resolve4(packageRoot, "assets/agents/ship-verifier.md"),
|
|
281
397
|
mode: 420,
|
|
282
|
-
profiles: ["engineering"]
|
|
398
|
+
profiles: ["engineering"],
|
|
399
|
+
legacy: true
|
|
283
400
|
},
|
|
284
401
|
...ENGINEERING_AGENTS.map((name) => ({
|
|
285
402
|
id: `agent:${name}`,
|
|
286
403
|
kind: "agent",
|
|
287
404
|
path: `.opencode/agents/${name}.md`,
|
|
288
|
-
source:
|
|
405
|
+
source: resolve4(packageRoot, `assets/agents/${name}.md`),
|
|
289
406
|
mode: 420,
|
|
290
407
|
profiles: ["engineering"]
|
|
291
408
|
})),
|
|
@@ -293,23 +410,32 @@ var init_catalog = __esm({
|
|
|
293
410
|
id: `command:${name}`,
|
|
294
411
|
kind: "support",
|
|
295
412
|
path: `.opencode/commands/${name}.md`,
|
|
296
|
-
source:
|
|
413
|
+
source: resolve4(packageRoot, `assets/commands/${name}.md`),
|
|
297
414
|
mode: 420,
|
|
298
415
|
profiles: ["engineering"]
|
|
299
416
|
})),
|
|
417
|
+
{
|
|
418
|
+
id: "skill:ship-workflow",
|
|
419
|
+
kind: "skill",
|
|
420
|
+
path: ".opencode/skills/ship-workflow/SKILL.md",
|
|
421
|
+
source: resolve4(packageRoot, "assets/skills/ship-workflow/SKILL.md"),
|
|
422
|
+
mode: 420,
|
|
423
|
+
profiles: ["engineering"]
|
|
424
|
+
},
|
|
300
425
|
{
|
|
301
426
|
id: "skill:delivery-workflow",
|
|
302
427
|
kind: "skill",
|
|
303
428
|
path: ".opencode/skills/delivery-workflow/SKILL.md",
|
|
304
|
-
source:
|
|
429
|
+
source: resolve4(packageRoot, "assets/skills/ship-workflow/SKILL.md"),
|
|
305
430
|
mode: 420,
|
|
306
|
-
profiles: ["engineering"]
|
|
431
|
+
profiles: ["engineering"],
|
|
432
|
+
legacy: true
|
|
307
433
|
},
|
|
308
434
|
{
|
|
309
435
|
id: "skill:planning-research-checkpoint",
|
|
310
436
|
kind: "skill",
|
|
311
437
|
path: ".opencode/skills/planning-research-checkpoint/SKILL.md",
|
|
312
|
-
source:
|
|
438
|
+
source: resolve4(packageRoot, "assets/skills/planning-research-checkpoint/SKILL.md"),
|
|
313
439
|
mode: 420,
|
|
314
440
|
profiles: ["engineering"]
|
|
315
441
|
},
|
|
@@ -317,7 +443,7 @@ var init_catalog = __esm({
|
|
|
317
443
|
id: `skill:matt:${name}`,
|
|
318
444
|
kind: "skill",
|
|
319
445
|
path: `.opencode/skills/${name}/SKILL.md`,
|
|
320
|
-
source:
|
|
446
|
+
source: resolve4(packageRoot, `assets/skills/${name}/SKILL.md`),
|
|
321
447
|
mode: 420,
|
|
322
448
|
profiles: ["engineering"]
|
|
323
449
|
})),
|
|
@@ -325,7 +451,7 @@ var init_catalog = __esm({
|
|
|
325
451
|
id: `skill:super:${name}`,
|
|
326
452
|
kind: "skill",
|
|
327
453
|
path: `.opencode/skills/${name}/SKILL.md`,
|
|
328
|
-
source:
|
|
454
|
+
source: resolve4(packageRoot, `assets/skills/${name}/SKILL.md`),
|
|
329
455
|
mode: 420,
|
|
330
456
|
profiles: ["engineering"]
|
|
331
457
|
})),
|
|
@@ -333,7 +459,7 @@ var init_catalog = __esm({
|
|
|
333
459
|
id: "skill:setup-ship-workflow",
|
|
334
460
|
kind: "skill",
|
|
335
461
|
path: ".opencode/skills/setup-ship-workflow/SKILL.md",
|
|
336
|
-
source:
|
|
462
|
+
source: resolve4(packageRoot, "assets/skills/setup-ship-workflow/SKILL.md"),
|
|
337
463
|
mode: 420,
|
|
338
464
|
profiles: ["engineering"]
|
|
339
465
|
},
|
|
@@ -341,7 +467,7 @@ var init_catalog = __esm({
|
|
|
341
467
|
id: "skill:skill-discovery",
|
|
342
468
|
kind: "skill",
|
|
343
469
|
path: ".opencode/skills/skill-discovery/SKILL.md",
|
|
344
|
-
source:
|
|
470
|
+
source: resolve4(packageRoot, "assets/skills/skill-discovery/SKILL.md"),
|
|
345
471
|
mode: 420,
|
|
346
472
|
profiles: ["engineering"]
|
|
347
473
|
},
|
|
@@ -349,7 +475,7 @@ var init_catalog = __esm({
|
|
|
349
475
|
id: "command:setup-ship-workflow",
|
|
350
476
|
kind: "support",
|
|
351
477
|
path: ".opencode/commands/setup-ship-workflow.md",
|
|
352
|
-
source:
|
|
478
|
+
source: resolve4(packageRoot, "assets/commands/setup-ship-workflow.md"),
|
|
353
479
|
mode: 420,
|
|
354
480
|
profiles: ["engineering"]
|
|
355
481
|
}
|
|
@@ -772,14 +898,14 @@ __export(config_exports, {
|
|
|
772
898
|
writeConfig: () => writeConfig
|
|
773
899
|
});
|
|
774
900
|
import { readFile, writeFile, rename, mkdir } from "node:fs/promises";
|
|
775
|
-
import { existsSync as
|
|
776
|
-
import { dirname as dirname3, resolve as
|
|
901
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
902
|
+
import { dirname as dirname3, resolve as resolve5 } from "node:path";
|
|
777
903
|
function configPath(repoRoot) {
|
|
778
|
-
return
|
|
904
|
+
return resolve5(repoRoot, ".opencode", "ship.config.json");
|
|
779
905
|
}
|
|
780
906
|
async function loadConfig(repoRoot) {
|
|
781
907
|
const path = configPath(repoRoot);
|
|
782
|
-
if (!
|
|
908
|
+
if (!existsSync5(path)) return null;
|
|
783
909
|
const raw = await readFile(path, "utf8");
|
|
784
910
|
let parsed;
|
|
785
911
|
try {
|
|
@@ -835,10 +961,10 @@ function renderDefaultConfig(detection, overrides = {}) {
|
|
|
835
961
|
requireCleanDiffAfter: true,
|
|
836
962
|
invalidateOnHeadChange: true
|
|
837
963
|
},
|
|
838
|
-
review: { agent: "
|
|
964
|
+
review: { agent: "ship-reviewer", required: true, invalidateOnHeadChange: true },
|
|
839
965
|
ci: {
|
|
840
966
|
driver: "github-status-checks",
|
|
841
|
-
requiredChecks: ["
|
|
967
|
+
requiredChecks: ["opencode-ship-verify"],
|
|
842
968
|
wait: true,
|
|
843
969
|
flakyRetry: 1
|
|
844
970
|
},
|
|
@@ -869,10 +995,10 @@ var init_config = __esm({
|
|
|
869
995
|
|
|
870
996
|
// src/installer/lock.js
|
|
871
997
|
import { readFile as readFile2, writeFile as writeFile2, rename as rename2, mkdir as mkdir2 } from "node:fs/promises";
|
|
872
|
-
import { existsSync as
|
|
873
|
-
import { dirname as dirname4, resolve as
|
|
998
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
999
|
+
import { dirname as dirname4, resolve as resolve6, posix } from "node:path";
|
|
874
1000
|
function lockPath(repoRoot) {
|
|
875
|
-
return
|
|
1001
|
+
return resolve6(repoRoot, ".opencode", "ship.lock.json");
|
|
876
1002
|
}
|
|
877
1003
|
function computeIntegrity(lock) {
|
|
878
1004
|
const { integrity: _ignored, ...without } = lock ?? {};
|
|
@@ -896,8 +1022,8 @@ function validateLock(rawLock) {
|
|
|
896
1022
|
}
|
|
897
1023
|
const issues = [];
|
|
898
1024
|
let kind = "ok";
|
|
899
|
-
if (rawLock.contractVersion !== CURRENT_LOCK_SCHEMA && rawLock.contractVersion !== 3 && rawLock.contractVersion !== 2 && rawLock.contractVersion !== 1) {
|
|
900
|
-
issues.push(`unsupported contractVersion: ${JSON.stringify(rawLock.contractVersion)} (expected ${CURRENT_LOCK_SCHEMA}, 3, 2, or 1)`);
|
|
1025
|
+
if (rawLock.contractVersion !== CURRENT_LOCK_SCHEMA && rawLock.contractVersion !== 4 && rawLock.contractVersion !== 3 && rawLock.contractVersion !== 2 && rawLock.contractVersion !== 1) {
|
|
1026
|
+
issues.push(`unsupported contractVersion: ${JSON.stringify(rawLock.contractVersion)} (expected ${CURRENT_LOCK_SCHEMA}, 4, 3, 2, or 1)`);
|
|
901
1027
|
kind = "schema";
|
|
902
1028
|
}
|
|
903
1029
|
const manager = rawLock.manager;
|
|
@@ -907,8 +1033,8 @@ function validateLock(rawLock) {
|
|
|
907
1033
|
} else if (typeof manager !== "object" || manager === null) {
|
|
908
1034
|
issues.push("manager section must be an object");
|
|
909
1035
|
kind = kind === "ok" ? "shape" : kind;
|
|
910
|
-
} else if (manager.schemaVersion !== CURRENT_LOCK_SCHEMA && manager.schemaVersion !== 3 && manager.schemaVersion !== 2 && manager.schemaVersion !== 1) {
|
|
911
|
-
issues.push(`unsupported manager.schemaVersion: ${JSON.stringify(manager.schemaVersion)} (expected ${CURRENT_LOCK_SCHEMA}, 3, 2, or 1)`);
|
|
1036
|
+
} else if (manager.schemaVersion !== CURRENT_LOCK_SCHEMA && manager.schemaVersion !== 4 && manager.schemaVersion !== 3 && manager.schemaVersion !== 2 && manager.schemaVersion !== 1) {
|
|
1037
|
+
issues.push(`unsupported manager.schemaVersion: ${JSON.stringify(manager.schemaVersion)} (expected ${CURRENT_LOCK_SCHEMA}, 4, 3, 2, or 1)`);
|
|
912
1038
|
kind = "schema";
|
|
913
1039
|
} else if (manager.name !== "opencode-ship") {
|
|
914
1040
|
issues.push(`unknown manager.name: ${JSON.stringify(manager.name)}`);
|
|
@@ -947,7 +1073,7 @@ function isSafeManagedPath(value) {
|
|
|
947
1073
|
}
|
|
948
1074
|
async function readValidatedLock(repoRoot) {
|
|
949
1075
|
const path = lockPath(repoRoot);
|
|
950
|
-
if (!
|
|
1076
|
+
if (!existsSync6(path)) {
|
|
951
1077
|
return { kind: "missing", lock: null, issues: [] };
|
|
952
1078
|
}
|
|
953
1079
|
let raw;
|
|
@@ -983,7 +1109,7 @@ var init_lock = __esm({
|
|
|
983
1109
|
init_hash();
|
|
984
1110
|
init_json_pointer();
|
|
985
1111
|
init_profile();
|
|
986
|
-
CURRENT_LOCK_SCHEMA =
|
|
1112
|
+
CURRENT_LOCK_SCHEMA = 5;
|
|
987
1113
|
}
|
|
988
1114
|
});
|
|
989
1115
|
|
|
@@ -1007,7 +1133,16 @@ function planModePermissions() {
|
|
|
1007
1133
|
delivery_pr: DENY_DEFAULT,
|
|
1008
1134
|
delivery_ready: DENY_DEFAULT,
|
|
1009
1135
|
delivery_merge: DENY_DEFAULT,
|
|
1010
|
-
delivery_cleanup: DENY_DEFAULT
|
|
1136
|
+
delivery_cleanup: DENY_DEFAULT,
|
|
1137
|
+
ship_inspect: DENY_DEFAULT,
|
|
1138
|
+
ship_issue: DENY_DEFAULT,
|
|
1139
|
+
ship_worktree: DENY_DEFAULT,
|
|
1140
|
+
ship_verify: DENY_DEFAULT,
|
|
1141
|
+
ship_review: DENY_DEFAULT,
|
|
1142
|
+
ship_pr: DENY_DEFAULT,
|
|
1143
|
+
ship_ready: DENY_DEFAULT,
|
|
1144
|
+
ship_merge: DENY_DEFAULT,
|
|
1145
|
+
ship_cleanup: DENY_DEFAULT
|
|
1011
1146
|
}
|
|
1012
1147
|
};
|
|
1013
1148
|
}
|
|
@@ -1089,24 +1224,24 @@ __export(root_config_exports, {
|
|
|
1089
1224
|
readRootConfig: () => readRootConfig,
|
|
1090
1225
|
synthesizeDefaultRootConfig: () => synthesizeDefaultRootConfig
|
|
1091
1226
|
});
|
|
1092
|
-
import { existsSync as
|
|
1227
|
+
import { existsSync as existsSync7, readFileSync as readFileSync4 } from "node:fs";
|
|
1093
1228
|
import { readFile as readFile3 } from "node:fs/promises";
|
|
1094
|
-
import { resolve as
|
|
1229
|
+
import { resolve as resolve7 } from "node:path";
|
|
1095
1230
|
function findRootConfig(repoRoot) {
|
|
1096
1231
|
for (const rel of ROOT_PATH_CANDIDATES) {
|
|
1097
|
-
const abs =
|
|
1098
|
-
if (
|
|
1232
|
+
const abs = resolve7(repoRoot, rel);
|
|
1233
|
+
if (existsSync7(abs)) return { path: abs, relative: rel, format: rel.endsWith(".jsonc") ? "jsonc" : "json" };
|
|
1099
1234
|
}
|
|
1100
1235
|
return { path: null, relative: ROOT_PATH_CANDIDATES[0], format: "json" };
|
|
1101
1236
|
}
|
|
1102
1237
|
function defaultRootConfigPath(repoRoot) {
|
|
1103
|
-
return
|
|
1238
|
+
return resolve7(repoRoot, ROOT_PATH_CANDIDATES[0]);
|
|
1104
1239
|
}
|
|
1105
1240
|
function readRootConfig(absPath) {
|
|
1106
|
-
if (!
|
|
1241
|
+
if (!existsSync7(absPath)) {
|
|
1107
1242
|
return { ok: false, error: { kind: "missing", path: absPath } };
|
|
1108
1243
|
}
|
|
1109
|
-
const raw =
|
|
1244
|
+
const raw = readFileSync4(absPath, "utf8");
|
|
1110
1245
|
const stripped = stripJsonc(raw);
|
|
1111
1246
|
try {
|
|
1112
1247
|
const value = JSON.parse(stripped);
|
|
@@ -1320,6 +1455,61 @@ var init_root_config = __esm({
|
|
|
1320
1455
|
strategy: "value",
|
|
1321
1456
|
value: "allow"
|
|
1322
1457
|
},
|
|
1458
|
+
{
|
|
1459
|
+
pointer: "/agent/build/permission/ship_inspect",
|
|
1460
|
+
strategy: "value",
|
|
1461
|
+
value: "allow"
|
|
1462
|
+
},
|
|
1463
|
+
{
|
|
1464
|
+
pointer: "/agent/build/permission/ship_issue",
|
|
1465
|
+
strategy: "value",
|
|
1466
|
+
value: "allow"
|
|
1467
|
+
},
|
|
1468
|
+
{
|
|
1469
|
+
pointer: "/agent/build/permission/ship_worktree",
|
|
1470
|
+
strategy: "value",
|
|
1471
|
+
value: "allow"
|
|
1472
|
+
},
|
|
1473
|
+
{
|
|
1474
|
+
pointer: "/agent/build/permission/ship_verify",
|
|
1475
|
+
strategy: "value",
|
|
1476
|
+
value: "deny"
|
|
1477
|
+
},
|
|
1478
|
+
{
|
|
1479
|
+
pointer: "/agent/build/permission/ship_review",
|
|
1480
|
+
strategy: "value",
|
|
1481
|
+
value: "deny"
|
|
1482
|
+
},
|
|
1483
|
+
{
|
|
1484
|
+
pointer: "/agent/build/permission/ship_pr",
|
|
1485
|
+
strategy: "value",
|
|
1486
|
+
value: "allow"
|
|
1487
|
+
},
|
|
1488
|
+
{
|
|
1489
|
+
pointer: "/agent/build/permission/ship_ready",
|
|
1490
|
+
strategy: "value",
|
|
1491
|
+
value: "allow"
|
|
1492
|
+
},
|
|
1493
|
+
{
|
|
1494
|
+
pointer: "/agent/build/permission/ship_merge",
|
|
1495
|
+
strategy: "value",
|
|
1496
|
+
value: "ask"
|
|
1497
|
+
},
|
|
1498
|
+
{
|
|
1499
|
+
pointer: "/agent/build/permission/ship_cleanup",
|
|
1500
|
+
strategy: "value",
|
|
1501
|
+
value: "allow"
|
|
1502
|
+
},
|
|
1503
|
+
{
|
|
1504
|
+
pointer: "/agent/build/permission/task/ship-reviewer",
|
|
1505
|
+
strategy: "value",
|
|
1506
|
+
value: "allow"
|
|
1507
|
+
},
|
|
1508
|
+
{
|
|
1509
|
+
pointer: "/agent/build/permission/task/ship-verifier",
|
|
1510
|
+
strategy: "value",
|
|
1511
|
+
value: "allow"
|
|
1512
|
+
},
|
|
1323
1513
|
// Build -> ship-controller delegation so the deep plan/build/review
|
|
1324
1514
|
// chain works with subagent_depth=2.
|
|
1325
1515
|
{
|
|
@@ -1556,9 +1746,9 @@ __export(setup_state_exports, {
|
|
|
1556
1746
|
modelsComplete: () => modelsComplete,
|
|
1557
1747
|
setupComplete: () => setupComplete
|
|
1558
1748
|
});
|
|
1559
|
-
import { existsSync as
|
|
1749
|
+
import { existsSync as existsSync15 } from "node:fs";
|
|
1560
1750
|
import { readFile as readFile10 } from "node:fs/promises";
|
|
1561
|
-
import { resolve as
|
|
1751
|
+
import { resolve as resolve14 } from "node:path";
|
|
1562
1752
|
function modelsComplete(repoRoot, configValue) {
|
|
1563
1753
|
if (configValue === void 0) return false;
|
|
1564
1754
|
return hasCompletedModels(configValue);
|
|
@@ -1566,12 +1756,12 @@ function modelsComplete(repoRoot, configValue) {
|
|
|
1566
1756
|
async function setupComplete(repoRoot, configValue) {
|
|
1567
1757
|
const missing = [];
|
|
1568
1758
|
for (const rel of REQUIRED_DOCS) {
|
|
1569
|
-
const path =
|
|
1570
|
-
if (!
|
|
1759
|
+
const path = resolve14(repoRoot, rel);
|
|
1760
|
+
if (!existsSync15(path)) missing.push(rel);
|
|
1571
1761
|
}
|
|
1572
|
-
const agentsPath =
|
|
1762
|
+
const agentsPath = resolve14(repoRoot, "AGENTS.md");
|
|
1573
1763
|
let agentsOk = false;
|
|
1574
|
-
if (
|
|
1764
|
+
if (existsSync15(agentsPath)) {
|
|
1575
1765
|
try {
|
|
1576
1766
|
const raw = await readFile10(agentsPath, "utf8");
|
|
1577
1767
|
agentsOk = /##\s+Ship workflow\b/.test(raw);
|
|
@@ -1611,6 +1801,840 @@ var init_setup_state = __esm({
|
|
|
1611
1801
|
}
|
|
1612
1802
|
});
|
|
1613
1803
|
|
|
1804
|
+
// src/tools/skill-discovery.js
|
|
1805
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
1806
|
+
import { existsSync as existsSync18, readFileSync as readFileSync8, writeFileSync, mkdirSync, readdirSync, statSync as statSync2 } from "node:fs";
|
|
1807
|
+
import { dirname as dirname10, join as join7, normalize, resolve as resolve17, sep as sep3 } from "node:path";
|
|
1808
|
+
function parseFindOutput(text) {
|
|
1809
|
+
if (typeof text !== "string") return [];
|
|
1810
|
+
const stripped = text.replace(ANSI_RE, "");
|
|
1811
|
+
const lines = stripped.split(/\r?\n/);
|
|
1812
|
+
const candidates = [];
|
|
1813
|
+
for (const raw of lines) {
|
|
1814
|
+
const line = raw.trim();
|
|
1815
|
+
if (!line) continue;
|
|
1816
|
+
const match = line.match(/^([a-zA-Z0-9_.\-]+)\/([a-zA-Z0-9_.\-]+)@([a-zA-Z0-9_.\-]+)\s+([0-9]+(?:\.[0-9]+)?)([KM]?)\s+installs\b/i);
|
|
1817
|
+
if (!match) continue;
|
|
1818
|
+
const num = Number.parseFloat(match[4]);
|
|
1819
|
+
if (!Number.isFinite(num)) continue;
|
|
1820
|
+
let installs = Math.round(num);
|
|
1821
|
+
const suffix = match[5].toUpperCase();
|
|
1822
|
+
if (suffix === "K") installs = Math.round(num * 1e3);
|
|
1823
|
+
else if (suffix === "M") installs = Math.round(num * 1e6);
|
|
1824
|
+
candidates.push({
|
|
1825
|
+
package: `${match[1]}/${match[2]}`,
|
|
1826
|
+
skill: match[3],
|
|
1827
|
+
installs
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
return candidates;
|
|
1831
|
+
}
|
|
1832
|
+
function discoverSkillsWithStdout(text) {
|
|
1833
|
+
const raw = typeof text === "string" ? text : "";
|
|
1834
|
+
const candidates = parseFindOutput(raw);
|
|
1835
|
+
if (candidates.length > 0) {
|
|
1836
|
+
return { ok: true, candidates, raw };
|
|
1837
|
+
}
|
|
1838
|
+
if (raw.trim().length === 0) {
|
|
1839
|
+
return { ok: true, candidates, raw };
|
|
1840
|
+
}
|
|
1841
|
+
return {
|
|
1842
|
+
ok: false,
|
|
1843
|
+
error: { kind: "registry-contract-mismatch", raw }
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
function runCapture(cmd, args, options) {
|
|
1847
|
+
const cwd = options?.cwd;
|
|
1848
|
+
const timeoutMs = options?.timeoutMs ?? 6e4;
|
|
1849
|
+
return new Promise((resolveP, rejectP) => {
|
|
1850
|
+
const child = spawn2(cmd, args, { cwd, shell: false, stdio: ["ignore", "pipe", "pipe"] });
|
|
1851
|
+
let stdout = "";
|
|
1852
|
+
let stderr = "";
|
|
1853
|
+
const timer = setTimeout(() => {
|
|
1854
|
+
child.kill("SIGKILL");
|
|
1855
|
+
rejectP(new Error(`skill-discovery: timeout running '${cmd} ${args.join(" ")}'`));
|
|
1856
|
+
}, timeoutMs);
|
|
1857
|
+
child.stdout.on("data", (chunk) => {
|
|
1858
|
+
stdout += chunk.toString("utf8");
|
|
1859
|
+
});
|
|
1860
|
+
child.stderr.on("data", (chunk) => {
|
|
1861
|
+
stderr += chunk.toString("utf8");
|
|
1862
|
+
});
|
|
1863
|
+
child.on("error", (err) => {
|
|
1864
|
+
clearTimeout(timer);
|
|
1865
|
+
rejectP(err);
|
|
1866
|
+
});
|
|
1867
|
+
child.on("close", (code) => {
|
|
1868
|
+
clearTimeout(timer);
|
|
1869
|
+
resolveP({ code, stdout, stderr });
|
|
1870
|
+
});
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
async function discoverSkills({ repoRoot, query, npmBin = "npx" }) {
|
|
1874
|
+
if (!repoRoot || !query) {
|
|
1875
|
+
return { ok: false, error: { kind: "missing-args" } };
|
|
1876
|
+
}
|
|
1877
|
+
const r = await runCapture(npmBin, ["skills", "find", query], { cwd: repoRoot, timeoutMs: 6e4 });
|
|
1878
|
+
if (r.code !== 0 && !r.stdout.trim()) {
|
|
1879
|
+
return { ok: false, error: { kind: "registry-unavailable", stderr: r.stderr } };
|
|
1880
|
+
}
|
|
1881
|
+
return discoverSkillsWithStdout(r.stdout);
|
|
1882
|
+
}
|
|
1883
|
+
var DEFAULT_TRUSTED_OWNERS, ANSI_RE;
|
|
1884
|
+
var init_skill_discovery = __esm({
|
|
1885
|
+
"src/tools/skill-discovery.js"() {
|
|
1886
|
+
DEFAULT_TRUSTED_OWNERS = Object.freeze([
|
|
1887
|
+
"vercel-labs",
|
|
1888
|
+
"anthropics",
|
|
1889
|
+
"obra",
|
|
1890
|
+
"mattpocock",
|
|
1891
|
+
"ComposioHQ"
|
|
1892
|
+
]);
|
|
1893
|
+
ANSI_RE = /\x1B\[[0-9;]*m/g;
|
|
1894
|
+
}
|
|
1895
|
+
});
|
|
1896
|
+
|
|
1897
|
+
// src/skills/registry.js
|
|
1898
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
1899
|
+
import { readFile as readFile11, writeFile as writeFile8, mkdir as mkdir7 } from "node:fs/promises";
|
|
1900
|
+
import { resolve as resolve18, dirname as dirname11 } from "node:path";
|
|
1901
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
1902
|
+
async function listSkills({ repoRoot, query, npmBin = "npx" }) {
|
|
1903
|
+
return discoverSkills({ repoRoot, query, npmBin });
|
|
1904
|
+
}
|
|
1905
|
+
var SKILLS_CLI_TIMEOUT_MS, SKILLS_INSTALL_TIMEOUT_MS;
|
|
1906
|
+
var init_registry = __esm({
|
|
1907
|
+
"src/skills/registry.js"() {
|
|
1908
|
+
init_skill_discovery();
|
|
1909
|
+
SKILLS_CLI_TIMEOUT_MS = 60 * 1e3;
|
|
1910
|
+
SKILLS_INSTALL_TIMEOUT_MS = 120 * 1e3;
|
|
1911
|
+
}
|
|
1912
|
+
});
|
|
1913
|
+
|
|
1914
|
+
// src/skills/policy.js
|
|
1915
|
+
import { readFile as readFile12, writeFile as writeFile9 } from "node:fs/promises";
|
|
1916
|
+
import { existsSync as existsSync19 } from "node:fs";
|
|
1917
|
+
import { resolve as resolve19 } from "node:path";
|
|
1918
|
+
function policyPath(repoRoot) {
|
|
1919
|
+
return resolve19(repoRoot, POLICY_PATH);
|
|
1920
|
+
}
|
|
1921
|
+
function defaultPolicy() {
|
|
1922
|
+
return {
|
|
1923
|
+
trustedOwners: [...DEFAULT_TRUSTED_OWNERS2],
|
|
1924
|
+
minInstalls: DEFAULT_MIN_INSTALLS,
|
|
1925
|
+
blocklist: [],
|
|
1926
|
+
maxTrustedPerRun: MAX_TRUSTED_PER_RUN
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
async function readPolicy(repoRoot) {
|
|
1930
|
+
const path = policyPath(repoRoot);
|
|
1931
|
+
if (!existsSync19(path)) return defaultPolicy();
|
|
1932
|
+
try {
|
|
1933
|
+
const raw = await readFile12(path, "utf8");
|
|
1934
|
+
const parsed = JSON.parse(raw);
|
|
1935
|
+
return mergePolicy(defaultPolicy(), parsed);
|
|
1936
|
+
} catch {
|
|
1937
|
+
return defaultPolicy();
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
function mergePolicy(base, override) {
|
|
1941
|
+
const out = { ...base };
|
|
1942
|
+
if (Array.isArray(override?.trustedOwners)) {
|
|
1943
|
+
out.trustedOwners = [...new Set(override.trustedOwners)];
|
|
1944
|
+
}
|
|
1945
|
+
if (Number.isInteger(override?.minInstalls)) {
|
|
1946
|
+
out.minInstalls = override.minInstalls;
|
|
1947
|
+
}
|
|
1948
|
+
if (Array.isArray(override?.blocklist)) {
|
|
1949
|
+
out.blocklist = [...new Set(override.blocklist)];
|
|
1950
|
+
}
|
|
1951
|
+
if (Number.isInteger(override?.maxTrustedPerRun)) {
|
|
1952
|
+
out.maxTrustedPerRun = override.maxTrustedPerRun;
|
|
1953
|
+
}
|
|
1954
|
+
return out;
|
|
1955
|
+
}
|
|
1956
|
+
function isAutoInstallable(candidate, policy) {
|
|
1957
|
+
if (!candidate || typeof candidate !== "object") {
|
|
1958
|
+
return { ok: false, reason: "missing-candidate" };
|
|
1959
|
+
}
|
|
1960
|
+
if ((policy.blocklist ?? []).includes(candidate.package)) {
|
|
1961
|
+
return { ok: false, reason: "blocked" };
|
|
1962
|
+
}
|
|
1963
|
+
const owner = String(candidate.package).split("/")[0];
|
|
1964
|
+
if (!(policy.trustedOwners ?? []).includes(owner)) {
|
|
1965
|
+
return { ok: false, reason: "untrusted-owner" };
|
|
1966
|
+
}
|
|
1967
|
+
if (candidate.installs < (policy.minInstalls ?? DEFAULT_MIN_INSTALLS)) {
|
|
1968
|
+
return { ok: false, reason: "below-threshold" };
|
|
1969
|
+
}
|
|
1970
|
+
return { ok: true };
|
|
1971
|
+
}
|
|
1972
|
+
var DEFAULT_TRUSTED_OWNERS2, DEFAULT_MIN_INSTALLS, MAX_TRUSTED_PER_RUN, POLICY_PATH;
|
|
1973
|
+
var init_policy = __esm({
|
|
1974
|
+
"src/skills/policy.js"() {
|
|
1975
|
+
init_hash();
|
|
1976
|
+
DEFAULT_TRUSTED_OWNERS2 = Object.freeze([
|
|
1977
|
+
"vercel-labs",
|
|
1978
|
+
"anthropics",
|
|
1979
|
+
"obra",
|
|
1980
|
+
"mattpocock",
|
|
1981
|
+
"ComposioHQ"
|
|
1982
|
+
]);
|
|
1983
|
+
DEFAULT_MIN_INSTALLS = 1e3;
|
|
1984
|
+
MAX_TRUSTED_PER_RUN = 5;
|
|
1985
|
+
POLICY_PATH = ".opencode/ship.skills.policy.json";
|
|
1986
|
+
}
|
|
1987
|
+
});
|
|
1988
|
+
|
|
1989
|
+
// src/tools/envelope.js
|
|
1990
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
1991
|
+
function operationId(prefix = "op") {
|
|
1992
|
+
return `${prefix}-${Date.now().toString(36)}-${randomBytes2(4).toString("hex")}`;
|
|
1993
|
+
}
|
|
1994
|
+
function success(kind, data, options = {}) {
|
|
1995
|
+
if (typeof kind !== "string" || kind.length === 0) {
|
|
1996
|
+
throw new Error("envelope.success: kind must be a non-empty string");
|
|
1997
|
+
}
|
|
1998
|
+
return {
|
|
1999
|
+
contractVersion: CONTRACT_VERSION,
|
|
2000
|
+
ok: true,
|
|
2001
|
+
kind,
|
|
2002
|
+
operationId: options.operationId ?? operationId(kind),
|
|
2003
|
+
idempotent: options.idempotent !== false,
|
|
2004
|
+
data
|
|
2005
|
+
};
|
|
2006
|
+
}
|
|
2007
|
+
function failure(kind, message, options = {}) {
|
|
2008
|
+
if (typeof kind !== "string" || kind.length === 0) {
|
|
2009
|
+
throw new Error("envelope.failure: kind must be a non-empty string");
|
|
2010
|
+
}
|
|
2011
|
+
if (typeof message !== "string" || message.length === 0) {
|
|
2012
|
+
throw new Error("envelope.failure: message must be a non-empty string");
|
|
2013
|
+
}
|
|
2014
|
+
const details = options.details ?? {};
|
|
2015
|
+
return {
|
|
2016
|
+
contractVersion: CONTRACT_VERSION,
|
|
2017
|
+
ok: false,
|
|
2018
|
+
kind,
|
|
2019
|
+
operationId: options.operationId ?? operationId(`${kind}-err`),
|
|
2020
|
+
retryable: options.retryable === true,
|
|
2021
|
+
message,
|
|
2022
|
+
details
|
|
2023
|
+
};
|
|
2024
|
+
}
|
|
2025
|
+
var CONTRACT_VERSION;
|
|
2026
|
+
var init_envelope = __esm({
|
|
2027
|
+
"src/tools/envelope.js"() {
|
|
2028
|
+
CONTRACT_VERSION = 2;
|
|
2029
|
+
}
|
|
2030
|
+
});
|
|
2031
|
+
|
|
2032
|
+
// src/skills/inventory.js
|
|
2033
|
+
import { readFile as readFile13, writeFile as writeFile10, mkdir as mkdir8, rename as rename6 } from "node:fs/promises";
|
|
2034
|
+
import { existsSync as existsSync20 } from "node:fs";
|
|
2035
|
+
import { resolve as resolve20, dirname as dirname12, isAbsolute, posix as posix2 } from "node:path";
|
|
2036
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
2037
|
+
function inventoryPath(repoRoot) {
|
|
2038
|
+
return resolve20(repoRoot, INVENTORY_PATH);
|
|
2039
|
+
}
|
|
2040
|
+
async function readInventory(repoRoot) {
|
|
2041
|
+
const path = inventoryPath(repoRoot);
|
|
2042
|
+
if (!existsSync20(path)) {
|
|
2043
|
+
return { schemaVersion: INVENTORY_SCHEMA, events: [] };
|
|
2044
|
+
}
|
|
2045
|
+
let raw;
|
|
2046
|
+
try {
|
|
2047
|
+
raw = await readFile13(path, "utf8");
|
|
2048
|
+
} catch (err) {
|
|
2049
|
+
return { schemaVersion: INVENTORY_SCHEMA, events: [], parseError: `read failed: ${err?.message ?? err}` };
|
|
2050
|
+
}
|
|
2051
|
+
let parsed;
|
|
2052
|
+
try {
|
|
2053
|
+
parsed = JSON.parse(raw);
|
|
2054
|
+
} catch (err) {
|
|
2055
|
+
return { schemaVersion: INVENTORY_SCHEMA, events: [], parseError: `malformed JSON: ${err?.message ?? err}` };
|
|
2056
|
+
}
|
|
2057
|
+
if (!parsed || typeof parsed !== "object") {
|
|
2058
|
+
return { schemaVersion: INVENTORY_SCHEMA, events: [], parseError: "inventory root is not an object" };
|
|
2059
|
+
}
|
|
2060
|
+
if (!Array.isArray(parsed.events)) {
|
|
2061
|
+
return { schemaVersion: INVENTORY_SCHEMA, events: [], parseError: "inventory.events is not an array" };
|
|
2062
|
+
}
|
|
2063
|
+
if (parsed.schemaVersion !== INVENTORY_SCHEMA) {
|
|
2064
|
+
return {
|
|
2065
|
+
schemaVersion: parsed.schemaVersion,
|
|
2066
|
+
events: [],
|
|
2067
|
+
parseError: `unsupported inventory schemaVersion ${parsed.schemaVersion} (expected ${INVENTORY_SCHEMA})`
|
|
2068
|
+
};
|
|
2069
|
+
}
|
|
2070
|
+
return { schemaVersion: INVENTORY_SCHEMA, events: parsed.events };
|
|
2071
|
+
}
|
|
2072
|
+
async function writeInventory(repoRoot, inventory) {
|
|
2073
|
+
const path = inventoryPath(repoRoot);
|
|
2074
|
+
await mkdir8(dirname12(path), { recursive: true });
|
|
2075
|
+
const tmp = `${path}.${Date.now().toString(36)}.tmp`;
|
|
2076
|
+
await writeFile10(tmp, JSON.stringify({ schemaVersion: INVENTORY_SCHEMA, events: inventory.events }, null, 2) + "\n", "utf8");
|
|
2077
|
+
await rename6(tmp, path);
|
|
2078
|
+
return path;
|
|
2079
|
+
}
|
|
2080
|
+
function canonicalize2(value) {
|
|
2081
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
2082
|
+
const sort = (v) => {
|
|
2083
|
+
if (v === null || typeof v !== "object") return v;
|
|
2084
|
+
if (seen.has(v)) return null;
|
|
2085
|
+
seen.add(v);
|
|
2086
|
+
if (Array.isArray(v)) return v.map(sort);
|
|
2087
|
+
const out = {};
|
|
2088
|
+
for (const k of Object.keys(v).sort()) out[k] = sort(v[k]);
|
|
2089
|
+
return out;
|
|
2090
|
+
};
|
|
2091
|
+
return JSON.stringify(sort(value));
|
|
2092
|
+
}
|
|
2093
|
+
function hashEvent(event) {
|
|
2094
|
+
return createHash4("sha256").update(canonicalize2(event), "utf8").digest("hex");
|
|
2095
|
+
}
|
|
2096
|
+
async function appendEvent(repoRoot, eventInput) {
|
|
2097
|
+
const inventory = await readInventory(repoRoot);
|
|
2098
|
+
if (inventory.parseError) {
|
|
2099
|
+
throw new Error(`inventory is unreadable: ${inventory.parseError}`);
|
|
2100
|
+
}
|
|
2101
|
+
const existingChain = await verifyInventory(repoRoot);
|
|
2102
|
+
if (!existingChain.ok) {
|
|
2103
|
+
throw new Error(`inventory chain invalid: ${existingChain.reason}`);
|
|
2104
|
+
}
|
|
2105
|
+
const previousHash = inventory.events.length > 0 ? inventory.events[inventory.events.length - 1].hash : "0".repeat(64);
|
|
2106
|
+
const sequence = inventory.events.length + 1;
|
|
2107
|
+
if (eventInput.destination && isAbsolute(eventInput.destination)) {
|
|
2108
|
+
throw new Error(`inventory refuses absolute destination: ${eventInput.destination}`);
|
|
2109
|
+
}
|
|
2110
|
+
const base = {
|
|
2111
|
+
sequence,
|
|
2112
|
+
type: eventInput.type,
|
|
2113
|
+
previousHash,
|
|
2114
|
+
recordedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2115
|
+
};
|
|
2116
|
+
const {
|
|
2117
|
+
hash: _hash,
|
|
2118
|
+
sequence: _sequence,
|
|
2119
|
+
previousHash: _previousHash,
|
|
2120
|
+
recordedAt: _recordedAt,
|
|
2121
|
+
payload: legacyPayload,
|
|
2122
|
+
type: _type,
|
|
2123
|
+
...fields
|
|
2124
|
+
} = eventInput;
|
|
2125
|
+
const payload = { ...legacyPayload ?? {}, ...fields, ...base };
|
|
2126
|
+
const shape = validateEventShape(payload);
|
|
2127
|
+
if (!shape.ok) throw new Error(shape.reason);
|
|
2128
|
+
delete payload.hash;
|
|
2129
|
+
const stamped = { ...payload, hash: hashEvent(payload) };
|
|
2130
|
+
inventory.events.push(stamped);
|
|
2131
|
+
await writeInventory(repoRoot, inventory);
|
|
2132
|
+
return stamped;
|
|
2133
|
+
}
|
|
2134
|
+
async function verifyInventory(repoRoot) {
|
|
2135
|
+
const inventory = await readInventory(repoRoot);
|
|
2136
|
+
if (inventory.parseError) {
|
|
2137
|
+
return { ok: false, reason: inventory.parseError };
|
|
2138
|
+
}
|
|
2139
|
+
if (inventory.events.length === 0) return { ok: true, count: 0 };
|
|
2140
|
+
let prev = "0".repeat(64);
|
|
2141
|
+
for (const ev of inventory.events) {
|
|
2142
|
+
const shape = validateEventShape(ev);
|
|
2143
|
+
if (!shape.ok) {
|
|
2144
|
+
return { ok: false, reason: shape.reason, sequence: ev.sequence };
|
|
2145
|
+
}
|
|
2146
|
+
if (ev.sequence !== inventory.events.indexOf(ev) + 1) {
|
|
2147
|
+
return { ok: false, reason: "sequence-gap", sequence: ev.sequence };
|
|
2148
|
+
}
|
|
2149
|
+
if (ev.previousHash !== prev) {
|
|
2150
|
+
return { ok: false, reason: "chain-break", sequence: ev.sequence };
|
|
2151
|
+
}
|
|
2152
|
+
const { hash: _h, ...rest } = ev;
|
|
2153
|
+
const recomputed = hashEvent(rest);
|
|
2154
|
+
if (recomputed !== ev.hash) {
|
|
2155
|
+
return { ok: false, reason: "hash-mismatch", sequence: ev.sequence };
|
|
2156
|
+
}
|
|
2157
|
+
prev = ev.hash;
|
|
2158
|
+
}
|
|
2159
|
+
return { ok: true, count: inventory.events.length };
|
|
2160
|
+
}
|
|
2161
|
+
function validateEventShape(event) {
|
|
2162
|
+
if (event?.type !== "install" && event?.type !== "uninstall") {
|
|
2163
|
+
return { ok: false, reason: `unsupported inventory event type: ${JSON.stringify(event?.type)}` };
|
|
2164
|
+
}
|
|
2165
|
+
if (typeof event.skill !== "string" || !/^[A-Za-z0-9._-]{1,128}$/.test(event.skill)) {
|
|
2166
|
+
return { ok: false, reason: `invalid skill id: ${JSON.stringify(event.skill)}` };
|
|
2167
|
+
}
|
|
2168
|
+
if (!isSafeRelativePosix(event.destination)) {
|
|
2169
|
+
return { ok: false, reason: `unsafe destination: ${JSON.stringify(event.destination)}` };
|
|
2170
|
+
}
|
|
2171
|
+
if (event.type === "install") {
|
|
2172
|
+
if (!Array.isArray(event.files)) return { ok: false, reason: "install event files must be an array" };
|
|
2173
|
+
for (const file of event.files) {
|
|
2174
|
+
if (!file || !isSafeRelativePosix(file.path)) {
|
|
2175
|
+
return { ok: false, reason: `unsafe file path: ${JSON.stringify(file?.path)}` };
|
|
2176
|
+
}
|
|
2177
|
+
if (typeof file.sha256 !== "string" || !/^[0-9a-f]{64}$/.test(file.sha256)) {
|
|
2178
|
+
return { ok: false, reason: `invalid file sha256: ${JSON.stringify(file.sha256)}` };
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
return { ok: true };
|
|
2183
|
+
}
|
|
2184
|
+
function isSafeRelativePosix(value) {
|
|
2185
|
+
if (typeof value !== "string" || value.length === 0 || value.includes("\\")) return false;
|
|
2186
|
+
if (posix2.isAbsolute(value) || posix2.normalize(value) !== value) return false;
|
|
2187
|
+
return value.split("/").every((part) => part !== "" && part !== "." && part !== "..");
|
|
2188
|
+
}
|
|
2189
|
+
var INVENTORY_PATH, INVENTORY_SCHEMA;
|
|
2190
|
+
var init_inventory = __esm({
|
|
2191
|
+
"src/skills/inventory.js"() {
|
|
2192
|
+
INVENTORY_PATH = ".opencode/ship.skills.lock.json";
|
|
2193
|
+
INVENTORY_SCHEMA = 2;
|
|
2194
|
+
}
|
|
2195
|
+
});
|
|
2196
|
+
|
|
2197
|
+
// src/skills/worktree.js
|
|
2198
|
+
import { execFile } from "node:child_process";
|
|
2199
|
+
import { promises as fs, existsSync as existsSync21 } from "node:fs";
|
|
2200
|
+
import { resolve as resolve21, dirname as dirname13, sep as sep4, isAbsolute as isAbsolute2, join as join9 } from "node:path";
|
|
2201
|
+
function listRegisteredWorktrees(mainRepo) {
|
|
2202
|
+
return new Promise((resolveP, rejectP) => {
|
|
2203
|
+
execFile(
|
|
2204
|
+
"git",
|
|
2205
|
+
["-C", mainRepo, "worktree", "list", "--porcelain", "-z"],
|
|
2206
|
+
{ shell: false, maxBuffer: 1024 * 1024 },
|
|
2207
|
+
(err, stdout) => {
|
|
2208
|
+
if (err) return rejectP(err);
|
|
2209
|
+
const records = parsePorcelain(stdout);
|
|
2210
|
+
const mainRecord = records.shift();
|
|
2211
|
+
const mainPath = mainRecord?.worktree ? resolve21(mainRecord.worktree) : null;
|
|
2212
|
+
const linked = [];
|
|
2213
|
+
for (const r of records) {
|
|
2214
|
+
if (!r.worktree) continue;
|
|
2215
|
+
const p = resolve21(r.worktree);
|
|
2216
|
+
if (mainPath && p === mainPath) continue;
|
|
2217
|
+
linked.push({ path: p, branch: r.HEAD ?? null });
|
|
2218
|
+
}
|
|
2219
|
+
resolveP(linked);
|
|
2220
|
+
}
|
|
2221
|
+
);
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
2224
|
+
function parsePorcelain(text) {
|
|
2225
|
+
const tokens = text.split("\0");
|
|
2226
|
+
const out = [];
|
|
2227
|
+
let current = {};
|
|
2228
|
+
for (const tok of tokens) {
|
|
2229
|
+
if (tok.length === 0) {
|
|
2230
|
+
if (Object.keys(current).length > 0) {
|
|
2231
|
+
out.push(current);
|
|
2232
|
+
current = {};
|
|
2233
|
+
}
|
|
2234
|
+
continue;
|
|
2235
|
+
}
|
|
2236
|
+
const idx = tok.indexOf(" ");
|
|
2237
|
+
const key = idx === -1 ? tok : tok.slice(0, idx);
|
|
2238
|
+
const value = idx === -1 ? "" : tok.slice(idx + 1);
|
|
2239
|
+
if (key === "branch") {
|
|
2240
|
+
current.HEAD = value.startsWith("refs/heads/") ? value : `refs/heads/${value}`;
|
|
2241
|
+
} else {
|
|
2242
|
+
current[key] = value;
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
if (Object.keys(current).length > 0) out.push(current);
|
|
2246
|
+
return out;
|
|
2247
|
+
}
|
|
2248
|
+
async function validateLinkedWorktree(mainRepo, worktreePath, options = {}) {
|
|
2249
|
+
const main2 = resolve21(mainRepo);
|
|
2250
|
+
if (!existsSync21(main2)) {
|
|
2251
|
+
return { ok: false, kind: "missing", message: `main repository ${main2} does not exist` };
|
|
2252
|
+
}
|
|
2253
|
+
if (!worktreePath) {
|
|
2254
|
+
return { ok: false, kind: "unlinked", message: "worktreePath is required" };
|
|
2255
|
+
}
|
|
2256
|
+
const wt = resolve21(worktreePath);
|
|
2257
|
+
if (!existsSync21(wt)) {
|
|
2258
|
+
return { ok: false, kind: "missing", message: `worktree ${wt} does not exist` };
|
|
2259
|
+
}
|
|
2260
|
+
const isCurrent = wt === main2;
|
|
2261
|
+
if (isCurrent) {
|
|
2262
|
+
const gitEntry = options.allowCurrentLinked ? await fs.lstat(join9(main2, ".git")).catch(() => null) : null;
|
|
2263
|
+
if (!gitEntry?.isFile()) {
|
|
2264
|
+
return { ok: false, kind: "main", message: "installs into the main worktree are forbidden" };
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
let cursor = wt;
|
|
2268
|
+
while (cursor !== dirname13(cursor)) {
|
|
2269
|
+
const stat5 = await fs.lstat(cursor).catch(() => null);
|
|
2270
|
+
if (stat5?.isSymbolicLink()) {
|
|
2271
|
+
return {
|
|
2272
|
+
ok: false,
|
|
2273
|
+
kind: "ancestor-symlink",
|
|
2274
|
+
message: `worktree path contains a symlink at ${cursor}`
|
|
2275
|
+
};
|
|
2276
|
+
}
|
|
2277
|
+
cursor = dirname13(cursor);
|
|
2278
|
+
}
|
|
2279
|
+
const real = await fs.realpath(wt).catch(() => null);
|
|
2280
|
+
if (real && real !== wt) {
|
|
2281
|
+
return {
|
|
2282
|
+
ok: false,
|
|
2283
|
+
kind: "symlink",
|
|
2284
|
+
message: `worktree ${wt} resolves through a symlink to ${real}`
|
|
2285
|
+
};
|
|
2286
|
+
}
|
|
2287
|
+
const linked = await listRegisteredWorktrees(main2);
|
|
2288
|
+
const matched = linked.find((entry) => entry.path === wt);
|
|
2289
|
+
if (!matched) {
|
|
2290
|
+
return {
|
|
2291
|
+
ok: false,
|
|
2292
|
+
kind: "unlinked",
|
|
2293
|
+
message: `worktree ${wt} is not registered (git worktree list)`
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2296
|
+
return { ok: true, path: wt, registered: !!matched };
|
|
2297
|
+
}
|
|
2298
|
+
function isProjectSkillDest(destRel) {
|
|
2299
|
+
return PROJECT_SKILL_DEST_RE.test(String(destRel ?? ""));
|
|
2300
|
+
}
|
|
2301
|
+
function validateRelativeInstallPath(destRel) {
|
|
2302
|
+
if (typeof destRel !== "string" || destRel.length === 0) {
|
|
2303
|
+
return { ok: false, kind: "absolute", message: "destination path required" };
|
|
2304
|
+
}
|
|
2305
|
+
if (isAbsolute2(destRel)) {
|
|
2306
|
+
return { ok: false, kind: "absolute", message: `destination must be relative: ${destRel}` };
|
|
2307
|
+
}
|
|
2308
|
+
if (destRel.includes("\\")) {
|
|
2309
|
+
return { ok: false, kind: "parent-relative", message: `destination must use POSIX separators: ${destRel}` };
|
|
2310
|
+
}
|
|
2311
|
+
const parts = destRel.split("/");
|
|
2312
|
+
for (const p of parts) {
|
|
2313
|
+
if (p === "" || p === "." || p === "..") {
|
|
2314
|
+
return { ok: false, kind: "parent-relative", message: `destination escapes worktree: ${destRel}` };
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
return { ok: true };
|
|
2318
|
+
}
|
|
2319
|
+
async function validateInstallDestination(worktreeRoot, destRel) {
|
|
2320
|
+
const relativeCheck = validateRelativeInstallPath(destRel);
|
|
2321
|
+
if (!relativeCheck.ok) return relativeCheck;
|
|
2322
|
+
const root = resolve21(worktreeRoot);
|
|
2323
|
+
const destination = resolve21(root, ...destRel.split("/"));
|
|
2324
|
+
if (destination !== root && !destination.startsWith(root + sep4)) {
|
|
2325
|
+
return { ok: false, kind: "escape", message: `destination escapes worktree: ${destRel}` };
|
|
2326
|
+
}
|
|
2327
|
+
let cursor = root;
|
|
2328
|
+
for (const part of destRel.split("/")) {
|
|
2329
|
+
cursor = join9(cursor, part);
|
|
2330
|
+
const entry = await fs.lstat(cursor).catch(() => null);
|
|
2331
|
+
if (!entry) continue;
|
|
2332
|
+
if (entry.isSymbolicLink()) {
|
|
2333
|
+
return { ok: false, kind: "symlink", message: `destination path contains a symlink at ${cursor}` };
|
|
2334
|
+
}
|
|
2335
|
+
if (cursor !== destination && !entry.isDirectory()) {
|
|
2336
|
+
return { ok: false, kind: "not-directory", message: `destination ancestor is not a directory: ${cursor}` };
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
return { ok: true, path: destination };
|
|
2340
|
+
}
|
|
2341
|
+
var PROJECT_SKILL_DEST_RE;
|
|
2342
|
+
var init_worktree = __esm({
|
|
2343
|
+
"src/skills/worktree.js"() {
|
|
2344
|
+
PROJECT_SKILL_DEST_RE = /^\.opencode\/skills\/[A-Za-z0-9._-]{1,128}$/;
|
|
2345
|
+
}
|
|
2346
|
+
});
|
|
2347
|
+
|
|
2348
|
+
// src/tools/ship-skill-install.js
|
|
2349
|
+
var ship_skill_install_exports = {};
|
|
2350
|
+
__export(ship_skill_install_exports, {
|
|
2351
|
+
createSkillInstallTool: () => createSkillInstallTool,
|
|
2352
|
+
findStagedSkillDir: () => findStagedSkillDir
|
|
2353
|
+
});
|
|
2354
|
+
import { readFile as readFile14, writeFile as writeFile11, mkdir as mkdir9, rm, rename as rename7, stat as stat4 } from "node:fs/promises";
|
|
2355
|
+
import { existsSync as existsSync22 } from "node:fs";
|
|
2356
|
+
import { resolve as resolve22, join as join10, dirname as dirname14, sep as sep5, isAbsolute as isAbsolute3 } from "node:path";
|
|
2357
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
2358
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
2359
|
+
import { mkdtemp } from "node:fs/promises";
|
|
2360
|
+
import { tmpdir } from "node:os";
|
|
2361
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
2362
|
+
function createSkillInstallTool(deps) {
|
|
2363
|
+
return async function skillInstall(input) {
|
|
2364
|
+
const opId = input.operationId ?? `skill-install-${Date.now().toString(36)}`;
|
|
2365
|
+
const packageSpec = String(input.package ?? "");
|
|
2366
|
+
const worktreePath = input.worktreePath == null ? "" : String(input.worktreePath);
|
|
2367
|
+
const skillName = String(input.skillName ?? "");
|
|
2368
|
+
const version = String(input.version ?? "");
|
|
2369
|
+
if (!packageSpec || !SAFE_NAME_RE.test(packageSpec)) {
|
|
2370
|
+
return failure("skill-install", "package required (safe npm spec)", { operationId: opId, retryable: false });
|
|
2371
|
+
}
|
|
2372
|
+
if (!skillName || !SAFE_ID_RE.test(skillName)) {
|
|
2373
|
+
return failure("skill-install", "skillName required (safe id)", { operationId: opId, retryable: false });
|
|
2374
|
+
}
|
|
2375
|
+
if (version && !SAFE_VERSION_RE.test(version)) {
|
|
2376
|
+
return failure("skill-install", "version must match a safe semver spec", { operationId: opId, retryable: false });
|
|
2377
|
+
}
|
|
2378
|
+
const destRel = `.opencode/skills/${skillName}`;
|
|
2379
|
+
if (!isProjectSkillDest(destRel)) {
|
|
2380
|
+
return failure("skill-install", `destination rejected: ${destRel}`, { operationId: opId, retryable: false });
|
|
2381
|
+
}
|
|
2382
|
+
const policy = await readPolicy(deps.repoRoot);
|
|
2383
|
+
const ownerCandidate = {
|
|
2384
|
+
package: packageSpec,
|
|
2385
|
+
skill: skillName,
|
|
2386
|
+
installs: Number.MAX_SAFE_INTEGER
|
|
2387
|
+
};
|
|
2388
|
+
const ownerDecision = isAutoInstallable(ownerCandidate, policy);
|
|
2389
|
+
if (!ownerDecision.ok) {
|
|
2390
|
+
return failure("skill-install", `policy forbids install: ${ownerDecision.reason}`, { operationId: opId, retryable: false });
|
|
2391
|
+
}
|
|
2392
|
+
const requested = worktreePath ? resolve22(worktreePath) : resolve22(deps.repoRoot);
|
|
2393
|
+
const main2 = resolve22(deps.repoRoot);
|
|
2394
|
+
let installRoot;
|
|
2395
|
+
if (requested === main2) {
|
|
2396
|
+
installRoot = main2;
|
|
2397
|
+
} else {
|
|
2398
|
+
const wtCheck = await validateLinkedWorktree(deps.repoRoot, worktreePath);
|
|
2399
|
+
if (!wtCheck.ok) {
|
|
2400
|
+
return failure("skill-install", `worktree rejected: ${wtCheck.message}`, { operationId: opId, retryable: false });
|
|
2401
|
+
}
|
|
2402
|
+
installRoot = wtCheck.path;
|
|
2403
|
+
}
|
|
2404
|
+
const pathCheck = validateRelativeInstallPath(destRel);
|
|
2405
|
+
if (!pathCheck.ok) {
|
|
2406
|
+
return failure("skill-install", `destination rejected: ${pathCheck.message}`, { operationId: opId, retryable: false });
|
|
2407
|
+
}
|
|
2408
|
+
const destinationCheck = await validateInstallDestination(installRoot, destRel);
|
|
2409
|
+
if (!destinationCheck.ok) {
|
|
2410
|
+
return failure("skill-install", `destination rejected: ${destinationCheck.message}`, { operationId: opId, retryable: false });
|
|
2411
|
+
}
|
|
2412
|
+
const destAbs = destinationCheck.path;
|
|
2413
|
+
if (existsSync22(destAbs)) {
|
|
2414
|
+
return failure("skill-install", "destination already exists; use ship_skill_audit to detect drift", { operationId: opId, retryable: false });
|
|
2415
|
+
}
|
|
2416
|
+
const managedCatalog = (deps.config?.value?.skills ?? []).map((s) => s?.name).filter(Boolean);
|
|
2417
|
+
if (managedCatalog.includes(skillName)) {
|
|
2418
|
+
return failure("skill-install", "candidate shadows a managed skill", { operationId: opId, retryable: false });
|
|
2419
|
+
}
|
|
2420
|
+
const discover = deps.discoverSkills ?? listSkills;
|
|
2421
|
+
const discovery = await discover({ repoRoot: deps.repoRoot, query: packageSpec });
|
|
2422
|
+
if (!discovery?.ok) {
|
|
2423
|
+
return failure("skill-install", "registry metadata unavailable; refusing unverified install", { operationId: opId, retryable: true });
|
|
2424
|
+
}
|
|
2425
|
+
const candidate = discovery.candidates?.find((entry) => entry.package === packageSpec && entry.skill === skillName);
|
|
2426
|
+
if (!candidate) {
|
|
2427
|
+
return failure("skill-install", "exact skill package was not found in registry metadata", { operationId: opId, retryable: false });
|
|
2428
|
+
}
|
|
2429
|
+
const decision = isAutoInstallable(candidate, policy);
|
|
2430
|
+
if (!decision.ok) {
|
|
2431
|
+
return failure("skill-install", `policy forbids install: ${decision.reason}`, { operationId: opId, retryable: false });
|
|
2432
|
+
}
|
|
2433
|
+
const stage = await mkdtemp(join10(tmpdir(), `ship-skill-stage-${randomBytes3(4).toString("hex")}-`));
|
|
2434
|
+
let installedFiles;
|
|
2435
|
+
try {
|
|
2436
|
+
const materialise = deps.materialiseFromSkillsCli ?? materialiseFromSkillsCli;
|
|
2437
|
+
installedFiles = await materialise({
|
|
2438
|
+
packageSpec,
|
|
2439
|
+
skillName,
|
|
2440
|
+
version,
|
|
2441
|
+
stageDir: stage
|
|
2442
|
+
});
|
|
2443
|
+
if (!installedFiles.ok) {
|
|
2444
|
+
return failure("skill-install", installedFiles.message, { operationId: opId, retryable: installedFiles.retryable ?? false });
|
|
2445
|
+
}
|
|
2446
|
+
const fileRecords = await hashDir(installedFiles.stagedDir);
|
|
2447
|
+
if (fileRecords.length === 0) {
|
|
2448
|
+
return failure("skill-install", "skills CLI produced an empty staging directory", { operationId: opId, retryable: false });
|
|
2449
|
+
}
|
|
2450
|
+
await mkdir9(dirname14(destAbs), { recursive: true });
|
|
2451
|
+
const destTmp = `${destAbs}.${randomBytes3(4).toString("hex")}.tmp`;
|
|
2452
|
+
await copyDir(installedFiles.stagedDir, destTmp);
|
|
2453
|
+
const finalDestinationCheck = await validateInstallDestination(installRoot, destRel);
|
|
2454
|
+
if (!finalDestinationCheck.ok) {
|
|
2455
|
+
await rm(destTmp, { recursive: true, force: true });
|
|
2456
|
+
return failure("skill-install", `destination rejected: ${finalDestinationCheck.message}`, { operationId: opId, retryable: false });
|
|
2457
|
+
}
|
|
2458
|
+
await rename7(destTmp, destAbs);
|
|
2459
|
+
const onDisk = await hashDir(destAbs);
|
|
2460
|
+
if (!hashesEqual(fileRecords, onDisk)) {
|
|
2461
|
+
await rm(destAbs, { recursive: true, force: true });
|
|
2462
|
+
return failure("skill-install", "drift detected after copy; rolled back", { operationId: opId, retryable: false });
|
|
2463
|
+
}
|
|
2464
|
+
const recorded = await appendEvent(installRoot, {
|
|
2465
|
+
type: "install",
|
|
2466
|
+
skill: skillName,
|
|
2467
|
+
package: packageSpec,
|
|
2468
|
+
version: version || null,
|
|
2469
|
+
source: installedFiles.source,
|
|
2470
|
+
destination: destRel,
|
|
2471
|
+
files: fileRecords
|
|
2472
|
+
});
|
|
2473
|
+
return success("skill-install", {
|
|
2474
|
+
skill: skillName,
|
|
2475
|
+
package: packageSpec,
|
|
2476
|
+
version: version || null,
|
|
2477
|
+
destination: destRel,
|
|
2478
|
+
worktree: installRoot,
|
|
2479
|
+
source: installedFiles.source,
|
|
2480
|
+
files: fileRecords,
|
|
2481
|
+
sequence: recorded.sequence
|
|
2482
|
+
}, { operationId: opId });
|
|
2483
|
+
} catch (err) {
|
|
2484
|
+
if (existsSync22(destAbs)) {
|
|
2485
|
+
await rm(destAbs, { recursive: true, force: true }).catch(() => null);
|
|
2486
|
+
}
|
|
2487
|
+
return failure("skill-install", String(err?.message ?? err), { operationId: opId, retryable: true });
|
|
2488
|
+
} finally {
|
|
2489
|
+
await rm(stage, { recursive: true, force: true }).catch(() => null);
|
|
2490
|
+
}
|
|
2491
|
+
};
|
|
2492
|
+
}
|
|
2493
|
+
function findStagedSkillDir(stageDir, skillName) {
|
|
2494
|
+
const candidates = [
|
|
2495
|
+
join10(stageDir, ".opencode", "skills", skillName),
|
|
2496
|
+
join10(stageDir, ".agents", "skills", skillName),
|
|
2497
|
+
join10(stageDir, "skills", skillName)
|
|
2498
|
+
];
|
|
2499
|
+
return candidates.find((path) => existsSync22(path)) ?? null;
|
|
2500
|
+
}
|
|
2501
|
+
async function materialiseFromSkillsCli({ packageSpec, skillName, version, stageDir }) {
|
|
2502
|
+
const cliPkg = `skills@${SKILLS_CLI_VERSION}`;
|
|
2503
|
+
const resolvedPackageSpec = version ? `${packageSpec}@${version}` : packageSpec;
|
|
2504
|
+
const args = [
|
|
2505
|
+
"exec",
|
|
2506
|
+
"--yes",
|
|
2507
|
+
`--package=${cliPkg}`,
|
|
2508
|
+
"--",
|
|
2509
|
+
"skills",
|
|
2510
|
+
"add",
|
|
2511
|
+
resolvedPackageSpec,
|
|
2512
|
+
"--skill",
|
|
2513
|
+
skillName,
|
|
2514
|
+
"--agent",
|
|
2515
|
+
"opencode",
|
|
2516
|
+
"--copy",
|
|
2517
|
+
"-y"
|
|
2518
|
+
];
|
|
2519
|
+
const result = await new Promise((resolveP, rejectP) => {
|
|
2520
|
+
execFile2(
|
|
2521
|
+
"npm",
|
|
2522
|
+
args,
|
|
2523
|
+
{ cwd: stageDir, shell: false, maxBuffer: 1024 * 1024, timeout: SKILLS_INSTALL_TIMEOUT_MS },
|
|
2524
|
+
(err, stdout, stderr) => {
|
|
2525
|
+
if (err) {
|
|
2526
|
+
const code = typeof err?.code === "number" ? err.code : -1;
|
|
2527
|
+
return resolveP({
|
|
2528
|
+
ok: false,
|
|
2529
|
+
retryable: code === -2 || code === 124,
|
|
2530
|
+
message: `skills CLI failed (code ${code}): ${(stderr || stdout || "").toString().trim().split("\n").slice(-5).join(" | ")}`
|
|
2531
|
+
});
|
|
2532
|
+
}
|
|
2533
|
+
resolveP({ ok: true, stdout: stdout?.toString?.() ?? "", stderr: stderr?.toString?.() ?? "" });
|
|
2534
|
+
}
|
|
2535
|
+
);
|
|
2536
|
+
});
|
|
2537
|
+
if (!result.ok) return result;
|
|
2538
|
+
const stagedDir = findStagedSkillDir(stageDir, skillName);
|
|
2539
|
+
if (!stagedDir) {
|
|
2540
|
+
return {
|
|
2541
|
+
ok: false,
|
|
2542
|
+
retryable: false,
|
|
2543
|
+
message: `skills CLI did not produce any of: ${[".opencode/skills", ".agents/skills", "skills"].map((p) => join10(stageDir, p, skillName)).join(", ")}`
|
|
2544
|
+
};
|
|
2545
|
+
}
|
|
2546
|
+
const skillMd = join10(stagedDir, "SKILL.md");
|
|
2547
|
+
if (!existsSync22(skillMd)) {
|
|
2548
|
+
return {
|
|
2549
|
+
ok: false,
|
|
2550
|
+
retryable: false,
|
|
2551
|
+
message: "skills CLI did not produce a SKILL.md"
|
|
2552
|
+
};
|
|
2553
|
+
}
|
|
2554
|
+
return {
|
|
2555
|
+
ok: true,
|
|
2556
|
+
stagedDir,
|
|
2557
|
+
source: {
|
|
2558
|
+
packageSpec: resolvedPackageSpec,
|
|
2559
|
+
skillName,
|
|
2560
|
+
cliPackage: cliPkg,
|
|
2561
|
+
registryId: `${resolvedPackageSpec}/${skillName}`,
|
|
2562
|
+
// The CLI does not currently expose a registry snapshot
|
|
2563
|
+
// hash; we record the staged directory's hash instead so
|
|
2564
|
+
// the audit tool can prove the staged bytes equal the
|
|
2565
|
+
// installed bytes.
|
|
2566
|
+
registrySnapshotHash: hashBytes(Buffer.from(result.stdout + "\n" + result.stderr, "utf8"))
|
|
2567
|
+
}
|
|
2568
|
+
};
|
|
2569
|
+
}
|
|
2570
|
+
async function hashDir(rootDir) {
|
|
2571
|
+
const out = [];
|
|
2572
|
+
await walk(rootDir, rootDir, out);
|
|
2573
|
+
return out;
|
|
2574
|
+
}
|
|
2575
|
+
async function walk(rootDir, currentDir, out) {
|
|
2576
|
+
const { readdir: readdir2 } = await import("node:fs/promises");
|
|
2577
|
+
const entries = await readdir2(currentDir, { withFileTypes: true });
|
|
2578
|
+
for (const e of entries) {
|
|
2579
|
+
const abs = join10(currentDir, e.name);
|
|
2580
|
+
if (e.isDirectory()) {
|
|
2581
|
+
if (e.name === ".git") continue;
|
|
2582
|
+
await walk(rootDir, abs, out);
|
|
2583
|
+
continue;
|
|
2584
|
+
}
|
|
2585
|
+
if (!e.isFile()) continue;
|
|
2586
|
+
const raw = await readFile14(abs);
|
|
2587
|
+
const fileStat = await stat4(abs);
|
|
2588
|
+
out.push({
|
|
2589
|
+
path: abs.slice(rootDir.length + 1).split(sep5).join("/"),
|
|
2590
|
+
sha256: createHash5("sha256").update(raw).digest("hex"),
|
|
2591
|
+
mode: fileStat.mode & 511,
|
|
2592
|
+
size: fileStat.size
|
|
2593
|
+
});
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
function hashBytes(bytes) {
|
|
2597
|
+
return createHash5("sha256").update(bytes).digest("hex");
|
|
2598
|
+
}
|
|
2599
|
+
function hashesEqual(a, b) {
|
|
2600
|
+
if (a.length !== b.length) return false;
|
|
2601
|
+
const map = new Map(a.map((f) => [f.path, f.sha256]));
|
|
2602
|
+
for (const f of b) {
|
|
2603
|
+
if (map.get(f.path) !== f.sha256) return false;
|
|
2604
|
+
}
|
|
2605
|
+
return true;
|
|
2606
|
+
}
|
|
2607
|
+
async function copyDir(srcDir, destDir) {
|
|
2608
|
+
await mkdir9(destDir, { recursive: true });
|
|
2609
|
+
const { readdir: readdir2 } = await import("node:fs/promises");
|
|
2610
|
+
const entries = await readdir2(srcDir, { withFileTypes: true });
|
|
2611
|
+
for (const e of entries) {
|
|
2612
|
+
const src = join10(srcDir, e.name);
|
|
2613
|
+
const dest = join10(destDir, e.name);
|
|
2614
|
+
if (e.isDirectory()) {
|
|
2615
|
+
if (e.name === ".git") continue;
|
|
2616
|
+
await copyDir(src, dest);
|
|
2617
|
+
} else if (e.isFile()) {
|
|
2618
|
+
const raw = await readFile14(src);
|
|
2619
|
+
await writeFile11(dest, raw, { mode: 420 });
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
var SAFE_ID_RE, SAFE_NAME_RE, SAFE_VERSION_RE, SKILLS_CLI_VERSION;
|
|
2624
|
+
var init_ship_skill_install = __esm({
|
|
2625
|
+
"src/tools/ship-skill-install.js"() {
|
|
2626
|
+
init_envelope();
|
|
2627
|
+
init_policy();
|
|
2628
|
+
init_inventory();
|
|
2629
|
+
init_worktree();
|
|
2630
|
+
init_registry();
|
|
2631
|
+
SAFE_ID_RE = /^[A-Za-z0-9._-]{1,128}$/;
|
|
2632
|
+
SAFE_NAME_RE = /^[A-Za-z0-9._/-]{1,160}$/;
|
|
2633
|
+
SAFE_VERSION_RE = /^[A-Za-z0-9._+-]{1,64}$/;
|
|
2634
|
+
SKILLS_CLI_VERSION = "1.0.4";
|
|
2635
|
+
}
|
|
2636
|
+
});
|
|
2637
|
+
|
|
1614
2638
|
// src/installer/cli-args.js
|
|
1615
2639
|
init_profile();
|
|
1616
2640
|
var USAGE = `opencode-ship <command> [options]
|
|
@@ -1725,24 +2749,25 @@ function helpText() {
|
|
|
1725
2749
|
|
|
1726
2750
|
// src/installer/commands/init.js
|
|
1727
2751
|
import { promisify as promisify2 } from "node:util";
|
|
1728
|
-
import { writeFile as
|
|
1729
|
-
import { dirname as
|
|
2752
|
+
import { writeFile as writeFile12, mkdir as mkdirAsync2 } from "node:fs/promises";
|
|
2753
|
+
import { dirname as dirname15, resolve as resolvePath } from "node:path";
|
|
1730
2754
|
|
|
1731
2755
|
// src/installer/executor.js
|
|
1732
2756
|
init_catalog();
|
|
1733
2757
|
init_version();
|
|
1734
|
-
import { existsSync as
|
|
2758
|
+
import { existsSync as existsSync14 } from "node:fs";
|
|
1735
2759
|
import { mkdir as mkdir6, readFile as readFile9, rename as rename5, unlink as unlink3, writeFile as writeFile6 } from "node:fs/promises";
|
|
1736
|
-
import { dirname as dirname8, relative as relative2, resolve as
|
|
2760
|
+
import { dirname as dirname8, relative as relative2, resolve as resolve13, sep as sep2 } from "node:path";
|
|
1737
2761
|
|
|
1738
2762
|
// src/installer/planner.js
|
|
1739
2763
|
init_catalog();
|
|
1740
2764
|
init_hash();
|
|
1741
2765
|
init_config();
|
|
1742
2766
|
init_lock();
|
|
2767
|
+
init_workflow_models();
|
|
1743
2768
|
init_json_pointer();
|
|
1744
2769
|
init_root_config();
|
|
1745
|
-
import { existsSync as
|
|
2770
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
1746
2771
|
import { readFile as readFile4, stat } from "node:fs/promises";
|
|
1747
2772
|
|
|
1748
2773
|
// src/installer/root-reconciliation.js
|
|
@@ -1750,7 +2775,7 @@ init_root_config();
|
|
|
1750
2775
|
init_json_pointer();
|
|
1751
2776
|
init_hash();
|
|
1752
2777
|
init_plan_mode_permissions();
|
|
1753
|
-
import { existsSync as
|
|
2778
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
1754
2779
|
import { join } from "node:path";
|
|
1755
2780
|
|
|
1756
2781
|
// src/installer/root-permissions.js
|
|
@@ -1760,15 +2785,30 @@ var ASK = "ask";
|
|
|
1760
2785
|
var ALLOW = "allow";
|
|
1761
2786
|
var DENY = "deny";
|
|
1762
2787
|
var SUBAGENT_DEPTH = 2;
|
|
2788
|
+
function withShipAliases(ids) {
|
|
2789
|
+
const out = [];
|
|
2790
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2791
|
+
for (const id of ids) {
|
|
2792
|
+
const extras = id.startsWith("delivery_") ? [`ship_${id.slice("delivery_".length)}`] : [];
|
|
2793
|
+
for (const candidate of [id, ...extras]) {
|
|
2794
|
+
if (seen.has(candidate)) continue;
|
|
2795
|
+
seen.add(candidate);
|
|
2796
|
+
out.push(candidate);
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
return out;
|
|
2800
|
+
}
|
|
1763
2801
|
var CONTROLLER_TASK_ALLOW = [
|
|
1764
2802
|
"ship-planner",
|
|
1765
2803
|
"ship-task-builder",
|
|
1766
2804
|
"ship-task-reviewer",
|
|
1767
2805
|
"ship-final-standards-reviewer",
|
|
1768
2806
|
"ship-final-spec-reviewer",
|
|
2807
|
+
"ship-verifier",
|
|
1769
2808
|
"delivery-verifier"
|
|
1770
2809
|
];
|
|
1771
|
-
var PUBLIC_TOOL_IDS = [
|
|
2810
|
+
var PUBLIC_TOOL_IDS = withShipAliases([
|
|
2811
|
+
"delivery_abandon",
|
|
1772
2812
|
"delivery_cleanup",
|
|
1773
2813
|
"delivery_github_read",
|
|
1774
2814
|
"delivery_inspect",
|
|
@@ -1785,6 +2825,7 @@ var PUBLIC_TOOL_IDS = [
|
|
|
1785
2825
|
"delivery_sync",
|
|
1786
2826
|
"delivery_verify",
|
|
1787
2827
|
"delivery_worktree",
|
|
2828
|
+
"ship_deliver",
|
|
1788
2829
|
"ship_final_review",
|
|
1789
2830
|
"ship_plan_approve",
|
|
1790
2831
|
"ship_plan_start",
|
|
@@ -1801,24 +2842,26 @@ var PUBLIC_TOOL_IDS = [
|
|
|
1801
2842
|
"ship_task_report",
|
|
1802
2843
|
"ship_task_review",
|
|
1803
2844
|
"ship_task_start"
|
|
1804
|
-
];
|
|
1805
|
-
var BUILD_TOOL_ALLOW = [
|
|
2845
|
+
]);
|
|
2846
|
+
var BUILD_TOOL_ALLOW = withShipAliases([
|
|
1806
2847
|
"delivery_cleanup",
|
|
1807
2848
|
"delivery_inspect",
|
|
1808
2849
|
"delivery_issue",
|
|
1809
2850
|
"delivery_pr",
|
|
1810
2851
|
"delivery_ready",
|
|
1811
2852
|
"delivery_worktree",
|
|
2853
|
+
"ship_deliver",
|
|
1812
2854
|
"ship_status",
|
|
1813
2855
|
"ship_resume"
|
|
1814
|
-
];
|
|
1815
|
-
var BUILD_TOOL_ASK = [
|
|
2856
|
+
]);
|
|
2857
|
+
var BUILD_TOOL_ASK = withShipAliases([
|
|
1816
2858
|
"ship_plan_approve",
|
|
1817
2859
|
"delivery_merge",
|
|
1818
2860
|
"delivery_issue_close",
|
|
2861
|
+
"delivery_abandon",
|
|
1819
2862
|
"ship_skill_install"
|
|
1820
|
-
];
|
|
1821
|
-
var CONTROLLER_TOOL_ALLOW = [
|
|
2863
|
+
]);
|
|
2864
|
+
var CONTROLLER_TOOL_ALLOW = withShipAliases([
|
|
1822
2865
|
"delivery_inspect",
|
|
1823
2866
|
"delivery_cleanup",
|
|
1824
2867
|
"delivery_github_read",
|
|
@@ -1842,12 +2885,13 @@ var CONTROLLER_TOOL_ALLOW = [
|
|
|
1842
2885
|
"ship_skill_install",
|
|
1843
2886
|
"ship_skill_audit",
|
|
1844
2887
|
"ship_skill_uninstall"
|
|
1845
|
-
];
|
|
1846
|
-
var CONTROLLER_TOOL_ASK = [
|
|
2888
|
+
]);
|
|
2889
|
+
var CONTROLLER_TOOL_ASK = withShipAliases([
|
|
1847
2890
|
"ship_plan_approve",
|
|
1848
2891
|
"delivery_merge",
|
|
1849
|
-
"delivery_issue_close"
|
|
1850
|
-
|
|
2892
|
+
"delivery_issue_close",
|
|
2893
|
+
"delivery_abandon"
|
|
2894
|
+
]);
|
|
1851
2895
|
var H = "git";
|
|
1852
2896
|
var RESET = "--hard";
|
|
1853
2897
|
var PUSH = "--force";
|
|
@@ -1891,6 +2935,8 @@ function rootPermissionMatrix() {
|
|
|
1891
2935
|
"ship-controller": "allow",
|
|
1892
2936
|
"general": "allow",
|
|
1893
2937
|
"plan": "deny",
|
|
2938
|
+
"ship-reviewer": "allow",
|
|
2939
|
+
"ship-verifier": "allow",
|
|
1894
2940
|
"delivery-reviewer": "allow",
|
|
1895
2941
|
"delivery-verifier": "allow"
|
|
1896
2942
|
},
|
|
@@ -2736,11 +3782,11 @@ function parseTree(text, errors = [], options = ParseOptions.DEFAULT) {
|
|
|
2736
3782
|
onValue({ type: getNodeType(value), offset, length, parent: currentParent, value });
|
|
2737
3783
|
ensurePropertyComplete(offset + length);
|
|
2738
3784
|
},
|
|
2739
|
-
onSeparator: (
|
|
3785
|
+
onSeparator: (sep6, offset, length) => {
|
|
2740
3786
|
if (currentParent.type === "property") {
|
|
2741
|
-
if (
|
|
3787
|
+
if (sep6 === ":") {
|
|
2742
3788
|
currentParent.colonOffset = offset;
|
|
2743
|
-
} else if (
|
|
3789
|
+
} else if (sep6 === ",") {
|
|
2744
3790
|
ensurePropertyComplete(offset);
|
|
2745
3791
|
}
|
|
2746
3792
|
}
|
|
@@ -3453,7 +4499,7 @@ async function planRootReconciliation(input) {
|
|
|
3453
4499
|
previousDocument: input.previousDocument
|
|
3454
4500
|
});
|
|
3455
4501
|
}
|
|
3456
|
-
const fileMissing = !
|
|
4502
|
+
const fileMissing = !existsSync8(target);
|
|
3457
4503
|
if (fileMissing && !input.forceRepair) {
|
|
3458
4504
|
if (mode === "profile-transition") {
|
|
3459
4505
|
return {
|
|
@@ -4100,13 +5146,13 @@ function mergePointerRecords(descriptors, previousRecords, result, beforeSnapsho
|
|
|
4100
5146
|
|
|
4101
5147
|
// src/installer/planner.js
|
|
4102
5148
|
async function readBytes(path) {
|
|
4103
|
-
if (!
|
|
5149
|
+
if (!existsSync9(path)) return null;
|
|
4104
5150
|
const buf = await readFile4(path);
|
|
4105
5151
|
const fileStat = await stat(path);
|
|
4106
5152
|
return { bytes: buf, hash: bytesHashString(buf.toString("utf8")), mode: fileStat.mode & 511 };
|
|
4107
5153
|
}
|
|
4108
5154
|
async function readDesiredBytes(source) {
|
|
4109
|
-
if (!source || !
|
|
5155
|
+
if (!source || !existsSync9(source)) return null;
|
|
4110
5156
|
const buf = await readFile4(source);
|
|
4111
5157
|
return { bytes: buf, hash: bytesHashString(buf.toString("utf8")) };
|
|
4112
5158
|
}
|
|
@@ -4309,7 +5355,15 @@ async function planUninstall({ repoRoot, lock }) {
|
|
|
4309
5355
|
async function planConfigSynthesis({ repoRoot, detection, lock, forceOverwrite, migrationSeed = null, models = null }) {
|
|
4310
5356
|
const existing = await loadConfig(repoRoot);
|
|
4311
5357
|
const hasModelFlags = Boolean(models && (models.planner || models.builder || models.finalReviewer));
|
|
4312
|
-
|
|
5358
|
+
const { current, history } = loadWorkflowModelDefaults();
|
|
5359
|
+
const resolved = resolveWorkflowModels({
|
|
5360
|
+
configModels: existing?.ok ? existing.value.workflow?.models ?? {} : {},
|
|
5361
|
+
lockModels: lock?.manager?.models ?? null,
|
|
5362
|
+
cliModels: models,
|
|
5363
|
+
current,
|
|
5364
|
+
history
|
|
5365
|
+
});
|
|
5366
|
+
if (existing?.ok && !forceOverwrite && resolved.changedRoles.length === 0) {
|
|
4313
5367
|
return {
|
|
4314
5368
|
kind: "noop",
|
|
4315
5369
|
op: "config",
|
|
@@ -4318,35 +5372,34 @@ async function planConfigSynthesis({ repoRoot, detection, lock, forceOverwrite,
|
|
|
4318
5372
|
currentSha: existing.sha256,
|
|
4319
5373
|
desiredSha: existing.sha256,
|
|
4320
5374
|
configValue: existing.value,
|
|
5375
|
+
modelsProvenance: resolved.provenance,
|
|
4321
5376
|
reason: "user config already present"
|
|
4322
5377
|
};
|
|
4323
5378
|
}
|
|
4324
5379
|
let desiredValue = migrationSeed ?? (existing?.ok ? structuredClone(existing.value) : renderDefaultConfig(detection));
|
|
4325
|
-
if (hasModelFlags) {
|
|
4326
|
-
desiredValue = {
|
|
4327
|
-
...desiredValue,
|
|
4328
|
-
schemaVersion: 2,
|
|
4329
|
-
profile: "engineering",
|
|
4330
|
-
workflow: {
|
|
4331
|
-
...desiredValue.workflow ?? {},
|
|
4332
|
-
models: {
|
|
4333
|
-
planner: models.planner ?? desiredValue?.workflow?.models?.planner,
|
|
4334
|
-
builder: models.builder ?? desiredValue?.workflow?.models?.builder,
|
|
4335
|
-
finalReviewer: models.finalReviewer ?? desiredValue?.workflow?.models?.finalReviewer
|
|
4336
|
-
},
|
|
4337
|
-
approval: {
|
|
4338
|
-
mirrorToIssue: true,
|
|
4339
|
-
maxFailedRounds: 3,
|
|
4340
|
-
...desiredValue?.workflow?.approval ?? {}
|
|
4341
|
-
}
|
|
4342
|
-
}
|
|
4343
|
-
};
|
|
4344
|
-
}
|
|
4345
5380
|
if (desiredValue.profile === "core") desiredValue.profile = "engineering";
|
|
5381
|
+
desiredValue.workflow = {
|
|
5382
|
+
...desiredValue.workflow ?? {},
|
|
5383
|
+
models: resolved.models,
|
|
5384
|
+
approval: {
|
|
5385
|
+
mirrorToIssue: true,
|
|
5386
|
+
maxFailedRounds: 3,
|
|
5387
|
+
...desiredValue.workflow?.approval ?? {}
|
|
5388
|
+
}
|
|
5389
|
+
};
|
|
4346
5390
|
const desiredJson = JSON.stringify(desiredValue, null, 2) + "\n";
|
|
4347
5391
|
const desiredSha = bytesHashString(desiredJson);
|
|
4348
|
-
const kind = existing?.ok
|
|
4349
|
-
|
|
5392
|
+
const kind = existing?.ok ? "update" : "create";
|
|
5393
|
+
let reason;
|
|
5394
|
+
if (!existing?.ok) {
|
|
5395
|
+
reason = migrationSeed ? "synthesising a default config from legacy adapter migration" : "synthesising a default config from detection";
|
|
5396
|
+
} else if (forceOverwrite) {
|
|
5397
|
+
reason = "user config overwritten via --force-config";
|
|
5398
|
+
} else if (hasModelFlags) {
|
|
5399
|
+
reason = "patching workflow.models from CLI model flags";
|
|
5400
|
+
} else {
|
|
5401
|
+
reason = "applying packaged workflow model defaults";
|
|
5402
|
+
}
|
|
4350
5403
|
return {
|
|
4351
5404
|
kind,
|
|
4352
5405
|
op: "config",
|
|
@@ -4356,6 +5409,7 @@ async function planConfigSynthesis({ repoRoot, detection, lock, forceOverwrite,
|
|
|
4356
5409
|
desiredSha,
|
|
4357
5410
|
bytes: Buffer.from(desiredJson, "utf8"),
|
|
4358
5411
|
configValue: desiredValue,
|
|
5412
|
+
modelsProvenance: resolved.provenance,
|
|
4359
5413
|
reason
|
|
4360
5414
|
};
|
|
4361
5415
|
}
|
|
@@ -4382,8 +5436,8 @@ init_json_pointer();
|
|
|
4382
5436
|
|
|
4383
5437
|
// src/installer/detection/project.js
|
|
4384
5438
|
import { spawnSync } from "node:child_process";
|
|
4385
|
-
import { existsSync as
|
|
4386
|
-
import { resolve as
|
|
5439
|
+
import { existsSync as existsSync10, readFileSync as readFileSync5 } from "node:fs";
|
|
5440
|
+
import { resolve as resolve8, join as join2 } from "node:path";
|
|
4387
5441
|
function runGit(cwd, args) {
|
|
4388
5442
|
const r = spawnSync("git", ["-C", cwd, ...args], {
|
|
4389
5443
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -4392,17 +5446,17 @@ function runGit(cwd, args) {
|
|
|
4392
5446
|
return { status: r.status ?? -1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
4393
5447
|
}
|
|
4394
5448
|
function detectPackageManager(repoRoot) {
|
|
4395
|
-
if (
|
|
4396
|
-
if (
|
|
4397
|
-
if (
|
|
4398
|
-
if (
|
|
5449
|
+
if (existsSync10(join2(repoRoot, "pnpm-lock.yaml"))) return "pnpm";
|
|
5450
|
+
if (existsSync10(join2(repoRoot, "yarn.lock"))) return "yarn";
|
|
5451
|
+
if (existsSync10(join2(repoRoot, "bun.lockb"))) return "bun";
|
|
5452
|
+
if (existsSync10(join2(repoRoot, "package-lock.json"))) return "npm";
|
|
4399
5453
|
return null;
|
|
4400
5454
|
}
|
|
4401
5455
|
function readPackageJson(repoRoot) {
|
|
4402
5456
|
const path = join2(repoRoot, "package.json");
|
|
4403
|
-
if (!
|
|
5457
|
+
if (!existsSync10(path)) return null;
|
|
4404
5458
|
try {
|
|
4405
|
-
return JSON.parse(
|
|
5459
|
+
return JSON.parse(readFileSync5(path, "utf8"));
|
|
4406
5460
|
} catch {
|
|
4407
5461
|
return null;
|
|
4408
5462
|
}
|
|
@@ -4477,7 +5531,7 @@ function detectOwner(repoRoot) {
|
|
|
4477
5531
|
}
|
|
4478
5532
|
function detectProject(repoRoot = process.cwd()) {
|
|
4479
5533
|
const errors = [];
|
|
4480
|
-
const cwd =
|
|
5534
|
+
const cwd = resolve8(repoRoot);
|
|
4481
5535
|
const inside = runGit(cwd, ["rev-parse", "--show-toplevel"]);
|
|
4482
5536
|
if (inside.status !== 0) {
|
|
4483
5537
|
errors.push({ kind: "not-a-git-repo", path: cwd, detail: inside.stderr.trim() });
|
|
@@ -4539,12 +5593,12 @@ import {
|
|
|
4539
5593
|
stat as stat3,
|
|
4540
5594
|
open
|
|
4541
5595
|
} from "node:fs/promises";
|
|
4542
|
-
import { existsSync as
|
|
4543
|
-
import { dirname as dirname6, resolve as
|
|
5596
|
+
import { existsSync as existsSync12 } from "node:fs";
|
|
5597
|
+
import { dirname as dirname6, resolve as resolve11, join as join5 } from "node:path";
|
|
4544
5598
|
|
|
4545
5599
|
// src/state/git-common-dir.js
|
|
4546
5600
|
import { spawn } from "node:child_process";
|
|
4547
|
-
import { resolve as
|
|
5601
|
+
import { resolve as resolve9, join as join3 } from "node:path";
|
|
4548
5602
|
var STATE_DIRNAME = "opencode-ship";
|
|
4549
5603
|
async function resolveGitCommonDir(repoRoot) {
|
|
4550
5604
|
if (typeof repoRoot !== "string" || repoRoot.length === 0) {
|
|
@@ -4577,7 +5631,7 @@ async function resolveGitCommonDir(repoRoot) {
|
|
|
4577
5631
|
reject(new Error("git rev-parse --git-common-dir returned an empty path"));
|
|
4578
5632
|
return;
|
|
4579
5633
|
}
|
|
4580
|
-
resolveP(
|
|
5634
|
+
resolveP(resolve9(repoRoot, trimmed));
|
|
4581
5635
|
});
|
|
4582
5636
|
});
|
|
4583
5637
|
}
|
|
@@ -4600,8 +5654,8 @@ import {
|
|
|
4600
5654
|
unlink,
|
|
4601
5655
|
stat as stat2
|
|
4602
5656
|
} from "node:fs/promises";
|
|
4603
|
-
import { existsSync as
|
|
4604
|
-
import { dirname as dirname5, join as join4, resolve as
|
|
5657
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
5658
|
+
import { dirname as dirname5, join as join4, resolve as resolve10 } from "node:path";
|
|
4605
5659
|
import { createHash as createHash2, randomBytes } from "node:crypto";
|
|
4606
5660
|
import { hostname as osHostname } from "node:os";
|
|
4607
5661
|
var STALE_LOCK_MS = 120 * 1e3;
|
|
@@ -4626,7 +5680,7 @@ async function atomicReplaceJson(path, value) {
|
|
|
4626
5680
|
if (typeof path !== "string" || path.length === 0) {
|
|
4627
5681
|
throw new Error("atomicReplaceJson: path must be a non-empty string");
|
|
4628
5682
|
}
|
|
4629
|
-
const target =
|
|
5683
|
+
const target = resolve10(path);
|
|
4630
5684
|
const parent = dirname5(target);
|
|
4631
5685
|
await mkdir3(parent, { recursive: true });
|
|
4632
5686
|
const tmp = `${target}.${randomToken()}.tmp`;
|
|
@@ -4650,10 +5704,10 @@ async function lockDirForRepo(repoRoot) {
|
|
|
4650
5704
|
return lockDirFromCommonDir(commonDir);
|
|
4651
5705
|
}
|
|
4652
5706
|
function transactionLockPath(lockDir) {
|
|
4653
|
-
return
|
|
5707
|
+
return resolve11(lockDir, ".txn.lock");
|
|
4654
5708
|
}
|
|
4655
5709
|
function journalPath(lockDir, txnId) {
|
|
4656
|
-
return
|
|
5710
|
+
return resolve11(lockDir, `.txn-${txnId}.journal`);
|
|
4657
5711
|
}
|
|
4658
5712
|
function backupPath(target, token) {
|
|
4659
5713
|
return `${target}.txn-${token}-backup`;
|
|
@@ -4688,7 +5742,7 @@ async function releaseLock(lockDir) {
|
|
|
4688
5742
|
}
|
|
4689
5743
|
async function liveLockOwner(lockDir) {
|
|
4690
5744
|
const path = transactionLockPath(lockDir);
|
|
4691
|
-
if (!
|
|
5745
|
+
if (!existsSync12(path)) return false;
|
|
4692
5746
|
try {
|
|
4693
5747
|
const lock = JSON.parse(await readFile6(path, "utf8"));
|
|
4694
5748
|
if (!Number.isInteger(lock?.pid) || lock.pid <= 0) return false;
|
|
@@ -4734,7 +5788,7 @@ async function clearJournal(lockDir, txnId) {
|
|
|
4734
5788
|
}
|
|
4735
5789
|
}
|
|
4736
5790
|
async function readJournal(lockDir, name) {
|
|
4737
|
-
const path =
|
|
5791
|
+
const path = resolve11(lockDir, name);
|
|
4738
5792
|
let raw;
|
|
4739
5793
|
try {
|
|
4740
5794
|
raw = await readFile6(path, "utf8");
|
|
@@ -4755,7 +5809,7 @@ async function readJournal(lockDir, name) {
|
|
|
4755
5809
|
async function isCommitted(journal) {
|
|
4756
5810
|
if (journal.committed) return true;
|
|
4757
5811
|
const marker = journal.ledger?.find((entry) => entry.commitMarker);
|
|
4758
|
-
if (!marker?.target || !marker.installedSha256 || !
|
|
5812
|
+
if (!marker?.target || !marker.installedSha256 || !existsSync12(marker.target)) return false;
|
|
4759
5813
|
try {
|
|
4760
5814
|
return bytesHashString(await readFile6(marker.target, "utf8")) === marker.installedSha256;
|
|
4761
5815
|
} catch {
|
|
@@ -4764,13 +5818,13 @@ async function isCommitted(journal) {
|
|
|
4764
5818
|
}
|
|
4765
5819
|
async function restoreEntry(entry) {
|
|
4766
5820
|
if (entry.op === "write") {
|
|
4767
|
-
if (entry.backup &&
|
|
5821
|
+
if (entry.backup && existsSync12(entry.backup)) {
|
|
4768
5822
|
await rename4(entry.backup, entry.target);
|
|
4769
|
-
} else if (entry.hadOriginal === false &&
|
|
5823
|
+
} else if (entry.hadOriginal === false && existsSync12(entry.target)) {
|
|
4770
5824
|
await unlink2(entry.target);
|
|
4771
5825
|
}
|
|
4772
|
-
if (entry.staged &&
|
|
4773
|
-
} else if (entry.op === "delete" && entry.backup &&
|
|
5826
|
+
if (entry.staged && existsSync12(entry.staged)) await unlink2(entry.staged);
|
|
5827
|
+
} else if (entry.op === "delete" && entry.backup && existsSync12(entry.backup)) {
|
|
4774
5828
|
await rename4(entry.backup, entry.target);
|
|
4775
5829
|
}
|
|
4776
5830
|
await fsyncDir2(dirname6(entry.target));
|
|
@@ -4793,11 +5847,11 @@ async function recoverJournal(lockDir, name) {
|
|
|
4793
5847
|
complete = false;
|
|
4794
5848
|
}
|
|
4795
5849
|
}
|
|
4796
|
-
if (complete) await unlink2(
|
|
5850
|
+
if (complete) await unlink2(resolve11(lockDir, name)).catch(() => null);
|
|
4797
5851
|
return { ok: complete };
|
|
4798
5852
|
}
|
|
4799
5853
|
async function recover(repoRoot, lockDir) {
|
|
4800
|
-
if (!
|
|
5854
|
+
if (!existsSync12(lockDir)) return { recovered: false, recoveredCount: 0 };
|
|
4801
5855
|
if (await liveLockOwner(lockDir)) {
|
|
4802
5856
|
return { recovered: false, recoveredCount: 0, blocked: true };
|
|
4803
5857
|
}
|
|
@@ -4831,11 +5885,11 @@ async function recover(repoRoot, lockDir) {
|
|
|
4831
5885
|
}
|
|
4832
5886
|
async function commitEntry(entry) {
|
|
4833
5887
|
let changed = false;
|
|
4834
|
-
if (entry.backup &&
|
|
5888
|
+
if (entry.backup && existsSync12(entry.backup)) {
|
|
4835
5889
|
await unlink2(entry.backup);
|
|
4836
5890
|
changed = true;
|
|
4837
5891
|
}
|
|
4838
|
-
if (entry.staged &&
|
|
5892
|
+
if (entry.staged && existsSync12(entry.staged)) {
|
|
4839
5893
|
await unlink2(entry.staged);
|
|
4840
5894
|
changed = true;
|
|
4841
5895
|
}
|
|
@@ -4865,7 +5919,7 @@ async function executePlan({ repoRoot, plan, newLockBuilder }) {
|
|
|
4865
5919
|
const backup = backupPath(op.target, token);
|
|
4866
5920
|
const staged = stagedPath(op.target, token);
|
|
4867
5921
|
if (op.kind === "delete") {
|
|
4868
|
-
if (!
|
|
5922
|
+
if (!existsSync12(op.target)) continue;
|
|
4869
5923
|
journal.entries.push({
|
|
4870
5924
|
op: "delete",
|
|
4871
5925
|
target: op.target,
|
|
@@ -4878,7 +5932,7 @@ async function executePlan({ repoRoot, plan, newLockBuilder }) {
|
|
|
4878
5932
|
await rename4(op.target, backup);
|
|
4879
5933
|
} else {
|
|
4880
5934
|
await mkdirp(dirname6(op.target));
|
|
4881
|
-
const hadOriginal =
|
|
5935
|
+
const hadOriginal = existsSync12(op.target);
|
|
4882
5936
|
journal.entries.push({
|
|
4883
5937
|
op: "write",
|
|
4884
5938
|
target: op.target,
|
|
@@ -4904,7 +5958,7 @@ async function executePlan({ repoRoot, plan, newLockBuilder }) {
|
|
|
4904
5958
|
const token = randomToken2();
|
|
4905
5959
|
const backup = backupPath(lockPathTarget, token);
|
|
4906
5960
|
const staged = stagedPath(lockPathTarget, token);
|
|
4907
|
-
const hadOriginal =
|
|
5961
|
+
const hadOriginal = existsSync12(lockPathTarget);
|
|
4908
5962
|
const finalLock = { ...lockValue, integrity: computeIntegrity(lockValue) };
|
|
4909
5963
|
const lockBytes = JSON.stringify(finalLock, null, 2) + "\n";
|
|
4910
5964
|
journal.entries.push({
|
|
@@ -4968,16 +6022,16 @@ async function rollback(lockDir, journal) {
|
|
|
4968
6022
|
init_lock();
|
|
4969
6023
|
init_config();
|
|
4970
6024
|
import { readFile as readFile7 } from "node:fs/promises";
|
|
4971
|
-
import { existsSync as
|
|
4972
|
-
import { resolve as
|
|
6025
|
+
import { existsSync as existsSync13 } from "node:fs";
|
|
6026
|
+
import { resolve as resolve12 } from "node:path";
|
|
4973
6027
|
function legacyAdapterPath(repoRoot) {
|
|
4974
|
-
return
|
|
6028
|
+
return resolve12(repoRoot, ".opencode", "delivery.json");
|
|
4975
6029
|
}
|
|
4976
6030
|
function legacyLockPath(repoRoot) {
|
|
4977
|
-
return
|
|
6031
|
+
return resolve12(repoRoot, ".opencode", "delivery.lock.json");
|
|
4978
6032
|
}
|
|
4979
6033
|
function legacyPluginPath(repoRoot) {
|
|
4980
|
-
return
|
|
6034
|
+
return resolve12(repoRoot, ".opencode", "plugin", "delivery.ts");
|
|
4981
6035
|
}
|
|
4982
6036
|
async function detectLegacyShapes(repoRoot) {
|
|
4983
6037
|
const out = {
|
|
@@ -4988,17 +6042,17 @@ async function detectLegacyShapes(repoRoot) {
|
|
|
4988
6042
|
reviewer: false,
|
|
4989
6043
|
verifier: false
|
|
4990
6044
|
};
|
|
4991
|
-
if (
|
|
4992
|
-
if (
|
|
4993
|
-
if (
|
|
4994
|
-
if (
|
|
4995
|
-
if (
|
|
4996
|
-
if (
|
|
6045
|
+
if (existsSync13(legacyAdapterPath(repoRoot))) out.adapter = true;
|
|
6046
|
+
if (existsSync13(legacyLockPath(repoRoot))) out.legacyLock = true;
|
|
6047
|
+
if (existsSync13(legacyPluginPath(repoRoot))) out.plugin = true;
|
|
6048
|
+
if (existsSync13(resolve12(repoRoot, ".opencode/plugin/opencode-ship.js"))) out.pluginOld = true;
|
|
6049
|
+
if (existsSync13(resolve12(repoRoot, ".opencode/agents/delivery-reviewer.md"))) out.reviewer = true;
|
|
6050
|
+
if (existsSync13(resolve12(repoRoot, ".opencode/agents/delivery-verifier.md"))) out.verifier = true;
|
|
4997
6051
|
return out;
|
|
4998
6052
|
}
|
|
4999
6053
|
async function readLegacyAdapter(repoRoot) {
|
|
5000
6054
|
const path = legacyAdapterPath(repoRoot);
|
|
5001
|
-
if (!
|
|
6055
|
+
if (!existsSync13(path)) return null;
|
|
5002
6056
|
try {
|
|
5003
6057
|
const raw = await readFile7(path, "utf8");
|
|
5004
6058
|
return { path, raw, value: JSON.parse(raw) };
|
|
@@ -5019,13 +6073,13 @@ async function migration({ repoRoot, lock, forceRepair, detection = null }) {
|
|
|
5019
6073
|
if (legacy && shapes.legacyLock && !lock?.manager) {
|
|
5020
6074
|
actions.push({ kind: "kept-legacy-lock", path: legacyLockPath(repoRoot) });
|
|
5021
6075
|
}
|
|
5022
|
-
if (shapes.plugin &&
|
|
6076
|
+
if (shapes.plugin && existsSync13(resolve12(repoRoot, ".opencode/plugins/opencode-ship.js"))) {
|
|
5023
6077
|
if (!forceRepair) {
|
|
5024
6078
|
actions.push({ kind: "candidate-remove-legacy-plugin", path: legacyPluginPath(repoRoot) });
|
|
5025
6079
|
}
|
|
5026
6080
|
}
|
|
5027
6081
|
if (shapes.pluginOld) {
|
|
5028
|
-
actions.push({ kind: "candidate-remove-legacy-plugin-path", path:
|
|
6082
|
+
actions.push({ kind: "candidate-remove-legacy-plugin-path", path: resolve12(repoRoot, ".opencode/plugin/opencode-ship.js") });
|
|
5029
6083
|
}
|
|
5030
6084
|
return { shapes, actions, legacyPresent: Boolean(legacy), proposedConfigSeed };
|
|
5031
6085
|
}
|
|
@@ -5073,7 +6127,7 @@ function legacyToShipConfig(legacy, detection = null) {
|
|
|
5073
6127
|
// src/installer/executor.js
|
|
5074
6128
|
init_profile();
|
|
5075
6129
|
async function readCurrentBytes(targetPath) {
|
|
5076
|
-
if (!
|
|
6130
|
+
if (!existsSync14(targetPath)) return null;
|
|
5077
6131
|
const buf = await readFile9(targetPath);
|
|
5078
6132
|
return { bytes: buf, hash: bytesHashString(buf.toString("utf8")) };
|
|
5079
6133
|
}
|
|
@@ -5164,7 +6218,7 @@ async function previewInstall({ rootPath, profile = null, replaceManaged, forceC
|
|
|
5164
6218
|
});
|
|
5165
6219
|
const planMode = null;
|
|
5166
6220
|
const rootPlan = await planRootConfigApply({ repoRoot, lock, forceRepair: Boolean(forceRootConfig), planMode });
|
|
5167
|
-
const setupPending = resolved.profile === "engineering" && !hasCompletedModels(configValue);
|
|
6221
|
+
const setupPending = resolved.profile === "engineering" && !hasCompletedModels(configPlan.configValue);
|
|
5168
6222
|
const plan = [...filePlan ?? [], ...staleFilePlan, ...migrationPlan, configPlan, rootPlan];
|
|
5169
6223
|
const conflicts = plan.filter((p) => p && p.kind === "conflict");
|
|
5170
6224
|
const summary = summarise(plan);
|
|
@@ -5180,7 +6234,8 @@ async function previewInstall({ rootPath, profile = null, replaceManaged, forceC
|
|
|
5180
6234
|
conflicts,
|
|
5181
6235
|
summary,
|
|
5182
6236
|
migrationReport,
|
|
5183
|
-
setupPending
|
|
6237
|
+
setupPending,
|
|
6238
|
+
modelsProvenance: configPlan.modelsProvenance
|
|
5184
6239
|
};
|
|
5185
6240
|
}
|
|
5186
6241
|
async function previewUninstall({ rootPath }) {
|
|
@@ -5215,7 +6270,7 @@ function summarise(plan) {
|
|
|
5215
6270
|
}
|
|
5216
6271
|
return counts;
|
|
5217
6272
|
}
|
|
5218
|
-
async function assembleLock({ repoRoot, plan, lock, configPlan, rootPlan, profile = null, models = null, fullSetupComplete = false }) {
|
|
6273
|
+
async function assembleLock({ repoRoot, plan, lock, configPlan, rootPlan, profile = null, models = null, modelsProvenance = null, fullSetupComplete = false }) {
|
|
5219
6274
|
const files = [];
|
|
5220
6275
|
const remain = lock?.files?.filter((f) => !plan.some((op) => op?.relPath === f.path)) ?? [];
|
|
5221
6276
|
for (const op of plan) {
|
|
@@ -5251,7 +6306,7 @@ async function assembleLock({ repoRoot, plan, lock, configPlan, rootPlan, profil
|
|
|
5251
6306
|
manager: {
|
|
5252
6307
|
schemaVersion: CURRENT_LOCK_SCHEMA,
|
|
5253
6308
|
name: "opencode-ship",
|
|
5254
|
-
version: "1.1.
|
|
6309
|
+
version: "1.1.9-rc.1",
|
|
5255
6310
|
templateSet: TEMPLATE_SET_ID,
|
|
5256
6311
|
profile: resolvedProfile,
|
|
5257
6312
|
appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -5261,6 +6316,7 @@ async function assembleLock({ repoRoot, plan, lock, configPlan, rootPlan, profil
|
|
|
5261
6316
|
sha256: configSha ?? lock?.manager?.config?.sha256 ?? "",
|
|
5262
6317
|
existed: Boolean(lock?.manager?.config?.existed)
|
|
5263
6318
|
},
|
|
6319
|
+
...modelsProvenance ? { models: modelsProvenance } : {},
|
|
5264
6320
|
rootDocuments: hasRootDocuments && (hasRootPlan || (lock?.manager?.rootDocuments?.length ?? 0) > 0) ? [{
|
|
5265
6321
|
path: rootPlan?.relPath ?? lock?.manager?.rootDocuments?.[0]?.path ?? "opencode.json",
|
|
5266
6322
|
format: rootPlan?.format ?? lock?.manager?.rootDocuments?.[0]?.format ?? "json",
|
|
@@ -5307,6 +6363,7 @@ async function commitInstall(preview, { json, command, fullSetupComplete = false
|
|
|
5307
6363
|
configPlan,
|
|
5308
6364
|
rootPlan,
|
|
5309
6365
|
profile: preview.profile?.profile,
|
|
6366
|
+
modelsProvenance: preview.modelsProvenance,
|
|
5310
6367
|
fullSetupComplete
|
|
5311
6368
|
});
|
|
5312
6369
|
const txPlan = await stageFiles(fileOnly, repoRoot);
|
|
@@ -5398,12 +6455,12 @@ function relativeTemplate(source) {
|
|
|
5398
6455
|
// src/installer/commands/doctor.js
|
|
5399
6456
|
init_lock();
|
|
5400
6457
|
init_config();
|
|
5401
|
-
import { existsSync as
|
|
6458
|
+
import { existsSync as existsSync16, readFileSync as readFileSync6 } from "node:fs";
|
|
5402
6459
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
5403
6460
|
init_hash();
|
|
5404
6461
|
init_catalog();
|
|
5405
6462
|
init_root_config();
|
|
5406
|
-
import { resolve as
|
|
6463
|
+
import { resolve as resolve15 } from "node:path";
|
|
5407
6464
|
|
|
5408
6465
|
// src/installer/report.js
|
|
5409
6466
|
var REPORT_VERSION = 1;
|
|
@@ -5454,6 +6511,7 @@ function renderJson({ command, plan, conflicts, summary, diagnostics = [], exitC
|
|
|
5454
6511
|
|
|
5455
6512
|
// src/installer/commands/doctor.js
|
|
5456
6513
|
init_profile();
|
|
6514
|
+
init_workflow_models();
|
|
5457
6515
|
function checkNode() {
|
|
5458
6516
|
return { name: "node>=22.6.0", ok: /^v2[2-9]/.test(process.version), detail: process.version };
|
|
5459
6517
|
}
|
|
@@ -5492,9 +6550,9 @@ function checkPackageIntegrity() {
|
|
|
5492
6550
|
function buildSourceHashIndex() {
|
|
5493
6551
|
const idx = /* @__PURE__ */ new Map();
|
|
5494
6552
|
for (const entry of CATALOG) {
|
|
5495
|
-
if (!
|
|
6553
|
+
if (!existsSync16(entry.source)) continue;
|
|
5496
6554
|
try {
|
|
5497
|
-
const buf =
|
|
6555
|
+
const buf = readFileSync6(entry.source, "utf8");
|
|
5498
6556
|
idx.set(entry.source, bytesHashString(buf));
|
|
5499
6557
|
} catch {
|
|
5500
6558
|
}
|
|
@@ -5505,13 +6563,13 @@ async function checkCatalogInstall(repoRoot, sourceHashes, profile, renderedAgen
|
|
|
5505
6563
|
const rows = [];
|
|
5506
6564
|
const scoped = profile ? filterCatalogByProfile(CATALOG, profile) : CATALOG;
|
|
5507
6565
|
for (const entry of scoped) {
|
|
5508
|
-
const target =
|
|
5509
|
-
if (!
|
|
6566
|
+
const target = resolve15(repoRoot, entry.path);
|
|
6567
|
+
if (!existsSync16(target)) {
|
|
5510
6568
|
rows.push(`${entry.id}: missing`);
|
|
5511
6569
|
continue;
|
|
5512
6570
|
}
|
|
5513
6571
|
try {
|
|
5514
|
-
const buf =
|
|
6572
|
+
const buf = readFileSync6(target, "utf8");
|
|
5515
6573
|
const actual = bytesHashString(buf);
|
|
5516
6574
|
const rendered = renderedAgentMap.get(entry.path);
|
|
5517
6575
|
const expected = rendered ? rendered.sha256 : sourceHashes.get(entry.source);
|
|
@@ -5567,12 +6625,12 @@ async function checkManagedHashes(repoRoot, validatedLock) {
|
|
|
5567
6625
|
const drift = [];
|
|
5568
6626
|
const renderedAgents = await loadRenderedAgentOverrides(repoRoot);
|
|
5569
6627
|
for (const entry of validatedLock.lock.files ?? []) {
|
|
5570
|
-
const p =
|
|
5571
|
-
if (!
|
|
6628
|
+
const p = resolve15(repoRoot, entry.path);
|
|
6629
|
+
if (!existsSync16(p)) {
|
|
5572
6630
|
drift.push(`missing:${entry.path}`);
|
|
5573
6631
|
continue;
|
|
5574
6632
|
}
|
|
5575
|
-
const buf =
|
|
6633
|
+
const buf = readFileSync6(p, "utf8");
|
|
5576
6634
|
const actual = bytesHashString(buf);
|
|
5577
6635
|
if (actual !== entry.sha256) drift.push(`drift:${entry.path}`);
|
|
5578
6636
|
}
|
|
@@ -5622,6 +6680,41 @@ async function checkRootConfig(repoRoot) {
|
|
|
5622
6680
|
detail: conflict ? `conflict on ${conflict.pointer}` : `applied=${r.applied.length}, skipped=${r.skipped.length}`
|
|
5623
6681
|
};
|
|
5624
6682
|
}
|
|
6683
|
+
async function checkWorkflowModelDefaults(repoRoot) {
|
|
6684
|
+
const cfg = await loadConfig(repoRoot);
|
|
6685
|
+
const lockResult = await readValidatedLock(repoRoot);
|
|
6686
|
+
if (!cfg?.ok) {
|
|
6687
|
+
return { name: "workflow model defaults", ok: true, detail: "no config; n/a" };
|
|
6688
|
+
}
|
|
6689
|
+
const { current, history } = loadWorkflowModelDefaults();
|
|
6690
|
+
const resolved = resolveWorkflowModels({
|
|
6691
|
+
configModels: cfg.value.workflow?.models ?? {},
|
|
6692
|
+
lockModels: lockResult.kind === "ok" ? lockResult.lock.manager?.models ?? null : null,
|
|
6693
|
+
cliModels: null,
|
|
6694
|
+
current,
|
|
6695
|
+
history
|
|
6696
|
+
});
|
|
6697
|
+
const stale = [];
|
|
6698
|
+
const parts = [];
|
|
6699
|
+
for (const role of ["planner", "builder", "finalReviewer"]) {
|
|
6700
|
+
parts.push(`${role}=${resolved.provenance[role].source}`);
|
|
6701
|
+
if (resolved.provenance[role].source === "default" && resolved.models[role] !== current[role]) {
|
|
6702
|
+
stale.push(role);
|
|
6703
|
+
}
|
|
6704
|
+
const live = cfg.value.workflow?.models?.[role];
|
|
6705
|
+
if (resolved.provenance[role].source === "default" && live && live !== current[role]) {
|
|
6706
|
+
if (!stale.includes(role)) stale.push(role);
|
|
6707
|
+
}
|
|
6708
|
+
}
|
|
6709
|
+
if (stale.length) {
|
|
6710
|
+
return {
|
|
6711
|
+
name: "workflow model defaults",
|
|
6712
|
+
ok: false,
|
|
6713
|
+
detail: `stale default; run update (${stale.join(", ")}); ${parts.join(",")}`
|
|
6714
|
+
};
|
|
6715
|
+
}
|
|
6716
|
+
return { name: "workflow model defaults", ok: true, detail: parts.join(",") };
|
|
6717
|
+
}
|
|
5625
6718
|
async function checkSetupState(repoRoot, configValue) {
|
|
5626
6719
|
const { setupComplete: setupComplete2 } = await Promise.resolve().then(() => (init_setup_state(), setup_state_exports));
|
|
5627
6720
|
const state = await setupComplete2(repoRoot, configValue);
|
|
@@ -5682,7 +6775,8 @@ async function runDoctor({ rootPath, profile, json, writeOutput = true }) {
|
|
|
5682
6775
|
await checkManagedHashes(repoRoot, validatedLock),
|
|
5683
6776
|
await checkActiveProfileFootprint(repoRoot, validatedLock, resolved.profile),
|
|
5684
6777
|
await checkRootConfig(repoRoot),
|
|
5685
|
-
await checkSetupState(repoRoot, configValue)
|
|
6778
|
+
await checkSetupState(repoRoot, configValue),
|
|
6779
|
+
await checkWorkflowModelDefaults(repoRoot)
|
|
5686
6780
|
];
|
|
5687
6781
|
const issues = checks.filter((c) => !c.ok).map((c) => `${c.name}: ${c.detail}`);
|
|
5688
6782
|
const plan = checks.map((c) => ({
|
|
@@ -5704,20 +6798,19 @@ async function runDoctor({ rootPath, profile, json, writeOutput = true }) {
|
|
|
5704
6798
|
|
|
5705
6799
|
// src/installer/commands/init.js
|
|
5706
6800
|
init_catalog();
|
|
5707
|
-
init_config();
|
|
5708
6801
|
|
|
5709
6802
|
// src/installer/setup-pending.js
|
|
5710
|
-
import { existsSync as
|
|
6803
|
+
import { existsSync as existsSync17, readFileSync as readFileSync7, unlinkSync, writeFile as writeFile7, mkdir as mkdirAsync } from "node:fs";
|
|
5711
6804
|
import { promisify } from "node:util";
|
|
5712
|
-
import { resolve as
|
|
6805
|
+
import { resolve as resolve16, dirname as dirname9 } from "node:path";
|
|
5713
6806
|
var writeFileAsync = promisify(writeFile7);
|
|
5714
6807
|
var mkdirAsyncAsync = promisify(mkdirAsync);
|
|
5715
6808
|
var REL_PATH = ".opencode/ship.setup-pending.json";
|
|
5716
6809
|
function setupPendingPath(repoRoot) {
|
|
5717
|
-
return
|
|
6810
|
+
return resolve16(repoRoot, REL_PATH);
|
|
5718
6811
|
}
|
|
5719
6812
|
function isSetupPending(repoRoot) {
|
|
5720
|
-
return
|
|
6813
|
+
return existsSync17(setupPendingPath(repoRoot));
|
|
5721
6814
|
}
|
|
5722
6815
|
async function writeSetupPending(repoRoot, payload) {
|
|
5723
6816
|
const path = setupPendingPath(repoRoot);
|
|
@@ -5726,8 +6819,161 @@ async function writeSetupPending(repoRoot, payload) {
|
|
|
5726
6819
|
}
|
|
5727
6820
|
var SETUP_PENDING_REL_PATH2 = REL_PATH;
|
|
5728
6821
|
|
|
6822
|
+
// src/skills/sync.js
|
|
6823
|
+
init_registry();
|
|
6824
|
+
init_policy();
|
|
6825
|
+
|
|
6826
|
+
// src/skills/stack-queries.js
|
|
6827
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
6828
|
+
import { join as join8 } from "node:path";
|
|
6829
|
+
var STACK_DEP_QUERIES = {
|
|
6830
|
+
react: "react",
|
|
6831
|
+
"react-dom": "react",
|
|
6832
|
+
next: "nextjs",
|
|
6833
|
+
vitest: "vitest",
|
|
6834
|
+
playwright: "playwright",
|
|
6835
|
+
tailwindcss: "tailwind",
|
|
6836
|
+
express: "express",
|
|
6837
|
+
fastify: "fastify",
|
|
6838
|
+
prisma: "prisma",
|
|
6839
|
+
"drizzle-orm": "drizzle"
|
|
6840
|
+
};
|
|
6841
|
+
function stackQueries({ packageJson, issueText } = {}) {
|
|
6842
|
+
const found = [];
|
|
6843
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6844
|
+
const add = (q) => {
|
|
6845
|
+
if (!q || seen.has(q) || found.length >= 5) return;
|
|
6846
|
+
seen.add(q);
|
|
6847
|
+
found.push(q);
|
|
6848
|
+
};
|
|
6849
|
+
const deps = {
|
|
6850
|
+
...packageJson?.dependencies ?? {},
|
|
6851
|
+
...packageJson?.devDependencies ?? {}
|
|
6852
|
+
};
|
|
6853
|
+
for (const name of Object.keys(deps)) {
|
|
6854
|
+
add(STACK_DEP_QUERIES[name]);
|
|
6855
|
+
}
|
|
6856
|
+
const text = String(issueText ?? "");
|
|
6857
|
+
if (/playwright/i.test(text)) add("playwright");
|
|
6858
|
+
if (/\breact\b/i.test(text)) add("react");
|
|
6859
|
+
if (/nextjs|next\.js|\bnext\b/i.test(text)) add("nextjs");
|
|
6860
|
+
if (/vitest|testing library/i.test(text)) add("vitest");
|
|
6861
|
+
if (/tailwind/i.test(text)) add("tailwind");
|
|
6862
|
+
return found;
|
|
6863
|
+
}
|
|
6864
|
+
function readPackageJson2(repoRoot) {
|
|
6865
|
+
try {
|
|
6866
|
+
const raw = readFileSync9(join8(repoRoot, "package.json"), "utf8");
|
|
6867
|
+
const parsed = JSON.parse(raw);
|
|
6868
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
6869
|
+
return parsed;
|
|
6870
|
+
} catch {
|
|
6871
|
+
return null;
|
|
6872
|
+
}
|
|
6873
|
+
}
|
|
6874
|
+
|
|
6875
|
+
// src/skills/sync.js
|
|
6876
|
+
var MANAGED_SKILL_NAMES = /* @__PURE__ */ new Set([
|
|
6877
|
+
"ship-workflow",
|
|
6878
|
+
"delivery-workflow",
|
|
6879
|
+
"planning-research-checkpoint",
|
|
6880
|
+
"skill-discovery",
|
|
6881
|
+
"setup-ship-workflow"
|
|
6882
|
+
]);
|
|
6883
|
+
async function syncSkills({
|
|
6884
|
+
repoRoot,
|
|
6885
|
+
mode: _mode,
|
|
6886
|
+
issueText = "",
|
|
6887
|
+
listSkillsFn,
|
|
6888
|
+
installFn,
|
|
6889
|
+
policy
|
|
6890
|
+
} = {}) {
|
|
6891
|
+
const queries = stackQueries({
|
|
6892
|
+
packageJson: readPackageJson2(repoRoot),
|
|
6893
|
+
issueText
|
|
6894
|
+
});
|
|
6895
|
+
const installed = [];
|
|
6896
|
+
const skippedUntrusted = [];
|
|
6897
|
+
const skippedPolicy = [];
|
|
6898
|
+
const errors = [];
|
|
6899
|
+
let registryUnavailable = false;
|
|
6900
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6901
|
+
const resolvedPolicy = policy ?? await readPolicy(repoRoot);
|
|
6902
|
+
const listFn = listSkillsFn ?? listSkills;
|
|
6903
|
+
const cap = resolvedPolicy.maxTrustedPerRun ?? 5;
|
|
6904
|
+
for (const query of queries) {
|
|
6905
|
+
let result;
|
|
6906
|
+
try {
|
|
6907
|
+
result = await listFn({ repoRoot, query });
|
|
6908
|
+
} catch (err) {
|
|
6909
|
+
registryUnavailable = true;
|
|
6910
|
+
errors.push(String(err?.message ?? err));
|
|
6911
|
+
continue;
|
|
6912
|
+
}
|
|
6913
|
+
if (!result?.ok) {
|
|
6914
|
+
registryUnavailable = true;
|
|
6915
|
+
errors.push(result?.error?.kind ?? "registry-unavailable");
|
|
6916
|
+
continue;
|
|
6917
|
+
}
|
|
6918
|
+
for (const candidate of result.candidates ?? []) {
|
|
6919
|
+
const skillName = candidate.skill;
|
|
6920
|
+
const pkg = candidate.package;
|
|
6921
|
+
if (!skillName || seen.has(skillName)) continue;
|
|
6922
|
+
if (MANAGED_SKILL_NAMES.has(skillName)) {
|
|
6923
|
+
skippedPolicy.push({ package: pkg, skillName, reason: "managed-skill" });
|
|
6924
|
+
seen.add(skillName);
|
|
6925
|
+
continue;
|
|
6926
|
+
}
|
|
6927
|
+
const decision = isAutoInstallable(candidate, resolvedPolicy);
|
|
6928
|
+
if (!decision.ok) {
|
|
6929
|
+
const entry = { package: pkg, skillName, reason: decision.reason };
|
|
6930
|
+
if (decision.reason === "untrusted-owner") skippedUntrusted.push(entry);
|
|
6931
|
+
else skippedPolicy.push(entry);
|
|
6932
|
+
seen.add(skillName);
|
|
6933
|
+
continue;
|
|
6934
|
+
}
|
|
6935
|
+
if (installed.length >= cap) {
|
|
6936
|
+
skippedPolicy.push({ package: pkg, skillName, reason: "max-per-run" });
|
|
6937
|
+
seen.add(skillName);
|
|
6938
|
+
continue;
|
|
6939
|
+
}
|
|
6940
|
+
if (typeof installFn !== "function") {
|
|
6941
|
+
errors.push(`installFn missing for ${skillName}`);
|
|
6942
|
+
continue;
|
|
6943
|
+
}
|
|
6944
|
+
let outcome;
|
|
6945
|
+
try {
|
|
6946
|
+
outcome = await installFn({ package: pkg, skillName, version: candidate.version });
|
|
6947
|
+
} catch (err) {
|
|
6948
|
+
errors.push(String(err?.message ?? err));
|
|
6949
|
+
continue;
|
|
6950
|
+
}
|
|
6951
|
+
if (outcome?.ok) {
|
|
6952
|
+
installed.push({ package: pkg, skillName });
|
|
6953
|
+
seen.add(skillName);
|
|
6954
|
+
continue;
|
|
6955
|
+
}
|
|
6956
|
+
const message = String(outcome?.message ?? "");
|
|
6957
|
+
if (/already exists/i.test(message)) {
|
|
6958
|
+
skippedPolicy.push({ package: pkg, skillName, reason: "destination-exists" });
|
|
6959
|
+
seen.add(skillName);
|
|
6960
|
+
continue;
|
|
6961
|
+
}
|
|
6962
|
+
errors.push(message || `install failed: ${skillName}`);
|
|
6963
|
+
}
|
|
6964
|
+
}
|
|
6965
|
+
return {
|
|
6966
|
+
queries,
|
|
6967
|
+
installed,
|
|
6968
|
+
skippedUntrusted,
|
|
6969
|
+
skippedPolicy,
|
|
6970
|
+
errors,
|
|
6971
|
+
registryUnavailable
|
|
6972
|
+
};
|
|
6973
|
+
}
|
|
6974
|
+
|
|
5729
6975
|
// src/installer/commands/init.js
|
|
5730
|
-
var writeFileAsync2 = promisify2(
|
|
6976
|
+
var writeFileAsync2 = promisify2(writeFile12);
|
|
5731
6977
|
var mkdirAsyncAsync2 = promisify2(mkdirAsync2);
|
|
5732
6978
|
async function runInit(options) {
|
|
5733
6979
|
try {
|
|
@@ -5771,6 +7017,28 @@ async function runInit(options) {
|
|
|
5771
7017
|
if (exitCode === 4) return emitFailure(4, committed?.diagnostics?.[0] ?? "transaction failure", options.json, "init");
|
|
5772
7018
|
return emitFailure(exitCode, committed?.diagnostics?.[0] ?? "unknown", options.json, "init");
|
|
5773
7019
|
}
|
|
7020
|
+
let skillsReport = { installed: [], skippedUntrusted: [], skippedPolicy: [], registryUnavailable: false, errors: [] };
|
|
7021
|
+
try {
|
|
7022
|
+
const syncFn = options.syncSkills ?? syncSkills;
|
|
7023
|
+
skillsReport = await syncFn({
|
|
7024
|
+
repoRoot: preview.repoRoot,
|
|
7025
|
+
mode: "init",
|
|
7026
|
+
installFn: async ({ package: pkg, skillName, version }) => {
|
|
7027
|
+
const { createSkillInstallTool: createSkillInstallTool2 } = await Promise.resolve().then(() => (init_ship_skill_install(), ship_skill_install_exports));
|
|
7028
|
+
const tool = createSkillInstallTool2({ repoRoot: preview.repoRoot, config: { value: { skills: [] } } });
|
|
7029
|
+
return tool({ package: pkg, skillName, version });
|
|
7030
|
+
}
|
|
7031
|
+
});
|
|
7032
|
+
} catch (err) {
|
|
7033
|
+
skillsReport = {
|
|
7034
|
+
installed: [],
|
|
7035
|
+
skippedUntrusted: [],
|
|
7036
|
+
skippedPolicy: [],
|
|
7037
|
+
registryUnavailable: true,
|
|
7038
|
+
errors: [String(err?.message ?? err)]
|
|
7039
|
+
};
|
|
7040
|
+
}
|
|
7041
|
+
committed.extra = { ...committed.extra ?? {}, skills: skillsReport };
|
|
5774
7042
|
const doctor = await runDoctor({
|
|
5775
7043
|
rootPath: options.rootPath ?? null,
|
|
5776
7044
|
profile: options.profile ?? null,
|
|
@@ -5789,7 +7057,7 @@ async function runInit(options) {
|
|
|
5789
7057
|
if (setupPending && preview.repoRoot) {
|
|
5790
7058
|
await writeSetupPending(preview.repoRoot, {
|
|
5791
7059
|
profile: preview.profile?.profile ?? "engineering",
|
|
5792
|
-
reason: "
|
|
7060
|
+
reason: "docs/AGENTS.md setup incomplete; run /setup-ship-workflow",
|
|
5793
7061
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5794
7062
|
});
|
|
5795
7063
|
}
|
|
@@ -5814,19 +7082,38 @@ async function runInit(options) {
|
|
|
5814
7082
|
prefix: "opencode-ship",
|
|
5815
7083
|
exitCode,
|
|
5816
7084
|
doctorIssues: doctor.issues,
|
|
5817
|
-
setupPending
|
|
7085
|
+
setupPending,
|
|
7086
|
+
skillsReport
|
|
5818
7087
|
});
|
|
5819
7088
|
}
|
|
5820
7089
|
process.exitCode = exitCode;
|
|
5821
|
-
return { ok: exitCode === 0, exitCode, setupPending };
|
|
7090
|
+
return { ok: exitCode === 0, exitCode, setupPending, extra: committed.extra };
|
|
5822
7091
|
}
|
|
5823
|
-
function printHumanResult({ prefix, exitCode, doctorIssues, setupPending }) {
|
|
7092
|
+
function printHumanResult({ prefix, exitCode, doctorIssues, setupPending, skillsReport }) {
|
|
5824
7093
|
const lines = [];
|
|
5825
7094
|
if (exitCode === 0) {
|
|
5826
7095
|
lines.push(`${prefix}: installed; doctor OK`);
|
|
5827
7096
|
} else {
|
|
5828
7097
|
lines.push(`${prefix}: installed with warnings`);
|
|
5829
7098
|
}
|
|
7099
|
+
const installed = Array.isArray(skillsReport?.installed) ? skillsReport.installed : [];
|
|
7100
|
+
if (installed.length > 0) {
|
|
7101
|
+
lines.push(`Skill discovery: ${installed.length} installed (${installed.map((i) => i.skillName).join(", ")}).`);
|
|
7102
|
+
} else {
|
|
7103
|
+
lines.push("Skill discovery: 0 installed, continuing with catalog skills only.");
|
|
7104
|
+
}
|
|
7105
|
+
const untrusted = Array.isArray(skillsReport?.skippedUntrusted) ? skillsReport.skippedUntrusted : [];
|
|
7106
|
+
if (untrusted.length > 0) {
|
|
7107
|
+
lines.push(`Untrusted skill candidates: ${untrusted.map((s) => `${s.package}/${s.skillName}`).join(", ")}`);
|
|
7108
|
+
}
|
|
7109
|
+
if (skillsReport?.registryUnavailable) {
|
|
7110
|
+
lines.push("Skill registry: unavailable; trusted skill discovery skipped this run.");
|
|
7111
|
+
}
|
|
7112
|
+
const errors = Array.isArray(skillsReport?.errors) ? skillsReport.errors : [];
|
|
7113
|
+
if (errors.length > 0) {
|
|
7114
|
+
lines.push("Skill discovery errors:");
|
|
7115
|
+
for (const err of errors) lines.push(` - ${err}`);
|
|
7116
|
+
}
|
|
5830
7117
|
if (Array.isArray(doctorIssues) && doctorIssues.length > 0) {
|
|
5831
7118
|
lines.push("");
|
|
5832
7119
|
lines.push("Doctor reported:");
|
|
@@ -6000,15 +7287,11 @@ async function runUpdate(options) {
|
|
|
6000
7287
|
}
|
|
6001
7288
|
throw e;
|
|
6002
7289
|
}
|
|
6003
|
-
const hasExplicitModels = options.models && (options.models.planner || options.models.builder || options.models.finalReviewer);
|
|
6004
7290
|
const preview = await previewInstall({
|
|
6005
7291
|
rootPath: options.rootPath,
|
|
6006
7292
|
profile: options.profile ?? null,
|
|
6007
7293
|
replaceManaged: options.replaceManaged,
|
|
6008
|
-
|
|
6009
|
-
// populating workflow.models. We must rewrite the config in
|
|
6010
|
-
// that case even when the existing config is otherwise valid.
|
|
6011
|
-
forceConfig: Boolean(options.forceConfig || hasExplicitModels),
|
|
7294
|
+
forceConfig: Boolean(options.forceConfig),
|
|
6012
7295
|
forceRootConfig: options.forceRootConfig,
|
|
6013
7296
|
models: options.models ?? null
|
|
6014
7297
|
});
|
|
@@ -6025,6 +7308,30 @@ async function runUpdate(options) {
|
|
|
6025
7308
|
return emitFailure2(3, "modified managed files; rerun with --replace-managed", options.json, "update");
|
|
6026
7309
|
}
|
|
6027
7310
|
const committed = await commitInstall(preview, { json: options.json, command: "update" });
|
|
7311
|
+
if (committed.extra?.exitCode === 0 && preview.repoRoot) {
|
|
7312
|
+
let skillsReport = { installed: [], skippedUntrusted: [], skippedPolicy: [], registryUnavailable: false, errors: [] };
|
|
7313
|
+
try {
|
|
7314
|
+
const syncFn = options.syncSkills ?? syncSkills;
|
|
7315
|
+
skillsReport = await syncFn({
|
|
7316
|
+
repoRoot: preview.repoRoot,
|
|
7317
|
+
mode: "deliver",
|
|
7318
|
+
installFn: async ({ package: pkg, skillName, version }) => {
|
|
7319
|
+
const { createSkillInstallTool: createSkillInstallTool2 } = await Promise.resolve().then(() => (init_ship_skill_install(), ship_skill_install_exports));
|
|
7320
|
+
const tool = createSkillInstallTool2({ repoRoot: preview.repoRoot, config: { value: { skills: [] } } });
|
|
7321
|
+
return tool({ package: pkg, skillName, version });
|
|
7322
|
+
}
|
|
7323
|
+
});
|
|
7324
|
+
} catch (err) {
|
|
7325
|
+
skillsReport = {
|
|
7326
|
+
installed: [],
|
|
7327
|
+
skippedUntrusted: [],
|
|
7328
|
+
skippedPolicy: [],
|
|
7329
|
+
registryUnavailable: true,
|
|
7330
|
+
errors: [String(err?.message ?? err)]
|
|
7331
|
+
};
|
|
7332
|
+
}
|
|
7333
|
+
committed.extra = { ...committed.extra ?? {}, skills: skillsReport };
|
|
7334
|
+
}
|
|
6028
7335
|
if (options.json) {
|
|
6029
7336
|
process.stdout.write(JSON.stringify({
|
|
6030
7337
|
reportVersion: 1,
|
|
@@ -6068,11 +7375,11 @@ function emitFailure2(code, message, json, command) {
|
|
|
6068
7375
|
// src/installer/commands/setup-complete.js
|
|
6069
7376
|
init_config();
|
|
6070
7377
|
init_setup_state();
|
|
6071
|
-
import { existsSync as
|
|
6072
|
-
import { resolve as
|
|
7378
|
+
import { existsSync as existsSync23 } from "node:fs";
|
|
7379
|
+
import { resolve as resolve23 } from "node:path";
|
|
6073
7380
|
async function runSetupComplete(options) {
|
|
6074
7381
|
const repoRoot = options.rootPath ?? process.cwd();
|
|
6075
|
-
if (!
|
|
7382
|
+
if (!existsSync23(resolve23(repoRoot, ".git"))) {
|
|
6076
7383
|
return emitFailure3(2, "not a git repository", options.json);
|
|
6077
7384
|
}
|
|
6078
7385
|
const config = await loadConfig(repoRoot);
|