@swarmclawai/swarmclaw 0.9.9 → 1.0.2

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 (44) hide show
  1. package/bin/doctor-cmd.js +149 -0
  2. package/bin/doctor-cmd.test.js +50 -0
  3. package/bin/install-root.js +194 -0
  4. package/bin/install-root.test.js +121 -0
  5. package/bin/server-cmd.js +90 -111
  6. package/bin/swarmclaw.js +83 -3
  7. package/bin/update-cmd.js +33 -20
  8. package/bin/update-cmd.test.js +1 -36
  9. package/bin/worker-cmd.js +23 -17
  10. package/next.config.ts +2 -0
  11. package/package.json +11 -10
  12. package/src/app/api/gateways/[id]/health/route.ts +2 -32
  13. package/src/app/api/gateways/health-route.test.ts +1 -1
  14. package/src/app/api/openclaw/dashboard-url/route.test.ts +166 -0
  15. package/src/app/api/openclaw/dashboard-url/route.ts +68 -0
  16. package/src/app/api/setup/check-provider/helpers.ts +28 -0
  17. package/src/app/api/setup/check-provider/route.test.ts +17 -1
  18. package/src/app/api/setup/check-provider/route.ts +29 -36
  19. package/src/app/api/tasks/import/github/helpers.ts +100 -0
  20. package/src/app/api/tasks/import/github/route.test.ts +1 -1
  21. package/src/app/api/tasks/import/github/route.ts +2 -92
  22. package/src/app/api/webhooks/[id]/helpers.ts +253 -0
  23. package/src/app/api/webhooks/[id]/route.ts +2 -243
  24. package/src/app/api/webhooks/route.test.ts +4 -2
  25. package/src/cli/binary.test.js +57 -0
  26. package/src/cli/index.js +14 -1
  27. package/src/cli/server-cmd.test.js +21 -20
  28. package/src/components/auth/setup-wizard/index.tsx +16 -0
  29. package/src/components/auth/setup-wizard/step-agents.tsx +34 -23
  30. package/src/components/auth/setup-wizard/step-connect.tsx +8 -0
  31. package/src/components/auth/setup-wizard/types.ts +2 -0
  32. package/src/components/auth/setup-wizard/utils.test.ts +79 -0
  33. package/src/components/chat/chat-header.tsx +45 -2
  34. package/src/lib/providers/openclaw-exports.test.ts +23 -0
  35. package/src/lib/providers/openclaw.ts +1 -1
  36. package/src/lib/server/data-dir.test.ts +35 -0
  37. package/src/lib/server/data-dir.ts +11 -0
  38. package/src/lib/server/openclaw/health.ts +30 -1
  39. package/src/lib/server/session-tools/file-send.test.ts +18 -2
  40. package/src/lib/server/session-tools/file.ts +11 -7
  41. package/src/lib/server/skills/skill-discovery.test.ts +34 -1
  42. package/src/lib/server/skills/skill-discovery.ts +9 -4
  43. package/src/lib/setup-defaults.test.ts +42 -0
  44. package/src/lib/setup-defaults.ts +1 -1
