@shardflux/cli 0.16.0 → 0.18.0-next.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +67 -2
- package/README.md +47 -8
- package/dist/agent-commands.d.ts +9 -1
- package/dist/agent-commands.js +65 -16
- package/dist/agent-models.d.ts +61 -0
- package/dist/agent-models.js +79 -0
- package/dist/agent-state.d.ts +10 -0
- package/dist/agent-state.js +13 -0
- package/dist/agent-target-commands.d.ts +6 -1
- package/dist/agent-target-commands.js +113 -16
- package/dist/agent-targets.d.ts +17 -5
- package/dist/agent-targets.js +27 -11
- package/dist/args.d.ts +5 -1
- package/dist/args.js +49 -9
- package/dist/cli-proxy-forward.d.ts +45 -0
- package/dist/cli-proxy-forward.js +432 -0
- package/dist/cli-proxy-hub.d.ts +273 -0
- package/dist/cli-proxy-hub.js +749 -0
- package/dist/cli-proxy.d.ts +325 -0
- package/dist/cli-proxy.js +1016 -0
- package/dist/codex-cloud.d.ts +2 -1
- package/dist/codex-cloud.js +16 -6
- package/dist/command-center-actions.js +16 -1
- package/dist/conversation-local.js +2 -2
- package/dist/conversation-stash.js +4 -3
- package/dist/conversation-terminal.d.ts +5 -0
- package/dist/conversation-terminal.js +45 -6
- package/dist/conversations.d.ts +18 -2
- package/dist/conversations.js +12 -5
- package/dist/desktop-linux.d.ts +40 -0
- package/dist/desktop-linux.js +186 -0
- package/dist/device-approvals.d.ts +12 -0
- package/dist/device-approvals.js +75 -0
- package/dist/device-helper.d.ts +3 -1
- package/dist/device-helper.js +5 -2
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +1 -1
- package/dist/git-remote-bin.js +0 -0
- package/dist/handlers.js +3 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/linux-helper.d.ts +12 -0
- package/dist/linux-helper.js +88 -0
- package/dist/linux-terminal.d.ts +11 -0
- package/dist/linux-terminal.js +42 -0
- package/dist/main.js +30 -6
- package/dist/menubar.js +44 -4
- package/dist/onboarding.d.ts +2 -0
- package/dist/onboarding.js +59 -9
- package/dist/t3-asar.d.ts +10 -0
- package/dist/t3-asar.js +91 -0
- package/dist/t3-launch.d.ts +37 -0
- package/dist/t3-launch.js +133 -0
- package/dist/t3-linux.d.ts +68 -0
- package/dist/t3-linux.js +343 -0
- package/dist/t3-mode.d.ts +129 -0
- package/dist/t3-mode.js +676 -0
- package/dist/t3-phone.d.ts +17 -0
- package/dist/t3-phone.js +15 -0
- package/dist/t3-provider.d.ts +5 -3
- package/dist/t3-provider.js +12 -49
- package/dist/t3-thread-script.d.ts +18 -0
- package/dist/t3-thread-script.js +245 -0
- package/dist/terminal-onboarding.d.ts +11 -0
- package/dist/terminal-onboarding.js +75 -0
- package/dist/thread-on-computer.d.ts +2 -2
- package/dist/thread-on-computer.js +13 -7
- package/dist/update.d.ts +1 -1
- package/dist/update.js +28 -1
- package/dist/worktree-twin-engine.d.ts +31 -1
- package/dist/worktree-twin-engine.js +253 -40
- package/dist/worktree-twin.d.ts +17 -0
- package/dist/worktree-twin.js +147 -22
- package/package.json +7 -19
|
@@ -5,17 +5,23 @@ import { ensureCloudClaude } from "./claude-cloud-login.js";
|
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
6
|
import { Readable, Duplex } from 'node:stream';
|
|
7
7
|
import { ShardfluxApiError, WorkspaceTunnels } from '@shardflux/sdk';
|
|
8
|
+
import { conversationWorkspace, getConversation } from "./conversations.js";
|
|
8
9
|
import { out, resolveWorkspace } from "./commands.js";
|
|
10
|
+
import { MODEL_CHOICES } from "./agent-models.js";
|
|
9
11
|
import { AgentTargets, installedTargets, targetLabel } from "./agent-targets.js";
|
|
10
12
|
import { join, relative } from 'node:path';
|
|
11
13
|
import { projectIdentity } from "./project-identity.js";
|
|
12
14
|
import { projectWorkspacePath } from "./agent-repository.js";
|
|
13
|
-
import { UsageError } from "./errors.js";
|
|
15
|
+
import { UsageError, describeError } from "./errors.js";
|
|
16
|
+
import { t3PhoneLink } from "./t3-phone.js";
|
|
14
17
|
import { prepareDesktopRouter } from "./conversation-router.js";
|
|
15
18
|
import { openUrl } from "./browser.js";
|
|
16
19
|
import { agentMachineName, prepareAgentMachine } from "./agent-machine.js";
|
|
17
20
|
import { prewarmT3 } from "./t3-runtime.js";
|
|
18
21
|
import { AgentStateStore } from "./agent-state.js";
|
|
22
|
+
import { workspaceRepositoryPath } from "./agent-repository.js";
|
|
23
|
+
import { openInChoices, openT3, saveOpenIn, startSshThread, t3Install, t3NotInstalled, t3OpenFailed, t3Status } from "./t3-mode.js";
|
|
24
|
+
import { openT3Conversation, t3Fields, withT3Context } from "./t3-launch.js";
|
|
19
25
|
const missing = (e) => e instanceof ShardfluxApiError && e.status === 404;
|
|
20
26
|
const keyOf = (ref) => ref.includes('/') ? ref : `${projectIdentity('./').slug}/${ref}`;
|
|
21
27
|
/** How long opening a Claude conversation this computer just started waits for Claude to publish its Remote Control URL. */
|
|
@@ -72,6 +78,8 @@ export async function prepareSsh(ws, ctx, targets, host) {
|
|
|
72
78
|
return cell;
|
|
73
79
|
}
|
|
74
80
|
async function openApp(target, env) {
|
|
81
|
+
if (target === 't3')
|
|
82
|
+
return openT3(env, t3Install(env));
|
|
75
83
|
const child = spawn('open', ['-b', { 'claude-desktop': 'com.anthropic.claudefordesktop', 'codex-app': 'com.openai.codex', t3: 'com.t3tools.t3code' }[target]], { env, stdio: 'ignore' });
|
|
76
84
|
await new Promise((resolve, reject) => {
|
|
77
85
|
const timer = setTimeout(() => { child.kill('SIGKILL'); reject(new Error('app launcher timed out')); }, 10_000);
|
|
@@ -212,23 +220,41 @@ async function openLink(url, env) {
|
|
|
212
220
|
}
|
|
213
221
|
const openTarget = (target) => async (ctx, values, [ref = 'main']) => {
|
|
214
222
|
const detected = installedTargets(ctx.io.env);
|
|
223
|
+
if (target === 't3' && !detected.targets.includes('t3'))
|
|
224
|
+
throw t3NotInstalled();
|
|
215
225
|
if (target !== 'web' && !detected.targets.includes(target))
|
|
216
226
|
throw new UsageError(`${target} is not installed on this machine`);
|
|
217
|
-
|
|
227
|
+
// A conversation started in T3 (its Shardflux provider) opens its own thread there (0.17.0+).
|
|
228
|
+
if (target === 't3' && /^conv_[a-z0-9]{26}$/.test(ref))
|
|
229
|
+
return openT3Conversation(ctx, ref, values);
|
|
230
|
+
// A conversation id opens that conversation's own session on its computer (claude.ai, the desktop apps).
|
|
231
|
+
const conversation = /^conv_[a-z0-9]{26}$/.test(ref) ? await getConversation(ctx, ref) : undefined;
|
|
232
|
+
let ws = conversation ? await conversationWorkspace(ctx, conversation) : await resolveWorkspace(ctx, keyOf(ref));
|
|
218
233
|
if (values.reachable !== undefined)
|
|
219
234
|
ws = await ctx.cloud().workspaces.open({ key: ws.key, ...agentReachability(values), wait: false });
|
|
220
|
-
|
|
235
|
+
// T3 lists both agents and opens without Claude (it shows Claude as signed out); the Claude apps need it.
|
|
236
|
+
if (target === 't3')
|
|
237
|
+
await ensureCloudClaude(ctx, ws).catch((error) => {
|
|
238
|
+
if (!(error instanceof UsageError))
|
|
239
|
+
throw error;
|
|
240
|
+
ctx.io.stderr.write(`Claude Code in T3: ${error.message}\n`);
|
|
241
|
+
});
|
|
242
|
+
else if (target !== 'codex-app')
|
|
221
243
|
await ensureCloudClaude(ctx, ws);
|
|
222
244
|
if (target === 'web') {
|
|
223
|
-
const url = await remoteControlUrl(ctx, ws);
|
|
245
|
+
const url = await remoteControlUrl(ctx, ws, conversation);
|
|
224
246
|
if (!ctx.json && !openUrl(url, ctx.io.env))
|
|
225
247
|
throw new Error(`could not open ${url}`);
|
|
226
|
-
out(ctx, `${url}\n`, { target, workspace: ws.data, url });
|
|
248
|
+
out(ctx, `${url}\n`, { target, workspace: ws.data, url, ...(conversation ? { conversation_id: conversation.id } : {}) });
|
|
227
249
|
return 0;
|
|
228
250
|
}
|
|
229
|
-
return openDesktopWorkspace(ctx, ws, target, detected.versions.t3);
|
|
251
|
+
return openDesktopWorkspace(ctx, ws, target, detected.versions.t3, false, undefined, undefined, conversation?.id);
|
|
230
252
|
};
|
|
231
|
-
|
|
253
|
+
/**
|
|
254
|
+
* Opens a workspace as a desktop app's SSH environment. For T3 (0.17.0+) a first message becomes a thread on the T3
|
|
255
|
+
* server in the VM (SSH mode), and T3 is brought to the front with --json too (`opened: true`).
|
|
256
|
+
*/
|
|
257
|
+
export async function openDesktopWorkspace(ctx, ws, target, t3Version, prewarmed = false, thread, install, conversation) {
|
|
232
258
|
if (target !== 't3')
|
|
233
259
|
ws = await prepareDesktopRouter(ctx, ws);
|
|
234
260
|
await prepareAgentMachine(ctx, ws);
|
|
@@ -241,13 +267,58 @@ export async function openDesktopWorkspace(ctx, ws, target, t3Version, prewarmed
|
|
|
241
267
|
: target === 'claude-desktop'
|
|
242
268
|
? `Restart Claude Desktop to load the connection, then choose ${targetLabel(entry)} in Code's environment dropdown and open ${projectWorkspacePath({ slug: entry.project })}.`
|
|
243
269
|
: `Restart Codex to import ${entry.host} and its remote project. If it has already been imported, enable ${entry.host} in Settings → Connections → SSH.`;
|
|
244
|
-
|
|
270
|
+
let started;
|
|
271
|
+
if (target === 't3' && thread) {
|
|
272
|
+
try {
|
|
273
|
+
started = await startSshThread(ctx, ws, t3Version, workspaceRepositoryPath(ws), thread);
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
throw withT3Context(error, { workspace_key: ws.key, conversation_id: conversation ?? (ws.labels.shard_conversation || null) });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (target === 't3') {
|
|
280
|
+
// The thread exists once startSshThread returns: a failure to show T3 names it rather than inviting a retry.
|
|
281
|
+
try {
|
|
282
|
+
await openT3(ctx.io.env, install ?? t3Install(ctx.io.env));
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
throw started ? t3OpenFailed(error, { t3_thread_id: started.t3_thread_id, conversation_id: conversation ?? (ws.labels.shard_conversation || null), workspace_key: ws.key }) : error;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
else if (!ctx.json)
|
|
245
289
|
await openApp(target, ctx.io.env);
|
|
246
290
|
new AgentStateStore(ctx.store.dir).saveTarget(ws.labels.repo_remote || ws.labels.shard_project || entry.project, target);
|
|
247
|
-
|
|
291
|
+
if (target === 't3')
|
|
292
|
+
new AgentStateStore(ctx.store.dir).saveT3Environment({ workspace: ws.key, host: entry.host, api_url: ctx.apiUrl });
|
|
293
|
+
const phone = target === 't3' ? await linkT3Phone(ctx, ws) : undefined;
|
|
294
|
+
const threadLine = started ? `Started a thread in T3 Code on ${agentMachineName(ws.key)}${started.model ? ` (${started.model}${started.effort ? `, ${started.effort}` : ''})` : ''}.\n` : '';
|
|
295
|
+
out(ctx, `${threadLine}${instructions}\n${phone?.line ?? ''}`, { target, workspace: ws.data, host: entry.host, instructions, ...(phone ? { t3_link: phone.link } : {}),
|
|
296
|
+
...(target === 't3' ? t3Fields(ws, entry.project, install ?? t3Install(ctx.io.env), started, conversation) : {}),
|
|
248
297
|
app_bundle_id: { 'claude-desktop': 'com.anthropic.claudefordesktop', 'codex-app': 'com.openai.codex', t3: 'com.t3tools.t3code' }[target] });
|
|
249
298
|
return 0;
|
|
250
299
|
}
|
|
300
|
+
/**
|
|
301
|
+
* The T3 app on the phone reaches this workspace through the account's T3 Connect sign-in (0.17.0+). The sign-in is
|
|
302
|
+
* asked for once per person; linking continues by itself once it is approved.
|
|
303
|
+
*/
|
|
304
|
+
async function linkT3Phone(ctx, ws) {
|
|
305
|
+
try {
|
|
306
|
+
const { link, signIn, needsLogin } = await t3PhoneLink(ctx, ws.id);
|
|
307
|
+
const line = signIn
|
|
308
|
+
? `Phone: to use ${link.label} from the T3 app, open ${signIn.verification_uri} and confirm ${signIn.user_code} (once).\n`
|
|
309
|
+
: needsLogin ? `Phone: to use ${link.label} from the T3 app, sign in with shard auth login, then run this again.\n`
|
|
310
|
+
: `Phone: ${link.label} appears in the T3 app's environments.\n`;
|
|
311
|
+
// --json carries the sign-in shard started with its session too, so the menu bar can show it.
|
|
312
|
+
return { line, link: signIn ? { ...link, sign_in: { ...link.sign_in, ...signIn } } : needsLogin ? { ...link, needs_login: true } : link };
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
// An API without T3 Connect leaves the desktop route as it is.
|
|
316
|
+
if (describeError(error).code === 'not_found')
|
|
317
|
+
return undefined;
|
|
318
|
+
ctx.io.stderr.write(`Phone link: ${describeError(error).message}\n`);
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
251
322
|
export async function queueT3Prewarm(ctx, ws, version) {
|
|
252
323
|
try {
|
|
253
324
|
await prewarmT3(ctx, ws, version);
|
|
@@ -269,30 +340,56 @@ function configuredTargets(targets, e, t3 = false) {
|
|
|
269
340
|
function targetFiles(targets, entries) {
|
|
270
341
|
return [...new Set(entries.flatMap(e => configuredTargets(targets, e).map(c => c.file)))];
|
|
271
342
|
}
|
|
343
|
+
/**
|
|
344
|
+
* The project `agents targets` reports T3 for: the agent argument's, else `--path`'s (resolved as `agents new --path`
|
|
345
|
+
* resolves it: the repository remote, else the folder), else the current folder's when it is one.
|
|
346
|
+
*/
|
|
347
|
+
function targetsProject(ref, path) {
|
|
348
|
+
if (ref)
|
|
349
|
+
return keyOf(ref).split('/')[0];
|
|
350
|
+
if (path !== undefined)
|
|
351
|
+
return projectIdentity(path).slug;
|
|
352
|
+
try {
|
|
353
|
+
return projectIdentity('./').slug;
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
272
359
|
export const TARGET_HANDLERS = {
|
|
273
360
|
async 'agents targets'(ctx, values, [ref]) {
|
|
274
|
-
const
|
|
361
|
+
const found = installedTargets(ctx.io.env);
|
|
362
|
+
// models (0.17.0+): the model and effort choices a new thread can start with; clients never hardcode them.
|
|
363
|
+
// T3 Code mode (0.17.0+): T3's state for this project (the agent's, else this folder's), and where `shard ./` opens
|
|
364
|
+
// each app's new conversations (--open-in sets it).
|
|
365
|
+
const project = targetsProject(ref, typeof values.path === 'string' ? values.path : undefined);
|
|
366
|
+
const t3 = t3Status(ctx, { project, lastUsed: true });
|
|
367
|
+
if (typeof values['open-in'] === 'string')
|
|
368
|
+
saveOpenIn(ctx, values['open-in'], t3, found.clients);
|
|
369
|
+
const detected = { ...found, t3, open_in: openInChoices(ctx, project, { clients: found.clients }) };
|
|
275
370
|
const targets = new AgentTargets(ctx.io.env, ctx.store.dir);
|
|
276
371
|
const key = ref ? keyOf(ref) : undefined;
|
|
277
372
|
const before = values.remove === true ? await targets.configure(undefined, ctx.apiUrl, [], false, ctx.signal) : [];
|
|
278
373
|
const entries = ref || values.remove === true
|
|
279
374
|
? await targets.configure(key, ctx.apiUrl, detected.targets.filter((t) => t === 'claude-desktop' || t === 'codex-app' || t === 't3'), values.remove === true, ctx.signal)
|
|
280
375
|
: [];
|
|
281
|
-
const detection = `${detected.clients.join(', ') || 'No installed agent clients'}\nOpen targets: ${detected.targets.join(', ')}\n
|
|
376
|
+
const detection = `${detected.clients.join(', ') || 'No installed agent clients'}\nOpen targets: ${detected.targets.join(', ')}\n` +
|
|
377
|
+
(t3.installed ? `T3 Code ${t3.version ?? ''}: ${t3.mode === 'acp' ? 'Shardflux provider' : 'SSH environments'}, ${t3.connected ? 'connected' : 'not connected'}\n`.replace(' :', ':') : '') +
|
|
378
|
+
`New conversations open in: Claude → ${detected.open_in.claude}, Codex → ${detected.open_in.codex}\n`;
|
|
282
379
|
if (values.remove === true) {
|
|
283
380
|
const removed = before.filter(e => !entries.some(n => n.host === e.host));
|
|
284
|
-
out(ctx, removed.length ? `Removed ${removed.map(e => `${e.workspace} (${e.host})`).join(', ')} from ${targetFiles(targets, removed).map(f => tilde(targets, f)).join(', ')}\n` : 'No managed targets to remove.\n', { ...detected, entries, removed });
|
|
381
|
+
out(ctx, removed.length ? `Removed ${removed.map(e => `${e.workspace} (${e.host})`).join(', ')} from ${targetFiles(targets, removed).map(f => tilde(targets, f)).join(', ')}\n` : 'No managed targets to remove.\n', { ...detected, entries, removed, models: MODEL_CHOICES });
|
|
285
382
|
return Promise.resolve(0);
|
|
286
383
|
}
|
|
287
384
|
const entry = key ? entries.find(e => e.workspace === key) : undefined;
|
|
288
385
|
if (!entry) {
|
|
289
|
-
out(ctx, detection, { ...detected, entries });
|
|
386
|
+
out(ctx, detection, { ...detected, entries, models: MODEL_CHOICES });
|
|
290
387
|
return Promise.resolve(0);
|
|
291
388
|
}
|
|
292
|
-
const
|
|
293
|
-
const configured = configuredTargets(targets, entry,
|
|
389
|
+
const t3Installed = detected.targets.includes('t3');
|
|
390
|
+
const configured = configuredTargets(targets, entry, t3Installed);
|
|
294
391
|
out(ctx, `Configured ${entry.workspace} as SSH host ${entry.host}:\n${configured.map(c => ` ${c.label.padEnd(15)} ${tilde(targets, c.file)}${c.detail ? ` ${c.detail}` : ''}`).join('\n')}\n` +
|
|
295
|
-
['claude-desktop', 'codex-app', 't3'].filter(t => t === 't3' ? !
|
|
392
|
+
['claude-desktop', 'codex-app', 't3'].filter(t => t === 't3' ? !t3Installed : !entry.targets.includes(t)).map(t => ` ${{ 'claude-desktop': 'Claude Desktop', 'codex-app': 'Codex app', t3: 'T3 Code' }[t].padEnd(15)} not installed on this computer\n`).join(''), { ...detected, entries, configured: configured.map(({ target, file, name }) => ({ target, file, name })), models: MODEL_CHOICES });
|
|
296
393
|
return Promise.resolve(0);
|
|
297
394
|
},
|
|
298
395
|
'agents open claude-desktop': openTarget('claude-desktop'),
|
package/dist/agent-targets.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { T3LinuxApp } from './t3-linux.js';
|
|
1
2
|
export type DesktopTarget = 'claude-desktop' | 'codex-app' | 't3';
|
|
2
3
|
export interface TargetEntry {
|
|
3
4
|
host: string;
|
|
@@ -13,13 +14,24 @@ export declare const targetLabel: (e: Pick<TargetEntry, "agent" | "project">) =>
|
|
|
13
14
|
export declare function targetIdentity(key: string): Pick<TargetEntry, 'host' | 'workspace' | 'project' | 'agent'>;
|
|
14
15
|
/** Replace only a marked block, retaining every byte outside it. Put Includes before user Host blocks. */
|
|
15
16
|
export declare function managedBlock(text: string, body: string): string;
|
|
16
|
-
/**
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* An installed T3 Code: on macOS `path` is the .app bundle; on Linux it is the program its desktop entry runs (the
|
|
19
|
+
* executable, or the AppImage), and `linux` says how it was installed and how it starts (t3-linux.ts).
|
|
20
|
+
*/
|
|
21
|
+
export interface T3App {
|
|
18
22
|
path: string;
|
|
19
23
|
version: string | null;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
linux?: T3LinuxApp;
|
|
25
|
+
}
|
|
26
|
+
/** The installed T3 Code apps (any channel), with their path and version, in the given app folders (applicationDirs). */
|
|
27
|
+
export declare function t3Apps(dirs: string[], platform?: NodeJS.Platform, env?: Record<string, string | undefined>): T3App[];
|
|
28
|
+
/**
|
|
29
|
+
* The folders scanned for desktop apps: SHARDFLUX_APPLICATIONS_DIRS (colon-separated) when set; else /Applications and
|
|
30
|
+
* ~/Applications on macOS, and on Linux the XDG application folders and the usual AppImage folders (linuxApplicationDirs).
|
|
31
|
+
*/
|
|
32
|
+
export declare function applicationDirs(env: Record<string, string | undefined>, platform?: NodeJS.Platform): string[];
|
|
33
|
+
/** The first `name` app bundle with this bundle id in the app folders (macOS only). */
|
|
34
|
+
export declare function desktopAppPath(dirs: string[], name: string, bundle: string, platform?: NodeJS.Platform): string | undefined;
|
|
23
35
|
export declare function installedTargets(env: Record<string, string | undefined>, platform?: NodeJS.Platform, appDirs?: string[]): {
|
|
24
36
|
targets: string[];
|
|
25
37
|
clients: string[];
|
package/dist/agent-targets.js
CHANGED
|
@@ -7,6 +7,7 @@ import { writePrivateFile } from "./credentials.js";
|
|
|
7
7
|
import { UsageError } from "./errors.js";
|
|
8
8
|
import { projectWorkspacePath } from "./agent-repository.js";
|
|
9
9
|
import { withTargetLock } from "./target-lock.js";
|
|
10
|
+
import { linuxApplicationDirs, linuxT3Apps } from "./t3-linux.js";
|
|
10
11
|
const PERSISTED_TARGETS = ['claude-desktop', 'codex-app'];
|
|
11
12
|
const BEGIN = '# BEGIN Shardflux managed targets';
|
|
12
13
|
const END = '# END Shardflux managed targets';
|
|
@@ -36,8 +37,10 @@ export function managedBlock(text, body) {
|
|
|
36
37
|
const stop = end + END.length + (text[end + END.length] === '\n' ? 1 : 0);
|
|
37
38
|
return text.slice(0, start) + block + text.slice(stop);
|
|
38
39
|
}
|
|
39
|
-
/** The installed T3 Code apps (any channel), with their path and version, in the given app folders. */
|
|
40
|
-
export function t3Apps(dirs, platform = process.platform) {
|
|
40
|
+
/** The installed T3 Code apps (any channel), with their path and version, in the given app folders (applicationDirs). */
|
|
41
|
+
export function t3Apps(dirs, platform = process.platform, env = process.env) {
|
|
42
|
+
if (platform === 'linux')
|
|
43
|
+
return linuxT3Apps(dirs, env);
|
|
41
44
|
const found = [];
|
|
42
45
|
if (platform !== 'darwin')
|
|
43
46
|
return found;
|
|
@@ -69,23 +72,34 @@ export function t3Apps(dirs, platform = process.platform) {
|
|
|
69
72
|
}
|
|
70
73
|
return found;
|
|
71
74
|
}
|
|
72
|
-
/**
|
|
73
|
-
|
|
75
|
+
/**
|
|
76
|
+
* The folders scanned for desktop apps: SHARDFLUX_APPLICATIONS_DIRS (colon-separated) when set; else /Applications and
|
|
77
|
+
* ~/Applications on macOS, and on Linux the XDG application folders and the usual AppImage folders (linuxApplicationDirs).
|
|
78
|
+
*/
|
|
79
|
+
export function applicationDirs(env, platform = process.platform) {
|
|
74
80
|
const home = env.HOME || homedir();
|
|
75
|
-
|
|
81
|
+
if (env.SHARDFLUX_APPLICATIONS_DIRS !== undefined)
|
|
82
|
+
return env.SHARDFLUX_APPLICATIONS_DIRS.split(':').filter(Boolean);
|
|
83
|
+
return platform === 'linux' ? linuxApplicationDirs(env) : ['/Applications', join(home, 'Applications')];
|
|
76
84
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
/** The first `name` app bundle with this bundle id in the app folders (macOS only). */
|
|
86
|
+
export function desktopAppPath(dirs, name, bundle, platform = process.platform) {
|
|
87
|
+
if (platform !== 'darwin')
|
|
88
|
+
return undefined;
|
|
89
|
+
return dirs.map(dir => join(dir, name)).find(path => {
|
|
81
90
|
const info = join(path, 'Contents', 'Info.plist');
|
|
82
91
|
if (!existsSync(info))
|
|
83
92
|
return false;
|
|
84
93
|
const result = spawnSync('/usr/libexec/PlistBuddy', ['-c', 'Print :CFBundleIdentifier', info], { input: '', encoding: 'utf8', timeout: 1000 });
|
|
85
94
|
return result.status === 0 && result.stdout.trim() === bundle;
|
|
86
95
|
});
|
|
96
|
+
}
|
|
97
|
+
export function installedTargets(env, platform = process.platform, appDirs) {
|
|
98
|
+
// SHARDFLUX_APPLICATIONS_DIRS (colon-separated) replaces the scanned app folders, like SHARDFLUX_CONFIG_DIR does for config.
|
|
99
|
+
const dirs = appDirs ?? applicationDirs(env, platform);
|
|
100
|
+
const hasApp = (name, bundle) => desktopAppPath(dirs, name, bundle, platform) !== undefined;
|
|
87
101
|
const versions = {};
|
|
88
|
-
const t3Bundles = t3Apps(dirs, platform);
|
|
102
|
+
const t3Bundles = t3Apps(dirs, platform, env);
|
|
89
103
|
const t3 = t3Bundles.length > 0;
|
|
90
104
|
for (const app of t3Bundles)
|
|
91
105
|
if (app.version)
|
|
@@ -100,7 +114,9 @@ export function installedTargets(env, platform = process.platform, appDirs) {
|
|
|
100
114
|
const clients = [hasApp('Claude.app', 'com.anthropic.claudefordesktop') && 'claude-desktop', (hasApp('Codex.app', 'com.openai.codex') || hasApp('ChatGPT.app', 'com.openai.codex')) && 'codex-app', executable('claude') && 'claude', executable('codex') && 'codex'].filter((v) => !!v);
|
|
101
115
|
if (t3)
|
|
102
116
|
clients.push('t3');
|
|
103
|
-
|
|
117
|
+
// Linux has T3 Code (0.17.0+); Claude Desktop and the Codex app are macOS apps.
|
|
118
|
+
const desktop = platform === 'darwin' ? clients.filter(c => c.endsWith('-app') || c === 'claude-desktop' || c === 't3') : platform === 'linux' && t3 ? ['t3'] : [];
|
|
119
|
+
return { targets: [...desktop, 'web'], clients, versions };
|
|
104
120
|
}
|
|
105
121
|
/** All paths are supplied by the invocation; tests use a temporary HOME, never the owner's app stores. */
|
|
106
122
|
export class AgentTargets {
|
package/dist/args.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "0.
|
|
1
|
+
export declare const CLI_VERSION = "0.17.0";
|
|
2
2
|
export interface OptionSpec {
|
|
3
3
|
type: 'string' | 'boolean';
|
|
4
4
|
short?: string;
|
|
@@ -27,9 +27,12 @@ export interface CommandSpec {
|
|
|
27
27
|
positionals: PositionalSpec[];
|
|
28
28
|
options: Record<string, OptionSpec>;
|
|
29
29
|
examples?: string[];
|
|
30
|
+
/** Runs, but is left out of the top and group help (a program other tools start, such as a forwarder). */
|
|
31
|
+
hidden?: boolean;
|
|
30
32
|
}
|
|
31
33
|
export declare const GLOBAL_OPTIONS: Record<string, OptionSpec>;
|
|
32
34
|
export declare const PROJECT_COMMAND: CommandSpec;
|
|
35
|
+
export declare const ONBOARDING_COMMAND: CommandSpec;
|
|
33
36
|
export declare const CENTER_COMMAND: CommandSpec;
|
|
34
37
|
export declare const COMMANDS: CommandSpec[];
|
|
35
38
|
export declare function normalizeWords(words: string[]): string[];
|
|
@@ -74,6 +77,7 @@ export type Parsed = {
|
|
|
74
77
|
*/
|
|
75
78
|
export declare function parseCommandLine(argv: readonly string[], options?: {
|
|
76
79
|
tty?: boolean;
|
|
80
|
+
platform?: NodeJS.Platform;
|
|
77
81
|
isProjectPath?: (path: string) => boolean;
|
|
78
82
|
}): Parsed;
|
|
79
83
|
export declare function usageLine(c: CommandSpec): string;
|
package/dist/args.js
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
import { parseArgs } from 'node:util';
|
|
13
13
|
import { ACCOUNT_COMMANDS } from "./account-args.js";
|
|
14
14
|
import { UsageError } from "./errors.js";
|
|
15
|
-
|
|
15
|
+
import { DEFAULT_CHOICE, MODEL_CHOICES } from "./agent-models.js";
|
|
16
|
+
export const CLI_VERSION = '0.18.0-next.4';
|
|
16
17
|
export const GLOBAL_OPTIONS = {
|
|
17
18
|
'api-url': { type: 'string', value: '<url>', description: 'API base URL (default $SHARDFLUX_API_URL, else https://api.shardflux.dev).' },
|
|
18
19
|
json: { type: 'boolean', description: 'Print machine-readable JSON on stdout (errors as JSON on stderr).' },
|
|
@@ -1188,6 +1189,18 @@ const AGENT_OPTIONS = {
|
|
|
1188
1189
|
reachable: { type: 'string', value: '<duration>', hidden: true, description: 'Deprecated; ignored. Claude conversations stay reachable without a time limit.' },
|
|
1189
1190
|
approvals: { type: 'boolean', description: 'Keep the agent’s permission prompts.' },
|
|
1190
1191
|
};
|
|
1192
|
+
/** `Claude fable, opus, sonnet or haiku; Codex …` from the CLI's one list (agent-models.ts), without "default". */
|
|
1193
|
+
function choiceIds(kind) {
|
|
1194
|
+
return ['claude', 'codex'].map(app => {
|
|
1195
|
+
const ids = MODEL_CHOICES[app][kind].map(c => c.id).filter(id => id !== DEFAULT_CHOICE);
|
|
1196
|
+
return `${app === 'claude' ? 'Claude' : 'Codex'} ${ids.slice(0, -1).join(', ')} or ${ids.at(-1)}`;
|
|
1197
|
+
}).join('; ');
|
|
1198
|
+
}
|
|
1199
|
+
/** The model and effort a new thread starts with; `shard agents targets --json` lists the choices (0.17.0+). */
|
|
1200
|
+
const MODEL_OPTIONS = {
|
|
1201
|
+
model: { type: 'string', value: '<model>', description: `Model the new thread starts with: ${choiceIds('models')}; default is the agent’s own (0.17.0+).` },
|
|
1202
|
+
effort: { type: 'string', value: '<level>', description: `Reasoning effort the new thread starts with: ${choiceIds('efforts')}; default is the agent’s own (0.17.0+).` },
|
|
1203
|
+
};
|
|
1191
1204
|
/** A new thread on an existing conversation's computer (0.16.0+). */
|
|
1192
1205
|
const ON_OPTIONS = {
|
|
1193
1206
|
on: { type: 'string', value: '<conversation|computer>', description: 'Start the thread on the computer of an existing conversation, sharing its packages, caches, services, ports and files, on its own branch: a conversation id, key or name, or its workspace key or id (shard agents computers lists them) (0.16.0+).' },
|
|
@@ -1198,6 +1211,7 @@ export const PROJECT_COMMAND = {
|
|
|
1198
1211
|
description: 'Opens Claude Code or Codex directly. --resume continues a project conversation. With --json it starts detached and prints the session. Detach with Ctrl-] then d.',
|
|
1199
1212
|
positionals: [{ name: 'path', required: true }, { name: 'claude|codex' }], options: {
|
|
1200
1213
|
...AGENT_OPTIONS,
|
|
1214
|
+
...MODEL_OPTIONS,
|
|
1201
1215
|
...ON_OPTIONS,
|
|
1202
1216
|
computer: COMPUTER_OPTION,
|
|
1203
1217
|
new: { type: 'boolean', description: 'Start a new conversation.' },
|
|
@@ -1211,10 +1225,17 @@ function projectExample(rest, app) {
|
|
|
1211
1225
|
const path = rest.find((r, i) => !r.startsWith('-') && !GLOBAL_VALUE_FLAGS.has(rest[i - 1] ?? '')) ?? './';
|
|
1212
1226
|
return `shard ${/^[\w./~@:+,=-]+$/.test(path) ? path : `'${path.replaceAll("'", `'\\''`)}'`} ${app}`;
|
|
1213
1227
|
}
|
|
1228
|
+
export const ONBOARDING_COMMAND = { path: ['<onboarding>'], summary: 'Set up Shardflux in your terminal', positionals: [], options: { 'accept-terms': { type: 'boolean', description: 'Accept the Terms of Service and Privacy Policy.' }, apps: { type: 'string', value: '<claude,codex>', description: 'Connect Claude, Codex or both.' } } };
|
|
1214
1229
|
export const CENTER_COMMAND = { path: ['<center>'], summary: 'Open the cloud agent command center', positionals: [], options: {} };
|
|
1215
1230
|
const AGENT_COMMANDS = [
|
|
1231
|
+
{ path: ['device', 'requests'], summary: 'Review local device requests waiting for approval', positionals: [], options: {} },
|
|
1232
|
+
...['approve', 'deny'].map((action) => ({ path: ['device', action], summary: `${action} a local device request`, positionals: [{ name: 'id', required: true }], options: action === 'approve' ? { always: { type: 'boolean', description: 'Allow matching requests for this project; never for outside-folder access.' } } : {} })),
|
|
1216
1233
|
{ path: ['conversations', 'try'], summary: 'Try a conversation in a synced local worktree', positionals: [{ name: 'conversation', required: true }], options: { path: { type: 'string', value: '<path>', description: 'Local project checkout.' }, stop: { type: 'boolean', description: 'Stop the device helper’s local sync and ports.' } } },
|
|
1217
|
-
{ path: ['agents', 'targets'], summary: 'Detect apps or configure managed desktop connections', positionals: [{ name: 'agent' }], options: {
|
|
1234
|
+
{ path: ['agents', 'targets'], summary: 'Detect apps or configure managed desktop connections', positionals: [{ name: 'agent' }], options: {
|
|
1235
|
+
remove: { type: 'boolean', description: 'Remove this agent’s managed entries, or all entries for this API URL.' },
|
|
1236
|
+
'open-in': { type: 'string', value: '<app=target,…>', description: 'Where new conversations open (0.17.0+): claude=terminal|claude-desktop|web|t3, codex=codex-app|t3, e.g. claude=t3,codex=t3.' },
|
|
1237
|
+
path: { type: 'string', value: '<path>', description: 'Local project folder whose T3 Code state and open_in to report, as agents new --path names it (default the current folder; the agent argument wins) (0.17.0+).' },
|
|
1238
|
+
} },
|
|
1218
1239
|
...['claude-desktop', 'codex-app', 'web', 't3'].map(target => ({ path: ['agents', 'open', target], summary: `Open the agent in ${target}`, positionals: [{ name: 'agent' }], options: { reachable: AGENT_OPTIONS.reachable } })),
|
|
1219
1240
|
{ path: ['ssh-proxy'], summary: 'Forward SSH standard IO to a managed workspace', positionals: [{ name: 'host', required: true }], options: {} },
|
|
1220
1241
|
...['worktree', 'merge', 'keep', 'discard', 'restore'].map((action) => ({
|
|
@@ -1232,12 +1253,13 @@ const AGENT_COMMANDS = [
|
|
|
1232
1253
|
} },
|
|
1233
1254
|
{ path: ['agents', 'new'], summary: 'Start a conversation in a fresh VM, or with --on on an existing conversation’s computer; Codex opens a genuine Cloud thread', positionals: [{ name: 'agent' }], options: {
|
|
1234
1255
|
...AGENT_OPTIONS,
|
|
1256
|
+
...MODEL_OPTIONS,
|
|
1235
1257
|
...ON_OPTIONS,
|
|
1236
1258
|
computer: COMPUTER_OPTION,
|
|
1237
1259
|
app: { type: 'string', value: '<claude|codex>', description: 'Agent app (default remembered choice).' },
|
|
1238
1260
|
path: { type: 'string', value: '<path>', description: 'Local project path for first launch.' },
|
|
1239
1261
|
name: { type: 'string', value: '<name>', description: 'New agent name; copies the project main workspace with --like.' },
|
|
1240
|
-
in: { type: 'string', value: '<terminal|claude-desktop|codex-app|t3>', description: 'Where to open the new agent (default terminal).' },
|
|
1262
|
+
in: { type: 'string', value: '<terminal|auto|claude-desktop|codex-app|t3>', description: 'Where to open the new agent (default terminal). t3 (0.17.0+): the first message starts a T3 Code thread (with --on, on that computer’s T3 server); auto: where agents targets --open-in says.' },
|
|
1241
1263
|
like: { type: 'string', value: '<id|key>', description: 'Copy the workspace configuration onto a fresh disk.' },
|
|
1242
1264
|
} },
|
|
1243
1265
|
{ path: ['agents', 'fork'], summary: 'Fork an agent’s memory and disk', positionals: [{ name: 'agent', required: true }, { name: 'name', required: true }], options: WAIT },
|
|
@@ -1248,13 +1270,29 @@ const AGENT_COMMANDS = [
|
|
|
1248
1270
|
follow: { type: 'boolean', description: 'Follow output until interrupted.' },
|
|
1249
1271
|
} },
|
|
1250
1272
|
];
|
|
1273
|
+
const CLI_PROXY_HELP = 'One CLIProxyAPI for your whole account: your Claude, Codex and other subscription logins live in a small hosted copy that sleeps between requests and wakes on the next one. This computer and every VM reach it at http://127.0.0.1:8317, so Claude Code, Codex and T3 Code keep their settings.';
|
|
1274
|
+
const CLI_PROXY_COMMANDS = [
|
|
1275
|
+
{ path: ['cli-proxy'], summary: 'Your hosted CLIProxyAPI: its accounts, and where it is reachable', description: CLI_PROXY_HELP, positionals: [], options: {
|
|
1276
|
+
config: { type: 'string', value: '<path>', description: 'This computer’s CLIProxyAPI config file, when none is set up yet (default: found automatically).' },
|
|
1277
|
+
} },
|
|
1278
|
+
{ path: ['cli-proxy', 'import'], summary: 'Move CLIProxyAPI from this computer into your account, reachable at 127.0.0.1:8317 everywhere', description: `${CLI_PROXY_HELP} Import brings your config and logins over, stops the local copy (one copy refreshes each login), and connects this computer and your machine template. On a second computer it connects that computer.`, positionals: [], options: {
|
|
1279
|
+
config: { type: 'string', value: '<path>', description: 'Your CLIProxyAPI config file (default: the one the running CLIProxyAPI uses, else the usual locations).' },
|
|
1280
|
+
yes: { type: 'boolean', description: 'Stop the local CLIProxyAPI without asking.' },
|
|
1281
|
+
}, examples: ['shard cli-proxy import', 'shard cli-proxy'] },
|
|
1282
|
+
{ path: ['cli-proxy', 'export'], summary: 'Bring your logins back to this computer and stop the hosted CLIProxyAPI', description: 'The hosted copy stops first, so only this computer refreshes your logins from then on. Its logins, as last refreshed, go to the auth folder the import took them from (0600), this computer stops forwarding 127.0.0.1:8317, and the command that starts your local CLIProxyAPI is shown. shard cli-proxy import moves them back.', positionals: [], options: {
|
|
1283
|
+
yes: { type: 'boolean', description: 'Do it without asking.' },
|
|
1284
|
+
} },
|
|
1285
|
+
{ path: ['cli-proxy', 'forward'], hidden: true, summary: 'Forward 127.0.0.1:8317 to your hosted CLIProxyAPI (started at login)', positionals: [], options: {} },
|
|
1286
|
+
];
|
|
1251
1287
|
/** Every command: the account plane first (sign in, set up), then the project-key commands. */
|
|
1252
1288
|
const MENUBAR_COMMANDS = ['start', 'stop', 'status'].map(action => ({
|
|
1253
1289
|
path: ['menubar', action], summary: `${action === 'start' ? 'Install and start' : action === 'stop' ? 'Stop' : 'Show'} the macOS menu bar app`,
|
|
1254
1290
|
positionals: [], options: { login: { type: 'string', value: '<on|off>', description: 'Enable or disable launch at login.' } },
|
|
1255
1291
|
}));
|
|
1256
|
-
export const COMMANDS = [...ACCOUNT_COMMANDS.filter((c) => c.path[0] !== 'templates'), ...AGENT_COMMANDS, ...AGENT_CREDENTIAL_COMMANDS, ...MENUBAR_COMMANDS, ...KEY_COMMANDS, ...ACCOUNT_COMMANDS.filter((c) => c.path[0] === 'templates')];
|
|
1292
|
+
export const COMMANDS = [...ACCOUNT_COMMANDS.filter((c) => c.path[0] !== 'templates'), ...AGENT_COMMANDS, ...AGENT_CREDENTIAL_COMMANDS, ...CLI_PROXY_COMMANDS, ...MENUBAR_COMMANDS, ...KEY_COMMANDS, ...ACCOUNT_COMMANDS.filter((c) => c.path[0] === 'templates')];
|
|
1257
1293
|
const GROUPS = {
|
|
1294
|
+
device: 'Local device approvals',
|
|
1295
|
+
'cli-proxy': 'Your hosted CLIProxyAPI, reachable at http://127.0.0.1:8317 on this computer and in every VM',
|
|
1258
1296
|
menubar: 'macOS menu bar app',
|
|
1259
1297
|
conversations: 'Project conversations, branches and messages',
|
|
1260
1298
|
agents: 'Cloud agent sessions and copied credentials',
|
|
@@ -1433,6 +1471,8 @@ export function parseCommandLine(argv, options = {}) {
|
|
|
1433
1471
|
// `menubar refresh` (0.15.2+, not in help): the background step each command may start (menubar.ts).
|
|
1434
1472
|
if (argv.length === 2 && argv[0] === 'menubar' && argv[1] === 'refresh')
|
|
1435
1473
|
return { kind: 'run', command: { path: ['menubar', 'refresh'], summary: '', positionals: [], options: {} }, values: {}, positionals: [], global: { apiUrl: undefined, json: false, agentLabel: undefined, wake: undefined, wakeTimeout: undefined } };
|
|
1474
|
+
if (argv[0] === 'device-helper' && argv.length === 2 && argv[1] === '--service')
|
|
1475
|
+
return { kind: 'run', command: { path: ['device-helper'], summary: '', positionals: [], options: {} }, values: { service: true }, positionals: [], global: { apiUrl: undefined, json: false, agentLabel: undefined, wake: undefined, wakeTimeout: undefined } };
|
|
1436
1476
|
if (argv.length === 1 && (argv[0] === 'acp' || argv[0] === 'device-helper'))
|
|
1437
1477
|
return { kind: 'run', command: { path: [argv[0]], summary: '', positionals: [], options: {} }, values: {}, positionals: [], global: { apiUrl: undefined, json: false, agentLabel: undefined, wake: undefined, wakeTimeout: undefined } };
|
|
1438
1478
|
const terminator = argv.indexOf('--');
|
|
@@ -1494,8 +1534,8 @@ export function parseCommandLine(argv, options = {}) {
|
|
|
1494
1534
|
const path = rest.find((r, i) => !r.startsWith('-') && !GLOBAL_VALUE_FLAGS.has(rest[i - 1] ?? ''));
|
|
1495
1535
|
if (path !== undefined && (/^(?:\.|\/|~\/)/.test(path) || options.isProjectPath?.(path)))
|
|
1496
1536
|
command = PROJECT_COMMAND;
|
|
1497
|
-
else if (path === undefined && !wantsHelp && !head.includes('--version') && !head.includes('-V') && (options.tty || head.includes('--json')))
|
|
1498
|
-
command = CENTER_COMMAND;
|
|
1537
|
+
else if (path === undefined && !wantsHelp && !head.includes('--version') && !head.includes('-V') && ((options.platform ?? process.platform) === 'linux' || options.tty || head.includes('--json')))
|
|
1538
|
+
command = (options.platform ?? process.platform) === 'linux' && !head.includes('--json') ? ONBOARDING_COMMAND : CENTER_COMMAND;
|
|
1499
1539
|
}
|
|
1500
1540
|
if (!command) {
|
|
1501
1541
|
if (words.length === 0) {
|
|
@@ -1623,7 +1663,7 @@ export function commandHelp(c) {
|
|
|
1623
1663
|
export function groupHelp(prefix) {
|
|
1624
1664
|
const words = typeof prefix === 'string' ? [prefix] : [...prefix];
|
|
1625
1665
|
const group = words.join(' ');
|
|
1626
|
-
const cmds = COMMANDS.filter((c) => words.every((w, i) => c.path[i] === w));
|
|
1666
|
+
const cmds = COMMANDS.filter((c) => !c.hidden && words.every((w, i) => c.path[i] === w));
|
|
1627
1667
|
const w = Math.max(...cmds.map((c) => usageLine(c).length));
|
|
1628
1668
|
return [`Usage: shard ${group} <command> [options]`, '', `${GROUPS[group] ?? GROUPS[words[0]] ?? ''}.`, '', 'Commands:', ...cmds.map((c) => ` ${usageLine(c).padEnd(w)} ${c.summary}`), ...(group === 'workspaces' ? ['', 'Also: shard workspaces files <read|write|ls|stat|search|patch> ..., shard workspaces operations <list|get|wait> ... and shard workspaces executions get ...'] : []), '', `Run "shard ${group} <command> --help" for details.`].join('\n') + '\n';
|
|
1629
1669
|
}
|
|
@@ -1641,14 +1681,14 @@ export const EXIT_CODE_HELP = [
|
|
|
1641
1681
|
/** The top help's sections: every command's group (first word) is in exactly one (args.test.ts checks). */
|
|
1642
1682
|
export const HELP_SECTIONS = [
|
|
1643
1683
|
{ title: 'Account (a user session: shard signup or shard auth login)', groups: ['signup', 'verify', 'auth', 'setup', 'env', 'orgs', 'projects', 'api-keys', 'members', 'invitations', 'billing', 'audit', 'account'] },
|
|
1644
|
-
{ title: 'Cloud agents (shard ./ starts one; a project API key)', groups: ['conversations', 'machine', 'template', 'agents', 'ssh-proxy'] },
|
|
1684
|
+
{ title: 'Cloud agents (shard ./ starts one; a project API key)', groups: ['conversations', 'machine', 'template', 'agents', 'device', 'cli-proxy', 'ssh-proxy'] },
|
|
1645
1685
|
{ title: 'Workspaces for your app (a project API key: SHARDFLUX_API_KEY, else the one shard setup saved)', groups: ['login', 'whoami', 'status', 'usage', 'workspaces', 'files', 'operations', 'executions', 'secrets', 'credentials', 'volumes', 'egress'] },
|
|
1646
1686
|
{ title: 'Templates for your app’s workspaces (not for cloud agents; publish and archive use the session, the rest the API key)', groups: ['templates'] },
|
|
1647
1687
|
{ title: 'Other', groups: ['menubar', 'update', 'feedback', 'version'] },
|
|
1648
1688
|
];
|
|
1649
1689
|
export function topHelp() {
|
|
1650
1690
|
const w = Math.max(...COMMANDS.map((c) => usageLine(c).length));
|
|
1651
|
-
const sections = HELP_SECTIONS.flatMap((s) => ['', `${s.title}:`, ...s.groups.flatMap((g) => COMMANDS.filter((c) => c.path[0] === g)).map((c) => ` ${usageLine(c).padEnd(w)} ${c.summary}`)]);
|
|
1691
|
+
const sections = HELP_SECTIONS.flatMap((s) => ['', `${s.title}:`, ...s.groups.flatMap((g) => COMMANDS.filter((c) => c.path[0] === g && !c.hidden)).map((c) => ` ${usageLine(c).padEnd(w)} ${c.summary}`)]);
|
|
1652
1692
|
return [
|
|
1653
1693
|
`shard ${CLI_VERSION} — the Shardflux command line (built on @shardflux/sdk)`,
|
|
1654
1694
|
'',
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { HubInfo } from './cli-proxy-hub.js';
|
|
2
|
+
export declare const REPLAY_LIMIT: number;
|
|
3
|
+
/** The forwarder answers this path itself (paths under /__shardflux/ are reserved by the edge, so no hub route is lost). */
|
|
4
|
+
export declare const IDENTITY_PATH = "/__shardflux/forwarder";
|
|
5
|
+
export interface ForwarderOptions {
|
|
6
|
+
/** The current hub info, read on every request (cheap: hubInfoSource caches by mtime). */
|
|
7
|
+
hub: () => HubInfo | null;
|
|
8
|
+
/** Starts CLIProxyAPI in the hub again (after `port_not_listening`). At most one runs at a time. */
|
|
9
|
+
repair?: () => Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Replaces the hub's callback URLs after the edge answers `callback_invalid` (the API key that created them was
|
|
12
|
+
* revoked) and gives VMs the new one. At most one runs at a time and at most one a minute; each request is renewed
|
|
13
|
+
* for once, so a URL that stays invalid is answered as is.
|
|
14
|
+
*/
|
|
15
|
+
renew?: () => Promise<void>;
|
|
16
|
+
host?: string;
|
|
17
|
+
port?: number;
|
|
18
|
+
/** How long one request may be retried for (default 60 s, plus the time a repair takes). */
|
|
19
|
+
retryBudgetMs?: number;
|
|
20
|
+
/** Between attempts to bind a port another process holds (default 5 s). */
|
|
21
|
+
bindRetryMs?: number;
|
|
22
|
+
log?: (line: string) => void;
|
|
23
|
+
sleep?: (ms: number) => Promise<void>;
|
|
24
|
+
/** Request bodies kept for a retry, in bytes (default REPLAY_LIMIT); a larger body streams and is not retried. */
|
|
25
|
+
replayLimit?: number;
|
|
26
|
+
}
|
|
27
|
+
/** The hub info file, re-read when it changes (a replaced callback URL takes effect on the next request). */
|
|
28
|
+
export declare function hubInfoSource(dir: string): () => HubInfo | null;
|
|
29
|
+
export declare class CliProxyForwarder {
|
|
30
|
+
#private;
|
|
31
|
+
constructor(opts: ForwarderOptions);
|
|
32
|
+
get address(): {
|
|
33
|
+
host: string;
|
|
34
|
+
port: number;
|
|
35
|
+
} | null;
|
|
36
|
+
/**
|
|
37
|
+
* Listens on host:port. While another program holds the port (also one listening on all interfaces, which a
|
|
38
|
+
* loopback bind would not notice), logs once and tries again every bindRetryMs (until close()); resolves once
|
|
39
|
+
* listening.
|
|
40
|
+
*/
|
|
41
|
+
listen(): Promise<void>;
|
|
42
|
+
/** Closes the idle keep-alive connections to the edge (the next request opens a fresh one). */
|
|
43
|
+
dropIdleConnections(): void;
|
|
44
|
+
close(): Promise<void>;
|
|
45
|
+
}
|