webclaw 0.1.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/README.md +31 -0
- package/bin/webclaw.js +328 -0
- package/package.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# webclaw
|
|
2
|
+
|
|
3
|
+
Official CLI for WebClaw. It initializes a new project by cloning the WebClaw repository.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx webclaw
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Initialize into a specific directory:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx webclaw init my-webclaw
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Run project commands from a WebClaw project directory:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
webclaw dev
|
|
21
|
+
webclaw build
|
|
22
|
+
webclaw preview
|
|
23
|
+
webclaw test
|
|
24
|
+
webclaw lint
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Commands
|
|
28
|
+
|
|
29
|
+
- `webclaw` - initialize in current directory
|
|
30
|
+
- `webclaw init [dir]` - initialize a project in `dir`
|
|
31
|
+
- `webclaw doctor` - validate local prerequisites
|
package/bin/webclaw.js
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { spawnSync } from 'node:child_process'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
import readline from 'node:readline/promises'
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
11
|
+
const __dirname = path.dirname(__filename)
|
|
12
|
+
const REPO_URL = 'https://github.com/ibelick/webclaw'
|
|
13
|
+
|
|
14
|
+
function printBanner() {
|
|
15
|
+
process.stdout.write(` ▄▄ ▄▄ \n`)
|
|
16
|
+
process.stdout.write(` ██ ██ \n`)
|
|
17
|
+
process.stdout.write(`██ ██ ▄█▀█▄ ████▄ ▄████ ██ ▀▀█▄ ██ ██ \n`)
|
|
18
|
+
process.stdout.write(`██ █ ██ ██▄█▀ ██ ██ ██ ██ ▄█▀██ ██ █ ██ \n`)
|
|
19
|
+
process.stdout.write(` ██▀██ ▀█▄▄▄ ████▀ ▀████ ██ ▀█▄██ ██▀██ \n\n`)
|
|
20
|
+
process.stdout.write(`Fast web client for OpenClaw\n\n`)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function printHelp() {
|
|
24
|
+
process.stdout.write(`webclaw CLI\n\n`)
|
|
25
|
+
process.stdout.write(`Usage:\n`)
|
|
26
|
+
process.stdout.write(` webclaw Initialize in current directory\n`)
|
|
27
|
+
process.stdout.write(` webclaw init [dir] Initialize a new project\n`)
|
|
28
|
+
process.stdout.write(` webclaw dev Run development server\n`)
|
|
29
|
+
process.stdout.write(` webclaw build Build project\n`)
|
|
30
|
+
process.stdout.write(` webclaw preview Preview production build\n`)
|
|
31
|
+
process.stdout.write(` webclaw test Run tests\n`)
|
|
32
|
+
process.stdout.write(` webclaw lint Run lint\n`)
|
|
33
|
+
process.stdout.write(` webclaw doctor Validate local setup\n`)
|
|
34
|
+
process.stdout.write(`\nOptions:\n`)
|
|
35
|
+
process.stdout.write(` --force Allow init in non-empty directory\n`)
|
|
36
|
+
process.stdout.write(` --skip-env Skip .env.local setup prompts\n`)
|
|
37
|
+
process.stdout.write(` -h, --help Show help\n`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function runCommand(command, args, cwd) {
|
|
41
|
+
const result = spawnSync(command, args, {
|
|
42
|
+
cwd,
|
|
43
|
+
stdio: 'inherit',
|
|
44
|
+
env: process.env,
|
|
45
|
+
})
|
|
46
|
+
if (result.error) {
|
|
47
|
+
throw result.error
|
|
48
|
+
}
|
|
49
|
+
if (typeof result.status === 'number' && result.status !== 0) {
|
|
50
|
+
process.exit(result.status)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function detectPackageManager(cwd) {
|
|
55
|
+
if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) return 'pnpm'
|
|
56
|
+
if (fs.existsSync(path.join(cwd, 'yarn.lock'))) return 'yarn'
|
|
57
|
+
return 'npm'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function detectProjectRoot(cwd) {
|
|
61
|
+
const appDir = path.join(cwd, 'apps', 'webclaw')
|
|
62
|
+
const appPackage = path.join(appDir, 'package.json')
|
|
63
|
+
if (fs.existsSync(appPackage)) {
|
|
64
|
+
return { mode: 'monorepo', appDir }
|
|
65
|
+
}
|
|
66
|
+
const packagePath = path.join(cwd, 'package.json')
|
|
67
|
+
if (fs.existsSync(packagePath)) {
|
|
68
|
+
return { mode: 'single', appDir: cwd }
|
|
69
|
+
}
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function runProjectScript(scriptName) {
|
|
74
|
+
const detected = detectProjectRoot(process.cwd())
|
|
75
|
+
if (!detected) {
|
|
76
|
+
process.stderr.write(
|
|
77
|
+
`No WebClaw project found in this directory. Run \`webclaw init\` first.\n`,
|
|
78
|
+
)
|
|
79
|
+
process.exit(1)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const packageManager = detectPackageManager(process.cwd())
|
|
83
|
+
|
|
84
|
+
if (detected.mode === 'monorepo') {
|
|
85
|
+
if (packageManager === 'pnpm') {
|
|
86
|
+
runCommand('pnpm', ['-C', 'apps/webclaw', scriptName], process.cwd())
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
runCommand(packageManager, ['run', scriptName], detected.appDir)
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
runCommand(packageManager, ['run', scriptName], detected.appDir)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function ensureDir(targetDir) {
|
|
97
|
+
if (!fs.existsSync(targetDir)) {
|
|
98
|
+
fs.mkdirSync(targetDir, { recursive: true })
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function copyDir(sourceDir, targetDir) {
|
|
103
|
+
ensureDir(targetDir)
|
|
104
|
+
const entries = fs.readdirSync(sourceDir, { withFileTypes: true })
|
|
105
|
+
for (const entry of entries) {
|
|
106
|
+
if (entry.name === 'node_modules') continue
|
|
107
|
+
if (entry.name === '.git') continue
|
|
108
|
+
if (entry.name === '.env.local') continue
|
|
109
|
+
if (entry.name === '.openclaw') continue
|
|
110
|
+
if (entry.name === '.webclaw') continue
|
|
111
|
+
if (entry.name === '.tanstack') continue
|
|
112
|
+
if (entry.name === '.DS_Store') continue
|
|
113
|
+
const sourcePath = path.join(sourceDir, entry.name)
|
|
114
|
+
const targetPath = path.join(targetDir, entry.name)
|
|
115
|
+
if (entry.isDirectory()) {
|
|
116
|
+
copyDir(sourcePath, targetPath)
|
|
117
|
+
} else {
|
|
118
|
+
fs.copyFileSync(sourcePath, targetPath)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isDirEmpty(targetDir) {
|
|
124
|
+
if (!fs.existsSync(targetDir)) return true
|
|
125
|
+
const files = fs
|
|
126
|
+
.readdirSync(targetDir)
|
|
127
|
+
.filter((file) => file !== '.DS_Store' && file !== '.git')
|
|
128
|
+
return files.length === 0
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function cloneRepo(targetDir) {
|
|
132
|
+
runCommand('git', ['clone', '--depth', '1', REPO_URL, targetDir], process.cwd())
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function resolveEnvFile(targetDir) {
|
|
136
|
+
const monorepoEnv = path.join(targetDir, 'apps', 'webclaw', '.env.local')
|
|
137
|
+
if (fs.existsSync(path.join(targetDir, 'apps', 'webclaw'))) {
|
|
138
|
+
return monorepoEnv
|
|
139
|
+
}
|
|
140
|
+
return path.join(targetDir, '.env.local')
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function askQuestion(rl, question) {
|
|
144
|
+
const answer = await rl.question(question)
|
|
145
|
+
return answer.trim()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function maybeSetupEnv(targetDir, options) {
|
|
149
|
+
if (options.has('--skip-env')) return
|
|
150
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return
|
|
151
|
+
|
|
152
|
+
const envFile = resolveEnvFile(targetDir)
|
|
153
|
+
const rl = readline.createInterface({
|
|
154
|
+
input: process.stdin,
|
|
155
|
+
output: process.stdout,
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const createAnswer = await askQuestion(
|
|
160
|
+
rl,
|
|
161
|
+
`Create ${envFile} now? [Y/n]: `,
|
|
162
|
+
)
|
|
163
|
+
const shouldCreate =
|
|
164
|
+
createAnswer.length === 0 ||
|
|
165
|
+
createAnswer.toLowerCase() === 'y' ||
|
|
166
|
+
createAnswer.toLowerCase() === 'yes'
|
|
167
|
+
|
|
168
|
+
if (!shouldCreate) {
|
|
169
|
+
process.stdout.write(
|
|
170
|
+
`Skipping env file. Create it later at ${envFile} with:\n` +
|
|
171
|
+
`CLAWDBOT_GATEWAY_URL=...\n` +
|
|
172
|
+
`CLAWDBOT_GATEWAY_TOKEN=...\n\n`,
|
|
173
|
+
)
|
|
174
|
+
return
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (fs.existsSync(envFile)) {
|
|
178
|
+
const overwriteAnswer = await askQuestion(
|
|
179
|
+
rl,
|
|
180
|
+
`${envFile} already exists. Overwrite? [y/N]: `,
|
|
181
|
+
)
|
|
182
|
+
const shouldOverwrite =
|
|
183
|
+
overwriteAnswer.toLowerCase() === 'y' ||
|
|
184
|
+
overwriteAnswer.toLowerCase() === 'yes'
|
|
185
|
+
if (!shouldOverwrite) {
|
|
186
|
+
process.stdout.write(`Keeping existing ${envFile}\n\n`)
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const gatewayUrl = await askQuestion(rl, 'CLAWDBOT_GATEWAY_URL: ')
|
|
192
|
+
const gatewayToken = await askQuestion(rl, 'CLAWDBOT_GATEWAY_TOKEN: ')
|
|
193
|
+
|
|
194
|
+
ensureDir(path.dirname(envFile))
|
|
195
|
+
fs.writeFileSync(
|
|
196
|
+
envFile,
|
|
197
|
+
`CLAWDBOT_GATEWAY_URL=${gatewayUrl}\nCLAWDBOT_GATEWAY_TOKEN=${gatewayToken}\n`,
|
|
198
|
+
)
|
|
199
|
+
process.stdout.write(`Wrote ${envFile}\n\n`)
|
|
200
|
+
} finally {
|
|
201
|
+
rl.close()
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function initProject(rawTarget, options) {
|
|
206
|
+
printBanner()
|
|
207
|
+
const targetDir = path.resolve(process.cwd(), rawTarget ?? '.')
|
|
208
|
+
const force = options.has('--force')
|
|
209
|
+
const isCurrentDir = targetDir === process.cwd()
|
|
210
|
+
|
|
211
|
+
ensureDir(targetDir)
|
|
212
|
+
if (!force && !isDirEmpty(targetDir)) {
|
|
213
|
+
process.stderr.write(
|
|
214
|
+
`Target directory is not empty. Use --force to continue: ${targetDir}\n`,
|
|
215
|
+
)
|
|
216
|
+
process.exit(1)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (force && !isDirEmpty(targetDir) && isCurrentDir) {
|
|
220
|
+
process.stderr.write(
|
|
221
|
+
'Refusing to overwrite current directory. Use an empty directory for init.\n',
|
|
222
|
+
)
|
|
223
|
+
process.exit(1)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (force && !isDirEmpty(targetDir) && !isCurrentDir) {
|
|
227
|
+
for (const entry of fs.readdirSync(targetDir)) {
|
|
228
|
+
if (entry === '.git') continue
|
|
229
|
+
fs.rmSync(path.join(targetDir, entry), { recursive: true, force: true })
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!fs.existsSync(path.join(targetDir, '.git')) && isDirEmpty(targetDir)) {
|
|
234
|
+
if (!isCurrentDir) {
|
|
235
|
+
cloneRepo(targetDir)
|
|
236
|
+
} else {
|
|
237
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'webclaw-'))
|
|
238
|
+
const tempCloneDir = path.join(tempRoot, 'repo')
|
|
239
|
+
cloneRepo(tempCloneDir)
|
|
240
|
+
copyDir(tempCloneDir, targetDir)
|
|
241
|
+
fs.rmSync(tempRoot, { recursive: true, force: true })
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
await maybeSetupEnv(targetDir, options)
|
|
246
|
+
|
|
247
|
+
process.stdout.write(`\nWebClaw project created at ${targetDir}\n\n`)
|
|
248
|
+
process.stdout.write(`Next steps:\n`)
|
|
249
|
+
process.stdout.write(` cd ${path.relative(process.cwd(), targetDir) || '.'}\n`)
|
|
250
|
+
process.stdout.write(` pnpm install\n`)
|
|
251
|
+
process.stdout.write(` pnpm dev\n\n`)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function doctor() {
|
|
255
|
+
const nodeMajor = Number(process.versions.node.split('.')[0] || 0)
|
|
256
|
+
const hasPnpm = spawnSync('pnpm', ['--version'], { stdio: 'ignore' }).status === 0
|
|
257
|
+
const issues = []
|
|
258
|
+
|
|
259
|
+
if (nodeMajor < 20) {
|
|
260
|
+
issues.push('Node.js >= 20 is required.')
|
|
261
|
+
}
|
|
262
|
+
if (!hasPnpm) {
|
|
263
|
+
issues.push('pnpm is recommended but was not found in PATH.')
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (issues.length === 0) {
|
|
267
|
+
process.stdout.write('Environment looks good.\n')
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
for (const issue of issues) {
|
|
272
|
+
process.stderr.write(`- ${issue}\n`)
|
|
273
|
+
}
|
|
274
|
+
process.exit(1)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function main() {
|
|
278
|
+
const args = process.argv.slice(2)
|
|
279
|
+
const options = new Set(args.filter((arg) => arg.startsWith('-')))
|
|
280
|
+
const command = args.find((arg) => !arg.startsWith('-'))
|
|
281
|
+
|
|
282
|
+
if (options.has('-h') || options.has('--help')) {
|
|
283
|
+
printHelp()
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (!command) {
|
|
288
|
+
await initProject('.', options)
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (command === 'init') {
|
|
293
|
+
const target = args.find((arg, index) => {
|
|
294
|
+
if (arg.startsWith('-')) return false
|
|
295
|
+
const previous = args[index - 1]
|
|
296
|
+
return previous === 'init'
|
|
297
|
+
})
|
|
298
|
+
await initProject(target, options)
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (command === 'doctor') {
|
|
303
|
+
doctor()
|
|
304
|
+
return
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (
|
|
308
|
+
command === 'dev' ||
|
|
309
|
+
command === 'build' ||
|
|
310
|
+
command === 'preview' ||
|
|
311
|
+
command === 'test' ||
|
|
312
|
+
command === 'lint'
|
|
313
|
+
) {
|
|
314
|
+
runProjectScript(command)
|
|
315
|
+
return
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
process.stderr.write(`Unknown command: ${command}\n\n`)
|
|
319
|
+
printHelp()
|
|
320
|
+
process.exit(1)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
void main().catch((error) => {
|
|
324
|
+
process.stderr.write(
|
|
325
|
+
`${error instanceof Error ? error.message : String(error)}\n`,
|
|
326
|
+
)
|
|
327
|
+
process.exit(1)
|
|
328
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "webclaw",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "WebClaw CLI",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"webclaw": "bin/webclaw.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"webclaw",
|
|
16
|
+
"openclaw",
|
|
17
|
+
"cli"
|
|
18
|
+
],
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=20.0.0"
|
|
25
|
+
}
|
|
26
|
+
}
|