@bybrawe/opencode-loop 0.6.2 → 0.6.4
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 +17 -0
- package/README.md +26 -10
- package/examples/OPENCODE2-0.6.3.md +71 -0
- package/examples/OPENCODE2-0.6.4.md +42 -0
- package/package.json +2 -2
- package/scripts/comprehensive-test.mjs +7 -5
- package/scripts/host-goal-coexistence-canary.mjs +27 -11
- package/scripts/install-node.mjs +116 -20
- package/scripts/native-guide-test.mjs +4 -1
- package/scripts/native-install-pair-test.mjs +6 -4
- package/scripts/native-install-test.mjs +40 -6
- package/scripts/native-release.mjs +3 -3
- package/scripts/publish-workflow-test.mjs +7 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.4 - 2026-10-02
|
|
4
|
+
|
|
5
|
+
- 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.
|
|
6
|
+
- Mirror OpenCode 2.0.21 normalization semantics: strings remain strings and valid legacy `[package, options]` tuples become native `{ package, options }` entries in order ahead of existing native registrations.
|
|
7
|
+
- Preserve malformed/unrecognized legacy entries and unrelated provider/model configuration instead of deleting or rewriting user-owned settings.
|
|
8
|
+
- Pin `@opencode/plugin` to exact 2.0.18 so package resolution and CI/release builds cannot drift to a newer SDK with a different peer graph.
|
|
9
|
+
- Replace a 25 ms blocked-shell test race with deterministic state waiting and update Goal+Loop installer regressions for the canonical single-list configuration.
|
|
10
|
+
- Remove the redundant post-publish `npm view` cache check; the retrying direct registry verifier already proves exact version, gitHead and latest tag.
|
|
11
|
+
|
|
12
|
+
## 0.6.3 - 2026-10-02
|
|
13
|
+
|
|
14
|
+
- Fix native OpenCode 2 fresh installs on Windows/Linux by registering the exact published Loop package in plural `plugins` instead of copying a loose global `plugins/opencode-loop` directory.
|
|
15
|
+
- Let OpenCode resolve `@opencode/plugin` from Loop's published production dependency graph, eliminating the reproduced `Cannot find package '@opencode/plugin'` startup failure from the 0.6.1/0.6.2 loose-install layout.
|
|
16
|
+
- Automatically migrate known loose V2 Loop copies on update while preserving project Loop state, unrelated plugin registrations, JSONC comments and configured package options.
|
|
17
|
+
- Add installer regressions for fresh package registration, idempotence and migration of a broken loose V2 copy.
|
|
18
|
+
- Keep the dedicated OpenCode Goal 1.3.42 companion unchanged and retain explicit `--legacy-v1` as the isolated compatibility path.
|
|
19
|
+
|
|
3
20
|
## 0.6.2 - 2026-09-30
|
|
4
21
|
|
|
5
22
|
- Conform the native OpenCode 2 runtime to the migration-guide location/options boundary: project scope comes only from `ctx.location.directory`; plugin `ctx.options` can no longer masquerade as project location.
|
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# OpenCode Loop
|
|
2
2
|
|
|
3
|
-
## Native OpenCode 2 (0.6.
|
|
3
|
+
## Native OpenCode 2 (0.6.4)
|
|
4
4
|
|
|
5
|
-
The npm and
|
|
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
|
|
|
7
|
-
The installer
|
|
7
|
+
The installer defaults to the native V2 package plugin. It needs no loose local V2 copy, V1 SDK, or legacy command files. Valid legacy `plugin` registrations are now persisted into the single native `plugins` list using OpenCode 2.0.21's own normalization semantics, so native installs no longer leave parallel `plugin` and `plugins` blocks. Object options and entry order are preserved, malformed legacy values are left untouched, and unrelated provider/model settings are not rewritten. Existing 0.6.1/0.6.2 loose `plugins/opencode-loop` installs are still removed on update. Use `--legacy-v1` explicitly for the compatibility installation.
|
|
8
8
|
|
|
9
9
|
V2 prompt admission and durable inbox/execution/compaction events own scheduling. Core-generated user-role messages never authorize a session abort. An unfinished dedicated Goal reserves its session, including paused and handed-off states; Loop does not wake or replace that Goal. Soft iteration timeouts do not abort a native model/tool/compaction turn.
|
|
10
10
|
|
|
@@ -17,32 +17,48 @@ 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.4`.**
|
|
21
21
|
|
|
22
22
|
## Install or update
|
|
23
23
|
|
|
24
|
-
Recommended
|
|
24
|
+
### Recommended OpenCode 2 setup
|
|
25
|
+
|
|
26
|
+
Install the dedicated Goal plugin first:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx -y @bybrawe/opencode-goal@latest
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Then install Loop by itself and disable Loop's older experimental Goal command files:
|
|
25
33
|
|
|
26
34
|
```bash
|
|
27
|
-
npx -y @bybrawe/opencode-loop@latest
|
|
35
|
+
npx -y @bybrawe/opencode-loop@latest --loop-only --without-loop-goals
|
|
28
36
|
```
|
|
29
37
|
|
|
30
|
-
|
|
38
|
+
This is the recommended split for OpenCode 2:
|
|
39
|
+
|
|
40
|
+
- **OpenCode Goal** owns `/goal`, durable outcome-driven work, evidence, verification, recovery, and semantic completion.
|
|
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.
|
|
44
|
+
|
|
45
|
+
Run the same two commands again to update both packages. Loop 0.6.4 also canonicalizes valid legacy `plugin` entries into the native `plugins` list; earlier 0.6.1/0.6.2 loose global V2 installs are removed and replaced by the pinned published package. Then **fully restart OpenCode** and verify:
|
|
31
46
|
|
|
32
47
|
```text
|
|
48
|
+
/goal status
|
|
33
49
|
/loop-help
|
|
34
50
|
/loop-doctor
|
|
35
51
|
```
|
|
36
52
|
|
|
37
|
-
|
|
53
|
+
Convenience alternative: let the Loop installer also install/update the dedicated Goal companion:
|
|
38
54
|
|
|
39
55
|
```bash
|
|
40
56
|
npx -y @bybrawe/opencode-loop@latest --with-goals --without-loop-goals
|
|
41
57
|
```
|
|
42
58
|
|
|
43
|
-
|
|
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.
|
|
44
60
|
|
|
45
|
-
Install only Loop
|
|
61
|
+
Install only Loop without touching Goal:
|
|
46
62
|
|
|
47
63
|
```bash
|
|
48
64
|
npx -y @bybrawe/opencode-loop@latest --loop-only
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# OpenCode Loop 0.6.3
|
|
2
|
+
|
|
3
|
+
OpenCode 2 installer dependency-resolution fix, paired with OpenCode Goal 1.3.42.
|
|
4
|
+
|
|
5
|
+
## What changed
|
|
6
|
+
|
|
7
|
+
OpenCode Loop now installs natively as an exact published package registration:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"plugins": [
|
|
12
|
+
"@bybrawe/opencode-loop@0.6.3"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The default V2 installer no longer copies a loose global
|
|
18
|
+
`~/.config/opencode/plugins/opencode-loop` package directory.
|
|
19
|
+
|
|
20
|
+
This fixes the reproduced OpenCode 2.0.21 startup failure:
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
Cannot find package '@opencode/plugin' imported from
|
|
24
|
+
.../.config/opencode/plugins/opencode-loop/index.js
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The V2 entrypoint still uses the official `@opencode/plugin`
|
|
28
|
+
`Plugin.define({ id, setup })` contract. The difference is distribution:
|
|
29
|
+
OpenCode now resolves the entrypoint and `@opencode/plugin` from the same
|
|
30
|
+
published package dependency graph.
|
|
31
|
+
|
|
32
|
+
## Upgrade behavior
|
|
33
|
+
|
|
34
|
+
Running the 0.6.3 installer over 0.6.1/0.6.2 automatically:
|
|
35
|
+
|
|
36
|
+
- adds or pins the exact Loop package in plural `plugins`;
|
|
37
|
+
- removes known loose `plugins/opencode-loop`, `opencode-loop.ts` and
|
|
38
|
+
`opencode-loop.js` copies;
|
|
39
|
+
- preserves unrelated plugin registrations and object options;
|
|
40
|
+
- preserves project state under `.opencode/opencode-loop/`;
|
|
41
|
+
- keeps explicit `--legacy-v1` behavior isolated.
|
|
42
|
+
|
|
43
|
+
## Paired Goal release
|
|
44
|
+
|
|
45
|
+
Publication remains paired with OpenCode Goal 1.3.42 from immutable source
|
|
46
|
+
`1dd58abf1e7c08594c2085883e28acea3dbe0418`.
|
|
47
|
+
|
|
48
|
+
## Verification
|
|
49
|
+
|
|
50
|
+
The release gate runs Windows/Linux installer regressions, package smoke tests,
|
|
51
|
+
exact-main CI, native V2 CI and the production Goal+Loop host pair before npm
|
|
52
|
+
publication.
|
|
53
|
+
|
|
54
|
+
Install or update:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
npx -y @bybrawe/opencode-goal@1.3.42
|
|
58
|
+
npx -y @bybrawe/opencode-loop@0.6.3 --loop-only --without-loop-goals
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Then fully restart OpenCode and verify:
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
/goal status
|
|
65
|
+
/loop-help
|
|
66
|
+
/loop-doctor
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
If OpenCode itself reports that managed service port 49374 is already in use,
|
|
70
|
+
that is a host service collision rather than this plugin dependency bug. Stop or
|
|
71
|
+
reconfigure the OpenCode service before testing the commands.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# OpenCode Loop 0.6.4
|
|
2
|
+
|
|
3
|
+
OpenCode 2 configuration canonicalization patch, paired with OpenCode Goal 1.3.43.
|
|
4
|
+
|
|
5
|
+
## One native plugin list
|
|
6
|
+
|
|
7
|
+
OpenCode 2.0.21's runtime schema uses the plural `plugins` field and its
|
|
8
|
+
normalizer accepts the legacy singular `plugin` field for compatibility. Loop
|
|
9
|
+
now persists the same normalization during a native V2 install.
|
|
10
|
+
|
|
11
|
+
For valid legacy entries:
|
|
12
|
+
|
|
13
|
+
- strings stay strings;
|
|
14
|
+
- `[package, options]` tuples become `{ "package": "...", "options": { ... } }`;
|
|
15
|
+
- migrated legacy entries retain their order ahead of existing native entries;
|
|
16
|
+
- Loop's own registration is de-duplicated and pinned to the exact package version;
|
|
17
|
+
- the old `plugin` property is removed when it becomes empty.
|
|
18
|
+
|
|
19
|
+
Malformed or unrecognized legacy entries are preserved. Loop does not rewrite
|
|
20
|
+
unrelated `provider`, `providers`, `model`, or other user configuration.
|
|
21
|
+
|
|
22
|
+
## Deterministic runtime dependency
|
|
23
|
+
|
|
24
|
+
The production `@opencode/plugin` dependency is pinned to exact 2.0.18. This
|
|
25
|
+
prevents npm dependency drift from silently changing Loop's V2 runtime contract.
|
|
26
|
+
|
|
27
|
+
## Upgrade
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
npx -y @bybrawe/opencode-goal@1.3.43
|
|
31
|
+
npx -y @bybrawe/opencode-loop@0.6.4 --loop-only --without-loop-goals
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Then fully restart OpenCode and verify:
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
/goal status
|
|
38
|
+
/loop-help
|
|
39
|
+
/loop-doctor
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The update preserves Loop project state and user-authored command files.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bybrawe/opencode-loop",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
4
4
|
"description": "Claude Code/Codex style /loop and experimental goal mode for OpenCode: heartbeat scheduler, idle-safe loops, scheduled commands, compact scheduling, verification, checkpoints, and persistent coding goals.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/plugin.js",
|
|
@@ -75,6 +75,6 @@
|
|
|
75
75
|
"access": "public"
|
|
76
76
|
},
|
|
77
77
|
"dependencies": {
|
|
78
|
-
"@opencode/plugin": "
|
|
78
|
+
"@opencode/plugin": "2.0.18"
|
|
79
79
|
}
|
|
80
80
|
}
|
|
@@ -348,12 +348,14 @@ async function testActionRoutingAndSafety() {
|
|
|
348
348
|
try {
|
|
349
349
|
await h.command("loop-shell", "0s --safe Remove-Item -Recurse ./important")
|
|
350
350
|
await h.command("loop-now", "shell")
|
|
351
|
-
await
|
|
351
|
+
const state = await waitForValue(async () => {
|
|
352
|
+
const candidate = await h.readState()
|
|
353
|
+
return candidate.jobs[0]?.paused === true && candidate.jobs[0]?.lastFailureReason === "safe_shell_blocked" ? candidate : undefined
|
|
354
|
+
}, 2_000)
|
|
352
355
|
assert.equal(h.records.shells.length, 0)
|
|
353
|
-
|
|
354
|
-
assert.equal(state
|
|
355
|
-
assert.equal(state
|
|
356
|
-
assert.equal(state.jobs[0].lastFailureReason, "safe_shell_blocked")
|
|
356
|
+
assert.equal(state?.jobs[0]?.paused, true, "a blocked synchronous action must pause instead of retrying forever")
|
|
357
|
+
assert.equal(state?.jobs[0]?.failureCount, 1)
|
|
358
|
+
assert.equal(state?.jobs[0]?.lastFailureReason, "safe_shell_blocked")
|
|
357
359
|
|
|
358
360
|
await h.command("loop-clear")
|
|
359
361
|
await h.command("loop-shell", "0s --safe rm -r -f ./important")
|
|
@@ -10,6 +10,9 @@ import { fileURLToPath, pathToFileURL } from "node:url"
|
|
|
10
10
|
|
|
11
11
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..")
|
|
12
12
|
const isWindows = process.platform === "win32"
|
|
13
|
+
const SERVER_READY_TIMEOUT_MS = isWindows ? 60_000 : 30_000
|
|
14
|
+
const API_BOOTSTRAP_TIMEOUT_MS = isWindows ? 90_000 : 30_000
|
|
15
|
+
const PREWARM_TIMEOUT_MS = isWindows ? 120_000 : 60_000
|
|
13
16
|
const GOAL_OBJECTIVE = "prove dedicated Goal owns continuation over Loop"
|
|
14
17
|
const GOAL_PROMPT_MARKER = "Continue working toward the active OpenCode goal."
|
|
15
18
|
const LOOP_PROMPT_MARKER = "AUTONOMOUS OPENCODE LOOP ITERATION"
|
|
@@ -301,28 +304,41 @@ async function main() {
|
|
|
301
304
|
}
|
|
302
305
|
|
|
303
306
|
try {
|
|
304
|
-
const prewarm = await runOpenCode(["debug", "config"], { cwd: workspace, env, timeoutMs:
|
|
307
|
+
const prewarm = await runOpenCode(["debug", "config"], { cwd: workspace, env, timeoutMs: PREWARM_TIMEOUT_MS })
|
|
305
308
|
assert.match(prewarm.stdout, /\{[\s\S]*\}/, `OpenCode config prewarm returned no JSON\n${prewarm.stdout}\n${prewarm.stderr}`)
|
|
306
309
|
|
|
307
310
|
const port = await reservePort()
|
|
308
311
|
server = spawnOpenCode(["serve", "--hostname", "127.0.0.1", "--port", String(port)], { cwd: workspace, env })
|
|
309
312
|
server.stdout?.on("data", (chunk) => { serverLog = appendLog(serverLog, chunk) })
|
|
310
313
|
server.stderr?.on("data", (chunk) => { serverLog = appendLog(serverLog, chunk) })
|
|
311
|
-
await waitForTcp(port, server, () => serverLog)
|
|
314
|
+
await waitForTcp(port, server, () => serverLog, SERVER_READY_TIMEOUT_MS)
|
|
312
315
|
|
|
313
316
|
const baseURL = `http://127.0.0.1:${port}`
|
|
314
317
|
const directoryQuery = `directory=${encodeURIComponent(workspace)}`
|
|
315
318
|
const api = async (pathname, init = {}) => {
|
|
316
319
|
const separator = pathname.includes("?") ? "&" : "?"
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
320
|
+
const url = `${baseURL}${pathname}${separator}${directoryQuery}`
|
|
321
|
+
const ownTimeout = !init.signal
|
|
322
|
+
const timeoutMs = API_BOOTSTRAP_TIMEOUT_MS
|
|
323
|
+
try {
|
|
324
|
+
const response = await fetch(url, {
|
|
325
|
+
...init,
|
|
326
|
+
headers: { "content-type": "application/json", ...(init.headers ?? {}) },
|
|
327
|
+
signal: init.signal ?? AbortSignal.timeout(timeoutMs),
|
|
328
|
+
})
|
|
329
|
+
const text = await response.text()
|
|
330
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}: ${text}`)
|
|
331
|
+
if (!text) return null
|
|
332
|
+
try { return JSON.parse(text) } catch { return text }
|
|
333
|
+
} catch (error) {
|
|
334
|
+
const method = init.method ?? "GET"
|
|
335
|
+
const boundary = ownTimeout ? ` after ${timeoutMs}ms bootstrap timeout` : " with caller-owned signal"
|
|
336
|
+
throw new Error(
|
|
337
|
+
`OpenCode API ${method} ${pathname} failed${boundary}: ${error?.name ?? "Error"}: ${error?.message ?? error}\n`
|
|
338
|
+
+ `provider=${JSON.stringify(provider.stats)}\nserver log:\n${serverLog}`,
|
|
339
|
+
{ cause: error },
|
|
340
|
+
)
|
|
341
|
+
}
|
|
326
342
|
}
|
|
327
343
|
|
|
328
344
|
const sessionsPayload = await api("/session", { method: "GET" })
|
package/scripts/install-node.mjs
CHANGED
|
@@ -22,7 +22,7 @@ const installerArgs = rawInstallerArgs.filter((arg) => arg !== "--legacy-v1")
|
|
|
22
22
|
const uninstallRequested = installerArgs.length === 1 && ["--uninstall", "uninstall", "--remove"].includes(installerArgs[0] || "")
|
|
23
23
|
|
|
24
24
|
if (installerArgs.includes("--help") || installerArgs.includes("-h")) {
|
|
25
|
-
console.log(`OpenCode Loop installer/updater\n\nUsage:\n opencode-loop\n npx -y @bybrawe/opencode-loop@latest\n npx -y @bybrawe/opencode-loop@latest --uninstall\n\nInstall/update defaults to native OpenCode 2:
|
|
25
|
+
console.log(`OpenCode Loop installer/updater\n\nUsage:\n opencode-loop\n npx -y @bybrawe/opencode-loop@latest\n npx -y @bybrawe/opencode-loop@latest --uninstall\n\nInstall/update defaults to native OpenCode 2: the exact package version is registered in the plural plugins config so OpenCode resolves the package and its V2 runtime dependencies together. No loose local V2 plugin copy, V1 SDK, or legacy command files are needed. Existing npm registrations are pinned to the exact version and migrated to plugins while retaining object options. Use --legacy-v1 to install the compatibility loader and legacy command files explicitly.\nUninstall removes Loop package registrations plus known local plugin/command/agent files while preserving project Loop state.\n\nSet OPENCODE_CONFIG_DIR to target a non-default OpenCode config directory.`)
|
|
26
26
|
process.exit(0)
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -206,6 +206,53 @@ function findRootProperty(source, propertyName) {
|
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
function removeRootProperty(source, propertyName) {
|
|
210
|
+
let index = skipTrivia(source, 0)
|
|
211
|
+
if (source[index] !== "{") throw new Error("OpenCode config must contain one root object")
|
|
212
|
+
index++
|
|
213
|
+
let previousComma
|
|
214
|
+
|
|
215
|
+
while (true) {
|
|
216
|
+
index = skipTrivia(source, index)
|
|
217
|
+
if (source[index] === "}") return source
|
|
218
|
+
|
|
219
|
+
const keyStart = index
|
|
220
|
+
const key = readJsonString(source, keyStart)
|
|
221
|
+
index = skipTrivia(source, key.end)
|
|
222
|
+
if (source[index] !== ":") throw new Error(`expected ':' after config property ${key.value}`)
|
|
223
|
+
const valueStart = skipTrivia(source, index + 1)
|
|
224
|
+
const valueEnd = skipJsonValue(source, valueStart)
|
|
225
|
+
const afterValue = skipTrivia(source, valueEnd)
|
|
226
|
+
const hadComma = source[afterValue] === ","
|
|
227
|
+
|
|
228
|
+
if (key.value === propertyName) {
|
|
229
|
+
if (hadComma) return `${source.slice(0, keyStart)}${source.slice(afterValue + 1)}`
|
|
230
|
+
if (previousComma !== undefined) return `${source.slice(0, previousComma)}${source.slice(valueEnd)}`
|
|
231
|
+
return `${source.slice(0, keyStart)}${source.slice(valueEnd)}`
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (hadComma) {
|
|
235
|
+
previousComma = afterValue
|
|
236
|
+
index = afterValue + 1
|
|
237
|
+
continue
|
|
238
|
+
}
|
|
239
|
+
if (source[afterValue] === "}") return source
|
|
240
|
+
throw new Error(`expected ',' or '}' after config property ${key.value}`)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function migrateLegacyPluginEntry(value) {
|
|
245
|
+
// Mirrors OpenCode 2.0.21 config normalization: strings stay strings and
|
|
246
|
+
// legacy [package, options] tuples become native { package, options } entries.
|
|
247
|
+
if (typeof value === "string") return value
|
|
248
|
+
if (Array.isArray(value)) {
|
|
249
|
+
if (value.length !== 2 || typeof value[0] !== "string" || !value[1] || typeof value[1] !== "object" || Array.isArray(value[1])) return undefined
|
|
250
|
+
return { package: value[0], options: value[1] }
|
|
251
|
+
}
|
|
252
|
+
if (value && typeof value === "object" && !Array.isArray(value) && typeof value.package === "string") return value
|
|
253
|
+
return undefined
|
|
254
|
+
}
|
|
255
|
+
|
|
209
256
|
function formatPluginArray(values, indent, eol) {
|
|
210
257
|
if (!values.length) return "[]"
|
|
211
258
|
const childIndent = `${indent} `
|
|
@@ -263,11 +310,13 @@ async function configurePackagePlugin() {
|
|
|
263
310
|
for (const key of ["plugin", "plugins"]) {
|
|
264
311
|
if (parsed[key] !== undefined && !Array.isArray(parsed[key])) throw new Error(`OpenCode config '${key}' must be an array`)
|
|
265
312
|
}
|
|
313
|
+
|
|
266
314
|
let updated = source
|
|
267
315
|
const own = [...(parsed.plugins || []), ...(parsed.plugin || [])].filter(isPackageSpec)
|
|
268
|
-
if (own.length)
|
|
269
|
-
|
|
270
|
-
|
|
316
|
+
if (own.length) configured = true
|
|
317
|
+
|
|
318
|
+
if (legacyV1) {
|
|
319
|
+
if (own.length) {
|
|
271
320
|
for (const key of ["plugin", "plugins"]) {
|
|
272
321
|
const entries = parsed[key] || []
|
|
273
322
|
const matching = entries.filter(isPackageSpec)
|
|
@@ -275,21 +324,75 @@ async function configurePackagePlugin() {
|
|
|
275
324
|
const pinned = pinRegistration(matching)
|
|
276
325
|
updated = rewriteExistingPluginArray(updated, [...entries.filter((entry) => !isPackageSpec(entry)), pinned], key)
|
|
277
326
|
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
// Persist OpenCode 2.0.21's compatibility normalization so users do not
|
|
330
|
+
// accumulate parallel plugin/plugins blocks. Valid legacy entries move
|
|
331
|
+
// to native plugins in the same order; invalid values stay untouched.
|
|
332
|
+
const legacyEntries = parsed.plugin || []
|
|
333
|
+
const migratedLegacy = []
|
|
334
|
+
const retainedLegacy = []
|
|
335
|
+
for (const entry of legacyEntries.filter((entry) => !isPackageSpec(entry))) {
|
|
336
|
+
const migrated = migrateLegacyPluginEntry(entry)
|
|
337
|
+
if (migrated === undefined) retainedLegacy.push(entry)
|
|
338
|
+
else migratedLegacy.push(migrated)
|
|
339
|
+
}
|
|
340
|
+
if (legacyEntries.length) {
|
|
341
|
+
updated = retainedLegacy.length
|
|
342
|
+
? rewriteExistingPluginArray(updated, retainedLegacy, "plugin")
|
|
343
|
+
: removeRootProperty(updated, "plugin")
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const current = parseJsonc(updated)
|
|
347
|
+
const nativeEntries = current.plugins || []
|
|
348
|
+
if (!Array.isArray(nativeEntries)) throw new Error("OpenCode config 'plugins' must be an array")
|
|
349
|
+
const next = [
|
|
350
|
+
...migratedLegacy,
|
|
351
|
+
...nativeEntries.filter((entry) => !isPackageSpec(entry)),
|
|
352
|
+
...(own.length ? [pinRegistration(own)] : []),
|
|
353
|
+
]
|
|
354
|
+
if (migratedLegacy.length || own.length) {
|
|
355
|
+
updated = current.plugins === undefined
|
|
356
|
+
? appendPluginProperty(updated, next, "plugins")
|
|
357
|
+
: rewriteExistingPluginArray(updated, next, "plugins")
|
|
283
358
|
}
|
|
284
359
|
}
|
|
360
|
+
|
|
285
361
|
parseJsonc(updated)
|
|
286
362
|
plans.push({ target, source, updated })
|
|
287
363
|
} catch (error) {
|
|
288
364
|
if (error?.code !== "ENOENT") throw new Error(`Could not inspect ${target}: ${error.message}`)
|
|
289
365
|
}
|
|
290
366
|
}
|
|
367
|
+
|
|
368
|
+
// Native V2 installs are package registrations, not loose discovered copies.
|
|
369
|
+
// The published package owns @opencode/plugin as a production dependency, so
|
|
370
|
+
// OpenCode resolves the entrypoint and SDK from the same package install.
|
|
371
|
+
// This also migrates broken 0.6.1/0.6.2 loose installs on the next update.
|
|
372
|
+
if (!configured && !legacyV1) {
|
|
373
|
+
if (plans.length) {
|
|
374
|
+
const plan = plans[0]
|
|
375
|
+
const parsed = parseJsonc(plan.updated)
|
|
376
|
+
const entries = parsed.plugins || []
|
|
377
|
+
if (!Array.isArray(entries)) throw new Error("OpenCode config 'plugins' must be an array")
|
|
378
|
+
plan.updated = parsed.plugins === undefined
|
|
379
|
+
? appendPluginProperty(plan.updated, [...entries, packageSpec], "plugins")
|
|
380
|
+
: rewriteExistingPluginArray(plan.updated, [...entries, packageSpec], "plugins")
|
|
381
|
+
parseJsonc(plan.updated)
|
|
382
|
+
} else {
|
|
383
|
+
const target = join(config, "opencode.json")
|
|
384
|
+
const updated = JSON.stringify({
|
|
385
|
+
$schema: "https://opencode.ai/config.json",
|
|
386
|
+
plugins: [packageSpec],
|
|
387
|
+
}, null, 2) + "\n"
|
|
388
|
+
plans.push({ target, source: "", updated })
|
|
389
|
+
}
|
|
390
|
+
configured = true
|
|
391
|
+
}
|
|
392
|
+
|
|
291
393
|
// Validate every candidate before writing any config. Malformed later files
|
|
292
|
-
// must not leave
|
|
394
|
+
// must not leave an earlier file migrated and the plugin half-installed.
|
|
395
|
+
for (const plan of plans) parseJsonc(plan.updated)
|
|
293
396
|
for (const plan of plans) if (plan.source !== plan.updated) await writeFile(plan.target, plan.updated, "utf8")
|
|
294
397
|
return { configured, updatedFiles: plans.filter((plan) => plan.source !== plan.updated).map((plan) => plan.target) }
|
|
295
398
|
}
|
|
@@ -367,14 +470,7 @@ async function installOrUpdate() {
|
|
|
367
470
|
await copyFile(join(root, "src", "index.js"), join(pluginDir, "opencode-loop.ts"))
|
|
368
471
|
await rm(join(pluginDir, "opencode-loop.js"), { force: true })
|
|
369
472
|
} else {
|
|
370
|
-
|
|
371
|
-
// Keep the generated native runtime beside it as a supporting module instead
|
|
372
|
-
// of auto-discovering that implementation object as a second plugin.
|
|
373
|
-
await rm(join(pluginDir, "opencode-loop.ts"), { force: true })
|
|
374
|
-
await rm(join(pluginDir, "opencode-loop.js"), { force: true })
|
|
375
|
-
await mkdir(nativePluginDir, { recursive: true })
|
|
376
|
-
await copyFile(join(root, "src", "v2.js"), join(nativePluginDir, "index.js"))
|
|
377
|
-
await copyFile(join(root, "src", "native.js"), join(nativePluginDir, "native.js"))
|
|
473
|
+
throw new Error("Native OpenCode 2 package registration was not created")
|
|
378
474
|
}
|
|
379
475
|
if (legacyV1) {
|
|
380
476
|
await mkdir(commandDir, { recursive: true })
|
|
@@ -388,11 +484,11 @@ async function installOrUpdate() {
|
|
|
388
484
|
if (packageConfig.configured) {
|
|
389
485
|
const pinResult = packageConfig.updatedFiles.length ? `pinned the config entry to ${packageSpec}` : `the config entry is already pinned to ${packageSpec}`
|
|
390
486
|
console.log(`OpenCode Loop is already configured as a package in ${config}; ${pinResult} and removed the duplicate local plugin copy.`)
|
|
391
|
-
} else console.log(`Installed OpenCode Loop plugin to ${
|
|
487
|
+
} else console.log(`Installed OpenCode Loop compatibility plugin to ${join(pluginDir, "opencode-loop.ts")}`)
|
|
392
488
|
if (legacyV1) {
|
|
393
489
|
console.log(`Installed ${packageName} commands to ${commandDir}`)
|
|
394
490
|
console.log(`Installed ${packageName} local command agent to ${agentDir}`)
|
|
395
|
-
} else console.log("Native OpenCode 2
|
|
491
|
+
} else console.log("Native OpenCode 2 is registered as an exact package plugin; OpenCode resolves @opencode/plugin from the package dependency graph. No loose local plugin copy, legacy command files, or V1 SDK are needed.")
|
|
396
492
|
console.log("Restart OpenCode, then run: /loop-help")
|
|
397
493
|
}
|
|
398
494
|
|
|
@@ -24,7 +24,10 @@ try {
|
|
|
24
24
|
assert.deepEqual(JSON.parse(await readFile(file, "utf8")).plugin, [other, [spec, options]])
|
|
25
25
|
result = run()
|
|
26
26
|
assert.equal(result.status, 0, result.stderr)
|
|
27
|
-
assert.deepEqual(JSON.parse(await readFile(file, "utf8")), {
|
|
27
|
+
assert.deepEqual(JSON.parse(await readFile(file, "utf8")), { plugins: [
|
|
28
|
+
{ package: "other-plugin", options: { enabled: false } },
|
|
29
|
+
{ package: spec, options },
|
|
30
|
+
] })
|
|
28
31
|
const migrated = await readFile(file, "utf8")
|
|
29
32
|
assert.equal(run().status, 0)
|
|
30
33
|
assert.equal(await readFile(file, "utf8"), migrated)
|
|
@@ -47,8 +47,9 @@ try {
|
|
|
47
47
|
run(config, ["--with-goals", "--without-loop-goals"])
|
|
48
48
|
let source = await readFile(path.join(config, "opencode.json"), "utf8")
|
|
49
49
|
let parsed = JSON.parse(source)
|
|
50
|
-
assert.
|
|
51
|
-
assert.equal(parsed.plugins.length,
|
|
50
|
+
assert.equal("plugin" in parsed, false)
|
|
51
|
+
assert.equal(parsed.plugins.length, 3)
|
|
52
|
+
assert.ok(parsed.plugins.includes("other-plugin"))
|
|
52
53
|
assert.deepEqual(parsed.plugins.find((item) => item.package?.startsWith("@bybrawe/opencode-goal@")).options, options)
|
|
53
54
|
assert.deepEqual(parsed.plugins.find((item) => item.package?.startsWith("@bybrawe/opencode-loop@")).options, { quiet: true })
|
|
54
55
|
assert.equal(await readFile(path.join(config, "commands/goal.md"), "utf8"), "Custom Goal command\n")
|
|
@@ -66,8 +67,9 @@ try {
|
|
|
66
67
|
assert.deepEqual((await readFile(targetLog, "utf8")).trim().split('\n').map((line) => JSON.parse(line)), ["2.0.0", "2.0.0", "1.0.0"])
|
|
67
68
|
run(config, ["--uninstall"])
|
|
68
69
|
parsed = JSON.parse(await readFile(path.join(config, "opencode.json"), "utf8"))
|
|
69
|
-
assert.equal(parsed.plugins.length,
|
|
70
|
-
assert.ok(parsed.plugins
|
|
70
|
+
assert.equal(parsed.plugins.length, 2, "Loop uninstall cannot remove the companion or unrelated plugins")
|
|
71
|
+
assert.ok(parsed.plugins.includes("other-plugin"))
|
|
72
|
+
assert.ok(parsed.plugins.some((item) => item?.package?.startsWith("@bybrawe/opencode-goal@")))
|
|
71
73
|
assert.equal(await readFile(state, "utf8"), '{"evidence":"unchanged"}\n')
|
|
72
74
|
console.log("Native Goal+Loop installer pair: dialect, options, idempotence, custom files and uninstall PASS")
|
|
73
75
|
} finally { await rm(temp, { recursive: true, force: true }) }
|
|
@@ -28,22 +28,33 @@ try {
|
|
|
28
28
|
run(fresh)
|
|
29
29
|
const native = await readFile(path.join(root, "src/native.js"), "utf8")
|
|
30
30
|
const v2 = await readFile(path.join(root, "src/v2.js"), "utf8")
|
|
31
|
-
|
|
32
|
-
assert.
|
|
31
|
+
const readConfig = async (directory) => JSON.parse(await readFile(path.join(directory, "opencode.json"), "utf8"))
|
|
32
|
+
assert.deepEqual((await readConfig(fresh)).plugins, [spec])
|
|
33
33
|
assert.match(v2, /from\s+["']@opencode\/plugin["']/)
|
|
34
34
|
assert.match(v2, /Plugin\.define\s*\(/)
|
|
35
35
|
assert.doesNotMatch(native, /(?:from\s*|import\s*\()["']@opencode-ai\/plugin/)
|
|
36
|
+
assert.equal(await exists(path.join(fresh, "plugins/opencode-loop")), false)
|
|
36
37
|
assert.equal(await exists(path.join(fresh, "plugins/opencode-loop.ts")), false)
|
|
37
38
|
assert.equal(await exists(path.join(fresh, "package.json")), false)
|
|
38
39
|
assert.equal(await exists(path.join(fresh, "commands/loop.md")), false)
|
|
39
40
|
assert.equal(await exists(path.join(fresh, "agents/opencode-loop-local.md")), false)
|
|
41
|
+
const freshBefore = await readFile(path.join(fresh, "opencode.json"), "utf8")
|
|
40
42
|
run(fresh)
|
|
43
|
+
assert.equal(await readFile(path.join(fresh, "opencode.json"), "utf8"), freshBefore)
|
|
44
|
+
cases++
|
|
45
|
+
|
|
46
|
+
const brokenLoose = await config("broken-loose")
|
|
47
|
+
await mkdir(path.join(brokenLoose, "plugins/opencode-loop"), { recursive: true })
|
|
48
|
+
await writeFile(path.join(brokenLoose, "plugins/opencode-loop/index.js"), 'import { Plugin } from "@opencode/plugin"\nexport default Plugin.define({ id: "broken", setup() {} })\n')
|
|
49
|
+
await writeFile(path.join(brokenLoose, "plugins/opencode-loop/native.js"), "export default {}\n")
|
|
50
|
+
run(brokenLoose)
|
|
51
|
+
assert.deepEqual((await readConfig(brokenLoose)).plugins, [spec])
|
|
52
|
+
assert.equal(await exists(path.join(brokenLoose, "plugins/opencode-loop")), false, "native update must remove the old loose V2 copy that cannot resolve package dependencies")
|
|
41
53
|
cases++
|
|
42
54
|
|
|
43
55
|
const options = { package: "@bybrawe/opencode-loop@0.5.38", options: { quiet: true, nested: { value: "preserved" } }, enabled: true }
|
|
44
56
|
const obj = await config("object", { plugins: ["other-plugin", options] })
|
|
45
57
|
run(obj)
|
|
46
|
-
const readConfig = async (directory) => JSON.parse(await readFile(path.join(directory, "opencode.json"), "utf8"))
|
|
47
58
|
assert.deepEqual((await readConfig(obj)).plugins, ["other-plugin", { ...options, package: spec }])
|
|
48
59
|
const before = await readFile(path.join(obj, "opencode.json"), "utf8")
|
|
49
60
|
run(obj)
|
|
@@ -53,7 +64,30 @@ try {
|
|
|
53
64
|
|
|
54
65
|
const both = await config("both", { plugin: ["legacy-unrelated", "@bybrawe/opencode-loop@old"], plugins: [options, "native-unrelated"] })
|
|
55
66
|
run(both)
|
|
56
|
-
assert.deepEqual(await readConfig(both), {
|
|
67
|
+
assert.deepEqual(await readConfig(both), { plugins: ["legacy-unrelated", "native-unrelated", { ...options, package: spec }] })
|
|
68
|
+
cases++
|
|
69
|
+
|
|
70
|
+
const userLegacy = await config("user-legacy", {
|
|
71
|
+
$schema: "https://opencode.ai/config.json",
|
|
72
|
+
plugin: [
|
|
73
|
+
"opencode-antigravity-auth@beta",
|
|
74
|
+
"@tarquinen/opencode-dcp@latest",
|
|
75
|
+
"opencode-power-pack@git+https://github.com/waybarrios/opencode-power-pack.git",
|
|
76
|
+
],
|
|
77
|
+
provider: { google: { models: { demo: { name: "Demo" } } } },
|
|
78
|
+
model: "",
|
|
79
|
+
})
|
|
80
|
+
run(userLegacy)
|
|
81
|
+
const canonicalUser = await readConfig(userLegacy)
|
|
82
|
+
assert.equal("plugin" in canonicalUser, false)
|
|
83
|
+
assert.deepEqual(canonicalUser.plugins, [
|
|
84
|
+
"opencode-antigravity-auth@beta",
|
|
85
|
+
"@tarquinen/opencode-dcp@latest",
|
|
86
|
+
"opencode-power-pack@git+https://github.com/waybarrios/opencode-power-pack.git",
|
|
87
|
+
spec,
|
|
88
|
+
])
|
|
89
|
+
assert.deepEqual(canonicalUser.provider, { google: { models: { demo: { name: "Demo" } } } })
|
|
90
|
+
assert.equal(canonicalUser.model, "")
|
|
57
91
|
cases++
|
|
58
92
|
|
|
59
93
|
const jsonc = await config("jsonc", '{\r\n // keep-before\r\n "theme": "url://example",\r\n "plugin": ["@bybrawe/opencode-loop@old"] // keep-after\r\n}\r\n')
|
|
@@ -88,8 +122,8 @@ try {
|
|
|
88
122
|
run(upgrade)
|
|
89
123
|
assert.equal(await exists(path.join(upgrade, "commands/loop.md")), false)
|
|
90
124
|
assert.equal(await readFile(path.join(upgrade, "commands/loop-help.md"), "utf8"), "This is a custom user command; keep it.")
|
|
91
|
-
assert.
|
|
92
|
-
assert.equal(await
|
|
125
|
+
assert.deepEqual((await readConfig(upgrade)).plugins, [spec])
|
|
126
|
+
assert.equal(await exists(path.join(upgrade, "plugins/opencode-loop")), false)
|
|
93
127
|
assert.equal(await exists(path.join(upgrade, "plugins/opencode-loop.ts")), false)
|
|
94
128
|
cases++
|
|
95
129
|
|
|
@@ -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.4", "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.3")
|
|
15
|
+
assert.equal(request.goal?.version, "1.3.43")
|
|
16
16
|
assert.match(request.goal?.sha ?? "", /^[a-f0-9]{40}$/)
|
|
17
17
|
return request
|
|
18
18
|
}
|
|
@@ -6,6 +6,7 @@ import { validateRelease, releaseDecision, assertPublishedSource, waitForPublish
|
|
|
6
6
|
|
|
7
7
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..")
|
|
8
8
|
const workflow = await readFile(path.join(repoRoot, ".github/workflows/publish-npm.yml"), "utf8")
|
|
9
|
+
const nativeV2Workflow = await readFile(path.join(repoRoot, ".github/workflows/native-v2-ci.yml"), "utf8")
|
|
9
10
|
assert.match(workflow, /ref:\s*\$\{\{ github\.sha \}\}/, "checkout must use the immutable trigger SHA")
|
|
10
11
|
assert.doesNotMatch(workflow, /ref:\s*main\b/, "publication cannot checkout moving main")
|
|
11
12
|
assert.match(workflow, /git merge-base --is-ancestor "\$GITHUB_SHA" origin\/main/)
|
|
@@ -23,11 +24,13 @@ assert.match(workflow, /windows-latest/)
|
|
|
23
24
|
assert.match(workflow, /Decide whether npm publish is needed/)
|
|
24
25
|
assert.match(workflow, /steps\.npm-state\.outputs\.publish == 'true'/)
|
|
25
26
|
assert.match(workflow, /for attempt in \{1\.\.60\}/)
|
|
26
|
-
assert.
|
|
27
|
+
assert.doesNotMatch(workflow, /npm view @bybrawe\/opencode-loop@latest/, "direct registry verification already checks latest; do not add a stale npm-view cache gate")
|
|
27
28
|
assert.match(workflow, /native-release\.mjs verify/)
|
|
28
29
|
assert.match(workflow, /node_modules\/@bybrawe\/opencode-loop\/scripts\/opencode2-loop-canary.mjs/)
|
|
29
30
|
assert.match(workflow, /node_modules\/@bybrawe\/opencode-goal\/dist\/server.js/)
|
|
30
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\.18/, "immutable Goal companion must build against its pinned OpenCode 2.0.18 plugin contract")
|
|
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")
|
|
31
34
|
assert.doesNotMatch(workflow, /secrets\.NPM_TOKEN/)
|
|
32
35
|
|
|
33
36
|
const publishIndex = workflow.indexOf("npm publish --access public")
|
|
@@ -41,14 +44,14 @@ const publisher = workflow.slice(workflow.indexOf(" publish:\n"), workflow.inde
|
|
|
41
44
|
assert.doesNotMatch(publisher, /contents:\s*write/, "OIDC publisher must not gain repository mutation privileges")
|
|
42
45
|
|
|
43
46
|
const sha = "a".repeat(40)
|
|
44
|
-
const request = { name: "@bybrawe/opencode-loop", version: "0.6.
|
|
47
|
+
const request = { name: "@bybrawe/opencode-loop", version: "0.6.4", predecessor: "0.6.3", goal: { version: "1.3.43", sha: "b".repeat(40) } }
|
|
45
48
|
const pkg = { name: request.name, version: request.version }
|
|
46
49
|
const env = { GITHUB_REPOSITORY: "ByBrawe/opencode-loop", GITHUB_REF: "refs/heads/main", GITHUB_SHA: sha }
|
|
47
50
|
assert.strictEqual(validateRelease(request, pkg, env), request)
|
|
48
51
|
for (const change of [{ GITHUB_REPOSITORY: "someone/fork" }, { GITHUB_REF: "refs/heads/release" }, { GITHUB_SHA: "main" }]) assert.throws(() => validateRelease(request, pkg, { ...env, ...change }))
|
|
49
52
|
assert.throws(() => validateRelease({ ...request, version: "9.0.0" }, pkg, env))
|
|
50
|
-
assert.throws(() => validateRelease(request, { ...pkg, version: "0.6.
|
|
51
|
-
assert.equal(releaseDecision(404, null, request, sha, "0.6.
|
|
53
|
+
assert.throws(() => validateRelease(request, { ...pkg, version: "0.6.3" }, env))
|
|
54
|
+
assert.equal(releaseDecision(404, null, request, sha, "0.6.3"), true)
|
|
52
55
|
for (const status of [401, 403, 429, 500, 503]) assert.throws(() => releaseDecision(status, null, request, sha, "0.6.0"))
|
|
53
56
|
assert.throws(() => releaseDecision(404, null, request, sha, "0.7.0"))
|
|
54
57
|
const manifest = { ...pkg, gitHead: sha }
|