@shardflux/cli 0.18.0-next.4 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +61 -8
  2. package/README.md +92 -33
  3. package/dist/account-args.js +5 -3
  4. package/dist/account-commands.d.ts +4 -1
  5. package/dist/account-commands.js +104 -14
  6. package/dist/account-pool.d.ts +31 -0
  7. package/dist/account-pool.js +313 -0
  8. package/dist/agent-setup.d.ts +38 -0
  9. package/dist/agent-setup.js +116 -0
  10. package/dist/agent-target-commands.js +10 -1
  11. package/dist/agent-targets.d.ts +11 -0
  12. package/dist/agent-targets.js +22 -4
  13. package/dist/args.d.ts +3 -1
  14. package/dist/args.js +50 -5
  15. package/dist/claude-cloud-login.d.ts +11 -0
  16. package/dist/claude-cloud-login.js +49 -39
  17. package/dist/cli-proxy-hub.d.ts +26 -1
  18. package/dist/cli-proxy-hub.js +60 -4
  19. package/dist/commands.d.ts +3 -0
  20. package/dist/commands.js +7 -3
  21. package/dist/conversation-commands.d.ts +5 -0
  22. package/dist/conversation-commands.js +8 -2
  23. package/dist/conversations.d.ts +9 -0
  24. package/dist/desktop-linux.js +6 -5
  25. package/dist/filesystem-commands.d.ts +12 -2
  26. package/dist/filesystem-commands.js +46 -18
  27. package/dist/git-remote-bin.js +0 -0
  28. package/dist/handlers.js +2 -1
  29. package/dist/input.d.ts +4 -2
  30. package/dist/input.js +6 -3
  31. package/dist/main.js +19 -8
  32. package/dist/menubar.js +1 -1
  33. package/dist/t3-asar.d.ts +12 -2
  34. package/dist/t3-asar.js +14 -5
  35. package/dist/t3-launch.js +4 -1
  36. package/dist/t3-linux.d.ts +12 -13
  37. package/dist/t3-linux.js +81 -87
  38. package/dist/t3-mode.d.ts +4 -3
  39. package/dist/t3-mode.js +15 -14
  40. package/dist/t3-version.d.ts +8 -0
  41. package/dist/t3-version.js +42 -0
  42. package/package.json +21 -7
package/dist/t3-linux.js CHANGED
@@ -3,29 +3,38 @@
3
3
  *
4
4
  * T3 ships a .deb and an AppImage (electron-builder, 0.0.45 and the nightlies, verified from the release assets):
