@phnx-labs/agents-cli 1.22.37 → 1.22.39
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 +50 -0
- package/README.md +8 -8
- package/dist/bin/agents +0 -0
- package/dist/bootstrap.js +3 -2
- package/dist/commands/artifacts-setup.d.ts +53 -0
- package/dist/commands/{setup-share.js → artifacts-setup.js} +59 -13
- package/dist/commands/artifacts.d.ts +18 -0
- package/dist/commands/artifacts.js +58 -0
- package/dist/commands/browser.js +2 -0
- package/dist/commands/config.js +31 -1
- package/dist/commands/exec.js +2 -2
- package/dist/commands/models.js +67 -0
- package/dist/commands/setup.js +5 -5
- package/dist/commands/share.d.ts +20 -7
- package/dist/commands/share.js +74 -75
- package/dist/commands/ssh.js +156 -8
- package/dist/lib/browser/hygiene.d.ts +90 -0
- package/dist/lib/browser/hygiene.js +146 -0
- package/dist/lib/browser/ipc.js +12 -0
- package/dist/lib/browser/service.d.ts +75 -1
- package/dist/lib/browser/service.js +201 -11
- package/dist/lib/browser/types.d.ts +44 -1
- package/dist/lib/config-keys.d.ts +11 -3
- package/dist/lib/config-keys.js +22 -3
- package/dist/lib/config-machine-keys.js +1 -0
- package/dist/lib/device-config.d.ts +34 -0
- package/dist/lib/device-config.js +96 -0
- package/dist/lib/devices/pool.d.ts +56 -0
- package/dist/lib/devices/pool.js +85 -0
- package/dist/lib/exec.d.ts +4 -1
- package/dist/lib/exec.js +8 -2
- package/dist/lib/git.d.ts +1 -1
- package/dist/lib/git.js +1 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/migrate.d.ts +36 -0
- package/dist/lib/migrate.js +107 -0
- package/dist/lib/routines.d.ts +3 -1
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/share/analytics.js +1 -1
- package/dist/lib/share/capture.d.ts +1 -1
- package/dist/lib/share/capture.js +3 -3
- package/dist/lib/share/config.d.ts +2 -2
- package/dist/lib/share/config.js +5 -5
- package/dist/lib/share/delete.js +3 -3
- package/dist/lib/share/provision.js +3 -3
- package/dist/lib/share/publish.d.ts +1 -1
- package/dist/lib/share/publish.js +3 -3
- package/dist/lib/share/worker-template.d.ts +12 -1
- package/dist/lib/share/worker-template.js +13 -2
- package/dist/lib/smart-launch.d.ts +33 -3
- package/dist/lib/smart-launch.js +61 -6
- package/dist/lib/startup/command-registry.d.ts +10 -2
- package/dist/lib/startup/command-registry.js +16 -7
- package/dist/lib/tmux/orphan-reap.d.ts +15 -19
- package/dist/lib/tmux/orphan-reap.js +15 -21
- package/dist/lib/tmux/session.js +4 -3
- package/dist/lib/triggers/handlers.js +10 -0
- package/dist/lib/triggers/webhook.js +10 -0
- package/dist/lib/types.d.ts +2 -2
- package/package.json +1 -1
- package/dist/commands/set.d.ts +0 -15
- package/dist/commands/set.js +0 -79
- package/dist/commands/setup-share.d.ts +0 -17
package/dist/commands/share.d.ts
CHANGED
|
@@ -57,8 +57,8 @@ interface ShareDeleteCliOpts {
|
|
|
57
57
|
githubUser?: string;
|
|
58
58
|
json?: boolean;
|
|
59
59
|
}
|
|
60
|
-
/** Shared handler for `agents share delete <targets...>` and the
|
|
61
|
-
* `agents unshare <targets...>` alias. Deletes each target independently and
|
|
60
|
+
/** Shared handler for `agents artifacts share delete <targets...>` and the
|
|
61
|
+
* top-level `agents unshare <targets...>` alias. Deletes each target independently and
|
|
62
62
|
* continues past a failed one (rm-style), reporting all results and exiting
|
|
63
63
|
* non-zero if any target failed to verify as gone.
|
|
64
64
|
*
|
|
@@ -66,10 +66,23 @@ interface ShareDeleteCliOpts {
|
|
|
66
66
|
* never exposed as a CLI flag, only used to inject a fake config/checker/deleter
|
|
67
67
|
* without touching the keychain or a live endpoint. */
|
|
68
68
|
export declare function runShareDelete(targets: string[], opts: ShareDeleteCliOpts, deleteFn?: typeof deleteShare): Promise<void>;
|
|
69
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Register the `share` subtree under its parent group — `agents artifacts share`
|
|
71
|
+
* (see commands/artifacts.ts). The top-level `agents unshare` alias is a sibling
|
|
72
|
+
* registration on the ROOT program, so it is {@link registerUnshareCommand}, not
|
|
73
|
+
* part of this subtree.
|
|
74
|
+
*/
|
|
75
|
+
export declare function registerShareCommands(artifactsCmd: Command): void;
|
|
76
|
+
/**
|
|
77
|
+
* Register the top-level `agents unshare <targets...>` alias of
|
|
78
|
+
* `agents artifacts share delete`. It takes the ROOT program (not the artifacts
|
|
79
|
+
* group) because it is deliberately a top-level convenience verb — taking a page
|
|
80
|
+
* down is the one artifact action typed often enough to keep at the root.
|
|
81
|
+
*/
|
|
82
|
+
export declare function registerUnshareCommand(program: Command): void;
|
|
70
83
|
/** Provision a fresh R2 bucket + Worker on the user's Cloudflare and persist the
|
|
71
|
-
* endpoint config + write token. Shared by `agents
|
|
72
|
-
*
|
|
84
|
+
* endpoint config + write token. Shared by both modes of `agents artifacts setup`
|
|
85
|
+
* (the flag-driven provision and the interactive wizard). */
|
|
73
86
|
export declare function runShareProvision(opts: {
|
|
74
87
|
bundle: string;
|
|
75
88
|
worker: string;
|
|
@@ -101,8 +114,8 @@ export declare function runShareUpdate(opts?: {
|
|
|
101
114
|
request?: CloudflareRequester;
|
|
102
115
|
}): Promise<ShareUpdateResult>;
|
|
103
116
|
/** Join an existing share endpoint (no provisioning): prompt for the endpoint
|
|
104
|
-
* details + write token and persist them. Shared by `agents share join`
|
|
105
|
-
*
|
|
117
|
+
* details + write token and persist them. Shared by `agents artifacts share join`
|
|
118
|
+
* and the `agents artifacts setup` wizard. */
|
|
106
119
|
export declare function runShareJoin(baseUrl?: string, opts?: {
|
|
107
120
|
token?: string;
|
|
108
121
|
}): Promise<void>;
|
package/dist/commands/share.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
// `agents share` — publish an HTML file to your own Cloudflare R2
|
|
2
|
-
// Worker, and get a shareable link (~$0). See apps/cli/docs/share.md.
|
|
1
|
+
// `agents artifacts share` — publish an HTML file to your own Cloudflare R2
|
|
2
|
+
// behind a tiny Worker, and get a shareable link (~$0). See apps/cli/docs/share.md.
|
|
3
|
+
//
|
|
4
|
+
// Registered under the `artifacts` group by commands/artifacts.ts; the
|
|
5
|
+
// provisioning door lives beside it at `agents artifacts setup`
|
|
6
|
+
// (commands/artifacts-setup.ts), which calls `runShareProvision` below.
|
|
3
7
|
import { existsSync } from 'node:fs';
|
|
4
8
|
import chalk from 'chalk';
|
|
5
9
|
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';
|
|
@@ -35,10 +39,10 @@ export function shareTemplateStatus(cfg) {
|
|
|
35
39
|
}
|
|
36
40
|
/** Shown whenever the deployed Worker has no `?format=json` listing route — an
|
|
37
41
|
* endpoint provisioned before this feature. Points at the RUSH-2449 update path
|
|
38
|
-
* (`agents share update`) instead of letting the caller hit a 404 or an
|
|
39
|
-
* and get a confusing parse error. */
|
|
40
|
-
const OUTDATED_TEMPLATE_HINT = 'Your deployed share Worker has no machine-readable listing route — it predates `agents share list`. ' +
|
|
41
|
-
'Run `agents share update` to deploy the current Worker template, then retry (`agents share status` shows whether an update is due).';
|
|
42
|
+
* (`agents artifacts share update`) instead of letting the caller hit a 404 or an
|
|
43
|
+
* HTML body and get a confusing parse error. */
|
|
44
|
+
const OUTDATED_TEMPLATE_HINT = 'Your deployed share Worker has no machine-readable listing route — it predates `agents artifacts share list`. ' +
|
|
45
|
+
'Run `agents artifacts share update` to deploy the current Worker template, then retry (`agents artifacts share status` shows whether an update is due).';
|
|
42
46
|
async function defaultListingFetch(url) {
|
|
43
47
|
const res = await fetch(url, { headers: { accept: 'application/json' } });
|
|
44
48
|
return { status: res.status, contentType: res.headers.get('content-type') ?? '', body: await res.text() };
|
|
@@ -80,7 +84,7 @@ export function parseShareListing(user, body) {
|
|
|
80
84
|
export async function runShareList(opts = {}) {
|
|
81
85
|
const cfg = opts.config ?? readShareConfig();
|
|
82
86
|
if (!cfg) {
|
|
83
|
-
throw new Error("Not set up yet. Run 'agents
|
|
87
|
+
throw new Error("Not set up yet. Run 'agents artifacts setup' (provision your own endpoint) or 'agents artifacts share join' (use an existing one).");
|
|
84
88
|
}
|
|
85
89
|
// A known-stale template can't have the listing route — say so before any network
|
|
86
90
|
// call. 'unknown' (provisioned before templateHash tracking) is attempted, then
|
|
@@ -100,14 +104,14 @@ export async function runShareList(opts = {}) {
|
|
|
100
104
|
// endpoint predates the listing route entirely. The recorded templateHash
|
|
101
105
|
// disambiguates: a 'current' template HAS the route, so its 404 means an empty
|
|
102
106
|
// namespace ("nothing published"); otherwise the route may be absent, so point
|
|
103
|
-
// at `agents share update`.
|
|
107
|
+
// at `agents artifacts share update`.
|
|
104
108
|
if (templateStatus === 'current') {
|
|
105
109
|
return { user, count: 0, objects: [] };
|
|
106
110
|
}
|
|
107
111
|
throw new Error(OUTDATED_TEMPLATE_HINT);
|
|
108
112
|
}
|
|
109
113
|
if (res.status !== 200) {
|
|
110
|
-
throw new Error(`Listing failed (${res.status}) for ${listUrl}. Check the endpoint is reachable, or that 'agents
|
|
114
|
+
throw new Error(`Listing failed (${res.status}) for ${listUrl}. Check the endpoint is reachable, or that 'agents artifacts setup' completed.`);
|
|
111
115
|
}
|
|
112
116
|
if (!/application\/json/i.test(res.contentType)) {
|
|
113
117
|
// A 200 that isn't JSON means the old Worker ignored ?format=json and served
|
|
@@ -157,8 +161,8 @@ export function formatShareDeleteResult(result, json = false) {
|
|
|
157
161
|
}
|
|
158
162
|
return lines.join('\n');
|
|
159
163
|
}
|
|
160
|
-
/** Shared handler for `agents share delete <targets...>` and the
|
|
161
|
-
* `agents unshare <targets...>` alias. Deletes each target independently and
|
|
164
|
+
/** Shared handler for `agents artifacts share delete <targets...>` and the
|
|
165
|
+
* top-level `agents unshare <targets...>` alias. Deletes each target independently and
|
|
162
166
|
* continues past a failed one (rm-style), reporting all results and exiting
|
|
163
167
|
* non-zero if any target failed to verify as gone.
|
|
164
168
|
*
|
|
@@ -200,10 +204,10 @@ function registerShareDeleteOptions(cmd) {
|
|
|
200
204
|
}
|
|
201
205
|
const SHARE_DELETE_EXAMPLES = `
|
|
202
206
|
# Delete by full URL — also takes down the sibling OG cover
|
|
203
|
-
agents share delete https://share.agents-cli.sh/octocat/my-plan-a1b2
|
|
207
|
+
agents artifacts share delete https://share.agents-cli.sh/octocat/my-plan-a1b2
|
|
204
208
|
|
|
205
209
|
# Delete by <user>/<slug>, or a bare slug in your own namespace
|
|
206
|
-
agents share delete octocat/my-plan-a1b2
|
|
210
|
+
agents artifacts share delete octocat/my-plan-a1b2
|
|
207
211
|
agents unshare my-plan-a1b2
|
|
208
212
|
|
|
209
213
|
# Several at once
|
|
@@ -220,17 +224,23 @@ const SHARE_DELETE_NOTES = `
|
|
|
220
224
|
Worker's DELETE is idempotent and returns {"ok":true} even for a key that was
|
|
221
225
|
never there, so that response alone is never proof of a takedown.
|
|
222
226
|
|
|
223
|
-
agents share delete === agents unshare (same command, different name).
|
|
227
|
+
agents artifacts share delete === agents unshare (same command, different name).
|
|
224
228
|
`;
|
|
225
|
-
|
|
226
|
-
|
|
229
|
+
/**
|
|
230
|
+
* Register the `share` subtree under its parent group — `agents artifacts share`
|
|
231
|
+
* (see commands/artifacts.ts). The top-level `agents unshare` alias is a sibling
|
|
232
|
+
* registration on the ROOT program, so it is {@link registerUnshareCommand}, not
|
|
233
|
+
* part of this subtree.
|
|
234
|
+
*/
|
|
235
|
+
export function registerShareCommands(artifactsCmd) {
|
|
236
|
+
const shareCmd = artifactsCmd
|
|
227
237
|
.command('share')
|
|
228
238
|
.description('Publish an HTML file to your own Cloudflare R2 and get a shareable link (~$0).')
|
|
229
239
|
.argument('[file]', 'file to publish (HTML or any static asset)')
|
|
230
240
|
.option('--slug <slug>', 'custom URL slug under your namespace (default: <project>-<feature>-<hash>)')
|
|
231
241
|
.option('--github-user <user>', 'GitHub username for the share namespace (default: resolved from gh/git config)')
|
|
232
242
|
.option('--expire <spec>', "auto-expire (default 30d). e.g. 12h, 30d, 2026-08-01, or 'never'")
|
|
233
|
-
.option('--unlisted', 'hide from the public gallery and `agents share list` (direct URL still works)')
|
|
243
|
+
.option('--unlisted', 'hide from the public gallery and `agents artifacts share list` (direct URL still works)')
|
|
234
244
|
.option('--private', 'alias of --unlisted')
|
|
235
245
|
.option('--force', 'publish even when the file contains emails or credential-shaped strings')
|
|
236
246
|
.option('--no-cover', 'skip the OG preview image (HTML pages get one by default)')
|
|
@@ -266,30 +276,30 @@ export function registerShareCommands(program) {
|
|
|
266
276
|
setHelpSections(shareCmd, {
|
|
267
277
|
examples: `
|
|
268
278
|
# Publish an HTML file — auto OG cover, default 30d expiry, shareable link
|
|
269
|
-
agents share ./out/plan.html
|
|
279
|
+
agents artifacts share ./out/plan.html
|
|
270
280
|
|
|
271
281
|
# Hide from the public gallery (direct URL still works) and expire sooner
|
|
272
|
-
agents share ./out/report.html --unlisted --expire 12h
|
|
282
|
+
agents artifacts share ./out/report.html --unlisted --expire 12h
|
|
273
283
|
|
|
274
284
|
# Permanent public page (opt out of the default 30d expiry)
|
|
275
|
-
agents share ./out/landing.html --slug landing --expire never
|
|
285
|
+
agents artifacts share ./out/landing.html --slug landing --expire never
|
|
276
286
|
|
|
277
287
|
# Custom slug, expiring in 7 days
|
|
278
|
-
agents share ./out/report.html --slug q3-report --expire 7d
|
|
288
|
+
agents artifacts share ./out/report.html --slug q3-report --expire 7d
|
|
279
289
|
${SHARE_DELETE_EXAMPLES}
|
|
280
290
|
# One-time setup (or join an existing endpoint)
|
|
281
|
-
agents
|
|
282
|
-
agents share join https://share.agents-cli.sh
|
|
291
|
+
agents artifacts setup
|
|
292
|
+
agents artifacts share join https://share.agents-cli.sh
|
|
283
293
|
|
|
284
294
|
# Push a worker-template.ts change out to an already-provisioned endpoint
|
|
285
|
-
agents share update
|
|
295
|
+
agents artifacts share update
|
|
286
296
|
`,
|
|
287
297
|
notes: `
|
|
288
298
|
Default expiry is 30d so an accidental publish decays. Pass --expire never for
|
|
289
299
|
a permanent link. --unlisted / --private hides the page from the public gallery
|
|
290
|
-
and agents share list; the direct URL is still world-readable
|
|
291
|
-
secret). A pre-publish scan refuses emails and credential-shaped
|
|
292
|
-
unless --force is passed.
|
|
300
|
+
and agents artifacts share list; the direct URL is still world-readable
|
|
301
|
+
(unlisted, not secret). A pre-publish scan refuses emails and credential-shaped
|
|
302
|
+
strings unless --force is passed.
|
|
293
303
|
${SHARE_DELETE_NOTES}
|
|
294
304
|
`,
|
|
295
305
|
});
|
|
@@ -300,32 +310,6 @@ ${SHARE_DELETE_NOTES}
|
|
|
300
310
|
shareDeleteCmd.action(async (targets, opts) => {
|
|
301
311
|
await runShareDelete(targets, opts);
|
|
302
312
|
});
|
|
303
|
-
const unshareCmd = registerShareDeleteOptions(program
|
|
304
|
-
.command('unshare <targets...>')
|
|
305
|
-
.description('Alias of `agents share delete` — take down a published page (and by default its OG cover).'));
|
|
306
|
-
setHelpSections(unshareCmd, { examples: SHARE_DELETE_EXAMPLES, notes: SHARE_DELETE_NOTES });
|
|
307
|
-
unshareCmd.action(async (targets, opts) => {
|
|
308
|
-
await runShareDelete(targets, opts);
|
|
309
|
-
});
|
|
310
|
-
shareCmd
|
|
311
|
-
.command('setup')
|
|
312
|
-
.description('One-time: provision an R2 bucket + Worker on your Cloudflare and save the config.')
|
|
313
|
-
.option('--bundle <name>', 'secrets bundle holding the Cloudflare API token', DEFAULT_CF_BUNDLE)
|
|
314
|
-
.option('--worker <name>', 'Worker name', DEFAULT_WORKER_NAME)
|
|
315
|
-
.option('--bucket <name>', 'R2 bucket name', DEFAULT_BUCKET_NAME)
|
|
316
|
-
.option('--account <id>', 'Cloudflare account id (else read from the bundle / prompt)')
|
|
317
|
-
.option('--token <t>', 'Cloudflare API token (else read from the --bundle)')
|
|
318
|
-
.option('--domain <host>', `custom domain to map (default: ${DEFAULT_SHARE_DOMAIN}; workers.dev if zone is not visible)`)
|
|
319
|
-
.option('--analytics-token <token>', 'Cloudflare Web Analytics token to inject into published HTML pages')
|
|
320
|
-
.action(async (opts) => {
|
|
321
|
-
try {
|
|
322
|
-
await runShareProvision(opts);
|
|
323
|
-
}
|
|
324
|
-
catch (e) {
|
|
325
|
-
console.error(chalk.red(e.message));
|
|
326
|
-
process.exitCode = 1;
|
|
327
|
-
}
|
|
328
|
-
});
|
|
329
313
|
shareCmd
|
|
330
314
|
.command('join')
|
|
331
315
|
.description('Use an existing synced share endpoint and write token (no provisioning).')
|
|
@@ -370,15 +354,15 @@ ${SHARE_DELETE_NOTES}
|
|
|
370
354
|
setHelpSections(shareUpdateCmd, {
|
|
371
355
|
examples: `
|
|
372
356
|
# Push a worker-template.ts change out to your already-provisioned endpoint
|
|
373
|
-
agents share update
|
|
357
|
+
agents artifacts share update
|
|
374
358
|
|
|
375
359
|
# Force a re-deploy even though the template hash already matches
|
|
376
|
-
agents share update --force
|
|
360
|
+
agents artifacts share update --force
|
|
377
361
|
`,
|
|
378
362
|
notes: `
|
|
379
|
-
Reuses the existing account/worker/bucket from 'agents share status' and the
|
|
363
|
+
Reuses the existing account/worker/bucket from 'agents artifacts share status' and the
|
|
380
364
|
existing write token — it never re-provisions a bucket, touches routes, or
|
|
381
|
-
regenerates the token. See 'agents share status' for whether an update is due.
|
|
365
|
+
regenerates the token. See 'agents artifacts share status' for whether an update is due.
|
|
382
366
|
`,
|
|
383
367
|
});
|
|
384
368
|
shareCmd
|
|
@@ -387,7 +371,7 @@ ${SHARE_DELETE_NOTES}
|
|
|
387
371
|
.action(async () => {
|
|
388
372
|
const cfg = readShareConfig();
|
|
389
373
|
if (!cfg) {
|
|
390
|
-
console.log(chalk.dim("Not configured. Run 'agents
|
|
374
|
+
console.log(chalk.dim("Not configured. Run 'agents artifacts setup' or 'agents artifacts share join'."));
|
|
391
375
|
return;
|
|
392
376
|
}
|
|
393
377
|
console.log(`${chalk.bold('endpoint')} ${chalk.green(cfg.baseUrl)}`);
|
|
@@ -399,8 +383,8 @@ ${SHARE_DELETE_NOTES}
|
|
|
399
383
|
const templateLabel = templateStatus === 'current'
|
|
400
384
|
? chalk.green('current')
|
|
401
385
|
: templateStatus === 'outdated'
|
|
402
|
-
? chalk.yellow('outdated — run `agents share update`')
|
|
403
|
-
: chalk.dim("unknown — provisioned before version tracking; run `agents share update` to adopt it");
|
|
386
|
+
? chalk.yellow('outdated — run `agents artifacts share update`')
|
|
387
|
+
: chalk.dim("unknown — provisioned before version tracking; run `agents artifacts share update` to adopt it");
|
|
404
388
|
console.log(`${chalk.bold('template')} ${templateLabel}`);
|
|
405
389
|
});
|
|
406
390
|
const shareListCmd = shareCmd
|
|
@@ -421,21 +405,21 @@ ${SHARE_DELETE_NOTES}
|
|
|
421
405
|
setHelpSections(shareListCmd, {
|
|
422
406
|
examples: `
|
|
423
407
|
# Everything you've published, newest first
|
|
424
|
-
agents share list
|
|
408
|
+
agents artifacts share list
|
|
425
409
|
|
|
426
410
|
# Machine-readable — e.g. pull every still-public URL with jq
|
|
427
|
-
agents share list --json | jq -r '.objects[].url'
|
|
411
|
+
agents artifacts share list --json | jq -r '.objects[].url'
|
|
428
412
|
|
|
429
413
|
# List another namespace
|
|
430
|
-
agents share list --github-user octocat
|
|
414
|
+
agents artifacts share list --github-user octocat
|
|
431
415
|
`,
|
|
432
416
|
notes: `
|
|
433
417
|
Lists the ACTIVE pages in your namespace — expired links and the sibling .png OG
|
|
434
418
|
covers are omitted (it mirrors the public gallery). It reads the endpoint's JSON
|
|
435
419
|
listing route, which ships with the current Worker template. If your deployed
|
|
436
|
-
Worker predates this feature the command says so and points you at 'agents
|
|
437
|
-
update' (RUSH-2449) rather than returning a wrong or empty result
|
|
438
|
-
share status' for whether an update is due.
|
|
420
|
+
Worker predates this feature the command says so and points you at 'agents
|
|
421
|
+
artifacts share update' (RUSH-2449) rather than returning a wrong or empty result
|
|
422
|
+
— see 'agents artifacts share status' for whether an update is due.
|
|
439
423
|
`,
|
|
440
424
|
});
|
|
441
425
|
shareCmd
|
|
@@ -444,7 +428,7 @@ ${SHARE_DELETE_NOTES}
|
|
|
444
428
|
.action(async () => {
|
|
445
429
|
const cfg = readShareConfig();
|
|
446
430
|
if (!cfg) {
|
|
447
|
-
console.log(chalk.dim("Not configured. Run 'agents
|
|
431
|
+
console.log(chalk.dim("Not configured. Run 'agents artifacts setup' or 'agents artifacts share join'."));
|
|
448
432
|
return;
|
|
449
433
|
}
|
|
450
434
|
if (!analyticsEnabled(cfg)) {
|
|
@@ -464,9 +448,24 @@ ${SHARE_DELETE_NOTES}
|
|
|
464
448
|
}
|
|
465
449
|
});
|
|
466
450
|
}
|
|
451
|
+
/**
|
|
452
|
+
* Register the top-level `agents unshare <targets...>` alias of
|
|
453
|
+
* `agents artifacts share delete`. It takes the ROOT program (not the artifacts
|
|
454
|
+
* group) because it is deliberately a top-level convenience verb — taking a page
|
|
455
|
+
* down is the one artifact action typed often enough to keep at the root.
|
|
456
|
+
*/
|
|
457
|
+
export function registerUnshareCommand(program) {
|
|
458
|
+
const unshareCmd = registerShareDeleteOptions(program
|
|
459
|
+
.command('unshare <targets...>')
|
|
460
|
+
.description('Alias of `agents artifacts share delete` — take down a published page (and by default its OG cover).'));
|
|
461
|
+
setHelpSections(unshareCmd, { examples: SHARE_DELETE_EXAMPLES, notes: SHARE_DELETE_NOTES });
|
|
462
|
+
unshareCmd.action(async (targets, opts) => {
|
|
463
|
+
await runShareDelete(targets, opts);
|
|
464
|
+
});
|
|
465
|
+
}
|
|
467
466
|
/** Provision a fresh R2 bucket + Worker on the user's Cloudflare and persist the
|
|
468
|
-
* endpoint config + write token. Shared by `agents
|
|
469
|
-
*
|
|
467
|
+
* endpoint config + write token. Shared by both modes of `agents artifacts setup`
|
|
468
|
+
* (the flag-driven provision and the interactive wizard). */
|
|
470
469
|
export async function runShareProvision(opts) {
|
|
471
470
|
const { default: ora } = await import('ora');
|
|
472
471
|
const { input } = await import('@inquirer/prompts');
|
|
@@ -521,7 +520,7 @@ export async function runShareProvision(opts) {
|
|
|
521
520
|
writeShareConfig(cfg);
|
|
522
521
|
storeWriteToken(token);
|
|
523
522
|
console.log(chalk.green(`\nShare endpoint ready → ${chalk.bold(baseUrl)}`));
|
|
524
|
-
console.log(chalk.dim('Publish with: ') + chalk.cyan('agents share <file>'));
|
|
523
|
+
console.log(chalk.dim('Publish with: ') + chalk.cyan('agents artifacts share <file>'));
|
|
525
524
|
console.log(chalk.dim(`Fleet: push the token with 'agents secrets export share --host <box>' and pull config with 'agents repo pull'.`));
|
|
526
525
|
}
|
|
527
526
|
catch (e) {
|
|
@@ -539,7 +538,7 @@ export async function runShareProvision(opts) {
|
|
|
539
538
|
export async function runShareUpdate(opts = {}) {
|
|
540
539
|
const cfg = readShareConfig();
|
|
541
540
|
if (!cfg) {
|
|
542
|
-
throw new Error("Not configured. Run 'agents
|
|
541
|
+
throw new Error("Not configured. Run 'agents artifacts setup' (to provision) or 'agents artifacts share join' first.");
|
|
543
542
|
}
|
|
544
543
|
const { apiToken, accountId: acctFromBundle } = readCloudflareCreds(opts.bundle ?? DEFAULT_CF_BUNDLE, {
|
|
545
544
|
apiToken: opts.token,
|
|
@@ -568,14 +567,14 @@ function cleanHostname(domain) {
|
|
|
568
567
|
}
|
|
569
568
|
}
|
|
570
569
|
/** Join an existing share endpoint (no provisioning): prompt for the endpoint
|
|
571
|
-
* details + write token and persist them. Shared by `agents share join`
|
|
572
|
-
*
|
|
570
|
+
* details + write token and persist them. Shared by `agents artifacts share join`
|
|
571
|
+
* and the `agents artifacts setup` wizard. */
|
|
573
572
|
export async function runShareJoin(baseUrl, opts = {}) {
|
|
574
573
|
const { password, input } = await import('@inquirer/prompts');
|
|
575
574
|
const existing = readShareConfig();
|
|
576
575
|
const clean = baseUrl?.replace(/\/+$/, '');
|
|
577
576
|
if (!clean && !existing) {
|
|
578
|
-
throw new Error("No synced share endpoint found. Pull config first with 'agents repo pull', or pass the endpoint URL: agents share join <baseUrl>.");
|
|
577
|
+
throw new Error("No synced share endpoint found. Pull config first with 'agents repo pull', or pass the endpoint URL: agents artifacts share join <baseUrl>.");
|
|
579
578
|
}
|
|
580
579
|
let cfg;
|
|
581
580
|
if (existing && (!clean || clean === existing.baseUrl)) {
|
|
@@ -608,5 +607,5 @@ export async function runShareJoin(baseUrl, opts = {}) {
|
|
|
608
607
|
throw new Error('A write token is required to join.');
|
|
609
608
|
writeShareConfig(cfg);
|
|
610
609
|
storeWriteToken(token);
|
|
611
|
-
console.log(chalk.green(`Joined ${chalk.bold(cfg.baseUrl)} — publish with `) + chalk.cyan('agents share <file>'));
|
|
610
|
+
console.log(chalk.green(`Joined ${chalk.bold(cfg.baseUrl)} — publish with `) + chalk.cyan('agents artifacts share <file>'));
|
|
612
611
|
}
|
package/dist/commands/ssh.js
CHANGED
|
@@ -56,7 +56,8 @@ import { crabboxList, crabboxFind, crabboxSshArgv } from '../lib/crabbox/cli.js'
|
|
|
56
56
|
import { boxAddress, boxStatus, fmtIdleShort, fmtExpiresShort, registerLeaseCommand } from './lease.js';
|
|
57
57
|
import { authCellColor, formatCheckedAge, isDeadVerdict, readAuthHealthCache, summarizeHostAuth, summarizeVerdicts, verdictColor, verdictLabel, writeFleetAuthRows, } from '../lib/auth-health.js';
|
|
58
58
|
import { runFleetLogin } from '../lib/fleet/remote-login.js';
|
|
59
|
-
import { getConfigValue, listConfig, setConfigValue, unsetConfigValue, configKeySpec } from '../lib/device-config.js';
|
|
59
|
+
import { getConfigValue, listConfig, setConfigValue, unsetConfigValue, configKeySpec, autoPoolMode, configuredDeviceRole, listConfiguredDeviceRoles, setConfiguredDeviceRole, } from '../lib/device-config.js';
|
|
60
|
+
import { filterAutoPool, listWorkerDevices } from '../lib/devices/pool.js';
|
|
60
61
|
import { registerCommandGroups, setHelpSections } from '../lib/help.js';
|
|
61
62
|
/** One-line summary of a device for `list`. `isSelf` marks the machine this
|
|
62
63
|
* command is running on so it stands out from the rest of the tailnet.
|
|
@@ -77,7 +78,20 @@ function deviceSummary(d, isSelf = false, stats, isInteractive = false) {
|
|
|
77
78
|
const name = isSelf ? chalk.bold.cyan(d.name.padEnd(16)) : chalk.bold(d.name.padEnd(16));
|
|
78
79
|
const here = isSelf ? chalk.cyan(' ← this machine') : '';
|
|
79
80
|
const interactive = isInteractive ? chalk.yellow(' ★ interactive') : '';
|
|
80
|
-
|
|
81
|
+
const role = roleTag(d.name, listConfiguredDeviceRoles());
|
|
82
|
+
return `${marker}${name} ${String(d.platform).padEnd(8)} ${(d.user ? d.user + '@' : '') + addr} ${online}${reach}${here}${interactive}${role}`;
|
|
83
|
+
}
|
|
84
|
+
/** The fleet-wide role mark, rendered for a device row. Empty when unmarked —
|
|
85
|
+
* an unmarked device is the common case and must not add a column of noise. */
|
|
86
|
+
function roleTag(name, roles) {
|
|
87
|
+
const role = roles[name];
|
|
88
|
+
if (!role)
|
|
89
|
+
return '';
|
|
90
|
+
if (role === 'worker')
|
|
91
|
+
return chalk.green(' worker');
|
|
92
|
+
if (role === 'personal')
|
|
93
|
+
return chalk.yellow(' personal');
|
|
94
|
+
return chalk.gray(' control');
|
|
81
95
|
}
|
|
82
96
|
const HEADROOM_BADGE = {
|
|
83
97
|
idle: chalk.green('○ idle'),
|
|
@@ -107,6 +121,7 @@ function pctCell(v, width) {
|
|
|
107
121
|
function renderDeviceTable(reg, names, self, statsMap, full = false, interactiveHost) {
|
|
108
122
|
if (!statsMap)
|
|
109
123
|
return names.map((n) => deviceSummary(reg[n], n === self, undefined, n === interactiveHost));
|
|
124
|
+
const deviceRoles = listConfiguredDeviceRoles();
|
|
110
125
|
const lines = [];
|
|
111
126
|
const head = ' ' +
|
|
112
127
|
chalk.gray('device'.padEnd(16)) +
|
|
@@ -149,7 +164,7 @@ function renderDeviceTable(reg, names, self, statsMap, full = false, interactive
|
|
|
149
164
|
const badge = HEADROOM_BADGE[headroom(stats)];
|
|
150
165
|
const here = isSelf ? chalk.cyan(' ← this machine') : '';
|
|
151
166
|
const interactive = name === interactiveHost ? chalk.yellow(' ★ interactive') : '';
|
|
152
|
-
lines.push(`${marker}${label}${plat} ${cores}${load}${mem}${freeTotal} ${badge}${relay}${here}${interactive}`);
|
|
167
|
+
lines.push(`${marker}${label}${plat} ${cores}${load}${mem}${freeTotal} ${badge}${relay}${here}${interactive}${roleTag(name, deviceRoles)}`);
|
|
153
168
|
}
|
|
154
169
|
// Fleet capacity summary — total cores + how much RAM is free right now.
|
|
155
170
|
const cap = fleetCapacity(statsMap.values());
|
|
@@ -1161,6 +1176,84 @@ function registerDevicesCommands(program) {
|
|
|
1161
1176
|
}
|
|
1162
1177
|
await runDevicesConfigMenu(name);
|
|
1163
1178
|
};
|
|
1179
|
+
/**
|
|
1180
|
+
* The `devices role` engine — read or write the fleet-wide role mark, and say
|
|
1181
|
+
* what it does to automatic placement.
|
|
1182
|
+
*
|
|
1183
|
+
* A role written here lands in the SHARED block (`fleet.devices.<name>.config.role`)
|
|
1184
|
+
* because every box has to agree on it. The vocabulary is deliberately
|
|
1185
|
+
* `worker | personal` only: a paired cockpit's `control` role lives in the
|
|
1186
|
+
* per-machine device registry, is written by `agents devices pair-ios`, and is
|
|
1187
|
+
* what the existing dial-exclusion filters (`isControlDevice`) read. Accepting
|
|
1188
|
+
* `control` here too would promise a fleet-wide dial exclusion this key cannot
|
|
1189
|
+
* deliver — those filters read each box's own registry, so the mark would only
|
|
1190
|
+
* hold on the machine that ran the command.
|
|
1191
|
+
*/
|
|
1192
|
+
const runDevicesRole = async (name, role, opts) => {
|
|
1193
|
+
if (!name) {
|
|
1194
|
+
if (role)
|
|
1195
|
+
throw new Error('Name a device: agents devices role <name> <worker|personal>');
|
|
1196
|
+
const roles = listConfiguredDeviceRoles();
|
|
1197
|
+
const mode = autoPoolMode();
|
|
1198
|
+
const reg = await loadDevices();
|
|
1199
|
+
const online = Object.entries(reg)
|
|
1200
|
+
.filter(([, d]) => d?.tailscale?.online !== false)
|
|
1201
|
+
.map(([n]) => n);
|
|
1202
|
+
const pool = filterAutoPool(online, { mode, roles });
|
|
1203
|
+
if (opts.json) {
|
|
1204
|
+
writeJson({ mode, roles, autoPool: pool });
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
const marked = Object.entries(roles);
|
|
1208
|
+
if (marked.length === 0) {
|
|
1209
|
+
console.log(chalk.gray('No device is marked. `--device auto` considers every online device.'));
|
|
1210
|
+
}
|
|
1211
|
+
else {
|
|
1212
|
+
for (const [device, r] of marked) {
|
|
1213
|
+
const tint = r === 'worker' ? chalk.green : r === 'personal' ? chalk.yellow : chalk.gray;
|
|
1214
|
+
console.log(` ${device.padEnd(20)} ${tint(r)}`);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
console.log();
|
|
1218
|
+
console.log(chalk.bold('--device auto picks from: ') + (pool.length > 0 ? pool.join(', ') : chalk.red('nothing — no eligible device')));
|
|
1219
|
+
if (mode === 'all')
|
|
1220
|
+
console.log(chalk.gray('auto.pool=all — worker marks are ignored (a personal device is still excluded).'));
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
await mustGetDevice(name);
|
|
1224
|
+
if (opts.clear || role === 'none') {
|
|
1225
|
+
setConfiguredDeviceRole(name, undefined);
|
|
1226
|
+
if (opts.json)
|
|
1227
|
+
writeJson({ device: name, role: null });
|
|
1228
|
+
else
|
|
1229
|
+
console.log(chalk.green(`Cleared the role on '${name}'.`));
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
if (!role) {
|
|
1233
|
+
const current = configuredDeviceRole(name);
|
|
1234
|
+
if (opts.json)
|
|
1235
|
+
writeJson({ device: name, role: current ?? null });
|
|
1236
|
+
else
|
|
1237
|
+
console.log(` ${name.padEnd(20)} ${current ? chalk.cyan(current) : chalk.gray('— (unmarked)')}`);
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
// configuredDeviceRole's key spec validates the value; a bad one throws with
|
|
1241
|
+
// the accepted list, which the command's catch turns into exit 1.
|
|
1242
|
+
setConfiguredDeviceRole(name, role);
|
|
1243
|
+
if (opts.json) {
|
|
1244
|
+
writeJson({ device: name, role, autoPoolWorkers: listWorkerDevices() });
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
console.log(chalk.green(`Marked '${name}' role=${role}.`));
|
|
1248
|
+
const workers = listWorkerDevices();
|
|
1249
|
+
if (workers.length > 0) {
|
|
1250
|
+
console.log(chalk.gray(`\`--device auto\` now picks only from: ${workers.join(', ')}`));
|
|
1251
|
+
}
|
|
1252
|
+
else {
|
|
1253
|
+
console.log(chalk.gray('No device is marked worker, so `--device auto` still considers every online device.'));
|
|
1254
|
+
}
|
|
1255
|
+
console.log(chalk.gray('Sync it to the fleet with `agents repo push`.'));
|
|
1256
|
+
};
|
|
1164
1257
|
/** The interactive settings menu: pick a key, edit it, repeat. TTY-only. */
|
|
1165
1258
|
const runDevicesConfigMenu = async (name) => {
|
|
1166
1259
|
const { select, input, confirm } = await import('@inquirer/prompts');
|
|
@@ -1250,22 +1343,27 @@ function registerDevicesCommands(program) {
|
|
|
1250
1343
|
agents devices config win-mini ssh.auth password # password auth…
|
|
1251
1344
|
agents devices config win-mini ssh.bundle muqsit # …from this secrets bundle
|
|
1252
1345
|
agents devices config worker ssh.identity-file ~/.ssh/worker_ed25519
|
|
1346
|
+
agents devices config mac-mini role worker # same as \`agents devices role mac-mini worker\`
|
|
1253
1347
|
agents devices config mac-mini auto-launch.enabled off # exclude from AGI EXT auto-launch
|
|
1254
1348
|
agents devices config mac-mini auto-launch.preferred on # boost in auto-launch ranking
|
|
1255
1349
|
agents devices config zion interactive.host zion # user scope: where agents show YOU artifacts
|
|
1256
1350
|
agents devices config mac-mini --json # machine-readable
|
|
1257
1351
|
`,
|
|
1258
1352
|
notes: `
|
|
1259
|
-
Keys:
|
|
1260
|
-
|
|
1261
|
-
|
|
1353
|
+
Keys: role (worker|personal), see 'agents devices role',
|
|
1354
|
+
agents.max-concurrent, scheduler.enabled, daemon.enabled,
|
|
1355
|
+
watchdog.enabled, tmux.enabled, browser.remote-control, browser.profile,
|
|
1356
|
+
notes, ssh.user, ssh.auth (key|password), ssh.bundle, ssh.bundle-key,
|
|
1262
1357
|
ssh.identity-file, platform (windows|linux|macos|unknown),
|
|
1263
1358
|
auto-launch.enabled, auto-launch.preferred — plus the user-scope
|
|
1264
1359
|
interactive.host (stored centrally; the device name is syntax only).
|
|
1265
1360
|
|
|
1266
1361
|
Booleans take on/off (or true/false). 'notes' appends one entry per
|
|
1267
|
-
invocation. Values land in ~/.agents/agents.yaml under
|
|
1268
|
-
fleet.devices.<name>.config and sync with 'agents repo push/pull'.
|
|
1362
|
+
invocation. Values a PEER reads land in ~/.agents/agents.yaml under
|
|
1363
|
+
fleet.devices.<name>.config and sync with 'agents repo push/pull'. The
|
|
1364
|
+
keys only the owning box reads — scheduler.enabled, daemon.enabled,
|
|
1365
|
+
tmux.enabled, browser.remote-control, browser.profile — stay in that
|
|
1366
|
+
machine's own doc, never sync, and can only be set on the device itself.
|
|
1269
1367
|
ssh.* / platform / user overlay the discovered registry profile at dial
|
|
1270
1368
|
time. scheduler.enabled / daemon.enabled take effect when the daemon
|
|
1271
1369
|
reloads or restarts on that device.
|
|
@@ -1274,6 +1372,49 @@ function registerDevicesCommands(program) {
|
|
|
1274
1372
|
set, set-interactive, enable, disable, prefer, unprefer.
|
|
1275
1373
|
`,
|
|
1276
1374
|
});
|
|
1375
|
+
const roleCmd = devicesCmd
|
|
1376
|
+
.command('role [name] [role]')
|
|
1377
|
+
.description('Show or set what a device is for: worker (agents run here) or personal (you sit here — never picked automatically). ' +
|
|
1378
|
+
'Marking any device worker makes `--device auto` an allowlist over the marked workers.')
|
|
1379
|
+
.option('--clear', 'remove the mark, returning the device to unmarked')
|
|
1380
|
+
.option('--json', 'output machine-readable JSON')
|
|
1381
|
+
.action(async (name, role, opts) => {
|
|
1382
|
+
try {
|
|
1383
|
+
await runDevicesRole(name, role, opts);
|
|
1384
|
+
}
|
|
1385
|
+
catch (err) {
|
|
1386
|
+
console.error(chalk.red(err.message));
|
|
1387
|
+
process.exit(1);
|
|
1388
|
+
}
|
|
1389
|
+
});
|
|
1390
|
+
setHelpSections(roleCmd, {
|
|
1391
|
+
examples: `
|
|
1392
|
+
agents devices role # who is marked what, and what --device auto would pick
|
|
1393
|
+
agents devices role yosemite-s0 worker # agents spin up here
|
|
1394
|
+
agents devices role yosemite-s1 worker # …and here; auto now rotates over these two only
|
|
1395
|
+
agents devices role zion personal # your laptop — keep automatic placement off it
|
|
1396
|
+
agents devices role yosemite-s0 --clear # unmark
|
|
1397
|
+
agents devices role --json # machine-readable
|
|
1398
|
+
`,
|
|
1399
|
+
notes: `
|
|
1400
|
+
Roles are stored fleet-wide in ~/.agents/agents.yaml under
|
|
1401
|
+
fleet.devices.<name>.config.role and travel with 'agents repo push/pull',
|
|
1402
|
+
so a mark set on one box is the whole fleet's answer.
|
|
1403
|
+
|
|
1404
|
+
Effect on '--device auto' (agents run, teams, agents ssh auto, and the AGI
|
|
1405
|
+
EXT launch commands, which all resolve placement through the CLI):
|
|
1406
|
+
no device marked -> every online device, as before
|
|
1407
|
+
any worker marked -> ONLY the marked workers
|
|
1408
|
+
personal -> never picked, under either state
|
|
1409
|
+
|
|
1410
|
+
Turn the allowlist off with 'agents config set auto.pool all'; a personal
|
|
1411
|
+
box stays excluded, since that is what the mark is for.
|
|
1412
|
+
|
|
1413
|
+
A paired iPhone/iPad cockpit is a separate role: 'agents devices pair-ios'
|
|
1414
|
+
marks it control in that box's device registry, and the fleet never dials
|
|
1415
|
+
it — including for placement. This command does not set that role.
|
|
1416
|
+
`,
|
|
1417
|
+
});
|
|
1277
1418
|
/** Deprecation notice for a retired subcommand — STDERR only, so a --json consumer's stdout stays parseable. */
|
|
1278
1419
|
const configTombstoneNotice = (retired, replacement) => {
|
|
1279
1420
|
console.error(chalk.yellow(`Deprecated: "agents devices ${retired}" is now "agents devices ${replacement}". Running that for you.\n`));
|
|
@@ -1493,12 +1634,19 @@ function registerDevicesCommands(program) {
|
|
|
1493
1634
|
await writeReachability(collectReachabilityWriteBacks(reg, statsMap)).catch(() => { });
|
|
1494
1635
|
}
|
|
1495
1636
|
if (opts.json) {
|
|
1637
|
+
const jsonRoles = listConfiguredDeviceRoles();
|
|
1638
|
+
const autoPool = new Set(filterAutoPool(names, { roles: jsonRoles }));
|
|
1496
1639
|
process.stdout.write(JSON.stringify(names.map((name) => {
|
|
1497
1640
|
const config = deviceConfigJson(name);
|
|
1498
1641
|
const health = statsMap?.get(name);
|
|
1499
1642
|
return {
|
|
1500
1643
|
...resolveDeviceProfile(reg[name]),
|
|
1501
1644
|
interactive: name === interactiveHost,
|
|
1645
|
+
// Roles as machine-readable fields: `role` is what the operator
|
|
1646
|
+
// marked (absent when unmarked), `autoPool` is the answer that
|
|
1647
|
+
// matters to a caller — may `--device auto` pick this box.
|
|
1648
|
+
...(jsonRoles[name] ? { role: jsonRoles[name] } : {}),
|
|
1649
|
+
autoPool: autoPool.has(name),
|
|
1502
1650
|
...(config ? { config } : {}),
|
|
1503
1651
|
...(health ? { health: { ...health, headroom: headroom(health) } } : {}),
|
|
1504
1652
|
};
|