opencode-ship 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +117 -7
- package/README.md +35 -4
- package/THIRD_PARTY_NOTICES.md +1 -13
- package/assets/agents/ship-controller.md +4 -0
- package/assets/commands/setup-ship-workflow.md +173 -0
- package/assets/commands/ship-deliver.md +77 -24
- package/assets/skills/delivery-workflow/SKILL.md +3 -1
- package/assets/skills/engineering-workflow/SKILL.md +5 -4
- package/assets/skills/planning-research-checkpoint/SKILL.md +18 -14
- package/assets/skills/setup-engineering-workflow/SKILL.md +121 -72
- package/assets/skills/skill-discovery/SKILL.md +104 -0
- package/dist/cli.js +199 -81
- package/dist/core.js +1 -1
- package/dist/plugin.js +18 -34
- package/docs/release/1.0.0-execution-plan.md +126 -35
- package/package.json +1 -1
- package/schema/ship-config.schema.json +5 -29
- package/vendor/sources.json +0 -198
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// opencode-ship CLI v1.
|
|
2
|
+
// opencode-ship CLI v1.1.0
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
5
|
var __esm = (fn, res) => function __init() {
|
|
@@ -516,8 +516,8 @@ var init_root_config = __esm({
|
|
|
516
516
|
});
|
|
517
517
|
|
|
518
518
|
// src/profile.js
|
|
519
|
-
var PROFILES = Object.freeze(["
|
|
520
|
-
var DEFAULT_PROFILE = "
|
|
519
|
+
var PROFILES = Object.freeze(["engineering"]);
|
|
520
|
+
var DEFAULT_PROFILE = "engineering";
|
|
521
521
|
function isValidProfile(name) {
|
|
522
522
|
return typeof name === "string" && PROFILES.includes(name);
|
|
523
523
|
}
|
|
@@ -526,19 +526,27 @@ function normalizeProfile(name) {
|
|
|
526
526
|
if (!isValidProfile(name)) return null;
|
|
527
527
|
return name;
|
|
528
528
|
}
|
|
529
|
+
function isLegacyCoreProfile(name) {
|
|
530
|
+
return name === "core";
|
|
531
|
+
}
|
|
529
532
|
function resolveProfile({ cli = null, config = null, lock = null } = {}) {
|
|
530
533
|
if (cli !== null && cli !== void 0) {
|
|
531
534
|
const v = normalizeProfile(cli);
|
|
532
535
|
if (v === null) {
|
|
533
|
-
throw new Error(
|
|
536
|
+
throw new Error(
|
|
537
|
+
`unknown CLI profile '${cli}' (only 'engineering' is supported in 1.1.0; the 'core' profile was removed)`
|
|
538
|
+
);
|
|
534
539
|
}
|
|
535
540
|
return { profile: v, source: "cli" };
|
|
536
541
|
}
|
|
537
542
|
if (config && typeof config === "object" && config.profile !== void 0 && config.profile !== null) {
|
|
538
543
|
const v = normalizeProfile(config.profile);
|
|
539
544
|
if (v === null) {
|
|
545
|
+
if (isLegacyCoreProfile(config.profile)) {
|
|
546
|
+
return { profile: DEFAULT_PROFILE, source: "default" };
|
|
547
|
+
}
|
|
540
548
|
throw new Error(
|
|
541
|
-
`unknown ship.config.json profile '${config.profile}' (
|
|
549
|
+
`unknown ship.config.json profile '${config.profile}' (only 'engineering' is supported in 1.1.0)`
|
|
542
550
|
);
|
|
543
551
|
}
|
|
544
552
|
return { profile: v, source: "config" };
|
|
@@ -546,8 +554,11 @@ function resolveProfile({ cli = null, config = null, lock = null } = {}) {
|
|
|
546
554
|
if (lock && typeof lock === "object" && lock.manager && lock.manager.profile !== void 0) {
|
|
547
555
|
const v = normalizeProfile(lock.manager.profile);
|
|
548
556
|
if (v === null) {
|
|
557
|
+
if (isLegacyCoreProfile(lock.manager.profile)) {
|
|
558
|
+
return { profile: DEFAULT_PROFILE, source: "default" };
|
|
559
|
+
}
|
|
549
560
|
throw new Error(
|
|
550
|
-
`unknown lock manager.profile '${lock.manager.profile}' (
|
|
561
|
+
`unknown lock manager.profile '${lock.manager.profile}' (only 'engineering' is supported in 1.1.0)`
|
|
551
562
|
);
|
|
552
563
|
}
|
|
553
564
|
return { profile: v, source: "lock" };
|
|
@@ -559,7 +570,7 @@ function resolveProfile({ cli = null, config = null, lock = null } = {}) {
|
|
|
559
570
|
var USAGE = `opencode-ship <command> [options]
|
|
560
571
|
|
|
561
572
|
Commands:
|
|
562
|
-
init Install
|
|
573
|
+
init Install managed files in this project. One-liner: pnpm dlx opencode-ship@latest init
|
|
563
574
|
diff Show what would change without writing.
|
|
564
575
|
update Apply pending updates after recovering the journal.
|
|
565
576
|
doctor Validate environment, lock, and references.
|
|
@@ -569,16 +580,19 @@ Commands:
|
|
|
569
580
|
|
|
570
581
|
Options:
|
|
571
582
|
--root <path> Project root (defaults to cwd).
|
|
572
|
-
--profile
|
|
583
|
+
--profile engineering Override active profile (engineering only).
|
|
573
584
|
--force-config Rewrite the user config from detection (init only).
|
|
574
585
|
--force-root-config Create opencode.json when absent (init only).
|
|
575
586
|
--strict-doctor Fail init when doctor reports unhealthy checks.
|
|
576
587
|
--replace-managed Replace locally-modified managed files (update only).
|
|
577
588
|
--purge-config Remove ship.config.json when uninstalling.
|
|
578
|
-
--planner-model <id>
|
|
579
|
-
--builder-model <id>
|
|
580
|
-
--final-reviewer-model <id>
|
|
589
|
+
--planner-model <id> Strong planner model id (init only, optional).
|
|
590
|
+
--builder-model <id> Cheap builder model id (init only, optional).
|
|
591
|
+
--final-reviewer-model <id> Final Standards + Spec reviewer model id (init only, optional).
|
|
581
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.
|
|
582
596
|
`;
|
|
583
597
|
var MODEL_ID_RE = /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/;
|
|
584
598
|
function parseFlags(argv) {
|
|
@@ -609,6 +623,11 @@ function parseFlags(argv) {
|
|
|
609
623
|
if (value === void 0) {
|
|
610
624
|
return { error: "--profile requires a value" };
|
|
611
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
|
+
}
|
|
612
631
|
if (!isValidProfile(value)) {
|
|
613
632
|
return { error: `unknown profile '${value}' (expected one of: ${PROFILES.join(", ")})` };
|
|
614
633
|
}
|
|
@@ -654,6 +673,11 @@ function helpText() {
|
|
|
654
673
|
return USAGE;
|
|
655
674
|
}
|
|
656
675
|
|
|
676
|
+
// src/installer/commands/init.js
|
|
677
|
+
import { promisify as promisify2 } from "node:util";
|
|
678
|
+
import { writeFile as writeFile7, mkdir as mkdirAsync2 } from "node:fs/promises";
|
|
679
|
+
import { dirname as dirname9, resolve as resolvePath } from "node:path";
|
|
680
|
+
|
|
657
681
|
// src/installer/executor.js
|
|
658
682
|
import { existsSync as existsSync13 } from "node:fs";
|
|
659
683
|
import { mkdir as mkdir5, readFile as readFile8, rename as rename5, unlink as unlink3, writeFile as writeFile5 } from "node:fs/promises";
|
|
@@ -689,7 +713,7 @@ function resolvePackageRoot(startUrl) {
|
|
|
689
713
|
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "node:fs";
|
|
690
714
|
import { dirname as dirname2, resolve as resolve2 } from "node:path";
|
|
691
715
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
692
|
-
var PACKAGE_VERSION = "1.
|
|
716
|
+
var PACKAGE_VERSION = "1.1.0";
|
|
693
717
|
var TEMPLATE_SET = `v${PACKAGE_VERSION}`;
|
|
694
718
|
|
|
695
719
|
// src/installer/catalog.js
|
|
@@ -743,7 +767,7 @@ var CATALOG = [
|
|
|
743
767
|
path: ".opencode/plugins/opencode-ship.js",
|
|
744
768
|
source: resolve3(packageRoot, "dist/plugin.js"),
|
|
745
769
|
mode: 420,
|
|
746
|
-
profiles: ["
|
|
770
|
+
profiles: ["engineering"]
|
|
747
771
|
},
|
|
748
772
|
{
|
|
749
773
|
id: "agent:delivery-reviewer",
|
|
@@ -751,7 +775,7 @@ var CATALOG = [
|
|
|
751
775
|
path: ".opencode/agents/delivery-reviewer.md",
|
|
752
776
|
source: resolve3(packageRoot, "assets/agents/delivery-reviewer.md"),
|
|
753
777
|
mode: 420,
|
|
754
|
-
profiles: ["
|
|
778
|
+
profiles: ["engineering"]
|
|
755
779
|
},
|
|
756
780
|
{
|
|
757
781
|
id: "agent:delivery-verifier",
|
|
@@ -759,7 +783,7 @@ var CATALOG = [
|
|
|
759
783
|
path: ".opencode/agents/delivery-verifier.md",
|
|
760
784
|
source: resolve3(packageRoot, "assets/agents/delivery-verifier.md"),
|
|
761
785
|
mode: 420,
|
|
762
|
-
profiles: ["
|
|
786
|
+
profiles: ["engineering"]
|
|
763
787
|
},
|
|
764
788
|
...ENGINEERING_AGENTS.map((name) => ({
|
|
765
789
|
id: `agent:${name}`,
|
|
@@ -783,7 +807,7 @@ var CATALOG = [
|
|
|
783
807
|
path: ".opencode/skills/delivery-workflow/SKILL.md",
|
|
784
808
|
source: resolve3(packageRoot, "assets/skills/delivery-workflow/SKILL.md"),
|
|
785
809
|
mode: 420,
|
|
786
|
-
profiles: ["
|
|
810
|
+
profiles: ["engineering"]
|
|
787
811
|
},
|
|
788
812
|
{
|
|
789
813
|
id: "skill:planning-research-checkpoint",
|
|
@@ -791,7 +815,7 @@ var CATALOG = [
|
|
|
791
815
|
path: ".opencode/skills/planning-research-checkpoint/SKILL.md",
|
|
792
816
|
source: resolve3(packageRoot, "assets/skills/planning-research-checkpoint/SKILL.md"),
|
|
793
817
|
mode: 420,
|
|
794
|
-
profiles: ["
|
|
818
|
+
profiles: ["engineering"]
|
|
795
819
|
},
|
|
796
820
|
...MATT_SKILLS.map((name) => ({
|
|
797
821
|
id: `skill:matt:${name}`,
|
|
@@ -808,7 +832,31 @@ var CATALOG = [
|
|
|
808
832
|
source: resolve3(packageRoot, `assets/skills/${name}/SKILL.md`),
|
|
809
833
|
mode: 420,
|
|
810
834
|
profiles: ["engineering"]
|
|
811
|
-
}))
|
|
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
|
+
}
|
|
812
860
|
];
|
|
813
861
|
function filterCatalogByProfile(catalog, profile) {
|
|
814
862
|
const effective = profile === void 0 || profile === null ? DEFAULT_PROFILE : profile;
|
|
@@ -913,8 +961,8 @@ var ship_config_schema_default = {
|
|
|
913
961
|
schemaVersion: { enum: [1, 2] },
|
|
914
962
|
profile: {
|
|
915
963
|
type: "string",
|
|
916
|
-
enum: ["
|
|
917
|
-
description: "Active profile
|
|
964
|
+
enum: ["engineering"],
|
|
965
|
+
description: "Active profile. Engineering is the only supported profile in 1.1.0."
|
|
918
966
|
},
|
|
919
967
|
owner: {
|
|
920
968
|
type: "string",
|
|
@@ -1043,13 +1091,13 @@ var ship_config_schema_default = {
|
|
|
1043
1091
|
},
|
|
1044
1092
|
workflow: {
|
|
1045
1093
|
type: "object",
|
|
1046
|
-
description: "
|
|
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.",
|
|
1047
1095
|
additionalProperties: false,
|
|
1048
1096
|
properties: {
|
|
1049
1097
|
models: {
|
|
1050
1098
|
type: "object",
|
|
1051
1099
|
additionalProperties: false,
|
|
1052
|
-
|
|
1100
|
+
description: "Optional model roles. All three roles must be present before ship-deliver can run.",
|
|
1053
1101
|
properties: {
|
|
1054
1102
|
planner: {
|
|
1055
1103
|
type: "string",
|
|
@@ -1078,31 +1126,7 @@ var ship_config_schema_default = {
|
|
|
1078
1126
|
}
|
|
1079
1127
|
}
|
|
1080
1128
|
}
|
|
1081
|
-
}
|
|
1082
|
-
allOf: [
|
|
1083
|
-
{
|
|
1084
|
-
if: {
|
|
1085
|
-
required: ["profile"],
|
|
1086
|
-
properties: { profile: { const: "engineering" } }
|
|
1087
|
-
},
|
|
1088
|
-
then: {
|
|
1089
|
-
required: ["workflow"],
|
|
1090
|
-
properties: {
|
|
1091
|
-
workflow: {
|
|
1092
|
-
required: ["models", "approval"],
|
|
1093
|
-
properties: {
|
|
1094
|
-
models: {
|
|
1095
|
-
required: ["planner", "builder", "finalReviewer"]
|
|
1096
|
-
},
|
|
1097
|
-
approval: {
|
|
1098
|
-
required: ["mirrorToIssue", "maxFailedRounds"]
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
]
|
|
1129
|
+
}
|
|
1106
1130
|
};
|
|
1107
1131
|
|
|
1108
1132
|
// src/installer/validation.js
|
|
@@ -1241,7 +1265,8 @@ function renderDefaultConfig(detection, overrides = {}) {
|
|
|
1241
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"] }];
|
|
1242
1266
|
const repo = detection?.repository ?? overrides.repository ?? "owner/repo";
|
|
1243
1267
|
return {
|
|
1244
|
-
schemaVersion:
|
|
1268
|
+
schemaVersion: 2,
|
|
1269
|
+
profile: "engineering",
|
|
1245
1270
|
project: {
|
|
1246
1271
|
remote: detection?.remote ?? "origin",
|
|
1247
1272
|
repository: repo,
|
|
@@ -1270,9 +1295,19 @@ function renderDefaultConfig(detection, overrides = {}) {
|
|
|
1270
1295
|
ready: { requires: ["review", "local-verification", "remote-ci"], stopAfterReady: true },
|
|
1271
1296
|
merge: { strategy: "squash", policy: "explicit-user-request-only", requireFreshGates: true },
|
|
1272
1297
|
cleanup: { when: "next-task", requireUnpublishedGuard: true }
|
|
1298
|
+
},
|
|
1299
|
+
workflow: {
|
|
1300
|
+
models: {},
|
|
1301
|
+
approval: { mirrorToIssue: true, maxFailedRounds: 3 }
|
|
1273
1302
|
}
|
|
1274
1303
|
};
|
|
1275
1304
|
}
|
|
1305
|
+
function hasCompletedModels(configValue) {
|
|
1306
|
+
const models = configValue?.workflow?.models;
|
|
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);
|
|
1310
|
+
}
|
|
1276
1311
|
|
|
1277
1312
|
// src/installer/planner.js
|
|
1278
1313
|
init_json_pointer();
|
|
@@ -2801,6 +2836,10 @@ function legacyToShipConfig(legacy, detection = null) {
|
|
|
2801
2836
|
ready: legacy.ready ?? { requires: ["review", "local-verification", "remote-ci"], stopAfterReady: true },
|
|
2802
2837
|
merge: legacy.merge ?? { strategy: "squash", policy: "explicit-user-request-only", requireFreshGates: true },
|
|
2803
2838
|
cleanup: legacy.cleanup && typeof legacy.cleanup === "object" && "when" in legacy.cleanup ? legacy.cleanup : { when: "next-task", requireUnpublishedGuard: true }
|
|
2839
|
+
},
|
|
2840
|
+
workflow: {
|
|
2841
|
+
models: {},
|
|
2842
|
+
approval: { mirrorToIssue: true, maxFailedRounds: 3 }
|
|
2804
2843
|
}
|
|
2805
2844
|
};
|
|
2806
2845
|
}
|
|
@@ -2845,14 +2884,7 @@ async function previewInstall({ rootPath, profile = null, replaceManaged, forceC
|
|
|
2845
2884
|
});
|
|
2846
2885
|
if (candidate.kind === "create" || candidate.kind === "update") {
|
|
2847
2886
|
const configValue2 = candidate.configValue;
|
|
2848
|
-
if (!configValue2.workflow || !configValue2.workflow.
|
|
2849
|
-
return { ok: false, error: { kind: "engineering-models-required", message: "engineering profile requires workflow.models.{planner,builder,finalReviewer}" } };
|
|
2850
|
-
}
|
|
2851
|
-
const { planner, builder, finalReviewer } = configValue2.workflow.models;
|
|
2852
|
-
if (!planner || !builder || !finalReviewer) {
|
|
2853
|
-
return { ok: false, error: { kind: "engineering-models-required", message: "all three workflow.models.* are required" } };
|
|
2854
|
-
}
|
|
2855
|
-
if (!configValue2.workflow.approval || !configValue2.workflow.approval.mirrorToIssue || configValue2.workflow.approval.maxFailedRounds !== 3) {
|
|
2887
|
+
if (!configValue2.workflow || !configValue2.workflow.approval) {
|
|
2856
2888
|
return { ok: false, error: { kind: "engineering-approval-required", message: "engineering profile requires workflow.approval.{mirrorToIssue:true, maxFailedRounds:3}" } };
|
|
2857
2889
|
}
|
|
2858
2890
|
}
|
|
@@ -2866,7 +2898,8 @@ async function previewInstall({ rootPath, profile = null, replaceManaged, forceC
|
|
|
2866
2898
|
detection,
|
|
2867
2899
|
lock,
|
|
2868
2900
|
forceOverwrite: Boolean(forceConfig),
|
|
2869
|
-
migrationSeed: migrationReport?.proposedConfigSeed ?? null
|
|
2901
|
+
migrationSeed: migrationReport?.proposedConfigSeed ?? null,
|
|
2902
|
+
models
|
|
2870
2903
|
});
|
|
2871
2904
|
const filePlan = await planFileInstall({ repoRoot, lock, allowUnowned: Boolean(replaceManaged), catalog: activeCatalog });
|
|
2872
2905
|
const staleFilePlan = await planStaleFileRemoval({ repoRoot, lock, staleCatalog });
|
|
@@ -2878,6 +2911,7 @@ async function previewInstall({ rootPath, profile = null, replaceManaged, forceC
|
|
|
2878
2911
|
});
|
|
2879
2912
|
const planMode = resolved.profile === "engineering" ? { id: "/agent/plan/permission", block: planModePermissions().build, scope: "engineering" } : null;
|
|
2880
2913
|
const rootPlan = await planRootConfigApply({ repoRoot, lock, forceRepair: Boolean(forceRootConfig), planMode });
|
|
2914
|
+
const setupPending = resolved.profile === "engineering" && !lock?.manager?.setupComplete && !hasCompletedModels(configValue?.workflow?.models ?? {}) && !models?.planner;
|
|
2881
2915
|
const plan = [...filePlan ?? [], ...staleFilePlan, ...migrationPlan, configPlan, rootPlan];
|
|
2882
2916
|
const conflicts = plan.filter((p) => p && p.kind === "conflict");
|
|
2883
2917
|
const summary = summarise(plan);
|
|
@@ -2892,7 +2926,8 @@ async function previewInstall({ rootPath, profile = null, replaceManaged, forceC
|
|
|
2892
2926
|
plan,
|
|
2893
2927
|
conflicts,
|
|
2894
2928
|
summary,
|
|
2895
|
-
migrationReport
|
|
2929
|
+
migrationReport,
|
|
2930
|
+
setupPending
|
|
2896
2931
|
};
|
|
2897
2932
|
}
|
|
2898
2933
|
async function previewUninstall({ rootPath }) {
|
|
@@ -2927,7 +2962,7 @@ function summarise(plan) {
|
|
|
2927
2962
|
}
|
|
2928
2963
|
return counts;
|
|
2929
2964
|
}
|
|
2930
|
-
async function assembleLock({ repoRoot, plan, lock, configPlan, rootPlan, profile = null }) {
|
|
2965
|
+
async function assembleLock({ repoRoot, plan, lock, configPlan, rootPlan, profile = null, models = null }) {
|
|
2931
2966
|
const files = [];
|
|
2932
2967
|
const remain = lock?.files?.filter((f) => !plan.some((op) => op?.relPath === f.path)) ?? [];
|
|
2933
2968
|
for (const op of plan) {
|
|
@@ -2955,18 +2990,18 @@ async function assembleLock({ repoRoot, plan, lock, configPlan, rootPlan, profil
|
|
|
2955
2990
|
const rootPointers = rootPlan?.pointerRecords ?? lock?.manager?.rootDocuments?.[0]?.pointers ?? [];
|
|
2956
2991
|
const hasRootPlan = Boolean(rootPlan?.target || rootPlan?.pointerRecords && rootPlan.pointerRecords.length > 0);
|
|
2957
2992
|
const hasRootDocuments = rootPlan?.pointerRecords && rootPlan.pointerRecords.length > 0 || lock?.manager?.rootDocuments && lock.manager.rootDocuments.length > 0;
|
|
2993
|
+
const configModels = configPlan?.configValue?.workflow?.models ?? lock?.manager?.config?.models ?? {};
|
|
2994
|
+
const completedModels = hasCompletedModels({ workflow: { models: configModels } });
|
|
2958
2995
|
return {
|
|
2959
2996
|
contractVersion: CURRENT_LOCK_SCHEMA,
|
|
2960
2997
|
manager: {
|
|
2961
2998
|
schemaVersion: CURRENT_LOCK_SCHEMA,
|
|
2962
2999
|
name: "opencode-ship",
|
|
2963
|
-
version: "1.
|
|
3000
|
+
version: "1.1.0",
|
|
2964
3001
|
templateSet: TEMPLATE_SET_ID,
|
|
2965
|
-
// Newly written locks always carry the resolved profile so
|
|
2966
|
-
// future CLI invocations without --profile still resolve to
|
|
2967
|
-
// the same choice through the lock-precedence layer.
|
|
2968
3002
|
profile: profile ?? lock?.manager?.profile,
|
|
2969
3003
|
appliedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3004
|
+
setupComplete: completedModels,
|
|
2970
3005
|
config: {
|
|
2971
3006
|
path: ".opencode/ship.config.json",
|
|
2972
3007
|
sha256: configSha ?? lock?.manager?.config?.sha256 ?? "",
|
|
@@ -3383,7 +3418,35 @@ async function runDoctor({ rootPath, profile, json, writeOutput = true }) {
|
|
|
3383
3418
|
return { issues, exitCode, plan, checks, profile: resolved };
|
|
3384
3419
|
}
|
|
3385
3420
|
|
|
3421
|
+
// src/installer/setup-pending.js
|
|
3422
|
+
import { existsSync as existsSync15, readFileSync as readFileSync6, unlinkSync, writeFile as writeFile6, mkdir as mkdirAsync } from "node:fs";
|
|
3423
|
+
import { promisify } from "node:util";
|
|
3424
|
+
import { resolve as resolve14, dirname as dirname8 } from "node:path";
|
|
3425
|
+
var writeFileAsync = promisify(writeFile6);
|
|
3426
|
+
var mkdirAsyncAsync = promisify(mkdirAsync);
|
|
3427
|
+
var REL_PATH = ".opencode/ship.setup-pending.json";
|
|
3428
|
+
function setupPendingPath(repoRoot) {
|
|
3429
|
+
return resolve14(repoRoot, REL_PATH);
|
|
3430
|
+
}
|
|
3431
|
+
async function writeSetupPending(repoRoot, payload) {
|
|
3432
|
+
const path = setupPendingPath(repoRoot);
|
|
3433
|
+
await mkdirAsyncAsync(dirname8(path), { recursive: true });
|
|
3434
|
+
await writeFileAsync(path, JSON.stringify(payload, null, 2) + "\n", "utf8");
|
|
3435
|
+
}
|
|
3436
|
+
function clearSetupPending(repoRoot) {
|
|
3437
|
+
const path = setupPendingPath(repoRoot);
|
|
3438
|
+
if (!existsSync15(path)) return false;
|
|
3439
|
+
try {
|
|
3440
|
+
unlinkSync(path);
|
|
3441
|
+
return true;
|
|
3442
|
+
} catch {
|
|
3443
|
+
return false;
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
|
|
3386
3447
|
// src/installer/commands/init.js
|
|
3448
|
+
var writeFileAsync2 = promisify2(writeFile7);
|
|
3449
|
+
var mkdirAsyncAsync2 = promisify2(mkdirAsync2);
|
|
3387
3450
|
async function runInit(options) {
|
|
3388
3451
|
try {
|
|
3389
3452
|
validateCatalog();
|
|
@@ -3393,21 +3456,23 @@ async function runInit(options) {
|
|
|
3393
3456
|
}
|
|
3394
3457
|
throw e;
|
|
3395
3458
|
}
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3459
|
+
let preview;
|
|
3460
|
+
try {
|
|
3461
|
+
preview = await previewInstall({
|
|
3462
|
+
rootPath: options.rootPath ?? null,
|
|
3463
|
+
profile: options.profile ?? null,
|
|
3464
|
+
replaceManaged: false,
|
|
3465
|
+
forceConfig: Boolean(options.forceConfig),
|
|
3466
|
+
forceRootConfig: Boolean(options.forceRootConfig),
|
|
3467
|
+
models: options.models ?? null
|
|
3468
|
+
});
|
|
3469
|
+
} catch (e) {
|
|
3470
|
+
return emitFailure(2, e?.message ?? "invalid input", options.json, "init");
|
|
3471
|
+
}
|
|
3404
3472
|
if (!preview.ok) {
|
|
3405
3473
|
if (preview.error?.kind === "unsupported-lock-schema") {
|
|
3406
3474
|
return emitFailure(5, `unsupported lock schema: ${(preview.error.issues ?? []).join("; ")}`, options.json, "init");
|
|
3407
3475
|
}
|
|
3408
|
-
if (preview.error?.kind === "engineering-models-required") {
|
|
3409
|
-
return emitFailure(2, preview.error.message, options.json, "init");
|
|
3410
|
-
}
|
|
3411
3476
|
if (preview.error?.kind === "engineering-approval-required") {
|
|
3412
3477
|
return emitFailure(2, preview.error.message, options.json, "init");
|
|
3413
3478
|
}
|
|
@@ -3438,6 +3503,14 @@ async function runInit(options) {
|
|
|
3438
3503
|
exitCode = 1;
|
|
3439
3504
|
}
|
|
3440
3505
|
}
|
|
3506
|
+
const setupPending = Boolean(preview.setupPending);
|
|
3507
|
+
if (setupPending && preview.repoRoot) {
|
|
3508
|
+
await writeSetupPending(preview.repoRoot, {
|
|
3509
|
+
profile: preview.profile?.profile ?? "engineering",
|
|
3510
|
+
reason: "workflow.models is empty; run /setup-ship-workflow to fill in model roles",
|
|
3511
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3512
|
+
});
|
|
3513
|
+
}
|
|
3441
3514
|
if (options.json) {
|
|
3442
3515
|
const envelope = {
|
|
3443
3516
|
reportVersion: 1,
|
|
@@ -3449,19 +3522,56 @@ async function runInit(options) {
|
|
|
3449
3522
|
diagnostics: committed.diagnostics ?? [],
|
|
3450
3523
|
doctor: doctor.issues ?? [],
|
|
3451
3524
|
doctorChecks: doctor.checks ?? [],
|
|
3525
|
+
setupPending,
|
|
3452
3526
|
exitCode
|
|
3453
3527
|
};
|
|
3454
3528
|
Object.assign(envelope, committed.extra ?? {}, { doctor: doctor.issues ?? [] });
|
|
3455
3529
|
process.stdout.write(JSON.stringify(envelope, null, 2) + "\n");
|
|
3456
|
-
} else if (exitCode !== 0) {
|
|
3457
|
-
process.stdout.write(`opencode-ship: doctor reported ${doctor.issues.length} unhealthy check(s)
|
|
3458
|
-
`);
|
|
3459
3530
|
} else {
|
|
3460
|
-
|
|
3461
|
-
|
|
3531
|
+
printHumanResult({
|
|
3532
|
+
prefix: "opencode-ship",
|
|
3533
|
+
exitCode,
|
|
3534
|
+
doctorIssues: doctor.issues,
|
|
3535
|
+
setupPending
|
|
3536
|
+
});
|
|
3462
3537
|
}
|
|
3463
3538
|
process.exitCode = exitCode;
|
|
3464
|
-
return { ok: exitCode === 0, exitCode };
|
|
3539
|
+
return { ok: exitCode === 0, exitCode, setupPending };
|
|
3540
|
+
}
|
|
3541
|
+
function printHumanResult({ prefix, exitCode, doctorIssues, setupPending }) {
|
|
3542
|
+
const lines = [];
|
|
3543
|
+
if (exitCode === 0) {
|
|
3544
|
+
lines.push(`${prefix}: installed; doctor OK`);
|
|
3545
|
+
} else {
|
|
3546
|
+
lines.push(`${prefix}: installed with warnings`);
|
|
3547
|
+
}
|
|
3548
|
+
if (Array.isArray(doctorIssues) && doctorIssues.length > 0) {
|
|
3549
|
+
lines.push("");
|
|
3550
|
+
lines.push("Doctor reported:");
|
|
3551
|
+
for (const issue of doctorIssues) lines.push(` - ${issue}`);
|
|
3552
|
+
}
|
|
3553
|
+
if (setupPending) {
|
|
3554
|
+
lines.push("");
|
|
3555
|
+
lines.push("NEXT:");
|
|
3556
|
+
lines.push(" 1. Restart OpenCode in this repo (if you haven't already).");
|
|
3557
|
+
lines.push(" 2. In chat, run: /setup-ship-workflow");
|
|
3558
|
+
lines.push(" (or type: continue ship setup)");
|
|
3559
|
+
lines.push(" 3. The skill will ask for:");
|
|
3560
|
+
lines.push(" - issue tracker (GitHub / GitLab / local / other)");
|
|
3561
|
+
lines.push(" - triage labels (defaults are fine)");
|
|
3562
|
+
lines.push(" - domain docs (single-context default)");
|
|
3563
|
+
lines.push(" - AI model roles (planner / builder / finalReviewer)");
|
|
3564
|
+
lines.push(" 4. After setup, try: Ship issue <number>");
|
|
3565
|
+
lines.push("");
|
|
3566
|
+
lines.push("The controller will refuse to dispatch until setup is complete.");
|
|
3567
|
+
} else {
|
|
3568
|
+
lines.push("");
|
|
3569
|
+
lines.push("NEXT:");
|
|
3570
|
+
lines.push(" 1. Restart OpenCode in this repo.");
|
|
3571
|
+
lines.push(" 2. Run: opencode-ship doctor && to confirm everything is clean.");
|
|
3572
|
+
lines.push(" 3. Run: Ship issue <number> (or /setup-ship-workflow to customise first)");
|
|
3573
|
+
}
|
|
3574
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
3465
3575
|
}
|
|
3466
3576
|
function emitFailure(code, message, json, command) {
|
|
3467
3577
|
if (json) {
|
|
@@ -3606,11 +3716,15 @@ async function runUpdate(options) {
|
|
|
3606
3716
|
}
|
|
3607
3717
|
throw e;
|
|
3608
3718
|
}
|
|
3719
|
+
const hasExplicitModels = options.models && (options.models.planner || options.models.builder || options.models.finalReviewer);
|
|
3609
3720
|
const preview = await previewInstall({
|
|
3610
3721
|
rootPath: options.rootPath,
|
|
3611
3722
|
profile: options.profile ?? null,
|
|
3612
3723
|
replaceManaged: options.replaceManaged,
|
|
3613
|
-
|
|
3724
|
+
// If the user passes model flags, the run is explicitly
|
|
3725
|
+
// populating workflow.models. We must rewrite the config in
|
|
3726
|
+
// that case even when the existing config is otherwise valid.
|
|
3727
|
+
forceConfig: Boolean(options.forceConfig || hasExplicitModels),
|
|
3614
3728
|
forceRootConfig: options.forceRootConfig,
|
|
3615
3729
|
models: options.models ?? null
|
|
3616
3730
|
});
|
|
@@ -3627,6 +3741,9 @@ async function runUpdate(options) {
|
|
|
3627
3741
|
return emitFailure2(3, "modified managed files; rerun with --replace-managed", options.json, "update");
|
|
3628
3742
|
}
|
|
3629
3743
|
const committed = await commitInstall(preview, { json: options.json, command: "update" });
|
|
3744
|
+
if (committed.extra?.exitCode === 0 && preview.repoRoot && !preview.setupPending) {
|
|
3745
|
+
clearSetupPending(preview.repoRoot);
|
|
3746
|
+
}
|
|
3630
3747
|
if (options.json) {
|
|
3631
3748
|
process.stdout.write(JSON.stringify({
|
|
3632
3749
|
reportVersion: 1,
|
|
@@ -3637,6 +3754,7 @@ async function runUpdate(options) {
|
|
|
3637
3754
|
summary: committed.summary ?? {},
|
|
3638
3755
|
diagnostics: committed.diagnostics ?? [],
|
|
3639
3756
|
exitCode: committed.extra?.exitCode ?? 0,
|
|
3757
|
+
setupPending: Boolean(preview.setupPending),
|
|
3640
3758
|
...committed.extra ?? {}
|
|
3641
3759
|
}, null, 2) + "\n");
|
|
3642
3760
|
} else if (committed.extra?.exitCode === 0) {
|
package/dist/core.js
CHANGED