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.
Files changed (3) hide show
  1. package/README.md +31 -0
  2. package/bin/webclaw.js +328 -0
  3. 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
+ }