@@ -0,0 +1,149 @@
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
+ } = require('./server-cmd.js')
15
+
16
+ function readPid(pidFile) {
17
+ try {
18
+ const pid = Number.parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10)
19
+ return Number.isFinite(pid) ? pid : null
20
+ } catch {
21
+ return null
22
+ }
23
+ }
24
+
25
+ function isProcessRunning(pid) {
26
+ try {
27
+ process.kill(pid, 0)
28
+ return true
29
+ } catch {
30
+ return false
31
+ }
32
+ }
33
+
34
+ function buildDoctorReport(opts = {}) {
35
+ const pkgRoot = opts.pkgRoot || PKG_ROOT
36
+ const homeDir = opts.homeDir || SWARMCLAW_HOME
37
+ const pidFile = path.join(homeDir, 'server.pid')
38
+ const dataDir = path.join(homeDir, 'data')
39
+ const workspaceDir = path.join(homeDir, 'workspace')
40
+ const browserProfilesDir = path.join(homeDir, 'browser-profiles')
41
+ const nextCliPath = path.join(pkgRoot, 'node_modules', 'next', 'dist', 'bin', 'next')
42
+ const standaloneServer = findStandaloneServer({ pkgRoot })
43
+ const pid = readPid(pidFile)
44
+ const running = pid ? isProcessRunning(pid) : false
45
+
46
+ const serverState = !pid
47
+ ? 'not-running'
48
+ : running
49
+ ? 'running'
50
+ : 'stale-pid'
51
+
52
+ const recommendations = []
53
+ if (!standaloneServer) {
54
+ if (fs.existsSync(nextCliPath)) {
55
+ recommendations.push('Standalone bundle is missing. Run `swarmclaw run` to build it automatically or `swarmclaw server --build` to prebuild it now.')
56
+ } else {
57
+ recommendations.push('Next.js build dependencies are missing from this install. Reinstall the package before starting SwarmClaw.')
58
+ }
59
+ }
60
+ if (serverState === 'stale-pid') {
61
+ recommendations.push('A stale PID file was found. Run `swarmclaw stop` to clean it up.')
62
+ }
63
+
64
+ return {
65
+ packageVersion: readPackageVersion(pkgRoot) || 'unknown',
66
+ packageRoot: pkgRoot,
67
+ installKind: isGitCheckout(pkgRoot) ? 'git' : 'package',
68
+ homeDir,
69
+ dataDir,
70
+ workspaceDir,
71
+ browserProfilesDir,
72
+ server: {
73
+ state: serverState,
74
+ pid,
75
+ pidFile,
76
+ },
77
+ build: {
78
+ standaloneServer,
79
+ nextCliPresent: fs.existsSync(nextCliPath),
80
+ nextCliPath,
81
+ },
82
+ recommendations,
83
+ }
84
+ }
85
+
86
+ function printHelp() {
87
+ process.stdout.write(`
88
+ Usage: swarmclaw doctor [--json]
89
+
90
+ Show local installation and build diagnostics for SwarmClaw.
91
+ `.trim() + '\n')
92
+ }
93
+
94
+ function printHumanReport(report) {
95
+ const lines = [
96
+ `Package version: ${report.packageVersion}`,
97
+ `Install kind: ${report.installKind}`,
98
+ `Package root: ${report.packageRoot}`,
99
+ `Home: ${report.homeDir}`,
100
+ `Data: ${report.dataDir}`,
101
+ `Workspace: ${report.workspaceDir}`,
102
+ `Browser profiles: ${report.browserProfilesDir}`,
103
+ `Server: ${report.server.state}${report.server.pid ? ` (PID: ${report.server.pid})` : ''}`,
104
+ `Standalone bundle: ${report.build.standaloneServer ? `yes (${report.build.standaloneServer})` : 'no'}`,
105
+ `Next CLI available: ${report.build.nextCliPresent ? 'yes' : 'no'}`,
106
+ ]
107
+
108
+ if (report.recommendations.length > 0) {
109
+ lines.push('', 'Recommendations:')
110
+ for (const recommendation of report.recommendations) {
111
+ lines.push(`- ${recommendation}`)
112
+ }
113
+ }
114
+
115
+ process.stdout.write(`${lines.join('\n')}\n`)
116
+ }
117
+
118
+ function main(args = process.argv.slice(3)) {
119
+ const json = args.includes('--json')
120
+ if (args.includes('-h') || args.includes('--help')) {
121
+ printHelp()
122
+ process.exit(0)
123
+ }
124
+
125
+ const unknown = args.filter((arg) => arg !== '--json')
126
+ if (unknown.length > 0) {
127
+ process.stderr.write(`[swarmclaw] Unknown argument: ${unknown[0]}\n`)
128
+ printHelp()
129
+ process.exit(1)
130
+ }
131
+
132
+ const report = buildDoctorReport()
133
+ if (json) {
134
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`)
135
+ return
136
+ }
137
+ printHumanReport(report)
138
+ }
139
+
140
+ if (require.main === module) {
141
+ main()
142
+ }
143
+
144
+ module.exports = {
145
+ buildDoctorReport,
146
+ isProcessRunning,
147
+ main,
148
+ readPid,
149
+ }
@@ -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
+ })
@@ -0,0 +1,194 @@
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 os = require('node:os')
7
+ const path = require('node:path')
8
+ const { execFileSync } = require('node:child_process')
9
+
10
+ const PACKAGE_NAME = '@swarmclawai/swarmclaw'
11
+ const CORE_PACKAGE_NAMES = new Set([PACKAGE_NAME])
12
+
13
+ function normalizeDir(value) {
14
+ if (!value) return null
15
+ const trimmed = String(value).trim()
16
+ if (!trimmed) return null
17
+ return path.resolve(trimmed)
18
+ }
19
+
20
+ function readPackageJson(rootDir) {
21
+ try {
22
+ return JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf8'))
23
+ } catch {
24
+ return null
25
+ }
26
+ }
27
+
28
+ function readPackageName(rootDir) {
29
+ return readPackageJson(rootDir)?.name?.trim() || null
30
+ }
31
+
32
+ function readPackageVersion(rootDir) {
33
+ const version = readPackageJson(rootDir)?.version
34
+ return typeof version === 'string' && version.trim() ? version.trim() : null
35
+ }
36
+
37
+ function* iterAncestorDirs(startDir, maxDepth = 12) {
38
+ let current = path.resolve(startDir)
39
+ for (let i = 0; i < maxDepth; i += 1) {
40
+ yield current
41
+ const parent = path.dirname(current)
42
+ if (parent === current) break
43
+ current = parent
44
+ }
45
+ }
46
+
47
+ function findPackageRoot(startDir, maxDepth = 12) {
48
+ for (const current of iterAncestorDirs(startDir, maxDepth)) {
49
+ const name = readPackageName(current)
50
+ if (name && CORE_PACKAGE_NAMES.has(name)) return current
51
+ }
52
+ return null
53
+ }
54
+
55
+ function candidateDirsFromArgv1(argv1) {
56
+ const normalized = normalizeDir(argv1)
57
+ if (!normalized) return []
58
+
59
+ const candidates = [path.dirname(normalized)]
60
+ try {
61
+ const resolved = fs.realpathSync(normalized)
62
+ if (resolved !== normalized) candidates.push(path.dirname(resolved))
63
+ } catch {}
64
+
65
+ const parts = normalized.split(path.sep)
66
+ const binIndex = parts.lastIndexOf('.bin')
67
+ if (binIndex > 0 && parts[binIndex - 1] === 'node_modules') {
68
+ const binName = path.basename(normalized)
69
+ const nodeModulesDir = parts.slice(0, binIndex).join(path.sep)
70
+ candidates.push(path.join(nodeModulesDir, binName))
71
+ }
72
+
73
+ return candidates
74
+ }
75
+
76
+ function resolvePackageRoot(opts = {}) {
77
+ const candidates = []
78
+ const moduleDir = normalizeDir(opts.moduleDir)
79
+ if (moduleDir) candidates.push(moduleDir)
80
+ const argv1 = opts.argv1 === undefined ? process.argv[1] : opts.argv1
81
+ candidates.push(...candidateDirsFromArgv1(argv1))
82
+ const cwd = opts.cwd === undefined ? process.cwd() : opts.cwd
83
+ if (normalizeDir(cwd)) candidates.push(path.resolve(cwd))
84
+
85
+ for (const candidate of candidates) {
86
+ const found = findPackageRoot(candidate)
87
+ if (found) return found
88
+ }
89
+
90
+ return moduleDir ? path.resolve(moduleDir, '..') : null
91
+ }
92
+
93
+ function tryRealpath(targetPath) {
94
+ try {
95
+ return fs.realpathSync(targetPath)
96
+ } catch {
97
+ return path.resolve(targetPath)
98
+ }
99
+ }
100
+
101
+ function runRootCommand(command, args, execImpl = execFileSync) {
102
+ try {
103
+ return String(execImpl(command, args, {
104
+ encoding: 'utf8',
105
+ stdio: ['ignore', 'pipe', 'pipe'],
106
+ })).trim()
107
+ } catch {
108
+ return null
109
+ }
110
+ }
111
+
112
+ function resolveGlobalRoot(manager, execImpl = execFileSync, env = process.env) {
113
+ if (manager === 'bun') {
114
+ const bunInstall = String(env.BUN_INSTALL || '').trim() || path.join(os.homedir(), '.bun')
115
+ return path.join(bunInstall, 'install', 'global', 'node_modules')
116
+ }
117
+
118
+ if (manager === 'pnpm') {
119
+ return runRootCommand('pnpm', ['root', '-g'], execImpl)
120
+ }
121
+
122
+ return runRootCommand('npm', ['root', '-g'], execImpl)
123
+ }
124
+
125
+ function detectGlobalInstallManagerForRoot(pkgRoot, execImpl = execFileSync, env = process.env) {
126
+ const pkgReal = tryRealpath(pkgRoot)
127
+
128
+ for (const manager of ['npm', 'pnpm']) {
129
+ const globalRoot = resolveGlobalRoot(manager, execImpl, env)
130
+ if (!globalRoot) continue
131
+
132
+ for (const name of CORE_PACKAGE_NAMES) {
133
+ const expectedReal = tryRealpath(path.join(globalRoot, name))
134
+ if (path.resolve(expectedReal) === path.resolve(pkgReal)) return manager
135
+ }
136
+ }
137
+
138
+ const bunRoot = resolveGlobalRoot('bun', execImpl, env)
139
+ for (const name of CORE_PACKAGE_NAMES) {
140
+ const expectedReal = tryRealpath(path.join(bunRoot, name))
141
+ if (path.resolve(expectedReal) === path.resolve(pkgReal)) return 'bun'
142
+ }
143
+
144
+ return null
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
+
183
+ module.exports = {
184
+ PACKAGE_NAME,
185
+ candidateDirsFromArgv1,
186
+ detectGlobalInstallManagerForRoot,
187
+ findPackageRoot,
188
+ findLocalInstallProjectRoot,
189
+ readPackageName,
190
+ readPackageVersion,
191
+ resolveGlobalRoot,
192
+ resolvePackageRoot,
193
+ resolveStateHome,
194
+ }
@@ -0,0 +1,121 @@
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 {
11
+ candidateDirsFromArgv1,
12
+ detectGlobalInstallManagerForRoot,
13
+ findLocalInstallProjectRoot,
14
+ resolvePackageRoot,
15
+ resolveStateHome,
16
+ } = require('./install-root.js')
17
+
18
+ test('candidateDirsFromArgv1 includes the package directory for node_modules/.bin launchers', () => {
19
+ const launcher = path.join('/tmp', 'example', 'node_modules', '.bin', 'swarmclaw')
20
+ const candidates = candidateDirsFromArgv1(launcher)
21
+ assert.deepEqual(candidates, [
22
+ path.join('/tmp', 'example', 'node_modules', '.bin'),
23
+ path.join('/tmp', 'example', 'node_modules', 'swarmclaw'),
24
+ ])
25
+ })
26
+
27
+ test('resolvePackageRoot finds the package root from argv1 candidates', () => {
28
+ const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-install-root-'))
29
+ const pkgRoot = path.join(rootDir, 'node_modules', '@swarmclawai', 'swarmclaw')
30
+ const binPath = path.join(rootDir, 'node_modules', '.bin', 'swarmclaw')
31
+ const actualBin = path.join(pkgRoot, 'bin', 'swarmclaw.js')
32
+
33
+ fs.mkdirSync(path.join(pkgRoot, 'bin'), { recursive: true })
34
+ fs.mkdirSync(path.dirname(binPath), { recursive: true })
35
+ fs.writeFileSync(path.join(pkgRoot, 'package.json'), JSON.stringify({ name: '@swarmclawai/swarmclaw' }), 'utf8')
36
+ fs.writeFileSync(actualBin, '#!/usr/bin/env node\n', 'utf8')
37
+ fs.symlinkSync(actualBin, binPath)
38
+
39
+ assert.equal(resolvePackageRoot({ argv1: binPath, cwd: rootDir }), fs.realpathSync(pkgRoot))
40
+
41
+ fs.rmSync(rootDir, { recursive: true, force: true })
42
+ })
43
+
44
+ test('detectGlobalInstallManagerForRoot matches the owning global root by realpath', () => {
45
+ const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-global-root-'))
46
+ const npmGlobalRoot = path.join(rootDir, 'npm-global')
47
+ const pnpmGlobalRoot = path.join(rootDir, 'pnpm-global')
48
+ const pkgRoot = path.join(pnpmGlobalRoot, '@swarmclawai', 'swarmclaw')
49
+
50
+ fs.mkdirSync(path.join(npmGlobalRoot, '@swarmclawai'), { recursive: true })
51
+ fs.mkdirSync(path.join(pnpmGlobalRoot, '@swarmclawai'), { recursive: true })
52
+ fs.mkdirSync(pkgRoot, { recursive: true })
53
+
54
+ const execImpl = (command, args) => {
55
+ if (command === 'npm' && args.join(' ') === 'root -g') return npmGlobalRoot
56
+ if (command === 'pnpm' && args.join(' ') === 'root -g') return pnpmGlobalRoot
57
+ throw new Error(`unexpected command: ${command} ${args.join(' ')}`)
58
+ }
59
+
60
+ assert.equal(detectGlobalInstallManagerForRoot(pkgRoot, execImpl), 'pnpm')
61
+
62
+ fs.rmSync(rootDir, { recursive: true, force: true })
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
+ })