0101-agents 0.1.3 → 0.1.6

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 +85 -32
  2. package/package.json +1 -1
package/bin/cli.js CHANGED
@@ -6,7 +6,7 @@
6
6
  // 0101-agents logout
7
7
  // 0101-agents whoami
8
8
  // 0101-agents install <agent> [--force]
9
- // 0101-agents update <agent>
9
+ // 0101-agents update <agent> [-f]
10
10
  // 0101-agents list
11
11
  // 0101-agents help
12
12
 
@@ -131,31 +131,64 @@ function ensureUnzipAvailable() {
131
131
  }
132
132
  }
133
133
 
134
- function unzipInto(zipPath, destDir, { skipPrefix } = {}) {
134
+ function unzipInto(zipPath, destDir, { exclude = [] } = {}) {
135
135
  // Use the system unzip binary. Available everywhere except bare Windows.
136
+ // -o overwrites; a single -x takes the whole pattern list (after -d), used on
137
+ // update to keep the manifest's `preserve` paths (e.g. data/, .env) intact.
136
138
  mkdirSync(destDir, { recursive: true })
137
- if (skipPrefix) {
138
- // unzip's -x excludes paths matching glob patterns. `<prefix>/*` catches
139
- // every file under the directory; the bare directory entry (e.g. `data/`)
140
- // is harmless to re-extract because it just re-creates the dir shell.
141
- execSync(
142
- `unzip -q -o "${zipPath}" -d "${destDir}" -x "${skipPrefix}/*"`,
143
- { stdio: 'inherit' },
144
- )
145
- } else {
146
- execSync(`unzip -q -o "${zipPath}" -d "${destDir}"`, { stdio: 'inherit' })
139
+ const xs = exclude.length ? `-x ${exclude.map((p) => `"${p}"`).join(' ')}` : ''
140
+ execSync(`unzip -q -o "${zipPath}" -d "${destDir}" ${xs}`.trim(), { stdio: 'inherit' })
141
+ }
142
+
143
+ // Read the installed artifact's own manifest.json (NOT the release manifest
144
+ // from the API). Drives per-artifact behavior: which paths to keep on update
145
+ // (`preserve`), an optional post-install command (`postInstall`), and how
146
+ // `start` launches. Fails soft to {} so agents without these fields behave
147
+ // exactly as before.
148
+ function readManifest(agent) {
149
+ try {
150
+ return JSON.parse(readFileSync(join(agentInstallDir(agent), 'manifest.json'), 'utf8'))
151
+ } catch {
152
+ return {}
147
153
  }
148
154
  }
149
155
 
150
- function backupDataDir(agent) {
151
- const dataDir = join(agentInstallDir(agent), 'data')
152
- if (!existsSync(dataDir)) return null
156
+ // Paths an update must NOT overwrite. Agents keep their data/ by default; the
157
+ // runner declares [".env", ".runner-state"]. Always returns a non-empty array.
158
+ function preserveList(manifest) {
159
+ const p = manifest?.preserve
160
+ return Array.isArray(p) && p.length ? p : ['data']
161
+ }
162
+
163
+ // unzip -x patterns: drop both the entry and everything under it, so a
164
+ // preserved file (.env) and a preserved dir (data/) are each left untouched.
165
+ function excludePatterns(preserve) {
166
+ return preserve.flatMap((p) => [p, `${p}/*`])
167
+ }
168
+
169
+ // Run the manifest's postInstall command (if any) in the install dir — e.g. the
170
+ // runner's `npm ci && node scripts/postinstall.mjs`. Trusted: the zip is the
171
+ // license-gated artifact we shipped. No-op for agents (no postInstall field).
172
+ function runPostInstall(agent, manifest) {
173
+ const cmd = manifest?.postInstall
174
+ if (!cmd || typeof cmd !== 'string') return
175
+ info('Running post-install…')
176
+ execSync(cmd, { cwd: agentInstallDir(agent), stdio: 'inherit' })
177
+ }
178
+
179
+ // Back up the paths an update would replace, so a bad release is recoverable.
180
+ // Tars only the preserve paths that actually exist (data/ for agents; .env +
181
+ // .runner-state/ for the runner).
182
+ function backupPreserved(agent, preserve) {
183
+ const dir = agentInstallDir(agent)
184
+ const existing = preserve.filter((p) => existsSync(join(dir, p)))
185
+ if (!existing.length) return null
153
186
  mkdirSync(BACKUPS_DIR, { recursive: true })
154
187
  const ts = new Date().toISOString().replace(/[:.]/g, '-')
155
188
  const oldVersion = installedVersion(agent) ?? 'unknown'
156
189
  const backupPath = join(BACKUPS_DIR, `${agent}-${oldVersion}-${ts}.tar.gz`)
157
190
  execSync(
158
- `tar -czf "${backupPath}" -C "${agentInstallDir(agent)}" data`,
191
+ `tar -czf "${backupPath}" -C "${dir}" ${existing.map((p) => `"${p}"`).join(' ')}`,
159
192
  { stdio: 'inherit' },
160
193
  )
161
194
  return backupPath
@@ -220,18 +253,23 @@ async function cmdInstall(args) {
220
253
  }
221
254
  mkdirSync(installDir, { recursive: true })
222
255
  unzipInto(zipPath, installDir)
223
- writeInstalledVersion(agent, manifest.latest)
224
256
  rmSync(zipPath, { force: true })
257
+ const localManifest = readManifest(agent)
258
+ runPostInstall(agent, localManifest)
259
+ writeInstalledVersion(agent, manifest.latest)
225
260
 
226
261
  console.log('')
227
262
  ok(`Installed ${bold(agent)} ${manifest.latest} → ${installDir}`)
228
263
  info(`Start: 0101-agents start ${agent}`)
229
- info(` or: cd ${installDir} && claude`)
264
+ // The "open in Claude" hint only applies to agent folders. Artifacts with a
265
+ // custom launch (manifest.start, e.g. the runner) aren't opened in Claude.
266
+ if (!localManifest.start) info(` or: cd ${installDir} && claude`)
230
267
  }
231
268
 
232
269
  async function cmdUpdate(args) {
233
- const agent = args[0]
234
- if (!agent) die('Usage: 0101-agents update <agent>')
270
+ const agent = args.find((a) => !a.startsWith('-'))
271
+ const assumeYes = args.some((a) => ['-f', '--force', '-y', '--yes'].includes(a))
272
+ if (!agent) die('Usage: 0101-agents update <agent> [-f]')
235
273
 
236
274
  ensureUnzipAvailable()
237
275
  const installDir = agentInstallDir(agent)
@@ -249,25 +287,29 @@ async function cmdUpdate(args) {
249
287
  return
250
288
  }
251
289
 
252
- console.log(dim('Your data/ folder is preserved (memory, projects, custom files).'))
253
- const ans = await prompt(`Update ${agent} ${current ?? '?'} → ${manifest.latest}? [Y/n] `)
254
- if (ans && !/^y(es)?$/i.test(ans)) {
255
- info('Cancelled.')
256
- return
290
+ console.log(dim('Your local data is preserved across the update.'))
291
+ if (!assumeYes) {
292
+ const ans = await prompt(`Update ${agent} ${current ?? '?'} → ${manifest.latest}? [Y/n] `)
293
+ if (ans && !/^y(es)?$/i.test(ans)) {
294
+ info('Cancelled.')
295
+ return
296
+ }
257
297
  }
258
298
 
259
- const backup = backupDataDir(agent)
260
- if (backup) info(`Backed up data → ${backup}`)
299
+ const preserve = preserveList(readManifest(agent))
300
+ const backup = backupPreserved(agent, preserve)
301
+ if (backup) info(`Backed up ${preserve.join(', ')} → ${backup}`)
261
302
 
262
303
  const zipPath = join(tmpdir(), `0101-${agent}-${manifest.latest}.zip`)
263
304
  info(`Downloading…`)
264
305
  await downloadZip(manifest.download_url, zipPath)
265
306
 
266
- // Extract into the install dir, skipping the data/ subtree so user state
267
- // survives. unzip's -x flag drops matching entries.
268
- unzipInto(zipPath, installDir, { skipPrefix: 'data' })
269
- writeInstalledVersion(agent, manifest.latest)
307
+ // Extract over the install, skipping the manifest's `preserve` paths so user
308
+ // state survives (agents: data/; runner: .env + .runner-state/).
309
+ unzipInto(zipPath, installDir, { exclude: excludePatterns(preserve) })
270
310
  rmSync(zipPath, { force: true })
311
+ runPostInstall(agent, readManifest(agent))
312
+ writeInstalledVersion(agent, manifest.latest)
271
313
 
272
314
  console.log('')
273
315
  ok(`Updated ${bold(agent)} → ${manifest.latest}`)
@@ -308,6 +350,17 @@ async function cmdStart(args) {
308
350
  // ignore
309
351
  }
310
352
 
353
+ // Artifacts can declare how `start` launches via manifest.start. The runner
354
+ // sets "npm start"; agents leave it unset and fall through to launching
355
+ // Claude Code below.
356
+ const startCmd = readManifest(agent).start
357
+ if (startCmd) {
358
+ const child = spawn(startCmd, { cwd: dir, stdio: 'inherit', shell: true })
359
+ child.on('error', (e) => die(`Failed to start: ${e.message}`))
360
+ child.on('exit', (code) => process.exit(code ?? 0))
361
+ return
362
+ }
363
+
311
364
  // Everything after the agent name passes through to `claude`. Supports both:
312
365
  // 0101-agents start marketer --resume
313
366
  // 0101-agents start marketer -- --resume (explicit separator)
@@ -349,7 +402,7 @@ Commands:
349
402
  ${bold('logout')} Forget your license key
350
403
  ${bold('whoami')} Show current license
351
404
  ${bold('install')} <agent> [--force] Install an agent
352
- ${bold('update')} <agent> Update an installed agent (preserves data/)
405
+ ${bold('update')} <agent> [-f] Update an installed agent (preserves data/). -f skips the prompt
353
406
  ${bold('start')} <agent> [--tg] [-- args] Open the agent (runs claude in its dir)
354
407
  --tg adds the right flags to run as a
355
408
  Telegram channel bot.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "0101-agents",
3
- "version": "0.1.3",
3
+ "version": "0.1.6",
4
4
  "description": "",
5
5
  "bin": {
6
6
  "0101-agents": "./bin/cli.js"