@clawops/cli 1.7.1 → 1.7.3

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.
Files changed (41) hide show
  1. package/README.md +37 -92
  2. package/dist/{apply-BTZFFGAD.js → apply-PZ5IIU7Z.js} +5 -4
  3. package/dist/{aws-D7Y6LCGK.js → aws-L4RR2LSE.js} +1 -1
  4. package/dist/{azure-JQAMNVHN.js → azure-7OXQENQL.js} +1 -1
  5. package/dist/{bootstrap-RY4VOSDJ.js → bootstrap-HL4DNUNG.js} +13 -4
  6. package/dist/bootstrap.sh.tmpl +204 -0
  7. package/dist/chunk-2YOP6N7X.js +126 -0
  8. package/dist/{chunk-GJEF6UQA.js → chunk-3XK66DKK.js} +100 -16
  9. package/dist/{chunk-ISFHLA4G.js → chunk-5SPESD5N.js} +8 -6
  10. package/dist/{chunk-YTH4L2GN.js → chunk-6X45DDGH.js} +5 -4
  11. package/dist/{chunk-XKT42M34.js → chunk-C5X375RO.js} +14 -1
  12. package/dist/{chunk-LCKD7L7X.js → chunk-DQDAWNGB.js} +1 -1
  13. package/dist/{chunk-CG3Y7Y2R.js → chunk-FSUWPL7Q.js} +9 -9
  14. package/dist/{chunk-FQ4JXUCR.js → chunk-NESXDU5F.js} +7 -4
  15. package/dist/{chunk-ZFNPM2WG.js → chunk-O3F7OGH7.js} +40 -3
  16. package/dist/{chunk-3MFZ7E74.js → chunk-P5LKWRPL.js} +3 -3
  17. package/dist/chunk-PVUVI35Y.js +29 -0
  18. package/dist/cli.js +122 -68
  19. package/dist/{context-ALSJMTHE.js → context-5LCUG2QR.js} +1 -1
  20. package/dist/{gcp-6FT2A45S.js → gcp-UG6AWPIZ.js} +1 -1
  21. package/dist/{generate-SPT7PJKR.js → generate-6E7J6BTM.js} +4 -3
  22. package/dist/{harden-BDVYW567.js → harden-URPXLSVF.js} +2 -2
  23. package/dist/{mcp-wire-ZWIYMLEW.js → mcp-wire-SLL4TLSC.js} +1 -1
  24. package/dist/{package-OMPI6RMV.js → package-QE7V2JAT.js} +3 -2
  25. package/dist/{pool-JZGKPP6K.js → pool-QGFQRMGU.js} +2 -2
  26. package/dist/{remote-config-QF5TT7GU.js → remote-config-PFB66RPY.js} +3 -1
  27. package/dist/{server-RGLKKCCU.js → server-L22AJQZO.js} +19 -18
  28. package/dist/{ssh-E2AMBPZY.js → ssh-CNJNLM3E.js} +1 -1
  29. package/dist/{validate-T5M5EHSJ.js → validate-CFQ4FH2T.js} +2 -1
  30. package/dist/version-guard-MZAO4KHS.js +25 -0
  31. package/dist/versions-OEPAS2EL.js +24 -0
  32. package/package.json +3 -2
  33. package/spec/_gen-schemas.md +136 -0
  34. package/spec/deploy-plan.schema.json +193 -0
  35. package/spec/errors.yaml +175 -0
  36. package/spec/integrations.yaml +97 -0
  37. package/spec/invariants.yaml +81 -0
  38. package/spec/mcp-tools.yaml +532 -0
  39. package/spec/models.yaml +113 -0
  40. package/spec/openclaw-versions.yaml +67 -0
  41. package/spec/providers.schema.json +120 -0
package/README.md CHANGED
@@ -12,113 +12,58 @@ and Cursor drive them through typed MCP tools with explicit safety controls.
12
12
 
13
13
  ---
14
14
 
15
- ## What's new in v1.7.0
15
+ ## What's new in v1.7.3
16
16
 
