@phnx-labs/agents-cli 1.22.52 → 1.22.54
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 +336 -0
- package/README.md +42 -9
- package/dist/bootstrap.js +55 -154
- package/dist/cli/command-registry.d.ts +5 -0
- package/dist/cli/command-registry.js +8 -1
- package/dist/commands/accounts.js +220 -174
- package/dist/commands/apply.js +6 -3
- package/dist/commands/auth-mint.d.ts +8 -0
- package/dist/commands/auth-mint.js +96 -0
- package/dist/commands/auth.js +5 -1
- package/dist/commands/browser.js +1 -1
- package/dist/commands/cost.js +8 -2
- package/dist/commands/daemon.js +2 -2
- package/dist/commands/doctor.js +6 -1
- package/dist/commands/exec.js +26 -17
- package/dist/commands/fleet-capture.js +7 -0
- package/dist/commands/focus.d.ts +1 -0
- package/dist/commands/focus.js +4 -2
- package/dist/commands/go.d.ts +5 -4
- package/dist/commands/go.js +8 -7
- package/dist/commands/insights.js +9 -0
- package/dist/commands/monitors.js +85 -30
- package/dist/commands/output.js +8 -2
- package/dist/commands/repo.js +18 -0
- package/dist/commands/secrets.js +33 -14
- package/dist/commands/sessions-inject.js +8 -3
- package/dist/commands/sessions-picker.js +2 -1
- package/dist/commands/sessions.d.ts +20 -12
- package/dist/commands/sessions.js +94 -40
- package/dist/commands/setup-accounts.d.ts +8 -0
- package/dist/commands/setup-accounts.js +47 -0
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +11 -2
- package/dist/commands/share.d.ts +52 -3
- package/dist/commands/share.js +262 -18
- package/dist/commands/ssh.d.ts +7 -0
- package/dist/commands/ssh.js +53 -14
- package/dist/commands/status.js +14 -0
- package/dist/commands/sync.js +44 -0
- package/dist/commands/view.d.ts +3 -1
- package/dist/commands/view.js +5 -4
- package/dist/lib/account-registry.d.ts +15 -5
- package/dist/lib/account-registry.js +165 -53
- package/dist/lib/accounting/rotate.d.ts +20 -6
- package/dist/lib/accounting/rotate.js +38 -7
- package/dist/lib/accounting/usage.d.ts +37 -1
- package/dist/lib/accounting/usage.js +71 -6
- package/dist/lib/agent-spec/agents.d.ts +5 -2
- package/dist/lib/agent-spec/agents.js +25 -7
- package/dist/lib/analytics/mix-commands.js +12 -6
- package/dist/lib/answer-router.js +2 -1
- package/dist/lib/auth-mint.d.ts +150 -0
- package/dist/lib/auth-mint.js +434 -0
- package/dist/lib/browser/profiles.d.ts +18 -0
- package/dist/lib/browser/profiles.js +26 -1
- package/dist/lib/browser/registry.d.ts +44 -14
- package/dist/lib/browser/registry.js +141 -45
- package/dist/lib/browser/remote-control.d.ts +9 -7
- package/dist/lib/browser/remote-control.js +9 -7
- package/dist/lib/claude-account-token.d.ts +10 -0
- package/dist/lib/claude-account-token.js +14 -4
- package/dist/lib/config-drift.d.ts +37 -0
- package/dist/lib/config-drift.js +72 -0
- package/dist/lib/daemon/auth-sync-service.d.ts +19 -0
- package/dist/lib/daemon/auth-sync-service.js +34 -0
- package/dist/lib/daemon/daemon.js +30 -4
- package/dist/lib/daemon/runner.js +10 -2
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +5 -0
- package/dist/lib/device-config.d.ts +3 -3
- package/dist/lib/device-config.js +8 -7
- package/dist/lib/devices/config-migration.js +147 -1
- package/dist/lib/devices/connect.d.ts +26 -0
- package/dist/lib/devices/connect.js +48 -1
- package/dist/lib/devices/device-docs.d.ts +35 -0
- package/dist/lib/devices/device-docs.js +163 -0
- package/dist/lib/devices/discovery-policy.d.ts +14 -2
- package/dist/lib/devices/discovery-policy.js +31 -21
- package/dist/lib/devices/doctor-findings.d.ts +5 -1
- package/dist/lib/devices/doctor-findings.js +19 -1
- package/dist/lib/devices/registry.d.ts +11 -5
- package/dist/lib/devices/registry.js +46 -18
- package/dist/lib/exec.d.ts +88 -30
- package/dist/lib/exec.js +138 -34
- package/dist/lib/feed/feed.d.ts +10 -2
- package/dist/lib/feed/feed.js +35 -2
- package/dist/lib/feed-broadcast.js +1 -1
- package/dist/lib/fleet/apply.d.ts +11 -0
- package/dist/lib/fleet/apply.js +23 -3
- package/dist/lib/fleet/auth-sync.js +5 -3
- package/dist/lib/help.d.ts +9 -0
- package/dist/lib/help.js +29 -1
- package/dist/lib/hosts/dispatch.d.ts +4 -3
- package/dist/lib/hosts/dispatch.js +12 -8
- package/dist/lib/hosts/passthrough.d.ts +1 -10
- package/dist/lib/hosts/passthrough.js +1 -13
- package/dist/lib/hosts/providers/local.d.ts +9 -3
- package/dist/lib/hosts/providers/local.js +23 -12
- package/dist/lib/hosts/reconnect.d.ts +7 -4
- package/dist/lib/hosts/reconnect.js +29 -25
- package/dist/lib/hosts/registry.js +4 -1
- package/dist/lib/hosts/remote-os.js +3 -1
- package/dist/lib/installations/versions.js +9 -1
- package/dist/lib/linux-userns.d.ts +58 -0
- package/dist/lib/linux-userns.js +116 -0
- package/dist/lib/memory.d.ts +26 -0
- package/dist/lib/memory.js +80 -1
- package/dist/lib/monitors/config.d.ts +11 -0
- package/dist/lib/monitors/config.js +8 -0
- package/dist/lib/monitors/engine.js +8 -1
- package/dist/lib/monitors/state.d.ts +37 -1
- package/dist/lib/monitors/state.js +79 -4
- package/dist/lib/permissions-registry.d.ts +2 -0
- package/dist/lib/permissions-registry.js +116 -14
- package/dist/lib/permissions.d.ts +5 -3
- package/dist/lib/permissions.js +25 -27
- package/dist/lib/profiles.d.ts +8 -7
- package/dist/lib/profiles.js +12 -0
- package/dist/lib/project-key.d.ts +9 -0
- package/dist/lib/project-key.js +11 -0
- package/dist/lib/secrets/bundles.d.ts +35 -0
- package/dist/lib/secrets/bundles.js +78 -1
- package/dist/lib/secrets/push.d.ts +3 -8
- package/dist/lib/secrets/push.js +18 -14
- package/dist/lib/secrets/remote.d.ts +9 -18
- package/dist/lib/secrets/remote.js +11 -26
- package/dist/lib/secrets/reserved-sync.d.ts +65 -0
- package/dist/lib/secrets/reserved-sync.js +129 -0
- package/dist/lib/self-heal/checks/hook-manifest.d.ts +2 -0
- package/dist/lib/self-heal/checks/hook-manifest.js +56 -0
- package/dist/lib/self-heal/registry.js +4 -0
- package/dist/lib/self-heal/types.d.ts +1 -1
- package/dist/lib/session/active.d.ts +10 -1
- package/dist/lib/session/active.js +8 -5
- package/dist/lib/session/actor-sidecar.d.ts +7 -0
- package/dist/lib/session/actor-sidecar.js +2 -0
- package/dist/lib/session/db.d.ts +39 -4
- package/dist/lib/session/db.js +168 -31
- package/dist/lib/session/discover.d.ts +32 -4
- package/dist/lib/session/discover.js +126 -37
- package/dist/lib/session/insights.d.ts +14 -0
- package/dist/lib/session/insights.js +25 -2
- package/dist/lib/session/linear.js +1 -1
- package/dist/lib/session/live-metadata.js +1 -0
- package/dist/lib/session/pid-registry.d.ts +7 -0
- package/dist/lib/session/prompt.d.ts +15 -0
- package/dist/lib/session/prompt.js +21 -0
- package/dist/lib/session/shell-programs.d.ts +17 -0
- package/dist/lib/session/shell-programs.js +21 -0
- package/dist/lib/session/state.js +2 -1
- package/dist/lib/session/stream-render.js +2 -1
- package/dist/lib/session/tool-calls.js +2 -5
- package/dist/lib/session/trajectory-html.js +2 -1
- package/dist/lib/session/trajectory.js +3 -12
- package/dist/lib/session/types.d.ts +25 -0
- package/dist/lib/session/types.js +10 -0
- package/dist/lib/share/publish.d.ts +53 -5
- package/dist/lib/share/publish.js +99 -17
- package/dist/lib/share/worker-template.js +594 -64
- package/dist/lib/startup/root-command.js +2 -1
- package/dist/lib/state.d.ts +24 -0
- package/dist/lib/state.js +318 -54
- package/dist/lib/sync-status.d.ts +4 -0
- package/dist/lib/sync-status.js +3 -0
- package/dist/lib/terminal/resolve.d.ts +7 -0
- package/dist/lib/terminal/resolve.js +41 -2
- package/dist/lib/traces/classify.js +24 -19
- package/dist/lib/traces/insights.d.ts +67 -0
- package/dist/lib/traces/insights.js +178 -0
- package/dist/lib/traces/phenotype.d.ts +67 -0
- package/dist/lib/traces/phenotype.js +437 -0
- package/dist/lib/traces/segments.d.ts +133 -0
- package/dist/lib/traces/segments.js +301 -0
- package/dist/lib/traces/sync.d.ts +33 -0
- package/dist/lib/traces/sync.js +11 -2
- package/dist/lib/types.d.ts +47 -1
- package/dist/lib/usage-refresh.js +2 -1
- package/dist/lib/view-types.d.ts +2 -0
- package/dist/lib/watchdog/runner.js +18 -4
- package/package.json +2 -1
package/dist/commands/share.js
CHANGED
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
// (commands/artifacts-setup.ts), which calls `runShareProvision` below.
|
|
8
8
|
import { existsSync } from 'node:fs';
|
|
9
9
|
import { formatBytes } from '../lib/format.js';
|
|
10
|
-
import { Option } from 'commander';
|
|
10
|
+
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, resolveShareVisibility, } from '../lib/share/publish.js';
|
|
14
|
+
import { publishFile, resolveShareUsername, parseMetaEntries, sanitizeLabel, resolveShareVisibility, scanShareContent, formatSensitiveContentError, SHARE_VISIBILITY_LEVELS, } from '../lib/share/publish.js';
|
|
15
15
|
import { deleteShare, resolveDeleteTarget } from '../lib/share/delete.js';
|
|
16
16
|
import { renderWorkerScript } from '../lib/share/worker-template.js';
|
|
17
17
|
import { analyticsEnabled } from '../lib/share/analytics.js';
|
|
@@ -55,14 +55,63 @@ export function shareTemplateStatus(cfg) {
|
|
|
55
55
|
return 'unknown';
|
|
56
56
|
return cfg.templateHash === hashWorkerScript(renderWorkerScript()) ? 'current' : 'outdated';
|
|
57
57
|
}
|
|
58
|
+
export async function runShareEdit(target, opts) {
|
|
59
|
+
// Same public-listing gate as publish: label + every metadata value are
|
|
60
|
+
// world-readable in the gallery and `share list --list-json`.
|
|
61
|
+
if (opts.force !== true) {
|
|
62
|
+
const hits = [
|
|
63
|
+
...(typeof opts.label === 'string' ? scanShareContent(opts.label) : []),
|
|
64
|
+
...Object.values(opts.meta ?? {}).flatMap((value) => scanShareContent(value)),
|
|
65
|
+
];
|
|
66
|
+
if (hits.length > 0)
|
|
67
|
+
throw new Error(formatSensitiveContentError(hits));
|
|
68
|
+
}
|
|
69
|
+
const backend = resolveShareBackend({ githubUser: opts.githubUser, config: opts.config, writeToken: opts.writeToken, byo: opts.byo, session: opts.session });
|
|
70
|
+
// me/org are Phoenix-only — surface a crisp login hint here rather than the
|
|
71
|
+
// Worker's generic 400 for a signed-out caller (BYO WRITE_TOKEN can only set
|
|
72
|
+
// public/unlisted).
|
|
73
|
+
if ((opts.visibility === 'me' || opts.visibility === 'org') && backend.kind !== 'managed') {
|
|
74
|
+
throw new Error(`Changing visibility to '${opts.visibility}' requires a Phoenix session. Run 'agents auth login'.`);
|
|
75
|
+
}
|
|
76
|
+
const { key } = await resolveDeleteTarget(target, { githubUser: backend.kind === 'managed' ? backend.namespace : opts.githubUser || backend.namespace });
|
|
77
|
+
const res = await (opts.fetchEdit ?? fetch)(`${backend.baseUrl.replace(/\/+$/, '')}/${key}`, {
|
|
78
|
+
method: 'PATCH',
|
|
79
|
+
headers: { authorization: `Bearer ${backend.token}`, 'content-type': 'application/json' },
|
|
80
|
+
body: JSON.stringify({
|
|
81
|
+
...(opts.label !== undefined ? { label: opts.label } : {}),
|
|
82
|
+
...(opts.visibility !== undefined ? { visibility: opts.visibility } : {}),
|
|
83
|
+
meta: opts.meta ?? {}, metaMode: opts.metaMode ?? 'merge', removeMeta: opts.removeMeta ?? [],
|
|
84
|
+
}),
|
|
85
|
+
});
|
|
86
|
+
const body = await res.text();
|
|
87
|
+
if (!res.ok) {
|
|
88
|
+
let detail = body;
|
|
89
|
+
try {
|
|
90
|
+
detail = String(JSON.parse(body).error ?? body);
|
|
91
|
+
}
|
|
92
|
+
catch { /* keep body */ }
|
|
93
|
+
throw new Error(`Share metadata edit failed (${res.status}): ${detail || 'unknown error'}`);
|
|
94
|
+
}
|
|
95
|
+
const result = JSON.parse(body);
|
|
96
|
+
// A Worker template that predates the visibility edit ignores the `visibility`
|
|
97
|
+
// field and 200s without echoing it back — which would otherwise report a
|
|
98
|
+
// silent success while the page's visibility never changed. Fail loud with the
|
|
99
|
+
// update path instead (visibility rode in on RUSH-3135's metadata-edit route).
|
|
100
|
+
if (opts.visibility !== undefined && result.visibility === undefined) {
|
|
101
|
+
throw new Error("This share endpoint's Worker doesn't support in-place visibility changes yet — it predates them. " +
|
|
102
|
+
"Run 'agents artifacts share update' to deploy the current Worker template, then retry " +
|
|
103
|
+
"('agents artifacts share status' shows whether an update is due).");
|
|
104
|
+
}
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
58
107
|
/** Shown whenever the deployed Worker has no `?format=json` listing route — an
|
|
59
108
|
* endpoint provisioned before this feature. Points at the RUSH-2449 update path
|
|
60
109
|
* (`agents artifacts share update`) instead of letting the caller hit a 404 or an
|
|
61
110
|
* HTML body and get a confusing parse error. */
|
|
62
111
|
const OUTDATED_TEMPLATE_HINT = 'Your deployed share Worker has no machine-readable listing route — it predates `agents artifacts share list`. ' +
|
|
63
112
|
'Run `agents artifacts share update` to deploy the current Worker template, then retry (`agents artifacts share status` shows whether an update is due).';
|
|
64
|
-
async function defaultListingFetch(url) {
|
|
65
|
-
const res = await fetch(url, { headers: { accept: 'application/json' } });
|
|
113
|
+
async function defaultListingFetch(url, headers) {
|
|
114
|
+
const res = await fetch(url, { headers: { accept: 'application/json', ...(headers || {}) } });
|
|
66
115
|
return { status: res.status, contentType: res.headers.get('content-type') ?? '', body: await res.text() };
|
|
67
116
|
}
|
|
68
117
|
/** Normalize the Worker's `meta` field (arbitrary `--meta key=value` entries,
|
|
@@ -108,6 +157,9 @@ export function parseShareListing(user, body) {
|
|
|
108
157
|
host: item.host == null ? null : String(item.host),
|
|
109
158
|
repo: item.repo == null ? null : String(item.repo),
|
|
110
159
|
revisionCount: typeof item.revisionCount === 'number' ? item.revisionCount : 0,
|
|
160
|
+
visibility: item.visibility === 'unlisted' || item.visibility === 'me' || item.visibility === 'org'
|
|
161
|
+
? item.visibility
|
|
162
|
+
: 'public',
|
|
111
163
|
meta: parseMetaField(item.meta),
|
|
112
164
|
};
|
|
113
165
|
});
|
|
@@ -148,9 +200,21 @@ export async function runShareList(opts = {}) {
|
|
|
148
200
|
}
|
|
149
201
|
}
|
|
150
202
|
const user = await namespaceForBackend(backend, opts.githubUser);
|
|
151
|
-
const
|
|
203
|
+
const scope = opts.scope ?? 'public';
|
|
204
|
+
const includeHidden = scope !== 'public';
|
|
205
|
+
const query = new URLSearchParams({ format: 'json' });
|
|
206
|
+
if (includeHidden)
|
|
207
|
+
query.set('scope', 'mine');
|
|
208
|
+
const listUrl = `${backend.baseUrl.replace(/\/+$/, '')}/${encodeURIComponent(user)}?${query.toString()}`;
|
|
209
|
+
const listingHeaders = {};
|
|
210
|
+
if (includeHidden) {
|
|
211
|
+
if (!backend.token) {
|
|
212
|
+
throw new Error(`Listing ${scope} shares requires owner authentication. Run 'agents auth login' for the managed endpoint, or ensure SHARE_WRITE_TOKEN / 'agents artifacts share join' is configured for BYO.`);
|
|
213
|
+
}
|
|
214
|
+
listingHeaders.authorization = `Bearer ${backend.token}`;
|
|
215
|
+
}
|
|
152
216
|
const fetchListing = opts.fetchListing ?? defaultListingFetch;
|
|
153
|
-
const res = await fetchListing(listUrl);
|
|
217
|
+
const res = await fetchListing(listUrl, listingHeaders);
|
|
154
218
|
if (res.status === 404) {
|
|
155
219
|
// A single-segment path 404s either because the namespace is genuinely empty
|
|
156
220
|
// (current template — the listing route gates on the namespace holding objects,
|
|
@@ -177,14 +241,23 @@ export async function runShareList(opts = {}) {
|
|
|
177
241
|
throw new Error(OUTDATED_TEMPLATE_HINT);
|
|
178
242
|
}
|
|
179
243
|
const parsed = parseShareListing(user, res.body);
|
|
180
|
-
return applyShareListFilters(parsed,
|
|
244
|
+
return applyShareListFilters(parsed, {
|
|
245
|
+
agent: opts.agent,
|
|
246
|
+
session: opts.session,
|
|
247
|
+
label: opts.label,
|
|
248
|
+
meta: opts.meta,
|
|
249
|
+
scope,
|
|
250
|
+
});
|
|
181
251
|
}
|
|
182
252
|
/** Client-side filtering over an already-fetched listing (RUSH-2683) — the
|
|
183
253
|
* Worker has no query surface for this, so it narrows the fetched set instead
|
|
184
254
|
* of a second round trip. `count` reflects the FILTERED set, matching what the
|
|
185
255
|
* caller actually sees. */
|
|
186
|
-
function applyShareListFilters(result, filters) {
|
|
256
|
+
export function applyShareListFilters(result, filters) {
|
|
187
257
|
let objects = result.objects;
|
|
258
|
+
if (filters.scope && filters.scope !== 'all') {
|
|
259
|
+
objects = objects.filter((o) => o.visibility === filters.scope);
|
|
260
|
+
}
|
|
188
261
|
if (filters.agent) {
|
|
189
262
|
const needle = filters.agent.toLowerCase();
|
|
190
263
|
objects = objects.filter((o) => (o.agent ?? '').toLowerCase() === needle);
|
|
@@ -196,6 +269,9 @@ function applyShareListFilters(result, filters) {
|
|
|
196
269
|
const needle = filters.label.toLowerCase();
|
|
197
270
|
objects = objects.filter((o) => (o.label ?? '').toLowerCase().includes(needle));
|
|
198
271
|
}
|
|
272
|
+
if (filters.meta) {
|
|
273
|
+
objects = objects.filter((o) => Object.entries(filters.meta).every(([key, value]) => o.meta[key] === value));
|
|
274
|
+
}
|
|
199
275
|
return { user: result.user, count: objects.length, objects };
|
|
200
276
|
}
|
|
201
277
|
/** Human-readable bytes, e.g. `1.2 KB`, `640 B`. */
|
|
@@ -219,7 +295,9 @@ export function formatShareList(result, json = false) {
|
|
|
219
295
|
chalk.dim(` ${result.count} published ${result.count === 1 ? 'page' : 'pages'}`);
|
|
220
296
|
const rows = result.objects.map((o) => {
|
|
221
297
|
const when = o.publishedAt ? o.publishedAt.slice(0, 10) : 'unknown';
|
|
222
|
-
const
|
|
298
|
+
const visibility = o.visibility ?? 'public';
|
|
299
|
+
const visibilityBit = visibility === 'public' ? chalk.green('public') : chalk.yellow(visibility);
|
|
300
|
+
const bits = [visibilityBit, when, formatBytes(o.size)];
|
|
223
301
|
if (o.agent)
|
|
224
302
|
bits.push(o.agent);
|
|
225
303
|
if (o.revisionCount > 0)
|
|
@@ -341,6 +419,18 @@ export function formatShareDeleteResult(result, json = false) {
|
|
|
341
419
|
}
|
|
342
420
|
return lines.join('\n');
|
|
343
421
|
}
|
|
422
|
+
/** Human summary of an in-place visibility change. */
|
|
423
|
+
function formatShareVisibilityResult(result, requested) {
|
|
424
|
+
const now = result.visibility ?? requested;
|
|
425
|
+
const from = result.previousVisibility && result.previousVisibility !== now
|
|
426
|
+
? `${chalk.dim(result.previousVisibility)} → `
|
|
427
|
+
: '';
|
|
428
|
+
return [
|
|
429
|
+
`${chalk.green('✓')} visibility ${from}${chalk.bold(now)}`,
|
|
430
|
+
chalk.dim(` ${result.url}`),
|
|
431
|
+
chalk.dim(' body unchanged — no revision created'),
|
|
432
|
+
].join('\n');
|
|
433
|
+
}
|
|
344
434
|
/** Shared handler for `agents artifacts share delete <targets...>` and the
|
|
345
435
|
* nested `agents artifacts unshare <targets...>` alias. Deletes each target independently and
|
|
346
436
|
* continues past a failed one (rm-style), reporting all results and exiting
|
|
@@ -426,6 +516,36 @@ const SHARE_DELETE_NOTES = `
|
|
|
426
516
|
|
|
427
517
|
agents artifacts share delete === agents artifacts unshare (same command, different name).
|
|
428
518
|
`;
|
|
519
|
+
const SHARE_VISIBILITY_EXAMPLES = `
|
|
520
|
+
# Take a public page private to just you (Phoenix session required for me/org)
|
|
521
|
+
agents artifacts share visibility https://share.agents-cli.sh/octocat/q3-plan me
|
|
522
|
+
|
|
523
|
+
# Make it public again — by <user>/<slug> or a bare slug in your namespace
|
|
524
|
+
agents artifacts share visibility octocat/q3-plan public
|
|
525
|
+
agents artifacts share visibility q3-plan unlisted
|
|
526
|
+
|
|
527
|
+
# Share with your Phoenix org (rejected on a public-inbox email domain)
|
|
528
|
+
agents artifacts share visibility q3-plan org
|
|
529
|
+
|
|
530
|
+
# Machine-readable result
|
|
531
|
+
agents artifacts share visibility q3-plan me --visibility-json
|
|
532
|
+
`;
|
|
533
|
+
const SHARE_VISIBILITY_NOTES = `
|
|
534
|
+
Changes an ALREADY-published page's visibility in place: the slug — and so the
|
|
535
|
+
URL — is preserved. It re-stamps only the visibility on the stored object; the
|
|
536
|
+
body, provenance, label, and --meta are untouched, so — like 'agents artifacts
|
|
537
|
+
share edit' — this is a metadata-only rewrite and creates no revision.
|
|
538
|
+
|
|
539
|
+
public is listed in the gallery; unlisted is a capability URL (GET still 200,
|
|
540
|
+
X-Robots-Tag: noindex, hidden from gallery/list); me is visible only to the
|
|
541
|
+
signed-in owner; org is visible to members of the same Phoenix organization.
|
|
542
|
+
me and org require a Phoenix session — run 'agents auth login' first — and org
|
|
543
|
+
is refused on a public-inbox email domain (gmail.com, outlook.com, …).
|
|
544
|
+
|
|
545
|
+
This hits the live endpoint via the owner's Phoenix session (or the BYO
|
|
546
|
+
WRITE_TOKEN for public/unlisted). It does not re-run the pre-publish
|
|
547
|
+
sensitive-content scan — the body is unchanged.
|
|
548
|
+
`;
|
|
429
549
|
/**
|
|
430
550
|
* Register the `share` subtree under its parent group — `agents artifacts share`
|
|
431
551
|
* (see commands/artifacts.ts). `agents artifacts unshare` is the nested alias
|
|
@@ -436,6 +556,10 @@ const SHARE_DELETE_NOTES = `
|
|
|
436
556
|
export function registerShareCommands(artifactsCmd) {
|
|
437
557
|
const shareCmd = artifactsCmd
|
|
438
558
|
.command('share')
|
|
559
|
+
// Child task commands intentionally reuse publish vocabulary (`edit --label`,
|
|
560
|
+
// `list --meta`). Keep option ownership positional within this subtree so
|
|
561
|
+
// Commander does not silently resolve those flags to the parent publisher.
|
|
562
|
+
.enablePositionalOptions()
|
|
439
563
|
.description('Publish an HTML file to a shareable link — managed if signed in, otherwise your Cloudflare R2.')
|
|
440
564
|
.argument('[file]', 'file to publish (HTML or any static asset)')
|
|
441
565
|
.option('--slug <slug>', 'URL slug override (default: stable slug of the artifact title, then filename)')
|
|
@@ -553,6 +677,43 @@ ${SHARE_DELETE_NOTES}
|
|
|
553
677
|
shareDeleteCmd.action(async (targets, opts) => {
|
|
554
678
|
await runShareDelete(targets, opts);
|
|
555
679
|
});
|
|
680
|
+
// `agents artifacts share visibility <target> <level>` — change an
|
|
681
|
+
// already-published page's visibility in place. Kept in its own block; it
|
|
682
|
+
// reuses the delete target parser and the same PATCH metadata-edit route as
|
|
683
|
+
// `share edit` (visibility is metadata), so it re-stamps the stored object
|
|
684
|
+
// rather than re-publishing — a re-publish over the PUT path can't express an
|
|
685
|
+
// in-place change (GET injects the attribution bar at serve time and never
|
|
686
|
+
// strips it, and the served body carries no provenance/label/--meta back to
|
|
687
|
+
// re-send). --for-user / --visibility-json (not --github-user / --json): the
|
|
688
|
+
// parent `share <file>` already owns both long names, and commander resolves a
|
|
689
|
+
// child option's long name against the whole ancestor chain, silently dropping
|
|
690
|
+
// a same-named child (RUSH-2687) — the same rename the delete/list blocks make.
|
|
691
|
+
const shareVisibilityCmd = shareCmd
|
|
692
|
+
.command('visibility')
|
|
693
|
+
.description("Change an already-published page's visibility in place (public | unlisted | me | org). The slug/URL is preserved; the body is untouched, so no revision is created.")
|
|
694
|
+
.addArgument(new Argument('<target>', 'the published page: a full URL, <user>/<slug>, or a bare slug in your namespace'))
|
|
695
|
+
.addArgument(new Argument('<level>', 'new visibility').choices([...SHARE_VISIBILITY_LEVELS]))
|
|
696
|
+
.option('--for-user <user>', 'GitHub username for resolving a bare-slug target (default: resolved from gh/git config)')
|
|
697
|
+
.option('--visibility-json', 'emit a machine-readable result')
|
|
698
|
+
.action(async (target, level, opts) => {
|
|
699
|
+
try {
|
|
700
|
+
const visibility = level;
|
|
701
|
+
const result = await runShareEdit(target, { visibility, githubUser: opts.forUser });
|
|
702
|
+
if (opts.visibilityJson) {
|
|
703
|
+
console.log(JSON.stringify(result, null, 2));
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
console.log(formatShareVisibilityResult(result, visibility));
|
|
707
|
+
}
|
|
708
|
+
catch (e) {
|
|
709
|
+
console.error(chalk.red(e.message));
|
|
710
|
+
process.exitCode = 1;
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
setHelpSections(shareVisibilityCmd, {
|
|
714
|
+
examples: SHARE_VISIBILITY_EXAMPLES,
|
|
715
|
+
notes: SHARE_VISIBILITY_NOTES,
|
|
716
|
+
});
|
|
556
717
|
shareCmd
|
|
557
718
|
.command('join')
|
|
558
719
|
.description('Use an existing synced share endpoint and write token (no provisioning).')
|
|
@@ -664,20 +825,33 @@ ${SHARE_DELETE_NOTES}
|
|
|
664
825
|
// share.test.ts). Matches the `revisions` precedent (--for-user/
|
|
665
826
|
// --revisions-json) and the `delete`/`unshare` precedent (--for-user/
|
|
666
827
|
// --delete-json) below.
|
|
828
|
+
//
|
|
829
|
+
// `--visibility` is also owned by the parent `share <file>` command (publish),
|
|
830
|
+
// so the listing filter is named `--scope` here; `--all` is the convenience
|
|
831
|
+
// alias for `--scope all`.
|
|
667
832
|
.option('--for-user <user>', 'GitHub username whose namespace to list (default: resolved from gh/git config)')
|
|
833
|
+
.addOption(new Option('--scope <level>', 'visibility filter: public (default), unlisted, me, org, or all')
|
|
834
|
+
.choices(['public', 'unlisted', 'me', 'org', 'all'])
|
|
835
|
+
.default('public'))
|
|
836
|
+
.option('--all', "list every page including hidden unlisted/me/org shares (alias for --scope all)")
|
|
668
837
|
.option('--agent <name>', 'filter to shares published by this agent/harness (case-insensitive)')
|
|
669
838
|
.option('--session <id>', 'filter to shares published from this session id')
|
|
670
839
|
// Named --label-contains, not --label: `share <file>` (the parent) already owns
|
|
671
840
|
// `--label`/`--title`, same collision class as --for-user/--list-json above.
|
|
672
841
|
.option('--label-contains <substr>', 'filter to shares whose label contains this text (case-insensitive)')
|
|
673
|
-
.option('--
|
|
842
|
+
.option('--meta <key=value>', 'filter by an exact arbitrary metadata entry (repeatable)', (v, p) => [...p, v], [])
|
|
843
|
+
.option('--list-json', 'emit the machine-readable listing (slug, url, size, contentType, publishedAt, expiresAt, label, visibility, agent, session, host, repo, revisionCount, meta)')
|
|
674
844
|
.action(async (opts) => {
|
|
675
845
|
try {
|
|
846
|
+
const scope = opts.all ? 'all' : (opts.scope ?? 'public');
|
|
847
|
+
const parentMeta = shareCmd.opts().meta ?? [];
|
|
676
848
|
const result = await runShareList({
|
|
677
849
|
githubUser: opts.forUser,
|
|
850
|
+
scope,
|
|
678
851
|
agent: opts.agent,
|
|
679
852
|
session: opts.session,
|
|
680
853
|
label: opts.labelContains,
|
|
854
|
+
meta: parseMetaEntries(opts.meta.length ? opts.meta : parentMeta),
|
|
681
855
|
});
|
|
682
856
|
console.log(formatShareList(result, Boolean(opts.listJson)));
|
|
683
857
|
}
|
|
@@ -691,6 +865,11 @@ ${SHARE_DELETE_NOTES}
|
|
|
691
865
|
# Everything you've published, newest first
|
|
692
866
|
agents artifacts share list
|
|
693
867
|
|
|
868
|
+
# Include your hidden pages (unlisted / me / org) so you can see everything
|
|
869
|
+
agents artifacts share list --all
|
|
870
|
+
agents artifacts share list --scope me
|
|
871
|
+
agents artifacts share list --scope unlisted
|
|
872
|
+
|
|
694
873
|
# Machine-readable — e.g. pull every still-public URL with jq
|
|
695
874
|
agents artifacts share list --list-json | jq -r '.objects[].url'
|
|
696
875
|
|
|
@@ -700,18 +879,83 @@ ${SHARE_DELETE_NOTES}
|
|
|
700
879
|
# Narrow by who/what published it
|
|
701
880
|
agents artifacts share list --agent claude
|
|
702
881
|
agents artifacts share list --label-contains "fleet plan"
|
|
882
|
+
agents artifacts share list --meta kind=plan --meta status=final
|
|
703
883
|
`,
|
|
704
884
|
notes: `
|
|
705
885
|
Lists the ACTIVE pages in your namespace — expired links and the sibling .png OG
|
|
706
|
-
covers are omitted
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
886
|
+
covers are omitted. By default this mirrors the public gallery (public pages only).
|
|
887
|
+
Use --scope unlisted|me|org or --all to include your hidden pages; those scopes
|
|
888
|
+
send the owner's bearer and a 'scope=mine' hint to the Worker's JSON listing route,
|
|
889
|
+
which returns hidden pages ONLY after verifying the caller owns the namespace.
|
|
890
|
+
Signed-in users list the managed endpoint (share.agents-cli.sh/<handle>); otherwise
|
|
891
|
+
BYO. It reads the endpoint's JSON listing route, which ships with the current
|
|
892
|
+
Worker template. If a BYO Worker predates this feature the command says so and
|
|
893
|
+
points you at 'agents artifacts share update' (RUSH-2449) rather than returning a
|
|
894
|
+
wrong or empty result — see 'agents artifacts share status' for whether an update
|
|
895
|
+
is due.
|
|
896
|
+
|
|
897
|
+
--agent/--session/--label-contains/--meta filter the fetched listing client-side;
|
|
898
|
+
--list-json's count reflects the filtered set. Each human row shows the page's
|
|
899
|
+
visibility so public vs hidden is obvious at a glance.
|
|
900
|
+
`,
|
|
901
|
+
});
|
|
902
|
+
const shareEditCmd = shareCmd
|
|
903
|
+
.command('edit <target>')
|
|
904
|
+
.description('Edit a published share\'s label or arbitrary metadata without republishing its body.')
|
|
905
|
+
.option('--for-user <user>', 'namespace for resolving a bare slug (default: your namespace)')
|
|
906
|
+
.option('--label <text>', 'set or replace the display label')
|
|
907
|
+
.option('--remove-label', 'remove the display label')
|
|
908
|
+
.option('--meta <key=value>', 'merge a metadata entry (repeatable)', (v, p) => [...p, v], [])
|
|
909
|
+
.option('--replace-meta <key=value>', 'replace all arbitrary metadata with these entries (repeatable)', (v, p) => [...p, v], [])
|
|
910
|
+
.option('--remove-meta <key>', 'remove an arbitrary metadata key (repeatable)', (v, p) => [...p, v], [])
|
|
911
|
+
.option('--edit-json', 'emit the machine-readable edit result')
|
|
912
|
+
.action(async (target, opts) => {
|
|
913
|
+
try {
|
|
914
|
+
const parent = shareCmd.opts();
|
|
915
|
+
const label = opts.label ?? parent.label;
|
|
916
|
+
const mergeMeta = opts.meta.length ? opts.meta : (parent.meta ?? []);
|
|
917
|
+
if (label !== undefined && opts.removeLabel)
|
|
918
|
+
throw new Error('--label and --remove-label are mutually exclusive.');
|
|
919
|
+
if (mergeMeta.length && opts.replaceMeta.length)
|
|
920
|
+
throw new Error('--meta and --replace-meta are mutually exclusive.');
|
|
921
|
+
if (opts.replaceMeta.length && opts.removeMeta.length)
|
|
922
|
+
throw new Error('--replace-meta and --remove-meta are mutually exclusive.');
|
|
923
|
+
if (label === undefined && !opts.removeLabel && !mergeMeta.length && !opts.replaceMeta.length && !opts.removeMeta.length)
|
|
924
|
+
throw new Error('Nothing to edit. Pass --label, --remove-label, --meta, --replace-meta, or --remove-meta.');
|
|
925
|
+
const removeMeta = opts.removeMeta.map((key) => Object.keys(parseMetaEntries([`${key}=x`]))[0]);
|
|
926
|
+
const result = await runShareEdit(target, {
|
|
927
|
+
githubUser: opts.forUser,
|
|
928
|
+
label: opts.removeLabel ? null : label === undefined ? undefined : sanitizeLabel(label),
|
|
929
|
+
meta: parseMetaEntries(opts.replaceMeta.length ? opts.replaceMeta : mergeMeta),
|
|
930
|
+
metaMode: opts.replaceMeta.length ? 'replace' : 'merge',
|
|
931
|
+
removeMeta,
|
|
932
|
+
// Parent `share --force` (RUSH-2687: a child --force is silently dropped).
|
|
933
|
+
force: parent.force,
|
|
934
|
+
});
|
|
935
|
+
console.log(opts.editJson ? JSON.stringify(result, null, 2) : chalk.green(`updated ${result.url}`));
|
|
936
|
+
}
|
|
937
|
+
catch (e) {
|
|
938
|
+
console.error(chalk.red(e.message));
|
|
939
|
+
process.exitCode = 1;
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
setHelpSections(shareEditCmd, {
|
|
943
|
+
examples: `
|
|
944
|
+
# Change only the gallery/list title
|
|
945
|
+
agents artifacts share edit q3-plan --label "Q3 fleet plan"
|
|
712
946
|
|
|
713
|
-
|
|
714
|
-
|
|
947
|
+
# Merge metadata, or deliberately replace/remove it
|
|
948
|
+
agents artifacts share edit q3-plan --meta status=final --meta ticket=PHNX-3278
|
|
949
|
+
agents artifacts share edit q3-plan --replace-meta kind=plan --replace-meta status=final
|
|
950
|
+
agents artifacts share edit q3-plan --remove-meta status
|
|
951
|
+
`,
|
|
952
|
+
notes: `
|
|
953
|
+
Metadata edits preserve the exact published body, content type/HTTP metadata,
|
|
954
|
+
publication time, visibility, expiry, provenance, cover, and retained revisions. --meta merges;
|
|
955
|
+
--replace-meta replaces all arbitrary metadata; --remove-meta deletes named keys.
|
|
956
|
+
Reserved Worker/provenance keys cannot be changed through metadata flags.
|
|
957
|
+
Edited labels and metadata values are scanned for emails and credential-shaped
|
|
958
|
+
strings the same way publish is — pass --force to write them anyway.
|
|
715
959
|
`,
|
|
716
960
|
});
|
|
717
961
|
const shareRevisionsCmd = shareCmd
|
package/dist/commands/ssh.d.ts
CHANGED
|
@@ -48,6 +48,13 @@ export declare function showLeasedBoxesSection(opts: {
|
|
|
48
48
|
all?: boolean;
|
|
49
49
|
stats?: boolean;
|
|
50
50
|
}): boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Remote argv for `agents ssh <slug>` into a leased crabbox. Crabbox ssh does
|
|
53
|
+
* not go through {@link buildSshInvocation}, so this stamps the same
|
|
54
|
+
* AGENTS_FLEET_REMOTE consent marker a registered-device ssh would (PHNX-3065).
|
|
55
|
+
* Exported so the branch is unit-testable without a live crabbox.
|
|
56
|
+
*/
|
|
57
|
+
export declare function leasedBoxRemoteCmd(cmd: string[]): string[];
|
|
51
58
|
/**
|
|
52
59
|
* Race `fanOut` against an overall wall-clock deadline (RUSH-2041).
|
|
53
60
|
*
|
package/dist/commands/ssh.js
CHANGED
|
@@ -34,7 +34,7 @@ import { clearPendingSentinel } from '../lib/devices/pending.js';
|
|
|
34
34
|
import { getDeviceDiscoveryStatus, setDeviceDiscoveryStatus } from '../lib/devices/discovery-policy.js';
|
|
35
35
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
36
36
|
import { hostNameFor, renderSshConfig } from '../lib/devices/ssh-config.js';
|
|
37
|
-
import { ASKPASS_BUNDLE_ENV, ASKPASS_KEY_ENV, buildSshInvocation, deviceIdentityArgs, fleetDialTarget, writeAskpassShim, } from '../lib/devices/connect.js';
|
|
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
40
|
import { fanOutDevices, fleetHealthSkip, planFleetTargets, remoteFleetTargets, runFleet, skipLabel, upgradeCommand, } from '../lib/devices/fleet.js';
|
|
@@ -351,9 +351,21 @@ function trySshLeasedBox(name, cmd) {
|
|
|
351
351
|
console.error(chalk.red(`Leased box '${name}' is not reachable yet (status: ${boxStatus(box)}).`));
|
|
352
352
|
process.exit(1);
|
|
353
353
|
}
|
|
354
|
-
|
|
354
|
+
// Crabbox ssh does not go through buildSshInvocation; stamp the same
|
|
355
|
+
// consent marker so a browser drive on a leased box is gated too.
|
|
356
|
+
const remoteCmd = leasedBoxRemoteCmd(cmd);
|
|
357
|
+
const res = spawnSync(sshArgv[0], [...sshArgv.slice(1), ...remoteCmd], { stdio: 'inherit' });
|
|
355
358
|
process.exit(res.status ?? 1);
|
|
356
359
|
}
|
|
360
|
+
/**
|
|
361
|
+
* Remote argv for `agents ssh <slug>` into a leased crabbox. Crabbox ssh does
|
|
362
|
+
* not go through {@link buildSshInvocation}, so this stamps the same
|
|
363
|
+
* AGENTS_FLEET_REMOTE consent marker a registered-device ssh would (PHNX-3065).
|
|
364
|
+
* Exported so the branch is unit-testable without a live crabbox.
|
|
365
|
+
*/
|
|
366
|
+
export function leasedBoxRemoteCmd(cmd) {
|
|
367
|
+
return isAgentsBrowserDrive(cmd) ? markFleetRemote(cmd, { shell: 'posix' }) : cmd;
|
|
368
|
+
}
|
|
357
369
|
/** Resolve a device or exit with a clear error. */
|
|
358
370
|
async function mustGetDevice(name) {
|
|
359
371
|
const d = await getDevice(name);
|
|
@@ -1136,7 +1148,7 @@ function registerDevicesCommands(program) {
|
|
|
1136
1148
|
registerFleetApplyAlias(devicesCmd);
|
|
1137
1149
|
devicesCmd
|
|
1138
1150
|
.command('register <name>')
|
|
1139
|
-
.description(
|
|
1151
|
+
.description("Register a discovered node and record the approval in this box's device doc (fleet.discovery), unioned fleet-wide.")
|
|
1140
1152
|
.action(async (name) => {
|
|
1141
1153
|
try {
|
|
1142
1154
|
const nodes = parseTailscaleStatus(tailscaleStatusJson());
|
|
@@ -1145,11 +1157,20 @@ function registerDevicesCommands(program) {
|
|
|
1145
1157
|
console.error(chalk.red(`'${name}' is not a current tailscale node. See 'agents devices sync'.`));
|
|
1146
1158
|
process.exit(1);
|
|
1147
1159
|
}
|
|
1148
|
-
await removeIgnored(name); //
|
|
1160
|
+
await removeIgnored(name); // clear THIS box's dismissal, if any
|
|
1149
1161
|
const d = await upsertDevice(name, nodeToDeviceInput(node));
|
|
1150
1162
|
setDeviceDiscoveryStatus(name, 'approved');
|
|
1151
1163
|
clearPendingSentinel(name); // drop the notification immediately
|
|
1152
1164
|
console.log(chalk.green(`Registered '${name}'`) + chalk.gray(` (${d.platform})`));
|
|
1165
|
+
// A dismissal on another box still wins the union (ignored beats
|
|
1166
|
+
// approved), so approving here does not un-dismiss it fleet-wide — say so
|
|
1167
|
+
// rather than imply the node is now discoverable everywhere (PHNX-3315).
|
|
1168
|
+
const otherDismissals = loadIgnoredEntries().filter((e) => e.name === name);
|
|
1169
|
+
if (otherDismissals.length > 0 || getDeviceDiscoveryStatus(name) === 'ignored') {
|
|
1170
|
+
const boxes = [...new Set(otherDismissals.map((e) => e.ignoredOn))].sort().join(', ') || 'another box';
|
|
1171
|
+
console.error(chalk.yellow(`Note: '${name}' is still dismissed on: ${boxes}, so it stays ignored fleet-wide`) +
|
|
1172
|
+
chalk.gray(` until you run \`agents devices unignore ${name}\` there.`));
|
|
1173
|
+
}
|
|
1153
1174
|
}
|
|
1154
1175
|
catch (err) {
|
|
1155
1176
|
console.error(chalk.red(err.message));
|
|
@@ -1158,7 +1179,7 @@ function registerDevicesCommands(program) {
|
|
|
1158
1179
|
});
|
|
1159
1180
|
devicesCmd
|
|
1160
1181
|
.command('ignore <name>')
|
|
1161
|
-
.description(
|
|
1182
|
+
.description("Dismiss a node and record the decision in this box's device doc (fleet.ignored), unioned fleet-wide (also removes it locally).")
|
|
1162
1183
|
.action(async (name) => {
|
|
1163
1184
|
try {
|
|
1164
1185
|
await removeDevice(name);
|
|
@@ -1176,13 +1197,25 @@ function registerDevicesCommands(program) {
|
|
|
1176
1197
|
.command('unignore <name>')
|
|
1177
1198
|
.description('Undo `ignore`: allow a node to be discovered and registered again.')
|
|
1178
1199
|
.action(async (name) => {
|
|
1179
|
-
const
|
|
1180
|
-
|
|
1181
|
-
|
|
1200
|
+
const wasIgnored = getDeviceDiscoveryStatus(name) === 'ignored' || loadIgnoredEntries().some((e) => e.name === name);
|
|
1201
|
+
// A box can edit only its OWN device doc (PHNX-3315), so this clears just
|
|
1202
|
+
// this box's dismissal/approval decision.
|
|
1203
|
+
await removeIgnored(name);
|
|
1204
|
+
setDeviceDiscoveryStatus(name, undefined);
|
|
1205
|
+
// Re-evaluate the cross-box union AFTER clearing our slice: another box's
|
|
1206
|
+
// dismissal keeps the node ignored fleet-wide (ignored beats approved), so
|
|
1207
|
+
// never report a false "no longer ignoring" — name the box(es) to clear.
|
|
1208
|
+
const remaining = loadIgnoredEntries().filter((e) => e.name === name);
|
|
1209
|
+
if (remaining.length > 0 || getDeviceDiscoveryStatus(name) === 'ignored') {
|
|
1210
|
+
const boxes = [...new Set(remaining.map((e) => e.ignoredOn))].sort().join(', ') || 'another box';
|
|
1211
|
+
console.error(chalk.yellow(`Cleared this box's decision, but '${name}' is still dismissed on: ${boxes}.`) +
|
|
1212
|
+
chalk.gray(`\nRun \`agents devices unignore ${name}\` there too to clear it fleet-wide.`));
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
if (!wasIgnored) {
|
|
1182
1216
|
console.error(chalk.gray(`'${name}' was not ignored.`));
|
|
1183
1217
|
return;
|
|
1184
1218
|
}
|
|
1185
|
-
setDeviceDiscoveryStatus(name, undefined);
|
|
1186
1219
|
console.log(chalk.green(`No longer ignoring '${name}'`) + chalk.gray(' — run `agents devices sync` to register it.'));
|
|
1187
1220
|
});
|
|
1188
1221
|
const ignoredCmd = devicesCmd
|
|
@@ -1219,11 +1252,13 @@ function registerDevicesCommands(program) {
|
|
|
1219
1252
|
agents devices unignore old-laptop # undo a dismissal
|
|
1220
1253
|
`,
|
|
1221
1254
|
notes: `
|
|
1222
|
-
Dismissals live in
|
|
1223
|
-
sync with 'agents repo
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1255
|
+
Dismissals live in each box's tracked device doc
|
|
1256
|
+
(devices/<host>/agents.yaml fleet.ignored) and sync with 'agents repo
|
|
1257
|
+
push/pull'; the effective list is their cross-box union, so a node
|
|
1258
|
+
dismissed on one box stays dismissed everywhere without the boxes ever
|
|
1259
|
+
rewriting one shared file. An ignored node is not a device — it never
|
|
1260
|
+
enters the registry, so 'agents devices list' never shows it; this command
|
|
1261
|
+
is where dismissals are visible.
|
|
1227
1262
|
`,
|
|
1228
1263
|
});
|
|
1229
1264
|
// ─── devices config (unified settings surface) ────────────────────────────
|
|
@@ -2389,6 +2424,10 @@ An interactive login with no command mirrors the home-relative directory you
|
|
|
2389
2424
|
launched from — 'agents ssh yosemite-s0' from ~/src/app lands in ~/src/app on
|
|
2390
2425
|
the target when it exists, else the remote home. Same portable-cwd rule as
|
|
2391
2426
|
'agents run --device'. Passing a command keeps the remote home.
|
|
2427
|
+
|
|
2428
|
+
An 'agents browser …', 'ag browser …', or standalone 'browser …' command is
|
|
2429
|
+
stamped AGENTS_FLEET_REMOTE so the target's browser.remote-control consent
|
|
2430
|
+
gate applies, same as 'agents browser <verb> --device <name>'.
|
|
2392
2431
|
`)
|
|
2393
2432
|
.action(async (name, cmd) => {
|
|
2394
2433
|
// Hidden askpass bridge: ssh execs the shim, which re-invokes us here.
|
package/dist/commands/status.js
CHANGED
|
@@ -97,6 +97,20 @@ export function registerStatusCommand(syncCmd) {
|
|
|
97
97
|
if (status.totals.orphan > 0) {
|
|
98
98
|
console.log(chalk.gray(` (${status.totals.orphan} orphan${status.totals.orphan === 1 ? '' : 's'} — run \`agents prune cleanup\`)`));
|
|
99
99
|
}
|
|
100
|
+
// Config drift is its OWN class, not a per-agent "N missing": a box that has
|
|
101
|
+
// not folded its device-scoped state still carries a stale top-level header or
|
|
102
|
+
// a lingering central fleet/hosts/accounts/browser block. Surface it distinctly
|
|
103
|
+
// so an un-drained box is SEEN here instead of via a mystery pull conflict
|
|
104
|
+
// (PHNX-3315).
|
|
105
|
+
if (status.config.staleHeader || status.config.centralLeaks.length > 0) {
|
|
106
|
+
console.log(` ${'config (device-scoped)'.padEnd(28)} ${chalk.yellow('not drained — folds automatically on normal `agents` use here (idempotent)')}`);
|
|
107
|
+
if (status.config.staleHeader) {
|
|
108
|
+
console.log(chalk.gray(' · top-level agents.yaml header is stale (pre-rename)'));
|
|
109
|
+
}
|
|
110
|
+
for (const leak of status.config.centralLeaks) {
|
|
111
|
+
console.log(chalk.gray(` · central ${leak} should be device-scoped`));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
100
114
|
// Hand off to the shared interactive/apply flow (summary already printed above).
|
|
101
115
|
await promptDriftSync({ cwd, yes: opts.yes, status, quiet: true });
|
|
102
116
|
});
|