@phnx-labs/agents-cli 1.14.1 → 1.14.3
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 +31 -3
- package/dist/commands/browser.d.ts +2 -0
- package/dist/commands/browser.js +388 -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/exec.js +17 -17
- 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 +206 -12
- package/dist/commands/sync.js +19 -0
- package/dist/commands/teams.js +162 -22
- package/dist/commands/trash.d.ts +10 -0
- package/dist/commands/trash.js +187 -0
- package/dist/commands/view.js +46 -13
- 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 +77 -0
- package/dist/lib/pty-client.js +3 -3
- package/dist/lib/pty-server.js +36 -7
- package/dist/lib/resources.js +1 -1
- package/dist/lib/rotate.d.ts +43 -26
- package/dist/lib/rotate.js +99 -44
- 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 -76
- 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 +8 -1
- package/dist/lib/teams/registry.js +8 -2
- package/dist/lib/teams/worktree.d.ts +30 -0
- package/dist/lib/teams/worktree.js +96 -0
- package/dist/lib/types.d.ts +13 -7
- package/dist/lib/types.js +3 -3
- package/dist/lib/versions.d.ts +30 -2
- package/dist/lib/versions.js +127 -105
- package/package.json +1 -1
- 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/secrets.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import chalk from 'chalk';
|
|
9
9
|
import * as fs from 'fs';
|
|
10
|
-
import { bundleExists, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, parseDotenv, readBundle, validateBundleName, validateEnvKey, writeBundle, } from '../lib/secrets/bundles.js';
|
|
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
12
|
import { registerCommandGroups } from '../lib/help.js';
|
|
13
13
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
@@ -114,7 +114,11 @@ function renderBundleRow(b) {
|
|
|
114
114
|
const entries = describeBundle(b);
|
|
115
115
|
const keys = entries.length;
|
|
116
116
|
const sensitive = entries.filter((e) => e.kind === 'keychain').length;
|
|
117
|
-
|
|
117
|
+
const expiring = countExpiringSoon(b.meta);
|
|
118
|
+
const expiringCol = expiring > 0
|
|
119
|
+
? chalk.yellow(String(expiring).padEnd(10))
|
|
120
|
+
: ''.padEnd(10);
|
|
121
|
+
return `${chalk.cyan(b.name.padEnd(20))} ${String(keys).padEnd(6)} ${chalk.yellow(String(sensitive).padEnd(10))} ${expiringCol} ${chalk.gray(b.description || '')}`;
|
|
118
122
|
}
|
|
119
123
|
/** Colorize a variable source kind (literal, keychain, env, file, exec). */
|
|
120
124
|
function kindLabel(kind) {
|
|
@@ -135,11 +139,102 @@ function redact(value, reveal) {
|
|
|
135
139
|
return '';
|
|
136
140
|
return '*'.repeat(Math.min(value.length, 8));
|
|
137
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Build a VarMeta patch from CLI flags. Validates each provided field. Returns
|
|
144
|
+
* undefined if no meta flag was passed (so callers know to skip meta updates).
|
|
145
|
+
*
|
|
146
|
+
* `--note -` reads the note from stdin so users can pass long/multi-line notes
|
|
147
|
+
* without shell-escaping. It's mutually exclusive with `--value-stdin`; both
|
|
148
|
+
* trying to consume stdin would race and silently corrupt one or the other.
|
|
149
|
+
*/
|
|
150
|
+
function buildMetaPatch(raw) {
|
|
151
|
+
if (raw.type === undefined && raw.expires === undefined && raw.note === undefined) {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
const patch = {};
|
|
155
|
+
if (raw.type !== undefined) {
|
|
156
|
+
validateSecretType(raw.type);
|
|
157
|
+
patch.type = raw.type;
|
|
158
|
+
}
|
|
159
|
+
if (raw.expires !== undefined) {
|
|
160
|
+
validateExpiresFutureDated(raw.expires);
|
|
161
|
+
patch.expires = raw.expires;
|
|
162
|
+
}
|
|
163
|
+
if (raw.note !== undefined) {
|
|
164
|
+
if (raw.note === '-') {
|
|
165
|
+
if (raw.valueStdin) {
|
|
166
|
+
throw new Error('--note - and --value-stdin both want stdin; only one can read it.');
|
|
167
|
+
}
|
|
168
|
+
const fromStdin = readStdinSync();
|
|
169
|
+
if (!fromStdin)
|
|
170
|
+
throw new Error('No note received on stdin.');
|
|
171
|
+
patch.note = fromStdin;
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
patch.note = raw.note;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return patch;
|
|
178
|
+
}
|
|
179
|
+
/** Whole days from now until midnight-UTC of the given ISO date. Negative if past. */
|
|
180
|
+
function daysUntil(iso) {
|
|
181
|
+
const target = new Date(iso + 'T23:59:59Z').getTime();
|
|
182
|
+
const now = Date.now();
|
|
183
|
+
return Math.floor((target - now) / (24 * 60 * 60 * 1000));
|
|
184
|
+
}
|
|
185
|
+
/** Render the meta line under a var, indented. Returns empty string if nothing to show. */
|
|
186
|
+
function renderMetaLine(meta, reveal) {
|
|
187
|
+
if (!meta)
|
|
188
|
+
return '';
|
|
189
|
+
const parts = [];
|
|
190
|
+
if (meta.type)
|
|
191
|
+
parts.push(`type: ${meta.type}`);
|
|
192
|
+
if (meta.expires) {
|
|
193
|
+
const days = daysUntil(meta.expires);
|
|
194
|
+
const tail = `(in ${days} days)`;
|
|
195
|
+
let colored;
|
|
196
|
+
if (days < 0) {
|
|
197
|
+
colored = chalk.red(`expires: ${meta.expires} ${tail}`);
|
|
198
|
+
}
|
|
199
|
+
else if (days < 30) {
|
|
200
|
+
colored = chalk.yellow(`expires: ${meta.expires} ${tail}`);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
colored = chalk.gray(`expires: ${meta.expires} ${tail}`);
|
|
204
|
+
}
|
|
205
|
+
parts.push(colored);
|
|
206
|
+
}
|
|
207
|
+
if (meta.note) {
|
|
208
|
+
let note = meta.note;
|
|
209
|
+
if (!reveal && note.length > 80) {
|
|
210
|
+
note = note.slice(0, 79) + '\u2026';
|
|
211
|
+
}
|
|
212
|
+
parts.push(`note: ${note}`);
|
|
213
|
+
}
|
|
214
|
+
if (parts.length === 0)
|
|
215
|
+
return '';
|
|
216
|
+
return ` ${parts.join(' ')}`;
|
|
217
|
+
}
|
|
218
|
+
/** Count entries in `meta` whose `expires` falls in the next 30 days. */
|
|
219
|
+
function countExpiringSoon(meta) {
|
|
220
|
+
if (!meta)
|
|
221
|
+
return 0;
|
|
222
|
+
let n = 0;
|
|
223
|
+
for (const m of Object.values(meta)) {
|
|
224
|
+
if (!m.expires)
|
|
225
|
+
continue;
|
|
226
|
+
const d = daysUntil(m.expires);
|
|
227
|
+
if (d >= 0 && d < 30)
|
|
228
|
+
n++;
|
|
229
|
+
}
|
|
230
|
+
return n;
|
|
231
|
+
}
|
|
138
232
|
/** Register the `agents secrets` command tree. */
|
|
139
233
|
export function registerSecretsCommands(program) {
|
|
140
234
|
const cmd = program
|
|
141
235
|
.command('secrets')
|
|
142
|
-
.description('Named bundles of env variables backed by macOS Keychain. Inject into agents via `agents run --secrets <name>`.')
|
|
236
|
+
.description('Named bundles of env variables backed by macOS Keychain (with optional iCloud sync). Inject into agents via `agents run --secrets <name>`.')
|
|
237
|
+
.hook('preAction', () => { migrateLegacyBundles(); })
|
|
143
238
|
.addHelpText('after', `
|
|
144
239
|
Workflow:
|
|
145
240
|
Bundles are containers; secrets are the variables inside them. Create a
|
|
@@ -147,16 +242,29 @@ Workflow:
|
|
|
147
242
|
run with --secrets <name>. Keychain-backed values never touch disk in
|
|
148
243
|
plaintext.
|
|
149
244
|
|
|
245
|
+
Pass --icloud-sync at create time to store values in the iCloud-synced
|
|
246
|
+
keychain so they appear automatically on your other Macs (same iCloud
|
|
247
|
+
account, iCloud Keychain enabled). Without the flag, values are device-local.
|
|
248
|
+
|
|
150
249
|
Examples:
|
|
151
250
|
# Create a bundle for production credentials
|
|
152
251
|
agents secrets create prod --description "Production keys for the api stack"
|
|
153
252
|
|
|
253
|
+
# Create a bundle that syncs to your other Macs via iCloud Keychain
|
|
254
|
+
agents secrets create npm-tokens --icloud-sync
|
|
255
|
+
|
|
154
256
|
# Add a keychain-backed secret (prompts for the value)
|
|
155
257
|
agents secrets add prod STRIPE_API_KEY
|
|
156
258
|
|
|
157
259
|
# Add a literal (non-sensitive) value
|
|
158
260
|
agents secrets add prod LOG_LEVEL --value info
|
|
159
261
|
|
|
262
|
+
# Add with metadata
|
|
263
|
+
agents secrets add prod STRIPE_API_KEY --type api-key --expires 2027-01-15 --note "Live key, owner: payments-team"
|
|
264
|
+
|
|
265
|
+
# Rotate a key (replaces the value, preserves metadata unless overridden)
|
|
266
|
+
agents secrets rotate prod STRIPE_API_KEY
|
|
267
|
+
|
|
160
268
|
# Import an entire .env file straight into keychain
|
|
161
269
|
agents secrets import prod --from .env.prod
|
|
162
270
|
|
|
@@ -180,7 +288,7 @@ Examples:
|
|
|
180
288
|
`);
|
|
181
289
|
registerCommandGroups(cmd, [
|
|
182
290
|
{ title: 'Bundle commands', names: ['list', 'view', 'create', 'delete'] },
|
|
183
|
-
{ title: 'Secret commands', names: ['add', 'remove', 'import', 'export'] },
|
|
291
|
+
{ title: 'Secret commands', names: ['add', 'rotate', 'remove', 'import', 'export'] },
|
|
184
292
|
]);
|
|
185
293
|
cmd
|
|
186
294
|
.command('list')
|
|
@@ -193,7 +301,7 @@ Examples:
|
|
|
193
301
|
console.log(chalk.gray('Try: agents secrets create <name>'));
|
|
194
302
|
return;
|
|
195
303
|
}
|
|
196
|
-
console.log(chalk.bold(`${'NAME'.padEnd(20)} ${'KEYS'.padEnd(6)} ${'SENSITIVE'.padEnd(10)} DESCRIPTION`));
|
|
304
|
+
console.log(chalk.bold(`${'NAME'.padEnd(20)} ${'KEYS'.padEnd(6)} ${'SENSITIVE'.padEnd(10)} ${'EXPIRING'.padEnd(10)} DESCRIPTION`));
|
|
197
305
|
for (const b of bundles) {
|
|
198
306
|
console.log(renderBundleRow(b));
|
|
199
307
|
}
|
|
@@ -252,6 +360,9 @@ Examples:
|
|
|
252
360
|
else {
|
|
253
361
|
console.log(` ${chalk.cyan(e.key.padEnd(28))} ${kindLabel(e.kind).padEnd(18)} ${e.detail}`);
|
|
254
362
|
}
|
|
363
|
+
const metaLine = renderMetaLine(bundle.meta?.[e.key], reveal);
|
|
364
|
+
if (metaLine)
|
|
365
|
+
console.log(metaLine);
|
|
255
366
|
}
|
|
256
367
|
}
|
|
257
368
|
catch (err) {
|
|
@@ -297,29 +408,45 @@ Examples:
|
|
|
297
408
|
cmd
|
|
298
409
|
.command('add [bundle] [key]')
|
|
299
410
|
.description('Add a variable to a bundle. Defaults to keychain-backed; pass --value for literal, --env/--file/--exec for refs.')
|
|
300
|
-
.option('--value <v>', 'Store as a plaintext literal in the
|
|
411
|
+
.option('--value <v>', 'Store as a plaintext literal in the bundle (non-sensitive values only)')
|
|
301
412
|
.option('--value-stdin', 'Read the value from stdin (stored in keychain unless combined with --value)')
|
|
302
413
|
.option('--env <VAR>', 'Store as an env: ref that reads from the parent process.env at run time')
|
|
303
414
|
.option('--file <path>', 'Store as a file: ref that reads from a file at run time')
|
|
304
415
|
.option('--exec <cmd>', 'Store as an exec: ref that runs a command at run time (requires allow_exec)')
|
|
416
|
+
.option('--type <kind>', 'Tag this secret with a type (api-key, token, password, url, database-url, ssh-key, certificate, webhook, note)')
|
|
417
|
+
.option('--expires <YYYY-MM-DD>', 'Mark when this secret expires (must be future-dated)')
|
|
418
|
+
.option('--note <text>', 'Attach a freeform note. Pass `-` to read from stdin (mutually exclusive with --value-stdin).')
|
|
305
419
|
.action(async (bundleName, key, opts) => {
|
|
306
420
|
try {
|
|
307
421
|
const resolvedBundleName = bundleName ?? (await pickBundleName('add to'));
|
|
308
422
|
const bundle = readBundle(resolvedBundleName);
|
|
309
423
|
const resolvedKey = key ?? (await promptKeyName(resolvedBundleName));
|
|
310
424
|
validateEnvKey(resolvedKey);
|
|
425
|
+
if (resolvedKey in bundle.vars) {
|
|
426
|
+
throw new Error(`Key '${resolvedKey}' already exists in bundle '${resolvedBundleName}'. Use 'agents secrets rotate' to refresh it.`);
|
|
427
|
+
}
|
|
311
428
|
const sources = [opts.value !== undefined, Boolean(opts.env), Boolean(opts.file), Boolean(opts.exec)].filter(Boolean).length;
|
|
312
429
|
if (sources > 1) {
|
|
313
430
|
throw new Error('Pick one of: --value, --env, --file, --exec.');
|
|
314
431
|
}
|
|
432
|
+
const metaPatch = buildMetaPatch(opts);
|
|
433
|
+
const applyMeta = () => {
|
|
434
|
+
if (!metaPatch)
|
|
435
|
+
return;
|
|
436
|
+
if (!bundle.meta)
|
|
437
|
+
bundle.meta = {};
|
|
438
|
+
bundle.meta[resolvedKey] = { ...(bundle.meta[resolvedKey] ?? {}), ...metaPatch };
|
|
439
|
+
};
|
|
315
440
|
if (opts.env) {
|
|
316
441
|
bundle.vars[resolvedKey] = `env:${opts.env}`;
|
|
442
|
+
applyMeta();
|
|
317
443
|
writeBundle(bundle);
|
|
318
444
|
console.log(chalk.green(`${resolvedBundleName}.${resolvedKey} -> env:${opts.env}`));
|
|
319
445
|
return;
|
|
320
446
|
}
|
|
321
447
|
if (opts.file) {
|
|
322
448
|
bundle.vars[resolvedKey] = `file:${opts.file}`;
|
|
449
|
+
applyMeta();
|
|
323
450
|
writeBundle(bundle);
|
|
324
451
|
console.log(chalk.green(`${resolvedBundleName}.${resolvedKey} -> file:${opts.file}`));
|
|
325
452
|
return;
|
|
@@ -329,12 +456,14 @@ Examples:
|
|
|
329
456
|
throw new Error(`Bundle '${resolvedBundleName}' does not allow exec refs. Re-create with --allow-exec.`);
|
|
330
457
|
}
|
|
331
458
|
bundle.vars[resolvedKey] = `exec:${opts.exec}`;
|
|
459
|
+
applyMeta();
|
|
332
460
|
writeBundle(bundle);
|
|
333
461
|
console.log(chalk.green(`${resolvedBundleName}.${resolvedKey} -> exec:${opts.exec}`));
|
|
334
462
|
return;
|
|
335
463
|
}
|
|
336
464
|
if (opts.value !== undefined) {
|
|
337
465
|
bundle.vars[resolvedKey] = { value: opts.value };
|
|
466
|
+
applyMeta();
|
|
338
467
|
writeBundle(bundle);
|
|
339
468
|
console.log(chalk.green(`${resolvedBundleName}.${resolvedKey} = <literal>`));
|
|
340
469
|
return;
|
|
@@ -352,6 +481,7 @@ Examples:
|
|
|
352
481
|
const item = secretsKeychainItem(resolvedBundleName, resolvedKey);
|
|
353
482
|
setKeychainToken(item, secretValue, bundle.icloud_sync);
|
|
354
483
|
bundle.vars[resolvedKey] = keychainRef(resolvedKey);
|
|
484
|
+
applyMeta();
|
|
355
485
|
writeBundle(bundle);
|
|
356
486
|
const where = bundle.icloud_sync ? 'iCloud Keychain' : 'keychain';
|
|
357
487
|
console.log(chalk.green(`${resolvedBundleName}.${resolvedKey} stored in ${where} (${item}).`));
|
|
@@ -363,10 +493,71 @@ Examples:
|
|
|
363
493
|
process.exit(1);
|
|
364
494
|
}
|
|
365
495
|
});
|
|
496
|
+
cmd
|
|
497
|
+
.command('rotate [bundle] [key]')
|
|
498
|
+
.description('Rotate an existing keychain-backed secret (replaces the value, preserves metadata unless overridden).')
|
|
499
|
+
.option('--value <v>', 'New value (non-secret cases). Prompts interactively if omitted.')
|
|
500
|
+
.option('--value-stdin', 'Read the new value from stdin (stored in keychain unless combined with --value)')
|
|
501
|
+
.option('--type <kind>', 'Update the type metadata (api-key, token, password, url, database-url, ssh-key, certificate, webhook, note)')
|
|
502
|
+
.option('--expires <YYYY-MM-DD>', 'Update the expiration date (must be future-dated)')
|
|
503
|
+
.option('--note <text>', 'Update the note. Pass `-` to read from stdin (mutually exclusive with --value-stdin).')
|
|
504
|
+
.option('--clear-meta', 'Wipe all metadata for this key while rotating')
|
|
505
|
+
.addHelpText('after', `
|
|
506
|
+
Examples:
|
|
507
|
+
# Rotate the value, preserve all metadata
|
|
508
|
+
agents secrets rotate prod STRIPE_API_KEY
|
|
509
|
+
|
|
510
|
+
# Rotate with a metadata refresh
|
|
511
|
+
agents secrets rotate prod STRIPE_API_KEY --type api-key --expires 2027-01-15 --note "rotated after employee offboarding"
|
|
512
|
+
`)
|
|
513
|
+
.action(async (bundleName, key, opts) => {
|
|
514
|
+
try {
|
|
515
|
+
const resolvedBundleName = bundleName ?? (await pickBundleName('rotate in'));
|
|
516
|
+
const bundle = readBundle(resolvedBundleName);
|
|
517
|
+
const resolvedKey = key ?? (await pickKey(bundle, 'rotate'));
|
|
518
|
+
if (!(resolvedKey in bundle.vars)) {
|
|
519
|
+
throw new Error(`Key '${resolvedKey}' not in bundle '${resolvedBundleName}'. Use 'agents secrets add' to add a new key.`);
|
|
520
|
+
}
|
|
521
|
+
const raw = bundle.vars[resolvedKey];
|
|
522
|
+
if (typeof raw !== 'string' || !raw.startsWith('keychain:')) {
|
|
523
|
+
throw new Error(`Key '${resolvedKey}' in bundle '${resolvedBundleName}' is not keychain-backed; cannot rotate.`);
|
|
524
|
+
}
|
|
525
|
+
const metaPatch = buildMetaPatch(opts);
|
|
526
|
+
if (opts.clearMeta && metaPatch) {
|
|
527
|
+
throw new Error('--clear-meta and --type/--expires/--note are mutually exclusive.');
|
|
528
|
+
}
|
|
529
|
+
// Resolve the new value: --value > --value-stdin > prompt.
|
|
530
|
+
let newValue;
|
|
531
|
+
if (opts.value !== undefined) {
|
|
532
|
+
newValue = opts.value;
|
|
533
|
+
}
|
|
534
|
+
else if (opts.valueStdin) {
|
|
535
|
+
newValue = readStdinSync();
|
|
536
|
+
if (!newValue)
|
|
537
|
+
throw new Error('No value received on stdin.');
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
newValue = await promptForSecret(`Enter new value for ${resolvedBundleName}.${resolvedKey}`);
|
|
541
|
+
}
|
|
542
|
+
rotateBundleSecret(bundle, resolvedKey, {
|
|
543
|
+
newValue,
|
|
544
|
+
clearMeta: opts.clearMeta,
|
|
545
|
+
meta: metaPatch,
|
|
546
|
+
});
|
|
547
|
+
const where = bundle.icloud_sync ? 'iCloud Keychain' : 'keychain';
|
|
548
|
+
console.log(chalk.green(`${resolvedBundleName}.${resolvedKey} rotated in ${where}.`));
|
|
549
|
+
}
|
|
550
|
+
catch (err) {
|
|
551
|
+
if (isPromptCancelled(err))
|
|
552
|
+
return;
|
|
553
|
+
console.error(chalk.red(err.message));
|
|
554
|
+
process.exit(1);
|
|
555
|
+
}
|
|
556
|
+
});
|
|
366
557
|
cmd
|
|
367
558
|
.command('remove [bundle] [key]')
|
|
368
559
|
.description('Remove a key from the bundle. Purges the keychain item if the ref was keychain:. Use --keep-secret to retain it.')
|
|
369
|
-
.option('--keep-secret', 'Leave the keychain item in place after removing the
|
|
560
|
+
.option('--keep-secret', 'Leave the keychain item in place after removing the ref from the bundle')
|
|
370
561
|
.action(async (bundleName, key, opts) => {
|
|
371
562
|
try {
|
|
372
563
|
const resolvedBundleName = bundleName ?? (await pickBundleName('remove from'));
|
|
@@ -399,7 +590,7 @@ Examples:
|
|
|
399
590
|
cmd
|
|
400
591
|
.command('delete [name]')
|
|
401
592
|
.description('Delete a bundle and purge all its keychain items (use --keep-secrets to retain them).')
|
|
402
|
-
.option('--keep-secrets', 'Leave keychain items in place after deleting the bundle
|
|
593
|
+
.option('--keep-secrets', 'Leave keychain items in place after deleting the bundle')
|
|
403
594
|
.option('-y, --yes', 'Skip the confirmation prompt')
|
|
404
595
|
.action(async (name, opts) => {
|
|
405
596
|
try {
|
|
@@ -447,7 +638,7 @@ Examples:
|
|
|
447
638
|
.command('import [bundle]')
|
|
448
639
|
.description('Import keys from a .env file into a bundle. By default every key is stored in keychain.')
|
|
449
640
|
.requiredOption('--from <path>', 'Path to a .env file')
|
|
450
|
-
.option('--all-plaintext', 'Store every imported value as a
|
|
641
|
+
.option('--all-plaintext', 'Store every imported value as a literal in the bundle metadata (skip keychain item creation)')
|
|
451
642
|
.option('--force', 'Overwrite an existing key in the bundle')
|
|
452
643
|
.action(async (bundleName, opts) => {
|
|
453
644
|
try {
|
|
@@ -488,7 +679,7 @@ Examples:
|
|
|
488
679
|
.option('--plaintext', 'Acknowledge that the resolved values will be printed in the clear')
|
|
489
680
|
.action(async (bundleName, opts) => {
|
|
490
681
|
try {
|
|
491
|
-
const { resolveBundleEnv } = await import('../lib/secrets/bundles.js');
|
|
682
|
+
const { resolveBundleEnv, bundleToEnvPrefix, isReservedEnvName } = await import('../lib/secrets/bundles.js');
|
|
492
683
|
const resolvedBundleName = bundleName ?? (await pickBundleName('export'));
|
|
493
684
|
const bundle = readBundle(resolvedBundleName);
|
|
494
685
|
if (isInteractiveTerminal() && !opts.plaintext) {
|
|
@@ -496,9 +687,12 @@ Examples:
|
|
|
496
687
|
process.exit(1);
|
|
497
688
|
}
|
|
498
689
|
const env = resolveBundleEnv(bundle);
|
|
690
|
+
const prefix = bundleToEnvPrefix(resolvedBundleName);
|
|
499
691
|
for (const [k, v] of Object.entries(env)) {
|
|
500
|
-
const
|
|
501
|
-
|
|
692
|
+
const exportKey = isReservedEnvName(k) ? `${prefix}_${k}` : k;
|
|
693
|
+
const needsQuotes = /[\s$`"'\\|&;<>(){}[\]!#~]/.test(v);
|
|
694
|
+
const output = needsQuotes ? `'${v.replace(/'/g, `'\\''`)}'` : v;
|
|
695
|
+
process.stdout.write(`export ${exportKey}=${output}\n`);
|
|
502
696
|
}
|
|
503
697
|
}
|
|
504
698
|
catch (err) {
|
package/dist/commands/sync.js
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
* synchronize resources (commands, skills, hooks, memory, MCP, etc.)
|
|
6
6
|
* into a specific agent version home before launch.
|
|
7
7
|
*/
|
|
8
|
+
import * as path from 'path';
|
|
8
9
|
import chalk from 'chalk';
|
|
9
10
|
import { AGENTS } from '../lib/agents.js';
|
|
10
11
|
import { isVersionInstalled, syncResourcesToVersion } from '../lib/versions.js';
|
|
12
|
+
import { compileRulesForProject } from '../lib/rules/compile.js';
|
|
11
13
|
/** Register the hidden `agents sync` command. */
|
|
12
14
|
export function registerSyncCommand(program) {
|
|
13
15
|
program
|
|
@@ -39,6 +41,14 @@ export function registerSyncCommand(program) {
|
|
|
39
41
|
return;
|
|
40
42
|
}
|
|
41
43
|
const result = syncResourcesToVersion(agentId, version, undefined, { projectDir, cwd });
|
|
44
|
+
// Compile project-scope rules into the workspace itself so each agent's
|
|
45
|
+
// native loader picks up cwd/<INSTRUCTIONS_FILE>. projectDir is the
|
|
46
|
+
// .agents/ directory; the workspace root is its parent.
|
|
47
|
+
let projectCompile = null;
|
|
48
|
+
if (projectDir) {
|
|
49
|
+
const projectRoot = path.dirname(projectDir);
|
|
50
|
+
projectCompile = compileRulesForProject(projectRoot);
|
|
51
|
+
}
|
|
42
52
|
if (quiet) {
|
|
43
53
|
return;
|
|
44
54
|
}
|
|
@@ -65,5 +75,14 @@ export function registerSyncCommand(program) {
|
|
|
65
75
|
else {
|
|
66
76
|
console.log(chalk.gray('No resources to sync'));
|
|
67
77
|
}
|
|
78
|
+
if (projectCompile?.compiled) {
|
|
79
|
+
const linkInfo = projectCompile.symlinks.length > 0
|
|
80
|
+
? ` (+ ${projectCompile.symlinks.join(', ')})`
|
|
81
|
+
: '';
|
|
82
|
+
console.log(chalk.gray(`Compiled project rules → ${projectCompile.agentsPath}${linkInfo}`));
|
|
83
|
+
}
|
|
84
|
+
if (projectCompile && projectCompile.skippedClobber.length > 0) {
|
|
85
|
+
console.log(chalk.yellow(`Skipped (user-authored, not overwritten): ${projectCompile.skippedClobber.join(', ')}`));
|
|
86
|
+
}
|
|
68
87
|
});
|
|
69
88
|
}
|