* fix(upgrade): make snapshot paths Windows-safe
Use a filesystem-safe UTC timestamp for new upgrade snapshot directories while retaining legacy ISO timestamp parsing. Normalize test paths with node:path so the Windows suite exercises the same behavior.
Co-Authored-By: claude-flow <ruv@ruv.net>
* fix(upgrade): sort snapshots by parsed timestamp
Order legacy colon and Windows-safe dash snapshot names chronologically so rollback and retention keep the actual newest snapshot across the migration boundary.
Co-Authored-By: claude-flow <ruv@ruv.net>
---------
Co-authored-by: nyxst4ck <nyxst4ck@users.noreply.github.com>
Co-authored-by: claude-flow <ruv@ruv.net>
Co-authored-by: nyxst4ck <289980115+nyxst4ck@users.noreply.github.com>
* feat(doctor): add ocp doctor with --json + next_action contract
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>
* feat(ocp): wire cmd_doctor into bash CLI; dispatch to scripts/doctor.mjs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(doctor): handle unparseable version + empty health body
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>
* feat(upgrade): add scripts/upgrade.mjs + scripts/lib/snapshot.mjs
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>
* fix(upgrade): error path completeness + observability
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>
* feat(upgrade): fresh-install + rollback paths
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>
* chore(upgrade): nit fixes from Bundle 3 code-quality review
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>
* refactor(ocp): cmd_update dispatches via doctor; --rollback added; light path preserved
cmd_update now calls scripts/doctor.mjs to determine which path to take:
noop → "already at latest" exit 0
update → existing light path (git pull + npm install + restart),
extracted into _cmd_update_light helper to keep the daily
case fast and shell-only
upgrade → exec node scripts/upgrade.mjs (full path with snapshot
+ post-flight)
fresh_install → exec node scripts/upgrade.mjs (gated by --yes)
fix_oauth/fix_service → print error referring user to `ocp doctor`
cmd_update --rollback path: exec node scripts/upgrade.mjs --rollback "$@"
forwards remaining args (--list, --dry-run, optional snapshot path).
cmd_update_help expanded to document new flags.
cmd_update --check fast path is preserved exactly (no doctor call there).
No cli.js citation needed: this is OCP-internal CLI dispatch with no
corresponding cli.js operation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(ocp): forward all args to cmd_update so multi-flag invocations work
Bug found via runtime smoke test:
./ocp update --rollback --list → "no snapshots" (wrong; should list)
Root cause: dispatch was `cmd_update "\${1:-}"` (only first arg). When
user typed `--rollback --list`, cmd_update only received `--rollback`,
the shift left $@ empty, and exec node ... --rollback got no flags.
Other commands using "\${1:-}" don't need multi-arg, but cmd_update now
does (--rollback --list, --rollback --dry-run, --target X --yes, etc.).
Change: dispatch is now `cmd_update "\$@"`. cmd_update internals already
handle multi-arg correctly (\$1 == --check fast path; \$1 == --rollback
shift+forward; otherwise doctor-driven).
Verified:
./ocp update --check → existing behaviour preserved
./ocp update --rollback --list → "Found 0 snapshots:" exit 0
./ocp update --rollback --dry-run → no-snapshot error exit 1
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(release): v3.15.0 — README AI prompt blocks + Upgrading rewrite + CHANGELOG
§Installation, §Upgrading, §Troubleshooting each start with a copy-paste
AI prompt block for Claude Code / Cursor / Copilot. The Upgrading section
explains the three paths (light / full / fresh-install) and rollback usage.
All Commands table gains an `ocp doctor` row.
package.json bumped to 3.15.0.
CHANGELOG.md gains the v3.15.0 entry covering doctor, the cross-version
update path, --rollback, fresh-install routing, and AI prompt blocks.
Notes the dependency on PR #90 (plist env merge bug fix, already merged).
No cli.js citation needed: docs + version bump only, no server.mjs change.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(readme): show --yes in rollback usage examples
Per Iron Rule 10 reviewer nit on PR #91: live rollback requires --yes
even for interactive humans. Update §Upgrading examples to show the
canonical human form. (AI agents pass --yes by convention; humans were
hitting a confusing "requires --yes" error following the prior README.)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(scripts): CLI entrypoint guard resilient to symlinked install paths
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>
---------
Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>