0101-agents 0.1.3 → 0.1.5

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 +81 -31
  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,8 +253,9 @@ 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
+ runPostInstall(agent, readManifest(agent))
258
+ writeInstalledVersion(agent, manifest.latest)
225
259
 
226
260
  console.log('')
227
261
  ok(`Installed ${bold(agent)} ${manifest.latest} → ${installDir}`)
@@ -230,8 +264,9 @@ async function cmdInstall(args) {
230
264
  }
231
265
 
232
266
  async function cmdUpdate(args) {
233
- const agent = args[0]
234
- if (!agent) die('Usage: 0101-agents update <agent>')
267
+ const agent = args.find((a) => !a.startsWith('-'))
268
+ const assumeYes = args.some((a) => ['-f', '--force', '-y', '--yes'].includes(a))
269
+ if (!agent) die('Usage: 0101-agents update <agent> [-f]')
235
270
 
236
271
  ensureUnzipAvailable()
237
272
  const installDir = agentInstallDir(agent)
@@ -249,25 +284,29 @@ async function cmdUpdate(args) {
249
284
  return
250
285
  }
251
286
 
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
287
+ console.log(dim('Your local data is preserved across the update.'))
288
+ if (!assumeYes) {
289
+ const ans = await prompt(`Update ${agent} ${current ?? '?'} → ${manifest.latest}? [Y/n] `)
290
+ if (ans && !/^y(es)?$/i.test(ans)) {
291
+ info('Cancelled.')
292
+ return
293
+ }
257
294
  }
258
295
 
259
- const backup = backupDataDir(agent)
260
- if (backup) info(`Backed up data → ${backup}`)
296
+ const preserve = preserveList(readManifest(agent))
297
+ const backup = backupPreserved(agent, preserve)
298
+ if (backup) info(`Backed up ${preserve.join(', ')} → ${backup}`)
261
299
 
262
300
  const zipPath = join(tmpdir(), `0101-${agent}-${manifest.latest}.zip`)
263
301
  info(`Downloading…`)
264
302
  await downloadZip(manifest.download_url, zipPath)
265
303
 
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)
304
+ // Extract over the install, skipping the manifest's `preserve` paths so user
305
+ // state survives (agents: data/; runner: .env + .runner-state/).
306
+ unzipInto(zipPath, installDir, { exclude: excludePatterns(preserve) })
270
307
  rmSync(zipPath, { force: true })
308
+ runPostInstall(agent, readManifest(agent))
309
+ writeInstalledVersion(agent, manifest.latest)
271
310
 
272
311
  console.log('')
273
312
  ok(`Updated ${bold(agent)} → ${manifest.latest}`)
@@ -308,6 +347,17 @@ async function cmdStart(args) {
308
347
  // ignore
309
348
  }
310
349
 
350
+ // Artifacts can declare how `start` launches via manifest.start. The runner
351
+ // sets "npm start"; agents leave it unset and fall through to launching
352
+ // Claude Code below.
353
+ const startCmd = readManifest(agent).start
354
+ if (startCmd) {
355
+ const child = spawn(startCmd, { cwd: dir, stdio: 'inherit', shell: true })
356
+ child.on('error', (e) => die(`Failed to start: ${e.message}`))
357
+ child.on('exit', (code) => process.exit(code ?? 0))
358
+ return
359
+ }
360
+
311
361
  // Everything after the agent name passes through to `claude`. Supports both:
312
362
  // 0101-agents start marketer --resume
313
363
  // 0101-agents start marketer -- --resume (explicit separator)
@@ -349,7 +399,7 @@ Commands:
349
399
  ${bold('logout')} Forget your license key
350
400
  ${bold('whoami')} Show current license
351
401
  ${bold('install')} <agent> [--force] Install an agent
352
- ${bold('update')} <agent> Update an installed agent (preserves data/)
402
+ ${bold('update')} <agent> [-f] Update an installed agent (preserves data/). -f skips the prompt
353
403
  ${bold('start')} <agent> [--tg] [-- args] Open the agent (runs claude in its dir)
354
404
  --tg adds the right flags to run as a
355
405
  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.5",
4
4
  "description": "",
5
5
  "bin": {
6
6
  "0101-agents": "./bin/cli.js"