@phnx-labs/agents-cli 1.20.72 → 1.20.74
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 +395 -0
- package/README.md +40 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/activity.d.ts +12 -0
- package/dist/commands/activity.js +68 -0
- package/dist/commands/apply.d.ts +13 -0
- package/dist/commands/apply.js +28 -3
- package/dist/commands/browser.js +1 -1
- package/dist/commands/cloud.js +21 -11
- package/dist/commands/doctor.js +21 -5
- package/dist/commands/events.d.ts +11 -10
- package/dist/commands/events.js +30 -21
- package/dist/commands/exec.d.ts +34 -0
- package/dist/commands/exec.js +337 -39
- package/dist/commands/export.d.ts +11 -0
- package/dist/commands/export.js +215 -0
- package/dist/commands/feed.d.ts +13 -0
- package/dist/commands/feed.js +130 -29
- package/dist/commands/hq.d.ts +2 -0
- package/dist/commands/hq.js +58 -0
- package/dist/commands/import.js +51 -3
- package/dist/commands/lease.d.ts +19 -0
- package/dist/commands/lease.js +138 -11
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +123 -0
- package/dist/commands/logs.js +1 -1
- package/dist/commands/mine.d.ts +27 -0
- package/dist/commands/mine.js +207 -0
- package/dist/commands/monitors.js +79 -49
- package/dist/commands/plugins.js +1 -0
- package/dist/commands/profiles.js +158 -0
- package/dist/commands/repo.js +2 -60
- package/dist/commands/resources.d.ts +5 -0
- package/dist/commands/resources.js +95 -0
- package/dist/commands/routines.d.ts +2 -0
- package/dist/commands/routines.js +139 -37
- package/dist/commands/secrets.js +234 -47
- package/dist/commands/send.d.ts +15 -0
- package/dist/commands/send.js +62 -0
- package/dist/commands/sessions-tail.d.ts +1 -0
- package/dist/commands/sessions-tail.js +20 -9
- package/dist/commands/sessions.js +18 -3
- package/dist/commands/setup-mine.d.ts +18 -0
- package/dist/commands/setup-mine.js +106 -0
- package/dist/commands/setup-share.js +2 -2
- package/dist/commands/setup.js +9 -2
- package/dist/commands/share.d.ts +8 -1
- package/dist/commands/share.js +122 -37
- package/dist/commands/ssh.d.ts +9 -0
- package/dist/commands/ssh.js +196 -26
- package/dist/commands/sync.js +1 -1
- package/dist/commands/teams.js +11 -2
- package/dist/commands/versions.js +36 -5
- package/dist/commands/view.js +75 -27
- package/dist/index.js +80 -10
- package/dist/lib/activity.d.ts +124 -0
- package/dist/lib/activity.js +542 -0
- package/dist/lib/agents.d.ts +35 -0
- package/dist/lib/agents.js +79 -6
- package/dist/lib/artifact-actions.d.ts +1 -1
- package/dist/lib/artifact-actions.js +1 -1
- package/dist/lib/ask-classifier.d.ts +2 -1
- package/dist/lib/ask-classifier.js +3 -3
- package/dist/lib/auth-health.d.ts +32 -4
- package/dist/lib/auth-health.js +40 -1
- package/dist/lib/auto-pull.js +8 -1
- package/dist/lib/brand.d.ts +40 -0
- package/dist/lib/brand.js +122 -0
- package/dist/lib/browser/drivers/ssh.js +4 -7
- package/dist/lib/browser/service.js +6 -8
- package/dist/lib/channels/providers/index.d.ts +2 -0
- package/dist/lib/channels/providers/index.js +20 -0
- package/dist/lib/channels/providers/mailbox.d.ts +2 -0
- package/dist/lib/channels/providers/mailbox.js +26 -0
- package/dist/lib/channels/providers/openclaw-telegram.d.ts +2 -0
- package/dist/lib/channels/providers/openclaw-telegram.js +34 -0
- package/dist/lib/channels/providers/rush.d.ts +6 -0
- package/dist/lib/channels/providers/rush.js +55 -0
- package/dist/lib/channels/registry.d.ts +42 -0
- package/dist/lib/channels/registry.js +20 -0
- package/dist/lib/channels/resolve.d.ts +11 -0
- package/dist/lib/channels/resolve.js +12 -0
- package/dist/lib/cli-resources.d.ts +18 -0
- package/dist/lib/cli-resources.js +53 -4
- package/dist/lib/cloud/codex.js +6 -3
- package/dist/lib/cloud/factory.d.ts +1 -0
- package/dist/lib/cloud/factory.js +10 -4
- package/dist/lib/cloud/rush.d.ts +3 -1
- package/dist/lib/cloud/rush.js +6 -1
- package/dist/lib/cloud/types.d.ts +2 -0
- package/dist/lib/codex-home.d.ts +35 -0
- package/dist/lib/codex-home.js +136 -0
- package/dist/lib/config-transfer.d.ts +16 -0
- package/dist/lib/config-transfer.js +56 -0
- package/dist/lib/crabbox/cli.d.ts +44 -0
- package/dist/lib/crabbox/cli.js +120 -20
- package/dist/lib/crabbox/lease.d.ts +52 -1
- package/dist/lib/crabbox/lease.js +117 -16
- package/dist/lib/crabbox/progress.d.ts +31 -0
- package/dist/lib/crabbox/progress.js +76 -0
- package/dist/lib/crabbox/runtimes.d.ts +4 -0
- package/dist/lib/crabbox/runtimes.js +33 -8
- package/dist/lib/crabbox/setup-copy.d.ts +87 -0
- package/dist/lib/crabbox/setup-copy.js +127 -0
- package/dist/lib/daemon.d.ts +5 -5
- package/dist/lib/daemon.js +9 -9
- package/dist/lib/drive-sync.js +6 -3
- package/dist/lib/event-stream.d.ts +36 -0
- package/dist/lib/event-stream.js +68 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +11 -0
- package/dist/lib/exec.js +23 -2
- package/dist/lib/export.d.ts +72 -0
- package/dist/lib/export.js +269 -0
- package/dist/lib/feed-policy.d.ts +1 -1
- package/dist/lib/feed-policy.js +14 -9
- package/dist/lib/feed-ranking.d.ts +32 -0
- package/dist/lib/feed-ranking.js +224 -0
- package/dist/lib/feed.d.ts +35 -2
- package/dist/lib/feed.js +66 -3
- package/dist/lib/fleet/auth-sync.d.ts +53 -0
- package/dist/lib/fleet/auth-sync.js +92 -0
- package/dist/lib/fleet/remote-login.d.ts +187 -0
- package/dist/lib/fleet/remote-login.js +556 -0
- package/dist/lib/format.d.ts +30 -3
- package/dist/lib/format.js +34 -5
- package/dist/lib/fs-atomic.d.ts +7 -2
- package/dist/lib/fs-atomic.js +8 -12
- package/dist/lib/git.d.ts +16 -0
- package/dist/lib/git.js +79 -2
- package/dist/lib/heal.js +11 -3
- package/dist/lib/hosts/progress.d.ts +28 -10
- package/dist/lib/hosts/progress.js +79 -22
- package/dist/lib/hosts/remote-cmd.js +4 -0
- package/dist/lib/hq/floor.d.ts +87 -0
- package/dist/lib/hq/floor.js +226 -0
- package/dist/lib/import.d.ts +23 -0
- package/dist/lib/import.js +74 -2
- package/dist/lib/isolation-boundary-report.d.ts +10 -0
- package/dist/lib/isolation-boundary-report.js +35 -0
- package/dist/lib/menubar/install-menubar.js +14 -20
- package/dist/lib/notify.d.ts +1 -0
- package/dist/lib/notify.js +3 -3
- package/dist/lib/open-url.d.ts +2 -0
- package/dist/lib/open-url.js +19 -0
- package/dist/lib/openclaw-keychain.d.ts +56 -0
- package/dist/lib/openclaw-keychain.js +236 -0
- package/dist/lib/overdue.js +10 -0
- package/dist/lib/permissions.d.ts +44 -1
- package/dist/lib/permissions.js +284 -7
- package/dist/lib/project-launch.d.ts +6 -12
- package/dist/lib/project-launch.js +13 -228
- package/dist/lib/project-resources.d.ts +7 -0
- package/dist/lib/project-resources.js +306 -0
- package/dist/lib/pty-client.d.ts +27 -0
- package/dist/lib/pty-client.js +136 -10
- package/dist/lib/refresh.js +14 -10
- package/dist/lib/resource-profiles.d.ts +26 -0
- package/dist/lib/resource-profiles.js +157 -0
- package/dist/lib/resources/permissions.js +7 -1
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/resources.d.ts +1 -1
- package/dist/lib/resources.js +32 -3
- package/dist/lib/routines.d.ts +11 -0
- package/dist/lib/routines.js +56 -15
- package/dist/lib/runner.d.ts +1 -0
- package/dist/lib/runner.js +66 -5
- package/dist/lib/secrets/bundles.d.ts +16 -3
- package/dist/lib/secrets/bundles.js +206 -37
- package/dist/lib/secrets/icloud-import.d.ts +2 -2
- package/dist/lib/secrets/icloud-import.js +9 -6
- package/dist/lib/secrets/mcp.js +1 -1
- package/dist/lib/secrets/vault-age-helper.d.ts +1 -0
- package/dist/lib/secrets/vault-age-helper.js +34 -0
- package/dist/lib/secrets/vault.d.ts +49 -0
- package/dist/lib/secrets/vault.js +397 -0
- package/dist/lib/self-heal/checks/path.js +3 -1
- package/dist/lib/self-heal/checks/shadowing.js +10 -2
- package/dist/lib/self-heal/checks/shims.js +19 -11
- package/dist/lib/session/cloud.d.ts +2 -2
- package/dist/lib/session/cloud.js +2 -2
- package/dist/lib/session/db.d.ts +4 -0
- package/dist/lib/session/db.js +44 -7
- package/dist/lib/session/discover.d.ts +2 -0
- package/dist/lib/session/discover.js +114 -3
- package/dist/lib/session/stream-render.d.ts +3 -0
- package/dist/lib/session/stream-render.js +130 -0
- package/dist/lib/session/types.d.ts +6 -0
- package/dist/lib/share/analytics.d.ts +13 -0
- package/dist/lib/share/analytics.js +45 -0
- package/dist/lib/share/config.d.ts +19 -5
- package/dist/lib/share/config.js +44 -8
- package/dist/lib/share/provision.d.ts +58 -6
- package/dist/lib/share/provision.js +97 -22
- package/dist/lib/share/publish.d.ts +36 -13
- package/dist/lib/share/publish.js +55 -8
- package/dist/lib/share/worker-template.js +83 -17
- package/dist/lib/shims.d.ts +35 -0
- package/dist/lib/shims.js +159 -13
- package/dist/lib/ssh-exec.d.ts +14 -0
- package/dist/lib/ssh-exec.js +57 -0
- package/dist/lib/staleness/detectors/hooks.js +25 -1
- package/dist/lib/staleness/detectors/permissions.js +66 -0
- package/dist/lib/staleness/detectors/workflows.js +20 -0
- package/dist/lib/staleness/writers/hooks.js +58 -4
- package/dist/lib/staleness/writers/permissions.js +2 -2
- package/dist/lib/staleness/writers/skills.js +1 -1
- package/dist/lib/staleness/writers/sources.d.ts +10 -1
- package/dist/lib/staleness/writers/sources.js +68 -1
- package/dist/lib/star-nudge.d.ts +45 -0
- package/dist/lib/star-nudge.js +91 -0
- package/dist/lib/startup/command-registry.d.ts +7 -1
- package/dist/lib/startup/command-registry.js +19 -3
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +17 -4
- package/dist/lib/subagents-registry.d.ts +2 -0
- package/dist/lib/subagents-registry.js +3 -0
- package/dist/lib/teams/parsers.js +214 -6
- package/dist/lib/teams/supervisor.d.ts +7 -0
- package/dist/lib/teams/supervisor.js +2 -1
- package/dist/lib/template.d.ts +1 -1
- package/dist/lib/template.js +1 -1
- package/dist/lib/triggers/webhook.js +36 -3
- package/dist/lib/types.d.ts +84 -0
- package/dist/lib/uninstall.js +1 -45
- package/dist/lib/version.d.ts +40 -0
- package/dist/lib/version.js +94 -16
- package/dist/lib/versions.d.ts +24 -0
- package/dist/lib/versions.js +269 -78
- package/dist/lib/workflows.d.ts +2 -0
- package/dist/lib/workflows.js +88 -0
- package/package.json +2 -1
- package/dist/commands/daemon.d.ts +0 -10
- package/dist/commands/daemon.js +0 -121
package/dist/commands/lease.d.ts
CHANGED
|
@@ -8,6 +8,25 @@
|
|
|
8
8
|
* so a box a concurrent run just reused is never stopped.
|
|
9
9
|
*/
|
|
10
10
|
import type { Command } from 'commander';
|
|
11
|
+
import { type CrabboxBox } from '../lib/crabbox/cli.js';
|
|
12
|
+
/** Compact human duration: "45s", "12m", "2h", "1h 5m". Clamps negatives to 0. */
|
|
13
|
+
export declare function fmtDurationShort(secs: number): string;
|
|
14
|
+
/** Idle time since the box was last touched, e.g. "idle 5m" / "idle ?". */
|
|
15
|
+
export declare function fmtIdleShort(box: CrabboxBox, nowSecs: number): string;
|
|
16
|
+
/** Time until the lease expires, e.g. "expires 42m" / "expires ?" / "expired". */
|
|
17
|
+
export declare function fmtExpiresShort(box: CrabboxBox, nowSecs: number): string;
|
|
18
|
+
/** Reachable address for a leased box: tailnet FQDN/IP first, else public IP. */
|
|
19
|
+
export declare function boxAddress(box: CrabboxBox): string | undefined;
|
|
20
|
+
/** Human status: "ready" when usable, else the raw bootstrap state/status. */
|
|
21
|
+
export declare function boxStatus(box: CrabboxBox): string;
|
|
22
|
+
/**
|
|
23
|
+
* Warm boxes eligible for reuse: `ready` and the lease has not expired.
|
|
24
|
+
* Sorted most-recently-touched first so `--reuse` / the auto-pick lands on the
|
|
25
|
+
* freshest box (an untouched `lastTouchedAt` sorts last).
|
|
26
|
+
*/
|
|
27
|
+
export declare function reusableBoxes(boxes: CrabboxBox[], nowSecs: number): CrabboxBox[];
|
|
28
|
+
/** One aligned row for the reuse picker / `agents lease list`. */
|
|
29
|
+
export declare function formatBoxRow(box: CrabboxBox, nowSecs: number): string;
|
|
11
30
|
/** Validate a Hetzner token against the live API. Exported for unit tests (fetch injectable). */
|
|
12
31
|
export declare function validateHetznerToken(token: string, fetchImpl?: typeof fetch): Promise<'valid' | 'invalid' | 'unreachable'>;
|
|
13
32
|
/**
|
package/dist/commands/lease.js
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
* that have been untouched for a safety window are eligible (see `isReapSafe`),
|
|
8
8
|
* so a box a concurrent run just reused is never stopped.
|
|
9
9
|
*/
|
|
10
|
-
import { spawn } from 'child_process';
|
|
11
10
|
import chalk from 'chalk';
|
|
12
|
-
import {
|
|
11
|
+
import { openUrl } from '../lib/open-url.js';
|
|
12
|
+
import { crabboxList, crabboxStop, reapSafeOrphans, reapOrphans, setLeaseSecretsBundle } from '../lib/crabbox/cli.js';
|
|
13
13
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
14
14
|
import { bundleExists, readBundle, writeBundle, keychainRef, bundleItemStore } from '../lib/secrets/bundles.js';
|
|
15
15
|
import { secretsKeychainItem } from '../lib/secrets/index.js';
|
|
@@ -19,21 +19,104 @@ function fmtIdle(box) {
|
|
|
19
19
|
const iso = new Date(box.lastTouchedAt * 1000).toISOString().slice(0, 16).replace('T', ' ');
|
|
20
20
|
return `idle since ${iso}Z`;
|
|
21
21
|
}
|
|
22
|
+
// ── Shared box helpers (consumed by exec.ts's reuse picker + ssh.ts's devices
|
|
23
|
+
// section, so the reuse/format logic lives in exactly one place) ─────────────
|
|
24
|
+
/** Compact human duration: "45s", "12m", "2h", "1h 5m". Clamps negatives to 0. */
|
|
25
|
+
export function fmtDurationShort(secs) {
|
|
26
|
+
const s = Math.max(0, Math.round(secs));
|
|
27
|
+
if (s < 60)
|
|
28
|
+
return `${s}s`;
|
|
29
|
+
const m = Math.round(s / 60);
|
|
30
|
+
if (m < 60)
|
|
31
|
+
return `${m}m`;
|
|
32
|
+
const h = Math.floor(m / 60);
|
|
33
|
+
const rem = m % 60;
|
|
34
|
+
return rem ? `${h}h ${rem}m` : `${h}h`;
|
|
35
|
+
}
|
|
36
|
+
/** Idle time since the box was last touched, e.g. "idle 5m" / "idle ?". */
|
|
37
|
+
export function fmtIdleShort(box, nowSecs) {
|
|
38
|
+
if (box.lastTouchedAt === null)
|
|
39
|
+
return 'idle ?';
|
|
40
|
+
return `idle ${fmtDurationShort(nowSecs - box.lastTouchedAt)}`;
|
|
41
|
+
}
|
|
42
|
+
/** Time until the lease expires, e.g. "expires 42m" / "expires ?" / "expired". */
|
|
43
|
+
export function fmtExpiresShort(box, nowSecs) {
|
|
44
|
+
if (box.expiresAt === null)
|
|
45
|
+
return 'expires ?';
|
|
46
|
+
const left = box.expiresAt - nowSecs;
|
|
47
|
+
return left <= 0 ? 'expired' : `expires ${fmtDurationShort(left)}`;
|
|
48
|
+
}
|
|
49
|
+
/** Reachable address for a leased box: tailnet FQDN/IP first, else public IP. */
|
|
50
|
+
export function boxAddress(box) {
|
|
51
|
+
return box.tailscaleFQDN || box.tailscaleIPv4 || box.ip || undefined;
|
|
52
|
+
}
|
|
53
|
+
/** Human status: "ready" when usable, else the raw bootstrap state/status. */
|
|
54
|
+
export function boxStatus(box) {
|
|
55
|
+
return box.ready ? 'ready' : box.state || box.status || 'pending';
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Warm boxes eligible for reuse: `ready` and the lease has not expired.
|
|
59
|
+
* Sorted most-recently-touched first so `--reuse` / the auto-pick lands on the
|
|
60
|
+
* freshest box (an untouched `lastTouchedAt` sorts last).
|
|
61
|
+
*/
|
|
62
|
+
export function reusableBoxes(boxes, nowSecs) {
|
|
63
|
+
return boxes
|
|
64
|
+
.filter((b) => b.ready && (b.expiresAt === null || b.expiresAt > nowSecs))
|
|
65
|
+
.sort((a, b) => (b.lastTouchedAt ?? 0) - (a.lastTouchedAt ?? 0));
|
|
66
|
+
}
|
|
67
|
+
/** One aligned row for the reuse picker / `agents lease list`. */
|
|
68
|
+
export function formatBoxRow(box, nowSecs) {
|
|
69
|
+
const slug = box.slug.padEnd(16);
|
|
70
|
+
const cls = (box.class ?? '?').padEnd(10);
|
|
71
|
+
const addr = (boxAddress(box) ?? '—').padEnd(24);
|
|
72
|
+
const status = boxStatus(box).padEnd(8);
|
|
73
|
+
const idle = fmtIdleShort(box, nowSecs).padEnd(12);
|
|
74
|
+
return `${slug} ${cls} ${addr} ${status} ${idle} ${fmtExpiresShort(box, nowSecs)}`;
|
|
75
|
+
}
|
|
22
76
|
const HETZNER_BUNDLE = 'hetzner.com';
|
|
23
77
|
const HCLOUD_KEY = 'HCLOUD_TOKEN';
|
|
24
78
|
const HETZNER_CONSOLE_URL = 'https://console.hetzner.cloud/';
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
79
|
+
const TAILSCALE_BUNDLE = 'tailscale.com';
|
|
80
|
+
const TAILSCALE_KEY = 'CRABBOX_TAILSCALE_AUTH_KEY';
|
|
81
|
+
const TAILSCALE_KEYS_URL = 'https://login.tailscale.com/admin/settings/keys';
|
|
82
|
+
/**
|
|
83
|
+
* Optional Tailscale setup for private-network leases (`--tailscale`). Collects
|
|
84
|
+
* an EPHEMERAL, pre-authorized, `tag:crabbox` auth key and stores it in the
|
|
85
|
+
* `tailscale.com` keychain bundle under `CRABBOX_TAILSCALE_AUTH_KEY` (the exact
|
|
86
|
+
* key `crabboxEnv` auto-injects). Blank input skips — public-IP leases still
|
|
87
|
+
* work with no Tailscale key. Never throws for cancel; mirrors the Hetzner
|
|
88
|
+
* capture above but does no live validation (Tailscale has no cheap probe).
|
|
89
|
+
*/
|
|
90
|
+
async function captureTailscaleAuthKey() {
|
|
91
|
+
console.error(chalk.bold('\nOptional: private-network leases over Tailscale'));
|
|
92
|
+
console.error(chalk.dim('Mint an EPHEMERAL, pre-authorized auth key tagged `tag:crabbox` in the Tailscale admin,'));
|
|
93
|
+
console.error(chalk.dim('then paste it to reach leased boxes only over your tailnet (`--tailscale`). Leave blank to skip.\n'));
|
|
94
|
+
const { password } = await import('@inquirer/prompts');
|
|
95
|
+
let key;
|
|
29
96
|
try {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
p.unref();
|
|
97
|
+
openUrl(TAILSCALE_KEYS_URL);
|
|
98
|
+
key = (await password({ message: 'Paste a Tailscale auth key (blank to skip):', mask: true })).trim();
|
|
33
99
|
}
|
|
34
|
-
catch {
|
|
35
|
-
|
|
100
|
+
catch (e) {
|
|
101
|
+
if (isPromptCancelled(e)) {
|
|
102
|
+
console.error(chalk.yellow('Skipped Tailscale setup.'));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
throw e;
|
|
106
|
+
}
|
|
107
|
+
if (!key) {
|
|
108
|
+
console.error(chalk.dim('Skipped Tailscale setup — public-IP leases only.'));
|
|
109
|
+
return;
|
|
36
110
|
}
|
|
111
|
+
const bundle = bundleExists(TAILSCALE_BUNDLE)
|
|
112
|
+
? readBundle(TAILSCALE_BUNDLE)
|
|
113
|
+
: { name: TAILSCALE_BUNDLE, description: 'Tailscale ephemeral auth key for crabbox tailnet leases', vars: {} };
|
|
114
|
+
const store = bundleItemStore(bundle.backend);
|
|
115
|
+
store.set(secretsKeychainItem(TAILSCALE_BUNDLE, TAILSCALE_KEY), key);
|
|
116
|
+
bundle.vars[TAILSCALE_KEY] = keychainRef(TAILSCALE_KEY);
|
|
117
|
+
writeBundle(bundle);
|
|
118
|
+
console.error(chalk.green(`✔ Stored Tailscale auth key in keychain bundle '${TAILSCALE_BUNDLE}'.`));
|
|
119
|
+
console.error(chalk.dim(' Add --tailscale to a lease (reuse defaults to it) to reach the box over your tailnet.'));
|
|
37
120
|
}
|
|
38
121
|
/** Validate a Hetzner token against the live API. Exported for unit tests (fetch injectable). */
|
|
39
122
|
export async function validateHetznerToken(token, fetchImpl = fetch) {
|
|
@@ -103,6 +186,8 @@ export async function runLeaseSetup(opts = {}) {
|
|
|
103
186
|
setLeaseSecretsBundle(HETZNER_BUNDLE);
|
|
104
187
|
console.error(chalk.green(`\n✔ Stored in keychain bundle '${HETZNER_BUNDLE}' and set as the default lease provider.`));
|
|
105
188
|
console.error(chalk.dim(' Run `agents run <agent> "…" --lease` — no env var, no flag needed.'));
|
|
189
|
+
// Also offer to capture a Tailscale auth key for private-network leases.
|
|
190
|
+
await captureTailscaleAuthKey();
|
|
106
191
|
return true;
|
|
107
192
|
}
|
|
108
193
|
console.error(chalk.yellow('lease setup: no valid token after 3 attempts — aborted.'));
|
|
@@ -128,6 +213,48 @@ export function registerLeaseCommand(program) {
|
|
|
128
213
|
const ok = await runLeaseSetup({ provider: opts.provider });
|
|
129
214
|
process.exit(ok ? 0 : 1);
|
|
130
215
|
});
|
|
216
|
+
lease
|
|
217
|
+
.command('list')
|
|
218
|
+
.alias('ls')
|
|
219
|
+
.description('List warm crabbox boxes you can reuse with `agents run --box <slug>`.')
|
|
220
|
+
.option('--json', 'Output JSON', false)
|
|
221
|
+
.action((opts) => {
|
|
222
|
+
const boxOpts = { secretsBundle: process.env.AGENTS_LEASE_SECRETS_BUNDLE };
|
|
223
|
+
let boxes;
|
|
224
|
+
try {
|
|
225
|
+
boxes = crabboxList(boxOpts);
|
|
226
|
+
}
|
|
227
|
+
catch (e) {
|
|
228
|
+
console.error(chalk.red(`lease list: ${e.message}`));
|
|
229
|
+
process.exit(1);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (opts.json) {
|
|
233
|
+
console.log(JSON.stringify(boxes, null, 2));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (boxes.length === 0) {
|
|
237
|
+
console.error(chalk.gray('No crabbox boxes. `agents run <agent> "…" --lease` provisions one.'));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const nowSecs = Math.floor(Date.now() / 1000);
|
|
241
|
+
console.log(chalk.bold(`Warm boxes (${boxes.length})`));
|
|
242
|
+
for (const b of boxes)
|
|
243
|
+
console.log(' ' + formatBoxRow(b, nowSecs));
|
|
244
|
+
console.log(chalk.gray(' Reuse: agents run <agent> "…" --box <slug> · Stop: agents lease stop <slug>'));
|
|
245
|
+
});
|
|
246
|
+
lease
|
|
247
|
+
.command('stop <slug>')
|
|
248
|
+
.description('Stop (release) a leased crabbox box now.')
|
|
249
|
+
.action((slug) => {
|
|
250
|
+
const boxOpts = { secretsBundle: process.env.AGENTS_LEASE_SECRETS_BUNDLE };
|
|
251
|
+
const ok = crabboxStop(slug, boxOpts);
|
|
252
|
+
if (ok)
|
|
253
|
+
console.error(chalk.green(`Stopped box ${slug}.`));
|
|
254
|
+
else
|
|
255
|
+
console.error(chalk.red(`Could not stop box ${slug} (already gone, or crabbox is unavailable).`));
|
|
256
|
+
process.exit(ok ? 0 : 1);
|
|
257
|
+
});
|
|
131
258
|
lease
|
|
132
259
|
.command('gc')
|
|
133
260
|
.description('Stop expired, idle lease boxes that are holding your provider quota. Safe: never stops a box in active use.')
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import { readStdinSync } from '../lib/format.js';
|
|
4
|
+
import { clearVaultKey, createVault, getVaultSession, joinVault, unlock, vaultExists, vaultPath, } from '../lib/secrets/vault.js';
|
|
5
|
+
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
6
|
+
async function promptPassword(message) {
|
|
7
|
+
if (!isInteractiveTerminal()) {
|
|
8
|
+
throw new Error('A password is required. Run from a TTY or pass --password-stdin.');
|
|
9
|
+
}
|
|
10
|
+
const { password } = await import('@inquirer/prompts');
|
|
11
|
+
return password({ message, mask: true });
|
|
12
|
+
}
|
|
13
|
+
async function readPassword(opts, message) {
|
|
14
|
+
if (opts.passwordStdin) {
|
|
15
|
+
const value = readStdinSync();
|
|
16
|
+
if (!value)
|
|
17
|
+
throw new Error('No password received on stdin.');
|
|
18
|
+
return value.replace(/\r?\n$/, '');
|
|
19
|
+
}
|
|
20
|
+
return promptPassword(message);
|
|
21
|
+
}
|
|
22
|
+
async function chooseFreshLoginMode(opts) {
|
|
23
|
+
if (opts.create && opts.join)
|
|
24
|
+
throw new Error('--create and --join are mutually exclusive.');
|
|
25
|
+
if (opts.create)
|
|
26
|
+
return 'create';
|
|
27
|
+
if (opts.join)
|
|
28
|
+
return 'join';
|
|
29
|
+
if (!isInteractiveTerminal()) {
|
|
30
|
+
throw new Error('No synced secrets file exists. Pass --create or --join <vault.age> in a non-interactive shell.');
|
|
31
|
+
}
|
|
32
|
+
const { select } = await import('@inquirer/prompts');
|
|
33
|
+
return select({
|
|
34
|
+
message: 'No synced secrets login found. What do you want to do?',
|
|
35
|
+
choices: [
|
|
36
|
+
{ name: 'Create a new login', value: 'create' },
|
|
37
|
+
{ name: 'Join with an existing synced secrets file', value: 'join' },
|
|
38
|
+
],
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function formatRemaining(expiresAt) {
|
|
42
|
+
const ms = expiresAt - Date.now();
|
|
43
|
+
if (ms <= 0)
|
|
44
|
+
return 'expired';
|
|
45
|
+
const mins = Math.round(ms / 60000);
|
|
46
|
+
if (mins < 60)
|
|
47
|
+
return `${mins} minute${mins === 1 ? '' : 's'}`;
|
|
48
|
+
const hours = Math.round(mins / 60);
|
|
49
|
+
if (hours < 24)
|
|
50
|
+
return `${hours} hour${hours === 1 ? '' : 's'}`;
|
|
51
|
+
const days = Math.round(hours / 24);
|
|
52
|
+
return `${days} day${days === 1 ? '' : 's'}`;
|
|
53
|
+
}
|
|
54
|
+
export function registerLoginCommands(program) {
|
|
55
|
+
program
|
|
56
|
+
.command('login')
|
|
57
|
+
.description('Unlock synced secrets for this shell session')
|
|
58
|
+
.option('--create', 'Create a new encrypted synced-secrets file at ~/.agents/vault.age')
|
|
59
|
+
.option('--join <path>', 'Copy and unlock an existing vault.age file')
|
|
60
|
+
.option('--force', 'Replace an existing synced-secrets file when used with --create or --join')
|
|
61
|
+
.option('--password-stdin', 'Read the master password from stdin')
|
|
62
|
+
.action(async (opts) => {
|
|
63
|
+
try {
|
|
64
|
+
if (opts.force && !opts.create && !opts.join) {
|
|
65
|
+
throw new Error('--force can only be used with --create or --join.');
|
|
66
|
+
}
|
|
67
|
+
if (vaultExists() && !opts.create && !opts.join) {
|
|
68
|
+
const password = await readPassword(opts, 'Master password');
|
|
69
|
+
unlock(password);
|
|
70
|
+
const session = getVaultSession();
|
|
71
|
+
const ttl = session.loggedIn ? formatRemaining(session.expiresAt) : '8 hours';
|
|
72
|
+
console.log(chalk.green(`Logged in. Synced secrets unlocked for ${ttl}.`));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const mode = await chooseFreshLoginMode(opts);
|
|
76
|
+
if (mode === 'join') {
|
|
77
|
+
let source = opts.join;
|
|
78
|
+
if (!source) {
|
|
79
|
+
if (!isInteractiveTerminal())
|
|
80
|
+
throw new Error('--join needs a vault.age path.');
|
|
81
|
+
const { input } = await import('@inquirer/prompts');
|
|
82
|
+
source = await input({
|
|
83
|
+
message: 'Path to existing vault.age',
|
|
84
|
+
validate: (value) => fs.existsSync(value) || 'File not found.',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const password = await readPassword(opts, 'Master password');
|
|
88
|
+
joinVault(password, source, { overwrite: opts.force });
|
|
89
|
+
console.log(chalk.green(`Logged in. Copied synced secrets file to ${vaultPath()}.`));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const password = await readPassword(opts, 'Choose a master password');
|
|
93
|
+
createVault(password, { overwrite: opts.force });
|
|
94
|
+
console.log(chalk.green(`Logged in. Created synced secrets file at ${vaultPath()}.`));
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
if (isPromptCancelled(err))
|
|
98
|
+
return;
|
|
99
|
+
console.error(chalk.red(err.message));
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
program
|
|
104
|
+
.command('logout')
|
|
105
|
+
.description('Forget the cached synced-secrets key')
|
|
106
|
+
.action(() => {
|
|
107
|
+
clearVaultKey();
|
|
108
|
+
console.log(chalk.green('Logged out. Synced secrets are locked.'));
|
|
109
|
+
});
|
|
110
|
+
program
|
|
111
|
+
.command('whoami')
|
|
112
|
+
.description('Show synced-secrets login status')
|
|
113
|
+
.action(() => {
|
|
114
|
+
const session = getVaultSession();
|
|
115
|
+
if (!session.loggedIn) {
|
|
116
|
+
console.log(chalk.yellow('Not logged in.'));
|
|
117
|
+
console.log(chalk.gray(`File: ${vaultPath()}${vaultExists() ? '' : ' (not created)'}`));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
console.log(chalk.green(`Logged in (${formatRemaining(session.expiresAt)} remaining).`));
|
|
121
|
+
console.log(chalk.gray(`File: ${vaultPath()}`));
|
|
122
|
+
});
|
|
123
|
+
}
|
package/dist/commands/logs.js
CHANGED
|
@@ -62,7 +62,7 @@ async function showSession(session, follow, full, json = false) {
|
|
|
62
62
|
console.error(chalk.red(`Tailing is supported for claude and codex sessions only (got ${session.agent}).`));
|
|
63
63
|
process.exit(2);
|
|
64
64
|
}
|
|
65
|
-
await streamSessionTail(session, {});
|
|
65
|
+
await streamSessionTail(session, { raw: full });
|
|
66
66
|
return;
|
|
67
67
|
}
|
|
68
68
|
await renderSessionLog(session, full ? 'markdown' : 'summary');
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents mine` — white-label the CLI under your own name.
|
|
3
|
+
*
|
|
4
|
+
* `agents mine init <name>` mints a personally-named binary (e.g. `jack`) that
|
|
5
|
+
* IS agents-cli: a pure pass-through shim on PATH that runs every `agents` verb
|
|
6
|
+
* under the brand's name, with the brand's disabled commands and curated
|
|
7
|
+
* resource profile applied. `list` / `toggle` / `remove` manage brands.
|
|
8
|
+
*
|
|
9
|
+
* The discoverable entry point is the `agents setup mine` wizard
|
|
10
|
+
* (see setup-mine.ts), which delegates to `initBrand` here.
|
|
11
|
+
*
|
|
12
|
+
* Storage: brand config in `meta.brands` (agents.yaml); the curated resource set
|
|
13
|
+
* reuses the resource-profile engine — each brand owns a preset named
|
|
14
|
+
* `mine-<name>` in `meta.profiles.presets`. See lib/brand.ts.
|
|
15
|
+
*/
|
|
16
|
+
import type { Command } from 'commander';
|
|
17
|
+
/**
|
|
18
|
+
* Create (or re-mint) a brand. Shared by `mine init` and the `setup mine`
|
|
19
|
+
* wizard. Writes the shim, the brand config, and an empty resource preset.
|
|
20
|
+
*/
|
|
21
|
+
export declare function initBrand(name: string, opts?: {
|
|
22
|
+
disabledCommands?: string[];
|
|
23
|
+
force?: boolean;
|
|
24
|
+
}): {
|
|
25
|
+
pathWarning: boolean;
|
|
26
|
+
};
|
|
27
|
+
export declare function registerMineCommand(program: Command): void;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { validateBrandName, listBrands, getBrandConfig, upsertBrand, removeBrand, brandPresetName, } from '../lib/brand.js';
|
|
3
|
+
import { createBrandShim, removeBrandShim, isShimsInPath } from '../lib/shims.js';
|
|
4
|
+
import { updateMeta } from '../lib/state.js';
|
|
5
|
+
import { COMMAND_LOADERS } from '../lib/startup/command-registry.js';
|
|
6
|
+
/** Built-in top-level command names, for validating `--disable <cmd>`. */
|
|
7
|
+
function knownCommandNames() {
|
|
8
|
+
const names = new Set(Object.keys(COMMAND_LOADERS));
|
|
9
|
+
// Inline aliases registered outside COMMAND_LOADERS (see src/index.ts).
|
|
10
|
+
for (const n of ['perms', 'exec', 'jobs', 'cron', 'upgrade'])
|
|
11
|
+
names.add(n);
|
|
12
|
+
return names;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Add/remove a `!name` exclusion on a preset's pattern list for one kind,
|
|
16
|
+
* keeping `*` so everything not explicitly disabled stays enabled. Deletes the
|
|
17
|
+
* key entirely once nothing but `*` remains, so a clean brand has an empty preset.
|
|
18
|
+
*/
|
|
19
|
+
function applyResourceToggle(preset, kind, name, enable) {
|
|
20
|
+
// Cast to a plain string-bag: writing through the union-keyed `preset[kind]`
|
|
21
|
+
// collapses to `never` under strict TS, so index the bag instead.
|
|
22
|
+
const bag = preset;
|
|
23
|
+
const arr = [...(bag[kind] ?? [])];
|
|
24
|
+
if (arr.length === 0)
|
|
25
|
+
arr.push('*');
|
|
26
|
+
const excl = `!${name}`;
|
|
27
|
+
const idx = arr.indexOf(excl);
|
|
28
|
+
if (enable) {
|
|
29
|
+
if (idx !== -1)
|
|
30
|
+
arr.splice(idx, 1);
|
|
31
|
+
}
|
|
32
|
+
else if (idx === -1) {
|
|
33
|
+
arr.push(excl);
|
|
34
|
+
}
|
|
35
|
+
const onlyStar = arr.length === 1 && arr[0] === '*';
|
|
36
|
+
if (onlyStar)
|
|
37
|
+
delete bag[kind];
|
|
38
|
+
else
|
|
39
|
+
bag[kind] = arr;
|
|
40
|
+
}
|
|
41
|
+
/** Ensure the brand's preset exists in meta.profiles.presets and return a mutable copy path. */
|
|
42
|
+
function ensurePresetExists(name) {
|
|
43
|
+
const presetName = brandPresetName(name);
|
|
44
|
+
updateMeta((meta) => {
|
|
45
|
+
const profiles = meta.profiles ?? {};
|
|
46
|
+
const presets = { ...(profiles.presets ?? {}) };
|
|
47
|
+
if (!presets[presetName]) {
|
|
48
|
+
presets[presetName] = { description: `Resource set for the "${name}" brand` };
|
|
49
|
+
}
|
|
50
|
+
return { ...meta, profiles: { ...profiles, presets } };
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/** Mutate the brand's resource preset under lock. */
|
|
54
|
+
function editPreset(name, fn) {
|
|
55
|
+
const presetName = brandPresetName(name);
|
|
56
|
+
updateMeta((meta) => {
|
|
57
|
+
const profiles = meta.profiles ?? {};
|
|
58
|
+
const presets = { ...(profiles.presets ?? {}) };
|
|
59
|
+
const preset = { ...(presets[presetName] ?? {}) };
|
|
60
|
+
fn(preset);
|
|
61
|
+
presets[presetName] = preset;
|
|
62
|
+
return { ...meta, profiles: { ...profiles, presets } };
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Create (or re-mint) a brand. Shared by `mine init` and the `setup mine`
|
|
67
|
+
* wizard. Writes the shim, the brand config, and an empty resource preset.
|
|
68
|
+
*/
|
|
69
|
+
export function initBrand(name, opts = {}) {
|
|
70
|
+
const err = validateBrandName(name);
|
|
71
|
+
if (err) {
|
|
72
|
+
console.error(chalk.red(err));
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
if (getBrandConfig(name) && !opts.force) {
|
|
76
|
+
console.error(chalk.red(`Brand "${name}" already exists. Use --force to re-mint, or 'agents mine toggle ${name}'.`));
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
ensurePresetExists(name);
|
|
80
|
+
const disabled = (opts.disabledCommands ?? []).filter((c) => c.length > 0);
|
|
81
|
+
const known = knownCommandNames();
|
|
82
|
+
for (const c of disabled) {
|
|
83
|
+
if (!known.has(c))
|
|
84
|
+
console.error(chalk.yellow(` note: "${c}" is not a known command — kept anyway.`));
|
|
85
|
+
}
|
|
86
|
+
const cfg = {
|
|
87
|
+
name,
|
|
88
|
+
enabled: true,
|
|
89
|
+
profile: brandPresetName(name),
|
|
90
|
+
...(disabled.length > 0 ? { disabledCommands: disabled } : {}),
|
|
91
|
+
};
|
|
92
|
+
upsertBrand(cfg);
|
|
93
|
+
createBrandShim(name);
|
|
94
|
+
const pathWarning = !isShimsInPath();
|
|
95
|
+
return { pathWarning };
|
|
96
|
+
}
|
|
97
|
+
function printMinted(name, pathWarning) {
|
|
98
|
+
console.log(`${chalk.green('Minted')} ${chalk.bold(name)} — your own agents CLI.`);
|
|
99
|
+
console.log(chalk.dim(` try: ${name} --help ${name} run claude "hello"`));
|
|
100
|
+
if (pathWarning) {
|
|
101
|
+
console.log(chalk.yellow(` note: the shims dir isn't on your PATH yet — run 'agents setup' or open a new shell.`));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export function registerMineCommand(program) {
|
|
105
|
+
const cmd = program
|
|
106
|
+
.command('mine')
|
|
107
|
+
.description('White-label the CLI: your own personally-named binary (e.g. `jack`)');
|
|
108
|
+
cmd
|
|
109
|
+
.command('init <name>')
|
|
110
|
+
.description('Mint your own branded CLI that runs every agents verb under <name>')
|
|
111
|
+
.option('--disable <commands...>', 'Built-in commands to hide from this brand (e.g. teams cloud)')
|
|
112
|
+
.option('--force', 'Re-mint even if the brand already exists')
|
|
113
|
+
.action((name, options) => {
|
|
114
|
+
const { pathWarning } = initBrand(name, {
|
|
115
|
+
disabledCommands: options.disable,
|
|
116
|
+
force: options.force,
|
|
117
|
+
});
|
|
118
|
+
printMinted(name, pathWarning);
|
|
119
|
+
});
|
|
120
|
+
cmd
|
|
121
|
+
.command('list')
|
|
122
|
+
.alias('ls')
|
|
123
|
+
.description('Show your brands and what each has turned off')
|
|
124
|
+
.action(() => {
|
|
125
|
+
const brands = listBrands();
|
|
126
|
+
const entries = Object.entries(brands);
|
|
127
|
+
if (entries.length === 0) {
|
|
128
|
+
console.log(chalk.gray("No brands yet. Try: agents setup mine (or: agents mine init jack)"));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const width = entries.reduce((m, [n]) => Math.max(m, n.length), 0);
|
|
132
|
+
for (const [name, cfg] of entries) {
|
|
133
|
+
const off = cfg.disabledCommands?.length ? `disabled: ${cfg.disabledCommands.join(', ')}` : 'all commands on';
|
|
134
|
+
const state = cfg.enabled === false ? chalk.yellow('(disabled) ') : '';
|
|
135
|
+
console.log(` ${chalk.bold(name.padEnd(width))} ${state}${chalk.dim(off)}`);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
cmd
|
|
139
|
+
.command('toggle <name>')
|
|
140
|
+
.description('Enable/disable features for a brand')
|
|
141
|
+
.option('--disable <commands...>', 'Hide these built-in commands')
|
|
142
|
+
.option('--enable <commands...>', 'Un-hide these built-in commands')
|
|
143
|
+
.option('--disable-plugin <names...>', 'Disable these plugins for the brand')
|
|
144
|
+
.option('--enable-plugin <names...>', 'Re-enable these plugins')
|
|
145
|
+
.option('--disable-skill <names...>', 'Disable these skills for the brand')
|
|
146
|
+
.option('--enable-skill <names...>', 'Re-enable these skills')
|
|
147
|
+
.action((name, options) => {
|
|
148
|
+
const cfg = getBrandConfig(name);
|
|
149
|
+
if (!cfg) {
|
|
150
|
+
console.error(chalk.red(`No brand named "${name}". Create it with 'agents mine init ${name}'.`));
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
// Built-in command toggles → brand.disabledCommands.
|
|
154
|
+
const disabledSet = new Set(cfg.disabledCommands ?? []);
|
|
155
|
+
const known = knownCommandNames();
|
|
156
|
+
for (const c of options.disable ?? []) {
|
|
157
|
+
if (!known.has(c))
|
|
158
|
+
console.error(chalk.yellow(` note: "${c}" is not a known command — kept anyway.`));
|
|
159
|
+
disabledSet.add(c);
|
|
160
|
+
}
|
|
161
|
+
for (const c of options.enable ?? [])
|
|
162
|
+
disabledSet.delete(c);
|
|
163
|
+
const nextDisabled = [...disabledSet];
|
|
164
|
+
const nextCfg = { ...cfg };
|
|
165
|
+
if (nextDisabled.length > 0)
|
|
166
|
+
nextCfg.disabledCommands = nextDisabled;
|
|
167
|
+
else
|
|
168
|
+
delete nextCfg.disabledCommands;
|
|
169
|
+
upsertBrand(nextCfg);
|
|
170
|
+
// Resource toggles → the brand's profile preset.
|
|
171
|
+
const resourceOps = [];
|
|
172
|
+
for (const p of options.disablePlugin ?? [])
|
|
173
|
+
resourceOps.push(['plugins', p, false]);
|
|
174
|
+
for (const p of options.enablePlugin ?? [])
|
|
175
|
+
resourceOps.push(['plugins', p, true]);
|
|
176
|
+
for (const s of options.disableSkill ?? [])
|
|
177
|
+
resourceOps.push(['skills', s, false]);
|
|
178
|
+
for (const s of options.enableSkill ?? [])
|
|
179
|
+
resourceOps.push(['skills', s, true]);
|
|
180
|
+
if (resourceOps.length > 0) {
|
|
181
|
+
ensurePresetExists(name);
|
|
182
|
+
editPreset(name, (preset) => {
|
|
183
|
+
for (const [kind, n, enable] of resourceOps)
|
|
184
|
+
applyResourceToggle(preset, kind, n, enable);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
console.log(`${chalk.green('Updated')} ${chalk.bold(name)}.`);
|
|
188
|
+
const finalCfg = getBrandConfig(name);
|
|
189
|
+
const off = finalCfg.disabledCommands?.length ? finalCfg.disabledCommands.join(', ') : '(none)';
|
|
190
|
+
console.log(chalk.dim(` disabled commands: ${off}`));
|
|
191
|
+
});
|
|
192
|
+
cmd
|
|
193
|
+
.command('remove <name>')
|
|
194
|
+
.alias('rm')
|
|
195
|
+
.description('Remove a brand (its shim + config)')
|
|
196
|
+
.option('--purge', "Also delete the brand's resource profile preset")
|
|
197
|
+
.action((name, options) => {
|
|
198
|
+
const existed = getBrandConfig(name) !== undefined;
|
|
199
|
+
const shimRemoved = removeBrandShim(name);
|
|
200
|
+
if (!existed && !shimRemoved) {
|
|
201
|
+
console.error(chalk.red(`No brand named "${name}".`));
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
removeBrand(name, options.purge === true);
|
|
205
|
+
console.log(`${chalk.yellow('Removed')} brand ${chalk.bold(name)}`);
|
|
206
|
+
});
|
|
207
|
+
}
|