@shardflux/cli 0.28.0 → 0.28.2
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 +54 -17
- package/README.md +91 -12
- package/dist/account-commands.d.ts +2 -4
- package/dist/account-commands.js +2 -6
- package/dist/agent-git-commands.d.ts +1 -2
- package/dist/agent-git-commands.js +2 -21
- package/dist/agent-setup.d.ts +4 -2
- package/dist/agent-setup.js +6 -4
- package/dist/agent-shell.d.ts +2 -0
- package/dist/agent-shell.js +5 -0
- package/dist/args.d.ts +1 -1
- package/dist/args.js +5 -1
- package/dist/board-args.js +7 -0
- package/dist/board-commands.d.ts +1 -1
- package/dist/board-commands.js +46 -6
- package/dist/cli-proxy.d.ts +1 -13
- package/dist/cli-proxy.js +2 -27
- package/dist/codex-cloud.js +3 -1
- package/dist/commands.d.ts +12 -0
- package/dist/commands.js +121 -4
- package/dist/conversations.d.ts +2 -0
- package/dist/conversations.js +1 -1
- package/dist/device-bridge.d.ts +4 -1
- package/dist/device-bridge.js +9 -2
- package/dist/device-folders.d.ts +2 -1
- package/dist/device-folders.js +3 -3
- package/dist/format.js +3 -0
- package/dist/forwarder-refresh.d.ts +15 -0
- package/dist/forwarder-refresh.js +32 -0
- package/dist/handler-loader.d.ts +3 -0
- package/dist/handler-loader.js +43 -0
- package/dist/land.d.ts +1 -1
- package/dist/land.js +6 -5
- package/dist/linux-helper.js +1 -2
- package/dist/machine-setup-guard.d.ts +9 -0
- package/dist/machine-setup-guard.js +22 -0
- package/dist/machine-setup.d.ts +1 -8
- package/dist/machine-setup.js +2 -22
- package/dist/main.js +17 -16
- package/dist/orchestrator-commands.js +1 -3
- package/dist/startup-housekeeping.d.ts +5 -0
- package/dist/startup-housekeeping.js +30 -0
- package/dist/t3-provider.js +4 -2
- package/dist/worktree-twin-engine.d.ts +1 -1
- package/dist/worktree-twin-engine.js +9 -6
- package/package.json +12 -7
package/dist/board-commands.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { findOrg, resolveOrg } from "./account-scope.js";
|
|
2
|
+
import { out, principal, str, strs, readSecretValue, askSecretValue } from "./commands.js";
|
|
2
3
|
import { detailLine, pickAnswers } from "./conversation-commands.js";
|
|
3
4
|
import { NotFoundError, UsageError } from "./errors.js";
|
|
4
5
|
import { time } from "./format.js";
|
|
@@ -36,8 +37,8 @@ export function taskLine(t) {
|
|
|
36
37
|
* request your orchestrator made to your computer while it was offline reads "Waiting for your computer: list ~/code".
|
|
37
38
|
*/
|
|
38
39
|
export function needsLine(n) {
|
|
39
|
-
const detail = n.permission ? detailLine(n.permission.detail) : n.options?.length ? n.options.join(' / ') : n.land ? n.land.branch : n.device ? 'cancel drops it' : '';
|
|
40
|
-
return `${n.id}\t${oneLine(n.ask, 100)}${detail ? `\t${detail}` : ''}`;
|
|
40
|
+
const detail = n.credential?.kind === 'environment' ? [n.credential.name, n.credential.suggested_scope].filter(Boolean).join(' · ') : n.permission ? detailLine(n.permission.detail) : n.options?.length ? n.options.map(o => n.option_details?.[o] ? `${o}: ${n.option_details[o]}` : o).join(' / ') : n.land ? n.land.branch : n.device ? 'cancel drops it' : '';
|
|
41
|
+
return `${n.id}\t${oneLine(n.ask, n.kind === 'question' || n.credential?.kind === 'environment' ? Infinity : 100)}${detail ? `\t${detail}` : ''}`;
|
|
41
42
|
}
|
|
42
43
|
/** A sealed report as --follow prints it: id, milestone, title, then its evidence labels. */
|
|
43
44
|
export function reportLine(r) {
|
|
@@ -140,7 +141,7 @@ export function taskDetailText(d) {
|
|
|
140
141
|
if (r.outcome.trim())
|
|
141
142
|
lines.push(` ${whole(r.outcome)}`);
|
|
142
143
|
if (r.options?.length)
|
|
143
|
-
lines.push(` Options: ${r.options.join(' / ')}`);
|
|
144
|
+
lines.push(` Options: ${r.options.map(o => r.option_details?.[o] ? `${o}: ${r.option_details[o]}` : o).join(' / ')}`);
|
|
144
145
|
if (r.evidence.length)
|
|
145
146
|
lines.push(` Evidence: ${r.evidence.map(evidenceText).join('; ')}`);
|
|
146
147
|
if (r.media.length)
|
|
@@ -166,8 +167,8 @@ function orderOf(values) {
|
|
|
166
167
|
return first ? null : after;
|
|
167
168
|
}
|
|
168
169
|
// ---- needs you -------------------------------------------------------------------------------
|
|
169
|
-
const KINDS = { perm: 'permission', cq: 'question', ask: 'question', oq: 'question', cred: 'credential', land: 'land', prune: 'prune', dev: 'device' };
|
|
170
|
-
/** An item's kind from its id (perm_, cq_, ask_, oq_, cred_, land_, prune_, dev_); undefined for another id. */
|
|
170
|
+
const KINDS = { perm: 'permission', cq: 'question', ask: 'question', oq: 'question', cred: 'credential', acred: 'credential', land: 'land', prune: 'prune', dev: 'device' };
|
|
171
|
+
/** An item's kind from its id (perm_, cq_, ask_, oq_, cred_, acred_, land_, prune_, dev_); undefined for another id. */
|
|
171
172
|
export function itemKind(id) {
|
|
172
173
|
return KINDS[id.slice(0, Math.max(0, id.indexOf('_')))];
|
|
173
174
|
}
|
|
@@ -364,6 +365,45 @@ export const BOARD_HANDLERS = {
|
|
|
364
365
|
return 0;
|
|
365
366
|
},
|
|
366
367
|
async 'needs-you answer'(ctx, values, [id, word]) {
|
|
368
|
+
if (values.credential === true) {
|
|
369
|
+
if (!id?.startsWith('acred_') || word !== undefined || values.text !== undefined || values.pick !== undefined || values.keep !== undefined || values.everywhere === true) {
|
|
370
|
+
throw new UsageError('Use --credential for an environment request, without a decision or chat answer.');
|
|
371
|
+
}
|
|
372
|
+
const hasKey = Boolean(ctx.io.env.SHARDFLUX_API_KEY?.trim()) || ctx.store.profile(ctx.apiUrl).api_key !== undefined;
|
|
373
|
+
const scope = str(values.scope);
|
|
374
|
+
if (scope !== undefined && !['agents', 'project', 'conversation'].includes(scope))
|
|
375
|
+
throw new UsageError('--scope must be agents, project or conversation');
|
|
376
|
+
const signed = scope === 'agents' || !hasKey;
|
|
377
|
+
let cloud = signed ? ctx.account() : ctx.cloud();
|
|
378
|
+
let organizationId = str(values.org);
|
|
379
|
+
if (signed) {
|
|
380
|
+
const org = organizationId ? await findOrg(ctx.account(), organizationId) : await resolveOrg(ctx, ctx.account(), {});
|
|
381
|
+
organizationId = org.id;
|
|
382
|
+
}
|
|
383
|
+
// Inspect the request before reading a file, stdin or opening the secure page.
|
|
384
|
+
const item = (await cloud.needsYou.list({ ...(organizationId ? { organizationId } : {}), signal: ctx.signal })).find(n => n.id === id);
|
|
385
|
+
if (!item || item.kind !== 'credential' || item.credential?.kind !== 'environment' || !item.credential.name)
|
|
386
|
+
throw new NotFoundError('Nothing waits for that environment credential.');
|
|
387
|
+
const target = scope ?? item.credential.suggested_scope ?? 'project';
|
|
388
|
+
if (target === 'agents' && !signed) {
|
|
389
|
+
organizationId ??= (await principal(ctx)).organization_id;
|
|
390
|
+
cloud = ctx.account();
|
|
391
|
+
await findOrg(ctx.account(), organizationId);
|
|
392
|
+
}
|
|
393
|
+
const save = (value) => {
|
|
394
|
+
ctx.addSecret(value);
|
|
395
|
+
return cloud.needsYou.answer(id, { credential: { value, scope: target } }, { ...(organizationId ? { organizationId } : {}), signal: ctx.signal });
|
|
396
|
+
};
|
|
397
|
+
const value = await readSecretValue(ctx, values, item.credential.name);
|
|
398
|
+
const r = value !== null ? await save(value) : await askSecretValue(ctx, values, {
|
|
399
|
+
title: `Provide ${item.credential.name}`, purpose: item.credential.purpose ?? item.ask, name: item.credential.name,
|
|
400
|
+
}, save);
|
|
401
|
+
out(ctx, `Credential ${item.credential.name} is available (${target}).\n`, r);
|
|
402
|
+
return 0;
|
|
403
|
+
}
|
|
404
|
+
if (values.scope !== undefined || values.ask === true || values['from-file'] !== undefined || values.org !== undefined || values.timeout !== undefined) {
|
|
405
|
+
throw new UsageError('Use --credential for a secure environment credential answer.');
|
|
406
|
+
}
|
|
367
407
|
const input = { word, text: str(values.text), picks: strs(values.pick), keep: strs(values.keep), everywhere: values.everywhere === true };
|
|
368
408
|
const cloud = boardClient(ctx);
|
|
369
409
|
// Land merges here (shard task land) and records itself after; the board only takes keep and discard.
|
package/dist/cli-proxy.d.ts
CHANGED
|
@@ -16,8 +16,7 @@ export declare const cliProxyTools: {
|
|
|
16
16
|
inVm: () => boolean;
|
|
17
17
|
kill: (pid: number) => void;
|
|
18
18
|
};
|
|
19
|
-
|
|
20
|
-
export declare const FORWARD_LABEL = "dev.shardflux.cli-proxy-forward";
|
|
19
|
+
export { FORWARD_LABEL, forwarderPlistPath, refreshForwarderAgent } from './forwarder-refresh.js';
|
|
21
20
|
/** Where every VM finds the hub info (the machine template carries it). */
|
|
22
21
|
export declare const VM_HUB_INFO_PATH = "/home/user/.config/shardflux-cli-proxy.json";
|
|
23
22
|
export interface LocalLogin {
|
|
@@ -151,7 +150,6 @@ export interface PlistOptions {
|
|
|
151
150
|
}
|
|
152
151
|
/** The launchd agent: `shard cli-proxy forward` at login, started again whenever it exits. */
|
|
153
152
|
export declare function forwarderPlist(o: PlistOptions): string;
|
|
154
|
-
export declare function forwarderPlistPath(home: string): string;
|
|
155
153
|
/** Whether Shardflux's forwarder answers on 127.0.0.1:port, and for which hub. */
|
|
156
154
|
export interface ForwarderIdentity {
|
|
157
155
|
hub: string | null; /** `mac`: shard cli-proxy forward; `vm`: sf-agent cli-proxy-forward. */
|
|
@@ -168,16 +166,6 @@ export interface LaunchdDeps {
|
|
|
168
166
|
identity?: () => Promise<ForwarderIdentity | null>;
|
|
169
167
|
sleep?: (ms: number) => Promise<void>;
|
|
170
168
|
}
|
|
171
|
-
/**
|
|
172
|
-
* After `shard` is updated, the launchd forwarder still runs the program it started with. Called on every shard run
|
|
173
|
-
* (one file read on macOS): an installed forwarder agent recorded for another shard version is started again, on this
|
|
174
|
-
* one. Returns whether it was.
|
|
175
|
-
*/
|
|
176
|
-
export declare function refreshForwarderAgent(env: Record<string, string | undefined>, dir: string, deps?: {
|
|
177
|
-
run?: ProgramRunner;
|
|
178
|
-
uid?: number;
|
|
179
|
-
platform?: NodeJS.Platform;
|
|
180
|
-
}): Promise<boolean>;
|
|
181
169
|
/** Installs (or refreshes) the launchd agent and waits until the forwarder answers. macOS only. */
|
|
182
170
|
export declare function installForwarderAgent(ctx: Ctx, apiUrl: string, deps?: LaunchdDeps): Promise<string>;
|
|
183
171
|
/**
|
package/dist/cli-proxy.js
CHANGED
|
@@ -47,7 +47,8 @@ export const cliProxyTools = {
|
|
|
47
47
|
kill: (pid) => { process.kill(pid, 'SIGTERM'); },
|
|
48
48
|
};
|
|
49
49
|
/** The launchd agent that keeps the forwarder on 127.0.0.1:8317 on a Mac. */
|
|
50
|
-
|
|
50
|
+
import { FORWARD_LABEL, forwarderPlistPath, recordForwarder } from "./forwarder-refresh.js";
|
|
51
|
+
export { FORWARD_LABEL, forwarderPlistPath, refreshForwarderAgent } from "./forwarder-refresh.js";
|
|
51
52
|
/** Where every VM finds the hub info (the machine template carries it). */
|
|
52
53
|
// Outside ~/.config/shardflux, which machines and templates never capture (the platform's per-VM state).
|
|
53
54
|
export const VM_HUB_INFO_PATH = '/home/user/.config/shardflux-cli-proxy.json';
|
|
@@ -470,9 +471,6 @@ ${Object.entries(env).map(([k, v]) => ` <key>${k}</key>\n <string>${xml(v)
|
|
|
470
471
|
</plist>
|
|
471
472
|
`;
|
|
472
473
|
}
|
|
473
|
-
export function forwarderPlistPath(home) {
|
|
474
|
-
return join(home, 'Library', 'LaunchAgents', `${FORWARD_LABEL}.plist`);
|
|
475
|
-
}
|
|
476
474
|
/** Whether Shardflux's forwarder (this computer's, or a VM's sf-agent) answers on 127.0.0.1:port, and for which hub. */
|
|
477
475
|
export async function forwarderIdentity(port = CLI_PROXY_PORT) {
|
|
478
476
|
try {
|
|
@@ -492,29 +490,6 @@ export function inShardfluxVm() {
|
|
|
492
490
|
}
|
|
493
491
|
/** The forwarder that answers is this shard's (an update replaces the program, not the running process). */
|
|
494
492
|
const current = (id) => id !== null && id.kind === 'mac' && id.version === CLI_VERSION;
|
|
495
|
-
/** The shard version the launchd forwarder runs (refreshForwarderAgent compares it with this one). */
|
|
496
|
-
function recordForwarder(dir) {
|
|
497
|
-
new AgentStateStore(dir).update((doc) => { doc.cli_proxy_forwarder = { version: CLI_VERSION }; });
|
|
498
|
-
}
|
|
499
|
-
/**
|
|
500
|
-
* After `shard` is updated, the launchd forwarder still runs the program it started with. Called on every shard run
|
|
501
|
-
* (one file read on macOS): an installed forwarder agent recorded for another shard version is started again, on this
|
|
502
|
-
* one. Returns whether it was.
|
|
503
|
-
*/
|
|
504
|
-
export async function refreshForwarderAgent(env, dir, deps = {}) {
|
|
505
|
-
if ((deps.platform ?? process.platform) !== 'darwin')
|
|
506
|
-
return false;
|
|
507
|
-
if (!existsSync(forwarderPlistPath(env.HOME || homedir())))
|
|
508
|
-
return false;
|
|
509
|
-
const store = new AgentStateStore(dir);
|
|
510
|
-
const record = store.read().cli_proxy_forwarder;
|
|
511
|
-
if (record?.version === CLI_VERSION)
|
|
512
|
-
return false;
|
|
513
|
-
// Recorded first: a failed restart is not retried on every command (the next import installs the agent again).
|
|
514
|
-
recordForwarder(dir);
|
|
515
|
-
await (deps.run ?? cliProxyTools.run)('launchctl', ['kickstart', '-k', `gui/${deps.uid ?? userInfo().uid}/${FORWARD_LABEL}`]);
|
|
516
|
-
return true;
|
|
517
|
-
}
|
|
518
493
|
/** Installs (or refreshes) the launchd agent and waits until the forwarder answers. macOS only. */
|
|
519
494
|
export async function installForwarderAgent(ctx, apiUrl, deps = {}) {
|
|
520
495
|
const runtime = ctx.runtime;
|
package/dist/codex-cloud.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
|
-
import { ShardfluxApiError, codexAccountMatches, localCodexLogin } from '@shardflux/sdk';
|
|
3
|
+
import { CodexAdmissionStoppedError, ShardfluxApiError, codexAccountMatches, localCodexLogin } from '@shardflux/sdk';
|
|
4
4
|
import { CodedError, UsageError } from "./errors.js";
|
|
5
5
|
import { json } from "./format.js";
|
|
6
6
|
import { syncAgentCredentials } from "./agent-credentials.js";
|
|
@@ -311,6 +311,8 @@ export async function openCodexConversation(ctx, c, history) {
|
|
|
311
311
|
if (task.state !== 'canceled')
|
|
312
312
|
task = await ctx.cloud().codex.waitTask(c.id, { timeoutMs: ctx.wakeTimeoutMs, signal: ctx.signal });
|
|
313
313
|
}
|
|
314
|
+
if (task.state === 'attention' && task.next_attempt_at === null)
|
|
315
|
+
throw new CodexAdmissionStoppedError(task);
|
|
314
316
|
if (task.state === 'canceled')
|
|
315
317
|
throw new UsageError('This conversation’s Codex executor was removed. Start a new Cloud conversation; its saved branch and history remain available.');
|
|
316
318
|
if (task.state !== 'active' || !task.thread_id)
|
package/dist/commands.d.ts
CHANGED
|
@@ -152,4 +152,16 @@ export declare function withConversations(rows: import('@shardflux/sdk').Agent[]
|
|
|
152
152
|
question?: string | null;
|
|
153
153
|
}[] | null): ThreadRow[];
|
|
154
154
|
export declare const KEY_HANDLERS: Record<string, Handler>;
|
|
155
|
+
/**
|
|
156
|
+
* The secret value (never from arguments): --from-file, piped standard input, or a no-echo prompt on a terminal. Null
|
|
157
|
+
* when the person types it on the page shard opens instead (0.20.1+): --ask, or a coding agent's shell with nothing
|
|
158
|
+
* piped (a terminal there is not the person's).
|
|
159
|
+
*/
|
|
160
|
+
export declare function readSecretValue(ctx: Ctx, values: Values, name: string): Promise<string | null>;
|
|
161
|
+
/** The secret's value from the page shard opens, handed straight to `save` (create or rotate); the page shows its outcome. */
|
|
162
|
+
export declare function askSecretValue<T>(ctx: Ctx, values: Values, page: {
|
|
163
|
+
title: string;
|
|
164
|
+
purpose: string;
|
|
165
|
+
name: string;
|
|
166
|
+
}, save: (value: string) => Promise<T>): Promise<T>;
|
|
155
167
|
export {};
|
package/dist/commands.js
CHANGED
|
@@ -2345,17 +2345,20 @@ export const KEY_HANDLERS = {
|
|
|
2345
2345
|
if (keychain && from !== 'keychain')
|
|
2346
2346
|
throw new UsageError('--keychain applies to the keychain vault');
|
|
2347
2347
|
const targets = secretNames(names.map(n => alias ?? n), '--as');
|
|
2348
|
-
const
|
|
2348
|
+
const agent = values.scope !== undefined ? await agentSecretContext(ctx, values) : null;
|
|
2349
|
+
const project = agent ? null : (await principal(ctx)).project_id;
|
|
2349
2350
|
const imported = [];
|
|
2350
2351
|
for (const [i, name] of names.entries()) {
|
|
2351
2352
|
const value = await vaultValue(from, name, { signal: ctx.signal, ...(keychain ? { keychain } : {}) });
|
|
2352
2353
|
ctx.addSecret(value);
|
|
2353
|
-
imported.push(await ctx.cloud().secrets.create(project, { name: targets[i], value }));
|
|
2354
|
+
imported.push(agent ? await agent.cloud.secrets.createAgent(agent.target, { name: targets[i], value }) : await ctx.cloud().secrets.create(project, { name: targets[i], value }));
|
|
2354
2355
|
}
|
|
2355
2356
|
out(ctx, `Imported ${imported.map(s => s.name).join(', ')}. ${SECRET_SECURITY_NOTE}\n`, { data: imported, security_note: SECRET_SECURITY_NOTE });
|
|
2356
2357
|
return 0;
|
|
2357
2358
|
},
|
|
2358
2359
|
async 'secrets create'(ctx, values, [name]) {
|
|
2360
|
+
if (values.scope !== undefined)
|
|
2361
|
+
return agentSecretCommand(ctx, values, 'create', name);
|
|
2359
2362
|
if (!SECRET_NAME.test(name))
|
|
2360
2363
|
throw new UsageError(`<name> must be an environment variable name like OPENAI_API_KEY (^[A-Z_][A-Z0-9_]*$), got "${name}"`);
|
|
2361
2364
|
const scope = scopeFlag(values);
|
|
@@ -2377,6 +2380,8 @@ export const KEY_HANDLERS = {
|
|
|
2377
2380
|
return 0;
|
|
2378
2381
|
},
|
|
2379
2382
|
async 'secrets list'(ctx, values) {
|
|
2383
|
+
if (values.scope !== undefined)
|
|
2384
|
+
return agentSecretCommand(ctx, values, 'list', undefined);
|
|
2380
2385
|
const cloud = ctx.cloud();
|
|
2381
2386
|
const org = scopeFlag(values);
|
|
2382
2387
|
const project = org === null ? (await principal(ctx)).project_id : null;
|
|
@@ -2404,11 +2409,15 @@ export const KEY_HANDLERS = {
|
|
|
2404
2409
|
return 0;
|
|
2405
2410
|
},
|
|
2406
2411
|
async 'secrets get'(ctx, values, [ref]) {
|
|
2412
|
+
if (values.scope !== undefined)
|
|
2413
|
+
return agentSecretCommand(ctx, values, 'get', ref);
|
|
2407
2414
|
const s = await resolveSecret(ctx, values, ref);
|
|
2408
2415
|
out(ctx, secretDetail(s), s);
|
|
2409
2416
|
return 0;
|
|
2410
2417
|
},
|
|
2411
2418
|
async 'secrets update'(ctx, values, [ref]) {
|
|
2419
|
+
if (values.scope !== undefined)
|
|
2420
|
+
return agentSecretCommand(ctx, values, 'update', ref);
|
|
2412
2421
|
const s = await resolveSecret(ctx, values, ref);
|
|
2413
2422
|
const params = { ...permissionParams(values, s.scope, 'update') };
|
|
2414
2423
|
const description = str(values.description);
|
|
@@ -2421,6 +2430,8 @@ export const KEY_HANDLERS = {
|
|
|
2421
2430
|
return 0;
|
|
2422
2431
|
},
|
|
2423
2432
|
async 'secrets rotate'(ctx, values, [ref]) {
|
|
2433
|
+
if (values.scope !== undefined)
|
|
2434
|
+
return agentSecretCommand(ctx, values, 'rotate', ref);
|
|
2424
2435
|
const value = await readSecretValue(ctx, values, ref);
|
|
2425
2436
|
const s = await resolveSecret(ctx, values, ref);
|
|
2426
2437
|
const rotate = (secret) => ctx.cloud().secrets.rotate(s.id, secret);
|
|
@@ -2433,6 +2444,8 @@ export const KEY_HANDLERS = {
|
|
|
2433
2444
|
return 0;
|
|
2434
2445
|
},
|
|
2435
2446
|
async 'secrets versions'(ctx, values, [ref]) {
|
|
2447
|
+
if (values.scope !== undefined)
|
|
2448
|
+
return agentSecretCommand(ctx, values, 'versions', ref);
|
|
2436
2449
|
const s = await resolveSecret(ctx, values, ref);
|
|
2437
2450
|
const limitText = str(values.limit);
|
|
2438
2451
|
const page = await ctx.cloud().secrets.versions(s.id, limitText ? { limit: parsePositiveInt(limitText, 'limit', 200) } : {});
|
|
@@ -2440,6 +2453,8 @@ export const KEY_HANDLERS = {
|
|
|
2440
2453
|
return 0;
|
|
2441
2454
|
},
|
|
2442
2455
|
async 'secrets delete'(ctx, values, [ref]) {
|
|
2456
|
+
if (values.scope !== undefined)
|
|
2457
|
+
return agentSecretCommand(ctx, values, 'delete', ref);
|
|
2443
2458
|
if (values.yes !== true)
|
|
2444
2459
|
throw new UsageError('refusing to delete without --yes (the values are erased and the name is removed from every workspace binding)');
|
|
2445
2460
|
const s = await resolveSecret(ctx, values, ref);
|
|
@@ -2448,6 +2463,8 @@ export const KEY_HANDLERS = {
|
|
|
2448
2463
|
return 0;
|
|
2449
2464
|
},
|
|
2450
2465
|
async 'secrets access-log'(ctx, values, [ref]) {
|
|
2466
|
+
if (values.scope !== undefined)
|
|
2467
|
+
return agentSecretCommand(ctx, values, 'access-log', ref);
|
|
2451
2468
|
const outcome = str(values.outcome);
|
|
2452
2469
|
if (outcome !== undefined && !['granted', 'denied', 'failed'].includes(outcome))
|
|
2453
2470
|
throw new UsageError('--outcome must be granted, denied or failed');
|
|
@@ -2549,6 +2566,106 @@ function updateLine(u, d) {
|
|
|
2549
2566
|
}
|
|
2550
2567
|
}
|
|
2551
2568
|
/** `--org <id>`: organization scope; null: the API key's project. */
|
|
2569
|
+
async function agentSecretContext(ctx, values) {
|
|
2570
|
+
const scope = str(values.scope);
|
|
2571
|
+
if (!['agents', 'project', 'conversation'].includes(scope ?? ''))
|
|
2572
|
+
throw new UsageError('--scope must be agents, project or conversation');
|
|
2573
|
+
const restrictions = ['tool', 'allow-workspace', 'any-workspace', 'allow-project', 'all-projects', 'clear-projects'];
|
|
2574
|
+
if (restrictions.some(k => Array.isArray(values[k]) ? values[k].length > 0 : values[k] !== undefined && values[k] !== false))
|
|
2575
|
+
throw new UsageError('Workspace tool permissions apply to workspace-bound secrets. Agent credentials use their agents, project or conversation scope.');
|
|
2576
|
+
if (scope === 'agents' && (values.project !== undefined || values.conversation !== undefined))
|
|
2577
|
+
throw new UsageError('--scope agents takes --org; use a local scope for --project or --conversation.');
|
|
2578
|
+
if (scope === 'project' && values.conversation !== undefined)
|
|
2579
|
+
throw new UsageError('Use --scope conversation with --conversation.');
|
|
2580
|
+
const hasKey = Boolean(ctx.io.env.SHARDFLUX_API_KEY?.trim()) || ctx.store.profile(ctx.apiUrl).api_key !== undefined;
|
|
2581
|
+
const cloud = scope === 'agents' || !hasKey ? ctx.account() : ctx.cloud();
|
|
2582
|
+
if (scope === 'agents') {
|
|
2583
|
+
const account = ctx.account(), org = str(values.org);
|
|
2584
|
+
const organization = org ? await findOrg(account, org) : await resolveOrg(ctx, account, {});
|
|
2585
|
+
return { cloud, target: { scope, organizationId: organization.id } };
|
|
2586
|
+
}
|
|
2587
|
+
const localOrg = str(values.org);
|
|
2588
|
+
if (localOrg !== undefined && !UUID.test(localOrg))
|
|
2589
|
+
throw new UsageError('--org must be an organization id for a local credential.');
|
|
2590
|
+
const organization = localOrg ? { organizationId: localOrg } : {};
|
|
2591
|
+
if (scope === 'conversation') {
|
|
2592
|
+
const conversationId = str(values.conversation);
|
|
2593
|
+
if (!conversationId)
|
|
2594
|
+
throw new UsageError('--conversation is required with --scope conversation');
|
|
2595
|
+
return { cloud, target: { scope, conversationId, ...organization, ...(str(values.project) ? { projectId: str(values.project) } : {}) } };
|
|
2596
|
+
}
|
|
2597
|
+
const projectId = str(values.project) ?? (hasKey ? (await principal(ctx)).project_id : undefined);
|
|
2598
|
+
if (!projectId)
|
|
2599
|
+
throw new UsageError('--project is required with a signed-in session and --scope project');
|
|
2600
|
+
return { cloud, target: { scope: 'project', projectId, ...organization } };
|
|
2601
|
+
}
|
|
2602
|
+
async function agentSecretCommand(ctx, values, action, ref) {
|
|
2603
|
+
const { cloud, target } = await agentSecretContext(ctx, values);
|
|
2604
|
+
if (action === 'list' && (values.limit !== undefined || values.cursor !== undefined || values['include-deleted'] === true))
|
|
2605
|
+
throw new UsageError('Agent credential lists return all active names. Pagination and deleted-history flags apply to workspace-bound secrets.');
|
|
2606
|
+
const describe = (r) => `${r.name} (${r.id}) · ${r.scope} · version ${r.current_version}\n`;
|
|
2607
|
+
if (action === 'list') {
|
|
2608
|
+
const data = await cloud.secrets.listAgents(target);
|
|
2609
|
+
out(ctx, data.map(describe).join('') || 'No credentials.\n', { data });
|
|
2610
|
+
return 0;
|
|
2611
|
+
}
|
|
2612
|
+
if (action === 'create') {
|
|
2613
|
+
if (!SECRET_NAME.test(ref ?? ''))
|
|
2614
|
+
throw new UsageError('Use an environment variable name for the credential.');
|
|
2615
|
+
const value = await readSecretValue(ctx, values, ref), save = (value) => { ctx.addSecret(value); return cloud.secrets.createAgent(target, { name: ref, value, ...(str(values.description) !== undefined ? { description: str(values.description) } : {}) }); };
|
|
2616
|
+
const r = value !== null ? await save(value) : await askSecretValue(ctx, values, { title: `Add ${ref}`, purpose: `Saved encrypted for your agents as ${ref}.`, name: ref }, save);
|
|
2617
|
+
out(ctx, describe(r), r);
|
|
2618
|
+
return 0;
|
|
2619
|
+
}
|
|
2620
|
+
const r = UUID.test(ref ?? '') ? await cloud.secrets.getAgent(ref) : (await cloud.secrets.listAgents(target)).find(r => r.name === ref);
|
|
2621
|
+
if (!r)
|
|
2622
|
+
throw new NotFoundError(`No credential named ${ref} in this scope.`);
|
|
2623
|
+
// An id is still checked against the explicitly named target rather than silently operating in another scope.
|
|
2624
|
+
if (r.scope !== target.scope || target.organizationId && r.organization_id !== target.organizationId || target.projectId && r.project_id !== target.projectId || target.conversationId && r.conversation_id !== target.conversationId)
|
|
2625
|
+
throw new NotFoundError('That credential belongs to another scope.');
|
|
2626
|
+
if (action === 'get') {
|
|
2627
|
+
out(ctx, describe(r), r);
|
|
2628
|
+
return 0;
|
|
2629
|
+
}
|
|
2630
|
+
if (action === 'update') {
|
|
2631
|
+
const description = str(values.description);
|
|
2632
|
+
if (description === undefined)
|
|
2633
|
+
throw new UsageError('Pass --description to update an agent credential.');
|
|
2634
|
+
const changed = await cloud.secrets.updateAgent(r.id, description);
|
|
2635
|
+
out(ctx, describe(changed), changed);
|
|
2636
|
+
return 0;
|
|
2637
|
+
}
|
|
2638
|
+
if (action === 'rotate') {
|
|
2639
|
+
const value = await readSecretValue(ctx, values, r.name), save = (value) => { ctx.addSecret(value); return cloud.secrets.rotateAgent(r.id, value); };
|
|
2640
|
+
const changed = value !== null ? await save(value) : await askSecretValue(ctx, values, { title: `New value for ${r.name}`, purpose: `Replaces the encrypted credential ${r.name}.`, name: r.name }, save);
|
|
2641
|
+
out(ctx, describe(changed), changed);
|
|
2642
|
+
return 0;
|
|
2643
|
+
}
|
|
2644
|
+
if (action === 'delete') {
|
|
2645
|
+
if (values.yes !== true)
|
|
2646
|
+
throw new UsageError('Pass --yes to erase this credential.');
|
|
2647
|
+
await cloud.secrets.deleteAgent(r.id);
|
|
2648
|
+
out(ctx, `Deleted ${r.name}.\n`, { deleted: true, id: r.id, name: r.name, scope: r.scope });
|
|
2649
|
+
return 0;
|
|
2650
|
+
}
|
|
2651
|
+
if (action === 'versions') {
|
|
2652
|
+
const limit = str(values.limit);
|
|
2653
|
+
const page = await cloud.secrets.versions(r.id, limit ? { limit: parsePositiveInt(limit, 'limit', 200) } : {});
|
|
2654
|
+
out(ctx, secretVersionTable(page.data), { data: page.data, next_cursor: page.nextCursor });
|
|
2655
|
+
return 0;
|
|
2656
|
+
}
|
|
2657
|
+
if (action === 'access-log') {
|
|
2658
|
+
const outcome = str(values.outcome), workspace = str(values.workspace), limit = str(values.limit);
|
|
2659
|
+
if (outcome !== undefined && !['granted', 'denied', 'failed'].includes(outcome))
|
|
2660
|
+
throw new UsageError('--outcome must be granted, denied or failed');
|
|
2661
|
+
if (workspace !== undefined && !UUID.test(workspace))
|
|
2662
|
+
throw new UsageError('--workspace must be a workspace id');
|
|
2663
|
+
const page = await cloud.secrets.accessEvents(r.id, { ...(outcome ? { outcome: outcome } : {}), ...(workspace ? { workspaceId: workspace } : {}), ...(limit ? { limit: parsePositiveInt(limit, 'limit', 200) } : {}) });
|
|
2664
|
+
out(ctx, JSON.stringify(page.data) + '\n', { data: page.data, next_cursor: page.nextCursor });
|
|
2665
|
+
return 0;
|
|
2666
|
+
}
|
|
2667
|
+
throw new UsageError('Unknown credential action.');
|
|
2668
|
+
}
|
|
2552
2669
|
function scopeFlag(values) {
|
|
2553
2670
|
const org = str(values.org);
|
|
2554
2671
|
if (org === undefined)
|
|
@@ -2599,7 +2716,7 @@ function permissionParams(values, scope, mode) {
|
|
|
2599
2716
|
* when the person types it on the page shard opens instead (0.20.1+): --ask, or a coding agent's shell with nothing
|
|
2600
2717
|
* piped (a terminal there is not the person's).
|
|
2601
2718
|
*/
|
|
2602
|
-
async function readSecretValue(ctx, values, name) {
|
|
2719
|
+
export async function readSecretValue(ctx, values, name) {
|
|
2603
2720
|
const file = str(values['from-file']);
|
|
2604
2721
|
if (values.ask === true) {
|
|
2605
2722
|
if (file !== undefined)
|
|
@@ -2643,7 +2760,7 @@ function secretText(value) {
|
|
|
2643
2760
|
return text;
|
|
2644
2761
|
}
|
|
2645
2762
|
/** The secret's value from the page shard opens, handed straight to `save` (create or rotate); the page shows its outcome. */
|
|
2646
|
-
async function askSecretValue(ctx, values, page, save) {
|
|
2763
|
+
export async function askSecretValue(ctx, values, page, save) {
|
|
2647
2764
|
const asked = await askInBrowser(ctx, {
|
|
2648
2765
|
kind: 'secret',
|
|
2649
2766
|
title: page.title,
|
package/dist/conversations.d.ts
CHANGED
package/dist/conversations.js
CHANGED
|
@@ -250,7 +250,7 @@ export async function createConversationRecord(ctx, folder, harness, route, name
|
|
|
250
250
|
capture.catch(() => undefined);
|
|
251
251
|
// The title marker this computer chose (`shard settings --title-prefix`, 0.20.1+); none sent means the platform's.
|
|
252
252
|
const titlePrefix = storedTitlePrefix(ctx.store.dir);
|
|
253
|
-
const response = await conversationClient(ctx).conversations.create({ repoRemote: project.remote ?? `file://${project.folder}`, harness, route, mode: 'new',
|
|
253
|
+
const response = await conversationClient(ctx).conversations.create({ fromConversationId: ctx.io.env.SF_CONVERSATION_ID, repoRemote: project.remote ?? `file://${project.folder}`, harness, route, mode: 'new',
|
|
254
254
|
base: { commit, ...(branch ? { branch } : {}) }, ...(name ? { name } : {}), ...choice, projectId: ctx.store.profile(ctx.apiUrl).project_id,
|
|
255
255
|
...(titlePrefix !== undefined ? { titlePrefix } : {}),
|
|
256
256
|
...(movedIn ? { harnessSessionId: movedIn.session_id, ...(movedIn.title ? { title: movedIn.title.slice(0, 200) } : {}) } : {}) }, { idempotencyKey, signal: ctx.signal });
|
package/dist/device-bridge.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { CellClient, Workspace } from '@shardflux/sdk';
|
|
|
2
2
|
import type { Ctx } from './commands.js';
|
|
3
3
|
import type { Conversation } from './conversations.js';
|
|
4
4
|
import type { DirListing, LinkedProject } from './device-folders.js';
|
|
5
|
+
import type { HubSeed } from './project-hub-seed.js';
|
|
5
6
|
export type DeviceChoice = 'once' | 'always' | 'deny';
|
|
6
7
|
export interface DeviceRequest {
|
|
7
8
|
id: string;
|
|
@@ -121,7 +122,9 @@ export declare function deviceExec(argv: string[], cwd: string, timeoutSeconds?:
|
|
|
121
122
|
export type DeviceAsk = (approval: DeviceApproval, signal?: AbortSignal) => Promise<DeviceChoice>;
|
|
122
123
|
export declare class DeviceExecutor {
|
|
123
124
|
#private;
|
|
124
|
-
constructor(ctx: Ctx, ask: DeviceAsk, deviceId: string
|
|
125
|
+
constructor(ctx: Ctx, ask: DeviceAsk, deviceId: string, options?: {
|
|
126
|
+
seedHub?: (ctx: Ctx, folder: string, projectId: string) => Promise<HubSeed>;
|
|
127
|
+
});
|
|
125
128
|
/** A new device id for a new sign-in (device-helper.ts): requests for the old one are no longer this device's. */
|
|
126
129
|
set deviceId(id: string);
|
|
127
130
|
get deviceId(): string;
|
package/dist/device-bridge.js
CHANGED
|
@@ -229,7 +229,14 @@ export class DeviceExecutor {
|
|
|
229
229
|
/** Requests that run without asking (device-mode.ts), until their outcome is recorded. */
|
|
230
230
|
#auto = new Map();
|
|
231
231
|
#identity = { at: 0 };
|
|
232
|
-
|
|
232
|
+
/** How an agent's folder link reaches the project hub (project-hub-seed.ts unless a test gives its own hub). */
|
|
233
|
+
#seedHub;
|
|
234
|
+
constructor(ctx, ask, deviceId, options = {}) {
|
|
235
|
+
this.#ctx = ctx;
|
|
236
|
+
this.#ask = ask;
|
|
237
|
+
this.#deviceId = deviceId;
|
|
238
|
+
this.#seedHub = options.seedHub;
|
|
239
|
+
}
|
|
233
240
|
/** A new device id for a new sign-in (device-helper.ts): requests for the old one are no longer this device's. */
|
|
234
241
|
set deviceId(id) { this.#deviceId = id; this.#identity = { at: 0 }; }
|
|
235
242
|
get deviceId() { return this.#deviceId; }
|
|
@@ -578,7 +585,7 @@ export class DeviceExecutor {
|
|
|
578
585
|
if (r.kind === 'dir_list')
|
|
579
586
|
result = { state: 'done', dir: await listDeviceDir(target, r.dir ?? {}, ctx.signal) };
|
|
580
587
|
else if (r.kind === 'project_link')
|
|
581
|
-
result = { state: 'done', project: await linkLocalProject(ctx, target, { ...(r.link?.brief ? { brief: r.link.brief } : {}), harness: r.link?.harness ?? current.harness, projectId: current.project_id, seed: true }) };
|
|
588
|
+
result = { state: 'done', project: await linkLocalProject(ctx, target, { ...(r.link?.brief ? { brief: r.link.brief } : {}), harness: r.link?.harness ?? current.harness, projectId: current.project_id, seed: true, fromConversationId: r.conversation_id }, undefined, this.#seedHub) };
|
|
582
589
|
else {
|
|
583
590
|
const f = r.files;
|
|
584
591
|
if (!f.remote_path.startsWith('/') || /[\0\r\n]/.test(f.remote_path))
|
package/dist/device-folders.d.ts
CHANGED
|
@@ -36,7 +36,7 @@ export interface LinkedProject {
|
|
|
36
36
|
commit?: string;
|
|
37
37
|
branch?: string;
|
|
38
38
|
}
|
|
39
|
-
type Starter = (ctx: Ctx, folder: string, harness: 'claude' | 'codex', brief: string) => Promise<{
|
|
39
|
+
type Starter = (ctx: Ctx, folder: string, harness: 'claude' | 'codex', brief: string, fromConversationId?: string) => Promise<{
|
|
40
40
|
id: string;
|
|
41
41
|
project_id: string;
|
|
42
42
|
}>;
|
|
@@ -65,6 +65,7 @@ export declare function linkLocalProject(ctx: Ctx, path: string, options?: {
|
|
|
65
65
|
harness?: 'claude' | 'codex';
|
|
66
66
|
projectId?: string;
|
|
67
67
|
seed?: boolean;
|
|
68
|
+
fromConversationId?: string;
|
|
68
69
|
}, start?: Starter, seedHub?: (ctx: Ctx, folder: string, projectId: string) => Promise<HubSeed>): Promise<LinkedProject>;
|
|
69
70
|
/** `shard projects link <path>` (0.25.0+): what the desktop app's "Add a folder…" runs, the same as a device's project_link. */
|
|
70
71
|
export declare const PROJECT_LINK_HANDLERS: Record<string, Handler>;
|
package/dist/device-folders.js
CHANGED
|
@@ -139,14 +139,14 @@ export async function listDeviceDir(root, options = {}, signal) {
|
|
|
139
139
|
* working tree (uncommitted files too, .gitignore respected) seeds it while its VM boots, and the brief is its first
|
|
140
140
|
* prompt. The brief goes in on standard input, never in a command line another user can read.
|
|
141
141
|
*/
|
|
142
|
-
export const startFromFolder = (ctx, folder, harness, brief) => {
|
|
142
|
+
export const startFromFolder = (ctx, folder, harness, brief, fromConversationId) => {
|
|
143
143
|
const node = ctx.runtime?.node ?? process.execPath, script = ctx.runtime?.script ?? process.argv[1];
|
|
144
144
|
if (!script)
|
|
145
145
|
return Promise.reject(new Error('The Shardflux CLI could not find itself to start the conversation.'));
|
|
146
146
|
return new Promise((resolveStart, reject) => {
|
|
147
147
|
const child = spawn(node, [script, 'agents', 'new', '--app', harness, '--path', folder, '--prompt-file', '-', '--json'], {
|
|
148
148
|
cwd: folder, stdio: ['pipe', 'pipe', 'pipe'],
|
|
149
|
-
env: { ...process.env, ...ctx.io.env, SHARDFLUX_CONFIG_DIR: ctx.store.dir, SHARDFLUX_API_URL: ctx.apiUrl, CI: '1' },
|
|
149
|
+
env: { ...process.env, ...ctx.io.env, SHARDFLUX_CONFIG_DIR: ctx.store.dir, SHARDFLUX_API_URL: ctx.apiUrl, CI: '1', ...(fromConversationId ? { SF_CONVERSATION_ID: fromConversationId } : {}) },
|
|
150
150
|
});
|
|
151
151
|
const out = [], err = [];
|
|
152
152
|
let outBytes = 0;
|
|
@@ -209,7 +209,7 @@ export async function linkLocalProject(ctx, path, options = {}, start = startFro
|
|
|
209
209
|
const profile = ctx.store.profile(ctx.apiUrl);
|
|
210
210
|
let projectId = profile.project_id ?? profile.api_key?.project_id ?? options.projectId, conversationId;
|
|
211
211
|
if (options.brief) {
|
|
212
|
-
const started = await start(ctx, folder, options.harness ?? 'claude', options.brief);
|
|
212
|
+
const started = await start(ctx, folder, options.harness ?? 'claude', options.brief, options.fromConversationId);
|
|
213
213
|
conversationId = started.id;
|
|
214
214
|
projectId = started.project_id || projectId;
|
|
215
215
|
}
|
package/dist/format.js
CHANGED
|
@@ -83,6 +83,8 @@ export function workspaceState(w) {
|
|
|
83
83
|
tags.push('session');
|
|
84
84
|
if (w.deleted_at)
|
|
85
85
|
tags.push(w.ended_reason ? `ended: ${w.ended_reason}` : 'deleted');
|
|
86
|
+
if (w.storage_recovery)
|
|
87
|
+
tags.push('files stored');
|
|
86
88
|
return tags.length ? `${w.observed_state} (${tags.join(', ')})` : w.observed_state;
|
|
87
89
|
}
|
|
88
90
|
export function workspaceRow(w) {
|
|
@@ -230,6 +232,7 @@ export function workspaceDetail(w) {
|
|
|
230
232
|
['id', w.id],
|
|
231
233
|
['key', w.workspace_key],
|
|
232
234
|
['state', `${w.observed_state} (desired ${w.desired_state})`],
|
|
235
|
+
...(w.storage_recovery ? [['storage', w.storage_recovery.message]] : []),
|
|
233
236
|
['ready', isReady(w) ? 'yes' : 'no'],
|
|
234
237
|
['template', `${w.template.slug} v${w.template.version} (${w.template.version_id})`],
|
|
235
238
|
// Immutable paths (0.8.0): the template version whose immutable paths the workspace has mounted, when it has one.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ProgramRunner } from './cli-proxy.js';
|
|
2
|
+
export declare const FORWARD_LABEL = "dev.shardflux.cli-proxy-forward";
|
|
3
|
+
export declare function forwarderPlistPath(home: string): string;
|
|
4
|
+
/** The shard version the launchd forwarder runs (refreshForwarderAgent compares it with this one). */
|
|
5
|
+
export declare function recordForwarder(dir: string): void;
|
|
6
|
+
/**
|
|
7
|
+
* After `shard` is updated, the launchd forwarder still runs the program it started with. Called on every shard run
|
|
8
|
+
* (one file read on macOS): an installed forwarder agent recorded for another shard version is started again, on this
|
|
9
|
+
* one. Returns whether it was.
|
|
10
|
+
*/
|
|
11
|
+
export declare function refreshForwarderAgent(env: Record<string, string | undefined>, dir: string, deps?: {
|
|
12
|
+
run?: ProgramRunner;
|
|
13
|
+
uid?: number;
|
|
14
|
+
platform?: NodeJS.Platform;
|
|
15
|
+
}): Promise<boolean>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { homedir, userInfo } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { AgentStateStore } from "./agent-state.js";
|
|
5
|
+
import { CLI_VERSION } from "./args.js";
|
|
6
|
+
export const FORWARD_LABEL = 'dev.shardflux.cli-proxy-forward';
|
|
7
|
+
export function forwarderPlistPath(home) {
|
|
8
|
+
return join(home, 'Library', 'LaunchAgents', `${FORWARD_LABEL}.plist`);
|
|
9
|
+
}
|
|
10
|
+
/** The shard version the launchd forwarder runs (refreshForwarderAgent compares it with this one). */
|
|
11
|
+
export function recordForwarder(dir) {
|
|
12
|
+
new AgentStateStore(dir).update((doc) => { doc.cli_proxy_forwarder = { version: CLI_VERSION }; });
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* After `shard` is updated, the launchd forwarder still runs the program it started with. Called on every shard run
|
|
16
|
+
* (one file read on macOS): an installed forwarder agent recorded for another shard version is started again, on this
|
|
17
|
+
* one. Returns whether it was.
|
|
18
|
+
*/
|
|
19
|
+
export async function refreshForwarderAgent(env, dir, deps = {}) {
|
|
20
|
+
if ((deps.platform ?? process.platform) !== 'darwin')
|
|
21
|
+
return false;
|
|
22
|
+
if (!existsSync(forwarderPlistPath(env.HOME || homedir())))
|
|
23
|
+
return false;
|
|
24
|
+
const store = new AgentStateStore(dir);
|
|
25
|
+
const record = store.read().cli_proxy_forwarder;
|
|
26
|
+
if (record?.version === CLI_VERSION)
|
|
27
|
+
return false;
|
|
28
|
+
// Recorded first: a failed restart is not retried on every command (the next import installs the agent again).
|
|
29
|
+
recordForwarder(dir);
|
|
30
|
+
await (deps.run ?? (await import("./cli-proxy.js")).cliProxyTools.run)('launchctl', ['kickstart', '-k', `gui/${deps.uid ?? userInfo().uid}/${FORWARD_LABEL}`]);
|
|
31
|
+
return true;
|
|
32
|
+
}
|