@phnx-labs/agents-cli 1.22.58 → 1.22.60
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 +260 -0
- package/README.md +29 -0
- package/dist/bootstrap.js +32 -1
- package/dist/commands/monitors.js +187 -23
- package/dist/commands/perf.js +10 -0
- package/dist/commands/routines.test-fixture.js +5 -0
- package/dist/commands/send.d.ts +2 -1
- package/dist/commands/send.js +7 -5
- package/dist/commands/sessions-picker.d.ts +13 -0
- package/dist/commands/sessions-picker.js +17 -8
- package/dist/commands/sessions-stats.js +37 -5
- package/dist/commands/sessions.js +52 -16
- package/dist/commands/ssh.js +12 -1
- package/dist/commands/teams-picker.js +20 -6
- package/dist/commands/teams.d.ts +2 -2
- package/dist/commands/teams.js +77 -21
- package/dist/commands/versions.js +12 -4
- package/dist/commands/view.js +7 -2
- package/dist/lib/accounting/rotate.js +12 -4
- package/dist/lib/accounting/usage-sync.d.ts +12 -2
- package/dist/lib/accounting/usage-sync.js +34 -6
- package/dist/lib/auto-pull-worker.js +7 -2
- package/dist/lib/cloud/rush.d.ts +7 -0
- package/dist/lib/cloud/rush.js +29 -1
- package/dist/lib/daemon/daemon.d.ts +22 -0
- package/dist/lib/daemon/daemon.js +39 -0
- package/dist/lib/daemon/session-index-service.js +9 -1
- package/dist/lib/daemon-ticks.d.ts +15 -0
- package/dist/lib/daemon-ticks.js +26 -0
- package/dist/lib/device-config.d.ts +5 -1
- package/dist/lib/device-config.js +2 -2
- package/dist/lib/devices/health.js +5 -1
- package/dist/lib/devices/pool.d.ts +25 -2
- package/dist/lib/devices/pool.js +32 -2
- package/dist/lib/devices/stats-cache.d.ts +0 -6
- package/dist/lib/devices/stats-cache.js +2 -9
- package/dist/lib/doctor-diff.d.ts +14 -0
- package/dist/lib/doctor-diff.js +43 -2
- package/dist/lib/feed/events.js +4 -0
- package/dist/lib/git.d.ts +38 -0
- package/dist/lib/git.js +58 -0
- package/dist/lib/hosts/ready.d.ts +8 -0
- package/dist/lib/hosts/ready.js +13 -2
- package/dist/lib/installations/versions.d.ts +17 -0
- package/dist/lib/installations/versions.js +53 -2
- package/dist/lib/monitors/config.d.ts +71 -3
- package/dist/lib/monitors/config.js +100 -12
- package/dist/lib/monitors/pid-watch.d.ts +35 -0
- package/dist/lib/monitors/pid-watch.js +45 -0
- package/dist/lib/monitors/remote.d.ts +18 -0
- package/dist/lib/monitors/remote.js +11 -0
- package/dist/lib/perf/db.d.ts +1 -1
- package/dist/lib/perf/db.js +53 -2
- package/dist/lib/perf/types.d.ts +14 -0
- package/dist/lib/permissions.js +7 -2
- package/dist/lib/plugins/plugins.d.ts +17 -3
- package/dist/lib/plugins/plugins.js +84 -9
- package/dist/lib/pty-server.d.ts +14 -0
- package/dist/lib/pty-server.js +49 -5
- package/dist/lib/secrets/drivers/rush.js +5 -0
- package/dist/lib/self-update.d.ts +42 -0
- package/dist/lib/self-update.js +88 -0
- package/dist/lib/session/cloud.js +5 -0
- package/dist/lib/session/db.d.ts +32 -6
- package/dist/lib/session/db.js +128 -12
- package/dist/lib/session/live-metadata.js +3 -3
- package/dist/lib/smart-launch.d.ts +6 -0
- package/dist/lib/smart-launch.js +5 -2
- package/dist/lib/staleness/writers/plugins.js +5 -2
- package/dist/lib/staleness/writers/subagents.js +13 -3
- package/dist/lib/state.d.ts +7 -4
- package/dist/lib/state.js +7 -4
- package/dist/lib/subagents.js +8 -2
- package/dist/lib/teams/api.d.ts +8 -0
- package/dist/lib/teams/api.js +50 -6
- package/dist/lib/teams/delivery.d.ts +14 -4
- package/dist/lib/teams/delivery.js +15 -5
- package/dist/lib/teams/scheduler.d.ts +10 -0
- package/dist/lib/teams/scheduler.js +8 -0
- package/dist/lib/traces/sync.d.ts +113 -6
- package/dist/lib/traces/sync.js +193 -19
- package/dist/lib/view-types.d.ts +12 -0
- package/package.json +2 -2
package/dist/lib/git.d.ts
CHANGED
|
@@ -161,6 +161,21 @@ export declare function canonicalGitRemote(url: string): string;
|
|
|
161
161
|
* checkout; {@link isSystemRepoOrigin} reads a dir's origin and delegates here.
|
|
162
162
|
*/
|
|
163
163
|
export declare function isSystemRepoRemote(remote: string | null | undefined): boolean;
|
|
164
|
+
/**
|
|
165
|
+
* True when `remote` is the origin the system repo is EXPECTED to track on this
|
|
166
|
+
* machine, honouring an operator's `AGENTS_SYSTEM_REPO` override.
|
|
167
|
+
*
|
|
168
|
+
* The system repo ships hooks that register as shell `command` strings run on
|
|
169
|
+
* every tool event, and its checkout auto-fast-forwards from origin — so a
|
|
170
|
+
* fast-forward from an origin the operator never chose is remote code execution
|
|
171
|
+
* on the next command that loads a system resource (PHNX-2957). This is the
|
|
172
|
+
* pinning predicate every auto-pull of the system repo gates on: pull only when
|
|
173
|
+
* origin is the canonical {@link isSystemRepoRemote} repo, or the exact
|
|
174
|
+
* `AGENTS_SYSTEM_REPO` the operator pointed at instead. Anything else — a
|
|
175
|
+
* repointed origin, a fork, an unset-then-swapped remote — is refused, not
|
|
176
|
+
* pulled. Pure string check; no git spawn.
|
|
177
|
+
*/
|
|
178
|
+
export declare function isExpectedSystemRepoRemote(remote: string | null | undefined): boolean;
|
|
164
179
|
/** True when two git remote URLs point at the same repo across transport forms. */
|
|
165
180
|
export declare function sameGitRemote(a: string | null | undefined, b: string | null | undefined): boolean;
|
|
166
181
|
/**
|
|
@@ -500,6 +515,29 @@ export declare function tryAutoPull(dir: string): Promise<{
|
|
|
500
515
|
pulled: boolean;
|
|
501
516
|
error?: string;
|
|
502
517
|
}>;
|
|
518
|
+
/** Result of {@link tryAutoPullSystemRepo}. `refused` is set only when the pull
|
|
519
|
+
* was blocked because origin is not the expected system remote. */
|
|
520
|
+
export interface SystemRepoPullResult {
|
|
521
|
+
pulled: boolean;
|
|
522
|
+
error?: string;
|
|
523
|
+
/** True when origin is present but is NOT the expected system remote; no
|
|
524
|
+
* fast-forward was attempted. `actualRemote` names what was found. */
|
|
525
|
+
refused?: boolean;
|
|
526
|
+
/** The origin fetch URL that was examined (present when a remote exists). */
|
|
527
|
+
actualRemote?: string;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Auto-pull the system repo ONLY after verifying its origin is the expected
|
|
531
|
+
* system remote (PHNX-2957). The system repo ships hooks that run as shell on
|
|
532
|
+
* tool events, so fast-forwarding it from an unexpected/repointed origin is
|
|
533
|
+
* remote code execution. An origin that fails {@link isExpectedSystemRepoRemote}
|
|
534
|
+
* is REFUSED loud (`refused: true`), never pulled — the canonical system repo
|
|
535
|
+
* (or an operator's `AGENTS_SYSTEM_REPO`) still fast-forwards exactly as before.
|
|
536
|
+
*
|
|
537
|
+
* A system dir with no origin at all is a plain no-op (`pulled: false`), not a
|
|
538
|
+
* refusal — there is nothing to pull from and nothing to distrust.
|
|
539
|
+
*/
|
|
540
|
+
export declare function tryAutoPullSystemRepo(dir: string): Promise<SystemRepoPullResult>;
|
|
503
541
|
/**
|
|
504
542
|
* How many commits `dir`'s checked-out branch is behind its upstream, read from
|
|
505
543
|
* the LAST-FETCHED remote-tracking ref — no network call. Returns null when the
|
package/dist/lib/git.js
CHANGED
|
@@ -537,6 +537,33 @@ export function isSystemRepoRemote(remote) {
|
|
|
537
537
|
const c = canonicalGitRemote(remote);
|
|
538
538
|
return c === canonicalGitRemote(`https://github.com/${systemRepoSlug(DEFAULT_SYSTEM_REPO)}`);
|
|
539
539
|
}
|
|
540
|
+
/**
|
|
541
|
+
* True when `remote` is the origin the system repo is EXPECTED to track on this
|
|
542
|
+
* machine, honouring an operator's `AGENTS_SYSTEM_REPO` override.
|
|
543
|
+
*
|
|
544
|
+
* The system repo ships hooks that register as shell `command` strings run on
|
|
545
|
+
* every tool event, and its checkout auto-fast-forwards from origin — so a
|
|
546
|
+
* fast-forward from an origin the operator never chose is remote code execution
|
|
547
|
+
* on the next command that loads a system resource (PHNX-2957). This is the
|
|
548
|
+
* pinning predicate every auto-pull of the system repo gates on: pull only when
|
|
549
|
+
* origin is the canonical {@link isSystemRepoRemote} repo, or the exact
|
|
550
|
+
* `AGENTS_SYSTEM_REPO` the operator pointed at instead. Anything else — a
|
|
551
|
+
* repointed origin, a fork, an unset-then-swapped remote — is refused, not
|
|
552
|
+
* pulled. Pure string check; no git spawn.
|
|
553
|
+
*/
|
|
554
|
+
export function isExpectedSystemRepoRemote(remote) {
|
|
555
|
+
if (!remote)
|
|
556
|
+
return false;
|
|
557
|
+
const override = process.env.AGENTS_SYSTEM_REPO?.trim();
|
|
558
|
+
if (override) {
|
|
559
|
+
// The override is a source spec (`gh:owner/repo`) or a full clone URL. Match
|
|
560
|
+
// the GitHub-slug form the setup path clones, and the raw spec itself, so a
|
|
561
|
+
// non-GitHub override URL still verifies.
|
|
562
|
+
return (sameGitRemote(remote, `https://github.com/${systemRepoSlug(override)}`) ||
|
|
563
|
+
sameGitRemote(remote, override.replace(/^gh:/, '')));
|
|
564
|
+
}
|
|
565
|
+
return isSystemRepoRemote(remote);
|
|
566
|
+
}
|
|
540
567
|
/** True when two git remote URLs point at the same repo across transport forms. */
|
|
541
568
|
export function sameGitRemote(a, b) {
|
|
542
569
|
if (!a || !b)
|
|
@@ -1665,6 +1692,37 @@ export async function tryAutoPull(dir) {
|
|
|
1665
1692
|
return { pulled: false, error: err.message };
|
|
1666
1693
|
}
|
|
1667
1694
|
}
|
|
1695
|
+
/**
|
|
1696
|
+
* Auto-pull the system repo ONLY after verifying its origin is the expected
|
|
1697
|
+
* system remote (PHNX-2957). The system repo ships hooks that run as shell on
|
|
1698
|
+
* tool events, so fast-forwarding it from an unexpected/repointed origin is
|
|
1699
|
+
* remote code execution. An origin that fails {@link isExpectedSystemRepoRemote}
|
|
1700
|
+
* is REFUSED loud (`refused: true`), never pulled — the canonical system repo
|
|
1701
|
+
* (or an operator's `AGENTS_SYSTEM_REPO`) still fast-forwards exactly as before.
|
|
1702
|
+
*
|
|
1703
|
+
* A system dir with no origin at all is a plain no-op (`pulled: false`), not a
|
|
1704
|
+
* refusal — there is nothing to pull from and nothing to distrust.
|
|
1705
|
+
*/
|
|
1706
|
+
export async function tryAutoPullSystemRepo(dir) {
|
|
1707
|
+
if (!isGitRepo(dir))
|
|
1708
|
+
return { pulled: false };
|
|
1709
|
+
let remote;
|
|
1710
|
+
try {
|
|
1711
|
+
const git = simpleGit(dir);
|
|
1712
|
+
const remotes = await git.getRemotes(true);
|
|
1713
|
+
remote = remotes.find(r => r.name === 'origin')?.refs?.fetch;
|
|
1714
|
+
}
|
|
1715
|
+
catch {
|
|
1716
|
+
return { pulled: false };
|
|
1717
|
+
}
|
|
1718
|
+
if (!remote)
|
|
1719
|
+
return { pulled: false };
|
|
1720
|
+
if (!isExpectedSystemRepoRemote(remote)) {
|
|
1721
|
+
return { pulled: false, refused: true, actualRemote: remote };
|
|
1722
|
+
}
|
|
1723
|
+
const res = await tryAutoPull(dir);
|
|
1724
|
+
return { ...res, actualRemote: remote };
|
|
1725
|
+
}
|
|
1668
1726
|
/**
|
|
1669
1727
|
* How many commits `dir`'s checked-out branch is behind its upstream, read from
|
|
1670
1728
|
* the LAST-FETCHED remote-tracking ref — no network call. Returns null when the
|
|
@@ -100,6 +100,14 @@ export interface ViewAgentAccountEligibility {
|
|
|
100
100
|
* --json`. A picker may route to a signed-out version because launching it is
|
|
101
101
|
* the login flow, but it must not route to a device whose every signed-in
|
|
102
102
|
* account is throttled.
|
|
103
|
+
*
|
|
104
|
+
* The sign-in gate reads the per-version `launchable` field — the strict
|
|
105
|
+
* per-version launch truth (`isLaunchableSignedIn`) — so a remote box is judged
|
|
106
|
+
* by the SAME launchability the local candidate uses (`collectRunCandidates` →
|
|
107
|
+
* `isLaunchableSignedIn`), not the display `signedIn` that inherits the
|
|
108
|
+
* active/global HOME login and passes a box that dies at spawn (PHNX-3466). An
|
|
109
|
+
* older remote CLI omits `launchable`, so it falls back to `signedIn` — the
|
|
110
|
+
* pre-fix behavior, so a rolling fleet does not regress.
|
|
103
111
|
*/
|
|
104
112
|
export declare function viewAgentAccountEligibility(view: string, agent: string): ViewAgentAccountEligibility;
|
|
105
113
|
export declare function viewAgentSignedIn(view: string, agent: string): boolean | undefined;
|
package/dist/lib/hosts/ready.js
CHANGED
|
@@ -213,6 +213,14 @@ export function missingPinnedVersionMessage(hostName, agent, version, installed)
|
|
|
213
213
|
* --json`. A picker may route to a signed-out version because launching it is
|
|
214
214
|
* the login flow, but it must not route to a device whose every signed-in
|
|
215
215
|
* account is throttled.
|
|
216
|
+
*
|
|
217
|
+
* The sign-in gate reads the per-version `launchable` field — the strict
|
|
218
|
+
* per-version launch truth (`isLaunchableSignedIn`) — so a remote box is judged
|
|
219
|
+
* by the SAME launchability the local candidate uses (`collectRunCandidates` →
|
|
220
|
+
* `isLaunchableSignedIn`), not the display `signedIn` that inherits the
|
|
221
|
+
* active/global HOME login and passes a box that dies at spawn (PHNX-3466). An
|
|
222
|
+
* older remote CLI omits `launchable`, so it falls back to `signedIn` — the
|
|
223
|
+
* pre-fix behavior, so a rolling fleet does not regress.
|
|
216
224
|
*/
|
|
217
225
|
export function viewAgentAccountEligibility(view, agent) {
|
|
218
226
|
try {
|
|
@@ -223,12 +231,15 @@ export function viewAgentAccountEligibility(view, agent) {
|
|
|
223
231
|
const verdicts = (row.versions ?? []).flatMap((version) => {
|
|
224
232
|
if (typeof version.signedIn !== 'boolean')
|
|
225
233
|
return [];
|
|
234
|
+
// Prefer the strict per-version launch signal; fall back to the display
|
|
235
|
+
// `signedIn` for an older remote CLI that does not emit `launchable`.
|
|
236
|
+
const launchable = typeof version.launchable === 'boolean' ? version.launchable : version.signedIn;
|
|
226
237
|
const throttled = version.usageStatus === 'rate_limited' || version.usageStatus === 'out_of_credits';
|
|
227
238
|
const authBlocked = version.authVerdict !== null
|
|
228
239
|
&& version.authVerdict !== undefined
|
|
229
240
|
&& isDeadVerdict(version.authVerdict);
|
|
230
|
-
const ready =
|
|
231
|
-
return [{ ready, pickerEligible: ready || !
|
|
241
|
+
const ready = launchable && !authBlocked && !throttled;
|
|
242
|
+
return [{ ready, pickerEligible: ready || !launchable || authBlocked }];
|
|
232
243
|
});
|
|
233
244
|
if (verdicts.length === 0)
|
|
234
245
|
return { signedIn: undefined, pickerEligible: undefined };
|
|
@@ -427,6 +427,23 @@ export declare function mergeRepoScopedSelections(repos: string[], cwd?: string)
|
|
|
427
427
|
*
|
|
428
428
|
* For Gemini: commands are converted from markdown to TOML.
|
|
429
429
|
*/
|
|
430
|
+
/**
|
|
431
|
+
* Resolve a caller's hook selection against the available hook set, matching a
|
|
432
|
+
* selection entry by exact name OR by its extensionless basename, and returning
|
|
433
|
+
* the AVAILABLE (extensioned) name in every case.
|
|
434
|
+
*
|
|
435
|
+
* `available.hooks` carries the source filename WITH its extension
|
|
436
|
+
* (`git-guard.sh`) — the shape the hooks writer's source resolver requires — but
|
|
437
|
+
* the doctor/heal resource diff identifies a hook by its extensionless basename
|
|
438
|
+
* (`git-guard`). A heal pass feeds those diff names straight back as the
|
|
439
|
+
* selection, so a plain exact-set filter matched NOTHING and `agents doctor
|
|
440
|
+
* --fix` could never reconcile a flagged hook (PHNX-3187). Basename tolerance
|
|
441
|
+
* closes that gap without changing the extensioned names the writer needs.
|
|
442
|
+
*
|
|
443
|
+
* Order-stable and de-duplicated; a selection entry that matches nothing is
|
|
444
|
+
* dropped (mirrors resolveSelection).
|
|
445
|
+
*/
|
|
446
|
+
export declare function resolveHookSelection(sel: string[] | 'all' | undefined, available: string[]): string[];
|
|
430
447
|
export declare function syncResourcesToVersion(agent: AgentId, version: string, selection?: ResourceSelection, options?: {
|
|
431
448
|
projectDir?: string;
|
|
432
449
|
cwd?: string;
|
|
@@ -2198,6 +2198,46 @@ export function mergeRepoScopedSelections(repos, cwd = process.cwd()) {
|
|
|
2198
2198
|
*
|
|
2199
2199
|
* For Gemini: commands are converted from markdown to TOML.
|
|
2200
2200
|
*/
|
|
2201
|
+
/**
|
|
2202
|
+
* Resolve a caller's hook selection against the available hook set, matching a
|
|
2203
|
+
* selection entry by exact name OR by its extensionless basename, and returning
|
|
2204
|
+
* the AVAILABLE (extensioned) name in every case.
|
|
2205
|
+
*
|
|
2206
|
+
* `available.hooks` carries the source filename WITH its extension
|
|
2207
|
+
* (`git-guard.sh`) — the shape the hooks writer's source resolver requires — but
|
|
2208
|
+
* the doctor/heal resource diff identifies a hook by its extensionless basename
|
|
2209
|
+
* (`git-guard`). A heal pass feeds those diff names straight back as the
|
|
2210
|
+
* selection, so a plain exact-set filter matched NOTHING and `agents doctor
|
|
2211
|
+
* --fix` could never reconcile a flagged hook (PHNX-3187). Basename tolerance
|
|
2212
|
+
* closes that gap without changing the extensioned names the writer needs.
|
|
2213
|
+
*
|
|
2214
|
+
* Order-stable and de-duplicated; a selection entry that matches nothing is
|
|
2215
|
+
* dropped (mirrors resolveSelection).
|
|
2216
|
+
*/
|
|
2217
|
+
export function resolveHookSelection(sel, available) {
|
|
2218
|
+
if (sel === 'all')
|
|
2219
|
+
return available;
|
|
2220
|
+
if (!Array.isArray(sel))
|
|
2221
|
+
return [];
|
|
2222
|
+
const stripExt = (n) => n.replace(/\.[^./\\]+$/, '');
|
|
2223
|
+
const exact = new Set(available);
|
|
2224
|
+
const byBase = new Map();
|
|
2225
|
+
for (const a of available) {
|
|
2226
|
+
const base = stripExt(a);
|
|
2227
|
+
if (!byBase.has(base))
|
|
2228
|
+
byBase.set(base, a); // first available wins the basename
|
|
2229
|
+
}
|
|
2230
|
+
const out = [];
|
|
2231
|
+
const seen = new Set();
|
|
2232
|
+
for (const name of sel) {
|
|
2233
|
+
const resolved = exact.has(name) ? name : byBase.get(stripExt(name));
|
|
2234
|
+
if (resolved && !seen.has(resolved)) {
|
|
2235
|
+
seen.add(resolved);
|
|
2236
|
+
out.push(resolved);
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
return out;
|
|
2240
|
+
}
|
|
2201
2241
|
export function syncResourcesToVersion(agent, version, selection, options = {}) {
|
|
2202
2242
|
if (isAgentHardDeprecated(agent)) {
|
|
2203
2243
|
return { commands: false, skills: false, hooks: false, memory: [], permissions: false, mcp: [], subagents: [], plugins: [], workflows: [], projectSkipped: [], pruned: { commands: [], skills: [] }, declined: [] };
|
|
@@ -2494,8 +2534,14 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
2494
2534
|
console.warn(explainSkip(agent, 'hooks', hooksGate, version) + ' -- skipped');
|
|
2495
2535
|
}
|
|
2496
2536
|
else {
|
|
2537
|
+
// Resolve requested hooks against the available set BY BASENAME as well as
|
|
2538
|
+
// exact name. `available.hooks` carries the source filename WITH its
|
|
2539
|
+
// extension (`git-guard.sh`), but the doctor/heal diff identifies a hook
|
|
2540
|
+
// by its extensionless basename (`git-guard`) — so a heal pass that feeds
|
|
2541
|
+
// the diff's names straight back through the plain resolveSelection
|
|
2542
|
+
// matched NOTHING and could never reconcile a flagged hook (PHNX-3187).
|
|
2497
2543
|
const hooksToSync = selection
|
|
2498
|
-
?
|
|
2544
|
+
? resolveHookSelection(selection.hooks, available.hooks)
|
|
2499
2545
|
: available.hooks;
|
|
2500
2546
|
let hookManifest = {};
|
|
2501
2547
|
if (hooksToSync.length > 0) {
|
|
@@ -2647,6 +2693,8 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
2647
2693
|
if (r.paths)
|
|
2648
2694
|
writtenTargets.push(...r.paths);
|
|
2649
2695
|
result.subagents.push(...r.synced);
|
|
2696
|
+
if (r.errors?.length)
|
|
2697
|
+
result.declined.push(...r.errors.map((e) => `subagents: ${e}`));
|
|
2650
2698
|
// Orphan-sweep for Claude only — see comment on commands/skills sweep
|
|
2651
2699
|
// for the no-selection guard. OpenClaw stores subagents as siblings of
|
|
2652
2700
|
// other resources so a readdir sweep would over-reach.
|
|
@@ -2673,7 +2721,10 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
2673
2721
|
if (pluginsToSync.length > 0 && pluginsWriter) {
|
|
2674
2722
|
if (options.allowExecSurfaces) {
|
|
2675
2723
|
const allPlugins = discoverPlugins();
|
|
2676
|
-
|
|
2724
|
+
// Pass the discovered plugins (with marketplace provenance) so a stale
|
|
2725
|
+
// install under one marketplace is trashed even when another marketplace
|
|
2726
|
+
// still ships that name — the PHNX-2618 shadow `code` plugin.
|
|
2727
|
+
cleanOrphanedPluginSkills(agent, versionHome, allPlugins);
|
|
2677
2728
|
const pluginMap = new Map(allPlugins.map(p => [p.name, p]));
|
|
2678
2729
|
for (const name of pluginsToSync) {
|
|
2679
2730
|
const plugin = pluginMap.get(name);
|
|
@@ -114,6 +114,30 @@ export interface MonitorConfig {
|
|
|
114
114
|
* independently, like routines' `devices`. Mutually exclusive with `device`.
|
|
115
115
|
*/
|
|
116
116
|
devices?: string[];
|
|
117
|
+
/**
|
|
118
|
+
* Does this monitor's SOURCE poll a fleet-shared queue (a PR list, a ticket
|
|
119
|
+
* tracker, the feed, a sync bucket) rather than the firing box's own state
|
|
120
|
+
* (its repos, sessions, caches)?
|
|
121
|
+
*
|
|
122
|
+
* This is the SING-9 placement switch for an UNPINNED monitor (no `device` /
|
|
123
|
+
* `devices`). A shared-input source has no per-box input, so every daemon
|
|
124
|
+
* firing it independently is a multi-executor race on shared state — the exact
|
|
125
|
+
* double-fire bug class. So an unpinned shared-input monitor fires only on the
|
|
126
|
+
* single owner (`interactive.host`, else the sole box on a one-device fleet),
|
|
127
|
+
* never on every daemon.
|
|
128
|
+
*
|
|
129
|
+
* Defaults differ by layer so the SAFE side is the default for each:
|
|
130
|
+
* - a **system built-in** (`scope: 'system'`) is treated as shared-input
|
|
131
|
+
* unless it sets `sharedInput: false`, so a built-in shipped with no pin
|
|
132
|
+
* can never fan out across the fleet — a device-local built-in opts back
|
|
133
|
+
* into fleet-wide firing with `sharedInput: false`;
|
|
134
|
+
* - a **user monitor** keeps its historical fleet-wide default and only
|
|
135
|
+
* becomes owner-restricted when it explicitly sets `sharedInput: true`.
|
|
136
|
+
*
|
|
137
|
+
* An explicit `device` / `devices` pin always wins and makes this moot — the
|
|
138
|
+
* author has already chosen the executor(s).
|
|
139
|
+
*/
|
|
140
|
+
sharedInput?: boolean;
|
|
117
141
|
/** Execute the ACTION on this machine over SSH (placement), distinct from the owner that fires it. */
|
|
118
142
|
runOn?: string;
|
|
119
143
|
/**
|
|
@@ -137,6 +161,15 @@ export interface MonitorConfig {
|
|
|
137
161
|
variables?: Record<string, string>;
|
|
138
162
|
/** Pin the agent version for `run` actions (omit to use the run strategy). */
|
|
139
163
|
version?: string;
|
|
164
|
+
/**
|
|
165
|
+
* Which layer this monitor was read from — `user` (~/.agents/monitors/) or
|
|
166
|
+
* `system` (the npm-shipped built-in mirror ~/.agents/.system/monitors/).
|
|
167
|
+
* A DERIVED, runtime-only annotation stamped by `readMonitorFile`, never a
|
|
168
|
+
* persisted YAML field: it tags a built-in in `list`/`view` (mirroring
|
|
169
|
+
* routines' `(built-in)` label) and `writeMonitor` strips it before writing,
|
|
170
|
+
* so materializing a user copy of a built-in always lands as `user`.
|
|
171
|
+
*/
|
|
172
|
+
scope?: 'user' | 'system';
|
|
140
173
|
}
|
|
141
174
|
/**
|
|
142
175
|
* A fired event. `summary` is injected into the action prompt as `{event}`;
|
|
@@ -154,13 +187,48 @@ export interface MonitorEvent {
|
|
|
154
187
|
* in seconds). Returns null on empty/unparseable/zero input.
|
|
155
188
|
*/
|
|
156
189
|
export declare function parseInterval(interval: string): number | null;
|
|
190
|
+
/**
|
|
191
|
+
* True when an UNPINNED monitor must be placed on a single owner rather than
|
|
192
|
+
* fired by every daemon — the SING-9 guard against a shared-queue double-fire.
|
|
193
|
+
*
|
|
194
|
+
* A `device` / `devices` pin is an explicit executor choice, so it is never
|
|
195
|
+
* owner-overridden (returns false here). For an unpinned monitor the default is
|
|
196
|
+
* layer-specific, SAFE side first: a **system built-in** is treated as
|
|
197
|
+
* shared-input unless it opts out with `sharedInput: false`; a **user monitor**
|
|
198
|
+
* keeps its fleet-wide default and only opts IN with `sharedInput: true`.
|
|
199
|
+
*/
|
|
200
|
+
export declare function requiresSingleOwner(config: Pick<MonitorConfig, 'device' | 'devices' | 'scope' | 'sharedInput'>): boolean;
|
|
201
|
+
/**
|
|
202
|
+
* The single fleet box that owns unpinned shared-input monitors — PURE, so the
|
|
203
|
+
* placement rule is unit-testable without a live tailnet. Priority:
|
|
204
|
+
*
|
|
205
|
+
* 1. the configured `interactive.host` (the box the operator sits at);
|
|
206
|
+
* 2. else, on a fleet with no OTHER registered device, this box — a single-box
|
|
207
|
+
* install has no peer to race, so the built-in still fires here;
|
|
208
|
+
* 3. else `undefined` — a multi-box fleet with no interactive host has no safe
|
|
209
|
+
* single owner, so an unpinned shared-input monitor fires NOWHERE (fail safe:
|
|
210
|
+
* a silent no-op beats a fleet-wide double-fire) until one is pinned.
|
|
211
|
+
*
|
|
212
|
+
* `deviceNames` is the registered fleet (registry keys); `self` is `machineId()`.
|
|
213
|
+
*/
|
|
214
|
+
export declare function resolveSharedInputOwner(interactiveHost: string | undefined, deviceNames: string[], self: string): string | undefined;
|
|
215
|
+
/** Resolve {@link resolveSharedInputOwner} from live config + the device registry. */
|
|
216
|
+
export declare function monitorSharedInputOwner(): string | undefined;
|
|
157
217
|
/**
|
|
158
218
|
* True when the monitor may evaluate + fire on this machine. Owner semantics:
|
|
159
219
|
* `device` (single owner, exactly-once) → only that machine; else `devices`
|
|
160
|
-
* (allowlist) → any listed machine; else
|
|
161
|
-
*
|
|
220
|
+
* (allowlist) → any listed machine; else placement depends on shared-input: an
|
|
221
|
+
* unpinned SHARED-INPUT monitor (a system built-in by default, or a user monitor
|
|
222
|
+
* that set `sharedInput: true`) fires only on the resolved owner
|
|
223
|
+
* ({@link monitorSharedInputOwner}), so a built-in that polls a fleet-shared
|
|
224
|
+
* queue can never fan out across every daemon (SING-9); anything else is
|
|
225
|
+
* unrestricted. Both sides normalize so `Yosemite-S0` and
|
|
226
|
+
* `yosemite-s0.tailnet.ts.net` agree with `yosemite-s0`.
|
|
227
|
+
*
|
|
228
|
+
* `ownerHost` overrides the resolved owner for tests/callers that already know
|
|
229
|
+
* it; omit it in production to resolve from config + the device registry.
|
|
162
230
|
*/
|
|
163
|
-
export declare function monitorRunsOnThisDevice(config: Pick<MonitorConfig, 'device' | 'devices'
|
|
231
|
+
export declare function monitorRunsOnThisDevice(config: Pick<MonitorConfig, 'device' | 'devices' | 'scope' | 'sharedInput'>, ownerHost?: string): boolean;
|
|
164
232
|
/**
|
|
165
233
|
* Validate a partial monitor config, returning a list of human-readable errors.
|
|
166
234
|
* Hand-rolled like validateJob (lib/routines.ts) — no zod. Rejects: no source,
|
|
@@ -12,10 +12,11 @@
|
|
|
12
12
|
import * as fs from 'fs';
|
|
13
13
|
import * as path from 'path';
|
|
14
14
|
import * as yaml from 'yaml';
|
|
15
|
-
import { getMonitorsDir, getSystemMonitorsDir, ensureAgentsDir } from '../state.js';
|
|
15
|
+
import { getMonitorsDir, getSystemMonitorsDir, ensureAgentsDir, readMeta } from '../state.js';
|
|
16
16
|
import { safeJoin, isSafeSegmentName } from '../paths.js';
|
|
17
17
|
import { atomicWriteFileSync } from '../fs-atomic.js';
|
|
18
18
|
import { machineId, normalizeHost } from '../machine-id.js';
|
|
19
|
+
import { loadDevicesSync } from '../devices/registry.js';
|
|
19
20
|
import { ALL_AGENT_IDS } from '../agents.js';
|
|
20
21
|
import { isCustomHarnessName } from '../profiles.js';
|
|
21
22
|
/** Default values applied to every monitor config when fields are omitted. */
|
|
@@ -50,19 +51,84 @@ export function parseInterval(interval) {
|
|
|
50
51
|
const ms = ((((weeks * 7 + days) * 24 + hours) * 60 + minutes) * 60 + seconds) * 1000;
|
|
51
52
|
return ms > 0 ? ms : null;
|
|
52
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* True when an UNPINNED monitor must be placed on a single owner rather than
|
|
56
|
+
* fired by every daemon — the SING-9 guard against a shared-queue double-fire.
|
|
57
|
+
*
|
|
58
|
+
* A `device` / `devices` pin is an explicit executor choice, so it is never
|
|
59
|
+
* owner-overridden (returns false here). For an unpinned monitor the default is
|
|
60
|
+
* layer-specific, SAFE side first: a **system built-in** is treated as
|
|
61
|
+
* shared-input unless it opts out with `sharedInput: false`; a **user monitor**
|
|
62
|
+
* keeps its fleet-wide default and only opts IN with `sharedInput: true`.
|
|
63
|
+
*/
|
|
64
|
+
export function requiresSingleOwner(config) {
|
|
65
|
+
if (config.device || (config.devices && config.devices.length > 0))
|
|
66
|
+
return false;
|
|
67
|
+
if (config.scope === 'system')
|
|
68
|
+
return config.sharedInput !== false;
|
|
69
|
+
return config.sharedInput === true;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The single fleet box that owns unpinned shared-input monitors — PURE, so the
|
|
73
|
+
* placement rule is unit-testable without a live tailnet. Priority:
|
|
74
|
+
*
|
|
75
|
+
* 1. the configured `interactive.host` (the box the operator sits at);
|
|
76
|
+
* 2. else, on a fleet with no OTHER registered device, this box — a single-box
|
|
77
|
+
* install has no peer to race, so the built-in still fires here;
|
|
78
|
+
* 3. else `undefined` — a multi-box fleet with no interactive host has no safe
|
|
79
|
+
* single owner, so an unpinned shared-input monitor fires NOWHERE (fail safe:
|
|
80
|
+
* a silent no-op beats a fleet-wide double-fire) until one is pinned.
|
|
81
|
+
*
|
|
82
|
+
* `deviceNames` is the registered fleet (registry keys); `self` is `machineId()`.
|
|
83
|
+
*/
|
|
84
|
+
export function resolveSharedInputOwner(interactiveHost, deviceNames, self) {
|
|
85
|
+
if (typeof interactiveHost === 'string' && interactiveHost.trim()) {
|
|
86
|
+
return normalizeHost(interactiveHost);
|
|
87
|
+
}
|
|
88
|
+
const others = deviceNames.map((d) => normalizeHost(d)).filter((d) => d && d !== self);
|
|
89
|
+
if (others.length === 0)
|
|
90
|
+
return self; // single-box fleet: no peer, no race
|
|
91
|
+
return undefined; // multi-box, no interactive host pinned → no safe owner
|
|
92
|
+
}
|
|
93
|
+
/** Resolve {@link resolveSharedInputOwner} from live config + the device registry. */
|
|
94
|
+
export function monitorSharedInputOwner() {
|
|
95
|
+
const self = machineId();
|
|
96
|
+
const interactiveHost = readMeta().config?.interactiveHost;
|
|
97
|
+
let deviceNames = [];
|
|
98
|
+
try {
|
|
99
|
+
deviceNames = Object.keys(loadDevicesSync());
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
deviceNames = [];
|
|
103
|
+
}
|
|
104
|
+
return resolveSharedInputOwner(typeof interactiveHost === 'string' ? interactiveHost : undefined, deviceNames, self);
|
|
105
|
+
}
|
|
53
106
|
/**
|
|
54
107
|
* True when the monitor may evaluate + fire on this machine. Owner semantics:
|
|
55
108
|
* `device` (single owner, exactly-once) → only that machine; else `devices`
|
|
56
|
-
* (allowlist) → any listed machine; else
|
|
57
|
-
*
|
|
109
|
+
* (allowlist) → any listed machine; else placement depends on shared-input: an
|
|
110
|
+
* unpinned SHARED-INPUT monitor (a system built-in by default, or a user monitor
|
|
111
|
+
* that set `sharedInput: true`) fires only on the resolved owner
|
|
112
|
+
* ({@link monitorSharedInputOwner}), so a built-in that polls a fleet-shared
|
|
113
|
+
* queue can never fan out across every daemon (SING-9); anything else is
|
|
114
|
+
* unrestricted. Both sides normalize so `Yosemite-S0` and
|
|
115
|
+
* `yosemite-s0.tailnet.ts.net` agree with `yosemite-s0`.
|
|
116
|
+
*
|
|
117
|
+
* `ownerHost` overrides the resolved owner for tests/callers that already know
|
|
118
|
+
* it; omit it in production to resolve from config + the device registry.
|
|
58
119
|
*/
|
|
59
|
-
export function monitorRunsOnThisDevice(config) {
|
|
120
|
+
export function monitorRunsOnThisDevice(config, ownerHost) {
|
|
60
121
|
const self = machineId();
|
|
61
122
|
if (config.device)
|
|
62
123
|
return normalizeHost(config.device) === self;
|
|
63
124
|
if (config.devices && config.devices.length > 0) {
|
|
64
125
|
return config.devices.some((d) => normalizeHost(d) === self);
|
|
65
126
|
}
|
|
127
|
+
if (requiresSingleOwner(config)) {
|
|
128
|
+
const resolved = ownerHost !== undefined ? ownerHost : monitorSharedInputOwner();
|
|
129
|
+
const owner = resolved && resolved.trim() ? normalizeHost(resolved) : undefined;
|
|
130
|
+
return owner !== undefined && owner === self;
|
|
131
|
+
}
|
|
66
132
|
return true;
|
|
67
133
|
}
|
|
68
134
|
/** Count the populated source-payload fields to detect "two sources". */
|
|
@@ -267,6 +333,9 @@ export function validateMonitor(config) {
|
|
|
267
333
|
}
|
|
268
334
|
}
|
|
269
335
|
}
|
|
336
|
+
if (config.sharedInput !== undefined && typeof config.sharedInput !== 'boolean') {
|
|
337
|
+
errors.push('sharedInput must be a boolean (true = source polls a fleet-shared queue)');
|
|
338
|
+
}
|
|
270
339
|
if (config.runOn !== undefined && (typeof config.runOn !== 'string' || config.runOn.trim() === '')) {
|
|
271
340
|
errors.push('runOn must be a non-empty machine name (a registered host, device, capability tag, or user@host)');
|
|
272
341
|
}
|
|
@@ -286,11 +355,24 @@ export function validateMonitor(config) {
|
|
|
286
355
|
return errors;
|
|
287
356
|
}
|
|
288
357
|
/**
|
|
289
|
-
* Read and normalize a monitor file.
|
|
290
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
358
|
+
* Read and normalize a monitor file. A built-in defaults to enabled exactly like
|
|
359
|
+
* every other system-layer resource (rules, hooks, commands, skills): a monitor
|
|
360
|
+
* shipped in the system mirror is on for every install unless the user shadows it
|
|
361
|
+
* with an explicit `enabled: false` (via `agents monitors pause`, which writes a
|
|
362
|
+
* user copy — the system mirror is pull-only). There is deliberately no
|
|
363
|
+
* system-scope special-case: monitors used to be the lone outlier that shipped
|
|
364
|
+
* disabled+invisible (PHNX-2506). `scope` no longer changes the enabled default;
|
|
365
|
+
* it is retained on the config so `list`/`view` can tag a built-in.
|
|
366
|
+
*
|
|
367
|
+
* Enabled-by-default is NOT, on its own, permission to fire on every daemon. A
|
|
368
|
+
* shared-input built-in (one whose source polls a fleet-shared queue such as `gh
|
|
369
|
+
* pr list --author @me`) is placed on a single owner by `monitorRunsOnThisDevice`
|
|
370
|
+
* / `requiresSingleOwner` even when the shipped YAML carries no `device:` pin: a
|
|
371
|
+
* system built-in is treated as shared-input unless it sets `sharedInput: false`,
|
|
372
|
+
* so it can never fan out across the fleet and double-fire on a shared queue
|
|
373
|
+
* (SING-9). A device-local built-in opts back into fleet-wide firing with
|
|
374
|
+
* `sharedInput: false`; a genuinely-shared one should still ship a `device:` pin
|
|
375
|
+
* (or `sharedInput: true`) to document the intent.
|
|
294
376
|
*/
|
|
295
377
|
function readMonitorFile(filePath, scope = 'user') {
|
|
296
378
|
try {
|
|
@@ -303,9 +385,12 @@ function readMonitorFile(filePath, scope = 'user') {
|
|
|
303
385
|
...MONITOR_DEFAULTS,
|
|
304
386
|
...parsed,
|
|
305
387
|
name: parsed.name || path.basename(filePath).replace(/\.ya?ml$/, ''),
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
|
|
388
|
+
// Enabled unless the user explicitly disables it — same default for user
|
|
389
|
+
// and system layers, so a built-in is visible and on like any other
|
|
390
|
+
// system resource. `scope` is stamped below for the (built-in) tag, not
|
|
391
|
+
// used to gate enablement.
|
|
392
|
+
enabled: hasEnabled ? parsed.enabled !== false : (MONITOR_DEFAULTS.enabled ?? true),
|
|
393
|
+
scope,
|
|
309
394
|
};
|
|
310
395
|
}
|
|
311
396
|
catch {
|
|
@@ -387,6 +472,9 @@ export function writeMonitor(config) {
|
|
|
387
472
|
const output = { ...config };
|
|
388
473
|
if (output.enabled === true)
|
|
389
474
|
delete output.enabled;
|
|
475
|
+
// `scope` is a derived read-time annotation, never part of the on-disk schema —
|
|
476
|
+
// strip it so a materialized user copy of a built-in doesn't persist `scope: system`.
|
|
477
|
+
delete output.scope;
|
|
390
478
|
const devArr = output.devices;
|
|
391
479
|
if (!devArr || devArr.length === 0)
|
|
392
480
|
delete output.devices;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--watch-pid` support — turns a backgrounded OS process into a durable,
|
|
3
|
+
* daemon-polled watcher instead of relying on a harness's own exit hook
|
|
4
|
+
* (PHNX-3023: a "will re-invoke me" background shell never fires when the
|
|
5
|
+
* harness only notifies on process exit and the watch loop itself never
|
|
6
|
+
* exits — `gh pr checks --watch`, a long sleep, a tick poll).
|
|
7
|
+
*
|
|
8
|
+
* The command built here reuses the same existence-check predicate
|
|
9
|
+
* `isPidAlive`'s existence branch uses (`process.kill(pid, 0)`), but it runs
|
|
10
|
+
* inside the monitor engine's own poll loop (`sources/command.ts`) rather than
|
|
11
|
+
* the caller's process — so the check survives past the CLI invocation that
|
|
12
|
+
* armed it.
|
|
13
|
+
*/
|
|
14
|
+
/** The token a --watch-pid source's condition matches on process exit. */
|
|
15
|
+
export declare const PID_WATCH_EXITED_TOKEN = "exited";
|
|
16
|
+
/** Emitted while the pid is alive. */
|
|
17
|
+
export declare const PID_WATCH_RUNNING_TOKEN = "running";
|
|
18
|
+
/**
|
|
19
|
+
* Emitted when the pid is not alive AND has never been observed alive — the
|
|
20
|
+
* `--force` not-yet-spawned case. Deliberately distinct from
|
|
21
|
+
* {@link PID_WATCH_EXITED_TOKEN} so it can never match the exit condition.
|
|
22
|
+
*/
|
|
23
|
+
export declare const PID_WATCH_NOT_YET_SPAWNED_TOKEN = "notyetspawned";
|
|
24
|
+
/**
|
|
25
|
+
* The shell command a --watch-pid source polls. A poll only reports "exited"
|
|
26
|
+
* once it has FIRST observed the pid running — tracked with a marker file the
|
|
27
|
+
* command touches on every "running" poll — otherwise a `--force`-armed watch
|
|
28
|
+
* on a not-yet-spawned pid would report "exited" on its very first poll (the
|
|
29
|
+
* pid doesn't exist *yet*, not *anymore*), which the engine's match-mode
|
|
30
|
+
* fires immediately (no prior state to diff against) and then persists as the
|
|
31
|
+
* baseline — silencing the real exit forever once the process actually spawns
|
|
32
|
+
* and later dies. Portable across the shells `sources/command.ts` invokes
|
|
33
|
+
* (`/bin/sh -c` posix, `cmd /c` Windows).
|
|
34
|
+
*/
|
|
35
|
+
export declare function pidLivenessCommand(pid: number, seenRunningMarkerPath: string): string;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--watch-pid` support — turns a backgrounded OS process into a durable,
|
|
3
|
+
* daemon-polled watcher instead of relying on a harness's own exit hook
|
|
4
|
+
* (PHNX-3023: a "will re-invoke me" background shell never fires when the
|
|
5
|
+
* harness only notifies on process exit and the watch loop itself never
|
|
6
|
+
* exits — `gh pr checks --watch`, a long sleep, a tick poll).
|
|
7
|
+
*
|
|
8
|
+
* The command built here reuses the same existence-check predicate
|
|
9
|
+
* `isPidAlive`'s existence branch uses (`process.kill(pid, 0)`), but it runs
|
|
10
|
+
* inside the monitor engine's own poll loop (`sources/command.ts`) rather than
|
|
11
|
+
* the caller's process — so the check survives past the CLI invocation that
|
|
12
|
+
* armed it.
|
|
13
|
+
*/
|
|
14
|
+
import { IS_WINDOWS } from '../platform/index.js';
|
|
15
|
+
/** The token a --watch-pid source's condition matches on process exit. */
|
|
16
|
+
export const PID_WATCH_EXITED_TOKEN = 'exited';
|
|
17
|
+
/** Emitted while the pid is alive. */
|
|
18
|
+
export const PID_WATCH_RUNNING_TOKEN = 'running';
|
|
19
|
+
/**
|
|
20
|
+
* Emitted when the pid is not alive AND has never been observed alive — the
|
|
21
|
+
* `--force` not-yet-spawned case. Deliberately distinct from
|
|
22
|
+
* {@link PID_WATCH_EXITED_TOKEN} so it can never match the exit condition.
|
|
23
|
+
*/
|
|
24
|
+
export const PID_WATCH_NOT_YET_SPAWNED_TOKEN = 'notyetspawned';
|
|
25
|
+
/**
|
|
26
|
+
* The shell command a --watch-pid source polls. A poll only reports "exited"
|
|
27
|
+
* once it has FIRST observed the pid running — tracked with a marker file the
|
|
28
|
+
* command touches on every "running" poll — otherwise a `--force`-armed watch
|
|
29
|
+
* on a not-yet-spawned pid would report "exited" on its very first poll (the
|
|
30
|
+
* pid doesn't exist *yet*, not *anymore*), which the engine's match-mode
|
|
31
|
+
* fires immediately (no prior state to diff against) and then persists as the
|
|
32
|
+
* baseline — silencing the real exit forever once the process actually spawns
|
|
33
|
+
* and later dies. Portable across the shells `sources/command.ts` invokes
|
|
34
|
+
* (`/bin/sh -c` posix, `cmd /c` Windows).
|
|
35
|
+
*/
|
|
36
|
+
export function pidLivenessCommand(pid, seenRunningMarkerPath) {
|
|
37
|
+
if (IS_WINDOWS) {
|
|
38
|
+
return (`tasklist /FI "PID eq ${pid}" 2>NUL | findstr /I "${pid}" >NUL ` +
|
|
39
|
+
`&& (type nul > "${seenRunningMarkerPath}" & echo ${PID_WATCH_RUNNING_TOKEN}) ` +
|
|
40
|
+
`|| (if exist "${seenRunningMarkerPath}" (echo ${PID_WATCH_EXITED_TOKEN}) else (echo ${PID_WATCH_NOT_YET_SPAWNED_TOKEN}))`);
|
|
41
|
+
}
|
|
42
|
+
return (`kill -0 ${pid} 2>/dev/null ` +
|
|
43
|
+
`&& { mkdir -p "$(dirname "${seenRunningMarkerPath}")" 2>/dev/null; : > "${seenRunningMarkerPath}"; echo ${PID_WATCH_RUNNING_TOKEN}; } ` +
|
|
44
|
+
`|| { [ -e "${seenRunningMarkerPath}" ] && echo ${PID_WATCH_EXITED_TOKEN} || echo ${PID_WATCH_NOT_YET_SPAWNED_TOKEN}; }`);
|
|
45
|
+
}
|