@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.
- package/bin/doctor-cmd.js +149 -0
- package/bin/doctor-cmd.test.js +50 -0
- package/bin/install-root.js +194 -0
- package/bin/install-root.test.js +121 -0
- package/bin/server-cmd.js +90 -111
- package/bin/swarmclaw.js +83 -3
- package/bin/update-cmd.js +33 -20
- package/bin/update-cmd.test.js +1 -36
- package/bin/worker-cmd.js +23 -17
- package/next.config.ts +2 -0
- package/package.json +11 -10
- 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/openclaw/dashboard-url/route.test.ts +166 -0
- package/src/app/api/openclaw/dashboard-url/route.ts +68 -0
- package/src/app/api/setup/check-provider/helpers.ts +28 -0
- package/src/app/api/setup/check-provider/route.test.ts +17 -1
- package/src/app/api/setup/check-provider/route.ts +29 -36
- 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/cli/binary.test.js +57 -0
- package/src/cli/index.js +14 -1
- package/src/cli/server-cmd.test.js +21 -20
- package/src/components/auth/setup-wizard/index.tsx +16 -0
- package/src/components/auth/setup-wizard/step-agents.tsx +34 -23
- package/src/components/auth/setup-wizard/step-connect.tsx +8 -0
- package/src/components/auth/setup-wizard/types.ts +2 -0
- package/src/components/auth/setup-wizard/utils.test.ts +79 -0
- package/src/components/chat/chat-header.tsx +45 -2
- package/src/lib/providers/openclaw-exports.test.ts +23 -0
- package/src/lib/providers/openclaw.ts +1 -1
- package/src/lib/server/data-dir.test.ts +35 -0
- 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/src/lib/setup-defaults.test.ts +42 -0
- 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
|
+
})
|