@babylonjs-toolkit/agent 1.1.2 → 1.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.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Babylon Toolkit Agent Skills (1.0.0)
1
+ # Babylon Toolkit Agent Skills (1.1.5)
2
2
 
3
3
  Universal [Agent Skills](https://agentskills.io) for the `Babylon Toolkit` web game development framework.
4
4
  Each `SKILL.md` follows the open standard, so the **same file works unchanged** in Claude Code, Codex CLI, and GitHub Copilot.
@@ -53,10 +53,13 @@ refresh the persona, and prune skills the new release no longer ships.
53
53
 
54
54
  If the upgrade is not possible it says so and continues with what is on disk:
55
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
56
+ - running through `npx`, from a git checkout, a `npm link`ed working copy, or as a
57
+ project dependency — there is no global package to upgrade, so it reinstalls the
58
+ bundled files
58
59
  - the registry is unreachable — it reinstalls the version you already have
59
60
  - the global install needs elevated permissions — it prints the exact command to run
61
+ - npm reports success but this CLI still runs the old version — you have more than
62
+ one Node/npm on PATH, and it tells you which path was left behind
60
63
 
61
64
  Use `--no-self-update` to skip the npm check entirely.
62
65
 
package/bin/bt-agent.js CHANGED
@@ -166,9 +166,17 @@ function printSelfUpdate(result) {
166
166
  break;
167
167
  case 'upgrade-failed':
168
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}`);
169
+ console.log(`Run this yourself (it may need elevated permissions): ${result.command}`);
170
+ console.log(` installed at: ${prettyPath(result.root)}`);
170
171
  console.log('Reinstalling the currently installed version instead.');
171
172
  break;
173
+ case 'upgrade-stale':
174
+ console.log(`\n${result.command} reported success, but this CLI still runs ${result.onDisk} from`);
175
+ console.log(` ${prettyPath(result.root)}`);
176
+ console.log('That path was not updated, so you likely have more than one Node/npm');
177
+ console.log('installation. Check which one is on PATH:');
178
+ console.log(process.platform === 'win32' ? ' where bt-agent && npm root -g' : ' which bt-agent && npm root -g');
179
+ break;
172
180
  case 'check-failed':
173
181
  console.log(`\nCould not check npm for a newer version: ${result.reason}`);
174
182
  console.log(`Reinstalling ${PACKAGE_NAME} ${result.current} from disk.`);
@@ -260,6 +268,7 @@ function main() {
260
268
  // payload already on disk — so it could never deliver a new release. Bring
261
269
  // the package itself up to date first, then hand over to the new CLI.
262
270
  let selfResult = null;
271
+ let selfFailed = false;
263
272
  if (opts.selfUpdate) {
264
273
  selfResult = selfUpdate({
265
274
  dryRun: opts.dryRun,
@@ -269,6 +278,10 @@ function main() {
269
278
  });
270
279
  if (!opts.json) printSelfUpdate(selfResult);
271
280
 
281
+ // The files were installed correctly, but the release the user asked for
282
+ // is not the one now running — that is a failure worth a non-zero exit.
283
+ selfFailed = selfResult.status === 'upgrade-stale' || selfResult.status === 'upgrade-failed';
284
+
272
285
  if (selfResult.status === 'upgraded') {
273
286
  const passthrough = process.argv.slice(2).filter((a) => a !== 'update');
274
287
  const code = reexec(['install', ...passthrough], selfResult.latest);
@@ -299,7 +312,7 @@ function main() {
299
312
  console.log('\nDry run — nothing was written.\n');
300
313
  }
301
314
  }
302
- process.exit(opts.dryRun || check.ok ? 0 : 1);
315
+ process.exit(opts.dryRun || (check.ok && !selfFailed) ? 0 : 1);
303
316
  break;
304
317
  }
305
318
 
package/lib/selfupdate.js CHANGED
@@ -22,10 +22,11 @@ function segments(p) {
22
22
  /**
23
23
  * Where is this copy of the CLI running from?
24
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.
25
+ * `update` may only shell out to a package manager when the answer is a real
26
+ * global install. An npx run already fetched the newest version, a git checkout or
27
+ * `npm link` is the developer's own working tree, and a project dependency belongs
28
+ * to that project's lockfile — upgrading any of those would write files nobody
29
+ * asked for, and in the linked case would silently replace the link.
29
30
  */
30
31
  function detectInstall() {
31
32
  const parts = segments(PACKAGE_ROOT);
@@ -38,6 +39,13 @@ function detectInstall() {
38
39
  return { kind: 'npx', manager: null, root: PACKAGE_ROOT };
39
40
  }
40
41
 
42
+ // Every installed copy — global or project — lives under a node_modules
43
+ // directory. Anything else is a working copy reached through a symlink
44
+ // (`npm link`, `npm i -g <dir>`); upgrading it would delete the link.
45
+ if (!lower.includes('node_modules')) {
46
+ return { kind: 'linked', manager: null, root: PACKAGE_ROOT };
47
+ }
48
+
41
49
  const manager = lower.includes('pnpm') || lower.includes('.pnpm')
42
50
  ? 'pnpm'
43
51
  : lower.includes('.yarn') || lower.includes('yarn')
@@ -46,31 +54,43 @@ function detectInstall() {
46
54
  ? 'bun'
47
55
  : 'npm';
48
56
 
49
- return { kind: isGlobal() ? 'global' : 'local', manager, root: PACKAGE_ROOT };
57
+ return { kind: globalKind(), manager, root: PACKAGE_ROOT };
50
58
  }
51
59
 
52
- /** True when PACKAGE_ROOT lives under this machine's global module directory. */
53
- function isGlobal() {
54
- const roots = [];
60
+ function npmGlobalRoot() {
55
61
  try {
56
- roots.push(String(execFileSync(npmBin(), ['root', '-g'], { encoding: 'utf8', timeout: 20000 })).trim());
62
+ return String(runManager('npm', ['root', '-g'], { encoding: 'utf8', timeout: 20000 })).trim() || null;
57
63
  } catch {
58
- /* npm unavailable or slow — fall back to path shape below */
64
+ return null;
59
65
  }
66
+ }
60
67
 
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;
68
+ const norm = (p) => path.resolve(p).toLowerCase().replace(/[\\/]+$/, '');
69
+
70
+ /** 'global' | 'linked' | 'local' for a package that is not npx or a git checkout. */
71
+ function globalKind() {
72
+ const root = npmGlobalRoot();
73
+
74
+ if (root) {
75
+ const entry = path.join(root, ...PACKAGE_NAME.split('/'));
76
+ try {
77
+ // `npm link` and `npm i -g <dir>` leave a symlink to the developer's tree.
78
+ // Upgrading that would delete the link and hide their working copy.
79
+ if (fs.lstatSync(entry).isSymbolicLink() && norm(fs.realpathSync(entry)) === norm(PACKAGE_ROOT)) {
80
+ return 'linked';
81
+ }
82
+ } catch {
83
+ /* this package is not installed under that prefix */
84
+ }
85
+ if (norm(PACKAGE_ROOT).startsWith(norm(root) + path.sep.toLowerCase())) return 'global';
86
+ }
64
87
 
65
88
  // pnpm/yarn/bun global stores never match `npm root -g`; recognise their shapes,
66
89
  // and treat anything outside the current project tree as global rather than local.
67
90
  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
- }
91
+ if (parts.includes('.pnpm') || parts.includes('.yarn') || parts.includes('.bun')) return 'global';
71
92
 
72
- function npmBin() {
73
- return IS_WINDOWS ? 'npm.cmd' : 'npm';
93
+ return norm(PACKAGE_ROOT).startsWith(norm(process.cwd()) + path.sep.toLowerCase()) ? 'local' : 'global';
74
94
  }
75
95
 
76
96
  function managerBin(manager) {
@@ -78,6 +98,50 @@ function managerBin(manager) {
78
98
  return IS_WINDOWS && name !== 'bun' ? `${name}.cmd` : name;
79
99
  }
80
100
 
101
+ /**
102
+ * npm's own JS entry point, shipped beside the node binary.
103
+ *
104
+ * Running it with `process.execPath` is the one invocation that behaves the same
105
+ * on every platform. It matters most on Windows: since the CVE-2024-27980 fix,
106
+ * Node refuses to `execFile` a `.cmd` shim without `shell: true` and throws
107
+ * EINVAL — which is exactly why `update` reported an upgrade it never performed.
108
+ */
109
+ function npmCliPath() {
110
+ const dir = path.dirname(process.execPath);
111
+ const candidates = [
112
+ process.env.npm_execpath,
113
+ path.join(dir, 'node_modules', 'npm', 'bin', 'npm-cli.js'),
114
+ path.join(dir, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
115
+ path.join(dir, '..', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
116
+ ];
117
+ for (const c of candidates) {
118
+ if (c && c.endsWith('.js') && fs.existsSync(c)) return path.resolve(c);
119
+ }
120
+ return null;
121
+ }
122
+
123
+ /** Quote an argument for cmd.exe, which is what `shell: true` uses on Windows. */
124
+ function shellQuote(arg) {
125
+ return /[\s"^&|<>()]/.test(arg) ? `"${arg.replace(/"/g, '""')}"` : arg;
126
+ }
127
+
128
+ /**
129
+ * Run a package manager command portably.
130
+ *
131
+ * npm goes through its JS entry point when we can find it; everything else falls
132
+ * back to the platform shim, with `shell: true` on Windows so a `.cmd` is allowed.
133
+ */
134
+ function runManager(manager, args, opts = {}) {
135
+ if (manager === 'npm') {
136
+ const cli = npmCliPath();
137
+ if (cli) return execFileSync(process.execPath, [cli, ...args], opts);
138
+ }
139
+ if (IS_WINDOWS && manager !== 'bun') {
140
+ return execFileSync(managerBin(manager), args.map(shellQuote), { ...opts, shell: true });
141
+ }
142
+ return execFileSync(managerBin(manager), args, opts);
143
+ }
144
+
81
145
  /** The exact command a user can copy/paste if the automatic upgrade fails. */