17
- **`clawops harden`** run an idempotent set of server-hardening modules against a deployed
18
- stack over SSH. Each module has a read-only `check()` and an `apply()` step; `check()` runs first
19
- and `apply()` is skipped when the box is already compliant. Sentinel files under
20
- `/etc/clawops/hardening/` record what has been applied.
21
-
22
- ```bash
23
- clawops harden --list # show available modules
24
- clawops harden --stack prod --dry-run # preview changes, apply nothing
25
- clawops harden --stack prod # apply the default module set
26
- clawops harden --stack prod --options ssh,ufw # apply a specific subset
27
- ```
28
-
29
- **Common modules (all providers), ON by default:** `ssh` (hardens `sshd_config`, guards against
30
- lockout by verifying `authorized_keys` is non-empty before restarting sshd), `ufw` (deny-all
31
- incoming, allow SSH + gateway 18789), `fail2ban` (SSH jail: 5 failures → 10-minute ban),
32
- `unattended-upgrades` (security-only auto updates), and `docker-socket` (verify
33
- `/var/run/docker.sock` is `root:docker 660`). **Opt-in:** `auditd`, `lynis` (CIS Level 1 scan →
34
- `~/.clawops/reports/`), `sysctl`.
35
-
36
- **AWS modules (check-only, ON by default):** `aws-sg-audit` (warns on `0.0.0.0/0` ingress on
37
- unexpected ports) and `aws-ssm-check` (verifies `AmazonSSMManagedInstanceCore` for emergency SSM
38
- shell access). **Opt-in:** `aws-flow-logs`, `aws-guardduty`.
39
-
40
- `clawops setup` now presents a multi-select hardening step after deploy (skippable with
41
- `--no-harden`), and `clawops doctor --stack <name>` gains a Hardening section showing which
42
- modules are applied, missing, or drifted.
17
+ **README maintenance.** The release-notes section had grown to four versions and was missing
18
+ v1.7.2 entirely. It now carries the current line only; [`CHANGELOG.md`](CHANGELOG.md) remains the
19
+ full history.
43
20
 
44
21
  ---
45
22
 
46
- ## What's new in v1.6.0
23
+ ## What's new in v1.7.2
47
24
 
48
- **`clawops bug`** open a pre-filled GitHub issue with system context (version, Node, OS,
49
- provider, stack count, SSH key presence) populated automatically. `--json` emits the URL without
50
- prompting or opening a browser, suitable for scripting. `clawops doctor` now prints a `clawops bug`
51
- hint in its footer when it exits with an error.
25
+ **Refuses OpenClaw 2.0.** OpenClaw `2026.8.1` changed the container runtime contract state moved
26
+ into SQLite, config moved to a writable path, and model providers became install-gated plugins.
27
+ Deploying it from this release line produces a crash-looping gateway. `doctor`, `plan`, `up` and
28
+ `apply` now refuse anything above `2026.7.1-2`.
52
29
 
53
- ```bash
54
- clawops bug # open a pre-filled issue in the browser
55
- clawops bug --json # print the issue URL for scripting
56
- ```
30
+ The support range in `spec/openclaw-versions.yaml` was previously unbounded *and read by no code*,
31
+ so any OpenClaw release was accepted. Moving tags are now resolved to a concrete version **before**
32
+ the range check, and an unresolved tag is refused rather than assumed safe — `latest` and `stable`
33
+ both point at 2.0 today. The default is now a concrete pin.
57
34
 
58
- **Cloud deploy bug fixes (AWS, GCP, Azure).** This release fixes 10 bugs found in a deploy audit,
59
- several deploy-blocking:
60
-
61
- - **Azure:** updated deprecated image reference `UbuntuServer/22.04-LTS` →
62
- `0001-com-ubuntu-server-jammy/22_04-lts-gen2` — every Azure deployment was broken; and fixed a
63
- malformed Key Vault `roleDefinitionId` that broke Key Vault RBAC entirely.
64
- - **AWS:** Bedrock startup script now uses the IMDSv2 PUT→GET token flow (a plain `curl` to IMDS
65
- returned 401, so the region always fell back to `us-east-1`); replaced
66
- `AmazonBedrockFullAccess` with a least-privilege inline policy.
67
- - **AWS (⚠️ migration impact):** migrated from inline Security Group ingress/egress arrays to
68
- individual `SecurityGroupIngressRule`/`SecurityGroupEgressRule` resources so CIDR changes no
69
- longer replace the whole Security Group. **Existing AWS stacks have their Security Group
70
- replaced on the first `clawops up` after this upgrade** — see
71
- `docs/decisions/0009-aws-sg-rule-resources.md` for the import-based mitigation.
72
- - **All providers:** `accessMode=auto` egress-IP detection now returns a `Result` and throws a
73
- clear error on failure instead of silently producing a VM with zero ingress rules.
35
+ `clawops doctor --stack <name>` reports the version a deployed gateway is actually running, so an
36
+ existing deployment that already picked up 2.0 through a moving tag can be identified.
74
37
 
