@shardflux/cli 0.17.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 +9 -4
- package/README.md +4 -3
- package/dist/agent-target-commands.js +3 -7
- package/dist/agent-targets.d.ts +15 -5
- package/dist/agent-targets.js +18 -8
- package/dist/args.js +2 -2
- package/dist/desktop-linux.d.ts +40 -0
- package/dist/desktop-linux.js +186 -0
- package/dist/device-approvals.js +5 -3
- package/dist/git-remote-bin.js +0 -0
- package/dist/main.js +2 -1
- package/dist/menubar.js +44 -4
- package/dist/onboarding.js +7 -5
- package/dist/t3-asar.d.ts +10 -0
- package/dist/t3-asar.js +91 -0
- package/dist/t3-launch.js +1 -1
- package/dist/t3-linux.d.ts +68 -0
- package/dist/t3-linux.js +343 -0
- package/dist/t3-mode.d.ts +16 -8
- package/dist/t3-mode.js +49 -27
- package/dist/t3-provider.d.ts +5 -3
- package/dist/t3-provider.js +12 -49
- package/dist/update.js +16 -0
- package/package.json +7 -19
package/CHANGELOG.md
CHANGED
|
@@ -2,10 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## 0.17.0 (not yet published)
|
|
4
4
|
|
|
5
|
-
- Linux (Ubuntu 24.04, x64 and arm64): bare `shard`
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
- Linux (Ubuntu 24.04, x64 and arm64): bare `shard` opens the Shardflux app in a graphical session. The optional CPU package installs without npm install scripts, adds Shardflux to the app grid, and starts at login. Headless sessions keep terminal onboarding. The systemd user helper keeps the live twin, files and conversation mirrors running; `shard device requests`, `shard device approve <id>` and `shard device deny <id>` handle local approvals.
|
|
6
|
+
- The Ubuntu desktop app includes the custom tray panel, launcher capture and project/model menus, all Settings panes, local device and credential approval panels, notifications with actions, file downloads and packaged-app updates. `shard device approve --always` remembers matching in-project requests; outside-folder requests remain one-time approvals.
|
|
7
|
+
- T3 Code on Linux: `shard` finds T3 Code installed from its .deb or AppImage (and AppImageLauncher, Gear Lever,
|
|
8
|
+
flatpak and snap entries), registers the Shardflux provider in it, shows it in onboarding, and opens it from its
|
|
9
|
+
desktop entry. `shard agents new --in t3`, `shard agents open t3` and `shard agents targets --open-in` work as on
|
|
10
|
+
macOS: a T3 build with ACP providers starts each thread on its Shardflux provider in a cloud VM of its own, and T3
|
|
11
|
+
Code 0.0.45 opens the project VM as an SSH environment. `shard agents targets --json` reports it under `t3`
|
|
12
|
+
(`last_used_at`: when T3 last started). The Shardflux provider is also recognized in the latest T3 nightlies on
|
|
13
|
+
macOS.
|
|
9
14
|
|
|
10
15
|
- A Codex project opens in T3 Code without a Claude sign-in (T3 shows Claude as signed out, and says how to sign in), and `--json` names the T3 Connect sign-in `shard` started for the phone (`t3_link.sign_in`, or `t3_link.needs_login` when this computer has no session).
|
|
11
16
|
- A conversation settles when its turn ends (`done` or `unresolved` right away) and its VM takes the next message.
|
package/README.md
CHANGED
|
@@ -87,9 +87,10 @@ time `shard` runs, with launch at login enabled; `shard menubar stop` turns it o
|
|
|
87
87
|
|
|
88
88
|
T3 Code uses one **Shardflux** provider; choose a Claude Code or Codex model to start that agent in the cloud.
|
|
89
89
|
Model choices come from your cloud agent's session and use the same coding tools and settings as its terminal.
|
|
90
|
-
The provider is registered automatically when T3 Code is installed
|
|
91
|
-
settings: T3 runs `shard acp --login` in its own terminal,
|
|
92
|
-
|
|
90
|
+
The provider is registered automatically when T3 Code is installed, on macOS and on Linux from its .deb or AppImage
|
|
91
|
+
(0.17.0+). Choose **Sign in** on its card in T3's provider settings: T3 runs `shard acp --login` in its own terminal,
|
|
92
|
+
which creates or signs in to your account (with this computer's Codex login, or your email) and connects Claude Code
|
|
93
|
+
and Codex, skipping every step that is already done (0.15.0+).
|
|
93
94
|
With T3 Code 0.0.45, `shard agents new --in t3` adds the project as an SSH environment instead.
|
|
94
95
|
|
|
95
96
|
Start another thread on a computer that is already set up (0.16.0+): it shares that conversation's installed
|
|
@@ -78,12 +78,8 @@ export async function prepareSsh(ws, ctx, targets, host) {
|
|
|
78
78
|
return cell;
|
|
79
79
|
}
|
|
80
80
|
async function openApp(target, env) {
|
|
81
|
-
if (target === 't3')
|
|
82
|
-
|
|
83
|
-
if (!install)
|
|
84
|
-
throw t3NotInstalled();
|
|
85
|
-
return openT3(env, install.path);
|
|
86
|
-
}
|
|
81
|
+
if (target === 't3')
|
|
82
|
+
return openT3(env, t3Install(env));
|
|
87
83
|
const child = spawn('open', ['-b', { 'claude-desktop': 'com.anthropic.claudefordesktop', 'codex-app': 'com.openai.codex', t3: 'com.t3tools.t3code' }[target]], { env, stdio: 'ignore' });
|
|
88
84
|
await new Promise((resolve, reject) => {
|
|
89
85
|
const timer = setTimeout(() => { child.kill('SIGKILL'); reject(new Error('app launcher timed out')); }, 10_000);
|
|
@@ -283,7 +279,7 @@ export async function openDesktopWorkspace(ctx, ws, target, t3Version, prewarmed
|
|
|
283
279
|
if (target === 't3') {
|
|
284
280
|
// The thread exists once startSshThread returns: a failure to show T3 names it rather than inviting a retry.
|
|
285
281
|
try {
|
|
286
|
-
await openT3(ctx.io.env,
|
|
282
|
+
await openT3(ctx.io.env, install ?? t3Install(ctx.io.env));
|
|
287
283
|
}
|
|
288
284
|
catch (error) {
|
|
289
285
|
throw started ? t3OpenFailed(error, { t3_thread_id: started.t3_thread_id, conversation_id: conversation ?? (ws.labels.shard_conversation || null), workspace_key: ws.key }) : error;
|
package/dist/agent-targets.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { T3LinuxApp } from './t3-linux.js';
|
|
1
2
|
export type DesktopTarget = 'claude-desktop' | 'codex-app' | 't3';
|
|
2
3
|
export interface TargetEntry {
|
|
3
4
|
host: string;
|
|
@@ -13,13 +14,22 @@ export declare const targetLabel: (e: Pick<TargetEntry, "agent" | "project">) =>
|
|
|
13
14
|
export declare function targetIdentity(key: string): Pick<TargetEntry, 'host' | 'workspace' | 'project' | 'agent'>;
|
|
14
15
|
/** Replace only a marked block, retaining every byte outside it. Put Includes before user Host blocks. */
|
|
15
16
|
export declare function managedBlock(text: string, body: string): string;
|
|
16
|
-
/**
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* An installed T3 Code: on macOS `path` is the .app bundle; on Linux it is the program its desktop entry runs (the
|
|
19
|
+
* executable, or the AppImage), and `linux` says how it was installed and how it starts (t3-linux.ts).
|
|
20
|
+
*/
|
|
21
|
+
export interface T3App {
|
|
18
22
|
path: string;
|
|
19
23
|
version: string | null;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
linux?: T3LinuxApp;
|
|
25
|
+
}
|
|
26
|
+
/** The installed T3 Code apps (any channel), with their path and version, in the given app folders (applicationDirs). */
|
|
27
|
+
export declare function t3Apps(dirs: string[], platform?: NodeJS.Platform, env?: Record<string, string | undefined>): T3App[];
|
|
28
|
+
/**
|
|
29
|
+
* The folders scanned for desktop apps: SHARDFLUX_APPLICATIONS_DIRS (colon-separated) when set; else /Applications and
|
|
30
|
+
* ~/Applications on macOS, and on Linux the XDG application folders and the usual AppImage folders (linuxApplicationDirs).
|
|
31
|
+
*/
|
|
32
|
+
export declare function applicationDirs(env: Record<string, string | undefined>, platform?: NodeJS.Platform): string[];
|
|
23
33
|
/** The first `name` app bundle with this bundle id in the app folders (macOS only). */
|
|
24
34
|
export declare function desktopAppPath(dirs: string[], name: string, bundle: string, platform?: NodeJS.Platform): string | undefined;
|
|
25
35
|
export declare function installedTargets(env: Record<string, string | undefined>, platform?: NodeJS.Platform, appDirs?: string[]): {
|
package/dist/agent-targets.js
CHANGED
|
@@ -7,6 +7,7 @@ import { writePrivateFile } from "./credentials.js";
|
|
|
7
7
|
import { UsageError } from "./errors.js";
|
|
8
8
|
import { projectWorkspacePath } from "./agent-repository.js";
|
|
9
9
|
import { withTargetLock } from "./target-lock.js";
|
|
10
|
+
import { linuxApplicationDirs, linuxT3Apps } from "./t3-linux.js";
|
|
10
11
|
const PERSISTED_TARGETS = ['claude-desktop', 'codex-app'];
|
|
11
12
|
const BEGIN = '# BEGIN Shardflux managed targets';
|
|
12
13
|
const END = '# END Shardflux managed targets';
|
|
@@ -36,8 +37,10 @@ export function managedBlock(text, body) {
|
|
|
36
37
|
const stop = end + END.length + (text[end + END.length] === '\n' ? 1 : 0);
|
|
37
38
|
return text.slice(0, start) + block + text.slice(stop);
|
|
38
39
|
}
|
|
39
|
-
/** The installed T3 Code apps (any channel), with their path and version, in the given app folders. */
|
|
40
|
-
export function t3Apps(dirs, platform = process.platform) {
|
|
40
|
+
/** The installed T3 Code apps (any channel), with their path and version, in the given app folders (applicationDirs). */
|
|
41
|
+
export function t3Apps(dirs, platform = process.platform, env = process.env) {
|
|
42
|
+
if (platform === 'linux')
|
|
43
|
+
return linuxT3Apps(dirs, env);
|
|
41
44
|
const found = [];
|
|
42
45
|
if (platform !== 'darwin')
|
|
43
46
|
return found;
|
|
@@ -69,10 +72,15 @@ export function t3Apps(dirs, platform = process.platform) {
|
|
|
69
72
|
}
|
|
70
73
|
return found;
|
|
71
74
|
}
|
|
72
|
-
/**
|
|
73
|
-
|
|
75
|
+
/**
|
|
76
|
+
* The folders scanned for desktop apps: SHARDFLUX_APPLICATIONS_DIRS (colon-separated) when set; else /Applications and
|
|
77
|
+
* ~/Applications on macOS, and on Linux the XDG application folders and the usual AppImage folders (linuxApplicationDirs).
|
|
78
|
+
*/
|
|
79
|
+
export function applicationDirs(env, platform = process.platform) {
|
|
74
80
|
const home = env.HOME || homedir();
|
|
75
|
-
|
|
81
|
+
if (env.SHARDFLUX_APPLICATIONS_DIRS !== undefined)
|
|
82
|
+
return env.SHARDFLUX_APPLICATIONS_DIRS.split(':').filter(Boolean);
|
|
83
|
+
return platform === 'linux' ? linuxApplicationDirs(env) : ['/Applications', join(home, 'Applications')];
|
|
76
84
|
}
|
|
77
85
|
/** The first `name` app bundle with this bundle id in the app folders (macOS only). */
|
|
78
86
|
export function desktopAppPath(dirs, name, bundle, platform = process.platform) {
|
|
@@ -88,10 +96,10 @@ export function desktopAppPath(dirs, name, bundle, platform = process.platform)
|
|
|
88
96
|
}
|
|
89
97
|
export function installedTargets(env, platform = process.platform, appDirs) {
|
|
90
98
|
// SHARDFLUX_APPLICATIONS_DIRS (colon-separated) replaces the scanned app folders, like SHARDFLUX_CONFIG_DIR does for config.
|
|
91
|
-
const dirs = appDirs ?? applicationDirs(env);
|
|
99
|
+
const dirs = appDirs ?? applicationDirs(env, platform);
|
|
92
100
|
const hasApp = (name, bundle) => desktopAppPath(dirs, name, bundle, platform) !== undefined;
|
|
93
101
|
const versions = {};
|
|
94
|
-
const t3Bundles = t3Apps(dirs, platform);
|
|
102
|
+
const t3Bundles = t3Apps(dirs, platform, env);
|
|
95
103
|
const t3 = t3Bundles.length > 0;
|
|
96
104
|
for (const app of t3Bundles)
|
|
97
105
|
if (app.version)
|
|
@@ -106,7 +114,9 @@ export function installedTargets(env, platform = process.platform, appDirs) {
|
|
|
106
114
|
const clients = [hasApp('Claude.app', 'com.anthropic.claudefordesktop') && 'claude-desktop', (hasApp('Codex.app', 'com.openai.codex') || hasApp('ChatGPT.app', 'com.openai.codex')) && 'codex-app', executable('claude') && 'claude', executable('codex') && 'codex'].filter((v) => !!v);
|
|
107
115
|
if (t3)
|
|
108
116
|
clients.push('t3');
|
|
109
|
-
|
|
117
|
+
// Linux has T3 Code (0.17.0+); Claude Desktop and the Codex app are macOS apps.
|
|
118
|
+
const desktop = platform === 'darwin' ? clients.filter(c => c.endsWith('-app') || c === 'claude-desktop' || c === 't3') : platform === 'linux' && t3 ? ['t3'] : [];
|
|
119
|
+
return { targets: [...desktop, 'web'], clients, versions };
|
|
110
120
|
}
|
|
111
121
|
/** All paths are supplied by the invocation; tests use a temporary HOME, never the owner's app stores. */
|
|
112
122
|
export class AgentTargets {
|
package/dist/args.js
CHANGED
|
@@ -13,7 +13,7 @@ import { parseArgs } from 'node:util';
|
|
|
13
13
|
import { ACCOUNT_COMMANDS } from "./account-args.js";
|
|
14
14
|
import { UsageError } from "./errors.js";
|
|
15
15
|
import { DEFAULT_CHOICE, MODEL_CHOICES } from "./agent-models.js";
|
|
16
|
-
export const CLI_VERSION = '0.
|
|
16
|
+
export const CLI_VERSION = '0.18.0-next.4';
|
|
17
17
|
export const GLOBAL_OPTIONS = {
|
|
18
18
|
'api-url': { type: 'string', value: '<url>', description: 'API base URL (default $SHARDFLUX_API_URL, else https://api.shardflux.dev).' },
|
|
19
19
|
json: { type: 'boolean', description: 'Print machine-readable JSON on stdout (errors as JSON on stderr).' },
|
|
@@ -1229,7 +1229,7 @@ export const ONBOARDING_COMMAND = { path: ['<onboarding>'], summary: 'Set up Sha
|
|
|
1229
1229
|
export const CENTER_COMMAND = { path: ['<center>'], summary: 'Open the cloud agent command center', positionals: [], options: {} };
|
|
1230
1230
|
const AGENT_COMMANDS = [
|
|
1231
1231
|
{ path: ['device', 'requests'], summary: 'Review local device requests waiting for approval', positionals: [], options: {} },
|
|
1232
|
-
...['approve', 'deny'].map(action => ({ path: ['device', action], summary: `${action} a local device request`, positionals: [{ name: 'id', required: true }], options: {} })),
|
|
1232
|
+
...['approve', 'deny'].map((action) => ({ path: ['device', action], summary: `${action} a local device request`, positionals: [{ name: 'id', required: true }], options: action === 'approve' ? { always: { type: 'boolean', description: 'Allow matching requests for this project; never for outside-folder access.' } } : {} })),
|
|
1233
1233
|
{ path: ['conversations', 'try'], summary: 'Try a conversation in a synced local worktree', positionals: [{ name: 'conversation', required: true }], options: { path: { type: 'string', value: '<path>', description: 'Local project checkout.' }, stop: { type: 'boolean', description: 'Stop the device helper’s local sync and ports.' } } },
|
|
1234
1234
|
{ path: ['agents', 'targets'], summary: 'Detect apps or configure managed desktop connections', positionals: [{ name: 'agent' }], options: {
|
|
1235
1235
|
remove: { type: 'boolean', description: 'Remove this agent’s managed entries, or all entries for this API URL.' },
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { spawn, execFile } from 'node:child_process';
|
|
2
|
+
import type { Ctx } from './commands.js';
|
|
3
|
+
declare const exec: typeof execFile.__promisify__;
|
|
4
|
+
export interface DesktopStatus {
|
|
5
|
+
installed: boolean;
|
|
6
|
+
version: string | null;
|
|
7
|
+
running: boolean;
|
|
8
|
+
pids: number[];
|
|
9
|
+
login: 'on' | 'off';
|
|
10
|
+
}
|
|
11
|
+
export declare function linuxDesktopPackage(arch?: NodeJS.Architecture): string | null;
|
|
12
|
+
export declare function graphicalSession(env: NodeJS.ProcessEnv): boolean;
|
|
13
|
+
export declare class LinuxDesktopManager {
|
|
14
|
+
#private;
|
|
15
|
+
readonly home: string;
|
|
16
|
+
readonly app: string;
|
|
17
|
+
constructor(options?: {
|
|
18
|
+
home?: string;
|
|
19
|
+
source?: () => string | null;
|
|
20
|
+
env?: NodeJS.ProcessEnv;
|
|
21
|
+
exec?: typeof exec;
|
|
22
|
+
spawn?: typeof spawn;
|
|
23
|
+
isProcess?: (pid: number, executable: string) => boolean;
|
|
24
|
+
});
|
|
25
|
+
status(dir: string): Promise<DesktopStatus>;
|
|
26
|
+
packagedVersion(): string | null;
|
|
27
|
+
login(on: boolean): Promise<void>;
|
|
28
|
+
stop(dir: string): Promise<DesktopStatus>;
|
|
29
|
+
refresh(dir: string, api: string): Promise<{
|
|
30
|
+
result: string;
|
|
31
|
+
from?: string | null;
|
|
32
|
+
to?: string | null;
|
|
33
|
+
}>;
|
|
34
|
+
start(dir: string, api: string, view?: string, runtime?: {
|
|
35
|
+
node: string;
|
|
36
|
+
script: string;
|
|
37
|
+
}): Promise<DesktopStatus>;
|
|
38
|
+
}
|
|
39
|
+
export declare function tryLinuxDesktop(ctx: Ctx): Promise<boolean>;
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { spawn, execFile } from 'node:child_process';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, chmodSync, mkdtempSync } from 'node:fs';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
import { UsageError } from "./errors.js";
|
|
8
|
+
import { compareVersions } from '@shardflux/sdk';
|
|
9
|
+
const exec = promisify(execFile);
|
|
10
|
+
export function linuxDesktopPackage(arch = process.arch) {
|
|
11
|
+
if (!['x64', 'arm64'].includes(arch))
|
|
12
|
+
return null;
|
|
13
|
+
try {
|
|
14
|
+
return join(dirname(createRequire(import.meta.url).resolve(`@shardflux/desktop-linux-${arch}/package.json`)), 'app');
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function graphicalSession(env) { return !!(env.DISPLAY || env.WAYLAND_DISPLAY); }
|
|
21
|
+
const desktopString = (text) => `"${text.replace(/(["`$\\])/g, '\\$1')}"`;
|
|
22
|
+
export class LinuxDesktopManager {
|
|
23
|
+
home;
|
|
24
|
+
app;
|
|
25
|
+
#source;
|
|
26
|
+
#env;
|
|
27
|
+
#exec;
|
|
28
|
+
#spawn;
|
|
29
|
+
#isProcess;
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
this.home = options.home ?? homedir();
|
|
32
|
+
this.app = join(this.home, '.local', 'share', 'shardflux', 'desktop');
|
|
33
|
+
this.#source = options.source ?? linuxDesktopPackage;
|
|
34
|
+
this.#env = { ...process.env, ...options.env, HOME: this.home };
|
|
35
|
+
this.#exec = options.exec ?? exec;
|
|
36
|
+
this.#spawn = options.spawn ?? spawn;
|
|
37
|
+
this.#isProcess = options.isProcess ?? ((pid, executable) => { try {
|
|
38
|
+
if (readFileSync(`/proc/${pid}/cmdline`, 'utf8').split('\0')[0] !== executable)
|
|
39
|
+
return false;
|
|
40
|
+
process.kill(pid, 0);
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
} });
|
|
46
|
+
}
|
|
47
|
+
#version(path) {
|
|
48
|
+
try {
|
|
49
|
+
const manifest = JSON.parse(readFileSync(join(path, 'manifest.json'), 'utf8'));
|
|
50
|
+
return manifest.id === 'dev.shardflux.Desktop' && typeof manifest.version === 'string' ? manifest.version : null;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async status(dir) {
|
|
57
|
+
const version = this.#version(this.app), pids = [];
|
|
58
|
+
try {
|
|
59
|
+
const pid = Number(readFileSync(join(dir, 'desktop-linux.pid'), 'utf8'));
|
|
60
|
+
// Recycled PIDs must never make us stop an unrelated process.
|
|
61
|
+
if (Number.isInteger(pid) && pid > 0 && this.#isProcess(pid, join(this.app, 'shardflux-desktop')))
|
|
62
|
+
pids.push(pid);
|
|
63
|
+
}
|
|
64
|
+
catch { /* no running app */ }
|
|
65
|
+
return { installed: version !== null, version, running: pids.length > 0, pids, login: existsSync(join(this.home, '.config/autostart/shardflux.desktop')) ? 'on' : 'off' };
|
|
66
|
+
}
|
|
67
|
+
packagedVersion() { const path = this.#source(); return path ? this.#version(path) : null; }
|
|
68
|
+
async login(on) {
|
|
69
|
+
const file = join(this.home, '.config/autostart/shardflux.desktop');
|
|
70
|
+
if (!on) {
|
|
71
|
+
rmSync(file, { force: true });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
75
|
+
const entry = join(this.home, '.local/share/applications/dev.shardflux.Desktop.desktop');
|
|
76
|
+
if (!existsSync(entry))
|
|
77
|
+
throw new UsageError('Run shard menubar start first.');
|
|
78
|
+
cpSync(entry, file);
|
|
79
|
+
}
|
|
80
|
+
async stop(dir) {
|
|
81
|
+
const status = await this.status(dir);
|
|
82
|
+
if (status.running) {
|
|
83
|
+
await this.#exec(join(this.app, 'shardflux-desktop'), ['--control', 'stop'], { env: { ...this.#env, SHARDFLUX_CONFIG_DIR: dir }, timeout: 5_000 });
|
|
84
|
+
for (let i = 0; i < 30; i++) {
|
|
85
|
+
await new Promise(r => setTimeout(r, 100));
|
|
86
|
+
if (!(await this.status(dir)).running)
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
if ((await this.status(dir)).running)
|
|
90
|
+
throw new UsageError('Shardflux is still stopping. Try shard menubar status.');
|
|
91
|
+
}
|
|
92
|
+
return this.status(dir);
|
|
93
|
+
}
|
|
94
|
+
async refresh(dir, api) {
|
|
95
|
+
const before = await this.status(dir), version = this.packagedVersion();
|
|
96
|
+
if (!before.installed || !version)
|
|
97
|
+
return { result: 'off' };
|
|
98
|
+
if ((compareVersions(before.version, version) ?? 0) >= 0)
|
|
99
|
+
return { result: 'current', from: before.version, to: version };
|
|
100
|
+
if (before.running) {
|
|
101
|
+
try {
|
|
102
|
+
const { stdout } = await this.#exec(join(this.app, 'shardflux-desktop'), ['--control', 'idle'], { env: this.#env, timeout: 5000 });
|
|
103
|
+
if (!JSON.parse(stdout).idle)
|
|
104
|
+
return { result: 'busy', from: before.version, to: version };
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return { result: 'busy', from: before.version, to: version };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const after = await this.start(dir, api);
|
|
111
|
+
return { result: 'updated', from: before.version, to: after.version };
|
|
112
|
+
}
|
|
113
|
+
async start(dir, api, view = 'onboarding', runtime = { node: process.execPath, script: process.argv[1] }) {
|
|
114
|
+
const source = this.#source(), version = source && this.#version(source);
|
|
115
|
+
if (!source || !version)
|
|
116
|
+
throw new UsageError('Install the window with npm i -g shardflux --include=optional, then run shard.');
|
|
117
|
+
if (!graphicalSession(this.#env))
|
|
118
|
+
throw new UsageError('Open shard from your desktop session to show the window.');
|
|
119
|
+
const current = await this.status(dir);
|
|
120
|
+
if (!current.version || (compareVersions(current.version, version) ?? -1) < 0) {
|
|
121
|
+
if (existsSync(this.app) && (lstatSync(this.app).isSymbolicLink() || !current.version))
|
|
122
|
+
throw new UsageError(`Refusing to replace an unrelated directory at ${this.app}.`);
|
|
123
|
+
// The loader probes every shared library before any installed copy is stopped or replaced.
|
|
124
|
+
await this.#exec('/usr/bin/ldd', [join(source, 'shardflux-desktop')], { timeout: 5_000 }).then(({ stdout }) => { if (stdout.includes('not found'))
|
|
125
|
+
throw new UsageError('The Shardflux window needs libwebkit2gtk-4.1-0 and libayatana-appindicator3-1. Install those desktop libraries, then run shard.'); });
|
|
126
|
+
mkdirSync(dirname(this.app), { recursive: true });
|
|
127
|
+
const staging = mkdtempSync(join(dirname(this.app), '.desktop-'));
|
|
128
|
+
const staged = join(staging, 'app'), backup = join(staging, 'previous');
|
|
129
|
+
try {
|
|
130
|
+
cpSync(source, staged, { recursive: true });
|
|
131
|
+
chmodSync(join(staged, 'shardflux-desktop'), 0o755);
|
|
132
|
+
if (current.running)
|
|
133
|
+
await this.stop(dir);
|
|
134
|
+
if (existsSync(this.app))
|
|
135
|
+
renameSync(this.app, backup);
|
|
136
|
+
try {
|
|
137
|
+
renameSync(staged, this.app);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
if (existsSync(backup))
|
|
141
|
+
renameSync(backup, this.app);
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
rmSync(staging, { recursive: true, force: true });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
150
|
+
const env = { ...this.#env, SHARDFLUX_CONFIG_DIR: dir, SHARDFLUX_API_URL: api, SHARDFLUX_NODE: runtime.node, SHARDFLUX_CLI: runtime.script };
|
|
151
|
+
const desktop = join(this.home, '.local/share/applications/dev.shardflux.Desktop.desktop');
|
|
152
|
+
mkdirSync(dirname(desktop), { recursive: true });
|
|
153
|
+
const args = ['/usr/bin/env', `SHARDFLUX_CONFIG_DIR=${dir}`, `SHARDFLUX_API_URL=${api}`, `SHARDFLUX_NODE=${runtime.node}`, `SHARDFLUX_CLI=${runtime.script}`, join(this.app, 'shardflux-desktop')];
|
|
154
|
+
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 });
|
|
155
|
+
if ((await this.status(dir)).running)
|
|
156
|
+
await this.#exec(join(this.app, 'shardflux-desktop'), ['--control', view], { env, timeout: 5_000 });
|
|
157
|
+
else {
|
|
158
|
+
const child = this.#spawn(join(this.app, 'shardflux-desktop'), [view], { env, detached: true, stdio: 'ignore' });
|
|
159
|
+
await new Promise((resolve, reject) => { child.once('error', reject); child.once('spawn', resolve); });
|
|
160
|
+
child.unref();
|
|
161
|
+
for (let i = 0; i < 40; i++) {
|
|
162
|
+
if ((await this.status(dir)).running)
|
|
163
|
+
break;
|
|
164
|
+
await new Promise(r => setTimeout(r, 100));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const result = await this.status(dir);
|
|
168
|
+
if (!result.running)
|
|
169
|
+
throw new UsageError('The Shardflux window did not open. Run shard menubar start from your desktop session.');
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
export async function tryLinuxDesktop(ctx) {
|
|
174
|
+
if (process.platform !== 'linux' || ctx.json || ctx.io.env.SHARDFLUX_DESKTOP_CHILD === '1' || !graphicalSession(ctx.io.env))
|
|
175
|
+
return false;
|
|
176
|
+
try {
|
|
177
|
+
const manager = new LinuxDesktopManager({ home: ctx.io.env.HOME, env: ctx.io.env });
|
|
178
|
+
await manager.start(ctx.store.dir, ctx.apiUrl);
|
|
179
|
+
await manager.login(true);
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
ctx.io.stderr.write(`${err instanceof Error ? err.message : 'The Shardflux window could not open.'}\n`);
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
package/dist/device-approvals.js
CHANGED
|
@@ -34,6 +34,8 @@ export function answerDeviceApproval(dir, id, choice) {
|
|
|
34
34
|
const pending = pendingDeviceApprovals(dir).find(p => p.request_id === id);
|
|
35
35
|
if (!pending)
|
|
36
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.');
|
|
37
39
|
writePrivateFile(join(dir, 'device-answers'), paths(dir, id).answer, JSON.stringify({ nonce: pending.nonce, choice }));
|
|
38
40
|
}
|
|
39
41
|
/** Local user-owned files carry approvals; cloud data can never answer its own request. */
|
|
@@ -65,9 +67,9 @@ export const DEVICE_HANDLERS = {
|
|
|
65
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');
|
|
66
68
|
return Promise.resolve(0);
|
|
67
69
|
},
|
|
68
|
-
...Object.fromEntries(['approve', 'deny'].map(action => [`device ${action}`, (ctx,
|
|
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`);
|
|
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`);
|
|
71
73
|
return Promise.resolve(0);
|
|
72
74
|
}])),
|
|
73
75
|
};
|
package/dist/git-remote-bin.js
CHANGED
|
File without changes
|
package/dist/main.js
CHANGED
|
@@ -21,6 +21,7 @@ import { registerInstalledT3 } from "./t3-provider.js";
|
|
|
21
21
|
import { refreshForwarderAgent } from "./cli-proxy.js";
|
|
22
22
|
import { runAcpLogin } from "./acp-login.js";
|
|
23
23
|
import { terminalOnboarding } from "./terminal-onboarding.js";
|
|
24
|
+
import { tryLinuxDesktop } from "./desktop-linux.js";
|
|
24
25
|
import { startLinuxHelper } from "./linux-helper.js";
|
|
25
26
|
import { runDeviceHelper } from "./device-helper.js";
|
|
26
27
|
import { housekeeping, startupHousekeeping } from "./agent-git-commands.js";
|
|
@@ -688,7 +689,7 @@ export async function run(argv, io, opts = {}) {
|
|
|
688
689
|
}
|
|
689
690
|
stopHelper = startLinuxHelper(ctx);
|
|
690
691
|
};
|
|
691
|
-
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 ? (ctx, values) => terminalOnboarding(ctx, values, { startHelper }) : parsed.command === CENTER_COMMAND ? commandCenter : HANDLERS[parsed.command.path.join(' ')];
|
|
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(' ')];
|
|
692
693
|
if (!handler)
|
|
693
694
|
return report(new UsageError(`unknown command "${parsed.command.path.join(' ')}"`));
|
|
694
695
|
// Long-running helpers other programs start (an editor, launchd, the menu bar) skip the per-command housekeeping.
|
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.js
CHANGED
|
@@ -9,8 +9,9 @@ import { t3PhoneLink } from "./t3-phone.js";
|
|
|
9
9
|
import { readAgentCredentials, syncAgentCredentials } from "./agent-credentials.js";
|
|
10
10
|
import { getConversation } from "./conversations.js";
|
|
11
11
|
import { codexHealth, readCodexConnector, restartCodexConnector } from "./codex-cloud.js";
|
|
12
|
-
import { applicationDirs
|
|
13
|
-
import { configureT3Provider
|
|
12
|
+
import { applicationDirs } from "./agent-targets.js";
|
|
13
|
+
import { configureT3Provider } from "./t3-provider.js";
|
|
14
|
+
import { t3Install } from "./t3-mode.js";
|
|
14
15
|
/**
|
|
15
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
|
|
16
17
|
* launcher themselves; Daylight's own CLAUDE.md and AGENTS.md say how to run it and test it in the desktop browser.
|
|
@@ -67,11 +68,12 @@ export function parseApps(value) {
|
|
|
67
68
|
* over SSH (`shard agents new --in t3`), and says which environment to add. Null when T3 Code is not installed.
|
|
68
69
|
*/
|
|
69
70
|
export async function t3Connection(ctx, signedIn, appDirs = applicationDirs(ctx.io.env), platform = process.platform) {
|
|
70
|
-
|
|
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);
|
|
71
73
|
if (!app)
|
|
72
74
|
return null;
|
|
73
75
|
const base = { id: 't3', optional: true, found: true, app_path: app.path };
|
|
74
|
-
if (
|
|
76
|
+
if (app.mode === 'acp') {
|
|
75
77
|
try {
|
|
76
78
|
await configureT3Provider(ctx.io.env, ctx.store.dir, false, ctx.signal);
|
|
77
79
|
// The provider works once there is an API key; without an account its own Sign in (shard acp --login) makes one.
|
|
@@ -123,7 +125,7 @@ async function t3Phone(ctx, key) {
|
|
|
123
125
|
action: { title: 'Connect phone', url: signIn.verification_uri } };
|
|
124
126
|
}
|
|
125
127
|
if (needsLogin)
|
|
126
|
-
return { state: 'info', fact: 'Sign in to Shardflux on this
|
|
128
|
+
return { state: 'info', fact: 'Sign in to Shardflux on this computer to use it from the T3 app.' };
|
|
127
129
|
return { state: 'pending', fact: `Adding ${link.label} to the T3 app on your phone…` };
|
|
128
130
|
}
|
|
129
131
|
catch (error) {
|
|
@@ -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;
|