@phnx-labs/agents-cli 1.22.30 → 1.22.32
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 +72 -0
- package/README.md +13 -6
- package/dist/bin/agents +0 -0
- package/dist/commands/daemon.js +52 -12
- package/dist/commands/doctor.d.ts +19 -0
- package/dist/commands/doctor.js +119 -17
- package/dist/commands/focus.d.ts +4 -1
- package/dist/commands/focus.js +19 -4
- package/dist/commands/routines.js +164 -36
- package/dist/commands/secrets.d.ts +20 -0
- package/dist/commands/secrets.js +59 -93
- package/dist/commands/{sessions-favorite.d.ts → sessions-bookmark.d.ts} +7 -7
- package/dist/commands/{sessions-favorite.js → sessions-bookmark.js} +38 -38
- package/dist/commands/sessions-browser.d.ts +10 -8
- package/dist/commands/sessions-browser.js +61 -32
- package/dist/commands/sessions-stats.js +1 -1
- package/dist/commands/sessions.d.ts +11 -9
- package/dist/commands/sessions.js +114 -65
- package/dist/commands/update.d.ts +2 -0
- package/dist/commands/update.js +148 -0
- package/dist/index.js +4 -2
- package/dist/lib/catchup.js +4 -1
- package/dist/lib/daemon.d.ts +17 -0
- package/dist/lib/daemon.js +69 -3
- package/dist/lib/devices/doctor-findings.d.ts +7 -2
- package/dist/lib/devices/doctor-findings.js +53 -2
- package/dist/lib/devices/doctor-overview-cache.d.ts +7 -0
- package/dist/lib/devices/doctor-overview-cache.js +15 -0
- package/dist/lib/devices/fleet-divergence.d.ts +11 -0
- package/dist/lib/devices/fleet-divergence.js +6 -0
- package/dist/lib/devices/fleet-inventory.js +16 -2
- package/dist/lib/drift.d.ts +6 -1
- package/dist/lib/drift.js +9 -0
- package/dist/lib/hooks/cache.js +20 -1
- package/dist/lib/hooks.d.ts +91 -1
- package/dist/lib/hooks.js +289 -3
- package/dist/lib/hosts/passthrough.js +3 -0
- package/dist/lib/installations/index.d.ts +14 -0
- package/dist/lib/installations/index.js +14 -0
- package/dist/lib/installations/resolve.d.ts +43 -0
- package/dist/lib/installations/resolve.js +93 -0
- package/dist/lib/installations/store.d.ts +56 -0
- package/dist/lib/installations/store.js +196 -0
- package/dist/lib/installations/strategies.d.ts +73 -0
- package/dist/lib/installations/strategies.js +293 -0
- package/dist/lib/installations/types.d.ts +78 -0
- package/dist/lib/installations/types.js +8 -0
- package/dist/lib/installations/update.d.ts +40 -0
- package/dist/lib/installations/update.js +131 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/migrate.d.ts +27 -0
- package/dist/lib/migrate.js +118 -2
- package/dist/lib/picker.d.ts +6 -3
- package/dist/lib/picker.js +7 -2
- package/dist/lib/routine-context.d.ts +144 -0
- package/dist/lib/routine-context.js +268 -0
- package/dist/lib/routine-readiness.d.ts +47 -0
- package/dist/lib/routine-readiness.js +239 -0
- package/dist/lib/routines.d.ts +97 -1
- package/dist/lib/routines.js +107 -1
- package/dist/lib/runner.d.ts +18 -4
- package/dist/lib/runner.js +291 -98
- package/dist/lib/scheduler.d.ts +7 -1
- package/dist/lib/scheduler.js +5 -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/agent.d.ts +0 -1
- package/dist/lib/secrets/agent.js +0 -4
- package/dist/lib/secrets/session-store.d.ts +0 -4
- package/dist/lib/secrets/session-store.js +0 -5
- package/dist/lib/self-heal/checks/hook-runtime.d.ts +2 -0
- package/dist/lib/self-heal/checks/hook-runtime.js +16 -0
- package/dist/lib/self-heal/registry.js +5 -2
- package/dist/lib/self-heal/types.d.ts +1 -1
- package/dist/lib/session/{favorites.d.ts → bookmarks.d.ts} +14 -14
- package/dist/lib/session/{favorites.js → bookmarks.js} +22 -22
- package/dist/lib/session/state.js +4 -1
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/versions.d.ts +24 -0
- package/dist/lib/versions.js +49 -16
- package/package.json +2 -2
|
@@ -43,6 +43,7 @@ import { sessionOwnerDevice, RESUME_PINNED_ENV } from '../lib/session/resume-own
|
|
|
43
43
|
import { renderMarkdown } from '../lib/markdown.js';
|
|
44
44
|
import { AGENTS, colorAgent, resolveAgentName } from '../lib/agents.js';
|
|
45
45
|
import { getShimsDir } from '../lib/state.js';
|
|
46
|
+
import { listJobs, listJobsWithRuns, listRuns } from '../lib/routines.js';
|
|
46
47
|
import { fuzzyMatch, FUZZY_PRESETS } from '../lib/fuzzy.js';
|
|
47
48
|
import { itemPicker } from '../lib/picker.js';
|
|
48
49
|
import { resolveSessionAlias } from '../lib/session/actor-sidecar.js';
|
|
@@ -55,8 +56,8 @@ import { setHelpSections } from '../lib/help.js';
|
|
|
55
56
|
import { registerSessionsTailCommand } from './sessions-tail.js';
|
|
56
57
|
import { registerSessionsResumeCommand } from './sessions-resume.js';
|
|
57
58
|
import { registerSessionsForkCommand } from './fork.js';
|
|
58
|
-
import {
|
|
59
|
-
import {
|
|
59
|
+
import { registerSessionsBookmarkCommand } from './sessions-bookmark.js';
|
|
60
|
+
import { isBookmarked, listBookmarks } from '../lib/session/bookmarks.js';
|
|
60
61
|
import { registerGoCommand } from './go.js';
|
|
61
62
|
import { registerFocusCommand } from './focus.js';
|
|
62
63
|
import { registerReconnectCommand } from './reconnect.js';
|
|
@@ -1361,26 +1362,25 @@ async function renderActiveSessions(asJson, waitingOnly = false, opts = {}) {
|
|
|
1361
1362
|
const self = machineId();
|
|
1362
1363
|
const gathered = await gatherActiveSessions(opts);
|
|
1363
1364
|
const { remoteDeviceCount } = gathered;
|
|
1364
|
-
//
|
|
1365
|
+
// Backfill agent version, refs, created time, and routine provenance from the
|
|
1366
|
+
// historical index. Routine filtering needs that provenance before it narrows
|
|
1367
|
+
// the live pool; doing it here also enriches both JSON and human output.
|
|
1368
|
+
backfillActiveRowsFromIndex(gathered.sessions);
|
|
1369
|
+
// --bookmarks narrows the live view too. Applied HERE, not only in the
|
|
1365
1370
|
// browser: the browser is skipped for --json, --waiting, a pipe, a multi-host
|
|
1366
1371
|
// scope, and an SSH-fanout peer, and the flag silently did nothing on every
|
|
1367
|
-
// one of those paths — including `--active --
|
|
1372
|
+
// one of those paths — including `--active --bookmarks --json`, which is
|
|
1368
1373
|
// exactly what the browser's own `y` copy-cmd hands to an agent.
|
|
1369
|
-
const merged = opts.
|
|
1370
|
-
? gathered.sessions.filter((s) => !!s.sessionId &&
|
|
1374
|
+
const merged = opts.bookmarksOnly
|
|
1375
|
+
? gathered.sessions.filter((s) => !!s.sessionId && listBookmarks().has(s.sessionId))
|
|
1371
1376
|
: gathered.sessions;
|
|
1377
|
+
const routineFiltered = filterActiveSessionsByRoutine(merged, opts.routine);
|
|
1372
1378
|
// Status flags form a union. --waiting additionally retains its scriptable
|
|
1373
1379
|
// gate: exit non-zero when the union contains a session awaiting the user.
|
|
1374
1380
|
const statusFiltered = opts.statuses?.length
|
|
1375
|
-
?
|
|
1376
|
-
:
|
|
1381
|
+
? routineFiltered.filter((session) => opts.statuses.some((status) => matchesLiveStatus(session, status)))
|
|
1382
|
+
: routineFiltered.filter(isRunningLiveSession);
|
|
1377
1383
|
const sessions = statusFiltered;
|
|
1378
|
-
// Backfill agent version + ticket/PR/label/created onto the live rows from the
|
|
1379
|
-
// historical index (RUSH-2205) — a running process reports none of these, and
|
|
1380
|
-
// an orphan row usually lacks them. Done before both the JSON and human paths
|
|
1381
|
-
// so every consumer (incl. the SSH fan-out's remote --json) sees enriched rows;
|
|
1382
|
-
// transcripts sync across the fleet, so a remote row resolves from the local DB.
|
|
1383
|
-
backfillActiveRowsFromIndex(sessions);
|
|
1384
1384
|
if (asJson) {
|
|
1385
1385
|
// Resolve who is watching each local tmux pane before serializing: `viewingIn`
|
|
1386
1386
|
// is how a consumer distinguishes a session someone is looking at from one
|
|
@@ -1426,6 +1426,19 @@ async function renderActiveSessions(asJson, waitingOnly = false, opts = {}) {
|
|
|
1426
1426
|
if (waitingOnly && sessions.some(isAwaitingUser))
|
|
1427
1427
|
process.exitCode = 1;
|
|
1428
1428
|
}
|
|
1429
|
+
/** Apply the routine selector to enriched live rows for every non-browser active view. */
|
|
1430
|
+
export function filterActiveSessionsByRoutine(sessions, routine) {
|
|
1431
|
+
if (!routine)
|
|
1432
|
+
return sessions;
|
|
1433
|
+
const routineSessions = sessions.filter((session) => session.origin === 'routine' || !!session.routineName);
|
|
1434
|
+
if (typeof routine !== 'string')
|
|
1435
|
+
return routineSessions;
|
|
1436
|
+
const names = [...new Set(routineSessions.map((session) => session.routineName).filter((name) => !!name))];
|
|
1437
|
+
const selected = resolveRoutineName(routine, names);
|
|
1438
|
+
return selected
|
|
1439
|
+
? routineSessions.filter((session) => session.routineName === selected)
|
|
1440
|
+
: [];
|
|
1441
|
+
}
|
|
1429
1442
|
/** Match the status words users see, preserving activity's richer working signal. */
|
|
1430
1443
|
export function matchesLiveStatus(session, status) {
|
|
1431
1444
|
if (status === 'working')
|
|
@@ -1492,7 +1505,9 @@ export function isBareBrowserListing(options, query) {
|
|
|
1492
1505
|
*/
|
|
1493
1506
|
export function hasNoBrowserDisqualifyingFlags(options, query) {
|
|
1494
1507
|
return (!query &&
|
|
1495
|
-
|
|
1508
|
+
// A named team view is a flat selectable pool; the bare --teams report
|
|
1509
|
+
// stays grouped and printed because the browser cannot preserve its shape.
|
|
1510
|
+
(!options.teams || !!options.inTeam) &&
|
|
1496
1511
|
!options.flat &&
|
|
1497
1512
|
!options.tree &&
|
|
1498
1513
|
!options.markdown &&
|
|
@@ -1543,14 +1558,14 @@ export function buildRoutineRunGroups(sessions) {
|
|
|
1543
1558
|
}))
|
|
1544
1559
|
.sort((a, b) => (a.timestamp < b.timestamp ? 1 : a.timestamp > b.timestamp ? -1 : a.runId.localeCompare(b.runId)));
|
|
1545
1560
|
}
|
|
1546
|
-
export function buildRoutineChoices(sessions) {
|
|
1561
|
+
export function buildRoutineChoices(sessions, runOnlyNames = []) {
|
|
1547
1562
|
const byName = new Map();
|
|
1548
1563
|
for (const session of sessions) {
|
|
1549
1564
|
if (!session.routineName)
|
|
1550
1565
|
continue;
|
|
1551
1566
|
(byName.get(session.routineName) ?? byName.set(session.routineName, []).get(session.routineName)).push(session);
|
|
1552
1567
|
}
|
|
1553
|
-
|
|
1568
|
+
const choices = [...byName.entries()]
|
|
1554
1569
|
.map(([name, rows]) => {
|
|
1555
1570
|
const runs = buildRoutineRunGroups(rows);
|
|
1556
1571
|
return {
|
|
@@ -1559,11 +1574,28 @@ export function buildRoutineChoices(sessions) {
|
|
|
1559
1574
|
runCount: runs.length,
|
|
1560
1575
|
latestRunSessionCount: runs[0].sessions.length,
|
|
1561
1576
|
};
|
|
1562
|
-
})
|
|
1563
|
-
|
|
1577
|
+
});
|
|
1578
|
+
// Routines whose only attempts are pre-session (blocked/skipped/failed-before-
|
|
1579
|
+
// spawn) have run records but no transcript. Surface them so the picker is
|
|
1580
|
+
// built from definitions+runs, never only transcripts — otherwise a routine
|
|
1581
|
+
// that has never produced a session reads as "No routines match" (the plan).
|
|
1582
|
+
const seen = new Set(choices.map((c) => c.name));
|
|
1583
|
+
for (const name of runOnlyNames) {
|
|
1584
|
+
if (seen.has(name))
|
|
1585
|
+
continue;
|
|
1586
|
+
const runs = listRuns(name);
|
|
1587
|
+
const latest = runs[runs.length - 1];
|
|
1588
|
+
choices.push({
|
|
1589
|
+
name,
|
|
1590
|
+
lastRunAt: latest?.startedAt ?? '',
|
|
1591
|
+
runCount: runs.length,
|
|
1592
|
+
latestRunSessionCount: 0,
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
return choices.sort((a, b) => (a.lastRunAt < b.lastRunAt ? 1 : a.lastRunAt > b.lastRunAt ? -1 : a.name.localeCompare(b.name)));
|
|
1564
1596
|
}
|
|
1565
1597
|
async function selectRoutineName(sessions) {
|
|
1566
|
-
const choices = buildRoutineChoices(sessions);
|
|
1598
|
+
const choices = buildRoutineChoices(sessions, safeListJobsWithRuns());
|
|
1567
1599
|
const picked = await itemPicker({
|
|
1568
1600
|
message: 'Select a routine:',
|
|
1569
1601
|
items: choices,
|
|
@@ -1576,7 +1608,8 @@ async function selectRoutineName(sessions) {
|
|
|
1576
1608
|
labelFor: (choice) => {
|
|
1577
1609
|
const runs = `${choice.runCount} run${choice.runCount === 1 ? '' : 's'}`;
|
|
1578
1610
|
const sessions = `${choice.latestRunSessionCount} session${choice.latestRunSessionCount === 1 ? '' : 's'} in latest`;
|
|
1579
|
-
|
|
1611
|
+
const age = choice.lastRunAt ? ` · ${formatRelativeTime(choice.lastRunAt)}` : '';
|
|
1612
|
+
return `${choice.name} ${chalk.gray(`${runs} · ${sessions}${age}`)}`;
|
|
1580
1613
|
},
|
|
1581
1614
|
shortIdFor: (choice) => choice.name,
|
|
1582
1615
|
emptyMessage: 'No routines match.',
|
|
@@ -1584,8 +1617,22 @@ async function selectRoutineName(sessions) {
|
|
|
1584
1617
|
});
|
|
1585
1618
|
return picked?.item.name ?? null;
|
|
1586
1619
|
}
|
|
1620
|
+
/** `listJobsWithRuns` but never throws into the sessions reader (best-effort). */
|
|
1621
|
+
function safeListJobsWithRuns() {
|
|
1622
|
+
try {
|
|
1623
|
+
return [...new Set([...listJobs().map((job) => job.name), ...listJobsWithRuns()])];
|
|
1624
|
+
}
|
|
1625
|
+
catch {
|
|
1626
|
+
return [];
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1587
1629
|
export async function filterSessionsByRoutine(sessions, routine, interactive) {
|
|
1588
|
-
|
|
1630
|
+
// The name universe is transcripts UNION routines that have any run record —
|
|
1631
|
+
// so a routine whose attempts never produced a session still resolves.
|
|
1632
|
+
const routineNames = [...new Set([
|
|
1633
|
+
...sessions.map((session) => session.routineName).filter((name) => !!name),
|
|
1634
|
+
...safeListJobsWithRuns(),
|
|
1635
|
+
])].sort((a, b) => a.localeCompare(b));
|
|
1589
1636
|
let selectedRoutine = null;
|
|
1590
1637
|
if (typeof routine === 'string') {
|
|
1591
1638
|
selectedRoutine = resolveRoutineName(routine, routineNames);
|
|
@@ -1602,9 +1649,10 @@ export async function filterSessionsByRoutine(sessions, routine, interactive) {
|
|
|
1602
1649
|
if (!selectedRoutine)
|
|
1603
1650
|
return null;
|
|
1604
1651
|
}
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1652
|
+
if (!selectedRoutine)
|
|
1653
|
+
return sessions;
|
|
1654
|
+
const matched = sessions.filter((session) => session.routineName === selectedRoutine);
|
|
1655
|
+
return matched;
|
|
1608
1656
|
}
|
|
1609
1657
|
/** The canonical `ag sessions …` command for a set of flags — the twin of the
|
|
1610
1658
|
* browser's `y` hotkey (see --print-cmd). Normalizes to the stable flag form. */
|
|
@@ -1657,8 +1705,8 @@ function canonicalSessionsCommand(query, options) {
|
|
|
1657
1705
|
a.push('--local');
|
|
1658
1706
|
if (options.waiting)
|
|
1659
1707
|
a.push('--waiting');
|
|
1660
|
-
if (options.
|
|
1661
|
-
a.push('--
|
|
1708
|
+
if (options.bookmarks)
|
|
1709
|
+
a.push('--bookmarks');
|
|
1662
1710
|
const q = (query ?? '').trim();
|
|
1663
1711
|
if (q)
|
|
1664
1712
|
a.push(JSON.stringify(q));
|
|
@@ -1768,7 +1816,7 @@ export async function renderSessionPreview(query, scope) {
|
|
|
1768
1816
|
}));
|
|
1769
1817
|
return;
|
|
1770
1818
|
}
|
|
1771
|
-
const headline = formatLiveStatusHeadline(live,
|
|
1819
|
+
const headline = formatLiveStatusHeadline(live, isBookmarked(session.id));
|
|
1772
1820
|
if (headline)
|
|
1773
1821
|
console.log(headline);
|
|
1774
1822
|
console.log(buildPreview(session));
|
|
@@ -1783,12 +1831,12 @@ export async function renderSessionPreview(query, scope) {
|
|
|
1783
1831
|
* reads "orphan" and knows it means "still running in tmux with no window
|
|
1784
1832
|
* attached", so those two spell it out.
|
|
1785
1833
|
*/
|
|
1786
|
-
export function formatLiveStatusHeadline(live,
|
|
1787
|
-
const star =
|
|
1834
|
+
export function formatLiveStatusHeadline(live, bookmarked = false) {
|
|
1835
|
+
const star = bookmarked ? chalk.yellow('★ ') : '';
|
|
1788
1836
|
// With no live row there is no status to lead with, so the star has to say
|
|
1789
1837
|
// what it means on its own — a bare `★` above a preview reads as noise.
|
|
1790
1838
|
if (!live)
|
|
1791
|
-
return
|
|
1839
|
+
return bookmarked ? chalk.yellow('★ bookmarked') : '';
|
|
1792
1840
|
const { glyph } = liveGlyphAndPreview(live);
|
|
1793
1841
|
const word = liveStatusWord(live) || live.status;
|
|
1794
1842
|
// One definition, shared with `--waiting`. A second local copy drifted: the
|
|
@@ -2140,7 +2188,8 @@ limitSource) {
|
|
|
2140
2188
|
host: options.host,
|
|
2141
2189
|
since: options.since,
|
|
2142
2190
|
all: options.all,
|
|
2143
|
-
|
|
2191
|
+
bookmarks: options.bookmarks,
|
|
2192
|
+
routine: options.routine,
|
|
2144
2193
|
}), { local: options.local === true, hosts: options.host });
|
|
2145
2194
|
return;
|
|
2146
2195
|
}
|
|
@@ -2150,8 +2199,9 @@ limitSource) {
|
|
|
2150
2199
|
await renderActiveSessions(options.json === true, options.waiting === true, {
|
|
2151
2200
|
local: forceLocal,
|
|
2152
2201
|
hosts: options.host,
|
|
2153
|
-
|
|
2202
|
+
bookmarksOnly: options.bookmarks === true,
|
|
2154
2203
|
statuses: liveStatuses,
|
|
2204
|
+
routine: options.routine,
|
|
2155
2205
|
});
|
|
2156
2206
|
return;
|
|
2157
2207
|
}
|
|
@@ -2165,12 +2215,10 @@ limitSource) {
|
|
|
2165
2215
|
// printed/render paths (agents and scripts unaffected). An explicit --since seeds
|
|
2166
2216
|
// the browser's window so the flag is honored, not swallowed.
|
|
2167
2217
|
//
|
|
2168
|
-
// `--teams` diverts to the printed team-grouped report
|
|
2169
|
-
//
|
|
2170
|
-
//
|
|
2171
|
-
|
|
2172
|
-
// the picker so `<term> --teams` still searches.
|
|
2173
|
-
if (isBareBrowserListing(options, query) && !options.teams) {
|
|
2218
|
+
// Bare `--teams` still diverts to the printed team-grouped report because the
|
|
2219
|
+
// flat picker cannot preserve that shape. `--in-team <name> --teams` is one
|
|
2220
|
+
// flat lineage, so it qualifies through hasNoBrowserDisqualifyingFlags.
|
|
2221
|
+
if (isBareBrowserListing(options, query)) {
|
|
2174
2222
|
const { runSessionBrowser, bareBrowserSeed } = await import('./sessions-browser.js');
|
|
2175
2223
|
await runSessionBrowser(bareBrowserSeed({
|
|
2176
2224
|
teams: options.teams,
|
|
@@ -2179,7 +2227,8 @@ limitSource) {
|
|
|
2179
2227
|
since: options.since,
|
|
2180
2228
|
host: options.host,
|
|
2181
2229
|
inTeam: options.inTeam,
|
|
2182
|
-
|
|
2230
|
+
bookmarks: options.bookmarks,
|
|
2231
|
+
routine: options.routine,
|
|
2183
2232
|
}), { local: options.local === true, hosts: options.host });
|
|
2184
2233
|
return;
|
|
2185
2234
|
}
|
|
@@ -2349,11 +2398,11 @@ limitSource) {
|
|
|
2349
2398
|
// read. Match either, after that pass has populated `teamOrigin`.
|
|
2350
2399
|
if (options.inTeam)
|
|
2351
2400
|
sessions = sessions.filter((s) => matchesTeam(s, options.inTeam));
|
|
2352
|
-
// --
|
|
2353
|
-
// emit, so `--
|
|
2354
|
-
if (options.
|
|
2355
|
-
const
|
|
2356
|
-
sessions = sessions.filter((s) =>
|
|
2401
|
+
// --bookmarks narrows to the bookmarked set. Applied here, before the JSON
|
|
2402
|
+
// emit, so `--bookmarks --json` is the machine-readable twin of the `b` key.
|
|
2403
|
+
if (options.bookmarks) {
|
|
2404
|
+
const bookmarks = listBookmarks();
|
|
2405
|
+
sessions = sessions.filter((s) => bookmarks.has(s.id));
|
|
2357
2406
|
}
|
|
2358
2407
|
if (toolEvidenceMode) {
|
|
2359
2408
|
const self = toolSelf;
|
|
@@ -2659,7 +2708,7 @@ function timeCell(age, topicSlack) {
|
|
|
2659
2708
|
* (tracker/PR ref, pulled out of the badge blob so refs align) is only rendered
|
|
2660
2709
|
* when `showTicket` — otherwise a listing with no refs would waste a column of
|
|
2661
2710
|
* dashes and needlessly truncate the topic. Worktree stays a trailing badge. */
|
|
2662
|
-
export function flatSessionRow(session, live, showTicket = false, cols = {},
|
|
2711
|
+
export function flatSessionRow(session, live, showTicket = false, cols = {}, bookmarked = false) {
|
|
2663
2712
|
const agentColor = colorAgent(session.agent);
|
|
2664
2713
|
const age = sessionAgeParts(session.timestamp, session.lastActivity);
|
|
2665
2714
|
const project = session.project || '-';
|
|
@@ -2696,18 +2745,18 @@ export function flatSessionRow(session, live, showTicket = false, cols = {}, fav
|
|
|
2696
2745
|
const width = terminalWidth();
|
|
2697
2746
|
const requestedModelW = cols.showModel ? (cols.modelWidth ?? PICKER_MODEL_MAX) : 0;
|
|
2698
2747
|
// Same conditional 2 cells as the picker's marker, for the same reason.
|
|
2699
|
-
const
|
|
2700
|
-
const
|
|
2748
|
+
const bookmarkW = cols.showBookmark ? 2 : 0;
|
|
2749
|
+
const bookmarkCell = cols.showBookmark ? (bookmarked ? chalk.yellow('★ ') : ' ') : '';
|
|
2701
2750
|
// Sized against the last-activity label alone, so the creation field is an
|
|
2702
2751
|
// additive decision the row makes only once it knows what space is left.
|
|
2703
|
-
const fixedW =
|
|
2752
|
+
const fixedW = bookmarkW + (10 + 9 + 8 + 16) + glyphW + statusW + machineW + ticketW + wtW + team.width + stringWidth(age.last) + 1;
|
|
2704
2753
|
const modelSlack = width - fixedW - MIN_TOPIC_W;
|
|
2705
2754
|
const modelW = requestedModelW <= modelSlack
|
|
2706
2755
|
? requestedModelW
|
|
2707
2756
|
: modelSlack >= PICKER_MODEL_MIN ? modelSlack : 0;
|
|
2708
2757
|
const when = timeCell(age, width - fixedW - modelW);
|
|
2709
2758
|
const topicW = Math.max(MIN_TOPIC_W, width - fixedW - modelW - when.extraW);
|
|
2710
|
-
return (
|
|
2759
|
+
return (bookmarkCell +
|
|
2711
2760
|
chalk.white(padToWidth(truncateToWidth(session.shortId, 9), 10)) +
|
|
2712
2761
|
agentColor(padToWidth(truncateToWidth(session.agent, 8), 9)) +
|
|
2713
2762
|
chalk.yellow(padToWidth(truncateToWidth(session.version || '-', 7), 8)) +
|
|
@@ -2907,9 +2956,9 @@ function printSessionTable(sessions, hiddenCount = 0, tree = false, liveIndex) {
|
|
|
2907
2956
|
// column (and its compact labels) is computed the same way the picker does it.
|
|
2908
2957
|
const showTicket = sessions.some((s) => ticketLabel(s) !== '');
|
|
2909
2958
|
const cols = pickerColumnsFor(sessions);
|
|
2910
|
-
const
|
|
2959
|
+
const bookmarks = listBookmarks();
|
|
2911
2960
|
for (const session of sessions) {
|
|
2912
|
-
console.log(flatSessionRow(session, liveIndex?.get(session.id), showTicket, cols,
|
|
2961
|
+
console.log(flatSessionRow(session, liveIndex?.get(session.id), showTicket, cols, bookmarks.has(session.id)));
|
|
2913
2962
|
}
|
|
2914
2963
|
const countLine = `${sessions.length} session${sessions.length === 1 ? '' : 's'}.`;
|
|
2915
2964
|
console.log(chalk.gray(`\n${countLine}`));
|
|
@@ -3252,10 +3301,10 @@ export function pickerColumnsFor(sessions) {
|
|
|
3252
3301
|
showModel: sessions.some((s) => !!s.model),
|
|
3253
3302
|
modelWidth: modelColumnWidth(sessions),
|
|
3254
3303
|
showTicket: sessions.some((s) => ticketLabel(s) !== ''),
|
|
3255
|
-
|
|
3304
|
+
showBookmark: (() => {
|
|
3256
3305
|
// One read of the store per pool, not one per row.
|
|
3257
|
-
const
|
|
3258
|
-
return
|
|
3306
|
+
const bookmarks = listBookmarks();
|
|
3307
|
+
return bookmarks.size > 0 && sessions.some((s) => bookmarks.has(s.id));
|
|
3259
3308
|
})(),
|
|
3260
3309
|
};
|
|
3261
3310
|
}
|
|
@@ -3275,7 +3324,7 @@ export function liveHostLabel(a) {
|
|
|
3275
3324
|
const viewer = a.viewingIn?.app;
|
|
3276
3325
|
return viewer && viewer !== a.host ? `${a.host}→${viewer}` : a.host;
|
|
3277
3326
|
}
|
|
3278
|
-
export function formatPickerLabel(s, query, cols = {}, ssh, host = '',
|
|
3327
|
+
export function formatPickerLabel(s, query, cols = {}, ssh, host = '', bookmarked = false, live) {
|
|
3279
3328
|
const agentColor = colorAgent(s.agent);
|
|
3280
3329
|
const age = sessionAgeParts(s.timestamp, s.lastActivity);
|
|
3281
3330
|
const project = s.project || '-';
|
|
@@ -3317,11 +3366,11 @@ export function formatPickerLabel(s, query, cols = {}, ssh, host = '', favorite
|
|
|
3317
3366
|
const ticketW = cols.showTicket ? TICKET_W + 1 : 0;
|
|
3318
3367
|
const hostW = cols.showHost ? PICKER_HOST_W : 0;
|
|
3319
3368
|
const wtW = wt ? stringWidth(wt) + 1 : 0;
|
|
3320
|
-
// Within a pool that HAS
|
|
3321
|
-
// row is
|
|
3322
|
-
// drops the column entirely (`
|
|
3323
|
-
const
|
|
3324
|
-
const
|
|
3369
|
+
// Within a pool that HAS bookmarked rows the marker holds its 2 cells whether
|
|
3370
|
+
// this row is bookmarked or not, so the columns after it never jog; a pool
|
|
3371
|
+
// with none drops the column entirely (`showBookmark`) and costs nothing.
|
|
3372
|
+
const bookmarkW = cols.showBookmark ? 2 : 0;
|
|
3373
|
+
const bookmarkCell = cols.showBookmark ? (bookmarked ? chalk.yellow('★ ') : ' ') : '';
|
|
3325
3374
|
// The same status word the flat listing shows, so a session that is `orphan`
|
|
3326
3375
|
// or `crashed` reads that way in the browser too — not only in its preview.
|
|
3327
3376
|
// Constant width whenever the column is on. `liveStatusCell` already pads its
|
|
@@ -3333,10 +3382,10 @@ export function formatPickerLabel(s, query, cols = {}, ssh, host = '', favorite
|
|
|
3333
3382
|
const statusCell = cols.showStatus ? (status.cell || ' '.repeat(LIVE_STATUS_W)) : '';
|
|
3334
3383
|
// Sized against the last-activity label alone; the creation field is then an
|
|
3335
3384
|
// additive decision made against whatever width is left (see the flat listing).
|
|
3336
|
-
const baseTopicW = terminalWidth() - gutter -
|
|
3385
|
+
const baseTopicW = terminalWidth() - gutter - bookmarkW - statusW - (10 + 9 + 8 + 16) - machineColW - hostW - ticketW - wtW - sshW - team.width - stringWidth(age.last) - 1;
|
|
3337
3386
|
const when = timeCell(age, baseTopicW);
|
|
3338
3387
|
const topicW = Math.max(MIN_TOPIC_W, baseTopicW - when.extraW);
|
|
3339
|
-
return (
|
|
3388
|
+
return (bookmarkCell +
|
|
3340
3389
|
// Truncated, not just padded: an indexed shortId is always 8 chars, but a
|
|
3341
3390
|
// live row with no session id is named by its pid or cloud task, which can
|
|
3342
3391
|
// run past the column and shunt every later column out of alignment.
|
|
@@ -4459,6 +4508,7 @@ export function registerSessionsCommands(program) {
|
|
|
4459
4508
|
.option('--grok', 'Shorthand for --agent grok')
|
|
4460
4509
|
.option('--opencode', 'Shorthand for --agent opencode')
|
|
4461
4510
|
.option('--all', 'Widen every non-status filter to "all": every directory (not just this project) and all time (no window cap). Status filters like --active still compose; -a/--device/--since still narrow their axis.')
|
|
4511
|
+
.option('--bookmarks', 'Show only bookmarked sessions — bookmark them with `*` in the browser or `agents sessions bookmark <id>`')
|
|
4462
4512
|
.option('--unmanaged', "Also show sessions from your own ~/.<agent> installs (hidden once agents-cli manages that agent)")
|
|
4463
4513
|
.option('--team, --teams', 'Show team-spawned sessions (hidden by default), grouped by team — each team names its spawner and spawn time, teammates show their mode + handle, and team-flagged spawns with no teammate record sink into a (no team) bucket. --flat/--tree keep the plain inline table')
|
|
4464
4514
|
.option('--in-team <name>', "Only this team: the session that spawned it plus (with --teams) its teammates. Spans every directory and all time, since a team's worktrees and history sit outside the default window.")
|
|
@@ -4492,7 +4542,6 @@ export function registerSessionsCommands(program) {
|
|
|
4492
4542
|
.option('--abandoned', 'Show sessions with no transcript progress for the abandonment window (implies --active)')
|
|
4493
4543
|
.option('--queued', 'Show queued sessions that have not started running (implies --active)')
|
|
4494
4544
|
.option('--unknown', 'Show sessions whose live state cannot be determined (implies --active)')
|
|
4495
|
-
.option('--favorites', 'Show only favorited sessions — favorite them with `*` in the browser or `agents sessions favorite <id>`')
|
|
4496
4545
|
.option('--tree', 'Group the listing by directory; drops the id/version columns for readability')
|
|
4497
4546
|
.option('--flat', 'Plain flat table (one row per session) instead of the grouped project overview')
|
|
4498
4547
|
.option('--no-live', 'Do not enrich the listing with live status/preview for running sessions')
|
|
@@ -4662,7 +4711,7 @@ export function registerSessionsCommands(program) {
|
|
|
4662
4711
|
registerSessionsTailCommand(sessionsCmd);
|
|
4663
4712
|
registerSessionsResumeCommand(sessionsCmd);
|
|
4664
4713
|
registerSessionsForkCommand(sessionsCmd);
|
|
4665
|
-
|
|
4714
|
+
registerSessionsBookmarkCommand(sessionsCmd);
|
|
4666
4715
|
registerGoCommand(sessionsCmd);
|
|
4667
4716
|
registerFocusCommand(sessionsCmd);
|
|
4668
4717
|
registerReconnectCommand(sessionsCmd);
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { formatAgentError, resolveAgentName } from '../lib/agents.js';
|
|
3
|
+
import { setHelpSections } from '../lib/help.js';
|
|
4
|
+
import { describeInstallation, listInstallations, resolveInstallation, selectUpdateStrategy, supportsPinnedUpdate, updateInstallation, } from '../lib/installations/index.js';
|
|
5
|
+
/**
|
|
6
|
+
* Split `<agent>[@<selector>]`. The selector names an INSTALLATION — its frozen
|
|
7
|
+
* label, or the release it currently carries — never a release to install; that
|
|
8
|
+
* is `--to`. Keeping them separate is what lets `agents update claude@2.0.65
|
|
9
|
+
* --to 2.0.71` read unambiguously.
|
|
10
|
+
*/
|
|
11
|
+
function parseTarget(raw) {
|
|
12
|
+
const at = raw.indexOf('@');
|
|
13
|
+
const name = at === -1 ? raw : raw.slice(0, at);
|
|
14
|
+
const selector = at === -1 ? undefined : raw.slice(at + 1).trim();
|
|
15
|
+
if (at !== -1 && !selector) {
|
|
16
|
+
throw new Error(`Missing installation in '${raw}'. Use <agent>@<installed-version>, or just <agent>.`);
|
|
17
|
+
}
|
|
18
|
+
const agent = resolveAgentName(name);
|
|
19
|
+
if (!agent)
|
|
20
|
+
throw new Error(formatAgentError(name));
|
|
21
|
+
return { agent, selector };
|
|
22
|
+
}
|
|
23
|
+
function serialize(installation) {
|
|
24
|
+
return {
|
|
25
|
+
id: installation.id,
|
|
26
|
+
agent: installation.agent,
|
|
27
|
+
label: installation.label,
|
|
28
|
+
releaseVersion: installation.releaseVersion,
|
|
29
|
+
createdAt: installation.createdAt,
|
|
30
|
+
updatedAt: installation.updatedAt,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function printOutcome(outcome, json) {
|
|
34
|
+
if (json) {
|
|
35
|
+
console.log(JSON.stringify({
|
|
36
|
+
installation: serialize(outcome.installation),
|
|
37
|
+
strategy: outcome.strategy,
|
|
38
|
+
fromRelease: outcome.fromRelease,
|
|
39
|
+
toRelease: outcome.toRelease,
|
|
40
|
+
unchanged: outcome.unchanged,
|
|
41
|
+
alsoUpdated: outcome.alsoUpdated.map(serialize),
|
|
42
|
+
}, null, 2));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const name = `${outcome.installation.agent}@${outcome.installation.label}`;
|
|
46
|
+
if (outcome.unchanged) {
|
|
47
|
+
console.log(chalk.gray(`${name} is already on release ${outcome.toRelease}.`));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
console.log(chalk.green(`Updated ${name}: release ${outcome.fromRelease} -> ${outcome.toRelease}`));
|
|
51
|
+
console.log(chalk.gray(`Its name is unchanged, so every default, project pin, and routine that names ${name} still resolves to it.`));
|
|
52
|
+
for (const other of outcome.alsoUpdated) {
|
|
53
|
+
console.log(chalk.gray(` ${other.agent}@${other.label} shares the same binary and now also reports release ${other.releaseVersion}.`));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function printInstallations(agent, json) {
|
|
57
|
+
const installations = listInstallations(agent);
|
|
58
|
+
if (json) {
|
|
59
|
+
console.log(JSON.stringify(installations.map(serialize), null, 2));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (!installations.length) {
|
|
63
|
+
console.log(chalk.gray(`No managed ${agent} installations. Install one with: agents add ${agent}@latest`));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
console.log(chalk.bold(`${agent} installations\n`));
|
|
67
|
+
for (const installation of installations) {
|
|
68
|
+
console.log(` ${chalk.cyan(installation.label)} release ${installation.releaseVersion} ${chalk.gray(installation.id)}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Surface a failure as one red line, not a stack trace. Everything this command
|
|
73
|
+
* can fail on — an unknown agent, an ambiguous selector, an unpinnable harness,
|
|
74
|
+
* a release that would not launch — is a message the user acts on, and a
|
|
75
|
+
* commander async action rejection otherwise reaches the user as a raw Node dump.
|
|
76
|
+
*/
|
|
77
|
+
function fail(err) {
|
|
78
|
+
console.error(chalk.red(err.message));
|
|
79
|
+
process.exitCode = 1;
|
|
80
|
+
}
|
|
81
|
+
export function registerUpdateCommand(program) {
|
|
82
|
+
const update = program
|
|
83
|
+
.command('update [target]')
|
|
84
|
+
.description('Move a frozen agent installation to a new release, keeping its name and every reference to it')
|
|
85
|
+
.option('--to <release>', 'Release to move to: latest (default), oldest, or an exact version')
|
|
86
|
+
.option('--account <label>', 'Disambiguate by signed-in account when several installations match')
|
|
87
|
+
.option('--json', 'Machine-readable result')
|
|
88
|
+
.action(async (target, options) => {
|
|
89
|
+
try {
|
|
90
|
+
if (!target) {
|
|
91
|
+
throw new Error('Which agent? Use: agents update <agent>[@<installed-version>]');
|
|
92
|
+
}
|
|
93
|
+
const { agent, selector } = parseTarget(target);
|
|
94
|
+
if (options.to && options.to !== 'latest' && !supportsPinnedUpdate(agent)) {
|
|
95
|
+
// Fail loud at the boundary rather than installing the current release
|
|
96
|
+
// and reporting it as the pin that was asked for.
|
|
97
|
+
throw new Error(`${agent} is a single self-updating binary with no pinnable releases — drop --to, or pass --to latest.`);
|
|
98
|
+
}
|
|
99
|
+
const installation = await resolveInstallation(agent, selector, { account: options.account });
|
|
100
|
+
const strategy = selectUpdateStrategy(agent);
|
|
101
|
+
if (!options.json && !strategy.transactional) {
|
|
102
|
+
console.log(chalk.yellow(`${agent} installs one vendor-managed binary, so this update cannot be staged or rolled back; `
|
|
103
|
+
+ `a failure leaves whatever its installer wrote.`));
|
|
104
|
+
}
|
|
105
|
+
if (!options.json) {
|
|
106
|
+
console.log(chalk.gray(`Updating ${agent}@${describeInstallation(installation)} via the ${strategy.id} strategy...`));
|
|
107
|
+
}
|
|
108
|
+
const outcome = await updateInstallation(installation, {
|
|
109
|
+
to: options.to,
|
|
110
|
+
onProgress: options.json ? undefined : (message) => console.log(chalk.gray(` ${message}`)),
|
|
111
|
+
});
|
|
112
|
+
printOutcome(outcome, !!options.json);
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
fail(err);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
update
|
|
119
|
+
.command('list <agent>')
|
|
120
|
+
.description('Show every frozen installation of an agent and the release each carries')
|
|
121
|
+
.option('--json', 'Machine-readable listing')
|
|
122
|
+
// `--json` is declared on both `update` and `update list`, and commander
|
|
123
|
+
// binds a flag the parent also declares to the PARENT's option store — so
|
|
124
|
+
// reading this subcommand's own opts alone silently drops it. Merge them.
|
|
125
|
+
.action((rawAgent, _options, command) => {
|
|
126
|
+
try {
|
|
127
|
+
const agent = resolveAgentName(rawAgent);
|
|
128
|
+
if (!agent)
|
|
129
|
+
throw new Error(formatAgentError(rawAgent));
|
|
130
|
+
printInstallations(agent, !!command.optsWithGlobals().json);
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
fail(err);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
setHelpSections(update, {
|
|
137
|
+
examples: `agents update list claude
|
|
138
|
+
agents update claude@2.0.65
|
|
139
|
+
agents update claude@2.0.65 --to 2.1.220
|
|
140
|
+
agents update claude --account work
|
|
141
|
+
agents update claude@2.0.65 --json`,
|
|
142
|
+
notes: 'An installation keeps its name for life; only the release inside it moves. That is why a default, a project pin, '
|
|
143
|
+
+ 'a routine version, or a profile that names claude@2.0.65 keeps working after you update it. '
|
|
144
|
+
+ 'The selector matches either the installation name or the release it currently carries — when two installations '
|
|
145
|
+
+ 'share a release, name one or pass --account <label> (see: agents accounts). '
|
|
146
|
+
+ 'The new release is fetched and launched before it replaces the working one, so a bad release leaves your agent running.',
|
|
147
|
+
});
|
|
148
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -94,7 +94,7 @@ if (IS_DEV_BUILD) {
|
|
|
94
94
|
// module on each invocation (which loaded the whole ~50-module tree before the
|
|
95
95
|
// first byte of output), the registry maps a command name to a thunk that
|
|
96
96
|
// imports only what that command needs. See src/lib/startup/command-registry.ts.
|
|
97
|
-
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadExport, loadPackages, loadRoutines, loadDaemon, loadMonitors, loadProjects, loadRun, loadFork, loadDefaults, loadSet, loadModels, loadModes, loadPrune, loadTrash, loadRestore, loadDoctor, loadApply, loadStatus, loadSnapshot, loadProfiles, loadHarness, loadSecrets, loadLogin, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadFactory, loadUsage, loadCost, loadInsights, loadPerf, loadTrends, loadOutput, loadBudget, loadAlias, loadMine, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadWebhook, loadFunnel, loadHumans, loadAccounts, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadUninstall, loadBench, loadShare, loadSend, loadFeed, loadMailboxes, } from './lib/startup/command-registry.js';
|
|
97
|
+
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadUpdate, loadImport, loadExport, loadPackages, loadRoutines, loadDaemon, loadMonitors, loadProjects, loadRun, loadFork, loadDefaults, loadSet, loadModels, loadModes, loadPrune, loadTrash, loadRestore, loadDoctor, loadApply, loadStatus, loadSnapshot, loadProfiles, loadHarness, loadSecrets, loadLogin, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadFactory, loadUsage, loadCost, loadInsights, loadPerf, loadTrends, loadOutput, loadBudget, loadAlias, loadMine, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadWebhook, loadFunnel, loadHumans, loadAccounts, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadUninstall, loadBench, loadShare, loadSend, loadFeed, loadMailboxes, } from './lib/startup/command-registry.js';
|
|
98
98
|
import { applyGlobalHelpConventions } from './lib/help.js';
|
|
99
99
|
import { renderWhatsNew } from './lib/whats-new.js';
|
|
100
100
|
import { getCliLaunch } from './lib/cli-entry.js';
|
|
@@ -292,6 +292,7 @@ Quick start:
|
|
|
292
292
|
Agent versions:
|
|
293
293
|
add <agent>[@version] Install an agent CLI (e.g. agents add grok or agents add codex)
|
|
294
294
|
import <agent> Adopt an existing global install (npm/homebrew) into agents-cli
|
|
295
|
+
update <agent>[@version] Move an installed agent to a new release, keeping its name (agents-cli itself is 'agents upgrade')
|
|
295
296
|
prune <agent>[@version] Uninstall a version
|
|
296
297
|
remove <agent>[@version] Alias for prune
|
|
297
298
|
use <agent>@<version> Set the default version
|
|
@@ -942,6 +943,7 @@ async function registerAllEagerCommands() {
|
|
|
942
943
|
await reg(loadWorkflows);
|
|
943
944
|
await reg(loadWorktree);
|
|
944
945
|
await reg(loadVersions);
|
|
946
|
+
await reg(loadUpdate);
|
|
945
947
|
await reg(loadImport);
|
|
946
948
|
await reg(loadExport);
|
|
947
949
|
await reg(loadPackages);
|
|
@@ -1247,7 +1249,7 @@ if (process.env.AGENTS_SKIP_MIGRATION !== '1') {
|
|
|
1247
1249
|
// Bumping the suffix re-runs migrations for every user; binary releases that
|
|
1248
1250
|
// don't change the schema must NOT re-run (they would destroy user content
|
|
1249
1251
|
// when migration steps overlap with user-authored paths). See issue #20.
|
|
1250
|
-
const sentinelValue = '
|
|
1252
|
+
const sentinelValue = 'v18';
|
|
1251
1253
|
let needRun = true;
|
|
1252
1254
|
try {
|
|
1253
1255
|
if (fs.existsSync(sentinel) && fs.readFileSync(sentinel, 'utf-8').trim() === sentinelValue) {
|
package/dist/lib/catchup.js
CHANGED
|
@@ -139,7 +139,10 @@ export async function runCatchup(opts = {}) {
|
|
|
139
139
|
continue;
|
|
140
140
|
}
|
|
141
141
|
try {
|
|
142
|
-
|
|
142
|
+
// No `scheduledFor` here on purpose: the missed slot is already claimed by
|
|
143
|
+
// `claimMissedFire` above (its atomic mkdir IS the catch-up single-fire), so
|
|
144
|
+
// the late run gets a fresh id rather than colliding with the missed record.
|
|
145
|
+
const meta = await executeJobDetached(config, undefined, { kind: 'catchup' });
|
|
143
146
|
outcomes.push({
|
|
144
147
|
name: entry.name,
|
|
145
148
|
expectedAt: entry.expectedAt,
|
package/dist/lib/daemon.d.ts
CHANGED
|
@@ -273,6 +273,23 @@ export interface DaemonStopResult {
|
|
|
273
273
|
surviving: string[];
|
|
274
274
|
detachedChildren: number[];
|
|
275
275
|
}
|
|
276
|
+
/**
|
|
277
|
+
* Live `__daemon-run` processes still registered in THIS state dir's instance
|
|
278
|
+
* registry, excluding `exclude`. State-dir-scoped by construction: the registry
|
|
279
|
+
* lives inside this daemon dir, so a daemon serving a DIFFERENT state dir (a test
|
|
280
|
+
* fixture with its own HOME, a separate install/home) registers elsewhere and is
|
|
281
|
+
* invisible here — it is never a stop/takeover target. POSIX-only (the registry
|
|
282
|
+
* and its `ps` liveness probe are); `[]` on Windows.
|
|
283
|
+
*
|
|
284
|
+
* Exported for `agents daemon status`/`doctor`/`services` (RUSH-2368): those
|
|
285
|
+
* commands previously flagged every `__daemon-run` on the box (a raw `ps` scan)
|
|
286
|
+
* as a "duplicate" of this daemon, which misreported test fixtures under their
|
|
287
|
+
* own HOME — and therefore their own state dir and registry — as strays to
|
|
288
|
+
* kill. This registry read is the same scope the reaper (`reapStrayDaemons`)
|
|
289
|
+
* and the stop postcondition (`stopDaemon`) already use, so the display and the
|
|
290
|
+
* reaper agree on what a duplicate is.
|
|
291
|
+
*/
|
|
292
|
+
export declare function findSurvivingStateDirDaemons(exclude: Set<number>): number[];
|
|
276
293
|
/**
|
|
277
294
|
* Stop the daemon and ASSERT its postcondition (SING-12, RUSH-2355), unloading it
|
|
278
295
|
* from launchd/systemd if applicable.
|