mirror of
https://github.com/ibelick/webclaw.git
synced 2026-08-14 00:57:51 +00:00
feat(cli): add publishable webclaw npm CLI with simple release flow
This commit is contained in:
@@ -14,6 +14,25 @@ Currently in beta.
|
||||
curl -fsSL https://webclaw.dev/install | bash
|
||||
```
|
||||
|
||||
## npm CLI
|
||||
|
||||
```bash
|
||||
npx webclaw
|
||||
```
|
||||
|
||||
Initialize in a target directory:
|
||||
|
||||
```bash
|
||||
npx webclaw init my-webclaw
|
||||
```
|
||||
|
||||
### CLI release (simple)
|
||||
|
||||
```bash
|
||||
pnpm bump:webclaw:patch
|
||||
pnpm release:webclaw
|
||||
```
|
||||
|
||||
## Other option
|
||||
|
||||
Create `apps/webclaw/.env.local` with `CLAWDBOT_GATEWAY_URL` and either
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
"lint": "pnpm -C apps/webclaw lint",
|
||||
"format": "pnpm -C apps/webclaw format",
|
||||
"check": "pnpm -C apps/webclaw check",
|
||||
"release:webclaw": "pnpm -C packages/webclaw publish --access public",
|
||||
"bump:webclaw:patch": "pnpm -C packages/webclaw version patch --no-git-tag-version",
|
||||
"bump:webclaw:minor": "pnpm -C packages/webclaw version minor --no-git-tag-version",
|
||||
"bump:webclaw:major": "pnpm -C packages/webclaw version major --no-git-tag-version",
|
||||
"postinstall": "lefthook install"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# webclaw
|
||||
|
||||
Official CLI for WebClaw. It initializes a new project by cloning the WebClaw repository.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
npx webclaw
|
||||
```
|
||||
|
||||
Initialize into a specific directory:
|
||||
|
||||
```bash
|
||||
npx webclaw init my-webclaw
|
||||
```
|
||||
|
||||
Run project commands from a WebClaw project directory:
|
||||
|
||||
```bash
|
||||
webclaw dev
|
||||
webclaw build
|
||||
webclaw preview
|
||||
webclaw test
|
||||
webclaw lint
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
- `webclaw` - initialize in current directory
|
||||
- `webclaw init [dir]` - initialize a project in `dir`
|
||||
- `webclaw doctor` - validate local prerequisites
|
||||
Executable
+239
@@ -0,0 +1,239 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
const REPO_URL = 'https://github.com/ibelick/webclaw'
|
||||
|
||||
function printHelp() {
|
||||
process.stdout.write(`webclaw CLI\n\n`)
|
||||
process.stdout.write(`Usage:\n`)
|
||||
process.stdout.write(` webclaw Initialize in current directory\n`)
|
||||
process.stdout.write(` webclaw init [dir] Initialize a new project\n`)
|
||||
process.stdout.write(` webclaw dev Run development server\n`)
|
||||
process.stdout.write(` webclaw build Build project\n`)
|
||||
process.stdout.write(` webclaw preview Preview production build\n`)
|
||||
process.stdout.write(` webclaw test Run tests\n`)
|
||||
process.stdout.write(` webclaw lint Run lint\n`)
|
||||
process.stdout.write(` webclaw doctor Validate local setup\n`)
|
||||
process.stdout.write(`\nOptions:\n`)
|
||||
process.stdout.write(` --force Allow init in non-empty directory\n`)
|
||||
process.stdout.write(` -h, --help Show help\n`)
|
||||
}
|
||||
|
||||
function runCommand(command, args, cwd) {
|
||||
const result = spawnSync(command, args, {
|
||||
cwd,
|
||||
stdio: 'inherit',
|
||||
env: process.env,
|
||||
})
|
||||
if (result.error) {
|
||||
throw result.error
|
||||
}
|
||||
if (typeof result.status === 'number' && result.status !== 0) {
|
||||
process.exit(result.status)
|
||||
}
|
||||
}
|
||||
|
||||
function detectPackageManager(cwd) {
|
||||
if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) return 'pnpm'
|
||||
if (fs.existsSync(path.join(cwd, 'yarn.lock'))) return 'yarn'
|
||||
return 'npm'
|
||||
}
|
||||
|
||||
function detectProjectRoot(cwd) {
|
||||
const appDir = path.join(cwd, 'apps', 'webclaw')
|
||||
const appPackage = path.join(appDir, 'package.json')
|
||||
if (fs.existsSync(appPackage)) {
|
||||
return { mode: 'monorepo', appDir }
|
||||
}
|
||||
const packagePath = path.join(cwd, 'package.json')
|
||||
if (fs.existsSync(packagePath)) {
|
||||
return { mode: 'single', appDir: cwd }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function runProjectScript(scriptName) {
|
||||
const detected = detectProjectRoot(process.cwd())
|
||||
if (!detected) {
|
||||
process.stderr.write(
|
||||
`No WebClaw project found in this directory. Run \`webclaw init\` first.\n`,
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const packageManager = detectPackageManager(process.cwd())
|
||||
|
||||
if (detected.mode === 'monorepo') {
|
||||
if (packageManager === 'pnpm') {
|
||||
runCommand('pnpm', ['-C', 'apps/webclaw', scriptName], process.cwd())
|
||||
return
|
||||
}
|
||||
runCommand(packageManager, ['run', scriptName], detected.appDir)
|
||||
return
|
||||
}
|
||||
|
||||
runCommand(packageManager, ['run', scriptName], detected.appDir)
|
||||
}
|
||||
|
||||
function ensureDir(targetDir) {
|
||||
if (!fs.existsSync(targetDir)) {
|
||||
fs.mkdirSync(targetDir, { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
function copyDir(sourceDir, targetDir) {
|
||||
ensureDir(targetDir)
|
||||
const entries = fs.readdirSync(sourceDir, { withFileTypes: true })
|
||||
for (const entry of entries) {
|
||||
if (entry.name === 'node_modules') continue
|
||||
if (entry.name === '.git') continue
|
||||
if (entry.name === '.env.local') continue
|
||||
if (entry.name === '.openclaw') continue
|
||||
if (entry.name === '.webclaw') continue
|
||||
if (entry.name === '.tanstack') continue
|
||||
if (entry.name === '.DS_Store') continue
|
||||
const sourcePath = path.join(sourceDir, entry.name)
|
||||
const targetPath = path.join(targetDir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
copyDir(sourcePath, targetPath)
|
||||
} else {
|
||||
fs.copyFileSync(sourcePath, targetPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isDirEmpty(targetDir) {
|
||||
if (!fs.existsSync(targetDir)) return true
|
||||
const files = fs
|
||||
.readdirSync(targetDir)
|
||||
.filter((file) => file !== '.DS_Store' && file !== '.git')
|
||||
return files.length === 0
|
||||
}
|
||||
|
||||
function cloneRepo(targetDir) {
|
||||
runCommand('git', ['clone', '--depth', '1', REPO_URL, targetDir], process.cwd())
|
||||
}
|
||||
|
||||
function initProject(rawTarget, options) {
|
||||
const targetDir = path.resolve(process.cwd(), rawTarget ?? '.')
|
||||
const force = options.has('--force')
|
||||
const isCurrentDir = targetDir === process.cwd()
|
||||
|
||||
ensureDir(targetDir)
|
||||
if (!force && !isDirEmpty(targetDir)) {
|
||||
process.stderr.write(
|
||||
`Target directory is not empty. Use --force to continue: ${targetDir}\n`,
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (force && !isDirEmpty(targetDir) && isCurrentDir) {
|
||||
process.stderr.write(
|
||||
'Refusing to overwrite current directory. Use an empty directory for init.\n',
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (force && !isDirEmpty(targetDir) && !isCurrentDir) {
|
||||
for (const entry of fs.readdirSync(targetDir)) {
|
||||
if (entry === '.git') continue
|
||||
fs.rmSync(path.join(targetDir, entry), { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs.existsSync(path.join(targetDir, '.git')) && isDirEmpty(targetDir)) {
|
||||
if (!isCurrentDir) {
|
||||
cloneRepo(targetDir)
|
||||
} else {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'webclaw-'))
|
||||
const tempCloneDir = path.join(tempRoot, 'repo')
|
||||
cloneRepo(tempCloneDir)
|
||||
copyDir(tempCloneDir, targetDir)
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
process.stdout.write(`\nWebClaw project created at ${targetDir}\n\n`)
|
||||
process.stdout.write(`Next steps:\n`)
|
||||
process.stdout.write(` cd ${path.relative(process.cwd(), targetDir) || '.'}\n`)
|
||||
process.stdout.write(` pnpm install\n`)
|
||||
process.stdout.write(` pnpm dev\n\n`)
|
||||
}
|
||||
|
||||
function doctor() {
|
||||
const nodeMajor = Number(process.versions.node.split('.')[0] || 0)
|
||||
const hasPnpm = spawnSync('pnpm', ['--version'], { stdio: 'ignore' }).status === 0
|
||||
const issues = []
|
||||
|
||||
if (nodeMajor < 20) {
|
||||
issues.push('Node.js >= 20 is required.')
|
||||
}
|
||||
if (!hasPnpm) {
|
||||
issues.push('pnpm is recommended but was not found in PATH.')
|
||||
}
|
||||
|
||||
if (issues.length === 0) {
|
||||
process.stdout.write('Environment looks good.\n')
|
||||
return
|
||||
}
|
||||
|
||||
for (const issue of issues) {
|
||||
process.stderr.write(`- ${issue}\n`)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = process.argv.slice(2)
|
||||
const options = new Set(args.filter((arg) => arg.startsWith('-')))
|
||||
const command = args.find((arg) => !arg.startsWith('-'))
|
||||
|
||||
if (options.has('-h') || options.has('--help')) {
|
||||
printHelp()
|
||||
return
|
||||
}
|
||||
|
||||
if (!command) {
|
||||
initProject('.', options)
|
||||
return
|
||||
}
|
||||
|
||||
if (command === 'init') {
|
||||
const target = args.find((arg, index) => {
|
||||
if (arg.startsWith('-')) return false
|
||||
const previous = args[index - 1]
|
||||
return previous === 'init'
|
||||
})
|
||||
initProject(target, options)
|
||||
return
|
||||
}
|
||||
|
||||
if (command === 'doctor') {
|
||||
doctor()
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
command === 'dev' ||
|
||||
command === 'build' ||
|
||||
command === 'preview' ||
|
||||
command === 'test' ||
|
||||
command === 'lint'
|
||||
) {
|
||||
runProjectScript(command)
|
||||
return
|
||||
}
|
||||
|
||||
process.stderr.write(`Unknown command: ${command}\n\n`)
|
||||
printHelp()
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "webclaw",
|
||||
"version": "0.1.0",
|
||||
"private": false,
|
||||
"description": "WebClaw CLI",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"webclaw": "./bin/webclaw.js"
|
||||
},
|
||||
"files": [
|
||||
"bin",
|
||||
"README.md"
|
||||
],
|
||||
"keywords": [
|
||||
"webclaw",
|
||||
"openclaw",
|
||||
"cli"
|
||||
],
|
||||
"license": "MIT",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
}
|
||||
Generated
+2
@@ -164,6 +164,8 @@ importers:
|
||||
specifier: ^5.1.0
|
||||
version: 5.1.0
|
||||
|
||||
packages/webclaw: {}
|
||||
|
||||
packages:
|
||||
|
||||
'@acemir/cssom@0.9.31':
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
packages:
|
||||
- apps/*
|
||||
- packages/*
|
||||
|
||||
Reference in New Issue
Block a user