@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 +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/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