@babylonjs-toolkit/agent 1.1.0 → 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 +22 -1
- package/bin/bt-agent.js +111 -6
- package/lib/manifest.js +8 -2
- package/lib/selfupdate.js +242 -0
- package/package.json +1 -1
- package/skills/bt-spec/SKILL.md +45 -5
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` |
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
package/skills/bt-spec/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: bt-spec
|
|
3
3
|
description: "The Babylon Toolkit Spec Skill creates a feature spec file and branch from a short idea. Use when asked to spec out, plan, or scaffold a new feature."
|
|
4
|
-
allowed-tools: Read, Grep, Glob, Write, WebFetch(domain:raw.githubusercontent.com), Bash(git switch:*), Agent, Task
|
|
4
|
+
allowed-tools: Read, Grep, Glob, Write, WebFetch(domain:raw.githubusercontent.com), Bash(git switch:*), AskUserQuestion, Agent, Task
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
You are helping to spin up a new feature spec for this application, from a short idea provided in the user input below. Always adhere to any rules or requirements set out in the project's agent instructions (AGENTS.md / CLAUDE.md / .github/copilot-instructions.md) when responding.
|
|
@@ -13,14 +13,16 @@ Use the user’s message after the skill name as the `arguments`.
|
|
|
13
13
|
# Invocation
|
|
14
14
|
|
|
15
15
|
```
|
|
16
|
-
/bt-spec <feature-brief>
|
|
16
|
+
/bt-spec [--grill-me] <feature-brief>
|
|
17
17
|
```
|
|
18
18
|
- **`<feature-brief>`** — the short idea or brief for the feature spec. This is the *variable*.
|
|
19
|
-
-
|
|
19
|
+
- **`--grill-me`** *(optional flag)* — interrogate the user relentlessly, one question at a time, until the design tree is actually resolved, instead of drafting the spec in a single pass from the brief. See **Step 2.7**. The flag may appear anywhere in the arguments and is never part of the brief.
|
|
20
|
+
- If the brief is missing, ask for it before starting. Never guess a file path or URL.
|
|
20
21
|
|
|
21
22
|
Example:
|
|
22
23
|
```
|
|
23
24
|
/bt-spec → "Generate a detailed implementation plan for the new feature"
|
|
25
|
+
/bt-spec --grill-me "add a settings toggle to mute all game audio" # interview first, then spec
|
|
24
26
|
```
|
|
25
27
|
|
|
26
28
|
---
|
|
@@ -93,7 +95,9 @@ convenience where the tooling happens to support it.
|
|
|
93
95
|
|
|
94
96
|
## Step 2. Parse the arguments
|
|
95
97
|
|
|
96
|
-
|
|
98
|
+
**First, extract and remove any flags — before deriving anything from the text.** Scan `arguments` for `--grill-me`; set `grill_me = true` if present, then **delete it from `arguments`** and collapse the surrounding whitespace. Everything below derives from the flag-stripped remainder. Skipping this produces a spec named `_specs/mute-game-audio-grill-me_spec.md`. Treat an unrecognised `--flag` as part of the brief only if the user clearly meant it as prose; otherwise ask.
|
|
99
|
+
|
|
100
|
+
From the flag-stripped `arguments`, extract:
|
|
97
101
|
|
|
98
102
|
1. `feature_title`
|
|
99
103
|
- A short, human readable title in Title Case.
|
|
@@ -167,6 +171,30 @@ Some features are built on a deterministic pattern **owned by a sibling skill**
|
|
|
167
171
|
|
|
168
172
|
If the feature matches no sibling-skill pattern, note that and continue.
|
|
169
173
|
|
|
174
|
+
## Step 2.7 `--grill-me` — interrogate until the design is decided (ONLY when the flag is present)
|
|
175
|
+
|
|
176
|
+
Run this step **only if `grill_me` is true**. Skip it entirely otherwise — the default one-shot path is unchanged.
|
|
177
|
+
|
|
178
|
+
Without the flag this skill drafts a spec from a short brief in a single pass, and every decision it could not make lands in `## Open Questions` — a section nothing downstream is obligated to read or resolve. Grill mode replaces that guessing with a real interview, and its output lands in the sections that *are* read: `## Functional Requirements`, `## Acceptance Criteria`, `## Possible Edge Cases`, and the `## Decisions` log.
|
|
179
|
+
|
|
180
|
+
Run this **after Step 2.4**, so every question is grounded in the real codebase rather than generic, and **after Step 2.6**, so a sibling skill's own intake questions are folded into this one interview instead of being asked twice.
|
|
181
|
+
|
|
182
|
+
**The loop:**
|
|
183
|
+
|
|
184
|
+
> Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one by one. For each question provide your recommended answer. Ask the questions one at a time. If a question can be answered by exploring the codebase, explore the codebase instead.
|
|
185
|
+
|
|
186
|
+
The rules that make that loop work:
|
|
187
|
+
|
|
188
|
+
1. **One question at a time.** Use `AskUserQuestion` where you have it, with your recommended answer as the FIRST option, labelled `(Recommended)`. Never batch a wall of questions, and never move on past a question the user has not answered.
|
|
189
|
+
2. **Explore before you ask.** If the answer is discoverable in the repo — an existing convention, how the closest prior feature did it, a dependency already present — go find it and offer what you found as the recommendation, rather than spending a question on it. Reuse the Step 2.4 findings first; a read-only subagent is the right tool only when the search is genuinely broad.
|
|
190
|
+
3. **Depth-first down the design tree.** Resolve a branch's dependencies before opening the next branch — a decision that invalidates three later questions must be asked *before* them. Name the branch you are on, so the user can see the shape of what remains.
|
|
191
|
+
4. **Never ask what is already settled.** The brief, `SPEC.md`, `DESIGN.md`, and a named sibling skill's documented defaults are answers, not questions. Confirm one of them in a single line only where this feature would plausibly override it.
|
|
192
|
+
5. **Record as you go.** Write every answer into the spec body — as a requirement, an acceptance criterion, or an edge case — *plus* a `## Decisions` entry carrying the *why* and the rejected alternative. An answer that exists only in this transcript is gone the moment the plan phase opens a fresh context.
|
|
193
|
+
6. **Stop when the tree is resolved, not when you run out of questions.** You are done when every remaining unknown is genuinely an implementation choice for bt-plan, or something only the user can decide later. Say so explicitly, then continue to Step 3.
|
|
194
|
+
7. **The user can end it at any time** — "that's enough", "you decide the rest". Honor that immediately: record each unexplored branch in `## Open Questions` *with your recommended answer*, note in the spec that grilling ended early, and continue.
|
|
195
|
+
|
|
196
|
+
**What must be true when this step ends:** `## Decisions` is non-empty and every entry carries a rationale, and `## Open Questions` holds only genuinely-undecided items — never something the user already answered.
|
|
197
|
+
|
|
170
198
|
## Step 3. Record the branch name (and switch to it where git is available)
|
|
171
199
|
|
|
172
200
|
The `branch_name` derived from the `arguments` is **always** recorded in the spec header, so the plan
|
|
@@ -179,7 +207,7 @@ and execute phases have a stable name to refer to regardless of host.
|
|
|
179
207
|
|
|
180
208
|
## Step 4. Draft the spec content
|
|
181
209
|
|
|
182
|
-
Create a markdown spec document that Plan mode can use directly and save it in the _specs folder as `<feature_slug>_spec.md`. Use the exact structure as defined in the feature spec template file @FEATURE.md located at the project root. The template includes a required `spec_impact` header field and a `Project Spec Alignment` section — fill both in from your SPEC.md read above (cite the SPEC.md sections the feature relies on, describe how it fits the architecture, and for `spec_impact: yes` state exactly what will change in SPEC.md and in which section). Do not add technical implementation details such as code examples. If the feature spec template file is missing, create a new feature spec file with the following sections:
|
|
210
|
+
Create a markdown spec document that Plan mode can use directly and save it in the _specs folder as `<feature_slug>_spec.md`. Use the exact structure as defined in the feature spec template file @FEATURE.md located at the project root. **If that project template predates grill mode and has no `## Decisions` section, append one anyway whenever `grill_me` is true** (using the shape in the fallback template below, placed directly before `## Open Questions`) — the rationale must be recorded regardless of which template the project ships. The template includes a required `spec_impact` header field and a `Project Spec Alignment` section — fill both in from your SPEC.md read above (cite the SPEC.md sections the feature relies on, describe how it fits the architecture, and for `spec_impact: yes` state exactly what will change in SPEC.md and in which section). Do not add technical implementation details such as code examples. If the feature spec template file is missing, create a new feature spec file with the following sections:
|
|
183
211
|
```
|
|
184
212
|
# Feature Spec Template
|
|
185
213
|
|
|
@@ -228,7 +256,19 @@ spec_impact: <yes|no> # yes if this feature adds/changes a system, convention,
|
|
|
228
256
|
## Acceptance Criteria
|
|
229
257
|
- ...
|
|
230
258
|
|
|
259
|
+
## Decisions _(the "why" — REQUIRED when run with `--grill-me`)_
|
|
260
|
+
<!-- Mirrors SPEC.md's Decisions log, at feature scope. One entry per settled
|
|
261
|
+
design decision: what was chosen, why, and what was rejected. bt-plan reads
|
|
262
|
+
the feature spec in full, so this is how a decision's rationale survives into
|
|
263
|
+
the plan phase instead of being relitigated there — and for
|
|
264
|
+
spec_impact: yes features it feeds the final `Update SPEC.md` task directly. -->
|
|
265
|
+
- **<decision>.** <why — the deciding constraint or the precedent in the codebase.>
|
|
266
|
+
Rejected: <alternative> (<why not>). → <FR-n, AC-n>
|
|
267
|
+
|
|
231
268
|
## Open Questions
|
|
269
|
+
<!-- ONLY genuinely-undecided items. Anything answered during grilling belongs in
|
|
270
|
+
Functional Requirements / Acceptance Criteria / Possible Edge Cases plus a
|
|
271
|
+
Decisions entry above — never parked here. -->
|
|
232
272
|
- ...
|
|
233
273
|
|
|
234
274
|
## Testing Guidelines
|