75
- ---
38
+ **`clawops config set` now applies.** It never has. The config clawops mounted was read by nothing
39
+ on either OpenClaw line — models, channels and auth mode were silently discarded. Setting
40
+ `OPENCLAW_CONFIG_PATH` fixes it, with four guards: `gateway.port` normalisation (with a warning),
41
+ an argv `--port` pin, a parse check, and a post-restart health gate. The MCP `gateway restart`
42
+ tool, which dropped the config mount entirely, now matches the CLI path.
76
43
 
77
- ## What's new in v1.5.0
44
+ **A fresh local deployment starts.** OpenClaw refuses a non-loopback bind without auth and always
45
+ binds `0.0.0.0` in a container, and the bootstrap never supplied a token — so the gateway exited 78
46
+ and systemd restart-looped. A token is now generated once, kept in a `0600` env file, and passed
47
+ via `--env-file`, never on the command line.
78
48
 
79
- **`clawops mcp wire`** wire the OpenClaw gateway's own AI as an MCP client of clawops.
80
- Once wired, in-conversation commands like "check if my stack is healthy" or "show me the last 20
81
- log lines" invoke the real `clawops` CLI rather than having the gateway AI guess.
49
+ **Ollama is reachable.** `localhost` inside the container is the container. clawops now passes
50
+ `--add-host=host.docker.internal:host-gateway` and defaults the Ollama address to match.
82
51
 
83
- ```bash
84
- clawops mcp wire # wire the default stack
85
- clawops mcp wire --stack prod # target a named stack
86
- clawops mcp wire --force # bypass gateway version check
87
- ```
52
+ **Packaging.** `spec/` was missing from the published files, and neither it nor
53
+ `bootstrap.sh.tmpl` resolved from the bundle — so `clawops plan` and `clawops up --provider local`
54
+ failed from an npm install. Both are now shipped and resolved correctly.
88
55
 
89
- Version-gated: requires OpenClaw 2026.4 on the gateway side. If the version is older, the
90
- command surfaces a clear upgrade prompt and exits cleanly.
56
+ **SSH host keys.** The verifier read each `known_hosts` line's *key type* as the key, so any
57
+ standard entry failed permanently with `Host denied`. Standard entries now parse, including
58
+ comma-separated host lists, `[host]:port`, hashed hostnames, `@revoked` / `@cert-authority`
59
+ markers, and wildcard and negated patterns.
91
60
 
92
- The `clawops setup` wizard now also asks at the end of a successful deploy:
93
- *"Should the OpenClaw gateway's AI also be able to manage this stack?"* — accepting wires the
94
- client automatically over the same SSH session.
61
+ > **Behaviour change:** a host covered by a wildcard whose key does not match is now refused where
62
+ > it previously connected. Ignoring wildcards meant trust-on-first-use accepted a key your own
63
+ > `known_hosts` contradicted. This matches OpenSSH.
95
64
 
96
65
  ---
97
66
 
