opencode-ship 1.1.8 → 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 +21 -0
- package/README.md +10 -10
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/assets/agents/ship-controller.md +50 -28
- 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 +21 -9
- 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/{delivery-workflow → ship-workflow}/SKILL.md +37 -11
- 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 +1476 -174
- package/dist/core.js +99 -7
- package/dist/plugin.js +18510 -17921
- 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 +46 -5
- package/tests/plugin/plugin-load.test.mjs +27 -5
- package/vendor/sources.json +21 -21
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,29 @@ 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([
|
|
1772
2811
|
"delivery_abandon",
|
|
1773
2812
|
"delivery_cleanup",
|
|
1774
2813
|
"delivery_github_read",
|
|
@@ -1803,8 +2842,8 @@ var PUBLIC_TOOL_IDS = [
|
|
|
1803
2842
|
"ship_task_report",
|
|
1804
2843
|
"ship_task_review",
|
|
1805
2844
|
"ship_task_start"
|
|
1806
|
-
];
|
|
1807
|
-
var BUILD_TOOL_ALLOW = [
|
|
2845
|
+
]);
|
|
2846
|
+
var BUILD_TOOL_ALLOW = withShipAliases([
|
|
1808
2847
|
"delivery_cleanup",
|
|
1809
2848
|
"delivery_inspect",
|
|
1810
2849
|
"delivery_issue",
|
|
@@ -1814,15 +2853,15 @@ var BUILD_TOOL_ALLOW = [
|
|
|
1814
2853
|
"ship_deliver",
|
|
1815
2854
|
"ship_status",
|
|
1816
2855
|
"ship_resume"
|
|
1817
|
-
];
|
|
1818
|
-
var BUILD_TOOL_ASK = [
|
|
2856
|
+
]);
|
|
2857
|
+
var BUILD_TOOL_ASK = withShipAliases([
|
|
1819
2858
|
"ship_plan_approve",
|
|
1820
2859
|
"delivery_merge",
|
|
1821
2860
|
"delivery_issue_close",
|
|
1822
2861
|
"delivery_abandon",
|
|
1823
2862
|
"ship_skill_install"
|
|
1824
|
-
];
|
|
1825
|
-
var CONTROLLER_TOOL_ALLOW = [
|
|
2863
|
+
]);
|
|
2864
|
+
var CONTROLLER_TOOL_ALLOW = withShipAliases([
|
|
1826
2865
|
"delivery_inspect",
|
|
1827
2866
|
"delivery_cleanup",
|
|
1828
2867
|
"delivery_github_read",
|
|
@@ -1846,13 +2885,13 @@ var CONTROLLER_TOOL_ALLOW = [
|
|
|
1846
2885
|
"ship_skill_install",
|
|
1847
2886
|
"ship_skill_audit",
|
|
1848
2887
|
"ship_skill_uninstall"
|
|
1849
|
-
];
|
|
1850
|
-
var CONTROLLER_TOOL_ASK = [
|
|
2888
|
+
]);
|
|
2889
|
+
var CONTROLLER_TOOL_ASK = withShipAliases([
|
|
1851
2890
|
"ship_plan_approve",
|
|
1852
2891
|
"delivery_merge",
|
|
1853
2892
|
"delivery_issue_close",
|
|
1854
2893
|
"delivery_abandon"
|
|
1855
|
-
];
|
|
2894
|
+
]);
|
|
1856
2895
|
var H = "git";
|
|
1857
2896
|
var RESET = "--hard";
|
|
1858
2897
|
var PUSH = "--force";
|
|
@@ -1896,6 +2935,8 @@ function rootPermissionMatrix() {
|
|
|
1896
2935
|
"ship-controller": "allow",
|
|
1897
2936
|
"general": "allow",
|
|
1898
2937
|
"plan": "deny",
|
|
2938
|
+
"ship-reviewer": "allow",
|
|
2939
|
+
"ship-verifier": "allow",
|
|
1899
2940
|
"delivery-reviewer": "allow",
|
|
1900
2941
|
"delivery-verifier": "allow"
|
|
1901
2942
|
},
|
|
@@ -2741,11 +3782,11 @@ function parseTree(text, errors = [], options = ParseOptions.DEFAULT) {
|
|
|
2741
3782
|
onValue({ type: getNodeType(value), offset, length, parent: currentParent, value });
|
|
2742
3783
|
ensurePropertyComplete(offset + length);
|
|
2743
3784
|
},
|
|
2744
|
-
onSeparator: (
|
|
3785
|
+
onSeparator: (sep6, offset, length) => {
|
|
2745
3786
|
if (currentParent.type === "property") {
|
|
2746
|
-
if (
|
|
3787
|
+
if (sep6 === ":") {
|
|
2747
3788
|
currentParent.colonOffset = offset;
|
|
2748
|
-
} else if (
|
|
3789
|
+
} else if (sep6 === ",") {
|
|
2749
3790
|
ensurePropertyComplete(offset);
|
|
2750
3791
|
}
|
|
2751
3792
|
}
|
|
@@ -3458,7 +4499,7 @@ async function planRootReconciliation(input) {
|
|
|
3458
4499
|
previousDocument: input.previousDocument
|
|
3459
4500
|
});
|
|
3460
4501
|
}
|
|
3461
|
-
const fileMissing = !
|
|
4502
|
+
const fileMissing = !existsSync8(target);
|
|
3462
4503
|
if (fileMissing && !input.forceRepair) {
|
|
3463
4504
|
if (mode === "profile-transition") {
|
|
3464
4505
|
return {
|
|
@@ -4105,13 +5146,13 @@ function mergePointerRecords(descriptors, previousRecords, result, beforeSnapsho
|
|
|
4105
5146
|
|
|
4106
5147
|
// src/installer/planner.js
|
|
4107
5148
|
async function readBytes(path) {
|
|
4108
|
-
if (!
|
|
5149
|
+
if (!existsSync9(path)) return null;
|
|
4109
5150
|
const buf = await readFile4(path);
|
|
4110
5151
|
const fileStat = await stat(path);
|
|
4111
5152
|
return { bytes: buf, hash: bytesHashString(buf.toString("utf8")), mode: fileStat.mode & 511 };
|
|
4112
5153
|
}
|
|
4113
5154
|
async function readDesiredBytes(source) {
|
|
4114
|
-
if (!source || !
|
|
5155
|
+
if (!source || !existsSync9(source)) return null;
|
|
4115
5156
|
const buf = await readFile4(source);
|
|
4116
5157
|
return { bytes: buf, hash: bytesHashString(buf.toString("utf8")) };
|
|
4117
5158
|
}
|
|
@@ -4314,7 +5355,15 @@ async function planUninstall({ repoRoot, lock }) {
|
|
|
4314
5355
|
async function planConfigSynthesis({ repoRoot, detection, lock, forceOverwrite, migrationSeed = null, models = null }) {
|
|
4315
5356
|
const existing = await loadConfig(repoRoot);
|
|
4316
5357
|
const hasModelFlags = Boolean(models && (models.planner || models.builder || models.finalReviewer));
|
|
4317
|
-
|
|
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) {
|
|
4318
5367
|
return {
|
|
4319
5368
|
kind: "noop",
|
|
4320
5369
|
op: "config",
|
|
@@ -4323,35 +5372,34 @@ async function planConfigSynthesis({ repoRoot, detection, lock, forceOverwrite,
|
|
|
4323
5372
|
currentSha: existing.sha256,
|
|
4324
5373
|
desiredSha: existing.sha256,
|
|
4325
5374
|
configValue: existing.value,
|
|
5375
|
+
modelsProvenance: resolved.provenance,
|
|
4326
5376
|
reason: "user config already present"
|
|
4327
5377
|
};
|
|
4328
5378
|
}
|
|
4329
5379
|
let desiredValue = migrationSeed ?? (existing?.ok ? structuredClone(existing.value) : renderDefaultConfig(detection));
|
|
4330
|
-
if (hasModelFlags) {
|
|
4331
|
-
desiredValue = {
|
|
4332
|
-
...desiredValue,
|
|
4333
|
-
schemaVersion: 2,
|
|
4334
|
-
profile: "engineering",
|
|
4335
|
-
workflow: {
|
|
4336
|
-
...desiredValue.workflow ?? {},
|
|
4337
|
-
models: {
|
|
4338
|
-
planner: models.planner ?? desiredValue?.workflow?.models?.planner,
|
|
4339
|
-
builder: models.builder ?? desiredValue?.workflow?.models?.builder,
|
|
4340
|
-
finalReviewer: models.finalReviewer ?? desiredValue?.workflow?.models?.finalReviewer
|
|
4341
|
-
},
|
|
4342
|
-
approval: {
|
|
4343
|
-
mirrorToIssue: true,
|
|
4344
|
-
maxFailedRounds: 3,
|
|
4345
|
-
...desiredValue?.workflow?.approval ?? {}
|
|
4346
|
-
}
|
|
4347
|
-
}
|
|
4348
|
-
};
|
|
4349
|
-
}
|
|
4350
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
|
+
};
|
|
4351
5390
|
const desiredJson = JSON.stringify(desiredValue, null, 2) + "\n";
|
|
4352
5391
|
const desiredSha = bytesHashString(desiredJson);
|
|
4353
|
-
const kind = existing?.ok
|
|
4354
|
-
|
|
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
|
+
}
|
|
4355
5403
|
return {
|
|
4356
5404
|
kind,
|
|
4357
5405
|
op: "config",
|
|
@@ -4361,6 +5409,7 @@ async function planConfigSynthesis({ repoRoot, detection, lock, forceOverwrite,
|
|
|
4361
5409
|
desiredSha,
|
|
4362
5410
|
bytes: Buffer.from(desiredJson, "utf8"),
|
|
4363
5411
|
configValue: desiredValue,
|
|
5412
|
+
modelsProvenance: resolved.provenance,
|
|
4364
5413
|
reason
|
|
4365
5414
|
};
|
|
4366
5415
|
}
|
|
@@ -4387,8 +5436,8 @@ init_json_pointer();
|
|
|
4387
5436
|
|
|
4388
5437
|
// src/installer/detection/project.js
|
|
4389
5438
|
import { spawnSync } from "node:child_process";
|
|
4390
|
-
import { existsSync as
|
|
4391
|
-
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";
|
|
4392
5441
|
function runGit(cwd, args) {
|
|
4393
5442
|
const r = spawnSync("git", ["-C", cwd, ...args], {
|
|
4394
5443
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -4397,17 +5446,17 @@ function runGit(cwd, args) {
|
|
|
4397
5446
|
return { status: r.status ?? -1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
4398
5447
|
}
|
|
4399
5448
|
function detectPackageManager(repoRoot) {
|
|
4400
|
-
if (
|
|
4401
|
-
if (
|
|
4402
|
-
if (
|
|
4403
|
-
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";
|
|
4404
5453
|
return null;
|
|
4405
5454
|
}
|
|
4406
5455
|
function readPackageJson(repoRoot) {
|
|
4407
5456
|
const path = join2(repoRoot, "package.json");
|
|
4408
|
-
if (!
|
|
5457
|
+
if (!existsSync10(path)) return null;
|
|
4409
5458
|
try {
|
|
4410
|
-
return JSON.parse(
|
|
5459
|
+
return JSON.parse(readFileSync5(path, "utf8"));
|
|
4411
5460
|
} catch {
|
|
4412
5461
|
return null;
|
|
4413
5462
|
}
|
|
@@ -4482,7 +5531,7 @@ function detectOwner(repoRoot) {
|
|
|
4482
5531
|
}
|
|
4483
5532
|
function detectProject(repoRoot = process.cwd()) {
|
|
4484
5533
|
const errors = [];
|
|
4485
|
-
const cwd =
|
|
5534
|
+
const cwd = resolve8(repoRoot);
|
|
4486
5535
|
const inside = runGit(cwd, ["rev-parse", "--show-toplevel"]);
|
|
4487
5536
|
if (inside.status !== 0) {
|
|
4488
5537
|
errors.push({ kind: "not-a-git-repo", path: cwd, detail: inside.stderr.trim() });
|
|
@@ -4544,12 +5593,12 @@ import {
|
|
|
4544
5593
|
stat as stat3,
|
|
4545
5594
|
open
|
|
4546
5595
|
} from "node:fs/promises";
|
|
4547
|
-
import { existsSync as
|
|
4548
|
-
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";
|
|
4549
5598
|
|
|
4550
5599
|
// src/state/git-common-dir.js
|
|
4551
5600
|
import { spawn } from "node:child_process";
|
|
4552
|
-
import { resolve as
|
|
5601
|
+
import { resolve as resolve9, join as join3 } from "node:path";
|
|
4553
5602
|
var STATE_DIRNAME = "opencode-ship";
|
|
4554
5603
|
async function resolveGitCommonDir(repoRoot) {
|
|
4555
5604
|
if (typeof repoRoot !== "string" || repoRoot.length === 0) {
|
|
@@ -4582,7 +5631,7 @@ async function resolveGitCommonDir(repoRoot) {
|
|
|
4582
5631
|
reject(new Error("git rev-parse --git-common-dir returned an empty path"));
|
|
4583
5632
|
return;
|
|
4584
5633
|
}
|
|
4585
|
-
resolveP(
|
|
5634
|
+
resolveP(resolve9(repoRoot, trimmed));
|
|
4586
5635
|
});
|
|
4587
5636
|
});
|
|
4588
5637
|
}
|
|
@@ -4605,8 +5654,8 @@ import {
|
|
|
4605
5654
|
unlink,
|
|
4606
5655
|
stat as stat2
|
|
4607
5656
|
} from "node:fs/promises";
|
|
4608
|
-
import { existsSync as
|
|
4609
|
-
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";
|
|
4610
5659
|
import { createHash as createHash2, randomBytes } from "node:crypto";
|
|
4611
5660
|
import { hostname as osHostname } from "node:os";
|
|
4612
5661
|
var STALE_LOCK_MS = 120 * 1e3;
|
|
@@ -4631,7 +5680,7 @@ async function atomicReplaceJson(path, value) {
|
|
|
4631
5680
|
if (typeof path !== "string" || path.length === 0) {
|
|
4632
5681
|
throw new Error("atomicReplaceJson: path must be a non-empty string");
|
|
4633
5682
|
}
|
|
4634
|
-
const target =
|
|
5683
|
+
const target = resolve10(path);
|
|
4635
5684
|
const parent = dirname5(target);
|
|
4636
5685
|
await mkdir3(parent, { recursive: true });
|
|
4637
5686
|
const tmp = `${target}.${randomToken()}.tmp`;
|
|
@@ -4655,10 +5704,10 @@ async function lockDirForRepo(repoRoot) {
|
|
|
4655
5704
|
return lockDirFromCommonDir(commonDir);
|
|
4656
5705
|
}
|
|
4657
5706
|
function transactionLockPath(lockDir) {
|
|
4658
|
-
return
|
|
5707
|
+
return resolve11(lockDir, ".txn.lock");
|
|
4659
5708
|
}
|
|
4660
5709
|
function journalPath(lockDir, txnId) {
|
|
4661
|
-
return
|
|
5710
|
+
return resolve11(lockDir, `.txn-${txnId}.journal`);
|
|
4662
5711
|
}
|
|
4663
5712
|
function backupPath(target, token) {
|
|
4664
5713
|
return `${target}.txn-${token}-backup`;
|
|
@@ -4693,7 +5742,7 @@ async function releaseLock(lockDir) {
|
|
|
4693
5742
|
}
|
|
4694
5743
|
async function liveLockOwner(lockDir) {
|
|
4695
5744
|
const path = transactionLockPath(lockDir);
|
|
4696
|
-
if (!
|
|
5745
|
+
if (!existsSync12(path)) return false;
|
|
4697
5746
|
try {
|
|
4698
5747
|
const lock = JSON.parse(await readFile6(path, "utf8"));
|
|
4699
5748
|
if (!Number.isInteger(lock?.pid) || lock.pid <= 0) return false;
|
|
@@ -4739,7 +5788,7 @@ async function clearJournal(lockDir, txnId) {
|
|
|
4739
5788
|
}
|
|
4740
5789
|
}
|
|
4741
5790
|
async function readJournal(lockDir, name) {
|
|
4742
|
-
const path =
|
|
5791
|
+
const path = resolve11(lockDir, name);
|
|
4743
5792
|
let raw;
|
|
4744
5793
|
try {
|
|
4745
5794
|
raw = await readFile6(path, "utf8");
|
|
@@ -4760,7 +5809,7 @@ async function readJournal(lockDir, name) {
|
|
|
4760
5809
|
async function isCommitted(journal) {
|
|
4761
5810
|
if (journal.committed) return true;
|
|
4762
5811
|
const marker = journal.ledger?.find((entry) => entry.commitMarker);
|
|
4763
|
-
if (!marker?.target || !marker.installedSha256 || !
|
|
5812
|
+
if (!marker?.target || !marker.installedSha256 || !existsSync12(marker.target)) return false;
|
|
4764
5813
|
try {
|
|
4765
5814
|
return bytesHashString(await readFile6(marker.target, "utf8")) === marker.installedSha256;
|
|
4766
5815
|
} catch {
|
|
@@ -4769,13 +5818,13 @@ async function isCommitted(journal) {
|
|
|
4769
5818
|
}
|
|
4770
5819
|
async function restoreEntry(entry) {
|
|
4771
5820
|
if (entry.op === "write") {
|
|
4772
|
-
if (entry.backup &&
|
|
5821
|
+
if (entry.backup && existsSync12(entry.backup)) {
|
|
4773
5822
|
await rename4(entry.backup, entry.target);
|
|
4774
|
-
} else if (entry.hadOriginal === false &&
|
|
5823
|
+
} else if (entry.hadOriginal === false && existsSync12(entry.target)) {
|
|
4775
5824
|
await unlink2(entry.target);
|
|
4776
5825
|
}
|
|
4777
|
-
if (entry.staged &&
|
|
4778
|
-
} 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)) {
|
|
4779
5828
|
await rename4(entry.backup, entry.target);
|
|
4780
5829
|
}
|
|
4781
5830
|
await fsyncDir2(dirname6(entry.target));
|
|
@@ -4798,11 +5847,11 @@ async function recoverJournal(lockDir, name) {
|
|
|
4798
5847
|
complete = false;
|
|
4799
5848
|
}
|
|
4800
5849
|
}
|
|
4801
|
-
if (complete) await unlink2(
|
|
5850
|
+
if (complete) await unlink2(resolve11(lockDir, name)).catch(() => null);
|
|
4802
5851
|
return { ok: complete };
|
|
4803
5852
|
}
|
|
4804
5853
|
async function recover(repoRoot, lockDir) {
|
|
4805
|
-
if (!
|
|
5854
|
+
if (!existsSync12(lockDir)) return { recovered: false, recoveredCount: 0 };
|
|
4806
5855
|
if (await liveLockOwner(lockDir)) {
|
|
4807
5856
|
return { recovered: false, recoveredCount: 0, blocked: true };
|
|
4808
5857
|
}
|
|
@@ -4836,11 +5885,11 @@ async function recover(repoRoot, lockDir) {
|
|
|
4836
5885
|
}
|
|
4837
5886
|
async function commitEntry(entry) {
|
|
4838
5887
|
let changed = false;
|
|
4839
|
-
if (entry.backup &&
|
|
5888
|
+
if (entry.backup && existsSync12(entry.backup)) {
|
|
4840
5889
|
await unlink2(entry.backup);
|
|
4841
5890
|
changed = true;
|
|
4842
5891
|
}
|
|
4843
|
-
if (entry.staged &&
|
|
5892
|
+
if (entry.staged && existsSync12(entry.staged)) {
|
|
4844
5893
|
await unlink2(entry.staged);
|
|
4845
5894
|
changed = true;
|
|
4846
5895
|
}
|
|
@@ -4870,7 +5919,7 @@ async function executePlan({ repoRoot, plan, newLockBuilder }) {
|
|
|
4870
5919
|
const backup = backupPath(op.target, token);
|
|
4871
5920
|
const staged = stagedPath(op.target, token);
|
|
4872
5921
|
if (op.kind === "delete") {
|
|
4873
|
-
if (!
|
|
5922
|
+
if (!existsSync12(op.target)) continue;
|
|
4874
5923
|
journal.entries.push({
|
|
4875
5924
|
op: "delete",
|
|
4876
5925
|
target: op.target,
|
|
@@ -4883,7 +5932,7 @@ async function executePlan({ repoRoot, plan, newLockBuilder }) {
|
|
|
4883
5932
|
await rename4(op.target, backup);
|
|
4884
5933
|
} else {
|
|
4885
5934
|
await mkdirp(dirname6(op.target));
|
|
4886
|
-
const hadOriginal =
|
|
5935
|
+
const hadOriginal = existsSync12(op.target);
|
|
4887
5936
|
journal.entries.push({
|
|
4888
5937
|
op: "write",
|
|
4889
5938
|
target: op.target,
|
|
@@ -4909,7 +5958,7 @@ async function executePlan({ repoRoot, plan, newLockBuilder }) {
|
|
|
4909
5958
|
const token = randomToken2();
|
|
4910
5959
|
const backup = backupPath(lockPathTarget, token);
|
|
4911
5960
|
const staged = stagedPath(lockPathTarget, token);
|
|
4912
|
-
const hadOriginal =
|
|
5961
|
+
const hadOriginal = existsSync12(lockPathTarget);
|
|
4913
5962
|
const finalLock = { ...lockValue, integrity: computeIntegrity(lockValue) };
|
|
4914
5963
|
const lockBytes = JSON.stringify(finalLock, null, 2) + "\n";
|
|
4915
5964
|
journal.entries.push({
|
|
@@ -4973,16 +6022,16 @@ async function rollback(lockDir, journal) {
|
|
|
4973
6022
|
init_lock();
|
|
4974
6023
|
init_config();
|
|
4975
6024
|
import { readFile as readFile7 } from "node:fs/promises";
|
|
4976
|
-
import { existsSync as
|
|
4977
|
-
import { resolve as
|
|
6025
|
+
import { existsSync as existsSync13 } from "node:fs";
|
|
6026
|
+
import { resolve as resolve12 } from "node:path";
|
|
4978
6027
|
function legacyAdapterPath(repoRoot) {
|
|
4979
|
-
return
|
|
6028
|
+
return resolve12(repoRoot, ".opencode", "delivery.json");
|
|
4980
6029
|
}
|
|
4981
6030
|
function legacyLockPath(repoRoot) {
|
|
4982
|
-
return
|
|
6031
|
+
return resolve12(repoRoot, ".opencode", "delivery.lock.json");
|
|
4983
6032
|
}
|
|
4984
6033
|
function legacyPluginPath(repoRoot) {
|
|
4985
|
-
return
|
|
6034
|
+
return resolve12(repoRoot, ".opencode", "plugin", "delivery.ts");
|
|
4986
6035
|
}
|
|
4987
6036
|
async function detectLegacyShapes(repoRoot) {
|
|
4988
6037
|
const out = {
|
|
@@ -4993,17 +6042,17 @@ async function detectLegacyShapes(repoRoot) {
|
|
|
4993
6042
|
reviewer: false,
|
|
4994
6043
|
verifier: false
|
|
4995
6044
|
};
|
|
4996
|
-
if (
|
|
4997
|
-
if (
|
|
4998
|
-
if (
|
|
4999
|
-
if (
|
|
5000
|
-
if (
|
|
5001
|
-
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;
|
|
5002
6051
|
return out;
|
|
5003
6052
|
}
|
|
5004
6053
|
async function readLegacyAdapter(repoRoot) {
|
|
5005
6054
|
const path = legacyAdapterPath(repoRoot);
|
|
5006
|
-
if (!
|
|
6055
|
+
if (!existsSync13(path)) return null;
|
|
5007
6056
|
try {
|
|
5008
6057
|
const raw = await readFile7(path, "utf8");
|
|
5009
6058
|
return { path, raw, value: JSON.parse(raw) };
|
|
@@ -5024,13 +6073,13 @@ async function migration({ repoRoot, lock, forceRepair, detection = null }) {
|
|
|
5024
6073
|
if (legacy && shapes.legacyLock && !lock?.manager) {
|
|
5025
6074
|
actions.push({ kind: "kept-legacy-lock", path: legacyLockPath(repoRoot) });
|
|
5026
6075
|
}
|
|
5027
|
-
if (shapes.plugin &&
|
|
6076
|
+
if (shapes.plugin && existsSync13(resolve12(repoRoot, ".opencode/plugins/opencode-ship.js"))) {
|
|
5028
6077
|
if (!forceRepair) {
|
|
5029
6078
|
actions.push({ kind: "candidate-remove-legacy-plugin", path: legacyPluginPath(repoRoot) });
|
|
5030
6079
|
}
|
|
5031
6080
|
}
|
|
5032
6081
|
if (shapes.pluginOld) {
|
|
5033
|
-
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") });
|
|
5034
6083
|
}
|
|
5035
6084
|
return { shapes, actions, legacyPresent: Boolean(legacy), proposedConfigSeed };
|
|
5036
6085
|
}
|
|
@@ -5078,7 +6127,7 @@ function legacyToShipConfig(legacy, detection = null) {
|
|
|
5078
6127
|
// src/installer/executor.js
|
|
5079
6128
|
init_profile();
|
|
5080
6129
|
async function readCurrentBytes(targetPath) {
|
|
5081
|
-
if (!
|
|
6130
|
+
if (!existsSync14(targetPath)) return null;
|
|
5082
6131
|
const buf = await readFile9(targetPath);
|
|
5083
6132
|
return { bytes: buf, hash: bytesHashString(buf.toString("utf8")) };
|
|
5084
6133
|
}
|
|
@@ -5169,7 +6218,7 @@ async function previewInstall({ rootPath, profile = null, replaceManaged, forceC
|
|
|
5169
6218
|
});
|
|
5170
6219
|
const planMode = null;
|
|
5171
6220
|
const rootPlan = await planRootConfigApply({ repoRoot, lock, forceRepair: Boolean(forceRootConfig), planMode });
|
|
5172
|
-
const setupPending = resolved.profile === "engineering" && !hasCompletedModels(configValue);
|
|
6221
|
+
const setupPending = resolved.profile === "engineering" && !hasCompletedModels(configPlan.configValue);
|
|
5173
6222
|
const plan = [...filePlan ?? [], ...staleFilePlan, ...migrationPlan, configPlan, rootPlan];
|
|
5174
6223
|
const conflicts = plan.filter((p) => p && p.kind === "conflict");
|
|
5175
6224
|
const summary = summarise(plan);
|
|
@@ -5185,7 +6234,8 @@ async function previewInstall({ rootPath, profile = null, replaceManaged, forceC
|
|
|
5185
6234
|
conflicts,
|
|
5186
6235
|
summary,
|
|
5187
6236
|
migrationReport,
|
|
5188
|
-
setupPending
|
|
6237
|
+
setupPending,
|
|
6238
|
+
modelsProvenance: configPlan.modelsProvenance
|
|
5189
6239
|
};
|
|
5190
6240
|
}
|
|
5191
6241
|
async function previewUninstall({ rootPath }) {
|
|
@@ -5220,7 +6270,7 @@ function summarise(plan) {
|
|
|
5220
6270
|
}
|
|
5221
6271
|
return counts;
|
|
5222
6272
|
}
|
|
5223
|
-
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 }) {
|
|
5224
6274
|
const files = [];
|
|
5225
6275
|
const remain = lock?.files?.filter((f) => !plan.some((op) => op?.relPath === f.path)) ?? [];
|
|
5226
6276
|
for (const op of plan) {
|
|
@@ -5256,7 +6306,7 @@ async function assembleLock({ repoRoot, plan, lock, configPlan, rootPlan, profil
|
|
|
5256
6306
|
manager: {
|
|
5257
6307
|
schemaVersion: CURRENT_LOCK_SCHEMA,
|
|
5258
6308
|
name: "opencode-ship",
|
|
5259
|
-
version: "1.1.
|
|
6309
|
+
version: "1.1.9-rc.1",
|
|
5260
6310
|
templateSet: TEMPLATE_SET_ID,
|
|
5261
6311
|
profile: resolvedProfile,
|
|
5262
6312
|
appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -5266,6 +6316,7 @@ async function assembleLock({ repoRoot, plan, lock, configPlan, rootPlan, profil
|
|
|
5266
6316
|
sha256: configSha ?? lock?.manager?.config?.sha256 ?? "",
|
|
5267
6317
|
existed: Boolean(lock?.manager?.config?.existed)
|
|
5268
6318
|
},
|
|
6319
|
+
...modelsProvenance ? { models: modelsProvenance } : {},
|
|
5269
6320
|
rootDocuments: hasRootDocuments && (hasRootPlan || (lock?.manager?.rootDocuments?.length ?? 0) > 0) ? [{
|
|
5270
6321
|
path: rootPlan?.relPath ?? lock?.manager?.rootDocuments?.[0]?.path ?? "opencode.json",
|
|
5271
6322
|
format: rootPlan?.format ?? lock?.manager?.rootDocuments?.[0]?.format ?? "json",
|
|
@@ -5312,6 +6363,7 @@ async function commitInstall(preview, { json, command, fullSetupComplete = false
|
|
|
5312
6363
|
configPlan,
|
|
5313
6364
|
rootPlan,
|
|
5314
6365
|
profile: preview.profile?.profile,
|
|
6366
|
+
modelsProvenance: preview.modelsProvenance,
|
|
5315
6367
|
fullSetupComplete
|
|
5316
6368
|
});
|
|
5317
6369
|
const txPlan = await stageFiles(fileOnly, repoRoot);
|
|
@@ -5403,12 +6455,12 @@ function relativeTemplate(source) {
|
|
|
5403
6455
|
// src/installer/commands/doctor.js
|
|
5404
6456
|
init_lock();
|
|
5405
6457
|
init_config();
|
|
5406
|
-
import { existsSync as
|
|
6458
|
+
import { existsSync as existsSync16, readFileSync as readFileSync6 } from "node:fs";
|
|
5407
6459
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
5408
6460
|
init_hash();
|
|
5409
6461
|
init_catalog();
|
|
5410
6462
|
init_root_config();
|
|
5411
|
-
import { resolve as
|
|
6463
|
+
import { resolve as resolve15 } from "node:path";
|
|
5412
6464
|
|
|
5413
6465
|
// src/installer/report.js
|
|
5414
6466
|
var REPORT_VERSION = 1;
|
|
@@ -5459,6 +6511,7 @@ function renderJson({ command, plan, conflicts, summary, diagnostics = [], exitC
|
|
|
5459
6511
|
|
|
5460
6512
|
// src/installer/commands/doctor.js
|
|
5461
6513
|
init_profile();
|
|
6514
|
+
init_workflow_models();
|
|
5462
6515
|
function checkNode() {
|
|
5463
6516
|
return { name: "node>=22.6.0", ok: /^v2[2-9]/.test(process.version), detail: process.version };
|
|
5464
6517
|
}
|
|
@@ -5497,9 +6550,9 @@ function checkPackageIntegrity() {
|
|
|
5497
6550
|
function buildSourceHashIndex() {
|
|
5498
6551
|
const idx = /* @__PURE__ */ new Map();
|
|
5499
6552
|
for (const entry of CATALOG) {
|
|
5500
|
-
if (!
|
|
6553
|
+
if (!existsSync16(entry.source)) continue;
|
|
5501
6554
|
try {
|
|
5502
|
-
const buf =
|
|
6555
|
+
const buf = readFileSync6(entry.source, "utf8");
|
|
5503
6556
|
idx.set(entry.source, bytesHashString(buf));
|
|
5504
6557
|
} catch {
|
|
5505
6558
|
}
|
|
@@ -5510,13 +6563,13 @@ async function checkCatalogInstall(repoRoot, sourceHashes, profile, renderedAgen
|
|
|
5510
6563
|
const rows = [];
|
|
5511
6564
|
const scoped = profile ? filterCatalogByProfile(CATALOG, profile) : CATALOG;
|
|
5512
6565
|
for (const entry of scoped) {
|
|
5513
|
-
const target =
|
|
5514
|
-
if (!
|
|
6566
|
+
const target = resolve15(repoRoot, entry.path);
|
|
6567
|
+
if (!existsSync16(target)) {
|
|
5515
6568
|
rows.push(`${entry.id}: missing`);
|
|
5516
6569
|
continue;
|
|
5517
6570
|
}
|
|
5518
6571
|
try {
|
|
5519
|
-
const buf =
|
|
6572
|
+
const buf = readFileSync6(target, "utf8");
|
|
5520
6573
|
const actual = bytesHashString(buf);
|
|
5521
6574
|
const rendered = renderedAgentMap.get(entry.path);
|
|
5522
6575
|
const expected = rendered ? rendered.sha256 : sourceHashes.get(entry.source);
|
|
@@ -5572,12 +6625,12 @@ async function checkManagedHashes(repoRoot, validatedLock) {
|
|
|
5572
6625
|
const drift = [];
|
|
5573
6626
|
const renderedAgents = await loadRenderedAgentOverrides(repoRoot);
|
|
5574
6627
|
for (const entry of validatedLock.lock.files ?? []) {
|
|
5575
|
-
const p =
|
|
5576
|
-
if (!
|
|
6628
|
+
const p = resolve15(repoRoot, entry.path);
|
|
6629
|
+
if (!existsSync16(p)) {
|
|
5577
6630
|
drift.push(`missing:${entry.path}`);
|
|
5578
6631
|
continue;
|
|
5579
6632
|
}
|
|
5580
|
-
const buf =
|
|
6633
|
+
const buf = readFileSync6(p, "utf8");
|
|
5581
6634
|
const actual = bytesHashString(buf);
|
|
5582
6635
|
if (actual !== entry.sha256) drift.push(`drift:${entry.path}`);
|
|
5583
6636
|
}
|
|
@@ -5627,6 +6680,41 @@ async function checkRootConfig(repoRoot) {
|
|
|
5627
6680
|
detail: conflict ? `conflict on ${conflict.pointer}` : `applied=${r.applied.length}, skipped=${r.skipped.length}`
|
|
5628
6681
|
};
|
|
5629
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
|
+
}
|
|
5630
6718
|
async function checkSetupState(repoRoot, configValue) {
|
|
5631
6719
|
const { setupComplete: setupComplete2 } = await Promise.resolve().then(() => (init_setup_state(), setup_state_exports));
|
|
5632
6720
|
const state = await setupComplete2(repoRoot, configValue);
|
|
@@ -5687,7 +6775,8 @@ async function runDoctor({ rootPath, profile, json, writeOutput = true }) {
|
|
|
5687
6775
|
await checkManagedHashes(repoRoot, validatedLock),
|
|
5688
6776
|
await checkActiveProfileFootprint(repoRoot, validatedLock, resolved.profile),
|
|
5689
6777
|
await checkRootConfig(repoRoot),
|
|
5690
|
-
await checkSetupState(repoRoot, configValue)
|
|
6778
|
+
await checkSetupState(repoRoot, configValue),
|
|
6779
|
+
await checkWorkflowModelDefaults(repoRoot)
|
|
5691
6780
|
];
|
|
5692
6781
|
const issues = checks.filter((c) => !c.ok).map((c) => `${c.name}: ${c.detail}`);
|
|
5693
6782
|
const plan = checks.map((c) => ({
|
|
@@ -5709,20 +6798,19 @@ async function runDoctor({ rootPath, profile, json, writeOutput = true }) {
|
|
|
5709
6798
|
|
|
5710
6799
|
// src/installer/commands/init.js
|
|
5711
6800
|
init_catalog();
|
|
5712
|
-
init_config();
|
|
5713
6801
|
|
|
5714
6802
|
// src/installer/setup-pending.js
|
|
5715
|
-
import { existsSync as
|
|
6803
|
+
import { existsSync as existsSync17, readFileSync as readFileSync7, unlinkSync, writeFile as writeFile7, mkdir as mkdirAsync } from "node:fs";
|
|
5716
6804
|
import { promisify } from "node:util";
|
|
5717
|
-
import { resolve as
|
|
6805
|
+
import { resolve as resolve16, dirname as dirname9 } from "node:path";
|
|
5718
6806
|
var writeFileAsync = promisify(writeFile7);
|
|
5719
6807
|
var mkdirAsyncAsync = promisify(mkdirAsync);
|
|
5720
6808
|
var REL_PATH = ".opencode/ship.setup-pending.json";
|
|
5721
6809
|
function setupPendingPath(repoRoot) {
|
|
5722
|
-
return
|
|
6810
|
+
return resolve16(repoRoot, REL_PATH);
|
|
5723
6811
|
}
|
|
5724
6812
|
function isSetupPending(repoRoot) {
|
|
5725
|
-
return
|
|
6813
|
+
return existsSync17(setupPendingPath(repoRoot));
|
|
5726
6814
|
}
|
|
5727
6815
|
async function writeSetupPending(repoRoot, payload) {
|
|
5728
6816
|
const path = setupPendingPath(repoRoot);
|
|
@@ -5731,8 +6819,161 @@ async function writeSetupPending(repoRoot, payload) {
|
|
|
5731
6819
|
}
|
|
5732
6820
|
var SETUP_PENDING_REL_PATH2 = REL_PATH;
|
|
5733
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
|
+
|
|
5734
6975
|
// src/installer/commands/init.js
|
|
5735
|
-
var writeFileAsync2 = promisify2(
|
|
6976
|
+
var writeFileAsync2 = promisify2(writeFile12);
|
|
5736
6977
|
var mkdirAsyncAsync2 = promisify2(mkdirAsync2);
|
|
5737
6978
|
async function runInit(options) {
|
|
5738
6979
|
try {
|
|
@@ -5776,6 +7017,28 @@ async function runInit(options) {
|
|
|
5776
7017
|
if (exitCode === 4) return emitFailure(4, committed?.diagnostics?.[0] ?? "transaction failure", options.json, "init");
|
|
5777
7018
|
return emitFailure(exitCode, committed?.diagnostics?.[0] ?? "unknown", options.json, "init");
|
|
5778
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 };
|
|
5779
7042
|
const doctor = await runDoctor({
|
|
5780
7043
|
rootPath: options.rootPath ?? null,
|
|
5781
7044
|
profile: options.profile ?? null,
|
|
@@ -5794,7 +7057,7 @@ async function runInit(options) {
|
|
|
5794
7057
|
if (setupPending && preview.repoRoot) {
|
|
5795
7058
|
await writeSetupPending(preview.repoRoot, {
|
|
5796
7059
|
profile: preview.profile?.profile ?? "engineering",
|
|
5797
|
-
reason: "
|
|
7060
|
+
reason: "docs/AGENTS.md setup incomplete; run /setup-ship-workflow",
|
|
5798
7061
|
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5799
7062
|
});
|
|
5800
7063
|
}
|
|
@@ -5819,19 +7082,38 @@ async function runInit(options) {
|
|
|
5819
7082
|
prefix: "opencode-ship",
|
|
5820
7083
|
exitCode,
|
|
5821
7084
|
doctorIssues: doctor.issues,
|
|
5822
|
-
setupPending
|
|
7085
|
+
setupPending,
|
|
7086
|
+
skillsReport
|
|
5823
7087
|
});
|
|
5824
7088
|
}
|
|
5825
7089
|
process.exitCode = exitCode;
|
|
5826
|
-
return { ok: exitCode === 0, exitCode, setupPending };
|
|
7090
|
+
return { ok: exitCode === 0, exitCode, setupPending, extra: committed.extra };
|
|
5827
7091
|
}
|
|
5828
|
-
function printHumanResult({ prefix, exitCode, doctorIssues, setupPending }) {
|
|
7092
|
+
function printHumanResult({ prefix, exitCode, doctorIssues, setupPending, skillsReport }) {
|
|
5829
7093
|
const lines = [];
|
|
5830
7094
|
if (exitCode === 0) {
|
|
5831
7095
|
lines.push(`${prefix}: installed; doctor OK`);
|
|
5832
7096
|
} else {
|
|
5833
7097
|
lines.push(`${prefix}: installed with warnings`);
|
|
5834
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
|
+
}
|
|
5835
7117
|
if (Array.isArray(doctorIssues) && doctorIssues.length > 0) {
|
|
5836
7118
|
lines.push("");
|
|
5837
7119
|
lines.push("Doctor reported:");
|
|
@@ -6005,15 +7287,11 @@ async function runUpdate(options) {
|
|
|
6005
7287
|
}
|
|
6006
7288
|
throw e;
|
|
6007
7289
|
}
|
|
6008
|
-
const hasExplicitModels = options.models && (options.models.planner || options.models.builder || options.models.finalReviewer);
|
|
6009
7290
|
const preview = await previewInstall({
|
|
6010
7291
|
rootPath: options.rootPath,
|
|
6011
7292
|
profile: options.profile ?? null,
|
|
6012
7293
|
replaceManaged: options.replaceManaged,
|
|
6013
|
-
|
|
6014
|
-
// populating workflow.models. We must rewrite the config in
|
|
6015
|
-
// that case even when the existing config is otherwise valid.
|
|
6016
|
-
forceConfig: Boolean(options.forceConfig || hasExplicitModels),
|
|
7294
|
+
forceConfig: Boolean(options.forceConfig),
|
|
6017
7295
|
forceRootConfig: options.forceRootConfig,
|
|
6018
7296
|
models: options.models ?? null
|
|
6019
7297
|
});
|
|
@@ -6030,6 +7308,30 @@ async function runUpdate(options) {
|
|
|
6030
7308
|
return emitFailure2(3, "modified managed files; rerun with --replace-managed", options.json, "update");
|
|
6031
7309
|
}
|
|
6032
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
|
+
}
|
|
6033
7335
|
if (options.json) {
|
|
6034
7336
|
process.stdout.write(JSON.stringify({
|
|
6035
7337
|
reportVersion: 1,
|
|
@@ -6073,11 +7375,11 @@ function emitFailure2(code, message, json, command) {
|
|
|
6073
7375
|
// src/installer/commands/setup-complete.js
|
|
6074
7376
|
init_config();
|
|
6075
7377
|
init_setup_state();
|
|
6076
|
-
import { existsSync as
|
|
6077
|
-
import { resolve as
|
|
7378
|
+
import { existsSync as existsSync23 } from "node:fs";
|
|
7379
|
+
import { resolve as resolve23 } from "node:path";
|
|
6078
7380
|
async function runSetupComplete(options) {
|
|
6079
7381
|
const repoRoot = options.rootPath ?? process.cwd();
|
|
6080
|
-
if (!
|
|
7382
|
+
if (!existsSync23(resolve23(repoRoot, ".git"))) {
|
|
6081
7383
|
return emitFailure3(2, "not a git repository", options.json);
|
|
6082
7384
|
}
|
|
6083
7385
|
const config = await loadConfig(repoRoot);
|