@phnx-labs/agents-cli 1.18.3 → 1.18.5
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 +89 -0
- package/README.md +14 -7
- package/dist/commands/browser.js +503 -132
- package/dist/commands/factory.js +13 -1
- package/dist/commands/rules.js +14 -0
- package/dist/commands/secrets.js +66 -11
- package/dist/lib/browser/cdp.js +7 -1
- package/dist/lib/browser/chrome.d.ts +1 -1
- package/dist/lib/browser/chrome.js +52 -26
- package/dist/lib/browser/devices.d.ts +11 -0
- package/dist/lib/browser/devices.js +14 -3
- package/dist/lib/browser/drivers/local.js +29 -2
- package/dist/lib/browser/drivers/ssh.js +82 -7
- package/dist/lib/browser/ipc.js +84 -9
- package/dist/lib/browser/profiles.d.ts +69 -3
- package/dist/lib/browser/profiles.js +184 -20
- package/dist/lib/browser/runtime-state.d.ts +117 -0
- package/dist/lib/browser/runtime-state.js +259 -0
- package/dist/lib/browser/service.d.ts +57 -10
- package/dist/lib/browser/service.js +477 -73
- package/dist/lib/browser/types.d.ts +67 -2
- package/dist/lib/browser/types.js +20 -0
- package/dist/lib/daemon.js +36 -3
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/help.js +30 -3
- package/dist/lib/secrets/bundles.d.ts +20 -0
- package/dist/lib/secrets/bundles.js +56 -0
- package/dist/lib/secrets/index.js +8 -8
- package/dist/lib/types.d.ts +16 -1
- package/dist/lib/version.d.ts +7 -0
- package/dist/lib/version.js +25 -0
- package/package.json +1 -1
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
export type BrowserType = 'chrome' | 'comet' | 'chromium' | 'brave' | 'edge' | 'custom';
|
|
2
|
+
/**
|
|
3
|
+
* A single named endpoint preset within a profile. Lets one profile cover
|
|
4
|
+
* the local + remote variants of the same app (e.g. Rush on this Mac vs.
|
|
5
|
+
* Rush on mac-mini) instead of forcing two parallel profiles.
|
|
6
|
+
*
|
|
7
|
+
* Per-endpoint overrides take precedence over profile-level fields.
|
|
8
|
+
*/
|
|
9
|
+
export interface EndpointPreset {
|
|
10
|
+
/** CDP URL — `cdp://host:port` or `ssh://host?port=N` */
|
|
11
|
+
target: string;
|
|
12
|
+
/** Override the profile-level binary (e.g. mac-mini has no local binary). */
|
|
13
|
+
binary?: string;
|
|
14
|
+
/** Override the profile-level targetFilter (Electron app builds may diverge). */
|
|
15
|
+
targetFilter?: string;
|
|
16
|
+
}
|
|
2
17
|
export interface BrowserProfile {
|
|
3
18
|
name: string;
|
|
4
19
|
description?: string;
|
|
@@ -10,7 +25,14 @@ export interface BrowserProfile {
|
|
|
10
25
|
* represents the visible UI for Electron apps with multiple WebContents.
|
|
11
26
|
*/
|
|
12
27
|
targetFilter?: string;
|
|
13
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Endpoint presets. Accepts two shapes for backward compatibility:
|
|
30
|
+
* - Legacy: `string[]` of CDP URLs; first entry is the default.
|
|
31
|
+
* - New: `{ [presetName]: EndpointPreset }`, with optional `defaultEndpoint`.
|
|
32
|
+
* Normalize via `resolveEndpoint(profile, name?)` instead of reading directly.
|
|
33
|
+
*/
|
|
34
|
+
endpoints: string[] | Record<string, EndpointPreset>;
|
|
35
|
+
defaultEndpoint?: string;
|
|
14
36
|
chrome?: ChromeOptions;
|
|
15
37
|
secrets?: string;
|
|
16
38
|
viewport?: {
|
|
@@ -19,6 +41,10 @@ export interface BrowserProfile {
|
|
|
19
41
|
x?: number;
|
|
20
42
|
y?: number;
|
|
21
43
|
};
|
|
44
|
+
/** Directory holding source-side JSONL logs (e.g. ~/.rush/logs). */
|
|
45
|
+
logDir?: string;
|
|
46
|
+
/** Optional SSH host where logDir lives, e.g. "muqsit@mac-mini". */
|
|
47
|
+
logHost?: string;
|
|
22
48
|
}
|
|
23
49
|
/** Parsed form of `BrowserProfile.targetFilter`. */
|
|
24
50
|
export interface TargetFilter {
|
|
@@ -83,7 +109,7 @@ export interface HistoricalTask {
|
|
|
83
109
|
domains: string[];
|
|
84
110
|
tabCount: number;
|
|
85
111
|
}
|
|
86
|
-
export type IPCAction = 'start' | '
|
|
112
|
+
export type IPCAction = 'start' | 'record-start' | 'record-stop' | 'done' | 'stop' | 'status' | 'history' | 'navigate' | 'tab-add' | 'tab-focus' | 'tab-close' | 'tab-list' | 'evaluate' | 'screenshot' | 'refs' | 'click' | 'type' | 'press' | 'hover' | 'scroll' | 'set-viewport' | 'set-device' | 'console' | 'errors' | 'requests' | 'response-body' | 'wait' | 'set-download-path' | 'wait-download' | 'upload' | 'getAppLogs' | 'version';
|
|
87
113
|
export interface IPCRequest {
|
|
88
114
|
action: IPCAction;
|
|
89
115
|
task?: string;
|
|
@@ -119,6 +145,28 @@ export interface IPCRequest {
|
|
|
119
145
|
files?: string[];
|
|
120
146
|
trigger?: number;
|
|
121
147
|
uploadMode?: 'auto' | 'input' | 'drop' | 'chooser';
|
|
148
|
+
quality?: 'compressed' | 'raw';
|
|
149
|
+
endpoint?: string;
|
|
150
|
+
fps?: number;
|
|
151
|
+
duration?: number;
|
|
152
|
+
maxMb?: number;
|
|
153
|
+
source?: string;
|
|
154
|
+
lines?: number;
|
|
155
|
+
message?: string;
|
|
156
|
+
since?: string;
|
|
157
|
+
until?: string;
|
|
158
|
+
appLevel?: string;
|
|
159
|
+
}
|
|
160
|
+
/** Subset of IPCResponse describing a recording start result. */
|
|
161
|
+
export interface RecordStartFields {
|
|
162
|
+
fps?: number;
|
|
163
|
+
durationCapSec?: number;
|
|
164
|
+
maxMb?: number;
|
|
165
|
+
}
|
|
166
|
+
/** Subset of IPCResponse describing a recording stop result. */
|
|
167
|
+
export interface RecordStopFields {
|
|
168
|
+
durationMs?: number;
|
|
169
|
+
stopReason?: 'manual' | 'duration-cap' | 'size-cap';
|
|
122
170
|
}
|
|
123
171
|
export interface IPCResponse {
|
|
124
172
|
ok: boolean;
|
|
@@ -131,10 +179,18 @@ export interface IPCResponse {
|
|
|
131
179
|
history?: HistoricalTask[];
|
|
132
180
|
result?: unknown;
|
|
133
181
|
path?: string;
|
|
182
|
+
bytes?: number;
|
|
183
|
+
width?: number;
|
|
184
|
+
height?: number;
|
|
134
185
|
refs?: string;
|
|
135
186
|
nodes?: RefNodeJson[];
|
|
136
187
|
port?: number;
|
|
137
188
|
pid?: number;
|
|
189
|
+
fps?: number;
|
|
190
|
+
durationCapSec?: number;
|
|
191
|
+
maxMb?: number;
|
|
192
|
+
durationMs?: number;
|
|
193
|
+
stopReason?: 'manual' | 'duration-cap' | 'size-cap';
|
|
138
194
|
logs?: ConsoleEntry[];
|
|
139
195
|
errors?: ErrorEntry[];
|
|
140
196
|
requests?: NetworkRequest[];
|
|
@@ -142,6 +198,8 @@ export interface IPCResponse {
|
|
|
142
198
|
downloadPath?: string;
|
|
143
199
|
devices?: string[];
|
|
144
200
|
uploadMode?: 'input' | 'drop' | 'chooser';
|
|
201
|
+
appLogs?: any[];
|
|
202
|
+
version?: string;
|
|
145
203
|
}
|
|
146
204
|
export interface ConsoleEntry {
|
|
147
205
|
level: 'log' | 'info' | 'warn' | 'error';
|
|
@@ -183,3 +241,10 @@ export declare function isValidTaskId(id: string): boolean;
|
|
|
183
241
|
export declare function generateTaskId(): string;
|
|
184
242
|
export declare function generateShortId(): string;
|
|
185
243
|
export declare function generateFunName(): string;
|
|
244
|
+
/**
|
|
245
|
+
* Auto-generated task name: `<adjective>-<noun>-<noun>-<hex8>`, e.g.
|
|
246
|
+
* `swift-crab-falcon-a3f92b1c`. Three English words make it memorable and
|
|
247
|
+
* easy to read; 32 bits of hex give every spawned task enough entropy that
|
|
248
|
+
* parallel agents never collide on the daemon side.
|
|
249
|
+
*/
|
|
250
|
+
export declare function generateTaskName(): string;
|
|
@@ -11,13 +11,33 @@ export function generateShortId() {
|
|
|
11
11
|
const ADJECTIVES = [
|
|
12
12
|
'swift', 'cosmic', 'jolly', 'quiet', 'bold', 'bright', 'calm', 'eager',
|
|
13
13
|
'golden', 'happy', 'keen', 'lucky', 'noble', 'proud', 'quick', 'royal',
|
|
14
|
+
'silver', 'amber', 'crimson', 'misty', 'sunny', 'gentle', 'wild', 'brave',
|
|
15
|
+
'merry', 'sleek', 'wise', 'fierce', 'curious', 'humble', 'spry', 'witty',
|
|
14
16
|
];
|
|
15
17
|
const NOUNS = [
|
|
16
18
|
'falcon', 'comet', 'tiger', 'nebula', 'phoenix', 'river', 'summit', 'wave',
|
|
17
19
|
'aurora', 'breeze', 'crystal', 'dragon', 'ember', 'forest', 'glacier', 'harbor',
|
|
20
|
+
'crab', 'otter', 'hawk', 'fox', 'wolf', 'panda', 'lynx', 'raven',
|
|
21
|
+
'meadow', 'canyon', 'valley', 'orchid', 'cedar', 'thistle', 'lotus', 'briar',
|
|
18
22
|
];
|
|
19
23
|
export function generateFunName() {
|
|
20
24
|
const adj = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
|
|
21
25
|
const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)];
|
|
22
26
|
return `${adj}-${noun}`;
|
|
23
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Auto-generated task name: `<adjective>-<noun>-<noun>-<hex8>`, e.g.
|
|
30
|
+
* `swift-crab-falcon-a3f92b1c`. Three English words make it memorable and
|
|
31
|
+
* easy to read; 32 bits of hex give every spawned task enough entropy that
|
|
32
|
+
* parallel agents never collide on the daemon side.
|
|
33
|
+
*/
|
|
34
|
+
export function generateTaskName() {
|
|
35
|
+
const adj = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)];
|
|
36
|
+
const noun1 = NOUNS[Math.floor(Math.random() * NOUNS.length)];
|
|
37
|
+
let noun2 = NOUNS[Math.floor(Math.random() * NOUNS.length)];
|
|
38
|
+
while (noun2 === noun1) {
|
|
39
|
+
noun2 = NOUNS[Math.floor(Math.random() * NOUNS.length)];
|
|
40
|
+
}
|
|
41
|
+
const hex8 = crypto.randomUUID().replace(/-/g, '').slice(0, 8);
|
|
42
|
+
return `${adj}-${noun1}-${noun2}-${hex8}`;
|
|
43
|
+
}
|
package/dist/lib/daemon.js
CHANGED
|
@@ -178,6 +178,24 @@ export async function runDaemon() {
|
|
|
178
178
|
for (const job of scheduled) {
|
|
179
179
|
log('INFO', ` ${job.name} -> next: ${job.nextRun?.toISOString() || 'unknown'}`);
|
|
180
180
|
}
|
|
181
|
+
// Before the BrowserService comes up, reap browser + tunnel processes
|
|
182
|
+
// spawned by previous daemons that are no longer alive. Without this,
|
|
183
|
+
// a daemon hard-crash (SIGKILL, OOM) would leak every browser and SSH
|
|
184
|
+
// tunnel it had open — and the next session would either hijack those
|
|
185
|
+
// (cdp:// profile silently driven via stale ssh tunnel) or fail to
|
|
186
|
+
// bind because the ports are still claimed.
|
|
187
|
+
try {
|
|
188
|
+
const { reapOrphanedProcesses } = await import('./browser/runtime-state.js');
|
|
189
|
+
const result = reapOrphanedProcesses();
|
|
190
|
+
if (result.reaped > 0) {
|
|
191
|
+
log('INFO', `Reaped ${result.reaped} orphan process(es) from prior daemon(s)`);
|
|
192
|
+
for (const d of result.details)
|
|
193
|
+
log('INFO', ` ${d}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
log('ERROR', `Orphan reaper failed: ${err.message}`);
|
|
198
|
+
}
|
|
181
199
|
const browserService = new BrowserService();
|
|
182
200
|
const browserIPC = new BrowserIPCServer(browserService);
|
|
183
201
|
try {
|
|
@@ -259,6 +277,14 @@ Environment=PATH=/usr/local/bin:/usr/bin:/bin:${os.homedir()}/.nvm/versions/node
|
|
|
259
277
|
WantedBy=default.target`;
|
|
260
278
|
}
|
|
261
279
|
function getAgentsBinPath() {
|
|
280
|
+
// Prefer the binary actively executing this code. `which agents` returns
|
|
281
|
+
// whatever happens to be first on PATH, which means a side-by-side dev
|
|
282
|
+
// build at ~/.local/bin would silently spawn the registry-installed
|
|
283
|
+
// daemon and run stale code. process.argv[1] is the absolute path of
|
|
284
|
+
// the JS entrypoint the user actually invoked.
|
|
285
|
+
const argv1 = process.argv[1];
|
|
286
|
+
if (argv1 && fs.existsSync(argv1))
|
|
287
|
+
return argv1;
|
|
262
288
|
try {
|
|
263
289
|
return execSync('which agents', { encoding: 'utf-8' }).trim();
|
|
264
290
|
}
|
|
@@ -299,13 +325,20 @@ function startDaemonLocked() {
|
|
|
299
325
|
execFileSync('launchctl', ['unload', plistPath], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
300
326
|
}
|
|
301
327
|
catch { /* not loaded, expected */ }
|
|
302
|
-
|
|
328
|
+
// launchctl prints `Load failed:` and exits 0 when the label is in a
|
|
329
|
+
// stuck state from a prior session — so a zero exit code isn't proof
|
|
330
|
+
// of success. If no pid materializes within the window, give up on
|
|
331
|
+
// launchd and fall through to a plain detached spawn.
|
|
332
|
+
execFileSync('launchctl', ['load', plistPath], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
303
333
|
const pid = waitForPid(3000);
|
|
304
|
-
|
|
334
|
+
if (pid)
|
|
335
|
+
return { pid, method: 'launchd' };
|
|
336
|
+
// launchctl claimed success but nothing ran. Fall through.
|
|
305
337
|
}
|
|
306
338
|
catch {
|
|
307
|
-
|
|
339
|
+
// load threw — fall through to detached spawn
|
|
308
340
|
}
|
|
341
|
+
return startDetached();
|
|
309
342
|
}
|
|
310
343
|
if (platform === 'linux') {
|
|
311
344
|
try {
|
package/dist/lib/events.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - Permissions: logs dir is 0700, files are 0600 (owner-only)
|
|
12
12
|
* - Performance tracking: withTiming() wrapper for any async function
|
|
13
13
|
*/
|
|
14
|
-
export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'secrets.get' | 'secrets.set' | 'secrets.delete' | 'cloud.dispatch' | 'cloud.complete' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'resource.sync' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'error' | 'warn' | 'info' | 'debug';
|
|
14
|
+
export type EventType = 'agent.run.start' | 'agent.run.end' | 'agent.spawn.start' | 'agent.spawn.end' | 'version.install' | 'version.switch' | 'version.remove' | 'skill.install' | 'skill.remove' | 'browser.launch' | 'browser.close' | 'browser.navigate' | 'browser.screenshot' | 'secrets.get' | 'secrets.set' | 'secrets.delete' | 'secrets.rename' | 'cloud.dispatch' | 'cloud.complete' | 'teams.create' | 'teams.add' | 'teams.start' | 'teams.complete' | 'hook.fire' | 'hook.complete' | 'hook.error' | 'resource.sync' | 'command.start' | 'command.end' | 'perf.timing' | 'session.start' | 'session.end' | 'error' | 'warn' | 'info' | 'debug';
|
|
15
15
|
export interface EventMeta {
|
|
16
16
|
ts: string;
|
|
17
17
|
tz: string;
|
package/dist/lib/help.js
CHANGED
|
@@ -23,12 +23,36 @@ function formatHelpCommandsFirst(cmd, helper) {
|
|
|
23
23
|
function formatList(textArray) {
|
|
24
24
|
return textArray.join('\n').replace(/^/gm, ' '.repeat(itemIndentWidth));
|
|
25
25
|
}
|
|
26
|
-
|
|
26
|
+
// Drop arguments flagged as hidden (deprecation / compat slots) from both
|
|
27
|
+
// the Usage line and the Arguments section. Commander v12's Argument lacks
|
|
28
|
+
// hideHelp(), so we read a custom `hidden` field that callers set directly.
|
|
29
|
+
const isHidden = (a) => a.hidden === true;
|
|
30
|
+
const registeredArgs = cmd.registeredArguments ?? [];
|
|
31
|
+
const parentNames = [];
|
|
32
|
+
for (let p = cmd.parent; p; p = p.parent)
|
|
33
|
+
parentNames.unshift(p.name());
|
|
34
|
+
const parentPrefix = parentNames.length > 0 ? parentNames.join(' ') + ' ' : '';
|
|
35
|
+
const visibleArgTokens = registeredArgs
|
|
36
|
+
.filter((a) => !isHidden(a))
|
|
37
|
+
.map((a) => {
|
|
38
|
+
const n = a.name() + (a.variadic ? '...' : '');
|
|
39
|
+
return a.required ? `<${n}>` : `[${n}]`;
|
|
40
|
+
})
|
|
41
|
+
.join(' ');
|
|
42
|
+
// commander always exposes -h/--help, so every command effectively has options.
|
|
43
|
+
// Order matches commander's default: name [options] <args> [command].
|
|
44
|
+
const argsToken = visibleArgTokens ? ` ${visibleArgTokens}` : '';
|
|
45
|
+
const commandToken = cmd.commands.length > 0 ? ' [command]' : '';
|
|
46
|
+
const usageLine = `${parentPrefix}${cmd.name()} [options]${argsToken}${commandToken}`;
|
|
47
|
+
let output = [`Usage: ${usageLine}`, ''];
|
|
27
48
|
const commandDescription = helper.commandDescription(cmd);
|
|
28
49
|
if (commandDescription.length > 0) {
|
|
29
50
|
output = output.concat([helper.wrap(commandDescription, helpWidth, 0), '']);
|
|
30
51
|
}
|
|
31
|
-
const argumentList = helper
|
|
52
|
+
const argumentList = helper
|
|
53
|
+
.visibleArguments(cmd)
|
|
54
|
+
.filter((a) => !isHidden(a))
|
|
55
|
+
.map((argument) => {
|
|
32
56
|
return formatItem(helper.argumentTerm(argument), helper.argumentDescription(argument));
|
|
33
57
|
});
|
|
34
58
|
if (argumentList.length > 0) {
|
|
@@ -36,9 +60,12 @@ function formatHelpCommandsFirst(cmd, helper) {
|
|
|
36
60
|
}
|
|
37
61
|
const visibleCommands = helper.visibleCommands(cmd);
|
|
38
62
|
const subcommandTermNoAlias = (sub) => {
|
|
39
|
-
// Mirror commander's default subcommandTerm but drop the |alias suffix
|
|
63
|
+
// Mirror commander's default subcommandTerm but drop the |alias suffix and
|
|
64
|
+
// skip arguments marked as hidden (Argument#hideHelp()), so deprecation /
|
|
65
|
+
// compatibility slots don't pollute the usage line.
|
|
40
66
|
const argList = sub.registeredArguments ?? [];
|
|
41
67
|
const args = argList
|
|
68
|
+
.filter((a) => !a.hidden)
|
|
42
69
|
.map((a) => {
|
|
43
70
|
const n = a.name() + (a.variadic ? '...' : '');
|
|
44
71
|
return a.required ? `<${n}>` : `[${n}]`;
|
|
@@ -82,6 +82,26 @@ export interface RotateOptions {
|
|
|
82
82
|
* unless `clearMeta` or a `meta` patch is supplied.
|
|
83
83
|
*/
|
|
84
84
|
export declare function rotateBundleSecret(bundle: SecretsBundle, key: string, opts: RotateOptions): void;
|
|
85
|
+
/** Options for renameBundle. */
|
|
86
|
+
export interface RenameOptions {
|
|
87
|
+
/** When true, overwrite an existing destination bundle (purges its keychain items first). */
|
|
88
|
+
force?: boolean;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Rename a bundle: move metadata + every keychain-backed value to a new name.
|
|
92
|
+
*
|
|
93
|
+
* Sequence is ordered so the source stays intact if anything in the copy
|
|
94
|
+
* phase fails:
|
|
95
|
+
* 1) read source, validate dest
|
|
96
|
+
* 2) purge dest if --force, refuse otherwise
|
|
97
|
+
* 3) copy each keychain value source -> dest
|
|
98
|
+
* 4) write new bundle metadata
|
|
99
|
+
* 5) delete the old per-key keychain items + old metadata
|
|
100
|
+
*
|
|
101
|
+
* Steps 1-4 are reversible. If 5 partially fails (e.g. iCloud Keychain
|
|
102
|
+
* hiccup), running `rename` again is a safe no-op for the source items.
|
|
103
|
+
*/
|
|
104
|
+
export declare function renameBundle(oldName: string, newName: string, opts?: RenameOptions): void;
|
|
85
105
|
export declare function keychainItemsForBundle(bundle: SecretsBundle): Array<{
|
|
86
106
|
key: string;
|
|
87
107
|
item: string;
|
|
@@ -306,6 +306,62 @@ export function rotateBundleSecret(bundle, key, opts) {
|
|
|
306
306
|
}
|
|
307
307
|
writeBundle(bundle);
|
|
308
308
|
}
|
|
309
|
+
/**
|
|
310
|
+
* Rename a bundle: move metadata + every keychain-backed value to a new name.
|
|
311
|
+
*
|
|
312
|
+
* Sequence is ordered so the source stays intact if anything in the copy
|
|
313
|
+
* phase fails:
|
|
314
|
+
* 1) read source, validate dest
|
|
315
|
+
* 2) purge dest if --force, refuse otherwise
|
|
316
|
+
* 3) copy each keychain value source -> dest
|
|
317
|
+
* 4) write new bundle metadata
|
|
318
|
+
* 5) delete the old per-key keychain items + old metadata
|
|
319
|
+
*
|
|
320
|
+
* Steps 1-4 are reversible. If 5 partially fails (e.g. iCloud Keychain
|
|
321
|
+
* hiccup), running `rename` again is a safe no-op for the source items.
|
|
322
|
+
*/
|
|
323
|
+
export function renameBundle(oldName, newName, opts = {}) {
|
|
324
|
+
validateBundleName(oldName);
|
|
325
|
+
validateBundleName(newName);
|
|
326
|
+
if (oldName === newName) {
|
|
327
|
+
throw new Error(`Bundle name unchanged ('${oldName}').`);
|
|
328
|
+
}
|
|
329
|
+
if (!bundleExists(oldName)) {
|
|
330
|
+
throw new Error(`Bundle '${oldName}' not found.`);
|
|
331
|
+
}
|
|
332
|
+
const source = readBundle(oldName);
|
|
333
|
+
if (bundleExists(newName)) {
|
|
334
|
+
if (!opts.force) {
|
|
335
|
+
throw new Error(`Bundle '${newName}' already exists. Use --force to overwrite.`);
|
|
336
|
+
}
|
|
337
|
+
const dest = readBundle(newName);
|
|
338
|
+
for (const { item } of keychainItemsForBundle(dest)) {
|
|
339
|
+
deleteKeychainToken(item, dest.icloud_sync);
|
|
340
|
+
}
|
|
341
|
+
deleteBundle(newName);
|
|
342
|
+
}
|
|
343
|
+
// Copy phase: read old item, write new item. Old items stay in place
|
|
344
|
+
// until step 5 so a partial failure here leaves the source intact.
|
|
345
|
+
const sourceItems = keychainItemsForBundle(source);
|
|
346
|
+
for (const { key, item: oldItem } of sourceItems) {
|
|
347
|
+
const raw = source.vars[key];
|
|
348
|
+
if (typeof raw !== 'string' || !raw.startsWith('keychain:'))
|
|
349
|
+
continue;
|
|
350
|
+
const shortId = raw.slice('keychain:'.length);
|
|
351
|
+
const newItem = secretsKeychainItem(newName, shortId);
|
|
352
|
+
const value = getKeychainToken(oldItem, source.icloud_sync);
|
|
353
|
+
setKeychainToken(newItem, value, source.icloud_sync);
|
|
354
|
+
}
|
|
355
|
+
// writeBundle preserves source.created_at and refreshes updated_at.
|
|
356
|
+
const renamed = { ...source, name: newName };
|
|
357
|
+
writeBundle(renamed);
|
|
358
|
+
// Cleanup: delete the old per-key keychain items, then the old metadata.
|
|
359
|
+
for (const { item: oldItem } of sourceItems) {
|
|
360
|
+
deleteKeychainToken(oldItem, source.icloud_sync);
|
|
361
|
+
}
|
|
362
|
+
deleteBundle(oldName);
|
|
363
|
+
emit('secrets.rename', { from: oldName, to: newName });
|
|
364
|
+
}
|
|
309
365
|
// Iterate all keychain-backed keys in a bundle for cleanup on rm/unset.
|
|
310
366
|
export function keychainItemsForBundle(bundle) {
|
|
311
367
|
const items = [];
|
|
@@ -182,14 +182,14 @@ export function deleteKeychainToken(item, sync = false) {
|
|
|
182
182
|
assertSupportedPlatform();
|
|
183
183
|
if (isLinux())
|
|
184
184
|
return linuxBackend.delete(item, sync);
|
|
185
|
-
// macOS
|
|
186
|
-
if (sync) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
return spawnSync(
|
|
185
|
+
// macOS: Try security first (no prompts for local items), fall back to binary for synced items.
|
|
186
|
+
if (!sync && spawnSync('security', ['delete-generic-password', '-a', os.userInfo().username, '-s', item], {
|
|
187
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
188
|
+
}).status === 0)
|
|
189
|
+
return true;
|
|
190
|
+
// Fallback: binary deletes synced items via kSecAttrSynchronizableAny
|
|
191
|
+
const bin = ensureKeychainHelper();
|
|
192
|
+
return spawnSync(bin, ['delete', item, os.userInfo().username], {
|
|
193
193
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
194
194
|
}).status === 0;
|
|
195
195
|
}
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -442,7 +442,18 @@ export interface BrowserProfileConfig {
|
|
|
442
442
|
* Only consulted when `electron` is true.
|
|
443
443
|
*/
|
|
444
444
|
targetFilter?: string;
|
|
445
|
-
|
|
445
|
+
/**
|
|
446
|
+
* Endpoint presets. Accepts two shapes for backward compatibility:
|
|
447
|
+
* - Legacy: `string[]` of CDP URLs; first entry is the default.
|
|
448
|
+
* - New: `{ [presetName]: { target, binary?, targetFilter? } }`.
|
|
449
|
+
*/
|
|
450
|
+
endpoints: string[] | Record<string, {
|
|
451
|
+
target: string;
|
|
452
|
+
binary?: string;
|
|
453
|
+
targetFilter?: string;
|
|
454
|
+
}>;
|
|
455
|
+
/** Preset name to use when `--endpoint` is not passed to `start`. */
|
|
456
|
+
defaultEndpoint?: string;
|
|
446
457
|
chrome?: {
|
|
447
458
|
headless?: boolean;
|
|
448
459
|
args?: string[];
|
|
@@ -452,6 +463,10 @@ export interface BrowserProfileConfig {
|
|
|
452
463
|
width: number;
|
|
453
464
|
height: number;
|
|
454
465
|
};
|
|
466
|
+
/** Directory holding source-side JSONL logs (e.g. ~/.rush/logs). */
|
|
467
|
+
logDir?: string;
|
|
468
|
+
/** Optional SSH host where logDir lives, e.g. "muqsit@mac-mini". */
|
|
469
|
+
logHost?: string;
|
|
455
470
|
}
|
|
456
471
|
/** Options controlling which agents and resources are synced during `agents pull` / `agents use`. */
|
|
457
472
|
export interface SyncOptions {
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the CLI version from the shipping package.json. Used by the daemon
|
|
3
|
+
* to answer `IPCAction: 'version'` and by the client to detect daemon drift —
|
|
4
|
+
* a dev-build CLI talking to a launchd-managed registry daemon would silently
|
|
5
|
+
* get stale behavior without this check.
|
|
6
|
+
*/
|
|
7
|
+
export declare function getCliVersion(): string;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
5
|
+
const __dirname = path.dirname(__filename);
|
|
6
|
+
let cached = null;
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the CLI version from the shipping package.json. Used by the daemon
|
|
9
|
+
* to answer `IPCAction: 'version'` and by the client to detect daemon drift —
|
|
10
|
+
* a dev-build CLI talking to a launchd-managed registry daemon would silently
|
|
11
|
+
* get stale behavior without this check.
|
|
12
|
+
*/
|
|
13
|
+
export function getCliVersion() {
|
|
14
|
+
if (cached)
|
|
15
|
+
return cached;
|
|
16
|
+
try {
|
|
17
|
+
const pkgPath = path.join(__dirname, '..', '..', 'package.json');
|
|
18
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
19
|
+
cached = String(pkg.version || 'unknown');
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
cached = 'unknown';
|
|
23
|
+
}
|
|
24
|
+
return cached;
|
|
25
|
+
}
|
package/package.json
CHANGED