@phnx-labs/agents-cli 1.21.1 → 1.21.3
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 +206 -0
- package/README.md +1 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/computer-actions.d.ts +4 -0
- package/dist/commands/computer-actions.js +34 -0
- package/dist/commands/computer.js +4 -2
- package/dist/commands/doctor.js +5 -2
- package/dist/commands/feed.js +28 -19
- package/dist/commands/hooks.js +9 -45
- package/dist/commands/menubar.js +24 -24
- package/dist/commands/message.js +23 -3
- package/dist/commands/perf.d.ts +29 -0
- package/dist/commands/perf.js +90 -23
- package/dist/commands/projects.d.ts +21 -0
- package/dist/commands/projects.js +169 -23
- package/dist/commands/routines.js +46 -1
- package/dist/commands/sessions-picker.js +17 -2
- package/dist/commands/sessions.d.ts +5 -0
- package/dist/commands/sessions.js +14 -0
- package/dist/commands/ssh.js +69 -0
- package/dist/commands/trends.d.ts +2 -0
- package/dist/commands/trends.js +158 -0
- package/dist/commands/usage.d.ts +4 -4
- package/dist/commands/view.d.ts +6 -0
- package/dist/commands/view.js +90 -45
- package/dist/index.js +22 -1
- package/dist/lib/agents.js +2 -2
- package/dist/lib/analytics/dashboard.d.ts +11 -0
- package/dist/lib/analytics/dashboard.js +31 -0
- package/dist/lib/analytics/recipes.d.ts +32 -0
- package/dist/lib/analytics/recipes.js +316 -0
- package/dist/lib/analytics/usage-db.d.ts +84 -0
- package/dist/lib/analytics/usage-db.js +301 -0
- package/dist/lib/browser/service.js +31 -0
- package/dist/lib/cli-resources.d.ts +20 -0
- package/dist/lib/cli-resources.js +48 -1
- package/dist/lib/computer/dispatch.d.ts +3 -1
- package/dist/lib/computer/dispatch.js +10 -2
- package/dist/lib/daemon.js +51 -14
- package/dist/lib/devices/health-report.d.ts +5 -0
- package/dist/lib/devices/health-report.js +3 -0
- package/dist/lib/event-stream.d.ts +2 -0
- package/dist/lib/event-stream.js +3 -0
- package/dist/lib/events.d.ts +3 -1
- package/dist/lib/events.js +4 -2
- package/dist/lib/feed-broadcast.d.ts +52 -7
- package/dist/lib/feed-broadcast.js +125 -18
- package/dist/lib/fleet-cache.d.ts +37 -0
- package/dist/lib/fleet-cache.js +40 -0
- package/dist/lib/fleet-status.d.ts +53 -0
- package/dist/lib/fleet-status.js +120 -0
- package/dist/lib/friction-heuristics.d.ts +32 -0
- package/dist/lib/friction-heuristics.js +47 -0
- package/dist/lib/git.d.ts +14 -0
- package/dist/lib/git.js +36 -0
- package/dist/lib/hooks/cache.js +28 -6
- package/dist/lib/hooks/profile.d.ts +8 -0
- package/dist/lib/hooks/profile.js +15 -18
- package/dist/lib/hooks.js +72 -17
- package/dist/lib/linear-cache.d.ts +63 -0
- package/dist/lib/linear-cache.js +146 -0
- package/dist/lib/linear-project-counts.d.ts +35 -5
- package/dist/lib/linear-project-counts.js +61 -16
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +3 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +7 -0
- package/dist/lib/menubar/install-menubar.js +36 -6
- package/dist/lib/percentile.d.ts +12 -0
- package/dist/lib/percentile.js +24 -0
- package/dist/lib/perf/db.d.ts +7 -3
- package/dist/lib/perf/db.js +37 -19
- package/dist/lib/perf/types.d.ts +10 -0
- package/dist/lib/plugins.js +12 -1
- package/dist/lib/project-doctor.d.ts +36 -0
- package/dist/lib/project-doctor.js +45 -0
- package/dist/lib/project-import.d.ts +11 -1
- package/dist/lib/project-import.js +17 -3
- package/dist/lib/project-status.d.ts +25 -5
- package/dist/lib/project-status.js +48 -6
- package/dist/lib/resources.d.ts +16 -0
- package/dist/lib/resources.js +25 -14
- package/dist/lib/rotate.d.ts +27 -0
- package/dist/lib/rotate.js +44 -17
- package/dist/lib/routines.d.ts +16 -0
- package/dist/lib/routines.js +26 -0
- package/dist/lib/runner.js +41 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/usage-db.d.ts +3 -63
- package/dist/lib/secrets/usage-db.js +46 -186
- package/dist/lib/session/db.d.ts +17 -1
- package/dist/lib/session/db.js +240 -6
- package/dist/lib/session/discover.d.ts +31 -0
- package/dist/lib/session/discover.js +50 -1
- package/dist/lib/session/highlights.d.ts +24 -4
- package/dist/lib/session/highlights.js +52 -7
- package/dist/lib/session/parse.js +19 -3
- package/dist/lib/session/prompt.d.ts +19 -0
- package/dist/lib/session/prompt.js +43 -0
- package/dist/lib/session/types.d.ts +43 -0
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/state.d.ts +31 -3
- package/dist/lib/state.js +53 -10
- package/dist/lib/types.d.ts +22 -4
- package/dist/lib/usage-refresh.d.ts +106 -0
- package/dist/lib/usage-refresh.js +238 -0
- package/dist/lib/usage.d.ts +152 -17
- package/dist/lib/usage.js +393 -79
- package/package.json +1 -1
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
<key>CFBundleIdentifier</key>
|
|
8
8
|
<string>com.phnx-labs.agents-menubar</string>
|
|
9
9
|
<key>CFBundleName</key>
|
|
10
|
-
<string>
|
|
10
|
+
<string>AGI Menu</string>
|
|
11
|
+
<key>CFBundleDisplayName</key>
|
|
12
|
+
<string>AGI Menu</string>
|
|
11
13
|
<key>CFBundleIconFile</key>
|
|
12
14
|
<string>AppIcon</string>
|
|
13
15
|
<key>CFBundlePackageType</key>
|
|
Binary file
|
|
@@ -40,10 +40,17 @@ export declare function codesignVerifies(appPath: string): boolean;
|
|
|
40
40
|
* bootstrap a helper macOS would reject.
|
|
41
41
|
*/
|
|
42
42
|
export declare function gatekeeperAssesses(appPath: string): boolean;
|
|
43
|
+
/** True when the bundle carries a Developer ID TeamIdentifier (not ad-hoc). */
|
|
44
|
+
export declare function hasDeveloperIdSignature(appPath: string): boolean;
|
|
43
45
|
/**
|
|
44
46
|
* Copy the bundled `.app` to the stable user path (idempotent unless forced).
|
|
45
47
|
* Returns the installed executable path, or null if no source bundle ships
|
|
46
48
|
* with this install (e.g. Linux package, or a build without the helper).
|
|
49
|
+
*
|
|
50
|
+
* Also heals an older install that was ad-hoc re-signed over a Developer ID
|
|
51
|
+
* source: that unstable identity made Accessibility re-prompt on every upgrade.
|
|
52
|
+
* When the shipped source is Developer ID and the installed copy is only
|
|
53
|
+
* ad-hoc, replace it — even without forceReinstall.
|
|
47
54
|
*/
|
|
48
55
|
export declare function ensureMenubarAppInstalled(opts?: {
|
|
49
56
|
forceReinstall?: boolean;
|
|
@@ -205,10 +205,25 @@ export function gatekeeperAssesses(appPath) {
|
|
|
205
205
|
const r = spawnSync('spctl', ['--assess', '--type', 'exec', appPath], { stdio: ['ignore', 'ignore', 'ignore'] });
|
|
206
206
|
return r.status === 0;
|
|
207
207
|
}
|
|
208
|
+
/** True when the bundle carries a Developer ID TeamIdentifier (not ad-hoc). */
|
|
209
|
+
export function hasDeveloperIdSignature(appPath) {
|
|
210
|
+
const r = spawnSync('codesign', ['-dv', '--verbose=4', appPath], {
|
|
211
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
212
|
+
encoding: 'utf-8',
|
|
213
|
+
});
|
|
214
|
+
const out = `${r.stderr || ''}${r.stdout || ''}`;
|
|
215
|
+
const team = out.match(/TeamIdentifier=([A-Z0-9]+)/)?.[1];
|
|
216
|
+
return Boolean(team && team !== 'not set');
|
|
217
|
+
}
|
|
208
218
|
/**
|
|
209
219
|
* Copy the bundled `.app` to the stable user path (idempotent unless forced).
|
|
210
220
|
* Returns the installed executable path, or null if no source bundle ships
|
|
211
221
|
* with this install (e.g. Linux package, or a build without the helper).
|
|
222
|
+
*
|
|
223
|
+
* Also heals an older install that was ad-hoc re-signed over a Developer ID
|
|
224
|
+
* source: that unstable identity made Accessibility re-prompt on every upgrade.
|
|
225
|
+
* When the shipped source is Developer ID and the installed copy is only
|
|
226
|
+
* ad-hoc, replace it — even without forceReinstall.
|
|
212
227
|
*/
|
|
213
228
|
export function ensureMenubarAppInstalled(opts = {}) {
|
|
214
229
|
if (!onDarwin())
|
|
@@ -218,7 +233,11 @@ export function ensureMenubarAppInstalled(opts = {}) {
|
|
|
218
233
|
return null;
|
|
219
234
|
const dest = installedAppPath();
|
|
220
235
|
if (!opts.forceReinstall && fs.existsSync(dest)) {
|
|
221
|
-
|
|
236
|
+
const sourceIsDevId = hasDeveloperIdSignature(src);
|
|
237
|
+
const destIsDevId = hasDeveloperIdSignature(dest);
|
|
238
|
+
if (!(sourceIsDevId && !destIsDevId)) {
|
|
239
|
+
return installedExecutablePath();
|
|
240
|
+
}
|
|
222
241
|
}
|
|
223
242
|
copyAppBundle(src, dest);
|
|
224
243
|
// A fresh copy is exactly when the bundle's icon can be new (first install) or
|
|
@@ -325,7 +344,7 @@ export function enableMenubarService(opts = { clearOptOut: true }) {
|
|
|
325
344
|
// this passes; if it ever doesn't, skip the service and point at the upgrade
|
|
326
345
|
// rather than re-signing over it (an ad-hoc re-sign never satisfies Gatekeeper).
|
|
327
346
|
if (!(codesignVerifies(installedAppPath()) && gatekeeperAssesses(installedAppPath()))) {
|
|
328
|
-
process.stderr.write('agents:
|
|
347
|
+
process.stderr.write('agents: AGI Menu is not notarized/valid on this machine; skipping launch. ' +
|
|
329
348
|
'Upgrade to a notarized build (npm i -g @phnx-labs/agents-cli), then `agents menubar setup`.\n');
|
|
330
349
|
return false;
|
|
331
350
|
}
|
|
@@ -472,8 +491,10 @@ export function installMenubarLaunchAgentOnUpgrade() {
|
|
|
472
491
|
}
|
|
473
492
|
// Re-enable (recopy helper + rewrite plist) when the version drifted OR the
|
|
474
493
|
// plist's baked interpreter/entry no longer point at the install now running
|
|
475
|
-
// `agents` — the dual-install skew a version bump alone can't catch
|
|
476
|
-
|
|
494
|
+
// `agents` — the dual-install skew a version bump alone can't catch — OR the
|
|
495
|
+
// installed copy is still ad-hoc while the shipped source is Developer ID
|
|
496
|
+
// (older heal path; Accessibility re-prompts until the identity is restored).
|
|
497
|
+
if (menubarSetupStale() || menubarSetupNeedsRepoint() || installedNeedsDevIdHeal()) {
|
|
477
498
|
enableMenubarService({ clearOptOut: false });
|
|
478
499
|
}
|
|
479
500
|
}
|
|
@@ -481,6 +502,15 @@ export function installMenubarLaunchAgentOnUpgrade() {
|
|
|
481
502
|
/* never block startup on the menu bar */
|
|
482
503
|
}
|
|
483
504
|
}
|
|
505
|
+
/** True when App Support still has an ad-hoc copy but the npm bundle is Developer ID. */
|
|
506
|
+
function installedNeedsDevIdHeal() {
|
|
507
|
+
const src = sourceAppPath();
|
|
508
|
+
if (!src || !fs.existsSync(installedAppPath()))
|
|
509
|
+
return false;
|
|
510
|
+
if (hasDeveloperIdSignature(installedAppPath()))
|
|
511
|
+
return false;
|
|
512
|
+
return hasDeveloperIdSignature(src);
|
|
513
|
+
}
|
|
484
514
|
/**
|
|
485
515
|
* Decide which live helper processes must be ended so exactly one status item
|
|
486
516
|
* survives. Pure so the choice is unit-testable without a live menu bar.
|
|
@@ -521,12 +551,12 @@ export function runMenubarSetup() {
|
|
|
521
551
|
steps.push({ name, outcome, detail });
|
|
522
552
|
};
|
|
523
553
|
if (!onDarwin()) {
|
|
524
|
-
step('platform', 'failed', `
|
|
554
|
+
step('platform', 'failed', `AGI Menu is macOS only (this is ${process.platform})`);
|
|
525
555
|
return { steps, configured: false, status: getMenubarStatus() };
|
|
526
556
|
}
|
|
527
557
|
const before = getMenubarStatus();
|
|
528
558
|
if (!sourceAppPath()) {
|
|
529
|
-
step('bundle', 'failed', 'no
|
|
559
|
+
step('bundle', 'failed', 'no AGI Menu bundle ships with this install');
|
|
530
560
|
return { steps, configured: false, status: before };
|
|
531
561
|
}
|
|
532
562
|
// 3 before 1: end the running copies BEFORE swapping the bundle underneath
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Percentile of a sorted-ascending array, linear interpolation. p in [0,100].
|
|
3
|
+
*
|
|
4
|
+
* Deliberately its own file with zero imports: `perf/db.ts` (the SQLite
|
|
5
|
+
* warehouse), `hooks/profile.ts` (the legacy JSONL hook profile), and
|
|
6
|
+
* `routines.ts` (routineStats) all need this exact formula, but `routines.ts`
|
|
7
|
+
* and `hooks/profile.ts` must NOT pull in `perf/db.ts`'s `../sqlite.js`
|
|
8
|
+
* dependency just to round a percentile — sqlite is a heavier, perf-warehouse-
|
|
9
|
+
* specific dependency that has no business loading into every routines- or
|
|
10
|
+
* hooks-touching code path.
|
|
11
|
+
*/
|
|
12
|
+
export declare function percentile(sorted: number[], p: number): number;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Percentile of a sorted-ascending array, linear interpolation. p in [0,100].
|
|
3
|
+
*
|
|
4
|
+
* Deliberately its own file with zero imports: `perf/db.ts` (the SQLite
|
|
5
|
+
* warehouse), `hooks/profile.ts` (the legacy JSONL hook profile), and
|
|
6
|
+
* `routines.ts` (routineStats) all need this exact formula, but `routines.ts`
|
|
7
|
+
* and `hooks/profile.ts` must NOT pull in `perf/db.ts`'s `../sqlite.js`
|
|
8
|
+
* dependency just to round a percentile — sqlite is a heavier, perf-warehouse-
|
|
9
|
+
* specific dependency that has no business loading into every routines- or
|
|
10
|
+
* hooks-touching code path.
|
|
11
|
+
*/
|
|
12
|
+
export function percentile(sorted, p) {
|
|
13
|
+
if (sorted.length === 0)
|
|
14
|
+
return 0;
|
|
15
|
+
if (sorted.length === 1)
|
|
16
|
+
return sorted[0];
|
|
17
|
+
const rank = (p / 100) * (sorted.length - 1);
|
|
18
|
+
const lo = Math.floor(rank);
|
|
19
|
+
const hi = Math.ceil(rank);
|
|
20
|
+
if (lo === hi)
|
|
21
|
+
return sorted[lo];
|
|
22
|
+
const frac = rank - lo;
|
|
23
|
+
return sorted[lo] * (1 - frac) + sorted[hi] * frac;
|
|
24
|
+
}
|
package/dist/lib/perf/db.d.ts
CHANGED
|
@@ -8,16 +8,20 @@ import Database from '../sqlite.js';
|
|
|
8
8
|
import type { AggregateOptions, PerfAggregateRow } from './types.js';
|
|
9
9
|
export type { AggregateOptions, PerfAggregateRow, PerfSample } from './types.js';
|
|
10
10
|
export { recordSample, shortSessionId, resolveSpoolPath } from './spool.js';
|
|
11
|
+
export { percentile } from '../percentile.js';
|
|
11
12
|
export declare const PERF_SCHEMA_VERSION = 1;
|
|
12
13
|
export declare const DEFAULT_RETENTION_DAYS = 30;
|
|
13
14
|
/** Test seam — redirect the warehouse path (like AGENTS_EVENTS_PATH). */
|
|
14
15
|
export declare function _resetPerfDbForTest(overridePath?: string | null): void;
|
|
15
16
|
/** Drain the NDJSON spool into samples. Idempotent; truncates on success. */
|
|
16
17
|
export declare function drainSpool(db?: Database.Database): number;
|
|
17
|
-
/** Percentile of a sorted-ascending array. p in [0,100]. */
|
|
18
|
-
export declare function percentile(sorted: number[], p: number): number;
|
|
19
18
|
/**
|
|
20
|
-
* Aggregate samples by (kind, label) with p50/p99. Drains the spool first.
|
|
19
|
+
* Aggregate samples by (kind, label) with p50/p95/p99. Drains the spool first.
|
|
20
|
+
*
|
|
21
|
+
* `opts.project` scopes the query to samples whose recorded `cwd` resolves to
|
|
22
|
+
* that project key (see project-key.ts) — resolution runs per unique cwd
|
|
23
|
+
* (memoized) rather than per row, since `resolveProjectKey` does a filesystem
|
|
24
|
+
* walk and a warehouse query can carry many rows sharing the same cwd.
|
|
21
25
|
*/
|
|
22
26
|
export declare function aggregateSamples(opts?: AggregateOptions): PerfAggregateRow[];
|
|
23
27
|
export declare function perfDbPath(): string;
|
package/dist/lib/perf/db.js
CHANGED
|
@@ -9,8 +9,11 @@ import * as path from 'path';
|
|
|
9
9
|
import Database from '../sqlite.js';
|
|
10
10
|
import { getPerfDbPath, getPerfDir } from '../state.js';
|
|
11
11
|
import { localMachineId } from '../session/origin-machine.js';
|
|
12
|
+
import { resolveProjectKey } from '../project-key.js';
|
|
13
|
+
import { percentile } from '../percentile.js';
|
|
12
14
|
import { resolveSpoolPath, shortSessionId, _resetPerfSpoolForTest } from './spool.js';
|
|
13
15
|
export { recordSample, shortSessionId, resolveSpoolPath } from './spool.js';
|
|
16
|
+
export { percentile } from '../percentile.js';
|
|
14
17
|
export const PERF_SCHEMA_VERSION = 1;
|
|
15
18
|
export const DEFAULT_RETENTION_DAYS = 30;
|
|
16
19
|
const SCHEMA = `
|
|
@@ -185,22 +188,13 @@ function maybeRetain(db) {
|
|
|
185
188
|
// ignore
|
|
186
189
|
}
|
|
187
190
|
}
|
|
188
|
-
/** Percentile of a sorted-ascending array. p in [0,100]. */
|
|
189
|
-
export function percentile(sorted, p) {
|
|
190
|
-
if (sorted.length === 0)
|
|
191
|
-
return 0;
|
|
192
|
-
if (sorted.length === 1)
|
|
193
|
-
return sorted[0];
|
|
194
|
-
const rank = (p / 100) * (sorted.length - 1);
|
|
195
|
-
const lo = Math.floor(rank);
|
|
196
|
-
const hi = Math.ceil(rank);
|
|
197
|
-
if (lo === hi)
|
|
198
|
-
return sorted[lo];
|
|
199
|
-
const frac = rank - lo;
|
|
200
|
-
return sorted[lo] * (1 - frac) + sorted[hi] * frac;
|
|
201
|
-
}
|
|
202
191
|
/**
|
|
203
|
-
* Aggregate samples by (kind, label) with p50/p99. Drains the spool first.
|
|
192
|
+
* Aggregate samples by (kind, label) with p50/p95/p99. Drains the spool first.
|
|
193
|
+
*
|
|
194
|
+
* `opts.project` scopes the query to samples whose recorded `cwd` resolves to
|
|
195
|
+
* that project key (see project-key.ts) — resolution runs per unique cwd
|
|
196
|
+
* (memoized) rather than per row, since `resolveProjectKey` does a filesystem
|
|
197
|
+
* walk and a warehouse query can carry many rows sharing the same cwd.
|
|
204
198
|
*/
|
|
205
199
|
export function aggregateSamples(opts = {}) {
|
|
206
200
|
const db = openDb();
|
|
@@ -228,14 +222,29 @@ export function aggregateSamples(opts = {}) {
|
|
|
228
222
|
clauses.push('agent = ?');
|
|
229
223
|
params.push(opts.agent);
|
|
230
224
|
}
|
|
231
|
-
const rows = db.prepare(`SELECT kind, label, duration_ms, cache, exit_code
|
|
225
|
+
const rows = db.prepare(`SELECT kind, label, duration_ms, cache, exit_code, status, cwd
|
|
232
226
|
FROM samples WHERE ${clauses.join(' AND ')}`).all(...params);
|
|
227
|
+
// Memoize cwd -> project key: resolveProjectKey walks the filesystem for
|
|
228
|
+
// a repo root, and many rows in one warehouse query share the same cwd.
|
|
229
|
+
const projectCache = new Map();
|
|
230
|
+
const projectForCwd = (cwd) => {
|
|
231
|
+
if (!cwd)
|
|
232
|
+
return undefined;
|
|
233
|
+
let key = projectCache.get(cwd);
|
|
234
|
+
if (key === undefined && !projectCache.has(cwd)) {
|
|
235
|
+
key = resolveProjectKey(cwd);
|
|
236
|
+
projectCache.set(cwd, key);
|
|
237
|
+
}
|
|
238
|
+
return key;
|
|
239
|
+
};
|
|
233
240
|
const map = new Map();
|
|
234
241
|
for (const r of rows) {
|
|
242
|
+
if (opts.project && projectForCwd(r.cwd) !== opts.project)
|
|
243
|
+
continue;
|
|
235
244
|
const key = `${r.kind}\0${r.label}`;
|
|
236
245
|
let b = map.get(key);
|
|
237
246
|
if (!b) {
|
|
238
|
-
b = { kind: r.kind, label: r.label, durations: [], hits: 0, stale: 0, misses: 0, errors: 0 };
|
|
247
|
+
b = { kind: r.kind, label: r.label, durations: [], hits: 0, stale: 0, misses: 0, errors: 0, timeouts: 0 };
|
|
239
248
|
map.set(key, b);
|
|
240
249
|
}
|
|
241
250
|
b.durations.push(Number(r.duration_ms));
|
|
@@ -245,7 +254,9 @@ export function aggregateSamples(opts = {}) {
|
|
|
245
254
|
b.stale++;
|
|
246
255
|
else if (r.cache === 'miss' || r.cache === 'none')
|
|
247
256
|
b.misses++;
|
|
248
|
-
if (
|
|
257
|
+
if (r.status === 'timeout')
|
|
258
|
+
b.timeouts++;
|
|
259
|
+
else if (typeof r.exit_code === 'number' && r.exit_code !== 0)
|
|
249
260
|
b.errors++;
|
|
250
261
|
}
|
|
251
262
|
const out = [];
|
|
@@ -260,6 +271,7 @@ export function aggregateSamples(opts = {}) {
|
|
|
260
271
|
label: b.label,
|
|
261
272
|
n,
|
|
262
273
|
p50Ms: Math.round(percentile(sorted, 50)),
|
|
274
|
+
p95Ms: Math.round(percentile(sorted, 95)),
|
|
263
275
|
p99Ms: Math.round(percentile(sorted, 99)),
|
|
264
276
|
meanMs: Math.round(sum / n),
|
|
265
277
|
maxMs: sorted[n - 1],
|
|
@@ -270,8 +282,14 @@ export function aggregateSamples(opts = {}) {
|
|
|
270
282
|
row.cacheStalePct = Math.round((b.stale / n) * 100);
|
|
271
283
|
row.cacheMissPct = Math.round((b.misses / n) * 100);
|
|
272
284
|
}
|
|
273
|
-
if (b.errors > 0)
|
|
285
|
+
if (b.errors > 0) {
|
|
274
286
|
row.errorCount = b.errors;
|
|
287
|
+
row.errorRate = Math.round((b.errors / n) * 1000) / 1000;
|
|
288
|
+
}
|
|
289
|
+
if (b.timeouts > 0)
|
|
290
|
+
row.timeoutRate = Math.round((b.timeouts / n) * 1000) / 1000;
|
|
291
|
+
if (opts.project)
|
|
292
|
+
row.project = opts.project;
|
|
275
293
|
out.push(row);
|
|
276
294
|
}
|
|
277
295
|
out.sort((a, b) => b.p99Ms - a.p99Ms);
|
package/dist/lib/perf/types.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export interface PerfAggregateRow {
|
|
|
26
26
|
label: string;
|
|
27
27
|
n: number;
|
|
28
28
|
p50Ms: number;
|
|
29
|
+
p95Ms: number;
|
|
29
30
|
p99Ms: number;
|
|
30
31
|
meanMs: number;
|
|
31
32
|
maxMs: number;
|
|
@@ -34,6 +35,13 @@ export interface PerfAggregateRow {
|
|
|
34
35
|
cacheStalePct?: number;
|
|
35
36
|
cacheMissPct?: number;
|
|
36
37
|
errorCount?: number;
|
|
38
|
+
/** Fraction (0-1) of samples with a nonzero exit code. */
|
|
39
|
+
errorRate?: number;
|
|
40
|
+
/** Fraction (0-1) of samples with status:'timeout'. */
|
|
41
|
+
timeoutRate?: number;
|
|
42
|
+
/** Project key (see project-key.ts) the row is scoped to — set only when
|
|
43
|
+
* the `project` filter narrowed the query to one project. */
|
|
44
|
+
project?: string;
|
|
37
45
|
}
|
|
38
46
|
export interface AggregateOptions {
|
|
39
47
|
days?: number;
|
|
@@ -42,4 +50,6 @@ export interface AggregateOptions {
|
|
|
42
50
|
machine?: string;
|
|
43
51
|
agent?: string;
|
|
44
52
|
minN?: number;
|
|
53
|
+
/** Scope results to samples whose cwd resolves to this project key. */
|
|
54
|
+
project?: string;
|
|
45
55
|
}
|
package/dist/lib/plugins.js
CHANGED
|
@@ -14,7 +14,7 @@ import * as yaml from 'yaml';
|
|
|
14
14
|
import { execFileSync } from 'child_process';
|
|
15
15
|
import { getPluginsDir, getTrashPluginsDir, getExtraPluginsDir, getProjectPluginsDir, getSystemPluginsDir } from './state.js';
|
|
16
16
|
import { IS_WINDOWS, isWindowsAbsolutePath, homeDir } from './platform/index.js';
|
|
17
|
-
import { assertSafeGitTransport } from './git.js';
|
|
17
|
+
import { assertSafeGitTransport, resolveSnapshotSha } from './git.js';
|
|
18
18
|
import { listInstalledVersions, getVersionHomePath } from './versions.js';
|
|
19
19
|
import { AGENTS, agentConfigDirName } from './agents.js';
|
|
20
20
|
import { capableAgents, isCapable } from './capabilities.js';
|
|
@@ -90,6 +90,13 @@ export function discoverPlugins(opts = {}) {
|
|
|
90
90
|
return out;
|
|
91
91
|
}
|
|
92
92
|
export function buildDiscoveredPlugin(pluginRoot, manifest, spec = { kind: 'user' }) {
|
|
93
|
+
// Every marketplace kind lays plugins out as `<repo>/plugins/<name>`, so the
|
|
94
|
+
// repo root is always the grandparent of pluginRoot — true for user
|
|
95
|
+
// (~/.agents), system (~/.agents/.system), each extra repo, and the project
|
|
96
|
+
// repo (<cwd>/.agents) alike. Deriving it here means every caller of
|
|
97
|
+
// buildDiscoveredPlugin (discoverPluginsInDir, inspectPluginCapabilities, …)
|
|
98
|
+
// gets provenance for free with no signature change.
|
|
99
|
+
const repoRoot = path.dirname(path.dirname(pluginRoot));
|
|
93
100
|
return {
|
|
94
101
|
name: manifest.name,
|
|
95
102
|
root: pluginRoot,
|
|
@@ -107,6 +114,10 @@ export function buildDiscoveredPlugin(pluginRoot, manifest, spec = { kind: 'user
|
|
|
107
114
|
monitors: discoverPluginMonitors(pluginRoot),
|
|
108
115
|
hasMcp: fs.existsSync(path.join(pluginRoot, '.mcp.json')),
|
|
109
116
|
hasSettings: pluginHasNonPermissionSettings(pluginRoot),
|
|
117
|
+
repoRoot,
|
|
118
|
+
get snapshotSha() {
|
|
119
|
+
return resolveSnapshotSha(repoRoot);
|
|
120
|
+
},
|
|
110
121
|
};
|
|
111
122
|
}
|
|
112
123
|
/**
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Definition-vs-reality checks for `agents projects`.
|
|
3
|
+
*
|
|
4
|
+
* A project definition is hand-editable YAML that nothing validates against the
|
|
5
|
+
* world it describes, so it can drift into being confidently wrong. The one that
|
|
6
|
+
* bit for real: `repo:` said `<user>/agents-cli` while the checkout's `origin`
|
|
7
|
+
* was `phnx-labs/agents-cli`. Both are real repositories, so nothing errored —
|
|
8
|
+
* the card's merged-PR and release lines simply reported a stranger's repo (0
|
|
9
|
+
* merges in 7 days instead of 100). A wrong answer that looks like a right one
|
|
10
|
+
* is worse than a missing one, so the mismatch gets said out loud wherever the
|
|
11
|
+
* def is shown.
|
|
12
|
+
*
|
|
13
|
+
* Findings carry their own fix, mirroring `DoctorFinding.remediation`
|
|
14
|
+
* (`lib/devices/doctor-findings.ts`) — a warning the reader has to go work out
|
|
15
|
+
* how to act on is half a warning. Pure: the caller supplies the observed
|
|
16
|
+
* remote, so this is unit-testable with no git and no fixture repo.
|
|
17
|
+
*/
|
|
18
|
+
import type { ProjectDef } from './projects.js';
|
|
19
|
+
/** A definition that disagrees with the machine it describes. */
|
|
20
|
+
export interface ProjectFinding {
|
|
21
|
+
project: string;
|
|
22
|
+
/** One line naming the disagreement, both sides quoted. */
|
|
23
|
+
message: string;
|
|
24
|
+
/** The exact command that fixes it. */
|
|
25
|
+
remediation: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Compare a def's `repo` slug against the checkout's real `origin` remote.
|
|
29
|
+
*
|
|
30
|
+
* - remote unreadable (no checkout on this machine, not a git repo) → no
|
|
31
|
+
* finding. The def may be perfectly right; this machine just can't say.
|
|
32
|
+
* - def has no `repo` → a finding only when a remote exists to adopt, so the
|
|
33
|
+
* fix is a real one-liner rather than a nag.
|
|
34
|
+
* - they disagree → a finding. This is the case that silently lies.
|
|
35
|
+
*/
|
|
36
|
+
export declare function checkRepoSlug(def: ProjectDef, actualRemote: string | undefined): ProjectFinding | undefined;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Definition-vs-reality checks for `agents projects`.
|
|
3
|
+
*
|
|
4
|
+
* A project definition is hand-editable YAML that nothing validates against the
|
|
5
|
+
* world it describes, so it can drift into being confidently wrong. The one that
|
|
6
|
+
* bit for real: `repo:` said `<user>/agents-cli` while the checkout's `origin`
|
|
7
|
+
* was `phnx-labs/agents-cli`. Both are real repositories, so nothing errored —
|
|
8
|
+
* the card's merged-PR and release lines simply reported a stranger's repo (0
|
|
9
|
+
* merges in 7 days instead of 100). A wrong answer that looks like a right one
|
|
10
|
+
* is worse than a missing one, so the mismatch gets said out loud wherever the
|
|
11
|
+
* def is shown.
|
|
12
|
+
*
|
|
13
|
+
* Findings carry their own fix, mirroring `DoctorFinding.remediation`
|
|
14
|
+
* (`lib/devices/doctor-findings.ts`) — a warning the reader has to go work out
|
|
15
|
+
* how to act on is half a warning. Pure: the caller supplies the observed
|
|
16
|
+
* remote, so this is unit-testable with no git and no fixture repo.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Compare a def's `repo` slug against the checkout's real `origin` remote.
|
|
20
|
+
*
|
|
21
|
+
* - remote unreadable (no checkout on this machine, not a git repo) → no
|
|
22
|
+
* finding. The def may be perfectly right; this machine just can't say.
|
|
23
|
+
* - def has no `repo` → a finding only when a remote exists to adopt, so the
|
|
24
|
+
* fix is a real one-liner rather than a nag.
|
|
25
|
+
* - they disagree → a finding. This is the case that silently lies.
|
|
26
|
+
*/
|
|
27
|
+
export function checkRepoSlug(def, actualRemote) {
|
|
28
|
+
if (!actualRemote)
|
|
29
|
+
return undefined;
|
|
30
|
+
if (def.repo === actualRemote)
|
|
31
|
+
return undefined;
|
|
32
|
+
const fix = `agents projects set ${def.name} --repo ${actualRemote}`;
|
|
33
|
+
if (!def.repo) {
|
|
34
|
+
return {
|
|
35
|
+
project: def.name,
|
|
36
|
+
message: `no repo set; origin is ${actualRemote}`,
|
|
37
|
+
remediation: fix,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
project: def.name,
|
|
42
|
+
message: `repo is ${def.repo} but origin is ${actualRemote} — PR and release counts are being read from the wrong repository`,
|
|
43
|
+
remediation: fix,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -63,8 +63,18 @@ export declare function validateImportOpts(flags: RawImportFlags): ImportOptions
|
|
|
63
63
|
* `repoSlug`→`repo`, `linearProjectId`→`linear.projectId`), now gated on the
|
|
64
64
|
* row's `confidence`. A row with no confidence field is a guess with no stated
|
|
65
65
|
* strength, so it ranks below every floor and only `--all` takes it.
|
|
66
|
+
*
|
|
67
|
+
* `resolveRemote` reads the checkout's actual `origin` and OVERRIDES the row's
|
|
68
|
+
* `repoSlug`. Factory derives that slug from the checkout path's last two
|
|
69
|
+
* segments (`apps/factory/src/core/projectIndex.ts:19-26`), which is only right
|
|
70
|
+
* when the directory tree happens to mirror the GitHub org. It commonly does
|
|
71
|
+
* not: a checkout at `~/src/github.com/<you>/agents-cli` whose remote is
|
|
72
|
+
* `phnx-labs/agents-cli` imported as `<you>/agents-cli` — a real, different
|
|
73
|
+
* repo — so the card's merged-PR and release lines silently reported a
|
|
74
|
+
* stranger's repository instead of failing. The remote is the only authority on
|
|
75
|
+
* which repo a checkout is; the path is a guess about it.
|
|
66
76
|
*/
|
|
67
|
-
export declare function buildFactoryImportCandidates(rows: unknown[], existing: Map<string, ProjectDef>, opts: Pick<ImportOptions, 'minConfidence' | 'force'
|
|
77
|
+
export declare function buildFactoryImportCandidates(rows: unknown[], existing: Map<string, ProjectDef>, opts: Pick<ImportOptions, 'minConfidence' | 'force'>, resolveRemote?: (root: string) => string | undefined): ImportPlan;
|
|
68
78
|
/**
|
|
69
79
|
* Turn a Linear project name into a definition slug: lowercase, every run of
|
|
70
80
|
* unusable characters collapsed to one `-`, trimmed of leading/trailing
|
|
@@ -60,8 +60,18 @@ export function validateImportOpts(flags) {
|
|
|
60
60
|
* `repoSlug`→`repo`, `linearProjectId`→`linear.projectId`), now gated on the
|
|
61
61
|
* row's `confidence`. A row with no confidence field is a guess with no stated
|
|
62
62
|
* strength, so it ranks below every floor and only `--all` takes it.
|
|
63
|
+
*
|
|
64
|
+
* `resolveRemote` reads the checkout's actual `origin` and OVERRIDES the row's
|
|
65
|
+
* `repoSlug`. Factory derives that slug from the checkout path's last two
|
|
66
|
+
* segments (`apps/factory/src/core/projectIndex.ts:19-26`), which is only right
|
|
67
|
+
* when the directory tree happens to mirror the GitHub org. It commonly does
|
|
68
|
+
* not: a checkout at `~/src/github.com/<you>/agents-cli` whose remote is
|
|
69
|
+
* `phnx-labs/agents-cli` imported as `<you>/agents-cli` — a real, different
|
|
70
|
+
* repo — so the card's merged-PR and release lines silently reported a
|
|
71
|
+
* stranger's repository instead of failing. The remote is the only authority on
|
|
72
|
+
* which repo a checkout is; the path is a guess about it.
|
|
63
73
|
*/
|
|
64
|
-
export function buildFactoryImportCandidates(rows, existing, opts) {
|
|
74
|
+
export function buildFactoryImportCandidates(rows, existing, opts, resolveRemote = () => undefined) {
|
|
65
75
|
const floor = CONFIDENCE_RANK[opts.minConfidence];
|
|
66
76
|
const defs = [];
|
|
67
77
|
const skipped = [];
|
|
@@ -97,8 +107,12 @@ export function buildFactoryImportCandidates(rows, existing, opts) {
|
|
|
97
107
|
const def = { name };
|
|
98
108
|
if (typeof o.path === 'string')
|
|
99
109
|
def.root = toHomeRelative(o.path);
|
|
100
|
-
|
|
101
|
-
|
|
110
|
+
// The checkout's own remote wins over the registry's path-derived guess;
|
|
111
|
+
// the guess is the fallback for a path with no git remote to ask.
|
|
112
|
+
const fromRemote = typeof o.path === 'string' ? resolveRemote(o.path) : undefined;
|
|
113
|
+
const repo = fromRemote ?? (typeof o.repoSlug === 'string' ? o.repoSlug : undefined);
|
|
114
|
+
if (repo)
|
|
115
|
+
def.repo = repo;
|
|
102
116
|
if (typeof o.linearProjectId === 'string')
|
|
103
117
|
def.linear = { projectId: o.linearProjectId };
|
|
104
118
|
defs.push(def);
|
|
@@ -54,11 +54,25 @@ export interface ProjectSessionRollup {
|
|
|
54
54
|
* with the full definition list to show zero-agent projects.
|
|
55
55
|
*/
|
|
56
56
|
export declare function rollupSessionsByProject(defs: ProjectDef[], sessions: ActiveSession[]): Map<string, ProjectSessionRollup>;
|
|
57
|
-
/**
|
|
58
|
-
export
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
/** Live vs finished sessions on a project. */
|
|
58
|
+
export interface LiveDeadSplit {
|
|
59
|
+
live: number;
|
|
60
|
+
dead: number;
|
|
61
|
+
/** Dead broken out by status, for the card's parenthetical. */
|
|
62
|
+
deadByStatus: Array<{
|
|
63
|
+
status: string;
|
|
64
|
+
n: number;
|
|
65
|
+
}>;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Split a rollup's sessions into what is working and what is wreckage.
|
|
69
|
+
*
|
|
70
|
+
* The headline used to be the raw session count, which on a real project read
|
|
71
|
+
* `39 agents` when 19 of those had crashed. A count that is half corpses is not
|
|
72
|
+
* a throughput signal — but the corpses are worth their own number, because 19
|
|
73
|
+
* crashed sessions is itself a thing to go fix.
|
|
74
|
+
*/
|
|
75
|
+
export declare function liveDeadSplit(byStatus: Partial<Record<ActiveStatus, number>>): LiveDeadSplit;
|
|
62
76
|
/** Sort members for the card: running first, then idle, then the rest; agent name asc within a state. */
|
|
63
77
|
export declare function sortProjectMembers(members: ProjectMember[]): ProjectMember[];
|
|
64
78
|
/** Cap for the members line before it collapses to `+N more`. */
|
|
@@ -77,6 +91,12 @@ export interface ProjectRemoteSignals {
|
|
|
77
91
|
windowDays: number;
|
|
78
92
|
/** PRs merged into the primary repo within the window (via `gh`). */
|
|
79
93
|
mergedPrs: number;
|
|
94
|
+
/**
|
|
95
|
+
* True when the `gh` fetch cap cut the count short — `mergedPrs` is then a
|
|
96
|
+
* LOWER bound (rendered `100+`), never presented as the complete count. Same
|
|
97
|
+
* contract `LinearProjectCounts.truncated` keeps for the Linear line.
|
|
98
|
+
*/
|
|
99
|
+
mergedPrsTruncated?: boolean;
|
|
80
100
|
/** Artifacts agents produced within the window (activity.created milestones). */
|
|
81
101
|
artifacts: number;
|
|
82
102
|
/** Basename of the most recent artifact, when any. */
|
|
@@ -17,6 +17,13 @@ import chalk from 'chalk';
|
|
|
17
17
|
import { projectNameForCwd } from './projects.js';
|
|
18
18
|
import { readRecentActivity } from './activity.js';
|
|
19
19
|
const execFileAsync = promisify(execFile);
|
|
20
|
+
/**
|
|
21
|
+
* How many recent merges `gh` is asked for. A repo busy enough that all of them
|
|
22
|
+
* land inside the window has more than this — agents-cli itself merged 100 of
|
|
23
|
+
* its 100 most recent PRs within 7 days — so the count is reported as a lower
|
|
24
|
+
* bound rather than as a total.
|
|
25
|
+
*/
|
|
26
|
+
const MERGED_PR_LIMIT = 100;
|
|
20
27
|
function blank(name) {
|
|
21
28
|
return {
|
|
22
29
|
name,
|
|
@@ -77,11 +84,40 @@ export function rollupSessionsByProject(defs, sessions) {
|
|
|
77
84
|
}
|
|
78
85
|
return map;
|
|
79
86
|
}
|
|
80
|
-
/**
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Statuses that mean the session is over. Taken from the repo's own rule
|
|
89
|
+
* (`commands/sessions.ts`): "`closed` and `crashed` are unconditionally dead".
|
|
90
|
+
* `orphaned` is NOT among them — `session/active.ts` defines it as "Alive, but
|
|
91
|
+
* no client is attached", i.e. the agent outlived its window and is still
|
|
92
|
+
* working. Counting it as dead understates the project by exactly the sessions
|
|
93
|
+
* that are running unattended.
|
|
94
|
+
*/
|
|
95
|
+
const DEAD_STATUSES = new Set(['closed', 'crashed']);
|
|
96
|
+
/**
|
|
97
|
+
* Split a rollup's sessions into what is working and what is wreckage.
|
|
98
|
+
*
|
|
99
|
+
* The headline used to be the raw session count, which on a real project read
|
|
100
|
+
* `39 agents` when 19 of those had crashed. A count that is half corpses is not
|
|
101
|
+
* a throughput signal — but the corpses are worth their own number, because 19
|
|
102
|
+
* crashed sessions is itself a thing to go fix.
|
|
103
|
+
*/
|
|
104
|
+
export function liveDeadSplit(byStatus) {
|
|
105
|
+
let live = 0;
|
|
106
|
+
let dead = 0;
|
|
107
|
+
const deadByStatus = [];
|
|
108
|
+
for (const [status, n] of Object.entries(byStatus)) {
|
|
109
|
+
if (!n)
|
|
110
|
+
continue;
|
|
111
|
+
if (DEAD_STATUSES.has(status)) {
|
|
112
|
+
dead += n;
|
|
113
|
+
deadByStatus.push({ status, n });
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
live += n;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
deadByStatus.sort((a, b) => b.n - a.n || a.status.localeCompare(b.status));
|
|
120
|
+
return { live, dead, deadByStatus };
|
|
85
121
|
}
|
|
86
122
|
/**
|
|
87
123
|
* Display order for the members line: the states a human scans for first
|
|
@@ -158,9 +194,15 @@ export async function enrichProjectSignals(def, windowDays, nowMs, opts = {}) {
|
|
|
158
194
|
}
|
|
159
195
|
if (def.repo && !opts.skipRemote) {
|
|
160
196
|
try {
|
|
161
|
-
const { stdout } = await execFileAsync('gh', ['pr', 'list', '--repo', def.repo, '--state', 'merged', '--json', 'number,mergedAt', '--limit',
|
|
197
|
+
const { stdout } = await execFileAsync('gh', ['pr', 'list', '--repo', def.repo, '--state', 'merged', '--json', 'number,mergedAt', '--limit', String(MERGED_PR_LIMIT)], { timeout: 8000, encoding: 'utf8' });
|
|
162
198
|
const rows = JSON.parse(stdout);
|
|
163
199
|
out.mergedPrs = rows.filter((r) => r.mergedAt && Date.parse(r.mergedAt) >= sinceMs).length;
|
|
200
|
+
// `--limit 100` caps the fetch, so a busy repo where every one of the 100
|
|
201
|
+
// most recent merges falls inside the window has MORE than 100 — this repo
|
|
202
|
+
// really does. Say so (`100+`) rather than presenting a cap as a count,
|
|
203
|
+
// the same contract `LinearProjectCounts.truncated` already keeps.
|
|
204
|
+
if (rows.length >= MERGED_PR_LIMIT && out.mergedPrs >= MERGED_PR_LIMIT)
|
|
205
|
+
out.mergedPrsTruncated = true;
|
|
164
206
|
}
|
|
165
207
|
catch {
|
|
166
208
|
/* gh missing / unauthenticated / repo not found — skip this signal */
|
package/dist/lib/resources.d.ts
CHANGED
|
@@ -16,6 +16,22 @@ export interface ResolvedResource {
|
|
|
16
16
|
* or the alias name (e.g. 'rush') for extra repos registered in agents.yaml.
|
|
17
17
|
*/
|
|
18
18
|
source: string;
|
|
19
|
+
/**
|
|
20
|
+
* Absolute path to the DotAgents repo root this resource resolved from (the
|
|
21
|
+
* project/user/system/extra-repo dir — one level above the `kind`
|
|
22
|
+
* subdirectory). DotAgents repos are git-tracked (plugins.ts), so this pairs
|
|
23
|
+
* with {@link snapshotSha} to answer "which commit of which repo".
|
|
24
|
+
*/
|
|
25
|
+
repoRoot: string;
|
|
26
|
+
/**
|
|
27
|
+
* Short HEAD sha of `repoRoot`'s git checkout, lazily resolved (a getter,
|
|
28
|
+
* not computed at construction) and memoized per repoRoot
|
|
29
|
+
* (`git.ts` `resolveSnapshotSha`) — a caller that never inspects provenance
|
|
30
|
+
* never pays for the git shell-out, and resolving many resources from the
|
|
31
|
+
* same repo pays for exactly one. `undefined` when `repoRoot` isn't a git
|
|
32
|
+
* repo (or has no commits).
|
|
33
|
+
*/
|
|
34
|
+
readonly snapshotSha: string | undefined;
|
|
19
35
|
}
|
|
20
36
|
/**
|
|
21
37
|
* True when `rawName` (a filename with its extension already stripped) names a
|