webclaw 0.1.0 → 0.1.1

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 (3) hide show
  1. package/README.md +7 -6
  2. package/bin/webclaw.js +284 -31
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -8,11 +8,12 @@ Official CLI for WebClaw. It initializes a new project by cloning the WebClaw re
8
8
  npx webclaw
9
9
  ```
10
10
 
11
- Initialize into a specific directory:
11
+ You will be prompted for:
12
+ - project name
13
+ - environment keys (`CLAWDBOT_GATEWAY_URL`, token/password)
14
+ - local dev port
12
15
 
13
- ```bash
14
- npx webclaw init my-webclaw
15
- ```
16
+ Then the CLI creates the project folder, installs dependencies, and starts WebClaw.
16
17
 
17
18
  Run project commands from a WebClaw project directory:
18
19
 
@@ -26,6 +27,6 @@ webclaw lint
26
27
 
27
28
  ## Commands
28
29
 
29
- - `webclaw` - initialize in current directory
30
- - `webclaw init [dir]` - initialize a project in `dir`
30
+ - `webclaw` - create and start a new project
31
+ - `webclaw init [dir]` - initialize a project in `dir` (legacy)
31
32
  - `webclaw doctor` - validate local prerequisites
package/bin/webclaw.js CHANGED
@@ -9,6 +9,7 @@ import readline from 'node:readline/promises'
9
9
 
10
10
  const __filename = fileURLToPath(import.meta.url)
11
11
  const __dirname = path.dirname(__filename)
12
+ const LOCAL_TEMPLATE_ROOT = path.resolve(__dirname, '..', '..', '..')
12
13
  const REPO_URL = 'https://github.com/ibelick/webclaw'
13
14
 
14
15
  function printBanner() {
@@ -17,14 +18,15 @@ function printBanner() {
17
18
  process.stdout.write(`██ ██ ▄█▀█▄ ████▄ ▄████ ██ ▀▀█▄ ██ ██ \n`)
18
19
  process.stdout.write(`██ █ ██ ██▄█▀ ██ ██ ██ ██ ▄█▀██ ██ █ ██ \n`)
19
20
  process.stdout.write(` ██▀██ ▀█▄▄▄ ████▀ ▀████ ██ ▀█▄██ ██▀██ \n\n`)
20
- process.stdout.write(`Fast web client for OpenClaw\n\n`)
21
+ process.stdout.write(`Fast web client for OpenClaw\n`)
22
+ process.stdout.write(`https://webclaw.dev/\n\n`)
21
23
  }
22
24
 
23
25
  function printHelp() {
24
26
  process.stdout.write(`webclaw CLI\n\n`)
25
27
  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 Create and start a new project\n`)
29
+ process.stdout.write(` webclaw init [dir] Initialize a new project (legacy)\n`)
28
30
  process.stdout.write(` webclaw dev Run development server\n`)
29
31
  process.stdout.write(` webclaw build Build project\n`)
30
32
  process.stdout.write(` webclaw preview Preview production build\n`)
@@ -32,11 +34,68 @@ function printHelp() {
32
34
  process.stdout.write(` webclaw lint Run lint\n`)
33
35
  process.stdout.write(` webclaw doctor Validate local setup\n`)
34
36
  process.stdout.write(`\nOptions:\n`)
37
+ process.stdout.write(` --project-name <name> Project directory name\n`)
38
+ process.stdout.write(` --gateway-url <url> CLAWDBOT_GATEWAY_URL value\n`)
39
+ process.stdout.write(` --gateway-token <token> CLAWDBOT_GATEWAY_TOKEN value\n`)
40
+ process.stdout.write(` --gateway-password <pw> CLAWDBOT_GATEWAY_PASSWORD value\n`)
41
+ process.stdout.write(` --port <port> Dev server port\n`)
42
+ process.stdout.write(` --yes Accept defaults (non-interactive)\n`)
43
+ process.stdout.write(` --no-start Do not auto-run install + dev\n`)
35
44
  process.stdout.write(` --force Allow init in non-empty directory\n`)
36
45
  process.stdout.write(` --skip-env Skip .env.local setup prompts\n`)
37
46
  process.stdout.write(` -h, --help Show help\n`)
38
47
  }
39
48
 
49
+ function parseCliArgs(args) {
50
+ const flags = new Set()
51
+ const values = new Map()
52
+ const positionals = []
53
+ const optionsWithValues = new Set([
54
+ '--project-name',
55
+ '--gateway-url',
56
+ '--gateway-token',
57
+ '--gateway-password',
58
+ '--port',
59
+ ])
60
+
61
+ for (let index = 0; index < args.length; index += 1) {
62
+ const arg = args[index]
63
+ if (!arg.startsWith('-')) {
64
+ positionals.push(arg)
65
+ continue
66
+ }
67
+
68
+ if (arg === '-h' || arg === '--help') {
69
+ flags.add(arg)
70
+ continue
71
+ }
72
+
73
+ if (!arg.startsWith('--')) {
74
+ flags.add(arg)
75
+ continue
76
+ }
77
+
78
+ const equalIndex = arg.indexOf('=')
79
+ if (equalIndex !== -1) {
80
+ const key = arg.slice(0, equalIndex)
81
+ const value = arg.slice(equalIndex + 1)
82
+ values.set(key, value)
83
+ continue
84
+ }
85
+
86
+ const nextArg = args[index + 1]
87
+ if (optionsWithValues.has(arg) && nextArg && !nextArg.startsWith('-')) {
88
+ values.set(arg, nextArg)
89
+ index += 1
90
+ continue
91
+ }
92
+
93
+ flags.add(arg)
94
+ }
95
+
96
+ return { flags, values, positionals }
97
+ }
98
+
40
99
  function runCommand(command, args, cwd) {
41
100
  const result = spawnSync(command, args, {
42
101
  cwd,
@@ -74,7 +133,7 @@ function runProjectScript(scriptName) {
74
133
  const detected = detectProjectRoot(process.cwd())
75
134
  if (!detected) {
76
135
  process.stderr.write(
77
- `No WebClaw project found in this directory. Run \`webclaw init\` first.\n`,
136
+ `No WebClaw project found in this directory. Run \`npx webclaw\` first.\n`,
78
137
  )
