@bybrawe/opencode-loop 0.6.4 → 0.6.6
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 +14 -0
- package/README.md +29 -25
- package/examples/OPENCODE2-0.6.5.md +42 -0
- package/examples/OPENCODE2-0.6.6.md +48 -0
- package/package.json +3 -3
- package/scripts/goal-companion-test.mjs +23 -15
- package/scripts/install-with-goals.mjs +17 -118
- package/scripts/native-release.mjs +3 -3
- package/scripts/publish-workflow-test.mjs +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.6 - 2026-10-04
|
|
4
|
+
|
|
5
|
+
- Make a normal Loop install/update strictly Loop-only: it no longer installs, detects, refreshes, migrates, or modifies the dedicated `@bybrawe/opencode-goal` package.
|
|
6
|
+
- Omit Loop's legacy experimental `/loop-goal*` commands by default; retain them only behind explicit `--with-loop-goals` compatibility opt-in.
|
|
7
|
+
- Keep `--with-goals` as the explicit combined-install opt-in, while recommending the independently installable OpenCode Goal plugin for durable verified goals.
|
|
8
|
+
- Link Loop and Goal documentation directly so the two packages remain complementary without hidden installer coupling.
|
|
9
|
+
|
|
10
|
+
## 0.6.5 - 2026-10-02
|
|
11
|
+
|
|
12
|
+
- Audit the native OpenCode 2 package contract against `@opencode/plugin@2.0.22` and pin the production SDK/runtime dependency to exact 2.0.22.
|
|
13
|
+
- Preserve the exact OpenCode 2.0.18 native pair as a backwards-compatibility gate while also requiring the moving latest-host pair; the real latest gate passes on OpenCode 2.0.22.
|
|
14
|
+
- Keep Ubuntu and Windows native bundle, regression, package-smoke, real-adapter, V1 peer-compatibility, and Loop+Goal coexistence gates green with the 2.0.22 dependency graph.
|
|
15
|
+
- Pair the release with Goal 1.3.44, which carries the same SDK audit plus persisted execution-ownership recovery and current OpenTUI peer alignment.
|
|
16
|
+
|
|
3
17
|
## 0.6.4 - 2026-10-02
|
|
4
18
|
|
|
5
19
|
- Persist OpenCode 2 native plugin configuration as one canonical plural `plugins` list instead of leaving valid legacy `plugin` entries in a parallel top-level block.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# OpenCode Loop
|
|
2
2
|
|
|
3
|
-
## Native OpenCode 2 (0.6.
|
|
3
|
+
## Native OpenCode 2 (0.6.6)
|
|
4
4
|
|
|
5
5
|
The npm and V2 entrypoints use the official `@opencode/plugin` `Plugin.define()` contract. The installer now registers the exact `@bybrawe/opencode-loop` package in OpenCode's plural `plugins` configuration instead of copying a loose global plugin directory. OpenCode therefore resolves `@opencode/plugin` from Loop's own production dependency graph; explicit `--legacy-v1` remains a separate compatibility path.
|
|
6
6
|
|
|
@@ -17,51 +17,55 @@ Prompt/command timers, watch and stop conditions, verification, preflight/postru
|
|
|
17
17
|
|
|
18
18
|
OpenCode Loop adds `/loop`, scheduled prompt/command/shell jobs, compact scheduling, verification/checkpoints, and the `opencode-loopd` background daemon.
|
|
19
19
|
|
|
20
|
-
> **Current stable release: `0.6.
|
|
20
|
+
> **Current stable release: `0.6.6`.**
|
|
21
21
|
|
|
22
22
|
## Install or update
|
|
23
23
|
|
|
24
24
|
### Recommended OpenCode 2 setup
|
|
25
25
|
|
|
26
|
-
Install
|
|
26
|
+
Install **both packages separately**:
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
|
+
npx -y @bybrawe/opencode-loop@latest
|
|
29
30
|
npx -y @bybrawe/opencode-goal@latest
|
|
30
31
|
```
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
This is the recommended setup. The two installers stay independent:
|
|
34
|
+
|
|
35
|
+
- the Loop command installs or updates **OpenCode Loop only**;
|
|
36
|
+
- the Goal command installs or updates the dedicated **OpenCode Goal** plugin;
|
|
37
|
+
- Loop does **not** install, detect, refresh, migrate, or otherwise modify `@bybrawe/opencode-goal` during a normal install/update;
|
|
38
|
+
- Loop omits/removes its older experimental `/loop-goal*` command files by default.
|
|
39
|
+
|
|
40
|
+
OpenCode Goal owns `/goal`, durable objective state, evidence, verification, restart recovery, and semantic completion. See [OpenCode Goal](https://github.com/ByBrawe/opencode-goal).
|
|
41
|
+
|
|
42
|
+
A combined `--with-goals` path remains only as a compatibility/convenience option; the recommended setup is the two separate commands above:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npx -y @bybrawe/opencode-loop@latest --with-goals
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
If you explicitly need Loop's **legacy experimental** `/loop-goal*` commands for compatibility, opt in with:
|
|
33
49
|
|
|
34
50
|
```bash
|
|
35
|
-
npx -y @bybrawe/opencode-loop@latest --
|
|
51
|
+
npx -y @bybrawe/opencode-loop@latest --with-loop-goals
|
|
36
52
|
```
|
|
37
53
|
|
|
38
|
-
|
|
54
|
+
Those legacy commands are not recommended for new setups; prefer the dedicated Goal plugin instead.
|
|
39
55
|
|
|
40
|
-
-
|
|
41
|
-
- **OpenCode Loop** owns `/loop`, scheduling, repeated prompts/commands/shell work, and `opencode-loopd`.
|
|
42
|
-
- `--loop-only` prevents the Loop installer from installing/updating the Goal companion.
|
|
43
|
-
- `--without-loop-goals` removes only Loop's older experimental `/loop-goal*` command files; it does not remove the dedicated OpenCode Goal plugin.
|
|
56
|
+
`--loop-only` remains as an explicit alias for the new default Loop-only behavior, and `--without-loop-goals` remains as a compatibility alias for the new default clean command surface.
|
|
44
57
|
|
|
45
|
-
Run the
|
|
58
|
+
Run the relevant install command again to update that package. Then **fully restart OpenCode** and verify:
|
|
46
59
|
|
|
47
60
|
```text
|
|
48
|
-
/goal status
|
|
49
61
|
/loop-help
|
|
50
62
|
/loop-doctor
|
|
51
63
|
```
|
|
52
64
|
|
|
53
|
-
|
|
65
|
+
If you installed Goal too:
|
|
54
66
|
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
For new OpenCode 2 installations, the explicit two-command setup above is preferred because ownership is clear and each package can be updated independently.
|
|
60
|
-
|
|
61
|
-
Install only Loop without touching Goal:
|
|
62
|
-
|
|
63
|
-
```bash
|
|
64
|
-
npx -y @bybrawe/opencode-loop@latest --loop-only
|
|
67
|
+
```text
|
|
68
|
+
/goal status
|
|
65
69
|
```
|
|
66
70
|
|
|
67
71
|
Global npm alternative:
|
|
@@ -84,7 +88,7 @@ opencode-loop --uninstall
|
|
|
84
88
|
npm uninstall -g @bybrawe/opencode-loop
|
|
85
89
|
```
|
|
86
90
|
|
|
87
|
-
Project Loop state under `.opencode/opencode-loop/` is intentionally preserved by uninstall.
|
|
91
|
+
Loop uninstall never removes the dedicated Goal plugin. Project Loop state under `.opencode/opencode-loop/` is intentionally preserved by uninstall.
|
|
88
92
|
|
|
89
93
|
## The mental model
|
|
90
94
|
|
|
@@ -374,7 +378,7 @@ Recommended choices are to finish/pause the Goal or use a separate session. Adva
|
|
|
374
378
|
|
|
375
379
|
That escape hatch can create competing autonomous turns; use it only when that is intentional.
|
|
376
380
|
|
|
377
|
-
Loop's older experimental `/loop-goal*` commands remain available
|
|
381
|
+
Loop's older experimental `/loop-goal*` commands are omitted by default. They remain available only as an explicit compatibility opt-in with `--with-loop-goals`. For new strongly verified Goal work, use the separate [OpenCode Goal](https://github.com/ByBrawe/opencode-goal) plugin:
|
|
378
382
|
|
|
379
383
|
```bash
|
|
380
384
|
npx -y @bybrawe/opencode-goal@latest
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# OpenCode Loop 0.6.5
|
|
2
|
+
|
|
3
|
+
OpenCode 2.0.22 SDK-contract patch, paired with OpenCode Goal 1.3.44.
|
|
4
|
+
|
|
5
|
+
## OpenCode 2.0.22 runtime contract
|
|
6
|
+
|
|
7
|
+
Loop's production `@opencode/plugin` dependency is pinned to exact 2.0.22. The
|
|
8
|
+
lockfile carries the matching `@opencode/{ai,client,protocol,schema,util}` runtime
|
|
9
|
+
closure so clean installs and release builds use the same native contract.
|
|
10
|
+
|
|
11
|
+
The moving real-host gate loads Loop main and the Goal companion together on
|
|
12
|
+
OpenCode 2.0.22. The exact OpenCode 2.0.18 native pair remains in CI as a
|
|
13
|
+
backwards-compatibility contract rather than being replaced by the current-host
|
|
14
|
+
check.
|
|
15
|
+
|
|
16
|
+
## Validation
|
|
17
|
+
|
|
18
|
+
The release candidate is gated by:
|
|
19
|
+
|
|
20
|
+
- Ubuntu and Windows native bundle/regression/package-smoke jobs;
|
|
21
|
+
- the real OpenCode 2 adapter;
|
|
22
|
+
- exact OpenCode 2.0.18 Loop+Goal coexistence;
|
|
23
|
+
- latest OpenCode 2.0.22 Loop+Goal coexistence;
|
|
24
|
+
- OpenCode 1 peer-compatibility tests.
|
|
25
|
+
|
|
26
|
+
## Upgrade
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
npx -y @bybrawe/opencode-goal@1.3.44
|
|
30
|
+
npx -y @bybrawe/opencode-loop@0.6.5 --loop-only --without-loop-goals
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Then fully restart OpenCode and verify:
|
|
34
|
+
|
|
35
|
+
```text
|
|
36
|
+
/goal status
|
|
37
|
+
/loop-help
|
|
38
|
+
/loop-doctor
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Loop state, installer canonicalization, V1 compatibility, and the host-owned
|
|
42
|
+
automatic compaction boundary are unchanged by this SDK patch.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# OpenCode Loop 0.6.6
|
|
2
|
+
|
|
3
|
+
Loop-only installation contract patch, paired for coexistence validation with OpenCode Goal 1.3.47.
|
|
4
|
+
|
|
5
|
+
## Default installation
|
|
6
|
+
|
|
7
|
+
A normal Loop install/update now touches **Loop only**:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npx -y @bybrawe/opencode-loop@0.6.6
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
It does not install, detect, refresh, migrate, or modify the dedicated OpenCode Goal package. Loop's older experimental `/loop-goal*` command files are omitted by default.
|
|
14
|
+
|
|
15
|
+
For durable outcome-driven work, install Goal independently:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
npx -y @bybrawe/opencode-goal@1.3.47
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or explicitly opt into a combined install:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npx -y @bybrawe/opencode-loop@0.6.6 --with-goals
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Legacy experimental Loop Goal commands remain available only when explicitly requested:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
npx -y @bybrawe/opencode-loop@0.6.6 --with-loop-goals
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Validation contract
|
|
34
|
+
|
|
35
|
+
The release remains gated by Ubuntu/Windows regressions, production package smoke, the real OpenCode 2 adapter, exact OpenCode 2.0.18 compatibility, current-host coexistence, and immutable Goal companion source verification.
|
|
36
|
+
|
|
37
|
+
After installation, fully restart OpenCode and verify:
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
/loop-help
|
|
41
|
+
/loop-doctor
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
If Goal is installed too:
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
/goal status
|
|
48
|
+
```
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bybrawe/opencode-loop",
|
|
3
|
-
"version": "0.6.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.6.6",
|
|
4
|
+
"description": "Idle-safe /loop scheduling and background automation for OpenCode, with optional legacy Loop Goal compatibility and a separately installable OpenCode Goal companion.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/plugin.js",
|
|
7
7
|
"exports": {
|
|
@@ -75,6 +75,6 @@
|
|
|
75
75
|
"access": "public"
|
|
76
76
|
},
|
|
77
77
|
"dependencies": {
|
|
78
|
-
"@opencode/plugin": "2.0.
|
|
78
|
+
"@opencode/plugin": "2.0.22"
|
|
79
79
|
}
|
|
80
80
|
}
|
|
@@ -94,8 +94,8 @@ try {
|
|
|
94
94
|
const absentLog = path.join(temporaryRoot, "absent.log")
|
|
95
95
|
const absent = await runInstaller(absentConfig, [], { FAKE_NPM_LOG: absentLog })
|
|
96
96
|
assert.equal(absent.code, 0, absent.stderr)
|
|
97
|
-
assert.deepEqual(await calls(absentLog), [], "Loop
|
|
98
|
-
assert.
|
|
97
|
+
assert.deepEqual(await calls(absentLog), [], "default Loop installs must not silently add or update Goal")
|
|
98
|
+
assert.deepEqual(await loopGoalCommands(absentConfig), [], "legacy Loop Goal commands must be omitted by default")
|
|
99
99
|
|
|
100
100
|
const withoutLoopGoalsConfig = path.join(temporaryRoot, "without-loop-goals")
|
|
101
101
|
const withoutLoopGoalsLog = path.join(temporaryRoot, "without-loop-goals.log")
|
|
@@ -111,15 +111,15 @@ try {
|
|
|
111
111
|
await writeConfig(existingConfig, ["other-plugin", "@bybrawe/opencode-goal@1.3.16"])
|
|
112
112
|
const existing = await runInstaller(existingConfig, [], { FAKE_NPM_LOG: existingLog })
|
|
113
113
|
assert.equal(existing.code, 0, existing.stderr)
|
|
114
|
-
assert.deepEqual(await calls(existingLog), [
|
|
115
|
-
assert.match(existing.stdout, /
|
|
114
|
+
assert.deepEqual(await calls(existingLog), [], "default Loop updates must not refresh an installed Goal package")
|
|
115
|
+
assert.match(existing.stdout, /Recommended companion install \(separate command\)/)
|
|
116
116
|
|
|
117
117
|
const localConfig = path.join(temporaryRoot, "local-goal")
|
|
118
118
|
const localLog = path.join(temporaryRoot, "local-goal.log")
|
|
119
119
|
await writeConfig(localConfig, ["./plugins/opencode-goal.ts"])
|
|
120
120
|
const local = await runInstaller(localConfig, [], { FAKE_NPM_LOG: localLog })
|
|
121
121
|
assert.equal(local.code, 0, local.stderr)
|
|
122
|
-
assert.deepEqual(await calls(localLog), [
|
|
122
|
+
assert.deepEqual(await calls(localLog), [], "default Loop updates must not migrate or refresh legacy Goal installs")
|
|
123
123
|
|
|
124
124
|
const managedCommandConfig = path.join(temporaryRoot, "managed-command")
|
|
125
125
|
const managedCommandLog = path.join(temporaryRoot, "managed-command.log")
|
|
@@ -127,7 +127,7 @@ try {
|
|
|
127
127
|
await fs.writeFile(path.join(managedCommandConfig, "commands", "goal.md"), "<!-- managed-by:@bybrawe/opencode-goal -->\n", "utf8")
|
|
128
128
|
const managedCommand = await runInstaller(managedCommandConfig, [], { FAKE_NPM_LOG: managedCommandLog })
|
|
129
129
|
assert.equal(managedCommand.code, 0, managedCommand.stderr)
|
|
130
|
-
assert.deepEqual(await calls(managedCommandLog), [
|
|
130
|
+
assert.deepEqual(await calls(managedCommandLog), [], "a managed /goal command must not trigger Goal network work during a default Loop update")
|
|
131
131
|
|
|
132
132
|
const commentedConfig = path.join(temporaryRoot, "comment-only")
|
|
133
133
|
const commentedLog = path.join(temporaryRoot, "comment-only.log")
|
|
@@ -149,21 +149,27 @@ try {
|
|
|
149
149
|
assert.deepEqual(await calls(explicitCleanLog), [expectedGoalExec], "the clean command surface option must compose with --with-goals")
|
|
150
150
|
assert.deepEqual(await loopGoalCommands(explicitCleanConfig), [], "combined install must keep the stronger Goals plugin without legacy Loop Goal command clutter")
|
|
151
151
|
|
|
152
|
+
const legacyGoalConfig = path.join(temporaryRoot, "legacy-loop-goals")
|
|
153
|
+
const legacyGoalLog = path.join(temporaryRoot, "legacy-loop-goals.log")
|
|
154
|
+
const legacyGoal = await runInstaller(legacyGoalConfig, ["--with-loop-goals"], { FAKE_NPM_LOG: legacyGoalLog })
|
|
155
|
+
assert.equal(legacyGoal.code, 0, legacyGoal.stderr)
|
|
156
|
+
assert.deepEqual(await calls(legacyGoalLog), [], "--with-loop-goals must not install the dedicated Goal package")
|
|
157
|
+
assert.ok((await loopGoalCommands(legacyGoalConfig)).length > 0, "--with-loop-goals must explicitly restore the legacy experimental command surface")
|
|
158
|
+
|
|
152
159
|
const optOutConfig = path.join(temporaryRoot, "opt-out")
|
|
153
160
|
const optOutLog = path.join(temporaryRoot, "opt-out.log")
|
|
154
161
|
await writeConfig(optOutConfig, ["@bybrawe/opencode-goal@1.3.16"])
|
|
155
162
|
const optOut = await runInstaller(optOutConfig, ["--loop-only"], { FAKE_NPM_LOG: optOutLog })
|
|
156
163
|
assert.equal(optOut.code, 0, optOut.stderr)
|
|
157
164
|
assert.deepEqual(await calls(optOutLog), [], "--loop-only must suppress companion network work")
|
|
158
|
-
assert.match(optOut.stdout, /
|
|
165
|
+
assert.match(optOut.stdout, /Installed\/updated OpenCode Loop only \(--loop-only\)/)
|
|
159
166
|
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
await writeConfig(
|
|
163
|
-
const
|
|
164
|
-
assert.equal(
|
|
165
|
-
assert.
|
|
166
|
-
assert.match(autoFailure.stderr, /npx -y @bybrawe\/opencode-goal@latest/)
|
|
167
|
+
const untouchedConfig = path.join(temporaryRoot, "untouched-existing-goal")
|
|
168
|
+
const untouchedLog = path.join(temporaryRoot, "untouched-existing-goal.log")
|
|
169
|
+
await writeConfig(untouchedConfig, ["@bybrawe/opencode-goal@1.3.16"])
|
|
170
|
+
const untouched = await runInstaller(untouchedConfig, [], { FAKE_NPM_LOG: untouchedLog, FAKE_NPM_EXIT: "7" })
|
|
171
|
+
assert.equal(untouched.code, 0, untouched.stderr)
|
|
172
|
+
assert.deepEqual(await calls(untouchedLog), [], "default Loop update must perform zero Goal network work even when Goal is already installed")
|
|
167
173
|
|
|
168
174
|
const explicitFailureConfig = path.join(temporaryRoot, "explicit-failure")
|
|
169
175
|
const explicitFailureLog = path.join(temporaryRoot, "explicit-failure.log")
|
|
@@ -181,8 +187,10 @@ try {
|
|
|
181
187
|
assert.match(help.stdout, /OpenCode Loop installer\/updater/)
|
|
182
188
|
assert.match(help.stdout, /--with-goals/)
|
|
183
189
|
assert.match(help.stdout, /--loop-only/)
|
|
190
|
+
assert.match(help.stdout, /--with-loop-goals/)
|
|
184
191
|
assert.match(help.stdout, /--without-loop-goals/)
|
|
185
|
-
assert.match(help.stdout, /
|
|
192
|
+
assert.match(help.stdout, /Default install\/update touches only OpenCode Loop/)
|
|
193
|
+
assert.match(help.stdout, /Loop uninstall never removes Goal/)
|
|
186
194
|
assert.deepEqual(await calls(helpLog), [], "help/version flows must never install companion packages")
|
|
187
195
|
|
|
188
196
|
console.log("OpenCode Loop Goals companion installer test passed")
|
|
@@ -9,118 +9,19 @@ import { fileURLToPath } from "node:url"
|
|
|
9
9
|
const root = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
10
10
|
const loopInstaller = join(root, "scripts", "install-node.mjs")
|
|
11
11
|
const config = process.env.OPENCODE_CONFIG_DIR || join(homedir(), ".config", "opencode")
|
|
12
|
-
const
|
|
13
|
-
const goalPackageName = "@bybrawe/opencode-goal"
|
|
14
|
-
const goalPackageSpec = `${goalPackageName}@latest`
|
|
15
|
-
const managedGoalCommandMarker = "<!-- managed-by:@bybrawe/opencode-goal -->"
|
|
12
|
+
const goalPackageSpec = "@bybrawe/opencode-goal@latest"
|
|
16
13
|
const rawArgs = process.argv.slice(2)
|
|
17
14
|
const withGoals = rawArgs.includes("--with-goals")
|
|
18
15
|
const loopOnly = rawArgs.includes("--loop-only")
|
|
16
|
+
const withLoopGoals = rawArgs.includes("--with-loop-goals")
|
|
19
17
|
const withoutLoopGoals = rawArgs.includes("--without-loop-goals")
|
|
20
|
-
const loopArgs = rawArgs.filter((arg) => !["--with-goals", "--loop-only", "--without-loop-goals"].includes(arg))
|
|
18
|
+
const loopArgs = rawArgs.filter((arg) => !["--with-goals", "--loop-only", "--with-loop-goals", "--without-loop-goals"].includes(arg))
|
|
21
19
|
const commandArgs = loopArgs.filter((arg) => arg !== "--legacy-v1")
|
|
22
20
|
const uninstallRequested = commandArgs.length === 1 && ["--uninstall", "uninstall", "--remove"].includes(commandArgs[0] || "")
|
|
23
21
|
const helpRequested = loopArgs.some((arg) => ["--help", "-h"].includes(arg))
|
|
24
22
|
const versionRequested = loopArgs.some((arg) => ["--version", "-v"].includes(arg))
|
|
25
23
|
const informational = helpRequested || versionRequested
|
|
26
24
|
|
|
27
|
-
function stripJsonComments(input) {
|
|
28
|
-
let output = ""
|
|
29
|
-
let quote = ""
|
|
30
|
-
let escaped = false
|
|
31
|
-
let lineComment = false
|
|
32
|
-
let blockComment = false
|
|
33
|
-
for (let index = 0; index < input.length; index++) {
|
|
34
|
-
const char = input[index]
|
|
35
|
-
const next = input[index + 1]
|
|
36
|
-
if (lineComment) {
|
|
37
|
-
if (char === "\n" || char === "\r") { lineComment = false; output += char }
|
|
38
|
-
continue
|
|
39
|
-
}
|
|
40
|
-
if (blockComment) {
|
|
41
|
-
if (char === "*" && next === "/") { blockComment = false; index++ }
|
|
42
|
-
else if (char === "\n" || char === "\r") output += char
|
|
43
|
-
continue
|
|
44
|
-
}
|
|
45
|
-
if (quote) {
|
|
46
|
-
output += char
|
|
47
|
-
if (escaped) escaped = false
|
|
48
|
-
else if (char === "\\") escaped = true
|
|
49
|
-
else if (char === quote) quote = ""
|
|
50
|
-
continue
|
|
51
|
-
}
|
|
52
|
-
if (char === '"') { quote = char; output += char; continue }
|
|
53
|
-
if (char === "/" && next === "/") { lineComment = true; index++; continue }
|
|
54
|
-
if (char === "/" && next === "*") { blockComment = true; index++; continue }
|
|
55
|
-
output += char
|
|
56
|
-
}
|
|
57
|
-
return output
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function stripTrailingCommas(input) {
|
|
61
|
-
let output = ""
|
|
62
|
-
let quote = ""
|
|
63
|
-
let escaped = false
|
|
64
|
-
for (let index = 0; index < input.length; index++) {
|
|
65
|
-
const char = input[index]
|
|
66
|
-
if (quote) {
|
|
67
|
-
output += char
|
|
68
|
-
if (escaped) escaped = false
|
|
69
|
-
else if (char === "\\") escaped = true
|
|
70
|
-
else if (char === quote) quote = ""
|
|
71
|
-
continue
|
|
72
|
-
}
|
|
73
|
-
if (char === '"') { quote = char; output += char; continue }
|
|
74
|
-
if (char === ",") {
|
|
75
|
-
let lookahead = index + 1
|
|
76
|
-
while (/\s/.test(input[lookahead] || "")) lookahead++
|
|
77
|
-
if (input[lookahead] === "]" || input[lookahead] === "}") continue
|
|
78
|
-
}
|
|
79
|
-
output += char
|
|
80
|
-
}
|
|
81
|
-
return output
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function parseJsonc(input) {
|
|
85
|
-
const parsed = JSON.parse(stripTrailingCommas(stripJsonComments(input)))
|
|
86
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("OpenCode config root must be a JSON object")
|
|
87
|
-
return parsed
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function isGoalPluginSpec(value) {
|
|
91
|
-
if (Array.isArray(value)) value = value[0]
|
|
92
|
-
if (value && typeof value === "object") value = value.package
|
|
93
|
-
if (typeof value !== "string") return false
|
|
94
|
-
const spec = value.trim()
|
|
95
|
-
if (spec === goalPackageName || spec.startsWith(`${goalPackageName}@`)) return true
|
|
96
|
-
const normalized = spec.replaceAll("\\", "/")
|
|
97
|
-
return normalized === "./plugins/opencode-goal.ts"
|
|
98
|
-
|| normalized === "./plugins/opencode-goal.js"
|
|
99
|
-
|| normalized.endsWith("/plugins/opencode-goal.ts")
|
|
100
|
-
|| normalized.endsWith("/plugins/opencode-goal.js")
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async function goalsAlreadyInstalled() {
|
|
104
|
-
for (const name of configCandidates) {
|
|
105
|
-
try {
|
|
106
|
-
const parsed = parseJsonc(await readFile(join(config, name), "utf8"))
|
|
107
|
-
if ([parsed.plugin, parsed.plugins].some((entries) => Array.isArray(entries) && entries.some(isGoalPluginSpec))) return true
|
|
108
|
-
} catch (error) {
|
|
109
|
-
if (error?.code !== "ENOENT") {
|
|
110
|
-
console.warn(`Could not inspect ${join(config, name)} for OpenCode Goals: ${error.message}`)
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
try {
|
|
116
|
-
const command = await readFile(join(config, "commands", "goal.md"), "utf8")
|
|
117
|
-
return command.includes(managedGoalCommandMarker)
|
|
118
|
-
} catch (error) {
|
|
119
|
-
if (error?.code !== "ENOENT") console.warn(`Could not inspect managed /goal command: ${error.message}`)
|
|
120
|
-
}
|
|
121
|
-
return false
|
|
122
|
-
}
|
|
123
|
-
|
|
124
25
|
async function removeLoopGoalCommands() {
|
|
125
26
|
const commandDir = join(config, "commands")
|
|
126
27
|
const names = (await readdir(join(root, "commands")))
|
|
@@ -178,7 +79,7 @@ function statusCode(result) {
|
|
|
178
79
|
}
|
|
179
80
|
|
|
180
81
|
function printCompanionHelp() {
|
|
181
|
-
console.log(`\nOpenCode
|
|
82
|
+
console.log(`\nOpenCode Goal options:\n --with-goals Explicitly install/update @bybrawe/opencode-goal@latest after Loop.\n --loop-only Explicit alias for the default: install/update Loop only and never touch Goal.\n --with-loop-goals Also install Loop's legacy experimental /loop-goal* command files (not recommended).\n --without-loop-goals Compatibility alias for the default: omit/remove legacy /loop-goal* commands.\n\nDefault install/update touches only OpenCode Loop. Recommended setup: install OpenCode Goal separately with: npx -y @bybrawe/opencode-goal@latest. Loop uninstall never removes Goal.`)
|
|
182
83
|
}
|
|
183
84
|
|
|
184
85
|
async function main() {
|
|
@@ -187,6 +88,11 @@ async function main() {
|
|
|
187
88
|
process.exitCode = 2
|
|
188
89
|
return
|
|
189
90
|
}
|
|
91
|
+
if (withLoopGoals && withoutLoopGoals) {
|
|
92
|
+
console.error("Use either --with-loop-goals or --without-loop-goals, not both.")
|
|
93
|
+
process.exitCode = 2
|
|
94
|
+
return
|
|
95
|
+
}
|
|
190
96
|
|
|
191
97
|
const loopResult = runLoopInstaller()
|
|
192
98
|
if (loopResult.error) {
|
|
@@ -206,19 +112,18 @@ async function main() {
|
|
|
206
112
|
}
|
|
207
113
|
if (uninstallRequested) return
|
|
208
114
|
|
|
209
|
-
if (
|
|
115
|
+
if (!withLoopGoals) {
|
|
210
116
|
const removed = await removeLoopGoalCommands()
|
|
211
|
-
|
|
117
|
+
const reason = withoutLoopGoals ? "--without-loop-goals" : "default Loop-only command surface"
|
|
118
|
+
console.log(`Omitted ${removed} packaged experimental /loop-goal* command file(s) (${reason}).`)
|
|
212
119
|
}
|
|
213
120
|
|
|
214
|
-
if (
|
|
215
|
-
console.log("
|
|
121
|
+
if (!withGoals) {
|
|
122
|
+
if (loopOnly) console.log("Installed/updated OpenCode Loop only (--loop-only).")
|
|
123
|
+
else console.log("Installed/updated OpenCode Loop only. Recommended companion install (separate command): npx -y @bybrawe/opencode-goal@latest")
|
|
216
124
|
return
|
|
217
125
|
}
|
|
218
126
|
|
|
219
|
-
const alreadyInstalled = await goalsAlreadyInstalled()
|
|
220
|
-
if (!alreadyInstalled && !withGoals) return
|
|
221
|
-
|
|
222
127
|
const goalResult = runGoalInstaller()
|
|
223
128
|
const goalCode = statusCode(goalResult)
|
|
224
129
|
if (goalCode === 0) {
|
|
@@ -227,14 +132,8 @@ async function main() {
|
|
|
227
132
|
}
|
|
228
133
|
|
|
229
134
|
const detail = goalResult.error?.message || `exit code ${goalCode}`
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
process.exitCode = goalCode || 1
|
|
233
|
-
return
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
console.warn(`OpenCode Loop updated, but the existing OpenCode Goals companion could not be refreshed (${detail}).`)
|
|
237
|
-
console.warn(`Retry later with: npx -y ${goalPackageSpec}`)
|
|
135
|
+
console.error(`OpenCode Goals companion install/update failed (${detail}).`)
|
|
136
|
+
process.exitCode = goalCode || 1
|
|
238
137
|
}
|
|
239
138
|
|
|
240
139
|
main().catch((error) => {
|
|
@@ -7,12 +7,12 @@ export function validateRelease(request, pkg, env) {
|
|
|
7
7
|
assert.equal(env.GITHUB_REPOSITORY, "ByBrawe/opencode-loop", "wrong release repository")
|
|
8
8
|
assert.equal(env.GITHUB_REF, "refs/heads/main", "npm release must come from main")
|
|
9
9
|
assert.match(env.GITHUB_SHA ?? "", /^[a-f0-9]{40}$/, "immutable release SHA required")
|
|
10
|
-
assert.equal(request?.version, "0.6.
|
|
10
|
+
assert.equal(request?.version, "0.6.6", "release version is not authorized")
|
|
11
11
|
assert.equal(request?.name, "@bybrawe/opencode-loop")
|
|
12
12
|
assert.equal(pkg.name, request.name)
|
|
13
13
|
assert.equal(pkg.version, request.version)
|
|
14
|
-
assert.equal(request.predecessor, "0.6.
|
|
15
|
-
assert.equal(request.goal?.version, "1.3.
|
|
14
|
+
assert.equal(request.predecessor, "0.6.5")
|
|
15
|
+
assert.equal(request.goal?.version, "1.3.47")
|
|
16
16
|
assert.match(request.goal?.sha ?? "", /^[a-f0-9]{40}$/)
|
|
17
17
|
return request
|
|
18
18
|
}
|
|
@@ -29,7 +29,7 @@ assert.match(workflow, /native-release\.mjs verify/)
|
|
|
29
29
|
assert.match(workflow, /node_modules\/@bybrawe\/opencode-loop\/scripts\/opencode2-loop-canary.mjs/)
|
|
30
30
|
assert.match(workflow, /node_modules\/@bybrawe\/opencode-goal\/dist\/server.js/)
|
|
31
31
|
assert.match(workflow, /ref: \$\{\{ steps.release.outputs.goal_sha \}\}/)
|
|
32
|
-
assert.match(workflow, /npm install --no-save --legacy-peer-deps @opencode\/plugin@2\.0\.
|
|
32
|
+
assert.match(workflow, /npm install --no-save --legacy-peer-deps @opencode\/plugin@2\.0\.22/, "immutable Goal companion must build against its pinned OpenCode 2.0.22 plugin contract")
|
|
33
33
|
assert.match(nativeV2Workflow, /npm install --no-save --legacy-peer-deps @opencode\/plugin@2\.0\.18/, "Native V2 CI must pin the immutable Goal companion to the same OpenCode 2.0.18 plugin contract")
|
|
34
34
|
assert.doesNotMatch(workflow, /secrets\.NPM_TOKEN/)
|
|
35
35
|
|
|
@@ -44,14 +44,14 @@ const publisher = workflow.slice(workflow.indexOf(" publish:\n"), workflow.inde
|
|
|
44
44
|
assert.doesNotMatch(publisher, /contents:\s*write/, "OIDC publisher must not gain repository mutation privileges")
|
|
45
45
|
|
|
46
46
|
const sha = "a".repeat(40)
|
|
47
|
-
const request = { name: "@bybrawe/opencode-loop", version: "0.6.
|
|
47
|
+
const request = { name: "@bybrawe/opencode-loop", version: "0.6.6", predecessor: "0.6.5", goal: { version: "1.3.47", sha: "b".repeat(40) } }
|
|
48
48
|
const pkg = { name: request.name, version: request.version }
|
|
49
49
|
const env = { GITHUB_REPOSITORY: "ByBrawe/opencode-loop", GITHUB_REF: "refs/heads/main", GITHUB_SHA: sha }
|
|
50
50
|
assert.strictEqual(validateRelease(request, pkg, env), request)
|
|
51
51
|
for (const change of [{ GITHUB_REPOSITORY: "someone/fork" }, { GITHUB_REF: "refs/heads/release" }, { GITHUB_SHA: "main" }]) assert.throws(() => validateRelease(request, pkg, { ...env, ...change }))
|
|
52
52
|
assert.throws(() => validateRelease({ ...request, version: "9.0.0" }, pkg, env))
|
|
53
|
-
assert.throws(() => validateRelease(request, { ...pkg, version: "0.6.
|
|
54
|
-
assert.equal(releaseDecision(404, null, request, sha, "0.6.
|
|
53
|
+
assert.throws(() => validateRelease(request, { ...pkg, version: "0.6.5" }, env))
|
|
54
|
+
assert.equal(releaseDecision(404, null, request, sha, "0.6.5"), true)
|
|
55
55
|
for (const status of [401, 403, 429, 500, 503]) assert.throws(() => releaseDecision(status, null, request, sha, "0.6.0"))
|
|
56
56
|
assert.throws(() => releaseDecision(404, null, request, sha, "0.7.0"))
|
|
57
57
|
const manifest = { ...pkg, gitHead: sha }
|