@phnx-labs/agents-cli 1.20.35 → 1.20.36
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 +91 -0
- package/LICENSE +185 -21
- package/README.md +8 -4
- package/dist/commands/audit.d.ts +14 -0
- package/dist/commands/audit.js +68 -0
- package/dist/commands/browser.js +82 -8
- package/dist/commands/check.d.ts +15 -0
- package/dist/commands/check.js +84 -0
- package/dist/commands/cloud.js +143 -3
- package/dist/commands/computer.js +81 -0
- package/dist/commands/daemon.js +4 -1
- package/dist/commands/doctor.js +1 -89
- package/dist/commands/events.js +3 -3
- package/dist/commands/exec.d.ts +19 -0
- package/dist/commands/exec.js +277 -11
- package/dist/commands/hosts.js +10 -6
- package/dist/commands/inspect.js +8 -9
- package/dist/commands/lock.d.ts +12 -0
- package/dist/commands/lock.js +70 -0
- package/dist/commands/message.d.ts +15 -0
- package/dist/commands/message.js +56 -0
- package/dist/commands/routines.js +101 -5
- package/dist/commands/secrets-migrate.js +106 -57
- package/dist/commands/secrets.d.ts +31 -18
- package/dist/commands/secrets.js +156 -75
- package/dist/commands/serve.d.ts +10 -0
- package/dist/commands/serve.js +37 -0
- package/dist/commands/sessions-inject.d.ts +14 -0
- package/dist/commands/sessions-inject.js +111 -0
- package/dist/commands/sessions-picker.d.ts +2 -0
- package/dist/commands/sessions-picker.js +24 -3
- package/dist/commands/sessions-resume.js +20 -8
- package/dist/commands/sessions.d.ts +71 -1
- package/dist/commands/sessions.js +365 -37
- package/dist/commands/setup.js +4 -2
- package/dist/commands/sync.d.ts +3 -1
- package/dist/commands/sync.js +156 -4
- package/dist/commands/teams.js +217 -0
- package/dist/commands/versions.js +2 -4
- package/dist/commands/watchdog.d.ts +18 -0
- package/dist/commands/watchdog.js +238 -0
- package/dist/index.js +25 -2
- package/dist/lib/audit/log.d.ts +92 -0
- package/dist/lib/audit/log.js +177 -0
- package/dist/lib/auto-pull.js +2 -1
- package/dist/lib/browser/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +18 -7
- package/dist/lib/browser/drivers/ssh.js +2 -1
- package/dist/lib/browser/har.d.ts +84 -0
- package/dist/lib/browser/har.js +77 -0
- package/dist/lib/browser/ipc.js +24 -3
- package/dist/lib/browser/profiles.d.ts +1 -1
- package/dist/lib/browser/profiles.js +8 -10
- package/dist/lib/browser/refs.d.ts +65 -0
- package/dist/lib/browser/refs.js +73 -1
- package/dist/lib/browser/runtime-state.js +1 -0
- package/dist/lib/browser/service.d.ts +38 -2
- package/dist/lib/browser/service.js +112 -8
- package/dist/lib/browser/types.d.ts +14 -1
- package/dist/lib/budget/live-cloud.d.ts +42 -0
- package/dist/lib/budget/live-cloud.js +79 -0
- package/dist/lib/budget/live-team.d.ts +31 -0
- package/dist/lib/budget/live-team.js +115 -0
- package/dist/lib/cloud/codex.js +4 -0
- package/dist/lib/cloud/rush.d.ts +12 -1
- package/dist/lib/cloud/rush.js +13 -3
- package/dist/lib/cloud/types.d.ts +9 -0
- package/dist/lib/computer/dispatch.d.ts +8 -0
- package/dist/lib/computer/dispatch.js +125 -0
- package/dist/lib/computer/loop.d.ts +62 -0
- package/dist/lib/computer/loop.js +98 -0
- package/dist/lib/computer/model.d.ts +44 -0
- package/dist/lib/computer/model.js +157 -0
- package/dist/lib/concurrency.d.ts +19 -0
- package/dist/lib/concurrency.js +33 -0
- package/dist/lib/daemon.d.ts +57 -0
- package/dist/lib/daemon.js +192 -16
- package/dist/lib/devices/registry.d.ts +7 -0
- package/dist/lib/devices/registry.js +24 -0
- package/dist/lib/devices/tailscale.js +1 -1
- package/dist/lib/drift.d.ts +52 -0
- package/dist/lib/drift.js +112 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/events.js +31 -13
- package/dist/lib/exec.d.ts +17 -0
- package/dist/lib/exec.js +79 -13
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +56 -1
- package/dist/lib/hooks/cache.d.ts +6 -0
- package/dist/lib/hooks/cache.js +54 -12
- package/dist/lib/hooks.d.ts +27 -0
- package/dist/lib/hooks.js +127 -8
- package/dist/lib/hosts/dispatch.d.ts +15 -0
- package/dist/lib/hosts/dispatch.js +39 -6
- package/dist/lib/hosts/logs.js +30 -1
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/passthrough.js +3 -1
- package/dist/lib/hosts/ready.d.ts +29 -6
- package/dist/lib/hosts/ready.js +66 -15
- package/dist/lib/hosts/registry.d.ts +19 -2
- package/dist/lib/hosts/registry.js +58 -2
- package/dist/lib/hosts/remote-cmd.d.ts +62 -1
- package/dist/lib/hosts/remote-cmd.js +70 -1
- package/dist/lib/hosts/remote-os.d.ts +17 -0
- package/dist/lib/hosts/remote-os.js +30 -0
- package/dist/lib/hosts/session-index.d.ts +34 -0
- package/dist/lib/hosts/session-index.js +56 -0
- package/dist/lib/hosts/tasks.d.ts +14 -0
- package/dist/lib/hosts/tasks.js +15 -0
- package/dist/lib/lock.d.ts +93 -0
- package/dist/lib/lock.js +207 -0
- package/dist/lib/loop.js +16 -1
- package/dist/lib/machine-id.d.ts +21 -0
- package/dist/lib/machine-id.js +26 -0
- package/dist/lib/mailbox-target.d.ts +36 -0
- package/dist/lib/mailbox-target.js +45 -0
- package/dist/lib/mailbox.d.ts +47 -0
- package/dist/lib/mailbox.js +194 -0
- package/dist/lib/mcp.d.ts +5 -0
- package/dist/lib/mcp.js +24 -8
- package/dist/lib/migrate.d.ts +19 -0
- package/dist/lib/migrate.js +134 -26
- package/dist/lib/overdue.js +3 -0
- package/dist/lib/picker.d.ts +2 -0
- package/dist/lib/picker.js +4 -1
- package/dist/lib/platform/exec.d.ts +46 -0
- package/dist/lib/platform/exec.js +74 -0
- package/dist/lib/platform/process.d.ts +31 -0
- package/dist/lib/platform/process.js +34 -1
- package/dist/lib/platform/winpath.js +2 -0
- package/dist/lib/plugins.js +16 -6
- package/dist/lib/profiles.d.ts +25 -0
- package/dist/lib/profiles.js +22 -6
- package/dist/lib/pty-client.js +2 -1
- package/dist/lib/rotate.d.ts +61 -0
- package/dist/lib/rotate.js +52 -0
- package/dist/lib/routines.d.ts +40 -2
- package/dist/lib/routines.js +66 -8
- package/dist/lib/runner.d.ts +11 -2
- package/dist/lib/runner.js +49 -7
- package/dist/lib/scheduler.js +6 -1
- package/dist/lib/secrets/bundles.d.ts +60 -4
- package/dist/lib/secrets/bundles.js +131 -12
- package/dist/lib/secrets/filestore.d.ts +3 -0
- package/dist/lib/secrets/filestore.js +42 -16
- package/dist/lib/secrets/index.d.ts +43 -2
- package/dist/lib/secrets/index.js +102 -3
- package/dist/lib/secrets/mcp.d.ts +93 -0
- package/dist/lib/secrets/mcp.js +205 -0
- package/dist/lib/secrets/remote.js +12 -5
- package/dist/lib/secrets/sync.js +83 -4
- package/dist/lib/secrets/windows.js +14 -3
- package/dist/lib/serve/data.d.ts +81 -0
- package/dist/lib/serve/data.js +91 -0
- package/dist/lib/serve/page.d.ts +7 -0
- package/dist/lib/serve/page.js +140 -0
- package/dist/lib/serve/server.d.ts +46 -0
- package/dist/lib/serve/server.js +115 -0
- package/dist/lib/session/active.d.ts +54 -0
- package/dist/lib/session/active.js +190 -19
- package/dist/lib/session/discover.d.ts +37 -0
- package/dist/lib/session/discover.js +111 -28
- package/dist/lib/session/inject.d.ts +18 -0
- package/dist/lib/session/inject.js +21 -0
- package/dist/lib/session/parse.js +23 -20
- package/dist/lib/session/pid-registry.d.ts +1 -0
- package/dist/lib/session/pid-registry.js +24 -0
- package/dist/lib/session/provenance.d.ts +14 -2
- package/dist/lib/session/provenance.js +39 -8
- package/dist/lib/session/remote-active.js +19 -7
- package/dist/lib/session/remote-list.d.ts +51 -0
- package/dist/lib/session/remote-list.js +213 -0
- package/dist/lib/session/remote.d.ts +7 -1
- package/dist/lib/session/remote.js +16 -2
- package/dist/lib/session/sync/config.d.ts +1 -15
- package/dist/lib/session/sync/config.js +4 -20
- package/dist/lib/session/types.d.ts +17 -0
- package/dist/lib/shims.d.ts +36 -6
- package/dist/lib/shims.js +91 -29
- package/dist/lib/ssh-exec.js +2 -0
- package/dist/lib/ssh-tunnel.js +2 -1
- package/dist/lib/startup/command-registry.d.ts +6 -0
- package/dist/lib/startup/command-registry.js +13 -1
- package/dist/lib/state.d.ts +13 -0
- package/dist/lib/state.js +103 -9
- package/dist/lib/sync-umbrella.d.ts +14 -7
- package/dist/lib/sync-umbrella.js +17 -9
- package/dist/lib/teams/forEach.d.ts +110 -0
- package/dist/lib/teams/forEach.js +186 -0
- package/dist/lib/teams/index.d.ts +1 -0
- package/dist/lib/teams/index.js +1 -0
- package/dist/lib/teams/pr-watch.d.ts +226 -0
- package/dist/lib/teams/pr-watch.js +371 -0
- package/dist/lib/teams/supervisor.d.ts +14 -1
- package/dist/lib/teams/supervisor.js +19 -0
- package/dist/lib/teams/worktree.d.ts +9 -0
- package/dist/lib/teams/worktree.js +32 -0
- package/dist/lib/terminal/backends/index.d.ts +2 -1
- package/dist/lib/terminal/backends/index.js +3 -1
- package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
- package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
- package/dist/lib/terminal/index.d.ts +4 -1
- package/dist/lib/terminal/index.js +4 -1
- package/dist/lib/terminal/inject.d.ts +204 -0
- package/dist/lib/terminal/inject.js +247 -0
- package/dist/lib/terminal/resolve.d.ts +64 -0
- package/dist/lib/terminal/resolve.js +90 -0
- package/dist/lib/terminal/types.d.ts +1 -1
- package/dist/lib/triggers/webhook.d.ts +85 -0
- package/dist/lib/triggers/webhook.js +141 -0
- package/dist/lib/versions.d.ts +23 -0
- package/dist/lib/versions.js +119 -13
- package/dist/lib/watchdog/index.d.ts +3 -0
- package/dist/lib/watchdog/index.js +5 -0
- package/dist/lib/watchdog/read.d.ts +35 -0
- package/dist/lib/watchdog/read.js +149 -0
- package/dist/lib/watchdog/runner.d.ts +127 -0
- package/dist/lib/watchdog/runner.js +322 -0
- package/dist/lib/watchdog/watchdog.d.ts +40 -0
- package/dist/lib/watchdog/watchdog.js +166 -0
- package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
- package/dist/lib/watchdog/watchdogTail.js +154 -0
- package/dist/lib/workflows.d.ts +166 -0
- package/dist/lib/workflows.js +193 -0
- package/package.json +5 -4
package/dist/commands/hosts.js
CHANGED
|
@@ -14,6 +14,7 @@ import { getProvider, listAllHosts, resolveHost } from '../lib/hosts/registry.js
|
|
|
14
14
|
import { sshTargetFor } from '../lib/hosts/types.js';
|
|
15
15
|
import { listSshConfigHosts, listKnownHosts, isSshConfigHost } from '../lib/hosts/ssh-config.js';
|
|
16
16
|
import { probeHost, remoteAgentsVersion, bootstrapAgentsCli, localCliVersion, } from '../lib/hosts/ready.js';
|
|
17
|
+
import { resolveRemoteOsSync } from '../lib/hosts/remote-os.js';
|
|
17
18
|
import { listTasks } from '../lib/hosts/tasks.js';
|
|
18
19
|
import { reconcileRunningTasks } from '../lib/hosts/reconcile.js';
|
|
19
20
|
import { showHostTaskLog } from '../lib/hosts/logs.js';
|
|
@@ -29,18 +30,20 @@ function parseTarget(target) {
|
|
|
29
30
|
* Callers that just probed the host pass the result through to avoid a second,
|
|
30
31
|
* redundant `uname` round-trip; otherwise we probe here.
|
|
31
32
|
*/
|
|
32
|
-
async function maybeBootstrap(target, hostName, probe = probeHost(target)) {
|
|
33
|
+
async function maybeBootstrap(target, hostName, probe = probeHost(target, resolveRemoteOsSync(hostName))) {
|
|
33
34
|
if (!probe.reachable) {
|
|
34
35
|
console.log(chalk.yellow(` Not reachable over SSH yet — skipping bootstrap. Fix key auth, then: agents hosts check ${hostName}`));
|
|
35
36
|
return;
|
|
36
37
|
}
|
|
37
|
-
|
|
38
|
+
// Prefer the OS the probe just observed; fall back to the registry hint.
|
|
39
|
+
const os = probe.os ?? resolveRemoteOsSync(hostName);
|
|
40
|
+
const remoteVer = remoteAgentsVersion(target, os);
|
|
38
41
|
const localVer = localCliVersion();
|
|
39
42
|
if (!remoteVer) {
|
|
40
43
|
const ok = await confirm({ message: ` agents-cli not found on ${hostName}. Install ${localVer ? `v${localVer}` : 'latest'} now?`, default: true });
|
|
41
44
|
if (ok) {
|
|
42
45
|
console.log(chalk.gray(' Installing agents-cli on the host…'));
|
|
43
|
-
const r = bootstrapAgentsCli(target, localVer);
|
|
46
|
+
const r = bootstrapAgentsCli(target, localVer, os);
|
|
44
47
|
console.log(r.ok ? chalk.green(' Installed.') : chalk.red(` Install failed:\n${r.output}`));
|
|
45
48
|
}
|
|
46
49
|
return;
|
|
@@ -49,7 +52,7 @@ async function maybeBootstrap(target, hostName, probe = probeHost(target)) {
|
|
|
49
52
|
if (localVer && remoteClean !== localVer) {
|
|
50
53
|
const ok = await confirm({ message: ` ${hostName} has agents-cli ${remoteClean}, you have ${localVer}. Upgrade to match?`, default: false });
|
|
51
54
|
if (ok) {
|
|
52
|
-
const r = bootstrapAgentsCli(target, localVer);
|
|
55
|
+
const r = bootstrapAgentsCli(target, localVer, os);
|
|
53
56
|
console.log(r.ok ? chalk.green(' Upgraded.') : chalk.red(` Upgrade failed:\n${r.output}`));
|
|
54
57
|
}
|
|
55
58
|
}
|
|
@@ -141,15 +144,16 @@ async function doCheck(name) {
|
|
|
141
144
|
return;
|
|
142
145
|
}
|
|
143
146
|
const target = sshTargetFor(host);
|
|
147
|
+
const os = host.os ?? resolveRemoteOsSync(name);
|
|
144
148
|
process.stdout.write(`Probing ${chalk.cyan(name)} (${target})… `);
|
|
145
|
-
const probe = probeHost(target);
|
|
149
|
+
const probe = probeHost(target, os);
|
|
146
150
|
if (!probe.reachable) {
|
|
147
151
|
console.log(chalk.red('unreachable'));
|
|
148
152
|
process.exitCode = 1;
|
|
149
153
|
return;
|
|
150
154
|
}
|
|
151
155
|
console.log(chalk.green('reachable') + chalk.gray(probe.os ? ` · ${probe.os}` : ''));
|
|
152
|
-
const ver = remoteAgentsVersion(target);
|
|
156
|
+
const ver = remoteAgentsVersion(target, probe.os ?? os);
|
|
153
157
|
console.log(` agents-cli: ${ver ? chalk.green(ver) : chalk.yellow('not installed')}`);
|
|
154
158
|
}
|
|
155
159
|
async function doRemove(name) {
|
package/dist/commands/inspect.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* names render as OSC-8 hyperlinks to the marker file (SKILL.md / WORKFLOW.md /
|
|
12
12
|
* AGENT.md / the file itself) so users can click straight to the source.
|
|
13
13
|
*/
|
|
14
|
-
import {
|
|
14
|
+
import { execFileSync } from 'child_process';
|
|
15
15
|
import { addHostOption } from '../lib/hosts/option.js';
|
|
16
16
|
import * as fs from 'fs';
|
|
17
17
|
import * as os from 'os';
|
|
@@ -512,35 +512,34 @@ function renderRepoSummary(repo, options) {
|
|
|
512
512
|
export function repoGitInfo(root) {
|
|
513
513
|
const git = (args) => {
|
|
514
514
|
try {
|
|
515
|
-
return
|
|
516
|
-
.toString().trim();
|
|
515
|
+
return execFileSync('git', ['-C', root, ...args], { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' }).trim();
|
|
517
516
|
}
|
|
518
517
|
catch {
|
|
519
518
|
return null;
|
|
520
519
|
}
|
|
521
520
|
};
|
|
522
|
-
const branch = git('rev-parse --abbrev-ref HEAD');
|
|
521
|
+
const branch = git(['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
523
522
|
if (branch === null)
|
|
524
523
|
return null;
|
|
525
|
-
// Read status WITHOUT trimming —
|
|
524
|
+
// Read status WITHOUT trimming — trimming would strip the leading
|
|
526
525
|
// space of the first porcelain line (`XY path`), corrupting the path slice.
|
|
527
526
|
let statusRaw;
|
|
528
527
|
try {
|
|
529
|
-
statusRaw =
|
|
528
|
+
statusRaw = execFileSync('git', ['-C', root, 'status', '--porcelain'], { stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' });
|
|
530
529
|
}
|
|
531
530
|
catch {
|
|
532
531
|
statusRaw = null;
|
|
533
532
|
}
|
|
534
533
|
const dirtyFiles = statusRaw ? statusRaw.split('\n').filter(Boolean).map(l => l.slice(3)) : [];
|
|
535
534
|
let lastCommit = null;
|
|
536
|
-
const log = git('log -1 --format=%h%x1f%s%x1f%cr');
|
|
535
|
+
const log = git(['log', '-1', '--format=%h%x1f%s%x1f%cr']);
|
|
537
536
|
if (log) {
|
|
538
537
|
const [sha, subject, relative] = log.split('\x1f');
|
|
539
538
|
if (sha)
|
|
540
539
|
lastCommit = { sha, subject: subject ?? '', relative: relative ?? '' };
|
|
541
540
|
}
|
|
542
541
|
let ahead = null, behind = null;
|
|
543
|
-
const counts = git(
|
|
542
|
+
const counts = git(['rev-list', '--left-right', '--count', '@{upstream}...HEAD']);
|
|
544
543
|
if (counts) {
|
|
545
544
|
const [b, a] = counts.split(/\s+/).map(n => parseInt(n, 10));
|
|
546
545
|
if (Number.isFinite(b) && Number.isFinite(a)) {
|
|
@@ -548,7 +547,7 @@ export function repoGitInfo(root) {
|
|
|
548
547
|
ahead = a;
|
|
549
548
|
}
|
|
550
549
|
}
|
|
551
|
-
return { branch, dirty: dirtyFiles.length, dirtyFiles, url: git('remote get-url origin'), lastCommit, ahead, behind };
|
|
550
|
+
return { branch, dirty: dirtyFiles.length, dirtyFiles, url: git(['remote', 'get-url', 'origin']), lastCommit, ahead, behind };
|
|
552
551
|
}
|
|
553
552
|
// ─── Summary mode ────────────────────────────────────────────────────────────
|
|
554
553
|
async function renderSummary(agent, version, versionHome, options) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents lock` — write a deterministic `agents.lock` for the resolved resource
|
|
3
|
+
* set at the project root; `agents lock --frozen` verifies the live resources
|
|
4
|
+
* match that lock EXACTLY and fails closed (non-zero exit + a clear diff) on any
|
|
5
|
+
* drift. The reproducible-CI slice of governance #337.
|
|
6
|
+
*
|
|
7
|
+
* This is OFF the default path: normal `agents add` / `agents sync` never touch
|
|
8
|
+
* the lock. Generation and verification only run when this command is invoked.
|
|
9
|
+
*/
|
|
10
|
+
import { Command } from 'commander';
|
|
11
|
+
/** Register the `agents lock` command. */
|
|
12
|
+
export declare function registerLockCommand(program: Command): void;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents lock` — write a deterministic `agents.lock` for the resolved resource
|
|
3
|
+
* set at the project root; `agents lock --frozen` verifies the live resources
|
|
4
|
+
* match that lock EXACTLY and fails closed (non-zero exit + a clear diff) on any
|
|
5
|
+
* drift. The reproducible-CI slice of governance #337.
|
|
6
|
+
*
|
|
7
|
+
* This is OFF the default path: normal `agents add` / `agents sync` never touch
|
|
8
|
+
* the lock. Generation and verification only run when this command is invoked.
|
|
9
|
+
*/
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import { enumerateLockSources, buildLock, writeLock, readLock, verifyLock, lockDiffIsClean, resolveProjectRoot, LOCK_FILENAME, } from '../lib/lock.js';
|
|
12
|
+
/** Register the `agents lock` command. */
|
|
13
|
+
export function registerLockCommand(program) {
|
|
14
|
+
program
|
|
15
|
+
.command('lock')
|
|
16
|
+
.summary('Write or verify agents.lock — a SHA-256 manifest of resolved resources')
|
|
17
|
+
.description('Capture a deterministic SHA-256 per resolved resource file (commands, skills, hooks, rules, mcp, permissions, subagents — project > user > system > extras) into an agents.lock at the project root.\n\n' +
|
|
18
|
+
'Run bare to (re)generate the lock. Pass --frozen to VERIFY the live resolved resources match an existing agents.lock exactly — it exits non-zero with an added/removed/changed diff on any mismatch, and errors if no lock exists yet. That is the reproducible-CI gate; normal installs and syncs are unaffected.')
|
|
19
|
+
.option('--cwd <path>', 'Working directory to resolve resources and the project root from')
|
|
20
|
+
.option('--frozen', 'Verify resolved resources match agents.lock exactly; fail (non-zero) on any drift', false)
|
|
21
|
+
.action((opts) => {
|
|
22
|
+
runLock(opts);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function runLock(opts) {
|
|
26
|
+
const cwd = opts.cwd || process.cwd();
|
|
27
|
+
const projectRoot = resolveProjectRoot(cwd);
|
|
28
|
+
const sources = enumerateLockSources(cwd);
|
|
29
|
+
if (opts.frozen) {
|
|
30
|
+
verifyFrozen(projectRoot, sources);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const lock = buildLock(sources);
|
|
34
|
+
const written = writeLock(projectRoot, lock);
|
|
35
|
+
const count = Object.keys(lock.resources).length;
|
|
36
|
+
console.log(chalk.green(`Wrote ${LOCK_FILENAME}`) +
|
|
37
|
+
chalk.gray(` — ${count} resource file(s) → ${written}`));
|
|
38
|
+
}
|
|
39
|
+
function verifyFrozen(projectRoot, sources) {
|
|
40
|
+
let existing;
|
|
41
|
+
try {
|
|
42
|
+
existing = readLock(projectRoot);
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
console.error(chalk.red(`agents lock --frozen: ${e.message}`));
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (!existing) {
|
|
50
|
+
console.error(chalk.red(`No ${LOCK_FILENAME} at ${projectRoot}.`));
|
|
51
|
+
console.error(chalk.gray('Generate one first: agents lock'));
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const diff = verifyLock(existing, sources);
|
|
56
|
+
if (lockDiffIsClean(diff)) {
|
|
57
|
+
const count = Object.keys(existing.resources).length;
|
|
58
|
+
console.log(chalk.green(`✓ ${count} resource file(s) match ${LOCK_FILENAME}`));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
console.error(chalk.red(`✗ resources drifted from ${LOCK_FILENAME}:`));
|
|
62
|
+
for (const key of diff.changed)
|
|
63
|
+
console.error(chalk.yellow(` changed ${key}`));
|
|
64
|
+
for (const key of diff.added)
|
|
65
|
+
console.error(chalk.yellow(` added ${key}`));
|
|
66
|
+
for (const key of diff.removed)
|
|
67
|
+
console.error(chalk.yellow(` removed ${key}`));
|
|
68
|
+
console.error(chalk.gray('Re-lock once the change is intended: agents lock'));
|
|
69
|
+
process.exitCode = 1;
|
|
70
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents message <target> <text>` — send a message to a running agent.
|
|
3
|
+
*
|
|
4
|
+
* Unifies two delivery paths behind one verb:
|
|
5
|
+
* - a live LOCAL/teams/loop agent → enqueue into its file-spool mailbox; the
|
|
6
|
+
* PreToolUse hook injects it at the agent's next tool call.
|
|
7
|
+
* - a CLOUD task id → the existing provider follow-up path (was
|
|
8
|
+
* `agents cloud message`).
|
|
9
|
+
*
|
|
10
|
+
* Cross-host is handled one layer up: `--host <h>` routes the whole command over
|
|
11
|
+
* ssh via `REMOTE_PASSTHROUGH` (see src/lib/hosts/passthrough.ts), so the box is
|
|
12
|
+
* written on the host that actually owns the agent.
|
|
13
|
+
*/
|
|
14
|
+
import type { Command } from 'commander';
|
|
15
|
+
export declare function registerMessageCommand(program: Command): void;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { getActiveSessions } from '../lib/session/active.js';
|
|
3
|
+
import { getTaskById, updateTaskStatus } from '../lib/cloud/store.js';
|
|
4
|
+
import { resolveProvider } from '../lib/cloud/registry.js';
|
|
5
|
+
import { mailboxDir, enqueue } from '../lib/mailbox.js';
|
|
6
|
+
import { resolveMessageTarget } from '../lib/mailbox-target.js';
|
|
7
|
+
function die(msg, code = 1) {
|
|
8
|
+
console.error(chalk.red(msg));
|
|
9
|
+
process.exit(code);
|
|
10
|
+
}
|
|
11
|
+
export function registerMessageCommand(program) {
|
|
12
|
+
program
|
|
13
|
+
.command('message <target> <text>')
|
|
14
|
+
.description('Send a message to a running agent (delivered at its next tool call) or a cloud task.')
|
|
15
|
+
.option('--from <who>', 'Label recorded as the sender of this message')
|
|
16
|
+
.action(async (target, text, opts) => {
|
|
17
|
+
if (!target.trim()) {
|
|
18
|
+
die('Target must be a session/agent id or cloud task id. Run `agents sessions --active` to list running agents.');
|
|
19
|
+
}
|
|
20
|
+
const sessions = await getActiveSessions();
|
|
21
|
+
const res = resolveMessageTarget(target, sessions, (id) => getTaskById(id) != null);
|
|
22
|
+
switch (res.kind) {
|
|
23
|
+
case 'cloud': {
|
|
24
|
+
const task = getTaskById(res.id);
|
|
25
|
+
const provider = resolveProvider(task.provider);
|
|
26
|
+
try {
|
|
27
|
+
await provider.message(res.id, text);
|
|
28
|
+
updateTaskStatus(res.id, 'running');
|
|
29
|
+
console.log(chalk.green(`Message sent to cloud task ${res.id}. Agent is continuing.`));
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
die(err.message);
|
|
33
|
+
}
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
case 'local': {
|
|
37
|
+
try {
|
|
38
|
+
const msgId = enqueue(mailboxDir(res.id), { to: res.id, text, from: opts.from });
|
|
39
|
+
console.log(chalk.green(`Queued message ${msgId} for ${res.id}. `) +
|
|
40
|
+
chalk.dim('The agent will see it at its next tool call.'));
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
die(err.message);
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
case 'ambiguous': {
|
|
48
|
+
const lines = res.candidates.map((c) => ` ${c.id} ${chalk.dim(c.label)}`).join('\n');
|
|
49
|
+
die(`"${target}" matches ${res.candidates.length} running agents:\n${lines}\nRe-run with a full id.`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
case 'none':
|
|
53
|
+
die(`No running agent or cloud task matches "${target}". List targets with \`agents sessions --active\`.`);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -13,6 +13,7 @@ import * as yaml from 'yaml';
|
|
|
13
13
|
import { isDaemonRunning, signalDaemonReload, startDaemon, stopDaemon, readDaemonPid, readDaemonLog, } from '../lib/daemon.js';
|
|
14
14
|
import { humanizeCron, humanizeNextRun, formatRepoLink, REPO_DISPLAY_MAX } from '../lib/routines-format.js';
|
|
15
15
|
import { listJobs as listAllJobs, deleteJob, readJob, validateJob, writeJob, setJobEnabled, listRuns, getLatestRun, getRunDir, getJobPath, parseAtTime, } from '../lib/routines.js';
|
|
16
|
+
import { fireWebhookJobs, matchJobsToWebhook } from '../lib/triggers/webhook.js';
|
|
16
17
|
import { getRoutinesDir } from '../lib/state.js';
|
|
17
18
|
import { IS_WINDOWS } from '../lib/platform/index.js';
|
|
18
19
|
import { safeJoin } from '../lib/paths.js';
|
|
@@ -21,6 +22,21 @@ import { JobScheduler } from '../lib/scheduler.js';
|
|
|
21
22
|
import { detectOverdueJobs } from '../lib/overdue.js';
|
|
22
23
|
import { isInteractiveTerminal, requireInteractiveSelection } from './utils.js';
|
|
23
24
|
import { setHelpSections } from '../lib/help.js';
|
|
25
|
+
/**
|
|
26
|
+
* Human label for what fires a job: its cron schedule, or its event trigger
|
|
27
|
+
* for schedule-less (trigger-only) routines.
|
|
28
|
+
*/
|
|
29
|
+
function fireConditionLabel(job) {
|
|
30
|
+
if (job.schedule)
|
|
31
|
+
return humanizeCron(job.schedule, job.timezone);
|
|
32
|
+
if (job.trigger) {
|
|
33
|
+
const scope = job.trigger.repo
|
|
34
|
+
? ` (${job.trigger.repo}${job.trigger.branch ? `@${job.trigger.branch}` : ''})`
|
|
35
|
+
: '';
|
|
36
|
+
return `on ${job.trigger.event}${scope}`;
|
|
37
|
+
}
|
|
38
|
+
return '-';
|
|
39
|
+
}
|
|
24
40
|
/** Start or reload the background scheduler so newly-added jobs fire on time. */
|
|
25
41
|
function ensureSchedulerRunning() {
|
|
26
42
|
if (isDaemonRunning()) {
|
|
@@ -73,7 +89,7 @@ async function pickJob(message, filter, alternatives = [], cwd) {
|
|
|
73
89
|
message,
|
|
74
90
|
choices: jobs.map((job) => ({
|
|
75
91
|
value: job.name,
|
|
76
|
-
name: `${job.name} ${chalk.gray(`(${job.workflow ? `wf:${job.workflow}` : job.agent}, ${job.schedule})`)}`,
|
|
92
|
+
name: `${job.name} ${chalk.gray(`(${job.workflow ? `wf:${job.workflow}` : job.agent}, ${job.schedule ?? fireConditionLabel(job)})`)}`,
|
|
77
93
|
})),
|
|
78
94
|
});
|
|
79
95
|
}
|
|
@@ -159,8 +175,9 @@ export function registerRoutinesCommands(program) {
|
|
|
159
175
|
agent: job.agent ?? null,
|
|
160
176
|
workflow: job.workflow ?? null,
|
|
161
177
|
repo: job.repo ?? null,
|
|
162
|
-
schedule: job.schedule,
|
|
163
|
-
scheduleHuman:
|
|
178
|
+
schedule: job.schedule ?? null,
|
|
179
|
+
scheduleHuman: fireConditionLabel(job),
|
|
180
|
+
trigger: job.trigger ?? null,
|
|
164
181
|
timezone: job.timezone ?? null,
|
|
165
182
|
enabled: job.enabled,
|
|
166
183
|
overdue: overdueSet.has(job.name),
|
|
@@ -193,7 +210,7 @@ export function registerRoutinesCommands(program) {
|
|
|
193
210
|
for (const job of jobs) {
|
|
194
211
|
const nextRun = scheduler.getNextRun(job.name);
|
|
195
212
|
const nextStr = humanizeNextRun(nextRun ?? null, now, job.timezone);
|
|
196
|
-
let schedStr =
|
|
213
|
+
let schedStr = fireConditionLabel(job);
|
|
197
214
|
if (job.endAt) {
|
|
198
215
|
const end = new Date(job.endAt);
|
|
199
216
|
const endLabel = Number.isFinite(end.getTime())
|
|
@@ -565,6 +582,82 @@ export function registerRoutinesCommands(program) {
|
|
|
565
582
|
}
|
|
566
583
|
console.log(chalk.gray('\nTrack progress with: agents routines runs <name>'));
|
|
567
584
|
});
|
|
585
|
+
routinesCmd
|
|
586
|
+
.command('webhook')
|
|
587
|
+
.description('Fire trigger-based routines from a single GitHub webhook payload (read from --file or stdin). One-shot: matches and fires, then exits. For a long-running receiver, run this behind your own HTTP forwarder.')
|
|
588
|
+
.requiredOption('--event <name>', 'GitHub event name, as sent in the X-GitHub-Event header (e.g. pull_request, push, workflow_run)')
|
|
589
|
+
.option('--file <path>', 'Read the webhook JSON payload from this file instead of stdin')
|
|
590
|
+
.option('--dry-run', 'Show which routines would fire without firing them')
|
|
591
|
+
.action(async (options) => {
|
|
592
|
+
// Load the raw JSON payload: --file wins, else drain stdin.
|
|
593
|
+
let raw;
|
|
594
|
+
if (options.file) {
|
|
595
|
+
const resolved = path.resolve(options.file);
|
|
596
|
+
if (!fs.existsSync(resolved)) {
|
|
597
|
+
console.log(chalk.red(`File not found: ${resolved}`));
|
|
598
|
+
process.exit(1);
|
|
599
|
+
}
|
|
600
|
+
raw = fs.readFileSync(resolved, 'utf-8');
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
if (process.stdin.isTTY) {
|
|
604
|
+
console.log(chalk.red('No payload provided. Pass --file <path> or pipe the webhook JSON on stdin.'));
|
|
605
|
+
process.exit(1);
|
|
606
|
+
}
|
|
607
|
+
const chunks = [];
|
|
608
|
+
for await (const chunk of process.stdin)
|
|
609
|
+
chunks.push(chunk);
|
|
610
|
+
raw = Buffer.concat(chunks).toString('utf-8');
|
|
611
|
+
}
|
|
612
|
+
let payload;
|
|
613
|
+
try {
|
|
614
|
+
const parsed = raw.trim() ? JSON.parse(raw) : {};
|
|
615
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
616
|
+
throw new Error('payload must be a JSON object');
|
|
617
|
+
}
|
|
618
|
+
payload = parsed;
|
|
619
|
+
}
|
|
620
|
+
catch (err) {
|
|
621
|
+
console.log(chalk.red(`Invalid webhook payload JSON: ${err.message}`));
|
|
622
|
+
process.exit(1);
|
|
623
|
+
}
|
|
624
|
+
const webhook = { event: options.event, payload };
|
|
625
|
+
// Matching is intentionally user-layer only (fireWebhookJobs defaults to
|
|
626
|
+
// listJobs() with no cwd), mirroring `run`/`catchup`: a webhook must never
|
|
627
|
+
// fire a cloned project repo's `.agents/routines/*.yml` and run an
|
|
628
|
+
// attacker-supplied prompt under the user's agent session.
|
|
629
|
+
if (options.dryRun) {
|
|
630
|
+
const matched = matchJobsToWebhook(listAllJobs(), webhook);
|
|
631
|
+
if (matched.length === 0) {
|
|
632
|
+
console.log(chalk.gray(`No routines match a ${options.event} event for this payload.`));
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
console.log(chalk.bold(`${matched.length} routine(s) would fire on ${options.event}:\n`));
|
|
636
|
+
for (const job of matched) {
|
|
637
|
+
console.log(` ${chalk.cyan(job.name)} — ${fireConditionLabel(job)}`);
|
|
638
|
+
}
|
|
639
|
+
console.log(chalk.gray('\n(dry run — no routines triggered)'));
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
// Fired jobs run detached via executeJobDetached (the same path cron
|
|
643
|
+
// uses). Keep the daemon alive so each run's meta.json is finalized.
|
|
644
|
+
if (!isDaemonRunning()) {
|
|
645
|
+
const started = startDaemon();
|
|
646
|
+
if (started.pid) {
|
|
647
|
+
console.log(chalk.gray(`Started scheduler (PID: ${started.pid}) so webhook runs are monitored.`));
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
const fired = await fireWebhookJobs(webhook);
|
|
651
|
+
if (fired.length === 0) {
|
|
652
|
+
console.log(chalk.gray(`No routines match a ${options.event} event for this payload.`));
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
console.log(chalk.bold(`Fired ${fired.length} routine(s) on ${options.event}:\n`));
|
|
656
|
+
for (const f of fired) {
|
|
657
|
+
console.log(` ${chalk.cyan(f.jobName)} → ${chalk.green('started')} (run: ${f.runId})`);
|
|
658
|
+
}
|
|
659
|
+
console.log(chalk.gray('\nTrack progress with: agents routines runs <name>'));
|
|
660
|
+
});
|
|
568
661
|
routinesCmd
|
|
569
662
|
.command('logs [name]')
|
|
570
663
|
.description('Read stdout from the most recent execution. Use --run to see a specific past run.')
|
|
@@ -675,9 +768,12 @@ export function registerRoutinesCommands(program) {
|
|
|
675
768
|
if (result.method === 'already-running') {
|
|
676
769
|
console.log(chalk.yellow(`Scheduler already running (PID: ${result.pid})`));
|
|
677
770
|
}
|
|
678
|
-
else {
|
|
771
|
+
else if (result.pid) {
|
|
679
772
|
console.log(chalk.green(`Scheduler started (PID: ${result.pid})`));
|
|
680
773
|
}
|
|
774
|
+
else {
|
|
775
|
+
console.log(chalk.yellow('Scheduler start dispatched but no PID surfaced. Check `agents routines status`.'));
|
|
776
|
+
}
|
|
681
777
|
});
|
|
682
778
|
routinesCmd
|
|
683
779
|
.command('stop')
|
|
@@ -23,7 +23,7 @@ import chalk from 'chalk';
|
|
|
23
23
|
import * as crypto from 'crypto';
|
|
24
24
|
import * as fs from 'fs';
|
|
25
25
|
import * as path from 'path';
|
|
26
|
-
import { deleteKeychainToken, getKeychainToken, getKeychainTokens, hasKeychainToken, listKeychainItems, listLegacyKeychainItems, setKeychainToken, } from '../lib/secrets/index.js';
|
|
26
|
+
import { deleteKeychainToken, getKeychainToken, getKeychainTokens, hasKeychainToken, listKeychainItems, listLegacyKeychainItems, listOrphanedKeychainItems, migrateOrphanedKeychainItems, setKeychainToken, } from '../lib/secrets/index.js';
|
|
27
27
|
import { getBackupsDir } from '../lib/state.js';
|
|
28
28
|
import { encryptBlob, MIN_PASSPHRASE_LEN } from '../lib/secrets/sync.js';
|
|
29
29
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
@@ -101,7 +101,7 @@ function migrateOne(record) {
|
|
|
101
101
|
export function registerSecretsMigrateAclCommand(secrets) {
|
|
102
102
|
secrets
|
|
103
103
|
.command('migrate-acl')
|
|
104
|
-
.description('Refresh
|
|
104
|
+
.description('Refresh legacy keychain ACLs and re-home items stranded in a stale access group. Dry-run by default.')
|
|
105
105
|
.option('--commit', 'Perform writes (default is dry-run reporting only)')
|
|
106
106
|
.option('--prefix <p>', `Restrict to items beginning with PREFIX (default ${ITEM_PREFIX})`, ITEM_PREFIX)
|
|
107
107
|
.option('--all', 'Rewrite EVERY matching item, not just legacy stragglers (slower; a Touch ID prompt per item)')
|
|
@@ -115,88 +115,137 @@ export function registerSecretsMigrateAclCommand(secrets) {
|
|
|
115
115
|
if (!prefix.startsWith(ITEM_PREFIX)) {
|
|
116
116
|
throw new Error(`--prefix must start with '${ITEM_PREFIX}' to avoid touching unrelated Keychain items (got '${prefix}').`);
|
|
117
117
|
}
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
118
|
+
// Two independent classes of work under one command:
|
|
119
|
+
// (a) Legacy ACL stragglers — items still in the file-based keychain
|
|
120
|
+
// with a pre-migration trusted-app ACL. Rewritten value-by-value.
|
|
121
|
+
// (b) Orphaned access groups — data-protection items filed under a
|
|
122
|
+
// pre-#279 non-concrete group, invisible to the pinned queries.
|
|
123
|
+
// Re-homed by the helper behind a single Touch ID.
|
|
124
|
+
// Default: only legacy stragglers for (a); `--all` forces a full rewrite.
|
|
123
125
|
const names = opts.all ? listKeychainItems(prefix) : listLegacyKeychainItems(prefix);
|
|
124
126
|
const items = names.map((item) => {
|
|
125
127
|
const localExists = hasKeychainToken(item);
|
|
126
128
|
return { item, sync: !localExists };
|
|
127
129
|
});
|
|
128
|
-
|
|
130
|
+
const orphans = listOrphanedKeychainItems(prefix);
|
|
131
|
+
if (items.length === 0 && orphans.length === 0) {
|
|
129
132
|
console.log(opts.all
|
|
130
133
|
? chalk.gray(`No keychain items with prefix '${prefix}'.`)
|
|
131
|
-
: chalk.green(`Nothing to migrate — all items under '${prefix}'
|
|
134
|
+
: chalk.green(`Nothing to migrate — all items under '${prefix}' use the modern ACL and access group.`));
|
|
132
135
|
return;
|
|
133
136
|
}
|
|
134
137
|
const label = opts.all ? 'item' : 'legacy item';
|
|
135
|
-
|
|
138
|
+
if (items.length > 0) {
|
|
139
|
+
console.log(chalk.bold(`Found ${items.length} ${label}(s) under '${prefix}' with an outdated ACL.`));
|
|
140
|
+
}
|
|
141
|
+
if (orphans.length > 0) {
|
|
142
|
+
console.log(chalk.bold(`Found ${orphans.length} orphaned item(s) under '${prefix}' in a stale access group.`));
|
|
143
|
+
}
|
|
136
144
|
if (!opts.commit) {
|
|
137
145
|
for (const { item, sync } of items) {
|
|
138
146
|
console.log(` ${chalk.cyan(item)} ${chalk.gray(sync ? '(synced)' : '(local)')}`);
|
|
139
147
|
}
|
|
148
|
+
for (const item of orphans) {
|
|
149
|
+
console.log(` ${chalk.cyan(item)} ${chalk.yellow('(orphaned access group)')}`);
|
|
150
|
+
}
|
|
140
151
|
console.log();
|
|
141
152
|
console.log(chalk.gray('Dry-run — pass --commit to perform the migration.'));
|
|
142
153
|
return;
|
|
143
154
|
}
|
|
144
|
-
// Commit phase
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
155
|
+
// ---- Commit phase ----
|
|
156
|
+
let okCount = 0;
|
|
157
|
+
let failCount = 0;
|
|
158
|
+
// (a) Legacy ACL rewrite. Snapshot every value first (one batched read
|
|
159
|
+
// behind a single helper process), encrypt, then delete + re-add. A total
|
|
160
|
+
// read failure here counts the legacy items as failed but does NOT abort —
|
|
161
|
+
// the orphan sweep (b) is independent and must still run.
|
|
162
|
+
if (items.length > 0) {
|
|
163
|
+
const fetched = getKeychainTokens(items.map((i) => i.item));
|
|
164
|
+
const records = [];
|
|
165
|
+
for (const { item, sync } of items) {
|
|
166
|
+
const value = fetched.get(item);
|
|
167
|
+
if (value === undefined) {
|
|
168
|
+
console.error(chalk.red(`Skipping '${item}': read failed or item absent.`));
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
records.push({ item, sync, value });
|
|
154
172
|
}
|
|
155
|
-
records.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
console.error(chalk.red('No items could be read. Aborting before any writes.'));
|
|
159
|
-
process.exit(1);
|
|
160
|
-
}
|
|
161
|
-
const passphrase = opts.passphraseEnv
|
|
162
|
-
? (() => {
|
|
163
|
-
const v = process.env[opts.passphraseEnv];
|
|
164
|
-
if (!v)
|
|
165
|
-
throw new Error(`Env var '${opts.passphraseEnv}' not set.`);
|
|
166
|
-
if (v.length < MIN_PASSPHRASE_LEN)
|
|
167
|
-
throw new Error(`Passphrase must be at least ${MIN_PASSPHRASE_LEN} characters.`);
|
|
168
|
-
return v;
|
|
169
|
-
})()
|
|
170
|
-
: await promptPassphrase();
|
|
171
|
-
const backupPath = writeEncryptedBackup(records, passphrase);
|
|
172
|
-
// Compute a quick fingerprint so the user can sanity-check recovery without
|
|
173
|
-
// decrypting. Hash of (count, sorted item names).
|
|
174
|
-
const fingerprint = crypto
|
|
175
|
-
.createHash('sha256')
|
|
176
|
-
.update(records.length + '\n' + records.map((r) => r.item).sort().join('\n'))
|
|
177
|
-
.digest('hex')
|
|
178
|
-
.slice(0, 12);
|
|
179
|
-
console.log(chalk.green(`Encrypted backup written to ${backupPath} (sha256-12: ${fingerprint}).`));
|
|
180
|
-
const results = [];
|
|
181
|
-
for (const record of records) {
|
|
182
|
-
const r = migrateOne(record);
|
|
183
|
-
results.push(r);
|
|
184
|
-
if (r.status === 'ok') {
|
|
185
|
-
console.log(` ${chalk.green('ok')} ${record.item}`);
|
|
173
|
+
if (records.length === 0) {
|
|
174
|
+
console.error(chalk.red('No legacy items could be read — skipping legacy rewrite.'));
|
|
175
|
+
failCount += items.length;
|
|
186
176
|
}
|
|
187
177
|
else {
|
|
188
|
-
|
|
178
|
+
const passphrase = opts.passphraseEnv
|
|
179
|
+
? (() => {
|
|
180
|
+
const v = process.env[opts.passphraseEnv];
|
|
181
|
+
if (!v)
|
|
182
|
+
throw new Error(`Env var '${opts.passphraseEnv}' not set.`);
|
|
183
|
+
if (v.length < MIN_PASSPHRASE_LEN)
|
|
184
|
+
throw new Error(`Passphrase must be at least ${MIN_PASSPHRASE_LEN} characters.`);
|
|
185
|
+
return v;
|
|
186
|
+
})()
|
|
187
|
+
: await promptPassphrase();
|
|
188
|
+
const backupPath = writeEncryptedBackup(records, passphrase);
|
|
189
|
+
// Quick fingerprint so the user can sanity-check recovery without decrypting.
|
|
190
|
+
const fingerprint = crypto
|
|
191
|
+
.createHash('sha256')
|
|
192
|
+
.update(records.length + '\n' + records.map((r) => r.item).sort().join('\n'))
|
|
193
|
+
.digest('hex')
|
|
194
|
+
.slice(0, 12);
|
|
195
|
+
console.log(chalk.green(`Encrypted backup written to ${backupPath} (sha256-12: ${fingerprint}).`));
|
|
196
|
+
const results = [];
|
|
197
|
+
for (const record of records) {
|
|
198
|
+
const r = migrateOne(record);
|
|
199
|
+
results.push(r);
|
|
200
|
+
if (r.status === 'ok') {
|
|
201
|
+
console.log(` ${chalk.green('ok')} ${record.item}`);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
console.log(` ${chalk.red(r.status)} ${record.item} ${chalk.gray(r.detail ?? '')}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const ok = results.filter((r) => r.status === 'ok').length;
|
|
208
|
+
okCount += ok;
|
|
209
|
+
failCount += results.length - ok;
|
|
210
|
+
if (results.length - ok > 0) {
|
|
211
|
+
console.error(chalk.gray(`Restore from ${backupPath} using the backup passphrase if needed.`));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// (b) Orphan re-home — one helper call, one Touch ID for the batch. The
|
|
216
|
+
// helper adds the pinned copy before deleting the orphan, so no pre-write
|
|
217
|
+
// backup is needed (a failed add leaves the orphan intact and readable).
|
|
218
|
+
if (orphans.length > 0) {
|
|
219
|
+
console.log(chalk.bold(`Re-homing ${orphans.length} orphaned item(s) into the current access group (one Touch ID)…`));
|
|
220
|
+
const results = migrateOrphanedKeychainItems(prefix);
|
|
221
|
+
const healed = new Set();
|
|
222
|
+
for (const r of results) {
|
|
223
|
+
healed.add(r.item);
|
|
224
|
+
if (r.status === 'ok') {
|
|
225
|
+
okCount += 1;
|
|
226
|
+
console.log(` ${chalk.green('ok')} ${r.item}`);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
failCount += 1;
|
|
230
|
+
console.log(` ${chalk.red(r.status)} ${r.item} ${chalk.gray(r.detail ?? '')}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// Any orphan we listed but the helper couldn't reach (e.g. under a
|
|
234
|
+
// different signing team) — surface it, never drop it silently.
|
|
235
|
+
for (const item of orphans) {
|
|
236
|
+
if (!healed.has(item)) {
|
|
237
|
+
failCount += 1;
|
|
238
|
+
console.log(` ${chalk.red('fail')} ${item} ${chalk.gray('unreachable (different signing team?)')}`);
|
|
239
|
+
}
|
|
189
240
|
}
|
|
190
241
|
}
|
|
191
|
-
const
|
|
192
|
-
const failCount = results.length - okCount;
|
|
242
|
+
const total = okCount + failCount;
|
|
193
243
|
console.log();
|
|
194
244
|
if (failCount === 0) {
|
|
195
|
-
console.log(chalk.green(`Migrated ${okCount}/${
|
|
245
|
+
console.log(chalk.green(`Migrated ${okCount}/${total} item(s).`));
|
|
196
246
|
}
|
|
197
247
|
else {
|
|
198
|
-
console.error(chalk.yellow(`Migrated ${okCount}/${
|
|
199
|
-
console.error(chalk.gray(`Restore from ${backupPath} using the backup passphrase if needed.`));
|
|
248
|
+
console.error(chalk.yellow(`Migrated ${okCount}/${total} item(s); ${failCount} failed.`));
|
|
200
249
|
process.exit(1);
|
|
201
250
|
}
|
|
202
251
|
}
|