@shardflux/cli 0.16.0 → 0.17.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 +62 -2
- package/README.md +43 -5
- 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 +117 -16
- package/dist/agent-targets.d.ts +2 -0
- package/dist/agent-targets.js +10 -4
- 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/device-approvals.d.ts +12 -0
- package/dist/device-approvals.js +73 -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/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 +29 -6
- package/dist/onboarding.d.ts +2 -0
- package/dist/onboarding.js +53 -5
- package/dist/t3-launch.d.ts +37 -0
- package/dist/t3-launch.js +133 -0
- package/dist/t3-mode.d.ts +121 -0
- package/dist/t3-mode.js +654 -0
- package/dist/t3-phone.d.ts +17 -0
- package/dist/t3-phone.js +15 -0
- 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 +12 -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 +3 -3
package/dist/codex-cloud.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import type { CodexCloudTask } from '@shardflux/sdk';
|
|
2
2
|
import type { Ctx, Handler } from './commands.js';
|
|
3
3
|
import type { Values } from './args.js';
|
|
4
|
+
import type { ModelChoice } from './agent-models.js';
|
|
4
5
|
import type { Conversation } from './conversations.js';
|
|
5
6
|
import type { InitialImage } from './initial-prompt.js';
|
|
6
7
|
/** Only provider-returned task ids enter the registered Codex deep-link route. */
|
|
7
8
|
export declare function openCodexThread(task: CodexCloudTask, env: Ctx['io']['env']): Promise<void>;
|
|
8
9
|
export declare function submitCloudInitialPrompt(ctx: Ctx, c: Conversation, prompt: string, workspaceKey: string): Promise<void>;
|
|
9
10
|
/** Prints the Cloud thread (and its link) and opens it in the Codex app; --json prints it instead. */
|
|
10
|
-
export declare function printCloudThread(ctx: Ctx, c: Conversation, task: CodexCloudTask, submitted?: boolean): Promise<void>;
|
|
11
|
+
export declare function printCloudThread(ctx: Ctx, c: Conversation, task: CodexCloudTask, submitted?: boolean, choice?: ModelChoice): Promise<void>;
|
|
11
12
|
/** How long the account's Codex remote device may take to come up before it counts as stuck. */
|
|
12
13
|
export declare const CODEX_STARTING_LIMIT_MS: number;
|
|
13
14
|
export type CodexConnectorView = {
|
package/dist/codex-cloud.js
CHANGED
|
@@ -2,7 +2,8 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { CodedError, UsageError } from "./errors.js";
|
|
3
3
|
import { json } from "./format.js";
|
|
4
4
|
import { syncAgentCredentials } from "./agent-credentials.js";
|
|
5
|
-
import { createConversation, resumeConversation } from "./conversations.js";
|
|
5
|
+
import { createConversation, getConversation, recordedChoice, resumeConversation } from "./conversations.js";
|
|
6
|
+
import { CHOICE_PENDING, choiceReport, hasChoice, modelChoice } from "./agent-models.js";
|
|
6
7
|
import { AgentStateStore } from "./agent-state.js";
|
|
7
8
|
import { ensureProjectGit } from "./project-git.js";
|
|
8
9
|
import { gitRunner } from "./agent-git.js";
|
|
@@ -33,16 +34,24 @@ export async function submitCloudInitialPrompt(ctx, c, prompt, workspaceKey) {
|
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
/** Prints the Cloud thread (and its link) and opens it in the Codex app; --json prints it instead. */
|
|
36
|
-
export async function printCloudThread(ctx, c, task, submitted = false) { return output(ctx, c, task, submitted); }
|
|
37
|
-
async function output(ctx, c, task, submitted = false) {
|
|
37
|
+
export async function printCloudThread(ctx, c, task, submitted = false, choice) { return output(ctx, c, task, submitted, choice); }
|
|
38
|
+
async function output(ctx, c, task, submitted = false, choice) {
|
|
38
39
|
const url = threadLink(task);
|
|
40
|
+
// model_choice (0.17.0+): an admitted thread's record keeps the model and effort only when Cloud started it with them.
|
|
41
|
+
const applied = hasChoice(choice) ? await cloudChoiceApplied(ctx, c, choice) : true;
|
|
39
42
|
if (ctx.json) {
|
|
40
|
-
ctx.io.stdout.write(json({ conversation: c, cloud: task, url, ...(submitted ? { initial_prompt: { submitted: true } } : {}) }));
|
|
43
|
+
ctx.io.stdout.write(json({ conversation: c, cloud: task, url, ...(submitted ? { initial_prompt: { submitted: true } } : {}), ...(hasChoice(choice) ? { model_choice: choiceReport(choice, applied) } : {}) }));
|
|
41
44
|
return;
|
|
42
45
|
}
|
|
46
|
+
if (!applied)
|
|
47
|
+
ctx.io.stderr.write(`${CHOICE_PENDING}\n`);
|
|
43
48
|
ctx.io.stdout.write(`${c.key}\n${task.desktop_url}\n`);
|
|
44
49
|
await openCodexThread(task, ctx.io.env);
|
|
45
50
|
}
|
|
51
|
+
async function cloudChoiceApplied(ctx, c, choice) {
|
|
52
|
+
const kept = recordedChoice(await getConversation(ctx, c.id));
|
|
53
|
+
return kept.model === choice.model && kept.effort === choice.effort;
|
|
54
|
+
}
|
|
46
55
|
/** How long the account's Codex remote device may take to come up before it counts as stuck. */
|
|
47
56
|
export const CODEX_STARTING_LIMIT_MS = 5 * 60_000;
|
|
48
57
|
/** When a connector generation started: its UUIDv7 time. */
|
|
@@ -114,11 +123,12 @@ export async function launchCodexCloud(ctx, values, path, images = []) {
|
|
|
114
123
|
throw new UsageError('New Codex conversations open in Codex Cloud. Omit --in or use --in codex-app.');
|
|
115
124
|
if (values.like !== undefined)
|
|
116
125
|
throw new UsageError('Choose the project directory with --path; new Codex conversations use its latest machine template.');
|
|
126
|
+
const choice = modelChoice(values, 'codex');
|
|
117
127
|
const project = projectIdentity(path);
|
|
118
128
|
await ensureProjectGit(ctx, project.folder);
|
|
119
129
|
await gitRunner(project.folder, { ...process.env, ...ctx.io.env }, ctx.signal)(['rev-parse', 'HEAD']);
|
|
120
130
|
await ensureCodexCloud(ctx);
|
|
121
|
-
const created = await createConversation(ctx, path, 'codex', 'codex-cloud', typeof values.name === 'string' ? values.name : undefined);
|
|
131
|
+
const created = await createConversation(ctx, path, 'codex', 'codex-cloud', typeof values.name === 'string' ? values.name : undefined, choice);
|
|
122
132
|
const c = created.conversation;
|
|
123
133
|
// --computer: the conversation VM's desktop is on before Codex starts, so the thread gets the computer tools.
|
|
124
134
|
if (values.computer === true)
|
|
@@ -132,7 +142,7 @@ export async function launchCodexCloud(ctx, values, path, images = []) {
|
|
|
132
142
|
const submitted = typeof values.initialPrompt === 'string';
|
|
133
143
|
if (submitted)
|
|
134
144
|
await submitCloudInitialPrompt(ctx, c, values.initialPrompt, created.workspace.key);
|
|
135
|
-
await output(ctx, c, task, submitted);
|
|
145
|
+
await output(ctx, c, task, submitted, choice);
|
|
136
146
|
return 0;
|
|
137
147
|
}
|
|
138
148
|
export async function openCodexConversation(ctx, c) {
|
|
@@ -11,8 +11,9 @@ import { agentName as gitAgentName } from "./agent-git.js";
|
|
|
11
11
|
import { randomUUID } from 'node:crypto';
|
|
12
12
|
import { prepareAgentMachine } from "./agent-machine.js";
|
|
13
13
|
import { OperationFailedError } from '@shardflux/sdk';
|
|
14
|
-
import { launchConversationProject } from "./conversation-terminal.js";
|
|
14
|
+
import { launchConversationProject, launchProjectCommand } from "./conversation-terminal.js";
|
|
15
15
|
import { startThreadOn } from "./thread-on-computer.js";
|
|
16
|
+
import { launchT3 } from "./t3-launch.js";
|
|
16
17
|
/** L9 can register target-specific handlers after detecting the installed applications. */
|
|
17
18
|
export async function availableTargets(ctx) {
|
|
18
19
|
const { HANDLERS } = await import("./handlers.js");
|
|
@@ -35,6 +36,20 @@ export async function agentAction(ctx, action, values, args) {
|
|
|
35
36
|
}
|
|
36
37
|
export const CENTER_HANDLERS = {
|
|
37
38
|
async 'agents new'(ctx, values, [ref]) {
|
|
39
|
+
const fresh = !ref && values.name === undefined && values.like === undefined;
|
|
40
|
+
// T3 Code mode (0.17.0+): --in t3 starts the first message as a T3 thread (--on: on that computer's T3 server);
|
|
41
|
+
// --in auto opens where open_in says.
|
|
42
|
+
if (fresh && values.in === 't3')
|
|
43
|
+
return launchT3(ctx, values, str(values.path) ?? './', str(values.app));
|
|
44
|
+
if (fresh && values.in === 'auto') {
|
|
45
|
+
const next = { ...values };
|
|
46
|
+
delete next.in;
|
|
47
|
+
return launchProjectCommand(ctx, next, [str(values.path) ?? './', ...(str(values.app) ? [str(values.app)] : [])]);
|
|
48
|
+
}
|
|
49
|
+
if (values.in === 'auto') {
|
|
50
|
+
values = { ...values };
|
|
51
|
+
delete values.in;
|
|
52
|
+
}
|
|
38
53
|
// A thread on an existing conversation's computer (0.16.0+).
|
|
39
54
|
if (values.on !== undefined) {
|
|
40
55
|
if (ref || values.like !== undefined)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
2
|
-
import { getConversation, resolveConversation, conversationWorkspace, localConversations } from "./conversations.js";
|
|
2
|
+
import { conversationEnded, getConversation, resolveConversation, conversationWorkspace, localConversations } from "./conversations.js";
|
|
3
3
|
import { ensureConversationWorktree, devicePort } from "./device-bridge.js";
|
|
4
4
|
import { startProjectSync } from "./project-sync.js";
|
|
5
5
|
import { projectIdentity } from "./project-identity.js";
|
|
@@ -48,7 +48,7 @@ export const CONVERSATION_HANDLERS = {
|
|
|
48
48
|
try {
|
|
49
49
|
while (!ctx.signal.aborted) {
|
|
50
50
|
const next = await getConversation(ctx, c.id);
|
|
51
|
-
if (next
|
|
51
|
+
if (conversationEnded(next) || next.workspace_id !== c.workspace_id)
|
|
52
52
|
break;
|
|
53
53
|
await delay(1000, undefined, { signal: ctx.signal }).catch(() => undefined);
|
|
54
54
|
}
|
|
@@ -182,7 +182,7 @@ export async function mirrorProjectConversations(ctx, state = new ConversationMi
|
|
|
182
182
|
const c = all.find(c => c.id === record.conversation.id);
|
|
183
183
|
if (!c || record.handed_off || !c.harness_session_id)
|
|
184
184
|
continue;
|
|
185
|
-
const revision = JSON.stringify([c.workspace_id, c.harness_session_id, c.state, c.durable, c.last_activity_at, c.summary]);
|
|
185
|
+
const revision = JSON.stringify([c.workspace_id, c.harness_session_id, c.state, c.durable, c.last_activity_at, c.summary, c.ended_at]);
|
|
186
186
|
if (state.mirrored.get(c.id) === revision)
|
|
187
187
|
continue;
|
|
188
188
|
let retry = state.retries.get(c.id);
|
|
@@ -220,8 +220,9 @@ export async function mirrorProjectConversations(ctx, state = new ConversationMi
|
|
|
220
220
|
await mirrorConversation(ctx, { ...record, conversation: c }, source, skipped);
|
|
221
221
|
retry.failures = 0;
|
|
222
222
|
retry.retryAt = 0;
|
|
223
|
-
// An ended record may precede its VM tombstone; retry until the handoff receipt is written.
|
|
224
|
-
|
|
223
|
+
// An ended record may precede its VM tombstone; retry until the handoff receipt is written. A finished turn is
|
|
224
|
+
// `done` while its VM lives: mirrored once per revision, and again when it ends.
|
|
225
|
+
if (c.state !== 'archived' && !(c.state === 'done' && c.ended_at))
|
|
225
226
|
state.mirrored.set(c.id, revision);
|
|
226
227
|
}
|
|
227
228
|
catch (e) {
|
|
@@ -4,5 +4,10 @@ import type { Values } from './args.js';
|
|
|
4
4
|
import type { Conversation } from './conversations.js';
|
|
5
5
|
/** Every terminal entry point uses the durable conversation's native harness, worktree and session. */
|
|
6
6
|
export declare function conversationTerminal(ctx: Ctx, ws: Workspace, c: Conversation, project: string, values: Values): Promise<number>;
|
|
7
|
+
/**
|
|
8
|
+
* `shard ./` (and `agents new --in auto`): new conversations open where `agents targets` says (open_in, 0.17.0+): T3
|
|
9
|
+
* Code mode, Claude Desktop or claude.ai for Claude (the conversation's Remote Control session), else this terminal.
|
|
10
|
+
*/
|
|
11
|
+
export declare const launchProjectCommand: Handler;
|
|
7
12
|
/** Select a durable conversation, then use the harness's native terminal for all interaction. */
|
|
8
13
|
export declare const launchConversationProject: Handler;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { linuxTerminalSession } from "./linux-terminal.js";
|
|
1
2
|
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { AgentStateStore } from "./agent-state.js";
|
|
3
4
|
import { projectIdentity, projectLabels } from "./project-identity.js";
|
|
4
|
-
import { createConversation, conversationName, conversationWorkspace, conversationsUnavailable, listConversations, localConversations, rememberConversation, resumeConversation, restoreConversationBinding } from "./conversations.js";
|
|
5
|
+
import { createConversation, conversationName, conversationWorkspace, conversationsUnavailable, listConversations, localConversations, recordedChoice, rememberConversation, resumeConversation, restoreConversationBinding } from "./conversations.js";
|
|
6
|
+
import { modelChoice } from "./agent-models.js";
|
|
5
7
|
import { terminal } from "./agent-commands.js";
|
|
6
8
|
import { cloudClaudeLogin, loginClaudeInWorkspace, requireClaudeTerminal } from "./claude-cloud-login.js";
|
|
7
9
|
import { syncAgentCredentials } from "./agent-credentials.js";
|
|
@@ -16,6 +18,11 @@ import { agentReachability } from "./agent-reachability.js";
|
|
|
16
18
|
import { promptWithImages, readInitialImages, readInitialPrompt, uploadInitialImages } from "./initial-prompt.js";
|
|
17
19
|
import { launchCodexCloud, openCodexConversation } from "./codex-cloud.js";
|
|
18
20
|
import { startThreadOn } from "./thread-on-computer.js";
|
|
21
|
+
import { openInChoices } from "./t3-mode.js";
|
|
22
|
+
import { launchT3 } from "./t3-launch.js";
|
|
23
|
+
import { openClaudeDesktopSession, remoteControlUrl } from "./agent-target-commands.js";
|
|
24
|
+
import { openUrl } from "./browser.js";
|
|
25
|
+
import { out } from "./commands.js";
|
|
19
26
|
/** Every terminal entry point uses the durable conversation's native harness, worktree and session. */
|
|
20
27
|
export async function conversationTerminal(ctx, ws, c, project, values) {
|
|
21
28
|
if (c.route === 'codex-cloud')
|
|
@@ -23,10 +30,28 @@ export async function conversationTerminal(ctx, ws, c, project, values) {
|
|
|
23
30
|
await restoreConversationBinding(ctx, c, ws);
|
|
24
31
|
// A thread on another conversation's computer has its own PTY there, never the VM's own (0.16.0+).
|
|
25
32
|
const pty = ws.labels.shard_conversation && ws.labels.shard_conversation !== c.id ? conversationName(c) : undefined;
|
|
26
|
-
return terminal(ctx, ws, c.harness, conversationName(c), project, values, undefined, c.worktree_path, { id: c.id, session: c.harness_session_id, title: c.title, ...(pty ? { pty } : {}) });
|
|
33
|
+
return terminal(ctx, ws, c.harness, conversationName(c), project, values, undefined, c.worktree_path, { id: c.id, session: c.harness_session_id, title: c.title, ...(pty ? { pty } : {}), choice: recordedChoice(c) });
|
|
27
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* `shard ./` (and `agents new --in auto`): new conversations open where `agents targets` says (open_in, 0.17.0+): T3
|
|
37
|
+
* Code mode, Claude Desktop or claude.ai for Claude (the conversation's Remote Control session), else this terminal.
|
|
38
|
+
*/
|
|
39
|
+
export const launchProjectCommand = async (ctx, values, [path = './', explicit]) => {
|
|
40
|
+
const named = explicit ?? (values.claude === true ? 'claude' : values.codex === true ? 'codex' : undefined);
|
|
41
|
+
if (values.resume !== true && values.review !== true) {
|
|
42
|
+
const project = projectIdentity(path);
|
|
43
|
+
const app = named === 'claude' || named === 'codex' ? named : new AgentStateStore(ctx.store.dir).app(project.remote ?? project.folder);
|
|
44
|
+
const where = process.platform === 'linux' && ctx.io.stdin.isTTY ? 'terminal' : openInChoices(ctx, project.slug)[app];
|
|
45
|
+
if (where === 't3')
|
|
46
|
+
return launchT3(ctx, values, path, app);
|
|
47
|
+
if (values.on === undefined && app === 'claude' && (where === 'claude-desktop' || where === 'web'))
|
|
48
|
+
return startConversation(ctx, values, [path, ...(explicit ? [explicit] : [])], where);
|
|
49
|
+
}
|
|
50
|
+
return launchConversationProject(ctx, values, [path, ...(explicit ? [explicit] : [])]);
|
|
51
|
+
};
|
|
28
52
|
/** Select a durable conversation, then use the harness's native terminal for all interaction. */
|
|
29
|
-
export const launchConversationProject =
|
|
53
|
+
export const launchConversationProject = (ctx, values, args) => startConversation(ctx, values, args);
|
|
54
|
+
async function startConversation(ctx, values, [path = './', explicit], openIn) {
|
|
30
55
|
if (values.on !== undefined) {
|
|
31
56
|
const app = explicit ?? (values.claude === true ? 'claude' : values.codex === true ? 'codex' : undefined);
|
|
32
57
|
return startThreadOn(ctx, values, path, app);
|
|
@@ -42,6 +67,8 @@ export const launchConversationProject = async (ctx, values, [path = './', expli
|
|
|
42
67
|
const app = explicit === 'claude' || explicit === 'codex' ? explicit : store.app(project.remote ?? project.folder);
|
|
43
68
|
if (explicit && explicit !== app)
|
|
44
69
|
throw new UsageError('Choose claude or codex.');
|
|
70
|
+
// Checked before a VM is allocated; the new conversation's record carries it to its guest (0.17.0+).
|
|
71
|
+
const choice = modelChoice(values, app);
|
|
45
72
|
if (values.resume === true) {
|
|
46
73
|
// The native pickers are VM-local. C-K6's project picker must select the binding first.
|
|
47
74
|
const records = localConversations(ctx);
|
|
@@ -62,7 +89,7 @@ export const launchConversationProject = async (ctx, values, [path = './', expli
|
|
|
62
89
|
const ws = await conversationWorkspace(ctx, next);
|
|
63
90
|
return conversationTerminal(ctx, ws, next, project.remote ?? project.folder, values);
|
|
64
91
|
}
|
|
65
|
-
if (app === 'codex')
|
|
92
|
+
if (app === 'codex' && !linuxTerminalSession(ctx, values))
|
|
66
93
|
return launchCodexCloud(ctx, values, path, images);
|
|
67
94
|
await syncAgentCredentials(ctx.cloud(), { home: ctx.io.env.HOME, redact: value => ctx.addSecret(value) });
|
|
68
95
|
const signedIn = app === 'claude' ? await cloudClaudeLogin(ctx) : true;
|
|
@@ -70,7 +97,7 @@ export const launchConversationProject = async (ctx, values, [path = './', expli
|
|
|
70
97
|
requireClaudeTerminal(ctx);
|
|
71
98
|
let created;
|
|
72
99
|
try {
|
|
73
|
-
created = await createConversation(ctx, project.folder, app, 'terminal');
|
|
100
|
+
created = await createConversation(ctx, project.folder, app, 'terminal', undefined, choice);
|
|
74
101
|
}
|
|
75
102
|
catch (e) {
|
|
76
103
|
if (!conversationsUnavailable(e))
|
|
@@ -102,5 +129,17 @@ export const launchConversationProject = async (ctx, values, [path = './', expli
|
|
|
102
129
|
if (!(await savedMachine(ctx))?.template)
|
|
103
130
|
await syncAgentConfig(ctx, ws);
|
|
104
131
|
await ensureMenubar(ctx.store.dir, ctx.apiUrl, new MenubarManager({ home: ctx.io.env.HOME, signal: ctx.signal }));
|
|
132
|
+
if (openIn) {
|
|
133
|
+
// The agent starts detached in its VM; the conversation then opens in Claude Desktop or claude.ai (Remote Control).
|
|
134
|
+
const quiet = { ...ctx, io: { ...ctx.io, stdout: { write: () => true } } };
|
|
135
|
+
await conversationTerminal(quiet, ws, c, project.remote ?? project.folder, { ...values, detach: true });
|
|
136
|
+
if (openIn === 'claude-desktop')
|
|
137
|
+
return openClaudeDesktopSession(ctx, ws, c);
|
|
138
|
+
const url = await remoteControlUrl(ctx, ws, c);
|
|
139
|
+
if (!ctx.json && !openUrl(url, ctx.io.env))
|
|
140
|
+
throw new Error(`could not open ${url}`);
|
|
141
|
+
out(ctx, `${url}\n`, { target: 'web', workspace: ws.data, conversation_id: c.id, url });
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
105
144
|
return conversationTerminal(ctx, ws, c, project.remote ?? project.folder, values);
|
|
106
|
-
}
|
|
145
|
+
}
|
package/dist/conversations.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Workspace, WorkspaceView } from '@shardflux/sdk';
|
|
2
2
|
import type { Ctx } from './commands.js';
|
|
3
|
+
import type { ModelChoice } from './agent-models.js';
|
|
3
4
|
export type Harness = 'claude' | 'codex';
|
|
4
5
|
export interface Conversation {
|
|
5
6
|
id: string;
|
|
@@ -17,6 +18,11 @@ export interface Conversation {
|
|
|
17
18
|
state: 'working' | 'needs_you' | 'unresolved' | 'done' | 'archived';
|
|
18
19
|
created_at: string;
|
|
19
20
|
ended_at: string | null;
|
|
21
|
+
landed?: {
|
|
22
|
+
how: string;
|
|
23
|
+
at: string;
|
|
24
|
+
ref: string;
|
|
25
|
+
} | null;
|
|
20
26
|
last_activity_at?: string;
|
|
21
27
|
durable?: {
|
|
22
28
|
branch_sha: string;
|
|
@@ -31,6 +37,9 @@ export interface Conversation {
|
|
|
31
37
|
base_branch?: string | null;
|
|
32
38
|
role?: string;
|
|
33
39
|
join_mode?: string | null;
|
|
40
|
+
/** The model and effort its harness starts with; null is the agent's own default (0.17.0+, APIs that keep them). */
|
|
41
|
+
model?: string | null;
|
|
42
|
+
effort?: string | null;
|
|
34
43
|
}
|
|
35
44
|
export interface LocalConversation {
|
|
36
45
|
api_url: string;
|
|
@@ -41,6 +50,11 @@ export interface LocalConversation {
|
|
|
41
50
|
/** The branch the conversation started from (0.16.0+), for APIs that do not return base_branch yet. */
|
|
42
51
|
base_branch?: string;
|
|
43
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Whether the conversation has ended: its VM was reclaimed, or it was archived or discarded. A finished turn is `done`
|
|
55
|
+
* while its VM still takes the next turn; on Codex Cloud, `done` ends the conversation.
|
|
56
|
+
*/
|
|
57
|
+
export declare const conversationEnded: (c: Pick<Conversation, "state" | "ended_at" | "route" | "landed">) => boolean;
|
|
44
58
|
export declare const conversationName: (c: Conversation) => string;
|
|
45
59
|
export declare function validateConversation(c: Conversation): Conversation;
|
|
46
60
|
export declare function localConversations(ctx: Pick<Ctx, 'store' | 'apiUrl'>): Record<string, LocalConversation>;
|
|
@@ -57,9 +71,11 @@ export declare function conversationWorkspace(ctx: Ctx, c: Conversation): Promis
|
|
|
57
71
|
*/
|
|
58
72
|
export declare function waitThreadReady(ctx: Ctx, c: Conversation, pollMs?: number): Promise<Conversation>;
|
|
59
73
|
export declare function restoreConversationBinding(ctx: Ctx, c: Conversation, ws: Workspace): Promise<void>;
|
|
60
|
-
export declare function createConversationRecord(ctx: Ctx, folder: string, harness: Harness, route: Conversation['route'], name?: string, allocated?: (workspace: WorkspaceView) => void, idempotencyKey?: string): Promise<Conversation>;
|
|
61
|
-
export declare function createConversation(ctx: Ctx, folder: string, harness: Harness, route: Conversation['route'], name?: string): Promise<{
|
|
74
|
+
export declare function createConversationRecord(ctx: Ctx, folder: string, harness: Harness, route: Conversation['route'], name?: string, allocated?: (workspace: WorkspaceView) => void, idempotencyKey?: string, choice?: ModelChoice): Promise<Conversation>;
|
|
75
|
+
export declare function createConversation(ctx: Ctx, folder: string, harness: Harness, route: Conversation['route'], name?: string, choice?: ModelChoice): Promise<{
|
|
62
76
|
conversation: Conversation;
|
|
63
77
|
workspace: Workspace;
|
|
64
78
|
}>;
|
|
65
79
|
export declare function resumeConversation(ctx: Ctx, c: Conversation): Promise<Conversation>;
|
|
80
|
+
/** The model and effort a conversation's harness starts with, as its record keeps them. */
|
|
81
|
+
export declare const recordedChoice: (c: Pick<Conversation, "model" | "effort">) => ModelChoice;
|
package/dist/conversations.js
CHANGED
|
@@ -9,6 +9,11 @@ import { UsageError } from "./errors.js";
|
|
|
9
9
|
import { conversationClient } from "./conversation-commands.js";
|
|
10
10
|
import { recordSeedSnapshot } from "./files-mirror.js";
|
|
11
11
|
import { rememberTwinSeed, sendTwinSeed, twinCheckoutArgv, twinSeed } from "./worktree-twin.js";
|
|
12
|
+
/**
|
|
13
|
+
* Whether the conversation has ended: its VM was reclaimed, or it was archived or discarded. A finished turn is `done`
|
|
14
|
+
* while its VM still takes the next turn; on Codex Cloud, `done` ends the conversation.
|
|
15
|
+
*/
|
|
16
|
+
export const conversationEnded = (c) => !!c.ended_at || c.state === 'archived' || c.landed?.how === 'discarded' || (c.route === 'codex-cloud' && c.state === 'done');
|
|
12
17
|
export const conversationName = (c) => {
|
|
13
18
|
const name = c.key.split('/').at(-1);
|
|
14
19
|
if (!/^[a-z0-9][a-z0-9-]{0,47}$/.test(name) || c.branch !== `shard/${name}`)
|
|
@@ -116,7 +121,7 @@ export async function restoreConversationBinding(ctx, c, ws) {
|
|
|
116
121
|
if (result.exitCode !== 0)
|
|
117
122
|
throw new Error(`Conversation restore failed: ${result.stderr.trim()}`);
|
|
118
123
|
}
|
|
119
|
-
export async function createConversationRecord(ctx, folder, harness, route, name, allocated, idempotencyKey = randomUUID()) {
|
|
124
|
+
export async function createConversationRecord(ctx, folder, harness, route, name, allocated, idempotencyKey = randomUUID(), choice = {}) {
|
|
120
125
|
const project = projectIdentity(folder);
|
|
121
126
|
await ensureProjectGit(ctx, project.folder);
|
|
122
127
|
// Both the conversation API and its legacy workspace fallback need a usable local HEAD.
|
|
@@ -128,16 +133,16 @@ export async function createConversationRecord(ctx, folder, harness, route, name
|
|
|
128
133
|
const seed = await twinSeed(ctx.store.dir, project.folder, head, ctx.store.profile(ctx.apiUrl).project_id).catch(() => ({ commit: head, snapshot: false }));
|
|
129
134
|
const commit = seed.commit;
|
|
130
135
|
const response = await conversationClient(ctx).conversations.create({ repoRemote: project.remote ?? `file://${project.folder}`, harness, route, mode: 'new',
|
|
131
|
-
base: { commit, ...(branch ? { branch } : {}) }, ...(name ? { name } : {}), projectId: ctx.store.profile(ctx.apiUrl).project_id }, { idempotencyKey, signal: ctx.signal });
|
|
136
|
+
base: { commit, ...(branch ? { branch } : {}) }, ...(name ? { name } : {}), ...choice, projectId: ctx.store.profile(ctx.apiUrl).project_id }, { idempotencyKey, signal: ctx.signal });
|
|
132
137
|
const c = validateConversation({ ...response.conversation, restore: response.restore });
|
|
133
138
|
allocated?.(response.workspace);
|
|
134
139
|
rememberTwinSeed(ctx.store.dir, c.id, project.folder, seed, branch);
|
|
135
140
|
rememberConversation(ctx, c, project.folder, false, branch);
|
|
136
141
|
return c;
|
|
137
142
|
}
|
|
138
|
-
export async function createConversation(ctx, folder, harness, route, name) {
|
|
143
|
+
export async function createConversation(ctx, folder, harness, route, name, choice = {}) {
|
|
139
144
|
const project = projectIdentity(folder);
|
|
140
|
-
const c = await createConversationRecord(ctx, project.folder, harness, route, name);
|
|
145
|
+
const c = await createConversationRecord(ctx, project.folder, harness, route, name, undefined, undefined, choice);
|
|
141
146
|
try {
|
|
142
147
|
const ws = await conversationWorkspace(ctx, c);
|
|
143
148
|
// This is a one-time seed. A conversation never watches or writes the owner's checkout.
|
|
@@ -163,8 +168,10 @@ export async function createConversation(ctx, folder, harness, route, name) {
|
|
|
163
168
|
}
|
|
164
169
|
}
|
|
165
170
|
export async function resumeConversation(ctx, c) {
|
|
166
|
-
if (c
|
|
171
|
+
if (!conversationEnded(c))
|
|
167
172
|
return getConversation(ctx, c.id);
|
|
168
173
|
const response = await conversationClient(ctx).conversations.resume(c.id, { idempotencyKey: randomUUID(), signal: ctx.signal });
|
|
169
174
|
return validateConversation({ ...response.conversation, restore: response.restore });
|
|
170
175
|
}
|
|
176
|
+
/** The model and effort a conversation's harness starts with, as its record keeps them. */
|
|
177
|
+
export const recordedChoice = (c) => ({ ...(c.model ? { model: c.model } : {}), ...(c.effort ? { effort: c.effort } : {}) });
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DeviceApproval, DeviceChoice } from './device-bridge.js';
|
|
2
|
+
import type { Ctx, Handler } from './commands.js';
|
|
3
|
+
type Pending = DeviceApproval & {
|
|
4
|
+
nonce: string;
|
|
5
|
+
pid: number;
|
|
6
|
+
};
|
|
7
|
+
export declare function pendingDeviceApprovals(dir: string): Pending[];
|
|
8
|
+
export declare function answerDeviceApproval(dir: string, id: string, choice: DeviceChoice): void;
|
|
9
|
+
/** Local user-owned files carry approvals; cloud data can never answer its own request. */
|
|
10
|
+
export declare function askLocalDeviceApproval(ctx: Ctx, approval: DeviceApproval): Promise<DeviceChoice>;
|
|
11
|
+
export declare const DEVICE_HANDLERS: Record<string, Handler>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { readdirSync, readFileSync, rmSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
6
|
+
import { writePrivateFile } from "./credentials.js";
|
|
7
|
+
import { UsageError } from "./errors.js";
|
|
8
|
+
import { json } from "./format.js";
|
|
9
|
+
const paths = (dir, id) => {
|
|
10
|
+
if (!/^[a-zA-Z0-9_-]{1,96}$/.test(id))
|
|
11
|
+
throw new UsageError('Invalid device request id.');
|
|
12
|
+
return { pending: join(dir, 'device-approvals', `${id}.json`), answer: join(dir, 'device-answers', `${id}.json`) };
|
|
13
|
+
};
|
|
14
|
+
export function pendingDeviceApprovals(dir) {
|
|
15
|
+
let names;
|
|
16
|
+
try {
|
|
17
|
+
names = readdirSync(join(dir, 'device-approvals'));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
return names.filter(n => n.endsWith('.json')).flatMap(n => {
|
|
23
|
+
try {
|
|
24
|
+
const p = JSON.parse(readFileSync(paths(dir, n.slice(0, -5)).pending, 'utf8'));
|
|
25
|
+
process.kill(p.pid, 0);
|
|
26
|
+
return [p];
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export function answerDeviceApproval(dir, id, choice) {
|
|
34
|
+
const pending = pendingDeviceApprovals(dir).find(p => p.request_id === id);
|
|
35
|
+
if (!pending)
|
|
36
|
+
throw new UsageError('That request is no longer waiting for approval. Run shard device requests.');
|
|
37
|
+
writePrivateFile(join(dir, 'device-answers'), paths(dir, id).answer, JSON.stringify({ nonce: pending.nonce, choice }));
|
|
38
|
+
}
|
|
39
|
+
/** Local user-owned files carry approvals; cloud data can never answer its own request. */
|
|
40
|
+
export async function askLocalDeviceApproval(ctx, approval) {
|
|
41
|
+
const file = paths(ctx.store.dir, approval.request_id), nonce = randomUUID();
|
|
42
|
+
rmSync(file.answer, { force: true });
|
|
43
|
+
writePrivateFile(join(ctx.store.dir, 'device-approvals'), file.pending, JSON.stringify({ ...approval, nonce, pid: process.pid }));
|
|
44
|
+
spawnSync('notify-send', ['--', 'Shardflux approval', `${approval.summary}\nReview: shard device requests`], { env: ctx.io.env, stdio: 'ignore', timeout: 2000 });
|
|
45
|
+
try {
|
|
46
|
+
while (!ctx.signal.aborted) {
|
|
47
|
+
try {
|
|
48
|
+
const answer = JSON.parse(readFileSync(file.answer, 'utf8'));
|
|
49
|
+
if (answer.nonce === nonce && ['once', 'always', 'deny'].includes(answer.choice))
|
|
50
|
+
return answer.choice;
|
|
51
|
+
}
|
|
52
|
+
catch { /* wait for the local CLI */ }
|
|
53
|
+
await delay(200, undefined, { signal: ctx.signal }).catch(() => undefined);
|
|
54
|
+
}
|
|
55
|
+
return 'deny';
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
rmSync(file.pending, { force: true });
|
|
59
|
+
rmSync(file.answer, { force: true });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export const DEVICE_HANDLERS = {
|
|
63
|
+
'device requests'(ctx) {
|
|
64
|
+
const requests = pendingDeviceApprovals(ctx.store.dir);
|
|
65
|
+
ctx.io.stdout.write(ctx.json ? json({ requests }) : requests.length ? requests.map(r => `${r.request_id}: ${r.summary}\n Project: ${r.project_name ?? r.project_id}; conversation: ${r.conversation_title ?? r.conversation_id}\n Local: ${r.local_path ?? '(none)'}; remote: ${r.remote_path ?? '(none)'}${r.argv ? `\n Command: ${JSON.stringify(r.argv)}` : ''}${r.local_port ? `\n Ports: ${r.local_port} → ${r.remote_port}` : ''}\n shard device approve ${r.request_id} | shard device deny ${r.request_id}\n`).join('') : 'No device requests waiting for approval.\n');
|
|
66
|
+
return Promise.resolve(0);
|
|
67
|
+
},
|
|
68
|
+
...Object.fromEntries(['approve', 'deny'].map(action => [`device ${action}`, (ctx, _values, [id]) => {
|
|
69
|
+
answerDeviceApproval(ctx.store.dir, id, action === 'approve' ? 'once' : 'deny');
|
|
70
|
+
ctx.io.stdout.write(ctx.json ? json({ request_id: id, choice: action }) : `Device request ${id}: ${action === 'approve' ? 'approved once' : 'denied'}.\n`);
|
|
71
|
+
return Promise.resolve(0);
|
|
72
|
+
}])),
|
|
73
|
+
};
|
package/dist/device-helper.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import type { Ctx } from './commands.js';
|
|
2
2
|
/** The menu bar owns this helper and its stdin; VM data can request actions but cannot answer approvals. */
|
|
3
|
-
export declare function runDeviceHelper(ctx: Ctx
|
|
3
|
+
export declare function runDeviceHelper(ctx: Ctx, options?: {
|
|
4
|
+
service?: boolean;
|
|
5
|
+
}): Promise<number>;
|
package/dist/device-helper.js
CHANGED
|
@@ -12,13 +12,14 @@ import { writePrivateFile } from "./credentials.js";
|
|
|
12
12
|
import { conversationClient } from "./conversation-commands.js";
|
|
13
13
|
import { Readable } from 'node:stream';
|
|
14
14
|
import { runWorktreeTwin } from "./worktree-twin-engine.js";
|
|
15
|
+
import { askLocalDeviceApproval } from "./device-approvals.js";
|
|
15
16
|
/** The menu bar owns this helper and its stdin; VM data can request actions but cannot answer approvals. */
|
|
16
|
-
export async function runDeviceHelper(ctx) {
|
|
17
|
+
export async function runDeviceHelper(ctx, options = {}) {
|
|
17
18
|
const abort = new AbortController(), signal = AbortSignal.any([ctx.signal, abort.signal]);
|
|
18
19
|
ctx = { ...ctx, signal };
|
|
19
20
|
const pending = new Map();
|
|
20
21
|
const inputSource = Readable.from(ctx.io.stdin, { objectMode: false, signal });
|
|
21
|
-
const input = (async () => {
|
|
22
|
+
const input = options.service ? Promise.resolve() : (async () => {
|
|
22
23
|
let tail = '';
|
|
23
24
|
for await (const bytes of inputSource) {
|
|
24
25
|
tail += Buffer.from(bytes).toString();
|
|
@@ -41,6 +42,8 @@ export async function runDeviceHelper(ctx) {
|
|
|
41
42
|
const acp = await askConnectedAcp(ctx.store.dir, approval, signal);
|
|
42
43
|
if (acp)
|
|
43
44
|
return acp;
|
|
45
|
+
if (options.service)
|
|
46
|
+
return askLocalDeviceApproval(ctx, approval);
|
|
44
47
|
return new Promise(resolve => {
|
|
45
48
|
const denied = () => { pending.delete(approval.request_id); resolve('deny'); };
|
|
46
49
|
signal.addEventListener('abort', denied, { once: true });
|
package/dist/errors.d.ts
CHANGED
|
@@ -114,6 +114,8 @@ export declare class ExecSessionRunningError extends HintedError {
|
|
|
114
114
|
export interface ErrorInfo {
|
|
115
115
|
code: string;
|
|
116
116
|
message: string;
|
|
117
|
+
/** A refusal `shard` detects itself (CodedError, 0.17.0+): the next step, as plain text (the `hint:` line). */
|
|
118
|
+
fix?: string;
|
|
117
119
|
status?: number;
|
|
118
120
|
request_id?: string;
|
|
119
121
|
retryable?: boolean;
|
package/dist/errors.js
CHANGED
|
@@ -241,7 +241,7 @@ export function describeError(err) {
|
|
|
241
241
|
if (err instanceof GitError)
|
|
242
242
|
return { code: 'git_error', message: err.message, retryable: false };
|
|
243
243
|
if (err instanceof CodedError)
|
|
244
|
-
return { code: err.code, message: err.message, retryable: false, ...(err.details ? { details: err.details } : {}) };
|
|
244
|
+
return { code: err.code, message: err.message, retryable: false, ...(err.hint ? { fix: err.hint } : {}), ...(err.details ? { details: err.details } : {}) };
|
|
245
245
|
if (err instanceof CheckoutTimeoutError) {
|
|
246
246
|
return { code: 'timeout', message: `${err.message}; the checkout stays open (pay at its URL, then check with: shard billing status)`, last_state: err.checkout.status, details: { checkout_id: err.checkout.id, status: err.checkout.status, url: err.checkout.url } };
|
|
247
247
|
}
|
package/dist/handlers.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DEVICE_HANDLERS } from "./device-approvals.js";
|
|
1
2
|
import { CREDENTIAL_HANDLERS } from "./credential-prompts.js";
|
|
2
3
|
import { CONVERSATION_HANDLERS } from "./conversation-commands.js";
|
|
3
4
|
/**
|
|
@@ -18,4 +19,5 @@ import { CONVERSATION_HANDLERS as LOCAL_CONVERSATION_HANDLERS } from "./conversa
|
|
|
18
19
|
import { TWIN_HANDLERS } from "./worktree-twin-engine.js";
|
|
19
20
|
import { CODEX_CLOUD_HANDLERS } from "./codex-cloud.js";
|
|
20
21
|
import { THREAD_HANDLERS } from "./thread-on-computer.js";
|
|
21
|
-
|
|
22
|
+
import { CLI_PROXY_HANDLERS } from "./cli-proxy.js";
|
|
23
|
+
export const HANDLERS = { ...DEVICE_HANDLERS, ...CODEX_CLOUD_HANDLERS, ...CLI_PROXY_HANDLERS, ...THREAD_HANDLERS, ...CONVERSATION_HANDLERS, ...LOCAL_CONVERSATION_HANDLERS, ...TWIN_HANDLERS, ...CREDENTIAL_HANDLERS, ...AGENT_CREDENTIAL_HANDLERS, ...KEY_HANDLERS, ...ACCOUNT_HANDLERS, ...AGENT_HANDLERS, ...CENTER_HANDLERS, ...TARGET_HANDLERS, ...AGENT_GIT_HANDLERS, ...MENUBAR_HANDLERS, ...FILESYSTEM_HANDLERS, ...UPDATE_HANDLERS };
|
package/dist/index.d.ts
CHANGED
|
@@ -24,4 +24,8 @@ export type { ErrorInfo } from './errors.js';
|
|
|
24
24
|
export { changedText, errorText, executionJson, executionSummary, feedbackCommand, json, suspendRequestText, suspendWhenIdleText, table, time, usageText, workspaceDetail, workspaceTable } from './format.js';
|
|
25
25
|
export { TimingRecorder, coldBootNotice, hostLostNotice, lostSuspendNotice, notices, timingJson, timingText } from './timing.js';
|
|
26
26
|
export { readAgentCredentials, syncAgentCredentials } from './agent-credentials.js';
|
|
27
|
+
export { detectLocalCliProxy, importCliProxy } from './cli-proxy.js';
|
|
28
|
+
export type { ImportOptions, ImportResult, LocalCliProxy, LocalLogin } from './cli-proxy.js';
|
|
29
|
+
export { ensureCliProxyHub, readHubInfo } from './cli-proxy-hub.js';
|
|
30
|
+
export type { EnsureOptions, EnsureResult, HubInfo } from './cli-proxy-hub.js';
|
|
27
31
|
export type { CredentialSources } from './agent-credentials.js';
|
package/dist/index.js
CHANGED
|
@@ -16,3 +16,6 @@ export { EXIT, AuthConfigError, CheckoutEndedError, ExecutionFailedError, Execut
|
|
|
16
16
|
export { changedText, errorText, executionJson, executionSummary, feedbackCommand, json, suspendRequestText, suspendWhenIdleText, table, time, usageText, workspaceDetail, workspaceTable } from "./format.js";
|
|
17
17
|
export { TimingRecorder, coldBootNotice, hostLostNotice, lostSuspendNotice, notices, timingJson, timingText } from "./timing.js";
|
|
18
18
|
export { readAgentCredentials, syncAgentCredentials } from "./agent-credentials.js";
|
|
19
|
+
// Hosted CLIProxyAPI (onboarding offers the import when detectLocalCliProxy() finds logins).
|
|
20
|
+
export { detectLocalCliProxy, importCliProxy } from "./cli-proxy.js";
|
|
21
|
+
export { ensureCliProxyHub, readHubInfo } from "./cli-proxy-hub.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Ctx } from './commands.js';
|
|
2
|
+
export declare const HELPER_UNIT = "shardflux-helper.service";
|
|
3
|
+
export declare const helperUnitPath: (home: string) => string;
|
|
4
|
+
/** No credentials in the unit: the helper reads the same private store as shard. */
|
|
5
|
+
export declare function helperUnit(ctx: Pick<Ctx, 'runtime' | 'store' | 'apiUrl' | 'io'>): string;
|
|
6
|
+
export type Systemctl = (args: string[]) => boolean;
|
|
7
|
+
export declare function installLinuxHelper(ctx: Ctx, deps?: {
|
|
8
|
+
platform?: NodeJS.Platform;
|
|
9
|
+
run?: Systemctl;
|
|
10
|
+
}): boolean;
|
|
11
|
+
/** The caller owns fallback lifetime; never leave an unmanaged background process behind. */
|
|
12
|
+
export declare function startLinuxHelper(ctx: Ctx): () => Promise<void>;
|