79
138
  process.exit(1)
80
139
  }
@@ -132,6 +191,55 @@ function cloneRepo(targetDir) {
132
191
  runCommand('git', ['clone', '--depth', '1', REPO_URL, targetDir], process.cwd())
133
192
  }
134
193
 
194
+ function getLocalTemplateSource() {
195
+ const rootPackage = path.join(LOCAL_TEMPLATE_ROOT, 'package.json')
196
+ const appPackage = path.join(LOCAL_TEMPLATE_ROOT, 'apps', 'webclaw', 'package.json')
197
+ if (!fs.existsSync(rootPackage) || !fs.existsSync(appPackage)) {
198
+ return null
199
+ }
200
+ return LOCAL_TEMPLATE_ROOT
201
+ }
202
+
203
+ function populateTemplate(targetDir, isCurrentDir) {
204
+ const localTemplateSource = getLocalTemplateSource()
205
+ if (localTemplateSource) {
206
+ if (!isCurrentDir) {
207
+ copyDir(localTemplateSource, targetDir)
208
+ return
209
+ }
210
+
211
+ copyDir(localTemplateSource, targetDir)
212
+ return
213
+ }
214
+
215
+ if (!isCurrentDir) {
216
+ cloneRepo(targetDir)
217
+ return
218
+ }
219
+
220
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'webclaw-'))
221
+ const tempCloneDir = path.join(tempRoot, 'repo')
222
+ cloneRepo(tempCloneDir)
223
+ copyDir(tempCloneDir, targetDir)
224
+ fs.rmSync(tempRoot, { recursive: true, force: true })
225
+ }
226
+
227
+ function ensureGitRepository(targetDir) {
228
+ if (fs.existsSync(path.join(targetDir, '.git'))) {
229
+ return
230
+ }
231
+
232
+ const result = spawnSync('git', ['init'], {
233
+ cwd: targetDir,
234
+ stdio: 'ignore',
235
+ env: process.env,
236
+ })
237
+
238
+ if (result.status === 0) {
239
+ process.stdout.write('Initialized git repository\n')
240
+ }
241
+ }
242
+
135
243
  function resolveEnvFile(targetDir) {
136
244
  const monorepoEnv = path.join(targetDir, 'apps', 'webclaw', '.env.local')
137
245
  if (fs.existsSync(path.join(targetDir, 'apps', 'webclaw'))) {
@@ -145,8 +253,61 @@ async function askQuestion(rl, question) {
145
253
  return answer.trim()
146
254
  }
147
255
 
148
- async function maybeSetupEnv(targetDir, options) {
256
+ function parsePort(value, fallback) {
257
+ const parsed = Number(value)
258
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
259
+ return fallback
260
+ }
261
+ return parsed
262
+ }
263
+
264
+ function setDevPort(targetDir, port) {
265
+ const appPackagePath = fs.existsSync(path.join(targetDir, 'apps', 'webclaw'))
266
+ ? path.join(targetDir, 'apps', 'webclaw', 'package.json')
267
+ : path.join(targetDir, 'package.json')
268
+
269
+ if (!fs.existsSync(appPackagePath)) return
270
+
271
+ const packageJson = JSON.parse(fs.readFileSync(appPackagePath, 'utf8'))
272
+ if (!packageJson.scripts || typeof packageJson.scripts.dev !== 'string') return
273
+
274
+ if (packageJson.scripts.dev.includes('--port')) {
275
+ packageJson.scripts.dev = packageJson.scripts.dev.replace(
276
+ /--port\s+\d+/,
277
+ `--port ${port}`,
278
+ )
279
+ } else {
280
+ packageJson.scripts.dev = `${packageJson.scripts.dev} --port ${port}`
281
+ }
282
+
283
+ fs.writeFileSync(appPackagePath, `${JSON.stringify(packageJson, null, 2)}\n`)
284
+ }
285
+
286
+ function writeEnvFile(targetDir, envValues) {
287
+ const envFile = resolveEnvFile(targetDir)
288
+ ensureDir(path.dirname(envFile))
289
+
290
+ const lines = [
291
+ `CLAWDBOT_GATEWAY_URL=${envValues.gatewayUrl}`,
292
+ `CLAWDBOT_GATEWAY_TOKEN=${envValues.gatewayToken}`,
293
+ ]
294
+
295
+ if (envValues.gatewayPassword.length > 0) {
296
+ lines.push(`CLAWDBOT_GATEWAY_PASSWORD=${envValues.gatewayPassword}`)
297
+ }
298
+
299
+ fs.writeFileSync(envFile, `${lines.join('\n')}\n`)
300
+ process.stdout.write(`Wrote ${envFile}\n\n`)
301
+ }
302
+
303
+ async function maybeSetupEnv(targetDir, options, envValues) {
149
304
  if (options.has('--skip-env')) return
305
+
306
+ if (envValues) {
307
+ writeEnvFile(targetDir, envValues)
308
+ return
309
+ }
310
+
150
311
  if (!process.stdin.isTTY || !process.stdout.isTTY) return
151
312
 
152
313
  const envFile = resolveEnvFile(targetDir)
@@ -191,19 +352,35 @@ async function maybeSetupEnv(targetDir, options) {
191
352
  const gatewayUrl = await askQuestion(rl, 'CLAWDBOT_GATEWAY_URL: ')
192
353
  const gatewayToken = await askQuestion(rl, 'CLAWDBOT_GATEWAY_TOKEN: ')
193
354
 
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`)
355
+ writeEnvFile(targetDir, {
356
+ gatewayUrl,
357
+ gatewayToken,
358
+ gatewayPassword: '',
359
+ })
200
360
  } finally {
201
361
  rl.close()
202
362
  }
203
363
  }
204
364
 
205
- async function initProject(rawTarget, options) {
206
- printBanner()
365
+ function installDependencies(targetDir) {
366
+ const packageManager = detectPackageManager(targetDir)
367
+ if (packageManager === 'yarn') {
368
+ runCommand('yarn', ['install'], targetDir)
369
+ return
370
+ }
371
+
372
+ runCommand(packageManager, ['install'], targetDir)
373
+ }
374
+
375
+ function startProject(targetDir) {
376
+ const packageManager = detectPackageManager(targetDir)
377
+ runCommand(packageManager, ['run', 'dev'], targetDir)
378
+ }
379
+
380
+ async function initProject(rawTarget, options, bootstrapConfig) {
381
+ if (!bootstrapConfig) {
382
+ printBanner()
383
+ }
207
384
  const targetDir = path.resolve(process.cwd(), rawTarget ?? '.')
208
385
  const force = options.has('--force')
209
386
  const isCurrentDir = targetDir === process.cwd()
@@ -231,26 +408,103 @@ async function initProject(rawTarget, options) {
231
408
  }
232
409
 
233
410
  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
- }
411
+ populateTemplate(targetDir, isCurrentDir)
243
412
  }
244
413
 
245
- await maybeSetupEnv(targetDir, options)
414
+ ensureGitRepository(targetDir)
415
+
416
+ if (bootstrapConfig) {
417
+ setDevPort(targetDir, bootstrapConfig.port)
418
+ }
419
+
420
+ await maybeSetupEnv(targetDir, options, bootstrapConfig?.envValues)
246
421
 
247
422
  process.stdout.write(`\nWebClaw project created at ${targetDir}\n\n`)
423
+
424
+ if (bootstrapConfig && bootstrapConfig.autoStart) {
425
+ process.stdout.write(`Installing dependencies...\n`)
426
+ installDependencies(targetDir)
427
+ process.stdout.write(`Starting WebClaw on port ${bootstrapConfig?.port ?? 3000}...\n\n`)
428
+ startProject(targetDir)
429
+ return
430
+ }
431
+
248
432
  process.stdout.write(`Next steps:\n`)
249
433
  process.stdout.write(` cd ${path.relative(process.cwd(), targetDir) || '.'}\n`)
250
434
  process.stdout.write(` pnpm install\n`)
251
435
  process.stdout.write(` pnpm dev\n\n`)
252
436
  }
253
437
 
438
+ async function askBootstrapConfig(defaultProjectName, parsedArgs) {
439
+ const nonInteractive =
440
+ parsedArgs.flags.has('--yes') || !process.stdin.isTTY || !process.stdout.isTTY
441
+
442
+ const initialProjectName =
443
+ parsedArgs.values.get('--project-name') || defaultProjectName || 'webclaw'
444
+ const initialGatewayUrl =
445
+ parsedArgs.values.get('--gateway-url') || 'ws://127.0.0.1:18789'
446
+ const initialGatewayToken = parsedArgs.values.get('--gateway-token') || ''
447
+ const initialGatewayPassword = parsedArgs.values.get('--gateway-password') || ''
448
+ const initialPort = parsePort(parsedArgs.values.get('--port') || 3000, 3000)
449
+
450
+ if (nonInteractive) {
451
+ return {
452
+ projectName: initialProjectName,
453
+ envValues: {
454
+ gatewayUrl: initialGatewayUrl,
455
+ gatewayToken: initialGatewayToken,
456
+ gatewayPassword: initialGatewayPassword,
457
+ },
458
+ port: initialPort,
459
+ autoStart: !parsedArgs.flags.has('--no-start'),
460
+ }
461
+ }
462
+
463
+ const rl = readline.createInterface({
464
+ input: process.stdin,
465
+ output: process.stdout,
466
+ })
467
+
468
+ try {
469
+ const projectNameInput = await askQuestion(
470
+ rl,
471
+ `Project name [${initialProjectName}]: `,
472
+ )
473
+ const projectName = projectNameInput || initialProjectName
474
+
475
+ const gatewayUrlInput = await askQuestion(
476
+ rl,
477
+ `CLAWDBOT_GATEWAY_URL [${initialGatewayUrl}]: `,
478
+ )
479
+ const gatewayUrl = gatewayUrlInput || initialGatewayUrl
480
+
481
+ const gatewayTokenInput = await askQuestion(
482
+ rl,
483
+ 'CLAWDBOT_GATEWAY_TOKEN (optional): ',
484
+ )
485
+ const gatewayPasswordInput = await askQuestion(
486
+ rl,
487
+ 'CLAWDBOT_GATEWAY_PASSWORD (optional): ',
488
+ )
489
+
490
+ const portInput = await askQuestion(rl, `Port [${initialPort}]: `)
491
+ const port = parsePort(portInput || initialPort, initialPort)
492
+
493
+ return {
494
+ projectName,
495
+ envValues: {
496
+ gatewayUrl,
497
+ gatewayToken: gatewayTokenInput || initialGatewayToken,
498
+ gatewayPassword: gatewayPasswordInput || initialGatewayPassword,
499
+ },
500
+ port,
501
+ autoStart: !parsedArgs.flags.has('--no-start'),
502
+ }
503
+ } finally {
504
+ rl.close()
505
+ }
506
+ }
507
+
254
508
  function doctor() {
255
509
  const nodeMajor = Number(process.versions.node.split('.')[0] || 0)
256
510
  const hasPnpm = spawnSync('pnpm', ['--version'], { stdio: 'ignore' }).status === 0
@@ -276,8 +530,9 @@ function doctor() {
276
530
 
277
531
  async function main() {
278
532
  const args = process.argv.slice(2)
279
- const options = new Set(args.filter((arg) => arg.startsWith('-')))
280
- const command = args.find((arg) => !arg.startsWith('-'))
533
+ const parsedArgs = parseCliArgs(args)
534
+ const options = parsedArgs.flags
535
+ const command = parsedArgs.positionals[0]
281
536
 
282
537
  if (options.has('-h') || options.has('--help')) {
283
538
  printHelp()
@@ -285,16 +540,14 @@ async function main() {
285
540
  }
286
541
 
287
542
  if (!command) {
288
- await initProject('.', options)
543
+ printBanner()
544
+ const bootstrapConfig = await askBootstrapConfig(null, parsedArgs)
545
+ await initProject(bootstrapConfig.projectName, options, bootstrapConfig)
289
546
  return
290
547
  }
291
548
 
292
549
  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
- })
550
+ const target = parsedArgs.positionals[1]
298
551
  await initProject(target, options)
299
552
  return
300
553
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webclaw",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "private": false,
5
5
  "description": "WebClaw CLI",
6
6
  "type": "module",