@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/dist/t3-asar.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T3 Code's app.asar, read in place (nothing is extracted or run): the app's release version (its package.json) and
|
|
3
|
+
* whether its server has the ACP Registry provider driver. The same archive ships in the macOS bundle
|
|
4
|
+
* (Contents/Resources/app.asar) and in the Linux packages (`/opt/T3 Code (Alpha)/resources/app.asar` from the .deb).
|
|
5
|
+
* Answers are kept for the process per archive, keyed by its size and modification time.
|
|
6
|
+
*/
|
|
7
|
+
import { closeSync, openSync, readFileSync, readSync, statSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
/** The server CLI inside the archive: what Electron runs as Node to issue T3 sessions. */
|
|
10
|
+
export const T3_SERVER_ENTRY = 'apps/server/dist/bin.mjs';
|
|
11
|
+
const RELEASE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
12
|
+
const DRIVER = ['ProviderDriverKind.make("acpRegistry")', "ProviderDriverKind.make('acpRegistry')"].map(s => Buffer.from(s));
|
|
13
|
+
const MAX_FILE = 64 << 20;
|
|
14
|
+
const known = new Map();
|
|
15
|
+
/** One fact about an archive, computed from its header once per size and modification time; `fallback` when unreadable. */
|
|
16
|
+
function fact(archive, key, compute, fallback) {
|
|
17
|
+
let fd;
|
|
18
|
+
try {
|
|
19
|
+
const stat = statSync(archive), stamp = `${stat.size}:${stat.mtimeMs}`;
|
|
20
|
+
const seen = known.get(archive)?.stamp === stamp ? known.get(archive) : { stamp };
|
|
21
|
+
if (seen[key] !== undefined)
|
|
22
|
+
return seen[key];
|
|
23
|
+
fd = openSync(archive, 'r');
|
|
24
|
+
const sizes = Buffer.alloc(16);
|
|
25
|
+
if (readSync(fd, sizes, 0, 16, 0) !== 16)
|
|
26
|
+
return fallback;
|
|
27
|
+
const headerSize = sizes.readUInt32LE(12);
|
|
28
|
+
if (headerSize < 1 || headerSize > 16 << 20)
|
|
29
|
+
return fallback;
|
|
30
|
+
const header = Buffer.alloc(headerSize);
|
|
31
|
+
if (readSync(fd, header, 0, headerSize, 16) !== headerSize)
|
|
32
|
+
return fallback;
|
|
33
|
+
const value = compute({ path: archive, fd, size: stat.size, base: 8 + sizes.readUInt32LE(4), header: JSON.parse(header.toString()) });
|
|
34
|
+
known.set(archive, { ...seen, [key]: value });
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return fallback;
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
if (fd !== undefined)
|
|
42
|
+
closeSync(fd);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function entry(a, path) {
|
|
46
|
+
let e = a.header;
|
|
47
|
+
for (const part of path.split('/'))
|
|
48
|
+
e = e?.files?.[part];
|
|
49
|
+
return e && !e.files ? e : undefined;
|
|
50
|
+
}
|
|
51
|
+
/** One file's bytes, from the archive or its `.unpacked` folder; undefined when it is missing or out of bounds. */
|
|
52
|
+
function read(a, path) {
|
|
53
|
+
const e = entry(a, path), size = e?.size;
|
|
54
|
+
if (!e || !Number.isSafeInteger(size) || !size || size < 1 || size > MAX_FILE)
|
|
55
|
+
return undefined;
|
|
56
|
+
if (e.unpacked)
|
|
57
|
+
return readFileSync(join(`${a.path}.unpacked`, path));
|
|
58
|
+
const offset = Number(e.offset) + a.base;
|
|
59
|
+
if (!Number.isSafeInteger(offset) || offset < 16 || offset + size > a.size)
|
|
60
|
+
return undefined;
|
|
61
|
+
const bytes = Buffer.alloc(size);
|
|
62
|
+
return readSync(a.fd, bytes, 0, size, offset) === size ? bytes : undefined;
|
|
63
|
+
}
|
|
64
|
+
/** The app's release version (package.json `version`, e.g. `0.0.46-nightly.20261010.2908`), or null. */
|
|
65
|
+
export function asarVersion(archive) {
|
|
66
|
+
return fact(archive, 'version', a => {
|
|
67
|
+
const version = JSON.parse(read(a, 'package.json')?.toString() ?? '{}').version;
|
|
68
|
+
return typeof version === 'string' && RELEASE.test(version) ? version : null;
|
|
69
|
+
}, null) ?? null;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Whether the server registers the ACP Registry driver. A version number alone is insufficient (0.0.45 lacks it while
|
|
73
|
+
* upstream main had it). Builds put the driver in `bin.mjs` itself, or (nightlies since October 2026) in a chunk the
|
|
74
|
+
* thin `bin.mjs` entry imports (`binCli-<hash>.mjs`): the entry and the chunks it names are searched.
|
|
75
|
+
*/
|
|
76
|
+
export function t3ArchiveSupportsAcp(archive) {
|
|
77
|
+
return fact(archive, 'acp', a => {
|
|
78
|
+
const has = (bytes) => !!bytes && DRIVER.some(marker => bytes.includes(marker));
|
|
79
|
+
const bin = read(a, T3_SERVER_ENTRY);
|
|
80
|
+
if (!bin)
|
|
81
|
+
return false;
|
|
82
|
+
if (has(bin))
|
|
83
|
+
return true;
|
|
84
|
+
// A whole server in bin.mjs (0.0.45: 9 MB) without the driver has none; only a thin entry defers to chunks.
|
|
85
|
+
if (bin.length > 256 << 10)
|
|
86
|
+
return false;
|
|
87
|
+
const dir = T3_SERVER_ENTRY.slice(0, T3_SERVER_ENTRY.lastIndexOf('/') + 1);
|
|
88
|
+
const chunks = [...bin.toString().matchAll(/(?:\bimport\s*\(\s*|\bfrom\s*)["']\.\/([\w.-]+\.m?js)["']/g)].map(m => m[1]);
|
|
89
|
+
return [...new Set(chunks)].slice(0, 16).some(chunk => has(read(a, dir + chunk)));
|
|
90
|
+
}, false) === true;
|
|
91
|
+
}
|
package/dist/t3-launch.js
CHANGED
|
@@ -115,7 +115,7 @@ export async function openT3Conversation(ctx, conversationId, values = {}) {
|
|
|
115
115
|
throw withT3Context(error, { conversation_id: conversationId });
|
|
116
116
|
}
|
|
117
117
|
try {
|
|
118
|
-
await openT3(ctx.io.env, install
|
|
118
|
+
await openT3(ctx.io.env, install);
|
|
119
119
|
}
|
|
120
120
|
catch (error) {
|
|
121
121
|
throw threadId ? t3OpenFailed(error, { t3_thread_id: threadId, conversation_id: conversationId }) : error;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
type Env = Record<string, string | undefined>;
|
|
2
|
+
export type T3LinuxFormat = 'deb' | 'directory' | 'appimage' | 'flatpak' | 'snap';
|
|
3
|
+
export interface T3LinuxApp {
|
|
4
|
+
format: T3LinuxFormat;
|
|
5
|
+
/** The desktop entry's file name (the id `gtk-launch` takes); null for an AppImage found by its file name. */
|
|
6
|
+
desktop_id: string | null;
|
|
7
|
+
/** What starts it: the entry's Exec without field codes, or the AppImage. */
|
|
8
|
+
exec: string[];
|
|
9
|
+
/** The app's resources/app.asar on this computer (deb, other unpacked installs, flatpak); null for an AppImage or snap. */
|
|
10
|
+
asar: string | null;
|
|
11
|
+
/** Flatpak: the app id and the app's files folder on this computer (mounted at /app in its sandbox). */
|
|
12
|
+
flatpak?: {
|
|
13
|
+
id: string;
|
|
14
|
+
files: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** A Linux T3 Code: `path` is the program it runs (the executable, or the AppImage). */
|
|
18
|
+
export interface T3LinuxInstall {
|
|
19
|
+
path: string;
|
|
20
|
+
version: string | null;
|
|
21
|
+
linux: T3LinuxApp;
|
|
22
|
+
}
|
|
23
|
+
/** The launcher T3 (nightlies since October 2026) writes to `<T3 home>/bin/t3` for its bundled CLI; for an AppImage it mounts the image itself. */
|
|
24
|
+
export declare const T3_LAUNCHER_MARKER = "Written by T3 Code: runs the desktop app's bundled t3 CLI.";
|
|
25
|
+
/**
|
|
26
|
+
* The folders scanned on Linux, in XDG precedence: the data home's and each data dir's `applications`, snapd's and
|
|
27
|
+
* flatpak's exported entries, then the usual AppImage folders. SHARDFLUX_APPLICATIONS_DIRS replaces all of them.
|
|
28
|
+
*/
|
|
29
|
+
export declare function linuxApplicationDirs(env: Env): string[];
|
|
30
|
+
/** The `[Desktop Entry]` group's unlocalized keys (string escapes undone), first value of each key. */
|
|
31
|
+
export declare function desktopEntry(text: string): Record<string, string>;
|
|
32
|
+
/** An entry's Exec as argv (the Desktop Entry quoting rules), field codes and flatpak's @@ markers removed; undefined when malformed. */
|
|
33
|
+
export declare function desktopExec(value: string): string[] | undefined;
|
|
34
|
+
/** The installed T3 Code apps on Linux, from the desktop entries and AppImages in `dirs` (linuxApplicationDirs). */
|
|
35
|
+
export declare function linuxT3Apps(dirs: string[], env: Env): T3LinuxInstall[];
|
|
36
|
+
/**
|
|
37
|
+
* Whether this install runs the Shardflux provider (ACP mode): the server in its app.asar has the ACP Registry driver
|
|
38
|
+
* (deb, unpacked installs, flatpak). An AppImage is never mounted to look: its version decides (0.0.46+ have the
|
|
39
|
+
* driver), and T3's own launcher issues its sessions. A snap starts threads over SSH.
|
|
40
|
+
*/
|
|
41
|
+
export declare function linuxSupportsAcp(app: T3LinuxInstall): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* The bundled T3 server CLI (Electron as Node, ELECTRON_RUN_AS_NODE=1 set by the caller): the app's executable with
|
|
44
|
+
* the entry inside its app.asar; a flatpak's through `flatpak run`; an AppImage's through T3's own launcher
|
|
45
|
+
* (`<T3 home>/bin/t3`, which mounts the image). Undefined when there is none yet.
|
|
46
|
+
*/
|
|
47
|
+
export declare function linuxServerCli(app: T3LinuxInstall, t3Home: string): {
|
|
48
|
+
command: string;
|
|
49
|
+
args: string[];
|
|
50
|
+
} | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* How T3 is opened (or brought to the front, as a second launch of T3 hands over to the running one): `gtk-launch`
|
|
53
|
+
* with its desktop entry when there is one, else its own program, started detached.
|
|
54
|
+
*/
|
|
55
|
+
export declare function linuxOpenCommands(app: T3LinuxInstall, env: Env): Array<{
|
|
56
|
+
command: string;
|
|
57
|
+
args: string[];
|
|
58
|
+
detached: boolean;
|
|
59
|
+
}>;
|
|
60
|
+
/** Opens T3 Code on Linux. Nothing here ever stops a T3 process: a launcher that hangs is left alone. */
|
|
61
|
+
export declare function openLinuxT3(env: Env, app: T3LinuxInstall): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* When T3 last started, as the filesystem records it: the program's access time once it is later than its modification
|
|
64
|
+
* time (dpkg installs both at the build time; relatime updates the access time on the first start, then daily). Null
|
|
65
|
+
* before the first start and on noatime mounts.
|
|
66
|
+
*/
|
|
67
|
+
export declare function linuxLastUsed(path: string): string | null;
|
|
68
|
+
export {};
|
package/dist/t3-linux.js
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T3 Code on Linux (0.17.0+): finding the installed desktop app, its version and route, and opening it.
|
|
3
|
+
*
|
|
4
|
+
* T3 ships a .deb and an AppImage (electron-builder, 0.0.45 and the nightlies, verified from the release assets):
|
|
5
|
+
* - .deb (package `t3code`): the app in `/opt/T3 Code (Alpha)/` (`/opt/T3 Code (Nightly)/` for nightlies) with the
|
|
6
|
+
* executable `t3code`, `resources/app.asar` and `resources/package-type` ("deb"); the entry
|
|
7
|
+
* `/usr/share/applications/t3code.desktop` (`Name=T3 Code (Alpha)`, `Exec="/opt/T3 Code (Alpha)/t3code" %U`,
|
|
8
|
+
* `StartupWMClass=t3code`, `MimeType=x-scheme-handler/t3code;…`); `/usr/bin/t3code` is an update-alternatives link
|
|
9
|
+
* made by its postinst. dpkg records nightlies as `0.0.46~nightly.<date>.<run>`.
|
|
10
|
+
* - AppImage: `T3-Code-<version>-<x86_64|arm64>.AppImage`, a squashfs image that is never mounted here; AppImageLauncher
|
|
11
|
+
* and Gear Lever integrate it with a copy of the same entry (`X-AppImage-Version`, `Exec=<the AppImage> %U`).
|
|
12
|
+
*
|
|
13
|
+
* Entries are found in the XDG application folders (also snapd's and flatpak's exports) and keyed on what the entry
|
|
14
|
+
* says (its Name, and StartupWMClass, the t3code URL scheme or a T3 flatpak id), never on its file name; only files
|
|
15
|
+
* whose name could be one are read, so a computer without T3 costs a few directory listings and no subprocess.
|
|
16
|
+
*/
|
|
17
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
18
|
+
import { accessSync, constants, openSync, readFileSync, readSync, closeSync, readdirSync, realpathSync, statSync } from 'node:fs';
|
|
19
|
+
import { homedir } from 'node:os';
|
|
20
|
+
import { basename, delimiter, dirname, isAbsolute, join, relative } from 'node:path';
|
|
21
|
+
import { T3_SERVER_ENTRY, asarVersion, t3ArchiveSupportsAcp } from "./t3-asar.js";
|
|
22
|
+
const RELEASE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
23
|
+
const MAYBE_ENTRY = /t3|^appimagekit_|^gearlever_/i;
|
|
24
|
+
const APPIMAGE = /^t3[-_ ]?code.*\.appimage$/i;
|
|
25
|
+
/** `T3-Code-<version>-<arch>.AppImage`, also as AppImageLauncher renames it (`…-x86_64_<hash>.AppImage`). */
|
|
26
|
+
const APPIMAGE_VERSION = /^T3[-_ ]?Code-(\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?)-(?:x86_64|x64|amd64|arm64|aarch64|armv7l)(?![0-9A-Za-z])/i;
|
|
27
|
+
const FLATPAK_ID = 'com.t3tools.t3code';
|
|
28
|
+
/** The launcher T3 (nightlies since October 2026) writes to `<T3 home>/bin/t3` for its bundled CLI; for an AppImage it mounts the image itself. */
|
|
29
|
+
export const T3_LAUNCHER_MARKER = "Written by T3 Code: runs the desktop app's bundled t3 CLI.";
|
|
30
|
+
/**
|
|
31
|
+
* The folders scanned on Linux, in XDG precedence: the data home's and each data dir's `applications`, snapd's and
|
|
32
|
+
* flatpak's exported entries, then the usual AppImage folders. SHARDFLUX_APPLICATIONS_DIRS replaces all of them.
|
|
33
|
+
*/
|
|
34
|
+
export function linuxApplicationDirs(env) {
|
|
35
|
+
const home = env.HOME || homedir();
|
|
36
|
+
const dataHome = env.XDG_DATA_HOME && isAbsolute(env.XDG_DATA_HOME) ? env.XDG_DATA_HOME : join(home, '.local', 'share');
|
|
37
|
+
const dataDirs = (env.XDG_DATA_DIRS || '/usr/local/share:/usr/share').split(':').filter(d => isAbsolute(d));
|
|
38
|
+
return [...new Set([dataHome, ...dataDirs, '/usr/local/share', '/usr/share'].map(d => join(d, 'applications')).concat('/var/lib/snapd/desktop/applications', '/var/lib/flatpak/exports/share/applications', join(dataHome, 'flatpak', 'exports', 'share', 'applications'), join(home, 'Applications'), join(home, 'AppImages'), join(home, '.local', 'bin'), join(home, 'Downloads')))];
|
|
39
|
+
}
|
|
40
|
+
/** The `[Desktop Entry]` group's unlocalized keys (string escapes undone), first value of each key. */
|
|
41
|
+
export function desktopEntry(text) {
|
|
42
|
+
const keys = {};
|
|
43
|
+
let main = false;
|
|
44
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
45
|
+
const line = raw.trim();
|
|
46
|
+
if (!line || line.startsWith('#'))
|
|
47
|
+
continue;
|
|
48
|
+
if (line.startsWith('[')) {
|
|
49
|
+
main = line === '[Desktop Entry]';
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const eq = line.indexOf('=');
|
|
53
|
+
if (!main || eq < 1)
|
|
54
|
+
continue;
|
|
55
|
+
const key = line.slice(0, eq).trim();
|
|
56
|
+
if (!key.includes('[') && !(key in keys))
|
|
57
|
+
keys[key] = line.slice(eq + 1).trim().replace(/\\([sntr\\])/g, (_, c) => ({ s: ' ', n: '\n', t: '\t', r: '\r', '\\': '\\' })[c]);
|
|
58
|
+
}
|
|
59
|
+
return keys;
|
|
60
|
+
}
|
|
61
|
+
/** An entry's Exec as argv (the Desktop Entry quoting rules), field codes and flatpak's @@ markers removed; undefined when malformed. */
|
|
62
|
+
export function desktopExec(value) {
|
|
63
|
+
const args = [];
|
|
64
|
+
let current = '', started = false, quoted = false;
|
|
65
|
+
for (let i = 0; i < value.length; i++) {
|
|
66
|
+
const c = value[i];
|
|
67
|
+
if (quoted) {
|
|
68
|
+
if (c === '\\' && i + 1 < value.length && '"`$\\'.includes(value[i + 1]))
|
|
69
|
+
current += value[++i];
|
|
70
|
+
else if (c === '"')
|
|
71
|
+
quoted = false;
|
|
72
|
+
else
|
|
73
|
+
current += c;
|
|
74
|
+
}
|
|
75
|
+
else if (c === '"') {
|
|
76
|
+
quoted = true;
|
|
77
|
+
started = true;
|
|
78
|
+
}
|
|
79
|
+
else if (c === ' ' || c === '\t') {
|
|
80
|
+
if (started)
|
|
81
|
+
args.push(current);
|
|
82
|
+
current = '';
|
|
83
|
+
started = false;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
current += c;
|
|
87
|
+
started = true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (quoted)
|
|
91
|
+
return undefined;
|
|
92
|
+
if (started)
|
|
93
|
+
args.push(current);
|
|
94
|
+
return args.filter(a => !/^%[fFuUdDnNickvm]$/.test(a) && !/^@@[uf]?$/.test(a)).map(a => a.replace(/%(%|[a-zA-Z])/g, (_, c) => c === '%' ? '%' : ''));
|
|
95
|
+
}
|
|
96
|
+
/** Whether an entry is T3 Code's: its Name, and its window class, URL scheme or flatpak id. */
|
|
97
|
+
function isT3Entry(e) {
|
|
98
|
+
if ((e.Type ?? 'Application') !== 'Application' || !/^T3 Code(?: \(.+\))?$/.test(e.Name ?? ''))
|
|
99
|
+
return false;
|
|
100
|
+
return e.StartupWMClass === 't3code' || (e.MimeType ?? '').split(';').includes('x-scheme-handler/t3code') || e['X-Flatpak'] === FLATPAK_ID;
|
|
101
|
+
}
|
|
102
|
+
const executable = (path) => { try {
|
|
103
|
+
accessSync(path, constants.X_OK);
|
|
104
|
+
return statSync(path).isFile();
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return false;
|
|
108
|
+
} };
|
|
109
|
+
const real = (path) => { try {
|
|
110
|
+
return realpathSync(path);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return path;
|
|
114
|
+
} };
|
|
115
|
+
const onPath = (name, env) => isAbsolute(name) ? (executable(name) ? name : undefined) : (env.PATH ?? '').split(delimiter).filter(Boolean).map(dir => join(dir, name)).find(executable);
|
|
116
|
+
const release = (value) => value && RELEASE.test(value) ? value : null;
|
|
117
|
+
/** A type 2 AppImage: an ELF file with `AI\x02` at byte 8. */
|
|
118
|
+
function isAppImage(path) {
|
|
119
|
+
if (/\.appimage$/i.test(path))
|
|
120
|
+
return true;
|
|
121
|
+
let fd;
|
|
122
|
+
try {
|
|
123
|
+
fd = openSync(path, 'r');
|
|
124
|
+
const b = Buffer.alloc(11);
|
|
125
|
+
return readSync(fd, b, 0, 11, 0) === 11 && b.subarray(0, 4).equals(Buffer.from('\x7fELF', 'latin1')) && b.subarray(8, 11).equals(Buffer.from('AI\x02', 'latin1'));
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
if (fd !== undefined)
|
|
132
|
+
closeSync(fd);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** dpkg's version of the `t3code` package, in T3's own form (dpkg writes `0.0.46~nightly…` for `0.0.46-nightly…`). */
|
|
136
|
+
function dpkgVersion(env) {
|
|
137
|
+
const r = spawnSync('dpkg-query', ['-W', '-f=${Version}', 't3code'], { env, encoding: 'utf8', input: '', timeout: 2000 });
|
|
138
|
+
return r.status === 0 ? release(r.stdout.trim().replace(/^\d+:/, '').replaceAll('~', '-')) : null;
|
|
139
|
+
}
|
|
140
|
+
/** The resources/app.asar of an unpacked Electron app, one or two folders below `root` (a flatpak's files). */
|
|
141
|
+
function findAsar(root) {
|
|
142
|
+
const at = (dir) => { const asar = join(dir, 'resources', 'app.asar'); try {
|
|
143
|
+
return statSync(asar).isFile() ? asar : undefined;
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return undefined;
|
|
147
|
+
} };
|
|
148
|
+
const children = (dir) => { try {
|
|
149
|
+
return readdirSync(dir, { withFileTypes: true }).filter(d => d.isDirectory()).map(d => join(dir, d.name));
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return [];
|
|
153
|
+
} };
|
|
154
|
+
return at(root) ?? children(root).map(at).find(Boolean) ?? children(root).flatMap(children).slice(0, 64).map(at).find(Boolean);
|
|
155
|
+
}
|
|
156
|
+
/** The install a T3 entry describes, from its program: an AppImage, a flatpak, a snap, or an unpacked app folder. */
|
|
157
|
+
function fromEntry(e, id, dir, env) {
|
|
158
|
+
const exec = desktopExec(e.Exec ?? '');
|
|
159
|
+
if (!exec?.length)
|
|
160
|
+
return undefined;
|
|
161
|
+
let i = 0;
|
|
162
|
+
if (basename(exec[0]) === 'env') {
|
|
163
|
+
i = 1;
|
|
164
|
+
while (i < exec.length && /^[A-Za-z_]\w*=/.test(exec[i]))
|
|
165
|
+
i++;
|
|
166
|
+
}
|
|
167
|
+
const program = exec[i];
|
|
168
|
+
if (!program)
|
|
169
|
+
return undefined;
|
|
170
|
+
const entryVersion = release(e['X-AppImage-Version']);
|
|
171
|
+
if (e['X-Flatpak']) {
|
|
172
|
+
const appId = e['X-Flatpak'];
|
|
173
|
+
const root = dir.endsWith(join('exports', 'share', 'applications')) ? dirname(dirname(dirname(dir))) : '/var/lib/flatpak';
|
|
174
|
+
const files = join(root, 'app', appId, 'current', 'active', 'files');
|
|
175
|
+
const asar = findAsar(files) ?? null;
|
|
176
|
+
const command = exec.find(a => a.startsWith('--command='))?.slice('--command='.length);
|
|
177
|
+
const exe = asar ? join(dirname(dirname(asar)), basename(command || 't3code')) : files;
|
|
178
|
+
return { path: exe, version: (asar ? asarVersion(asar) : null) ?? entryVersion, linux: { format: 'flatpak', desktop_id: id, exec, asar: executable(exe) ? asar : null, flatpak: { id: appId, files } } };
|
|
179
|
+
}
|
|
180
|
+
const resolved = onPath(program, env);
|
|
181
|
+
if (!resolved)
|
|
182
|
+
return undefined;
|
|
183
|
+
if (e['X-SnapInstanceName'] || resolved.startsWith('/snap/bin/') || dir === '/var/lib/snapd/desktop/applications') {
|
|
184
|
+
const name = e['X-SnapInstanceName'] || basename(resolved);
|
|
185
|
+
let version = null;
|
|
186
|
+
try {
|
|
187
|
+
version = release(/^version:\s*['"]?([^'"\s]+)/m.exec(readFileSync(join('/snap', name, 'current', 'meta', 'snap.yaml'), 'utf8'))?.[1]);
|
|
188
|
+
}
|
|
189
|
+
catch { /* no snap metadata */ }
|
|
190
|
+
return { path: resolved, version, linux: { format: 'snap', desktop_id: id, exec, asar: null } };
|
|
191
|
+
}
|
|
192
|
+
const target = real(resolved);
|
|
193
|
+
if (isAppImage(target)) {
|
|
194
|
+
return { path: target, version: entryVersion ?? release(APPIMAGE_VERSION.exec(basename(target))?.[1]), linux: { format: 'appimage', desktop_id: id, exec, asar: null } };
|
|
195
|
+
}
|
|
196
|
+
const asar = join(dirname(target), 'resources', 'app.asar');
|
|
197
|
+
let found = false;
|
|
198
|
+
try {
|
|
199
|
+
found = statSync(asar).isFile();
|
|
200
|
+
}
|
|
201
|
+
catch { /* not an unpacked Electron app */ }
|
|
202
|
+
let deb = false;
|
|
203
|
+
try {
|
|
204
|
+
deb = readFileSync(join(dirname(target), 'resources', 'package-type'), 'utf8').trim() === 'deb';
|
|
205
|
+
}
|
|
206
|
+
catch { /* not from the .deb */ }
|
|
207
|
+
const version = (found ? asarVersion(asar) : null) ?? (deb ? dpkgVersion(env) : null) ?? entryVersion;
|
|
208
|
+
return { path: target, version, linux: { format: deb ? 'deb' : 'directory', desktop_id: id, exec, asar: found ? asar : null } };
|
|
209
|
+
}
|
|
210
|
+
/** The installed T3 Code apps on Linux, from the desktop entries and AppImages in `dirs` (linuxApplicationDirs). */
|
|
211
|
+
export function linuxT3Apps(dirs, env) {
|
|
212
|
+
const found = [], ids = new Set(), loose = [];
|
|
213
|
+
for (const dir of dirs) {
|
|
214
|
+
let names;
|
|
215
|
+
try {
|
|
216
|
+
names = readdirSync(dir);
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
for (const name of names) {
|
|
222
|
+
if (APPIMAGE.test(name)) {
|
|
223
|
+
loose.push(join(dir, name));
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (!name.endsWith('.desktop') || !MAYBE_ENTRY.test(name) || ids.has(name))
|
|
227
|
+
continue;
|
|
228
|
+
let text;
|
|
229
|
+
try {
|
|
230
|
+
text = readFileSync(join(dir, name), 'utf8');
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
// The first entry with an id wins (XDG precedence); a Hidden one removes that id.
|
|
236
|
+
ids.add(name);
|
|
237
|
+
const e = desktopEntry(text);
|
|
238
|
+
if (e.Hidden === 'true' || !isT3Entry(e) || (e.TryExec && !onPath(e.TryExec, env)))
|
|
239
|
+
continue;
|
|
240
|
+
const app = fromEntry(e, name, dir, env);
|
|
241
|
+
if (app && !found.some(f => f.path === app.path))
|
|
242
|
+
found.push(app);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
for (const file of loose) {
|
|
246
|
+
const path = real(file);
|
|
247
|
+
if (!executable(path) || found.some(f => f.path === path))
|
|
248
|
+
continue;
|
|
249
|
+
found.push({ path, version: release(APPIMAGE_VERSION.exec(basename(path))?.[1]), linux: { format: 'appimage', desktop_id: null, exec: [path], asar: null } });
|
|
250
|
+
}
|
|
251
|
+
return found;
|
|
252
|
+
}
|
|
253
|
+
/** `0.0.46` and later (nightlies of 0.0.46 included). */
|
|
254
|
+
const atLeast046 = (version) => {
|
|
255
|
+
const [major = 0, minor = 0, patch = 0] = /^(\d+)\.(\d+)\.(\d+)/.exec(version ?? '')?.slice(1).map(Number) ?? [];
|
|
256
|
+
return major > 0 || minor > 0 || patch >= 46;
|
|
257
|
+
};
|
|
258
|
+
/**
|
|
259
|
+
* Whether this install runs the Shardflux provider (ACP mode): the server in its app.asar has the ACP Registry driver
|
|
260
|
+
* (deb, unpacked installs, flatpak). An AppImage is never mounted to look: its version decides (0.0.46+ have the
|
|
261
|
+
* driver), and T3's own launcher issues its sessions. A snap starts threads over SSH.
|
|
262
|
+
*/
|
|
263
|
+
export function linuxSupportsAcp(app) {
|
|
264
|
+
if (app.linux.format === 'appimage')
|
|
265
|
+
return !!app.version && atLeast046(app.version);
|
|
266
|
+
return app.linux.format !== 'snap' && !!app.linux.asar && t3ArchiveSupportsAcp(app.linux.asar);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* The bundled T3 server CLI (Electron as Node, ELECTRON_RUN_AS_NODE=1 set by the caller): the app's executable with
|
|
270
|
+
* the entry inside its app.asar; a flatpak's through `flatpak run`; an AppImage's through T3's own launcher
|
|
271
|
+
* (`<T3 home>/bin/t3`, which mounts the image). Undefined when there is none yet.
|
|
272
|
+
*/
|
|
273
|
+
export function linuxServerCli(app, t3Home) {
|
|
274
|
+
const { format, asar, flatpak } = app.linux;
|
|
275
|
+
if ((format === 'deb' || format === 'directory') && asar)
|
|
276
|
+
return { command: app.path, args: [join(asar, T3_SERVER_ENTRY)] };
|
|
277
|
+
if (format === 'flatpak' && flatpak && asar) {
|
|
278
|
+
const inside = (path) => `/app/${relative(flatpak.files, path)}`;
|
|
279
|
+
return { command: 'flatpak', args: ['run', `--command=${inside(app.path)}`, '--env=ELECTRON_RUN_AS_NODE=1', flatpak.id, inside(join(asar, T3_SERVER_ENTRY))] };
|
|
280
|
+
}
|
|
281
|
+
if (format === 'appimage') {
|
|
282
|
+
const launcher = join(t3Home, 'bin', 't3');
|
|
283
|
+
try {
|
|
284
|
+
if (executable(launcher) && readFileSync(launcher, 'utf8').includes(T3_LAUNCHER_MARKER))
|
|
285
|
+
return { command: launcher, args: [] };
|
|
286
|
+
}
|
|
287
|
+
catch { /* not written yet */ }
|
|
288
|
+
}
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* How T3 is opened (or brought to the front, as a second launch of T3 hands over to the running one): `gtk-launch`
|
|
293
|
+
* with its desktop entry when there is one, else its own program, started detached.
|
|
294
|
+
*/
|
|
295
|
+
export function linuxOpenCommands(app, env) {
|
|
296
|
+
const { desktop_id: id, exec, format, flatpak } = app.linux;
|
|
297
|
+
const gtk = id ? onPath('gtk-launch', env) : undefined;
|
|
298
|
+
return [
|
|
299
|
+
...(gtk && id ? [{ command: gtk, args: [id], detached: false }] : []),
|
|
300
|
+
format === 'flatpak' && flatpak ? { command: 'flatpak', args: ['run', flatpak.id], detached: true } : { command: exec[0], args: exec.slice(1), detached: true },
|
|
301
|
+
];
|
|
302
|
+
}
|
|
303
|
+
/** Opens T3 Code on Linux. Nothing here ever stops a T3 process: a launcher that hangs is left alone. */
|
|
304
|
+
export async function openLinuxT3(env, app) {
|
|
305
|
+
let last;
|
|
306
|
+
for (const { command, args, detached } of linuxOpenCommands(app, env)) {
|
|
307
|
+
try {
|
|
308
|
+
await new Promise((resolve, reject) => {
|
|
309
|
+
const child = spawn(command, args, { env, stdio: 'ignore', detached });
|
|
310
|
+
if (detached) {
|
|
311
|
+
child.once('spawn', () => { child.unref(); resolve(); });
|
|
312
|
+
child.once('error', reject);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const timer = setTimeout(() => { child.unref(); reject(new Error(`${basename(command)} did not answer in time`)); }, 10_000);
|
|
316
|
+
child.once('error', e => { clearTimeout(timer); reject(e); });
|
|
317
|
+
child.once('exit', code => { clearTimeout(timer); if (code === 0)
|
|
318
|
+
resolve();
|
|
319
|
+
else
|
|
320
|
+
reject(new Error(`${basename(command)} exited ${code}`)); });
|
|
321
|
+
});
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
last = error;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
throw new Error(`could not open T3 Code (${last instanceof Error ? last.message : String(last)})`);
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* When T3 last started, as the filesystem records it: the program's access time once it is later than its modification
|
|
332
|
+
* time (dpkg installs both at the build time; relatime updates the access time on the first start, then daily). Null
|
|
333
|
+
* before the first start and on noatime mounts.
|
|
334
|
+
*/
|
|
335
|
+
export function linuxLastUsed(path) {
|
|
336
|
+
try {
|
|
337
|
+
const s = statSync(path);
|
|
338
|
+
return s.atimeMs > s.mtimeMs + 1000 ? new Date(s.atimeMs).toISOString() : null;
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
}
|
package/dist/t3-mode.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Workspace } from '@shardflux/sdk';
|
|
2
2
|
import type { Ctx } from './commands.js';
|
|
3
3
|
import { CodedError } from './errors.js';
|
|
4
|
+
import { t3HomeDir } from './t3-provider.js';
|
|
5
|
+
import type { T3LinuxApp } from './t3-linux.js';
|
|
4
6
|
import type { InitialImage } from './initial-prompt.js';
|
|
5
7
|
export declare const T3_BUNDLE_ID = "com.t3tools.t3code";
|
|
6
8
|
export type T3Mode = 'acp' | 'ssh';
|
|
@@ -8,10 +10,13 @@ export type AgentApp = 'claude' | 'codex';
|
|
|
8
10
|
export type OpenIn = 'terminal' | 'claude-desktop' | 'web' | 'codex-app' | 't3';
|
|
9
11
|
/** Where each app's new conversations can open: Claude in a terminal, Claude Desktop, claude.ai or T3; Codex in the Codex app or T3. */
|
|
10
12
|
export declare const OPEN_IN_TARGETS: Record<AgentApp, readonly OpenIn[]>;
|
|
13
|
+
export declare const openInTargets: (app: AgentApp, platform?: NodeJS.Platform) => readonly OpenIn[];
|
|
14
|
+
/** The T3 Code `shard` uses: its .app bundle (macOS) or program (Linux, with how it was installed in `linux`). */
|
|
11
15
|
export interface T3Install {
|
|
12
16
|
path: string;
|
|
13
17
|
version: string | null;
|
|
14
18
|
mode: T3Mode;
|
|
19
|
+
linux?: T3LinuxApp;
|
|
15
20
|
}
|
|
16
21
|
export interface T3Status {
|
|
17
22
|
installed: boolean;
|
|
@@ -41,16 +46,19 @@ export interface T3ThreadResult {
|
|
|
41
46
|
effort: string | null;
|
|
42
47
|
effort_reason?: EffortReason;
|
|
43
48
|
}
|
|
44
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Native lookups that tests replace: Spotlight's last-used dates of app bundles, one `mdls` for all of them (macOS), or
|
|
51
|
+
* when each program last started, from its access time (Linux, t3-linux.ts).
|
|
52
|
+
*/
|
|
45
53
|
export declare const t3Native: {
|
|
46
|
-
lastUsedMany(paths: readonly string[], env: Record<string, string | undefined
|
|
54
|
+
lastUsedMany(paths: readonly string[], env: Record<string, string | undefined>, platform?: NodeJS.Platform): Record<string, string | null>;
|
|
47
55
|
};
|
|
48
56
|
/** Spotlight takes about 40 ms per call, so the dates are kept for a few minutes (app-usage.json in the config dir). */
|
|
49
57
|
export declare const LAST_USED_TTL_MS: number;
|
|
50
58
|
export declare function lastUsedDates(ctx: Ctx, paths: readonly string[], now?: number): Record<string, string | null>;
|
|
51
59
|
/** The installed T3 Code, preferring a build with ACP providers when several channels are installed. */
|
|
52
|
-
export declare function t3Install(env: Record<string, string | undefined>, platform?: NodeJS.Platform): T3Install | undefined;
|
|
53
|
-
export
|
|
60
|
+
export declare function t3Install(env: Record<string, string | undefined>, platform?: NodeJS.Platform, dirs?: string[]): T3Install | undefined;
|
|
61
|
+
export { t3HomeDir };
|
|
54
62
|
/** The Shardflux provider instance in T3's server settings (read only; every `shard` run writes it, see t3-provider.ts). */
|
|
55
63
|
export declare function t3ProviderRegistered(env: Record<string, string | undefined>): boolean;
|
|
56
64
|
/**
|
|
@@ -91,10 +99,11 @@ export declare const t3NotInstalled: () => CodedError;
|
|
|
91
99
|
/** The first line of the prompt, as T3 titles a thread before it names it. */
|
|
92
100
|
export declare function t3ThreadTitle(prompt: string): string;
|
|
93
101
|
/**
|
|
94
|
-
* Opens T3 Code (or brings it to the front): the install `shard` chose
|
|
95
|
-
* bundle id, so `open -b` could start another installed channel
|
|
102
|
+
* Opens T3 Code (or brings it to the front): the install `shard` chose. On macOS `open -a <path>` (every T3 channel
|
|
103
|
+
* shares one bundle id, so `open -b` could start another installed channel); on Linux its desktop entry with
|
|
104
|
+
* `gtk-launch`, else its program (t3-linux.ts).
|
|
96
105
|
*/
|
|
97
|
-
export declare function openT3(env: Record<string, string | undefined>,
|
|
106
|
+
export declare function openT3(env: Record<string, string | undefined>, install: Pick<T3Install, 'path' | 'version' | 'linux'> | undefined): Promise<void>;
|
|
98
107
|
/** Starts the thread in the VM's T3 server (T3_THREAD_SCRIPT); the prompt travels on stdin, never in argv. */
|
|
99
108
|
export declare function startSshThread(ctx: Ctx, ws: Workspace, version: string | undefined, projectPath: string, request: T3ThreadRequest): Promise<T3ThreadResult>;
|
|
100
109
|
interface Descriptor {
|
|
@@ -118,4 +127,3 @@ export declare function t3Rpc(server: LocalT3Server, session: T3Session, tag: st
|
|
|
118
127
|
export declare function startAcpThread(ctx: Ctx, install: T3Install, folder: string, request: T3ThreadRequest, waitMs?: number): Promise<T3ThreadResult>;
|
|
119
128
|
/** The T3 thread of a Shardflux conversation started in T3 (ACP mode): the one remembered, else a search of recent Shardflux threads. */
|
|
120
129
|
export declare function findAcpThread(ctx: Ctx, install: T3Install, conversationId: string): Promise<string | null>;
|
|
121
|
-
export {};
|