@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 +45 -0
- package/bin/claw-editor.js +101 -0
- package/lib/config.js +20 -0
- package/lib/github.js +71 -0
- package/lib/install.js +209 -0
- package/lib/paths.js +81 -0
- package/lib/platform.js +21 -0
- package/package.json +32 -0
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
|
+
}
|
package/lib/platform.js
ADDED
|
@@ -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
|
+
}
|