@swarmclawai/swarmclaw 1.0.0 → 1.0.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.
- package/README.md +15 -2
- package/bin/doctor-cmd.js +155 -0
- package/bin/doctor-cmd.test.js +50 -0
- package/bin/install-root.js +39 -0
- package/bin/install-root.test.js +60 -0
- package/bin/server-cmd.js +160 -38
- package/bin/swarmclaw.js +83 -3
- package/bin/update-cmd.js +1 -6
- package/bin/update-cmd.test.js +1 -36
- package/bin/worker-cmd.js +13 -6
- package/package.json +16 -15
- package/scripts/postinstall.mjs +17 -13
- package/src/app/api/gateways/[id]/health/route.ts +2 -32
- package/src/app/api/gateways/health-route.test.ts +1 -1
- package/src/app/api/setup/check-provider/helpers.ts +28 -0
- package/src/app/api/setup/check-provider/route.test.ts +1 -1
- package/src/app/api/setup/check-provider/route.ts +5 -32
- package/src/app/api/tasks/import/github/helpers.ts +100 -0
- package/src/app/api/tasks/import/github/route.test.ts +1 -1
- package/src/app/api/tasks/import/github/route.ts +2 -92
- package/src/app/api/webhooks/[id]/helpers.ts +253 -0
- package/src/app/api/webhooks/[id]/route.ts +2 -243
- package/src/app/api/webhooks/route.test.ts +4 -2
- package/src/app/usage/page.tsx +22 -12
- package/src/cli/binary.test.js +57 -0
- package/src/cli/index.js +13 -1
- package/src/cli/server-cmd.test.js +77 -0
- package/src/lib/server/data-dir.test.ts +38 -3
- package/src/lib/server/data-dir.ts +11 -0
- package/src/lib/server/openclaw/health.ts +30 -1
- package/src/lib/server/session-tools/file-send.test.ts +18 -2
- package/src/lib/server/session-tools/file.ts +11 -7
- package/src/lib/server/skills/skill-discovery.test.ts +34 -1
- package/src/lib/server/skills/skill-discovery.ts +9 -4
package/README.md
CHANGED
|
@@ -136,6 +136,17 @@ Running `swarmclaw` with no arguments starts the server on `http://localhost:345
|
|
|
136
136
|
Global install runs `postinstall`, which rebuilds `better-sqlite3` and prepares the sandbox browser image when Docker is available.
|
|
137
137
|
If Docker is not installed yet, SwarmClaw keeps running and falls back to host execution for shell, browser, and `sandbox_exec`.
|
|
138
138
|
No Deno install is required for the local `sandbox_exec` path.
|
|
139
|
+
Runtime state defaults to `~/.swarmclaw` unless you set `SWARMCLAW_HOME`.
|
|
140
|
+
If the standalone server bundle is missing, the first launch builds it under `<swarmclaw-home>/builds/package-<version>` before starting.
|
|
141
|
+
|
|
142
|
+
### Local project install
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
npm i @swarmclawai/swarmclaw
|
|
146
|
+
npx swarmclaw
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Local installs keep runtime state in `<project>/.swarmclaw` by default. The same project-local install works with `pnpm`, `yarn`, or `bun`; use that package manager's exec command to launch the local binary, or run `./node_modules/.bin/swarmclaw` directly.
|
|
139
150
|
|
|
140
151
|
### One-off run
|
|
141
152
|
|
|
@@ -146,6 +157,8 @@ yarn dlx @swarmclawai/swarmclaw
|
|
|
146
157
|
bunx @swarmclawai/swarmclaw
|
|
147
158
|
```
|
|
148
159
|
|
|
160
|
+
One-off runs use the published package without keeping a project-local install. Runtime state defaults to `~/.swarmclaw` unless you set `SWARMCLAW_HOME`.
|
|
161
|
+
|
|
149
162
|
### Install script
|
|
150
163
|
|
|
151
164
|
```bash
|
|
@@ -155,7 +168,7 @@ curl -fsSL https://raw.githubusercontent.com/swarmclawai/swarmclaw/main/install.
|
|
|
155
168
|
The installer resolves the latest stable release tag and installs that version by default.
|
|
156
169
|
It also builds the production bundle so `npm run start` is ready immediately after install.
|
|
157
170
|
No Deno install is required; local sandbox execution is Docker-first with automatic host Node fallback.
|
|
158
|
-
To pin a version: `SWARMCLAW_VERSION=
|
|
171
|
+
To pin a version: `SWARMCLAW_VERSION=v1.0.3 curl ... | bash`
|
|
159
172
|
|
|
160
173
|
Or run locally from the repo (friendly for non-technical users):
|
|
161
174
|
|
|
@@ -603,7 +616,7 @@ A fuller step-by-step walkthrough lives at https://swarmclaw.ai/docs/plugin-tuto
|
|
|
603
616
|
|
|
604
617
|
### Browser, Watch, and Delegation Upgrades
|
|
605
618
|
|
|
606
|
-
- **Persistent Browser Profiles**: The built-in `browser` plugin now keeps a reusable profile per chat/session, and subagents inherit the parent profile by default. Profiles live under `~/.swarmclaw/browser-profiles` unless you override `BROWSER_PROFILES_DIR`, so cookies, storage, and authenticated state survive longer-running work without polluting the project tree. Browser state is exposed at `GET /api/chats/[id]/browser`.
|
|
619
|
+
- **Persistent Browser Profiles**: The built-in `browser` plugin now keeps a reusable profile per chat/session, and subagents inherit the parent profile by default. Profiles live under `<swarmclaw-home>/browser-profiles` by default (`~/.swarmclaw/browser-profiles` for global installs, `<project>/.swarmclaw/browser-profiles` for local installs) unless you override `BROWSER_PROFILES_DIR`, so cookies, storage, and authenticated state survive longer-running work without polluting the project tree. Browser state is exposed at `GET /api/chats/[id]/browser`.
|
|
607
620
|
- **Higher-Level Browser Actions**: In addition to raw Playwright-style actions, `browser` supports workflow-oriented actions such as `read_page`, `extract_links`, `extract_form_fields`, `extract_table`, `fill_form`, `submit_form`, `scroll_until`, `download_file`, `complete_web_task`, `verify_text`, `verify_element`, `verify_list`, `verify_value`, `profile`, and `reset_profile`.
|
|
608
621
|
- **Structured Browser State**: Browser sessions persist recent observations, tabs, artifacts (screenshots / PDFs / downloads), current URL, and last errors in `browser_sessions`, which makes autonomous browser tasks easier to resume, inspect, and hand off across turns.
|
|
609
622
|
- **Durable Watches**: `schedule_wake` now uses persisted watch jobs instead of an in-memory timer, and `monitor_tool` supports `create_watch`, `list_watches`, `get_watch`, and `cancel_watch` across `time`, `http`, `file`, `task`, `webhook`, and `page` conditions. The same watch system also powers the new `mailbox`, session-mailbox, and approval waits used by human-loop flows. Watches support common checks like status/status sets, regex or text matches, content changes, existence checks, inbound mailbox correlation IDs, and webhook event filters.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
5
|
+
const fs = require('node:fs')
|
|
6
|
+
const path = require('node:path')
|
|
7
|
+
|
|
8
|
+
const { readPackageVersion } = require('./install-root.js')
|
|
9
|
+
const {
|
|
10
|
+
PKG_ROOT,
|
|
11
|
+
SWARMCLAW_HOME,
|
|
12
|
+
findStandaloneServer,
|
|
13
|
+
isGitCheckout,
|
|
14
|
+
resolvePackageBuildRoot,
|
|
15
|
+
resolveInstalledNext,
|
|
16
|
+
} = require('./server-cmd.js')
|
|
17
|
+
|
|
18
|
+
function readPid(pidFile) {
|
|
19
|
+
try {
|
|
20
|
+
const pid = Number.parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10)
|
|
21
|
+
return Number.isFinite(pid) ? pid : null
|
|
22
|
+
} catch {
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isProcessRunning(pid) {
|
|
28
|
+
try {
|
|
29
|
+
process.kill(pid, 0)
|
|
30
|
+
return true
|
|
31
|
+
} catch {
|
|
32
|
+
return false
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildDoctorReport(opts = {}) {
|
|
37
|
+
const pkgRoot = opts.pkgRoot || PKG_ROOT
|
|
38
|
+
const homeDir = opts.homeDir || SWARMCLAW_HOME
|
|
39
|
+
const pidFile = path.join(homeDir, 'server.pid')
|
|
40
|
+
const dataDir = path.join(homeDir, 'data')
|
|
41
|
+
const workspaceDir = path.join(homeDir, 'workspace')
|
|
42
|
+
const browserProfilesDir = path.join(homeDir, 'browser-profiles')
|
|
43
|
+
const nextInstall = resolveInstalledNext(pkgRoot)
|
|
44
|
+
const nextCliPath = nextInstall?.nextCli || path.join(pkgRoot, 'node_modules', 'next', 'dist', 'bin', 'next')
|
|
45
|
+
const standaloneServer = findStandaloneServer({ pkgRoot })
|
|
46
|
+
const buildRoot = resolvePackageBuildRoot(pkgRoot)
|
|
47
|
+
const pid = readPid(pidFile)
|
|
48
|
+
const running = pid ? isProcessRunning(pid) : false
|
|
49
|
+
|
|
50
|
+
const serverState = !pid
|
|
51
|
+
? 'not-running'
|
|
52
|
+
: running
|
|
53
|
+
? 'running'
|
|
54
|
+
: 'stale-pid'
|
|
55
|
+
|
|
56
|
+
const recommendations = []
|
|
57
|
+
if (!standaloneServer) {
|
|
58
|
+
if (fs.existsSync(nextCliPath)) {
|
|
59
|
+
recommendations.push('Standalone bundle is missing. Run `swarmclaw run` to build it automatically or `swarmclaw server --build` to prebuild it now.')
|
|
60
|
+
} else {
|
|
61
|
+
recommendations.push('Next.js build dependencies are missing from this install. Reinstall the package before starting SwarmClaw.')
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (serverState === 'stale-pid') {
|
|
65
|
+
recommendations.push('A stale PID file was found. Run `swarmclaw stop` to clean it up.')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
packageVersion: readPackageVersion(pkgRoot) || 'unknown',
|
|
70
|
+
packageRoot: pkgRoot,
|
|
71
|
+
buildRoot,
|
|
72
|
+
installKind: isGitCheckout(pkgRoot) ? 'git' : 'package',
|
|
73
|
+
homeDir,
|
|
74
|
+
dataDir,
|
|
75
|
+
workspaceDir,
|
|
76
|
+
browserProfilesDir,
|
|
77
|
+
server: {
|
|
78
|
+
state: serverState,
|
|
79
|
+
pid,
|
|
80
|
+
pidFile,
|
|
81
|
+
},
|
|
82
|
+
build: {
|
|
83
|
+
standaloneServer,
|
|
84
|
+
nextCliPresent: fs.existsSync(nextCliPath),
|
|
85
|
+
nextCliPath,
|
|
86
|
+
},
|
|
87
|
+
recommendations,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function printHelp() {
|
|
92
|
+
process.stdout.write(`
|
|
93
|
+
Usage: swarmclaw doctor [--json]
|
|
94
|
+
|
|
95
|
+
Show local installation and build diagnostics for SwarmClaw.
|
|
96
|
+
`.trim() + '\n')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function printHumanReport(report) {
|
|
100
|
+
const lines = [
|
|
101
|
+
`Package version: ${report.packageVersion}`,
|
|
102
|
+
`Install kind: ${report.installKind}`,
|
|
103
|
+
`Package root: ${report.packageRoot}`,
|
|
104
|
+
`Build root: ${report.buildRoot}`,
|
|
105
|
+
`Home: ${report.homeDir}`,
|
|
106
|
+
`Data: ${report.dataDir}`,
|
|
107
|
+
`Workspace: ${report.workspaceDir}`,
|
|
108
|
+
`Browser profiles: ${report.browserProfilesDir}`,
|
|
109
|
+
`Server: ${report.server.state}${report.server.pid ? ` (PID: ${report.server.pid})` : ''}`,
|
|
110
|
+
`Standalone bundle: ${report.build.standaloneServer ? `yes (${report.build.standaloneServer})` : 'no'}`,
|
|
111
|
+
`Next CLI available: ${report.build.nextCliPresent ? 'yes' : 'no'}`,
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
if (report.recommendations.length > 0) {
|
|
115
|
+
lines.push('', 'Recommendations:')
|
|
116
|
+
for (const recommendation of report.recommendations) {
|
|
117
|
+
lines.push(`- ${recommendation}`)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
process.stdout.write(`${lines.join('\n')}\n`)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function main(args = process.argv.slice(3)) {
|
|
125
|
+
const json = args.includes('--json')
|
|
126
|
+
if (args.includes('-h') || args.includes('--help')) {
|
|
127
|
+
printHelp()
|
|
128
|
+
process.exit(0)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const unknown = args.filter((arg) => arg !== '--json')
|
|
132
|
+
if (unknown.length > 0) {
|
|
133
|
+
process.stderr.write(`[swarmclaw] Unknown argument: ${unknown[0]}\n`)
|
|
134
|
+
printHelp()
|
|
135
|
+
process.exit(1)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const report = buildDoctorReport()
|
|
139
|
+
if (json) {
|
|
140
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`)
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
printHumanReport(report)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (require.main === module) {
|
|
147
|
+
main()
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = {
|
|
151
|
+
buildDoctorReport,
|
|
152
|
+
isProcessRunning,
|
|
153
|
+
main,
|
|
154
|
+
readPid,
|
|
155
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
3
|
+
|
|
4
|
+
const test = require('node:test')
|
|
5
|
+
const assert = require('node:assert/strict')
|
|
6
|
+
const fs = require('node:fs')
|
|
7
|
+
const os = require('node:os')
|
|
8
|
+
const path = require('node:path')
|
|
9
|
+
|
|
10
|
+
const { buildDoctorReport } = require('./doctor-cmd.js')
|
|
11
|
+
|
|
12
|
+
test('buildDoctorReport recommends a local build when standalone output is missing', () => {
|
|
13
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-doctor-'))
|
|
14
|
+
const pkgRoot = path.join(tempDir, 'pkg')
|
|
15
|
+
const homeDir = path.join(tempDir, '.swarmclaw')
|
|
16
|
+
const nextCli = path.join(pkgRoot, 'node_modules', 'next', 'dist', 'bin', 'next')
|
|
17
|
+
|
|
18
|
+
fs.mkdirSync(path.dirname(nextCli), { recursive: true })
|
|
19
|
+
fs.mkdirSync(homeDir, { recursive: true })
|
|
20
|
+
fs.writeFileSync(path.join(pkgRoot, 'package.json'), JSON.stringify({ name: '@swarmclawai/swarmclaw', version: '1.0.1' }), 'utf8')
|
|
21
|
+
fs.writeFileSync(nextCli, '#!/usr/bin/env node\n', 'utf8')
|
|
22
|
+
|
|
23
|
+
const report = buildDoctorReport({ pkgRoot, homeDir })
|
|
24
|
+
|
|
25
|
+
assert.equal(report.installKind, 'package')
|
|
26
|
+
assert.equal(report.build.nextCliPresent, true)
|
|
27
|
+
assert.equal(report.build.standaloneServer, null)
|
|
28
|
+
assert.match(report.recommendations.join('\n'), /swarmclaw run/)
|
|
29
|
+
|
|
30
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('buildDoctorReport flags stale PID files', () => {
|
|
34
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-doctor-stale-'))
|
|
35
|
+
const pkgRoot = path.join(tempDir, 'pkg')
|
|
36
|
+
const homeDir = path.join(tempDir, '.swarmclaw')
|
|
37
|
+
const pidFile = path.join(homeDir, 'server.pid')
|
|
38
|
+
|
|
39
|
+
fs.mkdirSync(homeDir, { recursive: true })
|
|
40
|
+
fs.mkdirSync(pkgRoot, { recursive: true })
|
|
41
|
+
fs.writeFileSync(path.join(pkgRoot, 'package.json'), JSON.stringify({ name: '@swarmclawai/swarmclaw', version: '1.0.1' }), 'utf8')
|
|
42
|
+
fs.writeFileSync(pidFile, '999999\n', 'utf8')
|
|
43
|
+
|
|
44
|
+
const report = buildDoctorReport({ pkgRoot, homeDir })
|
|
45
|
+
|
|
46
|
+
assert.equal(report.server.state, 'stale-pid')
|
|
47
|
+
assert.match(report.recommendations.join('\n'), /swarmclaw stop/)
|
|
48
|
+
|
|
49
|
+
fs.rmSync(tempDir, { recursive: true, force: true })
|
|
50
|
+
})
|
package/bin/install-root.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict'
|
|
3
3
|
|
|
4
|
+
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
4
5
|
const fs = require('node:fs')
|
|
5
6
|
const os = require('node:os')
|
|
6
7
|
const path = require('node:path')
|
|
@@ -143,13 +144,51 @@ function detectGlobalInstallManagerForRoot(pkgRoot, execImpl = execFileSync, env
|
|
|
143
144
|
return null
|
|
144
145
|
}
|
|
145
146
|
|
|
147
|
+
function findLocalInstallProjectRoot(pkgRoot) {
|
|
148
|
+
const normalized = normalizeDir(pkgRoot)
|
|
149
|
+
if (!normalized) return null
|
|
150
|
+
|
|
151
|
+
const marker = `${path.sep}node_modules${path.sep}`
|
|
152
|
+
const idx = normalized.indexOf(marker)
|
|
153
|
+
if (idx === -1) return null
|
|
154
|
+
|
|
155
|
+
const projectRoot = normalized.slice(0, idx)
|
|
156
|
+
return projectRoot ? path.resolve(projectRoot) : path.parse(normalized).root
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function resolveStateHome(opts = {}) {
|
|
160
|
+
const env = opts.env || process.env
|
|
161
|
+
const explicitHome = normalizeDir(env.SWARMCLAW_HOME)
|
|
162
|
+
if (explicitHome) return explicitHome
|
|
163
|
+
|
|
164
|
+
const pkgRoot = normalizeDir(opts.pkgRoot)
|
|
165
|
+
|| resolvePackageRoot({
|
|
166
|
+
moduleDir: opts.moduleDir,
|
|
167
|
+
argv1: opts.argv1,
|
|
168
|
+
cwd: opts.cwd,
|
|
169
|
+
})
|
|
170
|
+
if (!pkgRoot) return path.join(os.homedir(), '.swarmclaw')
|
|
171
|
+
|
|
172
|
+
const execImpl = opts.execImpl || execFileSync
|
|
173
|
+
if (detectGlobalInstallManagerForRoot(pkgRoot, execImpl, env)) {
|
|
174
|
+
return path.join(os.homedir(), '.swarmclaw')
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const projectRoot = findLocalInstallProjectRoot(pkgRoot)
|
|
178
|
+
if (projectRoot) return path.join(projectRoot, '.swarmclaw')
|
|
179
|
+
|
|
180
|
+
return path.join(os.homedir(), '.swarmclaw')
|
|
181
|
+
}
|
|
182
|
+
|
|
146
183
|
module.exports = {
|
|
147
184
|
PACKAGE_NAME,
|
|
148
185
|
candidateDirsFromArgv1,
|
|
149
186
|
detectGlobalInstallManagerForRoot,
|
|
150
187
|
findPackageRoot,
|
|
188
|
+
findLocalInstallProjectRoot,
|
|
151
189
|
readPackageName,
|
|
152
190
|
readPackageVersion,
|
|
153
191
|
resolveGlobalRoot,
|
|
154
192
|
resolvePackageRoot,
|
|
193
|
+
resolveStateHome,
|
|
155
194
|
}
|
package/bin/install-root.test.js
CHANGED
|
@@ -10,7 +10,9 @@ const path = require('node:path')
|
|
|
10
10
|
const {
|
|
11
11
|
candidateDirsFromArgv1,
|
|
12
12
|
detectGlobalInstallManagerForRoot,
|
|
13
|
+
findLocalInstallProjectRoot,
|
|
13
14
|
resolvePackageRoot,
|
|
15
|
+
resolveStateHome,
|
|
14
16
|
} = require('./install-root.js')
|
|
15
17
|
|
|
16
18
|
test('candidateDirsFromArgv1 includes the package directory for node_modules/.bin launchers', () => {
|
|
@@ -59,3 +61,61 @@ test('detectGlobalInstallManagerForRoot matches the owning global root by realpa
|
|
|
59
61
|
|
|
60
62
|
fs.rmSync(rootDir, { recursive: true, force: true })
|
|
61
63
|
})
|
|
64
|
+
|
|
65
|
+
test('findLocalInstallProjectRoot returns the project root for nested pnpm installs', () => {
|
|
66
|
+
const pkgRoot = path.join(
|
|
67
|
+
'/tmp',
|
|
68
|
+
'example',
|
|
69
|
+
'node_modules',
|
|
70
|
+
'.pnpm',
|
|
71
|
+
'@swarmclawai+swarmclaw@1.0.1',
|
|
72
|
+
'node_modules',
|
|
73
|
+
'@swarmclawai',
|
|
74
|
+
'swarmclaw',
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
assert.equal(findLocalInstallProjectRoot(pkgRoot), path.join('/tmp', 'example'))
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('resolveStateHome prefers the local project .swarmclaw directory for local installs', () => {
|
|
81
|
+
const projectRoot = path.join('/tmp', 'example')
|
|
82
|
+
const pkgRoot = path.join(projectRoot, 'node_modules', '@swarmclawai', 'swarmclaw')
|
|
83
|
+
const execImpl = () => {
|
|
84
|
+
throw new Error('unexpected global root lookup')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
assert.equal(
|
|
88
|
+
resolveStateHome({
|
|
89
|
+
pkgRoot,
|
|
90
|
+
env: {},
|
|
91
|
+
execImpl,
|
|
92
|
+
}),
|
|
93
|
+
path.join(projectRoot, '.swarmclaw'),
|
|
94
|
+
)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
test('resolveStateHome keeps global installs under the user home directory', () => {
|
|
98
|
+
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-state-home-'))
|
|
99
|
+
const npmGlobalRoot = path.join(rootDir, 'npm-global')
|
|
100
|
+
const pkgRoot = path.join(npmGlobalRoot, '@swarmclawai', 'swarmclaw')
|
|
101
|
+
|
|
102
|
+
fs.mkdirSync(path.join(npmGlobalRoot, '@swarmclawai'), { recursive: true })
|
|
103
|
+
fs.mkdirSync(pkgRoot, { recursive: true })
|
|
104
|
+
|
|
105
|
+
const execImpl = (command, args) => {
|
|
106
|
+
if (command === 'npm' && args.join(' ') === 'root -g') return npmGlobalRoot
|
|
107
|
+
if (command === 'pnpm' && args.join(' ') === 'root -g') return path.join(rootDir, 'pnpm-global')
|
|
108
|
+
throw new Error(`unexpected command: ${command} ${args.join(' ')}`)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
assert.equal(
|
|
112
|
+
resolveStateHome({
|
|
113
|
+
pkgRoot,
|
|
114
|
+
env: {},
|
|
115
|
+
execImpl,
|
|
116
|
+
}),
|
|
117
|
+
path.join(os.homedir(), '.swarmclaw'),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
fs.rmSync(rootDir, { recursive: true, force: true })
|
|
121
|
+
})
|