Bug found via integration test on MacBook Pro (macOS /tmp → /private/tmp):
`import.meta.url === \`file://\${process.argv[1]}\`` evaluates false when
the install path traverses a symlink, because import.meta.url is canonicalised
but process.argv[1] is not. Result: ./ocp doctor (and ./ocp update via
upgrade.mjs) exit silently with code 0 and no output, instead of running.
Fix: use fileURLToPath + realpathSync on both sides of the comparison.
Affects any install at a symlinked path (/tmp, NFS mounts, /var/ paths,
docker bind mounts, etc.). Normal ~/ocp installs were unaffected.
No cli.js citation needed: this is OCP-internal CLI dispatch with no
corresponding cli.js operation.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
3 micro-fixes on 48e9408:
1. Remove unused mkdirSync import
2. snapshot-not-found error message hints "must be inside ~/.ocp/upgrade-snapshot-*"
3. runFreshInstall failure now includes e.stderr (or e.message fallback) in the
thrown error and steps[].error so non-interactive callers see the actual reason
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implements the two missing branches of runUpgrade dispatcher:
- runFreshInstall: gated by --yes, runs doctor.next_action.ai_executable
steps in order, fails fast on first error, attaches steps[] to thrown
errors. Accepts mockExec for unit tests.
- runRollback: locates latest or named snapshot in ~/.ocp/, reads
from-commit.txt, restores plist + db + admin-key + service file (with
per-file warn lines on copy failure), git-checkouts the from-commit,
npm installs at that revision, restarts the service. --list shows all
snapshots; --dry-run prints the plan without mutation.
Both paths use the same exec() error-wrap pattern as runFullUpgrade
(stderr capture, phases attached to thrown errors, restart heads-up).
CLI entrypoint extended to parse --rollback / --list / --target / and
optional positional snapshot path after --rollback.
6 unit tests cover: --yes gate, fresh_install ai_executable run,
--rollback --list, no-snapshots error, --rollback --dry-run, mock-exec
restore.
No cli.js citation needed: this is OCP-internal upgrade tooling with
no corresponding cli.js operation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5 issues raised by code-quality reviewer on c12013a:
A. exec() wrapper now captures stderr from execSync failures and
re-throws with `phase X failed: <stderr>` instead of the terse
"Command failed: ..." default. Operators see the actual git/npm
error.
B. runFullUpgrade body wrapped in try/catch; any error after phase 2
(snapshot written) carries snapshotPath + phases + hint pointing
at `ocp update --rollback`. Aligns with the post-flight failure
pattern.
C. CLI entrypoint now prints snapshotPath + hint on error.
Plus minor:
- snapshot.mjs tryCopy logs a [snapshot] warn line instead of silently
swallowing copy errors (e.g. permission-denied admin-key)
- heads-up window 1s → 3s, more operable per the policy intent
- opts.yes intent comment added (Bundle 3 will use)
One regression test added.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements the upgrade dispatcher (noop / dry-run / light delegation /
full path) and the snapshot writer/reader/list module. Full path snapshots
plist + db + admin-key + openclaw.json before mutating, runs the 6 phases
(pre-flight, snapshot, fetch+install, reconfigure, restart, post-flight),
and emits a heads-up before launchctl bootout per
notify_before_prod_service_restart.md policy.
mockExec/mockDoctor injection points let tests verify the phase ordering
without touching the real shell. fresh_install + rollback paths are
deferred to Bundle 3.
No cli.js citation needed: this is OCP-internal upgrade tooling with no
corresponding cli.js operation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three issues raised by code-quality reviewer on b65201b:
1. semverCompare returned 0 for unparseable input, causing fromSupported=true
and kind=noop for an install with unreadable package.json. Now treats
unparseable currentVersion as fresh_install candidate.
2. mockHealth: { status: 200, body: null } routed to fix_oauth (because
health.body?.auth?.ok was undefined → falsy). 200 with empty body is
server-broken, not OAuth-broken; now routes to fix_service.
3. Removed unused KIND_ENUM declaration (dead code).
Two regression tests added.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements scripts/doctor.mjs with semver-aware path selection
(noop/update/upgrade/fresh_install/fix_oauth/fix_service) and the JSON
contract documented in the design spec.
Service health + OAuth checks integrated; mockable via opts.mockHealth
for unit tests. 8 unit tests cover the kind dispatch tree and the
next_action shape for each kind.
No cli.js citation needed: this is OCP-internal tooling with no
corresponding cli.js operation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(setup): merge plist/systemd env vars instead of overwriting
setup.mjs previously wrote the launchd plist and the Linux systemd unit
with writeFileSync(path, NEW_TEMPLATE_STRING), which silently dropped any
user-customised env vars (CLAUDE_HEARTBEAT_INTERVAL, CLAUDE_CACHE_TTL,
etc.) on every re-setup or upgrade. This change introduces
scripts/lib/plist-merge.mjs which preserves keys present only in the
existing file and lets the template's known keys win. Linux variant uses
the same logic against Environment=KEY=VALUE lines.
Tests added in test-features.mjs cover preserve / override / first-install
for both formats.
No cli.js citation needed: this is OCP-internal installer behaviour with
no corresponding cli.js operation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(setup): plist-merge code-quality follow-up
Three issues raised by the code-quality reviewer on 0fd1838:
1. mergeSystemdEnv now early-returns when the template has no
Environment= anchor, instead of silently dropping preserved lines
(defensive guard for a future template change).
2. Both parsers (parsePlistEnv, parseSystemdEnv) now Buffer-normalise
their input, removing the TypeError-vs-coerce asymmetry.
3. Two idempotency tests added (calling merge twice on the same
input/output pair must be stable).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Two related changes — both repo-hygiene, no behavior impact.
### `.gitignore` (new)
Ignores runtime artifacts that should never be tracked:
- `logs/` and `*.log` — proxy.log, last_send.log, heartbeat-field-check log
- `node_modules/` — dependency cache
- `.env`, `.env.*` — local secrets/config
- `.DS_Store`, `*.swp`, `*~` — editor/OS scratch
`logs/` was previously untracked-but-present in working trees; the new
ignore makes that intent explicit and prevents accidental commits.
### `scripts/heartbeat-field-check.sh` (commit existing untracked file)
This script was authored as a one-shot field-evidence gatherer for the
v3.12.0 SSE heartbeat work (PR #49 / issue #47). It fired successfully on
2026-05-02 09:00 Australia/Brisbane via launchd
(`~/Library/LaunchAgents/dev.ocp.heartbeat-check.plist`) and posted a
summary comment to issue #47.
Useful tooling pattern (one-shot field check + launchd schedule + dry-run
flag), worth keeping under version control rather than letting it die in
an untracked working tree.
Verification: `git status` clean after both adds.
Co-authored-by: dtzp555 <dtzp555@gmail.com>
Adds scripts/sync-openclaw.mjs which reconciles
config.models.providers["claude-local"].models and
config.agents.defaults.models["claude-local/*"] with models.json.
Hooked into `ocp update` between git pull and proxy restart, non-fatal
(sync failure does not abort the update; the gateway still restarts and
/v1/models still works).
Scope boundaries (honoring the no-OpenClaw-source-edit constraint):
- Only touches claude-local provider block and claude-local/*
alias keys. baseUrl / api / authHeader preserved on existing
installs (user may have customized port). All other providers
and top-level config keys untouched.
- Creates a timestamped backup (openclaw.json.bak.<ms>) before every
write. No-op path (already in sync) skips backup.
- Exits 0 with a skip message when ~/.openclaw/openclaw.json does not
exist (non-OpenClaw users).
server.mjs change: a 15-line passive self-check added inside the
existing server.listen() callback. It only reads the OpenClaw config
and emits console.warn on drift. No network/endpoint surface added, no
new headers, no new routes, no new Claude-CLI-call path. Per
ALIGNMENT.md Rule 2 this is not an operation cli.js performs, so no
cli.js:NNNN citation is required. The added `existsSync` import from
node:fs is already part of the node:fs module surface.
Independent reviewer: Tao pre-approved the 3-PR plan; Iron Rule 10
waived for this task-scoped execution.
Co-authored-by: Tao Deng <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>