@phnx-labs/agents-cli 1.16.0 → 1.17.1
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 +71 -0
- package/dist/commands/browser.js +248 -9
- package/dist/commands/cloud.js +8 -0
- package/dist/commands/exec.js +70 -1
- package/dist/commands/import.d.ts +24 -0
- package/dist/commands/import.js +203 -0
- package/dist/commands/plugins.js +179 -5
- package/dist/commands/prune.js +6 -0
- package/dist/commands/secrets.js +117 -19
- package/dist/commands/view.js +21 -8
- package/dist/commands/workflows.d.ts +10 -0
- package/dist/commands/workflows.js +457 -0
- package/dist/index.js +34 -16
- package/dist/lib/browser/cdp.js +7 -4
- package/dist/lib/browser/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +37 -2
- package/dist/lib/browser/drivers/local.js +13 -2
- package/dist/lib/browser/input.d.ts +1 -0
- package/dist/lib/browser/input.js +3 -0
- package/dist/lib/browser/ipc.js +14 -0
- package/dist/lib/browser/profiles.d.ts +5 -0
- package/dist/lib/browser/profiles.js +45 -0
- package/dist/lib/browser/service.d.ts +10 -0
- package/dist/lib/browser/service.js +29 -1
- package/dist/lib/browser/types.d.ts +11 -1
- package/dist/lib/cloud/rush.d.ts +28 -1
- package/dist/lib/cloud/rush.js +68 -13
- package/dist/lib/commands.d.ts +0 -15
- package/dist/lib/commands.js +5 -5
- package/dist/lib/hooks.js +24 -11
- package/dist/lib/import.d.ts +91 -0
- package/dist/lib/import.js +179 -0
- package/dist/lib/migrate.js +59 -1
- package/dist/lib/permissions.d.ts +0 -58
- package/dist/lib/permissions.js +10 -10
- package/dist/lib/plugins.d.ts +75 -34
- package/dist/lib/plugins.js +640 -133
- package/dist/lib/resource-patterns.d.ts +41 -0
- package/dist/lib/resource-patterns.js +82 -0
- package/dist/lib/resources/index.d.ts +17 -0
- package/dist/lib/resources/index.js +7 -0
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/resources/workflows.d.ts +24 -0
- package/dist/lib/resources/workflows.js +110 -0
- package/dist/lib/resources.d.ts +6 -1
- package/dist/lib/resources.js +12 -2
- package/dist/lib/session/db.d.ts +18 -0
- package/dist/lib/session/db.js +106 -7
- package/dist/lib/session/discover.d.ts +6 -0
- package/dist/lib/session/discover.js +28 -17
- package/dist/lib/shims.d.ts +3 -51
- package/dist/lib/shims.js +18 -10
- package/dist/lib/sqlite.js +10 -4
- package/dist/lib/state.d.ts +15 -2
- package/dist/lib/state.js +29 -8
- package/dist/lib/types.d.ts +43 -14
- package/dist/lib/versions.d.ts +3 -0
- package/dist/lib/versions.js +139 -27
- package/dist/lib/workflows.d.ts +79 -0
- package/dist/lib/workflows.js +233 -0
- package/package.json +1 -5
- package/scripts/postinstall.js +59 -58
- package/dist/commands/fork.d.ts +0 -10
- package/dist/commands/fork.js +0 -146
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents import` — adopt an existing unmanaged agent install into agents-cli.
|
|
3
|
+
*
|
|
4
|
+
* Three forms:
|
|
5
|
+
*
|
|
6
|
+
* agents import openclaw
|
|
7
|
+
* Auto-detect via the binary on PATH. Resolves the npm package directory,
|
|
8
|
+
* reads its version, and registers it under
|
|
9
|
+
* ~/.agents/.history/versions/<agent>/<version>/.
|
|
10
|
+
*
|
|
11
|
+
* agents import openclaw --version 2026.3.8
|
|
12
|
+
* Same auto-detect, but pin the version label rather than reading it from
|
|
13
|
+
* the package. Useful when the package metadata is stale or you want a
|
|
14
|
+
* canonical name.
|
|
15
|
+
*
|
|
16
|
+
* agents import openclaw --from-path /opt/homebrew/lib/node_modules/openclaw
|
|
17
|
+
* Skip detection entirely. The given path must be a directory containing
|
|
18
|
+
* a valid package.json with a `bin` entry.
|
|
19
|
+
*
|
|
20
|
+
* In all forms, the agent's config dir (e.g. ~/.openclaw) is also moved under
|
|
21
|
+
* management — same behavior as the first-run `agents setup` import flow.
|
|
22
|
+
*/
|
|
23
|
+
import chalk from 'chalk';
|
|
24
|
+
import ora from 'ora';
|
|
25
|
+
import * as fs from 'fs';
|
|
26
|
+
import * as path from 'path';
|
|
27
|
+
import { confirm } from '@inquirer/prompts';
|
|
28
|
+
import { ALL_AGENT_IDS } from '../lib/agents.js';
|
|
29
|
+
import { AGENTS, getCliPath, getCliVersion, agentLabel } from '../lib/agents.js';
|
|
30
|
+
import { getVersionDir } from '../lib/versions.js';
|
|
31
|
+
import { finalizeImport, importAgentBinary, importAgentConfig, resolvePackageDirFromBinary, } from '../lib/import.js';
|
|
32
|
+
import { isPromptCancelled, isInteractiveTerminal } from './utils.js';
|
|
33
|
+
function isValidAgentId(value) {
|
|
34
|
+
return ALL_AGENT_IDS.includes(value);
|
|
35
|
+
}
|
|
36
|
+
async function runImport(agentArg, opts) {
|
|
37
|
+
if (!isValidAgentId(agentArg)) {
|
|
38
|
+
console.error(chalk.red(`Unknown agent: ${agentArg}`));
|
|
39
|
+
console.error(chalk.gray(`Known agents: ${ALL_AGENT_IDS.join(', ')}`));
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
const agentId = agentArg;
|
|
43
|
+
const agent = AGENTS[agentId];
|
|
44
|
+
// Reject agents that don't ship via npm before we spin up PATH lookups and
|
|
45
|
+
// prompts. cursor/kiro/goose/roo all have npmPackage='' and use custom
|
|
46
|
+
// install scripts — the symlink-farm import doesn't apply to them.
|
|
47
|
+
if (!agent.npmPackage) {
|
|
48
|
+
console.error(chalk.red(`${agentLabel(agentId)} doesn't install via npm — \`agents import\` only handles npm-style packages.`));
|
|
49
|
+
console.error(chalk.gray(`Use \`agents add ${agentId}\` to install via its native script.`));
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
let globalPath = null;
|
|
53
|
+
if (opts.fromPath) {
|
|
54
|
+
globalPath = path.resolve(opts.fromPath);
|
|
55
|
+
if (!fs.existsSync(globalPath)) {
|
|
56
|
+
console.error(chalk.red(`Path does not exist: ${globalPath}`));
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
const binary = await getCliPath(agentId);
|
|
62
|
+
if (!binary) {
|
|
63
|
+
// Use || (not ??) so empty-string npmPackage falls back to cliCommand.
|
|
64
|
+
// Defensive: agents with empty npmPackage are rejected above, but keep
|
|
65
|
+
// the operator correct in case that early check is ever relaxed.
|
|
66
|
+
const installName = agent.npmPackage || agent.cliCommand;
|
|
67
|
+
console.error(chalk.red(`No "${agent.cliCommand}" found on PATH.`));
|
|
68
|
+
console.error(chalk.gray(`Install it first (e.g. \`npm i -g ${installName}\`) or pass --from-path.`));
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
globalPath = resolvePackageDirFromBinary(binary);
|
|
72
|
+
if (!globalPath) {
|
|
73
|
+
console.error(chalk.red(`Could not resolve npm package for binary: ${binary}`));
|
|
74
|
+
console.error(chalk.gray('Pass --from-path <dir> with the package directory explicitly.'));
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
let version = opts.version;
|
|
79
|
+
if (!version) {
|
|
80
|
+
try {
|
|
81
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(globalPath, 'package.json'), 'utf8'));
|
|
82
|
+
version = typeof pkg.version === 'string' ? pkg.version : undefined;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
/* fall through */
|
|
86
|
+
}
|
|
87
|
+
// Only fall back to running the PATH binary's --version when we're
|
|
88
|
+
// auto-detecting. With --from-path, the PATH binary may belong to a
|
|
89
|
+
// different install entirely; reporting its version here would silently
|
|
90
|
+
// mis-attribute the imported version.
|
|
91
|
+
if (!version && !opts.fromPath) {
|
|
92
|
+
const detected = await getCliVersion(agentId);
|
|
93
|
+
version = detected ?? undefined;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (!version) {
|
|
97
|
+
console.error(chalk.red(`Could not determine version for ${agentLabel(agentId)}.`));
|
|
98
|
+
console.error(chalk.gray('Pass --version <version> explicitly.'));
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
const versionDir = getVersionDir(agentId, version);
|
|
102
|
+
console.log(chalk.bold(`\nImport ${agentLabel(agentId)} v${version}`));
|
|
103
|
+
console.log(` from: ${chalk.gray(globalPath)}`);
|
|
104
|
+
console.log(` into: ${chalk.gray(versionDir)}`);
|
|
105
|
+
const configDirExists = fs.existsSync(agent.configDir);
|
|
106
|
+
let configAlreadyManaged = false;
|
|
107
|
+
if (configDirExists) {
|
|
108
|
+
const stat = fs.lstatSync(agent.configDir);
|
|
109
|
+
if (stat.isSymbolicLink()) {
|
|
110
|
+
configAlreadyManaged = true;
|
|
111
|
+
console.log(` config: ${chalk.gray(`${agent.configDir} (already managed — will skip)`)}`);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
console.log(` config: ${chalk.gray(`${agent.configDir} (will be moved into version home)`)}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
console.log(` config: ${chalk.gray(`${agent.configDir} (does not exist — will skip)`)}`);
|
|
119
|
+
}
|
|
120
|
+
if (!opts.yes && isInteractiveTerminal()) {
|
|
121
|
+
console.log();
|
|
122
|
+
const proceed = await confirm({
|
|
123
|
+
message: `Import ${agentLabel(agentId)} v${version} into agents-cli?`,
|
|
124
|
+
default: true,
|
|
125
|
+
}).catch((err) => {
|
|
126
|
+
if (isPromptCancelled(err))
|
|
127
|
+
return false;
|
|
128
|
+
throw err;
|
|
129
|
+
});
|
|
130
|
+
if (!proceed) {
|
|
131
|
+
console.log(chalk.gray('Aborted.'));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Order: config first, then binary, then finalize. Config does the
|
|
136
|
+
// user-visible side effect (renaming ~/.<agent>/), so if it fails we don't
|
|
137
|
+
// want a stranded symlink farm. Binary registration is cheap and reversible
|
|
138
|
+
// — if it fails after config, the next `agents import` call retries cleanly.
|
|
139
|
+
const willImportConfig = configDirExists && !configAlreadyManaged;
|
|
140
|
+
if (willImportConfig) {
|
|
141
|
+
const cfgSpinner = ora(`Importing config dir for ${agentLabel(agentId)} v${version}...`).start();
|
|
142
|
+
const cfgResult = await importAgentConfig(agentId, version);
|
|
143
|
+
if (cfgResult.success) {
|
|
144
|
+
cfgSpinner.succeed(`Config imported (${agent.configDir} -> ${versionDir}/home/.${agentId})`);
|
|
145
|
+
}
|
|
146
|
+
else if (cfgResult.skipped) {
|
|
147
|
+
cfgSpinner.warn(`Config: ${cfgResult.error}`);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
cfgSpinner.fail(`Config: ${cfgResult.error}`);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const binSpinner = ora(`Registering ${agentLabel(agentId)} v${version} binary...`).start();
|
|
155
|
+
const binResult = importAgentBinary({ agentId, npmPackage: agent.npmPackage, cliCommand: agent.cliCommand }, version, globalPath, versionDir);
|
|
156
|
+
if (binResult.success) {
|
|
157
|
+
binSpinner.succeed(`Binary registered (${agent.cliCommand} -> ${globalPath})`);
|
|
158
|
+
}
|
|
159
|
+
else if (binResult.skipped) {
|
|
160
|
+
binSpinner.warn(`Binary: ${binResult.error}`);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
binSpinner.fail(`Binary: ${binResult.error}`);
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
// Wire the imported version into the resolver: global default, main shim,
|
|
167
|
+
// versioned alias, home-file symlinks. Idempotent — safe to call even if
|
|
168
|
+
// importAgentConfig already set the global default.
|
|
169
|
+
const finalizeSpinner = ora(`Wiring ${agentLabel(agentId)} v${version} as the active version...`).start();
|
|
170
|
+
try {
|
|
171
|
+
finalizeImport(agentId, version);
|
|
172
|
+
finalizeSpinner.succeed(`${agentLabel(agentId)} v${version} set as default with shim + alias`);
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
finalizeSpinner.fail(`Finalize: ${err.message}`);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
console.log();
|
|
179
|
+
console.log(chalk.green(`${agentLabel(agentId)} v${version} is now managed.`));
|
|
180
|
+
console.log(chalk.gray(`Verify: agents view ${agentId}`));
|
|
181
|
+
}
|
|
182
|
+
export function registerImportCommand(program) {
|
|
183
|
+
program
|
|
184
|
+
.command('import')
|
|
185
|
+
.argument('<agent>', 'Agent id (e.g. openclaw, claude, codex)')
|
|
186
|
+
.description('Import an existing unmanaged agent install into agents-cli')
|
|
187
|
+
.option('--version <version>', 'Pin a version label (otherwise read from package.json)')
|
|
188
|
+
.option('--from-path <path>', 'Path to the npm package dir (otherwise auto-detected from PATH)')
|
|
189
|
+
.option('-y, --yes', 'Skip the confirmation prompt')
|
|
190
|
+
.addHelpText('after', `
|
|
191
|
+
Examples:
|
|
192
|
+
$ agents import openclaw Auto-detect via PATH
|
|
193
|
+
$ agents import openclaw --version 2026.3.8 Pin a version label
|
|
194
|
+
$ agents import openclaw --from-path /opt/homebrew/lib/node_modules/openclaw
|
|
195
|
+
|
|
196
|
+
When to use:
|
|
197
|
+
When an agent CLI is already installed globally via npm or homebrew and you
|
|
198
|
+
want to bring it under agents-cli management without reinstalling. Creates a
|
|
199
|
+
symlink farm pointing at the existing install — nothing is copied or moved
|
|
200
|
+
(except the agent's config dir, which is moved into the version's home).
|
|
201
|
+
`)
|
|
202
|
+
.action(runImport);
|
|
203
|
+
}
|
package/dist/commands/plugins.js
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
import * as fs from 'fs';
|
|
9
9
|
import * as path from 'path';
|
|
10
10
|
import chalk from 'chalk';
|
|
11
|
+
import { input } from '@inquirer/prompts';
|
|
11
12
|
import { PLUGINS_CAPABLE_AGENTS, agentLabel } from '../lib/agents.js';
|
|
12
|
-
import { discoverPlugins, getPlugin, pluginSupportsAgent, removePluginFromVersion } from '../lib/plugins.js';
|
|
13
|
+
import { discoverPlugins, getPlugin, pluginSupportsAgent, removePluginFromVersion, isPluginSynced, installPlugin, updatePlugin, loadUserConfig, saveUserConfig, checkPluginDependencies, } from '../lib/plugins.js';
|
|
13
14
|
import { listInstalledVersions, syncResourcesToVersion, getGlobalDefault, getVersionHomePath, } from '../lib/versions.js';
|
|
14
|
-
import { isPluginSynced } from '../lib/plugins.js';
|
|
15
15
|
import { isPromptCancelled, isInteractiveTerminal, requireDestructiveArg, requireInteractiveSelection, promptRemovalTargets, } from './utils.js';
|
|
16
16
|
import { itemPicker } from '../lib/picker.js';
|
|
17
17
|
import { showResourceList, buildTargetsSection, } from './resource-view.js';
|
|
@@ -308,21 +308,45 @@ Examples:
|
|
|
308
308
|
return;
|
|
309
309
|
}
|
|
310
310
|
let totalSkills = 0;
|
|
311
|
+
let totalCommands = 0;
|
|
312
|
+
let totalAgentDefs = 0;
|
|
311
313
|
let totalHooks = 0;
|
|
312
314
|
let totalPerms = 0;
|
|
315
|
+
let totalMcp = 0;
|
|
313
316
|
let versionsTouched = 0;
|
|
314
317
|
for (const target of selectedTargets) {
|
|
315
318
|
const versionHome = getVersionHomePath(target.agent, target.version);
|
|
316
319
|
const r = removePluginFromVersion(name, resolvedRoot, target.agent, versionHome);
|
|
317
|
-
|
|
320
|
+
const anyRemoved = r.skills.length > 0 || r.commands.length > 0 || r.agentDefs.length > 0 ||
|
|
321
|
+
r.bin.length > 0 || r.hooks.length > 0 || r.permissions > 0 || r.mcp > 0;
|
|
322
|
+
if (anyRemoved) {
|
|
318
323
|
versionsTouched += 1;
|
|
319
324
|
totalSkills += r.skills.length;
|
|
325
|
+
totalCommands += r.commands.length;
|
|
326
|
+
totalAgentDefs += r.agentDefs.length;
|
|
320
327
|
totalHooks += r.hooks.length;
|
|
321
328
|
totalPerms += r.permissions;
|
|
322
|
-
|
|
329
|
+
totalMcp += r.mcp;
|
|
330
|
+
const parts = [
|
|
331
|
+
r.skills.length > 0 ? `${r.skills.length} skill(s)` : null,
|
|
332
|
+
r.commands.length > 0 ? `${r.commands.length} command(s)` : null,
|
|
333
|
+
r.agentDefs.length > 0 ? `${r.agentDefs.length} agent def(s)` : null,
|
|
334
|
+
r.hooks.length > 0 ? `${r.hooks.length} hook(s)` : null,
|
|
335
|
+
r.permissions > 0 ? `${r.permissions} perm(s)` : null,
|
|
336
|
+
r.mcp > 0 ? `${r.mcp} MCP server(s)` : null,
|
|
337
|
+
].filter(Boolean);
|
|
338
|
+
console.log(` ${chalk.red('-')} ${target.label}: ${parts.join(', ')}`);
|
|
323
339
|
}
|
|
324
340
|
}
|
|
325
|
-
|
|
341
|
+
const summary = [
|
|
342
|
+
totalSkills > 0 ? `${totalSkills} skills` : null,
|
|
343
|
+
totalCommands > 0 ? `${totalCommands} commands` : null,
|
|
344
|
+
totalAgentDefs > 0 ? `${totalAgentDefs} agent defs` : null,
|
|
345
|
+
totalHooks > 0 ? `${totalHooks} hooks` : null,
|
|
346
|
+
totalPerms > 0 ? `${totalPerms} permissions` : null,
|
|
347
|
+
totalMcp > 0 ? `${totalMcp} MCP servers` : null,
|
|
348
|
+
].filter(Boolean).join(', ') || 'nothing';
|
|
349
|
+
console.log(chalk.green(`\nUnsynced ${name} from ${versionsTouched} version(s) — ${summary}`));
|
|
326
350
|
// Only delete source if ALL targets were selected
|
|
327
351
|
if (!options.keepSource && selectedTargets.length === availableTargets.length) {
|
|
328
352
|
if (fs.existsSync(pluginRoot)) {
|
|
@@ -337,6 +361,156 @@ Examples:
|
|
|
337
361
|
console.log(chalk.gray(`Kept source at ${formatPath(pluginRoot)}`));
|
|
338
362
|
}
|
|
339
363
|
});
|
|
364
|
+
// agents plugins install <spec>
|
|
365
|
+
pluginsCmd
|
|
366
|
+
.command('install <spec>')
|
|
367
|
+
.description('Install a plugin from a git URL or local path (format: name@source or source)')
|
|
368
|
+
.addHelpText('after', `
|
|
369
|
+
Examples:
|
|
370
|
+
# Install from a git URL
|
|
371
|
+
agents plugins install my-plugin@https://github.com/user/my-plugin.git
|
|
372
|
+
|
|
373
|
+
# Install from a local path
|
|
374
|
+
agents plugins install /path/to/plugin
|
|
375
|
+
|
|
376
|
+
# Named install from a local path
|
|
377
|
+
agents plugins install rush-toolkit@~/Projects/rush-toolkit
|
|
378
|
+
`)
|
|
379
|
+
.action(async (spec) => {
|
|
380
|
+
console.log(chalk.gray(`Installing plugin from: ${spec}`));
|
|
381
|
+
let name;
|
|
382
|
+
let root;
|
|
383
|
+
try {
|
|
384
|
+
const result = await installPlugin(spec);
|
|
385
|
+
name = result.name;
|
|
386
|
+
root = result.root;
|
|
387
|
+
}
|
|
388
|
+
catch (err) {
|
|
389
|
+
console.log(chalk.red(`Install failed: ${err.message}`));
|
|
390
|
+
process.exit(1);
|
|
391
|
+
}
|
|
392
|
+
const plugin = getPlugin(name);
|
|
393
|
+
if (!plugin) {
|
|
394
|
+
console.log(chalk.red(`Installed but could not load plugin '${name}'`));
|
|
395
|
+
process.exit(1);
|
|
396
|
+
}
|
|
397
|
+
// Check dependencies
|
|
398
|
+
const missingDeps = checkPluginDependencies(plugin.manifest);
|
|
399
|
+
if (missingDeps.length > 0) {
|
|
400
|
+
console.log(chalk.yellow(`Warning: missing dependencies: ${missingDeps.join(', ')}`));
|
|
401
|
+
console.log(chalk.gray('Install them with: agents plugins install <name>@<source>'));
|
|
402
|
+
}
|
|
403
|
+
// Prompt for userConfig fields
|
|
404
|
+
if (plugin.manifest.userConfig && plugin.manifest.userConfig.length > 0 && isInteractiveTerminal()) {
|
|
405
|
+
const existingConfig = loadUserConfig(name);
|
|
406
|
+
const newConfig = await promptUserConfig(plugin.manifest, existingConfig);
|
|
407
|
+
if (Object.keys(newConfig).length > 0) {
|
|
408
|
+
saveUserConfig(name, { ...existingConfig, ...newConfig });
|
|
409
|
+
console.log(chalk.gray('User config saved.'));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
// Sync to all supported installed versions
|
|
413
|
+
console.log();
|
|
414
|
+
let synced = 0;
|
|
415
|
+
for (const agentId of PLUGINS_CAPABLE_AGENTS) {
|
|
416
|
+
if (!pluginSupportsAgent(plugin, agentId))
|
|
417
|
+
continue;
|
|
418
|
+
const versions = listInstalledVersions(agentId);
|
|
419
|
+
if (versions.length === 0)
|
|
420
|
+
continue;
|
|
421
|
+
const defaultVer = getGlobalDefault(agentId);
|
|
422
|
+
const targetVersions = defaultVer ? [defaultVer] : [versions[versions.length - 1]];
|
|
423
|
+
for (const version of targetVersions) {
|
|
424
|
+
const syncResult = syncResourcesToVersion(agentId, version, { plugins: [name] });
|
|
425
|
+
if (syncResult.plugins.length > 0) {
|
|
426
|
+
console.log(chalk.green(` Synced to ${agentLabel(agentId)}@${version}`));
|
|
427
|
+
synced++;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (synced === 0) {
|
|
432
|
+
console.log(chalk.gray(' No supported agent versions installed — run "agents use <agent>@<version>" to sync.'));
|
|
433
|
+
}
|
|
434
|
+
console.log(chalk.bold(`\nInstalled ${plugin.name} v${plugin.manifest.version} to ${formatPath(root)}`));
|
|
435
|
+
});
|
|
436
|
+
// agents plugins update [name]
|
|
437
|
+
pluginsCmd
|
|
438
|
+
.command('update [name]')
|
|
439
|
+
.description('Re-pull a plugin from its original source and re-sync to all versions')
|
|
440
|
+
.addHelpText('after', `
|
|
441
|
+
Examples:
|
|
442
|
+
# Update a specific plugin
|
|
443
|
+
agents plugins update rush-toolkit
|
|
444
|
+
|
|
445
|
+
# Update all plugins
|
|
446
|
+
agents plugins update
|
|
447
|
+
`)
|
|
448
|
+
.action(async (nameArg) => {
|
|
449
|
+
const plugins = nameArg ? [getPlugin(nameArg)].filter(Boolean) : discoverPlugins();
|
|
450
|
+
if (nameArg && plugins.length === 0) {
|
|
451
|
+
console.log(chalk.red(`Plugin '${nameArg}' not found`));
|
|
452
|
+
process.exit(1);
|
|
453
|
+
}
|
|
454
|
+
if (plugins.length === 0) {
|
|
455
|
+
console.log(chalk.gray('No plugins installed.'));
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
for (const plugin of plugins) {
|
|
459
|
+
process.stdout.write(`Updating ${plugin.name}... `);
|
|
460
|
+
const result = await updatePlugin(plugin.name);
|
|
461
|
+
if (!result.success) {
|
|
462
|
+
console.log(chalk.red(`failed — ${result.error || 'unknown error'}`));
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
console.log(chalk.green('done'));
|
|
466
|
+
// Re-sync to all supported installed versions
|
|
467
|
+
for (const agentId of PLUGINS_CAPABLE_AGENTS) {
|
|
468
|
+
if (!pluginSupportsAgent(plugin, agentId))
|
|
469
|
+
continue;
|
|
470
|
+
const versions = listInstalledVersions(agentId);
|
|
471
|
+
const defaultVer = getGlobalDefault(agentId);
|
|
472
|
+
const targetVersions = defaultVer ? [defaultVer] : versions.slice(-1);
|
|
473
|
+
for (const version of targetVersions) {
|
|
474
|
+
const syncResult = syncResourcesToVersion(agentId, version, { plugins: [plugin.name] });
|
|
475
|
+
if (syncResult.plugins.length > 0) {
|
|
476
|
+
console.log(chalk.gray(` Re-synced to ${agentLabel(agentId)}@${version}`));
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Prompt for missing or empty userConfig fields interactively.
|
|
485
|
+
* Only prompts for fields not already present in existingConfig.
|
|
486
|
+
*/
|
|
487
|
+
async function promptUserConfig(manifest, existingConfig = {}) {
|
|
488
|
+
const result = {};
|
|
489
|
+
const fields = manifest.userConfig || [];
|
|
490
|
+
for (const field of fields) {
|
|
491
|
+
if (existingConfig[field.key] !== undefined)
|
|
492
|
+
continue;
|
|
493
|
+
const defaultValue = field.default ?? '';
|
|
494
|
+
try {
|
|
495
|
+
const value = await input({
|
|
496
|
+
message: field.description + (field.required ? ' (required)' : ' (optional)'),
|
|
497
|
+
default: defaultValue || undefined,
|
|
498
|
+
required: field.required ?? false,
|
|
499
|
+
});
|
|
500
|
+
if (value) {
|
|
501
|
+
result[field.key] = value;
|
|
502
|
+
}
|
|
503
|
+
else if (defaultValue) {
|
|
504
|
+
result[field.key] = defaultValue;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
catch (err) {
|
|
508
|
+
if (isPromptCancelled(err))
|
|
509
|
+
break;
|
|
510
|
+
throw err;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return result;
|
|
340
514
|
}
|
|
341
515
|
/** Convert discovered plugins into rows suitable for the resource list view. */
|
|
342
516
|
function buildPluginRows(plugins) {
|
package/dist/commands/prune.js
CHANGED
|
@@ -404,12 +404,18 @@ Examples:
|
|
|
404
404
|
# Full sweep: orphan resources + duplicate versions for current defaults
|
|
405
405
|
agents prune
|
|
406
406
|
|
|
407
|
+
# Preview what a full sweep would remove
|
|
408
|
+
agents prune --dry-run
|
|
409
|
+
|
|
407
410
|
# Just orphan skills
|
|
408
411
|
agents prune skills
|
|
409
412
|
|
|
410
413
|
# Just version dedup
|
|
411
414
|
agents prune versions
|
|
412
415
|
|
|
416
|
+
# Deduplicate versions for one agent only
|
|
417
|
+
agents prune claude
|
|
418
|
+
|
|
413
419
|
# Sweep every installed version's orphans, not only the defaults
|
|
414
420
|
agents prune --all
|
|
415
421
|
|
package/dist/commands/secrets.js
CHANGED
|
@@ -9,6 +9,7 @@ import chalk from 'chalk';
|
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import { bundleExists, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, migrateLegacyBundles, parseDotenv, readBundle, rotateBundleSecret, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, } from '../lib/secrets/bundles.js';
|
|
11
11
|
import { deleteKeychainToken, getKeychainToken, hasKeychainToken, secretsKeychainItem, setKeychainToken, } from '../lib/secrets/index.js';
|
|
12
|
+
import { assertOpAvailable, createPasswordItem, deleteItemByTitle, extractSecrets, itemExistsByTitle, listItems, listVaults, } from '../lib/onepassword.js';
|
|
12
13
|
import { registerCommandGroups } from '../lib/help.js';
|
|
13
14
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
14
15
|
/** Prompt the user for a secret value with masked input. Requires an interactive TTY. */
|
|
@@ -91,6 +92,24 @@ async function promptKeyName(bundleName) {
|
|
|
91
92
|
},
|
|
92
93
|
});
|
|
93
94
|
}
|
|
95
|
+
/** Resolve a 1Password vault name — use the provided value, or prompt interactively. */
|
|
96
|
+
async function resolveVault(vaultOpt) {
|
|
97
|
+
if (vaultOpt)
|
|
98
|
+
return vaultOpt;
|
|
99
|
+
const vaults = listVaults();
|
|
100
|
+
if (vaults.length === 0)
|
|
101
|
+
throw new Error('No 1Password vaults found. Make sure you are signed in: op signin');
|
|
102
|
+
if (vaults.length === 1)
|
|
103
|
+
return vaults[0].name;
|
|
104
|
+
if (!isInteractiveTerminal()) {
|
|
105
|
+
throw new Error(`Multiple vaults found. Pass --vault <name> (available: ${vaults.map((v) => v.name).join(', ')})`);
|
|
106
|
+
}
|
|
107
|
+
const { select } = await import('@inquirer/prompts');
|
|
108
|
+
return await select({
|
|
109
|
+
message: 'Which 1Password vault?',
|
|
110
|
+
choices: vaults.map((v) => ({ name: v.name, value: v.name })),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
94
113
|
/** Read all available data from stdin synchronously, trimmed. */
|
|
95
114
|
function readStdinSync() {
|
|
96
115
|
const chunks = [];
|
|
@@ -268,6 +287,13 @@ Examples:
|
|
|
268
287
|
# Import an entire .env file straight into keychain
|
|
269
288
|
agents secrets import prod --from .env.prod
|
|
270
289
|
|
|
290
|
+
# Import secrets from a 1Password vault
|
|
291
|
+
agents secrets import prod --from-1password --vault "Rush Prod"
|
|
292
|
+
|
|
293
|
+
# Push a bundle back to 1Password (vault migration, backup)
|
|
294
|
+
agents secrets export prod --to-1password --vault "Rush Prod"
|
|
295
|
+
agents secrets export prod --to-1password --vault "Rush Prod" --force
|
|
296
|
+
|
|
271
297
|
# See what's in a bundle (values masked)
|
|
272
298
|
agents secrets view prod
|
|
273
299
|
|
|
@@ -649,35 +675,71 @@ Examples:
|
|
|
649
675
|
});
|
|
650
676
|
cmd
|
|
651
677
|
.command('import [bundle]')
|
|
652
|
-
.description('Import keys from a .env file into a bundle. By default every key is stored in keychain.')
|
|
653
|
-
.
|
|
678
|
+
.description('Import keys from a .env file or a 1Password vault into a bundle. By default every key is stored in keychain.')
|
|
679
|
+
.option('--from <path>', 'Path to a .env file')
|
|
680
|
+
.option('--from-1password', 'Import secrets from a 1Password vault (requires the op CLI)')
|
|
681
|
+
.option('--vault <name>', '1Password vault name (used with --from-1password)')
|
|
654
682
|
.option('--all-plaintext', 'Store every imported value as a literal in the bundle metadata (skip keychain item creation)')
|
|
655
683
|
.option('--force', 'Overwrite an existing key in the bundle')
|
|
656
684
|
.action(async (bundleName, opts) => {
|
|
657
685
|
try {
|
|
686
|
+
if (!opts.from && !opts.from1password) {
|
|
687
|
+
throw new Error('Pass --from <path> to import a .env file, or --from-1password to import from a 1Password vault.');
|
|
688
|
+
}
|
|
689
|
+
if (opts.from && opts.from1password) {
|
|
690
|
+
throw new Error('--from and --from-1password are mutually exclusive.');
|
|
691
|
+
}
|
|
658
692
|
const resolvedBundleName = bundleName ?? (await pickBundleName('import into'));
|
|
659
693
|
const bundle = readBundle(resolvedBundleName);
|
|
660
|
-
const raw = fs.readFileSync(opts.from, 'utf-8');
|
|
661
|
-
const pairs = parseDotenv(raw);
|
|
662
694
|
let added = 0;
|
|
663
695
|
let skipped = 0;
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
696
|
+
if (opts.from1password) {
|
|
697
|
+
assertOpAvailable();
|
|
698
|
+
const vault = await resolveVault(opts.vault);
|
|
699
|
+
const items = listItems(vault);
|
|
700
|
+
const { secrets, skipped: opSkipped } = extractSecrets(items, vault);
|
|
701
|
+
for (const { envKey, value } of secrets) {
|
|
702
|
+
if (!opts.force && envKey in bundle.vars) {
|
|
703
|
+
skipped++;
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
if (opts.allPlaintext) {
|
|
707
|
+
bundle.vars[envKey] = { value };
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
const item = secretsKeychainItem(resolvedBundleName, envKey);
|
|
711
|
+
setKeychainToken(item, value, bundle.icloud_sync);
|
|
712
|
+
bundle.vars[envKey] = keychainRef(envKey);
|
|
713
|
+
}
|
|
714
|
+
added++;
|
|
668
715
|
}
|
|
669
|
-
|
|
670
|
-
|
|
716
|
+
writeBundle(bundle);
|
|
717
|
+
if (opSkipped.length) {
|
|
718
|
+
console.log(chalk.yellow(`Skipped ${opSkipped.length} item(s) with no importable fields.`));
|
|
671
719
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
720
|
+
console.log(chalk.green(`Imported ${added} key(s) from 1Password vault '${vault}'${skipped ? `, skipped ${skipped} (already set, pass --force)` : ''}.`));
|
|
721
|
+
}
|
|
722
|
+
else {
|
|
723
|
+
const raw = fs.readFileSync(opts.from, 'utf-8');
|
|
724
|
+
const pairs = parseDotenv(raw);
|
|
725
|
+
for (const [key, value] of Object.entries(pairs)) {
|
|
726
|
+
if (!opts.force && key in bundle.vars) {
|
|
727
|
+
skipped++;
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
if (opts.allPlaintext) {
|
|
731
|
+
bundle.vars[key] = { value };
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
const item = secretsKeychainItem(resolvedBundleName, key);
|
|
735
|
+
setKeychainToken(item, value, bundle.icloud_sync);
|
|
736
|
+
bundle.vars[key] = keychainRef(key);
|
|
737
|
+
}
|
|
738
|
+
added++;
|
|
676
739
|
}
|
|
677
|
-
|
|
740
|
+
writeBundle(bundle);
|
|
741
|
+
console.log(chalk.green(`Imported ${added} key(s)${skipped ? `, skipped ${skipped} (already set, pass --force)` : ''}.`));
|
|
678
742
|
}
|
|
679
|
-
writeBundle(bundle);
|
|
680
|
-
console.log(chalk.green(`Imported ${added} key(s)${skipped ? `, skipped ${skipped} (already set, pass --force)` : ''}.`));
|
|
681
743
|
}
|
|
682
744
|
catch (err) {
|
|
683
745
|
if (isPromptCancelled(err))
|
|
@@ -688,13 +750,49 @@ Examples:
|
|
|
688
750
|
});
|
|
689
751
|
cmd
|
|
690
752
|
.command('export [bundle]')
|
|
691
|
-
.description('Resolve a bundle and print KEY=VALUE lines
|
|
692
|
-
.option('--plaintext', 'Acknowledge that the resolved values will be printed in the clear')
|
|
753
|
+
.description('Resolve a bundle and print KEY=VALUE lines, or push it to a 1Password vault with --to-1password.')
|
|
754
|
+
.option('--plaintext', 'Acknowledge that the resolved values will be printed in the clear (shell export mode)')
|
|
755
|
+
.option('--to-1password', 'Push every key in the bundle as a PASSWORD item in a 1Password vault')
|
|
756
|
+
.option('--vault <name>', '1Password vault name (used with --to-1password)')
|
|
757
|
+
.option('--force', 'Overwrite existing 1Password items (used with --to-1password)')
|
|
693
758
|
.action(async (bundleName, opts) => {
|
|
694
759
|
try {
|
|
695
760
|
const { resolveBundleEnv, bundleToEnvPrefix, isReservedEnvName } = await import('../lib/secrets/bundles.js');
|
|
696
761
|
const resolvedBundleName = bundleName ?? (await pickBundleName('export'));
|
|
697
762
|
const bundle = readBundle(resolvedBundleName);
|
|
763
|
+
if (opts.to1password) {
|
|
764
|
+
assertOpAvailable();
|
|
765
|
+
const vault = await resolveVault(opts.vault);
|
|
766
|
+
const env = resolveBundleEnv(bundle);
|
|
767
|
+
let created = 0;
|
|
768
|
+
let overwritten = 0;
|
|
769
|
+
let skipped = 0;
|
|
770
|
+
for (const [key, value] of Object.entries(env)) {
|
|
771
|
+
const exists = itemExistsByTitle(key, vault);
|
|
772
|
+
if (exists) {
|
|
773
|
+
if (!opts.force) {
|
|
774
|
+
skipped++;
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
deleteItemByTitle(key, vault);
|
|
778
|
+
createPasswordItem(key, value, vault);
|
|
779
|
+
overwritten++;
|
|
780
|
+
}
|
|
781
|
+
else {
|
|
782
|
+
createPasswordItem(key, value, vault);
|
|
783
|
+
created++;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
const parts = [];
|
|
787
|
+
if (created)
|
|
788
|
+
parts.push(`${created} created`);
|
|
789
|
+
if (overwritten)
|
|
790
|
+
parts.push(`${overwritten} overwritten`);
|
|
791
|
+
if (skipped)
|
|
792
|
+
parts.push(`${skipped} skipped (already exist, pass --force)`);
|
|
793
|
+
console.log(chalk.green(`Exported to 1Password vault '${vault}': ${parts.join(', ')}.`));
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
698
796
|
if (isInteractiveTerminal() && !opts.plaintext) {
|
|
699
797
|
console.error(chalk.red('export to a TTY requires --plaintext (prevents shoulder-surfing).'));
|
|
700
798
|
process.exit(1);
|