5
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`).
6
+ * executable `t3code`, `resources/app.asar` (package.json `name: "t3code"`) and `resources/package-type` ("deb");
7
+ * the entry `/usr/share/applications/t3code.desktop` (`Name=T3 Code (Alpha)`, `Exec="/opt/T3 Code (Alpha)/t3code"
8
+ * %U`, `StartupWMClass=t3code`, `MimeType=x-scheme-handler/t3code;…`); `/usr/bin/t3code` is an update-alternatives
9
+ * link made by its postinst. dpkg records nightlies as `0.0.46~nightly.<date>.<run>`.
10
+ * - AppImage: `T3-Code-<version>-<x86_64|arm64>.AppImage`, a type 2 AppImage (ELF, `AI\x02` at byte 8) that is never
11
+ * mounted here; AppImageLauncher and Gear Lever integrate it with a copy of the same entry (`Exec=<the AppImage> %U`).
12
+ * T3 publishes no flatpak or snap: the flatpak handling below follows flatpak's export layout with T3's app id
13
+ * (`com.t3tools.t3code`, its AppStream id), an unverified layout; a snap entry is accepted only when it runs an unpacked
14
+ * T3 directly.
12
15
  *
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
+ * Entries are found in the XDG application folders (and snapd's and flatpak's exports) and keyed on what the entry says
17
+ * (its Name, and StartupWMClass, the t3code URL scheme or the flatpak id), never on its file name; only files whose name
18
+ * could be one are read. An entry is accepted only when it runs T3's own program with options: no shell, env prefix or
19
+ * other program, and an unpacked app's app.asar must be T3 Code's. Detection never reads the program of an unpacked app,
20
+ * and a computer without T3 costs a few directory listings and no subprocess.
16
21
  */
17
22
  import { spawn, spawnSync } from 'node:child_process';
18
23
  import { accessSync, constants, openSync, readFileSync, readSync, closeSync, readdirSync, realpathSync, statSync } from 'node:fs';
19
24
  import { homedir } from 'node:os';
20
25
  import { basename, delimiter, dirname, isAbsolute, join, relative } from 'node:path';
21
- import { T3_SERVER_ENTRY, asarVersion, t3ArchiveSupportsAcp } from "./t3-asar.js";
26
+ import { T3_SERVER_ENTRY, asarPackage, t3ArchiveSupportsAcp } from "./t3-asar.js";
27
+ import { t3WritesLauncher } from "./t3-version.js";
22
28
  const RELEASE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
23
29
  const MAYBE_ENTRY = /t3|^appimagekit_|^gearlever_/i;
24
30
  const APPIMAGE = /^t3[-_ ]?code.*\.appimage$/i;
25
31
  /** `T3-Code-<version>-<arch>.AppImage`, also as AppImageLauncher renames it (`…-x86_64_<hash>.AppImage`). */
26
32
  const APPIMAGE_VERSION = /^T3[-_ ]?Code-(\d+\.\d+\.\d+(?:-[0-9A-Za-z.]+)?)-(?:x86_64|x64|amd64|arm64|aarch64|armv7l)(?![0-9A-Za-z])/i;
33
+ /** T3's app id (its AppStream id); a T3 flatpak with it is an assumption, not a published package. */
27
34
  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. */
35
+ /** Programs that would run something other than T3 itself. */
36
+ const WRAPPERS = /^(?:env|sh|bash|dash|zsh|ksh|fish|busybox|sudo|pkexec|nohup)$/;
37
+ /** The launcher T3 (from v0.0.46-preview.20261008.2800) writes to `<T3 home>/bin/t3` for its bundled CLI; for an AppImage it mounts the image itself. */
29
38
  export const T3_LAUNCHER_MARKER = "Written by T3 Code: runs the desktop app's bundled t3 CLI.";
30
39
  /**
31
40
  * The folders scanned on Linux, in XDG precedence: the data home's and each data dir's `applications`, snapd's and
@@ -106,6 +115,12 @@ const executable = (path) => { try {
106
115
  catch {
107
116
  return false;
108
117
  } };
118
+ const isFile = (path) => { try {
119
+ return statSync(path).isFile();
120
+ }
121
+ catch {
122
+ return false;
123
+ } };
109
124
  const real = (path) => { try {
110
125
  return realpathSync(path);
111
126
  }
@@ -114,10 +129,8 @@ catch {
114
129
  } };
115
130
  const onPath = (name, env) => isAbsolute(name) ? (executable(name) ? name : undefined) : (env.PATH ?? '').split(delimiter).filter(Boolean).map(dir => join(dir, name)).find(executable);
116
131
  const release = (value) => value && RELEASE.test(value) ? value : null;
117
- /** A type 2 AppImage: an ELF file with `AI\x02` at byte 8. */
132
+ /** A type 2 AppImage: an ELF file with `AI\x02` at byte 8 (as T3-Code-0.0.45-x86_64.AppImage and the nightlies start). */
118
133
  function isAppImage(path) {
119
- if (/\.appimage$/i.test(path))
120
- return true;
121
134
  let fd;
122
135
  try {
123
136
  fd = openSync(path, 'r');
@@ -139,12 +152,7 @@ function dpkgVersion(env) {
139
152
  }
140
153
  /** The resources/app.asar of an unpacked Electron app, one or two folders below `root` (a flatpak's files). */
141
154
  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
- } };
155
+ const at = (dir) => { const asar = join(dir, 'resources', 'app.asar'); return isFile(asar) ? asar : undefined; };
148
156
  const children = (dir) => { try {
149
157
  return readdirSync(dir, { withFileTypes: true }).filter(d => d.isDirectory()).map(d => join(dir, d.name));
150
158
  }
@@ -153,59 +161,53 @@ function findAsar(root) {
153
161
  } };
154
162
  return at(root) ?? children(root).map(at).find(Boolean) ?? children(root).flatMap(children).slice(0, 64).map(at).find(Boolean);
155
163
  }
156
- /** The install a T3 entry describes, from its program: an AppImage, a flatpak, a snap, or an unpacked app folder. */
164
+ /** The flatpak an exported entry runs (unverified layout: `flatpak run [options] <id>`, files under the installation). */
165
+ function fromFlatpak(e, exec, id, dir) {
166
+ const appId = e['X-Flatpak'];
167
+ if (basename(exec[0]) !== 'flatpak' || !exec.slice(1).every(a => a.startsWith('-') || a === 'run' || a === appId))
168
+ return undefined;
169
+ const root = dir.endsWith(join('exports', 'share', 'applications')) ? dirname(dirname(dirname(dir))) : '/var/lib/flatpak';
170
+ const files = join(root, 'app', appId, 'current', 'active', 'files');
171
+ const asar = findAsar(files);
172
+ const pkg = asar ? asarPackage(asar) : null;
173
+ if (!asar || !pkg?.t3)
174
+ return undefined;
175
+ const command = exec.find(a => a.startsWith('--command='))?.slice('--command='.length);
176
+ const exe = join(dirname(dirname(asar)), basename(command || 't3code'));
177
+ return { path: exe, version: pkg.version, linux: { format: 'flatpak', desktop_id: id, exec, asar: executable(exe) ? asar : null, flatpak: { id: appId, files } } };
178
+ }
179
+ /**
180
+ * The install a T3 entry describes: an unpacked app (its program sits next to `resources/app.asar`, which must be T3
181
+ * Code's; the program itself is never read) or an AppImage (checked by its magic bytes). Refused: a shell, env prefix
182
+ * or other wrapper, an argument that is not an option, and a program that resolves anywhere else.
183
+ */
157
184
  function fromEntry(e, id, dir, env) {
158
185
  const exec = desktopExec(e.Exec ?? '');
159
186
  if (!exec?.length)
160
187
  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)
188
+ if (e['X-Flatpak'])
189
+ return fromFlatpak(e, exec, id, dir);
190
+ const [program, ...args] = exec;
191
+ if (WRAPPERS.test(basename(program)) || args.some(a => !a.startsWith('-')))
169
192
  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
193
  const resolved = onPath(program, env);
181
194
  if (!resolved)
182
195
  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;
196
+ const target = real(resolved), resources = join(dirname(target), 'resources');
197
+ if (isFile(join(resources, 'app.asar')) || isFile(join(resources, 'package-type'))) {
198
+ const asar = join(resources, 'app.asar'), pkg = asarPackage(asar);
199
+ if (!pkg?.t3)
200
+ return undefined;
201
+ let deb = false;
186
202
  try {
187
- version = release(/^version:\s*['"]?([^'"\s]+)/m.exec(readFileSync(join('/snap', name, 'current', 'meta', 'snap.yaml'), 'utf8'))?.[1]);
203
+ deb = readFileSync(join(resources, 'package-type'), 'utf8').trim() === 'deb';
188
204
  }
189
- catch { /* no snap metadata */ }
190
- return { path: resolved, version, linux: { format: 'snap', desktop_id: id, exec, asar: null } };
205
+ catch { /* not from the .deb */ }
206
+ return { path: target, version: pkg.version ?? (deb ? dpkgVersion(env) : null), linux: { format: deb ? 'deb' : 'directory', desktop_id: id, exec, asar } };
191
207
  }
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 } };
208
+ if (!isAppImage(target))
209
+ return undefined;
210
+ return { path: target, version: release(e['X-AppImage-Version']) ?? release(APPIMAGE_VERSION.exec(basename(target))?.[1]), linux: { format: 'appimage', desktop_id: id, exec, asar: null } };
209
211
  }
210
212
  /** The installed T3 Code apps on Linux, from the desktop entries and AppImages in `dirs` (linuxApplicationDirs). */
211
213
  export function linuxT3Apps(dirs, env) {
@@ -244,26 +246,21 @@ export function linuxT3Apps(dirs, env) {
244
246
  }
245
247
  for (const file of loose) {
246
248
  const path = real(file);
247
- if (!executable(path) || found.some(f => f.path === path))
249
+ if (found.some(f => f.path === path) || !executable(path) || !isAppImage(path))
248
250
  continue;
249
251
  found.push({ path, version: release(APPIMAGE_VERSION.exec(basename(path))?.[1]), linux: { format: 'appimage', desktop_id: null, exec: [path], asar: null } });
250
252
  }
251
253
  return found;
252
254
  }
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
255
  /**
259
256
  * 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.
257
+ * (deb, unpacked installs, flatpak). An AppImage is never mounted to look: its sessions come from T3's own launcher, so
258
+ * it is ACP from the first build that writes one (t3WritesLauncher); earlier builds start threads over SSH.
262
259
  */
263
260
  export function linuxSupportsAcp(app) {
264
261
  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);
262
+ return t3WritesLauncher(app.version);
263
+ return !!app.linux.asar && t3ArchiveSupportsAcp(app.linux.asar);
267
264
  }
268
265
  /**
269
266
  * The bundled T3 server CLI (Electron as Node, ELECTRON_RUN_AS_NODE=1 set by the caller): the app's executable with
@@ -288,6 +285,16 @@ export function linuxServerCli(app, t3Home) {
288
285
  }
289
286
  return undefined;
290
287
  }
288
+ /**
289
+ * The environment T3 is opened with: the caller's, without what would make its Electron run as plain Node
290
+ * (ELECTRON_RUN_AS_NODE: `t3code` would exit at once while the open still looked fine) or detach its console.
291
+ */
292
+ export function t3OpenEnv(env) {
293
+ const clean = { ...env };
294
+ delete clean.ELECTRON_RUN_AS_NODE;
295
+ delete clean.ELECTRON_NO_ATTACH_CONSOLE;
296
+ return clean;
297
+ }
291
298
  /**
292
299
  * How T3 is opened (or brought to the front, as a second launch of T3 hands over to the running one): `gtk-launch`
293
300
  * with its desktop entry when there is one, else its own program, started detached.
@@ -301,7 +308,8 @@ export function linuxOpenCommands(app, env) {
301
308
  ];
302
309
  }
303
310
  /** 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) {
311
+ export async function openLinuxT3(callerEnv, app) {
312
+ const env = t3OpenEnv(callerEnv);
305
313
  let last;
306
314
  for (const { command, args, detached } of linuxOpenCommands(app, env)) {
307
315
  try {
@@ -327,17 +335,3 @@ export async function openLinuxT3(env, app) {
327
335
  }
328
336
  throw new Error(`could not open T3 Code (${last instanceof Error ? last.message : String(last)})`);
329
337
  }
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
@@ -47,8 +47,9 @@ export interface T3ThreadResult {
47
47
  effort_reason?: EffortReason;
48
48
  }
49
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).
50
+ * Native lookups that tests replace: Spotlight's last-used dates of app bundles, one `mdls` for all of them (macOS).
51
+ * Linux has no reliable signal, so every date is null there: dpkg sets the program's access time to the install time,
52
+ * and any read (a scan, `dpkg --verify`, a backup) moves it.
52
53
  */
53
54
  export declare const t3Native: {
54
55
  lastUsedMany(paths: readonly string[], env: Record<string, string | undefined>, platform?: NodeJS.Platform): Record<string, string | null>;
@@ -56,7 +57,7 @@ export declare const t3Native: {
56
57
  /** Spotlight takes about 40 ms per call, so the dates are kept for a few minutes (app-usage.json in the config dir). */
57
58
  export declare const LAST_USED_TTL_MS: number;
58
59
  export declare function lastUsedDates(ctx: Ctx, paths: readonly string[], now?: number): Record<string, string | null>;
59
- /** The installed T3 Code, preferring a build with ACP providers when several channels are installed. */
60
+ /** The installed T3 Code: the newest build with ACP providers when several are installed (preferredT3). */
60
61
  export declare function t3Install(env: Record<string, string | undefined>, platform?: NodeJS.Platform, dirs?: string[]): T3Install | undefined;
61
62
  export { t3HomeDir };
62
63
  /** The Shardflux provider instance in T3's server settings (read only; every `shard` run writes it, see t3-provider.ts). */
package/dist/t3-mode.js CHANGED
@@ -24,9 +24,9 @@ import { basename, join } from 'node:path';
24
24
  import { setTimeout as delay } from 'node:timers/promises';
25
25
  import { CodedError, UsageError, describeError } from "./errors.js";
26
26
  import { AgentStateStore } from "./agent-state.js";
27
- import { applicationDirs, desktopAppPath, t3Apps } from "./agent-targets.js";
28
- import { configureT3Provider, t3BundleSupportsAcp, t3HomeDir } from "./t3-provider.js";
29
- import { linuxLastUsed, linuxServerCli, linuxSupportsAcp, openLinuxT3 } from "./t3-linux.js";
27
+ import { applicationDirs, desktopAppPath, preferredT3, t3Apps } from "./agent-targets.js";
28
+ import { configureT3Provider, t3HomeDir } from "./t3-provider.js";
29
+ import { linuxServerCli, openLinuxT3, t3OpenEnv } from "./t3-linux.js";
30
30
  import { acpSignedIn } from "./acp-login.js";
31
31
  import { AcpModelCache } from "./acp-models.js";
32
32
  import { nativeModelOptions } from "./acp-catalog.js";
@@ -49,15 +49,16 @@ const isoDate = (raw) => {
49
49
  return parsed && !Number.isNaN(parsed.getTime()) ? parsed.toISOString() : null;
50
50
  };
51
51
  /**
52
- * Native lookups that tests replace: Spotlight's last-used dates of app bundles, one `mdls` for all of them (macOS), or
53
- * when each program last started, from its access time (Linux, t3-linux.ts).
52
+ * Native lookups that tests replace: Spotlight's last-used dates of app bundles, one `mdls` for all of them (macOS).
53
+ * Linux has no reliable signal, so every date is null there: dpkg sets the program's access time to the install time,
54
+ * and any read (a scan, `dpkg --verify`, a backup) moves it.
54
55
  */
55
56
  export const t3Native = {
56
57
  lastUsedMany(paths, env, platform = process.platform) {
57
58
  if (!paths.length)
58
59
  return {};
59
60
  if (platform !== 'darwin')
60
- return Object.fromEntries(paths.map(path => [path, linuxLastUsed(path)]));
61
+ return Object.fromEntries(paths.map(path => [path, null]));
61
62
  const r = spawnSync('mdls', ['-name', 'kMDItemLastUsedDate', '-raw', ...paths], { env, encoding: 'utf8', input: '', timeout: 3000 });
62
63
  // -raw separates the values with NUL; a bundle Spotlight has no date for prints "(null)".
63
64
  const values = r.status === 0 ? r.stdout.split('\0').map(v => v.trim()) : [];
@@ -83,14 +84,13 @@ export function lastUsedDates(ctx, paths, now = Date.now()) {
83
84
  catch { /* a cache only */ }
84
85
  return dates;
85
86
  }
86
- /** Whether an installed T3 runs the Shardflux provider: its server has the ACP Registry driver (t3-asar.ts, t3-linux.ts). */
87
- const supportsAcp = (app) => app.linux ? linuxSupportsAcp({ ...app, linux: app.linux }) : t3BundleSupportsAcp(app.path);
88
- /** The installed T3 Code, preferring a build with ACP providers when several channels are installed. */
87
+ /** The installed T3 Code: the newest build with ACP providers when several are installed (preferredT3). */
89
88
  export function t3Install(env, platform = process.platform, dirs = applicationDirs(env, platform)) {
90
- const apps = t3Apps(dirs, platform, env);
91
- const acp = apps.find(supportsAcp);
92
- const app = acp ?? apps[0];
93
- return app ? { path: app.path, version: app.version, mode: acp ? 'acp' : 'ssh', ...(app.linux ? { linux: app.linux } : {}) } : undefined;
89
+ const picked = preferredT3(t3Apps(dirs, platform, env));
90
+ if (!picked)
91
+ return undefined;
92
+ const { app, acp } = picked;
93
+ return { path: app.path, version: app.version, mode: acp ? 'acp' : 'ssh', ...(app.linux ? { linux: app.linux } : {}) };
94
94
  }
95
95
  export { t3HomeDir };
96
96
  /** The Shardflux provider instance in T3's server settings (read only; every `shard` run writes it, see t3-provider.ts). */
@@ -219,7 +219,8 @@ export async function openT3(env, install) {
219
219
  throw t3NotInstalled();
220
220
  if (install.linux)
221
221
  return openLinuxT3(env, { path: install.path, version: install.version, linux: install.linux });
222
- const child = spawn('open', ['-a', install.path], { env, stdio: 'ignore' });
222
+ // As on Linux: an inherited ELECTRON_RUN_AS_NODE would start T3 as plain Node (t3OpenEnv).
223
+ const child = spawn('open', ['-a', install.path], { env: t3OpenEnv(env), stdio: 'ignore' });
223
224
  await new Promise((resolve, reject) => {
224
225
  const timer = setTimeout(() => { child.kill('SIGKILL'); reject(new Error('T3 Code did not open in time')); }, 10_000);
225
226
  child.once('error', e => { clearTimeout(timer); reject(e); });
@@ -0,0 +1,8 @@
1
+ /** Orders T3 versions (newest last): by release, a release after its own prereleases, builds of one release by run. */
2
+ export declare function compareT3Versions(a: string | null, b: string | null): number;
3
+ /**
4
+ * Whether this T3 writes its own CLI launcher (`<T3 home>/bin/t3`, DesktopCliShim, upstream 73e87f683ae1 of
5
+ * 2026-10-07): 0.0.46 builds from run 2800 on (v0.0.46-preview.20261008.2800 is the first; nightly 2787 predates it),
6
+ * 0.0.46 itself and later releases.
7
+ */
8
+ export declare function t3WritesLauncher(version: string | null): boolean;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * T3 Code release versions: `0.0.45`, and builds of the next release `0.0.46-<nightly|preview>.<YYYYMMDD>.<run>`, where
3
+ * `<run>` is one CI run counter shared by both channels (nightly 2787 < preview 2800 < nightly 2801).
4
+ */
5
+ const PARTS = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
6
+ function parse(version) {
7
+ const m = PARTS.exec(version);
8
+ if (!m)
9
+ return undefined;
10
+ const pre = m[4] ?? null;
11
+ return { core: [Number(m[1]), Number(m[2]), Number(m[3])], pre, run: pre ? Number(/\.(\d+)$/.exec(pre)?.[1] ?? NaN) : null };
12
+ }
13
+ /** Orders T3 versions (newest last): by release, a release after its own prereleases, builds of one release by run. */
14
+ export function compareT3Versions(a, b) {
15
+ const x = a ? parse(a) : undefined, y = b ? parse(b) : undefined;
16
+ if (!x || !y)
17
+ return x ? 1 : y ? -1 : 0;
18
+ for (let i = 0; i < 3; i++)
19
+ if (x.core[i] !== y.core[i])
20
+ return x.core[i] - y.core[i];
21
+ if (!x.pre || !y.pre)
22
+ return x.pre ? -1 : y.pre ? 1 : 0;
23
+ if (Number.isFinite(x.run) && Number.isFinite(y.run) && x.run !== y.run)
24
+ return x.run - y.run;
25
+ return x.pre < y.pre ? -1 : x.pre > y.pre ? 1 : 0;
26
+ }
27
+ /**
28
+ * Whether this T3 writes its own CLI launcher (`<T3 home>/bin/t3`, DesktopCliShim, upstream 73e87f683ae1 of
29
+ * 2026-10-07): 0.0.46 builds from run 2800 on (v0.0.46-preview.20261008.2800 is the first; nightly 2787 predates it),
30
+ * 0.0.46 itself and later releases.
31
+ */
32
+ export function t3WritesLauncher(version) {
33
+ const v = version ? parse(version) : undefined;
34
+ if (!v)
35
+ return false;
36
+ const [major, minor, patch] = v.core;
37
+ if (major > 0 || minor > 0 || patch > 46)
38
+ return true;
39
+ if (patch < 46)
40
+ return false;
41
+ return !v.pre || (Number.isFinite(v.run) && v.run >= 2800);
42
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shardflux/cli",
3
- "version": "0.18.0-next.4",
3
+ "version": "0.18.0",
4
4
  "type": "module",
5
5
  "description": "shard: the Shardflux command line. Create an account, sign in, set up keys and billing, then open, run commands in, move files to, suspend, resume and fork serverless agent workspaces.",
6
6
  "license": "Apache-2.0",
@@ -43,17 +43,31 @@
43
43
  "access": "public"
44
44
  },
45
45
  "dependencies": {
46
- "@shardflux/sdk": "0.24.0-next.4",
47
46
  "classic-level": "3.0.0",
48
47
  "smol-toml": "1.9.0",
49
48
  "snappyjs": "0.7.0",
50
- "yaml": "2.9.1"
49
+ "yaml": "2.9.1",
50
+ "@shardflux/sdk": "^0.24.0"
51
+ },
52
+ "devDependencies": {
53
+ "@eslint/js": "10.0.1",
54
+ "@types/node": "24.13.6",
55
+ "eslint": "10.11.0",
56
+ "playwright": "1.62.1",
57
+ "typescript": "5.9.3",
58
+ "typescript-eslint": "8.70.1"
51
59
  },
52
60
  "optionalDependencies": {
53
- "@shardflux/menubar-darwin": "^0.5.0",
54
- "@shardflux/desktop-linux-x64": "0.1.0-next.4",
55
- "@shardflux/desktop-linux-arm64": "0.1.0-next.4"
61
+ "@shardflux/desktop-linux-x64": "0.1.0",
62
+ "@shardflux/desktop-linux-arm64": "0.1.0",
63
+ "@shardflux/menubar-darwin": "^0.6.0"
64
+ },
65
+ "scripts": {
66
+ "build": "node scripts/build.mjs",
67
+ "typecheck": "tsc -p tsconfig.json --noEmit",
68
+ "lint": "eslint .",
69
+ "test": "node --test test/*.test.ts"
56
70
  },
57
71
  "main": "./dist/index.js",
58
72
  "types": "./dist/index.d.ts"
59
- }
73
+ }