@shardflux/cli 0.20.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 +312 -1
- package/README.md +242 -75
- package/dist/account-args.js +7 -2
- package/dist/account-commands.js +56 -106
- package/dist/account-pool.d.ts +19 -15
- package/dist/account-pool.js +47 -96
- package/dist/account-scope.d.ts +65 -0
- package/dist/account-scope.js +246 -0
- package/dist/acp-journal.js +58 -6
- package/dist/acp-login.d.ts +13 -5
- package/dist/acp-login.js +37 -8
- package/dist/acp-models.d.ts +6 -1
- package/dist/acp-models.js +29 -19
- package/dist/acp.js +120 -14
- package/dist/agent-commands.d.ts +4 -1
- package/dist/agent-commands.js +110 -42
- package/dist/agent-config-return.d.ts +46 -2
- package/dist/agent-config-return.js +110 -4
- package/dist/agent-credentials.js +7 -3
- package/dist/agent-repository.d.ts +16 -3
- package/dist/agent-repository.js +149 -74
- package/dist/agent-setup.d.ts +14 -3
- package/dist/agent-setup.js +78 -36
- package/dist/agent-target-commands.d.ts +20 -1
- package/dist/agent-target-commands.js +73 -31
- package/dist/agent-targets.js +6 -1
- package/dist/args.d.ts +1 -1
- package/dist/args.js +95 -54
- package/dist/bin.js +5 -1
- package/dist/browser.d.ts +13 -2
- package/dist/browser.js +30 -14
- package/dist/claude-cloud-login.d.ts +24 -2
- package/dist/claude-cloud-login.js +151 -11
- package/dist/cli-proxy-hub.d.ts +61 -25
- package/dist/cli-proxy-hub.js +202 -54
- package/dist/cli-proxy.d.ts +44 -1
- package/dist/cli-proxy.js +177 -28
- package/dist/client-settings.d.ts +39 -0
- package/dist/client-settings.js +91 -0
- package/dist/codex-cloud.d.ts +14 -4
- package/dist/codex-cloud.js +135 -31
- package/dist/codex-linux.d.ts +14 -0
- package/dist/codex-linux.js +69 -0
- package/dist/command-center-actions.js +18 -5
- 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 +173 -24
- package/dist/conversation-commands.d.ts +5 -0
- package/dist/conversation-commands.js +8 -5
- package/dist/conversation-terminal.d.ts +10 -2
- package/dist/conversation-terminal.js +78 -13
- package/dist/conversations.d.ts +86 -1
- package/dist/conversations.js +306 -23
- package/dist/desktop-linux.js +16 -3
- package/dist/device-bridge.d.ts +1 -1
- package/dist/device-bridge.js +6 -4
- package/dist/device-helper.js +43 -22
- package/dist/errors.d.ts +29 -2
- package/dist/errors.js +50 -7
- package/dist/filesystem-commands.d.ts +16 -9
- package/dist/filesystem-commands.js +133 -57
- package/dist/format.d.ts +2 -1
- package/dist/format.js +10 -1
- package/dist/handlers.js +3 -1
- package/dist/initial-prompt.d.ts +8 -0
- package/dist/initial-prompt.js +43 -1
- package/dist/large-media.d.ts +14 -0
- package/dist/large-media.js +100 -0
- package/dist/linux-terminal.d.ts +5 -3
- package/dist/linux-terminal.js +6 -20
- package/dist/local-sessions.d.ts +26 -2
- package/dist/local-sessions.js +113 -24
- package/dist/machine-setup.d.ts +11 -1
- package/dist/machine-setup.js +69 -22
- package/dist/machine-status.d.ts +67 -0
- package/dist/machine-status.js +140 -0
- package/dist/main.js +11 -13
- package/dist/menubar.d.ts +6 -5
- package/dist/menubar.js +38 -28
- package/dist/move-worktrees-restore.py +144 -0
- package/dist/move-worktrees.d.ts +69 -0
- package/dist/move-worktrees.js +252 -0
- package/dist/move.d.ts +10 -1
- package/dist/move.js +111 -16
- package/dist/onboarding.d.ts +11 -7
- package/dist/onboarding.js +63 -29
- package/dist/prepared-repository.d.ts +204 -0
- package/dist/prepared-repository.js +860 -0
- package/dist/prepared-repository.py +1307 -0
- package/dist/project-preparation.d.ts +62 -0
- package/dist/project-preparation.js +316 -0
- package/dist/project-sync-manifest.d.ts +4 -0
- package/dist/project-sync-manifest.js +5 -1
- package/dist/project-sync-transfer.js +1 -1
- package/dist/project-sync.d.ts +22 -4
- package/dist/project-sync.js +34 -8
- package/dist/project-template-setup.d.ts +33 -1
- package/dist/project-template-setup.js +59 -2
- package/dist/secret-ask.d.ts +72 -0
- package/dist/secret-ask.js +351 -0
- package/dist/seed-phases.d.ts +8 -1
- package/dist/seed-phases.js +17 -2
- package/dist/setup-cache.d.ts +7 -0
- package/dist/setup-cache.js +32 -0
- package/dist/t3-launch.d.ts +2 -1
- package/dist/t3-launch.js +12 -6
- package/dist/t3-mode.d.ts +8 -2
- package/dist/t3-mode.js +19 -4
- package/dist/t3-phone.d.ts +4 -2
- package/dist/t3-phone.js +12 -4
- package/dist/t3-proxy.d.ts +81 -0
- package/dist/t3-proxy.js +325 -0
- package/dist/terminal-apps.d.ts +168 -0
- package/dist/terminal-apps.js +574 -0
- package/dist/terminal-onboarding.js +6 -1
- package/dist/this-computer.d.ts +11 -0
- package/dist/this-computer.js +36 -0
- package/dist/thread-on-computer.js +4 -1
- package/dist/update.d.ts +2 -0
- package/dist/update.js +43 -9
- package/dist/worktree-twin.d.ts +10 -2
- package/dist/worktree-twin.js +27 -9
- package/package.json +5 -5
package/dist/conversations.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { CellClient, Workspace, WorkspaceView } from '@shardflux/sdk';
|
|
2
2
|
import type { Ctx } from './commands.js';
|
|
3
|
+
import type { LocalChanges } from './agent-repository.js';
|
|
3
4
|
import type { ModelChoice } from './agent-models.js';
|
|
5
|
+
import type { PreparedApply, PreparedTransfer } from './prepared-repository.js';
|
|
4
6
|
export type Harness = 'claude' | 'codex';
|
|
5
7
|
export interface Conversation {
|
|
6
8
|
id: string;
|
|
@@ -62,6 +64,33 @@ export interface LocalConversation {
|
|
|
62
64
|
handed_off?: boolean;
|
|
63
65
|
/** The branch the conversation started from (0.16.0+), for APIs that do not return base_branch yet. */
|
|
64
66
|
base_branch?: string;
|
|
67
|
+
/**
|
|
68
|
+
* The prepared revision this conversation starts from (0.22.0+): captured while its VM was allocated and kept until
|
|
69
|
+
* the start is done, so a start resumed after a deadline uses exactly that state. `deferred`: large files still
|
|
70
|
+
* following the first prompt.
|
|
71
|
+
*/
|
|
72
|
+
preparation?: {
|
|
73
|
+
revision: string;
|
|
74
|
+
cache: string;
|
|
75
|
+
state: 'accepted' | 'syncing' | 'deferred' | 'ready' | 'error';
|
|
76
|
+
error?: string;
|
|
77
|
+
workspace?: string;
|
|
78
|
+
transfer?: PreparedTransfer;
|
|
79
|
+
apply?: PreparedApply;
|
|
80
|
+
pending?: string[];
|
|
81
|
+
};
|
|
82
|
+
cloud_start?: {
|
|
83
|
+
prompt?: string;
|
|
84
|
+
computer: boolean;
|
|
85
|
+
attachments_pending: boolean;
|
|
86
|
+
submitted?: boolean;
|
|
87
|
+
state: 'accepted' | 'active' | 'error';
|
|
88
|
+
error?: string;
|
|
89
|
+
/** When the process carrying this start last said so (carryStart): the device helper resumes only one nobody carries. */
|
|
90
|
+
heartbeat_at?: number;
|
|
91
|
+
};
|
|
92
|
+
/** Why this start left its prepared revision for the copy seed. */
|
|
93
|
+
preparation_fallback?: string;
|
|
65
94
|
}
|
|
66
95
|
/**
|
|
67
96
|
* Whether the conversation has ended: its VM was reclaimed, or it was archived or discarded. A finished turn is `done`
|
|
@@ -82,6 +111,9 @@ export declare function rememberConversation(ctx: Pick<Ctx, 'store' | 'apiUrl'>,
|
|
|
82
111
|
export declare function conversationsUnavailable(e: unknown): boolean;
|
|
83
112
|
export declare function getConversation(ctx: Ctx, id: string): Promise<Conversation>;
|
|
84
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>;
|
|
85
117
|
export declare function resolveConversation(ctx: Ctx, ref: string, folder?: string): Promise<Conversation>;
|
|
86
118
|
/** Re-resolve the binding on every attach: a move changes the VM, never the conversation identity. */
|
|
87
119
|
export declare function conversationWorkspace(ctx: Ctx, c: Conversation): Promise<Workspace>;
|
|
@@ -95,14 +127,67 @@ export declare function restoreConversationBinding(ctx: Ctx, c: Conversation, ws
|
|
|
95
127
|
export interface MovedInSession {
|
|
96
128
|
session_id: string;
|
|
97
129
|
title?: string;
|
|
130
|
+
preserveWorktrees?: boolean;
|
|
98
131
|
}
|
|
99
|
-
|
|
132
|
+
/**
|
|
133
|
+
* `ready`: checks the caller runs beside this local work (a sign-in check, 0.21.1+); the VM is requested only after
|
|
134
|
+
* they pass. The conversation API's availability is read meanwhile too.
|
|
135
|
+
*/
|
|
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[];
|
|
142
|
+
/**
|
|
143
|
+
* Brings the conversation's accepted revision into its VM (0.22.0+): only what the project cache lacks is sent, and
|
|
144
|
+
* with `defer` large incompressible files follow the first prompt. Resumable: a later call (the device helper, or
|
|
145
|
+
* `shard agents open`) continues the same revision, never a newer checkout. False when the start has no revision.
|
|
146
|
+
*/
|
|
147
|
+
export declare function prepareAcceptedConversation(ctx: Ctx, c: Conversation, ws: Workspace, options?: {
|
|
148
|
+
defer?: boolean;
|
|
149
|
+
fallback?: boolean;
|
|
150
|
+
setup?: Promise<{
|
|
151
|
+
depsHash: string;
|
|
152
|
+
} | undefined>;
|
|
153
|
+
landNow?: boolean;
|
|
154
|
+
}): Promise<boolean>;
|
|
155
|
+
/**
|
|
156
|
+
* Sends a conversation's deferred large files and lands them in its worktree. Runs while its VM is awake (it never
|
|
157
|
+
* wakes one); the device helper retries until the start is whole.
|
|
158
|
+
*/
|
|
159
|
+
export declare function completeDeferredPreparation(ctx: Ctx, c: Conversation): Promise<boolean>;
|
|
160
|
+
/**
|
|
161
|
+
* The one-time seed of a new conversation's worktree (a conversation never watches or writes the owner's checkout):
|
|
162
|
+
* its prepared revision (0.22.0+), or the copy seed when the start has none or the VM cannot take one. Repeatable: a
|
|
163
|
+
* start resumed after a deadline continues here.
|
|
164
|
+
*/
|
|
165
|
+
export declare function seedConversation(ctx: Ctx, c: Conversation, ws: Workspace, folder: string, movedIn?: MovedInSession & {
|
|
166
|
+
seedRepository?(ws: Workspace, c: Conversation): Promise<void>;
|
|
167
|
+
}, changes?: Promise<LocalChanges>, setup?: Promise<{
|
|
168
|
+
depsHash: string;
|
|
169
|
+
} | undefined>, editor?: {
|
|
170
|
+
initialFiles?: readonly string[];
|
|
171
|
+
onInitialFiles?: () => void;
|
|
172
|
+
landNow?: boolean;
|
|
173
|
+
}): Promise<void>;
|
|
100
174
|
export declare function createConversation(ctx: Ctx, folder: string, harness: Harness, route: Conversation['route'], name?: string, choice?: ModelChoice, movedIn?: MovedInSession & {
|
|
101
175
|
beforeSeed(ws: Workspace, c: Conversation): Promise<void>;
|
|
176
|
+
seedRepository?(ws: Workspace, c: Conversation): Promise<void>;
|
|
177
|
+
}, setup?: Promise<{
|
|
178
|
+
depsHash: string;
|
|
179
|
+
} | undefined>, ready?: Promise<unknown>, startup?: {
|
|
180
|
+
accepted(c: Conversation): void;
|
|
181
|
+
beforeSeed(ws: Workspace, c: Conversation): Promise<void>;
|
|
102
182
|
}): Promise<{
|
|
103
183
|
conversation: Conversation;
|
|
104
184
|
workspace: Workspace;
|
|
105
185
|
}>;
|
|
186
|
+
/**
|
|
187
|
+
* The conversation branch, then the start state's snapshot, in one request (0.21.1+): a failed checkout fails the
|
|
188
|
+
* start, a failed snapshot only leaves it without one (recordSeedSnapshot). Its hub push stays detached in the VM.
|
|
189
|
+
*/
|
|
190
|
+
export declare function checkoutConversationBranch(ctx: Pick<Ctx, 'store' | 'signal'>, cell: Pick<CellClient, 'exec'>, c: Conversation): Promise<void>;
|
|
106
191
|
export declare function resumeConversation(ctx: Ctx, c: Conversation): Promise<Conversation>;
|
|
107
192
|
/** The model and effort a conversation's harness starts with, as its record keeps them. */
|
|
108
193
|
export declare const recordedChoice: (c: Pick<Conversation, "model" | "effort">) => ModelChoice;
|
package/dist/conversations.js
CHANGED
|
@@ -4,13 +4,19 @@ import { AgentStateStore } from "./agent-state.js";
|
|
|
4
4
|
import { gitRunner } from "./agent-git.js";
|
|
5
5
|
import { projectIdentity } from "./project-identity.js";
|
|
6
6
|
import { ensureProjectGit } from "./project-git.js";
|
|
7
|
-
import { preflightProjectSeed, seedAgentRepository } from "./agent-repository.js";
|
|
8
|
-
import { UsageError } from "./errors.js";
|
|
7
|
+
import { localChanges, preflightProjectSeed, runGitStep, seedAgentRepository } from "./agent-repository.js";
|
|
8
|
+
import { CodedError, UsageError } from "./errors.js";
|
|
9
9
|
import { conversationClient } from "./conversation-commands.js";
|
|
10
|
-
import { recordSeedSnapshot } from "./files-mirror.js";
|
|
10
|
+
import { HUB_PATH, recordSeedSnapshot, SEED_SNAPSHOT } from "./files-mirror.js";
|
|
11
11
|
import { rememberTwinSeed, sendTwinSeed, twinCheckoutArgv, twinSeed } from "./worktree-twin.js";
|
|
12
12
|
import { progress } from "./progress.js";
|
|
13
13
|
import { seedPhase, startProgress } from "./seed-phases.js";
|
|
14
|
+
import { storedTitlePrefix } from "./client-settings.js";
|
|
15
|
+
import { applyPreparedSnapshot, capturePreparedSnapshot, landDeferredFiles, preparedCachePath, preparedGitConfig, PreparedRefusal, pruneLocalPrepared, readPreparedSnapshot, releaseLocalSnapshot, transferPreparedSnapshot } from "./prepared-repository.js";
|
|
16
|
+
import { withTargetLock } from "./target-lock.js";
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { acceptedProgress } from "./seed-phases.js";
|
|
19
|
+
import { recordSetupIntent } from "./project-template-setup.js";
|
|
14
20
|
/**
|
|
15
21
|
* Whether the conversation has ended: its VM was reclaimed, or it was archived or discarded. A finished turn is `done`
|
|
16
22
|
* while its VM still takes the next turn; on Codex Cloud, `done` ends the conversation.
|
|
@@ -68,7 +74,7 @@ export function rememberConversation(ctx, c, folder, handedOff = false, baseBran
|
|
|
68
74
|
new AgentStateStore(ctx.store.dir).update(doc => {
|
|
69
75
|
const records = doc.conversations ?? {};
|
|
70
76
|
const base = baseBranch ?? records[c.id]?.base_branch;
|
|
71
|
-
records[c.id] = { api_url: ctx.apiUrl, folder, project_id: c.project_id, conversation: c, ...(handedOff ? { handed_off: true } : {}), ...(base ? { base_branch: base } : {}) };
|
|
77
|
+
records[c.id] = { ...records[c.id], api_url: ctx.apiUrl, folder, project_id: c.project_id, conversation: c, ...(handedOff ? { handed_off: true } : {}), ...(base ? { base_branch: base } : {}) };
|
|
72
78
|
doc.conversations = records;
|
|
73
79
|
});
|
|
74
80
|
}
|
|
@@ -90,6 +96,41 @@ export async function listConversations(ctx, projectId) {
|
|
|
90
96
|
} while (cursor);
|
|
91
97
|
return all;
|
|
92
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
|
+
}
|
|
93
134
|
export async function resolveConversation(ctx, ref, folder) {
|
|
94
135
|
if (ref.startsWith('conv_'))
|
|
95
136
|
return getConversation(ctx, ref);
|
|
@@ -157,58 +198,287 @@ export async function restoreConversationBinding(ctx, c, ws) {
|
|
|
157
198
|
if (c.restore.kind === 'resume')
|
|
158
199
|
progress(ctx, 'Restored');
|
|
159
200
|
}
|
|
160
|
-
|
|
201
|
+
/**
|
|
202
|
+
* `ready`: checks the caller runs beside this local work (a sign-in check, 0.21.1+); the VM is requested only after
|
|
203
|
+
* they pass. The conversation API's availability is read meanwhile too.
|
|
204
|
+
*/
|
|
205
|
+
export async function createConversationRecord(ctx, folder, harness, route, name, allocated, idempotencyKey = randomUUID(), choice = {}, movedIn, ready) {
|
|
161
206
|
const project = projectIdentity(folder);
|
|
162
207
|
await ensureProjectGit(ctx, project.folder);
|
|
163
208
|
// A start refuses before a VM is allocated, never after (0.19.0+).
|
|
164
209
|
await preflightProjectSeed(project.folder, ctx.signal);
|
|
165
|
-
// Both the conversation API and its legacy workspace fallback need a usable local HEAD.
|
|
166
|
-
|
|
210
|
+
// Both the conversation API and its legacy workspace fallback need a usable local HEAD. The API's answer (a legacy
|
|
211
|
+
// control plane refuses) is awaited before the create, after the local snapshot it runs beside.
|
|
212
|
+
const probe = conversationClient(ctx).conversations.list({ limit: 1 }).then(() => undefined, (error) => ({ error }));
|
|
167
213
|
const git = gitRunner(project.folder, { ...process.env, ...ctx.io.env }, ctx.signal);
|
|
168
214
|
const head = await git(['rev-parse', 'HEAD']);
|
|
169
215
|
const branch = await git(['symbolic-ref', '--quiet', '--short', 'HEAD']).catch(() => undefined);
|
|
170
216
|
// Uncommitted edits ride along as the twin snapshot the conversation starts from (worktree-twin.ts).
|
|
171
|
-
const seed =
|
|
217
|
+
const seed = movedIn?.preserveWorktrees ? { commit: head, snapshot: false }
|
|
218
|
+
: await twinSeed(ctx.store.dir, project.folder, head, ctx.store.profile(ctx.apiUrl).project_id).catch(() => ({ commit: head, snapshot: false }));
|
|
172
219
|
seedPhase('local_snapshot');
|
|
220
|
+
const refused = await probe;
|
|
221
|
+
if (refused)
|
|
222
|
+
throw refused.error;
|
|
223
|
+
await ready;
|
|
224
|
+
seedPhase('api_probe');
|
|
173
225
|
const commit = seed.commit;
|
|
226
|
+
// The prepared revision of that state (0.22.0+): its media and what Git does not hold (.env files, filter output),
|
|
227
|
+
// captured while the VM is requested and boots. A capture that cannot complete falls back to the copy seed, never fails
|
|
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) => {
|
|
230
|
+
if (ctx.signal.aborted)
|
|
231
|
+
throw error;
|
|
232
|
+
ctx.io.stderr.write(`Project snapshot: ${error instanceof Error ? error.message : String(error)}; copying the project instead.\n`);
|
|
233
|
+
return undefined;
|
|
234
|
+
});
|
|
235
|
+
capture.catch(() => undefined);
|
|
236
|
+
// The title marker this computer chose (`shard settings --title-prefix`, 0.20.1+); none sent means the platform's.
|
|
237
|
+
const titlePrefix = storedTitlePrefix(ctx.store.dir);
|
|
174
238
|
const response = await conversationClient(ctx).conversations.create({ repoRemote: project.remote ?? `file://${project.folder}`, harness, route, mode: 'new',
|
|
175
239
|
base: { commit, ...(branch ? { branch } : {}) }, ...(name ? { name } : {}), ...choice, projectId: ctx.store.profile(ctx.apiUrl).project_id,
|
|
240
|
+
...(titlePrefix !== undefined ? { titlePrefix } : {}),
|
|
176
241
|
...(movedIn ? { harnessSessionId: movedIn.session_id, ...(movedIn.title ? { title: movedIn.title.slice(0, 200) } : {}) } : {}) }, { idempotencyKey, signal: ctx.signal });
|
|
177
242
|
const c = validateConversation({ ...response.conversation, restore: response.restore });
|
|
178
243
|
allocated?.(response.workspace);
|
|
179
244
|
rememberTwinSeed(ctx.store.dir, c.id, project.folder, seed, branch);
|
|
180
245
|
rememberConversation(ctx, c, project.folder, false, branch);
|
|
246
|
+
// The seed awaits the capture (prepareAcceptedConversation), so it runs on while the VM boots, with the Git settings
|
|
247
|
+
// the VM repository copies from this one.
|
|
248
|
+
pendingCaptures.set(c.id, capture.then(async (prepared) => {
|
|
249
|
+
seedPhase('capture_done');
|
|
250
|
+
if (!prepared)
|
|
251
|
+
return undefined;
|
|
252
|
+
new AgentStateStore(ctx.store.dir).update(doc => {
|
|
253
|
+
const record = doc.conversations[c.id];
|
|
254
|
+
if (record)
|
|
255
|
+
record.preparation = { revision: prepared.revision, cache: preparedCachePath(project.remote ?? `file://${project.folder}`), state: 'accepted' };
|
|
256
|
+
});
|
|
257
|
+
return { snapshot: prepared, git: await preparedGitConfig(project.folder, prepared.manifest, ctx.signal) };
|
|
258
|
+
}));
|
|
259
|
+
acceptedProgress(c.id, c.key);
|
|
181
260
|
return c;
|
|
182
261
|
}
|
|
183
|
-
|
|
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
|
+
}
|
|
270
|
+
/** Captures still running for conversations this process created (createConversationRecord). */
|
|
271
|
+
const pendingCaptures = new Map();
|
|
272
|
+
/** A start the copy seed took over: its prepared revision is no longer this conversation's (recorded as the reason). */
|
|
273
|
+
function dropPreparation(ctx, id, reason) {
|
|
274
|
+
new AgentStateStore(ctx.store.dir).update(doc => {
|
|
275
|
+
const record = doc.conversations[id];
|
|
276
|
+
if (record?.preparation) {
|
|
277
|
+
delete record.preparation;
|
|
278
|
+
record.preparation_fallback = reason;
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
function updatePreparation(ctx, id, patch) {
|
|
283
|
+
new AgentStateStore(ctx.store.dir).update(doc => {
|
|
284
|
+
const record = doc.conversations[id];
|
|
285
|
+
if (record?.preparation)
|
|
286
|
+
record.preparation = { ...record.preparation, ...patch };
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
const errorText = (error) => (error instanceof Error ? error.message : String(error)).slice(0, 2000);
|
|
290
|
+
/** The template checkout a conversation outside it copies dependency folders from (a Codex Cloud worktree). */
|
|
291
|
+
const templateCheckout = (c) => `/home/user/${c.key.split('/')[0]}`;
|
|
292
|
+
/**
|
|
293
|
+
* Brings the conversation's accepted revision into its VM (0.22.0+): only what the project cache lacks is sent, and
|
|
294
|
+
* with `defer` large incompressible files follow the first prompt. Resumable: a later call (the device helper, or
|
|
295
|
+
* `shard agents open`) continues the same revision, never a newer checkout. False when the start has no revision.
|
|
296
|
+
*/
|
|
297
|
+
export async function prepareAcceptedConversation(ctx, c, ws, options = {}) {
|
|
298
|
+
const capturing = pendingCaptures.get(c.id);
|
|
299
|
+
let captured;
|
|
300
|
+
if (capturing) {
|
|
301
|
+
captured = await capturing.catch(() => undefined);
|
|
302
|
+
pendingCaptures.delete(c.id);
|
|
303
|
+
seedPhase('prepared_snapshot');
|
|
304
|
+
}
|
|
305
|
+
// The setup intent goes in the exec that checks the repository out, before the repository appears (0.22.0+).
|
|
306
|
+
const intent = await options.setup;
|
|
307
|
+
const local = localConversations(ctx)[c.id];
|
|
308
|
+
if (!local?.preparation)
|
|
309
|
+
return false;
|
|
310
|
+
if (local.preparation.state === 'ready' || local.preparation.state === 'deferred')
|
|
311
|
+
return true;
|
|
312
|
+
return withTargetLock(join(ctx.store.dir, 'prepared-starts', c.id), async () => {
|
|
313
|
+
const current = localConversations(ctx)[c.id]?.preparation;
|
|
314
|
+
if (!current)
|
|
315
|
+
return false;
|
|
316
|
+
if (current.state === 'ready' || current.state === 'deferred')
|
|
317
|
+
return true;
|
|
318
|
+
try {
|
|
319
|
+
updatePreparation(ctx, c.id, { state: 'syncing', error: undefined });
|
|
320
|
+
const fresh = captured?.snapshot.revision === current.revision ? captured : undefined;
|
|
321
|
+
const snapshot = fresh?.snapshot ?? await readPreparedSnapshot(ctx.store.dir, local.folder, current.revision);
|
|
322
|
+
const cell = ws.cell(), outside = c.worktree_path !== templateCheckout(c);
|
|
323
|
+
// The seed's copy to the project hub starts in the same exec as the checkout, detached (no extra round trip).
|
|
324
|
+
const target = { root: c.worktree_path, branch: c.branch, options: { signal: ctx.signal, adopt: true,
|
|
325
|
+
git: fresh?.git ?? await preparedGitConfig(local.folder, snapshot.manifest, ctx.signal), ...(outside ? { dependencySource: templateCheckout(c) } : {}),
|
|
326
|
+
after: { script: SEED_SNAPSHOT, args: [c.worktree_path, c.branch.replace(/^shard\//, ''), HUB_PATH, 'background'] },
|
|
327
|
+
...(intent ? { setupIntent: { conversation: c.id, depsHash: intent.depsHash } } : {}), workspace: ws.id, ...(options.landNow ? { landNow: true } : {}) } };
|
|
328
|
+
let transfer, apply;
|
|
329
|
+
try {
|
|
330
|
+
// A warm start: the probe checks the revision out in its own exec.
|
|
331
|
+
transfer = await transferPreparedSnapshot(cell, local.folder, current.cache, snapshot, { signal: ctx.signal, defer: options.defer ?? true, pin: c.id, apply: target });
|
|
332
|
+
seedPhase('prepared_transfer');
|
|
333
|
+
apply = transfer.applied ?? await applyPreparedSnapshot(cell, current.cache, current.revision, c.worktree_path, c.branch, { ...target.options, ...(transfer.complete ? { releasePin: c.id } : {}) });
|
|
334
|
+
}
|
|
335
|
+
catch (error) {
|
|
336
|
+
// No agent has this worktree yet: a VM that cannot take a prepared revision (a custom template without Python
|
|
337
|
+
// 3.11, say) still starts, through the copy seed, into a clean directory.
|
|
338
|
+
if (ctx.signal.aborted || !options.fallback || (error instanceof PreparedRefusal && error.reason === 'owned'))
|
|
339
|
+
throw error;
|
|
340
|
+
ctx.io.stderr.write(`Project transfer: ${errorText(error)}; copying the project instead.\n`);
|
|
341
|
+
// The copy seed owns this start from here: nothing resumes the prepared one onto its worktree.
|
|
342
|
+
dropPreparation(ctx, c.id, errorText(error));
|
|
343
|
+
await releaseLocalSnapshot(local.folder, current.revision).catch(() => undefined);
|
|
344
|
+
// A fresh start leaves nothing at the path; a checkout taken over in place (a template's) is set aside.
|
|
345
|
+
await cell.exec.run(['sh', '-c', 'if [ -e "$1" ]; then mv "$1" "$1.unfinished-$(date +%s)"; fi', 'shard-seed', c.worktree_path], { timeoutMs: 30_000, signal: ctx.signal });
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
seedPhase('prepared_apply');
|
|
349
|
+
updatePreparation(ctx, c.id, { state: transfer.complete ? 'ready' : 'deferred', transfer, apply, pending: transfer.pending, error: undefined, workspace: ws.id });
|
|
350
|
+
if (transfer.complete)
|
|
351
|
+
await releaseLocalSnapshot(local.folder, current.revision);
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
updatePreparation(ctx, c.id, { state: 'error', error: errorText(error) });
|
|
356
|
+
throw error;
|
|
357
|
+
}
|
|
358
|
+
}, ctx.signal);
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Sends a conversation's deferred large files and lands them in its worktree. Runs while its VM is awake (it never
|
|
362
|
+
* wakes one); the device helper retries until the start is whole.
|
|
363
|
+
*/
|
|
364
|
+
export async function completeDeferredPreparation(ctx, c) {
|
|
365
|
+
const local = localConversations(ctx)[c.id];
|
|
366
|
+
if (local?.preparation?.state !== 'deferred' || conversationEnded(c))
|
|
367
|
+
return false;
|
|
368
|
+
return withTargetLock(join(ctx.store.dir, 'prepared-starts', c.id), async () => {
|
|
369
|
+
const current = localConversations(ctx)[c.id]?.preparation;
|
|
370
|
+
if (current?.state !== 'deferred')
|
|
371
|
+
return false;
|
|
372
|
+
let ws;
|
|
373
|
+
try {
|
|
374
|
+
ws = await conversationClient(ctx).workspaces.get(c.workspace_id);
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
if (!(error instanceof ShardfluxApiError && error.status === 404))
|
|
378
|
+
throw error;
|
|
379
|
+
}
|
|
380
|
+
if (!ws || ws.data.deleted_at || (current.workspace && current.workspace !== c.workspace_id)) {
|
|
381
|
+
// The VM is gone (reclaimed, moved, deleted) or the conversation continues in another one (a resume restores its
|
|
382
|
+
// worktree there): there is no worktree left to complete.
|
|
383
|
+
updatePreparation(ctx, c.id, { state: 'error', error: 'The conversation\'s VM was removed before its media arrived.', pending: undefined });
|
|
384
|
+
await releaseLocalSnapshot(local.folder, current.revision);
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
if (!ws.ready)
|
|
388
|
+
return false;
|
|
389
|
+
const snapshot = await readPreparedSnapshot(ctx.store.dir, local.folder, current.revision);
|
|
390
|
+
const cell = ws.cell();
|
|
391
|
+
// Staged into the conversation's own repository and landed before this returns; the cache gets them after.
|
|
392
|
+
const transfer = await transferPreparedSnapshot(cell, local.folder, current.cache, snapshot, { signal: ctx.signal, pin: c.id,
|
|
393
|
+
apply: { root: c.worktree_path, branch: c.branch, options: { adopt: true } } });
|
|
394
|
+
// Media edited on this computer after the start never arrive as captured: the worktree goes on without them.
|
|
395
|
+
const drop = snapshot.manifest.files.filter(f => transfer.changed.includes(f.path)).map(f => f.oid);
|
|
396
|
+
const landed = await landDeferredFiles(cell, current.cache, current.revision, c.worktree_path, { signal: ctx.signal, drop });
|
|
397
|
+
if (landed.pending)
|
|
398
|
+
return false;
|
|
399
|
+
updatePreparation(ctx, c.id, { state: 'ready', transfer: { ...transfer, pending: current.pending ?? [] }, pending: undefined });
|
|
400
|
+
await releaseLocalSnapshot(local.folder, current.revision);
|
|
401
|
+
await pruneLocalPrepared(ctx.store.dir).catch(() => undefined);
|
|
402
|
+
return true;
|
|
403
|
+
}, ctx.signal);
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* The one-time seed of a new conversation's worktree (a conversation never watches or writes the owner's checkout):
|
|
407
|
+
* its prepared revision (0.22.0+), or the copy seed when the start has none or the VM cannot take one. Repeatable: a
|
|
408
|
+
* start resumed after a deadline continues here.
|
|
409
|
+
*/
|
|
410
|
+
export async function seedConversation(ctx, c, ws, folder, movedIn, changes, setup, editor = {}) {
|
|
411
|
+
// Requests the copy seed starts and nothing waits for (the history backfill's start), settled before this returns.
|
|
412
|
+
const background = [];
|
|
413
|
+
const cell = ws.cell();
|
|
414
|
+
// A conversation that sets its project up records it before its repository appears: its guest starts the agent as
|
|
415
|
+
// soon as the repository appears, then with the setup's context and toolchains (0.21.1+). A prepared start writes it
|
|
416
|
+
// in its checkout's exec; the copy seed beside its first request.
|
|
417
|
+
let recorded;
|
|
418
|
+
const recordIntent = () => recorded ??= (async () => {
|
|
419
|
+
const intent = await setup;
|
|
420
|
+
if (intent)
|
|
421
|
+
await recordSetupIntent(cell, c, intent.depsHash, ctx.signal).catch((error) => {
|
|
422
|
+
if (!ctx.signal.aborted)
|
|
423
|
+
ctx.io.stderr.write(`The agent starts without the project setup context: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
424
|
+
});
|
|
425
|
+
})();
|
|
426
|
+
if (movedIn?.seedRepository) {
|
|
427
|
+
void recordIntent();
|
|
428
|
+
await movedIn.seedRepository(ws, c);
|
|
429
|
+
}
|
|
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 {
|
|
435
|
+
await seedAgentRepository(ctx, ws, folder, { root: c.worktree_path, populate: true, changes: changes ?? localChanges(folder, ctx.signal),
|
|
436
|
+
background: started => background.push(started), beforeHistory: recordIntent(),
|
|
437
|
+
...(editor.initialFiles ? { initialFiles: editor.initialFiles } : {}), ...(editor.onInitialFiles ? { onInitialFiles: editor.onInitialFiles } : {}) });
|
|
438
|
+
}
|
|
439
|
+
await recorded;
|
|
440
|
+
seedPhase('seed_done');
|
|
441
|
+
const preparation = localConversations(ctx)[c.id]?.preparation?.state, prepared = preparation === 'ready' || preparation === 'deferred';
|
|
442
|
+
if (!prepared && !movedIn?.preserveWorktrees) {
|
|
443
|
+
await sendTwinSeed(ctx.store.dir, cell, c, folder, ctx.signal);
|
|
444
|
+
seedPhase('twin_seed');
|
|
445
|
+
await checkoutConversationBranch(ctx, cell, c);
|
|
446
|
+
seedPhase('branch_checkout');
|
|
447
|
+
}
|
|
448
|
+
else if (!prepared) {
|
|
449
|
+
await recordSeedSnapshot(ws, c, ctx.signal);
|
|
450
|
+
seedPhase('seed_snapshot');
|
|
451
|
+
}
|
|
452
|
+
await Promise.all(background);
|
|
453
|
+
// Large media that follow the first prompt keep coming while this process runs; the device helper finishes them.
|
|
454
|
+
if (preparation === 'deferred')
|
|
455
|
+
void completeDeferredPreparation(ctx, c).catch(() => undefined);
|
|
456
|
+
// This computer's spool and snapshots stay bounded with no selected project to prune them (hourly at most).
|
|
457
|
+
void pruneLocalPrepared(ctx.store.dir, undefined, 3_600_000).catch(() => undefined);
|
|
458
|
+
}
|
|
459
|
+
export async function createConversation(ctx, folder, harness, route, name, choice = {}, movedIn, setup, ready, startup) {
|
|
184
460
|
const project = projectIdentity(folder);
|
|
185
461
|
seedPhase('start');
|
|
186
462
|
// The launcher's phase line (SHARDFLUX_PROGRESS_FILE, 0.19.0+): a computer starts, then the seed's own marks move it on.
|
|
187
463
|
startProgress(harness, ctx.io.env);
|
|
188
|
-
const c = await createConversationRecord(ctx, project.folder, harness, route, name, undefined, undefined, choice, movedIn);
|
|
464
|
+
const c = await createConversationRecord(ctx, project.folder, harness, route, name, undefined, undefined, choice, movedIn, ready);
|
|
465
|
+
startup?.accepted(c);
|
|
189
466
|
seedPhase('conversation_create');
|
|
467
|
+
// A start without a prepared revision (a moved-in session) reads this computer's changes while its VM starts.
|
|
468
|
+
const changes = movedIn && !movedIn.seedRepository ? localChanges(project.folder, ctx.signal) : undefined;
|
|
469
|
+
changes?.catch(() => undefined);
|
|
190
470
|
try {
|
|
191
471
|
const ws = await conversationWorkspace(ctx, c);
|
|
192
472
|
seedPhase('vm_running');
|
|
193
473
|
// A moved-in session's transcript is in place before the repository: its guest resumes it once the seed is done.
|
|
194
474
|
await movedIn?.beforeSeed(ws, c);
|
|
195
|
-
|
|
196
|
-
await
|
|
197
|
-
const cell = ws.cell();
|
|
198
|
-
seedPhase('seed_done');
|
|
199
|
-
await sendTwinSeed(ctx.store.dir, cell, c, project.folder, ctx.signal);
|
|
200
|
-
seedPhase('twin_seed');
|
|
201
|
-
const result = await cell.exec.run(twinCheckoutArgv(ctx.store.dir, c), { timeoutMs: 30_000, signal: ctx.signal });
|
|
202
|
-
if (result.exitCode !== 0)
|
|
203
|
-
throw new Error(`Conversation branch could not be prepared: ${result.stderr.trim()}`);
|
|
204
|
-
seedPhase('branch_checkout');
|
|
205
|
-
await recordSeedSnapshot(ws, c, ctx.signal);
|
|
206
|
-
seedPhase('seed_snapshot');
|
|
475
|
+
await startup?.beforeSeed(ws, c);
|
|
476
|
+
await seedConversation(ctx, c, ws, project.folder, movedIn, changes, setup);
|
|
207
477
|
return { conversation: c, workspace: ws };
|
|
208
478
|
}
|
|
209
479
|
catch (error) {
|
|
210
480
|
// A failed move leaves nothing behind: its session can move again (the API counts only a live VM's move).
|
|
211
|
-
if (
|
|
481
|
+
if (movedIn) {
|
|
212
482
|
try {
|
|
213
483
|
await ctx.cloud().workspaces.delete(c.workspace_id);
|
|
214
484
|
}
|
|
@@ -216,9 +486,22 @@ export async function createConversation(ctx, folder, harness, route, name, choi
|
|
|
216
486
|
throw new AggregateError([error, cleanup], `Cloud setup failed; remove owned workspace ${c.workspace_id} before retrying.`, { cause: cleanup });
|
|
217
487
|
}
|
|
218
488
|
}
|
|
489
|
+
if (route === 'codex-cloud')
|
|
490
|
+
throw new CodedError('project_preparation_pending', `Accepted ${c.key}; project preparation can resume. Open this conversation to continue.`, `shard agents open ${c.id}`, { conversation_id: c.id, workspace_key: c.key, workspace_id: c.workspace_id, setup_state: localConversations(ctx)[c.id]?.preparation?.state ?? 'error' });
|
|
219
491
|
throw error;
|
|
220
492
|
}
|
|
221
493
|
}
|
|
494
|
+
const shellQuote = (s) => `'${s.replaceAll("'", "'\\''")}'`;
|
|
495
|
+
/**
|
|
496
|
+
* The conversation branch, then the start state's snapshot, in one request (0.21.1+): a failed checkout fails the
|
|
497
|
+
* start, a failed snapshot only leaves it without one (recordSeedSnapshot). Its hub push stays detached in the VM.
|
|
498
|
+
*/
|
|
499
|
+
export async function checkoutConversationBranch(ctx, cell, c) {
|
|
500
|
+
const checkout = `${twinCheckoutArgv(ctx.store.dir, c).map(shellQuote).join(' ')} || exit 3\nsh -c "$1" shard-seed "$2" "$3" "$4" background >/dev/null || true`;
|
|
501
|
+
const result = await runGitStep(cell, ['sh', '-c', checkout, 'shard-seed', SEED_SNAPSHOT, c.worktree_path, c.branch.replace(/^shard\//, ''), HUB_PATH], { timeoutMs: 120_000, maxOutputBytes: 4096, signal: ctx.signal });
|
|
502
|
+
if (result.exitCode !== 0)
|
|
503
|
+
throw new Error(`Conversation branch could not be prepared: ${result.stderr.trim()}`);
|
|
504
|
+
}
|
|
222
505
|
export async function resumeConversation(ctx, c) {
|
|
223
506
|
if (!conversationEnded(c))
|
|
224
507
|
return getConversation(ctx, c.id);
|
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/device-bridge.d.ts
CHANGED
|
@@ -63,7 +63,7 @@ export declare class DeviceGrants {
|
|
|
63
63
|
allows(api: string, project: string, root: string, kind: string, outside: boolean): boolean;
|
|
64
64
|
save(api: string, project: string, root: string, kind: string): Promise<void>;
|
|
65
65
|
}
|
|
66
|
-
export declare function deviceSummary(r: DeviceRequest): string;
|
|
66
|
+
export declare function deviceSummary(r: DeviceRequest, platform?: NodeJS.Platform): string;
|
|
67
67
|
/** Structured local approval details; the summary remains for older menu bar and ACP clients. */
|
|
68
68
|
export declare function deviceApproval(r: DeviceRequest, c: Conversation, target: {
|
|
69
69
|
path: string;
|
package/dist/device-bridge.js
CHANGED
|
@@ -14,6 +14,7 @@ import { UsageError } from "./errors.js";
|
|
|
14
14
|
import { conversationClient } from "./conversation-commands.js";
|
|
15
15
|
import { withTargetLock } from "./target-lock.js";
|
|
16
16
|
import { checkConversationWorktree, rememberConversationWorktree } from "./conversation-worktree.js";
|
|
17
|
+
import { yourComputer } from "./this-computer.js";
|
|
17
18
|
const LIMIT = 256 << 20, OUTPUT = (192 << 10) - 128;
|
|
18
19
|
const inside = (root, target) => target === root || target.startsWith(root + sep);
|
|
19
20
|
const safeId = (value) => { if (!/^[a-zA-Z0-9_-]{1,96}$/.test(value))
|
|
@@ -67,14 +68,15 @@ export class DeviceGrants {
|
|
|
67
68
|
});
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
|
-
export function deviceSummary(r) {
|
|
71
|
+
export function deviceSummary(r, platform = process.platform) {
|
|
72
|
+
const yours = yourComputer(platform);
|
|
71
73
|
if (tryLocallyAction(r))
|
|
72
74
|
return tryLocallyAction(r) === 'stop' ? 'Stop this conversation’s local sync and ports.' : 'Try this conversation in an isolated local worktree with two-way sync and localhost ports.';
|
|
73
75
|
if (r.kind === 'exec')
|
|
74
|
-
return `Run on
|
|
76
|
+
return `Run on ${yours}: ${JSON.stringify(r.exec?.argv)} · directory ${r.exec?.cwd_relative ?? '.'}`;
|
|
75
77
|
if (r.files)
|
|
76
|
-
return `${r.kind === 'files_pull' ? 'Share with conversation' :
|
|
77
|
-
return `${r.kind === 'port_forward' ?
|
|
78
|
+
return `${r.kind === 'files_pull' ? 'Share with conversation' : `Save on ${yours}`}: ${r.files.local_path_relative} ↔ ${r.files.remote_path}`;
|
|
79
|
+
return `${r.kind === 'port_forward' ? `Open conversation preview on ${yours}` : `Connect conversation to ${yours}`}: ${r.port?.remote_port} ↔ localhost:${r.port?.local_port}`;
|
|
78
80
|
}
|
|
79
81
|
/** Structured local approval details; the summary remains for older menu bar and ACP clients. */
|
|
80
82
|
export function deviceApproval(r, c, target, folder) {
|