@phnx-labs/agents-cli 1.20.93 → 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 +264 -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/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 +28 -0
- package/dist/commands/projects.js +173 -47
- package/dist/commands/routines.js +27 -1
- package/dist/commands/rules.js +6 -0
- 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 +4 -9
- package/dist/commands/watchdog.js +26 -3
- package/dist/index.js +22 -6
- package/dist/lib/activity.d.ts +7 -0
- package/dist/lib/activity.js +19 -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/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.d.ts +2 -2
- package/dist/lib/events.js +4 -53
- 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 +72 -5
- package/dist/lib/linear-project-counts.js +90 -6
- package/dist/lib/linear-projects.d.ts +12 -0
- package/dist/lib/linear-projects.js +30 -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/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/remote-agents-json.d.ts +14 -1
- package/dist/lib/remote-agents-json.js +21 -3
- 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/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/index.d.ts +4 -5
- package/dist/lib/secrets/index.js +4 -5
- 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-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/startup/command-registry.d.ts +0 -2
- package/dist/lib/startup/command-registry.js +1 -5
- package/dist/lib/state.d.ts +7 -2
- package/dist/lib/state.js +85 -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 +17 -1
- 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,12 +10,14 @@
|
|
|
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';
|
|
19
21
|
import { machineId } from '../lib/machine-id.js';
|
|
20
22
|
import { getActiveSessions } from '../lib/session/active.js';
|
|
21
23
|
import { gatherRemoteActive } from '../lib/session/remote-active.js';
|
|
@@ -26,6 +28,7 @@ import { listProjectDefs, loadProjectDef, writeProjectDef, removeProjectDef, pro
|
|
|
26
28
|
import { rollupSessionsByProject, planPct, enrichProjectSignals, formatProjectMembers, } from '../lib/project-status.js';
|
|
27
29
|
import { fetchLinearProjectCounts } from '../lib/linear-project-counts.js';
|
|
28
30
|
import { listLinearProjects, pickLinearProject } from '../lib/linear-projects.js';
|
|
31
|
+
import { buildFactoryImportCandidates, buildLinearImportCandidates, validateImportOpts, } from '../lib/project-import.js';
|
|
29
32
|
/** Recursion guard: a peer answering a probe fan-out never re-fans-out itself. */
|
|
30
33
|
export const PROJECTS_NO_FANOUT_ENV = 'AGENTS_PROJECTS_LOCAL';
|
|
31
34
|
/** Max peers named in the skipped note before the rest collapse to `+N`. */
|
|
@@ -63,6 +66,135 @@ function originSlug(cwd) {
|
|
|
63
66
|
return undefined;
|
|
64
67
|
}
|
|
65
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
|
+
}
|
|
66
198
|
function statusBar(r) {
|
|
67
199
|
const parts = [];
|
|
68
200
|
const push = (n, label, color) => {
|
|
@@ -80,7 +212,7 @@ function statusBar(r) {
|
|
|
80
212
|
parts.push(chalk.gray(`+${r.agents - shown} other`));
|
|
81
213
|
return parts.join(' · ') || chalk.gray('no live agents');
|
|
82
214
|
}
|
|
83
|
-
function renderCard(def, r, remote, fleet, linear) {
|
|
215
|
+
function renderCard(def, r, remote, fleet, linear, nowMs = Date.now()) {
|
|
84
216
|
const agents = r?.agents ?? 0;
|
|
85
217
|
const pct = r ? planPct(r.plan) : undefined;
|
|
86
218
|
const planStr = pct === undefined ? '' : ` · ${chalk.cyan(`${pct}% plan`)}`;
|
|
@@ -104,6 +236,9 @@ function renderCard(def, r, remote, fleet, linear) {
|
|
|
104
236
|
if (linear) {
|
|
105
237
|
console.log(` ${chalk.dim('linear')} ${linear.done}/${linear.total}${linear.truncated ? '+' : ''} done · ${linear.inProgress} in progress`);
|
|
106
238
|
}
|
|
239
|
+
if (linear?.nextMilestone) {
|
|
240
|
+
console.log(` ${chalk.dim('next')} ${formatNextMilestone(linear.nextMilestone, nowMs)}`);
|
|
241
|
+
}
|
|
107
242
|
if (r && r.tickets.length) {
|
|
108
243
|
console.log(` ${chalk.dim('tickets')} ${r.tickets.slice(0, 8).join(' · ')}${r.tickets.length > 8 ? ' …' : ''}`);
|
|
109
244
|
}
|
|
@@ -138,6 +273,7 @@ export function registerProjectsCommands(program) {
|
|
|
138
273
|
.description('Named multi-repo projects with a progress rollup.');
|
|
139
274
|
setHelpSections(projects, {
|
|
140
275
|
examples: `
|
|
276
|
+
agents projects import --from-linear # the projects you actually track
|
|
141
277
|
agents projects add rush --repo phnx-labs/rush --path apps/web
|
|
142
278
|
agents projects list
|
|
143
279
|
agents projects status # progress card for every project
|
|
@@ -149,6 +285,11 @@ export function registerProjectsCommands(program) {
|
|
|
149
285
|
notes: `
|
|
150
286
|
Definitions are hand-editable YAML in ~/.agents/projects/ and sync across
|
|
151
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>'.
|
|
152
293
|
`,
|
|
153
294
|
});
|
|
154
295
|
projects.hook('preAction', (_thisCommand, actionCommand) => {
|
|
@@ -180,10 +321,16 @@ export function registerProjectsCommands(program) {
|
|
|
180
321
|
return;
|
|
181
322
|
}
|
|
182
323
|
const roll = rollupSessionsByProject(defs, await getActiveSessions());
|
|
183
|
-
|
|
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()) {
|
|
184
331
|
const agents = roll.get(d.name)?.agents ?? 0;
|
|
185
|
-
const
|
|
186
|
-
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`);
|
|
187
334
|
}
|
|
188
335
|
});
|
|
189
336
|
// ---- add ----
|
|
@@ -384,7 +531,7 @@ export function registerProjectsCommands(program) {
|
|
|
384
531
|
return;
|
|
385
532
|
}
|
|
386
533
|
for (const d of defs) {
|
|
387
|
-
renderCard(d, roll.get(d.name), remote.get(d.name), opts.fleet ? fleetFor(d) : undefined, linear.get(d.name));
|
|
534
|
+
renderCard(d, roll.get(d.name), remote.get(d.name), opts.fleet ? fleetFor(d) : undefined, linear.get(d.name), nowMs);
|
|
388
535
|
}
|
|
389
536
|
if (fleetSkipped.length > 0)
|
|
390
537
|
process.stdout.write(formatFleetSkippedNote(fleetSkipped));
|
|
@@ -402,54 +549,33 @@ export function registerProjectsCommands(program) {
|
|
|
402
549
|
// ---- import ----
|
|
403
550
|
projects
|
|
404
551
|
.command('import')
|
|
405
|
-
.description('
|
|
406
|
-
.
|
|
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')
|
|
407
557
|
.option('--force', 'Overwrite existing definitions')
|
|
408
|
-
.action((
|
|
409
|
-
|
|
410
|
-
let rawText;
|
|
411
|
-
try {
|
|
412
|
-
rawText = fs.readFileSync(src, 'utf8');
|
|
413
|
-
}
|
|
414
|
-
catch {
|
|
415
|
-
console.error(chalk.red(`No Factory registry at ${src}`));
|
|
416
|
-
process.exit(1);
|
|
417
|
-
}
|
|
418
|
-
let rows;
|
|
558
|
+
.action((raw) => {
|
|
559
|
+
let opts;
|
|
419
560
|
try {
|
|
420
|
-
|
|
561
|
+
opts = validateImportOpts(raw);
|
|
421
562
|
}
|
|
422
|
-
catch {
|
|
423
|
-
console.error(chalk.red(
|
|
563
|
+
catch (e) {
|
|
564
|
+
console.error(chalk.red(e instanceof Error ? e.message : String(e)));
|
|
424
565
|
process.exit(1);
|
|
425
566
|
}
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
for (const raw of list) {
|
|
430
|
-
if (!raw || typeof raw !== 'object')
|
|
431
|
-
continue;
|
|
432
|
-
const o = raw;
|
|
433
|
-
const name = typeof o.name === 'string' ? o.name : undefined;
|
|
434
|
-
if (!name || !isSafeProjectName(name)) {
|
|
435
|
-
skipped++;
|
|
436
|
-
continue;
|
|
437
|
-
}
|
|
438
|
-
if (loadProjectDef(name) && !opts.force) {
|
|
439
|
-
skipped++;
|
|
440
|
-
continue;
|
|
441
|
-
}
|
|
442
|
-
const def = { name };
|
|
443
|
-
if (typeof o.path === 'string')
|
|
444
|
-
def.root = toHomeRelative(o.path);
|
|
445
|
-
if (typeof o.repoSlug === 'string')
|
|
446
|
-
def.repo = o.repoSlug;
|
|
447
|
-
if (typeof o.linearProjectId === 'string')
|
|
448
|
-
def.linear = { projectId: o.linearProjectId };
|
|
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)
|
|
449
570
|
writeProjectDef(def);
|
|
450
|
-
|
|
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)'));
|
|
451
578
|
}
|
|
452
|
-
console.log(chalk.green(`Imported ${created} project${created === 1 ? '' : 's'}${skipped ? chalk.gray(` (${skipped} skipped)`) : ''}`));
|
|
453
579
|
});
|
|
454
580
|
// ---- link ----
|
|
455
581
|
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
|
|
@@ -315,6 +315,13 @@ export declare function isBareBrowserListing(options: SessionsOptions, query: st
|
|
|
315
315
|
* `runSessionBrowser` picker cannot represent.
|
|
316
316
|
*/
|
|
317
317
|
export declare function hasNoBrowserDisqualifyingFlags(options: SessionsOptions, query: string | undefined): boolean;
|
|
318
|
+
/** Resolve a session by id/query globally and print its compact preview (no pager).
|
|
319
|
+
* Backs `--preview` — the fast path for the "peek before resume" hot loop. */
|
|
320
|
+
export declare function renderSessionPreview(query: string, scope: {
|
|
321
|
+
agent?: string;
|
|
322
|
+
project?: string;
|
|
323
|
+
local?: boolean;
|
|
324
|
+
}): Promise<void>;
|
|
318
325
|
/**
|
|
319
326
|
* The one-line live status banner shown above a session preview: the glyph, the
|
|
320
327
|
* status word, and — when the session needs a human or has LOST one — a plain
|
|
@@ -351,6 +358,16 @@ export declare function teamBadge(session: SessionMeta): {
|
|
|
351
358
|
* when `showTicket` — otherwise a listing with no refs would waste a column of
|
|
352
359
|
* dashes and needlessly truncate the topic. Worktree stays a trailing badge. */
|
|
353
360
|
export declare function flatSessionRow(session: SessionMeta, live?: ActiveSession, showTicket?: boolean, cols?: PickerColumns, favorite?: boolean): string;
|
|
361
|
+
/**
|
|
362
|
+
* Live-session index for enriching the default listing, or undefined when
|
|
363
|
+
* enrichment is off (`--no-live`) or irrelevant (`--json`, which serializes
|
|
364
|
+
* SessionMeta). Full detection (incl. the headless `ps` scan) is deliberate:
|
|
365
|
+
* bare-CLI and tmux agents are the common case here, and skipping them would
|
|
366
|
+
* leave the glyph almost never showing. The listing is a one-shot user action,
|
|
367
|
+
* not a hot loop, so the `ps`/`lsof` cost is acceptable; `--no-live` is the
|
|
368
|
+
* escape hatch. Never throws — a probe failure just yields a plain listing.
|
|
369
|
+
*/
|
|
370
|
+
export declare function maybeLiveIndex(options: SessionsOptions): Promise<Map<string, ActiveSession> | undefined>;
|
|
354
371
|
/**
|
|
355
372
|
* Group key for the overview: resolve the cwd through the same canonical
|
|
356
373
|
* resolver the `agents activity` timeline groups by — a defined project's name
|
|
@@ -46,7 +46,6 @@ import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
|
46
46
|
import { sessionPicker, buildPreview, formatTodoCompact, githubRepoUrlFromCwd, } from './sessions-picker.js';
|
|
47
47
|
import { setHelpSections } from '../lib/help.js';
|
|
48
48
|
import { registerSessionsTailCommand } from './sessions-tail.js';
|
|
49
|
-
import { registerSessionsSyncCommand } from './sessions-sync.js';
|
|
50
49
|
import { registerSessionsResumeCommand } from './sessions-resume.js';
|
|
51
50
|
import { registerSessionsFavoriteCommand } from './sessions-favorite.js';
|
|
52
51
|
import { isFavorite, listFavorites } from '../lib/session/favorites.js';
|
|
@@ -1044,7 +1043,11 @@ export async function gatherActiveSessions(opts = {}) {
|
|
|
1044
1043
|
const self = machineId();
|
|
1045
1044
|
// An explicit --host/--device list scopes the view: seed local sessions only
|
|
1046
1045
|
// when no hosts are named, or when this machine is one of the named targets.
|
|
1047
|
-
|
|
1046
|
+
// `localOnly: opts.local` (RUSH-2118) keeps a `--local` query from dialing a
|
|
1047
|
+
// remote-host teammate over ssh even for this machine's OWN local gather —
|
|
1048
|
+
// "this machine only" must mean zero ssh, not just "skip the cross-machine
|
|
1049
|
+
// device fan-out below".
|
|
1050
|
+
const local = shouldIncludeLocal(opts.hosts, self) ? await getActiveSessions({ localOnly: opts.local }) : [];
|
|
1048
1051
|
for (const s of local)
|
|
1049
1052
|
if (!s.machine)
|
|
1050
1053
|
s.machine = self;
|
|
@@ -1218,7 +1221,7 @@ function canonicalSessionsCommand(query, options) {
|
|
|
1218
1221
|
}
|
|
1219
1222
|
/** Resolve a session by id/query globally and print its compact preview (no pager).
|
|
1220
1223
|
* Backs `--preview` — the fast path for the "peek before resume" hot loop. */
|
|
1221
|
-
async function renderSessionPreview(query, scope) {
|
|
1224
|
+
export async function renderSessionPreview(query, scope) {
|
|
1222
1225
|
const discovered = await discoverSessions({ all: true, cwd: process.cwd(), limit: 5000 });
|
|
1223
1226
|
const pool = applyScopeFilters(discovered, scope);
|
|
1224
1227
|
const { matches, completeId } = resolveSessionQuery(pool, query);
|
|
@@ -1234,9 +1237,11 @@ async function renderSessionPreview(query, scope) {
|
|
|
1234
1237
|
}
|
|
1235
1238
|
// Lead with the live status when the session is still running, so the preview
|
|
1236
1239
|
// says working / waiting / idle up front — not just the historical transcript.
|
|
1240
|
+
// `--local --preview` is freely combinable with `--local` (RUSH-2118): thread
|
|
1241
|
+
// it through so this probe never dials a remote-host teammate either.
|
|
1237
1242
|
let live;
|
|
1238
1243
|
try {
|
|
1239
|
-
live = indexActiveBySessionId(await getActiveSessions()).get(session.id);
|
|
1244
|
+
live = indexActiveBySessionId(await getActiveSessions({ localOnly: scope.local === true })).get(session.id);
|
|
1240
1245
|
}
|
|
1241
1246
|
catch { /* plain preview on any probe failure */ }
|
|
1242
1247
|
const headline = formatLiveStatusHeadline(live, isFavorite(session.id));
|
|
@@ -1390,7 +1395,7 @@ limitSource) {
|
|
|
1390
1395
|
console.error(chalk.red('--preview requires a session id or query.'));
|
|
1391
1396
|
process.exit(1);
|
|
1392
1397
|
}
|
|
1393
|
-
await renderSessionPreview(query, { agent: options.agent, project: options.project });
|
|
1398
|
+
await renderSessionPreview(query, { agent: options.agent, project: options.project, local: options.local });
|
|
1394
1399
|
return;
|
|
1395
1400
|
}
|
|
1396
1401
|
if (options.active) {
|
|
@@ -1907,11 +1912,15 @@ function treeSessionRow(session, live) {
|
|
|
1907
1912
|
* not a hot loop, so the `ps`/`lsof` cost is acceptable; `--no-live` is the
|
|
1908
1913
|
* escape hatch. Never throws — a probe failure just yields a plain listing.
|
|
1909
1914
|
*/
|
|
1910
|
-
async function maybeLiveIndex(options) {
|
|
1915
|
+
export async function maybeLiveIndex(options) {
|
|
1911
1916
|
if (options.live === false || options.json)
|
|
1912
1917
|
return undefined;
|
|
1913
1918
|
try {
|
|
1914
|
-
|
|
1919
|
+
// `--local` promises "this machine only" for the default listing too (see
|
|
1920
|
+
// the `--local` help text), so it must gate the live-enrichment probe the
|
|
1921
|
+
// same way `--active --local` does — never dial a remote-host teammate
|
|
1922
|
+
// over ssh here either (RUSH-2118).
|
|
1923
|
+
return indexActiveBySessionId(await getActiveSessions({ localOnly: options.local === true }));
|
|
1915
1924
|
}
|
|
1916
1925
|
catch {
|
|
1917
1926
|
return undefined;
|
|
@@ -3387,7 +3396,6 @@ export function registerSessionsCommands(program) {
|
|
|
3387
3396
|
await sessionsAction(query, options, command.getOptionValueSource('limit'));
|
|
3388
3397
|
});
|
|
3389
3398
|
registerSessionsTailCommand(sessionsCmd);
|
|
3390
|
-
registerSessionsSyncCommand(sessionsCmd);
|
|
3391
3399
|
registerSessionsResumeCommand(sessionsCmd);
|
|
3392
3400
|
registerSessionsFavoriteCommand(sessionsCmd);
|
|
3393
3401
|
registerGoCommand(sessionsCmd);
|
|
@@ -13,6 +13,7 @@ import { findFirstInstalledBrowser, listInstalledBrowsers } from '../lib/browser
|
|
|
13
13
|
import { DEFAULT_BROWSER_PROFILE_NAME, createProfile, findFreeProfilePort, getConfiguredDefaultProfileName, getProfile, } from '../lib/browser/profiles.js';
|
|
14
14
|
import { DEFAULT_VIEWPORT } from '../lib/browser/devices.js';
|
|
15
15
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
16
|
+
import { defaultBrowserChoice } from './setup-preferences.js';
|
|
16
17
|
const INSTALL_HINT = 'Install one of: Google Chrome, Brave, Microsoft Edge, Chromium, or Comet, then re-run `agents setup browser`.\n' +
|
|
17
18
|
'(Safari and Firefox are not supported — agents browser drives over the Chrome DevTools Protocol.)';
|
|
18
19
|
/**
|
|
@@ -58,11 +59,14 @@ export async function runBrowserWizard() {
|
|
|
58
59
|
const { deleteProfile } = await import('../lib/browser/profiles.js');
|
|
59
60
|
await deleteProfile(existing.name);
|
|
60
61
|
}
|
|
61
|
-
// Pick which installed browser to pin (auto-select if only one).
|
|
62
|
+
// Pick which installed browser to pin (auto-select if only one). The picker
|
|
63
|
+
// highlights the same browser auto-detect would win — choosing is an
|
|
64
|
+
// override of the default, never a guess.
|
|
62
65
|
let chosen = installed[0];
|
|
63
66
|
if (installed.length > 1) {
|
|
64
67
|
const value = await select({
|
|
65
68
|
message: 'Which browser should the default profile use?',
|
|
69
|
+
default: defaultBrowserChoice(installed) ?? undefined,
|
|
66
70
|
choices: installed.map((b) => ({ name: `${b.browserType} ${chalk.dim(b.binary)}`, value: b.browserType })),
|
|
67
71
|
});
|
|
68
72
|
chosen = installed.find((b) => b.browserType === value) ?? installed[0];
|
|
@@ -12,6 +12,7 @@ import { loadDevices } from '../lib/devices/registry.js';
|
|
|
12
12
|
import { runDeviceSync } from '../lib/devices/sync.js';
|
|
13
13
|
import { tailscaleStatusJson } from '../lib/devices/tailscale.js';
|
|
14
14
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
15
|
+
import { maybePickInteractiveHost } from './setup-preferences.js';
|
|
15
16
|
function parseAuth(raw) {
|
|
16
17
|
const value = (raw ?? 'key').toLowerCase();
|
|
17
18
|
if (value === 'key' || value === 'password')
|
|
@@ -171,6 +172,10 @@ export async function runFleetSetupWizard(opts = {}) {
|
|
|
171
172
|
if (choices.fleetUpdate)
|
|
172
173
|
runAgentsSubcommand(['fleet', 'update']);
|
|
173
174
|
printOnboardingSummary(names, choices.auth);
|
|
175
|
+
// With a fleet registered, the next wrong-machine trap is artifacts opening on
|
|
176
|
+
// some other box — offer to pin the interactive host (TTY-only, skippable,
|
|
177
|
+
// silent non-TTY and when already set).
|
|
178
|
+
await maybePickInteractiveHost();
|
|
174
179
|
return true;
|
|
175
180
|
}
|
|
176
181
|
/** Register `agents setup fleet` under the parent `setup` command. */
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preferences onboarding — the guided path over the same device-config keys the
|
|
3
|
+
* `agents devices …` commands write (lib/device-config.ts). Two questions, each
|
|
4
|
+
* TTY-only, skippable, and absent non-interactively:
|
|
5
|
+
*
|
|
6
|
+
* 1. "Which machine do you sit at?" → `interactive.host` (central config:)
|
|
7
|
+
* 2. "Which browser should agents drive on THIS machine?"
|
|
8
|
+
* → `browser.profile` (device-local default)
|
|
9
|
+
*
|
|
10
|
+
* Shared by the bare `agents setup` flow (a short step after the capability
|
|
11
|
+
* hub) and by `agents setup fleet` (interactive host after a successful sync).
|
|
12
|
+
* Unset always keeps today's behavior — skipping is a real choice, not a
|
|
13
|
+
* partial state.
|
|
14
|
+
*/
|
|
15
|
+
import { type DeviceRegistry } from '../lib/devices/registry.js';
|
|
16
|
+
/** Registered macOS device names, sorted — the interactive-host candidates. */
|
|
17
|
+
export declare function macDeviceNames(reg: DeviceRegistry): string[];
|
|
18
|
+
/**
|
|
19
|
+
* The interactive-host picker's highlighted default: this machine when it is a
|
|
20
|
+
* candidate (the common case — you run setup on the box you sit at), else the
|
|
21
|
+
* first candidate. null when there are no candidates.
|
|
22
|
+
*/
|
|
23
|
+
export declare function defaultInteractiveHostChoice(candidates: string[], self?: string): string | null;
|
|
24
|
+
/**
|
|
25
|
+
* The browser picker's highlighted default — the same browser auto-detect
|
|
26
|
+
* would win, since `listInstalledBrowsers` returns platform priority order
|
|
27
|
+
* (macOS: chrome first). null when nothing is installed.
|
|
28
|
+
*/
|
|
29
|
+
export declare function defaultBrowserChoice<T extends {
|
|
30
|
+
browserType: string;
|
|
31
|
+
}>(installed: T[]): T['browserType'] | null;
|
|
32
|
+
/**
|
|
33
|
+
* Offer to set the interactive host when none is configured and the registry
|
|
34
|
+
* has more than one macOS device. Returns true when a host was set. Silent
|
|
35
|
+
* no-op non-TTY, when already set, or with fewer than two candidates (the
|
|
36
|
+
* answer is obvious or absent).
|
|
37
|
+
*/
|
|
38
|
+
export declare function maybePickInteractiveHost(): Promise<boolean>;
|
|
39
|
+
/**
|
|
40
|
+
* Offer to pin this machine's default browser profile to a chosen browser.
|
|
41
|
+
* Only asked when there is nothing to preserve: no configured device default
|
|
42
|
+
* AND no existing `default` profile (an existing profile is the user's earlier
|
|
43
|
+
* choice — never re-pinned behind their back). Returns true when a profile was
|
|
44
|
+
* created and set as the device default. Silent no-op non-TTY.
|
|
45
|
+
*/
|
|
46
|
+
export declare function maybePickBrowserProfile(): Promise<boolean>;
|
|
47
|
+
/**
|
|
48
|
+
* The bare-`agents setup` preferences step: interactive host, then browser.
|
|
49
|
+
* Runs AFTER the capability hub so it never delays the bootstrap. Never
|
|
50
|
+
* throws — a prompt cancel or a failed pick ends the step quietly and lets
|
|
51
|
+
* setup complete (the same semantics as the capability hub).
|
|
52
|
+
*/
|
|
53
|
+
export declare function runPreferencesStep(): Promise<void>;
|