@shardflux/cli 0.22.0 → 0.23.0
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 +98 -0
- package/README.md +37 -10
- package/dist/acp-journal.js +58 -6
- package/dist/acp-login.d.ts +10 -3
- package/dist/acp-login.js +31 -4
- package/dist/acp-models.d.ts +6 -1
- package/dist/acp-models.js +21 -16
- package/dist/acp.js +60 -7
- package/dist/agent-commands.js +29 -15
- package/dist/agent-target-commands.d.ts +20 -1
- package/dist/agent-target-commands.js +49 -18
- package/dist/agent-targets.js +6 -1
- package/dist/args.d.ts +1 -1
- package/dist/args.js +8 -7
- package/dist/cli-proxy-forward.d.ts +0 -2
- package/dist/cli-proxy-forward.js +2 -9
- package/dist/cli-proxy-hub.d.ts +22 -30
- package/dist/cli-proxy-hub.js +65 -94
- package/dist/cli-proxy.d.ts +12 -38
- package/dist/cli-proxy.js +51 -168
- package/dist/codex-cloud.d.ts +9 -4
- package/dist/codex-cloud.js +47 -14
- package/dist/codex-linux.d.ts +14 -0
- package/dist/codex-linux.js +69 -0
- package/dist/command-center-actions.js +13 -4
- package/dist/command-center-model.d.ts +2 -0
- package/dist/command-center-model.js +6 -1
- package/dist/command-center.js +3 -3
- package/dist/commands.js +2 -2
- package/dist/conversation-terminal.js +3 -2
- package/dist/conversations.d.ts +14 -1
- package/dist/conversations.js +55 -6
- package/dist/desktop-linux.js +16 -3
- package/dist/errors.d.ts +2 -1
- package/dist/errors.js +5 -2
- package/dist/filesystem-commands.js +25 -3
- package/dist/menubar.d.ts +6 -5
- package/dist/menubar.js +38 -28
- package/dist/move-worktrees.d.ts +13 -7
- package/dist/move-worktrees.js +68 -17
- package/dist/move.js +20 -25
- package/dist/onboarding.js +4 -2
- package/dist/prepared-repository.d.ts +2 -0
- package/dist/prepared-repository.js +1 -1
- package/dist/prepared-repository.py +3 -2
- package/dist/project-preparation.d.ts +14 -0
- package/dist/project-preparation.js +16 -4
- package/dist/project-sync.d.ts +3 -0
- package/dist/project-sync.js +4 -1
- package/dist/t3-launch.d.ts +2 -1
- package/dist/t3-launch.js +11 -2
- package/dist/t3-proxy.d.ts +81 -0
- package/dist/t3-proxy.js +325 -0
- package/dist/terminal-apps.d.ts +11 -0
- package/dist/terminal-apps.js +18 -5
- package/dist/update.d.ts +2 -0
- package/dist/update.js +32 -6
- package/package.json +5 -5
|
@@ -60,8 +60,14 @@ export const CENTER_HANDLERS = {
|
|
|
60
60
|
}
|
|
61
61
|
if (values.review === true)
|
|
62
62
|
throw new UsageError('--review needs --on <conversation>.');
|
|
63
|
-
if (
|
|
64
|
-
|
|
63
|
+
if (values.prompt !== undefined || values['prompt-file'] !== undefined || values.image !== undefined) {
|
|
64
|
+
// --in terminal is where a new conversation opens anyway: it starts one with its first message (0.23.0+). Codex
|
|
65
|
+
// opens in Codex Cloud, which says so.
|
|
66
|
+
if (fresh && values.in === 'terminal')
|
|
67
|
+
return launchConversationProject(ctx, values, [str(values.path) ?? './', ...(str(values.app) ? [str(values.app)] : [])]);
|
|
68
|
+
const other = ref ? `the agent argument ${ref}` : values.name !== undefined ? '--name' : values.like !== undefined ? '--like' : values.in !== undefined ? `--in ${str(values.in)}` : undefined;
|
|
69
|
+
if (other)
|
|
70
|
+
throw new UsageError(`--prompt, --prompt-file and --image are for a new conversation and cannot be combined with ${other}: omit it.`, 'start one with shard agents new --path <folder> --app claude|codex --prompt "<task>"; --in auto opens it where agents targets --open-in says');
|
|
65
71
|
}
|
|
66
72
|
if (!ref && values.name === undefined && values.like === undefined && values.in === undefined) {
|
|
67
73
|
return launchConversationProject(ctx, values, [str(values.path) ?? './', ...(str(values.app) ? [str(values.app)] : [])]);
|
|
@@ -145,8 +151,11 @@ export const CENTER_HANDLERS = {
|
|
|
145
151
|
if (ws.labels.shard_conversation && CONVERSATION_ID.test(ws.labels.shard_conversation))
|
|
146
152
|
conversation = await getConversation(ctx, ws.labels.shard_conversation);
|
|
147
153
|
}
|
|
148
|
-
|
|
149
|
-
|
|
154
|
+
// Codex Cloud holds the conversation's output in its Codex thread; no terminal in the VM shows it (0.23.0+: the
|
|
155
|
+
// refusal names the commands that do).
|
|
156
|
+
if (conversation?.route === 'codex-cloud') {
|
|
157
|
+
throw new UsageError(`Codex Cloud runs this conversation, so its output is in its Codex thread, not a terminal: open the thread with shard agents open ${conversation.id}; shard c wait ${conversation.id} waits for its reply and shard c get ${conversation.id} shows it.`);
|
|
158
|
+
}
|
|
150
159
|
if (conversation?.route === 'acp')
|
|
151
160
|
throw new UsageError('This conversation runs through its ACP client, which shows its output; it has no terminal to follow.');
|
|
152
161
|
if (conversation && values.app !== undefined && values.app !== conversation.harness)
|
|
@@ -24,6 +24,8 @@ export interface CenterRow {
|
|
|
24
24
|
/** Older K5 captures used the workspace key as the agent label. K1 needs its leaf name. */
|
|
25
25
|
export declare const agentName: (key: string, labels: Record<string, string>) => string;
|
|
26
26
|
export declare const projectName: (key: string, labels: Record<string, string>) => string;
|
|
27
|
+
/** What `shard agents open` gets for a row: a Cloud thread's conversation id, else the VM's key. */
|
|
28
|
+
export declare const openReference: (row: Pick<CenterRow, "key" | "agent">) => string;
|
|
27
29
|
export declare function loadCenter(ctx: Ctx): Promise<CenterData>;
|
|
28
30
|
export declare function scopedProject(data: CenterData, scope: ProjectScope): string;
|
|
29
31
|
export declare function centerRows(data: CenterData, scope?: ProjectScope, filter?: string): CenterRow[];
|
|
@@ -6,6 +6,11 @@ import { readSetupProgress } from "./assisted-setup.js";
|
|
|
6
6
|
export const agentName = (key, labels) => (labels.agent || key).split('/').at(-1);
|
|
7
7
|
export const projectName = (key, labels) => labels.shard_project || key.split('/')[0];
|
|
8
8
|
const parked = (workspace) => ['suspended', 'hibernated', 'parked'].includes(workspace?.observed_state ?? '');
|
|
9
|
+
/** A Codex Cloud thread (API rows labeled shard_route=codex-cloud, 0.23.0+): it runs in Codex, so its VM being parked
|
|
10
|
+
* says nothing about it, and it opens by its conversation id, in Codex, without waking that VM. */
|
|
11
|
+
const cloudThread = (agent) => agent?.labels.shard_route === 'codex-cloud' && !!agent.conversation_id;
|
|
12
|
+
/** What `shard agents open` gets for a row: a Cloud thread's conversation id, else the VM's key. */
|
|
13
|
+
export const openReference = (row) => cloudThread(row.agent) ? row.agent.conversation_id : row.key;
|
|
9
14
|
export async function loadCenter(ctx) {
|
|
10
15
|
const collect = async (path) => {
|
|
11
16
|
const rows = [], cursors = new Set();
|
|
@@ -62,7 +67,7 @@ export function centerRows(data, scope, filter = '') {
|
|
|
62
67
|
const workspace = workspaces.get(agent.workspace_id);
|
|
63
68
|
if (workspace?.deleted_at)
|
|
64
69
|
continue;
|
|
65
|
-
const badge = agent.app === 'claude' && agent.credential_status === 'needs_sign_in' ? 'Claude sign-in needed' : agent.state === 'exited' ? 'done' : parked(workspace) ? 'parked' : agent.state === 'waiting' ? 'needs you' : 'working';
|
|
70
|
+
const badge = agent.app === 'claude' && agent.credential_status === 'needs_sign_in' ? 'Claude sign-in needed' : agent.state === 'exited' ? 'done' : !cloudThread(agent) && parked(workspace) ? 'parked' : agent.state === 'waiting' ? 'needs you' : 'working';
|
|
66
71
|
rows.push({ id: agent.id, key: agent.workspace_key, project: projectName(agent.workspace_key, agent.labels), agent, workspace, badge,
|
|
67
72
|
attention: badge === 'Claude sign-in needed' ? -1 : ['needs you', 'working', 'parked', 'done'].indexOf(badge) });
|
|
68
73
|
withSessions.add(agent.workspace_id);
|
package/dist/command-center.js
CHANGED
|
@@ -5,7 +5,7 @@ import { AgentStateStore } from "./agent-state.js";
|
|
|
5
5
|
import { installedTargets } from "./agent-targets.js";
|
|
6
6
|
import { randomUUID } from 'node:crypto';
|
|
7
7
|
import { agentAction, availableTargets } from "./command-center-actions.js";
|
|
8
|
-
import { centerRows, loadCenter } from "./command-center-model.js";
|
|
8
|
+
import { centerRows, loadCenter, openReference } from "./command-center-model.js";
|
|
9
9
|
import { AnsiRenderer, renderCenter } from "./command-center-renderer.js";
|
|
10
10
|
import { CenterKeys } from "./command-center-keys.js";
|
|
11
11
|
import { ensureMenubar } from "./menubar.js";
|
|
@@ -320,13 +320,13 @@ export async function showCenter(ctx, scope, options = {}) {
|
|
|
320
320
|
const app = row.agent?.app || options.app || new AgentStateStore(ctx.store.dir).app(project, row.workspace?.labels.app === 'codex' ? 'codex' : 'claude');
|
|
321
321
|
if (key === 'enter') {
|
|
322
322
|
const target = new AgentStateStore(ctx.store.dir).target(project, ['terminal', ...installedTargets(ctx.io.env).targets]);
|
|
323
|
-
await dispatch('open', { app, project, in: target }, [row
|
|
323
|
+
await dispatch('open', { app, project, in: target }, [openReference(row)], true);
|
|
324
324
|
}
|
|
325
325
|
else if (key === 'o') {
|
|
326
326
|
const targets = await availableTargets(ctx);
|
|
327
327
|
menu('Open in…', targets.map(target => ({ label: target, run: async () => {
|
|
328
328
|
new AgentStateStore(ctx.store.dir).saveTarget(project, target);
|
|
329
|
-
await dispatch('open', { app, project, in: target }, [row
|
|
329
|
+
await dispatch('open', { app, project, in: target }, [openReference(row)], true);
|
|
330
330
|
} })));
|
|
331
331
|
}
|
|
332
332
|
else if (key === 'N' || key === 'f') {
|
package/dist/commands.js
CHANGED
|
@@ -1104,7 +1104,7 @@ export const KEY_HANDLERS = {
|
|
|
1104
1104
|
})));
|
|
1105
1105
|
}
|
|
1106
1106
|
if (s.state === 'starting' || s.state === 'running') {
|
|
1107
|
-
throw new ExecSessionRunningError(`Exec session ${s.session_id} is still ${s.state}.`, s, `follow it to its end with: shard ws exec-read ${ref} ${s.session_id} --follow`);
|
|
1107
|
+
throw new ExecSessionRunningError(`Exec session ${s.session_id} is still ${s.state}${s.error ? `: ${s.error}` : ''}.`, s, `follow it to its end with: shard ws exec-read ${ref} ${s.session_id} --follow`);
|
|
1108
1108
|
}
|
|
1109
1109
|
const result = { exitCode: s.exit_code ?? null, termSignal: s.term_signal ?? null, timedOut: s.timed_out ?? false, canceled: s.canceled ?? false };
|
|
1110
1110
|
if (!ctx.json && result.exitCode === null && !result.timedOut && !(result.termSignal !== null && result.termSignal > 0)) {
|
|
@@ -1121,7 +1121,7 @@ export const KEY_HANDLERS = {
|
|
|
1121
1121
|
const s = await cellOf(ctx, ws).exec.cancel(sessionId, graceMs);
|
|
1122
1122
|
const ended = s.state !== 'starting' && s.state !== 'running';
|
|
1123
1123
|
const status = !ended ? `${s.state} (cancel sent)` : s.canceled ? `canceled${s.term_signal ? ` (signal ${s.term_signal})` : ''}` : `already ${s.state}${typeof s.exit_code === 'number' ? ` (exit code ${s.exit_code})` : ''}`;
|
|
1124
|
-
out(ctx, `exec session ${s.session_id}: ${status}\n`, s);
|
|
1124
|
+
out(ctx, `exec session ${s.session_id}: ${status}${s.error ? `: ${s.error}` : ''}${!ended || s.error?.startsWith('cancel accepted; execution drain') ? `; follow with: shard ws exec-read ${ref} ${s.session_id} --follow` : ''}\n`, s);
|
|
1125
1125
|
return 0;
|
|
1126
1126
|
},
|
|
1127
1127
|
async 'workspaces exec-input'(ctx, values, [ref, sessionId, data]) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { linuxTerminalSession } from "./linux-terminal.js";
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
|
-
import { attachable, openShardInTerminal, reportTerminalOpen } from "./terminal-apps.js";
|
|
3
|
+
import { attachable, handedToTerminal, openShardInTerminal, reportTerminalOpen } from "./terminal-apps.js";
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
import { AgentStateStore } from "./agent-state.js";
|
|
6
6
|
import { projectIdentity, projectLabels } from "./project-identity.js";
|
|
@@ -71,7 +71,8 @@ export async function reopenConversation(ctx, c, values) {
|
|
|
71
71
|
*/
|
|
72
72
|
export const launchProjectCommand = async (ctx, values, [path = './', explicit]) => {
|
|
73
73
|
const named = explicit ?? (values.claude === true ? 'claude' : values.codex === true ? 'codex' : undefined);
|
|
74
|
-
|
|
74
|
+
// A terminal this CLI opened for it runs it there, never in yet another one (0.23.0+).
|
|
75
|
+
const typing = attachable(ctx.io), here = typing || ctx.json || handedToTerminal(ctx.io.env);
|
|
75
76
|
if (values.resume !== true && values.review !== true) {
|
|
76
77
|
const project = projectIdentity(path);
|
|
77
78
|
const app = named === 'claude' || named === 'codex' ? named : new AgentStateStore(ctx.store.dir).app(project.remote ?? project.folder);
|
package/dist/conversations.d.ts
CHANGED
|
@@ -111,6 +111,9 @@ export declare function rememberConversation(ctx: Pick<Ctx, 'store' | 'apiUrl'>,
|
|
|
111
111
|
export declare function conversationsUnavailable(e: unknown): boolean;
|
|
112
112
|
export declare function getConversation(ctx: Ctx, id: string): Promise<Conversation>;
|
|
113
113
|
export declare function listConversations(ctx: Ctx, projectId?: string): Promise<Conversation[]>;
|
|
114
|
+
/** Resolve an open reference without using the local cwd or waking a VM. A conversation key survives a VM move;
|
|
115
|
+
* a computer key names its bound conversation. Legacy computers without that binding keep the workspace path. */
|
|
116
|
+
export declare function conversationForOpen(ctx: Ctx, ref: string): Promise<Conversation | undefined>;
|
|
114
117
|
export declare function resolveConversation(ctx: Ctx, ref: string, folder?: string): Promise<Conversation>;
|
|
115
118
|
/** Re-resolve the binding on every attach: a move changes the VM, never the conversation identity. */
|
|
116
119
|
export declare function conversationWorkspace(ctx: Ctx, c: Conversation): Promise<Workspace>;
|
|
@@ -131,6 +134,11 @@ export interface MovedInSession {
|
|
|
131
134
|
* they pass. The conversation API's availability is read meanwhile too.
|
|
132
135
|
*/
|
|
133
136
|
export declare function createConversationRecord(ctx: Ctx, folder: string, harness: Harness, route: Conversation['route'], name?: string, allocated?: (workspace: WorkspaceView) => void, idempotencyKey?: string, choice?: ModelChoice, movedIn?: MovedInSession, ready?: Promise<unknown>): Promise<Conversation>;
|
|
137
|
+
/**
|
|
138
|
+
* Files of a conversation's prepared start still on their way (large media that follow the first prompt): an editor's
|
|
139
|
+
* file sync leaves them alone until they land, instead of sending them a second time.
|
|
140
|
+
*/
|
|
141
|
+
export declare function preparedInFlight(ctx: Pick<Ctx, 'store' | 'apiUrl'>, id: string): string[];
|
|
134
142
|
/**
|
|
135
143
|
* Brings the conversation's accepted revision into its VM (0.22.0+): only what the project cache lacks is sent, and
|
|
136
144
|
* with `defer` large incompressible files follow the first prompt. Resumable: a later call (the device helper, or
|
|
@@ -142,6 +150,7 @@ export declare function prepareAcceptedConversation(ctx: Ctx, c: Conversation, w
|
|
|
142
150
|
setup?: Promise<{
|
|
143
151
|
depsHash: string;
|
|
144
152
|
} | undefined>;
|
|
153
|
+
landNow?: boolean;
|
|
145
154
|
}): Promise<boolean>;
|
|
146
155
|
/**
|
|
147
156
|
* Sends a conversation's deferred large files and lands them in its worktree. Runs while its VM is awake (it never
|
|
@@ -157,7 +166,11 @@ export declare function seedConversation(ctx: Ctx, c: Conversation, ws: Workspac
|
|
|
157
166
|
seedRepository?(ws: Workspace, c: Conversation): Promise<void>;
|
|
158
167
|
}, changes?: Promise<LocalChanges>, setup?: Promise<{
|
|
159
168
|
depsHash: string;
|
|
160
|
-
} | undefined
|
|
169
|
+
} | undefined>, editor?: {
|
|
170
|
+
initialFiles?: readonly string[];
|
|
171
|
+
onInitialFiles?: () => void;
|
|
172
|
+
landNow?: boolean;
|
|
173
|
+
}): Promise<void>;
|
|
161
174
|
export declare function createConversation(ctx: Ctx, folder: string, harness: Harness, route: Conversation['route'], name?: string, choice?: ModelChoice, movedIn?: MovedInSession & {
|
|
162
175
|
beforeSeed(ws: Workspace, c: Conversation): Promise<void>;
|
|
163
176
|
seedRepository?(ws: Workspace, c: Conversation): Promise<void>;
|
package/dist/conversations.js
CHANGED
|
@@ -96,6 +96,41 @@ export async function listConversations(ctx, projectId) {
|
|
|
96
96
|
} while (cursor);
|
|
97
97
|
return all;
|
|
98
98
|
}
|
|
99
|
+
/** Resolve an open reference without using the local cwd or waking a VM. A conversation key survives a VM move;
|
|
100
|
+
* a computer key names its bound conversation. Legacy computers without that binding keep the workspace path. */
|
|
101
|
+
export async function conversationForOpen(ctx, ref) {
|
|
102
|
+
if (/^conv_[a-z0-9]{26}$/.test(ref))
|
|
103
|
+
return getConversation(ctx, ref);
|
|
104
|
+
if (!ref.includes('/'))
|
|
105
|
+
return undefined;
|
|
106
|
+
const ws = await ctx.cloud().workspaces.findByKey(ref, { signal: ctx.signal });
|
|
107
|
+
if (ws) {
|
|
108
|
+
const id = ws.labels.shard_conversation;
|
|
109
|
+
if (!id)
|
|
110
|
+
return undefined;
|
|
111
|
+
try {
|
|
112
|
+
const c = await getConversation(ctx, id);
|
|
113
|
+
return c.workspace_id === ws.data.id ? c : undefined;
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (conversationsUnavailable(error))
|
|
117
|
+
return undefined;
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
let matches;
|
|
122
|
+
try {
|
|
123
|
+
matches = (await listConversations(ctx)).filter(c => c.key === ref);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (conversationsUnavailable(error))
|
|
127
|
+
return undefined;
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
if (matches.length > 1)
|
|
131
|
+
throw new UsageError('Use the conversation id.');
|
|
132
|
+
return matches[0] ? getConversation(ctx, matches[0].id) : undefined;
|
|
133
|
+
}
|
|
99
134
|
export async function resolveConversation(ctx, ref, folder) {
|
|
100
135
|
if (ref.startsWith('conv_'))
|
|
101
136
|
return getConversation(ctx, ref);
|
|
@@ -190,8 +225,8 @@ export async function createConversationRecord(ctx, folder, harness, route, name
|
|
|
190
225
|
const commit = seed.commit;
|
|
191
226
|
// The prepared revision of that state (0.22.0+): its media and what Git does not hold (.env files, filter output),
|
|
192
227
|
// captured while the VM is requested and boots. A capture that cannot complete falls back to the copy seed, never fails
|
|
193
|
-
// the start.
|
|
194
|
-
const capture = movedIn
|
|
228
|
+
// the start. Every route but a moved-in session (its own worktrees) starts this way, editors' (ACP) too (0.22.0+).
|
|
229
|
+
const capture = movedIn ? Promise.resolve(undefined) : capturePreparedSnapshot(ctx.store.dir, project.folder, seed, project.remote, ctx.signal).catch((error) => {
|
|
195
230
|
if (ctx.signal.aborted)
|
|
196
231
|
throw error;
|
|
197
232
|
ctx.io.stderr.write(`Project snapshot: ${error instanceof Error ? error.message : String(error)}; copying the project instead.\n`);
|
|
@@ -211,6 +246,7 @@ export async function createConversationRecord(ctx, folder, harness, route, name
|
|
|
211
246
|
// The seed awaits the capture (prepareAcceptedConversation), so it runs on while the VM boots, with the Git settings
|
|
212
247
|
// the VM repository copies from this one.
|
|
213
248
|
pendingCaptures.set(c.id, capture.then(async (prepared) => {
|
|
249
|
+
seedPhase('capture_done');
|
|
214
250
|
if (!prepared)
|
|
215
251
|
return undefined;
|
|
216
252
|
new AgentStateStore(ctx.store.dir).update(doc => {
|
|
@@ -223,6 +259,14 @@ export async function createConversationRecord(ctx, folder, harness, route, name
|
|
|
223
259
|
acceptedProgress(c.id, c.key);
|
|
224
260
|
return c;
|
|
225
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* Files of a conversation's prepared start still on their way (large media that follow the first prompt): an editor's
|
|
264
|
+
* file sync leaves them alone until they land, instead of sending them a second time.
|
|
265
|
+
*/
|
|
266
|
+
export function preparedInFlight(ctx, id) {
|
|
267
|
+
const preparation = localConversations(ctx)[id]?.preparation;
|
|
268
|
+
return preparation?.state === 'deferred' ? preparation.pending ?? [] : [];
|
|
269
|
+
}
|
|
226
270
|
/** Captures still running for conversations this process created (createConversationRecord). */
|
|
227
271
|
const pendingCaptures = new Map();
|
|
228
272
|
/** A start the copy seed took over: its prepared revision is no longer this conversation's (recorded as the reason). */
|
|
@@ -280,7 +324,7 @@ export async function prepareAcceptedConversation(ctx, c, ws, options = {}) {
|
|
|
280
324
|
const target = { root: c.worktree_path, branch: c.branch, options: { signal: ctx.signal, adopt: true,
|
|
281
325
|
git: fresh?.git ?? await preparedGitConfig(local.folder, snapshot.manifest, ctx.signal), ...(outside ? { dependencySource: templateCheckout(c) } : {}),
|
|
282
326
|
after: { script: SEED_SNAPSHOT, args: [c.worktree_path, c.branch.replace(/^shard\//, ''), HUB_PATH, 'background'] },
|
|
283
|
-
...(intent ? { setupIntent: { conversation: c.id, depsHash: intent.depsHash } } : {}), workspace: ws.id } };
|
|
327
|
+
...(intent ? { setupIntent: { conversation: c.id, depsHash: intent.depsHash } } : {}), workspace: ws.id, ...(options.landNow ? { landNow: true } : {}) } };
|
|
284
328
|
let transfer, apply;
|
|
285
329
|
try {
|
|
286
330
|
// A warm start: the probe checks the revision out in its own exec.
|
|
@@ -363,7 +407,7 @@ export async function completeDeferredPreparation(ctx, c) {
|
|
|
363
407
|
* its prepared revision (0.22.0+), or the copy seed when the start has none or the VM cannot take one. Repeatable: a
|
|
364
408
|
* start resumed after a deadline continues here.
|
|
365
409
|
*/
|
|
366
|
-
export async function seedConversation(ctx, c, ws, folder, movedIn, changes, setup) {
|
|
410
|
+
export async function seedConversation(ctx, c, ws, folder, movedIn, changes, setup, editor = {}) {
|
|
367
411
|
// Requests the copy seed starts and nothing waits for (the history backfill's start), settled before this returns.
|
|
368
412
|
const background = [];
|
|
369
413
|
const cell = ws.cell();
|
|
@@ -383,9 +427,14 @@ export async function seedConversation(ctx, c, ws, folder, movedIn, changes, set
|
|
|
383
427
|
void recordIntent();
|
|
384
428
|
await movedIn.seedRepository(ws, c);
|
|
385
429
|
}
|
|
386
|
-
else if (
|
|
430
|
+
else if (await prepareAcceptedConversation(ctx, c, ws, { fallback: true, ...(setup ? { setup } : {}), ...(editor.landNow ? { landNow: true } : {}) })) {
|
|
431
|
+
// A prepared worktree appears whole: an editor's agent settings are there with everything else.
|
|
432
|
+
editor.onInitialFiles?.();
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
387
435
|
await seedAgentRepository(ctx, ws, folder, { root: c.worktree_path, populate: true, changes: changes ?? localChanges(folder, ctx.signal),
|
|
388
|
-
background: started => background.push(started), beforeHistory: recordIntent()
|
|
436
|
+
background: started => background.push(started), beforeHistory: recordIntent(),
|
|
437
|
+
...(editor.initialFiles ? { initialFiles: editor.initialFiles } : {}), ...(editor.onInitialFiles ? { onInitialFiles: editor.onInitialFiles } : {}) });
|
|
389
438
|
}
|
|
390
439
|
await recorded;
|
|
391
440
|
seedPhase('seed_done');
|
package/dist/desktop-linux.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn, execFile } from 'node:child_process';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
-
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, chmodSync, mkdtempSync } from 'node:fs';
|
|
3
|
+
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync, chmodSync, mkdtempSync } from 'node:fs';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import { promisify } from 'node:util';
|
|
@@ -19,6 +19,15 @@ export function linuxDesktopPackage(arch = process.arch) {
|
|
|
19
19
|
}
|
|
20
20
|
export function graphicalSession(env) { return !!(env.DISPLAY || env.WAYLAND_DISPLAY); }
|
|
21
21
|
const desktopString = (text) => `"${text.replace(/(["`$\\])/g, '\\$1')}"`;
|
|
22
|
+
// fnm's multishell aliases belong to the launching shell; the desktop and its autostart entry outlive it.
|
|
23
|
+
function stableDesktopRuntime(runtime) {
|
|
24
|
+
try {
|
|
25
|
+
return { node: realpathSync(runtime.node), script: realpathSync(runtime.script) };
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
throw new UsageError('Open shard from its installed CLI to start the Shardflux window.');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
22
31
|
export class LinuxDesktopManager {
|
|
23
32
|
home;
|
|
24
33
|
app;
|
|
@@ -117,6 +126,7 @@ export class LinuxDesktopManager {
|
|
|
117
126
|
throw new UsageError('Install the window with npm i -g shardflux --include=optional, then run shard.');
|
|
118
127
|
if (!graphicalSession(this.#env))
|
|
119
128
|
throw new UsageError('Open shard from your desktop session to show the window.');
|
|
129
|
+
const stable = stableDesktopRuntime(runtime);
|
|
120
130
|
const current = await this.status(dir);
|
|
121
131
|
if (!current.version || (compareVersions(current.version, version) ?? -1) < 0) {
|
|
122
132
|
if (existsSync(this.app) && (lstatSync(this.app).isSymbolicLink() || !current.version))
|
|
@@ -148,11 +158,14 @@ export class LinuxDesktopManager {
|
|
|
148
158
|
}
|
|
149
159
|
}
|
|
150
160
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
151
|
-
const env = { ...this.#env, SHARDFLUX_CONFIG_DIR: dir, SHARDFLUX_API_URL: api, SHARDFLUX_NODE:
|
|
161
|
+
const env = { ...this.#env, SHARDFLUX_CONFIG_DIR: dir, SHARDFLUX_API_URL: api, SHARDFLUX_NODE: stable.node, SHARDFLUX_CLI: stable.script };
|
|
152
162
|
const desktop = join(this.home, '.local/share/applications/dev.shardflux.Desktop.desktop');
|
|
153
163
|
mkdirSync(dirname(desktop), { recursive: true });
|
|
154
|
-
const args = ['/usr/bin/env', `SHARDFLUX_CONFIG_DIR=${dir}`, `SHARDFLUX_API_URL=${api}`, `SHARDFLUX_NODE=${
|
|
164
|
+
const args = ['/usr/bin/env', `SHARDFLUX_CONFIG_DIR=${dir}`, `SHARDFLUX_API_URL=${api}`, `SHARDFLUX_NODE=${stable.node}`, `SHARDFLUX_CLI=${stable.script}`, join(this.app, 'shardflux-desktop')];
|
|
155
165
|
writeFileSync(desktop, `[Desktop Entry]\nType=Application\nName=Shardflux\nComment=Your agents, a shortcut away\nExec=${args.map(desktopString).join(' ')}\nIcon=${join(this.app, 'ui/assets/shardflux-icon.png')}\nTerminal=false\nCategories=Development;\nStartupWMClass=dev.shardflux.Desktop\n`, { mode: 0o644 });
|
|
166
|
+
const autostart = join(this.home, '.config/autostart/shardflux.desktop');
|
|
167
|
+
if (existsSync(autostart))
|
|
168
|
+
cpSync(desktop, autostart);
|
|
156
169
|
if ((await this.status(dir)).running)
|
|
157
170
|
await this.#exec(join(this.app, 'shardflux-desktop'), ['--control', view], { env, timeout: 5_000 });
|
|
158
171
|
else {
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CheckoutStatus, ExecSession, ExecutionResult } from '@shardflux/sdk';
|
|
1
|
+
import type { RateLimitInfo, CheckoutStatus, ExecSession, ExecutionResult } from '@shardflux/sdk';
|
|
2
2
|
export declare const EXIT: {
|
|
3
3
|
readonly ok: 0;
|
|
4
4
|
/** Any other failure: API refusal (conflict, quota, ...), network, protocol. */
|
|
@@ -145,6 +145,7 @@ export interface ErrorInfo {
|
|
|
145
145
|
execution_id?: string;
|
|
146
146
|
/** The refusal's Retry-After in seconds (0.9.0+), e.g. 429 quota_exceeded from the cell gateway. */
|
|
147
147
|
retry_after_seconds?: number;
|
|
148
|
+
rate_limit?: RateLimitInfo;
|
|
148
149
|
}
|
|
149
150
|
/**
|
|
150
151
|
* Removes anything shaped like a Shardflux API key or user session token from text, and each of the given secrets
|
package/dist/errors.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* stderr with the API's error code, message and request id; the process exit
|
|
4
4
|
* code tells scripts what kind of failure it was (README "Exit codes").
|
|
5
5
|
*/
|
|
6
|
-
import { CheckoutTimeoutError, DurabilityLostError, ExecStartError, FilesystemSaveFailedError, FilesystemSaveTimeoutError, OperationFailedError, OperationTimeoutError, ShardfluxApiError, ShardfluxProtocolError, TemplateBuildTimeoutError, TemplateFileError, TemplateUploadError } from '@shardflux/sdk';
|
|
6
|
+
import { budgetHint, CheckoutTimeoutError, DurabilityLostError, ExecStartError, FilesystemSaveFailedError, FilesystemSaveTimeoutError, OperationFailedError, OperationTimeoutError, ShardfluxApiError, ShardfluxProtocolError, TemplateBuildTimeoutError, TemplateFileError, TemplateUploadError } from '@shardflux/sdk';
|
|
7
7
|
export const EXIT = {
|
|
8
8
|
ok: 0,
|
|
9
9
|
/** Any other failure: API refusal (conflict, quota, ...), network, protocol. */
|
|
@@ -212,7 +212,9 @@ export function planDiskLimit(failure) {
|
|
|
212
212
|
/** Bytes as the plan names a disk: `2 GiB`, `2.5 GiB`. */
|
|
213
213
|
export const gib = (bytes) => `${Number((bytes / 2 ** 30).toFixed(2))} GiB`;
|
|
214
214
|
export function ownHint(err) {
|
|
215
|
-
|
|
215
|
+
if (err instanceof ShardfluxApiError && err.reason === 'codex_chatgpt_login_required')
|
|
216
|
+
return 'Sign in to Codex with ChatGPT using codex login, then copy your Codex login with shard setup again.';
|
|
217
|
+
return budgetHint(err) ?? (err instanceof HintedError ? err.hint : undefined);
|
|
216
218
|
}
|
|
217
219
|
function isNamed(err, name) {
|
|
218
220
|
return typeof err === 'object' && err !== null && err.name === name;
|
|
@@ -238,6 +240,7 @@ export function describeError(err) {
|
|
|
238
240
|
...(err.operationId ? { operation_id: err.operationId } : {}),
|
|
239
241
|
...(err.details ? { details: err.details } : {}),
|
|
240
242
|
source: err.source,
|
|
243
|
+
...(err.rateLimit ? { rate_limit: err.rateLimit } : {}),
|
|
241
244
|
...(err.retryAfterSeconds !== undefined ? { retry_after_seconds: err.retryAfterSeconds } : {}),
|
|
242
245
|
};
|
|
243
246
|
}
|
|
@@ -8,10 +8,32 @@ import { AGENT_HANDLERS } from "./agent-commands.js";
|
|
|
8
8
|
import { readSetupProgress } from "./assisted-setup.js";
|
|
9
9
|
import { syncAgentConfig } from "./agent-config-sync.js";
|
|
10
10
|
import { progress } from "./progress.js";
|
|
11
|
+
import { getConversation, localConversations } from "./conversations.js";
|
|
11
12
|
function remote() {
|
|
12
13
|
const project = projectIdentity('./');
|
|
13
14
|
return { project, repoRemote: project.remote ?? normalizeRemote(`file://${project.folder}`) };
|
|
14
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* The VM this computer saves a project's template from (0.23.0+): the newest conversation this checkout started whose
|
|
18
|
+
* VM is there now, else the project's workspace (`<project>/main`, setups from before conversations). Each
|
|
19
|
+
* conversation's VM is read from the API: a restore by any client moves it to a new VM. In a VM, `shard template save`
|
|
20
|
+
* saves that VM itself.
|
|
21
|
+
*/
|
|
22
|
+
async function templateSource(ctx, project) {
|
|
23
|
+
const gone = (error) => { if (error instanceof ShardfluxApiError && error.status === 404)
|
|
24
|
+
return undefined; throw error; };
|
|
25
|
+
const started = Object.values(localConversations(ctx)).filter(r => r.folder === project.folder && r.api_url === ctx.apiUrl)
|
|
26
|
+
.sort((a, b) => Date.parse(b.conversation.created_at) - Date.parse(a.conversation.created_at));
|
|
27
|
+
for (const r of started) {
|
|
28
|
+
const c = await getConversation(ctx, r.conversation.id).catch(gone);
|
|
29
|
+
if (!c || c.ended_at || !c.workspace_id)
|
|
30
|
+
continue;
|
|
31
|
+
const ws = await ctx.cloud().workspaces.get(c.workspace_id, { signal: ctx.signal }).catch(gone);
|
|
32
|
+
if (ws && !ws.data.deleted_at)
|
|
33
|
+
return ws;
|
|
34
|
+
}
|
|
35
|
+
return await ctx.cloud().workspaces.findByKey(`${project.slug}/main`, { signal: ctx.signal }) ?? undefined;
|
|
36
|
+
}
|
|
15
37
|
/**
|
|
16
38
|
* One line per status: what is in use and what is saving or failed, its platform base and setup cache. A project
|
|
17
39
|
* template unused for 30 days (rebase_deferred, 0.20.1+) follows the machine when the next conversation starts.
|
|
@@ -202,7 +224,7 @@ export const FILESYSTEM_HANDLERS = {
|
|
|
202
224
|
async template(ctx) {
|
|
203
225
|
const { project, repoRemote } = remote();
|
|
204
226
|
const state = await ctx.cloud().templates.getProject(repoRemote, { signal: ctx.signal });
|
|
205
|
-
const ws = await
|
|
227
|
+
const ws = await templateSource(ctx, project);
|
|
206
228
|
const progress = ws ? await readSetupProgress(ctx, ws.data) : undefined;
|
|
207
229
|
const attachments = ws ? await ctx.cloud().volumes.listAttached(ws.id) : undefined;
|
|
208
230
|
const mounted = attachments?.data.some((a) => a.mount_path === '/mnt/shardflux-cache' && a.mounted && a.volume.name === 'shardflux-setup-cache' && a.volume.org_shared);
|
|
@@ -210,9 +232,9 @@ export const FILESYSTEM_HANDLERS = {
|
|
|
210
232
|
},
|
|
211
233
|
async 'template save'(ctx, values) {
|
|
212
234
|
const { project, repoRemote } = remote();
|
|
213
|
-
const ws = await
|
|
235
|
+
const ws = await templateSource(ctx, project);
|
|
214
236
|
if (!ws)
|
|
215
|
-
throw new UsageError('
|
|
237
|
+
throw new UsageError('Start a conversation in this project with shard ./ before saving its template', 'or run shard template save in a conversation\'s VM');
|
|
216
238
|
return saved(ctx, values, options => ctx.cloud().templates.saveProject({ repoRemote, workspaceId: ws.id }, options), () => ctx.cloud().templates.preflightProject({ workspaceId: ws.id }, { signal: ctx.signal }), 'template');
|
|
217
239
|
},
|
|
218
240
|
async 'template rollback'(ctx, values) {
|
package/dist/menubar.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import type { Handler } from './commands.js';
|
|
2
|
-
|
|
2
|
+
/** `env` adds variables to this process's environment for the tool (the app's --control reads its config directory). */
|
|
3
|
+
export type NativeRunner = (tool: string, args: string[], env?: Record<string, string>) => Promise<string>;
|
|
3
4
|
/**
|
|
4
5
|
* Every native tool a default MenubarManager runs (plutil, codesign, xattr, open, the app's --control) goes through
|
|
5
6
|
* `nativeTools.run`. The CLI test support replaces it, so a test run never starts, stops or inspects a real app,
|
|
6
7
|
* whether or not a built Shardflux.app is in the package.
|
|
7
8
|
*/
|
|
8
9
|
export declare const nativeTools: {
|
|
9
|
-
run: (tool: string, args: string[], signal?: AbortSignal) => Promise<string>;
|
|
10
|
+
run: (tool: string, args: string[], signal?: AbortSignal, env?: Record<string, string>) => Promise<string>;
|
|
10
11
|
};
|
|
11
12
|
export interface MenubarStatus {
|
|
12
13
|
installed: boolean;
|
|
@@ -57,9 +58,9 @@ export declare class MenubarManager {
|
|
|
57
58
|
packagedVersion(): Promise<string>;
|
|
58
59
|
/** Whether the app shows any window (see windowsOpen); null when that could not be read. */
|
|
59
60
|
windowsOpen(pids: number[]): Promise<boolean | null>;
|
|
60
|
-
status(): Promise<MenubarStatus>;
|
|
61
|
-
stop(): Promise<MenubarStatus>;
|
|
62
|
-
login(value: 'on' | 'off'): Promise<MenubarStatus>;
|
|
61
|
+
status(configDir?: string): Promise<MenubarStatus>;
|
|
62
|
+
stop(configDir?: string): Promise<MenubarStatus>;
|
|
63
|
+
login(value: 'on' | 'off', configDir?: string): Promise<MenubarStatus>;
|
|
63
64
|
start(configDir: string, apiUrl: string): Promise<MenubarStatus>;
|
|
64
65
|
/**
|
|
65
66
|
* Brings an installed app up to the one this CLI's npm package carries, without disturbing the person: nothing when
|