98
- ## What's new in v1.4.0
99
-
100
- **`clawops monitor`** — live dashboard for any deployed stack. Shows gateway health, container
101
- status, CPU/memory, disk usage, and a rolling log tail. Refreshes on an interval with `[r]`,
102
- toggles logs with `[l]`, and quits with `[q]`.
103
-
104
- Run without `--stack` to get an **interactive stack picker** first — probes all registered stacks
105
- in parallel, shows only running ones by default, and lets you toggle to a full list (`[a]`) where
106
- not-deployed stacks can be deleted from the registry with `[d]`. Press `[s]` inside the dashboard
107
- to go back to the picker.
108
-
109
- ```bash
110
- clawops monitor # interactive stack selection → dashboard
111
- clawops monitor --stack prod # jump straight to a named stack
112
- clawops monitor --stack prod | cat # one-shot snapshot for CI
113
- ```
114
-
115
- **`clawops_monitor` MCP tool** — same snapshot as a single structured JSON call, so Claude Code
116
- and Cursor can check stack health without opening a terminal.
117
-
118
- **`clawops stacks delete` guard** — now blocks deletion of a still-deployed stack and requires
119
- `--force` to remove from the registry without tearing down cloud resources first.
120
-
121
- ---
122
67
 
123
68
  ## Who this is for
124
69
 
