0101-agents 0.0.1 → 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 (2) hide show
  1. package/bin/cli.js +324 -1
  2. package/package.json +1 -1
package/bin/cli.js CHANGED
@@ -1,2 +1,325 @@
1
1
  #!/usr/bin/env node
2
- console.log('0101 — coming soon. See https://0101.fyi')
2
+ // 0101-agents CLI installs and updates curated Claude Code agents.
3
+ //
4
+ // Commands:
5
+ // 0101-agents login [<license-key>]
6
+ // 0101-agents logout
7
+ // 0101-agents whoami
8
+ // 0101-agents install <agent> [--force]
9
+ // 0101-agents update <agent>
10
+ // 0101-agents list
11
+ // 0101-agents help
12
+
13
+ import { execSync } from 'node:child_process'
14
+ import {
15
+ existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync,
16
+ rmSync, statSync, createWriteStream,
17
+ } from 'node:fs'
18
+ import { homedir, tmpdir } from 'node:os'
19
+ import { join } from 'node:path'
20
+ import { Readable } from 'node:stream'
21
+ import { pipeline } from 'node:stream/promises'
22
+ import { createInterface } from 'node:readline/promises'
23
+
24
+ const HOME = homedir()
25
+ const CONFIG_DIR = join(HOME, '.0101')
26
+ const CONFIG_PATH = join(CONFIG_DIR, 'config.json')
27
+ const INSTALL_ROOT = join(HOME, '0101')
28
+ const BACKUPS_DIR = join(INSTALL_ROOT, '.backups')
29
+
30
+ const DEFAULT_API_BASE = 'https://0101.fyi'
31
+
32
+ // ─── pretty output ───────────────────────────────────────────────────────────
33
+
34
+ const c = (code, s) => `\x1b[${code}m${s}\x1b[0m`
35
+ const green = s => c(32, s)
36
+ const red = s => c(31, s)
37
+ const dim = s => c(2, s)
38
+ const bold = s => c(1, s)
39
+
40
+ const ok = s => console.log(green('✓ ') + s)
41
+ const err = s => console.error(red('✗ ') + s)
42
+ const info = s => console.log(dim(' ' + s))
43
+
44
+ function die(msg, code = 1) {
45
+ err(msg)
46
+ process.exit(code)
47
+ }
48
+
49
+ // ─── config ──────────────────────────────────────────────────────────────────
50
+
51
+ function readConfig() {
52
+ try {
53
+ return JSON.parse(readFileSync(CONFIG_PATH, 'utf8'))
54
+ } catch {
55
+ return {}
56
+ }
57
+ }
58
+
59
+ function writeConfig(next) {
60
+ mkdirSync(CONFIG_DIR, { recursive: true })
61
+ writeFileSync(CONFIG_PATH, JSON.stringify(next, null, 2) + '\n', { mode: 0o600 })
62
+ }
63
+
64
+ function apiBase() {
65
+ return readConfig().api_base || process.env['0101_API_BASE'] || DEFAULT_API_BASE
66
+ }
67
+
68
+ function licenseKey() {
69
+ const k = readConfig().license_key
70
+ if (!k) die('No license key. Run: 0101-agents login')
71
+ return k
72
+ }
73
+
74
+ // ─── api ─────────────────────────────────────────────────────────────────────
75
+
76
+ async function fetchManifest(agent) {
77
+ const url = `${apiBase()}/api/releases/${encodeURIComponent(agent)}`
78
+ const res = await fetch(url, {
79
+ headers: { Authorization: `Bearer ${licenseKey()}` },
80
+ })
81
+ if (res.status === 401) die('License invalid. Run: 0101-agents login')
82
+ if (res.status === 403) {
83
+ const body = await res.json().catch(() => ({}))
84
+ die(body.error || 'License does not cover this agent.')
85
+ }
86
+ if (res.status === 404) die(`Agent not found: ${agent}`)
87
+ if (!res.ok) die(`Manifest fetch failed: ${res.status} ${res.statusText}`)
88
+ return res.json()
89
+ }
90
+
91
+ async function downloadZip(downloadUrl, destPath) {
92
+ const res = await fetch(downloadUrl, {
93
+ headers: { Authorization: `Bearer ${licenseKey()}` },
94
+ })
95
+ if (!res.ok) die(`Download failed: ${res.status} ${res.statusText}`)
96
+ if (!res.body) die('Download returned empty body.')
97
+
98
+ // Node fetch returns a web ReadableStream; pipe to a file.
99
+ const nodeReadable = Readable.fromWeb(res.body)
100
+ await pipeline(nodeReadable, createWriteStream(destPath))
101
+ }
102
+
103
+ // ─── helpers ─────────────────────────────────────────────────────────────────
104
+
105
+ function agentInstallDir(agent) {
106
+ return join(INSTALL_ROOT, agent)
107
+ }
108
+
109
+ function installedVersion(agent) {
110
+ const versionFile = join(agentInstallDir(agent), '.0101-version')
111
+ try { return readFileSync(versionFile, 'utf8').trim() } catch { return null }
112
+ }
113
+
114
+ function writeInstalledVersion(agent, version) {
115
+ writeFileSync(join(agentInstallDir(agent), '.0101-version'), version + '\n')
116
+ }
117
+
118
+ function listInstalled() {
119
+ if (!existsSync(INSTALL_ROOT)) return []
120
+ return readdirSync(INSTALL_ROOT)
121
+ .filter(name => !name.startsWith('.'))
122
+ .filter(name => statSync(join(INSTALL_ROOT, name)).isDirectory())
123
+ }
124
+
125
+ function ensureUnzipAvailable() {
126
+ try { execSync('command -v unzip', { stdio: 'ignore' }) } catch {
127
+ die('The `unzip` binary is required but not found.\n' +
128
+ ' • macOS: comes pre-installed.\n' +
129
+ ' • Linux: apt install unzip / dnf install unzip.\n' +
130
+ ' • Windows: install via WSL or 7-Zip + PATH.')
131
+ }
132
+ }
133
+
134
+ function unzipInto(zipPath, destDir, { skipPrefix } = {}) {
135
+ // Use the system unzip binary. Available everywhere except bare Windows.
136
+ mkdirSync(destDir, { recursive: true })
137
+ if (skipPrefix) {
138
+ // unzip with -x excludes paths matching glob patterns.
139
+ execSync(
140
+ `unzip -q -o "${zipPath}" -d "${destDir}" -x "${skipPrefix}/*" "${skipPrefix}"`,
141
+ { stdio: 'inherit' },
142
+ )
143
+ } else {
144
+ execSync(`unzip -q -o "${zipPath}" -d "${destDir}"`, { stdio: 'inherit' })
145
+ }
146
+ }
147
+
148
+ function backupDataDir(agent) {
149
+ const dataDir = join(agentInstallDir(agent), 'data')
150
+ if (!existsSync(dataDir)) return null
151
+ mkdirSync(BACKUPS_DIR, { recursive: true })
152
+ const ts = new Date().toISOString().replace(/[:.]/g, '-')
153
+ const oldVersion = installedVersion(agent) ?? 'unknown'
154
+ const backupPath = join(BACKUPS_DIR, `${agent}-${oldVersion}-${ts}.tar.gz`)
155
+ execSync(
156
+ `tar -czf "${backupPath}" -C "${agentInstallDir(agent)}" data`,
157
+ { stdio: 'inherit' },
158
+ )
159
+ return backupPath
160
+ }
161
+
162
+ async function prompt(question) {
163
+ const rl = createInterface({ input: process.stdin, output: process.stdout })
164
+ try { return (await rl.question(question)).trim() } finally { rl.close() }
165
+ }
166
+
167
+ // ─── commands ────────────────────────────────────────────────────────────────
168
+
169
+ async function cmdLogin(args) {
170
+ let key = args[0]
171
+ if (!key) {
172
+ key = await prompt('Paste your license key: ')
173
+ }
174
+ key = key.trim()
175
+ if (!key) die('No key provided.')
176
+ writeConfig({ ...readConfig(), license_key: key })
177
+ ok(`Saved license to ${CONFIG_PATH}`)
178
+ }
179
+
180
+ function cmdLogout() {
181
+ const cfg = readConfig()
182
+ delete cfg.license_key
183
+ writeConfig(cfg)
184
+ ok('Logged out.')
185
+ }
186
+
187
+ async function cmdWhoami() {
188
+ const key = readConfig().license_key
189
+ if (!key) die('Not logged in. Run: 0101-agents login')
190
+ // Don't validate against the server here — keep it offline. `install` /
191
+ // `update` will surface a real 401 if the key is bad.
192
+ console.log(dim('License: ') + key.slice(0, 6) + '…' + key.slice(-4))
193
+ console.log(dim('API: ') + apiBase())
194
+ }
195
+
196
+ async function cmdInstall(args) {
197
+ const agent = args[0]
198
+ if (!agent) die('Usage: 0101-agents install <agent> [--force]')
199
+ const force = args.includes('--force')
200
+
201
+ ensureUnzipAvailable()
202
+ const installDir = agentInstallDir(agent)
203
+ if (existsSync(installDir) && !force) {
204
+ die(`${installDir} already exists. Use --force to overwrite (will wipe data/).`)
205
+ }
206
+
207
+ info(`Fetching manifest for ${agent}…`)
208
+ const manifest = await fetchManifest(agent)
209
+ info(`Latest: ${manifest.latest}`)
210
+
211
+ const zipPath = join(tmpdir(), `0101-${agent}-${manifest.latest}.zip`)
212
+ info(`Downloading…`)
213
+ await downloadZip(manifest.download_url, zipPath)
214
+
215
+ if (existsSync(installDir) && force) {
216
+ rmSync(installDir, { recursive: true, force: true })
217
+ }
218
+ mkdirSync(installDir, { recursive: true })
219
+ unzipInto(zipPath, installDir)
220
+ writeInstalledVersion(agent, manifest.latest)
221
+ rmSync(zipPath, { force: true })
222
+
223
+ console.log('')
224
+ ok(`Installed ${bold(agent)} ${manifest.latest} → ${installDir}`)
225
+ info(`Start: cd ${installDir} && claude`)
226
+ }
227
+
228
+ async function cmdUpdate(args) {
229
+ const agent = args[0]
230
+ if (!agent) die('Usage: 0101-agents update <agent>')
231
+
232
+ ensureUnzipAvailable()
233
+ const installDir = agentInstallDir(agent)
234
+ if (!existsSync(installDir)) die(`${agent} is not installed. Try: 0101-agents install ${agent}`)
235
+
236
+ const current = installedVersion(agent)
237
+ info(`Installed: ${current ?? 'unknown'}`)
238
+
239
+ info(`Fetching manifest…`)
240
+ const manifest = await fetchManifest(agent)
241
+ info(`Latest: ${manifest.latest}`)
242
+
243
+ if (current === manifest.latest) {
244
+ ok('Already up to date.')
245
+ return
246
+ }
247
+
248
+ const ans = await prompt(`Update ${agent} ${current ?? '?'} → ${manifest.latest}? [Y/n] `)
249
+ if (ans && !/^y(es)?$/i.test(ans)) {
250
+ info('Cancelled.')
251
+ return
252
+ }
253
+
254
+ const backup = backupDataDir(agent)
255
+ if (backup) info(`Backed up data → ${backup}`)
256
+
257
+ const zipPath = join(tmpdir(), `0101-${agent}-${manifest.latest}.zip`)
258
+ info(`Downloading…`)
259
+ await downloadZip(manifest.download_url, zipPath)
260
+
261
+ // Extract into the install dir, skipping the data/ subtree so user state
262
+ // survives. unzip's -x flag drops matching entries.
263
+ unzipInto(zipPath, installDir, { skipPrefix: 'data' })
264
+ writeInstalledVersion(agent, manifest.latest)
265
+ rmSync(zipPath, { force: true })
266
+
267
+ console.log('')
268
+ ok(`Updated ${bold(agent)} → ${manifest.latest}`)
269
+ }
270
+
271
+ function cmdList() {
272
+ const installed = listInstalled()
273
+ if (installed.length === 0) {
274
+ info('No agents installed.')
275
+ info(`Install one: 0101-agents install <name>`)
276
+ return
277
+ }
278
+ for (const name of installed) {
279
+ const v = installedVersion(name) ?? dim('?')
280
+ console.log(` ${bold(name)} ${dim(v)}`)
281
+ }
282
+ }
283
+
284
+ function cmdHelp() {
285
+ console.log(`0101-agents — install and update Claude Code agents.
286
+
287
+ Commands:
288
+ ${bold('login')} [<key>] Save your license key
289
+ ${bold('logout')} Forget your license key
290
+ ${bold('whoami')} Show current license
291
+ ${bold('install')} <agent> [--force] Install an agent
292
+ ${bold('update')} <agent> Update an installed agent (preserves data/)
293
+ ${bold('list')} List installed agents
294
+ ${bold('help')} Show this message
295
+
296
+ Install dir: ${INSTALL_ROOT}
297
+ Config: ${CONFIG_PATH}`)
298
+ }
299
+
300
+ // ─── dispatch ────────────────────────────────────────────────────────────────
301
+
302
+ const [, , cmd, ...rest] = process.argv
303
+
304
+ try {
305
+ switch (cmd) {
306
+ case 'login': await cmdLogin(rest); break
307
+ case 'logout': cmdLogout(); break
308
+ case 'whoami': await cmdWhoami(); break
309
+ case 'install': await cmdInstall(rest); break
310
+ case 'update': await cmdUpdate(rest); break
311
+ case 'list': cmdList(); break
312
+ case 'help':
313
+ case '-h':
314
+ case '--help':
315
+ case undefined:
316
+ cmdHelp(); break
317
+ default:
318
+ err(`Unknown command: ${cmd}`)
319
+ cmdHelp()
320
+ process.exit(1)
321
+ }
322
+ } catch (e) {
323
+ err(e?.message ?? String(e))
324
+ process.exit(1)
325
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "0101-agents",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "description": "",
5
5
  "bin": {
6
6
  "0101-agents": "./bin/cli.js"