@babylonjs-toolkit/agent 1.1.1 → 1.1.2

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 CHANGED
@@ -24,7 +24,7 @@ and instruction files are only read at session start.
24
24
  | Command | What it does |
25
25
  |---------|--------------|
26
26
  | `bt-agent install` | Install skills + persona into every target (the default) |
27
- | `bt-agent update` | Reinstall, and remove skills this package no longer ships |
27
+ | `bt-agent update` | Fetch the newest release from npm, reinstall it, and prune dropped skills |
28
28
  | `bt-agent uninstall` | Remove what it installed — and nothing else |
29
29
  | `bt-agent doctor` | Verify the install; prints `INSTALL OK` or lists what is missing |
30
30
  | `bt-agent targets` | Show every target and the paths it writes to |
@@ -36,9 +36,30 @@ and instruction files are only read at session start.
36
36
  | `--legacy-codex` | Also write `~/.codex/skills` for pre-`.agents` Codex builds |
37
37
  | `--no-persona` | Install skills only; leave instruction files alone |
38
38
  | `--persona-only` | Install/refresh the Agent Persona only; do not copy skills |
39
+ | `--no-self-update` | `update` only: reinstall the bundled files; do not fetch npm |
39
40
  | `--dry-run` | Print what would happen and change nothing |
40
41
  | `--json` | Machine-readable output |
41
42
 
43
+ ### Updating
44
+
45
+ ```bash
46
+ bt-agent update
47
+ ```
48
+
49
+ `update` asks npm for the newest published version. If there is one it upgrades the
50
+ global package for you — with whichever manager installed it (`npm`, `pnpm`, `yarn`
51
+ or `bun`) — then hands over to the freshly installed CLI to copy the new skills,
52
+ refresh the persona, and prune skills the new release no longer ships.
53
+
54
+ If the upgrade is not possible it says so and continues with what is on disk:
55
+
56
+ - running through `npx`, from a git checkout, or as a project dependency — there is
57
+ no global package to upgrade, so it reinstalls the bundled files
58
+ - the registry is unreachable — it reinstalls the version you already have
59
+ - the global install needs elevated permissions — it prints the exact command to run
60
+
61
+ Use `--no-self-update` to skip the npm check entirely.
62
+
42
63
  ### What it writes
43
64
 
44
65
  | Target | Skills | Agent Persona |
package/bin/bt-agent.js CHANGED
@@ -4,7 +4,8 @@
4
4
  const { install, uninstall } = require('../lib/install');
5
5
  const { doctor } = require('../lib/doctor');
6
6
  const { TARGETS, targetIds } = require('../lib/targets');
7
- const { prettyPath } = require('../lib/paths');
7
+ const { selfUpdate, reexec, upgradeCommand, PACKAGE_NAME } = require('../lib/selfupdate');
8
+ const { prettyPath, resolveTargetPath } = require('../lib/paths');
8
9
  const { version } = require('../package.json');
9
10
 
10
11
  const USAGE = `
@@ -18,7 +19,7 @@ Usage
18
19
 
19
20
  Commands
20
21
  install Install skills + persona into every target (default)
21
- update Same as install, and remove skills no longer shipped
22
+ update Upgrade this package from npm, then reinstall and prune
22
23
  uninstall Remove what this tool installed, and nothing else
23
24
  doctor Verify the install; prints INSTALL OK or INSTALL FAILED
24
25
  targets List the available targets and their paths
@@ -30,6 +31,7 @@ Options
30
31
  --no-persona Install skills only; do not touch instruction files
31
32
  --persona-only Install/refresh the Agent Persona only; do not copy skills
32
33
  --no-migrate Leave a legacy unmarked persona in place, do not convert it
34
+ --no-self-update update: reinstall the bundled files only; do not fetch npm
33
35
  --dry-run Print what would happen; change nothing
34
36
  --json Machine-readable output
35
37
  -h, --help Show this help
@@ -40,6 +42,7 @@ Examples
40
42
  bt-agent install --legacy-codex
41
43
  bt-agent install --project
42
44
  bt-agent doctor
45
+ bt-agent update
43
46
  `;
44
47
 