82
146
  function upgradeCommand(manager, version) {
83
147
  const spec = `${PACKAGE_NAME}@${version || 'latest'}`;
@@ -122,7 +186,7 @@ function compareVersions(a, b) {
122
186
  */
123
187
  function fetchLatestVersion({ timeout = 20000 } = {}) {
124
188
  try {
125
- const out = execFileSync(npmBin(), ['view', PACKAGE_NAME, 'version'], {
189
+ const out = runManager('npm', ['view', PACKAGE_NAME, 'version'], {
126
190
  encoding: 'utf8',
127
191
  timeout,
128
192
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -161,6 +225,15 @@ function fetchLatestFromRegistrySync(timeout) {
161
225
  }
162
226
  }
163
227
 
228
+ /** Version of the package as it now sits on disk, read fresh (not the require cache). */
229
+ function installedVersionOnDisk() {
230
+ try {
231
+ return JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8')).version || null;
232
+ } catch {
233
+ return null;
234
+ }
235
+ }
236
+
164
237
  /**
165
238
  * Bring the installed package itself up to date.
166
239
  *
@@ -183,13 +256,14 @@ function selfUpdate({ dryRun = false, timeout = 20000, log = () => {} } = {}) {
183
256
  npx: 'running via npx — npx already fetched the latest version',
184
257
  dev: 'running from a git checkout — nothing to upgrade',
185
258
  local: 'installed as a project dependency — upgrade it through that project',
259
+ linked: `linked to ${PACKAGE_ROOT} — upgrade that working copy instead`,
186
260
  }[install.kind];
187
261
  return { ...base, status: 'skipped', reason: why };
188
262
  }
189
263
 
190
264
  log(`Checking npm for a newer ${PACKAGE_NAME} (installed: ${CURRENT_VERSION})...`);
191
265
  const { version: latest, error } = fetchLatestVersion({ timeout });
192
- if (!latest) return { ...base, status: 'check-failed', reason: error };
266
+ if (!latest) return { ...base, status: 'check-failed', reason: error, root: PACKAGE_ROOT };
193
267
 
194
268
  if (compareVersions(latest, CURRENT_VERSION) <= 0) {
195
269
  return { ...base, latest, status: 'current' };
@@ -200,16 +274,25 @@ function selfUpdate({ dryRun = false, timeout = 20000, log = () => {} } = {}) {
200
274
 
201
275
  log(`Upgrading ${CURRENT_VERSION} -> ${latest} (${command})`);
202
276
  try {
203
- execFileSync(managerBin(install.manager), upgradeArgs(install.manager, latest), {
277
+ runManager(install.manager, upgradeArgs(install.manager, latest), {
204
278
  stdio: ['ignore', 'inherit', 'inherit'],
205
279
  timeout: 10 * 60 * 1000,
206
280
  });
207
281
  } catch (err) {
208
282
  const reason = err && err.message ? String(err.message).split('\n')[0] : 'upgrade command failed';
209
- return { ...base, latest, status: 'upgrade-failed', command, reason };
283
+ return { ...base, latest, status: 'upgrade-failed', command, reason, root: PACKAGE_ROOT };
284
+ }
285
+
286
+ // Trust the files, not the exit code. A machine with more than one npm prefix
287
+ // (nvm-windows, a second Node install) happily reports success while installing
288
+ // somewhere this CLI does not run from — which looks exactly like "update did
289
+ // nothing", so name it instead of claiming an upgrade that did not land here.
290
+ const onDisk = installedVersionOnDisk();
291
+ if (onDisk && compareVersions(onDisk, CURRENT_VERSION) <= 0) {
292
+ return { ...base, latest, status: 'upgrade-stale', command, onDisk, root: PACKAGE_ROOT };
210
293
  }
211
294
 
212
- return { ...base, latest, status: 'upgraded', command };
295
+ return { ...base, latest, status: 'upgraded', command, onDisk };
213
296
  }
214
297
 
215
298
  /**
@@ -231,6 +314,9 @@ function reexec(argv, version) {
231
314
 
232
315
  module.exports = {
233
316
  PACKAGE_NAME,
317
+ runManager,
318
+ npmCliPath,
319
+ installedVersionOnDisk,
234
320
  CURRENT_VERSION,
235
321
  GUARD_ENV,
236
322
  detectInstall,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@babylonjs-toolkit/agent",
3
- "version": "1.1.2",
3
+ "version": "1.1.5",
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",