@bybrawe/opencode-loop 0.6.5 → 0.6.7
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 +19 -0
- package/README.md +32 -24
- package/examples/OPENCODE2-0.6.6.md +48 -0
- package/examples/OPENCODE2-0.6.7.md +26 -0
- package/package.json +4 -4
- package/scripts/comprehensive-test.mjs +3 -3
- package/scripts/desktop-command-diagnostics-test.mjs +92 -0
- package/scripts/desktop-command-diagnostics.mjs +237 -0
- package/scripts/goal-companion-test.mjs +23 -15
- package/scripts/host-goal-bootstrap-test.mjs +46 -0
- package/scripts/host-goal-bootstrap.mjs +71 -0
- package/scripts/host-goal-coexistence-canary.mjs +11 -2
- package/scripts/install-with-goals.mjs +28 -118
- package/scripts/native-plugin-lifetime-test.mjs +39 -6
- package/scripts/native-release.mjs +3 -3
- package/scripts/native-v2-test.mjs +64 -2
- package/scripts/opencode2-loop-canary.mjs +55 -1
- package/scripts/package-native-smoke.mjs +40 -5
- package/scripts/publish-workflow-test.mjs +7 -3
- package/scripts/state-integrity-test.mjs +47 -0
- package/scripts/state-process-test.mjs +172 -0
- package/src/index.js +217 -11
- package/src/native.js +236 -19
- package/src/server.js +236 -19
- package/src/source/core/state.js +203 -9
- package/src/source/opencode2/events.js +1 -1
- package/src/source/opencode2/native-plugin.js +4 -6
- package/src/source/opencode2/native-runtime.js +30 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased (main only)
|
|
4
|
+
|
|
5
|
+
No unreleased changes yet.
|
|
6
|
+
|
|
7
|
+
## 0.6.7 - 2026-10-09
|
|
8
|
+
|
|
9
|
+
- Recover the native Loop+Goal coexistence canary's cold Windows server bootstrap with bounded read-only session polling and distinguish server startup hangs from actual autonomous-ownership conflicts.
|
|
10
|
+
- Enforce fail-closed corrupt Loop state reads and protect per-session writes across multiple processes on Windows and Linux, including dead-owner recovery.
|
|
11
|
+
- Support manual native V2 compaction through the public `session.compact` capability, verified by matching manual `inputID`, completion and terminal boundaries.
|
|
12
|
+
- Add read-only Windows Desktop command-registry diagnostics for Loop and Goal and parent-versus-child workspace comparisons; the separate GUI `command.updated` refresh is proposed upstream but not published.
|
|
13
|
+
- Align `/loop-help` with the current compaction behavior, rather than advertising the removed V2 limitation.
|
|
14
|
+
|
|
15
|
+
## 0.6.6 - 2026-10-04
|
|
16
|
+
|
|
17
|
+
- Make a normal Loop install/update strictly Loop-only: it no longer installs, detects, refreshes, migrates, or modifies the dedicated `@bybrawe/opencode-goal` package.
|
|
18
|
+
- Omit Loop's legacy experimental `/loop-goal*` commands by default; retain them only behind explicit `--with-loop-goals` compatibility opt-in.
|
|
19
|
+
- Keep `--with-goals` as the explicit combined-install opt-in, while recommending the independently installable OpenCode Goal plugin for durable verified goals.
|
|
20
|
+
- Link Loop and Goal documentation directly so the two packages remain complementary without hidden installer coupling.
|
|
21
|
+
|
|
3
22
|
## 0.6.5 - 2026-10-02
|
|
4
23
|
|
|
5
24
|
- 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.
|
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.7)
|
|
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
|
|
|
@@ -8,7 +8,7 @@ The installer defaults to the native V2 package plugin. It needs no loose local
|
|
|
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
|
|
|
11
|
-
|
|
11
|
+
Automatic compaction remains host-owned. On OpenCode 2 versions exposing public `session.compact`, Loop's explicit native compaction is capability-gated and waits for its own manual `session.compaction.started` input ID, compaction completion, and a session terminal event. Older hosts reject unsupported compaction before scheduling.
|
|
12
12
|
|
|
13
13
|
Prompt/command timers, watch and stop conditions, verification, preflight/postrun, notifications, checkpoints and diagnostic commands use the V2 runtime. Uncertain restart admissions pause for review rather than replaying potentially admitted work. The dedicated `@bybrawe/opencode-goal` plugin owns `/goal`; legacy Loop Goal records are preserved and are not silently reinterpreted as new Goal contracts.
|
|
14
14
|
|
|
@@ -17,52 +17,60 @@ 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.7`.**
|
|
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
|
-
|
|
67
|
+
```text
|
|
68
|
+
/goal status
|
|
57
69
|
```
|
|
58
70
|
|
|
59
|
-
|
|
71
|
+
If both native commands work in terminal TUI but are missing from Windows Desktop, follow the [Windows Desktop command-registry diagnostic guide](docs/OPENCODE2-DESKTOP-WINDOWS.md). Do not replace native commands with legacy Markdown bridges.
|
|
60
72
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
```bash
|
|
64
|
-
npx -y @bybrawe/opencode-loop@latest --loop-only
|
|
65
|
-
```
|
|
73
|
+
On a version containing the diagnostic, run `npx -y @bybrawe/opencode-loop@latest --desktop-diagnostics --workspace "C:\\path\\to\\your\\project"` to check both commands against the currently registered *local* V2 service without printing its credentials. This does not install or modify Goal or Loop.
|
|
66
74
|
|
|
67
75
|
Global npm alternative:
|
|
68
76
|
|
|
@@ -84,7 +92,7 @@ opencode-loop --uninstall
|
|
|
84
92
|
npm uninstall -g @bybrawe/opencode-loop
|
|
85
93
|
```
|
|
86
94
|
|
|
87
|
-
Project Loop state under `.opencode/opencode-loop/` is intentionally preserved by uninstall.
|
|
95
|
+
Loop uninstall never removes the dedicated Goal plugin. Project Loop state under `.opencode/opencode-loop/` is intentionally preserved by uninstall.
|
|
88
96
|
|
|
89
97
|
## The mental model
|
|
90
98
|
|
|
@@ -374,7 +382,7 @@ Recommended choices are to finish/pause the Goal or use a separate session. Adva
|
|
|
374
382
|
|
|
375
383
|
That escape hatch can create competing autonomous turns; use it only when that is intentional.
|
|
376
384
|
|
|
377
|
-
Loop's older experimental `/loop-goal*` commands remain available
|
|
385
|
+
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
386
|
|
|
379
387
|
```bash
|
|
380
388
|
npx -y @bybrawe/opencode-goal@latest
|
|
@@ -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
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# OpenCode Loop 0.6.7
|
|
2
|
+
|
|
3
|
+
OpenCode 2 native reliability and package parity patch, tested alongside **separately installed** OpenCode Goal 1.3.48.
|
|
4
|
+
|
|
5
|
+
## Changes
|
|
6
|
+
|
|
7
|
+
- Protect per-session Loop state across multiple OpenCode processes with file locks, fail-closed corrupt-file reads, crash/dead-owner lock recovery and atomic writes on Windows/Linux.
|
|
8
|
+
- Restore manual native compaction with the public OpenCode 2 `session.compact` capability, requiring its exact manual compaction input ID, completion and terminal. Reject unsupported hosts without corrupting scheduled work.
|
|
9
|
+
- Provide read-only Windows Desktop native `/loop` and `/goal` command-catalog diagnostics and explicit project-directory comparisons. This is **not** a claim that Windows Desktop GUI autocomplete is fixed upstream.
|
|
10
|
+
- Bound Loop+Goal Windows cold-server startup probes and separate unresponsive host bootstrap from real Loop/Goal ownership conflicts.
|
|
11
|
+
- Align native `/loop-help` with the current V2 compaction contract.
|
|
12
|
+
|
|
13
|
+
## Install independently
|
|
14
|
+
|
|
15
|
+
```powershell
|
|
16
|
+
npx -y @bybrawe/opencode-loop@0.6.7
|
|
17
|
+
npx -y @bybrawe/opencode-goal@1.3.48
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
A normal Loop install touches Loop only; it never installs or updates the dedicated Goal package. Legacy `/loop-goal*` commands stay opt-in, and existing state is preserved.
|
|
21
|
+
|
|
22
|
+
## Verification
|
|
23
|
+
|
|
24
|
+
When npm confirms publication, fully restart OpenCode and run `/loop-help`, `/loop-doctor`, `/goal status` in terminal TUI. For Windows Desktop use the [read-only diagnostic](../docs/OPENCODE2-DESKTOP-WINDOWS.md) without adding Markdown slash-command bridges.
|
|
25
|
+
|
|
26
|
+
Required release gates: immutable Goal source, Windows/Ubuntu package regressions, exact-main CI, installed-pair real host and clean npm consumer checks. GitHub tag and release creation require separate repository authorization, which may fail independently of verified npm publication.
|
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.7",
|
|
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": {
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"build:plugin": "bun build src/source/v1.js --outfile=src/index.js --target=bun --format=esm --external=@opencode-ai/plugin/tool && bun build src/source/server.js --outfile=src/server.js --target=bun --format=esm --external=@opencode-ai/plugin/tool && bun build src/source/native.js --outfile=src/native.js --target=bun --format=esm",
|
|
25
25
|
"build:plugin:npm": "npm run build:plugin",
|
|
26
26
|
"prepack": "node --check src/plugin.js && node --check src/v2.js && node --check src/index.js && node --check src/server.js && node --check src/native.js",
|
|
27
|
-
"check": "node --check src/source/v1.js && node --check src/source/core/args.js && node --check src/source/core/continuation.js && node --check src/source/core/schedule-syntax.js && node --check src/source/core/state.js && node --check src/source/core/jobs.js && node --check src/source/core/process.js && node --check src/source/opencode/sdk.js && node --check src/source/opencode/session-context.js && node --check src/source/opencode/command-router.js && node --check src/source/opencode/goal-commands.js && node --check src/source/opencode/loop-commands.js && node --check src/source/opencode/loop-registration.js && node --check src/source/runtime/session-activity.js && node --check src/source/runtime/session-status.js && node --check src/source/runtime/schedule-policy.js && node --check src/source/runtime/scheduler-diagnostics.js && node --check src/source/runtime/companion-goal.js && node --check src/source/runtime/loop-diagnostics.js && node --check src/source/runtime/compaction.js && node --check src/source/runtime/action-dispatch.js && node --check src/source/runtime/network-recovery.js && node --check src/source/runtime/empty-turn.js && node --check src/source/runtime/terminal-guard.js && node --check src/source/runtime/run-finalization.js && node --check src/source/runtime/run-admission.js && node --check src/source/runtime/executor.js && node --check src/source/runtime/scheduler.js && node --check src/source/runtime/goal-prompt.js && node --check src/source/runtime/goal-report.js && node --check src/source/runtime/goal-evidence.js && node --check src/source/runtime/goal-runtime.js && node --check src/source/runtime/goal-policy.js && node --check src/source/runtime/goal-steering.js && node --check src/source/runtime/job-workspace.js && node --check src/source/opencode2/prompt-runtime.js && node --check src/source/opencode2/diagnostics.js && node --check src/source/opencode2/logging.js && node --check src/source/server.js && node --check src/source/legacy-v1.js && node --check src/index.js && node --check src/server.js && node --check scripts/install-node.mjs && node --check scripts/install-with-goals.mjs && node --check scripts/loopd.mjs && node --check scripts/pulse-check.mjs && node --check scripts/install-test.mjs && node --check scripts/goal-companion-test.mjs && node --check scripts/loopd-test.mjs && node --check scripts/smoke-test.mjs && node --check scripts/host-adapter-contract-test.mjs && node --check scripts/server-entry-test.mjs && node --check scripts/command-router-test.mjs && node --check scripts/goal-command-handlers-test.mjs && node --check scripts/loop-command-handlers-test.mjs && node --check scripts/loop-registration-test.mjs && node --check scripts/schedule-syntax-test.mjs && node --check scripts/schedule-policy-test.mjs && node --check scripts/session-activity-test.mjs && node --check scripts/session-status-test.mjs && node --check scripts/session-status-idle-recovery-test.mjs && node --check scripts/session-error-recovery-test.mjs && node --check scripts/empty-turn-recovery-test.mjs && node --check scripts/network-recovery-test.mjs && node --check scripts/compaction-runtime-test.mjs && node --check scripts/executor-runtime-test.mjs && node --check scripts/scheduler-runtime-test.mjs && node --check scripts/goal-runtime-test.mjs && node --check scripts/goal-policy-test.mjs && node --check scripts/goal-steering-test.mjs && node --check scripts/job-workspace-test.mjs && node --check scripts/v2-prompt-runtime-test.mjs && node --check scripts/v2-prompt-interval-test.mjs && node --check scripts/v2-command-runtime-test.mjs && node --check scripts/v2-command-adapter-test.mjs && node --check scripts/v2-diagnostics-test.mjs && node --check scripts/v2-logging-test.mjs && node --check scripts/comprehensive-watchdog.mjs && node --check scripts/comprehensive-test.mjs && node --check scripts/host-loop-canary.mjs && node --check scripts/host-goal-steering-canary.mjs && node --check scripts/publish-workflow-test.mjs && node --check scripts/goal-compaction-safety-test.mjs && node --check src/source/native.js && node --check src/source/opencode2/native-plugin.js && node --check src/source/opencode2/native-runtime.js && node --check src/source/opencode2/native-policy.js && node --check src/native.js && node --check scripts/native-entry-test.mjs && node --check scripts/package-native-smoke.mjs && node --check src/v1.js && node --check scripts/native-cli-io-test.mjs && node --check scripts/native-lifecycle-test.mjs && node --check scripts/native-plugin-lifetime-test.mjs && node --check src/source/core/progress.js && node --check scripts/native-presets-test.mjs && node --check scripts/control-plane-until-test.mjs && node --check src/source/opencode2/session-location.js && node --check scripts/native-session-location-test.mjs && node --check src/plugin.js && node --check src/v2.js",
|
|
28
|
-
"test": "node --test scripts/native-session-location-test.mjs && node --test scripts/control-plane-until-test.mjs && node --test scripts/native-presets-test.mjs && node --test scripts/native-plugin-lifetime-test.mjs && node --test scripts/native-cli-io-test.mjs scripts/native-lifecycle-test.mjs && node scripts/native-entry-test.mjs && node scripts/native-coexistence-test.mjs && node scripts/native-shell-test.mjs && node scripts/native-install-test.mjs && node scripts/native-v2-test.mjs && node scripts/goal-compaction-safety-test.mjs && node scripts/publish-workflow-test.mjs && node scripts/server-entry-test.mjs && node scripts/command-router-test.mjs && node scripts/goal-command-handlers-test.mjs && node scripts/loop-command-handlers-test.mjs && node scripts/loop-registration-test.mjs && node scripts/schedule-syntax-test.mjs && node scripts/schedule-policy-test.mjs && node scripts/session-activity-test.mjs && node scripts/session-status-test.mjs && node scripts/session-status-idle-recovery-test.mjs && node scripts/session-error-recovery-test.mjs && node scripts/empty-turn-recovery-test.mjs && node scripts/network-recovery-test.mjs && node scripts/compaction-runtime-test.mjs && node scripts/action-dispatch-test.mjs && node scripts/run-finalization-test.mjs && node scripts/run-admission-test.mjs && node scripts/executor-runtime-test.mjs && node scripts/scheduler-runtime-test.mjs && node scripts/goal-runtime-test.mjs && node scripts/goal-policy-test.mjs && node scripts/goal-steering-test.mjs && node scripts/job-workspace-test.mjs && node scripts/v2-prompt-runtime-test.mjs && node scripts/v2-prompt-interval-test.mjs && node scripts/v2-command-runtime-test.mjs && node scripts/v2-command-adapter-test.mjs && node scripts/v2-diagnostics-test.mjs && node scripts/v2-logging-test.mjs && node scripts/install-test.mjs && node scripts/goal-companion-test.mjs && node scripts/loopd-test.mjs && node scripts/smoke-test.mjs && node scripts/host-adapter-contract-test.mjs && node scripts/comprehensive-watchdog.mjs",
|
|
27
|
+
"check": "node --check src/source/v1.js && node --check src/source/core/args.js && node --check src/source/core/continuation.js && node --check src/source/core/schedule-syntax.js && node --check src/source/core/state.js && node --check src/source/core/jobs.js && node --check src/source/core/process.js && node --check src/source/opencode/sdk.js && node --check src/source/opencode/session-context.js && node --check src/source/opencode/command-router.js && node --check src/source/opencode/goal-commands.js && node --check src/source/opencode/loop-commands.js && node --check src/source/opencode/loop-registration.js && node --check src/source/runtime/session-activity.js && node --check src/source/runtime/session-status.js && node --check src/source/runtime/schedule-policy.js && node --check src/source/runtime/scheduler-diagnostics.js && node --check src/source/runtime/companion-goal.js && node --check src/source/runtime/loop-diagnostics.js && node --check src/source/runtime/compaction.js && node --check src/source/runtime/action-dispatch.js && node --check src/source/runtime/network-recovery.js && node --check src/source/runtime/empty-turn.js && node --check src/source/runtime/terminal-guard.js && node --check src/source/runtime/run-finalization.js && node --check src/source/runtime/run-admission.js && node --check src/source/runtime/executor.js && node --check src/source/runtime/scheduler.js && node --check src/source/runtime/goal-prompt.js && node --check src/source/runtime/goal-report.js && node --check src/source/runtime/goal-evidence.js && node --check src/source/runtime/goal-runtime.js && node --check src/source/runtime/goal-policy.js && node --check src/source/runtime/goal-steering.js && node --check src/source/runtime/job-workspace.js && node --check src/source/opencode2/prompt-runtime.js && node --check src/source/opencode2/diagnostics.js && node --check src/source/opencode2/logging.js && node --check src/source/server.js && node --check src/source/legacy-v1.js && node --check src/index.js && node --check src/server.js && node --check scripts/install-node.mjs && node --check scripts/install-with-goals.mjs && node --check scripts/loopd.mjs && node --check scripts/pulse-check.mjs && node --check scripts/state-process-test.mjs && node --check scripts/state-integrity-test.mjs && node --check scripts/install-test.mjs && node --check scripts/desktop-command-diagnostics.mjs && node --check scripts/desktop-command-diagnostics-test.mjs && node --check scripts/goal-companion-test.mjs && node --check scripts/loopd-test.mjs && node --check scripts/smoke-test.mjs && node --check scripts/host-adapter-contract-test.mjs && node --check scripts/server-entry-test.mjs && node --check scripts/command-router-test.mjs && node --check scripts/goal-command-handlers-test.mjs && node --check scripts/loop-command-handlers-test.mjs && node --check scripts/loop-registration-test.mjs && node --check scripts/schedule-syntax-test.mjs && node --check scripts/schedule-policy-test.mjs && node --check scripts/session-activity-test.mjs && node --check scripts/session-status-test.mjs && node --check scripts/session-status-idle-recovery-test.mjs && node --check scripts/session-error-recovery-test.mjs && node --check scripts/empty-turn-recovery-test.mjs && node --check scripts/network-recovery-test.mjs && node --check scripts/compaction-runtime-test.mjs && node --check scripts/executor-runtime-test.mjs && node --check scripts/scheduler-runtime-test.mjs && node --check scripts/goal-runtime-test.mjs && node --check scripts/goal-policy-test.mjs && node --check scripts/goal-steering-test.mjs && node --check scripts/job-workspace-test.mjs && node --check scripts/v2-prompt-runtime-test.mjs && node --check scripts/v2-prompt-interval-test.mjs && node --check scripts/v2-command-runtime-test.mjs && node --check scripts/v2-command-adapter-test.mjs && node --check scripts/v2-diagnostics-test.mjs && node --check scripts/v2-logging-test.mjs && node --check scripts/comprehensive-watchdog.mjs && node --check scripts/comprehensive-test.mjs && node --check scripts/host-goal-bootstrap.mjs && node --check scripts/host-goal-bootstrap-test.mjs && node --check scripts/host-loop-canary.mjs && node --check scripts/host-goal-steering-canary.mjs && node --check scripts/publish-workflow-test.mjs && node --check scripts/goal-compaction-safety-test.mjs && node --check src/source/native.js && node --check src/source/opencode2/native-plugin.js && node --check src/source/opencode2/native-runtime.js && node --check src/source/opencode2/native-policy.js && node --check src/native.js && node --check scripts/native-entry-test.mjs && node --check scripts/package-native-smoke.mjs && node --check src/v1.js && node --check scripts/native-cli-io-test.mjs && node --check scripts/native-lifecycle-test.mjs && node --check scripts/native-plugin-lifetime-test.mjs && node --check src/source/core/progress.js && node --check scripts/native-presets-test.mjs && node --check scripts/control-plane-until-test.mjs && node --check src/source/opencode2/session-location.js && node --check scripts/native-session-location-test.mjs && node --check src/plugin.js && node --check src/v2.js",
|
|
28
|
+
"test": "node --test scripts/native-session-location-test.mjs && node --test scripts/control-plane-until-test.mjs && node --test scripts/native-presets-test.mjs && node --test scripts/native-plugin-lifetime-test.mjs && node --test scripts/native-cli-io-test.mjs scripts/native-lifecycle-test.mjs && node scripts/native-entry-test.mjs && node scripts/native-coexistence-test.mjs && node scripts/native-shell-test.mjs && node scripts/native-install-test.mjs && node scripts/native-v2-test.mjs && node scripts/goal-compaction-safety-test.mjs && node scripts/publish-workflow-test.mjs && node scripts/server-entry-test.mjs && node scripts/command-router-test.mjs && node scripts/goal-command-handlers-test.mjs && node scripts/loop-command-handlers-test.mjs && node scripts/loop-registration-test.mjs && node scripts/schedule-syntax-test.mjs && node scripts/schedule-policy-test.mjs && node scripts/session-activity-test.mjs && node scripts/session-status-test.mjs && node scripts/session-status-idle-recovery-test.mjs && node scripts/session-error-recovery-test.mjs && node scripts/empty-turn-recovery-test.mjs && node scripts/network-recovery-test.mjs && node scripts/compaction-runtime-test.mjs && node scripts/action-dispatch-test.mjs && node scripts/run-finalization-test.mjs && node scripts/run-admission-test.mjs && node scripts/executor-runtime-test.mjs && node scripts/scheduler-runtime-test.mjs && node scripts/goal-runtime-test.mjs && node scripts/goal-policy-test.mjs && node scripts/goal-steering-test.mjs && node scripts/job-workspace-test.mjs && node scripts/v2-prompt-runtime-test.mjs && node scripts/v2-prompt-interval-test.mjs && node scripts/v2-command-runtime-test.mjs && node scripts/v2-command-adapter-test.mjs && node scripts/v2-diagnostics-test.mjs && node scripts/v2-logging-test.mjs && node scripts/state-process-test.mjs && node scripts/state-integrity-test.mjs && node scripts/install-test.mjs && node --test scripts/desktop-command-diagnostics-test.mjs && node scripts/goal-companion-test.mjs && node scripts/loopd-test.mjs && node scripts/smoke-test.mjs && node --test scripts/host-goal-bootstrap-test.mjs && node scripts/host-adapter-contract-test.mjs && node scripts/comprehensive-watchdog.mjs",
|
|
29
29
|
"canary:host": "node scripts/host-loop-canary.mjs && node scripts/host-goal-steering-canary.mjs",
|
|
30
30
|
"install:global": "node scripts/install-with-goals.mjs",
|
|
31
31
|
"pack:zip": "node scripts/make-zip.mjs",
|
|
@@ -337,7 +337,7 @@ async function testActionRoutingAndSafety() {
|
|
|
337
337
|
try {
|
|
338
338
|
await h.command("loop-shell", "0s --safe npm run format")
|
|
339
339
|
await h.command("loop-now", "shell")
|
|
340
|
-
await
|
|
340
|
+
await waitForValue(() => h.records.shells[0], 2_000)
|
|
341
341
|
assert.equal(h.records.shells.length, 1)
|
|
342
342
|
assert.equal(h.records.shells[0].command, "npm run format", "safe mode must allow a harmless format script")
|
|
343
343
|
} finally {
|
|
@@ -403,8 +403,8 @@ async function testActionRoutingAndSafety() {
|
|
|
403
403
|
try {
|
|
404
404
|
await h.command("loop-command", "0s /custom-command alpha beta")
|
|
405
405
|
await h.command("loop-now", "command")
|
|
406
|
-
await
|
|
407
|
-
assert.deepEqual(
|
|
406
|
+
const dispatched = await waitForValue(() => h.records.commands[0], 2_000)
|
|
407
|
+
assert.deepEqual(dispatched, {
|
|
408
408
|
command: "custom-command",
|
|
409
409
|
arguments: "alpha beta",
|
|
410
410
|
agent: "build",
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import assert from "node:assert/strict"
|
|
2
|
+
import { createServer } from "node:http"
|
|
3
|
+
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises"
|
|
4
|
+
import { tmpdir } from "node:os"
|
|
5
|
+
import path from "node:path"
|
|
6
|
+
import { spawnSync } from "node:child_process"
|
|
7
|
+
import { fileURLToPath } from "node:url"
|
|
8
|
+
import test from "node:test"
|
|
9
|
+
import { diagnose } from "./desktop-command-diagnostics.mjs"
|
|
10
|
+
|
|
11
|
+
test("Windows Desktop native catalog probe checks both commands but never returns credentials", async () => {
|
|
12
|
+
const home = await mkdtemp(path.join(tmpdir(), "loop-desktop-diagnostic-"))
|
|
13
|
+
const workspace = path.join(home, "project with spaces")
|
|
14
|
+
const desktopWorkspace = path.join(home, "desktop parent project")
|
|
15
|
+
const cfg = path.join(home, ".config", "opencode")
|
|
16
|
+
const state = path.join(home, ".local", "state", "opencode")
|
|
17
|
+
const calls = []
|
|
18
|
+
const password = "private-diagnostic-password"
|
|
19
|
+
const server = createServer((req, res) => {
|
|
20
|
+
calls.push({ url: req.url, directory: req.headers["x-opencode-directory"], auth: req.headers.authorization })
|
|
21
|
+
res.setHeader("Content-Type", "application/json")
|
|
22
|
+
if (req.url === "/api/info") return res.end(JSON.stringify({ pid: process.pid, version: "2.0.22" }))
|
|
23
|
+
if (req.url === "/api/command") {
|
|
24
|
+
const native = req.headers["x-opencode-directory"] === desktopWorkspace
|
|
25
|
+
? [{ name: "init" }] : [{ name: "loop" }, { name: "goal" }, { name: "loop-help" }]
|
|
26
|
+
return res.end(JSON.stringify(native))
|
|
27
|
+
}
|
|
28
|
+
res.writeHead(404).end("{}")
|
|
29
|
+
})
|
|
30
|
+
try {
|
|
31
|
+
await mkdir(cfg, { recursive: true })
|
|
32
|
+
await mkdir(state, { recursive: true })
|
|
33
|
+
await mkdir(workspace, { recursive: true })
|
|
34
|
+
await mkdir(desktopWorkspace, { recursive: true })
|
|
35
|
+
await writeFile(path.join(cfg, "opencode.json"), JSON.stringify({
|
|
36
|
+
plugins: ["@bybrawe/opencode-loop@0.6.6", "@bybrawe/opencode-goal@1.3.47"],
|
|
37
|
+
secrets: password,
|
|
38
|
+
}))
|
|
39
|
+
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve))
|
|
40
|
+
const url = "http://127.0.0.1:" + server.address().port
|
|
41
|
+
await writeFile(path.join(state, "service.json"), JSON.stringify({
|
|
42
|
+
url, pid: process.pid, version: "2.0.22", password,
|
|
43
|
+
}))
|
|
44
|
+
const result = await diagnose({ home, workspace, env: {} })
|
|
45
|
+
assert.equal(result.service.status, "ready")
|
|
46
|
+
assert.deepEqual(result.service.commands, { loop: true, goal: true, count: 3 })
|
|
47
|
+
assert.deepEqual(result.config.pins.loop, ["@bybrawe/opencode-loop@0.6.6"])
|
|
48
|
+
assert.deepEqual(result.config.pins.goal, ["@bybrawe/opencode-goal@1.3.47"])
|
|
49
|
+
assert.ok(calls.every((c) => c.directory === workspace))
|
|
50
|
+
assert.ok(calls.every((c) => c.auth === "Basic " + Buffer.from("opencode:" + password).toString("base64")))
|
|
51
|
+
assert.ok(!JSON.stringify(result).includes(password), "no secret may appear in diagnostic output")
|
|
52
|
+
const beforeCompare = calls.length
|
|
53
|
+
const compared = await diagnose({ home, workspace, compareWorkspace: desktopWorkspace, env: {} })
|
|
54
|
+
assert.equal(compared.comparison.workspace, desktopWorkspace)
|
|
55
|
+
assert.equal(compared.comparison.status, "ready")
|
|
56
|
+
assert.deepEqual(compared.comparison.commands, { loop: false, goal: false, count: 1 })
|
|
57
|
+
assert.equal(compared.comparison.matchesPrimary, false, "Desktop parent and terminal child must be comparable")
|
|
58
|
+
assert.ok(calls.slice(beforeCompare).some((entry) =>
|
|
59
|
+
entry.url === "/api/command" && entry.directory === desktopWorkspace &&
|
|
60
|
+
entry.auth === "Basic " + Buffer.from("opencode:" + password).toString("base64")
|
|
61
|
+
), "comparison must use the same authorized local server and the requested Desktop project location")
|
|
62
|
+
assert.ok(!JSON.stringify(compared).includes(password), "comparison never exposes credentials")
|
|
63
|
+
const same = await diagnose({ home, workspace, compareWorkspace: workspace, env: {} })
|
|
64
|
+
assert.equal(same.comparison, undefined, "identical workspace needs no redundant catalog probe")
|
|
65
|
+
const bad = await diagnose({
|
|
66
|
+
home, workspace, env: {}, server: "https://example.com", fetchImpl: () => { throw new Error("must not contact remote") },
|
|
67
|
+
})
|
|
68
|
+
assert.equal(bad.service.status, "refused-nonlocal-or-invalid-url")
|
|
69
|
+
await writeFile(path.join(state, "service.json"), JSON.stringify({ url, pid: process.pid + 5, password }))
|
|
70
|
+
const stale = await diagnose({ home, workspace, env: {} })
|
|
71
|
+
assert.equal(stale.service.status, "stale-service-registration")
|
|
72
|
+
} finally {
|
|
73
|
+
await new Promise((resolve) => server.close(resolve))
|
|
74
|
+
await rm(home, { recursive: true, force: true })
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test("installer desktop diagnosis is read-only and does not invoke Loop or Goal installers", async () => {
|
|
79
|
+
const home = await mkdtemp(path.join(tmpdir(), "loop-desktop-installer-"))
|
|
80
|
+
try {
|
|
81
|
+
const installer = fileURLToPath(new URL("./install-with-goals.mjs", import.meta.url))
|
|
82
|
+
const result = spawnSync(process.execPath, [installer, "--desktop-diagnostics", "--workspace", home, "--json"], {
|
|
83
|
+
encoding: "utf8", timeout: 15_000, env: { ...process.env, OPENCODE_CONFIG_DIR: path.join(home, "no-config"), XDG_STATE_HOME: path.join(home, "no-state") },
|
|
84
|
+
})
|
|
85
|
+
assert.equal(result.status, 0, result.stderr)
|
|
86
|
+
const report = JSON.parse(result.stdout)
|
|
87
|
+
assert.equal(report.service.status, "registration-not-found")
|
|
88
|
+
assert.equal(report.workspace, home)
|
|
89
|
+
assert.equal(await readFile(path.join(home, "no-config", "opencode.json"), "utf8").then(() => true, () => false), false)
|
|
90
|
+
assert.ok(!result.stdout.includes("Installed/updated"), "installer must not run in diagnosis mode")
|
|
91
|
+
} finally { await rm(home, { recursive: true, force: true }) }
|
|
92
|
+
})
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Read-only diagnosis for a Windows OpenCode Desktop/TUI command mismatch.
|
|
3
|
+
// Never publish service.json, config content, authorization or response bodies.
|
|
4
|
+
import { readFile, stat } from "node:fs/promises"
|
|
5
|
+
import { homedir } from "node:os"
|
|
6
|
+
import path from "node:path"
|
|
7
|
+
import process from "node:process"
|
|
8
|
+
import { fileURLToPath } from "node:url"
|
|
9
|
+
|
|
10
|
+
const packageNames = Object.freeze(["loop", "goal"])
|
|
11
|
+
const allowHosts = new Set(["localhost", "127.0.0.1", "[::1]"])
|
|
12
|
+
const knownConfigs = ["opencode.json", "opencode.jsonc", "config.json", "config.jsonc"]
|
|
13
|
+
|
|
14
|
+
async function readSmall(file) {
|
|
15
|
+
try {
|
|
16
|
+
const info = await stat(file)
|
|
17
|
+
if (!info.isFile() || info.size > 1024 * 1024) return undefined
|
|
18
|
+
return await readFile(file, "utf8")
|
|
19
|
+
} catch (error) {
|
|
20
|
+
if (error?.code === "ENOENT") return undefined
|
|
21
|
+
throw error
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function localUrl(raw) {
|
|
26
|
+
if (typeof raw !== "string") return
|
|
27
|
+
let url
|
|
28
|
+
try { url = new URL(raw) } catch { return }
|
|
29
|
+
if (!["http:", "https:"].includes(url.protocol)) return
|
|
30
|
+
if (!allowHosts.has(url.hostname.toLowerCase())) return
|
|
31
|
+
if (url.username || url.password) return
|
|
32
|
+
if ((url.pathname && url.pathname !== "/") || url.search || url.hash) return
|
|
33
|
+
return url.origin
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function presentPins(raw) {
|
|
37
|
+
const result = {}
|
|
38
|
+
for (const name of packageNames) {
|
|
39
|
+
const prefix = "@bybrawe/opencode-" + name + "@"
|
|
40
|
+
const hits = raw.match(new RegExp(prefix.replace(/[.*+?^$\\{\\}()|[\\]\\\\]/g, "\\$&") + "[0-9]+\\.[0-9]+\\.[0-9]+", "g")) ?? []
|
|
41
|
+
result[name] = [...new Set(hits)].sort()
|
|
42
|
+
}
|
|
43
|
+
return result
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function configPins(env, home) {
|
|
47
|
+
const configHome = env.OPENCODE_CONFIG_DIR
|
|
48
|
+
?? path.join(env.XDG_CONFIG_HOME ?? path.join(home, ".config"), "opencode")
|
|
49
|
+
const files = []
|
|
50
|
+
const pins = { loop: [], goal: [] }
|
|
51
|
+
for (const name of knownConfigs) {
|
|
52
|
+
const file = path.join(configHome, name)
|
|
53
|
+
const value = await readSmall(file)
|
|
54
|
+
if (value === undefined) continue
|
|
55
|
+
files.push(file)
|
|
56
|
+
const found = presentPins(value)
|
|
57
|
+
for (const kind of packageNames) pins[kind].push(...found[kind])
|
|
58
|
+
}
|
|
59
|
+
for (const kind of packageNames) pins[kind] = [...new Set(pins[kind])].sort()
|
|
60
|
+
return { directory: configHome, files, pins }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function commandsFrom(json) {
|
|
64
|
+
const body = Array.isArray(json) ? json : Array.isArray(json?.data) ? json.data : undefined
|
|
65
|
+
if (!body) return undefined
|
|
66
|
+
return new Set(body.flatMap((item) => typeof item?.name === "string" ? [item.name] : []))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function request(fetchImpl, base, pathname, headers) {
|
|
70
|
+
try {
|
|
71
|
+
const response = await fetchImpl(base + pathname, {
|
|
72
|
+
method: "GET",
|
|
73
|
+
headers,
|
|
74
|
+
signal: AbortSignal.timeout(5_000),
|
|
75
|
+
})
|
|
76
|
+
if (![200, 201].includes(response.status)) return { status: response.status }
|
|
77
|
+
const json = await response.json()
|
|
78
|
+
return { status: response.status, json }
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return { error: error?.name === "TimeoutError" ? "timeout" : "connection-failed" }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function catalog(fetchImpl, origin, headers) {
|
|
85
|
+
let response = await request(fetchImpl, origin, "/api/command", headers)
|
|
86
|
+
if (response.status === 404) response = await request(fetchImpl, origin, "/command", headers)
|
|
87
|
+
return response
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function diagnose({
|
|
91
|
+
env = process.env, home = homedir(), workspace = process.cwd(),
|
|
92
|
+
compareWorkspace, server, serviceFile, fetchImpl = fetch,
|
|
93
|
+
} = {}) {
|
|
94
|
+
const cwd = path.resolve(workspace)
|
|
95
|
+
const comparisonDirectory = typeof compareWorkspace === "string" && compareWorkspace.trim()
|
|
96
|
+
? path.resolve(compareWorkspace) : undefined
|
|
97
|
+
const config = await configPins(env, home)
|
|
98
|
+
const registeredFile = serviceFile ?? path.join(
|
|
99
|
+
env.XDG_STATE_HOME ?? path.join(home, ".local", "state"), "opencode", "service.json",
|
|
100
|
+
)
|
|
101
|
+
const output = { workspace: cwd, config, service: { source: server ? "explicit" : "registration", status: "unavailable" } }
|
|
102
|
+
let url = server
|
|
103
|
+
let password = env.OPENCODE_PASSWORD ?? env.OPENCODE_SERVER_PASSWORD
|
|
104
|
+
let version
|
|
105
|
+
let pid
|
|
106
|
+
if (!server) {
|
|
107
|
+
const raw = await readSmall(registeredFile)
|
|
108
|
+
if (raw === undefined) {
|
|
109
|
+
output.service.status = "registration-not-found"
|
|
110
|
+
return output
|
|
111
|
+
}
|
|
112
|
+
let info
|
|
113
|
+
try { info = JSON.parse(raw) }
|
|
114
|
+
catch {
|
|
115
|
+
output.service.status = "registration-invalid"
|
|
116
|
+
return output
|
|
117
|
+
}
|
|
118
|
+
url = info?.url
|
|
119
|
+
password = info?.password
|
|
120
|
+
version = typeof info?.version === "string" ? info.version : undefined
|
|
121
|
+
pid = Number.isInteger(info?.pid) ? info.pid : undefined
|
|
122
|
+
}
|
|
123
|
+
const origin = localUrl(url)
|
|
124
|
+
if (!origin) {
|
|
125
|
+
output.service.status = "refused-nonlocal-or-invalid-url"
|
|
126
|
+
return output
|
|
127
|
+
}
|
|
128
|
+
output.service.host = new URL(origin).host
|
|
129
|
+
if (version) output.service.registeredVersion = version
|
|
130
|
+
const headers = { "x-opencode-directory": cwd, accept: "application/json" }
|
|
131
|
+
if (typeof password === "string" && password.length) {
|
|
132
|
+
headers.authorization = "Basic " + Buffer.from("opencode:" + password).toString("base64")
|
|
133
|
+
}
|
|
134
|
+
const info = await request(fetchImpl, origin, "/api/info", headers)
|
|
135
|
+
if (info.error) {
|
|
136
|
+
output.service.status = info.error
|
|
137
|
+
return output
|
|
138
|
+
}
|
|
139
|
+
if (info.status === 401 || info.status === 403) {
|
|
140
|
+
output.service.status = "authorization-failed"
|
|
141
|
+
return output
|
|
142
|
+
}
|
|
143
|
+
if (info.status === 200 && info.json) {
|
|
144
|
+
const hostPID = info.json?.pid
|
|
145
|
+
if (pid !== undefined && hostPID !== undefined && pid !== hostPID) {
|
|
146
|
+
output.service.status = "stale-service-registration"
|
|
147
|
+
return output
|
|
148
|
+
}
|
|
149
|
+
if (typeof info.json.version === "string") output.service.actualVersion = info.json.version
|
|
150
|
+
}
|
|
151
|
+
const list = await catalog(fetchImpl, origin, headers)
|
|
152
|
+
if (list.error) {
|
|
153
|
+
output.service.status = list.error
|
|
154
|
+
return output
|
|
155
|
+
}
|
|
156
|
+
if (list.status === 401 || list.status === 403) {
|
|
157
|
+
output.service.status = "authorization-failed"
|
|
158
|
+
return output
|
|
159
|
+
}
|
|
160
|
+
const names = commandsFrom(list.json)
|
|
161
|
+
if (!names) {
|
|
162
|
+
output.service.status = "command-catalog-unavailable"
|
|
163
|
+
output.service.httpStatus = list.status
|
|
164
|
+
return output
|
|
165
|
+
}
|
|
166
|
+
output.service.status = "ready"
|
|
167
|
+
output.service.commands = { loop: names.has("loop"), goal: names.has("goal"), count: names.size }
|
|
168
|
+
if (comparisonDirectory && comparisonDirectory !== cwd) {
|
|
169
|
+
const comparisonHeaders = { ...headers, "x-opencode-directory": comparisonDirectory }
|
|
170
|
+
const compared = await catalog(fetchImpl, origin, comparisonHeaders)
|
|
171
|
+
const comparedNames = commandsFrom(compared.json)
|
|
172
|
+
output.comparison = {
|
|
173
|
+
workspace: comparisonDirectory,
|
|
174
|
+
status: comparedNames ? "ready" : compared.error || (
|
|
175
|
+
compared.status === 401 || compared.status === 403 ? "authorization-failed" : "command-catalog-unavailable"
|
|
176
|
+
),
|
|
177
|
+
...(comparedNames ? {
|
|
178
|
+
commands: { loop: comparedNames.has("loop"), goal: comparedNames.has("goal"), count: comparedNames.size },
|
|
179
|
+
matchesPrimary: comparedNames.has("loop") === names.has("loop") && comparedNames.has("goal") === names.has("goal"),
|
|
180
|
+
} : {}),
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return output
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function runDesktopDiagnosticCli(args = process.argv.slice(2), options = {}) {
|
|
187
|
+
let workspace = process.cwd(), compareWorkspace, server, json = false
|
|
188
|
+
for (let i = 0; i < args.length; i++) {
|
|
189
|
+
const arg = args[i]
|
|
190
|
+
if (arg === "--help" || arg === "-h") {
|
|
191
|
+
console.log("Usage: opencode-loop --desktop-diagnostics [--workspace PATH] [--compare-workspace PATH] [--server LOCAL_URL] [--json]")
|
|
192
|
+
console.log("Read-only check of both native /loop and /goal registrations; never prints credentials.")
|
|
193
|
+
return 0
|
|
194
|
+
}
|
|
195
|
+
if (arg === "--json") { json = true; continue }
|
|
196
|
+
if (arg === "--workspace" || arg === "--compare-workspace" || arg === "--server") {
|
|
197
|
+
if (!args[i + 1]) { console.error("Missing value for " + arg); return 2 }
|
|
198
|
+
if (arg === "--workspace") workspace = args[++i]
|
|
199
|
+
else if (arg === "--compare-workspace") compareWorkspace = args[++i]
|
|
200
|
+
else server = args[++i]
|
|
201
|
+
continue
|
|
202
|
+
}
|
|
203
|
+
console.error("Unknown Desktop diagnostic option: " + arg)
|
|
204
|
+
return 2
|
|
205
|
+
}
|
|
206
|
+
const result = await diagnose({ ...options, workspace, compareWorkspace, server })
|
|
207
|
+
if (json) console.log(JSON.stringify(result, null, 2))
|
|
208
|
+
else {
|
|
209
|
+
console.log("OpenCode Desktop native command diagnosis (read-only)")
|
|
210
|
+
console.log("Workspace: " + result.workspace)
|
|
211
|
+
console.log("Global config: " + result.config.directory)
|
|
212
|
+
for (const kind of packageNames)
|
|
213
|
+
console.log("Configured " + kind + " spec: " + (result.config.pins[kind].join(", ") || "not found (text scan)"))
|
|
214
|
+
console.log("Local service: " + result.service.status + (result.service.registeredVersion ? " (" + result.service.registeredVersion + ")" : ""))
|
|
215
|
+
if (result.service.commands)
|
|
216
|
+
console.log("Server /loop: " + result.service.commands.loop + " | /goal: " + result.service.commands.goal)
|
|
217
|
+
if (result.comparison) {
|
|
218
|
+
console.log("Compared workspace: " + result.comparison.workspace)
|
|
219
|
+
console.log("Compared catalog: " + result.comparison.status)
|
|
220
|
+
if (result.comparison.commands) {
|
|
221
|
+
console.log("Compared /loop: " + result.comparison.commands.loop + " | /goal: " + result.comparison.commands.goal)
|
|
222
|
+
console.log("Same native command visibility across directories: " + result.comparison.matchesPrimary)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (!result.service.commands)
|
|
226
|
+
console.log("No confirmed local server catalog. Desktop may use a different connection or service.")
|
|
227
|
+
console.log("This checks one local service and directory; it cannot prove which connection Desktop currently uses.")
|
|
228
|
+
}
|
|
229
|
+
return 0
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
|
|
233
|
+
runDesktopDiagnosticCli().then((code) => { process.exitCode = code }).catch((error) => {
|
|
234
|
+
console.error("Desktop diagnostic failed: " + (error?.code || error?.name || "unknown error"))
|
|
235
|
+
process.exitCode = 1
|
|
236
|
+
})
|
|
237
|
+
}
|