@phnx-labs/agents-cli 1.21.1 → 1.21.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/CHANGELOG.md +172 -0
- package/README.md +1 -0
- package/dist/bin/agents +0 -0
- 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 +13 -0
- package/dist/commands/perf.js +80 -23
- package/dist/commands/projects.d.ts +11 -0
- package/dist/commands/projects.js +153 -21
- package/dist/commands/routines.js +46 -1
- 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 +14 -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 +18 -0
- package/dist/lib/cli-resources.d.ts +20 -0
- package/dist/lib/cli-resources.js +48 -1
- 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/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/hooks/cache.js +28 -6
- package/dist/lib/hooks/profile.d.ts +8 -0
- package/dist/lib/hooks/profile.js +14 -4
- 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/perf/db.d.ts +6 -1
- package/dist/lib/perf/db.js +35 -5
- package/dist/lib/perf/types.d.ts +10 -0
- 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/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 +39 -0
- package/dist/lib/runner.js +34 -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 +2 -1
- package/dist/lib/session/db.js +14 -3
- package/dist/lib/session/discover.d.ts +3 -0
- package/dist/lib/session/discover.js +8 -0
- package/dist/lib/session/types.d.ts +1 -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 +8 -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
package/dist/commands/perf.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* agents perf hooks per-hook p50/p99 + cache hit rates
|
|
7
7
|
* agents perf commands slowest CLI command paths (from command.end)
|
|
8
8
|
* agents perf run agent.run / perf.timing labels
|
|
9
|
+
* agents perf friction sessions stuck repeatedly hitting the same guard
|
|
9
10
|
*
|
|
10
11
|
* Soft-joins sessions.db via shared string keys (session_id, agent, machine) —
|
|
11
12
|
* no foreign keys. Warehouse lives at ~/.agents/.cache/perf/perf.db (safe to wipe).
|
|
@@ -13,6 +14,8 @@
|
|
|
13
14
|
import chalk from 'chalk';
|
|
14
15
|
import { aggregateSamples, perfDbPath, } from '../lib/perf/db.js';
|
|
15
16
|
import { formatMs, formatCacheColumn, DEFAULT_SLOW_HOOK_WARN_MS, loadHookFireEvents, aggregateHookProfile, } from '../lib/hooks/profile.js';
|
|
17
|
+
import { query } from '../lib/events.js';
|
|
18
|
+
import { detectRepeatedGuardBlocks } from '../lib/friction-heuristics.js';
|
|
16
19
|
function parseDays(raw) {
|
|
17
20
|
const n = parseInt(raw ?? '7', 10);
|
|
18
21
|
return Number.isFinite(n) && n > 0 ? n : 7;
|
|
@@ -26,11 +29,12 @@ function parseLimit(raw, fallback) {
|
|
|
26
29
|
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
27
30
|
}
|
|
28
31
|
/** Map warehouse rows shaped like hook.fire into the existing HookProfileRow UI. */
|
|
29
|
-
function asHookRows(rows) {
|
|
32
|
+
export function asHookRows(rows) {
|
|
30
33
|
return rows.map((r) => ({
|
|
31
34
|
hook: r.label,
|
|
32
35
|
n: r.n,
|
|
33
36
|
p50Ms: r.p50Ms,
|
|
37
|
+
p95Ms: r.p95Ms,
|
|
34
38
|
p99Ms: r.p99Ms,
|
|
35
39
|
meanMs: r.meanMs,
|
|
36
40
|
maxMs: r.maxMs,
|
|
@@ -38,6 +42,9 @@ function asHookRows(rows) {
|
|
|
38
42
|
cacheStalePct: r.cacheStalePct ?? 0,
|
|
39
43
|
cacheMissPct: r.cacheMissPct ?? 0,
|
|
40
44
|
errorCount: r.errorCount ?? 0,
|
|
45
|
+
errorRate: r.errorRate,
|
|
46
|
+
timeoutRate: r.timeoutRate,
|
|
47
|
+
project: r.project,
|
|
41
48
|
}));
|
|
42
49
|
}
|
|
43
50
|
function printTable(headers, widths, lines, slowFlags) {
|
|
@@ -50,23 +57,34 @@ function printTable(headers, widths, lines, slowFlags) {
|
|
|
50
57
|
console.log(slowFlags[i] ? chalk.yellow(line) : line);
|
|
51
58
|
}
|
|
52
59
|
}
|
|
53
|
-
|
|
60
|
+
/** `err:12% to:4%` when either rate is present, else ''. */
|
|
61
|
+
function formatRateColumn(r) {
|
|
62
|
+
const parts = [];
|
|
63
|
+
if (r.errorRate)
|
|
64
|
+
parts.push(`err:${Math.round(r.errorRate * 100)}%`);
|
|
65
|
+
if (r.timeoutRate)
|
|
66
|
+
parts.push(`to:${Math.round(r.timeoutRate * 100)}%`);
|
|
67
|
+
return parts.join(' ');
|
|
68
|
+
}
|
|
69
|
+
export function renderHookTable(rows, warnMs) {
|
|
54
70
|
if (rows.length === 0) {
|
|
55
71
|
console.log(chalk.gray('No hook timing samples yet.'));
|
|
56
72
|
console.log(chalk.gray(`Warehouse: ${perfDbPath()}`));
|
|
57
73
|
console.log(chalk.gray('Hooks write via cache/matches shims into the spool; run a session or resync hooks.'));
|
|
58
74
|
return;
|
|
59
75
|
}
|
|
60
|
-
const widths = { hook: 36, n: 5, p50: 7, p99: 7, mean: 7, max: 7, cache:
|
|
76
|
+
const widths = { hook: 36, n: 5, p50: 7, p95: 7, p99: 7, mean: 7, max: 7, cache: 22, rate: 14 };
|
|
61
77
|
const pad = (s, w) => (s.length >= w ? s.slice(0, w) : s + ' '.repeat(w - s.length));
|
|
62
78
|
const header = [
|
|
63
79
|
pad('HOOK', widths.hook),
|
|
64
80
|
pad('N', widths.n),
|
|
65
81
|
pad('P50', widths.p50),
|
|
82
|
+
pad('P95', widths.p95),
|
|
66
83
|
pad('P99', widths.p99),
|
|
67
84
|
pad('MEAN', widths.mean),
|
|
68
85
|
pad('MAX', widths.max),
|
|
69
86
|
pad('CACHE', widths.cache),
|
|
87
|
+
pad('ERR/TIMEOUT', widths.rate),
|
|
70
88
|
].join(' ');
|
|
71
89
|
console.log(chalk.bold(header));
|
|
72
90
|
console.log(chalk.gray('─'.repeat(header.length)));
|
|
@@ -78,10 +96,12 @@ function renderHookTable(rows, warnMs) {
|
|
|
78
96
|
pad(r.hook, widths.hook),
|
|
79
97
|
pad(String(r.n), widths.n),
|
|
80
98
|
pad(formatMs(r.p50Ms), widths.p50),
|
|
99
|
+
pad(formatMs(r.p95Ms), widths.p95),
|
|
81
100
|
pad(formatMs(r.p99Ms), widths.p99),
|
|
82
101
|
pad(formatMs(r.meanMs), widths.mean),
|
|
83
102
|
pad(formatMs(r.maxMs), widths.max),
|
|
84
103
|
pad(cacheCol, widths.cache),
|
|
104
|
+
pad(formatRateColumn(r), widths.rate),
|
|
85
105
|
].join(' ') + warning;
|
|
86
106
|
console.log(slow ? chalk.yellow(line) : line);
|
|
87
107
|
}
|
|
@@ -92,30 +112,34 @@ function renderLabelTable(title, rows, warnMs, limit) {
|
|
|
92
112
|
console.log(chalk.gray(`No ${title} samples yet.`));
|
|
93
113
|
return;
|
|
94
114
|
}
|
|
95
|
-
const widths = [40, 5, 7, 7, 7, 7];
|
|
96
|
-
printTable(['LABEL', 'N', 'P50', 'P99', 'MEAN', 'MAX'], widths, sliced.map((r) => [
|
|
115
|
+
const widths = [40, 5, 7, 7, 7, 7, 7, 14];
|
|
116
|
+
printTable(['LABEL', 'N', 'P50', 'P95', 'P99', 'MEAN', 'MAX', 'ERR/TIMEOUT'], widths, sliced.map((r) => [
|
|
97
117
|
r.label,
|
|
98
118
|
String(r.n),
|
|
99
119
|
formatMs(r.p50Ms),
|
|
120
|
+
formatMs(r.p95Ms),
|
|
100
121
|
formatMs(r.p99Ms),
|
|
101
122
|
formatMs(r.meanMs),
|
|
102
123
|
formatMs(r.maxMs),
|
|
124
|
+
formatRateColumn(r),
|
|
103
125
|
]), sliced.map((r) => r.p99Ms > warnMs));
|
|
104
126
|
}
|
|
105
127
|
/**
|
|
106
128
|
* Prefer SQLite samples; fall back to the legacy daily JSONL so existing
|
|
107
|
-
* instrumentation still surfaces until shims are resynced.
|
|
129
|
+
* instrumentation still surfaces until shims are resynced. `project` only
|
|
130
|
+
* narrows the SQLite path — the legacy JSONL log has no cwd, so a fallback
|
|
131
|
+
* hit ignores it (a caller filtering by project has no legacy rows to miss).
|
|
108
132
|
*/
|
|
109
|
-
function loadHookProfile(days) {
|
|
110
|
-
const fromDb = asHookRows(aggregateSamples({ days, kinds: ['hook.fire'] }));
|
|
111
|
-
if (fromDb.length > 0)
|
|
133
|
+
export function loadHookProfile(days, project) {
|
|
134
|
+
const fromDb = asHookRows(aggregateSamples({ days, kinds: ['hook.fire'], project }));
|
|
135
|
+
if (fromDb.length > 0 || project)
|
|
112
136
|
return fromDb;
|
|
113
137
|
return aggregateHookProfile(loadHookFireEvents(days));
|
|
114
138
|
}
|
|
115
139
|
function hooksAction(opts) {
|
|
116
140
|
const days = parseDays(opts.days);
|
|
117
141
|
const warnMs = parseWarnMs(opts.warnMs, DEFAULT_SLOW_HOOK_WARN_MS);
|
|
118
|
-
const rows = loadHookProfile(days);
|
|
142
|
+
const rows = loadHookProfile(days, opts.project);
|
|
119
143
|
if (opts.json) {
|
|
120
144
|
console.log(JSON.stringify(rows, null, 2));
|
|
121
145
|
return;
|
|
@@ -126,7 +150,7 @@ function commandsAction(opts) {
|
|
|
126
150
|
const days = parseDays(opts.days);
|
|
127
151
|
const warnMs = parseWarnMs(opts.warnMs, 500);
|
|
128
152
|
const limit = parseLimit(opts.limit, 40);
|
|
129
|
-
const rows = aggregateSamples({ days, kinds: ['command.end'] });
|
|
153
|
+
const rows = aggregateSamples({ days, kinds: ['command.end'], project: opts.project });
|
|
130
154
|
if (opts.json) {
|
|
131
155
|
console.log(JSON.stringify(rows.slice(0, limit), null, 2));
|
|
132
156
|
return;
|
|
@@ -137,42 +161,69 @@ function runAction(opts) {
|
|
|
137
161
|
const days = parseDays(opts.days);
|
|
138
162
|
const warnMs = parseWarnMs(opts.warnMs, 60_000);
|
|
139
163
|
const limit = parseLimit(opts.limit, 40);
|
|
140
|
-
const rows = aggregateSamples({ days, kinds: ['perf.timing'] });
|
|
164
|
+
const rows = aggregateSamples({ days, kinds: ['perf.timing'], project: opts.project });
|
|
141
165
|
if (opts.json) {
|
|
142
166
|
console.log(JSON.stringify(rows.slice(0, limit), null, 2));
|
|
143
167
|
return;
|
|
144
168
|
}
|
|
145
169
|
renderLabelTable('run/timing', rows, warnMs, limit);
|
|
146
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* Sessions stuck repeatedly hitting the SAME guard block (git-guard,
|
|
173
|
+
* rm-guard, git-require-clean-tree, …) instead of adapting after the first
|
|
174
|
+
* denial. Reads the `friction` event sink (emitFriction in events.ts) that
|
|
175
|
+
* guard hooks self-report into via `agents _internal friction` before they
|
|
176
|
+
* exit 2 — see lib/friction-heuristics.ts for the grouping.
|
|
177
|
+
*/
|
|
178
|
+
function frictionAction(opts) {
|
|
179
|
+
const days = parseDays(opts.days);
|
|
180
|
+
const startDate = new Date(Date.now() - days * 86_400_000);
|
|
181
|
+
const events = query({ eventTypes: ['friction'], startDate });
|
|
182
|
+
const findings = detectRepeatedGuardBlocks(events);
|
|
183
|
+
if (opts.json) {
|
|
184
|
+
console.log(JSON.stringify(findings, null, 2));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (findings.length === 0) {
|
|
188
|
+
console.log(chalk.gray(`No repeated guard blocks in the last ${days} day${days === 1 ? '' : 's'}.`));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
console.log(chalk.bold(`Repeated guard blocks — last ${days} day${days === 1 ? '' : 's'}\n`));
|
|
192
|
+
const widths = [14, 10, 24, 5, 22, 22];
|
|
193
|
+
printTable(['SESSION', 'SURFACE', 'FAILURE ID', 'N', 'FIRST', 'LAST'], widths, findings.map((f) => [f.session, f.surface, f.failureId, String(f.count), f.firstTs, f.lastTs]), findings.map(() => false));
|
|
194
|
+
}
|
|
147
195
|
function summaryAction(opts) {
|
|
148
196
|
const days = parseDays(opts.days);
|
|
197
|
+
const project = opts.project;
|
|
149
198
|
if (opts.json) {
|
|
150
199
|
console.log(JSON.stringify({
|
|
151
200
|
days,
|
|
201
|
+
project: project ?? null,
|
|
152
202
|
warehouse: perfDbPath(),
|
|
153
|
-
hooks: loadHookProfile(days),
|
|
154
|
-
commands: aggregateSamples({ days, kinds: ['command.end'] }).slice(0, 20),
|
|
155
|
-
run: aggregateSamples({ days, kinds: ['perf.timing'] }).slice(0, 20),
|
|
203
|
+
hooks: loadHookProfile(days, project),
|
|
204
|
+
commands: aggregateSamples({ days, kinds: ['command.end'], project }).slice(0, 20),
|
|
205
|
+
run: aggregateSamples({ days, kinds: ['perf.timing'], project }).slice(0, 20),
|
|
156
206
|
}, null, 2));
|
|
157
207
|
return;
|
|
158
208
|
}
|
|
159
|
-
console.log(chalk.bold(`agents perf — last ${days} day${days === 1 ? '' : 's'}`));
|
|
209
|
+
console.log(chalk.bold(`agents perf — last ${days} day${days === 1 ? '' : 's'}${project ? ` — project ${project}` : ''}`));
|
|
160
210
|
console.log(chalk.gray(`warehouse: ${perfDbPath()} (disposable; soft-join sessions via session_id/agent/machine)`));
|
|
161
211
|
console.log('');
|
|
162
212
|
console.log(chalk.bold('Commands (slowest by p99)'));
|
|
163
|
-
renderLabelTable('command', aggregateSamples({ days, kinds: ['command.end'] }), parseWarnMs(opts.warnMs, 500), 12);
|
|
213
|
+
renderLabelTable('command', aggregateSamples({ days, kinds: ['command.end'], project }), parseWarnMs(opts.warnMs, 500), 12);
|
|
164
214
|
console.log('');
|
|
165
215
|
console.log(chalk.bold('Hooks'));
|
|
166
|
-
renderHookTable(loadHookProfile(days), parseWarnMs(opts.warnMs, DEFAULT_SLOW_HOOK_WARN_MS));
|
|
216
|
+
renderHookTable(loadHookProfile(days, project), parseWarnMs(opts.warnMs, DEFAULT_SLOW_HOOK_WARN_MS));
|
|
167
217
|
console.log('');
|
|
168
218
|
console.log(chalk.bold('Runs (perf.timing)'));
|
|
169
|
-
renderLabelTable('run/timing', aggregateSamples({ days, kinds: ['perf.timing'] }), parseWarnMs(opts.warnMs, 60_000), 12);
|
|
219
|
+
renderLabelTable('run/timing', aggregateSamples({ days, kinds: ['perf.timing'], project }), parseWarnMs(opts.warnMs, 60_000), 12);
|
|
170
220
|
}
|
|
171
221
|
function attachSharedOptions(cmd) {
|
|
172
222
|
return cmd
|
|
173
223
|
.option('--days <n>', 'Days of samples to include', '7')
|
|
174
224
|
.option('--warn-ms <n>', 'p99 above this is highlighted')
|
|
175
225
|
.option('--limit <n>', 'Max rows in the table', '40')
|
|
226
|
+
.option('--project <key>', 'Scope to one project (the repo directory name a sample\'s cwd resolves to — see project-key.ts)')
|
|
176
227
|
.option('--json', 'Emit JSON instead of a table');
|
|
177
228
|
}
|
|
178
229
|
/**
|
|
@@ -195,11 +246,13 @@ Identity columns reuse sessions/events string shapes (session_id, agent, machine
|
|
|
195
246
|
for soft cross-reference; there are no foreign keys.
|
|
196
247
|
|
|
197
248
|
Examples:
|
|
198
|
-
agents perf
|
|
199
|
-
agents perf hooks
|
|
200
|
-
agents perf commands --days 30
|
|
201
|
-
agents perf run --json
|
|
249
|
+
agents perf # summary: commands + hooks + runs
|
|
250
|
+
agents perf hooks # per-hook p50/p95/p99 + cache hit rate
|
|
251
|
+
agents perf commands --days 30 # slowest CLI entrypoints
|
|
252
|
+
agents perf run --json # agent.run timings as JSON
|
|
202
253
|
agents perf hooks --warn-ms 500
|
|
254
|
+
agents perf hooks --project agents-cli # scope to one repo's samples
|
|
255
|
+
agents perf friction # sessions stuck retrying the same guard block
|
|
203
256
|
`);
|
|
204
257
|
// Options live on the parent so `agents perf --json` and
|
|
205
258
|
// `agents perf commands --json` both work (see leafOpts).
|
|
@@ -218,4 +271,8 @@ Examples:
|
|
|
218
271
|
.action(function run() {
|
|
219
272
|
runAction(leafOpts(this));
|
|
220
273
|
});
|
|
274
|
+
perf.command('friction').description('Sessions stuck repeatedly hitting the same guard block')
|
|
275
|
+
.action(function friction() {
|
|
276
|
+
frictionAction(leafOpts(this));
|
|
277
|
+
});
|
|
221
278
|
}
|
|
@@ -20,6 +20,8 @@ export declare const PROJECTS_NO_FANOUT_ENV = "AGENTS_PROJECTS_LOCAL";
|
|
|
20
20
|
* string when everything answered.
|
|
21
21
|
*/
|
|
22
22
|
export declare function formatFleetSkippedNote(skipped: string[]): string;
|
|
23
|
+
/** Best-effort `owner/repo` from a repo's origin remote. */
|
|
24
|
+
export declare function originSlug(cwd: string): string | undefined;
|
|
23
25
|
/** One `projects list` row, pre-render. */
|
|
24
26
|
export interface ProjectListRow {
|
|
25
27
|
name: string;
|
|
@@ -45,6 +47,15 @@ export declare function computeProjectListWidths(rows: ProjectListRow[]): {
|
|
|
45
47
|
* read it as UTC and shift the answer by a day for anyone west of Greenwich.
|
|
46
48
|
*/
|
|
47
49
|
export declare function formatMilestoneDue(targetDate: string, nowMs: number): string | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* The milestone block. `status` prints one line — the next checkpoint — because
|
|
52
|
+
* a roll-up across projects has to stay scannable. `view` prints every declared
|
|
53
|
+
* milestone, because "how many are there and when are they due" is the shape of
|
|
54
|
+
* the plan and the reason to open one project.
|
|
55
|
+
*
|
|
56
|
+
* Pure (chalk only) so the layout is testable without a Linear account.
|
|
57
|
+
*/
|
|
58
|
+
export declare function formatMilestoneLines(milestones: LinearMilestone[], next: LinearMilestone | undefined, nowMs: number, limit: number): string[];
|
|
48
59
|
/** The `next` card line: what this project is due to hit, and how far along it is. */
|
|
49
60
|
export declare function formatNextMilestone(ms: LinearMilestone, nowMs: number): string;
|
|
50
61
|
export declare function registerProjectsCommands(program: Command): void;
|
|
@@ -25,9 +25,10 @@ import { gatherRemoteAgentsJson } from '../lib/remote-agents-json.js';
|
|
|
25
25
|
import { factoryProjectsPath } from '../lib/auto-dispatch.js';
|
|
26
26
|
import { formatFleetWorkspaces, parseRemoteProbe, probeProjectWorkspaces, workspaceTargetsForDef, } from '../lib/project-probe.js';
|
|
27
27
|
import { listProjectDefs, loadProjectDef, writeProjectDef, removeProjectDef, projectDefPath, isSafeProjectName, } from '../lib/projects.js';
|
|
28
|
-
import { rollupSessionsByProject,
|
|
28
|
+
import { rollupSessionsByProject, liveDeadSplit, enrichProjectSignals, formatProjectMembers, } from '../lib/project-status.js';
|
|
29
29
|
import { fetchLinearProjectCounts } from '../lib/linear-project-counts.js';
|
|
30
30
|
import { listLinearProjects, pickLinearProject } from '../lib/linear-projects.js';
|
|
31
|
+
import { checkRepoSlug } from '../lib/project-doctor.js';
|
|
31
32
|
import { buildFactoryImportCandidates, buildLinearImportCandidates, validateImportOpts, } from '../lib/project-import.js';
|
|
32
33
|
/** Recursion guard: a peer answering a probe fan-out never re-fans-out itself. */
|
|
33
34
|
export const PROJECTS_NO_FANOUT_ENV = 'AGENTS_PROJECTS_LOCAL';
|
|
@@ -57,7 +58,7 @@ function parseContextFlag(raw) {
|
|
|
57
58
|
return { path: raw.slice(0, i).trim(), purpose: raw.slice(i + 1).trim() };
|
|
58
59
|
}
|
|
59
60
|
/** Best-effort `owner/repo` from a repo's origin remote. */
|
|
60
|
-
function originSlug(cwd) {
|
|
61
|
+
export function originSlug(cwd) {
|
|
61
62
|
try {
|
|
62
63
|
const url = execFileSync('git', ['remote', 'get-url', 'origin'], { cwd, encoding: 'utf8' }).trim();
|
|
63
64
|
return parseOwnerRepoFromRemote(url) ?? undefined;
|
|
@@ -94,7 +95,9 @@ function runFactoryImport(existing, opts) {
|
|
|
94
95
|
: Array.isArray(parsed?.projects)
|
|
95
96
|
? parsed.projects
|
|
96
97
|
: [];
|
|
97
|
-
|
|
98
|
+
// The checkout's real `origin` overrides the registry's path-derived slug —
|
|
99
|
+
// see buildFactoryImportCandidates for why the path guess is not trustworthy.
|
|
100
|
+
return buildFactoryImportCandidates(rows, existing, opts, (root) => originSlug(expandLocalHome(root)));
|
|
98
101
|
}
|
|
99
102
|
/**
|
|
100
103
|
* List the workspace's Linear projects and plan their import, binding each to a
|
|
@@ -183,6 +186,40 @@ export function formatMilestoneDue(targetDate, nowMs) {
|
|
|
183
186
|
});
|
|
184
187
|
return `due ${label}`;
|
|
185
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* The milestone block. `status` prints one line — the next checkpoint — because
|
|
191
|
+
* a roll-up across projects has to stay scannable. `view` prints every declared
|
|
192
|
+
* milestone, because "how many are there and when are they due" is the shape of
|
|
193
|
+
* the plan and the reason to open one project.
|
|
194
|
+
*
|
|
195
|
+
* Pure (chalk only) so the layout is testable without a Linear account.
|
|
196
|
+
*/
|
|
197
|
+
export function formatMilestoneLines(milestones, next, nowMs, limit) {
|
|
198
|
+
if (milestones.length === 0) {
|
|
199
|
+
// `next` without a list only happens on a cached answer written before the
|
|
200
|
+
// list existed; render what we have rather than dropping the row.
|
|
201
|
+
return next ? [` ${chalk.dim('next')} ${formatNextMilestone(next, nowMs)}`] : [];
|
|
202
|
+
}
|
|
203
|
+
// The next milestone leads, always. Linear can flag a LATER-dated milestone
|
|
204
|
+
// as next, and `milestones` is date-ordered — so slicing the front would show
|
|
205
|
+
// an earlier one and hide the actual next behind "+N more", which is the one
|
|
206
|
+
// thing this row exists to say. Identity is name+targetDate: two milestones
|
|
207
|
+
// can share a name, and matching on name alone labelled the wrong row.
|
|
208
|
+
const key = (m) => `${m.name}${m.targetDate ?? ''}`;
|
|
209
|
+
const lead = next ? milestones.filter((m) => key(m) === key(next)).slice(0, 1) : [];
|
|
210
|
+
const others = next ? milestones.filter((m) => key(m) !== key(next)) : milestones;
|
|
211
|
+
const ordered = [...lead, ...others];
|
|
212
|
+
const shown = ordered.slice(0, Math.max(1, limit));
|
|
213
|
+
const out = shown.map((m, i) => {
|
|
214
|
+
const label = i === 0 && lead.length > 0 ? 'next' : i === 0 ? 'plan' : '';
|
|
215
|
+
return ` ${chalk.dim(label.padEnd(4))} ${formatNextMilestone(m, nowMs)}`;
|
|
216
|
+
});
|
|
217
|
+
const rest = milestones.length - shown.length;
|
|
218
|
+
if (rest > 0) {
|
|
219
|
+
out.push(` ${' '.repeat(4)} ${chalk.dim(`+${rest} more milestone${rest === 1 ? '' : 's'} — agents projects view <name>`)}`);
|
|
220
|
+
}
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
186
223
|
/** The `next` card line: what this project is due to hit, and how far along it is. */
|
|
187
224
|
export function formatNextMilestone(ms, nowMs) {
|
|
188
225
|
const parts = [chalk.bold(ms.name)];
|
|
@@ -206,25 +243,39 @@ function statusBar(r) {
|
|
|
206
243
|
push(r.byStatus.input_required, 'need-input', chalk.yellow);
|
|
207
244
|
push(r.byStatus.queued, 'queued', chalk.gray);
|
|
208
245
|
const shown = (r.byStatus.running ?? 0) + (r.byStatus.idle ?? 0) + (r.byStatus.input_required ?? 0) + (r.byStatus.queued ?? 0);
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
246
|
+
// The remainder is the LIVE sessions in a state without its own chip
|
|
247
|
+
// (orphaned, unknown). Dead ones are on their own row now, so counting them
|
|
248
|
+
// here made the line disagree with the headline — `19 live` beside
|
|
249
|
+
// `+24 other`, which invites the reader to trust neither.
|
|
250
|
+
const live = liveDeadSplit(r.byStatus).live;
|
|
251
|
+
if (live > shown)
|
|
252
|
+
parts.push(chalk.gray(`+${live - shown} other`));
|
|
213
253
|
return parts.join(' · ') || chalk.gray('no live agents');
|
|
214
254
|
}
|
|
215
|
-
function renderCard(def, r, remote, fleet, linear, nowMs = Date.now()
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
255
|
+
function renderCard(def, r, remote, fleet, linear, nowMs = Date.now(),
|
|
256
|
+
/** How many milestones to print. `status` shows the next one; `view` shows all. */
|
|
257
|
+
milestoneLimit = 1) {
|
|
258
|
+
// The headline counts LIVE agents. It used to be every matched session, which
|
|
259
|
+
// read `39 agents` on a project where 19 had crashed. `planPct` used to sit
|
|
260
|
+
// here too and is gone: it summed each session's latest checklist snapshot,
|
|
261
|
+
// so one agent opening a fresh 40-item plan rendered the whole project `0%`.
|
|
262
|
+
const split = r ? liveDeadSplit(r.byStatus) : { live: 0, dead: 0, deadByStatus: [] };
|
|
263
|
+
console.log(`${chalk.bold(def.name)} ${chalk.dim('·')} ${chalk.bold(`${split.live} live`)}`);
|
|
220
264
|
if (def.description)
|
|
221
265
|
console.log(` ${chalk.dim(def.description)}`);
|
|
222
266
|
console.log(` ${chalk.dim('live')} ${r ? statusBar(r) : chalk.gray('no live agents')}`);
|
|
267
|
+
if (split.dead > 0) {
|
|
268
|
+
// Wreckage is worth a number of its own — 19 crashed sessions is a thing to
|
|
269
|
+
// go fix, not a throughput signal to fold into the headline.
|
|
270
|
+
const detail = split.deadByStatus.map((d) => `${d.n} ${d.status}`).join(', ');
|
|
271
|
+
console.log(` ${chalk.dim('dead')} ${chalk.yellow(`${split.dead} finished or lost`)} ${chalk.dim(`(${detail})`)}`);
|
|
272
|
+
}
|
|
223
273
|
if (r && r.members.length)
|
|
224
274
|
console.log(` ${chalk.dim('agents')} ${formatProjectMembers(r.members)}`);
|
|
225
275
|
const ships = [];
|
|
226
|
-
if (remote?.mergedPrs)
|
|
227
|
-
ships.push(chalk.green(`${remote.mergedPrs} merged (${remote.windowDays}d)`));
|
|
276
|
+
if (remote?.mergedPrs) {
|
|
277
|
+
ships.push(chalk.green(`${remote.mergedPrs}${remote.mergedPrsTruncated ? '+' : ''} merged (${remote.windowDays}d)`));
|
|
278
|
+
}
|
|
228
279
|
if (r?.openPrs.length)
|
|
229
280
|
ships.push(`${r.openPrs.length} open PR${r.openPrs.length === 1 ? '' : 's'}`);
|
|
230
281
|
if (r?.worktrees)
|
|
@@ -236,8 +287,8 @@ function renderCard(def, r, remote, fleet, linear, nowMs = Date.now()) {
|
|
|
236
287
|
if (linear) {
|
|
237
288
|
console.log(` ${chalk.dim('linear')} ${linear.done}/${linear.total}${linear.truncated ? '+' : ''} done · ${linear.inProgress} in progress`);
|
|
238
289
|
}
|
|
239
|
-
|
|
240
|
-
console.log(
|
|
290
|
+
for (const line of formatMilestoneLines(linear?.milestones ?? [], linear?.nextMilestone, nowMs, milestoneLimit)) {
|
|
291
|
+
console.log(line);
|
|
241
292
|
}
|
|
242
293
|
if (r && r.tickets.length) {
|
|
243
294
|
console.log(` ${chalk.dim('tickets')} ${r.tickets.slice(0, 8).join(' · ')}${r.tickets.length > 8 ? ' …' : ''}`);
|
|
@@ -258,6 +309,14 @@ function renderCard(def, r, remote, fleet, linear, nowMs = Date.now()) {
|
|
|
258
309
|
const repos = [def.repo, ...(def.repos ?? []).map((x) => x.slug)].filter(Boolean);
|
|
259
310
|
if (repos.length)
|
|
260
311
|
console.log(` ${chalk.dim('repos')} ${[...new Set(repos)].join(' · ')}`);
|
|
312
|
+
// A def whose `repo` disagrees with the checkout's origin reads PR and release
|
|
313
|
+
// counts from a DIFFERENT repository — both slugs resolve, so nothing errors
|
|
314
|
+
// and the wrong numbers look right. Say it here, with the fix attached.
|
|
315
|
+
const mismatch = def.root ? checkRepoSlug(def, originSlug(expandLocalHome(def.root))) : undefined;
|
|
316
|
+
if (mismatch) {
|
|
317
|
+
console.log(` ${chalk.yellow('!')} ${chalk.yellow(mismatch.message)}`);
|
|
318
|
+
console.log(` ${' '.repeat(8)}${chalk.gray(mismatch.remediation)}`);
|
|
319
|
+
}
|
|
261
320
|
if (def.contexts?.length) {
|
|
262
321
|
console.log(` ${chalk.dim('context')} ${def.contexts.map((c) => c.path).join(' · ')}`);
|
|
263
322
|
}
|
|
@@ -381,17 +440,24 @@ export function registerProjectsCommands(program) {
|
|
|
381
440
|
});
|
|
382
441
|
// ---- show ----
|
|
383
442
|
projects
|
|
384
|
-
.command('
|
|
385
|
-
.
|
|
443
|
+
.command('view <name>')
|
|
444
|
+
.alias('show')
|
|
445
|
+
.description('Show a project definition, resolved paths, repos, contexts, links, and milestones.')
|
|
386
446
|
.option('--json', 'Machine-readable output')
|
|
387
|
-
.
|
|
447
|
+
.option('--no-remote', 'Skip the Linear lookup; definition only')
|
|
448
|
+
.action(async (name, opts) => {
|
|
388
449
|
const def = loadProjectDef(name);
|
|
389
450
|
if (!def) {
|
|
390
451
|
console.error(chalk.red(`No project named "${name}". List them: agents projects list`));
|
|
391
452
|
process.exit(1);
|
|
392
453
|
}
|
|
454
|
+
// Every milestone the project declares, not just the next one — the whole
|
|
455
|
+
// point of opening ONE project is to see the shape of its plan.
|
|
456
|
+
const counts = opts.remote !== false && def.linear?.projectId
|
|
457
|
+
? await fetchLinearProjectCounts(def.linear.projectId)
|
|
458
|
+
: undefined;
|
|
393
459
|
if (opts.json) {
|
|
394
|
-
console.log(JSON.stringify(def, null, 2));
|
|
460
|
+
console.log(JSON.stringify({ ...def, linear: { ...def.linear, ...(counts ?? {}) } }, null, 2));
|
|
395
461
|
return;
|
|
396
462
|
}
|
|
397
463
|
console.log(chalk.bold(def.name) + (def.description ? chalk.dim(` — ${def.description}`) : ''));
|
|
@@ -410,6 +476,24 @@ export function registerProjectsCommands(program) {
|
|
|
410
476
|
console.log(` linear ${def.linear.url ?? def.linear.projectId}`);
|
|
411
477
|
for (const d of def.docs ?? [])
|
|
412
478
|
console.log(` doc ${d}`);
|
|
479
|
+
if (counts) {
|
|
480
|
+
const staleNote = counts.stale ? chalk.dim(' (cached)') : '';
|
|
481
|
+
console.log(` issues ${counts.done}/${counts.total}${counts.truncated ? '+' : ''} done · ${counts.inProgress} in progress${staleNote}`);
|
|
482
|
+
}
|
|
483
|
+
const ms = counts?.milestones ?? [];
|
|
484
|
+
if (ms.length) {
|
|
485
|
+
console.log(` ${chalk.bold('milestones')}`);
|
|
486
|
+
for (const m of ms) {
|
|
487
|
+
const mark = m.name === counts?.nextMilestone?.name ? chalk.cyan('next') : '';
|
|
488
|
+
console.log(` ${mark.padEnd(6)} ${formatNextMilestone(m, Date.now())}`);
|
|
489
|
+
}
|
|
490
|
+
// A milestone nothing is filed against cannot report progress. Say that
|
|
491
|
+
// once, plainly, rather than printing a row of silent 0%s.
|
|
492
|
+
const unfiled = ms.filter((m) => m.total === 0).length;
|
|
493
|
+
if (unfiled === ms.length) {
|
|
494
|
+
console.log(` ${chalk.yellow('!')} ${chalk.yellow(`no issues are assigned to any milestone — progress against them cannot be measured`)}`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
413
497
|
console.log(chalk.gray(` ${projectDefPath(name)}`));
|
|
414
498
|
});
|
|
415
499
|
// ---- edit ----
|
|
@@ -514,7 +598,8 @@ export function registerProjectsCommands(program) {
|
|
|
514
598
|
byStatus: r?.byStatus ?? {},
|
|
515
599
|
members: r?.members ?? [],
|
|
516
600
|
plan: r?.plan ?? { done: 0, total: 0 },
|
|
517
|
-
|
|
601
|
+
live: r ? liveDeadSplit(r.byStatus).live : 0,
|
|
602
|
+
dead: r ? liveDeadSplit(r.byStatus).dead : 0,
|
|
518
603
|
openPrs: r?.openPrs ?? [],
|
|
519
604
|
mergedPrs: rem?.mergedPrs ?? 0,
|
|
520
605
|
latestRelease: rem?.latestRelease ?? null,
|
|
@@ -577,6 +662,53 @@ export function registerProjectsCommands(program) {
|
|
|
577
662
|
console.log(chalk.gray(' (widen with --min-confidence medium or --all)'));
|
|
578
663
|
}
|
|
579
664
|
});
|
|
665
|
+
// ---- set ----
|
|
666
|
+
projects
|
|
667
|
+
.command('set <name>')
|
|
668
|
+
.description('Change one field on a project definition, preserving everything else.')
|
|
669
|
+
.option('--repo <owner/repo>', 'Primary GitHub slug')
|
|
670
|
+
.option('--root <path>', 'Repo / monorepo root')
|
|
671
|
+
.option('--path <subdir>', 'Default cwd for agents (a monorepo subdir)')
|
|
672
|
+
.option('--description <text>', 'One-line description shown on the card')
|
|
673
|
+
.action((name, opts) => {
|
|
674
|
+
const def = loadProjectDef(name);
|
|
675
|
+
if (!def) {
|
|
676
|
+
console.error(chalk.red(`No project named "${name}". List them: agents projects list`));
|
|
677
|
+
process.exit(1);
|
|
678
|
+
}
|
|
679
|
+
const fields = ['repo', 'root', 'path', 'description'].filter((k) => opts[k] !== undefined);
|
|
680
|
+
if (fields.length === 0) {
|
|
681
|
+
console.error(chalk.red('Nothing to set. Pass a field, e.g. --repo <owner/repo>.'));
|
|
682
|
+
process.exit(1);
|
|
683
|
+
}
|
|
684
|
+
// Load, mutate the named fields, write back — the `link` pattern. NEVER
|
|
685
|
+
// the `add --force` pattern, which rebuilds the def from flags alone and
|
|
686
|
+
// silently drops linear/contexts/integrations that were not re-passed.
|
|
687
|
+
if (opts.repo !== undefined)
|
|
688
|
+
def.repo = opts.repo;
|
|
689
|
+
if (opts.root !== undefined)
|
|
690
|
+
def.root = opts.root;
|
|
691
|
+
if (opts.description !== undefined)
|
|
692
|
+
def.description = opts.description;
|
|
693
|
+
if (opts.path !== undefined) {
|
|
694
|
+
// `--path` is a subdir OF the root, so without one there is nothing to
|
|
695
|
+
// hang it off. A def imported with `--from-linear` that found no local
|
|
696
|
+
// checkout carries name + linear and no root — joining against '' there
|
|
697
|
+
// would silently write `/apps/cli`, an absolute path at the filesystem
|
|
698
|
+
// root, and the def would resolve somewhere that does not exist.
|
|
699
|
+
const base = (opts.root ?? def.root ?? '').replace(/\/$/, '');
|
|
700
|
+
if (!base) {
|
|
701
|
+
console.error(chalk.red(`"${def.name}" has no root, so --path has nothing to resolve against.`));
|
|
702
|
+
console.error(chalk.gray(` Set one first: agents projects set ${def.name} --root <path> --path ${opts.path}`));
|
|
703
|
+
process.exit(1);
|
|
704
|
+
}
|
|
705
|
+
def.defaultPath = `${base}/${opts.path.replace(/^\//, '')}`;
|
|
706
|
+
}
|
|
707
|
+
writeProjectDef(def);
|
|
708
|
+
console.log(chalk.green(`Updated ${def.name}`));
|
|
709
|
+
for (const f of fields)
|
|
710
|
+
console.log(chalk.gray(` ${f} ${f === 'path' ? def.defaultPath : def[f]}`));
|
|
711
|
+
});
|
|
580
712
|
// ---- link ----
|
|
581
713
|
projects
|
|
582
714
|
.command('link <name>')
|
|
@@ -14,7 +14,7 @@ import { isDaemonRunning, signalDaemonReload, startDaemon, stopDaemon, readDaemo
|
|
|
14
14
|
import { assertSchedulerEnabled } from '../lib/device-config.js';
|
|
15
15
|
import { resolveAgentName, isAgentHardDeprecated, hardDeprecationError } from '../lib/agents.js';
|
|
16
16
|
import { humanizeCron, humanizeNextRun, formatRepoLink, REPO_DISPLAY_MAX } from '../lib/routines-format.js';
|
|
17
|
-
import { listJobs as listAllJobs, deleteJob, readJob, validateJob, writeJob, setJobEnabled, listRuns, getLatestRun, getRunDir, getJobPath, parseAtTime, hasCompletedOneShotRun, isOneShotLikeSchedule, isOneShotRoutine, isPastOneShotRoutine, jobRunsOnThisDevice, checkJobDeviceEligibility, normalizeTriggerEvent, parseHostStrategy, resolveHostStrategy, placementRequiresFiringPin, HOST_STRATEGIES, } from '../lib/routines.js';
|
|
17
|
+
import { listJobs as listAllJobs, deleteJob, readJob, validateJob, writeJob, setJobEnabled, listRuns, routineStats, getLatestRun, getRunDir, getJobPath, parseAtTime, hasCompletedOneShotRun, isOneShotLikeSchedule, isOneShotRoutine, isPastOneShotRoutine, jobRunsOnThisDevice, checkJobDeviceEligibility, normalizeTriggerEvent, parseHostStrategy, resolveHostStrategy, placementRequiresFiringPin, HOST_STRATEGIES, } from '../lib/routines.js';
|
|
18
18
|
import { discoverProjectRoutinesAt, enableProjectRoutines, disableProjectRoutines, syncProjectRoutines, syncAllProjectRoutines, listEnabledProjectRoots, resolveProjectRoot, displayProjectPath, listProjectRoutineFiles, } from '../lib/routines-project.js';
|
|
19
19
|
import { fireWebhookJobs, matchJobsToWebhook } from '../lib/triggers/webhook.js';
|
|
20
20
|
import { getRoutinesDir } from '../lib/state.js';
|
|
@@ -359,6 +359,7 @@ function runMetaJson(run) {
|
|
|
359
359
|
completedAt: run.completedAt,
|
|
360
360
|
exitCode: run.exitCode,
|
|
361
361
|
errorMessage: run.errorMessage ?? null,
|
|
362
|
+
duration: run.duration ?? null,
|
|
362
363
|
};
|
|
363
364
|
}
|
|
364
365
|
export function buildRunsJson(runs) {
|
|
@@ -1034,6 +1035,50 @@ export function registerRoutinesCommands(program) {
|
|
|
1034
1035
|
console.log(` ${run.runId} ${status} ${run.startedAt}`);
|
|
1035
1036
|
}
|
|
1036
1037
|
});
|
|
1038
|
+
routinesCmd
|
|
1039
|
+
.command('stats [name]')
|
|
1040
|
+
.description('Duration + outcome rollup per job: run count, failed, missed, avg/p50/p95 duration')
|
|
1041
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
1042
|
+
.action(async (name, options) => {
|
|
1043
|
+
// No name: summarize every job (like `routines list`). A name narrows to
|
|
1044
|
+
// one job's rollup — no interactive picker, since a bare `stats` already
|
|
1045
|
+
// has a useful all-jobs default.
|
|
1046
|
+
if (!name) {
|
|
1047
|
+
const jobs = listAllJobs();
|
|
1048
|
+
const rows = jobs.map((j) => ({ name: j.name, ...routineStats(j.name) }));
|
|
1049
|
+
if (options.json) {
|
|
1050
|
+
writeJson({ jobs: rows });
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
if (rows.length === 0) {
|
|
1054
|
+
console.log(chalk.gray('No jobs configured'));
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
console.log(chalk.bold('Routine stats\n'));
|
|
1058
|
+
const pad = (s, w) => (s.length >= w ? s.slice(0, w) : s + ' '.repeat(w - s.length));
|
|
1059
|
+
console.log(chalk.gray(` ${pad('JOB', 28)} ${pad('N', 5)} ${pad('FAILED', 7)} ${pad('MISSED', 7)} ${pad('AVG', 7)} ${pad('P50', 7)} P95`));
|
|
1060
|
+
for (const r of rows) {
|
|
1061
|
+
console.log(` ${pad(r.name, 28)} ${pad(String(r.count), 5)} ${pad(String(r.failed), 7)} ${pad(String(r.missed), 7)} ${pad(`${r.avgMs}ms`, 7)} ${pad(`${r.p50}ms`, 7)} ${r.p95}ms`);
|
|
1062
|
+
}
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
const stats = routineStats(name);
|
|
1066
|
+
if (options.json) {
|
|
1067
|
+
writeJson({ jobId: name, name, ...stats });
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
if (stats.count === 0) {
|
|
1071
|
+
console.log(chalk.yellow(`No runs found for job '${name}'`));
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
console.log(chalk.bold(`Stats: ${name}\n`));
|
|
1075
|
+
console.log(` Runs: ${stats.count}`);
|
|
1076
|
+
console.log(` Failed: ${stats.failed}`);
|
|
1077
|
+
console.log(` Missed: ${stats.missed}`);
|
|
1078
|
+
console.log(` Avg: ${stats.avgMs}ms`);
|
|
1079
|
+
console.log(` P50: ${stats.p50}ms`);
|
|
1080
|
+
console.log(` P95: ${stats.p95}ms`);
|
|
1081
|
+
});
|
|
1037
1082
|
routinesCmd
|
|
1038
1083
|
.command('run [name]')
|
|
1039
1084
|
.description('Execute a routine right now in the foreground. Ignores the schedule; useful for testing before enabling.')
|