@phnx-labs/agents-cli 1.20.10 → 1.20.12
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/CHANGELOG.md +9 -0
- package/dist/commands/inspect.d.ts +35 -0
- package/dist/commands/inspect.js +160 -14
- package/dist/index.js +18 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
**`agents upgrade` now refreshes the macOS Keychain helper**
|
|
6
|
+
|
|
7
|
+
- Upgrading runs `npm install -g … --ignore-scripts`, so the postinstall that installs the signed Keychain helper never fired — a user upgrading away from a broken build (e.g. the entitlement-less 1.20.4 helper that failed `SecItemAdd` with `errSecMissingEntitlement -34018`) kept the broken helper until the lazy staleness check in `getKeychainHelperPath()` happened to repair it on their next secret operation. `installResolvedPackage` now force-refreshes the helper (`ensureKeychainHelperInstalled({ forceReinstall: true })`) on darwin after the install, so both the explicit `agents upgrade` and the auto-update prompt land the fixed helper immediately. Best-effort and non-fatal: an upgrade never fails because the helper could not be reinstalled, and `agents helper install --force` remains the manual path.
|
|
8
|
+
|
|
9
|
+
**`agents inspect <repo>` summary now shows what's actually inside, not just counts**
|
|
10
|
+
|
|
11
|
+
- The bare repo summary gained four enrichments so it reads as an inventory instead of a tally: (1) **resource name previews** — each kind lists its first few names with a `…(+N)` tail; (2) **manifest summary** — `agents.yaml` is parsed for its `run.<agent>.strategy` and any `agents.<agent>` version pins, shown under `manifests` instead of just the filename; (3) **git detail** — last commit (sha, subject, relative time), ahead/behind upstream when non-zero, and the names of dirty files; (4) **size + file counts** — total repo size and a per-kind byte size. `--json` carries all of it (`git.lastCommit`, `git.ahead/behind`, `manifest`, `size`, and per-kind `{count, bytes, files, names}`); `--brief` still skips resources and size.
|
|
12
|
+
- Fixed a path-parse bug surfaced by the dirty-files list: the shared git helper trimmed leading whitespace, which clipped the first character off the first `git status --porcelain` path; status is now read untrimmed.
|
|
13
|
+
|
|
5
14
|
**`agents inspect .` reads the project `.agents/`, and plugin drill-down shows bundled skills**
|
|
6
15
|
|
|
7
16
|
- `agents inspect .` (and any path to a repo root) now resolves to the project's nested `.agents/` tree when that tree is a populated DotAgents root, instead of the project root itself. Previously a top-level `agents.yaml` version-pin or an unrelated source `skills/` dir at the repo root was mistaken for a DotAgents root, so `inspect .` reported the wrong directory's resources (e.g. `plugins 0` while the real `.agents/plugins/` held a plugin). A bare `.agents`-named dir still resolves to itself, and standalone clones / extra repos that keep resources at the top level (using `.agents/` only for worktrees) are unaffected — their nested `.agents/` is not a DotAgents root, so the top level still wins.
|
|
@@ -56,4 +56,39 @@ export interface RepoTarget {
|
|
|
56
56
|
export declare function resolveRepoTarget(target: string, cwd?: string): RepoTarget | null;
|
|
57
57
|
/** List one resource kind from a single repo root — no layering, no overrides. */
|
|
58
58
|
export declare function collectRepoKind(repo: RepoTarget, kind: DrillableKind): ResourceItem[];
|
|
59
|
+
/** Recursive size + file count of a path; symlinks are not followed. */
|
|
60
|
+
export declare function pathSize(p: string): {
|
|
61
|
+
bytes: number;
|
|
62
|
+
files: number;
|
|
63
|
+
};
|
|
64
|
+
/** Human byte size: "84 KB", "3.1 MB". */
|
|
65
|
+
export declare function formatBytes(n: number): string;
|
|
66
|
+
export interface ManifestSummary {
|
|
67
|
+
/** `run.<agent>.strategy` pairs from agents.yaml. */
|
|
68
|
+
strategies: Array<{
|
|
69
|
+
agent: string;
|
|
70
|
+
strategy: string;
|
|
71
|
+
}>;
|
|
72
|
+
/** `agents.<agent>` version pins from agents.yaml, when present. */
|
|
73
|
+
versions: Array<{
|
|
74
|
+
agent: string;
|
|
75
|
+
version: string;
|
|
76
|
+
}>;
|
|
77
|
+
}
|
|
78
|
+
/** Parse the repo's own agents.yaml into the version pins + run strategies it declares. */
|
|
79
|
+
export declare function repoManifestSummary(root: string): ManifestSummary | null;
|
|
80
|
+
export interface RepoGitInfo {
|
|
81
|
+
branch: string;
|
|
82
|
+
dirty: number;
|
|
83
|
+
dirtyFiles: string[];
|
|
84
|
+
url: string | null;
|
|
85
|
+
lastCommit: {
|
|
86
|
+
sha: string;
|
|
87
|
+
subject: string;
|
|
88
|
+
relative: string;
|
|
89
|
+
} | null;
|
|
90
|
+
ahead: number | null;
|
|
91
|
+
behind: number | null;
|
|
92
|
+
}
|
|
93
|
+
export declare function repoGitInfo(root: string): RepoGitInfo | null;
|
|
59
94
|
export {};
|
package/dist/commands/inspect.js
CHANGED
|
@@ -229,6 +229,9 @@ export function collectRepoKind(repo, kind) {
|
|
|
229
229
|
for (const entry of entries) {
|
|
230
230
|
if (entry.name.startsWith('.'))
|
|
231
231
|
continue;
|
|
232
|
+
// Build/tooling caches are never resources — they only inflate counts.
|
|
233
|
+
if (entry.name === '__pycache__' || entry.name === 'node_modules')
|
|
234
|
+
continue;
|
|
232
235
|
const p = path.join(dir, entry.name);
|
|
233
236
|
items.push({
|
|
234
237
|
name: entry.name.replace(/\.(md|yaml|yml|toml|json)$/, ''),
|
|
@@ -240,14 +243,100 @@ export function collectRepoKind(repo, kind) {
|
|
|
240
243
|
}
|
|
241
244
|
return items.sort((a, b) => a.name.localeCompare(b.name));
|
|
242
245
|
}
|
|
246
|
+
/** A few resource names for the at-a-glance preview, with a `…(+N)` tail. */
|
|
247
|
+
function previewNames(items, n) {
|
|
248
|
+
if (items.length === 0)
|
|
249
|
+
return '';
|
|
250
|
+
const shown = items.slice(0, n).map(i => i.name);
|
|
251
|
+
const extra = items.length - shown.length;
|
|
252
|
+
return shown.join(', ') + (extra > 0 ? ` …(+${extra})` : '');
|
|
253
|
+
}
|
|
254
|
+
/** Recursive size + file count of a path; symlinks are not followed. */
|
|
255
|
+
export function pathSize(p) {
|
|
256
|
+
let stat;
|
|
257
|
+
try {
|
|
258
|
+
stat = fs.lstatSync(p);
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
return { bytes: 0, files: 0 };
|
|
262
|
+
}
|
|
263
|
+
if (stat.isSymbolicLink())
|
|
264
|
+
return { bytes: 0, files: 0 };
|
|
265
|
+
if (stat.isFile())
|
|
266
|
+
return { bytes: stat.size, files: 1 };
|
|
267
|
+
if (!stat.isDirectory())
|
|
268
|
+
return { bytes: 0, files: 0 };
|
|
269
|
+
let entries;
|
|
270
|
+
try {
|
|
271
|
+
entries = fs.readdirSync(p, { withFileTypes: true });
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
return { bytes: 0, files: 0 };
|
|
275
|
+
}
|
|
276
|
+
let bytes = 0, files = 0;
|
|
277
|
+
for (const e of entries) {
|
|
278
|
+
const sub = pathSize(path.join(p, e.name));
|
|
279
|
+
bytes += sub.bytes;
|
|
280
|
+
files += sub.files;
|
|
281
|
+
}
|
|
282
|
+
return { bytes, files };
|
|
283
|
+
}
|
|
284
|
+
/** Human byte size: "84 KB", "3.1 MB". */
|
|
285
|
+
export function formatBytes(n) {
|
|
286
|
+
if (n < 1024)
|
|
287
|
+
return `${n} B`;
|
|
288
|
+
const units = ['KB', 'MB', 'GB', 'TB'];
|
|
289
|
+
let v = n / 1024, i = 0;
|
|
290
|
+
while (v >= 1024 && i < units.length - 1) {
|
|
291
|
+
v /= 1024;
|
|
292
|
+
i++;
|
|
293
|
+
}
|
|
294
|
+
return `${v >= 10 ? Math.round(v) : v.toFixed(1)} ${units[i]}`;
|
|
295
|
+
}
|
|
296
|
+
/** Parse the repo's own agents.yaml into the version pins + run strategies it declares. */
|
|
297
|
+
export function repoManifestSummary(root) {
|
|
298
|
+
let parsed;
|
|
299
|
+
try {
|
|
300
|
+
parsed = yaml.parse(fs.readFileSync(path.join(root, 'agents.yaml'), 'utf-8'));
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
if (!parsed || typeof parsed !== 'object')
|
|
306
|
+
return null;
|
|
307
|
+
const obj = parsed;
|
|
308
|
+
const strategies = [];
|
|
309
|
+
if (obj.run && typeof obj.run === 'object') {
|
|
310
|
+
for (const [agent, cfg] of Object.entries(obj.run)) {
|
|
311
|
+
const strategy = cfg && typeof cfg === 'object' ? cfg.strategy : undefined;
|
|
312
|
+
if (typeof strategy === 'string')
|
|
313
|
+
strategies.push({ agent, strategy });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const versions = [];
|
|
317
|
+
if (obj.agents && typeof obj.agents === 'object') {
|
|
318
|
+
for (const [agent, ver] of Object.entries(obj.agents)) {
|
|
319
|
+
if (typeof ver === 'string')
|
|
320
|
+
versions.push({ agent, version: ver });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (strategies.length === 0 && versions.length === 0)
|
|
324
|
+
return null;
|
|
325
|
+
return { strategies, versions };
|
|
326
|
+
}
|
|
243
327
|
function renderRepoSummary(repo, options) {
|
|
244
328
|
const git = repoGitInfo(repo.root);
|
|
245
329
|
const manifests = REPO_MARKER_FILES.filter(m => fs.existsSync(path.join(repo.root, m)));
|
|
246
|
-
const
|
|
330
|
+
const manifest = repoManifestSummary(repo.root);
|
|
331
|
+
const kindData = {};
|
|
332
|
+
let totalBytes = 0, totalFiles = 0;
|
|
247
333
|
if (!options.brief) {
|
|
248
334
|
for (const kind of DRILLABLE_KINDS) {
|
|
249
335
|
const items = collectRepoKind(repo, kind);
|
|
250
|
-
|
|
336
|
+
const size = pathSize(path.join(repo.root, kind));
|
|
337
|
+
kindData[kind] = { items, size };
|
|
338
|
+
totalBytes += size.bytes;
|
|
339
|
+
totalFiles += size.files;
|
|
251
340
|
}
|
|
252
341
|
}
|
|
253
342
|
if (options.json) {
|
|
@@ -256,32 +345,65 @@ function renderRepoSummary(repo, options) {
|
|
|
256
345
|
root: repo.root,
|
|
257
346
|
git,
|
|
258
347
|
manifests,
|
|
259
|
-
|
|
348
|
+
manifest,
|
|
349
|
+
size: options.brief ? null : { bytes: totalBytes, files: totalFiles },
|
|
350
|
+
resources: options.brief ? null : Object.fromEntries(DRILLABLE_KINDS.map(kind => [kind, {
|
|
351
|
+
count: kindData[kind].items.length,
|
|
352
|
+
bytes: kindData[kind].size.bytes,
|
|
353
|
+
files: kindData[kind].size.files,
|
|
354
|
+
names: kindData[kind].items.map(i => i.name),
|
|
355
|
+
}])),
|
|
260
356
|
}, null, 2));
|
|
261
357
|
return;
|
|
262
358
|
}
|
|
263
359
|
console.log('\n' + chalk.bold(repo.label) + ' ' + chalk.gray('[dotagents repo]') + '\n');
|
|
264
|
-
|
|
360
|
+
// Indent for continuation sub-rows: 2 leading + 10 key column + 1 space.
|
|
361
|
+
const sub = (label, value) => console.log(` ${''.padEnd(10)} ${chalk.gray(label.padEnd(8))} ${value}`);
|
|
362
|
+
console.log(` ${'root'.padEnd(10)} ${termLink(repo.root, repo.root)}`);
|
|
265
363
|
if (git) {
|
|
266
364
|
const dirty = git.dirty > 0 ? ` ${chalk.gray('·')} ${chalk.yellow(`${git.dirty} dirty`)}` : '';
|
|
267
365
|
const url = git.url ? ` ${chalk.gray('·')} ${chalk.gray(git.url)}` : '';
|
|
268
|
-
|
|
366
|
+
console.log(` ${'git'.padEnd(10)} ${git.branch}${dirty}${url}`);
|
|
367
|
+
if (git.lastCommit) {
|
|
368
|
+
const rel = git.lastCommit.relative ? ` ${chalk.gray(`(${git.lastCommit.relative})`)}` : '';
|
|
369
|
+
sub('last', `${chalk.cyan(git.lastCommit.sha)} ${truncate(git.lastCommit.subject, 60)}${rel}`);
|
|
370
|
+
}
|
|
371
|
+
if (git.ahead !== null && git.behind !== null && (git.ahead > 0 || git.behind > 0)) {
|
|
372
|
+
sub('sync', `ahead ${git.ahead} ${chalk.gray('·')} behind ${git.behind}`);
|
|
373
|
+
}
|
|
374
|
+
if (git.dirtyFiles.length > 0) {
|
|
375
|
+
const shown = git.dirtyFiles.slice(0, 4).join(', ');
|
|
376
|
+
const extra = git.dirtyFiles.length - Math.min(4, git.dirtyFiles.length);
|
|
377
|
+
sub('dirty', chalk.yellow(shown + (extra > 0 ? ` …(+${extra})` : '')));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (manifests.length > 0) {
|
|
381
|
+
console.log(` ${'manifests'.padEnd(10)} ${manifests.join(', ')}`);
|
|
382
|
+
if (manifest) {
|
|
383
|
+
if (manifest.versions.length > 0) {
|
|
384
|
+
sub('versions', manifest.versions.map(v => `${v.agent} ${chalk.cyan(v.version)}`).join(chalk.gray(' · ')));
|
|
385
|
+
}
|
|
386
|
+
if (manifest.strategies.length > 0) {
|
|
387
|
+
sub('run', manifest.strategies.map(s => `${s.agent}:${s.strategy}`).join(chalk.gray(' · ')));
|
|
388
|
+
}
|
|
389
|
+
}
|
|
269
390
|
}
|
|
270
|
-
if (manifests.length > 0)
|
|
271
|
-
rows.push(['manifests', manifests.join(', ')]);
|
|
272
|
-
for (const [k, v] of rows)
|
|
273
|
-
console.log(` ${k.padEnd(10)} ${v}`);
|
|
274
391
|
if (!options.brief) {
|
|
392
|
+
console.log(` ${'size'.padEnd(10)} ${formatBytes(totalBytes)} ${chalk.gray('·')} ${totalFiles} files`);
|
|
275
393
|
console.log('\n' + chalk.bold('Resources'));
|
|
276
394
|
for (const kind of DRILLABLE_KINDS) {
|
|
277
|
-
|
|
395
|
+
const { items, size } = kindData[kind];
|
|
396
|
+
const count = String(items.length).padStart(4);
|
|
397
|
+
const sz = items.length > 0 ? formatBytes(size.bytes).padStart(8) : ''.padEnd(8);
|
|
398
|
+
const preview = items.length > 0 ? chalk.gray(truncate(previewNames(items, 4), 60)) : '';
|
|
399
|
+
console.log(` ${kind.padEnd(10)} ${count} ${sz} ${preview}`.trimEnd());
|
|
278
400
|
}
|
|
279
401
|
}
|
|
280
402
|
console.log('');
|
|
281
403
|
console.log(chalk.gray(`Drill in: agents inspect ${repo.label} --skills <query>`));
|
|
282
404
|
console.log('');
|
|
283
405
|
}
|
|
284
|
-
function repoGitInfo(root) {
|
|
406
|
+
export function repoGitInfo(root) {
|
|
285
407
|
const git = (args) => {
|
|
286
408
|
try {
|
|
287
409
|
return execSync(`git -C ${JSON.stringify(root)} ${args}`, { stdio: ['ignore', 'pipe', 'ignore'] })
|
|
@@ -294,9 +416,33 @@ function repoGitInfo(root) {
|
|
|
294
416
|
const branch = git('rev-parse --abbrev-ref HEAD');
|
|
295
417
|
if (branch === null)
|
|
296
418
|
return null;
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
419
|
+
// Read status WITHOUT trimming — git()'s .trim() would strip the leading
|
|
420
|
+
// space of the first porcelain line (`XY path`), corrupting the path slice.
|
|
421
|
+
let statusRaw;
|
|
422
|
+
try {
|
|
423
|
+
statusRaw = execSync(`git -C ${JSON.stringify(root)} status --porcelain`, { stdio: ['ignore', 'pipe', 'ignore'] }).toString();
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
statusRaw = null;
|
|
427
|
+
}
|
|
428
|
+
const dirtyFiles = statusRaw ? statusRaw.split('\n').filter(Boolean).map(l => l.slice(3)) : [];
|
|
429
|
+
let lastCommit = null;
|
|
430
|
+
const log = git('log -1 --format=%h%x1f%s%x1f%cr');
|
|
431
|
+
if (log) {
|
|
432
|
+
const [sha, subject, relative] = log.split('\x1f');
|
|
433
|
+
if (sha)
|
|
434
|
+
lastCommit = { sha, subject: subject ?? '', relative: relative ?? '' };
|
|
435
|
+
}
|
|
436
|
+
let ahead = null, behind = null;
|
|
437
|
+
const counts = git("rev-list --left-right --count '@{upstream}...HEAD'");
|
|
438
|
+
if (counts) {
|
|
439
|
+
const [b, a] = counts.split(/\s+/).map(n => parseInt(n, 10));
|
|
440
|
+
if (Number.isFinite(b) && Number.isFinite(a)) {
|
|
441
|
+
behind = b;
|
|
442
|
+
ahead = a;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return { branch, dirty: dirtyFiles.length, dirtyFiles, url: git('remote get-url origin'), lastCommit, ahead, behind };
|
|
300
446
|
}
|
|
301
447
|
// ─── Summary mode ────────────────────────────────────────────────────────────
|
|
302
448
|
async function renderSummary(agent, version, versionHome, options) {
|
package/dist/index.js
CHANGED
|
@@ -348,6 +348,24 @@ async function installResolvedPackage(metadata) {
|
|
|
348
348
|
await installPackageIntoPrefix(`${NPM_PACKAGE_NAME}@${metadata.version}`, prefix);
|
|
349
349
|
verifyInstalledVersion(packageRoot, metadata.version);
|
|
350
350
|
refreshAliasShims(packageRoot);
|
|
351
|
+
// The npm install above runs with --ignore-scripts, so the postinstall that
|
|
352
|
+
// installs the macOS Keychain helper never fires on upgrade. Force-refresh the
|
|
353
|
+
// helper here so a user upgrading FROM a broken build (e.g. the entitlement-less
|
|
354
|
+
// 1.20.4 helper that fails SecItemAdd with -34018) gets the fixed, signed bundle
|
|
355
|
+
// immediately — instead of waiting for the lazy staleness check in
|
|
356
|
+
// getKeychainHelperPath() to repair it on their next secret operation. The new
|
|
357
|
+
// package is already on disk, so the dynamic import resolves the freshly-installed
|
|
358
|
+
// helper module + bundle. Best-effort: an upgrade must never fail because the
|
|
359
|
+
// helper could not be reinstalled (`agents helper install --force` stays available).
|
|
360
|
+
if (process.platform === 'darwin') {
|
|
361
|
+
try {
|
|
362
|
+
const { ensureKeychainHelperInstalled } = await import('./lib/secrets/install-helper.js');
|
|
363
|
+
ensureKeychainHelperInstalled({ forceReinstall: true });
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
// Non-fatal.
|
|
367
|
+
}
|
|
368
|
+
}
|
|
351
369
|
}
|
|
352
370
|
/** Present an interactive upgrade prompt (TTY) or a one-line hint (non-TTY). */
|
|
353
371
|
async function promptUpgrade(latestVersion) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.12",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|