@@ -612,7 +557,7 @@ pnpm dev doctor # verify toolchain
612
557
  ```bash
613
558
  pnpm dev # run CLI from src/ via tsx
614
559
  pnpm build # tsup → dist/
615
- pnpm test # vitest (493 tests, ~3s)
560
+ pnpm test # vitest (786 tests, ~4s)
616
561
  pnpm test:changed # vitest --changed (fast edit loop)
617
562
  pnpm test:integration # Docker-based SSH integration tests
618
563
  pnpm typecheck # tsc --noEmit
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  applyPlan
4
- } from "./chunk-ISFHLA4G.js";
5
- import "./chunk-YTH4L2GN.js";
4
+ } from "./chunk-5SPESD5N.js";
6
5
  import "./chunk-6ZFIFDBJ.js";
7
6
  import "./chunk-BOPSG2LI.js";
8
- import "./chunk-ZFNPM2WG.js";
9
- import "./chunk-3MFZ7E74.js";
7
+ import "./chunk-6X45DDGH.js";
8
+ import "./chunk-O3F7OGH7.js";
9
+ import "./chunk-P5LKWRPL.js";
10
+ import "./chunk-PVUVI35Y.js";
10
11
  import "./chunk-A2I76FTA.js";
11
12
  import "./chunk-CX5SL5HP.js";
12
13
  import "./chunk-KGXPLI7W.js";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  makeStartupScript
4
- } from "./chunk-XKT42M34.js";
4
+ } from "./chunk-C5X375RO.js";
5
5
 
6
6
  // src/providers/aws/index.ts
7
7
  import process2 from "process";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  makeStartupScript
4
- } from "./chunk-XKT42M34.js";
4
+ } from "./chunk-C5X375RO.js";
5
5
 
6
6
  // src/providers/azure/index.ts
7
7
  import process2 from "process";
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  acquireSession
4
- } from "./chunk-LCKD7L7X.js";
5
- import "./chunk-GJEF6UQA.js";
4
+ } from "./chunk-DQDAWNGB.js";
5
+ import "./chunk-3XK66DKK.js";
6
6
  import {
7
7
  writeLocalState
8
8
  } from "./chunk-A2I76FTA.js";
@@ -12,7 +12,7 @@ import {
12
12
  } from "./chunk-KGXPLI7W.js";
13
13
 
14
14
  // src/providers/local/bootstrap.ts
15
- import { readFileSync } from "fs";
15
+ import { readFileSync, existsSync } from "fs";
16
16
  import path from "path";
17
17
  import { fileURLToPath } from "url";
18
18
  var GATEWAY_PORT = 18789;
@@ -20,7 +20,16 @@ var HEALTH_POLL_INTERVAL_MS = 3e3;
20
20
  var HEALTH_TIMEOUT_MS = 12e4;
21
21
  function loadTemplate() {
22
22
  const dir = path.dirname(fileURLToPath(import.meta.url));
23
- return readFileSync(path.join(dir, "bootstrap.sh.tmpl"), "utf-8");
23
+ const candidates = [
24
+ path.join(dir, "bootstrap.sh.tmpl"),
25
+ path.join(dir, "../src/providers/local/bootstrap.sh.tmpl")
26
+ ];
27
+ for (const candidate of candidates) {
28
+ if (existsSync(candidate)) return readFileSync(candidate, "utf-8");
29
+ }
30
+ throw new Error(
31
+ `Cannot find bootstrap.sh.tmpl (looked in: ${candidates.join(", ")}). If this is an installed package, the template may be missing from the build output.`
32
+ );
24
33
  }
25
34
  function renderScript(openclawVersion) {
26
35
  return loadTemplate().replace(/\{\{OPENCLAW_VERSION\}\}/g, openclawVersion);
@@ -0,0 +1,204 @@
1
+ #!/bin/bash
2
+ # clawops bootstrap script — idempotent; safe to re-run.
3
+ # Rendered by clawops and executed via SSH on the target host.
4
+ # Supports: Ubuntu/Debian, RHEL/Fedora/CentOS (dnf/yum), Alpine (apk), macOS.
5
+ set -euo pipefail
6
+
7
+ OPENCLAW_VERSION="{{OPENCLAW_VERSION}}"
8
+ OPENCLAW_PORT=18789
9
+
10
+ # ── OS detection ──────────────────────────────────────────────────────────────
11
+ OS="$(uname -s)"
12
+
13
+ case "$OS" in
14
+ Linux)
15
+ if command -v apt-get &>/dev/null; then
16
+ PKG_MANAGER=apt
17
+ elif command -v dnf &>/dev/null; then
18
+ PKG_MANAGER=dnf
19
+ elif command -v yum &>/dev/null; then
20
+ PKG_MANAGER=yum
21
+ elif command -v apk &>/dev/null; then
22
+ PKG_MANAGER=apk
23
+ else
24
+ echo "clawops: unsupported Linux distribution (no apt/dnf/yum/apk found)" >&2
25
+ exit 1
26
+ fi
27
+ OPENCLAW_USER=clawops
28
+ OPENCLAW_CONFIG=/home/${OPENCLAW_USER}/openclaw.json
29
+ ;;
30
+ Darwin)
31
+ PKG_MANAGER=darwin
32
+ # When invoked via sudo the script runs as root but SUDO_USER is the real caller.
33
+ # Fall back to id -un for passwordless (non-sudo) invocations.
34
+ OPENCLAW_USER="${SUDO_USER:-$(id -un)}"
35
+ OPENCLAW_CONFIG="${HOME}/.config/openclaw/config.json"
36
+ mkdir -p "${HOME}/.config/openclaw"
37
+ # Fix ownership so subsequent non-sudo SSH writes (atomicWriteConfig) can overwrite.
38
+ chown "${OPENCLAW_USER}" "${HOME}/.config/openclaw" 2>/dev/null || true
39
+ # sudo resets PATH; add common Docker install locations so `command -v docker` works.
40
+ # Covers: Docker Desktop (Intel + AS), Homebrew (AS), Colima.
41
+ export PATH="/usr/local/bin:/opt/homebrew/bin:/Applications/Docker.app/Contents/Resources/bin:$PATH"
42
+ ;;
43
+ *)
44
+ echo "clawops: unsupported OS: $OS" >&2
45
+ exit 1
46
+ ;;
47
+ esac
48
+
49
+ # ── Create clawops user (Linux only) ─────────────────────────────────────────
50
+ if [ "$OS" = "Linux" ]; then
51
+ id -u "${OPENCLAW_USER}" &>/dev/null || useradd -m -s /bin/bash "${OPENCLAW_USER}"
52
+ fi
53
+
54
+ # ── Install Docker ─────────────────────────────────────────────────────────────
55
+ if ! command -v docker &>/dev/null; then
56
+ case "$PKG_MANAGER" in
57
+ darwin)
58
+ echo "clawops: Docker not found on macOS." >&2
59
+ echo "Install one of:" >&2
60
+ echo " Docker Desktop: https://www.docker.com/products/docker-desktop/" >&2
61
+ echo " Homebrew: brew install --cask docker" >&2
62
+ echo " Colima (FOSS): brew install colima docker && colima start" >&2
63
+ exit 1
64
+ ;;
65
+ apt)
66
+ export DEBIAN_FRONTEND=noninteractive
67
+ apt-get update -q
68
+ apt-get install -y -q ca-certificates curl gnupg lsb-release
69
+ install -m 0755 -d /etc/apt/keyrings
70
+ # Detect Ubuntu vs Debian; use the correct Docker APT repo for each.
71
+ # /etc/os-release ID is 'ubuntu' on Ubuntu and 'debian' on Debian.
72
+ # Other debian-based distros (Raspbian, Mint, etc.) fall back to the debian repo.
73
+ . /etc/os-release
74
+ case "${ID:-ubuntu}" in
75
+ ubuntu) DOCKER_DISTRO=ubuntu ;;
76
+ *) DOCKER_DISTRO=debian ;;
77
+ esac
78
+ curl -fsSL "https://download.docker.com/linux/${DOCKER_DISTRO}/gpg" \
79
+ | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
80
+ chmod a+r /etc/apt/keyrings/docker.gpg
81
+ echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
82
+ https://download.docker.com/linux/${DOCKER_DISTRO} $(lsb_release -cs) stable" \
83
+ > /etc/apt/sources.list.d/docker.list
84
+ apt-get update -q
85
+ apt-get install -y -q docker-ce docker-ce-cli containerd.io
86
+ systemctl enable --now docker
87
+ ;;
88
+ dnf|yum)
89
+ $PKG_MANAGER install -y dnf-plugins-core 2>/dev/null || $PKG_MANAGER install -y yum-utils
90
+ $PKG_MANAGER config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
91
+ $PKG_MANAGER install -y docker-ce docker-ce-cli containerd.io
92
+ systemctl enable --now docker
93
+ ;;
94
+ apk)
95
+ apk add --no-cache docker
96
+ rc-update add docker boot
97
+ service docker start
98
+ ;;
99
+ esac
100
+ fi
101
+
102
+ # ── Verify Docker daemon is reachable ─────────────────────────────────────────
103
+ # Try docker version (faster than docker info) across all known socket paths.
104
+ docker_reachable() {
105
+ docker version >/dev/null 2>&1 && return 0
106
+ if [ "$OS" = "Darwin" ]; then
107
+ DOCKER_HOST="unix://${HOME}/.docker/run/docker.sock" docker version >/dev/null 2>&1 && return 0
108
+ DOCKER_HOST="unix://${HOME}/.colima/default/docker.sock" docker version >/dev/null 2>&1 && return 0
109
+ fi
110
+ return 1
111
+ }
112
+
113
+ if ! docker_reachable; then
114
+ echo "clawops: Docker is installed but the daemon is not running." >&2
115
+ if [ "$PKG_MANAGER" = "darwin" ]; then
116
+ echo "Start Docker Desktop or run: colima start" >&2
117
+ else
118
+ echo "Try: systemctl start docker" >&2
119
+ fi
120
+ exit 1
121
+ fi
122
+
123
+ # ── Add clawops user to docker group (Linux only) ────────────────────────────
124
+ if [ "$OS" = "Linux" ]; then
125
+ usermod -aG docker "${OPENCLAW_USER}" || true
126
+ fi
127
+
128
+ # ── Pull OpenClaw image ────────────────────────────────────────────────────────
129
+ docker pull "ghcr.io/openclaw/openclaw:${OPENCLAW_VERSION}"
130
+
131
+ # ── Gateway auth token ────────────────────────────────────────────────────────
132
+ # OpenClaw refuses to bind a non-loopback listener without auth, and in a container
133
+ # it always binds 0.0.0.0 — so without a token the gateway exits 78 and systemd
134
+ # restart-loops. Generated once and reused on re-runs so restarts don't rotate it.
135
+ OPENCLAW_ENV_FILE=/home/clawops/openclaw.env
136
+ if [ ! -s "${OPENCLAW_ENV_FILE}" ]; then
137
+ OPENCLAW_TOKEN=$(openssl rand -hex 32 2>/dev/null || head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n')
138
+ printf 'OPENCLAW_GATEWAY_TOKEN=%s\n' "${OPENCLAW_TOKEN}" > "${OPENCLAW_ENV_FILE}"
139
+ chmod 600 "${OPENCLAW_ENV_FILE}"
140
+ chown "${OPENCLAW_USER}" "${OPENCLAW_ENV_FILE}" 2>/dev/null || true
141
+ fi
142
+
143
+ # ── Write default openclaw.json if absent ─────────────────────────────────────
144
+ # clawops up --config will overwrite this with the plan config overlay.
145
+ if [ ! -f "${OPENCLAW_CONFIG}" ]; then
146
+ cat > "${OPENCLAW_CONFIG}" <<'OPENCLAWJSON'
147
+ {"meta":{"lastTouchedVersion":"2026.4"},"gateway":{"port":18789,"auth":{"mode":"token"}},"models":{},"channels":{}}
148
+ OPENCLAWJSON
149
+ if [ "$OS" = "Linux" ]; then
150
+ chown "${OPENCLAW_USER}:${OPENCLAW_USER}" "${OPENCLAW_CONFIG}"
151
+ else
152
+ chown "${OPENCLAW_USER}" "${OPENCLAW_CONFIG}" 2>/dev/null || true
153
+ fi
154
+ fi
155
+
156
+ # ── Start service ─────────────────────────────────────────────────────────────
157
+ if [ "$OS" = "Linux" ]; then
158
+ SERVICE_FILE=/etc/systemd/system/openclaw.service
159
+ cat > "${SERVICE_FILE}" <<UNIT
160
+ [Unit]
161
+ Description=OpenClaw gateway
162
+ After=docker.service
163
+ Requires=docker.service
164
+
165
+ [Service]
166
+ User=${OPENCLAW_USER}
167
+ Restart=always
168
+ RestartSec=5
169
+ ExecStartPre=-/usr/bin/docker stop openclaw
170
+ ExecStartPre=-/usr/bin/docker rm openclaw
171
+ ExecStart=/usr/bin/docker run --name openclaw --rm \
172
+ -p ${OPENCLAW_PORT}:${OPENCLAW_PORT} \
173
+ -e OPENCLAW_CONFIG_PATH=/app/config.json --add-host=host.docker.internal:host-gateway \
174
+ --env-file ${OPENCLAW_ENV_FILE} \
175
+ -v ${OPENCLAW_CONFIG}:/app/config.json:ro \
176
+ ghcr.io/openclaw/openclaw:${OPENCLAW_VERSION} \
177
+ node openclaw.mjs gateway run --allow-unconfigured --port ${OPENCLAW_PORT}
178
+ ExecStop=/usr/bin/docker stop openclaw
179
+
180
+ [Install]
181
+ WantedBy=multi-user.target
182
+ UNIT
183
+ systemctl daemon-reload
184
+ systemctl enable openclaw
185
+ systemctl restart openclaw
186
+
187
+ elif [ "$OS" = "Darwin" ]; then
188
+ # macOS: no systemd; run as foreground-detached container.
189
+ # LaunchAgent support is a future enhancement.
190
+ docker stop openclaw 2>/dev/null || true
191
+ docker rm openclaw 2>/dev/null || true
192
+ docker run -d \
193
+ --name openclaw \
194
+ --restart unless-stopped \
195
+ -p "${OPENCLAW_PORT}:${OPENCLAW_PORT}" \
196
+ -e OPENCLAW_CONFIG_PATH=/app/config.json --add-host=host.docker.internal:host-gateway \
197
+ --env-file "${OPENCLAW_ENV_FILE}" \
198
+ --env-file ${OPENCLAW_ENV_FILE} \
199
+ -v "${OPENCLAW_CONFIG}":/app/config.json:ro \
200
+ "ghcr.io/openclaw/openclaw:${OPENCLAW_VERSION}" \
201
+ node openclaw.mjs gateway run --allow-unconfigured --port ${OPENCLAW_PORT}
202
+ fi
203
+
204
+ echo "clawops: bootstrap complete"
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ resolveSpecDir
4
+ } from "./chunk-PVUVI35Y.js";
5
+
6
+ // src/openclaw/versions.ts
7
+ import { readFileSync } from "fs";
8
+ import { join } from "path";
9
+
10
+ // src/types/result.ts
11
+ function ok(value) {
12
+ return { ok: true, value };
13
+ }
14
+ function err(error) {
15
+ return { ok: false, error };
16
+ }
17
+
18
+ // src/openclaw/versions.ts
19
+ var MOVING_TAGS = ["latest", "stable", "dev", "main"];
20
+ function isMovingTag(v) {
21
+ return MOVING_TAGS.includes(v);
22
+ }
23
+ var _spec;
24
+ function loadVersionSpec(yaml) {
25
+ if (_spec) return _spec;
26
+ const specPath = join(resolveSpecDir(), "openclaw-versions.yaml");
27
+ try {
28
+ const raw = yaml.load(readFileSync(specPath, "utf-8"));
29
+ if (!raw?.support?.min) {
30
+ throw new Error("missing support.min");
31
+ }
32
+ _spec = {
33
+ support: {
34
+ min: raw.support.min,
35
+ max: raw.support.max ?? "",
36
+ recommended: raw.support.recommended ?? raw.support.min
37
+ }
38
+ };
39
+ return _spec;
40
+ } catch (e) {
41
+ throw new Error(
42
+ `Cannot load OpenClaw version support matrix from ${specPath}: ${e.message}`
43
+ );
44
+ }
45
+ }
46
+ function _resetVersionSpecCache() {
47
+ _spec = void 0;
48
+ }
49
+ function compareVersions(a, b) {
50
+ const parse = (v) => v.split(/[.-]/).map((seg) => Number.parseInt(seg, 10)).map((n) => Number.isNaN(n) ? 0 : n);
51
+ const pa = parse(a);
52
+ const pb = parse(b);
53
+ const len = Math.max(pa.length, pb.length);
54
+ for (let i = 0; i < len; i++) {
55
+ const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
56
+ if (diff !== 0) return diff;
57
+ }
58
+ return 0;
59
+ }
60
+ function checkVersion(version, support) {
61
+ if (isMovingTag(version)) {
62
+ return err({
63
+ requested: version,
64
+ resolved: version,
65
+ reason: "too-new",
66
+ supportedRange: describeRange(support),
67
+ message: `Cannot verify OpenClaw compatibility: "${version}" is a moving tag that was not resolved to a concrete release. Pin an explicit version (${support.recommended}) or re-run once tag resolution is available.`
68
+ });
69
+ }
70
+ if (compareVersions(version, support.min) < 0) {
71
+ return err({
72
+ requested: version,
73
+ resolved: version,
74
+ reason: "too-old",
75
+ supportedRange: describeRange(support),
76
+ message: `OpenClaw ${version} is older than this clawops release supports (${describeRange(support)}). Upgrade OpenClaw, or pin ${support.recommended}.`
77
+ });
78
+ }
79
+ if (support.max && compareVersions(version, support.max) > 0) {
80
+ return err({
81
+ requested: version,
82
+ resolved: version,
83
+ reason: "too-new",
84
+ supportedRange: describeRange(support),
85
+ message: `OpenClaw ${version} is newer than this clawops release supports (${describeRange(support)}).
86
+ OpenClaw 2026.8.1+ changed the container runtime contract: state moved to SQLite, config moved to a writable path, and provider plugins became install-gated.
87
+ Use clawops 2.x for OpenClaw 2026.9.1 and later: npm install -g @clawops/cli@latest
88
+ To stay on this line, pin an OpenClaw version at or below ${support.max}.`
89
+ });
90
+ }
91
+ return ok(version);
92
+ }
93
+ function describeRange(support) {
94
+ if (!support.max) return `>= ${support.min}`;
95
+ return `${support.min} \u2013 ${support.max}`;
96
+ }
97
+ async function resolveVersion(version, resolver) {
98
+ if (!isMovingTag(version)) return version;
99
+ if (!resolver) return version;
100
+ try {
101
+ return await resolver(version) ?? version;
102
+ } catch {
103
+ return version;
104
+ }
105
+ }
106
+ async function assertSupportedVersion(version, yaml, resolver) {
107
+ const spec = loadVersionSpec(yaml);
108
+ const resolved = await resolveVersion(version, resolver);
109
+ const result = checkVersion(resolved, spec.support);
110
+ if (!result.ok && resolved !== version) {
111
+ return err({ ...result.error, requested: version });
112
+ }
113
+ return result;
114
+ }
115
+
116
+ export {
117
+ MOVING_TAGS,
118
+ isMovingTag,
119
+ loadVersionSpec,
120
+ _resetVersionSpecCache,
121
+ compareVersions,
122
+ checkVersion,
123
+ describeRange,
124
+ resolveVersion,
125
+ assertSupportedVersion
126
+ };