45
48
  function parseArgs(argv) {
@@ -51,6 +54,7 @@ function parseArgs(argv) {
51
54
  persona: true,
52
55
  skills: true,
53
56
  migrate: true,
57
+ selfUpdate: true,
54
58
  dryRun: false,
55
59
  json: false,
56
60
  help: false,
@@ -83,6 +87,9 @@ function parseArgs(argv) {
83
87
  case '--no-migrate':
84
88
  opts.migrate = false;
85
89
  break;
90
+ case '--no-self-update':
91
+ opts.selfUpdate = false;
92
+ break;
86
93
  case '--dry-run':
87
94
  opts.dryRun = true;
88
95
  break;
@@ -146,6 +153,44 @@ function printInstall(report) {
146
153
  }
147
154
  }
148
155
 
156
+ function printSelfUpdate(result) {
157
+ switch (result.status) {
158
+ case 'upgraded':
159
+ console.log(`\nUpgraded ${PACKAGE_NAME} ${result.current} -> ${result.latest}.`);
160
+ break;
161
+ case 'current':
162
+ console.log(`\n${PACKAGE_NAME} ${result.current} is the latest published version.`);
163
+ break;
164
+ case 'would-upgrade':
165
+ console.log(`\nWould upgrade ${PACKAGE_NAME} ${result.current} -> ${result.latest} (${result.command}).`);
166
+ break;
167
+ case 'upgrade-failed':
168
+ console.log(`\nCould not upgrade ${PACKAGE_NAME} ${result.current} -> ${result.latest}: ${result.reason}`);
169
+ console.log(`Run this yourself (it may need sudo): ${result.command}`);
170
+ console.log('Reinstalling the currently installed version instead.');
171
+ break;
172
+ case 'check-failed':
173
+ console.log(`\nCould not check npm for a newer version: ${result.reason}`);
174
+ console.log(`Reinstalling ${PACKAGE_NAME} ${result.current} from disk.`);
175
+ break;
176
+ case 'skipped':
177
+ console.log(`\nSkipping the npm upgrade — ${result.reason}.`);
178
+ break;
179
+ default:
180
+ break;
181
+ }
182
+ }
183
+
184
+ /** Name only what this run actually installed. */
185
+ function restartNote(report) {
186
+ const parts = [];
187
+ if (report.skillsInstalled !== false) parts.push('skills');
188
+ if (report.persona.length) parts.push('persona');
189
+ const what = parts.length === 2 ? 'skills and persona are' : `${parts[0] || 'files'} ${parts[0] === 'skills' ? 'are' : 'is'}`;
190
+ console.log('\nRestart your agent session (Claude Code, Codex, Copilot, Gemini CLI)');
191
+ console.log(`so the ${what} picked up.\n`);
192
+ }
193
+
149
194
  function printDoctor(result) {
150
195
  const { checked } = result;
151
196
  console.log(
@@ -160,6 +205,19 @@ function printDoctor(result) {
160
205
  }
161
206
  }
162
207
 
208
+ function targetsJson(mode, projectRoot) {
209
+ return TARGETS.map((t) => {
210
+ const spec = mode === 'project' ? t.project : t.global;
211
+ return {
212
+ id: t.id,
213
+ label: t.label,
214
+ default: Boolean(t.default),
215
+ skills: spec.skills ? resolveTargetPath(spec.skills, mode, projectRoot) : null,
216
+ instructions: spec.instructions ? resolveTargetPath(spec.instructions, mode, projectRoot) : null,
217
+ };
218
+ });
219
+ }
220
+
163
221
  function printTargets(mode) {
164
222
  console.log('');
165
223
  for (const t of TARGETS) {
@@ -197,8 +255,55 @@ function main() {
197
255
 
198
256
  try {
199
257
  switch (opts.command) {
200
- case 'install':
201
258
  case 'update': {
259
+ // `update` used to be a plain alias for `install`, which only re-copied the
260
+ // payload already on disk — so it could never deliver a new release. Bring
261
+ // the package itself up to date first, then hand over to the new CLI.
262
+ let selfResult = null;
263
+ if (opts.selfUpdate) {
264
+ selfResult = selfUpdate({
265
+ dryRun: opts.dryRun,
266
+ log: (msg) => {
267
+ if (!opts.json) console.log(msg);
268
+ },
269
+ });
270
+ if (!opts.json) printSelfUpdate(selfResult);
271
+
272
+ if (selfResult.status === 'upgraded') {
273
+ const passthrough = process.argv.slice(2).filter((a) => a !== 'update');
274
+ const code = reexec(['install', ...passthrough], selfResult.latest);
275
+ if (code !== null) process.exit(code);
276
+ // Could not re-exec: fall through and install with what is loaded.
277
+ if (!opts.json) {
278
+ console.log('\nCould not restart the upgraded CLI — run `bt-agent install` to finish.');
279
+ }
280
+ }
281
+ }
282
+
283
+ const report = install(common);
284
+ const check = doctor(common);
285
+
286
+ if (opts.json) {
287
+ console.log(JSON.stringify({ ...report, selfUpdate: selfResult, doctor: check }, null, 2));
288
+ } else {
289
+ printInstall(report);
290
+ if (!opts.dryRun) {
291
+ console.log('');
292
+ if (check.ok) {
293
+ console.log('INSTALL OK');
294
+ restartNote(report);
295
+ } else {
296
+ printDoctor(check);
297
+ }
298
+ } else {
299
+ console.log('\nDry run — nothing was written.\n');
300
+ }
301
+ }
302
+ process.exit(opts.dryRun || check.ok ? 0 : 1);
303
+ break;
304
+ }
305
+
306
+ case 'install': {
202
307
  const report = install(common);
203
308
  const check = doctor(common);
204
309
 
@@ -210,8 +315,7 @@ function main() {
210
315
  console.log('');
211
316
  if (check.ok) {
212
317
  console.log('INSTALL OK');
213
- console.log('\nRestart your agent session (Claude Code, Codex, Copilot, Gemini CLI)');
214
- console.log('so the skills and persona are picked up.\n');
318
+ restartNote(report);
215
319
  } else {
216
320
  printDoctor(check);
217
321
  }
@@ -246,7 +350,8 @@ function main() {
246
350
  }
247
351
 
248
352
  case 'targets':
249
- printTargets(opts.mode);
353
+ if (opts.json) console.log(JSON.stringify(targetsJson(opts.mode, process.cwd()), null, 2));
354
+ else printTargets(opts.mode);
250
355
  break;
251
356
 
252
357
  case 'help':
package/lib/manifest.js CHANGED
@@ -3,7 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
 
6
- const { manifestPath } = require('./paths');
6
+ const { manifestPath, stateDir } = require('./paths');
7
7
 
8
8
  /**
9
9
  * Record of what this tool wrote, so update and uninstall can touch only its own
@@ -30,10 +30,16 @@ function remove(mode, projectRoot) {
30
30
  const file = manifestPath(mode, projectRoot);
31
31
  try {
32
32
  fs.unlinkSync(file);
33
- return true;
34
33
  } catch {
35
34
  return false;
36
35
  }
36
+ // Leave nothing of our own behind: drop the state directory once it is empty.
37
+ try {
38
+ fs.rmdirSync(stateDir(mode, projectRoot));
39
+ } catch {
40
+ /* the user put something else in there — keep it */
41
+ }
42
+ return true;
37
43
  }
38
44
 
39
45
  /** Skill folder names previously installed into `dir`, per the manifest. */
@@ -0,0 +1,242 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { execFileSync } = require('child_process');
6
+
7
+ const { PACKAGE_ROOT } = require('./paths');
8
+ const pkg = require('../package.json');
9
+
10
+ const PACKAGE_NAME = pkg.name;
11
+ const CURRENT_VERSION = pkg.version;
12
+
13
+ /** Set on the re-executed child so the upgraded CLI does not try to upgrade again. */
14
+ const GUARD_ENV = 'BT_AGENT_SELF_UPDATED';
15
+
16
+ const IS_WINDOWS = process.platform === 'win32';
17
+
18
+ function segments(p) {
19
+ return p.split(/[\\/]+/);
20
+ }
21
+
22
+ /**
23
+ * Where is this copy of the CLI running from?
24
+ *
25
+ * `update` may only shell out to a package manager when the answer is a global
26
+ * install. An npx run already fetched the newest version, a git checkout is the
27
+ * developer's own working tree, and a project dependency belongs to that project's
28
+ * lockfile — upgrading any of those globally would write files nobody asked for.
29
+ */
30
+ function detectInstall() {
31
+ const parts = segments(PACKAGE_ROOT);
32
+ const lower = parts.map((s) => s.toLowerCase());
33
+
34
+ if (fs.existsSync(path.join(PACKAGE_ROOT, '.git'))) {
35
+ return { kind: 'dev', manager: null, root: PACKAGE_ROOT };
36
+ }
37
+ if (lower.some((s) => s === '_npx' || s === '.npx')) {
38
+ return { kind: 'npx', manager: null, root: PACKAGE_ROOT };
39
+ }
40
+
41
+ const manager = lower.includes('pnpm') || lower.includes('.pnpm')
42
+ ? 'pnpm'
43
+ : lower.includes('.yarn') || lower.includes('yarn')
44
+ ? 'yarn'
45
+ : lower.includes('.bun')
46
+ ? 'bun'
47
+ : 'npm';
48
+
49
+ return { kind: isGlobal() ? 'global' : 'local', manager, root: PACKAGE_ROOT };
50
+ }
51
+
52
+ /** True when PACKAGE_ROOT lives under this machine's global module directory. */
53
+ function isGlobal() {
54
+ const roots = [];
55
+ try {
56
+ roots.push(String(execFileSync(npmBin(), ['root', '-g'], { encoding: 'utf8', timeout: 20000 })).trim());
57
+ } catch {
58
+ /* npm unavailable or slow — fall back to path shape below */
59
+ }
60
+
61
+ const norm = (p) => path.resolve(p).toLowerCase().replace(/[\\/]+$/, '');
62
+ const here = norm(PACKAGE_ROOT);
63
+ if (roots.some((r) => r && here.startsWith(norm(r) + path.sep.toLowerCase()))) return true;
64
+
65
+ // pnpm/yarn/bun global stores never match `npm root -g`; recognise their shapes,
66
+ // and treat anything outside the current project tree as global rather than local.
67
+ const parts = segments(PACKAGE_ROOT).map((s) => s.toLowerCase());
68
+ if (parts.includes('.pnpm') || parts.includes('.yarn') || parts.includes('.bun')) return true;
69
+ return !here.startsWith(norm(process.cwd()) + path.sep.toLowerCase());
70
+ }
71
+
72
+ function npmBin() {
73
+ return IS_WINDOWS ? 'npm.cmd' : 'npm';
74
+ }
75
+
76
+ function managerBin(manager) {
77
+ const name = manager === 'npm' ? 'npm' : manager;
78
+ return IS_WINDOWS && name !== 'bun' ? `${name}.cmd` : name;
79
+ }
80
+
81
+ /** The exact command a user can copy/paste if the automatic upgrade fails. */
82
+ function upgradeCommand(manager, version) {
83
+ const spec = `${PACKAGE_NAME}@${version || 'latest'}`;
84
+ switch (manager) {
85
+ case 'pnpm': return `pnpm add -g ${spec}`;
86
+ case 'yarn': return `yarn global add ${spec}`;
87
+ case 'bun': return `bun add -g ${spec}`;
88
+ default: return `npm install -g ${spec}`;
89
+ }
90
+ }
91
+
92
+ function upgradeArgs(manager, version) {
93
+ const spec = `${PACKAGE_NAME}@${version || 'latest'}`;
94
+ switch (manager) {
95
+ case 'pnpm': return ['add', '-g', spec];
96
+ case 'yarn': return ['global', 'add', spec];
97
+ case 'bun': return ['add', '-g', spec];
98
+ default: return ['install', '-g', spec];
99
+ }
100
+ }
101
+
102
+ /** Compare two semver-ish versions. Pre-release tags sort before their release. */
103
+ function compareVersions(a, b) {
104
+ const split = (v) => {
105
+ const [core, pre] = String(v).split('-');
106
+ return { nums: core.split('.').map((n) => parseInt(n, 10) || 0), pre: pre || null };
107
+ };
108
+ const x = split(a);
109
+ const y = split(b);
110
+ for (let i = 0; i < 3; i += 1) {
111
+ if ((x.nums[i] || 0) !== (y.nums[i] || 0)) return (x.nums[i] || 0) < (y.nums[i] || 0) ? -1 : 1;
112
+ }
113
+ if (x.pre === y.pre) return 0;
114
+ if (!x.pre) return 1;
115
+ if (!y.pre) return -1;
116
+ return x.pre < y.pre ? -1 : 1;
117
+ }
118
+
119
+ /**
120
+ * Latest published version, from npm first so the user's registry, proxy and auth
121
+ * settings are honoured, then from registry.npmjs.org if the npm CLI is missing.
122
+ */
123
+ function fetchLatestVersion({ timeout = 20000 } = {}) {
124
+ try {
125
+ const out = execFileSync(npmBin(), ['view', PACKAGE_NAME, 'version'], {
126
+ encoding: 'utf8',
127
+ timeout,
128
+ stdio: ['ignore', 'pipe', 'pipe'],
129
+ });
130
+ const v = String(out).trim().split(/\s+/).pop();
131
+ if (v) return { version: v };
132
+ } catch (err) {
133
+ const reason = (err && (err.stderr || err.message) ? String(err.stderr || err.message) : '').trim();
134
+ const fallback = fetchLatestFromRegistrySync(timeout);
135
+ if (fallback) return { version: fallback };
136
+ return { error: reason.split('\n').filter(Boolean).pop() || 'could not reach the npm registry' };
137
+ }
138
+ return { error: 'npm returned no version' };
139
+ }
140
+
141
+ /** Registry lookup without the npm CLI. Runs in a helper process so it can be sync. */
142
+ function fetchLatestFromRegistrySync(timeout) {
143
+ const script = `
144
+ const https = require('https');
145
+ const url = 'https://registry.npmjs.org/' + encodeURIComponent(${JSON.stringify(PACKAGE_NAME)}).replace('%40','@') + '/latest';
146
+ const req = https.get(url, { headers: { accept: 'application/json' } }, (res) => {
147
+ let body = '';
148
+ res.on('data', (c) => { body += c; });
149
+ res.on('end', () => {
150
+ try { process.stdout.write(String(JSON.parse(body).version || '')); } catch { process.stdout.write(''); }
151
+ });
152
+ });
153
+ req.setTimeout(${Math.max(1000, timeout)}, () => req.destroy());
154
+ req.on('error', () => process.stdout.write(''));
155
+ `;
156
+ try {
157
+ const out = execFileSync(process.execPath, ['-e', script], { encoding: 'utf8', timeout: timeout + 2000 });
158
+ return String(out).trim() || null;
159
+ } catch {
160
+ return null;
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Bring the installed package itself up to date.
166
+ *
167
+ * `update` used to be a pure alias for `install`: it re-copied the payload already
168
+ * on disk, so it could never deliver a newer release and `npm install -g` was the
169
+ * only way to actually update. This fetches the published version first, upgrades
170
+ * the global package when there is a newer one, and reports what happened.
171
+ *
172
+ * Returns a status object; it never throws.
173
+ */
174
+ function selfUpdate({ dryRun = false, timeout = 20000, log = () => {} } = {}) {
175
+ const install = detectInstall();
176
+ const base = { current: CURRENT_VERSION, install: install.kind, manager: install.manager };
177
+
178
+ if (process.env[GUARD_ENV]) {
179
+ return { ...base, status: 'skipped', reason: 'already upgraded in this run' };
180
+ }
181
+ if (install.kind !== 'global') {
182
+ const why = {
183
+ npx: 'running via npx — npx already fetched the latest version',
184
+ dev: 'running from a git checkout — nothing to upgrade',
185
+ local: 'installed as a project dependency — upgrade it through that project',
186
+ }[install.kind];
187
+ return { ...base, status: 'skipped', reason: why };
188
+ }
189
+
190
+ log(`Checking npm for a newer ${PACKAGE_NAME} (installed: ${CURRENT_VERSION})...`);
191
+ const { version: latest, error } = fetchLatestVersion({ timeout });
192
+ if (!latest) return { ...base, status: 'check-failed', reason: error };
193
+
194
+ if (compareVersions(latest, CURRENT_VERSION) <= 0) {
195
+ return { ...base, latest, status: 'current' };
196
+ }
197
+
198
+ const command = upgradeCommand(install.manager, latest);
199
+ if (dryRun) return { ...base, latest, status: 'would-upgrade', command };
200
+
201
+ log(`Upgrading ${CURRENT_VERSION} -> ${latest} (${command})`);
202
+ try {
203
+ execFileSync(managerBin(install.manager), upgradeArgs(install.manager, latest), {
204
+ stdio: ['ignore', 'inherit', 'inherit'],
205
+ timeout: 10 * 60 * 1000,
206
+ });
207
+ } catch (err) {
208
+ const reason = err && err.message ? String(err.message).split('\n')[0] : 'upgrade command failed';
209
+ return { ...base, latest, status: 'upgrade-failed', command, reason };
210
+ }
211
+
212
+ return { ...base, latest, status: 'upgraded', command };
213
+ }
214
+
215
+ /**
216
+ * Hand control to the freshly installed CLI so the skills that get copied are the
217
+ * new ones. The running process still has the previous release's modules in memory.
218
+ */
219
+ function reexec(argv, version) {
220
+ const { spawnSync } = require('child_process');
221
+ const script = process.argv[1];
222
+ if (!script || !fs.existsSync(script)) return null;
223
+
224
+ const result = spawnSync(process.execPath, [script, ...argv], {
225
+ stdio: 'inherit',
226
+ env: { ...process.env, [GUARD_ENV]: version || '1' },
227
+ });
228
+ if (result.error) return null;
229
+ return typeof result.status === 'number' ? result.status : 1;
230
+ }
231
+
232
+ module.exports = {
233
+ PACKAGE_NAME,
234
+ CURRENT_VERSION,
235
+ GUARD_ENV,
236
+ detectInstall,
237
+ compareVersions,
238
+ fetchLatestVersion,
239
+ upgradeCommand,
240
+ selfUpdate,
241
+ reexec,
242
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@babylonjs-toolkit/agent",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "Installs the Babylon Toolkit agent skills and Agent Persona for Claude Code, Codex, GitHub Copilot, Gemini CLI and Antigravity.",
5
5
  "keywords": [
6
6
  "babylonjs",