@phnx-labs/agents-cli 1.14.2 → 1.14.4
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/README.md +17 -7
- package/dist/browser.d.ts +2 -0
- package/dist/browser.js +7 -0
- package/dist/commands/browser.d.ts +3 -0
- package/dist/commands/browser.js +392 -0
- package/dist/commands/daemon.js +1 -1
- package/dist/commands/doctor.d.ts +16 -9
- package/dist/commands/doctor.js +248 -12
- package/dist/commands/prune.js +9 -3
- package/dist/commands/refresh-rules.d.ts +15 -0
- package/dist/commands/{refresh-memory.js → refresh-rules.js} +14 -14
- package/dist/commands/routines.js +1 -1
- package/dist/commands/rules.js +100 -4
- package/dist/commands/secrets.js +198 -11
- package/dist/commands/sync.js +19 -0
- package/dist/commands/teams.js +184 -22
- package/dist/commands/trash.d.ts +10 -0
- package/dist/commands/trash.js +187 -0
- package/dist/commands/view.js +47 -14
- package/dist/index.js +62 -4
- package/dist/lib/agents.js +2 -2
- package/dist/lib/browser/cdp.d.ts +24 -0
- package/dist/lib/browser/cdp.js +94 -0
- package/dist/lib/browser/chrome.d.ts +16 -0
- package/dist/lib/browser/chrome.js +157 -0
- package/dist/lib/browser/drivers/local.d.ts +8 -0
- package/dist/lib/browser/drivers/local.js +22 -0
- package/dist/lib/browser/drivers/ssh.d.ts +9 -0
- package/dist/lib/browser/drivers/ssh.js +129 -0
- package/dist/lib/browser/index.d.ts +5 -0
- package/dist/lib/browser/index.js +5 -0
- package/dist/lib/browser/input.d.ts +6 -0
- package/dist/lib/browser/input.js +52 -0
- package/dist/lib/browser/ipc.d.ts +12 -0
- package/dist/lib/browser/ipc.js +223 -0
- package/dist/lib/browser/profiles.d.ts +11 -0
- package/dist/lib/browser/profiles.js +61 -0
- package/dist/lib/browser/refs.d.ts +21 -0
- package/dist/lib/browser/refs.js +88 -0
- package/dist/lib/browser/service.d.ts +45 -0
- package/dist/lib/browser/service.js +404 -0
- package/dist/lib/browser/types.d.ts +73 -0
- package/dist/lib/browser/types.js +7 -0
- package/dist/lib/cloud/codex.js +1 -1
- package/dist/lib/cloud/registry.js +2 -2
- package/dist/lib/cloud/rush.js +2 -2
- package/dist/lib/cloud/store.js +2 -2
- package/dist/lib/daemon.d.ts +1 -1
- package/dist/lib/daemon.js +47 -11
- package/dist/lib/diff-text.d.ts +25 -0
- package/dist/lib/diff-text.js +47 -0
- package/dist/lib/doctor-diff.d.ts +64 -0
- package/dist/lib/doctor-diff.js +497 -0
- package/dist/lib/git.js +3 -3
- package/dist/lib/hooks.d.ts +6 -0
- package/dist/lib/hooks.js +6 -1
- package/dist/lib/migrate.js +123 -0
- package/dist/lib/pty-client.js +3 -3
- package/dist/lib/pty-server.js +36 -7
- package/dist/lib/resources/commands.d.ts +46 -0
- package/dist/lib/resources/commands.js +208 -0
- package/dist/lib/resources/hooks.d.ts +12 -0
- package/dist/lib/resources/hooks.js +136 -0
- package/dist/lib/resources/index.d.ts +36 -0
- package/dist/lib/resources/index.js +69 -0
- package/dist/lib/resources/mcp.d.ts +34 -0
- package/dist/lib/resources/mcp.js +483 -0
- package/dist/lib/resources/permissions.d.ts +13 -0
- package/dist/lib/resources/permissions.js +184 -0
- package/dist/lib/resources/rules.d.ts +43 -0
- package/dist/lib/resources/rules.js +146 -0
- package/dist/lib/resources/skills.d.ts +37 -0
- package/dist/lib/resources/skills.js +238 -0
- package/dist/lib/resources/subagents.d.ts +46 -0
- package/dist/lib/resources/subagents.js +198 -0
- package/dist/lib/resources/types.d.ts +82 -0
- package/dist/lib/resources/types.js +8 -0
- package/dist/lib/resources.js +1 -1
- package/dist/lib/rotate.d.ts +8 -1
- package/dist/lib/rotate.js +17 -4
- package/dist/lib/rules/compile.d.ts +104 -0
- package/dist/lib/{memory-compile.js → rules/compile.js} +160 -21
- package/dist/lib/rules/compose.d.ts +78 -0
- package/dist/lib/rules/compose.js +170 -0
- package/dist/lib/{memory.d.ts → rules/rules.d.ts} +5 -5
- package/dist/lib/{memory.js → rules/rules.js} +10 -10
- package/dist/lib/secrets/AgentsKeychain.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/AgentsKeychain.app/Contents/MacOS/AgentsKeychain +0 -0
- package/dist/lib/secrets/bundles.d.ts +61 -4
- package/dist/lib/secrets/bundles.js +222 -54
- package/dist/lib/secrets/index.d.ts +24 -5
- package/dist/lib/secrets/index.js +70 -41
- package/dist/lib/session/active.js +5 -5
- package/dist/lib/session/db.js +4 -4
- package/dist/lib/session/discover.js +2 -2
- package/dist/lib/session/render.js +21 -7
- package/dist/lib/shims.d.ts +28 -4
- package/dist/lib/shims.js +72 -14
- package/dist/lib/state.d.ts +22 -28
- package/dist/lib/state.js +83 -78
- package/dist/lib/sync-manifest.d.ts +2 -2
- package/dist/lib/sync-manifest.js +5 -5
- package/dist/lib/teams/agents.d.ts +4 -2
- package/dist/lib/teams/agents.js +11 -4
- package/dist/lib/teams/api.d.ts +1 -1
- package/dist/lib/teams/api.js +2 -2
- package/dist/lib/teams/index.d.ts +1 -0
- package/dist/lib/teams/index.js +1 -0
- package/dist/lib/teams/persistence.js +3 -3
- package/dist/lib/teams/registry.d.ts +12 -1
- package/dist/lib/teams/registry.js +12 -2
- package/dist/lib/teams/worktree.d.ts +30 -0
- package/dist/lib/teams/worktree.js +96 -0
- package/dist/lib/types.d.ts +12 -6
- package/dist/lib/types.js +3 -3
- package/dist/lib/versions.d.ts +32 -3
- package/dist/lib/versions.js +147 -119
- package/package.json +3 -2
- package/scripts/postinstall.js +29 -0
- package/dist/commands/refresh-memory.d.ts +0 -15
- package/dist/lib/memory-compile.d.ts +0 -66
package/dist/commands/teams.js
CHANGED
|
@@ -5,7 +5,8 @@ import { AgentManager, checkAllClis, getAgentsDir, VALID_TASK_TYPES, } from '../
|
|
|
5
5
|
import { resolveProvider } from '../lib/cloud/registry.js';
|
|
6
6
|
import { runSupervisor } from '../lib/teams/supervisor.js';
|
|
7
7
|
import { handleSpawn, handleStatus, handleStop, handleTasks, } from '../lib/teams/api.js';
|
|
8
|
-
import { createTeam, ensureTeam, loadTeams, removeTeam, teamExists, } from '../lib/teams/registry.js';
|
|
8
|
+
import { createTeam, ensureTeam, getTeam, loadTeams, removeTeam, teamExists, } from '../lib/teams/registry.js';
|
|
9
|
+
import { createWorktree, isGitRepo, hasUncommittedChanges, removeWorktree, } from '../lib/teams/worktree.js';
|
|
9
10
|
import { isVersionInstalled, resolveVersionAlias, resolveVersionAliasLoose } from '../lib/versions.js';
|
|
10
11
|
import { discoverSessions, parseTimeFilter, resolveSessionById } from '../lib/session/discover.js';
|
|
11
12
|
import { buildPreview as buildSessionPreview } from './sessions-picker.js';
|
|
@@ -542,7 +543,8 @@ Examples:
|
|
|
542
543
|
# Pull the live log of one teammate
|
|
543
544
|
agents teams logs frontend
|
|
544
545
|
|
|
545
|
-
# A teammate is stuck —
|
|
546
|
+
# A teammate is stuck — stop them, then remove if needed
|
|
547
|
+
agents teams stop pricing-page frontend
|
|
546
548
|
agents teams remove pricing-page frontend
|
|
547
549
|
|
|
548
550
|
# Ship done — wind everyone down
|
|
@@ -676,10 +678,16 @@ Name teammates with --name alice to refer to them as 'alice' instead of a UUID.
|
|
|
676
678
|
.aliases(['c', 'new'])
|
|
677
679
|
.description('Start a new team. No teammates yet; add them with `teams add`.')
|
|
678
680
|
.option('-d, --description <text>', 'One-line summary of what this team is working on')
|
|
681
|
+
.option('--enable-worktrees', 'Each teammate works in its own git worktree (requires --worktree on add)')
|
|
682
|
+
.option('--use-worktree <path>', 'All teammates share this existing worktree path (mutually exclusive with --enable-worktrees)')
|
|
679
683
|
.option('--json', 'Output machine-readable JSON')
|
|
680
684
|
.action(async (team, opts) => {
|
|
681
685
|
try {
|
|
682
|
-
const meta = await createTeam(team,
|
|
686
|
+
const meta = await createTeam(team, {
|
|
687
|
+
description: opts.description,
|
|
688
|
+
enableWorktrees: opts.enableWorktrees,
|
|
689
|
+
useWorktree: opts.useWorktree,
|
|
690
|
+
});
|
|
683
691
|
if (isJsonMode(opts)) {
|
|
684
692
|
console.log(JSON.stringify({ team, ...meta }, null, 2));
|
|
685
693
|
return;
|
|
@@ -687,9 +695,18 @@ Name teammates with --name alice to refer to them as 'alice' instead of a UUID.
|
|
|
687
695
|
console.log(chalk.green(`New team: ${chalk.cyan(team)}`));
|
|
688
696
|
if (meta.description)
|
|
689
697
|
console.log(chalk.gray(` ${meta.description}`));
|
|
698
|
+
if (meta.enable_worktrees)
|
|
699
|
+
console.log(chalk.gray(` worktrees: per-teammate`));
|
|
700
|
+
if (meta.use_worktree)
|
|
701
|
+
console.log(chalk.gray(` worktree: ${meta.use_worktree}`));
|
|
690
702
|
console.log();
|
|
691
703
|
console.log(chalk.gray('Add your first teammate:'));
|
|
692
|
-
|
|
704
|
+
if (meta.enable_worktrees) {
|
|
705
|
+
console.log(chalk.gray(` agents teams add ${team} claude "your task here" --name alice --worktree feature-name`));
|
|
706
|
+
}
|
|
707
|
+
else {
|
|
708
|
+
console.log(chalk.gray(` agents teams add ${team} claude "your task here"`));
|
|
709
|
+
}
|
|
693
710
|
}
|
|
694
711
|
catch (err) {
|
|
695
712
|
die(err.message);
|
|
@@ -706,6 +723,7 @@ Name teammates with --name alice to refer to them as 'alice' instead of a UUID.
|
|
|
706
723
|
.option('--model <model>', 'Override the effort tier and use this specific model (e.g. claude-opus-4-6)')
|
|
707
724
|
.option('--env <key=value>', 'Set an environment variable for this teammate (repeatable for multiple vars)', (val, prev) => [...prev, val], [])
|
|
708
725
|
.option('--cwd <dir>', 'Working directory for this teammate (default: current directory)')
|
|
726
|
+
.option('--worktree <name>', 'Run this teammate in a dedicated git worktree (required when team has --enable-worktrees)')
|
|
709
727
|
.option('--after <names>', "DAG dependencies: comma-separated teammate names to wait for. Stages as PENDING; run 'teams start' to launch when ready.")
|
|
710
728
|
.option('--task-type <type>', `Factory label: ${VALID_TASK_TYPES.join('|')}. Drives planner fan-out + test-oracle bugfix loop.`)
|
|
711
729
|
.option('--cloud <provider>', `Dispatch to cloud backend instead of local CLI: ${VALID_CLOUD_PROVIDERS.join('|')}`)
|
|
@@ -762,7 +780,52 @@ Name teammates with --name alice to refer to them as 'alice' instead of a UUID.
|
|
|
762
780
|
}
|
|
763
781
|
// Auto-create the team if it doesn't exist yet (friendlier UX than erroring).
|
|
764
782
|
await ensureTeam(team);
|
|
765
|
-
|
|
783
|
+
// Check if team has worktrees enabled or a shared worktree
|
|
784
|
+
const teamMeta = await getTeam(team);
|
|
785
|
+
const worktreesEnabled = teamMeta?.enable_worktrees ?? false;
|
|
786
|
+
const sharedWorktree = teamMeta?.use_worktree ?? null;
|
|
787
|
+
let worktreeName = null;
|
|
788
|
+
let worktreePath = null;
|
|
789
|
+
if (sharedWorktree) {
|
|
790
|
+
// Team uses a shared worktree for all teammates
|
|
791
|
+
const fsp = await import('fs/promises');
|
|
792
|
+
try {
|
|
793
|
+
const stat = await fsp.stat(sharedWorktree);
|
|
794
|
+
if (!stat.isDirectory()) {
|
|
795
|
+
die(`Shared worktree path is not a directory: ${sharedWorktree}`);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
catch {
|
|
799
|
+
die(`Shared worktree path does not exist: ${sharedWorktree}`);
|
|
800
|
+
}
|
|
801
|
+
worktreePath = sharedWorktree;
|
|
802
|
+
if (opts.worktree) {
|
|
803
|
+
die(`Team '${team}' uses --use-worktree (shared). Don't pass --worktree on add.`);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
else if (worktreesEnabled) {
|
|
807
|
+
if (!opts.worktree) {
|
|
808
|
+
die(`Team '${team}' has worktrees enabled. Use --worktree <name> to specify a worktree name.`);
|
|
809
|
+
}
|
|
810
|
+
if (!opts.name) {
|
|
811
|
+
die(`Team '${team}' has worktrees enabled. Use --name <name> to identify this teammate.`);
|
|
812
|
+
}
|
|
813
|
+
const baseCwd = opts.cwd ?? process.cwd();
|
|
814
|
+
if (!(await isGitRepo(baseCwd))) {
|
|
815
|
+
die(`Worktrees require a git repository. ${baseCwd} is not inside a git repo.`);
|
|
816
|
+
}
|
|
817
|
+
try {
|
|
818
|
+
worktreeName = opts.worktree;
|
|
819
|
+
worktreePath = await createWorktree(baseCwd, worktreeName);
|
|
820
|
+
}
|
|
821
|
+
catch (err) {
|
|
822
|
+
die(`Failed to create worktree '${opts.worktree}': ${err.message}`);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
else if (opts.worktree) {
|
|
826
|
+
die(`--worktree requires --enable-worktrees on the team. Recreate the team with: agents teams create ${team} --enable-worktrees`);
|
|
827
|
+
}
|
|
828
|
+
const cwd = worktreePath ?? opts.cwd ?? process.cwd();
|
|
766
829
|
const mgr = mkManager();
|
|
767
830
|
// Factory teammates: prepend the worker-skill preamble to every task
|
|
768
831
|
// prompt so implementers/testers/reviewers know about the Ledger, the
|
|
@@ -811,7 +874,7 @@ Name teammates with --name alice to refer to them as 'alice' instead of a UUID.
|
|
|
811
874
|
}
|
|
812
875
|
}
|
|
813
876
|
try {
|
|
814
|
-
const result = await handleSpawn(mgr, team, agent, effectiveTask, cwd, opts.mode, opts.effort, null, cwd, version, opts.name ?? null, after, opts.model ?? null, envOverrides ?? null, taskType, cloudProviderId, cloudSessionId, opts.repo ?? null, opts.branch ?? null);
|
|
877
|
+
const result = await handleSpawn(mgr, team, agent, effectiveTask, cwd, opts.mode, opts.effort, null, cwd, version, opts.name ?? null, after, opts.model ?? null, envOverrides ?? null, taskType, cloudProviderId, cloudSessionId, opts.repo ?? null, opts.branch ?? null, worktreeName, worktreePath);
|
|
815
878
|
if (isJsonMode(opts)) {
|
|
816
879
|
console.log(JSON.stringify(result, null, 2));
|
|
817
880
|
return;
|
|
@@ -830,6 +893,9 @@ Name teammates with --name alice to refer to them as 'alice' instead of a UUID.
|
|
|
830
893
|
console.log(` ${chalk.gray('status ')} ${statusColor(result.status)(result.status)}`);
|
|
831
894
|
console.log(` ${chalk.gray('mode ')} ${opts.mode}`);
|
|
832
895
|
console.log(` ${chalk.gray('working ')} ${cwd}`);
|
|
896
|
+
if (worktreeName) {
|
|
897
|
+
console.log(` ${chalk.gray('worktree')} ${chalk.cyan(worktreeName)}`);
|
|
898
|
+
}
|
|
833
899
|
if (result.task_type) {
|
|
834
900
|
console.log(` ${chalk.gray('task ')} ${chalk.cyan(result.task_type)}`);
|
|
835
901
|
}
|
|
@@ -988,11 +1054,90 @@ Name teammates with --name alice to refer to them as 'alice' instead of a UUID.
|
|
|
988
1054
|
console.error(chalk.yellow(`Stopped by signal after ${result.waves} waves.`));
|
|
989
1055
|
}
|
|
990
1056
|
});
|
|
1057
|
+
// stop
|
|
1058
|
+
teams
|
|
1059
|
+
.command('stop [team] [teammate]')
|
|
1060
|
+
.description('Stop a running teammate. Can be restarted later. Cleans up worktree if no uncommitted changes.')
|
|
1061
|
+
.option('--json', 'Output machine-readable JSON')
|
|
1062
|
+
.action(async (team, ref, opts) => {
|
|
1063
|
+
const mgr = mkManager();
|
|
1064
|
+
if (!team) {
|
|
1065
|
+
const { names } = await loadTeamRows(mgr);
|
|
1066
|
+
requireDestructiveArg({
|
|
1067
|
+
argName: 'team',
|
|
1068
|
+
command: 'agents teams stop',
|
|
1069
|
+
itemNoun: 'team',
|
|
1070
|
+
available: names,
|
|
1071
|
+
emptyHint: "You don't have any teams yet.",
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
if (!ref) {
|
|
1075
|
+
const roster = await mgr.listByTask(team);
|
|
1076
|
+
const running = roster.filter((a) => a.status === 'running');
|
|
1077
|
+
requireDestructiveArg({
|
|
1078
|
+
argName: 'teammate',
|
|
1079
|
+
command: `agents teams stop ${team}`,
|
|
1080
|
+
itemNoun: 'teammate',
|
|
1081
|
+
available: running.map((a) => a.name || shortId(a.agentId)),
|
|
1082
|
+
emptyHint: `Team ${team} has no running teammates.`,
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
const lookup = await mgr.resolveAgentIdInTask(team, ref);
|
|
1086
|
+
if (lookup.kind === 'none') {
|
|
1087
|
+
die(`No teammate matching '${ref}' in team ${team}`, 2);
|
|
1088
|
+
}
|
|
1089
|
+
if (lookup.kind === 'ambiguous') {
|
|
1090
|
+
const shorts = lookup.matches.map(shortId).join(', ');
|
|
1091
|
+
die(`'${ref}' matches multiple teammates: ${shorts}. Use more characters or a name.`, 2);
|
|
1092
|
+
}
|
|
1093
|
+
const agentId = lookup.agentId;
|
|
1094
|
+
const agent = await mgr.get(agentId);
|
|
1095
|
+
const display = agent?.name || shortId(agentId);
|
|
1096
|
+
const stopRes = await handleStop(mgr, team, agentId);
|
|
1097
|
+
if ('error' in stopRes)
|
|
1098
|
+
die(stopRes.error);
|
|
1099
|
+
// Clean up worktree if this teammate had one
|
|
1100
|
+
let worktreeKept = false;
|
|
1101
|
+
if (agent?.worktreeName && agent?.worktreePath) {
|
|
1102
|
+
try {
|
|
1103
|
+
const dirty = await hasUncommittedChanges(agent.worktreePath);
|
|
1104
|
+
if (dirty) {
|
|
1105
|
+
worktreeKept = true;
|
|
1106
|
+
}
|
|
1107
|
+
else {
|
|
1108
|
+
const baseCwd = process.cwd();
|
|
1109
|
+
await removeWorktree(baseCwd, agent.worktreeName);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
catch {
|
|
1113
|
+
// best-effort cleanup
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
if (isJsonMode(opts)) {
|
|
1117
|
+
console.log(JSON.stringify({
|
|
1118
|
+
team,
|
|
1119
|
+
agent_id: agentId,
|
|
1120
|
+
name: agent?.name ?? null,
|
|
1121
|
+
stopped: stopRes.stopped.length > 0,
|
|
1122
|
+
worktree_kept: worktreeKept,
|
|
1123
|
+
}, null, 2));
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
if (stopRes.stopped.length) {
|
|
1127
|
+
console.log(chalk.green(`Stopped ${chalk.cyan(display)} in team ${chalk.cyan(team)}.`));
|
|
1128
|
+
}
|
|
1129
|
+
else if (stopRes.already_stopped.length) {
|
|
1130
|
+
console.log(chalk.gray(`${display} was already stopped.`));
|
|
1131
|
+
}
|
|
1132
|
+
if (worktreeKept && agent?.worktreeName) {
|
|
1133
|
+
console.log(chalk.yellow(`Worktree '${agent.worktreeName}' has uncommitted changes. Keeping it at: ${agent.worktreePath}`));
|
|
1134
|
+
}
|
|
1135
|
+
});
|
|
991
1136
|
// remove
|
|
992
1137
|
teams
|
|
993
1138
|
.command('remove [team] [teammate]')
|
|
994
1139
|
.alias('rm')
|
|
995
|
-
.description("Remove a teammate
|
|
1140
|
+
.description("Remove a stopped teammate's logs and metadata. Use 'stop' first to end a running teammate.")
|
|
996
1141
|
.option('--keep-logs', 'Keep their log files on disk (default: delete them)')
|
|
997
1142
|
.option('--json', 'Output machine-readable JSON')
|
|
998
1143
|
.action(async (team, ref, opts) => {
|
|
@@ -1009,12 +1154,13 @@ Name teammates with --name alice to refer to them as 'alice' instead of a UUID.
|
|
|
1009
1154
|
}
|
|
1010
1155
|
if (!ref) {
|
|
1011
1156
|
const roster = await mgr.listByTask(team);
|
|
1157
|
+
const stopped = roster.filter((a) => a.status !== 'running' && a.status !== 'pending');
|
|
1012
1158
|
requireDestructiveArg({
|
|
1013
1159
|
argName: 'teammate',
|
|
1014
1160
|
command: `agents teams remove ${team}`,
|
|
1015
|
-
itemNoun: 'teammate',
|
|
1016
|
-
available:
|
|
1017
|
-
emptyHint: `Team ${team} has no teammates.`,
|
|
1161
|
+
itemNoun: 'stopped teammate',
|
|
1162
|
+
available: stopped.map((a) => a.name || shortId(a.agentId)),
|
|
1163
|
+
emptyHint: `Team ${team} has no stopped teammates. Use 'agents teams stop' first.`,
|
|
1018
1164
|
});
|
|
1019
1165
|
}
|
|
1020
1166
|
const lookup = await mgr.resolveAgentIdInTask(team, ref);
|
|
@@ -1026,13 +1172,13 @@ Name teammates with --name alice to refer to them as 'alice' instead of a UUID.
|
|
|
1026
1172
|
die(`'${ref}' matches multiple teammates: ${shorts}. Use more characters or a name.`, 2);
|
|
1027
1173
|
}
|
|
1028
1174
|
const agentId = lookup.agentId;
|
|
1029
|
-
// Look up the display handle (name if they had one) before we tear down.
|
|
1030
1175
|
const agent = await mgr.get(agentId);
|
|
1031
1176
|
const display = agent?.name || shortId(agentId);
|
|
1032
|
-
|
|
1033
|
-
if ('
|
|
1034
|
-
die(
|
|
1035
|
-
|
|
1177
|
+
// Require agent to be stopped first
|
|
1178
|
+
if (agent?.status === 'running' || agent?.status === 'pending') {
|
|
1179
|
+
die(`Teammate '${display}' is still ${agent.status}. Run 'agents teams stop ${team} ${display}' first.`);
|
|
1180
|
+
}
|
|
1181
|
+
if (!opts.keepLogs) {
|
|
1036
1182
|
try {
|
|
1037
1183
|
const dir = path.join(await getAgentsDir(), agentId);
|
|
1038
1184
|
await fs.rm(dir, { recursive: true, force: true });
|
|
@@ -1042,15 +1188,10 @@ Name teammates with --name alice to refer to them as 'alice' instead of a UUID.
|
|
|
1042
1188
|
}
|
|
1043
1189
|
}
|
|
1044
1190
|
if (isJsonMode(opts)) {
|
|
1045
|
-
console.log(JSON.stringify({ team, agent_id: agentId, name: agent?.name ?? null,
|
|
1191
|
+
console.log(JSON.stringify({ team, agent_id: agentId, name: agent?.name ?? null, removed: true }, null, 2));
|
|
1046
1192
|
return;
|
|
1047
1193
|
}
|
|
1048
|
-
|
|
1049
|
-
console.log(chalk.green(`${display} has left team ${chalk.cyan(team)} (was working, now stopped).`));
|
|
1050
|
-
}
|
|
1051
|
-
else {
|
|
1052
|
-
console.log(chalk.green(`${display} has left team ${chalk.cyan(team)}.`));
|
|
1053
|
-
}
|
|
1194
|
+
console.log(chalk.green(`Removed ${chalk.cyan(display)} from team ${chalk.cyan(team)}.`));
|
|
1054
1195
|
});
|
|
1055
1196
|
// disband
|
|
1056
1197
|
teams
|
|
@@ -1075,6 +1216,24 @@ Name teammates with --name alice to refer to them as 'alice' instead of a UUID.
|
|
|
1075
1216
|
if ('error' in stopRes)
|
|
1076
1217
|
die(stopRes.error);
|
|
1077
1218
|
const status = await handleStatus(mgr, team, 'all');
|
|
1219
|
+
// Clean up worktrees for all teammates
|
|
1220
|
+
const baseCwd = process.cwd();
|
|
1221
|
+
const keptWorktrees = [];
|
|
1222
|
+
for (const a of status.agents) {
|
|
1223
|
+
const agent = await mgr.get(a.agent_id);
|
|
1224
|
+
if (agent?.worktreeName && agent?.worktreePath) {
|
|
1225
|
+
try {
|
|
1226
|
+
const dirty = await hasUncommittedChanges(agent.worktreePath);
|
|
1227
|
+
if (dirty) {
|
|
1228
|
+
keptWorktrees.push(agent.worktreeName);
|
|
1229
|
+
}
|
|
1230
|
+
else {
|
|
1231
|
+
await removeWorktree(baseCwd, agent.worktreeName);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
catch { /* best-effort */ }
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1078
1237
|
const removedIds = [];
|
|
1079
1238
|
if (!opts.keepLogs) {
|
|
1080
1239
|
const base = await getAgentsDir();
|
|
@@ -1099,6 +1258,9 @@ Name teammates with --name alice to refer to them as 'alice' instead of a UUID.
|
|
|
1099
1258
|
console.log(chalk.gray(` Stopped ${stopRes.stopped.length} working teammate(s).`));
|
|
1100
1259
|
if (removedIds.length)
|
|
1101
1260
|
console.log(chalk.gray(` Cleared ${removedIds.length} teammate log(s).`));
|
|
1261
|
+
if (keptWorktrees.length) {
|
|
1262
|
+
console.log(chalk.yellow(` Kept ${keptWorktrees.length} worktree(s) with uncommitted changes: ${keptWorktrees.join(', ')}`));
|
|
1263
|
+
}
|
|
1102
1264
|
});
|
|
1103
1265
|
// logs
|
|
1104
1266
|
teams
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents trash` — list and restore soft-deleted version directories.
|
|
3
|
+
*
|
|
4
|
+
* `removeVersion` moves a version dir to ~/.agents-system/trash/versions/<agent>/<version>/<timestamp>/
|
|
5
|
+
* instead of hard-deleting. These commands let the user inspect what's there
|
|
6
|
+
* and put a soft-deleted version back. The trash never auto-expires; only
|
|
7
|
+
* `rm -rf ~/.agents-system/trash/` removes bytes from disk.
|
|
8
|
+
*/
|
|
9
|
+
import type { Command } from 'commander';
|
|
10
|
+
export declare function registerTrashCommands(program: Command): void;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { resolveAgentName, agentLabel } from '../lib/agents.js';
|
|
5
|
+
import { getTrashVersionsDir } from '../lib/state.js';
|
|
6
|
+
import { getVersionDir } from '../lib/versions.js';
|
|
7
|
+
function listTrashEntries(filterAgent) {
|
|
8
|
+
const root = getTrashVersionsDir();
|
|
9
|
+
if (!fs.existsSync(root))
|
|
10
|
+
return [];
|
|
11
|
+
const out = [];
|
|
12
|
+
let agentDirs;
|
|
13
|
+
try {
|
|
14
|
+
agentDirs = fs.readdirSync(root, { withFileTypes: true });
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
for (const agentEntry of agentDirs) {
|
|
20
|
+
if (!agentEntry.isDirectory())
|
|
21
|
+
continue;
|
|
22
|
+
const agent = resolveAgentName(agentEntry.name);
|
|
23
|
+
if (!agent)
|
|
24
|
+
continue;
|
|
25
|
+
if (filterAgent && agent !== filterAgent)
|
|
26
|
+
continue;
|
|
27
|
+
const agentDir = path.join(root, agentEntry.name);
|
|
28
|
+
let versionDirs;
|
|
29
|
+
try {
|
|
30
|
+
versionDirs = fs.readdirSync(agentDir, { withFileTypes: true });
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
for (const ver of versionDirs) {
|
|
36
|
+
if (!ver.isDirectory())
|
|
37
|
+
continue;
|
|
38
|
+
const versionDir = path.join(agentDir, ver.name);
|
|
39
|
+
let stampDirs;
|
|
40
|
+
try {
|
|
41
|
+
stampDirs = fs.readdirSync(versionDir, { withFileTypes: true });
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
for (const stamp of stampDirs) {
|
|
47
|
+
if (!stamp.isDirectory())
|
|
48
|
+
continue;
|
|
49
|
+
out.push({
|
|
50
|
+
agent,
|
|
51
|
+
version: ver.name,
|
|
52
|
+
stamp: stamp.name,
|
|
53
|
+
trashPath: path.join(versionDir, stamp.name),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return out.sort((a, b) => a.stamp.localeCompare(b.stamp)).reverse();
|
|
59
|
+
}
|
|
60
|
+
function pickLatest(entries, agent, version) {
|
|
61
|
+
const matches = entries.filter((e) => e.agent === agent && e.version === version);
|
|
62
|
+
if (matches.length === 0)
|
|
63
|
+
return null;
|
|
64
|
+
return matches[0];
|
|
65
|
+
}
|
|
66
|
+
function parseAgentVersion(target) {
|
|
67
|
+
const at = target.indexOf('@');
|
|
68
|
+
if (at < 1 || at === target.length - 1)
|
|
69
|
+
return null;
|
|
70
|
+
const agent = resolveAgentName(target.slice(0, at));
|
|
71
|
+
if (!agent)
|
|
72
|
+
return null;
|
|
73
|
+
return { agent, version: target.slice(at + 1) };
|
|
74
|
+
}
|
|
75
|
+
function dirSizeBytes(dir) {
|
|
76
|
+
let total = 0;
|
|
77
|
+
const stack = [dir];
|
|
78
|
+
while (stack.length > 0) {
|
|
79
|
+
const cur = stack.pop();
|
|
80
|
+
let stat;
|
|
81
|
+
try {
|
|
82
|
+
stat = fs.lstatSync(cur);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (stat.isDirectory()) {
|
|
88
|
+
let entries;
|
|
89
|
+
try {
|
|
90
|
+
entries = fs.readdirSync(cur);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
for (const e of entries)
|
|
96
|
+
stack.push(path.join(cur, e));
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
total += stat.size;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return total;
|
|
103
|
+
}
|
|
104
|
+
function humanSize(bytes) {
|
|
105
|
+
if (bytes < 1024)
|
|
106
|
+
return `${bytes} B`;
|
|
107
|
+
if (bytes < 1024 * 1024)
|
|
108
|
+
return `${(bytes / 1024).toFixed(0)} KB`;
|
|
109
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
110
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
111
|
+
return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`;
|
|
112
|
+
}
|
|
113
|
+
export function registerTrashCommands(program) {
|
|
114
|
+
const trash = program
|
|
115
|
+
.command('trash')
|
|
116
|
+
.description('Inspect and restore soft-deleted agent version directories');
|
|
117
|
+
trash
|
|
118
|
+
.command('list [agent]')
|
|
119
|
+
.description('List soft-deleted version directories (optionally filtered to one agent)')
|
|
120
|
+
.action((agentArg) => {
|
|
121
|
+
let filter;
|
|
122
|
+
if (agentArg) {
|
|
123
|
+
const a = resolveAgentName(agentArg);
|
|
124
|
+
if (!a) {
|
|
125
|
+
console.error(chalk.red(`Unknown agent: ${agentArg}`));
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
filter = a;
|
|
129
|
+
}
|
|
130
|
+
const entries = listTrashEntries(filter);
|
|
131
|
+
if (entries.length === 0) {
|
|
132
|
+
console.log(chalk.gray('Trash is empty.'));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
console.log(chalk.bold(`Soft-deleted versions (${entries.length})`));
|
|
136
|
+
for (const e of entries) {
|
|
137
|
+
const size = humanSize(dirSizeBytes(e.trashPath));
|
|
138
|
+
console.log(` ${agentLabel(e.agent)}@${e.version} ` +
|
|
139
|
+
chalk.gray(`${e.stamp} ${size} ${e.trashPath}`));
|
|
140
|
+
}
|
|
141
|
+
console.log();
|
|
142
|
+
console.log(chalk.gray('Restore with: agents trash restore <agent>@<version>'));
|
|
143
|
+
});
|
|
144
|
+
trash
|
|
145
|
+
.command('restore <target>')
|
|
146
|
+
.description('Restore a soft-deleted version (e.g. "claude@2.1.110") back to ~/.agents-system/versions/')
|
|
147
|
+
.action((target) => {
|
|
148
|
+
const parsed = parseAgentVersion(target);
|
|
149
|
+
if (!parsed) {
|
|
150
|
+
console.error(chalk.red(`Expected <agent>@<version>, got: ${target}`));
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
const { agent, version } = parsed;
|
|
154
|
+
const entries = listTrashEntries(agent);
|
|
155
|
+
const entry = pickLatest(entries, agent, version);
|
|
156
|
+
if (!entry) {
|
|
157
|
+
console.error(chalk.red(`No trashed copy found for ${agent}@${version}`));
|
|
158
|
+
console.error(chalk.gray('Run `agents trash list` to see what exists.'));
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
const dest = getVersionDir(agent, version);
|
|
162
|
+
if (fs.existsSync(dest)) {
|
|
163
|
+
console.error(chalk.red(`Cannot restore: ${dest} already exists.`));
|
|
164
|
+
console.error(chalk.gray('Move or remove the existing dir first, then re-run restore.'));
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true, mode: 0o700 });
|
|
169
|
+
fs.renameSync(entry.trashPath, dest);
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
console.error(chalk.red(`Restore failed: ${err.message}`));
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
// Best-effort cleanup of empty stamp/version parents in trash.
|
|
176
|
+
try {
|
|
177
|
+
const verDir = path.dirname(entry.trashPath);
|
|
178
|
+
if (fs.readdirSync(verDir).length === 0)
|
|
179
|
+
fs.rmdirSync(verDir);
|
|
180
|
+
const agentDir = path.dirname(verDir);
|
|
181
|
+
if (fs.readdirSync(agentDir).length === 0)
|
|
182
|
+
fs.rmdirSync(agentDir);
|
|
183
|
+
}
|
|
184
|
+
catch { /* best-effort */ }
|
|
185
|
+
console.log(chalk.green(`Restored ${agentLabel(agent)}@${version} to ${dest}`));
|
|
186
|
+
});
|
|
187
|
+
}
|
package/dist/commands/view.js
CHANGED
|
@@ -6,12 +6,12 @@ import * as yaml from 'yaml';
|
|
|
6
6
|
import { AGENTS, ALL_AGENT_IDS, getAllCliStates, getAccountInfo, resolveAgentName, formatAgentError, agentLabel, colorAgent, } from '../lib/agents.js';
|
|
7
7
|
import { formatUsageSection, formatUsageSummary, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
|
|
8
8
|
import { readManifest } from '../lib/manifest.js';
|
|
9
|
-
import { listInstalledVersions, getGlobalDefault, getVersionHomePath, getVersionDir, resolveVersionAlias, getAvailableResources, getActuallySyncedResources, getNewResources, hasNewResources, promptNewResourceSelection, syncResourcesToVersion, removeVersion, } from '../lib/versions.js';
|
|
9
|
+
import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getVersionHomePath, getVersionDir, resolveVersionAlias, getAvailableResources, getActuallySyncedResources, getNewResources, hasNewResources, promptNewResourceSelection, syncResourcesToVersion, removeVersion, } from '../lib/versions.js';
|
|
10
10
|
import { getShimsDir, isShimsInPath, ensureVersionedAliasCurrent, removeShim, } from '../lib/shims.js';
|
|
11
11
|
import { getAgentResources } from '../lib/resources.js';
|
|
12
12
|
import { getAgentsDir, getUserAgentsDir, getPromptcutsPath } from '../lib/state.js';
|
|
13
13
|
import { isGitRepo, getGitSyncStatus } from '../lib/git.js';
|
|
14
|
-
import {
|
|
14
|
+
import { getCentralRulesFileName } from '../lib/rules/rules.js';
|
|
15
15
|
import { getConfiguredRunStrategy } from '../lib/rotate.js';
|
|
16
16
|
import { confirm } from '@inquirer/prompts';
|
|
17
17
|
import { formatPath, isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
@@ -336,7 +336,7 @@ async function showInstalledVersions(filterAgentId) {
|
|
|
336
336
|
const available = getAvailableResources();
|
|
337
337
|
const synced = getActuallySyncedResources(filterAgentId, defaultVersion);
|
|
338
338
|
const newResources = getNewResources(available, synced);
|
|
339
|
-
if (hasNewResources(newResources, filterAgentId)) {
|
|
339
|
+
if (hasNewResources(newResources, filterAgentId, defaultVersion)) {
|
|
340
340
|
try {
|
|
341
341
|
const selection = await promptNewResourceSelection(filterAgentId, newResources);
|
|
342
342
|
if (selection && Object.keys(selection).length > 0) {
|
|
@@ -426,7 +426,7 @@ async function showAgentResources(agentId, requestedVersion) {
|
|
|
426
426
|
}
|
|
427
427
|
else {
|
|
428
428
|
// Rules files: map agent-specific name (CLAUDE.md) back to canonical (AGENTS.md)
|
|
429
|
-
const centralName =
|
|
429
|
+
const centralName = getCentralRulesFileName(agentId);
|
|
430
430
|
relativePath = `rules/${centralName}`;
|
|
431
431
|
}
|
|
432
432
|
const matchesPath = (f) => f === relativePath || f.startsWith(relativePath + '/');
|
|
@@ -653,14 +653,21 @@ async function collectAgentsJson(filterAgentId) {
|
|
|
653
653
|
return out;
|
|
654
654
|
}
|
|
655
655
|
async function buildAgentPrunePlan(agentId) {
|
|
656
|
-
const
|
|
656
|
+
const dirInfos = listInstalledVersionDirs(agentId);
|
|
657
|
+
const entries = await Promise.all(dirInfos.map(async ({ version, hasBinary }) => {
|
|
657
658
|
const home = getVersionHomePath(agentId, version);
|
|
658
659
|
const info = await getAccountInfo(agentId, home);
|
|
659
|
-
return { version, info };
|
|
660
|
+
return { version, info, hasBinary };
|
|
660
661
|
}));
|
|
661
662
|
const globalDefault = getGlobalDefault(agentId);
|
|
663
|
+
const toPrune = [];
|
|
664
|
+
const skippedDefaults = [];
|
|
665
|
+
// Duplicate-account detection runs only over installs that actually have a
|
|
666
|
+
// working binary — those are the things that compete for "the live install
|
|
667
|
+
// for this account."
|
|
668
|
+
const installed = entries.filter((e) => e.hasBinary);
|
|
662
669
|
const byEmail = new Map();
|
|
663
|
-
for (const e of
|
|
670
|
+
for (const e of installed) {
|
|
664
671
|
if (!e.info.email)
|
|
665
672
|
continue;
|
|
666
673
|
const key = e.info.email.toLowerCase();
|
|
@@ -668,8 +675,6 @@ async function buildAgentPrunePlan(agentId) {
|
|
|
668
675
|
list.push(e);
|
|
669
676
|
byEmail.set(key, list);
|
|
670
677
|
}
|
|
671
|
-
const toPrune = [];
|
|
672
|
-
const skippedDefaults = [];
|
|
673
678
|
for (const [, group] of byEmail) {
|
|
674
679
|
if (group.length < 2)
|
|
675
680
|
continue;
|
|
@@ -682,6 +687,7 @@ async function buildAgentPrunePlan(agentId) {
|
|
|
682
687
|
email: older.info.email,
|
|
683
688
|
keeper,
|
|
684
689
|
isDefault: older.version === globalDefault,
|
|
690
|
+
reason: 'duplicate',
|
|
685
691
|
};
|
|
686
692
|
if (plan.isDefault)
|
|
687
693
|
skippedDefaults.push(plan);
|
|
@@ -689,6 +695,23 @@ async function buildAgentPrunePlan(agentId) {
|
|
|
689
695
|
toPrune.push(plan);
|
|
690
696
|
}
|
|
691
697
|
}
|
|
698
|
+
// Home-only leftovers: dirs without a binary. These are residue from a
|
|
699
|
+
// prior removeVersion before the soft-delete migration, plus any hand-edited
|
|
700
|
+
// installs. Surface them so the user can move them to trash.
|
|
701
|
+
for (const e of entries) {
|
|
702
|
+
if (e.hasBinary)
|
|
703
|
+
continue;
|
|
704
|
+
if (e.version === globalDefault)
|
|
705
|
+
continue; // never auto-suggest the default
|
|
706
|
+
toPrune.push({
|
|
707
|
+
agentId,
|
|
708
|
+
version: e.version,
|
|
709
|
+
email: e.info.email || '',
|
|
710
|
+
keeper: '',
|
|
711
|
+
isDefault: false,
|
|
712
|
+
reason: 'home-leftover',
|
|
713
|
+
});
|
|
714
|
+
}
|
|
692
715
|
return { agentId, toPrune, skippedDefaults };
|
|
693
716
|
}
|
|
694
717
|
async function executePrunePlan(plan) {
|
|
@@ -720,11 +743,17 @@ function printPrunePlan(plan, isFirst) {
|
|
|
720
743
|
}
|
|
721
744
|
if (plan.toPrune.length === 0)
|
|
722
745
|
return;
|
|
723
|
-
const heading = isFirst ? `Will
|
|
746
|
+
const heading = isFirst ? `Will move to trash for ${agentLabel(plan.agentId)}:` : `Also found candidates for ${agentLabel(plan.agentId)}:`;
|
|
724
747
|
console.log(chalk.bold(heading));
|
|
725
748
|
for (const p of plan.toPrune) {
|
|
726
|
-
|
|
727
|
-
|
|
749
|
+
if (p.reason === 'duplicate') {
|
|
750
|
+
console.log(` ${agentLabel(p.agentId)}@${p.version} ${chalk.cyan(p.email)} ` +
|
|
751
|
+
chalk.gray(`— duplicate, keeping ${p.agentId}@${p.keeper}`));
|
|
752
|
+
}
|
|
753
|
+
else {
|
|
754
|
+
console.log(` ${agentLabel(p.agentId)}@${p.version} ` +
|
|
755
|
+
chalk.gray(`— home-only leftover (no binary; transcripts preserved in trash)`));
|
|
756
|
+
}
|
|
728
757
|
}
|
|
729
758
|
console.log();
|
|
730
759
|
}
|
|
@@ -746,7 +775,7 @@ export async function pruneDuplicates(filterAgentId, yes, dryRun) {
|
|
|
746
775
|
spinner.stop();
|
|
747
776
|
const actionable = plans.filter((p) => p.toPrune.length > 0 || p.skippedDefaults.length > 0);
|
|
748
777
|
if (actionable.length === 0) {
|
|
749
|
-
console.log(chalk.gray('Nothing to prune — no
|
|
778
|
+
console.log(chalk.gray('Nothing to prune — no duplicate-account installs and no home-only leftovers.'));
|
|
750
779
|
return;
|
|
751
780
|
}
|
|
752
781
|
const totalCandidates = actionable.reduce((n, plan) => n + plan.toPrune.length, 0);
|
|
@@ -844,7 +873,11 @@ export async function viewAction(agentArg, options) {
|
|
|
844
873
|
console.log(chalk.red(formatAgentError(agentName)));
|
|
845
874
|
process.exit(1);
|
|
846
875
|
}
|
|
847
|
-
|
|
876
|
+
// Keep 'default' as-is since showAgentResources handles it; resolveVersionAlias
|
|
877
|
+
// returns undefined for 'default' which would skip the detailed view.
|
|
878
|
+
const requestedVersion = parts[1] === 'default'
|
|
879
|
+
? 'default'
|
|
880
|
+
: (resolveVersionAlias(agentId, parts[1]) ?? null);
|
|
848
881
|
if (prune) {
|
|
849
882
|
if (requestedVersion) {
|
|
850
883
|
console.log(chalk.red('--prune does not take a @version suffix.'));
|