@claweditor/cli 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 ADDED
@@ -0,0 +1,45 @@
1
+ # @claweditor/cli
2
+
3
+ Install and launch the [ClawEditor](https://github.com/i1see1you/ClawEditor) desktop app from GitHub Releases.
4
+
5
+ ## Install CLI
6
+
7
+ ```bash
8
+ npm install -g @claweditor/cli
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ # Download (if needed) and launch
15
+ claw-editor
16
+
17
+ # Install only
18
+ claw-editor install
19
+
20
+ # Re-download latest release
21
+ claw-editor update
22
+
23
+ # Install a specific version
24
+ claw-editor install --tag 0.1.0
25
+
26
+ # Show versions
27
+ claw-editor version
28
+ ```
29
+
30
+ Install location:
31
+
32
+ | OS | Path |
33
+ |----|------|
34
+ | macOS | `~/Library/Application Support/claweditor/` |
35
+ | Linux | `~/.local/share/claweditor/` |
36
+ | Windows | `%LOCALAPPDATA%\claweditor\` |
37
+
38
+ ## Publish (maintainers)
39
+
40
+ ```bash
41
+ cd packages/cli
42
+ npm publish --access public
43
+ ```
44
+
45
+ Desktop bundles must be uploaded to [GitHub Releases](https://github.com/i1see1you/ClawEditor/releases) first (see `.github/workflows/release.yml`).
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync } from 'node:fs'
4
+ import path from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+ import { install, runApp } from '../lib/install.js'
7
+ import { readInstallMeta, resolveLaunchPath } from '../lib/paths.js'
8
+ import { GITHUB_OWNER, GITHUB_REPO } from '../lib/config.js'
9
+
10
+ const pkg = JSON.parse(
11
+ readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8'),
12
+ )
13
+
14
+ function printHelp() {
15
+ process.stdout.write(`ClawEditor desktop launcher (@claweditor/cli)
16
+
17
+ Usage:
18
+ claw-editor Download (if needed) and launch ClawEditor
19
+ claw-editor install Install from GitHub Releases
20
+ claw-editor update Re-download and install latest release
21
+ claw-editor run Same as default (launch)
22
+ claw-editor version Show CLI and installed app version
23
+ claw-editor help Show this help
24
+
25
+ Options:
26
+ --tag <version> Install a specific release (e.g. 0.1.0 or v0.1.0)
27
+ --force Force re-download
28
+
29
+ Releases: https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/releases
30
+ `)
31
+ }
32
+
33
+ function parseArgs(argv) {
34
+ /** @type {{ command: string, tag?: string, force: boolean }} */
35
+ const out = { command: 'run', force: false }
36
+ const args = [...argv]
37
+ while (args.length) {
38
+ const a = args.shift()
39
+ if (a === '--tag') {
40
+ out.tag = args.shift()
41
+ continue
42
+ }
43
+ if (a === '--force') {
44
+ out.force = true
45
+ continue
46
+ }
47
+ if (a === '--help' || a === '-h') {
48
+ out.command = 'help'
49
+ continue
50
+ }
51
+ if (!a.startsWith('-')) {
52
+ out.command = a
53
+ }
54
+ }
55
+ return out
56
+ }
57
+
58
+ async function main() {
59
+ const { command, tag, force } = parseArgs(process.argv.slice(2))
60
+
61
+ if (command === 'help') {
62
+ printHelp()
63
+ return
64
+ }
65
+
66
+ if (command === 'version' || command === '--version' || command === '-v') {
67
+ const meta = readInstallMeta()
68
+ const launchPath = resolveLaunchPath(meta)
69
+ process.stdout.write(`@claweditor/cli ${pkg.version}\n`)
70
+ if (meta?.version) {
71
+ process.stdout.write(`ClawEditor ${meta.version}${launchPath ? ` (${launchPath})` : ''}\n`)
72
+ } else {
73
+ process.stdout.write('ClawEditor: not installed\n')
74
+ }
75
+ return
76
+ }
77
+
78
+ if (command === 'install') {
79
+ await install({ tag, force })
80
+ return
81
+ }
82
+
83
+ if (command === 'update') {
84
+ await install({ tag, force: true })
85
+ return
86
+ }
87
+
88
+ if (command === 'run' || command === 'start') {
89
+ await runApp({ tag, force })
90
+ return
91
+ }
92
+
93
+ process.stderr.write(`Unknown command: ${command}\n\n`)
94
+ printHelp()
95
+ process.exit(1)
96
+ }
97
+
98
+ main().catch((err) => {
99
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`)
100
+ process.exit(1)
101
+ })
package/lib/config.js ADDED
@@ -0,0 +1,20 @@
1
+ /** @typedef {'darwin-arm64' | 'darwin-x64' | 'linux-x64' | 'linux-arm64' | 'win32-x64' | 'win32-arm64'} PlatformKey */
2
+
3
+ export const GITHUB_OWNER = 'i1see1you'
4
+ export const GITHUB_REPO = 'ClawEditor'
5
+ export const PRODUCT_NAME = 'ClawEditor'
6
+ export const APP_BINARY_NAME = process.platform === 'win32' ? 'claw-editor.exe' : 'claw-editor'
7
+
8
+ /**
9
+ * GitHub Release asset name patterns (Tauri bundle naming).
10
+ * First matching asset wins.
11
+ * @type {Record<PlatformKey, RegExp[]>}
12
+ */
13
+ export const ASSET_PATTERNS = {
14
+ 'darwin-arm64': [/aarch64\.app\.tar\.gz$/i, /aarch64\.dmg$/i],
15
+ 'darwin-x64': [/x64\.app\.tar\.gz$/i, /x64\.dmg$/i],
16
+ 'linux-x64': [/amd64\.AppImage$/i, /amd64\.deb$/i],
17
+ 'linux-arm64': [/aarch64\.AppImage$/i, /arm64\.deb$/i],
18
+ 'win32-x64': [/x64-setup\.exe$/i, /x64\.msi$/i],
19
+ 'win32-arm64': [/arm64-setup\.exe$/i, /arm64\.msi$/i],
20
+ }
package/lib/github.js ADDED
@@ -0,0 +1,71 @@
1
+ import { GITHUB_OWNER, GITHUB_REPO } from './config.js'
2
+
3
+ const API = `https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}`
4
+
5
+ /**
6
+ * @param {string | undefined} tag `latest` or `v0.1.0`
7
+ */
8
+ export async function fetchRelease(tag) {
9
+ const url = tag && tag !== 'latest'
10
+ ? `${API}/releases/tags/${tag.startsWith('v') ? tag : `v${tag}`}`
11
+ : `${API}/releases/latest`
12
+
13
+ const res = await fetch(url, {
14
+ headers: {
15
+ Accept: 'application/vnd.github+json',
16
+ 'User-Agent': '@claweditor/cli',
17
+ },
18
+ })
19
+
20
+ if (!res.ok) {
21
+ const body = await res.text().catch(() => '')
22
+ throw new Error(
23
+ `GitHub release not found (${res.status}): ${url}${body ? `\n${body.slice(0, 200)}` : ''}`,
24
+ )
25
+ }
26
+
27
+ return /** @type {Promise<{ tag_name: string, assets: Array<{ name: string, browser_download_url: string, size: number }> }>} */ (res.json())
28
+ }
29
+
30
+ /**
31
+ * @param {Array<{ name: string, browser_download_url: string }>} assets
32
+ * @param {RegExp[]} patterns
33
+ */
34
+ export function pickAsset(assets, patterns) {
35
+ for (const pattern of patterns) {
36
+ const hit = assets.find((a) => pattern.test(a.name))
37
+ if (hit) return hit
38
+ }
39
+ return null
40
+ }
41
+
42
+ /**
43
+ * @param {string} url
44
+ * @param {string} dest
45
+ * @param {(received: number, total: number) => void} [onProgress]
46
+ */
47
+ export async function downloadFile(url, dest, onProgress) {
48
+ const res = await fetch(url, {
49
+ headers: { 'User-Agent': '@claweditor/cli' },
50
+ redirect: 'follow',
51
+ })
52
+ if (!res.ok || !res.body) {
53
+ throw new Error(`Download failed (${res.status}): ${url}`)
54
+ }
55
+
56
+ const total = Number(res.headers.get('content-length') || 0)
57
+ const reader = res.body.getReader()
58
+ const chunks = []
59
+ let received = 0
60
+
61
+ while (true) {
62
+ const { done, value } = await reader.read()
63
+ if (done) break
64
+ chunks.push(value)
65
+ received += value.byteLength
66
+ onProgress?.(received, total)
67
+ }
68
+
69
+ const { writeFileSync } = await import('node:fs')
70
+ writeFileSync(dest, Buffer.concat(chunks))
71
+ }
package/lib/install.js ADDED
@@ -0,0 +1,209 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { spawn } from 'node:child_process'
4
+ import { APP_BINARY_NAME, ASSET_PATTERNS, PRODUCT_NAME } from './config.js'
5
+ import { downloadFile, fetchRelease, pickAsset } from './github.js'
6
+ import {
7
+ ensureDir,
8
+ getCacheDir,
9
+ getDataDir,
10
+ readInstallMeta,
11
+ resolveLaunchPath,
12
+ writeInstallMeta,
13
+ } from './paths.js'
14
+ import { detectPlatformKey } from './platform.js'
15
+
16
+ function log(msg) {
17
+ process.stderr.write(`${msg}\n`)
18
+ }
19
+
20
+ function formatProgress(received, total) {
21
+ if (!total) return ''
22
+ const pct = Math.min(100, Math.round((received / total) * 100))
23
+ return ` ${pct}%`
24
+ }
25
+
26
+ /**
27
+ * @param {string} cmd
28
+ * @param {string[]} args
29
+ * @param {import('node:child_process').SpawnOptions} [opts]
30
+ */
31
+ function run(cmd, args, opts = {}) {
32
+ return new Promise((resolve, reject) => {
33
+ const child = spawn(cmd, args, { stdio: 'inherit', ...opts })
34
+ child.on('error', reject)
35
+ child.on('close', (code) => {
36
+ if (code === 0) resolve(undefined)
37
+ else reject(new Error(`Command failed (${code}): ${cmd} ${args.join(' ')}`))
38
+ })
39
+ })
40
+ }
41
+
42
+ /**
43
+ * @param {string} archivePath
44
+ * @param {string} destDir
45
+ */
46
+ async function extractTarGz(archivePath, destDir) {
47
+ ensureDir(destDir)
48
+ await run('tar', ['-xzf', archivePath, '-C', destDir])
49
+ }
50
+
51
+ function findAppBundle(rootDir) {
52
+ const direct = path.join(rootDir, `${PRODUCT_NAME}.app`)
53
+ if (fs.existsSync(direct)) return direct
54
+
55
+ const entries = fs.readdirSync(rootDir, { withFileTypes: true })
56
+ for (const e of entries) {
57
+ if (e.isDirectory() && e.name.endsWith('.app')) {
58
+ return path.join(rootDir, e.name)
59
+ }
60
+ }
61
+ return null
62
+ }
63
+
64
+ /**
65
+ * @param {string} assetPath
66
+ * @param {string} assetName
67
+ * @param {string} version
68
+ */
69
+ async function installFromAsset(assetPath, assetName, version) {
70
+ const dataDir = getDataDir()
71
+ ensureDir(dataDir)
72
+ const lower = assetName.toLowerCase()
73
+
74
+ if (lower.endsWith('.app.tar.gz')) {
75
+ const extractDir = path.join(dataDir, 'current')
76
+ if (fs.existsSync(extractDir)) {
77
+ fs.rmSync(extractDir, { recursive: true, force: true })
78
+ }
79
+ ensureDir(extractDir)
80
+ await extractTarGz(assetPath, extractDir)
81
+ const appBundle = findAppBundle(extractDir)
82
+ if (!appBundle) {
83
+ throw new Error(`Could not find ${PRODUCT_NAME}.app after extracting ${assetName}`)
84
+ }
85
+ const launchPath = path.join(appBundle, 'Contents', 'MacOS', APP_BINARY_NAME)
86
+ if (!fs.existsSync(launchPath)) {
87
+ throw new Error(`Missing binary in app bundle: ${launchPath}`)
88
+ }
89
+ writeInstallMeta({ version, assetName, launchPath, installedAt: new Date().toISOString() })
90
+ return launchPath
91
+ }
92
+
93
+ if (lower.endsWith('.appimage')) {
94
+ const dest = path.join(dataDir, 'ClawEditor.AppImage')
95
+ fs.copyFileSync(assetPath, dest)
96
+ fs.chmodSync(dest, 0o755)
97
+ writeInstallMeta({ version, assetName, launchPath: dest, installedAt: new Date().toISOString() })
98
+ return dest
99
+ }
100
+
101
+ if (lower.endsWith('-setup.exe') || lower.endsWith('.msi')) {
102
+ log(`Running Windows installer: ${assetName}`)
103
+ log('Complete the installer dialog, then run `claw-editor` again.')
104
+ await run(assetPath, [], { shell: true, stdio: 'inherit' })
105
+ const launchPath = path.join(
106
+ process.env.LOCALAPPDATA || '',
107
+ PRODUCT_NAME,
108
+ APP_BINARY_NAME,
109
+ )
110
+ writeInstallMeta({
111
+ version,
112
+ assetName,
113
+ launchPath,
114
+ installedAt: new Date().toISOString(),
115
+ note: 'Windows GUI installer',
116
+ })
117
+ return launchPath
118
+ }
119
+
120
+ if (lower.endsWith('.dmg')) {
121
+ throw new Error(
122
+ `Automatic install for .dmg is not supported yet. Download ${assetName} from GitHub Releases and drag ${PRODUCT_NAME} to Applications.`,
123
+ )
124
+ }
125
+
126
+ throw new Error(`Unsupported release asset: ${assetName}`)
127
+ }
128
+
129
+ /**
130
+ * @param {{ tag?: string, force?: boolean }} [opts]
131
+ */
132
+ export async function install(opts = {}) {
133
+ const platformKey = detectPlatformKey()
134
+ const patterns = ASSET_PATTERNS[platformKey]
135
+ if (!patterns) {
136
+ throw new Error(`No release asset mapping for ${platformKey}`)
137
+ }
138
+
139
+ const release = await fetchRelease(opts.tag)
140
+ const asset = pickAsset(release.assets, patterns)
141
+ if (!asset) {
142
+ const names = release.assets.map((a) => a.name).join(', ')
143
+ throw new Error(
144
+ `No matching asset for ${platformKey} in ${release.tag_name}. Available: ${names || '(none)'}`,
145
+ )
146
+ }
147
+
148
+ const version = release.tag_name.replace(/^v/, '')
149
+ ensureDir(getCacheDir())
150
+ const cachePath = path.join(getCacheDir(), asset.name)
151
+
152
+ if (!opts.force && fs.existsSync(cachePath)) {
153
+ log(`Using cached ${asset.name}`)
154
+ } else {
155
+ log(`Downloading ${asset.name} from ${release.tag_name}…`)
156
+ await downloadFile(asset.browser_download_url, cachePath, (received, total) => {
157
+ process.stderr.write(`\rDownloading${formatProgress(received, total)}`)
158
+ })
159
+ process.stderr.write('\n')
160
+ }
161
+
162
+ const launchPath = await installFromAsset(cachePath, asset.name, version)
163
+ log(`Installed ${PRODUCT_NAME} ${version}`)
164
+ return { version, launchPath, assetName: asset.name }
165
+ }
166
+
167
+ /**
168
+ * @param {string} launchPath
169
+ */
170
+ export function launchApp(launchPath) {
171
+ if (process.platform === 'darwin') {
172
+ if (launchPath.endsWith('.app') || launchPath.includes('.app/')) {
173
+ const appPath = launchPath.includes('.app/')
174
+ ? launchPath.split('.app/')[0] + '.app'
175
+ : launchPath
176
+ spawn('open', ['-a', appPath], { detached: true, stdio: 'ignore' }).unref()
177
+ return
178
+ }
179
+ }
180
+
181
+ if (launchPath.endsWith('.AppImage')) {
182
+ spawn(launchPath, [], { detached: true, stdio: 'ignore' }).unref()
183
+ return
184
+ }
185
+
186
+ spawn(launchPath, [], { detached: true, stdio: 'ignore', shell: process.platform === 'win32' }).unref()
187
+ }
188
+
189
+ /**
190
+ * Ensure app is installed and launch it.
191
+ * @param {{ tag?: string, force?: boolean }} [opts]
192
+ */
193
+ export async function runApp(opts = {}) {
194
+ let launchPath = resolveLaunchPath(opts.force ? null : readInstallMeta())
195
+
196
+ if (!launchPath || opts.force) {
197
+ const result = await install({ tag: opts.tag, force: opts.force })
198
+ launchPath = result.launchPath
199
+ }
200
+
201
+ if (!launchPath || !fs.existsSync(launchPath)) {
202
+ throw new Error(
203
+ `${PRODUCT_NAME} is not installed. Run: npm install -g @claweditor/cli && claw-editor install`,
204
+ )
205
+ }
206
+
207
+ launchApp(launchPath)
208
+ log(`Launched ${PRODUCT_NAME}`)
209
+ }
package/lib/paths.js ADDED
@@ -0,0 +1,81 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { APP_BINARY_NAME, PRODUCT_NAME } from './config.js'
5
+
6
+ export function getDataDir() {
7
+ const home = os.homedir()
8
+ if (process.platform === 'darwin') {
9
+ return path.join(home, 'Library', 'Application Support', 'claweditor')
10
+ }
11
+ if (process.platform === 'win32') {
12
+ return path.join(process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local'), 'claweditor')
13
+ }
14
+ return path.join(process.env.XDG_DATA_HOME || path.join(home, '.local', 'share'), 'claweditor')
15
+ }
16
+
17
+ export function getInstallMetaPath() {
18
+ return path.join(getDataDir(), 'install.json')
19
+ }
20
+
21
+ export function getCacheDir() {
22
+ return path.join(getDataDir(), 'cache')
23
+ }
24
+
25
+ export function ensureDir(dir) {
26
+ fs.mkdirSync(dir, { recursive: true })
27
+ }
28
+
29
+ /**
30
+ * Resolve the executable path from an install record or common install locations.
31
+ * @param {{ version?: string, launchPath?: string, assetName?: string } | null} meta
32
+ */
33
+ export function resolveLaunchPath(meta) {
34
+ if (meta?.launchPath && fs.existsSync(meta.launchPath)) {
35
+ return meta.launchPath
36
+ }
37
+
38
+ if (process.platform === 'darwin') {
39
+ const appBundle = path.join(getDataDir(), `${PRODUCT_NAME}.app`, 'Contents', 'MacOS', APP_BINARY_NAME)
40
+ if (fs.existsSync(appBundle)) return appBundle
41
+ }
42
+
43
+ if (process.platform === 'linux') {
44
+ const appImage = path.join(getDataDir(), 'ClawEditor.AppImage')
45
+ if (fs.existsSync(appImage)) return appImage
46
+ const bin = path.join(getDataDir(), 'bin', APP_BINARY_NAME)
47
+ if (fs.existsSync(bin)) return bin
48
+ }
49
+
50
+ if (process.platform === 'win32') {
51
+ const candidates = [
52
+ path.join(getDataDir(), APP_BINARY_NAME),
53
+ path.join(getDataDir(), `${PRODUCT_NAME}.exe`),
54
+ path.join(process.env.LOCALAPPDATA || '', PRODUCT_NAME, `${PRODUCT_NAME}.exe`),
55
+ path.join(process.env.LOCALAPPDATA || '', PRODUCT_NAME, APP_BINARY_NAME),
56
+ path.join(process.env.LOCALAPPDATA || '', 'Programs', PRODUCT_NAME, `${PRODUCT_NAME}.exe`),
57
+ path.join(process.env.LOCALAPPDATA || '', 'Programs', PRODUCT_NAME, APP_BINARY_NAME),
58
+ ]
59
+ for (const c of candidates) {
60
+ if (c && fs.existsSync(c)) return c
61
+ }
62
+ }
63
+
64
+ return null
65
+ }
66
+
67
+ export function readInstallMeta() {
68
+ const metaPath = getInstallMetaPath()
69
+ if (!fs.existsSync(metaPath)) return null
70
+ try {
71
+ return JSON.parse(fs.readFileSync(metaPath, 'utf8'))
72
+ } catch {
73
+ return null
74
+ }
75
+ }
76
+
77
+ /** @param {Record<string, unknown>} meta */
78
+ export function writeInstallMeta(meta) {
79
+ ensureDir(getDataDir())
80
+ fs.writeFileSync(getInstallMetaPath(), JSON.stringify(meta, null, 2) + '\n')
81
+ }
@@ -0,0 +1,21 @@
1
+ /** @typedef {import('./config.js').PlatformKey} PlatformKey */
2
+
3
+ /**
4
+ * @returns {PlatformKey}
5
+ */
6
+ export function detectPlatformKey() {
7
+ const arch = process.arch === 'x64' ? 'x64' : process.arch === 'arm64' ? 'arm64' : process.arch
8
+ if (process.platform === 'darwin') {
9
+ if (arch === 'arm64') return 'darwin-arm64'
10
+ if (arch === 'x64') return 'darwin-x64'
11
+ }
12
+ if (process.platform === 'linux') {
13
+ if (arch === 'arm64') return 'linux-arm64'
14
+ return 'linux-x64'
15
+ }
16
+ if (process.platform === 'win32') {
17
+ if (arch === 'arm64') return 'win32-arm64'
18
+ return 'win32-x64'
19
+ }
20
+ throw new Error(`Unsupported platform: ${process.platform} ${process.arch}`)
21
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@claweditor/cli",
3
+ "version": "0.1.0",
4
+ "description": "Install and launch the ClawEditor desktop app from GitHub Releases",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "engines": {
8
+ "node": ">=18"
9
+ },
10
+ "bin": {
11
+ "claw-editor": "bin/claw-editor.js"
12
+ },
13
+ "files": [
14
+ "bin",
15
+ "lib",
16
+ "README.md"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/i1see1you/ClawEditor.git",
21
+ "directory": "packages/cli"
22
+ },
23
+ "keywords": [
24
+ "claweditor",
25
+ "tauri",
26
+ "openclaw",
27
+ "installer"
28
+ ],
29
+ "publishConfig": {
30
+ "access": "public"
31
+ }
32
+ }