opencode-ship 1.1.0 → 1.1.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 +86 -11
- package/README.md +14 -13
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/assets/agents/ship-controller.md +1 -1
- package/assets/agents/ship-final-spec-reviewer.md +1 -1
- package/assets/agents/ship-final-standards-reviewer.md +1 -1
- package/assets/agents/ship-planner.md +1 -1
- package/assets/agents/ship-task-builder.md +1 -1
- package/assets/agents/ship-task-reviewer.md +1 -1
- package/dist/cli.js +1411 -1202
- package/dist/core.js +57 -4
- package/dist/plugin.js +203 -233
- package/docs/release/1.1.1-stabilization-plan.md +655 -0
- package/package.json +1 -1
- package/schema/ship-config.schema.json +24 -2
- /package/assets/skills/{setup-engineering-workflow → setup-ship-workflow}/SKILL.md +0 -0
- /package/assets/skills/{setup-engineering-workflow → setup-ship-workflow}/domain.md +0 -0
- /package/assets/skills/{setup-engineering-workflow → setup-ship-workflow}/issue-tracker-github.md +0 -0
- /package/assets/skills/{setup-engineering-workflow → setup-ship-workflow}/issue-tracker-gitlab.md +0 -0
- /package/assets/skills/{setup-engineering-workflow → setup-ship-workflow}/issue-tracker-local.md +0 -0
- /package/assets/skills/{setup-engineering-workflow → setup-ship-workflow}/triage-labels.md +0 -0
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.1
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
5
|
var __esm = (fn, res) => function __init() {
|
|
@@ -10,6 +10,351 @@ var __export = (target, all) => {
|
|
|
10
10
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
// src/profile.js
|
|
14
|
+
function isValidProfile(name) {
|
|
15
|
+
return typeof name === "string" && PROFILES.includes(name);
|
|
16
|
+
}
|
|
17
|
+
function isLegacyProfile(name) {
|
|
18
|
+
return typeof name === "string" && LEGACY_PROFILES.includes(name);
|
|
19
|
+
}
|
|
20
|
+
function normalizeProfile(name) {
|
|
21
|
+
if (name === void 0 || name === null) return DEFAULT_PROFILE;
|
|
22
|
+
if (isValidProfile(name)) return name;
|
|
23
|
+
if (isLegacyProfile(name)) return DEFAULT_PROFILE;
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
function isLegacyCoreProfile(name) {
|
|
27
|
+
return name === "core";
|
|
28
|
+
}
|
|
29
|
+
function resolveProfile({ cli = null, config = null, lock = null } = {}) {
|
|
30
|
+
if (cli !== null && cli !== void 0) {
|
|
31
|
+
if (isLegacyCoreProfile(cli)) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`unknown CLI profile 'core' (only 'engineering' is supported in this release; the 'core' profile was removed; existing persisted 'core' is promoted to engineering on next init/update)`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
const v = normalizeProfile(cli);
|
|
37
|
+
if (v === null) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`unknown CLI profile '${cli}' (only 'engineering' is supported in current release)`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return { profile: v, source: "cli" };
|
|
43
|
+
}
|
|
44
|
+
if (config && typeof config === "object" && config.profile !== void 0 && config.profile !== null) {
|
|
45
|
+
if (isLegacyCoreProfile(config.profile)) {
|
|
46
|
+
return { profile: DEFAULT_PROFILE, source: "default", promotedFrom: "core" };
|
|
47
|
+
}
|
|
48
|
+
const v = normalizeProfile(config.profile);
|
|
49
|
+
if (v === null) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`unknown ship.config.json profile '${config.profile}' (only 'engineering' is supported in current release)`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return { profile: v, source: "config" };
|
|
55
|
+
}
|
|
56
|
+
if (lock && typeof lock === "object" && lock.manager && lock.manager.profile !== void 0 && lock.manager.profile !== null) {
|
|
57
|
+
if (isLegacyCoreProfile(lock.manager.profile)) {
|
|
58
|
+
return { profile: DEFAULT_PROFILE, source: "default", promotedFrom: "core" };
|
|
59
|
+
}
|
|
60
|
+
const v = normalizeProfile(lock.manager.profile);
|
|
61
|
+
if (v === null) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`unknown lock manager.profile '${lock.manager.profile}' (only 'engineering' is supported in current release)`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return { profile: v, source: "lock" };
|
|
67
|
+
}
|
|
68
|
+
return { profile: DEFAULT_PROFILE, source: "default" };
|
|
69
|
+
}
|
|
70
|
+
var PROFILES, DEFAULT_PROFILE, LEGACY_PROFILES;
|
|
71
|
+
var init_profile = __esm({
|
|
72
|
+
"src/profile.js"() {
|
|
73
|
+
PROFILES = Object.freeze(["engineering"]);
|
|
74
|
+
DEFAULT_PROFILE = "engineering";
|
|
75
|
+
LEGACY_PROFILES = Object.freeze(["core"]);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// src/installer/package-root.js
|
|
80
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
81
|
+
import { fileURLToPath } from "node:url";
|
|
82
|
+
import { dirname, resolve } from "node:path";
|
|
83
|
+
function resolvePackageRoot(startUrl) {
|
|
84
|
+
let candidate = dirname(fileURLToPath(startUrl ?? import.meta.url));
|
|
85
|
+
while (candidate && candidate !== "/") {
|
|
86
|
+
const pkgPath = resolve(candidate, "package.json");
|
|
87
|
+
if (existsSync(pkgPath)) {
|
|
88
|
+
try {
|
|
89
|
+
const raw = readFileSync(pkgPath, "utf8");
|
|
90
|
+
const pkg = JSON.parse(raw);
|
|
91
|
+
if (pkg && pkg.name === PACKAGE_NAME) return candidate;
|
|
92
|
+
} catch {
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
candidate = dirname(candidate);
|
|
96
|
+
}
|
|
97
|
+
throw new Error(`opencode-ship package root not found from ${startUrl ?? import.meta.url}`);
|
|
98
|
+
}
|
|
99
|
+
var PACKAGE_NAME;
|
|
100
|
+
var init_package_root = __esm({
|
|
101
|
+
"src/installer/package-root.js"() {
|
|
102
|
+
PACKAGE_NAME = "opencode-ship";
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// src/version.js
|
|
107
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "node:fs";
|
|
108
|
+
import { dirname as dirname2, resolve as resolve2 } from "node:path";
|
|
109
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
110
|
+
var PACKAGE_VERSION, TEMPLATE_SET;
|
|
111
|
+
var init_version = __esm({
|
|
112
|
+
"src/version.js"() {
|
|
113
|
+
PACKAGE_VERSION = "1.1.1";
|
|
114
|
+
TEMPLATE_SET = `v${PACKAGE_VERSION}`;
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// src/installer/catalog.js
|
|
119
|
+
var catalog_exports = {};
|
|
120
|
+
__export(catalog_exports, {
|
|
121
|
+
CATALOG: () => CATALOG,
|
|
122
|
+
PACKAGE_VERSION: () => PACKAGE_VERSION,
|
|
123
|
+
TEMPLATE_SET_ID: () => TEMPLATE_SET_ID,
|
|
124
|
+
filterCatalogByProfile: () => filterCatalogByProfile,
|
|
125
|
+
validateCatalog: () => validateCatalog
|
|
126
|
+
});
|
|
127
|
+
import { resolve as resolve3, relative, sep } from "node:path";
|
|
128
|
+
import { existsSync as existsSync3, statSync } from "node:fs";
|
|
129
|
+
function filterCatalogByProfile(catalog, profile) {
|
|
130
|
+
const effective = profile === void 0 || profile === null ? DEFAULT_PROFILE : isValidProfile(profile) ? profile : profile === "core" ? DEFAULT_PROFILE : null;
|
|
131
|
+
if (effective === null) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`filterCatalogByProfile: unknown profile '${profile}' (expected one of: ${PROFILES.join(", ")})`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
return catalog.filter((entry) => Array.isArray(entry.profiles) && entry.profiles.includes(effective));
|
|
137
|
+
}
|
|
138
|
+
function validateCatalog({ catalog = CATALOG } = {}) {
|
|
139
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
140
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
141
|
+
const issues = [];
|
|
142
|
+
for (const entry of catalog) {
|
|
143
|
+
if (!entry || typeof entry !== "object") {
|
|
144
|
+
issues.push({ id: null, kind: "shape", message: "catalog entry is not an object" });
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const { id, kind, path, source, mode } = entry;
|
|
148
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
149
|
+
issues.push({ id: null, kind: "id", message: `entry id missing: ${JSON.stringify(entry)}` });
|
|
150
|
+
} else if (seenIds.has(id)) {
|
|
151
|
+
issues.push({ id, kind: "duplicate-id", message: `duplicate catalog id: ${id}` });
|
|
152
|
+
} else {
|
|
153
|
+
seenIds.add(id);
|
|
154
|
+
}
|
|
155
|
+
if (typeof path !== "string" || !path.startsWith(".opencode" + sep)) {
|
|
156
|
+
issues.push({ id, kind: "path", message: `path must be rooted under .opencode/: ${path}` });
|
|
157
|
+
}
|
|
158
|
+
if (seenPaths.has(path)) {
|
|
159
|
+
issues.push({ id, kind: "duplicate-path", message: `duplicate target path: ${path}` });
|
|
160
|
+
} else {
|
|
161
|
+
seenPaths.add(path);
|
|
162
|
+
}
|
|
163
|
+
if (!ALLOWED_KINDS.has(kind)) {
|
|
164
|
+
issues.push({ id, kind: "kind", message: `unsupported entry kind: ${kind}` });
|
|
165
|
+
}
|
|
166
|
+
if (typeof source !== "string" || source.length === 0) {
|
|
167
|
+
issues.push({ id, kind: "source", message: `source path missing: ${id}` });
|
|
168
|
+
} else if (!existsSync3(source)) {
|
|
169
|
+
issues.push({ id, kind: "source-missing", message: `source file not found: ${source}` });
|
|
170
|
+
} else {
|
|
171
|
+
try {
|
|
172
|
+
const stats = statSync(source);
|
|
173
|
+
if (!stats.isFile()) {
|
|
174
|
+
issues.push({ id, kind: "source-not-file", message: `source is not a regular file: ${source}` });
|
|
175
|
+
} else if (stats.size === 0) {
|
|
176
|
+
issues.push({ id, kind: "source-empty", message: `source file is empty: ${source}` });
|
|
177
|
+
}
|
|
178
|
+
} catch (e) {
|
|
179
|
+
issues.push({ id, kind: "source-stat", message: `unable to stat source: ${e?.message ?? e}` });
|
|
180
|
+
}
|
|
181
|
+
const rel = relative(packageRoot, source);
|
|
182
|
+
if (rel.startsWith("..")) {
|
|
183
|
+
issues.push({ id, kind: "source-out-of-package", message: `source escapes package root: ${source}` });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (mode !== 420) {
|
|
187
|
+
issues.push({ id, kind: "mode", message: `mode must be 0o644: ${id}` });
|
|
188
|
+
}
|
|
189
|
+
if (!Array.isArray(entry.profiles) || entry.profiles.length === 0) {
|
|
190
|
+
issues.push({ id, kind: "profiles", message: `profiles must be a non-empty array: ${id}` });
|
|
191
|
+
} else {
|
|
192
|
+
for (const p of entry.profiles) {
|
|
193
|
+
if (!isValidProfile(p)) {
|
|
194
|
+
issues.push({ id, kind: "profiles", message: `unknown profile in profiles[${entry.profiles.indexOf(p)}]: ${p} (expected one of: ${PROFILES.join(", ")})` });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (issues.length > 0) {
|
|
200
|
+
const summary = issues.map((i) => i.message).join("; ");
|
|
201
|
+
const err = new Error(`opencode-ship catalog validation failed: ${summary}`);
|
|
202
|
+
err.issues = issues;
|
|
203
|
+
err.catalogValidation = true;
|
|
204
|
+
throw err;
|
|
205
|
+
}
|
|
206
|
+
return catalog;
|
|
207
|
+
}
|
|
208
|
+
var TEMPLATE_SET_ID, packageRoot, MATT_SKILLS, SUPER_SKILLS, ENGINEERING_AGENTS, ENGINEERING_COMMANDS, CATALOG, ALLOWED_KINDS;
|
|
209
|
+
var init_catalog = __esm({
|
|
210
|
+
"src/installer/catalog.js"() {
|
|
211
|
+
init_package_root();
|
|
212
|
+
init_version();
|
|
213
|
+
init_profile();
|
|
214
|
+
TEMPLATE_SET_ID = TEMPLATE_SET;
|
|
215
|
+
packageRoot = resolvePackageRoot(import.meta.url);
|
|
216
|
+
MATT_SKILLS = [
|
|
217
|
+
"engineering-workflow",
|
|
218
|
+
"grilling",
|
|
219
|
+
"domain-modeling",
|
|
220
|
+
"grill-with-docs",
|
|
221
|
+
"triage",
|
|
222
|
+
"to-spec",
|
|
223
|
+
"to-tickets",
|
|
224
|
+
"wayfinder",
|
|
225
|
+
"handoff",
|
|
226
|
+
"research",
|
|
227
|
+
"prototype",
|
|
228
|
+
"codebase-design",
|
|
229
|
+
"code-review"
|
|
230
|
+
];
|
|
231
|
+
SUPER_SKILLS = [
|
|
232
|
+
"brainstorming",
|
|
233
|
+
"writing-plans",
|
|
234
|
+
"executing-plans",
|
|
235
|
+
"subagent-driven-development",
|
|
236
|
+
"dispatching-parallel-agents",
|
|
237
|
+
"test-driven-development",
|
|
238
|
+
"systematic-debugging",
|
|
239
|
+
"verification-before-completion",
|
|
240
|
+
"requesting-code-review",
|
|
241
|
+
"receiving-code-review"
|
|
242
|
+
];
|
|
243
|
+
ENGINEERING_AGENTS = [
|
|
244
|
+
"ship-controller",
|
|
245
|
+
"ship-planner",
|
|
246
|
+
"ship-task-builder",
|
|
247
|
+
"ship-task-reviewer",
|
|
248
|
+
"ship-final-standards-reviewer",
|
|
249
|
+
"ship-final-spec-reviewer"
|
|
250
|
+
];
|
|
251
|
+
ENGINEERING_COMMANDS = [
|
|
252
|
+
"ship-deliver",
|
|
253
|
+
"ship-resume",
|
|
254
|
+
"ship-status"
|
|
255
|
+
];
|
|
256
|
+
CATALOG = [
|
|
257
|
+
{
|
|
258
|
+
id: "plugin:opencode-ship",
|
|
259
|
+
kind: "plugin",
|
|
260
|
+
path: ".opencode/plugins/opencode-ship.js",
|
|
261
|
+
source: resolve3(packageRoot, "dist/plugin.js"),
|
|
262
|
+
mode: 420,
|
|
263
|
+
profiles: ["engineering"]
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
id: "agent:delivery-reviewer",
|
|
267
|
+
kind: "agent",
|
|
268
|
+
path: ".opencode/agents/delivery-reviewer.md",
|
|
269
|
+
source: resolve3(packageRoot, "assets/agents/delivery-reviewer.md"),
|
|
270
|
+
mode: 420,
|
|
271
|
+
profiles: ["engineering"]
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
id: "agent:delivery-verifier",
|
|
275
|
+
kind: "agent",
|
|
276
|
+
path: ".opencode/agents/delivery-verifier.md",
|
|
277
|
+
source: resolve3(packageRoot, "assets/agents/delivery-verifier.md"),
|
|
278
|
+
mode: 420,
|
|
279
|
+
profiles: ["engineering"]
|
|
280
|
+
},
|
|
281
|
+
...ENGINEERING_AGENTS.map((name) => ({
|
|
282
|
+
id: `agent:${name}`,
|
|
283
|
+
kind: "agent",
|
|
284
|
+
path: `.opencode/agents/${name}.md`,
|
|
285
|
+
source: resolve3(packageRoot, `assets/agents/${name}.md`),
|
|
286
|
+
mode: 420,
|
|
287
|
+
profiles: ["engineering"]
|
|
288
|
+
})),
|
|
289
|
+
...ENGINEERING_COMMANDS.map((name) => ({
|
|
290
|
+
id: `command:${name}`,
|
|
291
|
+
kind: "support",
|
|
292
|
+
path: `.opencode/commands/${name}.md`,
|
|
293
|
+
source: resolve3(packageRoot, `assets/commands/${name}.md`),
|
|
294
|
+
mode: 420,
|
|
295
|
+
profiles: ["engineering"]
|
|
296
|
+
})),
|
|
297
|
+
{
|
|
298
|
+
id: "skill:delivery-workflow",
|
|
299
|
+
kind: "skill",
|
|
300
|
+
path: ".opencode/skills/delivery-workflow/SKILL.md",
|
|
301
|
+
source: resolve3(packageRoot, "assets/skills/delivery-workflow/SKILL.md"),
|
|
302
|
+
mode: 420,
|
|
303
|
+
profiles: ["engineering"]
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
id: "skill:planning-research-checkpoint",
|
|
307
|
+
kind: "skill",
|
|
308
|
+
path: ".opencode/skills/planning-research-checkpoint/SKILL.md",
|
|
309
|
+
source: resolve3(packageRoot, "assets/skills/planning-research-checkpoint/SKILL.md"),
|
|
310
|
+
mode: 420,
|
|
311
|
+
profiles: ["engineering"]
|
|
312
|
+
},
|
|
313
|
+
...MATT_SKILLS.map((name) => ({
|
|
314
|
+
id: `skill:matt:${name}`,
|
|
315
|
+
kind: "skill",
|
|
316
|
+
path: `.opencode/skills/${name}/SKILL.md`,
|
|
317
|
+
source: resolve3(packageRoot, `assets/skills/${name}/SKILL.md`),
|
|
318
|
+
mode: 420,
|
|
319
|
+
profiles: ["engineering"]
|
|
320
|
+
})),
|
|
321
|
+
...SUPER_SKILLS.map((name) => ({
|
|
322
|
+
id: `skill:super:${name}`,
|
|
323
|
+
kind: "skill",
|
|
324
|
+
path: `.opencode/skills/${name}/SKILL.md`,
|
|
325
|
+
source: resolve3(packageRoot, `assets/skills/${name}/SKILL.md`),
|
|
326
|
+
mode: 420,
|
|
327
|
+
profiles: ["engineering"]
|
|
328
|
+
})),
|
|
329
|
+
{
|
|
330
|
+
id: "skill:setup-ship-workflow",
|
|
331
|
+
kind: "skill",
|
|
332
|
+
path: ".opencode/skills/setup-ship-workflow/SKILL.md",
|
|
333
|
+
source: resolve3(packageRoot, "assets/skills/setup-ship-workflow/SKILL.md"),
|
|
334
|
+
mode: 420,
|
|
335
|
+
profiles: ["engineering"]
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
id: "skill:skill-discovery",
|
|
339
|
+
kind: "skill",
|
|
340
|
+
path: ".opencode/skills/skill-discovery/SKILL.md",
|
|
341
|
+
source: resolve3(packageRoot, "assets/skills/skill-discovery/SKILL.md"),
|
|
342
|
+
mode: 420,
|
|
343
|
+
profiles: ["engineering"]
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
id: "command:setup-ship-workflow",
|
|
347
|
+
kind: "support",
|
|
348
|
+
path: ".opencode/commands/setup-ship-workflow.md",
|
|
349
|
+
source: resolve3(packageRoot, "assets/commands/setup-ship-workflow.md"),
|
|
350
|
+
mode: 420,
|
|
351
|
+
profiles: ["engineering"]
|
|
352
|
+
}
|
|
353
|
+
];
|
|
354
|
+
ALLOWED_KINDS = /* @__PURE__ */ new Set(["plugin", "agent", "skill", "support"]);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
13
358
|
// src/installer/json-pointer.js
|
|
14
359
|
function unescape(token) {
|
|
15
360
|
return token.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
@@ -95,1223 +440,1040 @@ var init_hash = __esm({
|
|
|
95
440
|
}
|
|
96
441
|
});
|
|
97
442
|
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
443
|
+
// schema/ship-config.schema.json
|
|
444
|
+
var ship_config_schema_default;
|
|
445
|
+
var init_ship_config_schema = __esm({
|
|
446
|
+
"schema/ship-config.schema.json"() {
|
|
447
|
+
ship_config_schema_default = {
|
|
448
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
449
|
+
$id: "https://github.com/Viktorxyz/opencode-ship/schema/ship-config.schema.json",
|
|
450
|
+
title: "opencode-ship user config",
|
|
451
|
+
type: "object",
|
|
452
|
+
required: ["schemaVersion"],
|
|
453
|
+
additionalProperties: false,
|
|
454
|
+
properties: {
|
|
455
|
+
schemaVersion: { enum: [1, 2] },
|
|
456
|
+
profile: {
|
|
457
|
+
type: "string",
|
|
458
|
+
enum: ["engineering", "core"],
|
|
459
|
+
description: "Active profile. Engineering is the only supported profile in 1.1.0; core is accepted on read for legacy consumer migration."
|
|
460
|
+
},
|
|
461
|
+
owner: {
|
|
462
|
+
type: "string",
|
|
463
|
+
description: "Optional override for the issue/manifest owner field. Defaults to the agent's local user.name."
|
|
464
|
+
},
|
|
465
|
+
project: {
|
|
466
|
+
type: "object",
|
|
467
|
+
additionalProperties: false,
|
|
468
|
+
properties: {
|
|
469
|
+
remote: { type: "string", minLength: 1 },
|
|
470
|
+
repository: { type: "string", pattern: "^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$" },
|
|
471
|
+
defaultBranch: { type: "string", minLength: 1 },
|
|
472
|
+
packageManager: { enum: ["npm", "pnpm", "yarn", "bun"] },
|
|
473
|
+
detectOverrides: { type: "boolean", description: "Permit detection to refresh previously persisted values." }
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
delivery: {
|
|
477
|
+
type: "object",
|
|
478
|
+
additionalProperties: false,
|
|
479
|
+
properties: {
|
|
480
|
+
worktree: {
|
|
481
|
+
type: "object",
|
|
482
|
+
additionalProperties: false,
|
|
483
|
+
properties: {
|
|
484
|
+
root: { type: "string", minLength: 1 },
|
|
485
|
+
branchTemplate: { type: "string", minLength: 1 },
|
|
486
|
+
bootstrap: {
|
|
487
|
+
type: "array",
|
|
488
|
+
items: {
|
|
489
|
+
type: "array",
|
|
490
|
+
items: { type: "string", minLength: 1 },
|
|
491
|
+
minItems: 1
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
verification: {
|
|
497
|
+
type: "object",
|
|
498
|
+
additionalProperties: false,
|
|
499
|
+
properties: {
|
|
500
|
+
commands: {
|
|
501
|
+
type: "array",
|
|
502
|
+
minItems: 1,
|
|
503
|
+
items: {
|
|
504
|
+
type: "object",
|
|
505
|
+
required: ["id", "argv"],
|
|
506
|
+
additionalProperties: false,
|
|
507
|
+
properties: {
|
|
508
|
+
id: { type: "string", minLength: 1 },
|
|
509
|
+
argv: {
|
|
510
|
+
type: "array",
|
|
511
|
+
items: { type: "string", minLength: 1 },
|
|
512
|
+
minItems: 1
|
|
513
|
+
},
|
|
514
|
+
timeoutMs: { type: "integer", minimum: 1 }
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
},
|
|
518
|
+
requireCleanDiffAfter: { type: "boolean" },
|
|
519
|
+
invalidateOnHeadChange: { type: "boolean" }
|
|
520
|
+
}
|
|
521
|
+
},
|
|
522
|
+
review: {
|
|
523
|
+
type: "object",
|
|
524
|
+
additionalProperties: false,
|
|
525
|
+
properties: {
|
|
526
|
+
agent: { type: "string", minLength: 1 },
|
|
527
|
+
required: { type: "boolean" },
|
|
528
|
+
invalidateOnHeadChange: { type: "boolean" }
|
|
529
|
+
}
|
|
530
|
+
},
|
|
531
|
+
ci: {
|
|
532
|
+
type: "object",
|
|
533
|
+
additionalProperties: false,
|
|
534
|
+
properties: {
|
|
535
|
+
driver: { const: "github-status-checks" },
|
|
536
|
+
requiredChecks: {
|
|
537
|
+
type: "array",
|
|
538
|
+
items: { type: "string", minLength: 1 },
|
|
539
|
+
uniqueItems: true
|
|
540
|
+
},
|
|
541
|
+
wait: { type: "boolean" },
|
|
542
|
+
flakyRetry: { type: "integer", enum: [0, 1] }
|
|
543
|
+
}
|
|
544
|
+
},
|
|
545
|
+
ready: {
|
|
546
|
+
type: "object",
|
|
547
|
+
additionalProperties: false,
|
|
548
|
+
properties: {
|
|
549
|
+
requires: {
|
|
550
|
+
type: "array",
|
|
551
|
+
items: { enum: ["review", "local-verification", "remote-ci"] },
|
|
552
|
+
uniqueItems: true
|
|
553
|
+
},
|
|
554
|
+
stopAfterReady: { type: "boolean" }
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
merge: {
|
|
558
|
+
type: "object",
|
|
559
|
+
additionalProperties: false,
|
|
560
|
+
properties: {
|
|
561
|
+
strategy: { const: "squash" },
|
|
562
|
+
policy: { const: "explicit-user-request-only" },
|
|
563
|
+
requireFreshGates: { type: "boolean" }
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
cleanup: {
|
|
567
|
+
type: "object",
|
|
568
|
+
additionalProperties: false,
|
|
569
|
+
properties: {
|
|
570
|
+
when: { const: "next-task" },
|
|
571
|
+
requireUnpublishedGuard: { type: "boolean" }
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
tasks: {
|
|
577
|
+
type: "object",
|
|
578
|
+
description: "Optional override of the managed-file paths. Use only to relocate a target.",
|
|
579
|
+
additionalProperties: false,
|
|
580
|
+
properties: {
|
|
581
|
+
pluginPath: { type: "string", pattern: "^\\.opencode/.+\\.js$" },
|
|
582
|
+
agentsDir: { type: "string", pattern: "^\\.opencode/agents/?$" },
|
|
583
|
+
skillsDir: { type: "string", pattern: "^\\.opencode/skills/?$" }
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
workflow: {
|
|
587
|
+
type: "object",
|
|
588
|
+
description: "Workflow configuration. Models are optional at write time; the setup-ship-workflow skill fills them in. Once all three are present, ship-deliver can start.",
|
|
589
|
+
additionalProperties: false,
|
|
590
|
+
properties: {
|
|
591
|
+
models: {
|
|
592
|
+
type: "object",
|
|
593
|
+
additionalProperties: false,
|
|
594
|
+
description: "Optional model roles. All three roles must be present before ship-deliver can run.",
|
|
595
|
+
properties: {
|
|
596
|
+
planner: {
|
|
597
|
+
type: "string",
|
|
598
|
+
pattern: "^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$",
|
|
599
|
+
description: "Provider/model id for the strong planning child session."
|
|
600
|
+
},
|
|
601
|
+
builder: {
|
|
602
|
+
type: "string",
|
|
603
|
+
pattern: "^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$",
|
|
604
|
+
description: "Provider/model id for the cheap builder child session."
|
|
605
|
+
},
|
|
606
|
+
finalReviewer: {
|
|
607
|
+
type: "string",
|
|
608
|
+
pattern: "^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$",
|
|
609
|
+
description: "Provider/model id for the final Standards + Spec reviewers."
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
approval: {
|
|
614
|
+
type: "object",
|
|
615
|
+
additionalProperties: false,
|
|
616
|
+
properties: {
|
|
617
|
+
mirrorToIssue: { const: true },
|
|
618
|
+
maxFailedRounds: { const: 3 }
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
},
|
|
623
|
+
skillDiscovery: {
|
|
624
|
+
type: "object",
|
|
625
|
+
additionalProperties: false,
|
|
626
|
+
description: "Trusted-auto skill discovery policy. Default mode is trusted-auto with the canonical owner allowlist and install-count threshold.",
|
|
627
|
+
properties: {
|
|
628
|
+
mode: {
|
|
629
|
+
type: "string",
|
|
630
|
+
enum: ["suggest-only", "trusted-auto", "disabled"]
|
|
631
|
+
},
|
|
632
|
+
trustedOwners: {
|
|
633
|
+
type: "array",
|
|
634
|
+
items: { type: "string", pattern: "^[A-Za-z0-9_.-]+$" }
|
|
635
|
+
},
|
|
636
|
+
minInstalls: { type: "integer", minimum: 0 },
|
|
637
|
+
maxAutoInstall: { type: "integer", minimum: 0, maximum: 20 },
|
|
638
|
+
blocklist: {
|
|
639
|
+
type: "array",
|
|
640
|
+
items: { type: "string", pattern: "^[A-Za-z0-9_./-]+$" }
|
|
641
|
+
},
|
|
642
|
+
requireImmutableRef: { type: "boolean" }
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
176
646
|
};
|
|
177
|
-
} catch (e) {
|
|
178
|
-
return { ok: false, error: { kind: "parse", path: absPath, message: e.message } };
|
|
179
647
|
}
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
// src/installer/validation.js
|
|
651
|
+
function isObject2(v) {
|
|
652
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
180
653
|
}
|
|
181
|
-
function
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
654
|
+
function validate(value, schema, pointer, issues) {
|
|
655
|
+
if (!isObject2(schema)) return;
|
|
656
|
+
if (schema.const !== void 0 && value !== schema.const) {
|
|
657
|
+
issues.push(`${pointer}: expected const ${JSON.stringify(schema.const)}, got ${JSON.stringify(value)}`);
|
|
658
|
+
return;
|
|
185
659
|
}
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const
|
|
195
|
-
if
|
|
196
|
-
|
|
197
|
-
if (
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
660
|
+
if (schema.enum !== void 0 && !schema.enum.includes(value)) {
|
|
661
|
+
issues.push(`${pointer}: expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
if (Array.isArray(schema.allOf)) {
|
|
665
|
+
for (const sub of schema.allOf) validate(value, sub, pointer, issues);
|
|
666
|
+
}
|
|
667
|
+
if (isObject2(schema.if)) {
|
|
668
|
+
const ifIssues = [];
|
|
669
|
+
validate(value, schema.if, pointer, ifIssues);
|
|
670
|
+
if (ifIssues.length === 0) {
|
|
671
|
+
if (isObject2(schema.then)) validate(value, schema.then, pointer, issues);
|
|
672
|
+
} else if (isObject2(schema.else)) {
|
|
673
|
+
validate(value, schema.else, pointer, issues);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
const type = schema.type;
|
|
677
|
+
if (type !== void 0) {
|
|
678
|
+
const actual = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
|
|
679
|
+
if (type !== actual) {
|
|
680
|
+
if (!(type === "integer" && typeof value === "number" && Number.isInteger(value))) {
|
|
681
|
+
issues.push(`${pointer}: expected ${type}, got ${actual}`);
|
|
682
|
+
return;
|
|
203
683
|
}
|
|
204
|
-
i += 1;
|
|
205
|
-
continue;
|
|
206
684
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
continue;
|
|
685
|
+
}
|
|
686
|
+
if (type === "string") {
|
|
687
|
+
if (schema.minLength !== void 0 && value.length < schema.minLength) {
|
|
688
|
+
issues.push(`${pointer}: shorter than minLength ${schema.minLength}`);
|
|
212
689
|
}
|
|
213
|
-
if (
|
|
214
|
-
|
|
215
|
-
|
|
690
|
+
if (schema.pattern !== void 0) {
|
|
691
|
+
const re = new RegExp(schema.pattern);
|
|
692
|
+
if (!re.test(value)) issues.push(`${pointer}: does not match pattern ${schema.pattern}`);
|
|
216
693
|
}
|
|
217
|
-
if (
|
|
218
|
-
|
|
219
|
-
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i += 1;
|
|
220
|
-
i += 2;
|
|
221
|
-
continue;
|
|
694
|
+
if (schema.format === "date-time" && !FORMAT_DATE_TIME.test(value)) {
|
|
695
|
+
issues.push(`${pointer}: not a date-time string`);
|
|
222
696
|
}
|
|
223
|
-
stripped += ch;
|
|
224
|
-
i += 1;
|
|
225
697
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const result = { doc: rootDoc, applied: [], skipped: [] };
|
|
230
|
-
let doc = rootDoc;
|
|
231
|
-
for (const entry of pointerEntries) {
|
|
232
|
-
const existing = getPointer(doc, entry.pointer);
|
|
233
|
-
if (existing === void 0) {
|
|
234
|
-
doc = setPointer(doc, entry.pointer, entry.value);
|
|
235
|
-
result.applied.push({ pointer: entry.pointer, value: entry.value });
|
|
236
|
-
continue;
|
|
698
|
+
if (type === "integer" || type === "number") {
|
|
699
|
+
if (schema.minimum !== void 0 && value < schema.minimum) {
|
|
700
|
+
issues.push(`${pointer}: less than minimum ${schema.minimum}`);
|
|
237
701
|
}
|
|
238
|
-
if (
|
|
239
|
-
|
|
240
|
-
|
|
702
|
+
if (schema.maximum !== void 0 && value > schema.maximum) {
|
|
703
|
+
issues.push(`${pointer}: greater than maximum ${schema.maximum}`);
|
|
704
|
+
}
|
|
705
|
+
if (schema.enum !== void 0) {
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
if (type === "array") {
|
|
709
|
+
if (schema.minItems !== void 0 && value.length < schema.minItems) {
|
|
710
|
+
issues.push(`${pointer}: fewer items than minItems ${schema.minItems}`);
|
|
711
|
+
}
|
|
712
|
+
if (Array.isArray(schema.items)) {
|
|
713
|
+
value.forEach((entry, i) => validate(entry, schema.items[i] ?? {}, `${pointer}/${i}`, issues));
|
|
714
|
+
} else if (schema.items) {
|
|
715
|
+
if (schema.uniqueItems) {
|
|
716
|
+
const seen = /* @__PURE__ */ new Set();
|
|
717
|
+
value.forEach((entry, i) => {
|
|
718
|
+
const key = JSON.stringify(entry);
|
|
719
|
+
if (seen.has(key)) issues.push(`${pointer}/${i}: duplicate unique item`);
|
|
720
|
+
seen.add(key);
|
|
721
|
+
});
|
|
241
722
|
}
|
|
242
|
-
|
|
723
|
+
value.forEach((entry, i) => validate(entry, schema.items, `${pointer}/${i}`, issues));
|
|
243
724
|
}
|
|
244
|
-
result.skipped.push({
|
|
245
|
-
pointer: entry.pointer,
|
|
246
|
-
reason: "different existing value",
|
|
247
|
-
existing,
|
|
248
|
-
desired: entry.value
|
|
249
|
-
});
|
|
250
725
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const doc = setPointer(rootDoc, pointer, block);
|
|
256
|
-
return { doc, previous: previous === void 0 ? null : previous, id: pointer };
|
|
257
|
-
}
|
|
258
|
-
function planModeBlock() {
|
|
259
|
-
return planModePermissions().build;
|
|
260
|
-
}
|
|
261
|
-
function synthesizeDefaultRootConfig() {
|
|
262
|
-
return {
|
|
263
|
-
$schema: "https://opencode.ai/config.json",
|
|
264
|
-
agent: {
|
|
265
|
-
build: {
|
|
266
|
-
permission: {
|
|
267
|
-
delivery_inspect: "allow",
|
|
268
|
-
delivery_issue: "allow",
|
|
269
|
-
delivery_worktree: "allow",
|
|
270
|
-
delivery_verify: "deny",
|
|
271
|
-
delivery_review: "deny",
|
|
272
|
-
delivery_pr: "allow",
|
|
273
|
-
delivery_ready: "allow",
|
|
274
|
-
delivery_merge: "ask",
|
|
275
|
-
delivery_cleanup: "allow",
|
|
276
|
-
task: {
|
|
277
|
-
"delivery-reviewer": "allow",
|
|
278
|
-
"delivery-verifier": "allow"
|
|
279
|
-
}
|
|
280
|
-
}
|
|
726
|
+
if (type === "object" || isObject2(schema.properties) || Array.isArray(schema.required)) {
|
|
727
|
+
if (Array.isArray(schema.required)) {
|
|
728
|
+
for (const key of schema.required) {
|
|
729
|
+
if (!(key in value)) issues.push(`${pointer}: missing required field ${key}`);
|
|
281
730
|
}
|
|
282
731
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const seen = /* @__PURE__ */ new Set();
|
|
293
|
-
if (order) {
|
|
294
|
-
for (const k of order) {
|
|
295
|
-
if (k === "__sourceOrder__") continue;
|
|
296
|
-
if (!(k in value)) continue;
|
|
297
|
-
seen.add(k);
|
|
298
|
-
out[k] = stripSourceOrder(value[k]);
|
|
732
|
+
if (schema.additionalProperties === false && isObject2(schema.properties)) {
|
|
733
|
+
for (const key of Object.keys(value)) {
|
|
734
|
+
if (!(key in schema.properties)) issues.push(`${pointer}: unknown field ${key}`);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
if (isObject2(schema.properties)) {
|
|
738
|
+
for (const key of Object.keys(schema.properties)) {
|
|
739
|
+
if (key in value) validate(value[key], schema.properties[key], `${pointer}/${key}`, issues);
|
|
740
|
+
}
|
|
299
741
|
}
|
|
300
742
|
}
|
|
301
|
-
for (const k of Object.keys(value)) {
|
|
302
|
-
if (k === "__sourceOrder__") continue;
|
|
303
|
-
if (seen.has(k)) continue;
|
|
304
|
-
out[k] = stripSourceOrder(value[k]);
|
|
305
|
-
}
|
|
306
|
-
return out;
|
|
307
743
|
}
|
|
308
|
-
function
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
if (typeof text !== "string" || text.length === 0) {
|
|
313
|
-
return { value: {}, format: "json" };
|
|
314
|
-
}
|
|
315
|
-
const parser = new RootConfigParser(text);
|
|
316
|
-
const value = parser.parseValue(
|
|
317
|
-
0,
|
|
318
|
-
/*atTop*/
|
|
319
|
-
true
|
|
320
|
-
);
|
|
321
|
-
const isJsonc = text.includes("//") || text.includes("/*");
|
|
322
|
-
return { value, format: isJsonc ? "jsonc" : "json" };
|
|
744
|
+
function validateSchema(value, schema) {
|
|
745
|
+
const issues = [];
|
|
746
|
+
validate(value, schema, "#", issues);
|
|
747
|
+
return { ok: issues.length === 0, issues };
|
|
323
748
|
}
|
|
324
|
-
var
|
|
325
|
-
var
|
|
326
|
-
"src/installer/
|
|
327
|
-
|
|
328
|
-
init_hash();
|
|
329
|
-
init_plan_mode_permissions();
|
|
330
|
-
POINTER_ENTRIES = [
|
|
331
|
-
{
|
|
332
|
-
pointer: "/agent/build/permission/delivery_inspect",
|
|
333
|
-
strategy: "value",
|
|
334
|
-
value: "allow"
|
|
335
|
-
},
|
|
336
|
-
{
|
|
337
|
-
pointer: "/agent/build/permission/delivery_issue",
|
|
338
|
-
strategy: "value",
|
|
339
|
-
value: "allow"
|
|
340
|
-
},
|
|
341
|
-
{
|
|
342
|
-
pointer: "/agent/build/permission/delivery_worktree",
|
|
343
|
-
strategy: "value",
|
|
344
|
-
value: "allow"
|
|
345
|
-
},
|
|
346
|
-
{
|
|
347
|
-
pointer: "/agent/build/permission/delivery_verify",
|
|
348
|
-
strategy: "value",
|
|
349
|
-
value: "deny"
|
|
350
|
-
},
|
|
351
|
-
{
|
|
352
|
-
pointer: "/agent/build/permission/delivery_review",
|
|
353
|
-
strategy: "value",
|
|
354
|
-
value: "deny"
|
|
355
|
-
},
|
|
356
|
-
{
|
|
357
|
-
pointer: "/agent/build/permission/delivery_pr",
|
|
358
|
-
strategy: "value",
|
|
359
|
-
value: "allow"
|
|
360
|
-
},
|
|
361
|
-
{
|
|
362
|
-
pointer: "/agent/build/permission/delivery_ready",
|
|
363
|
-
strategy: "value",
|
|
364
|
-
value: "allow"
|
|
365
|
-
},
|
|
366
|
-
{
|
|
367
|
-
pointer: "/agent/build/permission/delivery_merge",
|
|
368
|
-
strategy: "value",
|
|
369
|
-
value: "ask"
|
|
370
|
-
},
|
|
371
|
-
{
|
|
372
|
-
pointer: "/agent/build/permission/delivery_cleanup",
|
|
373
|
-
strategy: "value",
|
|
374
|
-
value: "allow"
|
|
375
|
-
},
|
|
376
|
-
{
|
|
377
|
-
pointer: "/agent/build/permission/task/delivery-reviewer",
|
|
378
|
-
strategy: "value",
|
|
379
|
-
value: "allow"
|
|
380
|
-
},
|
|
381
|
-
{
|
|
382
|
-
pointer: "/agent/build/permission/task/delivery-verifier",
|
|
383
|
-
strategy: "value",
|
|
384
|
-
value: "allow"
|
|
385
|
-
}
|
|
386
|
-
];
|
|
387
|
-
ROOT_PATH_CANDIDATES = ["opencode.json", "opencode.jsonc"];
|
|
388
|
-
PLAN_MODE_POINTER = "/agent/plan/permission";
|
|
389
|
-
RootConfigParser = class {
|
|
390
|
-
constructor(text) {
|
|
391
|
-
this.text = text;
|
|
392
|
-
this.pos = 0;
|
|
393
|
-
}
|
|
394
|
-
skipWS() {
|
|
395
|
-
while (this.pos < this.text.length) {
|
|
396
|
-
const ch = this.text[this.pos];
|
|
397
|
-
if (ch === " " || ch === "\n" || ch === " " || ch === "\r") {
|
|
398
|
-
this.pos += 1;
|
|
399
|
-
continue;
|
|
400
|
-
}
|
|
401
|
-
if (ch === "/" && this.text[this.pos + 1] === "/") {
|
|
402
|
-
while (this.pos < this.text.length && this.text[this.pos] !== "\n") this.pos += 1;
|
|
403
|
-
continue;
|
|
404
|
-
}
|
|
405
|
-
if (ch === "/" && this.text[this.pos + 1] === "*") {
|
|
406
|
-
this.pos += 2;
|
|
407
|
-
while (this.pos < this.text.length && !(this.text[this.pos] === "*" && this.text[this.pos + 1] === "/")) this.pos += 1;
|
|
408
|
-
this.pos += 2;
|
|
409
|
-
continue;
|
|
410
|
-
}
|
|
411
|
-
break;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
parseValue(depth, atTop) {
|
|
415
|
-
this.skipWS();
|
|
416
|
-
const ch = this.text[this.pos];
|
|
417
|
-
if (ch === "{") return this.parseObject(depth, atTop);
|
|
418
|
-
if (ch === "[") return this.parseArray(depth);
|
|
419
|
-
if (ch === '"') return this.parseString();
|
|
420
|
-
if (ch === "-" || ch >= "0" && ch <= "9") return this.parseNumber();
|
|
421
|
-
if (this.text.startsWith("true", this.pos)) {
|
|
422
|
-
this.pos += 4;
|
|
423
|
-
return true;
|
|
424
|
-
}
|
|
425
|
-
if (this.text.startsWith("false", this.pos)) {
|
|
426
|
-
this.pos += 5;
|
|
427
|
-
return false;
|
|
428
|
-
}
|
|
429
|
-
if (this.text.startsWith("null", this.pos)) {
|
|
430
|
-
this.pos += 4;
|
|
431
|
-
return null;
|
|
432
|
-
}
|
|
433
|
-
throw new Error(`unexpected token at ${this.pos}: ${this.text.slice(this.pos, this.pos + 8)}`);
|
|
434
|
-
}
|
|
435
|
-
parseObject(depth, atTop) {
|
|
436
|
-
const out = /* @__PURE__ */ Object.create(null);
|
|
437
|
-
out.__sourceOrder__ = [];
|
|
438
|
-
this.pos += 1;
|
|
439
|
-
while (this.pos < this.text.length) {
|
|
440
|
-
this.skipWS();
|
|
441
|
-
if (this.text[this.pos] === "}") {
|
|
442
|
-
this.pos += 1;
|
|
443
|
-
return out;
|
|
444
|
-
}
|
|
445
|
-
const key = this.parseString();
|
|
446
|
-
out.__sourceOrder__.push(key);
|
|
447
|
-
this.skipWS();
|
|
448
|
-
if (this.text[this.pos] !== ":") throw new Error(`expected : at ${this.pos}`);
|
|
449
|
-
this.pos += 1;
|
|
450
|
-
out[key] = this.parseValue(depth + 1, false);
|
|
451
|
-
this.skipWS();
|
|
452
|
-
if (this.text[this.pos] === ",") {
|
|
453
|
-
this.pos += 1;
|
|
454
|
-
continue;
|
|
455
|
-
}
|
|
456
|
-
if (this.text[this.pos] === "}") {
|
|
457
|
-
this.pos += 1;
|
|
458
|
-
return out;
|
|
459
|
-
}
|
|
460
|
-
throw new Error(`expected , or } at ${this.pos}`);
|
|
461
|
-
}
|
|
462
|
-
throw new Error("unterminated object");
|
|
463
|
-
}
|
|
464
|
-
parseArray(depth) {
|
|
465
|
-
const out = [];
|
|
466
|
-
this.pos += 1;
|
|
467
|
-
while (this.pos < this.text.length) {
|
|
468
|
-
this.skipWS();
|
|
469
|
-
if (this.text[this.pos] === "]") {
|
|
470
|
-
this.pos += 1;
|
|
471
|
-
return out;
|
|
472
|
-
}
|
|
473
|
-
out.push(this.parseValue(depth + 1, false));
|
|
474
|
-
this.skipWS();
|
|
475
|
-
if (this.text[this.pos] === ",") {
|
|
476
|
-
this.pos += 1;
|
|
477
|
-
continue;
|
|
478
|
-
}
|
|
479
|
-
if (this.text[this.pos] === "]") {
|
|
480
|
-
this.pos += 1;
|
|
481
|
-
return out;
|
|
482
|
-
}
|
|
483
|
-
throw new Error(`expected , or ] at ${this.pos}`);
|
|
484
|
-
}
|
|
485
|
-
throw new Error("unterminated array");
|
|
486
|
-
}
|
|
487
|
-
parseString() {
|
|
488
|
-
if (this.text[this.pos] !== '"') throw new Error(`expected " at ${this.pos}`);
|
|
489
|
-
this.pos += 1;
|
|
490
|
-
let out = "";
|
|
491
|
-
while (this.pos < this.text.length) {
|
|
492
|
-
const ch = this.text[this.pos];
|
|
493
|
-
if (ch === "\\") {
|
|
494
|
-
const next = this.text[this.pos + 1];
|
|
495
|
-
out += ch + next;
|
|
496
|
-
this.pos += 2;
|
|
497
|
-
continue;
|
|
498
|
-
}
|
|
499
|
-
if (ch === '"') {
|
|
500
|
-
this.pos += 1;
|
|
501
|
-
return JSON.parse('"' + out + '"');
|
|
502
|
-
}
|
|
503
|
-
out += ch;
|
|
504
|
-
this.pos += 1;
|
|
505
|
-
}
|
|
506
|
-
throw new Error("unterminated string");
|
|
507
|
-
}
|
|
508
|
-
parseNumber() {
|
|
509
|
-
const start = this.pos;
|
|
510
|
-
if (this.text[this.pos] === "-") this.pos += 1;
|
|
511
|
-
while (this.pos < this.text.length && /[0-9.eE+\-]/.test(this.text[this.pos])) this.pos += 1;
|
|
512
|
-
return Number(this.text.slice(start, this.pos));
|
|
513
|
-
}
|
|
514
|
-
};
|
|
749
|
+
var FORMAT_DATE_TIME;
|
|
750
|
+
var init_validation = __esm({
|
|
751
|
+
"src/installer/validation.js"() {
|
|
752
|
+
FORMAT_DATE_TIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:?\d{2})$/;
|
|
515
753
|
}
|
|
516
754
|
});
|
|
517
755
|
|
|
518
|
-
// src/
|
|
519
|
-
var
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
756
|
+
// src/installer/config.js
|
|
757
|
+
var config_exports = {};
|
|
758
|
+
__export(config_exports, {
|
|
759
|
+
configPath: () => configPath,
|
|
760
|
+
hasCompletedModels: () => hasCompletedModels,
|
|
761
|
+
loadConfig: () => loadConfig,
|
|
762
|
+
renderDefaultConfig: () => renderDefaultConfig,
|
|
763
|
+
writeConfig: () => writeConfig
|
|
764
|
+
});
|
|
765
|
+
import { readFile, writeFile, rename, mkdir } from "node:fs/promises";
|
|
766
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
767
|
+
import { dirname as dirname3, resolve as resolve4 } from "node:path";
|
|
768
|
+
function configPath(repoRoot) {
|
|
769
|
+
return resolve4(repoRoot, ".opencode", "ship.config.json");
|
|
523
770
|
}
|
|
524
|
-
function
|
|
525
|
-
|
|
526
|
-
if (!
|
|
527
|
-
|
|
771
|
+
async function loadConfig(repoRoot) {
|
|
772
|
+
const path = configPath(repoRoot);
|
|
773
|
+
if (!existsSync4(path)) return null;
|
|
774
|
+
const raw = await readFile(path, "utf8");
|
|
775
|
+
let parsed;
|
|
776
|
+
try {
|
|
777
|
+
parsed = JSON.parse(raw);
|
|
778
|
+
} catch (e) {
|
|
779
|
+
return { ok: false, error: { kind: "parse", path, message: e.message } };
|
|
780
|
+
}
|
|
781
|
+
const validation = validateSchema(parsed, ship_config_schema_default);
|
|
782
|
+
if (!validation.ok) {
|
|
783
|
+
return { ok: false, error: { kind: "contract", path, issues: validation.issues } };
|
|
784
|
+
}
|
|
785
|
+
return {
|
|
786
|
+
ok: true,
|
|
787
|
+
path,
|
|
788
|
+
raw,
|
|
789
|
+
sha256: bytesHashString(raw),
|
|
790
|
+
canonicalSha256: bytesHashString(stableStringify(parsed)),
|
|
791
|
+
value: parsed
|
|
792
|
+
};
|
|
528
793
|
}
|
|
529
|
-
function
|
|
530
|
-
|
|
794
|
+
async function writeConfig(repoRoot, value) {
|
|
795
|
+
const path = configPath(repoRoot);
|
|
796
|
+
await mkdir(dirname3(path), { recursive: true });
|
|
797
|
+
const raw = JSON.stringify(value, null, 2) + "\n";
|
|
798
|
+
const tmp = `${path}.tmp`;
|
|
799
|
+
await writeFile(tmp, raw, "utf8");
|
|
800
|
+
await rename(tmp, path);
|
|
801
|
+
return { path, raw, sha256: bytesHashString(raw) };
|
|
531
802
|
}
|
|
532
|
-
function
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
803
|
+
function renderDefaultConfig(detection, overrides = {}) {
|
|
804
|
+
const pm = detection?.packageManager ?? "npm";
|
|
805
|
+
const safeBootstrap = Array.isArray(detection?.worktreeBootstrap) && detection.worktreeBootstrap.length ? detection.worktreeBootstrap : [["npm", "install"]];
|
|
806
|
+
const safeVerification = Array.isArray(detection?.verificationPlan) && detection.verificationPlan.length ? detection.verificationPlan.map((step) => ({ id: step.id, argv: step.argv })) : [{ id: "typecheck", argv: ["npm", "run", "typecheck"] }];
|
|
807
|
+
const repo = detection?.repository ?? overrides.repository ?? "owner/repo";
|
|
808
|
+
return {
|
|
809
|
+
schemaVersion: 2,
|
|
810
|
+
profile: "engineering",
|
|
811
|
+
project: {
|
|
812
|
+
remote: detection?.remote ?? "origin",
|
|
813
|
+
repository: repo,
|
|
814
|
+
defaultBranch: detection?.defaultBranch ?? "main",
|
|
815
|
+
packageManager: pm,
|
|
816
|
+
detectOverrides: false
|
|
817
|
+
},
|
|
818
|
+
delivery: {
|
|
819
|
+
worktree: {
|
|
820
|
+
root: detection?.worktreeRoot ?? ".worktrees",
|
|
821
|
+
branchTemplate: "{actor}/{slug}",
|
|
822
|
+
bootstrap: safeBootstrap
|
|
823
|
+
},
|
|
824
|
+
verification: {
|
|
825
|
+
commands: safeVerification,
|
|
826
|
+
requireCleanDiffAfter: true,
|
|
827
|
+
invalidateOnHeadChange: true
|
|
828
|
+
},
|
|
829
|
+
review: { agent: "delivery-reviewer", required: true, invalidateOnHeadChange: true },
|
|
830
|
+
ci: {
|
|
831
|
+
driver: "github-status-checks",
|
|
832
|
+
requiredChecks: ["delivery-verify"],
|
|
833
|
+
wait: true,
|
|
834
|
+
flakyRetry: 1
|
|
835
|
+
},
|
|
836
|
+
ready: { requires: ["review", "local-verification", "remote-ci"], stopAfterReady: true },
|
|
837
|
+
merge: { strategy: "squash", policy: "explicit-user-request-only", requireFreshGates: true },
|
|
838
|
+
cleanup: { when: "next-task", requireUnpublishedGuard: true }
|
|
839
|
+
},
|
|
840
|
+
workflow: {
|
|
841
|
+
models: {},
|
|
842
|
+
approval: { mirrorToIssue: true, maxFailedRounds: 3 }
|
|
551
843
|
}
|
|
552
|
-
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
function hasCompletedModels(configValue) {
|
|
847
|
+
const models = configValue?.workflow?.models;
|
|
848
|
+
if (!models || typeof models !== "object") return false;
|
|
849
|
+
const idRe = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
850
|
+
return typeof models.planner === "string" && idRe.test(models.planner) && typeof models.builder === "string" && idRe.test(models.builder) && typeof models.finalReviewer === "string" && idRe.test(models.finalReviewer);
|
|
851
|
+
}
|
|
852
|
+
var init_config = __esm({
|
|
853
|
+
"src/installer/config.js"() {
|
|
854
|
+
init_ship_config_schema();
|
|
855
|
+
init_validation();
|
|
856
|
+
init_json_pointer();
|
|
857
|
+
init_hash();
|
|
553
858
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
// src/installer/plan-mode-permissions.js
|
|
862
|
+
function planModePermissions() {
|
|
863
|
+
return {
|
|
864
|
+
build: {
|
|
865
|
+
bash: DENY_DEFAULT,
|
|
866
|
+
edit: {
|
|
867
|
+
"*": DENY_DEFAULT,
|
|
868
|
+
[PLANS_GLOB]: ALLOW_PLANS
|
|
869
|
+
},
|
|
870
|
+
webfetch: DENY_DEFAULT,
|
|
871
|
+
task: DENY_DEFAULT,
|
|
872
|
+
delivery_inspect: DENY_DEFAULT,
|
|
873
|
+
delivery_issue: DENY_DEFAULT,
|
|
874
|
+
delivery_worktree: DENY_DEFAULT,
|
|
875
|
+
delivery_verify: DENY_DEFAULT,
|
|
876
|
+
delivery_review: DENY_DEFAULT,
|
|
877
|
+
delivery_pr: DENY_DEFAULT,
|
|
878
|
+
delivery_ready: DENY_DEFAULT,
|
|
879
|
+
delivery_merge: DENY_DEFAULT,
|
|
880
|
+
delivery_cleanup: DENY_DEFAULT
|
|
563
881
|
}
|
|
564
|
-
|
|
565
|
-
}
|
|
566
|
-
return { profile: DEFAULT_PROFILE, source: "default" };
|
|
882
|
+
};
|
|
567
883
|
}
|
|
884
|
+
var PLAN_PATH_PREFIX, DENY_DEFAULT, ALLOW_PLANS, PLANS_GLOB;
|
|
885
|
+
var init_plan_mode_permissions = __esm({
|
|
886
|
+
"src/installer/plan-mode-permissions.js"() {
|
|
887
|
+
PLAN_PATH_PREFIX = ".git/opencode-ship/plans";
|
|
888
|
+
DENY_DEFAULT = "deny";
|
|
889
|
+
ALLOW_PLANS = "allow";
|
|
890
|
+
PLANS_GLOB = `${PLAN_PATH_PREFIX}/**`;
|
|
891
|
+
}
|
|
892
|
+
});
|
|
568
893
|
|
|
569
|
-
// src/installer/
|
|
570
|
-
var
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
--json Emit a JSON envelope instead of human output.
|
|
593
|
-
|
|
594
|
-
After init succeeds, restart OpenCode and run /setup-ship-workflow to
|
|
595
|
-
fill in the workflow.models fields and the per-repo docs.
|
|
596
|
-
`;
|
|
597
|
-
var MODEL_ID_RE = /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/;
|
|
598
|
-
function parseFlags(argv) {
|
|
599
|
-
const options = {
|
|
600
|
-
rootPath: null,
|
|
601
|
-
profile: null,
|
|
602
|
-
json: false,
|
|
603
|
-
replaceManaged: false,
|
|
604
|
-
purgeConfig: false,
|
|
605
|
-
forceConfig: false,
|
|
606
|
-
forceRootConfig: false,
|
|
607
|
-
strictDoctor: false,
|
|
608
|
-
plannerModel: null,
|
|
609
|
-
builderModel: null,
|
|
610
|
-
finalReviewerModel: null
|
|
611
|
-
};
|
|
612
|
-
for (let i = 0; i < argv.length; i++) {
|
|
613
|
-
const arg = argv[i];
|
|
614
|
-
if (arg === "--json") options.json = true;
|
|
615
|
-
else if (arg === "--replace-managed") options.replaceManaged = true;
|
|
616
|
-
else if (arg === "--purge-config") options.purgeConfig = true;
|
|
617
|
-
else if (arg === "--force-config") options.forceConfig = true;
|
|
618
|
-
else if (arg === "--force-root-config") options.forceRootConfig = true;
|
|
619
|
-
else if (arg === "--strict-doctor") options.strictDoctor = true;
|
|
620
|
-
else if (arg === "--root") options.rootPath = argv[++i];
|
|
621
|
-
else if (arg === "--profile") {
|
|
622
|
-
const value = argv[++i];
|
|
623
|
-
if (value === void 0) {
|
|
624
|
-
return { error: "--profile requires a value" };
|
|
625
|
-
}
|
|
626
|
-
if (value === "core") {
|
|
627
|
-
return {
|
|
628
|
-
error: "the 'core' profile was removed in opencode-ship 1.1.0; only 'engineering' is supported. Run /setup-ship-workflow to migrate."
|
|
629
|
-
};
|
|
630
|
-
}
|
|
631
|
-
if (!isValidProfile(value)) {
|
|
632
|
-
return { error: `unknown profile '${value}' (expected one of: ${PROFILES.join(", ")})` };
|
|
633
|
-
}
|
|
634
|
-
options.profile = value;
|
|
635
|
-
} else if (arg === "--planner-model" || arg === "--builder-model" || arg === "--final-reviewer-model") {
|
|
636
|
-
const value = argv[++i];
|
|
637
|
-
if (value === void 0) return { error: `${arg} requires a value` };
|
|
638
|
-
if (!MODEL_ID_RE.test(value)) {
|
|
639
|
-
return { error: `${arg} must be a "<provider>/<model>" id, got ${JSON.stringify(value)}` };
|
|
640
|
-
}
|
|
641
|
-
if (arg === "--planner-model") options.plannerModel = value;
|
|
642
|
-
else if (arg === "--builder-model") options.builderModel = value;
|
|
643
|
-
else options.finalReviewerModel = value;
|
|
644
|
-
} else if (arg === "-h" || arg === "--help") return { help: true };
|
|
645
|
-
else if (arg === "-v" || arg === "--version") return { version: true };
|
|
646
|
-
else return { error: `unknown flag ${arg}` };
|
|
894
|
+
// src/installer/root-config.js
|
|
895
|
+
var root_config_exports = {};
|
|
896
|
+
__export(root_config_exports, {
|
|
897
|
+
PLAN_MODE_POINTER: () => PLAN_MODE_POINTER,
|
|
898
|
+
POINTER_ENTRIES: () => POINTER_ENTRIES,
|
|
899
|
+
applyOwnedPointers: () => applyOwnedPointers,
|
|
900
|
+
applyPlanModeOwnership: () => applyPlanModeOwnership,
|
|
901
|
+
defaultRootConfigPath: () => defaultRootConfigPath,
|
|
902
|
+
findRootConfig: () => findRootConfig,
|
|
903
|
+
formatRootConfig: () => formatRootConfig,
|
|
904
|
+
formatRootConfigPreserving: () => formatRootConfigPreserving,
|
|
905
|
+
parseRootConfigPreservingOrder: () => parseRootConfigPreservingOrder,
|
|
906
|
+
planModeBlock: () => planModeBlock,
|
|
907
|
+
readRootConfig: () => readRootConfig,
|
|
908
|
+
synthesizeDefaultRootConfig: () => synthesizeDefaultRootConfig
|
|
909
|
+
});
|
|
910
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
|
|
911
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
912
|
+
import { resolve as resolve5 } from "node:path";
|
|
913
|
+
function findRootConfig(repoRoot) {
|
|
914
|
+
for (const rel of ROOT_PATH_CANDIDATES) {
|
|
915
|
+
const abs = resolve5(repoRoot, rel);
|
|
916
|
+
if (existsSync5(abs)) return { path: abs, relative: rel, format: rel.endsWith(".jsonc") ? "jsonc" : "json" };
|
|
647
917
|
}
|
|
648
|
-
return
|
|
918
|
+
return { path: null, relative: ROOT_PATH_CANDIDATES[0], format: "json" };
|
|
649
919
|
}
|
|
650
|
-
function
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
920
|
+
function defaultRootConfigPath(repoRoot) {
|
|
921
|
+
return resolve5(repoRoot, ROOT_PATH_CANDIDATES[0]);
|
|
922
|
+
}
|
|
923
|
+
function readRootConfig(absPath) {
|
|
924
|
+
if (!existsSync5(absPath)) {
|
|
925
|
+
return { ok: false, error: { kind: "missing", path: absPath } };
|
|
654
926
|
}
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
927
|
+
const raw = readFileSync3(absPath, "utf8");
|
|
928
|
+
const stripped = stripJsonc(raw);
|
|
929
|
+
try {
|
|
930
|
+
const value = JSON.parse(stripped);
|
|
931
|
+
return {
|
|
932
|
+
ok: true,
|
|
933
|
+
path: absPath,
|
|
934
|
+
raw,
|
|
935
|
+
sha256: bytesHashString(raw),
|
|
936
|
+
value,
|
|
937
|
+
before: snapshotValues(value),
|
|
938
|
+
format: absPath.endsWith(".jsonc") ? "jsonc" : "json"
|
|
939
|
+
};
|
|
940
|
+
} catch (e) {
|
|
941
|
+
return { ok: false, error: { kind: "parse", path: absPath, message: e.message } };
|
|
670
942
|
}
|
|
671
943
|
}
|
|
672
|
-
function
|
|
673
|
-
|
|
944
|
+
function snapshotValues(doc) {
|
|
945
|
+
const out = {};
|
|
946
|
+
for (const entry of POINTER_ENTRIES) {
|
|
947
|
+
out[entry.pointer] = getPointer(doc, entry.pointer);
|
|
948
|
+
}
|
|
949
|
+
return out;
|
|
674
950
|
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
// src/installer/package-root.js
|
|
691
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
692
|
-
import { fileURLToPath } from "node:url";
|
|
693
|
-
import { dirname, resolve } from "node:path";
|
|
694
|
-
var PACKAGE_NAME = "opencode-ship";
|
|
695
|
-
function resolvePackageRoot(startUrl) {
|
|
696
|
-
let candidate = dirname(fileURLToPath(startUrl ?? import.meta.url));
|
|
697
|
-
while (candidate && candidate !== "/") {
|
|
698
|
-
const pkgPath = resolve(candidate, "package.json");
|
|
699
|
-
if (existsSync(pkgPath)) {
|
|
700
|
-
try {
|
|
701
|
-
const raw = readFileSync(pkgPath, "utf8");
|
|
702
|
-
const pkg = JSON.parse(raw);
|
|
703
|
-
if (pkg && pkg.name === PACKAGE_NAME) return candidate;
|
|
704
|
-
} catch {
|
|
951
|
+
function stripJsonc(text) {
|
|
952
|
+
let stripped = "";
|
|
953
|
+
let i = 0;
|
|
954
|
+
let inString = false;
|
|
955
|
+
let escape = false;
|
|
956
|
+
while (i < text.length) {
|
|
957
|
+
const ch = text[i];
|
|
958
|
+
if (inString) {
|
|
959
|
+
stripped += ch;
|
|
960
|
+
if (escape) {
|
|
961
|
+
escape = false;
|
|
962
|
+
} else if (ch === "\\") {
|
|
963
|
+
escape = true;
|
|
964
|
+
} else if (ch === '"') {
|
|
965
|
+
inString = false;
|
|
705
966
|
}
|
|
967
|
+
i += 1;
|
|
968
|
+
continue;
|
|
706
969
|
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
// src/version.js
|
|
713
|
-
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "node:fs";
|
|
714
|
-
import { dirname as dirname2, resolve as resolve2 } from "node:path";
|
|
715
|
-
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
716
|
-
var PACKAGE_VERSION = "1.1.0";
|
|
717
|
-
var TEMPLATE_SET = `v${PACKAGE_VERSION}`;
|
|
718
|
-
|
|
719
|
-
// src/installer/catalog.js
|
|
720
|
-
var TEMPLATE_SET_ID = TEMPLATE_SET;
|
|
721
|
-
var packageRoot = resolvePackageRoot(import.meta.url);
|
|
722
|
-
var MATT_SKILLS = [
|
|
723
|
-
"setup-engineering-workflow",
|
|
724
|
-
"engineering-workflow",
|
|
725
|
-
"grilling",
|
|
726
|
-
"domain-modeling",
|
|
727
|
-
"grill-with-docs",
|
|
728
|
-
"triage",
|
|
729
|
-
"to-spec",
|
|
730
|
-
"to-tickets",
|
|
731
|
-
"wayfinder",
|
|
732
|
-
"handoff",
|
|
733
|
-
"research",
|
|
734
|
-
"prototype",
|
|
735
|
-
"codebase-design",
|
|
736
|
-
"code-review"
|
|
737
|
-
];
|
|
738
|
-
var SUPER_SKILLS = [
|
|
739
|
-
"brainstorming",
|
|
740
|
-
"writing-plans",
|
|
741
|
-
"executing-plans",
|
|
742
|
-
"subagent-driven-development",
|
|
743
|
-
"dispatching-parallel-agents",
|
|
744
|
-
"test-driven-development",
|
|
745
|
-
"systematic-debugging",
|
|
746
|
-
"verification-before-completion",
|
|
747
|
-
"requesting-code-review",
|
|
748
|
-
"receiving-code-review"
|
|
749
|
-
];
|
|
750
|
-
var ENGINEERING_AGENTS = [
|
|
751
|
-
"ship-controller",
|
|
752
|
-
"ship-planner",
|
|
753
|
-
"ship-task-builder",
|
|
754
|
-
"ship-task-reviewer",
|
|
755
|
-
"ship-final-standards-reviewer",
|
|
756
|
-
"ship-final-spec-reviewer"
|
|
757
|
-
];
|
|
758
|
-
var ENGINEERING_COMMANDS = [
|
|
759
|
-
"ship-deliver",
|
|
760
|
-
"ship-resume",
|
|
761
|
-
"ship-status"
|
|
762
|
-
];
|
|
763
|
-
var CATALOG = [
|
|
764
|
-
{
|
|
765
|
-
id: "plugin:opencode-ship",
|
|
766
|
-
kind: "plugin",
|
|
767
|
-
path: ".opencode/plugins/opencode-ship.js",
|
|
768
|
-
source: resolve3(packageRoot, "dist/plugin.js"),
|
|
769
|
-
mode: 420,
|
|
770
|
-
profiles: ["engineering"]
|
|
771
|
-
},
|
|
772
|
-
{
|
|
773
|
-
id: "agent:delivery-reviewer",
|
|
774
|
-
kind: "agent",
|
|
775
|
-
path: ".opencode/agents/delivery-reviewer.md",
|
|
776
|
-
source: resolve3(packageRoot, "assets/agents/delivery-reviewer.md"),
|
|
777
|
-
mode: 420,
|
|
778
|
-
profiles: ["engineering"]
|
|
779
|
-
},
|
|
780
|
-
{
|
|
781
|
-
id: "agent:delivery-verifier",
|
|
782
|
-
kind: "agent",
|
|
783
|
-
path: ".opencode/agents/delivery-verifier.md",
|
|
784
|
-
source: resolve3(packageRoot, "assets/agents/delivery-verifier.md"),
|
|
785
|
-
mode: 420,
|
|
786
|
-
profiles: ["engineering"]
|
|
787
|
-
},
|
|
788
|
-
...ENGINEERING_AGENTS.map((name) => ({
|
|
789
|
-
id: `agent:${name}`,
|
|
790
|
-
kind: "agent",
|
|
791
|
-
path: `.opencode/agents/${name}.md`,
|
|
792
|
-
source: resolve3(packageRoot, `assets/agents/${name}.md`),
|
|
793
|
-
mode: 420,
|
|
794
|
-
profiles: ["engineering"]
|
|
795
|
-
})),
|
|
796
|
-
...ENGINEERING_COMMANDS.map((name) => ({
|
|
797
|
-
id: `command:${name}`,
|
|
798
|
-
kind: "support",
|
|
799
|
-
path: `.opencode/commands/${name}.md`,
|
|
800
|
-
source: resolve3(packageRoot, `assets/commands/${name}.md`),
|
|
801
|
-
mode: 420,
|
|
802
|
-
profiles: ["engineering"]
|
|
803
|
-
})),
|
|
804
|
-
{
|
|
805
|
-
id: "skill:delivery-workflow",
|
|
806
|
-
kind: "skill",
|
|
807
|
-
path: ".opencode/skills/delivery-workflow/SKILL.md",
|
|
808
|
-
source: resolve3(packageRoot, "assets/skills/delivery-workflow/SKILL.md"),
|
|
809
|
-
mode: 420,
|
|
810
|
-
profiles: ["engineering"]
|
|
811
|
-
},
|
|
812
|
-
{
|
|
813
|
-
id: "skill:planning-research-checkpoint",
|
|
814
|
-
kind: "skill",
|
|
815
|
-
path: ".opencode/skills/planning-research-checkpoint/SKILL.md",
|
|
816
|
-
source: resolve3(packageRoot, "assets/skills/planning-research-checkpoint/SKILL.md"),
|
|
817
|
-
mode: 420,
|
|
818
|
-
profiles: ["engineering"]
|
|
819
|
-
},
|
|
820
|
-
...MATT_SKILLS.map((name) => ({
|
|
821
|
-
id: `skill:matt:${name}`,
|
|
822
|
-
kind: "skill",
|
|
823
|
-
path: `.opencode/skills/${name}/SKILL.md`,
|
|
824
|
-
source: resolve3(packageRoot, `assets/skills/${name}/SKILL.md`),
|
|
825
|
-
mode: 420,
|
|
826
|
-
profiles: ["engineering"]
|
|
827
|
-
})),
|
|
828
|
-
...SUPER_SKILLS.map((name) => ({
|
|
829
|
-
id: `skill:super:${name}`,
|
|
830
|
-
kind: "skill",
|
|
831
|
-
path: `.opencode/skills/${name}/SKILL.md`,
|
|
832
|
-
source: resolve3(packageRoot, `assets/skills/${name}/SKILL.md`),
|
|
833
|
-
mode: 420,
|
|
834
|
-
profiles: ["engineering"]
|
|
835
|
-
})),
|
|
836
|
-
{
|
|
837
|
-
id: "skill:setup-ship-workflow",
|
|
838
|
-
kind: "skill",
|
|
839
|
-
path: ".opencode/skills/setup-ship-workflow/SKILL.md",
|
|
840
|
-
source: resolve3(packageRoot, "assets/skills/setup-engineering-workflow/SKILL.md"),
|
|
841
|
-
mode: 420,
|
|
842
|
-
profiles: ["engineering"]
|
|
843
|
-
},
|
|
844
|
-
{
|
|
845
|
-
id: "skill:skill-discovery",
|
|
846
|
-
kind: "skill",
|
|
847
|
-
path: ".opencode/skills/skill-discovery/SKILL.md",
|
|
848
|
-
source: resolve3(packageRoot, "assets/skills/skill-discovery/SKILL.md"),
|
|
849
|
-
mode: 420,
|
|
850
|
-
profiles: ["engineering"]
|
|
851
|
-
},
|
|
852
|
-
{
|
|
853
|
-
id: "command:setup-ship-workflow",
|
|
854
|
-
kind: "support",
|
|
855
|
-
path: ".opencode/commands/setup-ship-workflow.md",
|
|
856
|
-
source: resolve3(packageRoot, "assets/commands/setup-ship-workflow.md"),
|
|
857
|
-
mode: 420,
|
|
858
|
-
profiles: ["engineering"]
|
|
859
|
-
}
|
|
860
|
-
];
|
|
861
|
-
function filterCatalogByProfile(catalog, profile) {
|
|
862
|
-
const effective = profile === void 0 || profile === null ? DEFAULT_PROFILE : profile;
|
|
863
|
-
if (!isValidProfile(effective)) {
|
|
864
|
-
throw new Error(
|
|
865
|
-
`filterCatalogByProfile: unknown profile '${profile}' (expected one of: ${PROFILES.join(", ")})`
|
|
866
|
-
);
|
|
867
|
-
}
|
|
868
|
-
return catalog.filter((entry) => Array.isArray(entry.profiles) && entry.profiles.includes(effective));
|
|
869
|
-
}
|
|
870
|
-
var ALLOWED_KINDS = /* @__PURE__ */ new Set(["plugin", "agent", "skill", "support"]);
|
|
871
|
-
function validateCatalog({ catalog = CATALOG } = {}) {
|
|
872
|
-
const seenIds = /* @__PURE__ */ new Set();
|
|
873
|
-
const seenPaths = /* @__PURE__ */ new Set();
|
|
874
|
-
const issues = [];
|
|
875
|
-
for (const entry of catalog) {
|
|
876
|
-
if (!entry || typeof entry !== "object") {
|
|
877
|
-
issues.push({ id: null, kind: "shape", message: "catalog entry is not an object" });
|
|
970
|
+
if (ch === '"') {
|
|
971
|
+
inString = true;
|
|
972
|
+
stripped += ch;
|
|
973
|
+
i += 1;
|
|
878
974
|
continue;
|
|
879
975
|
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
} else if (seenIds.has(id)) {
|
|
884
|
-
issues.push({ id, kind: "duplicate-id", message: `duplicate catalog id: ${id}` });
|
|
885
|
-
} else {
|
|
886
|
-
seenIds.add(id);
|
|
976
|
+
if (ch === "/" && text[i + 1] === "/") {
|
|
977
|
+
while (i < text.length && text[i] !== "\n") i += 1;
|
|
978
|
+
continue;
|
|
887
979
|
}
|
|
888
|
-
if (
|
|
889
|
-
|
|
980
|
+
if (ch === "/" && text[i + 1] === "*") {
|
|
981
|
+
i += 2;
|
|
982
|
+
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i += 1;
|
|
983
|
+
i += 2;
|
|
984
|
+
continue;
|
|
890
985
|
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
986
|
+
stripped += ch;
|
|
987
|
+
i += 1;
|
|
988
|
+
}
|
|
989
|
+
return stripped.replace(/,\s*([}\]])/g, "$1");
|
|
990
|
+
}
|
|
991
|
+
function applyOwnedPointers(rootDoc, { pointerEntries = POINTER_ENTRIES, allowEqualValues = true } = {}) {
|
|
992
|
+
const result = { doc: rootDoc, applied: [], skipped: [] };
|
|
993
|
+
let doc = rootDoc;
|
|
994
|
+
for (const entry of pointerEntries) {
|
|
995
|
+
const existing = getPointer(doc, entry.pointer);
|
|
996
|
+
if (existing === void 0) {
|
|
997
|
+
doc = setPointer(doc, entry.pointer, entry.value);
|
|
998
|
+
result.applied.push({ pointer: entry.pointer, value: entry.value });
|
|
999
|
+
continue;
|
|
895
1000
|
}
|
|
896
|
-
if (
|
|
897
|
-
|
|
1001
|
+
if (existing === entry.value || stableStringify(existing) === stableStringify(entry.value)) {
|
|
1002
|
+
if (allowEqualValues) {
|
|
1003
|
+
result.skipped.push({ pointer: entry.pointer, reason: "already equal" });
|
|
1004
|
+
}
|
|
1005
|
+
continue;
|
|
898
1006
|
}
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
1007
|
+
result.skipped.push({
|
|
1008
|
+
pointer: entry.pointer,
|
|
1009
|
+
reason: "different existing value",
|
|
1010
|
+
existing,
|
|
1011
|
+
desired: entry.value
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
return result;
|
|
1015
|
+
}
|
|
1016
|
+
function applyPlanModeOwnership(rootDoc, { pointer = PLAN_MODE_POINTER, block = planModePermissions().build } = {}) {
|
|
1017
|
+
const previous = getPointer(rootDoc, pointer);
|
|
1018
|
+
const doc = setPointer(rootDoc, pointer, block);
|
|
1019
|
+
return { doc, previous: previous === void 0 ? null : previous, id: pointer };
|
|
1020
|
+
}
|
|
1021
|
+
function planModeBlock() {
|
|
1022
|
+
return planModePermissions().build;
|
|
1023
|
+
}
|
|
1024
|
+
function synthesizeDefaultRootConfig() {
|
|
1025
|
+
return {
|
|
1026
|
+
$schema: "https://opencode.ai/config.json",
|
|
1027
|
+
agent: {
|
|
1028
|
+
build: {
|
|
1029
|
+
permission: {
|
|
1030
|
+
delivery_inspect: "allow",
|
|
1031
|
+
delivery_issue: "allow",
|
|
1032
|
+
delivery_worktree: "allow",
|
|
1033
|
+
delivery_verify: "deny",
|
|
1034
|
+
delivery_review: "deny",
|
|
1035
|
+
delivery_pr: "allow",
|
|
1036
|
+
delivery_ready: "allow",
|
|
1037
|
+
delivery_merge: "ask",
|
|
1038
|
+
delivery_cleanup: "allow",
|
|
1039
|
+
task: {
|
|
1040
|
+
"delivery-reviewer": "allow",
|
|
1041
|
+
"delivery-verifier": "allow"
|
|
1042
|
+
}
|
|
910
1043
|
}
|
|
911
|
-
} catch (e) {
|
|
912
|
-
issues.push({ id, kind: "source-stat", message: `unable to stat source: ${e?.message ?? e}` });
|
|
913
|
-
}
|
|
914
|
-
const rel = relative(packageRoot, source);
|
|
915
|
-
if (rel.startsWith("..")) {
|
|
916
|
-
issues.push({ id, kind: "source-out-of-package", message: `source escapes package root: ${source}` });
|
|
917
1044
|
}
|
|
918
1045
|
}
|
|
919
|
-
|
|
920
|
-
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
function formatRootConfig(value) {
|
|
1049
|
+
return JSON.stringify(stripSourceOrder(value), null, 2) + "\n";
|
|
1050
|
+
}
|
|
1051
|
+
function stripSourceOrder(value) {
|
|
1052
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
1053
|
+
const out = Array.isArray(value) ? [] : {};
|
|
1054
|
+
const order = Array.isArray(value.__sourceOrder__) ? value.__sourceOrder__ : null;
|
|
1055
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1056
|
+
if (order) {
|
|
1057
|
+
for (const k of order) {
|
|
1058
|
+
if (k === "__sourceOrder__") continue;
|
|
1059
|
+
if (!(k in value)) continue;
|
|
1060
|
+
seen.add(k);
|
|
1061
|
+
out[k] = stripSourceOrder(value[k]);
|
|
921
1062
|
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1063
|
+
}
|
|
1064
|
+
for (const k of Object.keys(value)) {
|
|
1065
|
+
if (k === "__sourceOrder__") continue;
|
|
1066
|
+
if (seen.has(k)) continue;
|
|
1067
|
+
out[k] = stripSourceOrder(value[k]);
|
|
1068
|
+
}
|
|
1069
|
+
return out;
|
|
1070
|
+
}
|
|
1071
|
+
function formatRootConfigPreserving(value) {
|
|
1072
|
+
return formatRootConfig(value);
|
|
1073
|
+
}
|
|
1074
|
+
function parseRootConfigPreservingOrder(text) {
|
|
1075
|
+
if (typeof text !== "string" || text.length === 0) {
|
|
1076
|
+
return { value: {}, format: "json" };
|
|
1077
|
+
}
|
|
1078
|
+
const parser = new RootConfigParser(text);
|
|
1079
|
+
const value = parser.parseValue(
|
|
1080
|
+
0,
|
|
1081
|
+
/*atTop*/
|
|
1082
|
+
true
|
|
1083
|
+
);
|
|
1084
|
+
const isJsonc = text.includes("//") || text.includes("/*");
|
|
1085
|
+
return { value, format: isJsonc ? "jsonc" : "json" };
|
|
1086
|
+
}
|
|
1087
|
+
var POINTER_ENTRIES, ROOT_PATH_CANDIDATES, PLAN_MODE_POINTER, RootConfigParser;
|
|
1088
|
+
var init_root_config = __esm({
|
|
1089
|
+
"src/installer/root-config.js"() {
|
|
1090
|
+
init_json_pointer();
|
|
1091
|
+
init_hash();
|
|
1092
|
+
init_plan_mode_permissions();
|
|
1093
|
+
POINTER_ENTRIES = [
|
|
1094
|
+
{
|
|
1095
|
+
pointer: "/agent/build/permission/delivery_inspect",
|
|
1096
|
+
strategy: "value",
|
|
1097
|
+
value: "allow"
|
|
1098
|
+
},
|
|
1099
|
+
{
|
|
1100
|
+
pointer: "/agent/build/permission/delivery_issue",
|
|
1101
|
+
strategy: "value",
|
|
1102
|
+
value: "allow"
|
|
1103
|
+
},
|
|
1104
|
+
{
|
|
1105
|
+
pointer: "/agent/build/permission/delivery_worktree",
|
|
1106
|
+
strategy: "value",
|
|
1107
|
+
value: "allow"
|
|
1108
|
+
},
|
|
1109
|
+
{
|
|
1110
|
+
pointer: "/agent/build/permission/delivery_verify",
|
|
1111
|
+
strategy: "value",
|
|
1112
|
+
value: "deny"
|
|
1113
|
+
},
|
|
1114
|
+
{
|
|
1115
|
+
pointer: "/agent/build/permission/delivery_review",
|
|
1116
|
+
strategy: "value",
|
|
1117
|
+
value: "deny"
|
|
1118
|
+
},
|
|
1119
|
+
{
|
|
1120
|
+
pointer: "/agent/build/permission/delivery_pr",
|
|
1121
|
+
strategy: "value",
|
|
1122
|
+
value: "allow"
|
|
1123
|
+
},
|
|
1124
|
+
{
|
|
1125
|
+
pointer: "/agent/build/permission/delivery_ready",
|
|
1126
|
+
strategy: "value",
|
|
1127
|
+
value: "allow"
|
|
1128
|
+
},
|
|
1129
|
+
{
|
|
1130
|
+
pointer: "/agent/build/permission/delivery_merge",
|
|
1131
|
+
strategy: "value",
|
|
1132
|
+
value: "ask"
|
|
1133
|
+
},
|
|
1134
|
+
{
|
|
1135
|
+
pointer: "/agent/build/permission/delivery_cleanup",
|
|
1136
|
+
strategy: "value",
|
|
1137
|
+
value: "allow"
|
|
1138
|
+
},
|
|
1139
|
+
{
|
|
1140
|
+
pointer: "/agent/build/permission/task/delivery-reviewer",
|
|
1141
|
+
strategy: "value",
|
|
1142
|
+
value: "allow"
|
|
1143
|
+
},
|
|
1144
|
+
{
|
|
1145
|
+
pointer: "/agent/build/permission/task/delivery-verifier",
|
|
1146
|
+
strategy: "value",
|
|
1147
|
+
value: "allow"
|
|
1148
|
+
}
|
|
1149
|
+
];
|
|
1150
|
+
ROOT_PATH_CANDIDATES = ["opencode.json", "opencode.jsonc"];
|
|
1151
|
+
PLAN_MODE_POINTER = "/agent/plan/permission";
|
|
1152
|
+
RootConfigParser = class {
|
|
1153
|
+
constructor(text) {
|
|
1154
|
+
this.text = text;
|
|
1155
|
+
this.pos = 0;
|
|
1156
|
+
}
|
|
1157
|
+
skipWS() {
|
|
1158
|
+
while (this.pos < this.text.length) {
|
|
1159
|
+
const ch = this.text[this.pos];
|
|
1160
|
+
if (ch === " " || ch === "\n" || ch === " " || ch === "\r") {
|
|
1161
|
+
this.pos += 1;
|
|
1162
|
+
continue;
|
|
1163
|
+
}
|
|
1164
|
+
if (ch === "/" && this.text[this.pos + 1] === "/") {
|
|
1165
|
+
while (this.pos < this.text.length && this.text[this.pos] !== "\n") this.pos += 1;
|
|
1166
|
+
continue;
|
|
1167
|
+
}
|
|
1168
|
+
if (ch === "/" && this.text[this.pos + 1] === "*") {
|
|
1169
|
+
this.pos += 2;
|
|
1170
|
+
while (this.pos < this.text.length && !(this.text[this.pos] === "*" && this.text[this.pos + 1] === "/")) this.pos += 1;
|
|
1171
|
+
this.pos += 2;
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
1174
|
+
break;
|
|
928
1175
|
}
|
|
929
1176
|
}
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
import { dirname as dirname3, resolve as resolve4 } from "node:path";
|
|
951
|
-
|
|
952
|
-
// schema/ship-config.schema.json
|
|
953
|
-
var ship_config_schema_default = {
|
|
954
|
-
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
955
|
-
$id: "https://github.com/Viktorxyz/opencode-ship/schema/ship-config.schema.json",
|
|
956
|
-
title: "opencode-ship user config",
|
|
957
|
-
type: "object",
|
|
958
|
-
required: ["schemaVersion"],
|
|
959
|
-
additionalProperties: false,
|
|
960
|
-
properties: {
|
|
961
|
-
schemaVersion: { enum: [1, 2] },
|
|
962
|
-
profile: {
|
|
963
|
-
type: "string",
|
|
964
|
-
enum: ["engineering"],
|
|
965
|
-
description: "Active profile. Engineering is the only supported profile in 1.1.0."
|
|
966
|
-
},
|
|
967
|
-
owner: {
|
|
968
|
-
type: "string",
|
|
969
|
-
description: "Optional override for the issue/manifest owner field. Defaults to the agent's local user.name."
|
|
970
|
-
},
|
|
971
|
-
project: {
|
|
972
|
-
type: "object",
|
|
973
|
-
additionalProperties: false,
|
|
974
|
-
properties: {
|
|
975
|
-
remote: { type: "string", minLength: 1 },
|
|
976
|
-
repository: { type: "string", pattern: "^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$" },
|
|
977
|
-
defaultBranch: { type: "string", minLength: 1 },
|
|
978
|
-
packageManager: { enum: ["npm", "pnpm", "yarn", "bun"] },
|
|
979
|
-
detectOverrides: { type: "boolean", description: "Permit detection to refresh previously persisted values." }
|
|
1177
|
+
parseValue(depth, atTop) {
|
|
1178
|
+
this.skipWS();
|
|
1179
|
+
const ch = this.text[this.pos];
|
|
1180
|
+
if (ch === "{") return this.parseObject(depth, atTop);
|
|
1181
|
+
if (ch === "[") return this.parseArray(depth);
|
|
1182
|
+
if (ch === '"') return this.parseString();
|
|
1183
|
+
if (ch === "-" || ch >= "0" && ch <= "9") return this.parseNumber();
|
|
1184
|
+
if (this.text.startsWith("true", this.pos)) {
|
|
1185
|
+
this.pos += 4;
|
|
1186
|
+
return true;
|
|
1187
|
+
}
|
|
1188
|
+
if (this.text.startsWith("false", this.pos)) {
|
|
1189
|
+
this.pos += 5;
|
|
1190
|
+
return false;
|
|
1191
|
+
}
|
|
1192
|
+
if (this.text.startsWith("null", this.pos)) {
|
|
1193
|
+
this.pos += 4;
|
|
1194
|
+
return null;
|
|
1195
|
+
}
|
|
1196
|
+
throw new Error(`unexpected token at ${this.pos}: ${this.text.slice(this.pos, this.pos + 8)}`);
|
|
980
1197
|
}
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
root: { type: "string", minLength: 1 },
|
|
991
|
-
branchTemplate: { type: "string", minLength: 1 },
|
|
992
|
-
bootstrap: {
|
|
993
|
-
type: "array",
|
|
994
|
-
items: {
|
|
995
|
-
type: "array",
|
|
996
|
-
items: { type: "string", minLength: 1 },
|
|
997
|
-
minItems: 1
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
},
|
|
1002
|
-
verification: {
|
|
1003
|
-
type: "object",
|
|
1004
|
-
additionalProperties: false,
|
|
1005
|
-
properties: {
|
|
1006
|
-
commands: {
|
|
1007
|
-
type: "array",
|
|
1008
|
-
minItems: 1,
|
|
1009
|
-
items: {
|
|
1010
|
-
type: "object",
|
|
1011
|
-
required: ["id", "argv"],
|
|
1012
|
-
additionalProperties: false,
|
|
1013
|
-
properties: {
|
|
1014
|
-
id: { type: "string", minLength: 1 },
|
|
1015
|
-
argv: {
|
|
1016
|
-
type: "array",
|
|
1017
|
-
items: { type: "string", minLength: 1 },
|
|
1018
|
-
minItems: 1
|
|
1019
|
-
},
|
|
1020
|
-
timeoutMs: { type: "integer", minimum: 1 }
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
},
|
|
1024
|
-
requireCleanDiffAfter: { type: "boolean" },
|
|
1025
|
-
invalidateOnHeadChange: { type: "boolean" }
|
|
1198
|
+
parseObject(depth, atTop) {
|
|
1199
|
+
const out = /* @__PURE__ */ Object.create(null);
|
|
1200
|
+
out.__sourceOrder__ = [];
|
|
1201
|
+
this.pos += 1;
|
|
1202
|
+
while (this.pos < this.text.length) {
|
|
1203
|
+
this.skipWS();
|
|
1204
|
+
if (this.text[this.pos] === "}") {
|
|
1205
|
+
this.pos += 1;
|
|
1206
|
+
return out;
|
|
1026
1207
|
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1208
|
+
const key = this.parseString();
|
|
1209
|
+
out.__sourceOrder__.push(key);
|
|
1210
|
+
this.skipWS();
|
|
1211
|
+
if (this.text[this.pos] !== ":") throw new Error(`expected : at ${this.pos}`);
|
|
1212
|
+
this.pos += 1;
|
|
1213
|
+
out[key] = this.parseValue(depth + 1, false);
|
|
1214
|
+
this.skipWS();
|
|
1215
|
+
if (this.text[this.pos] === ",") {
|
|
1216
|
+
this.pos += 1;
|
|
1217
|
+
continue;
|
|
1035
1218
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
additionalProperties: false,
|
|
1040
|
-
properties: {
|
|
1041
|
-
driver: { const: "github-status-checks" },
|
|
1042
|
-
requiredChecks: {
|
|
1043
|
-
type: "array",
|
|
1044
|
-
items: { type: "string", minLength: 1 },
|
|
1045
|
-
uniqueItems: true
|
|
1046
|
-
},
|
|
1047
|
-
wait: { type: "boolean" },
|
|
1048
|
-
flakyRetry: { type: "integer", enum: [0, 1] }
|
|
1219
|
+
if (this.text[this.pos] === "}") {
|
|
1220
|
+
this.pos += 1;
|
|
1221
|
+
return out;
|
|
1049
1222
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1223
|
+
throw new Error(`expected , or } at ${this.pos}`);
|
|
1224
|
+
}
|
|
1225
|
+
throw new Error("unterminated object");
|
|
1226
|
+
}
|
|
1227
|
+
parseArray(depth) {
|
|
1228
|
+
const out = [];
|
|
1229
|
+
this.pos += 1;
|
|
1230
|
+
while (this.pos < this.text.length) {
|
|
1231
|
+
this.skipWS();
|
|
1232
|
+
if (this.text[this.pos] === "]") {
|
|
1233
|
+
this.pos += 1;
|
|
1234
|
+
return out;
|
|
1061
1235
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
strategy: { const: "squash" },
|
|
1068
|
-
policy: { const: "explicit-user-request-only" },
|
|
1069
|
-
requireFreshGates: { type: "boolean" }
|
|
1236
|
+
out.push(this.parseValue(depth + 1, false));
|
|
1237
|
+
this.skipWS();
|
|
1238
|
+
if (this.text[this.pos] === ",") {
|
|
1239
|
+
this.pos += 1;
|
|
1240
|
+
continue;
|
|
1070
1241
|
}
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
additionalProperties: false,
|
|
1075
|
-
properties: {
|
|
1076
|
-
when: { const: "next-task" },
|
|
1077
|
-
requireUnpublishedGuard: { type: "boolean" }
|
|
1242
|
+
if (this.text[this.pos] === "]") {
|
|
1243
|
+
this.pos += 1;
|
|
1244
|
+
return out;
|
|
1078
1245
|
}
|
|
1246
|
+
throw new Error(`expected , or ] at ${this.pos}`);
|
|
1079
1247
|
}
|
|
1248
|
+
throw new Error("unterminated array");
|
|
1080
1249
|
}
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
workflow: {
|
|
1093
|
-
type: "object",
|
|
1094
|
-
description: "Workflow configuration. Models are optional at write time; the setup-ship-workflow skill fills them in. Once all three are present, ship-deliver can start.",
|
|
1095
|
-
additionalProperties: false,
|
|
1096
|
-
properties: {
|
|
1097
|
-
models: {
|
|
1098
|
-
type: "object",
|
|
1099
|
-
additionalProperties: false,
|
|
1100
|
-
description: "Optional model roles. All three roles must be present before ship-deliver can run.",
|
|
1101
|
-
properties: {
|
|
1102
|
-
planner: {
|
|
1103
|
-
type: "string",
|
|
1104
|
-
pattern: "^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$",
|
|
1105
|
-
description: "Provider/model id for the strong planning child session."
|
|
1106
|
-
},
|
|
1107
|
-
builder: {
|
|
1108
|
-
type: "string",
|
|
1109
|
-
pattern: "^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$",
|
|
1110
|
-
description: "Provider/model id for the cheap builder child session."
|
|
1111
|
-
},
|
|
1112
|
-
finalReviewer: {
|
|
1113
|
-
type: "string",
|
|
1114
|
-
pattern: "^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$",
|
|
1115
|
-
description: "Provider/model id for the final Standards + Spec reviewers."
|
|
1116
|
-
}
|
|
1250
|
+
parseString() {
|
|
1251
|
+
if (this.text[this.pos] !== '"') throw new Error(`expected " at ${this.pos}`);
|
|
1252
|
+
this.pos += 1;
|
|
1253
|
+
let out = "";
|
|
1254
|
+
while (this.pos < this.text.length) {
|
|
1255
|
+
const ch = this.text[this.pos];
|
|
1256
|
+
if (ch === "\\") {
|
|
1257
|
+
const next = this.text[this.pos + 1];
|
|
1258
|
+
out += ch + next;
|
|
1259
|
+
this.pos += 2;
|
|
1260
|
+
continue;
|
|
1117
1261
|
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
additionalProperties: false,
|
|
1122
|
-
properties: {
|
|
1123
|
-
mirrorToIssue: { const: true },
|
|
1124
|
-
maxFailedRounds: { const: 3 }
|
|
1262
|
+
if (ch === '"') {
|
|
1263
|
+
this.pos += 1;
|
|
1264
|
+
return JSON.parse('"' + out + '"');
|
|
1125
1265
|
}
|
|
1266
|
+
out += ch;
|
|
1267
|
+
this.pos += 1;
|
|
1126
1268
|
}
|
|
1269
|
+
throw new Error("unterminated string");
|
|
1127
1270
|
}
|
|
1128
|
-
|
|
1271
|
+
parseNumber() {
|
|
1272
|
+
const start = this.pos;
|
|
1273
|
+
if (this.text[this.pos] === "-") this.pos += 1;
|
|
1274
|
+
while (this.pos < this.text.length && /[0-9.eE+\-]/.test(this.text[this.pos])) this.pos += 1;
|
|
1275
|
+
return Number(this.text.slice(start, this.pos));
|
|
1276
|
+
}
|
|
1277
|
+
};
|
|
1129
1278
|
}
|
|
1130
|
-
};
|
|
1279
|
+
});
|
|
1131
1280
|
|
|
1132
|
-
// src/installer/
|
|
1133
|
-
var
|
|
1134
|
-
|
|
1135
|
-
|
|
1281
|
+
// src/installer/agent-renderer.js
|
|
1282
|
+
var agent_renderer_exports = {};
|
|
1283
|
+
__export(agent_renderer_exports, {
|
|
1284
|
+
AGENT_ROLE_MAP: () => AGENT_ROLE_MAP,
|
|
1285
|
+
buildRenderedOverride: () => buildRenderedOverride,
|
|
1286
|
+
computeRenderedAgents: () => computeRenderedAgents,
|
|
1287
|
+
modelMarker: () => modelMarker,
|
|
1288
|
+
renderAgentFrontmatter: () => renderAgentFrontmatter,
|
|
1289
|
+
renderedModelFor: () => renderedModelFor
|
|
1290
|
+
});
|
|
1291
|
+
import { dirname as dirname7, join as join6 } from "node:path";
|
|
1292
|
+
import { readFile as readFile8, writeFile as writeFile5, mkdir as mkdir5 } from "node:fs/promises";
|
|
1293
|
+
function modelMarker() {
|
|
1294
|
+
return MODEL_FROM_CONFIG;
|
|
1136
1295
|
}
|
|
1137
|
-
function
|
|
1138
|
-
if (
|
|
1139
|
-
if (
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
for (const
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
validate(value, schema.else, pointer, issues);
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
const type = schema.type;
|
|
1160
|
-
if (type !== void 0) {
|
|
1161
|
-
const actual = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
|
|
1162
|
-
if (type !== actual) {
|
|
1163
|
-
if (!(type === "integer" && typeof value === "number" && Number.isInteger(value))) {
|
|
1164
|
-
issues.push(`${pointer}: expected ${type}, got ${actual}`);
|
|
1165
|
-
return;
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
if (type === "string") {
|
|
1170
|
-
if (schema.minLength !== void 0 && value.length < schema.minLength) {
|
|
1171
|
-
issues.push(`${pointer}: shorter than minLength ${schema.minLength}`);
|
|
1172
|
-
}
|
|
1173
|
-
if (schema.pattern !== void 0) {
|
|
1174
|
-
const re = new RegExp(schema.pattern);
|
|
1175
|
-
if (!re.test(value)) issues.push(`${pointer}: does not match pattern ${schema.pattern}`);
|
|
1176
|
-
}
|
|
1177
|
-
if (schema.format === "date-time" && !FORMAT_DATE_TIME.test(value)) {
|
|
1178
|
-
issues.push(`${pointer}: not a date-time string`);
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
if (type === "integer" || type === "number") {
|
|
1182
|
-
if (schema.minimum !== void 0 && value < schema.minimum) {
|
|
1183
|
-
issues.push(`${pointer}: less than minimum ${schema.minimum}`);
|
|
1184
|
-
}
|
|
1185
|
-
if (schema.maximum !== void 0 && value > schema.maximum) {
|
|
1186
|
-
issues.push(`${pointer}: greater than maximum ${schema.maximum}`);
|
|
1187
|
-
}
|
|
1188
|
-
if (schema.enum !== void 0) {
|
|
1296
|
+
function renderAgentFrontmatter(source, model) {
|
|
1297
|
+
if (typeof source !== "string") return source;
|
|
1298
|
+
if (typeof model !== "string" || !model) return source;
|
|
1299
|
+
return source.replaceAll(MODEL_FROM_CONFIG, model);
|
|
1300
|
+
}
|
|
1301
|
+
async function computeRenderedAgents({ models, catalog }) {
|
|
1302
|
+
const out = [];
|
|
1303
|
+
if (!models || typeof models !== "object") return out;
|
|
1304
|
+
for (const [role, agentNames] of Object.entries(AGENT_ROLE_MAP)) {
|
|
1305
|
+
const model = models[role];
|
|
1306
|
+
if (typeof model !== "string" || !model) continue;
|
|
1307
|
+
for (const agentName of agentNames) {
|
|
1308
|
+
const entry = (catalog ?? []).find((c) => c.kind === "agent" && c.path.endsWith(`/${agentName}.md`));
|
|
1309
|
+
if (!entry) continue;
|
|
1310
|
+
const source = await readFile8(entry.source, "utf8");
|
|
1311
|
+
const renderedText = renderAgentFrontmatter(source, model);
|
|
1312
|
+
const bytes = Buffer.from(renderedText, "utf8");
|
|
1313
|
+
const sha256 = bytesHashString(renderedText);
|
|
1314
|
+
out.push({ relPath: entry.path, bytes, sha256, role, agentName, target: join6(process.cwd(), entry.path) });
|
|
1189
1315
|
}
|
|
1190
1316
|
}
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
value.forEach((entry, i) => validate(entry, schema.items, `${pointer}/${i}`, issues));
|
|
1317
|
+
return out;
|
|
1318
|
+
}
|
|
1319
|
+
async function buildRenderedOverride({ models, catalog }) {
|
|
1320
|
+
const rendered = await computeRenderedAgents({ models, catalog });
|
|
1321
|
+
const map = /* @__PURE__ */ new Map();
|
|
1322
|
+
for (const entry of rendered) {
|
|
1323
|
+
map.set(entry.relPath, entry);
|
|
1324
|
+
}
|
|
1325
|
+
return { rendered, map };
|
|
1326
|
+
}
|
|
1327
|
+
function renderedModelFor({ agentName, models }) {
|
|
1328
|
+
for (const [role, agentNames] of Object.entries(AGENT_ROLE_MAP)) {
|
|
1329
|
+
if (agentNames.includes(agentName)) {
|
|
1330
|
+
const m = models?.[role];
|
|
1331
|
+
return typeof m === "string" && m ? m : MODEL_FROM_CONFIG;
|
|
1207
1332
|
}
|
|
1208
1333
|
}
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1334
|
+
return MODEL_FROM_CONFIG;
|
|
1335
|
+
}
|
|
1336
|
+
var MODEL_FROM_CONFIG, AGENT_ROLE_MAP;
|
|
1337
|
+
var init_agent_renderer = __esm({
|
|
1338
|
+
"src/installer/agent-renderer.js"() {
|
|
1339
|
+
init_hash();
|
|
1340
|
+
MODEL_FROM_CONFIG = "<model-from-config>";
|
|
1341
|
+
AGENT_ROLE_MAP = Object.freeze({
|
|
1342
|
+
planner: ["ship-planner"],
|
|
1343
|
+
builder: ["ship-controller", "ship-task-builder", "ship-task-reviewer"],
|
|
1344
|
+
finalReviewer: ["ship-final-standards-reviewer", "ship-final-spec-reviewer"]
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
// src/installer/cli-args.js
|
|
1350
|
+
init_profile();
|
|
1351
|
+
var USAGE = `opencode-ship <command> [options]
|
|
1352
|
+
|
|
1353
|
+
Commands:
|
|
1354
|
+
init Install managed files in this project. One-liner: pnpm dlx opencode-ship@latest init
|
|
1355
|
+
diff Show what would change without writing.
|
|
1356
|
+
update Apply pending updates after recovering the journal.
|
|
1357
|
+
doctor Validate environment, lock, and references.
|
|
1358
|
+
uninstall Remove managed files that still match the lock.
|
|
1359
|
+
--version Print the version and exit.
|
|
1360
|
+
--help Show this usage and exit.
|
|
1361
|
+
|
|
1362
|
+
Options:
|
|
1363
|
+
--root <path> Project root (defaults to cwd).
|
|
1364
|
+
--profile engineering Override active profile (engineering only).
|
|
1365
|
+
--force-config Rewrite the user config from detection (init only).
|
|
1366
|
+
--force-root-config Create opencode.json when absent (init only).
|
|
1367
|
+
--strict-doctor Fail init when doctor reports unhealthy checks.
|
|
1368
|
+
--replace-managed Replace locally-modified managed files (update only).
|
|
1369
|
+
--purge-config Remove ship.config.json when uninstalling.
|
|
1370
|
+
--planner-model <id> Strong planner model id (init only, optional).
|
|
1371
|
+
--builder-model <id> Cheap builder model id (init only, optional).
|
|
1372
|
+
--final-reviewer-model <id> Final Standards + Spec reviewer model id (init only, optional).
|
|
1373
|
+
--json Emit a JSON envelope instead of human output.
|
|
1374
|
+
|
|
1375
|
+
After init succeeds, restart OpenCode and run /setup-ship-workflow to
|
|
1376
|
+
fill in the workflow.models fields and the per-repo docs.
|
|
1377
|
+
`;
|
|
1378
|
+
var MODEL_ID_RE = /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/;
|
|
1379
|
+
function parseFlags(argv) {
|
|
1380
|
+
const options = {
|
|
1381
|
+
rootPath: null,
|
|
1382
|
+
profile: null,
|
|
1383
|
+
json: false,
|
|
1384
|
+
replaceManaged: false,
|
|
1385
|
+
purgeConfig: false,
|
|
1386
|
+
forceConfig: false,
|
|
1387
|
+
forceRootConfig: false,
|
|
1388
|
+
strictDoctor: false,
|
|
1389
|
+
plannerModel: null,
|
|
1390
|
+
builderModel: null,
|
|
1391
|
+
finalReviewerModel: null
|
|
1392
|
+
};
|
|
1393
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1394
|
+
const arg = argv[i];
|
|
1395
|
+
if (arg === "--json") options.json = true;
|
|
1396
|
+
else if (arg === "--replace-managed") options.replaceManaged = true;
|
|
1397
|
+
else if (arg === "--purge-config") options.purgeConfig = true;
|
|
1398
|
+
else if (arg === "--force-config") options.forceConfig = true;
|
|
1399
|
+
else if (arg === "--force-root-config") options.forceRootConfig = true;
|
|
1400
|
+
else if (arg === "--strict-doctor") options.strictDoctor = true;
|
|
1401
|
+
else if (arg === "--root") options.rootPath = argv[++i];
|
|
1402
|
+
else if (arg === "--profile") {
|
|
1403
|
+
const value = argv[++i];
|
|
1404
|
+
if (value === void 0) {
|
|
1405
|
+
return { error: "--profile requires a value" };
|
|
1213
1406
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1407
|
+
if (value === "core") {
|
|
1408
|
+
return {
|
|
1409
|
+
error: "the 'core' profile was removed in opencode-ship 1.1.0; only 'engineering' is supported. Run /setup-ship-workflow to migrate."
|
|
1410
|
+
};
|
|
1218
1411
|
}
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
for (const key of Object.keys(schema.properties)) {
|
|
1222
|
-
if (key in value) validate(value[key], schema.properties[key], `${pointer}/${key}`, issues);
|
|
1412
|
+
if (!isValidProfile(value)) {
|
|
1413
|
+
return { error: `unknown profile '${value}' (expected one of: ${PROFILES.join(", ")})` };
|
|
1223
1414
|
}
|
|
1224
|
-
|
|
1415
|
+
options.profile = value;
|
|
1416
|
+
} else if (arg === "--planner-model" || arg === "--builder-model" || arg === "--final-reviewer-model") {
|
|
1417
|
+
const value = argv[++i];
|
|
1418
|
+
if (value === void 0) return { error: `${arg} requires a value` };
|
|
1419
|
+
if (!MODEL_ID_RE.test(value)) {
|
|
1420
|
+
return { error: `${arg} must be a "<provider>/<model>" id, got ${JSON.stringify(value)}` };
|
|
1421
|
+
}
|
|
1422
|
+
if (arg === "--planner-model") options.plannerModel = value;
|
|
1423
|
+
else if (arg === "--builder-model") options.builderModel = value;
|
|
1424
|
+
else options.finalReviewerModel = value;
|
|
1425
|
+
} else if (arg === "-h" || arg === "--help") return { help: true };
|
|
1426
|
+
else if (arg === "-v" || arg === "--version") return { version: true };
|
|
1427
|
+
else return { error: `unknown flag ${arg}` };
|
|
1225
1428
|
}
|
|
1429
|
+
return options;
|
|
1226
1430
|
}
|
|
1227
|
-
function
|
|
1228
|
-
const
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
// src/installer/config.js
|
|
1234
|
-
init_json_pointer();
|
|
1235
|
-
init_hash();
|
|
1236
|
-
function configPath(repoRoot) {
|
|
1237
|
-
return resolve4(repoRoot, ".opencode", "ship.config.json");
|
|
1238
|
-
}
|
|
1239
|
-
async function loadConfig(repoRoot) {
|
|
1240
|
-
const path = configPath(repoRoot);
|
|
1241
|
-
if (!existsSync4(path)) return null;
|
|
1242
|
-
const raw = await readFile(path, "utf8");
|
|
1243
|
-
let parsed;
|
|
1244
|
-
try {
|
|
1245
|
-
parsed = JSON.parse(raw);
|
|
1246
|
-
} catch (e) {
|
|
1247
|
-
return { ok: false, error: { kind: "parse", path, message: e.message } };
|
|
1431
|
+
function parseCommand(argv) {
|
|
1432
|
+
for (const arg of argv) {
|
|
1433
|
+
if (arg === "--help" || arg === "-h") return { command: "help" };
|
|
1434
|
+
if (arg === "--version" || arg === "-v") return { command: "version" };
|
|
1248
1435
|
}
|
|
1249
|
-
const
|
|
1250
|
-
if (!
|
|
1251
|
-
|
|
1436
|
+
const [cmd, ...rest] = argv;
|
|
1437
|
+
if (!cmd) return { command: "help" };
|
|
1438
|
+
const flags = parseFlags(rest);
|
|
1439
|
+
if ("help" in flags) return { command: "help" };
|
|
1440
|
+
if ("version" in flags) return { command: "version" };
|
|
1441
|
+
if ("error" in flags) return { error: flags.error };
|
|
1442
|
+
switch (cmd) {
|
|
1443
|
+
case "init":
|
|
1444
|
+
case "diff":
|
|
1445
|
+
case "update":
|
|
1446
|
+
case "doctor":
|
|
1447
|
+
case "uninstall":
|
|
1448
|
+
return { command: cmd, options: flags };
|
|
1449
|
+
default:
|
|
1450
|
+
return { error: `unknown command ${cmd}` };
|
|
1252
1451
|
}
|
|
1253
|
-
return {
|
|
1254
|
-
ok: true,
|
|
1255
|
-
path,
|
|
1256
|
-
raw,
|
|
1257
|
-
sha256: bytesHashString(raw),
|
|
1258
|
-
canonicalSha256: bytesHashString(stableStringify(parsed)),
|
|
1259
|
-
value: parsed
|
|
1260
|
-
};
|
|
1261
|
-
}
|
|
1262
|
-
function renderDefaultConfig(detection, overrides = {}) {
|
|
1263
|
-
const pm = detection?.packageManager ?? "npm";
|
|
1264
|
-
const safeBootstrap = Array.isArray(detection?.worktreeBootstrap) && detection.worktreeBootstrap.length ? detection.worktreeBootstrap : [["npm", "install"]];
|
|
1265
|
-
const safeVerification = Array.isArray(detection?.verificationPlan) && detection.verificationPlan.length ? detection.verificationPlan.map((step) => ({ id: step.id, argv: step.argv })) : [{ id: "typecheck", argv: ["npm", "run", "typecheck"] }];
|
|
1266
|
-
const repo = detection?.repository ?? overrides.repository ?? "owner/repo";
|
|
1267
|
-
return {
|
|
1268
|
-
schemaVersion: 2,
|
|
1269
|
-
profile: "engineering",
|
|
1270
|
-
project: {
|
|
1271
|
-
remote: detection?.remote ?? "origin",
|
|
1272
|
-
repository: repo,
|
|
1273
|
-
defaultBranch: detection?.defaultBranch ?? "main",
|
|
1274
|
-
packageManager: pm,
|
|
1275
|
-
detectOverrides: false
|
|
1276
|
-
},
|
|
1277
|
-
delivery: {
|
|
1278
|
-
worktree: {
|
|
1279
|
-
root: detection?.worktreeRoot ?? ".worktrees",
|
|
1280
|
-
branchTemplate: "{actor}/{slug}",
|
|
1281
|
-
bootstrap: safeBootstrap
|
|
1282
|
-
},
|
|
1283
|
-
verification: {
|
|
1284
|
-
commands: safeVerification,
|
|
1285
|
-
requireCleanDiffAfter: true,
|
|
1286
|
-
invalidateOnHeadChange: true
|
|
1287
|
-
},
|
|
1288
|
-
review: { agent: "delivery-reviewer", required: true, invalidateOnHeadChange: true },
|
|
1289
|
-
ci: {
|
|
1290
|
-
driver: "github-status-checks",
|
|
1291
|
-
requiredChecks: ["delivery-verify"],
|
|
1292
|
-
wait: true,
|
|
1293
|
-
flakyRetry: 1
|
|
1294
|
-
},
|
|
1295
|
-
ready: { requires: ["review", "local-verification", "remote-ci"], stopAfterReady: true },
|
|
1296
|
-
merge: { strategy: "squash", policy: "explicit-user-request-only", requireFreshGates: true },
|
|
1297
|
-
cleanup: { when: "next-task", requireUnpublishedGuard: true }
|
|
1298
|
-
},
|
|
1299
|
-
workflow: {
|
|
1300
|
-
models: {},
|
|
1301
|
-
approval: { mirrorToIssue: true, maxFailedRounds: 3 }
|
|
1302
|
-
}
|
|
1303
|
-
};
|
|
1304
1452
|
}
|
|
1305
|
-
function
|
|
1306
|
-
|
|
1307
|
-
if (!models || typeof models !== "object") return false;
|
|
1308
|
-
const idRe = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
1309
|
-
return typeof models.planner === "string" && idRe.test(models.planner) && typeof models.builder === "string" && idRe.test(models.builder) && typeof models.finalReviewer === "string" && idRe.test(models.finalReviewer);
|
|
1453
|
+
function helpText() {
|
|
1454
|
+
return USAGE;
|
|
1310
1455
|
}
|
|
1311
1456
|
|
|
1457
|
+
// src/installer/commands/init.js
|
|
1458
|
+
import { promisify as promisify2 } from "node:util";
|
|
1459
|
+
import { writeFile as writeFile8, mkdir as mkdirAsync2 } from "node:fs/promises";
|
|
1460
|
+
import { dirname as dirname10, resolve as resolvePath } from "node:path";
|
|
1461
|
+
|
|
1462
|
+
// src/installer/executor.js
|
|
1463
|
+
init_catalog();
|
|
1464
|
+
init_version();
|
|
1465
|
+
import { existsSync as existsSync13 } from "node:fs";
|
|
1466
|
+
import { mkdir as mkdir6, readFile as readFile9, rename as rename5, unlink as unlink3, writeFile as writeFile6 } from "node:fs/promises";
|
|
1467
|
+
import { dirname as dirname8, resolve as resolve12 } from "node:path";
|
|
1468
|
+
|
|
1312
1469
|
// src/installer/planner.js
|
|
1470
|
+
init_catalog();
|
|
1471
|
+
init_hash();
|
|
1472
|
+
init_config();
|
|
1313
1473
|
init_json_pointer();
|
|
1314
1474
|
init_root_config();
|
|
1475
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
1476
|
+
import { readFile as readFile3, stat } from "node:fs/promises";
|
|
1315
1477
|
|
|
1316
1478
|
// src/installer/root-reconciliation.js
|
|
1317
1479
|
init_root_config();
|
|
@@ -1320,9 +1482,8 @@ init_hash();
|
|
|
1320
1482
|
init_plan_mode_permissions();
|
|
1321
1483
|
import { existsSync as existsSync6 } from "node:fs";
|
|
1322
1484
|
import { join } from "node:path";
|
|
1323
|
-
var PLAN_MODE_POINTER2 = "/agent/plan/permission";
|
|
1324
1485
|
function desiredPointersForProfile(profile) {
|
|
1325
|
-
|
|
1486
|
+
return POINTER_ENTRIES.map((entry) => ({
|
|
1326
1487
|
pointer: entry.pointer,
|
|
1327
1488
|
strategy: (
|
|
1328
1489
|
/** @type {"value" | "object-entry" | "array-member"} */
|
|
@@ -1330,28 +1491,10 @@ function desiredPointersForProfile(profile) {
|
|
|
1330
1491
|
),
|
|
1331
1492
|
scope: (
|
|
1332
1493
|
/** @type {Profile} */
|
|
1333
|
-
"
|
|
1494
|
+
"engineering"
|
|
1334
1495
|
),
|
|
1335
1496
|
value: entry.value
|
|
1336
1497
|
}));
|
|
1337
|
-
if (profile === "engineering") {
|
|
1338
|
-
out.push({
|
|
1339
|
-
pointer: PLAN_MODE_POINTER2,
|
|
1340
|
-
strategy: (
|
|
1341
|
-
/** @type {"value" | "object-entry" | "array-member"} */
|
|
1342
|
-
"value"
|
|
1343
|
-
),
|
|
1344
|
-
scope: (
|
|
1345
|
-
/** @type {Profile} */
|
|
1346
|
-
"engineering"
|
|
1347
|
-
),
|
|
1348
|
-
value: (
|
|
1349
|
-
/** @type {any} */
|
|
1350
|
-
planModePermissions().build
|
|
1351
|
-
)
|
|
1352
|
-
});
|
|
1353
|
-
}
|
|
1354
|
-
return out;
|
|
1355
1498
|
}
|
|
1356
1499
|
async function planRootReconciliation(input) {
|
|
1357
1500
|
const profile = input.profile;
|
|
@@ -1400,10 +1543,6 @@ async function planRootReconciliation(input) {
|
|
|
1400
1543
|
let doc;
|
|
1401
1544
|
if (fileMissing && input.forceRepair) {
|
|
1402
1545
|
doc = synthesizeDefaultRootConfig();
|
|
1403
|
-
if (mode === "install" && profile === "engineering") {
|
|
1404
|
-
const applied = applyPlanModeOwnership(doc, { block: planModePermissions().build });
|
|
1405
|
-
doc = applied.doc;
|
|
1406
|
-
}
|
|
1407
1546
|
const bytes = Buffer.from(formatRootConfig(doc), "utf8");
|
|
1408
1547
|
return {
|
|
1409
1548
|
kind: "create",
|
|
@@ -1823,11 +1962,12 @@ function lookupLockedFile(lock, targetPath) {
|
|
|
1823
1962
|
if (!lock?.files) return null;
|
|
1824
1963
|
return lock.files.find((entry) => entry.path === targetPath) ?? null;
|
|
1825
1964
|
}
|
|
1826
|
-
async function planManagedFile({ entry, repoRoot, lock, allowUnowned }) {
|
|
1965
|
+
async function planManagedFile({ entry, repoRoot, lock, allowUnowned, renderedOverride = null }) {
|
|
1966
|
+
const override = renderedOverride && renderedOverride.get?.(entry.path);
|
|
1827
1967
|
const targetPath = `${repoRoot}/${entry.path}`;
|
|
1828
1968
|
const locked = lookupLockedFile(lock, entry.path);
|
|
1829
1969
|
const current = await readBytes(targetPath);
|
|
1830
|
-
const desired = await readDesiredBytes(entry.source);
|
|
1970
|
+
const desired = override ? { bytes: override.bytes, hash: override.sha256 } : await readDesiredBytes(entry.source);
|
|
1831
1971
|
if (!current) {
|
|
1832
1972
|
return {
|
|
1833
1973
|
kind: "create",
|
|
@@ -1838,7 +1978,7 @@ async function planManagedFile({ entry, repoRoot, lock, allowUnowned }) {
|
|
|
1838
1978
|
bytes: desired?.bytes ?? Buffer.alloc(0),
|
|
1839
1979
|
sha256: desired?.hash,
|
|
1840
1980
|
mode: 420,
|
|
1841
|
-
reason: "managed file missing"
|
|
1981
|
+
reason: override ? "rendered agent with configured model" : "managed file missing"
|
|
1842
1982
|
};
|
|
1843
1983
|
}
|
|
1844
1984
|
if (desired.hash === current.hash) {
|
|
@@ -1862,7 +2002,7 @@ async function planManagedFile({ entry, repoRoot, lock, allowUnowned }) {
|
|
|
1862
2002
|
bytes: desired?.bytes ?? Buffer.alloc(0),
|
|
1863
2003
|
sha256: desired?.hash,
|
|
1864
2004
|
mode: 420,
|
|
1865
|
-
reason: "safe update: previous lock matches current bytes"
|
|
2005
|
+
reason: override ? "rendered agent with new configured model" : "safe update: previous lock matches current bytes"
|
|
1866
2006
|
};
|
|
1867
2007
|
}
|
|
1868
2008
|
if (locked?.sha256 && locked.sha256 !== current.hash && allowUnowned) {
|
|
@@ -1888,10 +2028,10 @@ async function planManagedFile({ entry, repoRoot, lock, allowUnowned }) {
|
|
|
1888
2028
|
reason: locked?.sha256 == null ? "managed file already exists; bytes differ from upstream" : "managed file is locally modified"
|
|
1889
2029
|
};
|
|
1890
2030
|
}
|
|
1891
|
-
async function planFileInstall({ repoRoot, lock, allowUnowned = false, catalog = CATALOG }) {
|
|
2031
|
+
async function planFileInstall({ repoRoot, lock, allowUnowned = false, catalog = CATALOG, renderedOverride = null }) {
|
|
1892
2032
|
const plan = [];
|
|
1893
2033
|
for (const entry of catalog) {
|
|
1894
|
-
plan.push(await planManagedFile({ entry, repoRoot, lock, allowUnowned }));
|
|
2034
|
+
plan.push(await planManagedFile({ entry, repoRoot, lock, allowUnowned, renderedOverride }));
|
|
1895
2035
|
}
|
|
1896
2036
|
return plan;
|
|
1897
2037
|
}
|
|
@@ -2006,7 +2146,8 @@ async function planUninstall({ repoRoot, lock }) {
|
|
|
2006
2146
|
}
|
|
2007
2147
|
async function planConfigSynthesis({ repoRoot, detection, lock, forceOverwrite, migrationSeed = null, models = null }) {
|
|
2008
2148
|
const existing = await loadConfig(repoRoot);
|
|
2009
|
-
|
|
2149
|
+
const hasModelFlags = Boolean(models && (models.planner || models.builder || models.finalReviewer));
|
|
2150
|
+
if (existing?.ok && !forceOverwrite && !hasModelFlags) {
|
|
2010
2151
|
return {
|
|
2011
2152
|
kind: "noop",
|
|
2012
2153
|
op: "config",
|
|
@@ -2018,9 +2159,9 @@ async function planConfigSynthesis({ repoRoot, detection, lock, forceOverwrite,
|
|
|
2018
2159
|
reason: "user config already present"
|
|
2019
2160
|
};
|
|
2020
2161
|
}
|
|
2021
|
-
let desiredValue = migrationSeed ?? renderDefaultConfig(detection);
|
|
2022
|
-
if (
|
|
2023
|
-
|
|
2162
|
+
let desiredValue = migrationSeed ?? (existing?.ok ? structuredClone(existing.value) : renderDefaultConfig(detection));
|
|
2163
|
+
if (hasModelFlags) {
|
|
2164
|
+
desiredValue = {
|
|
2024
2165
|
...desiredValue,
|
|
2025
2166
|
schemaVersion: 2,
|
|
2026
2167
|
profile: "engineering",
|
|
@@ -2038,12 +2179,12 @@ async function planConfigSynthesis({ repoRoot, detection, lock, forceOverwrite,
|
|
|
2038
2179
|
}
|
|
2039
2180
|
}
|
|
2040
2181
|
};
|
|
2041
|
-
desiredValue = merged;
|
|
2042
2182
|
}
|
|
2183
|
+
if (desiredValue.profile === "core") desiredValue.profile = "engineering";
|
|
2043
2184
|
const desiredJson = JSON.stringify(desiredValue, null, 2) + "\n";
|
|
2044
2185
|
const desiredSha = bytesHashString(desiredJson);
|
|
2045
|
-
const kind = existing?.ok && forceOverwrite ? "update" : "create";
|
|
2046
|
-
const reason = existing?.ok ? "user config overwritten via --force-config" : migrationSeed ? "synthesising a default config from legacy adapter migration" : "synthesising a default config from detection";
|
|
2186
|
+
const kind = existing?.ok && (forceOverwrite || hasModelFlags) ? "update" : "create";
|
|
2187
|
+
const reason = existing?.ok ? hasModelFlags && !forceOverwrite ? "patching workflow.models from CLI model flags" : "user config overwritten via --force-config" : migrationSeed ? "synthesising a default config from legacy adapter migration" : "synthesising a default config from detection";
|
|
2047
2188
|
return {
|
|
2048
2189
|
kind,
|
|
2049
2190
|
op: "config",
|
|
@@ -2059,8 +2200,8 @@ async function planConfigSynthesis({ repoRoot, detection, lock, forceOverwrite,
|
|
|
2059
2200
|
async function planRootConfigApply({ repoRoot, lock, forceRepair, planMode = null }) {
|
|
2060
2201
|
const previous = (lock?.manager?.rootDocuments ?? []).flatMap((d) => d.pointers ?? []);
|
|
2061
2202
|
const previousProfile = lock?.manager?.profile ?? null;
|
|
2062
|
-
const desiredProfile =
|
|
2063
|
-
const isTransition = previousProfile !== null && previousProfile !==
|
|
2203
|
+
const desiredProfile = "engineering";
|
|
2204
|
+
const isTransition = previousProfile !== null && previousProfile !== "engineering" && previousProfile !== "core";
|
|
2064
2205
|
const mode = previous.length === 0 ? "install" : isTransition ? "profile-transition" : "install";
|
|
2065
2206
|
return planRootReconciliation({
|
|
2066
2207
|
repoRoot,
|
|
@@ -2074,10 +2215,11 @@ async function planRootConfigApply({ repoRoot, lock, forceRepair, planMode = nul
|
|
|
2074
2215
|
// src/installer/lock.js
|
|
2075
2216
|
init_hash();
|
|
2076
2217
|
init_json_pointer();
|
|
2218
|
+
init_profile();
|
|
2077
2219
|
import { readFile as readFile4, writeFile as writeFile2, rename as rename2, mkdir as mkdir2 } from "node:fs/promises";
|
|
2078
2220
|
import { existsSync as existsSync8 } from "node:fs";
|
|
2079
2221
|
import { dirname as dirname4, resolve as resolve6 } from "node:path";
|
|
2080
|
-
var CURRENT_LOCK_SCHEMA =
|
|
2222
|
+
var CURRENT_LOCK_SCHEMA = 4;
|
|
2081
2223
|
function lockPath(repoRoot) {
|
|
2082
2224
|
return resolve6(repoRoot, ".opencode", "ship.lock.json");
|
|
2083
2225
|
}
|
|
@@ -2088,6 +2230,12 @@ function computeIntegrity(lock) {
|
|
|
2088
2230
|
lockSha256: bytesHashString(stableStringify(without))
|
|
2089
2231
|
};
|
|
2090
2232
|
}
|
|
2233
|
+
function normalizeLegacyLock(lock) {
|
|
2234
|
+
if (!lock || typeof lock !== "object") return lock;
|
|
2235
|
+
const { cleanupPending: _drop, ...rest } = lock;
|
|
2236
|
+
void _drop;
|
|
2237
|
+
return rest;
|
|
2238
|
+
}
|
|
2091
2239
|
function validateLock(rawLock) {
|
|
2092
2240
|
if (rawLock === null || rawLock === void 0) {
|
|
2093
2241
|
return { ok: true, kind: "missing", issues: [] };
|
|
@@ -2097,8 +2245,8 @@ function validateLock(rawLock) {
|
|
|
2097
2245
|
}
|
|
2098
2246
|
const issues = [];
|
|
2099
2247
|
let kind = "ok";
|
|
2100
|
-
if (rawLock.contractVersion !== CURRENT_LOCK_SCHEMA && rawLock.contractVersion !==
|
|
2101
|
-
issues.push(`unsupported contractVersion: ${JSON.stringify(rawLock.contractVersion)} (expected ${CURRENT_LOCK_SCHEMA}, 2, or 1)`);
|
|
2248
|
+
if (rawLock.contractVersion !== CURRENT_LOCK_SCHEMA && rawLock.contractVersion !== 3 && rawLock.contractVersion !== 2 && rawLock.contractVersion !== 1) {
|
|
2249
|
+
issues.push(`unsupported contractVersion: ${JSON.stringify(rawLock.contractVersion)} (expected ${CURRENT_LOCK_SCHEMA}, 3, 2, or 1)`);
|
|
2102
2250
|
kind = "schema";
|
|
2103
2251
|
}
|
|
2104
2252
|
const manager = rawLock.manager;
|
|
@@ -2108,14 +2256,14 @@ function validateLock(rawLock) {
|
|
|
2108
2256
|
} else if (typeof manager !== "object" || manager === null) {
|
|
2109
2257
|
issues.push("manager section must be an object");
|
|
2110
2258
|
kind = kind === "ok" ? "shape" : kind;
|
|
2111
|
-
} else if (manager.schemaVersion !== CURRENT_LOCK_SCHEMA && manager.schemaVersion !== 2 && manager.schemaVersion !== 1) {
|
|
2112
|
-
issues.push(`unsupported manager.schemaVersion: ${JSON.stringify(manager.schemaVersion)} (expected ${CURRENT_LOCK_SCHEMA}, 2, or 1)`);
|
|
2259
|
+
} else if (manager.schemaVersion !== CURRENT_LOCK_SCHEMA && manager.schemaVersion !== 3 && manager.schemaVersion !== 2 && manager.schemaVersion !== 1) {
|
|
2260
|
+
issues.push(`unsupported manager.schemaVersion: ${JSON.stringify(manager.schemaVersion)} (expected ${CURRENT_LOCK_SCHEMA}, 3, 2, or 1)`);
|
|
2113
2261
|
kind = "schema";
|
|
2114
2262
|
} else if (manager.name !== "opencode-ship") {
|
|
2115
2263
|
issues.push(`unknown manager.name: ${JSON.stringify(manager.name)}`);
|
|
2116
2264
|
kind = "shape";
|
|
2117
|
-
} else if (rawLock.contractVersion >= 2 && manager.schemaVersion >= 2 && manager.profile !== void 0 &&
|
|
2118
|
-
issues.push(`invalid manager.profile: ${JSON.stringify(manager.profile)} (expected one of:
|
|
2265
|
+
} else if (rawLock.contractVersion >= 2 && manager.schemaVersion >= 2 && manager.profile !== void 0 && manager.profile !== "core" && manager.profile !== "engineering") {
|
|
2266
|
+
issues.push(`invalid manager.profile: ${JSON.stringify(manager.profile)} (expected one of: engineering, core [legacy])`);
|
|
2119
2267
|
kind = "shape";
|
|
2120
2268
|
}
|
|
2121
2269
|
if (!rawLock.files || !Array.isArray(rawLock.files)) {
|
|
@@ -2151,10 +2299,18 @@ async function readValidatedLock(repoRoot) {
|
|
|
2151
2299
|
};
|
|
2152
2300
|
}
|
|
2153
2301
|
const validation = validateLock(raw);
|
|
2154
|
-
|
|
2302
|
+
if (validation.ok) {
|
|
2303
|
+
return {
|
|
2304
|
+
kind: validation.kind,
|
|
2305
|
+
lock: normalizeLegacyLock(raw),
|
|
2306
|
+
issues: []
|
|
2307
|
+
};
|
|
2308
|
+
}
|
|
2309
|
+
return { kind: validation.kind, lock: null, issues: validation.issues };
|
|
2155
2310
|
}
|
|
2156
2311
|
|
|
2157
2312
|
// src/installer/executor.js
|
|
2313
|
+
init_config();
|
|
2158
2314
|
init_hash();
|
|
2159
2315
|
init_json_pointer();
|
|
2160
2316
|
|
|
@@ -2744,6 +2900,7 @@ async function rollback(lockDir, journal) {
|
|
|
2744
2900
|
import { readFile as readFile7 } from "node:fs/promises";
|
|
2745
2901
|
import { existsSync as existsSync12 } from "node:fs";
|
|
2746
2902
|
import { resolve as resolve11 } from "node:path";
|
|
2903
|
+
init_config();
|
|
2747
2904
|
function legacyAdapterPath(repoRoot) {
|
|
2748
2905
|
return resolve11(repoRoot, ".opencode", "delivery.json");
|
|
2749
2906
|
}
|
|
@@ -2845,10 +3002,10 @@ function legacyToShipConfig(legacy, detection = null) {
|
|
|
2845
3002
|
}
|
|
2846
3003
|
|
|
2847
3004
|
// src/installer/executor.js
|
|
2848
|
-
|
|
3005
|
+
init_profile();
|
|
2849
3006
|
async function readCurrentBytes(targetPath) {
|
|
2850
3007
|
if (!existsSync13(targetPath)) return null;
|
|
2851
|
-
const buf = await
|
|
3008
|
+
const buf = await readFile9(targetPath);
|
|
2852
3009
|
return { bytes: buf, hash: bytesHashString(buf.toString("utf8")) };
|
|
2853
3010
|
}
|
|
2854
3011
|
async function previewInstall({ rootPath, profile = null, replaceManaged, forceConfig, forceRootConfig, models = null }) {
|
|
@@ -2873,6 +3030,21 @@ async function previewInstall({ rootPath, profile = null, replaceManaged, forceC
|
|
|
2873
3030
|
config: configValue,
|
|
2874
3031
|
lock
|
|
2875
3032
|
});
|
|
3033
|
+
if (resolved.profile === "engineering" && forceConfig) {
|
|
3034
|
+
const existingModels = configValue?.workflow?.models ?? {};
|
|
3035
|
+
const planner = models?.planner ?? existingModels.planner;
|
|
3036
|
+
const builder = models?.builder ?? existingModels.builder;
|
|
3037
|
+
const finalReviewer = models?.finalReviewer ?? existingModels.finalReviewer;
|
|
3038
|
+
if (!planner || !builder || !finalReviewer) {
|
|
3039
|
+
return {
|
|
3040
|
+
ok: false,
|
|
3041
|
+
error: {
|
|
3042
|
+
kind: "engineering-models-required",
|
|
3043
|
+
message: "engineering profile with --force-config requires --planner-model, --builder-model, and --final-reviewer-model"
|
|
3044
|
+
}
|
|
3045
|
+
};
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
2876
3048
|
if (resolved.profile === "engineering") {
|
|
2877
3049
|
const candidate = await planConfigSynthesis({
|
|
2878
3050
|
repoRoot,
|
|
@@ -2901,7 +3073,19 @@ async function previewInstall({ rootPath, profile = null, replaceManaged, forceC
|
|
|
2901
3073
|
migrationSeed: migrationReport?.proposedConfigSeed ?? null,
|
|
2902
3074
|
models
|
|
2903
3075
|
});
|
|
2904
|
-
const
|
|
3076
|
+
const { buildRenderedOverride: buildRenderedOverride2 } = await Promise.resolve().then(() => (init_agent_renderer(), agent_renderer_exports));
|
|
3077
|
+
const configModels = configPlan?.configValue?.workflow?.models ?? {};
|
|
3078
|
+
const rendered = await buildRenderedOverride2({
|
|
3079
|
+
models: resolved.profile === "engineering" ? configModels : null,
|
|
3080
|
+
catalog: CATALOG
|
|
3081
|
+
});
|
|
3082
|
+
const filePlan = await planFileInstall({
|
|
3083
|
+
repoRoot,
|
|
3084
|
+
lock,
|
|
3085
|
+
allowUnowned: Boolean(replaceManaged),
|
|
3086
|
+
catalog: activeCatalog,
|
|
3087
|
+
renderedOverride: rendered.map
|
|
3088
|
+
});
|
|
2905
3089
|
const staleFilePlan = await planStaleFileRemoval({ repoRoot, lock, staleCatalog });
|
|
2906
3090
|
const migrationPlan = await planMigrationCleanup({
|
|
2907
3091
|
repoRoot,
|
|
@@ -2909,9 +3093,9 @@ async function previewInstall({ rootPath, profile = null, replaceManaged, forceC
|
|
|
2909
3093
|
migrationReport,
|
|
2910
3094
|
allowUnowned: Boolean(replaceManaged)
|
|
2911
3095
|
});
|
|
2912
|
-
const planMode =
|
|
3096
|
+
const planMode = null;
|
|
2913
3097
|
const rootPlan = await planRootConfigApply({ repoRoot, lock, forceRepair: Boolean(forceRootConfig), planMode });
|
|
2914
|
-
const setupPending = resolved.profile === "engineering" && !lock?.manager?.setupComplete && !hasCompletedModels(configValue
|
|
3098
|
+
const setupPending = resolved.profile === "engineering" && !lock?.manager?.setupComplete && !hasCompletedModels(configValue) && !models?.planner;
|
|
2915
3099
|
const plan = [...filePlan ?? [], ...staleFilePlan, ...migrationPlan, configPlan, rootPlan];
|
|
2916
3100
|
const conflicts = plan.filter((p) => p && p.kind === "conflict");
|
|
2917
3101
|
const summary = summarise(plan);
|
|
@@ -2990,16 +3174,17 @@ async function assembleLock({ repoRoot, plan, lock, configPlan, rootPlan, profil
|
|
|
2990
3174
|
const rootPointers = rootPlan?.pointerRecords ?? lock?.manager?.rootDocuments?.[0]?.pointers ?? [];
|
|
2991
3175
|
const hasRootPlan = Boolean(rootPlan?.target || rootPlan?.pointerRecords && rootPlan.pointerRecords.length > 0);
|
|
2992
3176
|
const hasRootDocuments = rootPlan?.pointerRecords && rootPlan.pointerRecords.length > 0 || lock?.manager?.rootDocuments && lock.manager.rootDocuments.length > 0;
|
|
2993
|
-
const
|
|
2994
|
-
const completedModels = hasCompletedModels(
|
|
3177
|
+
const assembledConfig = configPlan?.configValue ?? (lock?.manager?.config?.models ? { workflow: { models: lock.manager.config.models } } : null);
|
|
3178
|
+
const completedModels = hasCompletedModels(assembledConfig);
|
|
3179
|
+
const resolvedProfile = profile ?? (lock?.manager?.profile === "core" ? "engineering" : lock?.manager?.profile) ?? "engineering";
|
|
2995
3180
|
return {
|
|
2996
3181
|
contractVersion: CURRENT_LOCK_SCHEMA,
|
|
2997
3182
|
manager: {
|
|
2998
3183
|
schemaVersion: CURRENT_LOCK_SCHEMA,
|
|
2999
3184
|
name: "opencode-ship",
|
|
3000
|
-
version: "1.1.
|
|
3185
|
+
version: "1.1.1",
|
|
3001
3186
|
templateSet: TEMPLATE_SET_ID,
|
|
3002
|
-
profile:
|
|
3187
|
+
profile: resolvedProfile,
|
|
3003
3188
|
appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3004
3189
|
setupComplete: completedModels,
|
|
3005
3190
|
config: {
|
|
@@ -3013,8 +3198,7 @@ async function assembleLock({ repoRoot, plan, lock, configPlan, rootPlan, profil
|
|
|
3013
3198
|
pointers: rootPlan?.pointerRecords && rootPlan.pointerRecords.length > 0 ? rootPlan.pointerRecords : lock?.manager?.rootDocuments?.[0]?.pointers ?? []
|
|
3014
3199
|
}] : []
|
|
3015
3200
|
},
|
|
3016
|
-
files
|
|
3017
|
-
cleanupPending: lock?.cleanupPending ?? []
|
|
3201
|
+
files
|
|
3018
3202
|
};
|
|
3019
3203
|
}
|
|
3020
3204
|
async function commitInstall(preview, { json, command }) {
|
|
@@ -3145,9 +3329,11 @@ function relativeTemplate(source) {
|
|
|
3145
3329
|
// src/installer/commands/doctor.js
|
|
3146
3330
|
import { existsSync as existsSync14, readFileSync as readFileSync5 } from "node:fs";
|
|
3147
3331
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
3332
|
+
init_config();
|
|
3148
3333
|
init_hash();
|
|
3149
|
-
|
|
3334
|
+
init_catalog();
|
|
3150
3335
|
init_root_config();
|
|
3336
|
+
import { resolve as resolve13 } from "node:path";
|
|
3151
3337
|
|
|
3152
3338
|
// src/installer/report.js
|
|
3153
3339
|
var REPORT_VERSION = 1;
|
|
@@ -3197,6 +3383,7 @@ function renderJson({ command, plan, conflicts, summary, diagnostics = [], exitC
|
|
|
3197
3383
|
}
|
|
3198
3384
|
|
|
3199
3385
|
// src/installer/commands/doctor.js
|
|
3386
|
+
init_profile();
|
|
3200
3387
|
function checkNode() {
|
|
3201
3388
|
return { name: "node>=22.6.0", ok: /^v2[2-9]/.test(process.version), detail: process.version };
|
|
3202
3389
|
}
|
|
@@ -3244,7 +3431,7 @@ function buildSourceHashIndex() {
|
|
|
3244
3431
|
}
|
|
3245
3432
|
return idx;
|
|
3246
3433
|
}
|
|
3247
|
-
function checkCatalogInstall(repoRoot, sourceHashes, profile) {
|
|
3434
|
+
async function checkCatalogInstall(repoRoot, sourceHashes, profile, renderedAgentMap = /* @__PURE__ */ new Map()) {
|
|
3248
3435
|
const rows = [];
|
|
3249
3436
|
const scoped = profile ? filterCatalogByProfile(CATALOG, profile) : CATALOG;
|
|
3250
3437
|
for (const entry of scoped) {
|
|
@@ -3256,7 +3443,8 @@ function checkCatalogInstall(repoRoot, sourceHashes, profile) {
|
|
|
3256
3443
|
try {
|
|
3257
3444
|
const buf = readFileSync5(target, "utf8");
|
|
3258
3445
|
const actual = bytesHashString(buf);
|
|
3259
|
-
const
|
|
3446
|
+
const rendered = renderedAgentMap.get(entry.path);
|
|
3447
|
+
const expected = rendered ? rendered.sha256 : sourceHashes.get(entry.source);
|
|
3260
3448
|
if (expected && expected !== actual) {
|
|
3261
3449
|
rows.push(`${entry.id}: drift`);
|
|
3262
3450
|
} else {
|
|
@@ -3307,6 +3495,7 @@ async function checkManagedHashes(repoRoot, validatedLock) {
|
|
|
3307
3495
|
return { name: "managed hashes", ok: false, detail: "no usable lock" };
|
|
3308
3496
|
}
|
|
3309
3497
|
const drift = [];
|
|
3498
|
+
const renderedAgents = await loadRenderedAgentOverrides(repoRoot);
|
|
3310
3499
|
for (const entry of validatedLock.lock.files ?? []) {
|
|
3311
3500
|
const p = resolve13(repoRoot, entry.path);
|
|
3312
3501
|
if (!existsSync14(p)) {
|
|
@@ -3319,6 +3508,18 @@ async function checkManagedHashes(repoRoot, validatedLock) {
|
|
|
3319
3508
|
}
|
|
3320
3509
|
return { name: "managed hashes", ok: drift.length === 0, detail: drift.length ? drift.join(",") : "match" };
|
|
3321
3510
|
}
|
|
3511
|
+
async function loadRenderedAgentOverrides(repoRoot) {
|
|
3512
|
+
const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
3513
|
+
const { CATALOG: CATALOG2 } = await Promise.resolve().then(() => (init_catalog(), catalog_exports));
|
|
3514
|
+
const { computeRenderedAgents: computeRenderedAgents2 } = await Promise.resolve().then(() => (init_agent_renderer(), agent_renderer_exports));
|
|
3515
|
+
const cfg = await loadConfig2(repoRoot);
|
|
3516
|
+
const models = cfg?.ok ? cfg.value?.workflow?.models : null;
|
|
3517
|
+
if (!models) return /* @__PURE__ */ new Map();
|
|
3518
|
+
const rendered = await computeRenderedAgents2({ models, catalog: CATALOG2 });
|
|
3519
|
+
const map = /* @__PURE__ */ new Map();
|
|
3520
|
+
for (const e of rendered) map.set(e.relPath, e);
|
|
3521
|
+
return map;
|
|
3522
|
+
}
|
|
3322
3523
|
async function checkActiveProfileFootprint(repoRoot, validatedLock, profile) {
|
|
3323
3524
|
if (validatedLock.kind !== "ok" || !validatedLock.lock) {
|
|
3324
3525
|
return { name: "profile footprint", ok: true, detail: "no lock; n/a" };
|
|
@@ -3393,7 +3594,7 @@ async function runDoctor({ rootPath, profile, json, writeOutput = true }) {
|
|
|
3393
3594
|
checkGh(),
|
|
3394
3595
|
checkGhAuth(),
|
|
3395
3596
|
packageIntegrity,
|
|
3396
|
-
checkCatalogInstall(repoRoot, sourceHashes, resolved.profile),
|
|
3597
|
+
await checkCatalogInstall(repoRoot, sourceHashes, resolved.profile, await loadRenderedAgentOverrides(repoRoot)),
|
|
3397
3598
|
await checkLock(repoRoot),
|
|
3398
3599
|
await checkConfig(repoRoot),
|
|
3399
3600
|
await checkManagedHashes(repoRoot, validatedLock),
|
|
@@ -3418,11 +3619,15 @@ async function runDoctor({ rootPath, profile, json, writeOutput = true }) {
|
|
|
3418
3619
|
return { issues, exitCode, plan, checks, profile: resolved };
|
|
3419
3620
|
}
|
|
3420
3621
|
|
|
3622
|
+
// src/installer/commands/init.js
|
|
3623
|
+
init_catalog();
|
|
3624
|
+
init_config();
|
|
3625
|
+
|
|
3421
3626
|
// src/installer/setup-pending.js
|
|
3422
|
-
import { existsSync as existsSync15, readFileSync as readFileSync6, unlinkSync, writeFile as
|
|
3627
|
+
import { existsSync as existsSync15, readFileSync as readFileSync6, unlinkSync, writeFile as writeFile7, mkdir as mkdirAsync } from "node:fs";
|
|
3423
3628
|
import { promisify } from "node:util";
|
|
3424
|
-
import { resolve as resolve14, dirname as
|
|
3425
|
-
var writeFileAsync = promisify(
|
|
3629
|
+
import { resolve as resolve14, dirname as dirname9 } from "node:path";
|
|
3630
|
+
var writeFileAsync = promisify(writeFile7);
|
|
3426
3631
|
var mkdirAsyncAsync = promisify(mkdirAsync);
|
|
3427
3632
|
var REL_PATH = ".opencode/ship.setup-pending.json";
|
|
3428
3633
|
function setupPendingPath(repoRoot) {
|
|
@@ -3430,7 +3635,7 @@ function setupPendingPath(repoRoot) {
|
|
|
3430
3635
|
}
|
|
3431
3636
|
async function writeSetupPending(repoRoot, payload) {
|
|
3432
3637
|
const path = setupPendingPath(repoRoot);
|
|
3433
|
-
await mkdirAsyncAsync(
|
|
3638
|
+
await mkdirAsyncAsync(dirname9(path), { recursive: true });
|
|
3434
3639
|
await writeFileAsync(path, JSON.stringify(payload, null, 2) + "\n", "utf8");
|
|
3435
3640
|
}
|
|
3436
3641
|
function clearSetupPending(repoRoot) {
|
|
@@ -3445,7 +3650,7 @@ function clearSetupPending(repoRoot) {
|
|
|
3445
3650
|
}
|
|
3446
3651
|
|
|
3447
3652
|
// src/installer/commands/init.js
|
|
3448
|
-
var writeFileAsync2 = promisify2(
|
|
3653
|
+
var writeFileAsync2 = promisify2(writeFile8);
|
|
3449
3654
|
var mkdirAsyncAsync2 = promisify2(mkdirAsync2);
|
|
3450
3655
|
async function runInit(options) {
|
|
3451
3656
|
try {
|
|
@@ -3594,6 +3799,7 @@ function emitFailure(code, message, json, command) {
|
|
|
3594
3799
|
}
|
|
3595
3800
|
|
|
3596
3801
|
// src/installer/commands/diff.js
|
|
3802
|
+
init_catalog();
|
|
3597
3803
|
function summarise3(plan) {
|
|
3598
3804
|
const counts = { create: 0, update: 0, noop: 0, delete: 0, conflict: 0, converge: 0, lock: 0, config: 0, rootConfig: 0 };
|
|
3599
3805
|
for (const op of plan) {
|
|
@@ -3707,6 +3913,7 @@ async function runDiff(options) {
|
|
|
3707
3913
|
}
|
|
3708
3914
|
|
|
3709
3915
|
// src/installer/commands/update.js
|
|
3916
|
+
init_catalog();
|
|
3710
3917
|
async function runUpdate(options) {
|
|
3711
3918
|
try {
|
|
3712
3919
|
validateCatalog();
|
|
@@ -3785,6 +3992,7 @@ function emitFailure2(code, message, json, command) {
|
|
|
3785
3992
|
}
|
|
3786
3993
|
|
|
3787
3994
|
// src/installer/commands/uninstall.js
|
|
3995
|
+
init_config();
|
|
3788
3996
|
import { unlink as unlink4 } from "node:fs/promises";
|
|
3789
3997
|
async function runUninstall(options) {
|
|
3790
3998
|
const preview = await previewUninstall({ rootPath: options.rootPath });
|
|
@@ -3868,6 +4076,7 @@ function emitReport(plan, conflicts, summary, json, exitCode, diagnostics) {
|
|
|
3868
4076
|
}
|
|
3869
4077
|
|
|
3870
4078
|
// src/cli.js
|
|
4079
|
+
init_version();
|
|
3871
4080
|
var VERSION = PACKAGE_VERSION;
|
|
3872
4081
|
async function main() {
|
|
3873
4082
|
const parsed = parseCommand(process.argv.slice(2));
|