opencode-ship 0.9.0 → 0.10.0-rc.18
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 +96 -0
- package/README.md +3 -1
- package/THIRD_PARTY_NOTICES.md +77 -19
- package/assets/agents/ship-controller.md +122 -0
- package/assets/agents/ship-final-spec-reviewer.md +87 -0
- package/assets/agents/ship-final-standards-reviewer.md +83 -0
- package/assets/agents/ship-planner.md +78 -0
- package/assets/agents/ship-task-builder.md +91 -0
- package/assets/agents/ship-task-reviewer.md +78 -0
- package/assets/commands/ship-deliver.md +30 -0
- package/assets/commands/ship-resume.md +28 -0
- package/assets/commands/ship-status.md +25 -0
- package/assets/skills/brainstorming/SKILL.md +160 -0
- package/assets/skills/brainstorming/spec-document-reviewer-prompt.md +49 -0
- package/assets/skills/brainstorming/visual-companion.md +298 -0
- package/assets/skills/code-review/SKILL.md +98 -0
- package/assets/skills/codebase-design/DEEPENING.md +37 -0
- package/assets/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
- package/assets/skills/codebase-design/SKILL.md +123 -0
- package/assets/skills/dispatching-parallel-agents/SKILL.md +176 -0
- package/assets/skills/domain-modeling/ADR-FORMAT.md +47 -0
- package/assets/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/assets/skills/domain-modeling/SKILL.md +83 -0
- package/assets/skills/engineering-workflow/SKILL.md +125 -0
- package/assets/skills/engineering-workflow/domain.md +51 -0
- package/assets/skills/engineering-workflow/issue-tracker-github.md +45 -0
- package/assets/skills/engineering-workflow/issue-tracker-gitlab.md +46 -0
- package/assets/skills/engineering-workflow/issue-tracker-local.md +30 -0
- package/assets/skills/engineering-workflow/triage-labels.md +15 -0
- package/assets/skills/executing-plans/SKILL.md +73 -0
- package/assets/skills/grill-with-docs/SKILL.md +10 -14
- package/assets/skills/grilling/SKILL.md +21 -0
- package/assets/skills/handoff/SKILL.md +25 -0
- package/assets/skills/prototype/LOGIC.md +79 -0
- package/assets/skills/prototype/SKILL.md +35 -0
- package/assets/skills/prototype/UI.md +112 -0
- package/assets/skills/receiving-code-review/SKILL.md +214 -0
- package/assets/skills/requesting-code-review/SKILL.md +104 -0
- package/assets/skills/requesting-code-review/code-reviewer.md +172 -0
- package/assets/skills/research/SKILL.md +21 -0
- package/assets/skills/setup-engineering-workflow/SKILL.md +125 -0
- package/assets/skills/setup-engineering-workflow/domain.md +51 -0
- package/assets/skills/setup-engineering-workflow/issue-tracker-github.md +45 -0
- package/assets/skills/setup-engineering-workflow/issue-tracker-gitlab.md +46 -0
- package/assets/skills/setup-engineering-workflow/issue-tracker-local.md +30 -0
- package/assets/skills/setup-engineering-workflow/triage-labels.md +15 -0
- package/assets/skills/subagent-driven-development/SKILL.md +512 -0
- package/assets/skills/subagent-driven-development/implementer-prompt.md +142 -0
- package/assets/skills/subagent-driven-development/re-review-prompt.md +106 -0
- package/assets/skills/subagent-driven-development/task-reviewer-prompt.md +185 -0
- package/assets/skills/systematic-debugging/CREATION-LOG.md +119 -0
- package/assets/skills/systematic-debugging/SKILL.md +292 -0
- package/assets/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/assets/skills/systematic-debugging/condition-based-waiting.md +115 -0
- package/assets/skills/systematic-debugging/defense-in-depth.md +122 -0
- package/assets/skills/systematic-debugging/find-polluter.sh +72 -0
- package/assets/skills/systematic-debugging/root-cause-tracing.md +169 -0
- package/assets/skills/systematic-debugging/test-academic.md +14 -0
- package/assets/skills/systematic-debugging/test-pressure-1.md +58 -0
- package/assets/skills/systematic-debugging/test-pressure-2.md +68 -0
- package/assets/skills/systematic-debugging/test-pressure-3.md +69 -0
- package/assets/skills/test-driven-development/SKILL.md +329 -0
- package/assets/skills/test-driven-development/writing-good-tests.md +198 -0
- package/assets/skills/to-spec/SKILL.md +84 -0
- package/assets/skills/to-tickets/SKILL.md +114 -0
- package/assets/skills/triage/AGENT-BRIEF.md +207 -0
- package/assets/skills/triage/OUT-OF-SCOPE.md +105 -0
- package/assets/skills/triage/SKILL.md +114 -15
- package/assets/skills/verification-before-completion/SKILL.md +129 -0
- package/assets/skills/wayfinder/SKILL.md +137 -0
- package/assets/skills/writing-plans/SKILL.md +177 -0
- package/assets/skills/writing-plans/plan-document-reviewer-prompt.md +49 -0
- package/dist/cli.js +1146 -435
- package/dist/core.js +210 -61
- package/dist/plugin.js +2368 -230
- package/package.json +3 -1
- package/schema/ship-config.schema.json +63 -2
- package/schema/ship-lock.schema.json +5 -3
- package/tests/plugin/plugin-load.test.mjs +85 -0
- package/vendor/mattpocock/LICENSE +30 -0
- package/vendor/obra/LICENSE +30 -0
- package/vendor/sources.json +976 -0
- package/vendor/superpowers/LICENSE +30 -0
- package/vendor/upstreams/mattpocock/skills/engineering/code-review/SKILL.md +89 -0
- package/vendor/upstreams/mattpocock/skills/engineering/codebase-design/DEEPENING.md +37 -0
- package/vendor/upstreams/mattpocock/skills/engineering/codebase-design/DESIGN-IT-TWICE.md +44 -0
- package/vendor/upstreams/mattpocock/skills/engineering/codebase-design/SKILL.md +114 -0
- package/vendor/upstreams/mattpocock/skills/engineering/domain-modeling/ADR-FORMAT.md +47 -0
- package/vendor/upstreams/mattpocock/skills/engineering/domain-modeling/CONTEXT-FORMAT.md +60 -0
- package/vendor/upstreams/mattpocock/skills/engineering/domain-modeling/SKILL.md +74 -0
- package/vendor/upstreams/mattpocock/skills/engineering/grill-with-docs/SKILL.md +7 -0
- package/vendor/upstreams/mattpocock/skills/engineering/prototype/LOGIC.md +79 -0
- package/vendor/upstreams/mattpocock/skills/engineering/prototype/SKILL.md +26 -0
- package/vendor/upstreams/mattpocock/skills/engineering/prototype/UI.md +112 -0
- package/vendor/upstreams/mattpocock/skills/engineering/research/SKILL.md +12 -0
- package/vendor/upstreams/mattpocock/skills/engineering/setup-matt-pocock-skills/SKILL.md +116 -0
- package/vendor/upstreams/mattpocock/skills/engineering/setup-matt-pocock-skills/domain.md +51 -0
- package/vendor/upstreams/mattpocock/skills/engineering/setup-matt-pocock-skills/issue-tracker-github.md +45 -0
- package/vendor/upstreams/mattpocock/skills/engineering/setup-matt-pocock-skills/issue-tracker-gitlab.md +46 -0
- package/vendor/upstreams/mattpocock/skills/engineering/setup-matt-pocock-skills/issue-tracker-local.md +30 -0
- package/vendor/upstreams/mattpocock/skills/engineering/setup-matt-pocock-skills/triage-labels.md +15 -0
- package/vendor/upstreams/mattpocock/skills/engineering/to-spec/SKILL.md +75 -0
- package/vendor/upstreams/mattpocock/skills/engineering/to-tickets/SKILL.md +105 -0
- package/vendor/upstreams/mattpocock/skills/engineering/triage/AGENT-BRIEF.md +207 -0
- package/vendor/upstreams/mattpocock/skills/engineering/triage/OUT-OF-SCOPE.md +105 -0
- package/vendor/upstreams/mattpocock/skills/engineering/triage/SKILL.md +112 -0
- package/vendor/upstreams/mattpocock/skills/engineering/wayfinder/SKILL.md +128 -0
- package/vendor/upstreams/mattpocock/skills/productivity/grilling/SKILL.md +12 -0
- package/vendor/upstreams/mattpocock/skills/productivity/handoff/SKILL.md +16 -0
- package/vendor/upstreams/obra/skills/brainstorming/SKILL.md +151 -0
- package/vendor/upstreams/obra/skills/brainstorming/spec-document-reviewer-prompt.md +49 -0
- package/vendor/upstreams/obra/skills/brainstorming/visual-companion.md +298 -0
- package/vendor/upstreams/obra/skills/dispatching-parallel-agents/SKILL.md +167 -0
- package/vendor/upstreams/obra/skills/executing-plans/SKILL.md +64 -0
- package/vendor/upstreams/obra/skills/receiving-code-review/SKILL.md +205 -0
- package/vendor/upstreams/obra/skills/requesting-code-review/SKILL.md +95 -0
- package/vendor/upstreams/obra/skills/requesting-code-review/code-reviewer.md +172 -0
- package/vendor/upstreams/obra/skills/subagent-driven-development/SKILL.md +503 -0
- package/vendor/upstreams/obra/skills/subagent-driven-development/implementer-prompt.md +142 -0
- package/vendor/upstreams/obra/skills/subagent-driven-development/re-review-prompt.md +106 -0
- package/vendor/upstreams/obra/skills/subagent-driven-development/task-reviewer-prompt.md +185 -0
- package/vendor/upstreams/obra/skills/systematic-debugging/CREATION-LOG.md +119 -0
- package/vendor/upstreams/obra/skills/systematic-debugging/SKILL.md +283 -0
- package/vendor/upstreams/obra/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/vendor/upstreams/obra/skills/systematic-debugging/condition-based-waiting.md +115 -0
- package/vendor/upstreams/obra/skills/systematic-debugging/defense-in-depth.md +122 -0
- package/vendor/upstreams/obra/skills/systematic-debugging/find-polluter.sh +72 -0
- package/vendor/upstreams/obra/skills/systematic-debugging/root-cause-tracing.md +169 -0
- package/vendor/upstreams/obra/skills/systematic-debugging/test-academic.md +14 -0
- package/vendor/upstreams/obra/skills/systematic-debugging/test-pressure-1.md +58 -0
- package/vendor/upstreams/obra/skills/systematic-debugging/test-pressure-2.md +68 -0
- package/vendor/upstreams/obra/skills/systematic-debugging/test-pressure-3.md +69 -0
- package/vendor/upstreams/obra/skills/test-driven-development/SKILL.md +320 -0
- package/vendor/upstreams/obra/skills/test-driven-development/writing-good-tests.md +198 -0
- package/vendor/upstreams/obra/skills/verification-before-completion/SKILL.md +120 -0
- package/vendor/upstreams/obra/skills/writing-plans/SKILL.md +168 -0
- package/vendor/upstreams/obra/skills/writing-plans/plan-document-reviewer-prompt.md +49 -0
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// opencode-ship CLI v0.
|
|
2
|
+
// opencode-ship CLI v0.10.0-rc.18
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
5
|
var __esm = (fn, res) => function __init() {
|
|
@@ -11,21 +11,9 @@ var __export = (target, all) => {
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
// src/installer/json-pointer.js
|
|
14
|
-
var json_pointer_exports = {};
|
|
15
|
-
__export(json_pointer_exports, {
|
|
16
|
-
getPointer: () => getPointer,
|
|
17
|
-
parsePointer: () => parsePointer,
|
|
18
|
-
pointerPath: () => pointerPath,
|
|
19
|
-
removePointer: () => removePointer,
|
|
20
|
-
setPointer: () => setPointer,
|
|
21
|
-
stableStringify: () => stableStringify
|
|
22
|
-
});
|
|
23
14
|
function unescape(token) {
|
|
24
15
|
return token.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
25
16
|
}
|
|
26
|
-
function escape(token) {
|
|
27
|
-
return token.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
28
|
-
}
|
|
29
17
|
function* parsePointer(pointer) {
|
|
30
18
|
if (typeof pointer !== "string" || pointer === "") return;
|
|
31
19
|
if (pointer[0] !== "/") {
|
|
@@ -88,9 +76,6 @@ function stableStringify(value) {
|
|
|
88
76
|
}
|
|
89
77
|
return JSON.stringify(value);
|
|
90
78
|
}
|
|
91
|
-
function pointerPath(segments) {
|
|
92
|
-
return "/" + segments.map((s) => escape(String(s))).join("/");
|
|
93
|
-
}
|
|
94
79
|
var init_json_pointer = __esm({
|
|
95
80
|
"src/installer/json-pointer.js"() {
|
|
96
81
|
}
|
|
@@ -114,21 +99,22 @@ var init_hash = __esm({
|
|
|
114
99
|
function planModePermissions() {
|
|
115
100
|
return {
|
|
116
101
|
build: {
|
|
117
|
-
// Broad-deny entries for the standard Build verbs. These
|
|
118
|
-
// cover bash, source edits, and writes outside the plans
|
|
119
|
-
// path. opencode.js evaluates the last matching rule, so
|
|
120
|
-
// a narrow object-typed entry after a string-typed deny
|
|
121
|
-
// creates a per-path exception.
|
|
122
102
|
bash: DENY_DEFAULT,
|
|
123
|
-
edit: {
|
|
124
|
-
|
|
103
|
+
edit: {
|
|
104
|
+
"*": DENY_DEFAULT,
|
|
105
|
+
[PLANS_GLOB]: ALLOW_PLANS
|
|
106
|
+
},
|
|
125
107
|
webfetch: DENY_DEFAULT,
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
108
|
+
task: DENY_DEFAULT,
|
|
109
|
+
delivery_inspect: DENY_DEFAULT,
|
|
110
|
+
delivery_issue: DENY_DEFAULT,
|
|
111
|
+
delivery_worktree: DENY_DEFAULT,
|
|
112
|
+
delivery_verify: DENY_DEFAULT,
|
|
113
|
+
delivery_review: DENY_DEFAULT,
|
|
114
|
+
delivery_pr: DENY_DEFAULT,
|
|
115
|
+
delivery_ready: DENY_DEFAULT,
|
|
116
|
+
delivery_merge: DENY_DEFAULT,
|
|
117
|
+
delivery_cleanup: DENY_DEFAULT
|
|
132
118
|
}
|
|
133
119
|
};
|
|
134
120
|
}
|
|
@@ -203,15 +189,15 @@ function stripJsonc(text) {
|
|
|
203
189
|
let stripped = "";
|
|
204
190
|
let i = 0;
|
|
205
191
|
let inString = false;
|
|
206
|
-
let
|
|
192
|
+
let escape = false;
|
|
207
193
|
while (i < text.length) {
|
|
208
194
|
const ch = text[i];
|
|
209
195
|
if (inString) {
|
|
210
196
|
stripped += ch;
|
|
211
|
-
if (
|
|
212
|
-
|
|
197
|
+
if (escape) {
|
|
198
|
+
escape = false;
|
|
213
199
|
} else if (ch === "\\") {
|
|
214
|
-
|
|
200
|
+
escape = true;
|
|
215
201
|
} else if (ch === '"') {
|
|
216
202
|
inString = false;
|
|
217
203
|
}
|
|
@@ -582,15 +568,19 @@ Commands:
|
|
|
582
568
|
--help Show this usage and exit.
|
|
583
569
|
|
|
584
570
|
Options:
|
|
585
|
-
--root <path>
|
|
586
|
-
--profile <name>
|
|
587
|
-
--force-config
|
|
588
|
-
--force-root-config
|
|
589
|
-
--strict-doctor
|
|
590
|
-
--replace-managed
|
|
591
|
-
--purge-config
|
|
592
|
-
--
|
|
571
|
+
--root <path> Project root (defaults to cwd).
|
|
572
|
+
--profile <name> Override active profile: ${PROFILES.join(", ")}.
|
|
573
|
+
--force-config Rewrite the user config from detection (init only).
|
|
574
|
+
--force-root-config Create opencode.json when absent (init only).
|
|
575
|
+
--strict-doctor Fail init when doctor reports unhealthy checks.
|
|
576
|
+
--replace-managed Replace locally-modified managed files (update only).
|
|
577
|
+
--purge-config Remove ship.config.json when uninstalling.
|
|
578
|
+
--planner-model <id> Engineering model id for the strong planner.
|
|
579
|
+
--builder-model <id> Engineering model id for the cheap builder.
|
|
580
|
+
--final-reviewer-model <id> Engineering model id for the Standards + Spec reviewer.
|
|
581
|
+
--json Emit a JSON envelope instead of human output.
|
|
593
582
|
`;
|
|
583
|
+
var MODEL_ID_RE = /^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/;
|
|
594
584
|
function parseFlags(argv) {
|
|
595
585
|
const options = {
|
|
596
586
|
rootPath: null,
|
|
@@ -600,7 +590,10 @@ function parseFlags(argv) {
|
|
|
600
590
|
purgeConfig: false,
|
|
601
591
|
forceConfig: false,
|
|
602
592
|
forceRootConfig: false,
|
|
603
|
-
strictDoctor: false
|
|
593
|
+
strictDoctor: false,
|
|
594
|
+
plannerModel: null,
|
|
595
|
+
builderModel: null,
|
|
596
|
+
finalReviewerModel: null
|
|
604
597
|
};
|
|
605
598
|
for (let i = 0; i < argv.length; i++) {
|
|
606
599
|
const arg = argv[i];
|
|
@@ -620,6 +613,15 @@ function parseFlags(argv) {
|
|
|
620
613
|
return { error: `unknown profile '${value}' (expected one of: ${PROFILES.join(", ")})` };
|
|
621
614
|
}
|
|
622
615
|
options.profile = value;
|
|
616
|
+
} else if (arg === "--planner-model" || arg === "--builder-model" || arg === "--final-reviewer-model") {
|
|
617
|
+
const value = argv[++i];
|
|
618
|
+
if (value === void 0) return { error: `${arg} requires a value` };
|
|
619
|
+
if (!MODEL_ID_RE.test(value)) {
|
|
620
|
+
return { error: `${arg} must be a "<provider>/<model>" id, got ${JSON.stringify(value)}` };
|
|
621
|
+
}
|
|
622
|
+
if (arg === "--planner-model") options.plannerModel = value;
|
|
623
|
+
else if (arg === "--builder-model") options.builderModel = value;
|
|
624
|
+
else options.finalReviewerModel = value;
|
|
623
625
|
} else if (arg === "-h" || arg === "--help") return { help: true };
|
|
624
626
|
else if (arg === "-v" || arg === "--version") return { version: true };
|
|
625
627
|
else return { error: `unknown flag ${arg}` };
|
|
@@ -653,9 +655,9 @@ function helpText() {
|
|
|
653
655
|
}
|
|
654
656
|
|
|
655
657
|
// src/installer/executor.js
|
|
656
|
-
import { existsSync as
|
|
657
|
-
import { mkdir as
|
|
658
|
-
import { dirname as
|
|
658
|
+
import { existsSync as existsSync13 } from "node:fs";
|
|
659
|
+
import { mkdir as mkdir5, readFile as readFile8, rename as rename5, unlink as unlink3, writeFile as writeFile5 } from "node:fs/promises";
|
|
660
|
+
import { dirname as dirname7, resolve as resolve12 } from "node:path";
|
|
659
661
|
|
|
660
662
|
// src/installer/catalog.js
|
|
661
663
|
import { resolve as resolve3, relative, sep } from "node:path";
|
|
@@ -687,12 +689,53 @@ function resolvePackageRoot(startUrl) {
|
|
|
687
689
|
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "node:fs";
|
|
688
690
|
import { dirname as dirname2, resolve as resolve2 } from "node:path";
|
|
689
691
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
690
|
-
var PACKAGE_VERSION = "0.
|
|
692
|
+
var PACKAGE_VERSION = "0.10.0-rc.18";
|
|
691
693
|
var TEMPLATE_SET = `v${PACKAGE_VERSION}`;
|
|
692
694
|
|
|
693
695
|
// src/installer/catalog.js
|
|
694
696
|
var TEMPLATE_SET_ID = TEMPLATE_SET;
|
|
695
697
|
var packageRoot = resolvePackageRoot(import.meta.url);
|
|
698
|
+
var MATT_SKILLS = [
|
|
699
|
+
"setup-engineering-workflow",
|
|
700
|
+
"engineering-workflow",
|
|
701
|
+
"grilling",
|
|
702
|
+
"domain-modeling",
|
|
703
|
+
"grill-with-docs",
|
|
704
|
+
"triage",
|
|
705
|
+
"to-spec",
|
|
706
|
+
"to-tickets",
|
|
707
|
+
"wayfinder",
|
|
708
|
+
"handoff",
|
|
709
|
+
"research",
|
|
710
|
+
"prototype",
|
|
711
|
+
"codebase-design",
|
|
712
|
+
"code-review"
|
|
713
|
+
];
|
|
714
|
+
var SUPER_SKILLS = [
|
|
715
|
+
"brainstorming",
|
|
716
|
+
"writing-plans",
|
|
717
|
+
"executing-plans",
|
|
718
|
+
"subagent-driven-development",
|
|
719
|
+
"dispatching-parallel-agents",
|
|
720
|
+
"test-driven-development",
|
|
721
|
+
"systematic-debugging",
|
|
722
|
+
"verification-before-completion",
|
|
723
|
+
"requesting-code-review",
|
|
724
|
+
"receiving-code-review"
|
|
725
|
+
];
|
|
726
|
+
var ENGINEERING_AGENTS = [
|
|
727
|
+
"ship-controller",
|
|
728
|
+
"ship-planner",
|
|
729
|
+
"ship-task-builder",
|
|
730
|
+
"ship-task-reviewer",
|
|
731
|
+
"ship-final-standards-reviewer",
|
|
732
|
+
"ship-final-spec-reviewer"
|
|
733
|
+
];
|
|
734
|
+
var ENGINEERING_COMMANDS = [
|
|
735
|
+
"ship-deliver",
|
|
736
|
+
"ship-resume",
|
|
737
|
+
"ship-status"
|
|
738
|
+
];
|
|
696
739
|
var CATALOG = [
|
|
697
740
|
{
|
|
698
741
|
id: "plugin:opencode-ship",
|
|
@@ -718,6 +761,22 @@ var CATALOG = [
|
|
|
718
761
|
mode: 420,
|
|
719
762
|
profiles: ["core", "engineering"]
|
|
720
763
|
},
|
|
764
|
+
...ENGINEERING_AGENTS.map((name) => ({
|
|
765
|
+
id: `agent:${name}`,
|
|
766
|
+
kind: "agent",
|
|
767
|
+
path: `.opencode/agents/${name}.md`,
|
|
768
|
+
source: resolve3(packageRoot, `assets/agents/${name}.md`),
|
|
769
|
+
mode: 420,
|
|
770
|
+
profiles: ["engineering"]
|
|
771
|
+
})),
|
|
772
|
+
...ENGINEERING_COMMANDS.map((name) => ({
|
|
773
|
+
id: `command:${name}`,
|
|
774
|
+
kind: "support",
|
|
775
|
+
path: `.opencode/commands/${name}.md`,
|
|
776
|
+
source: resolve3(packageRoot, `assets/commands/${name}.md`),
|
|
777
|
+
mode: 420,
|
|
778
|
+
profiles: ["engineering"]
|
|
779
|
+
})),
|
|
721
780
|
{
|
|
722
781
|
id: "skill:delivery-workflow",
|
|
723
782
|
kind: "skill",
|
|
@@ -734,22 +793,22 @@ var CATALOG = [
|
|
|
734
793
|
mode: 420,
|
|
735
794
|
profiles: ["core", "engineering"]
|
|
736
795
|
},
|
|
737
|
-
{
|
|
738
|
-
id:
|
|
796
|
+
...MATT_SKILLS.map((name) => ({
|
|
797
|
+
id: `skill:matt:${name}`,
|
|
739
798
|
kind: "skill",
|
|
740
|
-
path:
|
|
741
|
-
source: resolve3(packageRoot,
|
|
799
|
+
path: `.opencode/skills/${name}/SKILL.md`,
|
|
800
|
+
source: resolve3(packageRoot, `assets/skills/${name}/SKILL.md`),
|
|
742
801
|
mode: 420,
|
|
743
802
|
profiles: ["engineering"]
|
|
744
|
-
},
|
|
745
|
-
{
|
|
746
|
-
id:
|
|
803
|
+
})),
|
|
804
|
+
...SUPER_SKILLS.map((name) => ({
|
|
805
|
+
id: `skill:super:${name}`,
|
|
747
806
|
kind: "skill",
|
|
748
|
-
path:
|
|
749
|
-
source: resolve3(packageRoot,
|
|
807
|
+
path: `.opencode/skills/${name}/SKILL.md`,
|
|
808
|
+
source: resolve3(packageRoot, `assets/skills/${name}/SKILL.md`),
|
|
750
809
|
mode: 420,
|
|
751
810
|
profiles: ["engineering"]
|
|
752
|
-
}
|
|
811
|
+
}))
|
|
753
812
|
];
|
|
754
813
|
function filterCatalogByProfile(catalog, profile) {
|
|
755
814
|
const effective = profile === void 0 || profile === null ? DEFAULT_PROFILE : profile;
|
|
@@ -833,7 +892,7 @@ function validateCatalog({ catalog = CATALOG } = {}) {
|
|
|
833
892
|
}
|
|
834
893
|
|
|
835
894
|
// src/installer/planner.js
|
|
836
|
-
import { existsSync as
|
|
895
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
837
896
|
import { readFile as readFile3, stat } from "node:fs/promises";
|
|
838
897
|
init_hash();
|
|
839
898
|
|
|
@@ -851,7 +910,7 @@ var ship_config_schema_default = {
|
|
|
851
910
|
required: ["schemaVersion"],
|
|
852
911
|
additionalProperties: false,
|
|
853
912
|
properties: {
|
|
854
|
-
schemaVersion: {
|
|
913
|
+
schemaVersion: { enum: [1, 2] },
|
|
855
914
|
profile: {
|
|
856
915
|
type: "string",
|
|
857
916
|
enum: ["core", "engineering"],
|
|
@@ -981,8 +1040,69 @@ var ship_config_schema_default = {
|
|
|
981
1040
|
agentsDir: { type: "string", pattern: "^\\.opencode/agents/?$" },
|
|
982
1041
|
skillsDir: { type: "string", pattern: "^\\.opencode/skills/?$" }
|
|
983
1042
|
}
|
|
1043
|
+
},
|
|
1044
|
+
workflow: {
|
|
1045
|
+
type: "object",
|
|
1046
|
+
description: "Required when profile is engineering. Ignored when profile is core.",
|
|
1047
|
+
additionalProperties: false,
|
|
1048
|
+
properties: {
|
|
1049
|
+
models: {
|
|
1050
|
+
type: "object",
|
|
1051
|
+
additionalProperties: false,
|
|
1052
|
+
required: ["planner", "builder", "finalReviewer"],
|
|
1053
|
+
properties: {
|
|
1054
|
+
planner: {
|
|
1055
|
+
type: "string",
|
|
1056
|
+
pattern: "^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$",
|
|
1057
|
+
description: "Provider/model id for the strong planning child session."
|
|
1058
|
+
},
|
|
1059
|
+
builder: {
|
|
1060
|
+
type: "string",
|
|
1061
|
+
pattern: "^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$",
|
|
1062
|
+
description: "Provider/model id for the cheap builder child session."
|
|
1063
|
+
},
|
|
1064
|
+
finalReviewer: {
|
|
1065
|
+
type: "string",
|
|
1066
|
+
pattern: "^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$",
|
|
1067
|
+
description: "Provider/model id for the final Standards + Spec reviewers."
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
},
|
|
1071
|
+
approval: {
|
|
1072
|
+
type: "object",
|
|
1073
|
+
additionalProperties: false,
|
|
1074
|
+
properties: {
|
|
1075
|
+
mirrorToIssue: { const: true },
|
|
1076
|
+
maxFailedRounds: { const: 3 }
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
984
1080
|
}
|
|
985
|
-
}
|
|
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
|
+
]
|
|
986
1106
|
};
|
|
987
1107
|
|
|
988
1108
|
// src/installer/validation.js
|
|
@@ -1000,6 +1120,18 @@ function validate(value, schema, pointer, issues) {
|
|
|
1000
1120
|
issues.push(`${pointer}: expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`);
|
|
1001
1121
|
return;
|
|
1002
1122
|
}
|
|
1123
|
+
if (Array.isArray(schema.allOf)) {
|
|
1124
|
+
for (const sub of schema.allOf) validate(value, sub, pointer, issues);
|
|
1125
|
+
}
|
|
1126
|
+
if (isObject2(schema.if)) {
|
|
1127
|
+
const ifIssues = [];
|
|
1128
|
+
validate(value, schema.if, pointer, ifIssues);
|
|
1129
|
+
if (ifIssues.length === 0) {
|
|
1130
|
+
if (isObject2(schema.then)) validate(value, schema.then, pointer, issues);
|
|
1131
|
+
} else if (isObject2(schema.else)) {
|
|
1132
|
+
validate(value, schema.else, pointer, issues);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1003
1135
|
const type = schema.type;
|
|
1004
1136
|
if (type !== void 0) {
|
|
1005
1137
|
const actual = Array.isArray(value) ? "array" : value === null ? "null" : typeof value;
|
|
@@ -1050,7 +1182,7 @@ function validate(value, schema, pointer, issues) {
|
|
|
1050
1182
|
value.forEach((entry, i) => validate(entry, schema.items, `${pointer}/${i}`, issues));
|
|
1051
1183
|
}
|
|
1052
1184
|
}
|
|
1053
|
-
if (type === "object") {
|
|
1185
|
+
if (type === "object" || isObject2(schema.properties) || Array.isArray(schema.required)) {
|
|
1054
1186
|
if (Array.isArray(schema.required)) {
|
|
1055
1187
|
for (const key of schema.required) {
|
|
1056
1188
|
if (!(key in value)) issues.push(`${pointer}: missing required field ${key}`);
|
|
@@ -1145,15 +1277,510 @@ function renderDefaultConfig(detection, overrides = {}) {
|
|
|
1145
1277
|
// src/installer/planner.js
|
|
1146
1278
|
init_json_pointer();
|
|
1147
1279
|
init_root_config();
|
|
1280
|
+
|
|
1281
|
+
// src/installer/root-reconciliation.js
|
|
1148
1282
|
init_root_config();
|
|
1283
|
+
init_json_pointer();
|
|
1284
|
+
init_hash();
|
|
1285
|
+
init_plan_mode_permissions();
|
|
1286
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
1287
|
+
import { join } from "node:path";
|
|
1288
|
+
var PLAN_MODE_POINTER2 = "/agent/plan/permission";
|
|
1289
|
+
function desiredPointersForProfile(profile) {
|
|
1290
|
+
const out = POINTER_ENTRIES.map((entry) => ({
|
|
1291
|
+
pointer: entry.pointer,
|
|
1292
|
+
strategy: (
|
|
1293
|
+
/** @type {"value" | "object-entry" | "array-member"} */
|
|
1294
|
+
entry.strategy
|
|
1295
|
+
),
|
|
1296
|
+
scope: (
|
|
1297
|
+
/** @type {Profile} */
|
|
1298
|
+
"core"
|
|
1299
|
+
),
|
|
1300
|
+
value: entry.value
|
|
1301
|
+
}));
|
|
1302
|
+
if (profile === "engineering") {
|
|
1303
|
+
out.push({
|
|
1304
|
+
pointer: PLAN_MODE_POINTER2,
|
|
1305
|
+
strategy: (
|
|
1306
|
+
/** @type {"value" | "object-entry" | "array-member"} */
|
|
1307
|
+
"value"
|
|
1308
|
+
),
|
|
1309
|
+
scope: (
|
|
1310
|
+
/** @type {Profile} */
|
|
1311
|
+
"engineering"
|
|
1312
|
+
),
|
|
1313
|
+
value: (
|
|
1314
|
+
/** @type {any} */
|
|
1315
|
+
planModePermissions().build
|
|
1316
|
+
)
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
return out;
|
|
1320
|
+
}
|
|
1321
|
+
async function planRootReconciliation(input) {
|
|
1322
|
+
const profile = input.profile;
|
|
1323
|
+
const mode = input.mode;
|
|
1324
|
+
if (!["core", "engineering"].includes(profile)) {
|
|
1325
|
+
throw new Error(`planRootReconciliation: invalid profile ${profile}`);
|
|
1326
|
+
}
|
|
1327
|
+
if (!["install", "profile-transition", "uninstall"].includes(mode)) {
|
|
1328
|
+
throw new Error(`planRootReconciliation: invalid mode ${mode}`);
|
|
1329
|
+
}
|
|
1330
|
+
const detected = findRootConfig(input.repoRoot);
|
|
1331
|
+
const target = detected.path ?? join(input.repoRoot, detected.relative);
|
|
1332
|
+
const descriptors = input.pointerDescriptors ?? desiredPointersForProfile(profile);
|
|
1333
|
+
const previousRecords = input.previousRecords ?? [];
|
|
1334
|
+
if (mode === "uninstall") {
|
|
1335
|
+
return planUninstallRoot({
|
|
1336
|
+
target,
|
|
1337
|
+
relPath: detected.relative,
|
|
1338
|
+
previousRecords,
|
|
1339
|
+
previousDocument: input.previousDocument
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
const fileMissing = !existsSync6(target);
|
|
1343
|
+
if (fileMissing && !input.forceRepair) {
|
|
1344
|
+
if (mode === "profile-transition") {
|
|
1345
|
+
return {
|
|
1346
|
+
kind: "noop",
|
|
1347
|
+
op: "root-config",
|
|
1348
|
+
target,
|
|
1349
|
+
relPath: detected.relative,
|
|
1350
|
+
reason: "no root opencode.json present",
|
|
1351
|
+
edits: [],
|
|
1352
|
+
pointerRecords: reconcileRecordsAfterTransition(descriptors, previousRecords)
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
return {
|
|
1356
|
+
kind: "noop",
|
|
1357
|
+
op: "root-config",
|
|
1358
|
+
target,
|
|
1359
|
+
relPath: detected.relative,
|
|
1360
|
+
reason: "no root opencode.json present",
|
|
1361
|
+
edits: [],
|
|
1362
|
+
pointerRecords: seedPointerRecords(descriptors, previousRecords)
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
let doc;
|
|
1366
|
+
if (fileMissing && input.forceRepair) {
|
|
1367
|
+
doc = synthesizeDefaultRootConfig();
|
|
1368
|
+
if (mode === "install" && profile === "engineering") {
|
|
1369
|
+
const applied = applyPlanModeOwnership(doc, { block: planModePermissions().build });
|
|
1370
|
+
doc = applied.doc;
|
|
1371
|
+
}
|
|
1372
|
+
const bytes = Buffer.from(formatRootConfig(doc), "utf8");
|
|
1373
|
+
return {
|
|
1374
|
+
kind: "create",
|
|
1375
|
+
op: "root-config",
|
|
1376
|
+
target,
|
|
1377
|
+
relPath: detected.relative,
|
|
1378
|
+
bytes,
|
|
1379
|
+
desiredSha: bytesHashString(stableStringify(doc)),
|
|
1380
|
+
currentSha: null,
|
|
1381
|
+
edits: descriptors.map((d) => ({ kind: "create", pointer: d.pointer, value: "(synthesized)" })),
|
|
1382
|
+
pointerRecords: seedPointerRecords(descriptors, previousRecords),
|
|
1383
|
+
format: "json",
|
|
1384
|
+
document: doc,
|
|
1385
|
+
reason: "creating root opencode.json with installer-owned Build permissions"
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
const docResult = readRootConfig(target);
|
|
1389
|
+
if (!docResult.ok) {
|
|
1390
|
+
return {
|
|
1391
|
+
kind: "noop",
|
|
1392
|
+
op: "root-config",
|
|
1393
|
+
target,
|
|
1394
|
+
relPath: detected.relative,
|
|
1395
|
+
reason: `root config ${docResult.error.kind}`,
|
|
1396
|
+
edits: [],
|
|
1397
|
+
pointerRecords: previousRecords
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
if (mode === "profile-transition") {
|
|
1401
|
+
return planProfileTransitionRoot({
|
|
1402
|
+
doc: docResult,
|
|
1403
|
+
target,
|
|
1404
|
+
relPath: detected.relative,
|
|
1405
|
+
descriptors,
|
|
1406
|
+
previousRecords
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
return planInstallRoot({
|
|
1410
|
+
doc: docResult,
|
|
1411
|
+
target,
|
|
1412
|
+
relPath: detected.relative,
|
|
1413
|
+
descriptors,
|
|
1414
|
+
previousRecords
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
function reconcileRecordsAfterTransition(descriptors, previousRecords) {
|
|
1418
|
+
const desiredCore = new Set(descriptors.filter((d) => d.scope === "core").map((d) => d.pointer));
|
|
1419
|
+
const out = previousRecords.filter((r) => r.scope === "core" || r.scope === "engineering" && desiredCore.has(r.pointer));
|
|
1420
|
+
const seen = new Set(out.map((r) => r.pointer));
|
|
1421
|
+
for (const d of descriptors) {
|
|
1422
|
+
if (seen.has(d.pointer)) continue;
|
|
1423
|
+
out.push({
|
|
1424
|
+
pointer: d.pointer,
|
|
1425
|
+
strategy: d.strategy,
|
|
1426
|
+
scope: d.scope,
|
|
1427
|
+
installedSha256: d.value !== void 0 ? bytesHashString(stableStringify(d.value)) : null,
|
|
1428
|
+
previous: { existed: false }
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
return out;
|
|
1432
|
+
}
|
|
1433
|
+
function planInstallRoot({ doc, target, relPath, descriptors, previousRecords }) {
|
|
1434
|
+
const result = applyOwnedPointers(doc.value, {
|
|
1435
|
+
pointerEntries: descriptors.map((d) => ({ pointer: d.pointer, strategy: d.strategy, value: d.value })),
|
|
1436
|
+
allowEqualValues: true
|
|
1437
|
+
});
|
|
1438
|
+
const edits = [];
|
|
1439
|
+
for (const a of result.applied) edits.push({ kind: "create", pointer: a.pointer, value: a.value });
|
|
1440
|
+
for (const s of result.skipped) {
|
|
1441
|
+
if (s.reason === "already equal") continue;
|
|
1442
|
+
edits.push({ kind: "conflict", pointer: s.pointer, reason: s.reason, existing: s.existing, desired: s.desired });
|
|
1443
|
+
}
|
|
1444
|
+
let docForWrite = result.doc;
|
|
1445
|
+
let bytes;
|
|
1446
|
+
try {
|
|
1447
|
+
const { value: sourceValue } = parseRootConfigPreservingOrder(doc.raw ?? "");
|
|
1448
|
+
if (sourceValue && typeof sourceValue === "object") {
|
|
1449
|
+
docForWrite = sourceValue;
|
|
1450
|
+
for (const a of result.applied) {
|
|
1451
|
+
docForWrite = setPointer(docForWrite, a.pointer, a.value);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
bytes = Buffer.from(formatRootConfigPreserving(docForWrite), "utf8");
|
|
1455
|
+
} catch {
|
|
1456
|
+
bytes = Buffer.from(formatRootConfig(result.doc), "utf8");
|
|
1457
|
+
}
|
|
1458
|
+
const records = mergePointerRecords(descriptors, previousRecords, result, doc.before);
|
|
1459
|
+
return {
|
|
1460
|
+
kind: edits.some((e) => e.kind === "conflict") ? "conflict" : edits.length ? "update" : "noop",
|
|
1461
|
+
op: "root-config",
|
|
1462
|
+
target,
|
|
1463
|
+
relPath,
|
|
1464
|
+
bytes,
|
|
1465
|
+
desiredSha: bytesHashString(stableStringify(docForWrite)),
|
|
1466
|
+
currentSha: doc.sha256 ?? null,
|
|
1467
|
+
edits,
|
|
1468
|
+
pointerRecords: records,
|
|
1469
|
+
format: doc.format,
|
|
1470
|
+
document: docForWrite,
|
|
1471
|
+
reason: edits.length === 0 ? "no installer-owned entries missing" : `apply ${result.applied.length} / skip ${result.skipped.length}`
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
function planProfileTransitionRoot({ doc, target, relPath, descriptors, previousRecords }) {
|
|
1475
|
+
const desired = descriptors;
|
|
1476
|
+
const drift = [];
|
|
1477
|
+
for (const rec of previousRecords) {
|
|
1478
|
+
const stillDesired = desired.some((d) => d.pointer === rec.pointer && d.scope === rec.scope);
|
|
1479
|
+
if (stillDesired) continue;
|
|
1480
|
+
if (typeof rec.installedSha256 !== "string" || rec.installedSha256.length === 0) continue;
|
|
1481
|
+
const currentValue = getPointer(doc.value, rec.pointer);
|
|
1482
|
+
if (currentValue === void 0) {
|
|
1483
|
+
continue;
|
|
1484
|
+
}
|
|
1485
|
+
const currentHash = bytesHashString(stableStringify(currentValue));
|
|
1486
|
+
if (currentHash !== rec.installedSha256) {
|
|
1487
|
+
drift.push({ pointer: rec.pointer, recorded: rec.installedSha256, current: currentHash });
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
if (drift.length > 0) {
|
|
1491
|
+
return {
|
|
1492
|
+
kind: "conflict",
|
|
1493
|
+
op: "root-config",
|
|
1494
|
+
target,
|
|
1495
|
+
relPath,
|
|
1496
|
+
reason: `installed pointer drift: ${drift.map((d) => `${d.pointer} (recorded ${d.recorded.slice(0, 8)}\u2026, current ${d.current.slice(0, 8)}\u2026)`).join("; ")}`,
|
|
1497
|
+
edits: drift.map((d) => ({ kind: "conflict", pointer: d.pointer, reason: "installed-pointer-drift" })),
|
|
1498
|
+
pointerRecords: previousRecords
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
let next = JSON.parse(JSON.stringify(doc.value));
|
|
1502
|
+
const edits = [];
|
|
1503
|
+
const mergedRecords = previousRecords.map((r) => ({ ...r }));
|
|
1504
|
+
const recordIndex = new Map(mergedRecords.map((r, idx) => [r.pointer, idx]));
|
|
1505
|
+
for (let i = mergedRecords.length - 1; i >= 0; i--) {
|
|
1506
|
+
const rec = mergedRecords[i];
|
|
1507
|
+
const isStillDesired = desired.some((d) => d.pointer === rec.pointer && d.scope === rec.scope);
|
|
1508
|
+
if (isStillDesired) continue;
|
|
1509
|
+
if (rec.previous && rec.previous.existed) {
|
|
1510
|
+
next = setPointer(next, rec.pointer, rec.previous.value);
|
|
1511
|
+
edits.push({ kind: "restore", pointer: rec.pointer, value: rec.previous.value });
|
|
1512
|
+
} else {
|
|
1513
|
+
if (getPointer(next, rec.pointer) !== void 0) {
|
|
1514
|
+
next = removePointer(next, rec.pointer);
|
|
1515
|
+
edits.push({ kind: "remove", pointer: rec.pointer });
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
mergedRecords.splice(i, 1);
|
|
1519
|
+
recordIndex.delete(rec.pointer);
|
|
1520
|
+
}
|
|
1521
|
+
for (const d of desired) {
|
|
1522
|
+
const existing = getPointer(next, d.pointer);
|
|
1523
|
+
if (existing === d.value) {
|
|
1524
|
+
const idx = recordIndex.get(d.pointer);
|
|
1525
|
+
if (idx != null) {
|
|
1526
|
+
mergedRecords[idx] = {
|
|
1527
|
+
...mergedRecords[idx],
|
|
1528
|
+
scope: d.scope,
|
|
1529
|
+
installedSha256: bytesHashString(stableStringify(d.value))
|
|
1530
|
+
};
|
|
1531
|
+
} else {
|
|
1532
|
+
mergedRecords.push({
|
|
1533
|
+
pointer: d.pointer,
|
|
1534
|
+
strategy: d.strategy,
|
|
1535
|
+
scope: d.scope,
|
|
1536
|
+
installedSha256: bytesHashString(stableStringify(d.value)),
|
|
1537
|
+
previous: { existed: existing !== void 0, value: existing ?? null }
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
continue;
|
|
1541
|
+
}
|
|
1542
|
+
if (existing === void 0) {
|
|
1543
|
+
next = setPointer(next, d.pointer, d.value);
|
|
1544
|
+
edits.push({ kind: "create", pointer: d.pointer, value: d.value });
|
|
1545
|
+
const idx = recordIndex.get(d.pointer);
|
|
1546
|
+
if (idx != null) {
|
|
1547
|
+
mergedRecords[idx] = {
|
|
1548
|
+
...mergedRecords[idx],
|
|
1549
|
+
scope: d.scope,
|
|
1550
|
+
installedSha256: bytesHashString(stableStringify(d.value)),
|
|
1551
|
+
previous: { existed: false }
|
|
1552
|
+
};
|
|
1553
|
+
} else {
|
|
1554
|
+
mergedRecords.push({
|
|
1555
|
+
pointer: d.pointer,
|
|
1556
|
+
strategy: d.strategy,
|
|
1557
|
+
scope: d.scope,
|
|
1558
|
+
installedSha256: bytesHashString(stableStringify(d.value)),
|
|
1559
|
+
previous: { existed: false }
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
} else {
|
|
1563
|
+
edits.push({
|
|
1564
|
+
kind: "conflict",
|
|
1565
|
+
pointer: d.pointer,
|
|
1566
|
+
reason: "different existing value",
|
|
1567
|
+
existing,
|
|
1568
|
+
desired: d.value
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
let docForWrite = next;
|
|
1573
|
+
let bytes;
|
|
1574
|
+
try {
|
|
1575
|
+
const { value: sourceValue } = parseRootConfigPreservingOrder(doc.raw ?? "");
|
|
1576
|
+
if (sourceValue && typeof sourceValue === "object") {
|
|
1577
|
+
docForWrite = sourceValue;
|
|
1578
|
+
for (const e of edits) {
|
|
1579
|
+
if (e.kind === "create" || e.kind === "restore") {
|
|
1580
|
+
docForWrite = setPointer(docForWrite, e.pointer, e.value);
|
|
1581
|
+
} else if (e.kind === "remove") {
|
|
1582
|
+
docForWrite = removePointer(docForWrite, e.pointer);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
docForWrite = collapseEmptyAncestors(docForWrite) ?? docForWrite;
|
|
1587
|
+
bytes = Buffer.from(formatRootConfigPreserving(docForWrite), "utf8");
|
|
1588
|
+
} catch {
|
|
1589
|
+
bytes = Buffer.from(formatRootConfig(collapseEmptyAncestors(next) ?? next), "utf8");
|
|
1590
|
+
}
|
|
1591
|
+
return {
|
|
1592
|
+
kind: edits.some((e) => e.kind === "conflict") ? "conflict" : edits.some((e) => e.kind === "create" || e.kind === "remove" || e.kind === "restore") ? "update" : "noop",
|
|
1593
|
+
op: "root-config",
|
|
1594
|
+
target,
|
|
1595
|
+
relPath,
|
|
1596
|
+
bytes,
|
|
1597
|
+
desiredSha: bytesHashString(stableStringify(docForWrite)),
|
|
1598
|
+
currentSha: doc.sha256 ?? null,
|
|
1599
|
+
edits,
|
|
1600
|
+
pointerRecords: mergedRecords,
|
|
1601
|
+
format: doc.format,
|
|
1602
|
+
document: docForWrite,
|
|
1603
|
+
reason: "profile transition reconciliation"
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1606
|
+
function planUninstallRoot({ target, relPath, previousRecords, previousDocument }) {
|
|
1607
|
+
if (previousRecords.length === 0) {
|
|
1608
|
+
return {
|
|
1609
|
+
kind: "noop",
|
|
1610
|
+
op: "root-config",
|
|
1611
|
+
target,
|
|
1612
|
+
relPath,
|
|
1613
|
+
reason: "no installer-owned pointers recorded",
|
|
1614
|
+
edits: [],
|
|
1615
|
+
pointerRecords: []
|
|
1616
|
+
};
|
|
1617
|
+
}
|
|
1618
|
+
const docResult = readRootConfig(target);
|
|
1619
|
+
if (!docResult.ok) {
|
|
1620
|
+
return {
|
|
1621
|
+
kind: "noop",
|
|
1622
|
+
op: "root-config",
|
|
1623
|
+
target,
|
|
1624
|
+
relPath,
|
|
1625
|
+
reason: `root config ${docResult.error.kind}`,
|
|
1626
|
+
edits: [],
|
|
1627
|
+
pointerRecords: previousRecords
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1630
|
+
const drift = [];
|
|
1631
|
+
for (const rec of previousRecords) {
|
|
1632
|
+
if (typeof rec.installedSha256 !== "string" || rec.installedSha256.length === 0) continue;
|
|
1633
|
+
const currentValue = getPointer(docResult.value, rec.pointer);
|
|
1634
|
+
if (currentValue === void 0) continue;
|
|
1635
|
+
const currentHash = bytesHashString(stableStringify(currentValue));
|
|
1636
|
+
if (currentHash !== rec.installedSha256) {
|
|
1637
|
+
drift.push({ pointer: rec.pointer, recorded: rec.installedSha256, current: currentHash });
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
if (drift.length > 0) {
|
|
1641
|
+
return {
|
|
1642
|
+
kind: "conflict",
|
|
1643
|
+
op: "root-config",
|
|
1644
|
+
target,
|
|
1645
|
+
relPath,
|
|
1646
|
+
reason: `installed pointer drift: ${drift.map((d) => `${d.pointer} (recorded ${d.recorded.slice(0, 8)}\u2026, current ${d.current.slice(0, 8)}\u2026)`).join("; ")}`,
|
|
1647
|
+
edits: drift.map((d) => ({ kind: "conflict", pointer: d.pointer, reason: "installed-pointer-drift" })),
|
|
1648
|
+
pointerRecords: previousRecords
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
let doc = docResult.value;
|
|
1652
|
+
const edits = [];
|
|
1653
|
+
for (const rec of previousRecords) {
|
|
1654
|
+
if (rec.previous && rec.previous.existed) {
|
|
1655
|
+
doc = setPointer(doc, rec.pointer, rec.previous.value);
|
|
1656
|
+
edits.push({ kind: "restore", pointer: rec.pointer, value: rec.previous.value });
|
|
1657
|
+
} else {
|
|
1658
|
+
const current = getPointer(doc, rec.pointer);
|
|
1659
|
+
if (current !== void 0) {
|
|
1660
|
+
doc = removePointer(doc, rec.pointer);
|
|
1661
|
+
edits.push({ kind: "remove", pointer: rec.pointer });
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
let docForWrite = doc;
|
|
1666
|
+
let bytes;
|
|
1667
|
+
try {
|
|
1668
|
+
const { value: sourceValue } = parseRootConfigPreservingOrder(docResult.raw ?? "");
|
|
1669
|
+
if (sourceValue && typeof sourceValue === "object") {
|
|
1670
|
+
docForWrite = sourceValue;
|
|
1671
|
+
for (const e of edits) {
|
|
1672
|
+
if (e.kind === "restore") {
|
|
1673
|
+
docForWrite = setPointer(docForWrite, e.pointer, e.value);
|
|
1674
|
+
} else if (e.kind === "remove") {
|
|
1675
|
+
docForWrite = removePointer(docForWrite, e.pointer);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
docForWrite = collapseEmptyAncestors(docForWrite);
|
|
1680
|
+
bytes = Buffer.from(formatRootConfigPreserving(docForWrite), "utf8");
|
|
1681
|
+
} catch {
|
|
1682
|
+
bytes = Buffer.from(formatRootConfig(collapseEmptyAncestors(doc)), "utf8");
|
|
1683
|
+
}
|
|
1684
|
+
return {
|
|
1685
|
+
kind: edits.length ? "update" : "noop",
|
|
1686
|
+
op: "root-config",
|
|
1687
|
+
target,
|
|
1688
|
+
relPath,
|
|
1689
|
+
bytes,
|
|
1690
|
+
desiredSha: bytesHashString(stableStringify(docForWrite)),
|
|
1691
|
+
currentSha: docResult.sha256 ?? null,
|
|
1692
|
+
edits,
|
|
1693
|
+
pointerRecords: [],
|
|
1694
|
+
format: docResult.format,
|
|
1695
|
+
document: docForWrite,
|
|
1696
|
+
reason: "uninstall restores the preinstall root config"
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
function collapseEmptyAncestors(doc) {
|
|
1700
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) return doc;
|
|
1701
|
+
const out = {};
|
|
1702
|
+
for (const k of Object.keys(doc)) {
|
|
1703
|
+
if (k === "__sourceOrder__") continue;
|
|
1704
|
+
out[k] = collapseEmptyAncestors(doc[k]);
|
|
1705
|
+
}
|
|
1706
|
+
if (Array.isArray(doc.__sourceOrder__)) {
|
|
1707
|
+
out.__sourceOrder__ = doc.__sourceOrder__.filter((k) => k in out);
|
|
1708
|
+
}
|
|
1709
|
+
const remainingKeys = Object.keys(out).filter((k) => k !== "__sourceOrder__");
|
|
1710
|
+
if (remainingKeys.length === 0) return void 0;
|
|
1711
|
+
return out;
|
|
1712
|
+
}
|
|
1713
|
+
function seedPointerRecords(descriptors, previousRecords) {
|
|
1714
|
+
const out = previousRecords.map((r) => ({ ...r }));
|
|
1715
|
+
const seen = new Set(out.map((r) => r.pointer));
|
|
1716
|
+
for (const d of descriptors) {
|
|
1717
|
+
if (seen.has(d.pointer)) continue;
|
|
1718
|
+
out.push({
|
|
1719
|
+
pointer: d.pointer,
|
|
1720
|
+
strategy: (
|
|
1721
|
+
/** @type {any} */
|
|
1722
|
+
d.strategy
|
|
1723
|
+
),
|
|
1724
|
+
scope: d.scope,
|
|
1725
|
+
installedSha256: d.value !== void 0 ? bytesHashString(stableStringify(d.value)) : null,
|
|
1726
|
+
previous: { existed: false }
|
|
1727
|
+
});
|
|
1728
|
+
}
|
|
1729
|
+
return out;
|
|
1730
|
+
}
|
|
1731
|
+
function mergePointerRecords(descriptors, previousRecords, result, beforeSnapshot) {
|
|
1732
|
+
const out = previousRecords.map((r) => ({ ...r }));
|
|
1733
|
+
const index = new Map(out.map((r, idx) => [r.pointer, idx]));
|
|
1734
|
+
for (const a of result.applied) {
|
|
1735
|
+
const previousEntry = beforeSnapshot?.[a.pointer];
|
|
1736
|
+
const previous = previousEntry === void 0 ? { existed: false } : { existed: true, value: previousEntry };
|
|
1737
|
+
const next = {
|
|
1738
|
+
pointer: a.pointer,
|
|
1739
|
+
strategy: "value",
|
|
1740
|
+
scope: descriptors.find((d) => d.pointer === a.pointer)?.scope ?? "core",
|
|
1741
|
+
installedSha256: bytesHashString(stableStringify(a.value)),
|
|
1742
|
+
previous
|
|
1743
|
+
};
|
|
1744
|
+
if (index.has(a.pointer)) {
|
|
1745
|
+
const idx = index.get(a.pointer);
|
|
1746
|
+
out[idx] = {
|
|
1747
|
+
...out[idx],
|
|
1748
|
+
scope: next.scope,
|
|
1749
|
+
installedSha256: next.installedSha256,
|
|
1750
|
+
// If the lock already had a record, keep its previous value
|
|
1751
|
+
// verbatim so uninstall restores the original pre-install
|
|
1752
|
+
// state even after multiple in-place updates.
|
|
1753
|
+
previous: out[idx].previous ?? next.previous
|
|
1754
|
+
};
|
|
1755
|
+
} else {
|
|
1756
|
+
out.push(next);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
for (const s of result.skipped) {
|
|
1760
|
+
if (s.reason !== "already equal") continue;
|
|
1761
|
+
if (index.has(s.pointer)) continue;
|
|
1762
|
+
const existing = beforeSnapshot?.[s.pointer];
|
|
1763
|
+
const previous = existing === void 0 ? { existed: false } : { existed: true, value: existing };
|
|
1764
|
+
out.push({
|
|
1765
|
+
pointer: s.pointer,
|
|
1766
|
+
strategy: "value",
|
|
1767
|
+
scope: descriptors.find((d) => d.pointer === s.pointer)?.scope ?? "core",
|
|
1768
|
+
installedSha256: bytesHashString(stableStringify(existing ?? null)),
|
|
1769
|
+
previous
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
return out;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
// src/installer/planner.js
|
|
1149
1776
|
async function readBytes(path) {
|
|
1150
|
-
if (!
|
|
1777
|
+
if (!existsSync7(path)) return null;
|
|
1151
1778
|
const buf = await readFile3(path);
|
|
1152
1779
|
const fileStat = await stat(path);
|
|
1153
1780
|
return { bytes: buf, hash: bytesHashString(buf.toString("utf8")), mode: fileStat.mode & 511 };
|
|
1154
1781
|
}
|
|
1155
1782
|
async function readDesiredBytes(source) {
|
|
1156
|
-
if (!source || !
|
|
1783
|
+
if (!source || !existsSync7(source)) return null;
|
|
1157
1784
|
const buf = await readFile3(source);
|
|
1158
1785
|
return { bytes: buf, hash: bytesHashString(buf.toString("utf8")) };
|
|
1159
1786
|
}
|
|
@@ -1233,6 +1860,34 @@ async function planFileInstall({ repoRoot, lock, allowUnowned = false, catalog =
|
|
|
1233
1860
|
}
|
|
1234
1861
|
return plan;
|
|
1235
1862
|
}
|
|
1863
|
+
async function planStaleFileRemoval({ repoRoot, lock, staleCatalog }) {
|
|
1864
|
+
if (!Array.isArray(staleCatalog) || staleCatalog.length === 0) return [];
|
|
1865
|
+
const out = [];
|
|
1866
|
+
for (const entry of staleCatalog) {
|
|
1867
|
+
const targetPath = `${repoRoot}/${entry.path}`;
|
|
1868
|
+
const current = await readBytes(targetPath);
|
|
1869
|
+
if (!current) continue;
|
|
1870
|
+
const locked = lookupLockedFile(lock, entry.path);
|
|
1871
|
+
if (current.hash !== (locked?.sha256 ?? null)) {
|
|
1872
|
+
out.push({
|
|
1873
|
+
kind: "conflict",
|
|
1874
|
+
op: "file",
|
|
1875
|
+
target: targetPath,
|
|
1876
|
+
relPath: entry.path,
|
|
1877
|
+
reason: "stale profile asset is locally modified; refusing to remove"
|
|
1878
|
+
});
|
|
1879
|
+
continue;
|
|
1880
|
+
}
|
|
1881
|
+
out.push({
|
|
1882
|
+
kind: "delete",
|
|
1883
|
+
op: "file",
|
|
1884
|
+
target: targetPath,
|
|
1885
|
+
relPath: entry.path,
|
|
1886
|
+
reason: "remove stale profile asset on profile transition"
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
return out;
|
|
1890
|
+
}
|
|
1236
1891
|
async function planMigrationCleanup({ repoRoot, lock, migrationReport, allowUnowned = false }) {
|
|
1237
1892
|
const action = migrationReport?.actions?.find((entry) => entry.kind === "candidate-remove-legacy-plugin-path");
|
|
1238
1893
|
if (!action) return [];
|
|
@@ -1263,6 +1918,7 @@ async function planMigrationCleanup({ repoRoot, lock, migrationReport, allowUnow
|
|
|
1263
1918
|
async function planUninstall({ repoRoot, lock }) {
|
|
1264
1919
|
if (!lock) return [];
|
|
1265
1920
|
const plan = [];
|
|
1921
|
+
const activeProfile = lock?.manager?.profile ?? "core";
|
|
1266
1922
|
for (const entry of lock.files ?? []) {
|
|
1267
1923
|
const targetPath = `${repoRoot}/${entry.path}`;
|
|
1268
1924
|
const current = await readBytes(targetPath);
|
|
@@ -1279,9 +1935,41 @@ async function planUninstall({ repoRoot, lock }) {
|
|
|
1279
1935
|
}
|
|
1280
1936
|
plan.push({ kind: "delete", op: "file", target: targetPath, relPath: entry.path });
|
|
1281
1937
|
}
|
|
1938
|
+
const rootPlan = await planRootReconciliation({
|
|
1939
|
+
repoRoot,
|
|
1940
|
+
profile: activeProfile,
|
|
1941
|
+
mode: "uninstall",
|
|
1942
|
+
previousRecords: (lock?.manager?.rootDocuments ?? []).flatMap((d) => d.pointers ?? [])
|
|
1943
|
+
});
|
|
1944
|
+
if (rootPlan?.kind === "conflict") {
|
|
1945
|
+
plan.push({
|
|
1946
|
+
op: "root-config",
|
|
1947
|
+
kind: "conflict",
|
|
1948
|
+
target: rootPlan.target,
|
|
1949
|
+
relPath: rootPlan.relPath,
|
|
1950
|
+
reason: rootPlan.reason
|
|
1951
|
+
});
|
|
1952
|
+
} else if (rootPlan && rootPlan.kind && rootPlan.kind !== "noop" && rootPlan.bytes) {
|
|
1953
|
+
plan.push({
|
|
1954
|
+
op: "file",
|
|
1955
|
+
kind: "update",
|
|
1956
|
+
target: rootPlan.target,
|
|
1957
|
+
relPath: rootPlan.relPath,
|
|
1958
|
+
bytes: rootPlan.bytes,
|
|
1959
|
+
mode: 420,
|
|
1960
|
+
reason: rootPlan.reason
|
|
1961
|
+
});
|
|
1962
|
+
}
|
|
1963
|
+
plan.push({
|
|
1964
|
+
kind: "delete",
|
|
1965
|
+
op: "file",
|
|
1966
|
+
target: `${repoRoot}/.opencode/ship.lock.json`,
|
|
1967
|
+
relPath: ".opencode/ship.lock.json",
|
|
1968
|
+
reason: "remove the install lock inside the transaction"
|
|
1969
|
+
});
|
|
1282
1970
|
return plan;
|
|
1283
1971
|
}
|
|
1284
|
-
async function planConfigSynthesis({ repoRoot, detection, lock, forceOverwrite, migrationSeed = null }) {
|
|
1972
|
+
async function planConfigSynthesis({ repoRoot, detection, lock, forceOverwrite, migrationSeed = null, models = null }) {
|
|
1285
1973
|
const existing = await loadConfig(repoRoot);
|
|
1286
1974
|
if (existing?.ok && !forceOverwrite) {
|
|
1287
1975
|
return {
|
|
@@ -1295,7 +1983,28 @@ async function planConfigSynthesis({ repoRoot, detection, lock, forceOverwrite,
|
|
|
1295
1983
|
reason: "user config already present"
|
|
1296
1984
|
};
|
|
1297
1985
|
}
|
|
1298
|
-
|
|
1986
|
+
let desiredValue = migrationSeed ?? renderDefaultConfig(detection);
|
|
1987
|
+
if (models && (models.planner || models.builder || models.finalReviewer)) {
|
|
1988
|
+
const merged = {
|
|
1989
|
+
...desiredValue,
|
|
1990
|
+
schemaVersion: 2,
|
|
1991
|
+
profile: "engineering",
|
|
1992
|
+
workflow: {
|
|
1993
|
+
...desiredValue.workflow ?? {},
|
|
1994
|
+
models: {
|
|
1995
|
+
planner: models.planner ?? desiredValue?.workflow?.models?.planner,
|
|
1996
|
+
builder: models.builder ?? desiredValue?.workflow?.models?.builder,
|
|
1997
|
+
finalReviewer: models.finalReviewer ?? desiredValue?.workflow?.models?.finalReviewer
|
|
1998
|
+
},
|
|
1999
|
+
approval: {
|
|
2000
|
+
mirrorToIssue: true,
|
|
2001
|
+
maxFailedRounds: 3,
|
|
2002
|
+
...desiredValue?.workflow?.approval ?? {}
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
};
|
|
2006
|
+
desiredValue = merged;
|
|
2007
|
+
}
|
|
1299
2008
|
const desiredJson = JSON.stringify(desiredValue, null, 2) + "\n";
|
|
1300
2009
|
const desiredSha = bytesHashString(desiredJson);
|
|
1301
2010
|
const kind = existing?.ok && forceOverwrite ? "update" : "create";
|
|
@@ -1313,184 +2022,27 @@ async function planConfigSynthesis({ repoRoot, detection, lock, forceOverwrite,
|
|
|
1313
2022
|
};
|
|
1314
2023
|
}
|
|
1315
2024
|
async function planRootConfigApply({ repoRoot, lock, forceRepair, planMode = null }) {
|
|
1316
|
-
const
|
|
1317
|
-
const
|
|
1318
|
-
const
|
|
1319
|
-
const
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
seededRecords.push({
|
|
1329
|
-
pointer: planMode.id,
|
|
1330
|
-
strategy: "value",
|
|
1331
|
-
installedSha256: bytesHashString(stableStringify(planMode.block)),
|
|
1332
|
-
previous: { existed: false }
|
|
1333
|
-
});
|
|
1334
|
-
}
|
|
1335
|
-
return {
|
|
1336
|
-
kind: "noop",
|
|
1337
|
-
op: "root-config",
|
|
1338
|
-
target,
|
|
1339
|
-
relPath: detected.relative,
|
|
1340
|
-
reason: "no root opencode.json present",
|
|
1341
|
-
edits: [],
|
|
1342
|
-
pointerRecords: seededRecords
|
|
1343
|
-
};
|
|
1344
|
-
}
|
|
1345
|
-
if (fileMissing && forceRepair) {
|
|
1346
|
-
const { synthesizeDefaultRootConfig: synthesizeDefaultRootConfig2, formatRootConfig: formatRootConfig3, applyPlanModeOwnership: applyPlanModeOwnership2 } = await Promise.resolve().then(() => (init_root_config(), root_config_exports));
|
|
1347
|
-
let doc = synthesizeDefaultRootConfig2();
|
|
1348
|
-
if (planMode) {
|
|
1349
|
-
const applied = applyPlanModeOwnership2(doc, { block: planMode.block, pointer: planMode.id });
|
|
1350
|
-
doc = applied.doc;
|
|
1351
|
-
}
|
|
1352
|
-
const bytes2 = Buffer.from(formatRootConfig3(doc), "utf8");
|
|
1353
|
-
const newSha2 = bytesHashString(stableStringify(doc));
|
|
1354
|
-
const installedPointers2 = POINTER_ENTRIES.map((entry) => ({
|
|
1355
|
-
pointer: entry.pointer,
|
|
1356
|
-
strategy: entry.strategy,
|
|
1357
|
-
installedSha256: bytesHashString(stableStringify(entry.value)),
|
|
1358
|
-
previous: { existed: false }
|
|
1359
|
-
}));
|
|
1360
|
-
if (planMode) {
|
|
1361
|
-
installedPointers2.push({
|
|
1362
|
-
pointer: planMode.id,
|
|
1363
|
-
strategy: "value",
|
|
1364
|
-
installedSha256: bytesHashString(stableStringify(planMode.block)),
|
|
1365
|
-
previous: { existed: false }
|
|
1366
|
-
});
|
|
1367
|
-
}
|
|
1368
|
-
return {
|
|
1369
|
-
kind: "create",
|
|
1370
|
-
op: "root-config",
|
|
1371
|
-
target,
|
|
1372
|
-
relPath: detected.relative,
|
|
1373
|
-
bytes: bytes2,
|
|
1374
|
-
desiredSha: newSha2,
|
|
1375
|
-
currentSha: null,
|
|
1376
|
-
edits: installedPointers2.map((p) => ({ kind: "create", pointer: p.pointer, value: "(synthesized)" })),
|
|
1377
|
-
pointerRecords: installedPointers2,
|
|
1378
|
-
format: "json",
|
|
1379
|
-
document: doc,
|
|
1380
|
-
reason: "creating root opencode.json with installer-owned Build permissions"
|
|
1381
|
-
};
|
|
1382
|
-
}
|
|
1383
|
-
const docResult = readRootConfig(target);
|
|
1384
|
-
if (!docResult.ok) {
|
|
1385
|
-
return {
|
|
1386
|
-
kind: "noop",
|
|
1387
|
-
op: "root-config",
|
|
1388
|
-
target,
|
|
1389
|
-
relPath: detected.relative,
|
|
1390
|
-
reason: `root config ${docResult.error.kind}`,
|
|
1391
|
-
edits: [],
|
|
1392
|
-
pointerRecords: previous?.pointers ?? []
|
|
1393
|
-
};
|
|
1394
|
-
}
|
|
1395
|
-
const previousPointers = previous?.pointers ?? [];
|
|
1396
|
-
const result = applyOwnedPointers(docResult.value, { allowEqualValues: true });
|
|
1397
|
-
if (result.applied.length === 0 && result.skipped.every((s) => s.reason === "already equal")) {
|
|
1398
|
-
const ownedPointers = previousPointers.length > 0 ? previousPointers : POINTER_ENTRIES.map((entry) => {
|
|
1399
|
-
const existing = getPointer(docResult.value, entry.pointer);
|
|
1400
|
-
return {
|
|
1401
|
-
pointer: entry.pointer,
|
|
1402
|
-
strategy: entry.strategy,
|
|
1403
|
-
installedSha256: bytesHashString(stableStringify(existing ?? entry.value)),
|
|
1404
|
-
previous: existing === void 0 ? { existed: false } : { existed: true, value: existing }
|
|
1405
|
-
};
|
|
1406
|
-
});
|
|
1407
|
-
return {
|
|
1408
|
-
kind: "noop",
|
|
1409
|
-
op: "root-config",
|
|
1410
|
-
target,
|
|
1411
|
-
relPath: detected.relative,
|
|
1412
|
-
reason: "no installer-owned entries missing",
|
|
1413
|
-
edits: [],
|
|
1414
|
-
pointerRecords: ownedPointers,
|
|
1415
|
-
format: docResult.format,
|
|
1416
|
-
document: docResult.value,
|
|
1417
|
-
currentSha: docResult.sha256 ?? null,
|
|
1418
|
-
desiredSha: docResult.sha256 ?? null
|
|
1419
|
-
};
|
|
1420
|
-
}
|
|
1421
|
-
const edits = result.applied.map((a) => ({
|
|
1422
|
-
kind: "create",
|
|
1423
|
-
pointer: a.pointer,
|
|
1424
|
-
value: a.value
|
|
1425
|
-
}));
|
|
1426
|
-
for (const s of result.skipped) {
|
|
1427
|
-
if (s.reason === "already equal") continue;
|
|
1428
|
-
edits.push({ kind: "conflict", pointer: s.pointer, reason: s.reason, existing: s.existing, desired: s.desired });
|
|
1429
|
-
}
|
|
1430
|
-
const { formatRootConfigPreserving: formatRootConfigPreserving2, formatRootConfig: formatRootConfig2 } = await Promise.resolve().then(() => (init_root_config(), root_config_exports));
|
|
1431
|
-
const { parseRootConfigPreservingOrder: parseRootConfigPreservingOrder2 } = await Promise.resolve().then(() => (init_root_config(), root_config_exports));
|
|
1432
|
-
const { setPointer: setPointer2 } = await Promise.resolve().then(() => (init_json_pointer(), json_pointer_exports));
|
|
1433
|
-
let bytes;
|
|
1434
|
-
let docForWrite = result.doc;
|
|
1435
|
-
try {
|
|
1436
|
-
const { value: sourceValue, format: sourceFormat } = parseRootConfigPreservingOrder2(docResult.raw ?? "");
|
|
1437
|
-
docForWrite = sourceValue && typeof sourceValue === "object" ? sourceValue : result.doc;
|
|
1438
|
-
for (const entry of result.applied) {
|
|
1439
|
-
docForWrite = setPointer2(docForWrite, entry.pointer, entry.value);
|
|
1440
|
-
}
|
|
1441
|
-
bytes = Buffer.from(formatRootConfigPreserving2(docForWrite), "utf8");
|
|
1442
|
-
void sourceFormat;
|
|
1443
|
-
} catch {
|
|
1444
|
-
bytes = Buffer.from(formatRootConfig2(result.doc), "utf8");
|
|
1445
|
-
}
|
|
1446
|
-
const newSha = bytesHashString(stableStringify(docForWrite));
|
|
1447
|
-
const installedPointers = [...previousPointers];
|
|
1448
|
-
for (const e of result.applied) {
|
|
1449
|
-
const idx = installedPointers.findIndex((p) => p.pointer === e.pointer);
|
|
1450
|
-
const previousEntry = docResult.before?.[e.pointer];
|
|
1451
|
-
const next = {
|
|
1452
|
-
pointer: e.pointer,
|
|
1453
|
-
strategy: "value",
|
|
1454
|
-
installedSha256: bytesHashString(stableStringify(e.value)),
|
|
1455
|
-
previous: previousEntry == null ? { existed: false } : { existed: true, value: previousEntry }
|
|
1456
|
-
};
|
|
1457
|
-
if (idx >= 0) installedPointers[idx] = next;
|
|
1458
|
-
else installedPointers.push(next);
|
|
1459
|
-
}
|
|
1460
|
-
for (const e of result.skipped) {
|
|
1461
|
-
if (e.reason !== "already equal") continue;
|
|
1462
|
-
const idx = installedPointers.findIndex((p) => p.pointer === e.pointer);
|
|
1463
|
-
if (idx >= 0) continue;
|
|
1464
|
-
installedPointers.push({
|
|
1465
|
-
pointer: e.pointer,
|
|
1466
|
-
strategy: "value",
|
|
1467
|
-
installedSha256: bytesHashString(stableStringify(e.existing ?? null)),
|
|
1468
|
-
previous: { existed: true, value: e.existing }
|
|
1469
|
-
});
|
|
1470
|
-
}
|
|
1471
|
-
return {
|
|
1472
|
-
kind: edits.some((e) => e.kind === "conflict") ? "conflict" : edits.length ? "update" : "noop",
|
|
1473
|
-
op: "root-config",
|
|
1474
|
-
target,
|
|
1475
|
-
relPath: detected.relative,
|
|
1476
|
-
bytes,
|
|
1477
|
-
desiredSha: newSha,
|
|
1478
|
-
currentSha: docResult.sha256 ?? null,
|
|
1479
|
-
edits,
|
|
1480
|
-
pointerRecords: installedPointers,
|
|
1481
|
-
format: docResult.format,
|
|
1482
|
-
document: docForWrite,
|
|
1483
|
-
reason: edits.length === 0 ? "no installer-owned entries missing" : `apply ${result.applied.length} / skip ${result.skipped.length}`
|
|
1484
|
-
};
|
|
2025
|
+
const previous = (lock?.manager?.rootDocuments ?? []).flatMap((d) => d.pointers ?? []);
|
|
2026
|
+
const previousProfile = lock?.manager?.profile ?? null;
|
|
2027
|
+
const desiredProfile = planMode && planMode.scope === "engineering" ? "engineering" : "core";
|
|
2028
|
+
const isTransition = previousProfile !== null && previousProfile !== desiredProfile;
|
|
2029
|
+
const mode = previous.length === 0 ? "install" : isTransition ? "profile-transition" : "install";
|
|
2030
|
+
return planRootReconciliation({
|
|
2031
|
+
repoRoot,
|
|
2032
|
+
profile: desiredProfile,
|
|
2033
|
+
mode,
|
|
2034
|
+
previousRecords: previous,
|
|
2035
|
+
forceRepair: Boolean(forceRepair)
|
|
2036
|
+
});
|
|
1485
2037
|
}
|
|
1486
2038
|
|
|
1487
2039
|
// src/installer/lock.js
|
|
1488
2040
|
init_hash();
|
|
1489
2041
|
init_json_pointer();
|
|
1490
2042
|
import { readFile as readFile4, writeFile as writeFile2, rename as rename2, mkdir as mkdir2 } from "node:fs/promises";
|
|
1491
|
-
import { existsSync as
|
|
2043
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
1492
2044
|
import { dirname as dirname4, resolve as resolve6 } from "node:path";
|
|
1493
|
-
var CURRENT_LOCK_SCHEMA =
|
|
2045
|
+
var CURRENT_LOCK_SCHEMA = 3;
|
|
1494
2046
|
function lockPath(repoRoot) {
|
|
1495
2047
|
return resolve6(repoRoot, ".opencode", "ship.lock.json");
|
|
1496
2048
|
}
|
|
@@ -1510,8 +2062,8 @@ function validateLock(rawLock) {
|
|
|
1510
2062
|
}
|
|
1511
2063
|
const issues = [];
|
|
1512
2064
|
let kind = "ok";
|
|
1513
|
-
if (rawLock.contractVersion !== CURRENT_LOCK_SCHEMA && rawLock.contractVersion !== 1) {
|
|
1514
|
-
issues.push(`unsupported contractVersion: ${JSON.stringify(rawLock.contractVersion)} (expected ${CURRENT_LOCK_SCHEMA} or 1)`);
|
|
2065
|
+
if (rawLock.contractVersion !== CURRENT_LOCK_SCHEMA && rawLock.contractVersion !== 1 && rawLock.contractVersion !== 2) {
|
|
2066
|
+
issues.push(`unsupported contractVersion: ${JSON.stringify(rawLock.contractVersion)} (expected ${CURRENT_LOCK_SCHEMA}, 2, or 1)`);
|
|
1515
2067
|
kind = "schema";
|
|
1516
2068
|
}
|
|
1517
2069
|
const manager = rawLock.manager;
|
|
@@ -1521,13 +2073,13 @@ function validateLock(rawLock) {
|
|
|
1521
2073
|
} else if (typeof manager !== "object" || manager === null) {
|
|
1522
2074
|
issues.push("manager section must be an object");
|
|
1523
2075
|
kind = kind === "ok" ? "shape" : kind;
|
|
1524
|
-
} else if (manager.schemaVersion !== CURRENT_LOCK_SCHEMA && manager.schemaVersion !== 1) {
|
|
1525
|
-
issues.push(`unsupported manager.schemaVersion: ${JSON.stringify(manager.schemaVersion)} (expected ${CURRENT_LOCK_SCHEMA} or 1)`);
|
|
2076
|
+
} else if (manager.schemaVersion !== CURRENT_LOCK_SCHEMA && manager.schemaVersion !== 2 && manager.schemaVersion !== 1) {
|
|
2077
|
+
issues.push(`unsupported manager.schemaVersion: ${JSON.stringify(manager.schemaVersion)} (expected ${CURRENT_LOCK_SCHEMA}, 2, or 1)`);
|
|
1526
2078
|
kind = "schema";
|
|
1527
2079
|
} else if (manager.name !== "opencode-ship") {
|
|
1528
2080
|
issues.push(`unknown manager.name: ${JSON.stringify(manager.name)}`);
|
|
1529
2081
|
kind = "shape";
|
|
1530
|
-
} else if (rawLock.contractVersion
|
|
2082
|
+
} else if (rawLock.contractVersion >= 2 && manager.schemaVersion >= 2 && manager.profile !== void 0 && !isValidProfile(manager.profile)) {
|
|
1531
2083
|
issues.push(`invalid manager.profile: ${JSON.stringify(manager.profile)} (expected one of: core, engineering)`);
|
|
1532
2084
|
kind = "shape";
|
|
1533
2085
|
}
|
|
@@ -1549,7 +2101,7 @@ function validateLock(rawLock) {
|
|
|
1549
2101
|
}
|
|
1550
2102
|
async function readValidatedLock(repoRoot) {
|
|
1551
2103
|
const path = lockPath(repoRoot);
|
|
1552
|
-
if (!
|
|
2104
|
+
if (!existsSync8(path)) {
|
|
1553
2105
|
return { kind: "missing", lock: null, issues: [] };
|
|
1554
2106
|
}
|
|
1555
2107
|
let raw;
|
|
@@ -1573,8 +2125,8 @@ init_json_pointer();
|
|
|
1573
2125
|
|
|
1574
2126
|
// src/installer/detection/project.js
|
|
1575
2127
|
import { spawnSync } from "node:child_process";
|
|
1576
|
-
import { existsSync as
|
|
1577
|
-
import { resolve as resolve7, join } from "node:path";
|
|
2128
|
+
import { existsSync as existsSync9, readFileSync as readFileSync4 } from "node:fs";
|
|
2129
|
+
import { resolve as resolve7, join as join2 } from "node:path";
|
|
1578
2130
|
function runGit(cwd, args) {
|
|
1579
2131
|
const r = spawnSync("git", ["-C", cwd, ...args], {
|
|
1580
2132
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -1583,15 +2135,15 @@ function runGit(cwd, args) {
|
|
|
1583
2135
|
return { status: r.status ?? -1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
1584
2136
|
}
|
|
1585
2137
|
function detectPackageManager(repoRoot) {
|
|
1586
|
-
if (
|
|
1587
|
-
if (
|
|
1588
|
-
if (
|
|
1589
|
-
if (
|
|
2138
|
+
if (existsSync9(join2(repoRoot, "pnpm-lock.yaml"))) return "pnpm";
|
|
2139
|
+
if (existsSync9(join2(repoRoot, "yarn.lock"))) return "yarn";
|
|
2140
|
+
if (existsSync9(join2(repoRoot, "bun.lockb"))) return "bun";
|
|
2141
|
+
if (existsSync9(join2(repoRoot, "package-lock.json"))) return "npm";
|
|
1590
2142
|
return null;
|
|
1591
2143
|
}
|
|
1592
2144
|
function readPackageJson(repoRoot) {
|
|
1593
|
-
const path =
|
|
1594
|
-
if (!
|
|
2145
|
+
const path = join2(repoRoot, "package.json");
|
|
2146
|
+
if (!existsSync9(path)) return null;
|
|
1595
2147
|
try {
|
|
1596
2148
|
return JSON.parse(readFileSync4(path, "utf8"));
|
|
1597
2149
|
} catch {
|
|
@@ -1719,34 +2271,130 @@ init_root_config();
|
|
|
1719
2271
|
|
|
1720
2272
|
// src/installer/transaction.js
|
|
1721
2273
|
import {
|
|
1722
|
-
writeFile as
|
|
1723
|
-
rename as
|
|
1724
|
-
readFile as
|
|
1725
|
-
unlink,
|
|
1726
|
-
mkdir as
|
|
1727
|
-
stat as
|
|
2274
|
+
writeFile as writeFile4,
|
|
2275
|
+
rename as rename4,
|
|
2276
|
+
readFile as readFile6,
|
|
2277
|
+
unlink as unlink2,
|
|
2278
|
+
mkdir as mkdir4,
|
|
2279
|
+
stat as stat3,
|
|
1728
2280
|
open
|
|
1729
2281
|
} from "node:fs/promises";
|
|
1730
|
-
import { existsSync as
|
|
1731
|
-
import { dirname as
|
|
1732
|
-
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
2282
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
2283
|
+
import { dirname as dirname6, resolve as resolve10, join as join5 } from "node:path";
|
|
1733
2284
|
init_hash();
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
2285
|
+
|
|
2286
|
+
// src/state/git-common-dir.js
|
|
2287
|
+
import { spawn } from "node:child_process";
|
|
2288
|
+
import { resolve as resolve8, join as join3 } from "node:path";
|
|
2289
|
+
var STATE_DIRNAME = "opencode-ship";
|
|
2290
|
+
async function resolveGitCommonDir(repoRoot) {
|
|
2291
|
+
if (typeof repoRoot !== "string" || repoRoot.length === 0) {
|
|
2292
|
+
throw new Error("resolveGitCommonDir: repoRoot must be a non-empty string");
|
|
2293
|
+
}
|
|
2294
|
+
return new Promise((resolveP, reject) => {
|
|
2295
|
+
const proc = spawn(
|
|
2296
|
+
"git",
|
|
2297
|
+
["-C", repoRoot, "rev-parse", "--path-format=absolute", "--git-common-dir"],
|
|
2298
|
+
{ stdio: ["ignore", "pipe", "pipe"], shell: false }
|
|
2299
|
+
);
|
|
2300
|
+
let out = "";
|
|
2301
|
+
let err = "";
|
|
2302
|
+
proc.stdout.on("data", (d) => {
|
|
2303
|
+
out += d.toString("utf8");
|
|
2304
|
+
});
|
|
2305
|
+
proc.stderr.on("data", (d) => {
|
|
2306
|
+
err += d.toString("utf8");
|
|
2307
|
+
});
|
|
2308
|
+
proc.on("error", reject);
|
|
2309
|
+
proc.on("close", (code) => {
|
|
2310
|
+
if (code !== 0) {
|
|
2311
|
+
reject(new Error(
|
|
2312
|
+
`git rev-parse --git-common-dir failed (exit ${code}): ${err.trim() || "unknown error"}`
|
|
2313
|
+
));
|
|
2314
|
+
return;
|
|
2315
|
+
}
|
|
2316
|
+
const trimmed = out.trim();
|
|
2317
|
+
if (!trimmed) {
|
|
2318
|
+
reject(new Error("git rev-parse --git-common-dir returned an empty path"));
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
resolveP(resolve8(repoRoot, trimmed));
|
|
2322
|
+
});
|
|
1737
2323
|
});
|
|
1738
|
-
if (r.status !== 0) return null;
|
|
1739
|
-
const trimmed = r.stdout.trim();
|
|
1740
|
-
return trimmed ? resolve8(repoRoot, trimmed) : null;
|
|
1741
2324
|
}
|
|
1742
|
-
function
|
|
1743
|
-
|
|
2325
|
+
function opencodeShipStateDir(commonDir, ...segments) {
|
|
2326
|
+
if (typeof commonDir !== "string" || commonDir.length === 0) {
|
|
2327
|
+
throw new Error("opencodeShipStateDir: commonDir must be a non-empty string");
|
|
2328
|
+
}
|
|
2329
|
+
return join3(commonDir, STATE_DIRNAME, ...segments);
|
|
1744
2330
|
}
|
|
1745
|
-
|
|
1746
|
-
|
|
2331
|
+
|
|
2332
|
+
// src/state/durable-store.js
|
|
2333
|
+
import {
|
|
2334
|
+
open as fsOpen,
|
|
2335
|
+
writeFile as writeFile3,
|
|
2336
|
+
readFile as readFile5,
|
|
2337
|
+
rename as rename3,
|
|
2338
|
+
link,
|
|
2339
|
+
mkdir as mkdir3,
|
|
2340
|
+
readdir,
|
|
2341
|
+
unlink,
|
|
2342
|
+
stat as stat2
|
|
2343
|
+
} from "node:fs/promises";
|
|
2344
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
2345
|
+
import { dirname as dirname5, join as join4, resolve as resolve9 } from "node:path";
|
|
2346
|
+
import { createHash as createHash2, randomBytes } from "node:crypto";
|
|
2347
|
+
import { hostname as osHostname } from "node:os";
|
|
2348
|
+
var STALE_LOCK_MS = 120 * 1e3;
|
|
2349
|
+
function randomToken() {
|
|
2350
|
+
return randomBytes(8).toString("hex");
|
|
1747
2351
|
}
|
|
1748
|
-
function
|
|
1749
|
-
return
|
|
2352
|
+
function ensureString(value) {
|
|
2353
|
+
return JSON.stringify(value, null, 2) + "\n";
|
|
2354
|
+
}
|
|
2355
|
+
async function fsyncDir(path) {
|
|
2356
|
+
try {
|
|
2357
|
+
const handle = await fsOpen(path, "r");
|
|
2358
|
+
try {
|
|
2359
|
+
await handle.sync();
|
|
2360
|
+
} finally {
|
|
2361
|
+
await handle.close();
|
|
2362
|
+
}
|
|
2363
|
+
} catch {
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
async function atomicReplaceJson(path, value) {
|
|
2367
|
+
if (typeof path !== "string" || path.length === 0) {
|
|
2368
|
+
throw new Error("atomicReplaceJson: path must be a non-empty string");
|
|
2369
|
+
}
|
|
2370
|
+
const target = resolve9(path);
|
|
2371
|
+
const parent = dirname5(target);
|
|
2372
|
+
await mkdir3(parent, { recursive: true });
|
|
2373
|
+
const tmp = `${target}.${randomToken()}.tmp`;
|
|
2374
|
+
const handle = await fsOpen(tmp, "w", 384);
|
|
2375
|
+
try {
|
|
2376
|
+
await handle.writeFile(ensureString(value), "utf8");
|
|
2377
|
+
await handle.sync();
|
|
2378
|
+
} finally {
|
|
2379
|
+
await handle.close();
|
|
2380
|
+
}
|
|
2381
|
+
await rename3(tmp, target);
|
|
2382
|
+
await fsyncDir(parent);
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
// src/installer/transaction.js
|
|
2386
|
+
function lockDirFromCommonDir(commonDir) {
|
|
2387
|
+
return opencodeShipStateDir(commonDir);
|
|
2388
|
+
}
|
|
2389
|
+
async function lockDirForRepo(repoRoot) {
|
|
2390
|
+
const commonDir = await resolveGitCommonDir(repoRoot);
|
|
2391
|
+
return lockDirFromCommonDir(commonDir);
|
|
2392
|
+
}
|
|
2393
|
+
function transactionLockPath(lockDir) {
|
|
2394
|
+
return resolve10(lockDir, ".txn.lock");
|
|
2395
|
+
}
|
|
2396
|
+
function journalPath(lockDir, txnId) {
|
|
2397
|
+
return resolve10(lockDir, `.txn-${txnId}.journal`);
|
|
1750
2398
|
}
|
|
1751
2399
|
function backupPath(target, token) {
|
|
1752
2400
|
return `${target}.txn-${token}-backup`;
|
|
@@ -1755,46 +2403,35 @@ function stagedPath(target, token) {
|
|
|
1755
2403
|
return `${target}.txn-${token}-staged`;
|
|
1756
2404
|
}
|
|
1757
2405
|
async function mkdirp(path) {
|
|
1758
|
-
await
|
|
2406
|
+
await mkdir4(path, { recursive: true });
|
|
1759
2407
|
}
|
|
1760
|
-
async function acquireLock(
|
|
1761
|
-
|
|
1762
|
-
const path = transactionLockPath(
|
|
1763
|
-
await
|
|
1764
|
-
let created = false;
|
|
2408
|
+
async function acquireLock(lockDir, txnId) {
|
|
2409
|
+
await mkdirp(lockDir);
|
|
2410
|
+
const path = transactionLockPath(lockDir);
|
|
2411
|
+
const handle = await open(path, "wx", 384);
|
|
1765
2412
|
try {
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
await handle.sync();
|
|
1775
|
-
} finally {
|
|
1776
|
-
await handle.close();
|
|
1777
|
-
}
|
|
1778
|
-
await fsyncDir(dir);
|
|
1779
|
-
} catch (error) {
|
|
1780
|
-
if (created) {
|
|
1781
|
-
await unlink(path).catch(() => null);
|
|
1782
|
-
await fsyncDir(dir);
|
|
1783
|
-
}
|
|
1784
|
-
throw error;
|
|
2413
|
+
await handle.writeFile(JSON.stringify({
|
|
2414
|
+
pid: process.pid,
|
|
2415
|
+
txnId,
|
|
2416
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2417
|
+
}));
|
|
2418
|
+
await handle.sync();
|
|
2419
|
+
} finally {
|
|
2420
|
+
await handle.close();
|
|
1785
2421
|
}
|
|
2422
|
+
await fsyncDir2(lockDir);
|
|
1786
2423
|
}
|
|
1787
|
-
async function releaseLock(
|
|
2424
|
+
async function releaseLock(lockDir) {
|
|
1788
2425
|
try {
|
|
1789
|
-
await
|
|
2426
|
+
await unlink2(transactionLockPath(lockDir));
|
|
1790
2427
|
} catch {
|
|
1791
2428
|
}
|
|
1792
2429
|
}
|
|
1793
|
-
async function liveLockOwner(
|
|
1794
|
-
const path = transactionLockPath(
|
|
1795
|
-
if (!
|
|
2430
|
+
async function liveLockOwner(lockDir) {
|
|
2431
|
+
const path = transactionLockPath(lockDir);
|
|
2432
|
+
if (!existsSync11(path)) return false;
|
|
1796
2433
|
try {
|
|
1797
|
-
const lock = JSON.parse(await
|
|
2434
|
+
const lock = JSON.parse(await readFile6(path, "utf8"));
|
|
1798
2435
|
if (!Number.isInteger(lock?.pid) || lock.pid <= 0) return false;
|
|
1799
2436
|
process.kill(lock.pid, 0);
|
|
1800
2437
|
return true;
|
|
@@ -1802,7 +2439,7 @@ async function liveLockOwner(repoRoot) {
|
|
|
1802
2439
|
return error?.code === "EPERM";
|
|
1803
2440
|
}
|
|
1804
2441
|
}
|
|
1805
|
-
async function
|
|
2442
|
+
async function fsyncDir2(path) {
|
|
1806
2443
|
try {
|
|
1807
2444
|
const handle = await open(path, "r");
|
|
1808
2445
|
await handle.sync();
|
|
@@ -1810,13 +2447,13 @@ async function fsyncDir(path) {
|
|
|
1810
2447
|
} catch {
|
|
1811
2448
|
}
|
|
1812
2449
|
}
|
|
1813
|
-
function
|
|
2450
|
+
function randomToken2() {
|
|
1814
2451
|
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1815
2452
|
}
|
|
1816
2453
|
function asMode(mode) {
|
|
1817
2454
|
return typeof mode === "number" ? mode : 420;
|
|
1818
2455
|
}
|
|
1819
|
-
async function writeJournal(
|
|
2456
|
+
async function writeJournal(lockDir, txnId, journal) {
|
|
1820
2457
|
const { entries, ...header } = journal;
|
|
1821
2458
|
const json = { ...header, ledger: entries.map((entry) => ({
|
|
1822
2459
|
op: entry.op,
|
|
@@ -1828,62 +2465,63 @@ async function writeJournal(repoRoot, txnId, journal) {
|
|
|
1828
2465
|
installedSha256: entry.installedSha256 ?? null,
|
|
1829
2466
|
mode: entry.mode ?? null
|
|
1830
2467
|
})) };
|
|
1831
|
-
|
|
1832
|
-
const staged = `${target}.tmp`;
|
|
1833
|
-
const handle = await open(staged, "w", 384);
|
|
1834
|
-
try {
|
|
1835
|
-
await handle.writeFile(JSON.stringify(json, null, 2), "utf8");
|
|
1836
|
-
await handle.sync();
|
|
1837
|
-
} finally {
|
|
1838
|
-
await handle.close();
|
|
1839
|
-
}
|
|
1840
|
-
await rename3(staged, target);
|
|
1841
|
-
await fsyncDir(dirname5(target));
|
|
2468
|
+
await atomicReplaceJson(journalPath(lockDir, txnId), json);
|
|
1842
2469
|
}
|
|
1843
|
-
async function clearJournal(
|
|
2470
|
+
async function clearJournal(lockDir, txnId) {
|
|
1844
2471
|
if (!txnId) return;
|
|
1845
2472
|
try {
|
|
1846
|
-
await
|
|
2473
|
+
await unlink2(journalPath(lockDir, txnId));
|
|
1847
2474
|
} catch {
|
|
1848
2475
|
}
|
|
1849
2476
|
}
|
|
1850
|
-
async function readJournal(
|
|
1851
|
-
const path =
|
|
2477
|
+
async function readJournal(lockDir, name) {
|
|
2478
|
+
const path = resolve10(lockDir, name);
|
|
2479
|
+
let raw;
|
|
1852
2480
|
try {
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
2481
|
+
raw = await readFile6(path, "utf8");
|
|
2482
|
+
} catch (err) {
|
|
2483
|
+
throw new Error(`transaction journal unreadable: ${path}: ${err?.message ?? err}`);
|
|
2484
|
+
}
|
|
2485
|
+
let parsed;
|
|
2486
|
+
try {
|
|
2487
|
+
parsed = JSON.parse(raw);
|
|
2488
|
+
} catch (err) {
|
|
2489
|
+
throw new Error(`transaction journal malformed JSON: ${path}: ${err?.message ?? err}`);
|
|
1857
2490
|
}
|
|
2491
|
+
if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.ledger)) {
|
|
2492
|
+
throw new Error(`transaction journal missing ledger: ${path}`);
|
|
2493
|
+
}
|
|
2494
|
+
return parsed;
|
|
1858
2495
|
}
|
|
1859
2496
|
async function isCommitted(journal) {
|
|
1860
2497
|
if (journal.committed) return true;
|
|
1861
2498
|
const marker = journal.ledger?.find((entry) => entry.commitMarker);
|
|
1862
|
-
if (!marker?.target || !marker.installedSha256 || !
|
|
2499
|
+
if (!marker?.target || !marker.installedSha256 || !existsSync11(marker.target)) return false;
|
|
1863
2500
|
try {
|
|
1864
|
-
return bytesHashString(await
|
|
2501
|
+
return bytesHashString(await readFile6(marker.target, "utf8")) === marker.installedSha256;
|
|
1865
2502
|
} catch {
|
|
1866
2503
|
return false;
|
|
1867
2504
|
}
|
|
1868
2505
|
}
|
|
1869
2506
|
async function restoreEntry(entry) {
|
|
1870
2507
|
if (entry.op === "write") {
|
|
1871
|
-
if (entry.backup &&
|
|
1872
|
-
await
|
|
1873
|
-
} else if (entry.hadOriginal === false &&
|
|
1874
|
-
await
|
|
2508
|
+
if (entry.backup && existsSync11(entry.backup)) {
|
|
2509
|
+
await rename4(entry.backup, entry.target);
|
|
2510
|
+
} else if (entry.hadOriginal === false && existsSync11(entry.target)) {
|
|
2511
|
+
await unlink2(entry.target);
|
|
1875
2512
|
}
|
|
1876
|
-
if (entry.staged &&
|
|
1877
|
-
} else if (entry.op === "delete" && entry.backup &&
|
|
1878
|
-
await
|
|
2513
|
+
if (entry.staged && existsSync11(entry.staged)) await unlink2(entry.staged);
|
|
2514
|
+
} else if (entry.op === "delete" && entry.backup && existsSync11(entry.backup)) {
|
|
2515
|
+
await rename4(entry.backup, entry.target);
|
|
1879
2516
|
}
|
|
1880
|
-
await
|
|
2517
|
+
await fsyncDir2(dirname6(entry.target));
|
|
1881
2518
|
}
|
|
1882
|
-
async function recoverJournal(
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
await
|
|
1886
|
-
|
|
2519
|
+
async function recoverJournal(lockDir, name) {
|
|
2520
|
+
let journal;
|
|
2521
|
+
try {
|
|
2522
|
+
journal = await readJournal(lockDir, name);
|
|
2523
|
+
} catch (err) {
|
|
2524
|
+
return { ok: false, error: err };
|
|
1887
2525
|
}
|
|
1888
2526
|
const committed = await isCommitted(journal);
|
|
1889
2527
|
const entries = committed ? journal.ledger ?? [] : [...journal.ledger ?? []].reverse();
|
|
@@ -1896,72 +2534,79 @@ async function recoverJournal(repoRoot, dir, name) {
|
|
|
1896
2534
|
complete = false;
|
|
1897
2535
|
}
|
|
1898
2536
|
}
|
|
1899
|
-
if (complete) await
|
|
1900
|
-
return complete;
|
|
2537
|
+
if (complete) await unlink2(resolve10(lockDir, name)).catch(() => null);
|
|
2538
|
+
return { ok: complete };
|
|
1901
2539
|
}
|
|
1902
|
-
async function recover(repoRoot) {
|
|
1903
|
-
|
|
1904
|
-
if (
|
|
1905
|
-
if (await liveLockOwner(repoRoot)) {
|
|
2540
|
+
async function recover(repoRoot, lockDir) {
|
|
2541
|
+
if (!existsSync11(lockDir)) return { recovered: false, recoveredCount: 0 };
|
|
2542
|
+
if (await liveLockOwner(lockDir)) {
|
|
1906
2543
|
return { recovered: false, recoveredCount: 0, blocked: true };
|
|
1907
2544
|
}
|
|
1908
|
-
const { readdir } = await import("node:fs/promises");
|
|
1909
|
-
const names = await
|
|
2545
|
+
const { readdir: readdir2 } = await import("node:fs/promises");
|
|
2546
|
+
const names = await readdir2(lockDir).catch(() => []);
|
|
1910
2547
|
const journals = names.filter((n) => n.startsWith(".txn-") && n.endsWith(".journal"));
|
|
1911
2548
|
if (!journals.length) {
|
|
1912
|
-
await releaseLock(
|
|
2549
|
+
await releaseLock(lockDir);
|
|
1913
2550
|
return { recovered: false, recoveredCount: 0 };
|
|
1914
2551
|
}
|
|
1915
2552
|
let totalRecovered = 0;
|
|
1916
2553
|
let recoveryFailed = false;
|
|
2554
|
+
let recoveryError = null;
|
|
1917
2555
|
for (const name of journals) {
|
|
1918
|
-
|
|
1919
|
-
|
|
2556
|
+
const result = await recoverJournal(lockDir, name);
|
|
2557
|
+
if (!result.ok) {
|
|
2558
|
+
recoveryFailed = true;
|
|
2559
|
+
if (result.error) recoveryError = result.error.message ?? String(result.error);
|
|
2560
|
+
} else {
|
|
2561
|
+
totalRecovered += 1;
|
|
2562
|
+
}
|
|
1920
2563
|
}
|
|
1921
|
-
await releaseLock(
|
|
2564
|
+
await releaseLock(lockDir);
|
|
1922
2565
|
return {
|
|
1923
2566
|
recovered: totalRecovered > 0,
|
|
1924
2567
|
recoveredCount: totalRecovered,
|
|
1925
2568
|
blocked: recoveryFailed,
|
|
1926
|
-
reason: recoveryFailed ? "recovery-failed" : null
|
|
2569
|
+
reason: recoveryFailed ? "recovery-failed" : null,
|
|
2570
|
+
recoveryError
|
|
1927
2571
|
};
|
|
1928
2572
|
}
|
|
1929
2573
|
async function commitEntry(entry) {
|
|
1930
2574
|
let changed = false;
|
|
1931
|
-
if (entry.backup &&
|
|
1932
|
-
await
|
|
2575
|
+
if (entry.backup && existsSync11(entry.backup)) {
|
|
2576
|
+
await unlink2(entry.backup);
|
|
1933
2577
|
changed = true;
|
|
1934
2578
|
}
|
|
1935
|
-
if (entry.staged &&
|
|
1936
|
-
await
|
|
2579
|
+
if (entry.staged && existsSync11(entry.staged)) {
|
|
2580
|
+
await unlink2(entry.staged);
|
|
1937
2581
|
changed = true;
|
|
1938
2582
|
}
|
|
1939
|
-
if (changed) await
|
|
2583
|
+
if (changed) await fsyncDir2(dirname6(entry.target));
|
|
1940
2584
|
}
|
|
1941
2585
|
async function executePlan({ repoRoot, plan, newLockBuilder }) {
|
|
1942
|
-
const
|
|
2586
|
+
const lockDir = await lockDirForRepo(repoRoot);
|
|
2587
|
+
const recovered = await recover(repoRoot, lockDir);
|
|
1943
2588
|
if (recovered.blocked) {
|
|
1944
2589
|
const kind = recovered.reason ?? "lock-held";
|
|
1945
|
-
const message = kind === "lock-held" ? "another opencode-ship transaction is active" :
|
|
2590
|
+
const message = kind === "lock-held" ? "another opencode-ship transaction is active" : `a previous opencode-ship transaction could not be recovered: ${recovered.recoveryError ?? "unknown error"}`;
|
|
1946
2591
|
return { ok: false, error: { kind, message } };
|
|
1947
2592
|
}
|
|
1948
2593
|
const txnId = `txn-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
1949
2594
|
try {
|
|
1950
|
-
await acquireLock(
|
|
2595
|
+
await acquireLock(lockDir, txnId);
|
|
1951
2596
|
} catch (e) {
|
|
1952
2597
|
return { ok: false, error: { kind: "lock-held", message: e?.message ?? String(e) } };
|
|
1953
2598
|
}
|
|
1954
2599
|
const journal = { repoRoot, txnId, startedAt: (/* @__PURE__ */ new Date()).toISOString(), entries: [] };
|
|
1955
2600
|
try {
|
|
1956
|
-
await writeJournal(
|
|
2601
|
+
await writeJournal(lockDir, txnId, journal);
|
|
1957
2602
|
for (const op of plan) {
|
|
1958
2603
|
if (op.op !== "file") continue;
|
|
1959
2604
|
if (op.kind === "conflict" || op.kind === "noop" || op.kind === "converge") continue;
|
|
1960
|
-
const token =
|
|
2605
|
+
const token = randomToken2();
|
|
1961
2606
|
const backup = backupPath(op.target, token);
|
|
1962
2607
|
const staged = stagedPath(op.target, token);
|
|
1963
2608
|
if (op.kind === "delete") {
|
|
1964
|
-
if (!
|
|
2609
|
+
if (!existsSync11(op.target)) continue;
|
|
1965
2610
|
journal.entries.push({
|
|
1966
2611
|
op: "delete",
|
|
1967
2612
|
target: op.target,
|
|
@@ -1970,11 +2615,11 @@ async function executePlan({ repoRoot, plan, newLockBuilder }) {
|
|
|
1970
2615
|
hadOriginal: true,
|
|
1971
2616
|
mode: asMode(op.mode)
|
|
1972
2617
|
});
|
|
1973
|
-
await writeJournal(
|
|
1974
|
-
await
|
|
2618
|
+
await writeJournal(lockDir, txnId, journal);
|
|
2619
|
+
await rename4(op.target, backup);
|
|
1975
2620
|
} else {
|
|
1976
|
-
await mkdirp(
|
|
1977
|
-
const hadOriginal =
|
|
2621
|
+
await mkdirp(dirname6(op.target));
|
|
2622
|
+
const hadOriginal = existsSync11(op.target);
|
|
1978
2623
|
journal.entries.push({
|
|
1979
2624
|
op: "write",
|
|
1980
2625
|
target: op.target,
|
|
@@ -1983,24 +2628,24 @@ async function executePlan({ repoRoot, plan, newLockBuilder }) {
|
|
|
1983
2628
|
hadOriginal,
|
|
1984
2629
|
mode: asMode(op.mode)
|
|
1985
2630
|
});
|
|
1986
|
-
await writeJournal(
|
|
1987
|
-
await
|
|
2631
|
+
await writeJournal(lockDir, txnId, journal);
|
|
2632
|
+
await writeFile4(staged, op.bytes ?? Buffer.alloc(0), { mode: asMode(op.mode) });
|
|
1988
2633
|
const h = await open(staged, "r+");
|
|
1989
2634
|
await h.sync();
|
|
1990
2635
|
await h.close();
|
|
1991
|
-
if (hadOriginal) await
|
|
1992
|
-
await
|
|
2636
|
+
if (hadOriginal) await rename4(op.target, backup);
|
|
2637
|
+
await rename4(staged, op.target);
|
|
1993
2638
|
}
|
|
1994
|
-
await
|
|
2639
|
+
await fsyncDir2(dirname6(op.target));
|
|
1995
2640
|
}
|
|
1996
2641
|
if (newLockBuilder) {
|
|
1997
2642
|
const lockValue = await newLockBuilder();
|
|
1998
2643
|
const lockPathTarget = `${repoRoot}/.opencode/ship.lock.json`;
|
|
1999
|
-
await mkdirp(
|
|
2000
|
-
const token =
|
|
2644
|
+
await mkdirp(dirname6(lockPathTarget));
|
|
2645
|
+
const token = randomToken2();
|
|
2001
2646
|
const backup = backupPath(lockPathTarget, token);
|
|
2002
2647
|
const staged = stagedPath(lockPathTarget, token);
|
|
2003
|
-
const hadOriginal =
|
|
2648
|
+
const hadOriginal = existsSync11(lockPathTarget);
|
|
2004
2649
|
const finalLock = { ...lockValue, integrity: computeIntegrity(lockValue) };
|
|
2005
2650
|
const lockBytes = JSON.stringify(finalLock, null, 2) + "\n";
|
|
2006
2651
|
journal.entries.push({
|
|
@@ -2013,17 +2658,17 @@ async function executePlan({ repoRoot, plan, newLockBuilder }) {
|
|
|
2013
2658
|
installedSha256: bytesHashString(lockBytes),
|
|
2014
2659
|
mode: 420
|
|
2015
2660
|
});
|
|
2016
|
-
await writeJournal(
|
|
2017
|
-
await
|
|
2661
|
+
await writeJournal(lockDir, txnId, journal);
|
|
2662
|
+
await writeFile4(staged, lockBytes, { mode: 420 });
|
|
2018
2663
|
const handle = await open(staged, "r+");
|
|
2019
2664
|
await handle.sync();
|
|
2020
2665
|
await handle.close();
|
|
2021
|
-
if (hadOriginal) await
|
|
2022
|
-
await
|
|
2023
|
-
await
|
|
2666
|
+
if (hadOriginal) await rename4(lockPathTarget, backup);
|
|
2667
|
+
await rename4(staged, lockPathTarget);
|
|
2668
|
+
await fsyncDir2(dirname6(lockPathTarget));
|
|
2024
2669
|
}
|
|
2025
2670
|
journal.committed = true;
|
|
2026
|
-
await writeJournal(
|
|
2671
|
+
await writeJournal(lockDir, txnId, journal);
|
|
2027
2672
|
let cleanupComplete = true;
|
|
2028
2673
|
for (const entry of journal.entries) {
|
|
2029
2674
|
try {
|
|
@@ -2032,7 +2677,7 @@ async function executePlan({ repoRoot, plan, newLockBuilder }) {
|
|
|
2032
2677
|
cleanupComplete = false;
|
|
2033
2678
|
}
|
|
2034
2679
|
}
|
|
2035
|
-
if (cleanupComplete) await clearJournal(
|
|
2680
|
+
if (cleanupComplete) await clearJournal(lockDir, txnId);
|
|
2036
2681
|
return {
|
|
2037
2682
|
ok: true,
|
|
2038
2683
|
txnId,
|
|
@@ -2041,13 +2686,13 @@ async function executePlan({ repoRoot, plan, newLockBuilder }) {
|
|
|
2041
2686
|
cleanupPending: !cleanupComplete
|
|
2042
2687
|
};
|
|
2043
2688
|
} catch (e) {
|
|
2044
|
-
await rollback(
|
|
2689
|
+
await rollback(lockDir, journal);
|
|
2045
2690
|
return { ok: false, error: { kind: "transaction-failed", message: e?.message ?? String(e) } };
|
|
2046
2691
|
} finally {
|
|
2047
|
-
await releaseLock(
|
|
2692
|
+
await releaseLock(lockDir);
|
|
2048
2693
|
}
|
|
2049
2694
|
}
|
|
2050
|
-
async function rollback(
|
|
2695
|
+
async function rollback(lockDir, journal) {
|
|
2051
2696
|
const entries = journal.entries ?? journal.ledger ?? [];
|
|
2052
2697
|
let complete = true;
|
|
2053
2698
|
for (const entry of [...entries].reverse()) {
|
|
@@ -2057,21 +2702,21 @@ async function rollback(repoRoot, journal) {
|
|
|
2057
2702
|
complete = false;
|
|
2058
2703
|
}
|
|
2059
2704
|
}
|
|
2060
|
-
if (complete && journal.txnId) await clearJournal(
|
|
2705
|
+
if (complete && journal.txnId) await clearJournal(lockDir, journal.txnId);
|
|
2061
2706
|
}
|
|
2062
2707
|
|
|
2063
2708
|
// src/installer/migration.js
|
|
2064
|
-
import { readFile as
|
|
2065
|
-
import { existsSync as
|
|
2066
|
-
import { resolve as
|
|
2709
|
+
import { readFile as readFile7 } from "node:fs/promises";
|
|
2710
|
+
import { existsSync as existsSync12 } from "node:fs";
|
|
2711
|
+
import { resolve as resolve11 } from "node:path";
|
|
2067
2712
|
function legacyAdapterPath(repoRoot) {
|
|
2068
|
-
return
|
|
2713
|
+
return resolve11(repoRoot, ".opencode", "delivery.json");
|
|
2069
2714
|
}
|
|
2070
2715
|
function legacyLockPath(repoRoot) {
|
|
2071
|
-
return
|
|
2716
|
+
return resolve11(repoRoot, ".opencode", "delivery.lock.json");
|
|
2072
2717
|
}
|
|
2073
2718
|
function legacyPluginPath(repoRoot) {
|
|
2074
|
-
return
|
|
2719
|
+
return resolve11(repoRoot, ".opencode", "plugin", "delivery.ts");
|
|
2075
2720
|
}
|
|
2076
2721
|
async function detectLegacyShapes(repoRoot) {
|
|
2077
2722
|
const out = {
|
|
@@ -2082,19 +2727,19 @@ async function detectLegacyShapes(repoRoot) {
|
|
|
2082
2727
|
reviewer: false,
|
|
2083
2728
|
verifier: false
|
|
2084
2729
|
};
|
|
2085
|
-
if (
|
|
2086
|
-
if (
|
|
2087
|
-
if (
|
|
2088
|
-
if (
|
|
2089
|
-
if (
|
|
2090
|
-
if (
|
|
2730
|
+
if (existsSync12(legacyAdapterPath(repoRoot))) out.adapter = true;
|
|
2731
|
+
if (existsSync12(legacyLockPath(repoRoot))) out.legacyLock = true;
|
|
2732
|
+
if (existsSync12(legacyPluginPath(repoRoot))) out.plugin = true;
|
|
2733
|
+
if (existsSync12(resolve11(repoRoot, ".opencode/plugin/opencode-ship.js"))) out.pluginOld = true;
|
|
2734
|
+
if (existsSync12(resolve11(repoRoot, ".opencode/agents/delivery-reviewer.md"))) out.reviewer = true;
|
|
2735
|
+
if (existsSync12(resolve11(repoRoot, ".opencode/agents/delivery-verifier.md"))) out.verifier = true;
|
|
2091
2736
|
return out;
|
|
2092
2737
|
}
|
|
2093
2738
|
async function readLegacyAdapter(repoRoot) {
|
|
2094
2739
|
const path = legacyAdapterPath(repoRoot);
|
|
2095
|
-
if (!
|
|
2740
|
+
if (!existsSync12(path)) return null;
|
|
2096
2741
|
try {
|
|
2097
|
-
const raw = await
|
|
2742
|
+
const raw = await readFile7(path, "utf8");
|
|
2098
2743
|
return { path, raw, value: JSON.parse(raw) };
|
|
2099
2744
|
} catch {
|
|
2100
2745
|
return null;
|
|
@@ -2113,13 +2758,13 @@ async function migration({ repoRoot, lock, forceRepair, detection = null }) {
|
|
|
2113
2758
|
if (legacy && shapes.legacyLock && !lock?.manager) {
|
|
2114
2759
|
actions.push({ kind: "kept-legacy-lock", path: legacyLockPath(repoRoot) });
|
|
2115
2760
|
}
|
|
2116
|
-
if (shapes.plugin &&
|
|
2761
|
+
if (shapes.plugin && existsSync12(resolve11(repoRoot, ".opencode/plugins/opencode-ship.js"))) {
|
|
2117
2762
|
if (!forceRepair) {
|
|
2118
2763
|
actions.push({ kind: "candidate-remove-legacy-plugin", path: legacyPluginPath(repoRoot) });
|
|
2119
2764
|
}
|
|
2120
2765
|
}
|
|
2121
2766
|
if (shapes.pluginOld) {
|
|
2122
|
-
actions.push({ kind: "candidate-remove-legacy-plugin-path", path:
|
|
2767
|
+
actions.push({ kind: "candidate-remove-legacy-plugin-path", path: resolve11(repoRoot, ".opencode/plugin/opencode-ship.js") });
|
|
2123
2768
|
}
|
|
2124
2769
|
return { shapes, actions, legacyPresent: Boolean(legacy), proposedConfigSeed };
|
|
2125
2770
|
}
|
|
@@ -2163,11 +2808,11 @@ function legacyToShipConfig(legacy, detection = null) {
|
|
|
2163
2808
|
// src/installer/executor.js
|
|
2164
2809
|
init_plan_mode_permissions();
|
|
2165
2810
|
async function readCurrentBytes(targetPath) {
|
|
2166
|
-
if (!
|
|
2167
|
-
const buf = await
|
|
2811
|
+
if (!existsSync13(targetPath)) return null;
|
|
2812
|
+
const buf = await readFile8(targetPath);
|
|
2168
2813
|
return { bytes: buf, hash: bytesHashString(buf.toString("utf8")) };
|
|
2169
2814
|
}
|
|
2170
|
-
async function previewInstall({ rootPath, profile = null, replaceManaged, forceConfig, forceRootConfig }) {
|
|
2815
|
+
async function previewInstall({ rootPath, profile = null, replaceManaged, forceConfig, forceRootConfig, models = null }) {
|
|
2171
2816
|
const detection = detectProject(rootPath ?? process.cwd());
|
|
2172
2817
|
if (detection.errors.some((e) => e.kind === "not-a-git-repo")) {
|
|
2173
2818
|
return { ok: false, error: { kind: "invalid-project", errors: detection.errors } };
|
|
@@ -2189,7 +2834,33 @@ async function previewInstall({ rootPath, profile = null, replaceManaged, forceC
|
|
|
2189
2834
|
config: configValue,
|
|
2190
2835
|
lock
|
|
2191
2836
|
});
|
|
2837
|
+
if (resolved.profile === "engineering") {
|
|
2838
|
+
const candidate = await planConfigSynthesis({
|
|
2839
|
+
repoRoot,
|
|
2840
|
+
detection,
|
|
2841
|
+
lock,
|
|
2842
|
+
forceOverwrite: Boolean(forceConfig),
|
|
2843
|
+
migrationSeed: migrationReport?.proposedConfigSeed ?? null,
|
|
2844
|
+
models
|
|
2845
|
+
});
|
|
2846
|
+
if (candidate.kind === "create" || candidate.kind === "update") {
|
|
2847
|
+
const configValue2 = candidate.configValue;
|
|
2848
|
+
if (!configValue2.workflow || !configValue2.workflow.models) {
|
|
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) {
|
|
2856
|
+
return { ok: false, error: { kind: "engineering-approval-required", message: "engineering profile requires workflow.approval.{mirrorToIssue:true, maxFailedRounds:3}" } };
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
const previousProfile = lock?.manager?.profile ?? null;
|
|
2861
|
+
const isProfileTransition = previousProfile && previousProfile !== resolved.profile;
|
|
2192
2862
|
const activeCatalog = filterCatalogByProfile(CATALOG, resolved.profile);
|
|
2863
|
+
const staleCatalog = isProfileTransition ? filterCatalogByProfile(CATALOG, previousProfile).filter((e) => !activeCatalog.includes(e)) : [];
|
|
2193
2864
|
const configPlan = await planConfigSynthesis({
|
|
2194
2865
|
repoRoot,
|
|
2195
2866
|
detection,
|
|
@@ -2198,15 +2869,16 @@ async function previewInstall({ rootPath, profile = null, replaceManaged, forceC
|
|
|
2198
2869
|
migrationSeed: migrationReport?.proposedConfigSeed ?? null
|
|
2199
2870
|
});
|
|
2200
2871
|
const filePlan = await planFileInstall({ repoRoot, lock, allowUnowned: Boolean(replaceManaged), catalog: activeCatalog });
|
|
2872
|
+
const staleFilePlan = await planStaleFileRemoval({ repoRoot, lock, staleCatalog });
|
|
2201
2873
|
const migrationPlan = await planMigrationCleanup({
|
|
2202
2874
|
repoRoot,
|
|
2203
2875
|
lock,
|
|
2204
2876
|
migrationReport,
|
|
2205
2877
|
allowUnowned: Boolean(replaceManaged)
|
|
2206
2878
|
});
|
|
2207
|
-
const planMode = resolved.profile === "engineering" ? { id: "/agent/plan/permission", block: planModePermissions().build } : null;
|
|
2879
|
+
const planMode = resolved.profile === "engineering" ? { id: "/agent/plan/permission", block: planModePermissions().build, scope: "engineering" } : null;
|
|
2208
2880
|
const rootPlan = await planRootConfigApply({ repoRoot, lock, forceRepair: Boolean(forceRootConfig), planMode });
|
|
2209
|
-
const plan = [...filePlan ?? [], ...migrationPlan, configPlan, rootPlan];
|
|
2881
|
+
const plan = [...filePlan ?? [], ...staleFilePlan, ...migrationPlan, configPlan, rootPlan];
|
|
2210
2882
|
const conflicts = plan.filter((p) => p && p.kind === "conflict");
|
|
2211
2883
|
const summary = summarise(plan);
|
|
2212
2884
|
return {
|
|
@@ -2215,6 +2887,8 @@ async function previewInstall({ rootPath, profile = null, replaceManaged, forceC
|
|
|
2215
2887
|
detection,
|
|
2216
2888
|
lock,
|
|
2217
2889
|
profile: resolved,
|
|
2890
|
+
previousProfile,
|
|
2891
|
+
isProfileTransition,
|
|
2218
2892
|
plan,
|
|
2219
2893
|
conflicts,
|
|
2220
2894
|
summary,
|
|
@@ -2286,7 +2960,7 @@ async function assembleLock({ repoRoot, plan, lock, configPlan, rootPlan, profil
|
|
|
2286
2960
|
manager: {
|
|
2287
2961
|
schemaVersion: CURRENT_LOCK_SCHEMA,
|
|
2288
2962
|
name: "opencode-ship",
|
|
2289
|
-
version: "0.
|
|
2963
|
+
version: "0.10.0-rc.18",
|
|
2290
2964
|
templateSet: TEMPLATE_SET_ID,
|
|
2291
2965
|
// Newly written locks always carry the resolved profile so
|
|
2292
2966
|
// future CLI invocations without --profile still resolve to
|
|
@@ -2434,10 +3108,10 @@ function relativeTemplate(source) {
|
|
|
2434
3108
|
}
|
|
2435
3109
|
|
|
2436
3110
|
// src/installer/commands/doctor.js
|
|
2437
|
-
import { existsSync as
|
|
2438
|
-
import { spawnSync as
|
|
3111
|
+
import { existsSync as existsSync14, readFileSync as readFileSync5 } from "node:fs";
|
|
3112
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
2439
3113
|
init_hash();
|
|
2440
|
-
import { resolve as
|
|
3114
|
+
import { resolve as resolve13 } from "node:path";
|
|
2441
3115
|
init_root_config();
|
|
2442
3116
|
|
|
2443
3117
|
// src/installer/report.js
|
|
@@ -2492,11 +3166,11 @@ function checkNode() {
|
|
|
2492
3166
|
return { name: "node>=22.6.0", ok: /^v2[2-9]/.test(process.version), detail: process.version };
|
|
2493
3167
|
}
|
|
2494
3168
|
function checkGit() {
|
|
2495
|
-
const r =
|
|
3169
|
+
const r = spawnSync2("git", ["--version"], { encoding: "utf8" });
|
|
2496
3170
|
return { name: "git installed", ok: r.status === 0, detail: r.status === 0 ? r.stdout.trim() : "git not on PATH" };
|
|
2497
3171
|
}
|
|
2498
3172
|
function checkGh() {
|
|
2499
|
-
const r =
|
|
3173
|
+
const r = spawnSync2("gh", ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
2500
3174
|
return { name: "gh installed", ok: r.status === 0, detail: r.status === 0 ? r.stdout.trim() : "gh CLI not on PATH" };
|
|
2501
3175
|
}
|
|
2502
3176
|
function checkGhAuth() {
|
|
@@ -2504,7 +3178,7 @@ function checkGhAuth() {
|
|
|
2504
3178
|
if (!envToken) {
|
|
2505
3179
|
return { name: "gh auth status", ok: false, detail: "no GH_TOKEN / GITHUB_TOKEN in environment; gh auth skipped" };
|
|
2506
3180
|
}
|
|
2507
|
-
const r =
|
|
3181
|
+
const r = spawnSync2("gh", ["auth", "status"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
2508
3182
|
return {
|
|
2509
3183
|
name: "gh auth status",
|
|
2510
3184
|
ok: r.status === 0,
|
|
@@ -2526,7 +3200,7 @@ function checkPackageIntegrity() {
|
|
|
2526
3200
|
function buildSourceHashIndex() {
|
|
2527
3201
|
const idx = /* @__PURE__ */ new Map();
|
|
2528
3202
|
for (const entry of CATALOG) {
|
|
2529
|
-
if (!
|
|
3203
|
+
if (!existsSync14(entry.source)) continue;
|
|
2530
3204
|
try {
|
|
2531
3205
|
const buf = readFileSync5(entry.source, "utf8");
|
|
2532
3206
|
idx.set(entry.source, bytesHashString(buf));
|
|
@@ -2535,11 +3209,12 @@ function buildSourceHashIndex() {
|
|
|
2535
3209
|
}
|
|
2536
3210
|
return idx;
|
|
2537
3211
|
}
|
|
2538
|
-
function checkCatalogInstall(repoRoot, sourceHashes) {
|
|
3212
|
+
function checkCatalogInstall(repoRoot, sourceHashes, profile) {
|
|
2539
3213
|
const rows = [];
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
3214
|
+
const scoped = profile ? filterCatalogByProfile(CATALOG, profile) : CATALOG;
|
|
3215
|
+
for (const entry of scoped) {
|
|
3216
|
+
const target = resolve13(repoRoot, entry.path);
|
|
3217
|
+
if (!existsSync14(target)) {
|
|
2543
3218
|
rows.push(`${entry.id}: missing`);
|
|
2544
3219
|
continue;
|
|
2545
3220
|
}
|
|
@@ -2558,7 +3233,7 @@ function checkCatalogInstall(repoRoot, sourceHashes) {
|
|
|
2558
3233
|
}
|
|
2559
3234
|
const allOk = rows.length === 0 || rows.every((r) => r.endsWith("ok"));
|
|
2560
3235
|
return {
|
|
2561
|
-
name:
|
|
3236
|
+
name: `catalog assets present (${profile ?? "core"})`,
|
|
2562
3237
|
ok: allOk,
|
|
2563
3238
|
detail: rows.join(",")
|
|
2564
3239
|
};
|
|
@@ -2598,8 +3273,8 @@ async function checkManagedHashes(repoRoot, validatedLock) {
|
|
|
2598
3273
|
}
|
|
2599
3274
|
const drift = [];
|
|
2600
3275
|
for (const entry of validatedLock.lock.files ?? []) {
|
|
2601
|
-
const p =
|
|
2602
|
-
if (!
|
|
3276
|
+
const p = resolve13(repoRoot, entry.path);
|
|
3277
|
+
if (!existsSync14(p)) {
|
|
2603
3278
|
drift.push(`missing:${entry.path}`);
|
|
2604
3279
|
continue;
|
|
2605
3280
|
}
|
|
@@ -2683,7 +3358,7 @@ async function runDoctor({ rootPath, profile, json, writeOutput = true }) {
|
|
|
2683
3358
|
checkGh(),
|
|
2684
3359
|
checkGhAuth(),
|
|
2685
3360
|
packageIntegrity,
|
|
2686
|
-
checkCatalogInstall(repoRoot, sourceHashes),
|
|
3361
|
+
checkCatalogInstall(repoRoot, sourceHashes, resolved.profile),
|
|
2687
3362
|
await checkLock(repoRoot),
|
|
2688
3363
|
await checkConfig(repoRoot),
|
|
2689
3364
|
await checkManagedHashes(repoRoot, validatedLock),
|
|
@@ -2723,12 +3398,19 @@ async function runInit(options) {
|
|
|
2723
3398
|
profile: options.profile ?? null,
|
|
2724
3399
|
replaceManaged: false,
|
|
2725
3400
|
forceConfig: Boolean(options.forceConfig),
|
|
2726
|
-
forceRootConfig: Boolean(options.forceRootConfig)
|
|
3401
|
+
forceRootConfig: Boolean(options.forceRootConfig),
|
|
3402
|
+
models: options.models ?? null
|
|
2727
3403
|
});
|
|
2728
3404
|
if (!preview.ok) {
|
|
2729
3405
|
if (preview.error?.kind === "unsupported-lock-schema") {
|
|
2730
3406
|
return emitFailure(5, `unsupported lock schema: ${(preview.error.issues ?? []).join("; ")}`, options.json, "init");
|
|
2731
3407
|
}
|
|
3408
|
+
if (preview.error?.kind === "engineering-models-required") {
|
|
3409
|
+
return emitFailure(2, preview.error.message, options.json, "init");
|
|
3410
|
+
}
|
|
3411
|
+
if (preview.error?.kind === "engineering-approval-required") {
|
|
3412
|
+
return emitFailure(2, preview.error.message, options.json, "init");
|
|
3413
|
+
}
|
|
2732
3414
|
if (preview.error?.kind === "lock-invalid") {
|
|
2733
3415
|
return emitFailure(3, `lock invalid: ${(preview.error.issues ?? []).join("; ")}`, options.json, "init");
|
|
2734
3416
|
}
|
|
@@ -2895,7 +3577,11 @@ async function runDiff(options) {
|
|
|
2895
3577
|
const head = "# opencode-ship diff";
|
|
2896
3578
|
const lines = [head, "", "## Plan"];
|
|
2897
3579
|
for (const op of plan.filter(Boolean)) {
|
|
2898
|
-
const bytesHint =
|
|
3580
|
+
const bytesHint = (
|
|
3581
|
+
/** @type {any} */
|
|
3582
|
+
op.bytes ? `${/** @type {any} */
|
|
3583
|
+
op.bytes.length ?? 0}b` : ""
|
|
3584
|
+
);
|
|
2899
3585
|
lines.push(` - ${op.kind.padEnd(9)} ${op.op} ${op.relPath ?? op.target}${bytesHint ? ` (${bytesHint})` : ""}${op.reason ? ` \u2014 ${op.reason}` : ""}`);
|
|
2900
3586
|
}
|
|
2901
3587
|
if (conflicts.length) {
|
|
@@ -2979,11 +3665,9 @@ function emitFailure2(code, message, json, command) {
|
|
|
2979
3665
|
}
|
|
2980
3666
|
|
|
2981
3667
|
// src/installer/commands/uninstall.js
|
|
2982
|
-
import { unlink as
|
|
2983
|
-
import { existsSync as existsSync13 } from "node:fs";
|
|
2984
|
-
import { resolve as resolve12 } from "node:path";
|
|
3668
|
+
import { unlink as unlink4 } from "node:fs/promises";
|
|
2985
3669
|
async function runUninstall(options) {
|
|
2986
|
-
const preview = await previewUninstall({ rootPath: options.rootPath
|
|
3670
|
+
const preview = await previewUninstall({ rootPath: options.rootPath });
|
|
2987
3671
|
if (!preview.ok) {
|
|
2988
3672
|
if (preview.error?.kind === "unsupported-lock-schema") {
|
|
2989
3673
|
return emitFailure3(5, `unsupported lock schema: ${(preview.error.issues ?? []).join("; ")}`, options.json);
|
|
@@ -2994,6 +3678,16 @@ async function runUninstall(options) {
|
|
|
2994
3678
|
return emitFailure3(2, preview.error?.kind ?? "invalid-project", options.json);
|
|
2995
3679
|
}
|
|
2996
3680
|
const { repoRoot, plan, conflicts, summary } = preview;
|
|
3681
|
+
if (options.purgeConfig) {
|
|
3682
|
+
const cfg = configPath(repoRoot);
|
|
3683
|
+
plan.push({
|
|
3684
|
+
op: "file",
|
|
3685
|
+
kind: "delete",
|
|
3686
|
+
target: cfg,
|
|
3687
|
+
relPath: ".opencode/ship.config.json",
|
|
3688
|
+
reason: "purge user-owned ship.config.json"
|
|
3689
|
+
});
|
|
3690
|
+
}
|
|
2997
3691
|
if (conflicts.length > 0) {
|
|
2998
3692
|
return emitReport(plan, conflicts, summary, options.json, 3, ["modified managed files; refusing to delete"]);
|
|
2999
3693
|
}
|
|
@@ -3001,11 +3695,6 @@ async function runUninstall(options) {
|
|
|
3001
3695
|
if (!tx.ok) {
|
|
3002
3696
|
return emitFailure3(4, tx.error?.message ?? "transaction failure", options.json);
|
|
3003
3697
|
}
|
|
3004
|
-
const lockPath2 = resolve12(repoRoot, ".opencode", "ship.lock.json");
|
|
3005
|
-
if (existsSync13(lockPath2)) await unlink3(lockPath2).catch(() => null);
|
|
3006
|
-
if (options.purgeConfig) {
|
|
3007
|
-
await unlink3(configPath(repoRoot)).catch(() => null);
|
|
3008
|
-
}
|
|
3009
3698
|
return emitReport(plan, [], summary, options.json, 0, [tx.recovered ? "journal recovered before uninstall" : ""].filter(Boolean));
|
|
3010
3699
|
}
|
|
3011
3700
|
function emitFailure3(code, message, json) {
|
|
@@ -3083,15 +3772,37 @@ ${helpText()}`);
|
|
|
3083
3772
|
const options = parsed.options ?? {};
|
|
3084
3773
|
const opts = options;
|
|
3085
3774
|
const profile = opts.profile ?? null;
|
|
3775
|
+
const models = {
|
|
3776
|
+
planner: opts.plannerModel ?? null,
|
|
3777
|
+
builder: opts.builderModel ?? null,
|
|
3778
|
+
finalReviewer: opts.finalReviewerModel ?? null
|
|
3779
|
+
};
|
|
3780
|
+
const hasModels = Boolean(models.planner || models.builder || models.finalReviewer);
|
|
3086
3781
|
switch (parsed.command) {
|
|
3087
3782
|
case "init":
|
|
3088
|
-
await runInit({
|
|
3783
|
+
await runInit({
|
|
3784
|
+
json: !!opts.json,
|
|
3785
|
+
rootPath: opts.rootPath,
|
|
3786
|
+
profile,
|
|
3787
|
+
forceConfig: !!opts.forceConfig,
|
|
3788
|
+
forceRootConfig: !!opts.forceRootConfig,
|
|
3789
|
+
strictDoctor: !!opts.strictDoctor,
|
|
3790
|
+
models: hasModels ? models : null
|
|
3791
|
+
});
|
|
3089
3792
|
return;
|
|
3090
3793
|
case "diff":
|
|
3091
3794
|
await runDiff({ json: !!opts.json, rootPath: opts.rootPath, profile });
|
|
3092
3795
|
return;
|
|
3093
3796
|
case "update":
|
|
3094
|
-
await runUpdate({
|
|
3797
|
+
await runUpdate({
|
|
3798
|
+
json: !!opts.json,
|
|
3799
|
+
rootPath: opts.rootPath,
|
|
3800
|
+
profile,
|
|
3801
|
+
replaceManaged: !!opts.replaceManaged,
|
|
3802
|
+
forceConfig: !!opts.forceConfig,
|
|
3803
|
+
forceRootConfig: !!opts.forceRootConfig,
|
|
3804
|
+
models: hasModels ? models : null
|
|
3805
|
+
});
|
|
3095
3806
|
return;
|
|
3096
3807
|
case "doctor":
|
|
3097
3808
|
await runDoctor({ json: !!opts.json, rootPath: opts.rootPath, profile });
|