@phnx-labs/agents-cli 1.22.66 → 1.22.69
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 +56 -0
- package/README.md +21 -8
- package/dist/bootstrap.js +6 -3
- package/dist/commands/browser.js +46 -21
- package/dist/commands/daemon-test-harness.d.ts +1 -1
- package/dist/commands/daemon-test-harness.js +2 -2
- package/dist/commands/daemon.js +21 -22
- package/dist/commands/exec.js +50 -0
- package/dist/commands/feed.js +20 -7
- package/dist/commands/monitors.js +5 -2
- package/dist/commands/projects.d.ts +26 -6
- package/dist/commands/projects.js +55 -22
- package/dist/commands/repo.js +57 -19
- package/dist/commands/resume.d.ts +16 -0
- package/dist/commands/resume.js +41 -8
- package/dist/commands/routines.js +42 -21
- package/dist/commands/send.js +29 -2
- package/dist/commands/sessions-inject.d.ts +58 -0
- package/dist/commands/sessions-inject.js +143 -7
- package/dist/commands/sessions-optimize.js +1 -1
- package/dist/commands/sessions-picker.js +1 -0
- package/dist/commands/sessions.js +4 -11
- package/dist/commands/share.d.ts +5 -3
- package/dist/commands/share.js +73 -20
- package/dist/commands/ssh.js +205 -2
- package/dist/lib/accounting/account-pool-collect.d.ts +6 -4
- package/dist/lib/accounting/account-pool-collect.js +6 -4
- package/dist/lib/accounting/usage-ingest.js +4 -2
- package/dist/lib/accounting/usage-sync.d.ts +38 -93
- package/dist/lib/accounting/usage-sync.js +66 -210
- package/dist/lib/accounting/usage.d.ts +18 -5
- package/dist/lib/accounting/usage.js +165 -20
- package/dist/lib/auth-health.d.ts +8 -0
- package/dist/lib/auth-health.js +4 -4
- package/dist/lib/boot-profile.d.ts +14 -0
- package/dist/lib/boot-profile.js +66 -0
- package/dist/lib/browser/caller-identity.d.ts +12 -0
- package/dist/lib/browser/caller-identity.js +19 -0
- package/dist/lib/browser/ipc.d.ts +37 -32
- package/dist/lib/browser/ipc.js +146 -94
- package/dist/lib/browser/task-index.d.ts +10 -2
- package/dist/lib/browser/task-index.js +22 -3
- package/dist/lib/channels/providers/desktop.d.ts +5 -4
- package/dist/lib/channels/providers/desktop.js +5 -4
- package/dist/lib/claude-account-token.js +108 -4
- package/dist/lib/daemon/account-state-daemon-service.d.ts +49 -9
- package/dist/lib/daemon/account-state-daemon-service.js +81 -18
- package/dist/lib/daemon/auth-sync-service.d.ts +4 -4
- package/dist/lib/daemon/auth-sync-service.js +17 -6
- package/dist/lib/daemon/catchup-service.d.ts +51 -0
- package/dist/lib/daemon/catchup-service.js +51 -0
- package/dist/lib/daemon/daemon.d.ts +12 -22
- package/dist/lib/daemon/daemon.js +463 -176
- package/dist/lib/daemon/runner.js +2 -0
- package/dist/lib/daemon/service.d.ts +22 -4
- package/dist/lib/daemon/service.js +2 -2
- package/dist/lib/daemon/supervisor.d.ts +55 -15
- package/dist/lib/daemon/supervisor.js +119 -29
- package/dist/lib/daemon/usage-sync-service.d.ts +4 -6
- package/dist/lib/daemon/usage-sync-service.js +22 -18
- package/dist/lib/daemon-health.js +36 -31
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +12 -2
- package/dist/lib/daemon-ticks.d.ts +9 -6
- package/dist/lib/daemon-ticks.js +14 -8
- package/dist/lib/devices/health.d.ts +38 -2
- package/dist/lib/devices/health.js +43 -5
- package/dist/lib/devices/registry.js +2 -0
- package/dist/lib/devices/worker-pick.d.ts +1 -1
- package/dist/lib/devices/worker-pick.js +4 -1
- package/dist/lib/exec.js +15 -0
- package/dist/lib/feed/watch.d.ts +3 -0
- package/dist/lib/feed/watch.js +13 -3
- package/dist/lib/feed-broadcast.d.ts +64 -5
- package/dist/lib/feed-broadcast.js +124 -22
- package/dist/lib/fleet-shared-repo-sync.d.ts +36 -0
- package/dist/lib/fleet-shared-repo-sync.js +333 -0
- package/dist/lib/fleet-shared-state.d.ts +38 -0
- package/dist/lib/fleet-shared-state.js +105 -0
- package/dist/lib/hosts/remote-cmd.d.ts +2 -0
- package/dist/lib/hosts/remote-cmd.js +12 -3
- package/dist/lib/lock-compromise.d.ts +8 -0
- package/dist/lib/lock-compromise.js +12 -0
- package/dist/lib/monitors/engine.d.ts +2 -1
- package/dist/lib/monitors/engine.js +27 -2
- package/dist/lib/monitors/sources/command.js +13 -3
- package/dist/lib/monitors/sources/failure.d.ts +32 -0
- package/dist/lib/monitors/sources/failure.js +52 -0
- package/dist/lib/monitors/sources/types.d.ts +9 -0
- package/dist/lib/owner-message.d.ts +12 -0
- package/dist/lib/owner-message.js +44 -0
- package/dist/lib/refresh-coordinator.js +2 -0
- package/dist/lib/run-trace-sync.d.ts +28 -0
- package/dist/lib/run-trace-sync.js +99 -0
- package/dist/lib/secrets/filestore.d.ts +4 -0
- package/dist/lib/secrets/filestore.js +164 -3
- package/dist/lib/secrets/push.d.ts +10 -0
- package/dist/lib/secrets/push.js +86 -7
- package/dist/lib/secrets/remote.d.ts +18 -6
- package/dist/lib/secrets/remote.js +29 -4
- package/dist/lib/secrets/reserved-sync.d.ts +28 -27
- package/dist/lib/secrets/reserved-sync.js +119 -101
- package/dist/lib/session/active.d.ts +13 -1
- package/dist/lib/session/active.js +5 -0
- package/dist/lib/session/actor-sidecar.d.ts +12 -0
- package/dist/lib/session/actor-sidecar.js +2 -0
- package/dist/lib/session/db.d.ts +16 -2
- package/dist/lib/session/db.js +73 -22
- package/dist/lib/session/discover.d.ts +12 -3
- package/dist/lib/session/discover.js +187 -26
- package/dist/lib/session/linear.d.ts +13 -0
- package/dist/lib/session/linear.js +44 -0
- package/dist/lib/session/live-metadata.js +1 -0
- package/dist/lib/session/parse.js +2 -3
- package/dist/lib/session/prompt.d.ts +17 -0
- package/dist/lib/session/prompt.js +35 -0
- package/dist/lib/session/recovery.d.ts +43 -6
- package/dist/lib/session/recovery.js +80 -10
- package/dist/lib/session/remote/remote-list.d.ts +17 -1
- package/dist/lib/session/remote/remote-list.js +29 -4
- package/dist/lib/session/remote/watch.d.ts +25 -2
- package/dist/lib/session/remote/watch.js +188 -11
- package/dist/lib/session/session-cache.d.ts +2 -1
- package/dist/lib/session/session-cache.js +1 -0
- package/dist/lib/session/state.js +11 -13
- package/dist/lib/session/types.d.ts +2 -0
- package/dist/lib/share/backend.d.ts +2 -2
- package/dist/lib/share/backend.js +20 -9
- package/dist/lib/share/delete.d.ts +5 -1
- package/dist/lib/share/delete.js +7 -2
- package/dist/lib/share/http-error.d.ts +52 -0
- package/dist/lib/share/http-error.js +65 -0
- package/dist/lib/share/publish.d.ts +67 -11
- package/dist/lib/share/publish.js +98 -16
- package/dist/lib/share/worker-template.js +105 -9
- package/dist/lib/smart-launch.js +27 -4
- package/dist/lib/ssh-exec.d.ts +2 -0
- package/dist/lib/ssh-exec.js +20 -4
- package/dist/lib/storage/index.d.ts +14 -0
- package/dist/lib/storage/index.js +14 -0
- package/dist/lib/storage/selection.d.ts +48 -0
- package/dist/lib/storage/selection.js +39 -0
- package/dist/lib/storage/visibility.d.ts +82 -0
- package/dist/lib/storage/visibility.js +99 -0
- package/dist/lib/teams/agents.js +3 -1
- package/dist/lib/teams/placement-probe.js +1 -0
- package/dist/lib/teams/registry.js +2 -0
- package/dist/lib/teams/scheduler.d.ts +8 -1
- package/dist/lib/teams/scheduler.js +4 -1
- package/dist/lib/testdata/daemon-health-writer.d.ts +1 -0
- package/dist/lib/testdata/daemon-health-writer.js +8 -0
- package/dist/lib/traces/backend.js +13 -2
- package/dist/lib/traces/sync.d.ts +7 -0
- package/dist/lib/traces/sync.js +9 -0
- package/dist/lib/usage-refresh.d.ts +8 -2
- package/dist/lib/usage-refresh.js +3 -3
- package/dist/lib/worktree/held.d.ts +166 -0
- package/dist/lib/worktree/held.js +368 -0
- package/package.json +2 -2
- package/dist/lib/account-state-service.d.ts +0 -21
- package/dist/lib/account-state-service.js +0 -60
package/dist/commands/share.js
CHANGED
|
@@ -11,11 +11,13 @@ import { Argument, Option } from 'commander';
|
|
|
11
11
|
import chalk from 'chalk';
|
|
12
12
|
import { DEFAULT_BUCKET_NAME, DEFAULT_CF_BUNDLE, DEFAULT_SHARE_DOMAIN, DEFAULT_WORKER_NAME, generateWriteToken, readCloudflareCreds, readShareConfig, readWriteToken, readWriteTokenEnv, readWriteTokenFromBundle, storeWriteToken, writeShareConfig, } from '../lib/share/config.js';
|
|
13
13
|
import { addCustomDomain, configureBucketLifecycle, createBucket, deployWorker, enableWorkersDev, findZoneId, hashWorkerScript, putWorkerSecret, updateWorker, WORKER_PHOENIX_ID_BASE_SECRET, setWorkerSecret, } from '../lib/share/provision.js';
|
|
14
|
-
import { publishFile, resolveShareUsername, parseMetaEntries, sanitizeLabel,
|
|
14
|
+
import { publishFile, resolveShareUsername, parseMetaEntries, sanitizeLabel, scanShareContent, formatSensitiveContentError, unlistedNotPrivateWarning, SHARE_VISIBILITY_LEVELS, PUBLISH_VISIBILITY_LEVELS, } from '../lib/share/publish.js';
|
|
15
15
|
import { deleteShare, resolveDeleteTarget } from '../lib/share/delete.js';
|
|
16
16
|
import { renderWorkerBundle } from '../lib/share/worker-template.js';
|
|
17
17
|
import { analyticsEnabled } from '../lib/share/analytics.js';
|
|
18
|
-
import {
|
|
18
|
+
import { extractShareHttpError, formatShareHttpErrorDetail } from '../lib/share/http-error.js';
|
|
19
|
+
import { phoenixIdBaseForDeploy, resolveShareBackend, shouldUseManaged, } from '../lib/share/backend.js';
|
|
20
|
+
import { publishVisibility } from '../lib/storage/visibility.js';
|
|
19
21
|
import { resolveGitHubUsername } from '../lib/git.js';
|
|
20
22
|
import { setHelpSections } from '../lib/help.js';
|
|
21
23
|
import { showUrl } from '../lib/open-url.js';
|
|
@@ -37,7 +39,11 @@ export function formatSharePublishResult(result, json = false) {
|
|
|
37
39
|
lines.push(chalk.dim(' expires never'));
|
|
38
40
|
const visibility = result.visibility ?? (result.unlisted ? 'unlisted' : 'public');
|
|
39
41
|
if (visibility === 'unlisted') {
|
|
40
|
-
lines.push(chalk.dim(' visibility: unlisted (noindex, hidden from gallery)'));
|
|
42
|
+
lines.push(chalk.dim(' visibility: unlisted (noindex, hidden from gallery — NOT authenticated)'));
|
|
43
|
+
}
|
|
44
|
+
else if (visibility === 'private') {
|
|
45
|
+
lines.push(chalk.dim(' visibility: private (token-gated — 404 without the key; hidden from gallery)'));
|
|
46
|
+
lines.push(chalk.yellow(' the key is in the URL above (?k=…) — treat the whole link as a secret'));
|
|
41
47
|
}
|
|
42
48
|
else if (visibility === 'me' || visibility === 'org') {
|
|
43
49
|
lines.push(chalk.dim(` visibility: ${visibility} (login required, hidden from gallery)`));
|
|
@@ -57,6 +63,14 @@ export function shareTemplateStatus(cfg) {
|
|
|
57
63
|
return cfg.templateHash === hashWorkerScript(renderWorkerBundle().script) ? 'current' : 'outdated';
|
|
58
64
|
}
|
|
59
65
|
export async function runShareEdit(target, opts) {
|
|
66
|
+
// Token-gated `private` can't be set in place: the metadata-edit route carries
|
|
67
|
+
// no viewer token, so re-stamping visibility=private alone would leave the page
|
|
68
|
+
// gated by a hash that doesn't exist — inaccessible to everyone (PHNX-3654).
|
|
69
|
+
// Fail loud pointing at the publish path, which mints the key.
|
|
70
|
+
if (opts.visibility === 'private') {
|
|
71
|
+
throw new Error("Can't change a page to 'private' in place — a token-gated link needs a fresh viewer key. " +
|
|
72
|
+
"Re-publish it with 'agents artifacts share <file> --protected' instead.");
|
|
73
|
+
}
|
|
60
74
|
// Same public-listing gate as publish: label + every metadata value are
|
|
61
75
|
// world-readable in the gallery and `share list --list-json`.
|
|
62
76
|
if (opts.force !== true) {
|
|
@@ -158,7 +172,10 @@ export function parseShareListing(user, body) {
|
|
|
158
172
|
host: item.host == null ? null : String(item.host),
|
|
159
173
|
repo: item.repo == null ? null : String(item.repo),
|
|
160
174
|
revisionCount: typeof item.revisionCount === 'number' ? item.revisionCount : 0,
|
|
161
|
-
visibility: item.visibility === 'unlisted' ||
|
|
175
|
+
visibility: item.visibility === 'unlisted' ||
|
|
176
|
+
item.visibility === 'private' ||
|
|
177
|
+
item.visibility === 'me' ||
|
|
178
|
+
item.visibility === 'org'
|
|
162
179
|
? item.visibility
|
|
163
180
|
: 'public',
|
|
164
181
|
meta: parseMetaField(item.meta),
|
|
@@ -230,7 +247,8 @@ export async function runShareList(opts = {}) {
|
|
|
230
247
|
throw new Error(OUTDATED_TEMPLATE_HINT);
|
|
231
248
|
}
|
|
232
249
|
if (res.status !== 200) {
|
|
233
|
-
|
|
250
|
+
const detail = formatShareHttpErrorDetail(extractShareHttpError({ status: res.status, body: res.body }));
|
|
251
|
+
throw new Error(`Listing failed (${res.status}) for ${listUrl}${detail}. Check the endpoint is reachable, or that 'agents artifacts setup' / 'agents auth login' completed.`);
|
|
234
252
|
}
|
|
235
253
|
if (!/application\/json/i.test(res.contentType)) {
|
|
236
254
|
// A 200 that isn't JSON means the old Worker ignored ?format=json and served
|
|
@@ -374,7 +392,8 @@ export async function runShareRevisions(target, opts = {}) {
|
|
|
374
392
|
const fetchListing = opts.fetchListing ?? defaultListingFetch;
|
|
375
393
|
const res = await fetchListing(revUrl);
|
|
376
394
|
if (res.status !== 200) {
|
|
377
|
-
|
|
395
|
+
const detail = formatShareHttpErrorDetail(extractShareHttpError({ status: res.status, body: res.body }));
|
|
396
|
+
throw new Error(`Revisions lookup failed (${res.status}) for ${revUrl}${detail}. Check the endpoint is reachable, or that 'agents artifacts setup' / 'agents auth login' completed.`);
|
|
378
397
|
}
|
|
379
398
|
if (!/application\/json/i.test(res.contentType)) {
|
|
380
399
|
if (backend.kind === 'managed') {
|
|
@@ -609,11 +628,11 @@ export function registerShareCommands(artifactsCmd) {
|
|
|
609
628
|
.option('--slug <slug>', 'URL slug override (default: stable slug of the artifact title, then filename)')
|
|
610
629
|
.option('--github-user <user>', 'GitHub username for the share namespace (default: resolved from gh/git config; ignored on the managed endpoint)')
|
|
611
630
|
.option('--expire <spec>', "auto-expire (default 30d). e.g. 12h, 30d, 2026-08-01, or 'never'")
|
|
612
|
-
.addOption(new Option('--visibility <level>', 'public | unlisted | me | org (
|
|
613
|
-
.choices([
|
|
614
|
-
.
|
|
615
|
-
.addOption(new Option('--unlisted', 'hidden alias of --visibility unlisted').hideHelp())
|
|
616
|
-
.addOption(new Option('--private', 'hidden alias of --visibility unlisted').hideHelp())
|
|
631
|
+
.addOption(new Option('--visibility <level>', 'public | unlisted | private | me | org. Default when signed in: me (owner-only, Phoenix-gated); use org for your whole email domain, public to opt into the gallery. unlisted is an UNAUTHENTICATED capability URL; private is token-gated (404 without the key). All but public are hidden from the gallery')
|
|
632
|
+
.choices([...PUBLISH_VISIBILITY_LEVELS]))
|
|
633
|
+
.addOption(new Option('--protected', 'token-gated link (= --visibility private): the URL carries a secret key and returns 404 without it — the authenticated alternative to --unlisted'))
|
|
634
|
+
.addOption(new Option('--unlisted', 'hidden alias of --visibility unlisted (obscurity, NOT authentication)').hideHelp())
|
|
635
|
+
.addOption(new Option('--private', 'hidden alias of --visibility unlisted (obscurity, NOT authentication — for real read-auth use --protected)').hideHelp())
|
|
617
636
|
.option('--force', 'publish even when the file contains emails or credential-shaped strings')
|
|
618
637
|
.option('--no-cover', 'skip the OG preview image (HTML pages get one by default)')
|
|
619
638
|
.option('--no-analytics', 'skip injecting the Cloudflare Web Analytics beacon')
|
|
@@ -634,16 +653,29 @@ export function registerShareCommands(artifactsCmd) {
|
|
|
634
653
|
}
|
|
635
654
|
try {
|
|
636
655
|
const meta = parseMetaEntries(opts.meta);
|
|
637
|
-
|
|
656
|
+
// Private by default: an explicit flag wins; otherwise the backend-aware
|
|
657
|
+
// product default — `me` (owner-only) when signed in to Phoenix, `public`
|
|
658
|
+
// for a BYO bucket (the Worker refuses `me`/`org` without a Phoenix owner).
|
|
659
|
+
// `shouldUseManaged` reads the same session/BYO signals `publishFile` will
|
|
660
|
+
// resolve, so the default matches the backend the publish actually uses.
|
|
661
|
+
const visibility = publishVisibility({
|
|
638
662
|
visibility: opts.visibility,
|
|
639
663
|
unlisted: opts.unlisted === true || opts.private === true,
|
|
640
|
-
|
|
664
|
+
protected: opts.protected === true,
|
|
665
|
+
}, shouldUseManaged({}) ? 'managed' : 'byo');
|
|
666
|
+
// unlisted (incl. its --private alias) is obscurity, not read-auth — warn
|
|
667
|
+
// loudly before the publish so a "private" link is never mistaken for a
|
|
668
|
+
// gated one (PHNX-3654). Skipped for --json so machine output stays clean.
|
|
669
|
+
if (visibility === 'unlisted' && !opts.json) {
|
|
670
|
+
console.error(chalk.yellow(`⚠ ${unlistedNotPrivateWarning()}`));
|
|
671
|
+
}
|
|
641
672
|
const result = await publishFile(file, {
|
|
642
673
|
slug: opts.slug,
|
|
643
674
|
githubUser: opts.githubUser,
|
|
644
675
|
expire: opts.expire,
|
|
645
676
|
visibility,
|
|
646
677
|
unlisted: visibility === 'unlisted',
|
|
678
|
+
protected: visibility === 'private',
|
|
647
679
|
force: opts.force,
|
|
648
680
|
cover: opts.cover,
|
|
649
681
|
analytics: opts.analytics,
|
|
@@ -660,15 +692,25 @@ export function registerShareCommands(artifactsCmd) {
|
|
|
660
692
|
});
|
|
661
693
|
setHelpSections(shareCmd, {
|
|
662
694
|
examples: `
|
|
663
|
-
# Signed in? Just publish — no Cloudflare setup (managed endpoint)
|
|
695
|
+
# Signed in? Just publish — no Cloudflare setup (managed endpoint).
|
|
696
|
+
# Default is private: an owner-only 'me' link, Phoenix-gated.
|
|
664
697
|
agents auth login
|
|
665
698
|
agents artifacts share ./out/plan.html
|
|
666
699
|
|
|
700
|
+
# Share with everyone at your email domain (no org setup — the domain is it)
|
|
701
|
+
agents artifacts share ./out/plan.html --visibility org
|
|
702
|
+
|
|
703
|
+
# Opt into the public gallery with an OG preview card
|
|
704
|
+
agents artifacts share ./out/landing.html --visibility public
|
|
705
|
+
|
|
667
706
|
# Hide from the public gallery (direct URL still works, noindex) and expire sooner
|
|
668
707
|
agents artifacts share ./out/report.html --visibility unlisted --expire 12h
|
|
669
708
|
|
|
709
|
+
# Token-gated link — GET returns 404 without the secret ?k= key
|
|
710
|
+
agents artifacts share ./out/secret.html --protected
|
|
711
|
+
|
|
670
712
|
# Permanent public page (the slug is derived from its title)
|
|
671
|
-
agents artifacts share ./out/landing.html --expire never
|
|
713
|
+
agents artifacts share ./out/landing.html --visibility public --expire never
|
|
672
714
|
|
|
673
715
|
# Optional slug override
|
|
674
716
|
agents artifacts share ./out/report.html --slug q3-report --expire 7d
|
|
@@ -696,11 +738,21 @@ ${SHARE_DELETE_EXAMPLES}
|
|
|
696
738
|
applies (setup / join). HTML is stored as one object: local images are inlined
|
|
697
739
|
and Chrome-saved file:// TOC links become in-page hashes.
|
|
698
740
|
|
|
741
|
+
Private by default: signed in, a publish with no --visibility is 'me' —
|
|
742
|
+
owner-only, Phoenix-gated, not in the gallery. Pass --visibility org to share
|
|
743
|
+
with everyone at your email domain (no org to create — the domain is the whole
|
|
744
|
+
mechanism), or --visibility public to opt into the gallery with an OG card. A
|
|
745
|
+
BYO publish (no Phoenix session) still defaults to public, since me/org need a
|
|
746
|
+
Phoenix owner to gate on.
|
|
747
|
+
|
|
699
748
|
Default expiry is 30d so an accidental publish decays. Pass --expire never for
|
|
700
749
|
a permanent link. --visibility unlisted (hidden aliases: --unlisted / --private)
|
|
701
750
|
hides the page from the public gallery and agents artifacts share list; the
|
|
702
751
|
direct URL is still world-readable (unlisted, not secret) and GET sends
|
|
703
|
-
X-Robots-Tag: noindex. --
|
|
752
|
+
X-Robots-Tag: noindex. --protected (= --visibility private) is token-gated
|
|
753
|
+
read-auth: the published URL carries a secret ?k= key (Authorization: Bearer
|
|
754
|
+
is accepted too) and GET returns 404 without it — treat the whole link as a
|
|
755
|
+
secret. --visibility me requires a Phoenix session and is only
|
|
704
756
|
visible to the signed-in owner; --visibility org requires the same and is
|
|
705
757
|
visible to members of the same Phoenix organization. A pre-publish scan
|
|
706
758
|
refuses emails and credential-shaped strings unless --force is passed.
|
|
@@ -929,10 +981,10 @@ Prefer to change visibility without a browser? Use 'agents artifacts share visib
|
|
|
929
981
|
// so the listing filter is named `--scope` here; `--all` is the convenience
|
|
930
982
|
// alias for `--scope all`.
|
|
931
983
|
.option('--for-user <user>', 'GitHub username whose namespace to list (default: resolved from gh/git config)')
|
|
932
|
-
.addOption(new Option('--scope <level>', 'visibility filter: public (default), unlisted, me, org, or all')
|
|
933
|
-
.choices(['public', 'unlisted', 'me', 'org', 'all'])
|
|
984
|
+
.addOption(new Option('--scope <level>', 'visibility filter: public (default), unlisted, private, me, org, or all')
|
|
985
|
+
.choices(['public', 'unlisted', 'private', 'me', 'org', 'all'])
|
|
934
986
|
.default('public'))
|
|
935
|
-
.option('--all', "list every page including hidden unlisted/me/org shares (alias for --scope all)")
|
|
987
|
+
.option('--all', "list every page including hidden unlisted/private/me/org shares (alias for --scope all)")
|
|
936
988
|
.option('--agent <name>', 'filter to shares published by this agent/harness (case-insensitive)')
|
|
937
989
|
.option('--session <id>', 'filter to shares published from this session id')
|
|
938
990
|
// Named --label-contains, not --label: `share <file>` (the parent) already owns
|
|
@@ -964,10 +1016,11 @@ Prefer to change visibility without a browser? Use 'agents artifacts share visib
|
|
|
964
1016
|
# Everything you've published, newest first
|
|
965
1017
|
agents artifacts share list
|
|
966
1018
|
|
|
967
|
-
# Include your hidden pages (unlisted / me / org) so you can see everything
|
|
1019
|
+
# Include your hidden pages (unlisted / private / me / org) so you can see everything
|
|
968
1020
|
agents artifacts share list --all
|
|
969
1021
|
agents artifacts share list --scope me
|
|
970
1022
|
agents artifacts share list --scope unlisted
|
|
1023
|
+
agents artifacts share list --scope private
|
|
971
1024
|
|
|
972
1025
|
# Machine-readable — e.g. pull every still-public URL with jq
|
|
973
1026
|
agents artifacts share list --list-json | jq -r '.objects[].url'
|
package/dist/commands/ssh.js
CHANGED
|
@@ -37,7 +37,7 @@ import { hostNameFor, renderSshConfig } from '../lib/devices/ssh-config.js';
|
|
|
37
37
|
import { ASKPASS_BUNDLE_ENV, ASKPASS_KEY_ENV, buildSshInvocation, deviceIdentityArgs, fleetDialTarget, isAgentsBrowserDrive, markFleetRemote, writeAskpassShim, } from '../lib/devices/connect.js';
|
|
38
38
|
import { ensureManagedKnownHostsDir, isHostPinned } from '../lib/devices/known-hosts.js';
|
|
39
39
|
import { shouldSyncTerminfo, syncTerminfoToDevice, terminfoHostKey } from '../lib/devices/terminfo.js';
|
|
40
|
-
import { fanOutDevices, fleetHealthSkip, planFleetTargets, remoteFleetTargets, runFleet, skipLabel, upgradeCommand, } from '../lib/devices/fleet.js';
|
|
40
|
+
import { fanOutDevices, fleetHealthSkip, planFleetTargets, remoteFleetTargets, runFleet, runOnDevice, runLocalCommand, skipLabel, upgradeCommand, } from '../lib/devices/fleet.js';
|
|
41
41
|
import { isRolloutSuccess, verifyFleetRollout, } from '../lib/devices/rollout-verify.js';
|
|
42
42
|
import { fleetCapacity, fmtBytes, headroom, } from '../lib/devices/health.js';
|
|
43
43
|
import { buildFleetHealthReport, renderFleetMatrix, renderFleetSummary, renderFleetWarnings, } from '../lib/devices/health-report.js';
|
|
@@ -61,6 +61,8 @@ import { runFleetLogin } from '../lib/fleet/remote-login.js';
|
|
|
61
61
|
import { getConfigValue, listConfig, setConfigValue, unsetConfigValue, configKeySpec, autoPoolMode, configuredDeviceRole, listConfiguredDeviceRoles, setConfiguredDeviceRole, } from '../lib/device-config.js';
|
|
62
62
|
import { filterAutoPool, listWorkerDevices } from '../lib/devices/pool.js';
|
|
63
63
|
import { registerCommandGroups, setHelpSections } from '../lib/help.js';
|
|
64
|
+
import { isSelfHost } from '../lib/devices/self-host.js';
|
|
65
|
+
import { collectHeldWorktrees, collectHeldWorktreesUnder, summarizeHeld, aggregateHeld, pushStrandedBranch, } from '../lib/worktree/held.js';
|
|
64
66
|
/** One-line summary of a device for `list`. `isSelf` marks the machine this
|
|
65
67
|
* command is running on so it stands out from the rest of the tailnet.
|
|
66
68
|
* `isInteractive` marks the configured interactive host (`devices config <name> interactive.host`). */
|
|
@@ -1130,7 +1132,7 @@ function registerDevicesCommands(program) {
|
|
|
1130
1132
|
{ title: 'Inspect', names: ['list', 'show', 'status', 'ping', 'harnesses', 'accounts', 'snapshot'] },
|
|
1131
1133
|
{ title: 'Disposable devices', names: ['lease'] },
|
|
1132
1134
|
{ title: 'Configure a device', names: ['config', 'describe', 'render'] },
|
|
1133
|
-
{ title: 'Fleet operations', names: ['update', 'run', 'login', 'capture', 'apply'] },
|
|
1135
|
+
{ title: 'Fleet operations', names: ['update', 'run', 'login', 'capture', 'apply', 'worktrees'] },
|
|
1134
1136
|
]);
|
|
1135
1137
|
devicesCmd
|
|
1136
1138
|
.command('sync')
|
|
@@ -2363,6 +2365,207 @@ is reported \`failed\` (exit non-zero), never a stranded \`ok\`.
|
|
|
2363
2365
|
.alias('kill')
|
|
2364
2366
|
.description('Terminate a running dispatched task from this machine (SIGTERM the remote process group; marks it failed/143).')
|
|
2365
2367
|
.action((id) => doDeviceTaskStop(id));
|
|
2368
|
+
const worktreesCmd = devicesCmd
|
|
2369
|
+
.command('worktrees')
|
|
2370
|
+
.description("Surface the held set of agent worktrees the sweep only counts, broken into buckets: unmerged-commits (real stranded work), uncommitted-changes, undeterminable. Read-only; --push publishes stranded branches.")
|
|
2371
|
+
.option('--json', 'emit the structured held set (device, repo, worktree, branch, reason, age, size) for machine callers / fleet aggregation')
|
|
2372
|
+
.option('--home <dir>', 'search root to discover repos under (default: $HOME)')
|
|
2373
|
+
.option('--repo <path>', 'scope to a single repo root instead of discovering')
|
|
2374
|
+
.option('--bucket <name>', 'show only one bucket: unmerged-commits | uncommitted-changes | undeterminable')
|
|
2375
|
+
.option('--fleet', 'fan out across every online device and aggregate the held set fleet-wide')
|
|
2376
|
+
.option('--push', 'PUBLISH each unmerged-commits branch that is on no remote (the safe recovery action); never deletes')
|
|
2377
|
+
.option('--yes', 'skip the confirmation prompt for --push')
|
|
2378
|
+
.action((opts) => runWorktreesHeld(opts));
|
|
2379
|
+
setHelpSections(worktreesCmd, {
|
|
2380
|
+
examples: `
|
|
2381
|
+
Inspect the residue:
|
|
2382
|
+
agents fleet worktrees # this box, grouped by bucket
|
|
2383
|
+
agents fleet worktrees --bucket unmerged-commits
|
|
2384
|
+
agents fleet worktrees --json # structured, for scripts
|
|
2385
|
+
|
|
2386
|
+
Fleet-wide:
|
|
2387
|
+
agents fleet worktrees --fleet # aggregate every online device
|
|
2388
|
+
agents fleet run 'agents fleet worktrees --json' # per-device composition
|
|
2389
|
+
|
|
2390
|
+
Recover stranded work (never deletes):
|
|
2391
|
+
agents fleet worktrees --push # publish on-no-remote branches
|
|
2392
|
+
`,
|
|
2393
|
+
notes: 'The nightly worktree-sweep (phnx-labs/.agents) reclaims merged worktrees and HOLDS the rest, reporting only a count. This surfaces that held set. unmerged-commits is the one that matters — a branch with commits on no remote is the PHNX-2951/PHNX-2732 stranded-work class; --push makes it visible. --push and --fleet are mutually exclusive (recovery is per-device on purpose).',
|
|
2394
|
+
});
|
|
2395
|
+
}
|
|
2396
|
+
const BUCKET_ORDER = ['unmerged-commits', 'uncommitted-changes', 'undeterminable'];
|
|
2397
|
+
const BUCKET_LABEL = {
|
|
2398
|
+
'unmerged-commits': 'unmerged-commits (stranded work — push or open a PR)',
|
|
2399
|
+
'uncommitted-changes': 'uncommitted-changes (dirty tree — needs review)',
|
|
2400
|
+
'undeterminable': 'undeterminable (broken/locked — re-examine)',
|
|
2401
|
+
};
|
|
2402
|
+
function isHeldBucket(v) {
|
|
2403
|
+
return BUCKET_ORDER.includes(v);
|
|
2404
|
+
}
|
|
2405
|
+
function fmtWtSize(n) {
|
|
2406
|
+
if (n < 0)
|
|
2407
|
+
return '?';
|
|
2408
|
+
if (n < 1024)
|
|
2409
|
+
return `${n}B`;
|
|
2410
|
+
const units = ['K', 'M', 'G', 'T'];
|
|
2411
|
+
let v = n / 1024;
|
|
2412
|
+
let i = 0;
|
|
2413
|
+
while (v >= 1024 && i < units.length - 1) {
|
|
2414
|
+
v /= 1024;
|
|
2415
|
+
i++;
|
|
2416
|
+
}
|
|
2417
|
+
return `${v.toFixed(v < 10 ? 1 : 0)}${units[i]}`;
|
|
2418
|
+
}
|
|
2419
|
+
/**
|
|
2420
|
+
* `agents fleet worktrees` — surface the held set the sweep only counts. Never
|
|
2421
|
+
* removes anything; `--push` publishes on-no-remote stranded branches. This is
|
|
2422
|
+
* the CLI-native surfacing half of PHNX-3520 (the sweep itself lives in
|
|
2423
|
+
* phnx-labs/.agents; it still emits a bare count until a sibling change teaches
|
|
2424
|
+
* it to consume this).
|
|
2425
|
+
*/
|
|
2426
|
+
async function runWorktreesHeld(opts) {
|
|
2427
|
+
if (opts.bucket && !isHeldBucket(opts.bucket)) {
|
|
2428
|
+
console.error(chalk.red(`Unknown bucket '${opts.bucket}'. Use one of: ${BUCKET_ORDER.join(', ')}`));
|
|
2429
|
+
process.exitCode = 1;
|
|
2430
|
+
return;
|
|
2431
|
+
}
|
|
2432
|
+
if (opts.fleet && opts.push) {
|
|
2433
|
+
console.error(chalk.red('--push and --fleet are mutually exclusive; run --push on the device that holds the work.'));
|
|
2434
|
+
process.exitCode = 1;
|
|
2435
|
+
return;
|
|
2436
|
+
}
|
|
2437
|
+
if (opts.fleet && opts.repo) {
|
|
2438
|
+
console.error(chalk.red('--repo scopes a single local repo and cannot combine with --fleet; drop one. To scope on each device, run `agents fleet run \'agents fleet worktrees --repo <path> --json\'`.'));
|
|
2439
|
+
process.exitCode = 1;
|
|
2440
|
+
return;
|
|
2441
|
+
}
|
|
2442
|
+
if (opts.fleet) {
|
|
2443
|
+
await runWorktreesHeldFleet(opts);
|
|
2444
|
+
return;
|
|
2445
|
+
}
|
|
2446
|
+
const searchHome = opts.home ?? os.homedir();
|
|
2447
|
+
const held = opts.repo
|
|
2448
|
+
? await collectHeldWorktrees(path.resolve(opts.repo))
|
|
2449
|
+
: await collectHeldWorktreesUnder(searchHome);
|
|
2450
|
+
if (opts.push) {
|
|
2451
|
+
await runWorktreesPush(held, opts);
|
|
2452
|
+
return;
|
|
2453
|
+
}
|
|
2454
|
+
const shown = opts.bucket ? held.filter((w) => w.bucket === opts.bucket) : held;
|
|
2455
|
+
if (opts.json) {
|
|
2456
|
+
console.log(JSON.stringify(shown, null, 2));
|
|
2457
|
+
return;
|
|
2458
|
+
}
|
|
2459
|
+
const summary = summarizeHeld(shown);
|
|
2460
|
+
if (summary.total === 0) {
|
|
2461
|
+
console.log(chalk.green('No held worktrees — nothing stranded, dirty, or undeterminable.'));
|
|
2462
|
+
return;
|
|
2463
|
+
}
|
|
2464
|
+
for (const bucket of BUCKET_ORDER) {
|
|
2465
|
+
const items = summary.buckets[bucket];
|
|
2466
|
+
if (items.length === 0)
|
|
2467
|
+
continue;
|
|
2468
|
+
const color = bucket === 'unmerged-commits' ? chalk.yellow : chalk.gray;
|
|
2469
|
+
console.log('\n' + color(chalk.bold(`${BUCKET_LABEL[bucket]} — ${items.length}`)));
|
|
2470
|
+
for (const w of items.sort((a, b) => b.unmergedCommits - a.unmergedCommits)) {
|
|
2471
|
+
const detail = bucket === 'unmerged-commits'
|
|
2472
|
+
? `${w.unmergedCommits} commit${w.unmergedCommits === 1 ? '' : 's'}, ${w.hasRemoteBranch ? 'on remote' : chalk.red('NO remote')}`
|
|
2473
|
+
: bucket === 'uncommitted-changes'
|
|
2474
|
+
? `${w.dirtyFiles} dirty file${w.dirtyFiles === 1 ? '' : 's'}`
|
|
2475
|
+
: w.reason;
|
|
2476
|
+
console.log(` ${chalk.cyan(w.repoName + '/' + w.name).padEnd(48)} ${chalk.gray((w.branch ?? 'detached').padEnd(28))} ${detail} ${chalk.dim(`${w.ageDays}d · ${fmtWtSize(w.sizeBytes)}`)}`);
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
const stranded = summary.buckets['unmerged-commits'].filter((w) => !w.hasRemoteBranch).length;
|
|
2480
|
+
if (stranded > 0) {
|
|
2481
|
+
console.log('\n' + chalk.yellow(`${stranded} branch${stranded === 1 ? '' : 'es'} with unmerged commits on no remote. Recover: agents fleet worktrees --push`));
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
/** `--push`: publish every on-no-remote stranded branch. Never deletes. */
|
|
2485
|
+
async function runWorktreesPush(held, opts) {
|
|
2486
|
+
const candidates = held.filter((w) => w.bucket === 'unmerged-commits' && !w.hasRemoteBranch && w.branch);
|
|
2487
|
+
if (candidates.length === 0) {
|
|
2488
|
+
console.log(chalk.green('No stranded branches to publish (nothing with unmerged commits on no remote).'));
|
|
2489
|
+
return;
|
|
2490
|
+
}
|
|
2491
|
+
if (isInteractiveTerminal() && !opts.yes) {
|
|
2492
|
+
console.log(chalk.yellow(`About to push ${candidates.length} stranded branch${candidates.length === 1 ? '' : 'es'} to origin:`));
|
|
2493
|
+
for (const w of candidates)
|
|
2494
|
+
console.log(` ${chalk.cyan(w.repoName + '/' + w.name)} → ${w.branch}`);
|
|
2495
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
2496
|
+
const go = await confirm({ message: 'Push these branches?', default: true }).catch(() => false);
|
|
2497
|
+
if (!go) {
|
|
2498
|
+
console.log(chalk.gray('Cancelled — nothing pushed.'));
|
|
2499
|
+
return;
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
let pushed = 0;
|
|
2503
|
+
for (const w of candidates) {
|
|
2504
|
+
const res = await pushStrandedBranch(w.repo, w);
|
|
2505
|
+
if (res.pushed) {
|
|
2506
|
+
pushed++;
|
|
2507
|
+
console.log(chalk.green(` pushed ${w.repoName}/${w.name} (${res.branch})`));
|
|
2508
|
+
}
|
|
2509
|
+
else {
|
|
2510
|
+
console.log(chalk.yellow(` skipped ${w.repoName}/${w.name}: ${res.reason}`));
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
console.log('\n' + chalk.green(`Published ${pushed}/${candidates.length} stranded branch${candidates.length === 1 ? '' : 'es'}.`));
|
|
2514
|
+
}
|
|
2515
|
+
/** `--fleet`: run this same read-only surface on every online device, aggregate. */
|
|
2516
|
+
async function runWorktreesHeldFleet(opts) {
|
|
2517
|
+
const reg = await loadDevices();
|
|
2518
|
+
const targets = planFleetTargets(reg);
|
|
2519
|
+
const online = targets.filter((t) => !t.skip);
|
|
2520
|
+
if (online.length === 0) {
|
|
2521
|
+
console.log(chalk.gray("No online devices. Run 'agents devices sync' first."));
|
|
2522
|
+
return;
|
|
2523
|
+
}
|
|
2524
|
+
const self = machineId();
|
|
2525
|
+
const remoteCmd = ['agents', 'fleet', 'worktrees', '--json'];
|
|
2526
|
+
if (opts.home)
|
|
2527
|
+
remoteCmd.push('--home', opts.home);
|
|
2528
|
+
const perDevice = [];
|
|
2529
|
+
for (const t of online) {
|
|
2530
|
+
const name = t.device.name;
|
|
2531
|
+
const isSelf = name === self || isSelfHost(name);
|
|
2532
|
+
const res = isSelf ? runLocalCommand(remoteCmd) : runOnDevice(t.device, remoteCmd);
|
|
2533
|
+
if (res.code !== 0) {
|
|
2534
|
+
console.error(chalk.gray(` ${name}: ${(res.stderr || 'failed').trim().slice(0, 120)}`));
|
|
2535
|
+
continue;
|
|
2536
|
+
}
|
|
2537
|
+
try {
|
|
2538
|
+
perDevice.push({ device: name, held: JSON.parse(res.stdout) });
|
|
2539
|
+
}
|
|
2540
|
+
catch {
|
|
2541
|
+
console.error(chalk.gray(` ${name}: unparseable output (older CLI?)`));
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
// Apply the bucket filter to the per-device inputs BEFORE aggregating, so
|
|
2545
|
+
// `total`, per-`devices` totals, and `buckets` all describe the same filtered
|
|
2546
|
+
// set — zeroing buckets after the sum leaves `total` counting rows the output
|
|
2547
|
+
// no longer lists.
|
|
2548
|
+
const scoped = opts.bucket
|
|
2549
|
+
? perDevice.map((d) => ({ device: d.device, held: d.held.filter((w) => w.bucket === opts.bucket) }))
|
|
2550
|
+
: perDevice;
|
|
2551
|
+
const agg = aggregateHeld(scoped);
|
|
2552
|
+
if (opts.json) {
|
|
2553
|
+
console.log(JSON.stringify(agg, null, 2));
|
|
2554
|
+
return;
|
|
2555
|
+
}
|
|
2556
|
+
console.log(chalk.bold(`Held worktrees across ${perDevice.length} device(s): ${agg.total}`));
|
|
2557
|
+
for (const bucket of BUCKET_ORDER) {
|
|
2558
|
+
const items = agg.buckets[bucket];
|
|
2559
|
+
if (items.length === 0)
|
|
2560
|
+
continue;
|
|
2561
|
+
const color = bucket === 'unmerged-commits' ? chalk.yellow : chalk.gray;
|
|
2562
|
+
console.log('\n' + color(chalk.bold(`${BUCKET_LABEL[bucket]} — ${items.length}`)));
|
|
2563
|
+
for (const w of items) {
|
|
2564
|
+
const dev = w.device ?? '?';
|
|
2565
|
+
const detail = bucket === 'unmerged-commits' ? `${w.unmergedCommits} commits, ${w.hasRemoteBranch ? 'on remote' : chalk.red('NO remote')}` : w.reason;
|
|
2566
|
+
console.log(` ${chalk.magenta(dev.padEnd(14))} ${chalk.cyan((w.repoName + '/' + w.name).padEnd(40))} ${chalk.gray((w.branch ?? 'detached').padEnd(24))} ${detail}`);
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2366
2569
|
}
|
|
2367
2570
|
/**
|
|
2368
2571
|
* `agents devices ps` — list tasks dispatched to devices (`agents run --device
|
|
@@ -25,10 +25,12 @@ export declare function foldRegistryCandidates(agent: AgentId, inputs: RunCandid
|
|
|
25
25
|
* provider can authenticate `agent` and whose credential is present locally
|
|
26
26
|
* (RUSH-3182). This is what makes a setup-token / API-key account balance-eligible.
|
|
27
27
|
*
|
|
28
|
-
* Passed as the `collect` argument to {@link resolveRunVersion} on the RUN path
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
28
|
+
* Passed as the `collect` argument to {@link resolveRunVersion} on the RUN path,
|
|
29
|
+
* and used by session recovery (`resolveSessionRecovery`, PHNX-3626) so an
|
|
30
|
+
* origin-account limit can rotate to a healthy provider account and stay NATIVE.
|
|
31
|
+
* The other consumers of {@link collectRunCandidates} (watchdog, teams
|
|
32
|
+
* placement) keep calling it directly and are unaffected. The existing
|
|
33
|
+
* {@link pickBalancedCandidate} selects over the combined list, so the
|
|
32
34
|
* revoked / rate-limit / exhausted logic is unchanged.
|
|
33
35
|
*/
|
|
34
36
|
export declare function collectRunCandidatesForRun(agent: AgentId): Promise<RotateCandidate[]>;
|
|
@@ -63,10 +63,12 @@ export function foldRegistryCandidates(agent, inputs) {
|
|
|
63
63
|
* provider can authenticate `agent` and whose credential is present locally
|
|
64
64
|
* (RUSH-3182). This is what makes a setup-token / API-key account balance-eligible.
|
|
65
65
|
*
|
|
66
|
-
* Passed as the `collect` argument to {@link resolveRunVersion} on the RUN path
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
66
|
+
* Passed as the `collect` argument to {@link resolveRunVersion} on the RUN path,
|
|
67
|
+
* and used by session recovery (`resolveSessionRecovery`, PHNX-3626) so an
|
|
68
|
+
* origin-account limit can rotate to a healthy provider account and stay NATIVE.
|
|
69
|
+
* The other consumers of {@link collectRunCandidates} (watchdog, teams
|
|
70
|
+
* placement) keep calling it directly and are unaffected. The existing
|
|
71
|
+
* {@link pickBalancedCandidate} selects over the combined list, so the
|
|
70
72
|
* revoked / rate-limit / exhausted logic is unchanged.
|
|
71
73
|
*/
|
|
72
74
|
export async function collectRunCandidatesForRun(agent) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The `agents __usage-ingest` receiver (PHNX-3392 usage-sync).
|
|
3
3
|
*
|
|
4
|
-
* A headed peer
|
|
4
|
+
* A legacy headed peer may pipe a {@link UsageSyncPayload} JSON envelope to our
|
|
5
5
|
* stdin; we merge its identity-keyed rows into the local usage cache newest-wins
|
|
6
6
|
* ({@link ingestPeerClaudeUsageRows}). Hidden internal verb — intercepted in
|
|
7
7
|
* index.ts before bootstrap, so it never triggers an update check or a detached
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
* accepting a wrong shape, but a busy cache lock degrades to best-effort inside
|
|
13
13
|
* `ingestPeerClaudeUsageRows` like every other cache writer.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
15
|
+
* New daemon ticks use the fleet-shared store instead. This compatibility
|
|
16
|
+
* receiver remains for older installed versions. The payload arrives on stdin,
|
|
17
|
+
* EXCEPT on a Windows receiver: the `agents.ps1`
|
|
16
18
|
* shim does not forward ssh-piped stdin to the node process, so the pusher writes
|
|
17
19
|
* the payload to a temp file and passes `agents __usage-ingest --from <path>`
|
|
18
20
|
* (the same workaround the secrets push uses — `buildWindowsStdinImportCommand`).
|