@phnx-labs/agents-cli 1.18.5 → 1.19.0
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 +13 -2
- package/README.md +22 -20
- package/dist/commands/browser.js +25 -2
- package/dist/commands/cloud.js +3 -3
- package/dist/commands/computer.d.ts +6 -0
- package/dist/commands/computer.js +477 -0
- package/dist/commands/doctor.js +19 -17
- package/dist/commands/exec.js +37 -59
- package/dist/commands/factory.js +12 -5
- package/dist/commands/import.js +6 -1
- package/dist/commands/mcp.js +9 -4
- package/dist/commands/packages.d.ts +3 -0
- package/dist/commands/packages.js +20 -12
- package/dist/commands/permissions.d.ts +2 -0
- package/dist/commands/permissions.js +20 -1
- package/dist/commands/plugins.d.ts +2 -0
- package/dist/commands/plugins.js +23 -4
- package/dist/commands/profiles.js +1 -1
- package/dist/commands/pty.js +126 -112
- package/dist/commands/pull.js +29 -25
- package/dist/commands/repo.js +24 -26
- package/dist/commands/routines.js +29 -26
- package/dist/commands/secrets.js +66 -73
- package/dist/commands/sessions-tail.js +21 -22
- package/dist/commands/sessions.js +36 -68
- package/dist/commands/setup.js +20 -24
- package/dist/commands/teams.js +30 -39
- package/dist/commands/versions.js +60 -68
- package/dist/commands/worktree.d.ts +20 -0
- package/dist/commands/worktree.js +242 -0
- package/dist/computer.d.ts +2 -0
- package/dist/computer.js +7 -0
- package/dist/index.js +70 -26
- package/dist/lib/agents.d.ts +4 -1
- package/dist/lib/agents.js +23 -5
- package/dist/lib/browser/cdp.d.ts +15 -1
- package/dist/lib/browser/cdp.js +77 -8
- package/dist/lib/browser/chrome.js +17 -24
- package/dist/lib/browser/drivers/ssh.d.ts +1 -0
- package/dist/lib/browser/drivers/ssh.js +20 -8
- package/dist/lib/browser/ipc.js +38 -5
- package/dist/lib/browser/profiles.js +34 -2
- package/dist/lib/browser/runtime-state.d.ts +1 -2
- package/dist/lib/browser/runtime-state.js +11 -3
- package/dist/lib/browser/service.d.ts +5 -0
- package/dist/lib/browser/service.js +32 -4
- package/dist/lib/browser/types.d.ts +1 -1
- package/dist/lib/browser/upload.d.ts +2 -0
- package/dist/lib/browser/upload.js +34 -0
- package/dist/lib/cloud/rush.d.ts +2 -1
- package/dist/lib/cloud/rush.js +28 -9
- package/dist/lib/computer-rpc.d.ts +24 -0
- package/dist/lib/computer-rpc.js +263 -0
- package/dist/lib/daemon.js +7 -7
- package/dist/lib/exec.d.ts +2 -1
- package/dist/lib/exec.js +3 -2
- package/dist/lib/fs-atomic.d.ts +18 -0
- package/dist/lib/fs-atomic.js +76 -0
- package/dist/lib/git.js +2 -4
- package/dist/lib/help.d.ts +15 -0
- package/dist/lib/help.js +41 -0
- package/dist/lib/hooks/match.d.ts +1 -0
- package/dist/lib/hooks/match.js +57 -12
- package/dist/lib/hooks.d.ts +1 -0
- package/dist/lib/hooks.js +27 -10
- package/dist/lib/import.d.ts +1 -0
- package/dist/lib/import.js +7 -0
- package/dist/lib/manifest.js +27 -1
- package/dist/lib/mcp.d.ts +14 -0
- package/dist/lib/mcp.js +79 -14
- package/dist/lib/migrate.js +3 -3
- package/dist/lib/models.js +3 -1
- package/dist/lib/permissions.d.ts +5 -0
- package/dist/lib/permissions.js +35 -0
- package/dist/lib/plugin-marketplace.d.ts +3 -1
- package/dist/lib/plugin-marketplace.js +36 -1
- package/dist/lib/plugins.d.ts +19 -1
- package/dist/lib/plugins.js +99 -8
- package/dist/lib/redact.d.ts +4 -0
- package/dist/lib/redact.js +18 -0
- package/dist/lib/registry.d.ts +2 -0
- package/dist/lib/registry.js +15 -0
- package/dist/lib/sandbox.js +15 -5
- package/dist/lib/secrets/bundles.d.ts +7 -12
- package/dist/lib/secrets/bundles.js +45 -29
- package/dist/lib/secrets/index.js +4 -4
- package/dist/lib/session/cloud.d.ts +2 -0
- package/dist/lib/session/cloud.js +34 -6
- package/dist/lib/session/parse.js +7 -2
- package/dist/lib/session/render.d.ts +4 -1
- package/dist/lib/session/render.js +81 -35
- package/dist/lib/shims.d.ts +5 -2
- package/dist/lib/shims.js +29 -7
- package/dist/lib/state.d.ts +5 -5
- package/dist/lib/state.js +43 -13
- package/dist/lib/teams/agents.d.ts +1 -1
- package/dist/lib/teams/agents.js +2 -2
- package/dist/lib/types.d.ts +4 -3
- package/dist/lib/types.js +0 -2
- package/dist/lib/versions.js +65 -40
- package/dist/lib/workflows.d.ts +7 -0
- package/dist/lib/workflows.js +42 -1
- package/npm-shrinkwrap.json +3256 -0
- package/package.json +32 -26
- package/scripts/postinstall.js +8 -2
|
@@ -12,6 +12,7 @@ import { createShim, createVersionedAlias, removeShim, shimExists, getShimsDir,
|
|
|
12
12
|
import { isInteractiveTerminal, isPromptCancelled, requireInteractiveSelection } from './utils.js';
|
|
13
13
|
import { tryAutoPull } from '../lib/git.js';
|
|
14
14
|
import { getAgentsDir } from '../lib/state.js';
|
|
15
|
+
import { setHelpSections } from '../lib/help.js';
|
|
15
16
|
/**
|
|
16
17
|
* Helper to get actual installed version for an agent.
|
|
17
18
|
* Returns the latest installed version, or throws if none installed.
|
|
@@ -79,34 +80,32 @@ function warnIfShimShadowed(agent) {
|
|
|
79
80
|
}
|
|
80
81
|
/** Register `agents add`, `agents remove`, `agents use`, and `agents list` (deprecated). */
|
|
81
82
|
export function registerVersionsCommands(program) {
|
|
82
|
-
program
|
|
83
|
+
const addCmd = program
|
|
83
84
|
.command('add <specs...>')
|
|
84
85
|
.description('Download and install agent CLI versions. Enables subsidized API usage through managed binaries.')
|
|
85
|
-
.option('-p, --project', 'Lock this version to the current project directory only, stored in
|
|
86
|
-
.option('-y, --yes', 'Auto-accept defaults without prompting (useful for scripts and CI)')
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
# Install specific version (use this when you need reproducibility)
|
|
93
|
-
agents add claude@2.1.112
|
|
86
|
+
.option('-p, --project', 'Lock this version to the current project directory only, stored in project-root agents.yaml')
|
|
87
|
+
.option('-y, --yes', 'Auto-accept defaults without prompting (useful for scripts and CI)');
|
|
88
|
+
setHelpSections(addCmd, {
|
|
89
|
+
examples: `
|
|
90
|
+
# Install the latest version of an agent
|
|
91
|
+
agents add claude@latest
|
|
94
92
|
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
# Install a specific version (reproducibility)
|
|
94
|
+
agents add claude@2.1.112
|
|
97
95
|
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
# Install multiple agents at once
|
|
97
|
+
agents add claude@latest codex@0.116.0
|
|
100
98
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
99
|
+
# Lock a version to this project only (won't affect global default)
|
|
100
|
+
agents add claude@2.1.100 --project
|
|
101
|
+
`,
|
|
102
|
+
notes: `
|
|
103
|
+
- The first version you install becomes the default automatically.
|
|
104
|
+
- 'add' does NOT change the default if a default already exists. Use 'agents use' to switch.
|
|
105
|
+
- Multi-account: each installed version has separate auth, so you can install the same agent twice for two accounts.
|
|
106
|
+
`,
|
|
107
|
+
});
|
|
108
|
+
addCmd.action(async (specs, options) => {
|
|
110
109
|
const isProject = options.project;
|
|
111
110
|
const skipPrompts = options.yes || !isInteractiveTerminal();
|
|
112
111
|
for (const spec of specs) {
|
|
@@ -228,7 +227,7 @@ Note: The first version you install becomes the default automatically.
|
|
|
228
227
|
await setDefaultVersion(agent, installedVersion);
|
|
229
228
|
}
|
|
230
229
|
else if (skipPrompts) {
|
|
231
|
-
|
|
230
|
+
console.log(chalk.gray(` Default remains ${agentLabel(agentConfig.id)}@${currentDefault}. Run 'agents use ${agent}@${installedVersion}' to switch.`));
|
|
232
231
|
}
|
|
233
232
|
else {
|
|
234
233
|
try {
|
|
@@ -297,31 +296,27 @@ Note: The first version you install becomes the default automatically.
|
|
|
297
296
|
}
|
|
298
297
|
}
|
|
299
298
|
});
|
|
300
|
-
program
|
|
299
|
+
const removeCmd = program
|
|
301
300
|
.command('remove <specs...>')
|
|
302
|
-
.description('Uninstall agent CLI versions
|
|
303
|
-
.option('-p, --project', 'Also clear the pinned version from .agents/agents.yaml in the current project')
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
# Remove without specifying version (opens interactive picker)
|
|
310
|
-
agents remove claude
|
|
301
|
+
.description('Uninstall agent CLI versions. Frees disk space and removes the version\'s auth token.')
|
|
302
|
+
.option('-p, --project', 'Also clear the pinned version from .agents/agents.yaml in the current project');
|
|
303
|
+
setHelpSections(removeCmd, {
|
|
304
|
+
examples: `
|
|
305
|
+
# Remove a specific version
|
|
306
|
+
agents remove claude@2.0.50
|
|
311
307
|
|
|
312
|
-
|
|
313
|
-
|
|
308
|
+
# Pick interactively if you omit the version
|
|
309
|
+
agents remove claude
|
|
314
310
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
.action(async (specs, options) => {
|
|
311
|
+
# Remove and also clear the project pin
|
|
312
|
+
agents remove claude@2.0.50 --project
|
|
313
|
+
`,
|
|
314
|
+
notes: `
|
|
315
|
+
- Removing the default version unsets the default; run 'agents use' to pick a new one.
|
|
316
|
+
- Reinstall any time with 'agents add'.
|
|
317
|
+
`,
|
|
318
|
+
});
|
|
319
|
+
removeCmd.action(async (specs, options) => {
|
|
325
320
|
const isProject = options.project;
|
|
326
321
|
for (const spec of specs) {
|
|
327
322
|
const parsed = parseAgentSpec(spec);
|
|
@@ -418,32 +413,29 @@ Notes:
|
|
|
418
413
|
}
|
|
419
414
|
}
|
|
420
415
|
});
|
|
421
|
-
program
|
|
416
|
+
const useCmd = program
|
|
422
417
|
.command('use <agent> [version]')
|
|
423
|
-
.description('Switch the active version for an agent. This is
|
|
418
|
+
.description('Switch the active version for an agent. This is the only command that sets the default.')
|
|
424
419
|
.option('-p, --project', 'Pin to this project directory only (stored in .agents/agents.yaml)')
|
|
425
|
-
.option('-y, --yes', 'Auto-sync resources without prompting')
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
420
|
+
.option('-y, --yes', 'Auto-sync resources without prompting');
|
|
421
|
+
setHelpSections(useCmd, {
|
|
422
|
+
examples: `
|
|
423
|
+
# Set global default (interactive picker if version omitted)
|
|
424
|
+
agents use claude
|
|
425
|
+
agents use claude@2.1.112
|
|
431
426
|
|
|
432
|
-
|
|
433
|
-
|
|
427
|
+
# Pin this project to a version (overrides the global default in this directory)
|
|
428
|
+
agents use claude@2.1.100 --project
|
|
434
429
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
Important: This is the ONLY command that sets the default version. If you want a version to be active, you must run 'agents use'.
|
|
445
|
-
`)
|
|
446
|
-
.action(async (agentArg, versionArg, options) => {
|
|
430
|
+
# Switch accounts — each installed version has its own auth
|
|
431
|
+
agents use claude@2.1.50
|
|
432
|
+
`,
|
|
433
|
+
notes: `
|
|
434
|
+
- 'agents add' installs but does NOT set the default. Always follow with 'agents use'.
|
|
435
|
+
- --project pins to the current directory only via .agents/agents.yaml.
|
|
436
|
+
`,
|
|
437
|
+
});
|
|
438
|
+
useCmd.action(async (agentArg, versionArg, options) => {
|
|
447
439
|
try {
|
|
448
440
|
const skipPrompts = options.yes || !isInteractiveTerminal();
|
|
449
441
|
// Auto-pull ~/.agents-system if it's a git repo with remote (silent on success)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agents worktree -- provision, release, and prune per-terminal git worktrees.
|
|
3
|
+
*
|
|
4
|
+
* Used by surfaces that want to spawn each agent terminal in an isolated
|
|
5
|
+
* working tree (Companion extension opt-in toggle). Mirrors the in-process
|
|
6
|
+
* worktree helpers in lib/teams/worktree.ts but exposes them as a CLI so
|
|
7
|
+
* other processes (IDE extensions, shell aliases, hooks) can call them.
|
|
8
|
+
*
|
|
9
|
+
* agents worktree provision <terminal-id> -> prints absolute worktree path
|
|
10
|
+
* agents worktree release <terminal-id> -> removes if clean + merged
|
|
11
|
+
* agents worktree prune -> removes every clean+merged one
|
|
12
|
+
*
|
|
13
|
+
* Worktrees live at <repo>/.history/worktrees/<terminal-id>, on a branch
|
|
14
|
+
* named agent/<terminal-id>. The branch starts at HEAD of the parent repo.
|
|
15
|
+
* .history/ mirrors the agents-cli runtime-state convention at ~/.agents/.history/
|
|
16
|
+
* but scoped to the repo. .agents/ is reserved for project resources
|
|
17
|
+
* (skills, hooks, commands) per the agents-cli DotAgents repo layout.
|
|
18
|
+
*/
|
|
19
|
+
import type { Command } from 'commander';
|
|
20
|
+
export declare function registerWorktreeCommands(program: Command): void;
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { execFile } from 'child_process';
|
|
3
|
+
import { promisify } from 'util';
|
|
4
|
+
import * as fs from 'fs/promises';
|
|
5
|
+
import * as fsSync from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { setHelpSections } from '../lib/help.js';
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
const WORKTREE_SUBDIR = path.join('.history', 'worktrees');
|
|
10
|
+
const BRANCH_PREFIX = 'agent/';
|
|
11
|
+
function die(msg, code = 1) {
|
|
12
|
+
console.error(chalk.red(msg));
|
|
13
|
+
process.exit(code);
|
|
14
|
+
}
|
|
15
|
+
function isValidTerminalId(id) {
|
|
16
|
+
// Allow letters, digits, dot, dash, underscore. Reject anything else so a
|
|
17
|
+
// hostile or buggy caller can't inject path traversal or shell metachars.
|
|
18
|
+
return /^[A-Za-z0-9._-]+$/.test(id) && id.length > 0 && id.length <= 128;
|
|
19
|
+
}
|
|
20
|
+
async function gitRoot(cwd) {
|
|
21
|
+
try {
|
|
22
|
+
const { stdout } = await execFileAsync('git', ['rev-parse', '--show-toplevel'], { cwd });
|
|
23
|
+
return stdout.trim();
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
die(`Not inside a git repo: ${cwd}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function worktreePathFor(root, terminalId) {
|
|
30
|
+
return path.join(root, WORKTREE_SUBDIR, terminalId);
|
|
31
|
+
}
|
|
32
|
+
function branchNameFor(terminalId) {
|
|
33
|
+
return `${BRANCH_PREFIX}${terminalId}`;
|
|
34
|
+
}
|
|
35
|
+
async function inspect(root, terminalId) {
|
|
36
|
+
const wt = worktreePathFor(root, terminalId);
|
|
37
|
+
const branch = branchNameFor(terminalId);
|
|
38
|
+
const exists = fsSync.existsSync(wt);
|
|
39
|
+
if (!exists) {
|
|
40
|
+
return { exists: false, dirty: false, aheadOfUpstream: false, hasUpstream: false, branchMerged: false };
|
|
41
|
+
}
|
|
42
|
+
let dirty = false;
|
|
43
|
+
try {
|
|
44
|
+
const { stdout } = await execFileAsync('git', ['status', '--porcelain'], { cwd: wt });
|
|
45
|
+
dirty = stdout.trim().length > 0;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
dirty = true; // err on the side of caution
|
|
49
|
+
}
|
|
50
|
+
let hasUpstream = false;
|
|
51
|
+
let aheadOfUpstream = false;
|
|
52
|
+
try {
|
|
53
|
+
await execFileAsync('git', ['rev-parse', '--abbrev-ref', '@{u}'], { cwd: wt });
|
|
54
|
+
hasUpstream = true;
|
|
55
|
+
const { stdout } = await execFileAsync('git', ['rev-list', '--count', '@{u}..HEAD'], { cwd: wt });
|
|
56
|
+
aheadOfUpstream = parseInt(stdout.trim(), 10) > 0;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
hasUpstream = false; // never pushed -- treat as "has commits we'd lose"
|
|
60
|
+
}
|
|
61
|
+
let branchMerged = false;
|
|
62
|
+
try {
|
|
63
|
+
const { stdout } = await execFileAsync('git', ['branch', '--merged', 'origin/main', '--list', branch], { cwd: root });
|
|
64
|
+
branchMerged = stdout.trim().length > 0;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
branchMerged = false;
|
|
68
|
+
}
|
|
69
|
+
return { exists, dirty, aheadOfUpstream, hasUpstream, branchMerged };
|
|
70
|
+
}
|
|
71
|
+
async function provision(root, terminalId) {
|
|
72
|
+
const wt = worktreePathFor(root, terminalId);
|
|
73
|
+
const branch = branchNameFor(terminalId);
|
|
74
|
+
if (fsSync.existsSync(wt))
|
|
75
|
+
return wt;
|
|
76
|
+
await fs.mkdir(path.dirname(wt), { recursive: true });
|
|
77
|
+
// If the branch already exists (e.g. left over from a previous run), reuse
|
|
78
|
+
// it instead of failing. Surfaces typically reuse a terminal-id when
|
|
79
|
+
// restoring a session.
|
|
80
|
+
let branchExists = false;
|
|
81
|
+
try {
|
|
82
|
+
await execFileAsync('git', ['rev-parse', '--verify', `refs/heads/${branch}`], { cwd: root });
|
|
83
|
+
branchExists = true;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
branchExists = false;
|
|
87
|
+
}
|
|
88
|
+
const args = branchExists
|
|
89
|
+
? ['worktree', 'add', wt, branch]
|
|
90
|
+
: ['worktree', 'add', '-b', branch, wt, 'HEAD'];
|
|
91
|
+
await execFileAsync('git', args, { cwd: root });
|
|
92
|
+
return wt;
|
|
93
|
+
}
|
|
94
|
+
async function release(root, terminalId, force) {
|
|
95
|
+
const wt = worktreePathFor(root, terminalId);
|
|
96
|
+
const branch = branchNameFor(terminalId);
|
|
97
|
+
if (!fsSync.existsSync(wt)) {
|
|
98
|
+
// Already gone; treat as success but tell the caller.
|
|
99
|
+
return { removed: false, reason: 'worktree does not exist' };
|
|
100
|
+
}
|
|
101
|
+
if (!force) {
|
|
102
|
+
const report = await inspect(root, terminalId);
|
|
103
|
+
if (report.dirty)
|
|
104
|
+
return { removed: false, reason: 'worktree has uncommitted changes' };
|
|
105
|
+
if (!report.hasUpstream) {
|
|
106
|
+
// Local-only commits exist if HEAD differs from origin/main.
|
|
107
|
+
try {
|
|
108
|
+
const { stdout } = await execFileAsync('git', ['rev-list', '--count', 'origin/main..HEAD'], { cwd: wt });
|
|
109
|
+
if (parseInt(stdout.trim(), 10) > 0) {
|
|
110
|
+
return { removed: false, reason: 'branch has local commits not on origin/main' };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return { removed: false, reason: 'cannot verify branch state vs origin/main' };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (report.aheadOfUpstream)
|
|
118
|
+
return { removed: false, reason: 'branch has unpushed commits' };
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
await execFileAsync('git', ['worktree', 'remove', force ? '--force' : '', wt].filter(Boolean), { cwd: root });
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
if (err.message?.includes('is not a working tree')) {
|
|
125
|
+
await execFileAsync('git', ['worktree', 'prune'], { cwd: root });
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
throw err;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Delete the branch if it's safe to do so. -d (lowercase) refuses to drop
|
|
132
|
+
// an unmerged branch by itself, which is exactly the safety net we want.
|
|
133
|
+
try {
|
|
134
|
+
await execFileAsync('git', ['branch', '-d', branch], { cwd: root });
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// Branch may not exist or may not be merged. We already validated above;
|
|
138
|
+
// leaving the branch behind is preferable to silently dropping commits.
|
|
139
|
+
}
|
|
140
|
+
return { removed: true };
|
|
141
|
+
}
|
|
142
|
+
async function listAgentWorktrees(root) {
|
|
143
|
+
const dir = path.join(root, WORKTREE_SUBDIR);
|
|
144
|
+
if (!fsSync.existsSync(dir))
|
|
145
|
+
return [];
|
|
146
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
147
|
+
return entries.filter(e => e.isDirectory()).map(e => e.name);
|
|
148
|
+
}
|
|
149
|
+
export function registerWorktreeCommands(program) {
|
|
150
|
+
const wt = program
|
|
151
|
+
.command('worktree')
|
|
152
|
+
.description('Provision, release, and prune per-terminal git worktrees for agent isolation.');
|
|
153
|
+
setHelpSections(wt, {
|
|
154
|
+
examples: `
|
|
155
|
+
# Create (or reuse) an isolated worktree for an agent terminal — prints the path
|
|
156
|
+
agents worktree provision CC-1747509823-3
|
|
157
|
+
|
|
158
|
+
# Remove the worktree if it's clean and merged/pushed
|
|
159
|
+
agents worktree release CC-1747509823-3
|
|
160
|
+
|
|
161
|
+
# Preview what 'prune' would release across all agent worktrees
|
|
162
|
+
agents worktree prune --dry-run
|
|
163
|
+
|
|
164
|
+
# Release every agent worktree that's safe to remove
|
|
165
|
+
agents worktree prune
|
|
166
|
+
`,
|
|
167
|
+
notes: `
|
|
168
|
+
Worktrees live at <repo>/.history/worktrees/<terminal-id> on branch agent/<terminal-id>.
|
|
169
|
+
Use --force on 'release' to skip safety checks (DANGEROUS — discards unpushed work).
|
|
170
|
+
`,
|
|
171
|
+
});
|
|
172
|
+
wt.command('provision <terminal-id>')
|
|
173
|
+
.description('Create (or reuse) an isolated worktree for an agent terminal. Prints the absolute path.')
|
|
174
|
+
.option('--root <path>', 'Repo root (defaults to current working directory)')
|
|
175
|
+
.action(async (terminalId, opts) => {
|
|
176
|
+
if (!isValidTerminalId(terminalId)) {
|
|
177
|
+
die(`Invalid terminal-id: ${terminalId} (allowed: [A-Za-z0-9._-], <=128 chars)`);
|
|
178
|
+
}
|
|
179
|
+
const root = await gitRoot(opts.root ?? process.cwd());
|
|
180
|
+
const wtPath = await provision(root, terminalId);
|
|
181
|
+
console.log(wtPath);
|
|
182
|
+
});
|
|
183
|
+
wt.command('release <terminal-id>')
|
|
184
|
+
.description('Remove the worktree if clean and the branch is merged or has no unpushed commits.')
|
|
185
|
+
.option('--root <path>', 'Repo root (defaults to current working directory)')
|
|
186
|
+
.option('--force', 'Skip safety checks (DANGEROUS: discards unpushed work)')
|
|
187
|
+
.action(async (terminalId, opts) => {
|
|
188
|
+
if (!isValidTerminalId(terminalId)) {
|
|
189
|
+
die(`Invalid terminal-id: ${terminalId} (allowed: [A-Za-z0-9._-], <=128 chars)`);
|
|
190
|
+
}
|
|
191
|
+
const root = await gitRoot(opts.root ?? process.cwd());
|
|
192
|
+
const result = await release(root, terminalId, Boolean(opts.force));
|
|
193
|
+
if (result.removed) {
|
|
194
|
+
console.log(chalk.green(`removed ${worktreePathFor(root, terminalId)}`));
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
console.log(chalk.yellow(`kept ${worktreePathFor(root, terminalId)} (${result.reason})`));
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
wt.command('prune')
|
|
201
|
+
.description('Try to release every agent worktree under .history/worktrees/. Skips dirty or unpushed ones.')
|
|
202
|
+
.option('--root <path>', 'Repo root (defaults to current working directory)')
|
|
203
|
+
.option('--dry-run', 'Report what would be removed without touching anything')
|
|
204
|
+
.action(async (opts) => {
|
|
205
|
+
const root = await gitRoot(opts.root ?? process.cwd());
|
|
206
|
+
const ids = await listAgentWorktrees(root);
|
|
207
|
+
if (ids.length === 0) {
|
|
208
|
+
console.log(chalk.gray('no agent worktrees to prune'));
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
for (const id of ids) {
|
|
212
|
+
if (!isValidTerminalId(id)) {
|
|
213
|
+
console.log(chalk.yellow(`skip ${id} (name not in expected format)`));
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (opts.dryRun) {
|
|
217
|
+
const report = await inspect(root, id);
|
|
218
|
+
const blocker = report.dirty
|
|
219
|
+
? 'dirty'
|
|
220
|
+
: report.aheadOfUpstream
|
|
221
|
+
? 'unpushed'
|
|
222
|
+
: !report.hasUpstream
|
|
223
|
+
? 'never pushed'
|
|
224
|
+
: null;
|
|
225
|
+
if (blocker) {
|
|
226
|
+
console.log(chalk.yellow(`keep ${id} (${blocker})`));
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
console.log(chalk.green(`remove ${id}`));
|
|
230
|
+
}
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const result = await release(root, id, false);
|
|
234
|
+
if (result.removed) {
|
|
235
|
+
console.log(chalk.green(`removed ${id}`));
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
console.log(chalk.yellow(`kept ${id} (${result.reason})`));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
}
|
package/dist/computer.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { registerComputerSubcommands } from './commands/computer.js';
|
|
4
|
+
const program = new Command();
|
|
5
|
+
program.name('computer').description('Drive macOS apps via Accessibility — list, screenshot, click, type');
|
|
6
|
+
registerComputerSubcommands(program);
|
|
7
|
+
program.parse();
|
package/dist/index.js
CHANGED
|
@@ -23,6 +23,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
23
23
|
const packageJsonPath = path.join(__dirname, '..', 'package.json');
|
|
24
24
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
25
25
|
const VERSION = packageJson.version;
|
|
26
|
+
const NPM_PACKAGE_NAME = '@phnx-labs/agents-cli';
|
|
26
27
|
// Detect dev/working-tree builds and default the noisy startup steps off.
|
|
27
28
|
// Three cases trip this:
|
|
28
29
|
// 1. Dev install (scripts/install.sh) — package.json version stamped 0.0.0-dev.<sha>
|
|
@@ -78,11 +79,13 @@ import { registerDoctorCommand } from './commands/doctor.js';
|
|
|
78
79
|
import { registerSubagentsCommands } from './commands/subagents.js';
|
|
79
80
|
import { registerPluginsCommands } from './commands/plugins.js';
|
|
80
81
|
import { registerWorkflowsCommands } from './commands/workflows.js';
|
|
82
|
+
import { registerWorktreeCommands } from './commands/worktree.js';
|
|
81
83
|
import { registerSyncCommand } from './commands/sync.js';
|
|
82
84
|
import { registerRefreshRulesCommand } from './commands/refresh-rules.js';
|
|
83
85
|
import { registerDriveCommands } from './commands/drive.js';
|
|
84
86
|
import { registerPtyCommands } from './commands/pty.js';
|
|
85
87
|
import { registerBrowserCommand } from './commands/browser.js';
|
|
88
|
+
import { registerComputerCommand } from './commands/computer.js';
|
|
86
89
|
import { registerProfilesCommands } from './commands/profiles.js';
|
|
87
90
|
import { registerSecretsCommands } from './commands/secrets.js';
|
|
88
91
|
import { registerFactoryCommands } from './commands/factory.js';
|
|
@@ -268,10 +271,36 @@ function saveUpdateCheck(latestVersion) {
|
|
|
268
271
|
/* best-effort cache update */
|
|
269
272
|
}
|
|
270
273
|
}
|
|
274
|
+
/** Fetch the exact latest npm version plus its registry integrity hash. */
|
|
275
|
+
async function fetchLatestNpmPackageMetadata(timeoutMs = 5000) {
|
|
276
|
+
const response = await fetch(`https://registry.npmjs.org/${NPM_PACKAGE_NAME}/latest`, {
|
|
277
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
278
|
+
});
|
|
279
|
+
if (!response.ok) {
|
|
280
|
+
throw new Error('Could not reach npm registry');
|
|
281
|
+
}
|
|
282
|
+
const data = await response.json();
|
|
283
|
+
if (typeof data.version !== 'string' || typeof data.dist?.integrity !== 'string') {
|
|
284
|
+
throw new Error('npm registry response did not include version and integrity');
|
|
285
|
+
}
|
|
286
|
+
return { version: data.version, integrity: data.dist.integrity };
|
|
287
|
+
}
|
|
288
|
+
function printResolvedPackage(metadata) {
|
|
289
|
+
console.log(chalk.gray(`Resolved: ${NPM_PACKAGE_NAME}@${metadata.version}`));
|
|
290
|
+
console.log(chalk.gray(`Integrity: ${metadata.integrity}`));
|
|
291
|
+
}
|
|
292
|
+
async function installResolvedPackage(metadata) {
|
|
293
|
+
const { execFile } = await import('child_process');
|
|
294
|
+
const { promisify } = await import('util');
|
|
295
|
+
const execFileAsync = promisify(execFile);
|
|
296
|
+
const installArgs = ['install', '-g', '@phnx-labs/agents-cli', '--ignore-scripts'];
|
|
297
|
+
installArgs[2] = `${NPM_PACKAGE_NAME}@${metadata.version}`;
|
|
298
|
+
await execFileAsync('npm', installArgs);
|
|
299
|
+
}
|
|
271
300
|
/** Present an interactive upgrade prompt (TTY) or a one-line hint (non-TTY). */
|
|
272
301
|
async function promptUpgrade(latestVersion) {
|
|
273
302
|
if (!isInteractiveTerminal()) {
|
|
274
|
-
console.error(chalk.yellow(`Update available: ${VERSION} -> ${latestVersion}. Run:
|
|
303
|
+
console.error(chalk.yellow(`Update available: ${VERSION} -> ${latestVersion}. Run: agents upgrade --yes`));
|
|
275
304
|
return;
|
|
276
305
|
}
|
|
277
306
|
const answer = await select({
|
|
@@ -299,14 +328,24 @@ async function promptUpgrade(latestVersion) {
|
|
|
299
328
|
return;
|
|
300
329
|
}
|
|
301
330
|
if (answer === 'now') {
|
|
302
|
-
const {
|
|
303
|
-
|
|
304
|
-
const execFileAsync = promisify(execFile);
|
|
305
|
-
const spinner = ora('Upgrading...').start();
|
|
331
|
+
const { spawnSync } = await import('child_process');
|
|
332
|
+
let spinner = ora('Resolving package metadata...').start();
|
|
306
333
|
try {
|
|
307
|
-
|
|
308
|
-
spinner.succeed(`
|
|
309
|
-
|
|
334
|
+
const metadata = await fetchLatestNpmPackageMetadata();
|
|
335
|
+
spinner.succeed(`Resolved ${NPM_PACKAGE_NAME}@${metadata.version}`);
|
|
336
|
+
printResolvedPackage(metadata);
|
|
337
|
+
const approved = await confirm({
|
|
338
|
+
message: `Install ${NPM_PACKAGE_NAME}@${metadata.version}?`,
|
|
339
|
+
default: false,
|
|
340
|
+
});
|
|
341
|
+
if (!approved) {
|
|
342
|
+
console.log(chalk.gray('Upgrade cancelled'));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
spinner = ora('Upgrading...').start();
|
|
346
|
+
await installResolvedPackage(metadata);
|
|
347
|
+
spinner.succeed(`Upgraded to ${metadata.version}`);
|
|
348
|
+
await showWhatsNew(VERSION, metadata.version);
|
|
310
349
|
console.log();
|
|
311
350
|
// Re-exec with new version and exit
|
|
312
351
|
const result = spawnSync('agents', process.argv.slice(2), {
|
|
@@ -317,7 +356,7 @@ async function promptUpgrade(latestVersion) {
|
|
|
317
356
|
}
|
|
318
357
|
catch {
|
|
319
358
|
spinner.fail('Upgrade failed');
|
|
320
|
-
console.log(chalk.gray('Run manually:
|
|
359
|
+
console.log(chalk.gray('Run manually: agents upgrade --yes'));
|
|
321
360
|
}
|
|
322
361
|
console.log();
|
|
323
362
|
}
|
|
@@ -493,6 +532,7 @@ registerMcpCommands(program);
|
|
|
493
532
|
registerSubagentsCommands(program);
|
|
494
533
|
registerPluginsCommands(program);
|
|
495
534
|
registerWorkflowsCommands(program);
|
|
535
|
+
registerWorktreeCommands(program);
|
|
496
536
|
registerVersionsCommands(program);
|
|
497
537
|
registerImportCommand(program);
|
|
498
538
|
registerPackagesCommands(program);
|
|
@@ -525,6 +565,7 @@ registerUsageCommand(program);
|
|
|
525
565
|
registerAliasCommand(program);
|
|
526
566
|
registerPtyCommands(program);
|
|
527
567
|
registerBrowserCommand(program);
|
|
568
|
+
registerComputerCommand(program);
|
|
528
569
|
// Deprecated 'jobs' and 'cron' aliases for 'routines'
|
|
529
570
|
for (const alias of ['jobs', 'cron']) {
|
|
530
571
|
program
|
|
@@ -541,18 +582,12 @@ for (const alias of ['jobs', 'cron']) {
|
|
|
541
582
|
program
|
|
542
583
|
.command('upgrade')
|
|
543
584
|
.description('Upgrade agents-cli to the latest version')
|
|
544
|
-
.
|
|
545
|
-
|
|
585
|
+
.option('-y, --yes', 'Install without an interactive confirmation prompt')
|
|
586
|
+
.action(async (options) => {
|
|
587
|
+
let spinner = ora('Checking for updates...').start();
|
|
546
588
|
try {
|
|
547
|
-
const
|
|
548
|
-
|
|
549
|
-
});
|
|
550
|
-
if (!response.ok) {
|
|
551
|
-
spinner.fail('Could not reach npm registry');
|
|
552
|
-
process.exit(1);
|
|
553
|
-
}
|
|
554
|
-
const data = (await response.json());
|
|
555
|
-
const latestVersion = data.version;
|
|
589
|
+
const metadata = await fetchLatestNpmPackageMetadata();
|
|
590
|
+
const latestVersion = metadata.version;
|
|
556
591
|
if (latestVersion === VERSION) {
|
|
557
592
|
spinner.succeed(`Already on latest version (${VERSION})`);
|
|
558
593
|
return;
|
|
@@ -561,17 +596,26 @@ program
|
|
|
561
596
|
spinner.succeed(`Already ahead of latest (${VERSION} >= ${latestVersion})`);
|
|
562
597
|
return;
|
|
563
598
|
}
|
|
564
|
-
spinner.
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
599
|
+
spinner.succeed(`Resolved ${NPM_PACKAGE_NAME}@${latestVersion}`);
|
|
600
|
+
printResolvedPackage(metadata);
|
|
601
|
+
if (isInteractiveTerminal() && !options.yes) {
|
|
602
|
+
const approved = await confirm({
|
|
603
|
+
message: `Install ${NPM_PACKAGE_NAME}@${latestVersion}?`,
|
|
604
|
+
default: false,
|
|
605
|
+
});
|
|
606
|
+
if (!approved) {
|
|
607
|
+
console.log(chalk.gray('Upgrade cancelled'));
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
spinner = ora(`Upgrading ${VERSION} -> ${latestVersion}...`).start();
|
|
612
|
+
await installResolvedPackage(metadata);
|
|
569
613
|
spinner.succeed(`Upgraded to ${latestVersion}`);
|
|
570
614
|
await showWhatsNew(VERSION, latestVersion);
|
|
571
615
|
}
|
|
572
616
|
catch (err) {
|
|
573
617
|
spinner.fail('Upgrade failed');
|
|
574
|
-
console.log(chalk.gray('Run manually:
|
|
618
|
+
console.log(chalk.gray('Run manually: agents upgrade --yes'));
|
|
575
619
|
}
|
|
576
620
|
});
|
|
577
621
|
registerPullCommand(program);
|
package/dist/lib/agents.d.ts
CHANGED
|
@@ -102,6 +102,7 @@ export declare function isMcpRegistered(agentId: AgentId, mcpName: string): Prom
|
|
|
102
102
|
export declare function registerMcp(agentId: AgentId, name: string, command: string, scope?: 'user' | 'project', transport?: string, options?: {
|
|
103
103
|
home?: string;
|
|
104
104
|
binary?: string;
|
|
105
|
+
headers?: Record<string, string>;
|
|
105
106
|
}): Promise<{
|
|
106
107
|
success: boolean;
|
|
107
108
|
error?: string;
|
|
@@ -128,7 +129,9 @@ export interface McpTargetOperationResult {
|
|
|
128
129
|
export declare function registerMcpToTargets(targets: {
|
|
129
130
|
directAgents: AgentId[];
|
|
130
131
|
versionSelections: Map<AgentId, string[]>;
|
|
131
|
-
}, name: string, command: string, scope?: 'user' | 'project', transport?: string
|
|
132
|
+
}, name: string, command: string, scope?: 'user' | 'project', transport?: string, options?: {
|
|
133
|
+
headers?: Record<string, string>;
|
|
134
|
+
}): Promise<McpTargetOperationResult[]>;
|
|
132
135
|
/**
|
|
133
136
|
* Unregister an MCP server from multiple agent targets, including both direct
|
|
134
137
|
* agents and specific version-managed installs.
|