@shardflux/cli 0.16.0 → 0.18.0-next.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +67 -2
- package/README.md +47 -8
- package/dist/agent-commands.d.ts +9 -1
- package/dist/agent-commands.js +65 -16
- package/dist/agent-models.d.ts +61 -0
- package/dist/agent-models.js +79 -0
- package/dist/agent-state.d.ts +10 -0
- package/dist/agent-state.js +13 -0
- package/dist/agent-target-commands.d.ts +6 -1
- package/dist/agent-target-commands.js +113 -16
- package/dist/agent-targets.d.ts +17 -5
- package/dist/agent-targets.js +27 -11
- package/dist/args.d.ts +5 -1
- package/dist/args.js +49 -9
- package/dist/cli-proxy-forward.d.ts +45 -0
- package/dist/cli-proxy-forward.js +432 -0
- package/dist/cli-proxy-hub.d.ts +273 -0
- package/dist/cli-proxy-hub.js +749 -0
- package/dist/cli-proxy.d.ts +325 -0
- package/dist/cli-proxy.js +1016 -0
- package/dist/codex-cloud.d.ts +2 -1
- package/dist/codex-cloud.js +16 -6
- package/dist/command-center-actions.js +16 -1
- package/dist/conversation-local.js +2 -2
- package/dist/conversation-stash.js +4 -3
- package/dist/conversation-terminal.d.ts +5 -0
- package/dist/conversation-terminal.js +45 -6
- package/dist/conversations.d.ts +18 -2
- package/dist/conversations.js +12 -5
- package/dist/desktop-linux.d.ts +40 -0
- package/dist/desktop-linux.js +186 -0
- package/dist/device-approvals.d.ts +12 -0
- package/dist/device-approvals.js +75 -0
- package/dist/device-helper.d.ts +3 -1
- package/dist/device-helper.js +5 -2
- package/dist/errors.d.ts +2 -0
- package/dist/errors.js +1 -1
- package/dist/git-remote-bin.js +0 -0
- package/dist/handlers.js +3 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/linux-helper.d.ts +12 -0
- package/dist/linux-helper.js +88 -0
- package/dist/linux-terminal.d.ts +11 -0
- package/dist/linux-terminal.js +42 -0
- package/dist/main.js +30 -6
- package/dist/menubar.js +44 -4
- package/dist/onboarding.d.ts +2 -0
- package/dist/onboarding.js +59 -9
- package/dist/t3-asar.d.ts +10 -0
- package/dist/t3-asar.js +91 -0
- package/dist/t3-launch.d.ts +37 -0
- package/dist/t3-launch.js +133 -0
- package/dist/t3-linux.d.ts +68 -0
- package/dist/t3-linux.js +343 -0
- package/dist/t3-mode.d.ts +129 -0
- package/dist/t3-mode.js +676 -0
- package/dist/t3-phone.d.ts +17 -0
- package/dist/t3-phone.js +15 -0
- package/dist/t3-provider.d.ts +5 -3
- package/dist/t3-provider.js +12 -49
- package/dist/t3-thread-script.d.ts +18 -0
- package/dist/t3-thread-script.js +245 -0
- package/dist/terminal-onboarding.d.ts +11 -0
- package/dist/terminal-onboarding.js +75 -0
- package/dist/thread-on-computer.d.ts +2 -2
- package/dist/thread-on-computer.js +13 -7
- package/dist/update.d.ts +1 -1
- package/dist/update.js +28 -1
- package/dist/worktree-twin-engine.d.ts +31 -1
- package/dist/worktree-twin-engine.js +253 -40
- package/dist/worktree-twin.d.ts +17 -0
- package/dist/worktree-twin.js +147 -22
- package/package.json +7 -19
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
if (choice === 'always' && pending.outside)
|
|
38
|
+
throw new UsageError('Access outside the conversation folder must be approved once.');
|
|
39
|
+
writePrivateFile(join(dir, 'device-answers'), paths(dir, id).answer, JSON.stringify({ nonce: pending.nonce, choice }));
|
|
40
|
+
}
|
|
41
|
+
/** Local user-owned files carry approvals; cloud data can never answer its own request. */
|
|
42
|
+
export async function askLocalDeviceApproval(ctx, approval) {
|
|
43
|
+
const file = paths(ctx.store.dir, approval.request_id), nonce = randomUUID();
|
|
44
|
+
rmSync(file.answer, { force: true });
|
|
45
|
+
writePrivateFile(join(ctx.store.dir, 'device-approvals'), file.pending, JSON.stringify({ ...approval, nonce, pid: process.pid }));
|
|
46
|
+
spawnSync('notify-send', ['--', 'Shardflux approval', `${approval.summary}\nReview: shard device requests`], { env: ctx.io.env, stdio: 'ignore', timeout: 2000 });
|
|
47
|
+
try {
|
|
48
|
+
while (!ctx.signal.aborted) {
|
|
49
|
+
try {
|
|
50
|
+
const answer = JSON.parse(readFileSync(file.answer, 'utf8'));
|
|
51
|
+
if (answer.nonce === nonce && ['once', 'always', 'deny'].includes(answer.choice))
|
|
52
|
+
return answer.choice;
|
|
53
|
+
}
|
|
54
|
+
catch { /* wait for the local CLI */ }
|
|
55
|
+
await delay(200, undefined, { signal: ctx.signal }).catch(() => undefined);
|
|
56
|
+
}
|
|
57
|
+
return 'deny';
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
rmSync(file.pending, { force: true });
|
|
61
|
+
rmSync(file.answer, { force: true });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export const DEVICE_HANDLERS = {
|
|
65
|
+
'device requests'(ctx) {
|
|
66
|
+
const requests = pendingDeviceApprovals(ctx.store.dir);
|
|
67
|
+
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');
|
|
68
|
+
return Promise.resolve(0);
|
|
69
|
+
},
|
|
70
|
+
...Object.fromEntries(['approve', 'deny'].map(action => [`device ${action}`, (ctx, values, [id]) => {
|
|
71
|
+
answerDeviceApproval(ctx.store.dir, id, action === 'approve' ? values.always === true ? 'always' : 'once' : 'deny');
|
|
72
|
+
ctx.io.stdout.write(ctx.json ? json({ request_id: id, choice: action === 'approve' && values.always ? 'always' : action }) : `Device request ${id}: ${action === 'approve' ? values.always ? 'allowed for project' : 'approved once' : 'denied'}.\n`);
|
|
73
|
+
return Promise.resolve(0);
|
|
74
|
+
}])),
|
|
75
|
+
};
|
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/git-remote-bin.js
CHANGED
|
File without changes
|
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>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { CLI_VERSION } from "./args.js";
|
|
6
|
+
import { writePrivateFile } from "./credentials.js";
|
|
7
|
+
import { runDeviceHelper } from "./device-helper.js";
|
|
8
|
+
export const HELPER_UNIT = 'shardflux-helper.service';
|
|
9
|
+
export const helperUnitPath = (home) => join(home, '.config/systemd/user', HELPER_UNIT);
|
|
10
|
+
const quote = (s) => '"' + s.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('%', '%%').replaceAll('\n', '\\n').replaceAll('\r', '\\r') + '"';
|
|
11
|
+
/** No credentials in the unit: the helper reads the same private store as shard. */
|
|
12
|
+
export function helperUnit(ctx) {
|
|
13
|
+
if (!ctx.runtime)
|
|
14
|
+
throw new Error('An installed shard is required.');
|
|
15
|
+
let script = ctx.runtime.script;
|
|
16
|
+
try {
|
|
17
|
+
script = realpathSync(script);
|
|
18
|
+
}
|
|
19
|
+
catch { /* preserve the supplied installed path */ }
|
|
20
|
+
const env = { HOME: ctx.io.env.HOME, PATH: ctx.io.env.PATH ?? '/usr/local/bin:/usr/bin:/bin', SHARDFLUX_CONFIG_DIR: ctx.store.dir,
|
|
21
|
+
SHARDFLUX_API_URL: ctx.apiUrl, SHARDFLUX_NO_UPDATE_CHECK: '1',
|
|
22
|
+
...Object.fromEntries(['DISPLAY', 'WAYLAND_DISPLAY', 'DBUS_SESSION_BUS_ADDRESS', 'XDG_RUNTIME_DIR', 'CODEX_HOME', 'CLAUDE_CONFIG_DIR'].flatMap(k => ctx.io.env[k] ? [[k, ctx.io.env[k]]] : [])) };
|
|
23
|
+
return `# Shardflux CLI ${CLI_VERSION}\n[Unit]\nDescription=Shardflux background helper\nAfter=network-online.target\n\n[Service]\nType=simple\nExecStart=${[ctx.runtime.node, script, 'device-helper', '--service'].map(s => quote(s.replaceAll('$', '$$'))).join(' ')}\n${Object.entries(env).map(([k, v]) => `Environment=${quote(`${k}=${v}`)}`).join('\n')}\nRestart=on-failure\nRestartSec=5\nTimeoutStopSec=10\nUMask=0077\n\n[Install]\nWantedBy=default.target\n`;
|
|
24
|
+
}
|
|
25
|
+
export function installLinuxHelper(ctx, deps = {}) {
|
|
26
|
+
if ((deps.platform ?? process.platform) !== 'linux' || !ctx.runtime || !ctx.io.env.HOME)
|
|
27
|
+
return false;
|
|
28
|
+
const run = deps.run ?? (args => spawnSync('systemctl', ['--user', ...args], { env: ctx.io.env, stdio: 'ignore', timeout: 5000 }).status === 0);
|
|
29
|
+
// Probe before writing: containers without a user manager use the command-lifetime helper.
|
|
30
|
+
if (!run(['show-environment']))
|
|
31
|
+
return false;
|
|
32
|
+
const file = helperUnitPath(ctx.io.env.HOME), text = helperUnit(ctx);
|
|
33
|
+
const receipt = join(ctx.store.dir, 'linux-helper-started'), digest = createHash('sha256').update(text).digest('hex');
|
|
34
|
+
let before = '', started = '';
|
|
35
|
+
try {
|
|
36
|
+
before = readFileSync(file, 'utf8');
|
|
37
|
+
}
|
|
38
|
+
catch { /* first sign-in */ }
|
|
39
|
+
try {
|
|
40
|
+
started = readFileSync(receipt, 'utf8');
|
|
41
|
+
}
|
|
42
|
+
catch { /* no successful start yet */ }
|
|
43
|
+
if (before !== text) {
|
|
44
|
+
writePrivateFile(dirname(file), file, text);
|
|
45
|
+
if (!run(['daemon-reload']) || !run(['enable', '--now', HELPER_UNIT]))
|
|
46
|
+
return false;
|
|
47
|
+
if (before && !run(['restart', HELPER_UNIT]))
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
else if (!run(['is-active', '--quiet', HELPER_UNIT])) {
|
|
51
|
+
if (!run(['enable', '--now', HELPER_UNIT]))
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
else if (started !== digest && !run(['restart', HELPER_UNIT]))
|
|
55
|
+
return false;
|
|
56
|
+
// A failed restart must be retried by the next command even though the new unit was already written.
|
|
57
|
+
if (started !== digest)
|
|
58
|
+
writePrivateFile(ctx.store.dir, receipt, digest);
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
/** The caller owns fallback lifetime; never leave an unmanaged background process behind. */
|
|
62
|
+
export function startLinuxHelper(ctx) {
|
|
63
|
+
const noop = () => Promise.resolve();
|
|
64
|
+
if (process.platform !== 'linux' || !ctx.runtime)
|
|
65
|
+
return noop;
|
|
66
|
+
try {
|
|
67
|
+
ctx.apiKey();
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return noop;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
if (installLinuxHelper(ctx))
|
|
74
|
+
return noop;
|
|
75
|
+
}
|
|
76
|
+
catch { /* use the command-lifetime helper */ }
|
|
77
|
+
const notice = join(ctx.store.dir, 'linux-helper-notice');
|
|
78
|
+
try {
|
|
79
|
+
readFileSync(notice);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
ctx.io.stderr.write('Shardflux helper runs while shard commands are open on this computer.\n');
|
|
83
|
+
writePrivateFile(ctx.store.dir, notice, '1\n');
|
|
84
|
+
}
|
|
85
|
+
const abort = new AbortController();
|
|
86
|
+
const work = runDeviceHelper({ ...ctx, signal: AbortSignal.any([ctx.signal, abort.signal]) }, { service: true }).catch(() => undefined);
|
|
87
|
+
return async () => { abort.abort(); await work; };
|
|
88
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Ctx } from './commands.js';
|
|
2
|
+
import type { Values } from './args.js';
|
|
3
|
+
/** Linux terminals use the existing native Codex harness; explicit app routes keep Cloud. */
|
|
4
|
+
export declare function linuxTerminalSession(ctx: Pick<Ctx, 'io' | 'json'>, values: Values, platform?: NodeJS.Platform): boolean;
|
|
5
|
+
export declare const shellWord: (s: string) => string;
|
|
6
|
+
export declare function linuxTerminalCommand(argv: string[], env: Record<string, string | undefined>): {
|
|
7
|
+
command: string;
|
|
8
|
+
args: string[];
|
|
9
|
+
} | null;
|
|
10
|
+
/** Invoked before cloud work: the new terminal owns the attach, credentials and any prompts. */
|
|
11
|
+
export declare function openLinuxTerminal(ctx: Ctx, ref: string, app?: string): Promise<number>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { accessSync, constants } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
/** Linux terminals use the existing native Codex harness; explicit app routes keep Cloud. */
|
|
5
|
+
export function linuxTerminalSession(ctx, values, platform = process.platform) {
|
|
6
|
+
return platform === 'linux' && (values.in === 'terminal' || (values.in === undefined && !!ctx.io.stdin.isTTY && !ctx.json));
|
|
7
|
+
}
|
|
8
|
+
export const shellWord = (s) => `'${s.replaceAll("'", "'\\''")}'`;
|
|
9
|
+
export function linuxTerminalCommand(argv, env) {
|
|
10
|
+
const executable = (name) => name.includes('/') ? [name] : (env.PATH ?? '').split(':').filter(Boolean).map(p => join(p, name));
|
|
11
|
+
const exists = (name) => executable(name).some(p => { try {
|
|
12
|
+
accessSync(p, constants.X_OK);
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
} });
|
|
18
|
+
// The shell keeps the window open on an attach error so the user can read and retry it.
|
|
19
|
+
const attach = ['sh', '-c', `${argv.map(shellWord).join(' ')}; status=$?; if [ "$status" -ne 0 ]; then printf '\nPress Enter to close.'; read answer; fi; exit "$status"`];
|
|
20
|
+
for (const command of [...new Set([env.TERMINAL?.trim(), 'x-terminal-emulator', 'gnome-terminal'].filter((s) => !!s))]) {
|
|
21
|
+
if (exists(command))
|
|
22
|
+
return { command, args: [command === 'gnome-terminal' ? '--' : '-e', ...attach] };
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
/** Invoked before cloud work: the new terminal owns the attach, credentials and any prompts. */
|
|
27
|
+
export async function openLinuxTerminal(ctx, ref, app) {
|
|
28
|
+
const words = ['agents', 'open', ref, '--in', 'terminal', ...(app ? ['--app', app] : [])];
|
|
29
|
+
const argv = ctx.runtime ? [ctx.runtime.node, ctx.runtime.script, '--api-url', ctx.apiUrl, ...words] : ['shard', ...words];
|
|
30
|
+
const choice = linuxTerminalCommand(argv, ctx.io.env);
|
|
31
|
+
if (choice) {
|
|
32
|
+
const child = spawn(choice.command, choice.args, { env: ctx.io.env, cwd: process.cwd(), detached: true, stdio: 'ignore' });
|
|
33
|
+
try {
|
|
34
|
+
await new Promise((resolve, reject) => { child.once('spawn', resolve); child.once('error', reject); });
|
|
35
|
+
child.unref();
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
catch { /* print a runnable command if the terminal could not start */ }
|
|
39
|
+
}
|
|
40
|
+
ctx.io.stdout.write(`${['shard', '--api-url', ctx.apiUrl, ...words].map(shellWord).join(' ')}\n`);
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
package/dist/main.js
CHANGED
|
@@ -13,12 +13,16 @@
|
|
|
13
13
|
import { homedir, hostname } from 'node:os';
|
|
14
14
|
import { statSync } from 'node:fs';
|
|
15
15
|
import { SDK_VERSION, Shardflux, ShardfluxAccount, isSessionToken } from '@shardflux/sdk';
|
|
16
|
-
import { CLI_VERSION, CENTER_COMMAND, PROJECT_COMMAND, commandHelp, parseCommandLine, resolveApiUrl, resolveWake, resolveWakeTimeout } from "./args.js";
|
|
16
|
+
import { CLI_VERSION, CENTER_COMMAND, ONBOARDING_COMMAND, PROJECT_COMMAND, commandHelp, parseCommandLine, resolveApiUrl, resolveWake, resolveWakeTimeout } from "./args.js";
|
|
17
17
|
import { commandCenter } from "./command-center.js";
|
|
18
|
-
import {
|
|
18
|
+
import { launchProjectCommand } from "./conversation-terminal.js";
|
|
19
19
|
import { runAcp } from "./acp.js";
|
|
20
20
|
import { registerInstalledT3 } from "./t3-provider.js";
|
|
21
|
+
import { refreshForwarderAgent } from "./cli-proxy.js";
|
|
21
22
|
import { runAcpLogin } from "./acp-login.js";
|
|
23
|
+
import { terminalOnboarding } from "./terminal-onboarding.js";
|
|
24
|
+
import { tryLinuxDesktop } from "./desktop-linux.js";
|
|
25
|
+
import { startLinuxHelper } from "./linux-helper.js";
|
|
22
26
|
import { runDeviceHelper } from "./device-helper.js";
|
|
23
27
|
import { housekeeping, startupHousekeeping } from "./agent-git-commands.js";
|
|
24
28
|
import { CredentialStore, configDir } from "./credentials.js";
|
|
@@ -660,7 +664,7 @@ export async function run(argv, io, opts = {}) {
|
|
|
660
664
|
refreshMenubarInBackground({ dir: store.dir, home: io.env.HOME || homedir(), runtime: opts.runtime, env: { ...process.env, ...io.env } });
|
|
661
665
|
}
|
|
662
666
|
// The update check runs next to the command (not for `version`, which reports it itself).
|
|
663
|
-
if (!updateCheckDisabled(io.env) && !['version', 'update', 'ssh-proxy', 'acp', 'device-helper'].includes(parsed.command.path[0]) && command !== 'menubar refresh') {
|
|
667
|
+
if (!updateCheckDisabled(io.env) && !['version', 'update', 'ssh-proxy', 'acp', 'device-helper'].includes(parsed.command.path[0]) && command !== 'menubar refresh' && command !== 'cli-proxy forward') {
|
|
664
668
|
try {
|
|
665
669
|
updates = new UpdateCheck({ dir: store.dir, apiUrl, distribution, fetch: base, userAgent, registry: npmRegistry(io.env) });
|
|
666
670
|
updates.start();
|
|
@@ -673,11 +677,28 @@ export async function run(argv, io, opts = {}) {
|
|
|
673
677
|
catch (err) {
|
|
674
678
|
return report(err);
|
|
675
679
|
}
|
|
676
|
-
|
|
680
|
+
let stopHelper;
|
|
681
|
+
const startHelper = () => {
|
|
682
|
+
if (stopHelper)
|
|
683
|
+
return;
|
|
684
|
+
try {
|
|
685
|
+
ctx.apiKey();
|
|
686
|
+
}
|
|
687
|
+
catch {
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
stopHelper = startLinuxHelper(ctx);
|
|
691
|
+
};
|
|
692
|
+
const handler = parsed.command.path[0] === 'acp' ? (ctx) => parsed.values.login === true ? runAcpLogin(ctx) : runAcp(ctx) : parsed.command.path[0] === 'device-helper' ? (ctx) => runDeviceHelper(ctx, { service: parsed.values.service === true }) : command === 'menubar refresh' ? runMenubarRefresh : parsed.command === PROJECT_COMMAND ? launchProjectCommand : parsed.command === ONBOARDING_COMMAND ? async (ctx, values) => await tryLinuxDesktop(ctx) ? 0 : terminalOnboarding(ctx, values, { startHelper }) : parsed.command === CENTER_COMMAND ? commandCenter : HANDLERS[parsed.command.path.join(' ')];
|
|
677
693
|
if (!handler)
|
|
678
694
|
return report(new UsageError(`unknown command "${parsed.command.path.join(' ')}"`));
|
|
679
|
-
|
|
695
|
+
// Long-running helpers other programs start (an editor, launchd, the menu bar) skip the per-command housekeeping.
|
|
696
|
+
if (!['acp', 'device-helper'].includes(parsed.command.path[0]) && command !== 'menubar refresh' && command !== 'cli-proxy forward') {
|
|
697
|
+
// An updated shard restarts this computer's CLIProxyAPI forwarder on itself (a no-op unless the version changed).
|
|
698
|
+
await refreshForwarderAgent(io.env, ctx.store.dir).catch(() => false);
|
|
680
699
|
await housekeeping(ctx);
|
|
700
|
+
startHelper();
|
|
701
|
+
}
|
|
681
702
|
const running = handler(ctx, parsed.values, parsed.positionals).then((code) => ({ code }), (err) => ({ err }));
|
|
682
703
|
// On Ctrl-C give the command a moment to clean up (exec sends a cancel), then stop regardless.
|
|
683
704
|
const interrupted = new Promise((resolve) => {
|
|
@@ -712,6 +733,9 @@ export async function run(argv, io, opts = {}) {
|
|
|
712
733
|
if (notice)
|
|
713
734
|
io.stderr.write(notice);
|
|
714
735
|
}
|
|
736
|
+
if (!['acp', 'device-helper'].includes(parsed.command.path[0]) && command !== 'menubar refresh' && command !== 'cli-proxy forward')
|
|
737
|
+
startHelper();
|
|
738
|
+
await stopHelper?.();
|
|
715
739
|
return code;
|
|
716
740
|
}
|
|
717
741
|
/**
|
|
@@ -738,7 +762,7 @@ export async function main(opts = {}) {
|
|
|
738
762
|
const runtime = { node: process.execPath, script: process.argv[1] ?? '' };
|
|
739
763
|
// The menu bar's update button runs this shard; it finds it here (macOS only, written when it changes).
|
|
740
764
|
if (process.platform === 'darwin' && runtime.script)
|
|
741
|
-
recordInstall(configDir(process.env), runtime, opts.distribution ?? CLI_DISTRIBUTION);
|
|
765
|
+
recordInstall(configDir(process.env), runtime, opts.distribution ?? CLI_DISTRIBUTION, process.env);
|
|
742
766
|
const code = await run(process.argv.slice(2), { stdout: process.stdout, stderr: process.stderr, stdin: process.stdin, env: process.env }, { signal: controller.signal, runtime, ...(opts.distribution ? { distribution: opts.distribution } : {}) });
|
|
743
767
|
await Promise.all([flush(process.stdout), flush(process.stderr)]);
|
|
744
768
|
process.exit(code);
|
package/dist/menubar.js
CHANGED
|
@@ -8,6 +8,7 @@ import { compareVersions } from '@shardflux/sdk';
|
|
|
8
8
|
import { AgentStateStore } from "./agent-state.js";
|
|
9
9
|
import { UsageError } from "./errors.js";
|
|
10
10
|
import { json } from "./format.js";
|
|
11
|
+
import { LinuxDesktopManager, linuxDesktopPackage } from "./desktop-linux.js";
|
|
11
12
|
const execute = promisify(execFile);
|
|
12
13
|
/**
|
|
13
14
|
* Every native tool a default MenubarManager runs (plutil, codesign, xattr, open, the app's --control) goes through
|
|
@@ -336,11 +337,19 @@ export function packagedAppPath() {
|
|
|
336
337
|
*/
|
|
337
338
|
export function refreshMenubarInBackground(o) {
|
|
338
339
|
try {
|
|
339
|
-
|
|
340
|
+
const platform = o.platform ?? process.platform;
|
|
341
|
+
if (!['darwin', 'linux'].includes(platform))
|
|
340
342
|
return false;
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
343
|
+
const manifest = (dir) => { try {
|
|
344
|
+
const m = JSON.parse(readFileSync(join(dir, 'manifest.json'), 'utf8'));
|
|
345
|
+
return m.id === 'dev.shardflux.Desktop' ? m.version : null;
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
return null;
|
|
349
|
+
} };
|
|
350
|
+
const installed = platform === 'linux' ? manifest(join(o.home, '.local/share/shardflux/desktop')) : plistVersion(join(o.home, 'Applications', 'Shardflux.app'));
|
|
351
|
+
const source = (o.packageApp ?? (platform === 'linux' ? linuxDesktopPackage : packagedAppPath))();
|
|
352
|
+
const packaged = source ? platform === 'linux' ? manifest(source) : plistVersion(source) : null;
|
|
344
353
|
if (!installed || !packaged || compareAppVersions(installed, packaged) >= 0)
|
|
345
354
|
return false;
|
|
346
355
|
if (new AgentStateStore(o.dir).read().menubar_stopped === true)
|
|
@@ -404,6 +413,19 @@ function refreshLock(dir) {
|
|
|
404
413
|
}
|
|
405
414
|
/** `shard menubar refresh` (not listed in help, so not in HANDLERS): what refreshMenubarInBackground starts. */
|
|
406
415
|
export const runMenubarRefresh = async (ctx) => {
|
|
416
|
+
if (process.platform === 'linux') {
|
|
417
|
+
const manager = new LinuxDesktopManager({ home: ctx.io.env.HOME, env: ctx.io.env });
|
|
418
|
+
const stopped = new AgentStateStore(ctx.store.dir).read().menubar_stopped === true;
|
|
419
|
+
const unlock = refreshLock(ctx.store.dir);
|
|
420
|
+
try {
|
|
421
|
+
const result = stopped ? { result: 'off' } : !unlock ? { result: 'locked' } : await manager.refresh(ctx.store.dir, ctx.apiUrl);
|
|
422
|
+
ctx.io.stdout.write(ctx.json ? json(result) : `Desktop: ${result.result}\n`);
|
|
423
|
+
}
|
|
424
|
+
finally {
|
|
425
|
+
unlock?.();
|
|
426
|
+
}
|
|
427
|
+
return 0;
|
|
428
|
+
}
|
|
407
429
|
const manager = new MenubarManager({ home: ctx.io.env.HOME || homedir(), signal: ctx.signal });
|
|
408
430
|
let outcome;
|
|
409
431
|
if (new AgentStateStore(ctx.store.dir).read().menubar_stopped === true)
|
|
@@ -425,6 +447,24 @@ export const runMenubarRefresh = async (ctx) => {
|
|
|
425
447
|
};
|
|
426
448
|
export const MENUBAR_HANDLERS = Object.fromEntries(['start', 'stop', 'status'].map((action) => {
|
|
427
449
|
const handler = async (ctx, values) => {
|
|
450
|
+
if (process.platform === 'linux') {
|
|
451
|
+
const { LinuxDesktopManager } = await import("./desktop-linux.js");
|
|
452
|
+
const manager = new LinuxDesktopManager({ home: ctx.io.env.HOME, env: ctx.io.env });
|
|
453
|
+
let status = action === 'start' ? await manager.start(ctx.store.dir, ctx.apiUrl) : action === 'stop' ? await manager.stop(ctx.store.dir) : await manager.status(ctx.store.dir);
|
|
454
|
+
if (values.login !== undefined) {
|
|
455
|
+
if (values.login !== 'on' && values.login !== 'off')
|
|
456
|
+
throw new UsageError('--login must be on or off.');
|
|
457
|
+
await manager.login(values.login === 'on');
|
|
458
|
+
status = await manager.status(ctx.store.dir);
|
|
459
|
+
}
|
|
460
|
+
if (action !== 'status')
|
|
461
|
+
new AgentStateStore(ctx.store.dir).update(doc => { if (action === 'stop')
|
|
462
|
+
doc.menubar_stopped = true;
|
|
463
|
+
else
|
|
464
|
+
delete doc.menubar_stopped; });
|
|
465
|
+
ctx.io.stdout.write(ctx.json ? json({ ...status, packaged_version: manager.packagedVersion() }) : `Shardflux: ${status.running ? 'running' : 'stopped'} · login ${status.login}\n`);
|
|
466
|
+
return 0;
|
|
467
|
+
}
|
|
428
468
|
if (values.login !== undefined && !['on', 'off'].includes(String(values.login)))
|
|
429
469
|
throw new UsageError('--login must be on or off.');
|
|
430
470
|
const manager = new MenubarManager({ home: ctx.io.env.HOME || homedir(), signal: ctx.signal });
|
package/dist/onboarding.d.ts
CHANGED
|
@@ -26,10 +26,12 @@ type CredentialRow = {
|
|
|
26
26
|
checked_at: string;
|
|
27
27
|
};
|
|
28
28
|
};
|
|
29
|
+
/** `url`: a page the person opens (the T3 Connect sign-in), instead of a command. */
|
|
29
30
|
type Action = {
|
|
30
31
|
title: string;
|
|
31
32
|
command?: string[];
|
|
32
33
|
retry?: boolean;
|
|
34
|
+
url?: string;
|
|
33
35
|
};
|
|
34
36
|
export interface Connection {
|
|
35
37
|
id: 'account' | AgentApp | 't3';
|
package/dist/onboarding.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { readFile, stat } from 'node:fs/promises';
|
|
2
2
|
import { delimiter, join, resolve, basename } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
+
import { ShardfluxApiError } from '@shardflux/sdk';
|
|
4
5
|
import { resolveWorkspace } from "./commands.js";
|
|
6
|
+
import { AgentStateStore } from "./agent-state.js";
|
|
7
|
+
import { describeError } from "./errors.js";
|
|
8
|
+
import { t3PhoneLink } from "./t3-phone.js";
|
|
5
9
|
import { readAgentCredentials, syncAgentCredentials } from "./agent-credentials.js";
|
|
6
10
|
import { getConversation } from "./conversations.js";
|
|
7
11
|
import { codexHealth, readCodexConnector, restartCodexConnector } from "./codex-cloud.js";
|
|
8
|
-
import { applicationDirs
|
|
9
|
-
import { configureT3Provider
|
|
12
|
+
import { applicationDirs } from "./agent-targets.js";
|
|
13
|
+
import { configureT3Provider } from "./t3-provider.js";
|
|
14
|
+
import { t3Install } from "./t3-mode.js";
|
|
10
15
|
/**
|
|
11
16
|
* The first-run demo's task: it shows the agent's own computer at work. The user copies it and pastes it into the
|
|
12
17
|
* launcher themselves; Daylight's own CLAUDE.md and AGENTS.md say how to run it and test it in the desktop browser.
|
|
@@ -63,11 +68,12 @@ export function parseApps(value) {
|
|
|
63
68
|
* over SSH (`shard agents new --in t3`), and says which environment to add. Null when T3 Code is not installed.
|
|
64
69
|
*/
|
|
65
70
|
export async function t3Connection(ctx, signedIn, appDirs = applicationDirs(ctx.io.env), platform = process.platform) {
|
|
66
|
-
|
|
71
|
+
// The same install every T3 command uses (macOS and Linux): a build with ACP providers first.
|
|
72
|
+
const app = t3Install(ctx.io.env, platform, appDirs);
|
|
67
73
|
if (!app)
|
|
68
74
|
return null;
|
|
69
75
|
const base = { id: 't3', optional: true, found: true, app_path: app.path };
|
|
70
|
-
if (
|
|
76
|
+
if (app.mode === 'acp') {
|
|
71
77
|
try {
|
|
72
78
|
await configureT3Provider(ctx.io.env, ctx.store.dir, false, ctx.signal);
|
|
73
79
|
// The provider works once there is an API key; without an account its own Sign in (shard acp --login) makes one.
|
|
@@ -79,15 +85,59 @@ export async function t3Connection(ctx, signedIn, appDirs = applicationDirs(ctx.
|
|
|
79
85
|
return { ...base, mode: 'provider', state: 'action', fact: error instanceof Error ? error.message : 'T3 Code’s settings could not be updated.', action: { title: 'Check again' } };
|
|
80
86
|
}
|
|
81
87
|
}
|
|
82
|
-
// Stable T3: an SSH environment per project; the newest one this API set up is the one to add.
|
|
88
|
+
// Stable T3: an SSH environment per project; the newest one this API set up is the one to add. They are recorded in
|
|
89
|
+
// agents.json (targets.json keeps T3 out of its manifest); a manifest entry from an older CLI still counts.
|
|
90
|
+
const recorded = new AgentStateStore(ctx.store.dir).t3Environments(ctx.apiUrl).at(-1);
|
|
83
91
|
const manifest = await jsonFile(join(ctx.store.dir, 'targets.json'));
|
|
84
92
|
const entries = Array.isArray(manifest?.entries) ? manifest.entries : [];
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
93
|
+
const legacy = entries.filter(e => e.api_url === ctx.apiUrl && Array.isArray(e.targets) && e.targets.includes('t3') && typeof e.host === 'string').at(-1);
|
|
94
|
+
const host = recorded?.host ?? legacy?.host;
|
|
95
|
+
const workspaceKey = recorded?.workspace ?? (typeof legacy?.workspace === 'string' ? legacy.workspace : undefined);
|
|
96
|
+
if (host) {
|
|
97
|
+
const desktop = `In T3 Code: Add environment → SSH → ${host}`;
|
|
98
|
+
const phone = await t3Phone(ctx, workspaceKey);
|
|
99
|
+
if (!phone)
|
|
100
|
+
return { ...base, mode: 'ssh', state: 'verified', fact: desktop };
|
|
101
|
+
return { ...base, mode: 'ssh', ...phone, fact: phone.state === 'verified' ? `${desktop}. On your phone: ${phone.fact}` : phone.fact };
|
|
102
|
+
}
|
|
88
103
|
// An SSH environment is per project (shardflux-<project>-main): it is set up once a project is picked.
|
|
89
104
|
return { ...base, mode: 'ssh', state: 'info', fact: 'Available after you pick a project.', next: shard(['agents', 'new', '--in', 't3', '--path', '<project>']) };
|
|
90
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* The T3 app on the phone (T3 Connect, 0.17.0+): the workspace is linked with the person's one T3 Connect sign-in. The
|
|
108
|
+
* sign-in is a page to open once; afterwards every project appears in the T3 app by itself. Undefined when the API has
|
|
109
|
+
* no T3 Connect: the desktop route is the whole card then.
|
|
110
|
+
*/
|
|
111
|
+
async function t3Phone(ctx, key) {
|
|
112
|
+
if (!key)
|
|
113
|
+
return undefined;
|
|
114
|
+
try {
|
|
115
|
+
const ws = await resolveWorkspace(ctx, key);
|
|
116
|
+
const cloud = ctx.cloud();
|
|
117
|
+
const known = (await cloud.t3.status()).links.find(l => l.workspace_id === ws.id);
|
|
118
|
+
// A workspace connected to T3 before T3 Connect, or one whose link failed, is linked now: it is the person's choice.
|
|
119
|
+
const { link, signIn, needsLogin } = !known || known.status === 'error' || known.status === 'needs_sign_in'
|
|
120
|
+
? await t3PhoneLink(ctx, ws.id) : { link: known, signIn: undefined, needsLogin: false };
|
|
121
|
+
if (link.status === 'linked')
|
|
122
|
+
return { state: 'verified', fact: `${link.label} in the T3 app.` };
|
|
123
|
+
if (signIn) {
|
|
124
|
+
return { state: 'action', fact: `To use it from the T3 app on your phone, confirm ${signIn.user_code} once.`,
|
|
125
|
+
action: { title: 'Connect phone', url: signIn.verification_uri } };
|
|
126
|
+
}
|
|
127
|
+
if (needsLogin)
|
|
128
|
+
return { state: 'info', fact: 'Sign in to Shardflux on this computer to use it from the T3 app.' };
|
|
129
|
+
return { state: 'pending', fact: `Adding ${link.label} to the T3 app on your phone…` };
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
// No T3 Connect on this API, or nothing to ask it: the desktop route stands on its own.
|
|
133
|
+
if (!(error instanceof ShardfluxApiError) || describeError(error).code === 'not_found')
|
|
134
|
+
return undefined;
|
|
135
|
+
// Another member's T3 Connect sign-in links this project: it is in their T3 app, not something to fix here.
|
|
136
|
+
if (error.details?.reason === 't3_linked_by_other_member')
|
|
137
|
+
return { state: 'info', fact: 'In the T3 app of the member who linked it.' };
|
|
138
|
+
return { state: 'unknown', fact: 'The T3 app link could not be checked just now.', action: { title: 'Retry', retry: true } };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
91
141
|
const shard = (words) => ['shard', ...words];
|
|
92
142
|
/** Claude: the account login is ready and Claude itself accepts it right now. */
|
|
93
143
|
export function claudeConnection(row) {
|
|
@@ -105,7 +155,7 @@ export function codexConnection(local, synced, connector, health) {
|
|
|
105
155
|
const enable = { title: 'Connect Codex', command: shard(['agents', 'codex', 'enable']) };
|
|
106
156
|
if (!synced) {
|
|
107
157
|
return local ? { state: 'action', fact: 'Available on web and phone once connected.', action: enable }
|
|
108
|
-
: { state: 'action', fact: 'Sign in to Codex on this
|
|
158
|
+
: { state: 'action', fact: 'Sign in to Codex on this computer first.', action: { title: 'Sign in to Codex', command: ['codex', 'login'] } };
|
|
109
159
|
}
|
|
110
160
|
const retry = { title: 'Retry', retry: true, command: shard(['setup', '--inspect', '--apps', 'codex', '--connect', '--retry-codex']) };
|
|
111
161
|
switch (health) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** The server CLI inside the archive: what Electron runs as Node to issue T3 sessions. */
|
|
2
|
+
export declare const T3_SERVER_ENTRY = "apps/server/dist/bin.mjs";
|
|
3
|
+
/** The app's release version (package.json `version`, e.g. `0.0.46-nightly.20261010.2908`), or null. */
|
|
4
|
+
export declare function asarVersion(archive: string): string | null;
|
|
5
|
+
/**
|
|
6
|
+
* Whether the server registers the ACP Registry driver. A version number alone is insufficient (0.0.45 lacks it while
|
|
7
|
+
* upstream main had it). Builds put the driver in `bin.mjs` itself, or (nightlies since October 2026) in a chunk the
|
|
8
|
+
* thin `bin.mjs` entry imports (`binCli-<hash>.mjs`): the entry and the chunks it names are searched.
|
|
9
|
+
*/
|
|
10
|
+
export declare function t3ArchiveSupportsAcp(archive: string): boolean;
|