@phnx-labs/agents-cli 1.20.92 → 1.21.0
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 +385 -0
- package/README.md +3 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/beta.js +3 -5
- package/dist/commands/doctor.js +23 -0
- package/dist/commands/events.js +91 -1
- package/dist/commands/exec.js +13 -0
- package/dist/commands/mailboxes.js +39 -1
- package/dist/commands/message.js +12 -1
- package/dist/commands/packages.js +14 -5
- package/dist/commands/projects.d.ts +38 -0
- package/dist/commands/projects.js +355 -48
- package/dist/commands/routines.js +27 -1
- package/dist/commands/rules.js +6 -0
- package/dist/commands/secrets.d.ts +17 -0
- package/dist/commands/secrets.js +198 -7
- package/dist/commands/send.d.ts +14 -12
- package/dist/commands/send.js +105 -35
- package/dist/commands/sessions.d.ts +17 -0
- package/dist/commands/sessions.js +16 -8
- package/dist/commands/setup-browser.js +5 -1
- package/dist/commands/setup-fleet.js +5 -0
- package/dist/commands/setup-preferences.d.ts +53 -0
- package/dist/commands/setup-preferences.js +142 -0
- package/dist/commands/setup.js +8 -9
- package/dist/commands/ssh.js +220 -10
- package/dist/commands/sync.d.ts +2 -2
- package/dist/commands/sync.js +13 -12
- package/dist/commands/view.js +4 -0
- package/dist/commands/watchdog.js +26 -3
- package/dist/index.js +38 -6
- package/dist/lib/activity.d.ts +15 -0
- package/dist/lib/activity.js +26 -2
- package/dist/lib/auto-dispatch.d.ts +6 -1
- package/dist/lib/auto-dispatch.js +7 -2
- package/dist/lib/beta.d.ts +1 -1
- package/dist/lib/beta.js +1 -1
- package/dist/lib/channels/send.d.ts +83 -0
- package/dist/lib/channels/send.js +112 -0
- package/dist/lib/daemon.d.ts +13 -0
- package/dist/lib/daemon.js +80 -60
- package/dist/lib/device-config.d.ts +82 -0
- package/dist/lib/device-config.js +296 -0
- package/dist/lib/event-provenance.d.ts +19 -0
- package/dist/lib/event-provenance.js +48 -0
- package/dist/lib/events-ingest.d.ts +46 -0
- package/dist/lib/events-ingest.js +182 -0
- package/dist/lib/events.d.ts +17 -5
- package/dist/lib/events.js +59 -56
- package/dist/lib/feed.d.ts +1 -1
- package/dist/lib/feed.js +2 -0
- package/dist/lib/git.d.ts +13 -2
- package/dist/lib/git.js +38 -6
- package/dist/lib/linear-project-counts.d.ts +129 -0
- package/dist/lib/linear-project-counts.js +206 -0
- package/dist/lib/linear-projects.d.ts +62 -0
- package/dist/lib/linear-projects.js +144 -0
- package/dist/lib/mailbox-gc.js +30 -7
- package/dist/lib/mailbox.d.ts +14 -1
- package/dist/lib/mailbox.js +35 -3
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +5 -21
- package/dist/lib/menubar/install-menubar.js +35 -44
- package/dist/lib/menubar/notify-desktop.d.ts +17 -2
- package/dist/lib/menubar/notify-desktop.js +8 -2
- package/dist/lib/migrate.js +1 -1
- package/dist/lib/models.js +21 -11
- package/dist/lib/overdue.d.ts +1 -1
- package/dist/lib/overdue.js +54 -19
- package/dist/lib/project-import.d.ts +96 -0
- package/dist/lib/project-import.js +171 -0
- package/dist/lib/project-probe.d.ts +75 -0
- package/dist/lib/project-probe.js +160 -0
- package/dist/lib/project-resources.d.ts +8 -0
- package/dist/lib/project-resources.js +31 -3
- package/dist/lib/project-status.d.ts +32 -1
- package/dist/lib/project-status.js +82 -1
- package/dist/lib/projects.d.ts +6 -0
- package/dist/lib/projects.js +12 -0
- package/dist/lib/remote-agents-json.d.ts +14 -1
- package/dist/lib/remote-agents-json.js +21 -3
- package/dist/lib/routine-notify.d.ts +11 -0
- package/dist/lib/routine-notify.js +22 -0
- package/dist/lib/routines.d.ts +53 -0
- package/dist/lib/routines.js +105 -4
- package/dist/lib/rules/run-sync.d.ts +18 -0
- package/dist/lib/rules/run-sync.js +92 -0
- package/dist/lib/run-notify.js +3 -0
- package/dist/lib/scheduler.js +12 -2
- 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/audit.d.ts +1 -1
- package/dist/lib/secrets/audit.js +53 -10
- package/dist/lib/secrets/index.d.ts +4 -5
- package/dist/lib/secrets/index.js +4 -5
- package/dist/lib/secrets/list-filter.d.ts +20 -5
- package/dist/lib/secrets/list-filter.js +22 -6
- package/dist/lib/secrets/usage-db.d.ts +106 -0
- package/dist/lib/secrets/usage-db.js +236 -0
- package/dist/lib/session/active.d.ts +16 -2
- package/dist/lib/session/active.js +8 -4
- package/dist/lib/session/bundle.js +5 -1
- package/dist/lib/session/remote-active.d.ts +5 -1
- package/dist/lib/session/remote-active.js +4 -1
- package/dist/lib/session/remote-list.d.ts +1 -15
- package/dist/lib/session/remote-list.js +22 -111
- package/dist/lib/session/sync/config.d.ts +13 -5
- package/dist/lib/session/sync/config.js +21 -13
- package/dist/lib/sqlite.js +28 -1
- package/dist/lib/startup/command-registry.d.ts +0 -2
- package/dist/lib/startup/command-registry.js +1 -5
- package/dist/lib/state.d.ts +19 -2
- package/dist/lib/state.js +99 -10
- package/dist/lib/sync-umbrella.d.ts +11 -23
- package/dist/lib/sync-umbrella.js +14 -45
- package/dist/lib/teams/agents.d.ts +35 -3
- package/dist/lib/teams/agents.js +52 -8
- package/dist/lib/teams/scheduler.d.ts +29 -2
- package/dist/lib/teams/scheduler.js +60 -16
- package/dist/lib/types.d.ts +22 -5
- package/dist/lib/versions.d.ts +6 -0
- package/dist/lib/versions.js +6 -4
- package/package.json +1 -2
- package/dist/commands/drive.d.ts +0 -10
- package/dist/commands/drive.js +0 -183
- package/dist/commands/hq.d.ts +0 -2
- package/dist/commands/hq.js +0 -58
- package/dist/commands/sessions-sync.d.ts +0 -17
- package/dist/commands/sessions-sync.js +0 -135
- package/dist/commands/sync-provision.d.ts +0 -23
- package/dist/commands/sync-provision.js +0 -107
- package/dist/lib/drive-sync.d.ts +0 -45
- package/dist/lib/drive-sync.js +0 -238
- package/dist/lib/hq/floor.d.ts +0 -87
- package/dist/lib/hq/floor.js +0 -243
- package/dist/lib/session/sync/crdt.d.ts +0 -44
- package/dist/lib/session/sync/crdt.js +0 -119
- package/dist/lib/session/sync/manifest.d.ts +0 -62
- package/dist/lib/session/sync/manifest.js +0 -100
- package/dist/lib/session/sync/provision.d.ts +0 -49
- package/dist/lib/session/sync/provision.js +0 -91
- package/dist/lib/session/sync/r2.d.ts +0 -32
- package/dist/lib/session/sync/r2.js +0 -121
- package/dist/lib/session/sync/sync.d.ts +0 -106
- package/dist/lib/session/sync/sync.js +0 -374
|
@@ -10,16 +10,45 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import chalk from 'chalk';
|
|
12
12
|
import * as fs from 'fs';
|
|
13
|
+
import * as path from 'path';
|
|
13
14
|
import { execFileSync, spawnSync } from 'child_process';
|
|
14
15
|
import { betaEnableHint, isBetaEnabled } from '../lib/beta.js';
|
|
15
16
|
import { setHelpSections } from '../lib/help.js';
|
|
16
17
|
import { getMainRepoRoot } from '../lib/git.js';
|
|
17
18
|
import { parseOwnerRepoFromRemote } from '../lib/registry.js';
|
|
18
|
-
import {
|
|
19
|
+
import { truncate } from '../lib/format.js';
|
|
20
|
+
import { expandLocalHome, getProjectRoot, toHomeRelative } from '../lib/project-root.js';
|
|
21
|
+
import { machineId } from '../lib/machine-id.js';
|
|
19
22
|
import { getActiveSessions } from '../lib/session/active.js';
|
|
23
|
+
import { gatherRemoteActive } from '../lib/session/remote-active.js';
|
|
24
|
+
import { gatherRemoteAgentsJson } from '../lib/remote-agents-json.js';
|
|
20
25
|
import { factoryProjectsPath } from '../lib/auto-dispatch.js';
|
|
26
|
+
import { formatFleetWorkspaces, parseRemoteProbe, probeProjectWorkspaces, workspaceTargetsForDef, } from '../lib/project-probe.js';
|
|
21
27
|
import { listProjectDefs, loadProjectDef, writeProjectDef, removeProjectDef, projectDefPath, isSafeProjectName, } from '../lib/projects.js';
|
|
22
|
-
import { rollupSessionsByProject, planPct, enrichProjectSignals, } from '../lib/project-status.js';
|
|
28
|
+
import { rollupSessionsByProject, planPct, enrichProjectSignals, formatProjectMembers, } from '../lib/project-status.js';
|
|
29
|
+
import { fetchLinearProjectCounts } from '../lib/linear-project-counts.js';
|
|
30
|
+
import { listLinearProjects, pickLinearProject } from '../lib/linear-projects.js';
|
|
31
|
+
import { buildFactoryImportCandidates, buildLinearImportCandidates, validateImportOpts, } from '../lib/project-import.js';
|
|
32
|
+
/** Recursion guard: a peer answering a probe fan-out never re-fans-out itself. */
|
|
33
|
+
export const PROJECTS_NO_FANOUT_ENV = 'AGENTS_PROJECTS_LOCAL';
|
|
34
|
+
/** Max peers named in the skipped note before the rest collapse to `+N`. */
|
|
35
|
+
const SKIPPED_NAME_LIMIT = 4;
|
|
36
|
+
/**
|
|
37
|
+
* One compact trailing note for peers that didn't answer the `--fleet`
|
|
38
|
+
* fan-out — unreachable, running an agents-cli too old to carry `projects
|
|
39
|
+
* probe`, or too slow to finish inside the 12s SSH budget. Mirrors
|
|
40
|
+
* `formatUnreachableNote` (activity) with the probe-specific reasons; empty
|
|
41
|
+
* string when everything answered.
|
|
42
|
+
*/
|
|
43
|
+
export function formatFleetSkippedNote(skipped) {
|
|
44
|
+
if (skipped.length === 0)
|
|
45
|
+
return '';
|
|
46
|
+
const named = skipped.slice(0, SKIPPED_NAME_LIMIT);
|
|
47
|
+
const rest = skipped.length - named.length;
|
|
48
|
+
const list = rest > 0 ? `${named.join(', ')} +${rest}` : named.join(', ');
|
|
49
|
+
const noun = skipped.length === 1 ? 'device' : 'devices';
|
|
50
|
+
return chalk.gray(` · ${skipped.length} ${noun} didn't answer (unreachable, older agents-cli, or timed out): ${list}\n`);
|
|
51
|
+
}
|
|
23
52
|
/** `path:purpose` → a context anchor. Purpose may contain colons. */
|
|
24
53
|
function parseContextFlag(raw) {
|
|
25
54
|
const i = raw.indexOf(':');
|
|
@@ -37,6 +66,135 @@ function originSlug(cwd) {
|
|
|
37
66
|
return undefined;
|
|
38
67
|
}
|
|
39
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Read the Factory registry off disk and plan its import. Every read failure is
|
|
71
|
+
* fatal and named — an unreadable or malformed registry must not read as "0
|
|
72
|
+
* projects to import".
|
|
73
|
+
*/
|
|
74
|
+
function runFactoryImport(existing, opts) {
|
|
75
|
+
const src = factoryProjectsPath();
|
|
76
|
+
let rawText;
|
|
77
|
+
try {
|
|
78
|
+
rawText = fs.readFileSync(src, 'utf8');
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
console.error(chalk.red(`No Factory registry at ${src}`));
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
let parsed;
|
|
85
|
+
try {
|
|
86
|
+
parsed = JSON.parse(rawText);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
console.error(chalk.red(`Factory registry at ${src} is not valid JSON`));
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
const rows = Array.isArray(parsed)
|
|
93
|
+
? parsed
|
|
94
|
+
: Array.isArray(parsed?.projects)
|
|
95
|
+
? parsed.projects
|
|
96
|
+
: [];
|
|
97
|
+
return buildFactoryImportCandidates(rows, existing, opts);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* List the workspace's Linear projects and plan their import, binding each to a
|
|
101
|
+
* local checkout under the configured projects root when the names match
|
|
102
|
+
* exactly. A missing / logged-out `linear` CLI is loud (this is an explicit user
|
|
103
|
+
* command), and nothing is written before the whole plan is built.
|
|
104
|
+
*/
|
|
105
|
+
function runLinearImport(existing, opts) {
|
|
106
|
+
let list;
|
|
107
|
+
try {
|
|
108
|
+
list = listLinearProjects();
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
console.error(chalk.red(e instanceof Error ? e.message : String(e)));
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
// No projects root configured (or unreadable) → no local matching; every def
|
|
115
|
+
// still imports, carrying its Linear link and nothing it can't prove.
|
|
116
|
+
const rootAbs = getProjectRoot() ? expandLocalHome(getProjectRoot()) : undefined;
|
|
117
|
+
let localDirs = [];
|
|
118
|
+
if (rootAbs) {
|
|
119
|
+
try {
|
|
120
|
+
localDirs = fs
|
|
121
|
+
.readdirSync(rootAbs, { withFileTypes: true })
|
|
122
|
+
.filter((d) => d.isDirectory() && !d.name.startsWith('.'))
|
|
123
|
+
.map((d) => d.name);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
localDirs = [];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return buildLinearImportCandidates(list, existing, {
|
|
130
|
+
localDirs,
|
|
131
|
+
resolveRoot: (dir) => (rootAbs ? toHomeRelative(path.join(rootAbs, dir)) : undefined),
|
|
132
|
+
resolveOrigin: (dir) => (rootAbs ? originSlug(path.join(rootAbs, dir)) : undefined),
|
|
133
|
+
}, opts);
|
|
134
|
+
}
|
|
135
|
+
/** Longest path a `list` row shows before it truncates. */
|
|
136
|
+
const LIST_PATH_MAX = 48;
|
|
137
|
+
/**
|
|
138
|
+
* Column widths for `projects list`, sized to the rows actually being printed.
|
|
139
|
+
* Fixed padding was the bug: home-relative roots run past 50 characters, so a
|
|
140
|
+
* hardcoded 32 pushed the repo column off its gridline on every long path.
|
|
141
|
+
* Paths longer than {@link LIST_PATH_MAX} truncate rather than widen the table.
|
|
142
|
+
*/
|
|
143
|
+
export function computeProjectListWidths(rows) {
|
|
144
|
+
const widest = (pick, cap) => Math.min(cap, rows.reduce((w, r) => Math.max(w, pick(r).length), 0));
|
|
145
|
+
return {
|
|
146
|
+
name: widest((r) => r.name, 64),
|
|
147
|
+
path: widest((r) => r.path, LIST_PATH_MAX),
|
|
148
|
+
repo: widest((r) => r.repo, 64),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* A milestone's target date as a person would say it — "due tomorrow",
|
|
153
|
+
* "overdue by 3 days", "due Aug 21" — never a raw `2026-08-21` or a duration in
|
|
154
|
+
* hours. Linear stores a calendar date with no timezone, so both sides are
|
|
155
|
+
* compared at LOCAL midnight; parsing `YYYY-MM-DD` with `new Date(str)` would
|
|
156
|
+
* read it as UTC and shift the answer by a day for anyone west of Greenwich.
|
|
157
|
+
*/
|
|
158
|
+
export function formatMilestoneDue(targetDate, nowMs) {
|
|
159
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(targetDate.trim());
|
|
160
|
+
if (!m)
|
|
161
|
+
return undefined;
|
|
162
|
+
const due = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
|
163
|
+
if (Number.isNaN(due.getTime()))
|
|
164
|
+
return undefined;
|
|
165
|
+
const now = new Date(nowMs);
|
|
166
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
167
|
+
const days = Math.round((due.getTime() - today.getTime()) / 86_400_000);
|
|
168
|
+
if (days === 0)
|
|
169
|
+
return 'due today';
|
|
170
|
+
if (days === 1)
|
|
171
|
+
return 'due tomorrow';
|
|
172
|
+
if (days === -1)
|
|
173
|
+
return 'overdue by a day';
|
|
174
|
+
if (days < 0)
|
|
175
|
+
return `overdue by ${-days} days`;
|
|
176
|
+
if (days <= 14)
|
|
177
|
+
return `due in ${days} days`;
|
|
178
|
+
const sameYear = due.getFullYear() === today.getFullYear();
|
|
179
|
+
const label = due.toLocaleDateString('en-US', {
|
|
180
|
+
month: 'short',
|
|
181
|
+
day: 'numeric',
|
|
182
|
+
...(sameYear ? {} : { year: 'numeric' }),
|
|
183
|
+
});
|
|
184
|
+
return `due ${label}`;
|
|
185
|
+
}
|
|
186
|
+
/** The `next` card line: what this project is due to hit, and how far along it is. */
|
|
187
|
+
export function formatNextMilestone(ms, nowMs) {
|
|
188
|
+
const parts = [chalk.bold(ms.name)];
|
|
189
|
+
// A milestone with nothing filed under it yet has no progress to report —
|
|
190
|
+
// `0/0` is noise, not information.
|
|
191
|
+
if (ms.total > 0)
|
|
192
|
+
parts.push(`${ms.done}/${ms.total}`);
|
|
193
|
+
const due = ms.targetDate ? formatMilestoneDue(ms.targetDate, nowMs) : undefined;
|
|
194
|
+
if (due)
|
|
195
|
+
parts.push(due.startsWith('overdue') ? chalk.yellow(due) : chalk.dim(due));
|
|
196
|
+
return parts.join(chalk.dim(' · '));
|
|
197
|
+
}
|
|
40
198
|
function statusBar(r) {
|
|
41
199
|
const parts = [];
|
|
42
200
|
const push = (n, label, color) => {
|
|
@@ -54,7 +212,7 @@ function statusBar(r) {
|
|
|
54
212
|
parts.push(chalk.gray(`+${r.agents - shown} other`));
|
|
55
213
|
return parts.join(' · ') || chalk.gray('no live agents');
|
|
56
214
|
}
|
|
57
|
-
function renderCard(def, r, remote) {
|
|
215
|
+
function renderCard(def, r, remote, fleet, linear, nowMs = Date.now()) {
|
|
58
216
|
const agents = r?.agents ?? 0;
|
|
59
217
|
const pct = r ? planPct(r.plan) : undefined;
|
|
60
218
|
const planStr = pct === undefined ? '' : ` · ${chalk.cyan(`${pct}% plan`)}`;
|
|
@@ -62,6 +220,8 @@ function renderCard(def, r, remote) {
|
|
|
62
220
|
if (def.description)
|
|
63
221
|
console.log(` ${chalk.dim(def.description)}`);
|
|
64
222
|
console.log(` ${chalk.dim('live')} ${r ? statusBar(r) : chalk.gray('no live agents')}`);
|
|
223
|
+
if (r && r.members.length)
|
|
224
|
+
console.log(` ${chalk.dim('agents')} ${formatProjectMembers(r.members)}`);
|
|
65
225
|
const ships = [];
|
|
66
226
|
if (remote?.mergedPrs)
|
|
67
227
|
ships.push(chalk.green(`${remote.mergedPrs} merged (${remote.windowDays}d)`));
|
|
@@ -69,11 +229,28 @@ function renderCard(def, r, remote) {
|
|
|
69
229
|
ships.push(`${r.openPrs.length} open PR${r.openPrs.length === 1 ? '' : 's'}`);
|
|
70
230
|
if (r?.worktrees)
|
|
71
231
|
ships.push(`${r.worktrees} worktree${r.worktrees === 1 ? '' : 's'}`);
|
|
232
|
+
if (remote?.latestRelease)
|
|
233
|
+
ships.push(remote.latestRelease.tag);
|
|
72
234
|
if (ships.length)
|
|
73
235
|
console.log(` ${chalk.dim('ships')} ${ships.join(' · ')}`);
|
|
236
|
+
if (linear) {
|
|
237
|
+
console.log(` ${chalk.dim('linear')} ${linear.done}/${linear.total}${linear.truncated ? '+' : ''} done · ${linear.inProgress} in progress`);
|
|
238
|
+
}
|
|
239
|
+
if (linear?.nextMilestone) {
|
|
240
|
+
console.log(` ${chalk.dim('next')} ${formatNextMilestone(linear.nextMilestone, nowMs)}`);
|
|
241
|
+
}
|
|
74
242
|
if (r && r.tickets.length) {
|
|
75
243
|
console.log(` ${chalk.dim('tickets')} ${r.tickets.slice(0, 8).join(' · ')}${r.tickets.length > 8 ? ' …' : ''}`);
|
|
76
244
|
}
|
|
245
|
+
if (fleet) {
|
|
246
|
+
const lines = formatFleetWorkspaces(fleet);
|
|
247
|
+
if (lines.length === 0) {
|
|
248
|
+
console.log(` ${chalk.dim('fleet')} ${chalk.gray('no workspace paths (set root or repos[].path)')}`);
|
|
249
|
+
}
|
|
250
|
+
lines.forEach((line, i) => {
|
|
251
|
+
console.log(` ${chalk.dim((i === 0 ? 'fleet' : '').padEnd(5))} ${line}`);
|
|
252
|
+
});
|
|
253
|
+
}
|
|
77
254
|
if (remote?.artifacts) {
|
|
78
255
|
const last = remote.lastArtifact ? ` ${chalk.dim(`· last: ${remote.lastArtifact}`)}` : '';
|
|
79
256
|
console.log(` ${chalk.dim('proof')} ${remote.artifacts} artifact${remote.artifacts === 1 ? '' : 's'} (${remote.windowDays}d)${last}`);
|
|
@@ -96,20 +273,33 @@ export function registerProjectsCommands(program) {
|
|
|
96
273
|
.description('Named multi-repo projects with a progress rollup.');
|
|
97
274
|
setHelpSections(projects, {
|
|
98
275
|
examples: `
|
|
276
|
+
agents projects import --from-linear # the projects you actually track
|
|
99
277
|
agents projects add rush --repo phnx-labs/rush --path apps/web
|
|
100
278
|
agents projects list
|
|
101
279
|
agents projects status # progress card for every project
|
|
102
280
|
agents projects status rush --json # one project, machine-readable
|
|
281
|
+
agents projects status --fleet # + per-device workspace drift over SSH
|
|
282
|
+
agents projects link rush --linear # bind the Linear project (auto-suggest)
|
|
103
283
|
agents run --project rush # land an agent in the project
|
|
104
284
|
`,
|
|
105
285
|
notes: `
|
|
106
286
|
Definitions are hand-editable YAML in ~/.agents/projects/ and sync across
|
|
107
287
|
machines with 'agents push/pull'. Enable with: agents beta enable projects.
|
|
288
|
+
|
|
289
|
+
'import --from-factory' reads Factory's auto-detected registry, which
|
|
290
|
+
guesses from checkouts on disk — it imports only 'high' confidence rows by
|
|
291
|
+
default. Widen with --min-confidence medium or --all, and drop a bad guess
|
|
292
|
+
with 'agents projects rm <name>'.
|
|
108
293
|
`,
|
|
109
294
|
});
|
|
110
|
-
projects.hook('preAction', () => {
|
|
295
|
+
projects.hook('preAction', (_thisCommand, actionCommand) => {
|
|
111
296
|
if (enabled)
|
|
112
297
|
return;
|
|
298
|
+
// `probe` is the peer half of `status --fleet`: it must answer whenever the
|
|
299
|
+
// binary carries it, even where the beta flag is off — a gated peer would
|
|
300
|
+
// look unreachable to every fleet member on a newer CLI.
|
|
301
|
+
if (actionCommand.name() === 'probe')
|
|
302
|
+
return;
|
|
113
303
|
console.error(chalk.red('agents projects is in beta.'));
|
|
114
304
|
console.error(chalk.gray(betaEnableHint('projects')));
|
|
115
305
|
process.exit(1);
|
|
@@ -131,10 +321,16 @@ export function registerProjectsCommands(program) {
|
|
|
131
321
|
return;
|
|
132
322
|
}
|
|
133
323
|
const roll = rollupSessionsByProject(defs, await getActiveSessions());
|
|
134
|
-
|
|
324
|
+
const rows = defs.map((d) => ({
|
|
325
|
+
name: d.name,
|
|
326
|
+
path: truncate(d.root ?? d.defaultPath ?? '', LIST_PATH_MAX),
|
|
327
|
+
repo: d.repo ?? d.repos?.[0]?.slug ?? '',
|
|
328
|
+
}));
|
|
329
|
+
const w = computeProjectListWidths(rows);
|
|
330
|
+
for (const [i, d] of defs.entries()) {
|
|
135
331
|
const agents = roll.get(d.name)?.agents ?? 0;
|
|
136
|
-
const
|
|
137
|
-
console.log(` ${chalk.bold(
|
|
332
|
+
const row = rows[i];
|
|
333
|
+
console.log(` ${chalk.bold(row.name.padEnd(w.name))} ${chalk.dim(row.path.padEnd(w.path))} ${chalk.cyan(row.repo.padEnd(w.repo))} ${agents} agents`);
|
|
138
334
|
}
|
|
139
335
|
});
|
|
140
336
|
// ---- add ----
|
|
@@ -239,6 +435,7 @@ export function registerProjectsCommands(program) {
|
|
|
239
435
|
.option('--json', 'Machine-readable output')
|
|
240
436
|
.option('--window <days>', 'Window for merged PRs and artifacts', '7')
|
|
241
437
|
.option('--no-remote', 'Skip the GitHub lookup (merged-PR count); faster, offline')
|
|
438
|
+
.option('--fleet', 'Also dial every fleet device for workspace presence, branch, and drift (one SSH per peer)')
|
|
242
439
|
.action(async (name, opts) => {
|
|
243
440
|
const all = listProjectDefs();
|
|
244
441
|
// Named lookup goes through the strict single-def loader so a broken
|
|
@@ -257,14 +454,57 @@ export function registerProjectsCommands(program) {
|
|
|
257
454
|
}
|
|
258
455
|
const windowDays = Math.max(1, Number.parseInt(opts.window ?? '7', 10) || 7);
|
|
259
456
|
const nowMs = Date.now();
|
|
260
|
-
|
|
457
|
+
// --fleet: probe each shown def's workspace paths (root + repos[].path)
|
|
458
|
+
// locally and on every peer in one parallel SSH round, and widen the
|
|
459
|
+
// live-session rollup to the whole fleet via the existing sessions
|
|
460
|
+
// fan-out. Both are opt-in — they dial the fleet.
|
|
461
|
+
const fleetTargets = opts.fleet ? [...new Set(defs.flatMap(workspaceTargetsForDef))] : [];
|
|
462
|
+
let fleetWs = [];
|
|
463
|
+
let fleetSkipped = [];
|
|
464
|
+
let fleetSessions = [];
|
|
465
|
+
if (opts.fleet) {
|
|
466
|
+
const self = machineId();
|
|
467
|
+
fleetWs.push(...probeProjectWorkspaces(fleetTargets).map((s) => ({ ...s, host: self })));
|
|
468
|
+
const [probeRes, activeRes] = await Promise.all([
|
|
469
|
+
fleetTargets.length > 0
|
|
470
|
+
? gatherRemoteAgentsJson({
|
|
471
|
+
args: ['projects', 'probe', '--json', ...fleetTargets],
|
|
472
|
+
noFanoutEnv: PROJECTS_NO_FANOUT_ENV,
|
|
473
|
+
parse: parseRemoteProbe,
|
|
474
|
+
quiet: true,
|
|
475
|
+
})
|
|
476
|
+
: Promise.resolve({ items: [], deviceCount: 0, skipped: [] }),
|
|
477
|
+
gatherRemoteActive(undefined, { quiet: true }),
|
|
478
|
+
]);
|
|
479
|
+
fleetWs.push(...probeRes.items);
|
|
480
|
+
fleetSkipped = probeRes.skipped;
|
|
481
|
+
fleetSessions = activeRes.sessions;
|
|
482
|
+
}
|
|
483
|
+
const roll = rollupSessionsByProject(all, [...await getActiveSessions(), ...fleetSessions]);
|
|
261
484
|
// Enrich only the shown projects. --no-remote still reads the local artifact
|
|
262
|
-
// log but skips the gh
|
|
485
|
+
// log but skips the gh calls + Linear counts (both are network).
|
|
263
486
|
const remote = new Map();
|
|
487
|
+
const linear = new Map();
|
|
264
488
|
await Promise.all(defs.map(async (d) => {
|
|
265
|
-
|
|
489
|
+
const skipRemote = opts.remote === false;
|
|
490
|
+
const [sig, counts] = await Promise.all([
|
|
491
|
+
enrichProjectSignals(d, windowDays, nowMs, { skipRemote }),
|
|
492
|
+
!skipRemote && d.linear?.projectId
|
|
493
|
+
? fetchLinearProjectCounts(d.linear.projectId)
|
|
494
|
+
: Promise.resolve(undefined),
|
|
495
|
+
]);
|
|
496
|
+
remote.set(d.name, sig);
|
|
497
|
+
if (counts)
|
|
498
|
+
linear.set(d.name, counts);
|
|
266
499
|
}));
|
|
500
|
+
/** This def's slice of the fleet probe, in its own target order. */
|
|
501
|
+
const fleetFor = (d) => {
|
|
502
|
+
const targets = new Set(workspaceTargetsForDef(d));
|
|
503
|
+
return fleetWs.filter((s) => targets.has(s.path));
|
|
504
|
+
};
|
|
267
505
|
if (opts.json) {
|
|
506
|
+
if (fleetSkipped.length > 0)
|
|
507
|
+
process.stderr.write(formatFleetSkippedNote(fleetSkipped));
|
|
268
508
|
console.log(JSON.stringify(defs.map((d) => {
|
|
269
509
|
const r = roll.get(d.name);
|
|
270
510
|
const rem = remote.get(d.name);
|
|
@@ -272,74 +512,141 @@ export function registerProjectsCommands(program) {
|
|
|
272
512
|
name: d.name,
|
|
273
513
|
agents: r?.agents ?? 0,
|
|
274
514
|
byStatus: r?.byStatus ?? {},
|
|
515
|
+
members: r?.members ?? [],
|
|
275
516
|
plan: r?.plan ?? { done: 0, total: 0 },
|
|
276
517
|
planPct: r ? planPct(r.plan) ?? null : null,
|
|
277
518
|
openPrs: r?.openPrs ?? [],
|
|
278
519
|
mergedPrs: rem?.mergedPrs ?? 0,
|
|
520
|
+
latestRelease: rem?.latestRelease ?? null,
|
|
521
|
+
linear: linear.get(d.name) ?? null,
|
|
279
522
|
tickets: r?.tickets ?? [],
|
|
280
523
|
worktrees: r?.worktrees ?? 0,
|
|
281
524
|
artifacts: rem?.artifacts ?? 0,
|
|
282
525
|
lastArtifact: rem?.lastArtifact ?? null,
|
|
283
526
|
windowDays,
|
|
284
527
|
repos: [d.repo, ...(d.repos ?? []).map((r2) => r2.slug)].filter(Boolean),
|
|
528
|
+
...(opts.fleet ? { workspaces: fleetFor(d) } : {}),
|
|
285
529
|
};
|
|
286
530
|
}), null, 2));
|
|
287
531
|
return;
|
|
288
532
|
}
|
|
289
|
-
for (const d of defs)
|
|
290
|
-
renderCard(d, roll.get(d.name), remote.get(d.name));
|
|
533
|
+
for (const d of defs) {
|
|
534
|
+
renderCard(d, roll.get(d.name), remote.get(d.name), opts.fleet ? fleetFor(d) : undefined, linear.get(d.name), nowMs);
|
|
535
|
+
}
|
|
536
|
+
if (fleetSkipped.length > 0)
|
|
537
|
+
process.stdout.write(formatFleetSkippedNote(fleetSkipped));
|
|
538
|
+
});
|
|
539
|
+
// ---- probe (hidden; the peer half of `status --fleet`) ----
|
|
540
|
+
projects
|
|
541
|
+
.command('probe [paths...]', { hidden: true })
|
|
542
|
+
.description('Probe workspace repos (presence, branch, drift, dirtiness) and print JSON. Answers for this machine only.')
|
|
543
|
+
.option('--json', 'Machine-readable output (the only output format)')
|
|
544
|
+
.action((paths) => {
|
|
545
|
+
// Never fans out — the recursion guard env is set by the parent fan-out,
|
|
546
|
+
// and this command has no remote code path either way.
|
|
547
|
+
console.log(JSON.stringify(probeProjectWorkspaces(paths ?? []), null, 2));
|
|
291
548
|
});
|
|
292
549
|
// ---- import ----
|
|
293
550
|
projects
|
|
294
551
|
.command('import')
|
|
295
|
-
.description('
|
|
296
|
-
.
|
|
552
|
+
.description('Import project definitions from Linear (preferred) or the Factory auto-detection registry.')
|
|
553
|
+
.option('--from-linear', 'Import the workspace\'s Linear projects (via the `linear` CLI)')
|
|
554
|
+
.option('--from-factory', 'Import the Factory registry (~/.agents/factory/projects.json)')
|
|
555
|
+
.option('--min-confidence <level>', '--from-factory only: lowest detection confidence to import — low|medium|high (default: high)')
|
|
556
|
+
.option('--all', '--from-factory only: import every row regardless of confidence')
|
|
297
557
|
.option('--force', 'Overwrite existing definitions')
|
|
298
|
-
.action((
|
|
299
|
-
|
|
300
|
-
let rawText;
|
|
558
|
+
.action((raw) => {
|
|
559
|
+
let opts;
|
|
301
560
|
try {
|
|
302
|
-
|
|
561
|
+
opts = validateImportOpts(raw);
|
|
303
562
|
}
|
|
304
|
-
catch {
|
|
305
|
-
console.error(chalk.red(
|
|
563
|
+
catch (e) {
|
|
564
|
+
console.error(chalk.red(e instanceof Error ? e.message : String(e)));
|
|
565
|
+
process.exit(1);
|
|
566
|
+
}
|
|
567
|
+
const existing = new Map(listProjectDefs().map((d) => [d.name, d]));
|
|
568
|
+
const result = opts.source === 'linear' ? runLinearImport(existing, opts) : runFactoryImport(existing, opts);
|
|
569
|
+
for (const def of result.defs)
|
|
570
|
+
writeProjectDef(def);
|
|
571
|
+
const n = result.defs.length;
|
|
572
|
+
const s = result.skipped.length;
|
|
573
|
+
console.log(chalk.green(`Imported ${n} project${n === 1 ? '' : 's'}${s ? chalk.gray(` (${s} skipped)`) : ''}`));
|
|
574
|
+
for (const skip of result.skipped)
|
|
575
|
+
console.log(chalk.gray(` skip ${skip.name}: ${skip.reason}`));
|
|
576
|
+
if (opts.source === 'factory' && s > 0 && opts.minConfidence === 'high') {
|
|
577
|
+
console.log(chalk.gray(' (widen with --min-confidence medium or --all)'));
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
// ---- link ----
|
|
581
|
+
projects
|
|
582
|
+
.command('link <name>')
|
|
583
|
+
.description('Attach an external tracker to a project definition (writes linear.projectId into the YAML).')
|
|
584
|
+
.option('--linear [query]', 'Bind a Linear project by exact name or id; no value auto-suggests from the def name + repo')
|
|
585
|
+
.action((name, opts) => {
|
|
586
|
+
const def = loadProjectDef(name);
|
|
587
|
+
if (!def) {
|
|
588
|
+
console.error(chalk.red(`No project named "${name}". List them: agents projects list`));
|
|
589
|
+
process.exit(1);
|
|
590
|
+
}
|
|
591
|
+
if (opts.linear === undefined) {
|
|
592
|
+
console.error(chalk.red('Nothing to link. Pass a tracker flag, e.g. --linear [query].'));
|
|
306
593
|
process.exit(1);
|
|
307
594
|
}
|
|
308
|
-
|
|
595
|
+
// Explicit user command — a missing/unauthenticated `linear` CLI is loud,
|
|
596
|
+
// unlike the best-effort card enrichment.
|
|
597
|
+
let list;
|
|
309
598
|
try {
|
|
310
|
-
|
|
599
|
+
list = listLinearProjects();
|
|
311
600
|
}
|
|
312
|
-
catch {
|
|
313
|
-
console.error(chalk.red(
|
|
601
|
+
catch (e) {
|
|
602
|
+
console.error(chalk.red(e instanceof Error ? e.message : String(e)));
|
|
314
603
|
process.exit(1);
|
|
315
604
|
}
|
|
316
|
-
const
|
|
317
|
-
let
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
605
|
+
const query = typeof opts.linear === 'string' ? opts.linear.trim() : '';
|
|
606
|
+
let pick;
|
|
607
|
+
if (query) {
|
|
608
|
+
pick = pickLinearProject(query, list);
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
// Auto-suggest from the def's own identity — the primary repo slug is
|
|
612
|
+
// the sharpest key, then the def name. Only an exact normalized match
|
|
613
|
+
// writes itself; anything weaker asks the user to disambiguate.
|
|
614
|
+
pick = { kind: 'none' };
|
|
615
|
+
for (const hint of [def.repo, def.name].filter((h) => typeof h === 'string' && h.length > 0)) {
|
|
616
|
+
const d = pickLinearProject(hint, list);
|
|
617
|
+
if (d.kind === 'match') {
|
|
618
|
+
pick = d;
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
if (pick.kind === 'none')
|
|
622
|
+
pick = d;
|
|
327
623
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
624
|
+
}
|
|
625
|
+
if (pick.kind !== 'match') {
|
|
626
|
+
if (pick.kind === 'candidates') {
|
|
627
|
+
console.error(chalk.yellow(`No confident Linear match${query ? ` for "${query}"` : ` for "${def.name}"`}. Candidates:`));
|
|
628
|
+
for (const c of pick.projects)
|
|
629
|
+
console.error(` ${chalk.cyan(c.id)} ${c.name}`);
|
|
331
630
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
631
|
+
else {
|
|
632
|
+
console.error(chalk.yellow(`No Linear project matches${query ? ` "${query}"` : ` "${def.name}"`}. Available:`));
|
|
633
|
+
for (const c of list)
|
|
634
|
+
console.error(` ${chalk.cyan(c.id)} ${c.name}`);
|
|
635
|
+
}
|
|
636
|
+
console.error(chalk.gray(`Re-run with an explicit name or id: agents projects link ${name} --linear "<name-or-id>"`));
|
|
637
|
+
process.exit(1);
|
|
638
|
+
}
|
|
639
|
+
const p = pick.project;
|
|
640
|
+
// Preserve every other field — load, set linear, write back. Keep a
|
|
641
|
+
// previously linked url when the new CLI row carries none.
|
|
642
|
+
if (def.linear?.projectId && def.linear.projectId !== p.id) {
|
|
643
|
+
console.log(chalk.gray(` replacing previous Linear link (${def.linear.projectId})`));
|
|
341
644
|
}
|
|
342
|
-
|
|
645
|
+
def.linear = { ...def.linear, projectId: p.id };
|
|
646
|
+
if (p.url)
|
|
647
|
+
def.linear.url = p.url;
|
|
648
|
+
writeProjectDef(def);
|
|
649
|
+
console.log(chalk.green(`${def.name} → Linear project "${p.name}" (${p.id})${p.url ? ` ${p.url}` : ''}`));
|
|
343
650
|
});
|
|
344
651
|
// ---- rm ----
|
|
345
652
|
projects
|
|
@@ -11,6 +11,7 @@ import * as fs from 'fs';
|
|
|
11
11
|
import * as path from 'path';
|
|
12
12
|
import * as yaml from 'yaml';
|
|
13
13
|
import { isDaemonRunning, signalDaemonReload, startDaemon, stopDaemon, readDaemonLog, getDaemonStatus, } from '../lib/daemon.js';
|
|
14
|
+
import { assertSchedulerEnabled } from '../lib/device-config.js';
|
|
14
15
|
import { resolveAgentName, isAgentHardDeprecated, hardDeprecationError } from '../lib/agents.js';
|
|
15
16
|
import { humanizeCron, humanizeNextRun, formatRepoLink, REPO_DISPLAY_MAX } from '../lib/routines-format.js';
|
|
16
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';
|
|
@@ -295,9 +296,21 @@ function parseRoutineTrigger(options) {
|
|
|
295
296
|
/**
|
|
296
297
|
* Start or reload the background scheduler so newly-added jobs fire on time.
|
|
297
298
|
* `quiet` suppresses human status lines for JSON callers.
|
|
299
|
+
*
|
|
300
|
+
* When this device has `scheduler.enabled=false` the auto-start is skipped with
|
|
301
|
+
* the stated reason (the add itself already succeeded — the job is config and
|
|
302
|
+
* stays valid fleet-wide); the refusal message names the setting and the fix.
|
|
298
303
|
*/
|
|
299
304
|
function ensureSchedulerRunning(opts = {}) {
|
|
300
305
|
const log = opts.stderr ? console.error : console.log;
|
|
306
|
+
try {
|
|
307
|
+
assertSchedulerEnabled();
|
|
308
|
+
}
|
|
309
|
+
catch (err) {
|
|
310
|
+
// Loud stated skip, on stderr so --json stdout stays clean.
|
|
311
|
+
console.error(chalk.yellow(err.message));
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
301
314
|
if (isDaemonRunning()) {
|
|
302
315
|
signalDaemonReload();
|
|
303
316
|
if (!opts.quiet)
|
|
@@ -1480,9 +1493,22 @@ export function registerRoutinesCommands(program) {
|
|
|
1480
1493
|
.command('start')
|
|
1481
1494
|
.description('Start the background scheduler. Usually unnecessary — it auto-starts when you add your first routine.')
|
|
1482
1495
|
.action(() => {
|
|
1496
|
+
try {
|
|
1497
|
+
// A manual start on a scheduler-disabled device refuses with the same
|
|
1498
|
+
// message the auto-start surfaces give.
|
|
1499
|
+
assertSchedulerEnabled();
|
|
1500
|
+
}
|
|
1501
|
+
catch (err) {
|
|
1502
|
+
console.error(chalk.red(err.message));
|
|
1503
|
+
process.exit(1);
|
|
1504
|
+
}
|
|
1483
1505
|
const result = startDaemon();
|
|
1484
1506
|
if (result.method === 'already-running') {
|
|
1485
|
-
|
|
1507
|
+
// Signal a reload even here: if the daemon booted while this device had
|
|
1508
|
+
// scheduler.enabled=false, the reload re-evaluates the gate and boots
|
|
1509
|
+
// the scheduler — a manual start heals a scheduler-less daemon.
|
|
1510
|
+
signalDaemonReload();
|
|
1511
|
+
console.log(chalk.yellow(`Scheduler already running (PID: ${result.pid}) — reloaded`));
|
|
1486
1512
|
}
|
|
1487
1513
|
else if (result.pid) {
|
|
1488
1514
|
console.log(chalk.green(`Scheduler started (PID: ${result.pid})`));
|
package/dist/commands/rules.js
CHANGED
|
@@ -512,6 +512,12 @@ Examples:
|
|
|
512
512
|
.addHelpText('after', `
|
|
513
513
|
Targets are <agent>@<version>. Use 'default' for the alias of the global default version.
|
|
514
514
|
|
|
515
|
+
This re-syncs the version home immediately, AND the choice sticks: every later
|
|
516
|
+
'agents run' for this agent+version re-applies the active preset before launch
|
|
517
|
+
(skip-fast when nothing changed), so a preset set here never goes stale even if
|
|
518
|
+
something changes it again without running 'switch' — you don't need to re-run
|
|
519
|
+
'switch' or 'sync' by hand before the next launch.
|
|
520
|
+
|
|
515
521
|
Examples:
|
|
516
522
|
# Persist the cautious preset for claude 2.1.111
|
|
517
523
|
agents rules switch claude@2.1.111 --preset cautious
|
|
@@ -10,6 +10,7 @@ import { SSH_TARGET_RE, assertValidSshTarget } from '../lib/ssh-exec.js';
|
|
|
10
10
|
import { quoteWin32ExecArg } from '../lib/platform/index.js';
|
|
11
11
|
import { type SecretsBackend, type SecretsBundle, type SecretsPolicy } from '../lib/secrets/bundles.js';
|
|
12
12
|
import { type SecretsListFilterOpts } from '../lib/secrets/list-filter.js';
|
|
13
|
+
import { type BundleUsageSummary } from '../lib/secrets/usage-db.js';
|
|
13
14
|
/** Read all available data from stdin synchronously, trimmed. */
|
|
14
15
|
/**
|
|
15
16
|
* Read the raw `.env` text for `import --from <path|->`. A `-` reads the .env
|
|
@@ -125,6 +126,22 @@ export declare function formatHoldWindow(ms: number): string;
|
|
|
125
126
|
* shared by the POLICY column, the `--held` filter, and the JSON payload, so the
|
|
126
127
|
* three can never disagree about the same bundle. */
|
|
127
128
|
export declare function liveHold(expiresAt: number | undefined, now?: number): number | null;
|
|
129
|
+
/**
|
|
130
|
+
* Compact one-line usage summary for `secrets view` — each recorded event with
|
|
131
|
+
* its count and how long ago it last happened ("accessed 42× (last 2h ago) ·
|
|
132
|
+
* exported 3× (last 1d ago)"). Empty string when nothing has been recorded yet.
|
|
133
|
+
* Pure — unit-tested.
|
|
134
|
+
*/
|
|
135
|
+
export declare function formatUsageLine(summary: BundleUsageSummary | undefined): string;
|
|
136
|
+
/**
|
|
137
|
+
* The held-state line for `secrets view`: whether the secrets-agent currently
|
|
138
|
+
* holds this bundle (so reads are prompt-free) and for how long, or that Touch
|
|
139
|
+
* ID is required on the next read. Only meaningful for keychain bundles on macOS
|
|
140
|
+
* — file/vault backends have no broker to hold, and a `never`-policy bundle is
|
|
141
|
+
* always readable — so both return an empty line and the caller skips it.
|
|
142
|
+
* `heldExpiresAt` is the max expiry across every held scope.
|
|
143
|
+
*/
|
|
144
|
+
export declare function renderViewStatusLine(b: SecretsBundle, heldExpiresAt: number | null): string;
|
|
128
145
|
/**
|
|
129
146
|
* The EXPIRING cell. Counts keys needing attention — already expired plus due
|
|
130
147
|
* within 30 days — and colours by the worst of the two: red once anything has
|