@swarmclawai/swarmclaw 0.8.9 → 0.9.0
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/Dockerfile.sandbox-browser +29 -0
- package/README.md +21 -14
- package/package.json +7 -1
- package/scripts/easy-setup.mjs +145 -0
- package/scripts/easy-update.mjs +151 -0
- package/scripts/ensure-sandbox-browser-image.mjs +102 -0
- package/scripts/postinstall.mjs +54 -3
- package/scripts/sandbox-browser-entrypoint.sh +82 -0
- package/src/app/api/agents/[id]/route.ts +4 -0
- package/src/app/api/agents/route.ts +2 -0
- package/src/app/api/chats/[id]/browser/route.ts +16 -1
- package/src/app/api/chats/[id]/chat/route.ts +104 -93
- package/src/app/api/chats/[id]/route.ts +5 -0
- package/src/app/api/setup/doctor/route.ts +5 -18
- package/src/components/agents/inspector-panel.tsx +6 -5
- package/src/components/agents/sandbox-env-panel.tsx +1 -1
- package/src/lib/agent-sandbox-defaults.test.ts +37 -0
- package/src/lib/agent-sandbox-defaults.ts +91 -0
- package/src/lib/sandbox-defaults.ts +17 -0
- package/src/lib/server/agents/main-agent-loop.test.ts +175 -0
- package/src/lib/server/agents/main-agent-loop.ts +26 -24
- package/src/lib/server/browser-state.ts +12 -0
- package/src/lib/server/chat-execution/chat-execution.ts +4 -1
- package/src/lib/server/chat-execution/stream-agent-chat.test.ts +12 -1
- package/src/lib/server/chat-execution/stream-agent-chat.ts +68 -7
- package/src/lib/server/context-manager.test.ts +34 -2
- package/src/lib/server/context-manager.ts +40 -6
- package/src/lib/server/runtime/heartbeat-wake.ts +8 -2
- package/src/lib/server/runtime/process-manager.test.ts +86 -0
- package/src/lib/server/runtime/process-manager.ts +74 -11
- package/src/lib/server/runtime/session-run-manager.test.ts +86 -0
- package/src/lib/server/runtime/session-run-manager.ts +121 -2
- package/src/lib/server/sandbox/bridge-auth-registry.ts +26 -0
- package/src/lib/server/sandbox/browser-bridge.test.ts +56 -0
- package/src/lib/server/sandbox/browser-bridge.ts +220 -0
- package/src/lib/server/sandbox/browser-runtime.ts +314 -0
- package/src/lib/server/sandbox/constants.ts +24 -0
- package/src/lib/server/sandbox/docker.ts +107 -0
- package/src/lib/server/sandbox/fs-bridge.test.ts +59 -0
- package/src/lib/server/sandbox/fs-bridge.ts +128 -0
- package/src/lib/server/sandbox/novnc-auth.test.ts +28 -0
- package/src/lib/server/sandbox/novnc-auth.ts +77 -0
- package/src/lib/server/sandbox/prune.ts +58 -0
- package/src/lib/server/sandbox/registry.test.ts +52 -0
- package/src/lib/server/sandbox/registry.ts +110 -0
- package/src/lib/server/sandbox/session-runtime.test.ts +71 -0
- package/src/lib/server/sandbox/session-runtime.ts +307 -0
- package/src/lib/server/session-tools/autonomy-tools.test.ts +9 -7
- package/src/lib/server/session-tools/context.ts +3 -9
- package/src/lib/server/session-tools/sandbox.ts +268 -92
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +5 -5
- package/src/lib/server/session-tools/shell.ts +95 -30
- package/src/lib/server/session-tools/web-browser-config.test.ts +18 -0
- package/src/lib/server/session-tools/web-inputs.test.ts +24 -0
- package/src/lib/server/session-tools/web-utils.ts +57 -16
- package/src/lib/server/session-tools/web.ts +135 -25
- package/src/lib/server/storage.ts +65 -24
- package/src/lib/tool-definitions.ts +1 -1
- package/src/lib/validation/schemas.ts +38 -0
- package/src/types/index.ts +35 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
FROM debian:bookworm-slim@sha256:98f4b71de414932439ac6ac690d7060df1f27161073c5036a7553723881bffbe
|
|
2
|
+
|
|
3
|
+
ENV DEBIAN_FRONTEND=noninteractive
|
|
4
|
+
|
|
5
|
+
RUN apt-get update \
|
|
6
|
+
&& apt-get install -y --no-install-recommends \
|
|
7
|
+
bash \
|
|
8
|
+
ca-certificates \
|
|
9
|
+
chromium \
|
|
10
|
+
curl \
|
|
11
|
+
fonts-liberation \
|
|
12
|
+
fonts-noto-color-emoji \
|
|
13
|
+
novnc \
|
|
14
|
+
socat \
|
|
15
|
+
websockify \
|
|
16
|
+
x11vnc \
|
|
17
|
+
xvfb \
|
|
18
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
19
|
+
|
|
20
|
+
COPY scripts/sandbox-browser-entrypoint.sh /usr/local/bin/swarmclaw-sandbox-browser
|
|
21
|
+
RUN chmod +x /usr/local/bin/swarmclaw-sandbox-browser
|
|
22
|
+
|
|
23
|
+
RUN useradd --create-home --shell /bin/bash sandbox
|
|
24
|
+
USER sandbox
|
|
25
|
+
WORKDIR /home/sandbox
|
|
26
|
+
|
|
27
|
+
EXPOSE 9222 5900 6080
|
|
28
|
+
|
|
29
|
+
CMD ["swarmclaw-sandbox-browser"]
|
package/README.md
CHANGED
|
@@ -116,7 +116,7 @@ Skill source and runbook: [`swarmclaw/SKILL.md`](swarmclaw/SKILL.md).
|
|
|
116
116
|
- **OpenAI Codex CLI** (optional, for `codex-cli` provider) — [Install](https://github.com/openai/codex)
|
|
117
117
|
- **OpenCode CLI** (optional, for `opencode-cli` provider) — [Install](https://github.com/opencode-ai/opencode)
|
|
118
118
|
- **Gemini CLI** (optional, for `delegate` backend `gemini`) — install and authenticate `gemini` on your host
|
|
119
|
-
- **
|
|
119
|
+
- **Docker Desktop** (optional, recommended) — enables container sandboxes for shell, browser, and `sandbox_exec`; without Docker, SwarmClaw falls back to host execution
|
|
120
120
|
|
|
121
121
|
## Quick Start
|
|
122
122
|
|
|
@@ -133,6 +133,9 @@ swarmclaw server
|
|
|
133
133
|
```
|
|
134
134
|
|
|
135
135
|
`swarmclaw` by itself opens the CLI. `swarmclaw server` launches the packaged standalone server on `http://localhost:3456`.
|
|
136
|
+
Global install runs `postinstall`, which rebuilds `better-sqlite3` and prepares the sandbox browser image when Docker is available.
|
|
137
|
+
If Docker is not installed yet, SwarmClaw keeps running and falls back to host execution for shell, browser, and `sandbox_exec`.
|
|
138
|
+
No Deno install is required for the local `sandbox_exec` path.
|
|
136
139
|
|
|
137
140
|
### One-off run
|
|
138
141
|
|
|
@@ -151,7 +154,8 @@ curl -fsSL https://raw.githubusercontent.com/swarmclawai/swarmclaw/main/install.
|
|
|
151
154
|
|
|
152
155
|
The installer resolves the latest stable release tag and installs that version by default.
|
|
153
156
|
It also builds the production bundle so `npm run start` is ready immediately after install.
|
|
154
|
-
|
|
157
|
+
No Deno install is required; local sandbox execution is Docker-first with automatic host Node fallback.
|
|
158
|
+
To pin a version: `SWARMCLAW_VERSION=v0.9.0 curl ... | bash`
|
|
155
159
|
|
|
156
160
|
Or run locally from the repo (friendly for non-technical users):
|
|
157
161
|
|
|
@@ -163,8 +167,8 @@ npm run quickstart
|
|
|
163
167
|
|
|
164
168
|
`npm run quickstart` will:
|
|
165
169
|
- Check Node/npm versions
|
|
166
|
-
- Install Deno if the sandbox runtime is missing
|
|
167
170
|
- Install dependencies
|
|
171
|
+
- Prepare the Docker sandbox/browser runtime when Docker is available
|
|
168
172
|
- Prepare `.env.local` and `data/`
|
|
169
173
|
- Start the app at `http://localhost:3456`
|
|
170
174
|
|
|
@@ -182,7 +186,9 @@ yarn install && yarn dev
|
|
|
182
186
|
bun install && bun run dev
|
|
183
187
|
```
|
|
184
188
|
|
|
185
|
-
`postinstall` rebuilds `better-sqlite3` natively
|
|
189
|
+
`postinstall` rebuilds `better-sqlite3` natively and prepares the sandbox browser image when Docker is available.
|
|
190
|
+
If Docker is missing, SwarmClaw falls back to host execution until Docker is installed.
|
|
191
|
+
If you install with `--ignore-scripts`, run `npm rebuild better-sqlite3` manually and then `node ./scripts/ensure-sandbox-browser-image.mjs`.
|
|
186
192
|
|
|
187
193
|
On first launch, SwarmClaw will:
|
|
188
194
|
1. Generate an **access key** and display it in the terminal
|
|
@@ -383,7 +389,7 @@ Agents can use the following tools when enabled:
|
|
|
383
389
|
| Image Generation | Generate images from prompts (`generate_image`) via OpenAI, Stability, Replicate, fal.ai, Together, Fireworks, BFL, or custom endpoints; saved to uploads |
|
|
384
390
|
| Email | Send outbound email via SMTP (`email`) with `send`/`status` actions |
|
|
385
391
|
| Calendar | Manage Google/Outlook events (`calendar`) with list/create/update/delete/status actions |
|
|
386
|
-
| Sandbox | Run JS/TS in a
|
|
392
|
+
| Sandbox | Run JS/TS in a Docker-preferred Node.js sandbox when custom code is necessary. New and existing agents default to sandbox-enabled configs. If Docker is unavailable, SwarmClaw falls back to host execution; for simple API calls, prefer HTTP Request. |
|
|
387
393
|
| MCP Servers | Connect to external Model Context Protocol servers. Tools from MCP servers are injected as first-class agent tools |
|
|
388
394
|
|
|
389
395
|
### Platform Tools
|
|
@@ -639,6 +645,7 @@ docker compose up -d
|
|
|
639
645
|
```
|
|
640
646
|
|
|
641
647
|
Data is persisted in `data/` and `.env.local` via volume mounts. Updates: `git pull && docker compose up -d --build`.
|
|
648
|
+
In Docker deployments, local shell, browser, and `sandbox_exec` execution fall back to host execution inside the app container unless you separately provide Docker access to that container.
|
|
642
649
|
|
|
643
650
|
For prebuilt images (recommended for non-technical users after releases):
|
|
644
651
|
|
|
@@ -699,7 +706,7 @@ npm run dev:webpack
|
|
|
699
706
|
### First-Run Helpers
|
|
700
707
|
|
|
701
708
|
```bash
|
|
702
|
-
npm run setup:easy # setup only (
|
|
709
|
+
npm run setup:easy # setup only (prepares sandbox/browser runtime when Docker is available; does not start server)
|
|
703
710
|
npm run quickstart # setup + start dev server
|
|
704
711
|
npm run quickstart:prod # setup + build + start production server
|
|
705
712
|
npm run update:easy # safe update helper for local installs
|
|
@@ -710,7 +717,7 @@ npm run update:easy # safe update helper for local installs
|
|
|
710
717
|
SwarmClaw uses tag-based releases (`vX.Y.Z`) as the stable channel.
|
|
711
718
|
|
|
712
719
|
```bash
|
|
713
|
-
# example
|
|
720
|
+
# example release
|
|
714
721
|
npm version patch
|
|
715
722
|
git push origin main --follow-tags
|
|
716
723
|
```
|
|
@@ -720,15 +727,15 @@ On `v*` tags, GitHub Actions will:
|
|
|
720
727
|
2. Create a GitHub Release
|
|
721
728
|
3. Build and publish Docker images to `ghcr.io/swarmclawai/swarmclaw` (`:vX.Y.Z`, `:latest`, `:sha-*`)
|
|
722
729
|
|
|
723
|
-
#### v0.
|
|
730
|
+
#### v0.9.0 Release Readiness Notes
|
|
724
731
|
|
|
725
|
-
Before shipping `v0.
|
|
732
|
+
Before shipping `v0.9.0`, confirm the following user-facing changes are reflected in docs:
|
|
726
733
|
|
|
727
|
-
1. Install docs make it explicit that global npm installs use `swarmclaw server`,
|
|
728
|
-
2.
|
|
729
|
-
3.
|
|
730
|
-
4.
|
|
731
|
-
5. The release
|
|
734
|
+
1. Install docs make it explicit that global npm installs use `swarmclaw server`, and that package-manager installs plus the curl installer prepare the sandbox/browser runtime automatically when Docker is available.
|
|
735
|
+
2. Sandbox docs say local `sandbox_exec` no longer requires Deno, defaults to sandbox-enabled agent configs, and falls back to host Node when Docker is unavailable.
|
|
736
|
+
3. Release docs mention the OpenClaw-style sandbox/runtime refresh, heartbeat deferral improvements, and the HMR-safe live chat route fix.
|
|
737
|
+
4. Site and README install/version strings are updated to `v0.9.0`, including pinned install snippets, release notes index text, and sidebar/footer labels.
|
|
738
|
+
5. The release tag, npm package version, and generated GitHub release install snippet all agree on the non-prefixed npm version (`0.9.0`) versus the git tag (`v0.9.0`).
|
|
732
739
|
|
|
733
740
|
## CLI
|
|
734
741
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Self-hosted AI agent orchestration dashboard — manage LLM providers, orchestrate agent swarms, schedule tasks, and bridge agents to chat platforms.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -32,7 +32,12 @@
|
|
|
32
32
|
"bundled-skills/",
|
|
33
33
|
"src/",
|
|
34
34
|
"public/",
|
|
35
|
+
"Dockerfile.sandbox-browser",
|
|
36
|
+
"scripts/easy-setup.mjs",
|
|
37
|
+
"scripts/easy-update.mjs",
|
|
38
|
+
"scripts/ensure-sandbox-browser-image.mjs",
|
|
35
39
|
"scripts/postinstall.mjs",
|
|
40
|
+
"scripts/sandbox-browser-entrypoint.sh",
|
|
36
41
|
"next.config.ts",
|
|
37
42
|
"tsconfig.json",
|
|
38
43
|
"postcss.config.mjs",
|
|
@@ -52,6 +57,7 @@
|
|
|
52
57
|
"start:standalone": "node .next/standalone/server.js",
|
|
53
58
|
"smoke:browser": "node ./scripts/browser-route-smoke.mjs",
|
|
54
59
|
"smoke:browser:workbench": "node ./scripts/browser-workbench-smoke.mjs",
|
|
60
|
+
"sandbox:build:browser": "docker build -f Dockerfile.sandbox-browser -t swarmclaw-sandbox-browser:bookworm-slim .",
|
|
55
61
|
"benchmark:autonomy": "node ./scripts/benchmark-autonomy-harness.mjs",
|
|
56
62
|
"benchmark:agent-regression": "node --import tsx ./scripts/run-agent-regression-suite.ts",
|
|
57
63
|
"lint": "eslint",
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { spawnSync } from 'node:child_process'
|
|
6
|
+
|
|
7
|
+
const args = new Set(process.argv.slice(2))
|
|
8
|
+
const startAfterSetup = args.has('--start') || args.has('--prod')
|
|
9
|
+
const productionMode = args.has('--prod')
|
|
10
|
+
const skipInstall = args.has('--skip-install')
|
|
11
|
+
const cwd = process.cwd()
|
|
12
|
+
|
|
13
|
+
function log(message) {
|
|
14
|
+
process.stdout.write(`[setup] ${message}\n`)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function fail(message, code = 1) {
|
|
18
|
+
process.stderr.write(`[setup] ERROR: ${message}\n`)
|
|
19
|
+
process.exit(code)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function run(command, commandArgs, options = {}) {
|
|
23
|
+
const printable = `${command} ${commandArgs.join(' ')}`.trim()
|
|
24
|
+
log(`$ ${printable}`)
|
|
25
|
+
const result = spawnSync(command, commandArgs, {
|
|
26
|
+
cwd,
|
|
27
|
+
stdio: 'inherit',
|
|
28
|
+
...options,
|
|
29
|
+
})
|
|
30
|
+
if (result.error) fail(result.error.message)
|
|
31
|
+
if ((result.status ?? 1) !== 0) {
|
|
32
|
+
fail(`Command failed: ${printable}`, result.status ?? 1)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function runOptional(command, commandArgs, options = {}) {
|
|
37
|
+
const printable = `${command} ${commandArgs.join(' ')}`.trim()
|
|
38
|
+
log(`$ ${printable}`)
|
|
39
|
+
const result = spawnSync(command, commandArgs, {
|
|
40
|
+
cwd,
|
|
41
|
+
stdio: 'inherit',
|
|
42
|
+
...options,
|
|
43
|
+
})
|
|
44
|
+
if (result.error || (result.status ?? 1) !== 0) {
|
|
45
|
+
log(`Optional step failed: ${printable}`)
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
return true
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function ensureNodeVersion() {
|
|
52
|
+
const version = process.versions.node
|
|
53
|
+
const [majorRaw, minorRaw] = version.split('.')
|
|
54
|
+
const major = Number.parseInt(majorRaw || '0', 10)
|
|
55
|
+
const minor = Number.parseInt(minorRaw || '0', 10)
|
|
56
|
+
if (major < 22 || (major === 22 && minor < 6)) {
|
|
57
|
+
fail(`Detected Node ${version}. SwarmClaw requires Node 22.6 or newer.`)
|
|
58
|
+
}
|
|
59
|
+
log(`Node ${version} detected.`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function ensureNpm() {
|
|
63
|
+
const result = spawnSync('npm', ['--version'], { cwd, encoding: 'utf8' })
|
|
64
|
+
if (result.error || (result.status ?? 1) !== 0) {
|
|
65
|
+
fail('npm was not found. Install npm and rerun this setup command.')
|
|
66
|
+
}
|
|
67
|
+
log(`npm ${String(result.stdout || '').trim()} detected.`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function commandExists(name) {
|
|
71
|
+
const lookup = process.platform === 'win32' ? 'where' : 'which'
|
|
72
|
+
const result = spawnSync(lookup, [name], { cwd, encoding: 'utf8' })
|
|
73
|
+
return !result.error && (result.status ?? 1) === 0
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function ensureProjectRoot() {
|
|
77
|
+
const pkgPath = path.join(cwd, 'package.json')
|
|
78
|
+
if (!fs.existsSync(pkgPath)) {
|
|
79
|
+
fail(`package.json was not found in ${cwd}. Run this command from the SwarmClaw project root.`)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function ensureEnvFile() {
|
|
84
|
+
const envPath = path.join(cwd, '.env.local')
|
|
85
|
+
if (!fs.existsSync(envPath)) {
|
|
86
|
+
fs.writeFileSync(
|
|
87
|
+
envPath,
|
|
88
|
+
'# SwarmClaw local environment variables\n# ACCESS_KEY and CREDENTIAL_SECRET are auto-generated on first app run.\n',
|
|
89
|
+
'utf8',
|
|
90
|
+
)
|
|
91
|
+
log('Created .env.local.')
|
|
92
|
+
} else {
|
|
93
|
+
log('.env.local already exists.')
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function ensureDataDir() {
|
|
98
|
+
const dataDir = path.join(cwd, 'data')
|
|
99
|
+
fs.mkdirSync(dataDir, { recursive: true })
|
|
100
|
+
log(`Data directory ready at ${dataDir}.`)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function printNextSteps() {
|
|
104
|
+
process.stdout.write('\n')
|
|
105
|
+
log('Setup complete.')
|
|
106
|
+
process.stdout.write('\n')
|
|
107
|
+
process.stdout.write('Next steps:\n')
|
|
108
|
+
process.stdout.write('1. Run `npm run dev`.\n')
|
|
109
|
+
process.stdout.write('2. Open http://localhost:3456 in your browser.\n')
|
|
110
|
+
process.stdout.write('3. Copy the access key printed in the terminal and finish the setup wizard.\n')
|
|
111
|
+
process.stdout.write(' Or run `npx swarmclaw setup init` for interactive CLI setup.\n')
|
|
112
|
+
process.stdout.write('4. For updates later, run `npm run update:easy`.\n')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function main() {
|
|
116
|
+
ensureProjectRoot()
|
|
117
|
+
ensureNodeVersion()
|
|
118
|
+
ensureNpm()
|
|
119
|
+
ensureDataDir()
|
|
120
|
+
ensureEnvFile()
|
|
121
|
+
|
|
122
|
+
if (!skipInstall) {
|
|
123
|
+
run('npm', ['install'])
|
|
124
|
+
} else {
|
|
125
|
+
log('Skipping dependency install (--skip-install).')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
runOptional('node', ['./scripts/ensure-sandbox-browser-image.mjs'])
|
|
129
|
+
if (!commandExists('docker')) {
|
|
130
|
+
log('Docker not detected. SwarmClaw will fall back to host execution until Docker Desktop is installed.')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (productionMode) {
|
|
134
|
+
run('npm', ['run', 'build'])
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (startAfterSetup) {
|
|
138
|
+
run('npm', ['run', productionMode ? 'start' : 'dev'])
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
printNextSteps()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
main()
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from 'node:child_process'
|
|
4
|
+
|
|
5
|
+
const args = new Set(process.argv.slice(2))
|
|
6
|
+
const skipBuild = args.has('--skip-build')
|
|
7
|
+
const allowDirty = args.has('--allow-dirty')
|
|
8
|
+
const forceMain = args.has('--main')
|
|
9
|
+
const cwd = process.cwd()
|
|
10
|
+
const RELEASE_TAG_RE = /^v\d+\.\d+\.\d+([-.+][0-9A-Za-z.-]+)?$/
|
|
11
|
+
|
|
12
|
+
function log(message) {
|
|
13
|
+
process.stdout.write(`[update] ${message}\n`)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function fail(message, code = 1) {
|
|
17
|
+
process.stderr.write(`[update] ERROR: ${message}\n`)
|
|
18
|
+
process.exit(code)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function run(command, commandArgs, options = {}) {
|
|
22
|
+
const result = spawnSync(command, commandArgs, {
|
|
23
|
+
cwd,
|
|
24
|
+
encoding: 'utf8',
|
|
25
|
+
...options,
|
|
26
|
+
})
|
|
27
|
+
if (result.error) {
|
|
28
|
+
return { ok: false, out: '', err: result.error.message, code: result.status ?? 1 }
|
|
29
|
+
}
|
|
30
|
+
if ((result.status ?? 1) !== 0) {
|
|
31
|
+
const err = String(result.stderr || result.stdout || '').trim() || `exit ${result.status}`
|
|
32
|
+
return { ok: false, out: '', err, code: result.status ?? 1 }
|
|
33
|
+
}
|
|
34
|
+
return { ok: true, out: String(result.stdout || '').trim(), err: '', code: 0 }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function runOrThrow(command, commandArgs, options = {}) {
|
|
38
|
+
log(`$ ${command} ${commandArgs.join(' ')}`.trim())
|
|
39
|
+
const result = spawnSync(command, commandArgs, {
|
|
40
|
+
cwd,
|
|
41
|
+
stdio: 'inherit',
|
|
42
|
+
...options,
|
|
43
|
+
})
|
|
44
|
+
if (result.error) fail(result.error.message)
|
|
45
|
+
if ((result.status ?? 1) !== 0) {
|
|
46
|
+
fail(`Command failed: ${command} ${commandArgs.join(' ')}`, result.status ?? 1)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function runOptional(command, commandArgs, options = {}) {
|
|
51
|
+
log(`$ ${command} ${commandArgs.join(' ')}`.trim())
|
|
52
|
+
const result = spawnSync(command, commandArgs, {
|
|
53
|
+
cwd,
|
|
54
|
+
stdio: 'inherit',
|
|
55
|
+
...options,
|
|
56
|
+
})
|
|
57
|
+
if (result.error || (result.status ?? 1) !== 0) {
|
|
58
|
+
log(`Optional step failed: ${command} ${commandArgs.join(' ')}`)
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
return true
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getLatestStableTag() {
|
|
65
|
+
const tagList = run('git', ['tag', '--list', 'v*', '--sort=-v:refname'])
|
|
66
|
+
if (!tagList.ok) return null
|
|
67
|
+
const tags = tagList.out.split('\n').map((line) => line.trim()).filter(Boolean)
|
|
68
|
+
return tags.find((tag) => RELEASE_TAG_RE.test(tag)) || null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function main() {
|
|
72
|
+
const gitCheck = run('git', ['rev-parse', '--is-inside-work-tree'])
|
|
73
|
+
if (!gitCheck.ok) {
|
|
74
|
+
fail('This folder is not a git repository. Automatic updates require git.')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const dirty = run('git', ['status', '--porcelain'])
|
|
78
|
+
const isDirty = !!dirty.out
|
|
79
|
+
if (isDirty && !allowDirty) {
|
|
80
|
+
const changed = dirty.out.split('\n').map((line) => line.trim()).filter(Boolean)
|
|
81
|
+
const preview = changed.slice(0, 20)
|
|
82
|
+
process.stdout.write(`${preview.join('\n')}\n`)
|
|
83
|
+
if (changed.length > preview.length) {
|
|
84
|
+
log(`...and ${changed.length - preview.length} more changed file(s).`)
|
|
85
|
+
}
|
|
86
|
+
fail('Local changes detected. Commit/stash them first, or rerun with --allow-dirty.')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const beforeSha = run('git', ['rev-parse', '--short', 'HEAD'])
|
|
90
|
+
if (!beforeSha.ok || !beforeSha.out) {
|
|
91
|
+
fail('Could not resolve current git SHA.')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
runOrThrow('git', ['fetch', '--tags', 'origin', '--quiet'])
|
|
95
|
+
|
|
96
|
+
let updateSource = 'main'
|
|
97
|
+
let pullOutput = ''
|
|
98
|
+
const latestTag = forceMain ? null : getLatestStableTag()
|
|
99
|
+
|
|
100
|
+
if (latestTag) {
|
|
101
|
+
const behind = run('git', ['rev-list', `HEAD..${latestTag}^{commit}`, '--count'])
|
|
102
|
+
const behindBy = Number.parseInt(behind.out || '0', 10) || 0
|
|
103
|
+
|
|
104
|
+
if (behindBy <= 0) {
|
|
105
|
+
log(`Already on latest stable release (${latestTag}) or newer.`)
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
updateSource = `stable release ${latestTag}`
|
|
110
|
+
log(`Found ${behindBy} commit(s) behind ${latestTag}. Updating now...`)
|
|
111
|
+
runOrThrow('git', ['checkout', '-B', 'stable', `${latestTag}^{commit}`])
|
|
112
|
+
pullOutput = `Updated to ${latestTag}`
|
|
113
|
+
} else {
|
|
114
|
+
runOrThrow('git', ['fetch', 'origin', 'main', '--quiet'])
|
|
115
|
+
const behind = run('git', ['rev-list', 'HEAD..origin/main', '--count'])
|
|
116
|
+
const behindBy = Number.parseInt(behind.out || '0', 10) || 0
|
|
117
|
+
|
|
118
|
+
if (behindBy <= 0) {
|
|
119
|
+
log('Already up to date. Nothing to install.')
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
updateSource = 'main branch'
|
|
124
|
+
log(`Found ${behindBy} new commit(s) on origin/main. Updating now...`)
|
|
125
|
+
runOrThrow('git', ['pull', '--ff-only', 'origin', 'main'])
|
|
126
|
+
pullOutput = `Pulled origin/main (+${behindBy})`
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const changed = run('git', ['diff', '--name-only', `${beforeSha.out}..HEAD`])
|
|
130
|
+
const changedFiles = new Set((changed.out || '').split('\n').map((s) => s.trim()).filter(Boolean))
|
|
131
|
+
const depsChanged = changedFiles.has('package.json') || changedFiles.has('package-lock.json')
|
|
132
|
+
|
|
133
|
+
if (depsChanged) {
|
|
134
|
+
runOrThrow('npm', ['install'])
|
|
135
|
+
} else {
|
|
136
|
+
log('No dependency changes detected. Skipping npm install.')
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!skipBuild) {
|
|
140
|
+
runOptional('node', ['./scripts/ensure-sandbox-browser-image.mjs'])
|
|
141
|
+
runOrThrow('npm', ['run', 'build'])
|
|
142
|
+
} else {
|
|
143
|
+
log('Skipping build step (--skip-build).')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
log('Update complete.')
|
|
147
|
+
log(`Source: ${updateSource}. ${pullOutput}`.trim())
|
|
148
|
+
log('Restart SwarmClaw to apply the new version.')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
main()
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import crypto from 'node:crypto'
|
|
6
|
+
import { spawnSync } from 'node:child_process'
|
|
7
|
+
|
|
8
|
+
const cwd = process.cwd()
|
|
9
|
+
const args = new Set(process.argv.slice(2))
|
|
10
|
+
const quiet = args.has('--quiet')
|
|
11
|
+
const required = args.has('--required')
|
|
12
|
+
const image = process.env.SWARMCLAW_SANDBOX_BROWSER_IMAGE || 'swarmclaw-sandbox-browser:bookworm-slim'
|
|
13
|
+
const SOURCE_LABEL = 'swarmclaw.sandboxBrowserSourceHash'
|
|
14
|
+
|
|
15
|
+
function log(message) {
|
|
16
|
+
if (!quiet) process.stdout.write(`[sandbox-browser] ${message}\n`)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function fail(message, code = 1) {
|
|
20
|
+
process.stderr.write(`[sandbox-browser] ERROR: ${message}\n`)
|
|
21
|
+
process.exit(code)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function run(command, commandArgs, options = {}) {
|
|
25
|
+
return spawnSync(command, commandArgs, {
|
|
26
|
+
cwd,
|
|
27
|
+
encoding: 'utf8',
|
|
28
|
+
...options,
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function commandExists(name) {
|
|
33
|
+
const lookup = process.platform === 'win32' ? 'where' : 'which'
|
|
34
|
+
const result = run(lookup, [name])
|
|
35
|
+
return !result.error && (result.status ?? 1) === 0
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function computeSourceHash() {
|
|
39
|
+
const hash = crypto.createHash('sha1')
|
|
40
|
+
for (const relative of ['Dockerfile.sandbox-browser', 'scripts/sandbox-browser-entrypoint.sh']) {
|
|
41
|
+
const absolute = path.join(cwd, relative)
|
|
42
|
+
if (!fs.existsSync(absolute)) {
|
|
43
|
+
fail(`Missing sandbox browser source file: ${relative}`)
|
|
44
|
+
}
|
|
45
|
+
hash.update(relative)
|
|
46
|
+
hash.update(fs.readFileSync(absolute))
|
|
47
|
+
}
|
|
48
|
+
return hash.digest('hex')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readImageLabel(name, label) {
|
|
52
|
+
const result = run('docker', ['image', 'inspect', '--format', `{{ index .Config.Labels "${label}" }}`, name])
|
|
53
|
+
if (result.error || (result.status ?? 1) !== 0) return null
|
|
54
|
+
const value = String(result.stdout || '').trim()
|
|
55
|
+
return value && value !== '<no value>' ? value : null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function buildImage(sourceHash) {
|
|
59
|
+
log(`Building sandbox browser image ${image}...`)
|
|
60
|
+
const result = spawnSync(
|
|
61
|
+
'docker',
|
|
62
|
+
[
|
|
63
|
+
'build',
|
|
64
|
+
'-f', 'Dockerfile.sandbox-browser',
|
|
65
|
+
'-t', image,
|
|
66
|
+
'--label', `${SOURCE_LABEL}=${sourceHash}`,
|
|
67
|
+
'.',
|
|
68
|
+
],
|
|
69
|
+
{
|
|
70
|
+
cwd,
|
|
71
|
+
stdio: 'inherit',
|
|
72
|
+
},
|
|
73
|
+
)
|
|
74
|
+
if (result.error || (result.status ?? 1) !== 0) {
|
|
75
|
+
if (required) {
|
|
76
|
+
fail(`Failed to build sandbox browser image ${image}.`, result.status ?? 1)
|
|
77
|
+
}
|
|
78
|
+
log(`Skipping sandbox browser image after build failure.`)
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
log(`Sandbox browser image ready: ${image}`)
|
|
82
|
+
return true
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function main() {
|
|
86
|
+
if (!commandExists('docker')) {
|
|
87
|
+
if (required) fail('Docker is required to build the sandbox browser image.')
|
|
88
|
+
log('Docker not available. Skipping sandbox browser image build.')
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const sourceHash = computeSourceHash()
|
|
93
|
+
const currentHash = readImageLabel(image, SOURCE_LABEL)
|
|
94
|
+
if (currentHash === sourceHash) {
|
|
95
|
+
log(`Sandbox browser image already current: ${image}`)
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
buildImage(sourceHash)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
main()
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
3
5
|
import { writeFileSync } from 'node:fs'
|
|
4
6
|
import { spawnSync } from 'node:child_process'
|
|
5
7
|
const INSTALL_METADATA_FILE = '.swarmclaw-install.json'
|
|
8
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
const packageRoot = path.resolve(scriptDir, '..')
|
|
10
|
+
const ensureSandboxBrowserScript = path.join(packageRoot, 'scripts', 'ensure-sandbox-browser-image.mjs')
|
|
6
11
|
|
|
7
12
|
function detectPackageManagerFromUserAgent(userAgent) {
|
|
8
13
|
const normalized = String(userAgent || '').toLowerCase()
|
|
@@ -15,6 +20,33 @@ function detectPackageManagerFromUserAgent(userAgent) {
|
|
|
15
20
|
|
|
16
21
|
const installedWith = detectPackageManagerFromUserAgent(process.env.npm_config_user_agent) || 'npm'
|
|
17
22
|
|
|
23
|
+
function logNote(message) {
|
|
24
|
+
process.stdout.write(`[postinstall] ${message}\n`)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function logWarn(message) {
|
|
28
|
+
process.stderr.write(`[postinstall] WARN: ${message}\n`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function commandExists(name) {
|
|
32
|
+
const lookup = process.platform === 'win32' ? 'where' : 'which'
|
|
33
|
+
const result = spawnSync(lookup, [name], {
|
|
34
|
+
cwd: packageRoot,
|
|
35
|
+
encoding: 'utf8',
|
|
36
|
+
stdio: 'pipe',
|
|
37
|
+
})
|
|
38
|
+
return !result.error && (result.status ?? 1) === 0
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function formatFailure(result) {
|
|
42
|
+
const detail = [
|
|
43
|
+
result.error?.message,
|
|
44
|
+
String(result.stderr || '').trim(),
|
|
45
|
+
String(result.stdout || '').trim(),
|
|
46
|
+
].find(Boolean)
|
|
47
|
+
return detail || `exit ${result.status ?? 1}`
|
|
48
|
+
}
|
|
49
|
+
|
|
18
50
|
try {
|
|
19
51
|
writeFileSync(
|
|
20
52
|
new URL(`../${INSTALL_METADATA_FILE}`, import.meta.url),
|
|
@@ -29,11 +61,30 @@ try {
|
|
|
29
61
|
}
|
|
30
62
|
|
|
31
63
|
const result = spawnSync('npm', ['rebuild', 'better-sqlite3', '--silent'], {
|
|
32
|
-
|
|
64
|
+
cwd: packageRoot,
|
|
65
|
+
encoding: 'utf8',
|
|
66
|
+
stdio: 'pipe',
|
|
33
67
|
})
|
|
34
68
|
|
|
35
|
-
if (result.error) {
|
|
36
|
-
|
|
69
|
+
if (result.error || (result.status ?? 0) !== 0) {
|
|
70
|
+
logWarn(`better-sqlite3 rebuild failed: ${formatFailure(result)}`)
|
|
71
|
+
logWarn('Retry manually with: npm rebuild better-sqlite3')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!process.env.CI) {
|
|
75
|
+
const sandboxImage = spawnSync(process.execPath, [ensureSandboxBrowserScript, '--quiet'], {
|
|
76
|
+
cwd: packageRoot,
|
|
77
|
+
encoding: 'utf8',
|
|
78
|
+
stdio: 'pipe',
|
|
79
|
+
})
|
|
80
|
+
if (sandboxImage.error || (sandboxImage.status ?? 0) !== 0) {
|
|
81
|
+
logWarn(`sandbox browser image setup failed: ${formatFailure(sandboxImage)}`)
|
|
82
|
+
logWarn('Retry manually with: node ./scripts/ensure-sandbox-browser-image.mjs')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!commandExists('docker')) {
|
|
86
|
+
logNote('Docker was not found. Container sandboxes will fall back to host execution until Docker is installed.')
|
|
87
|
+
}
|
|
37
88
|
}
|
|
38
89
|
|
|
39
90
|
if (!process.env.CI) {
|