tmux-ide 2.6.1 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -14
- package/bin/cli.js +4174 -1235
- package/bin/cli.ts +426 -72
- package/package.json +10 -6
- package/packages/contracts/src/__tests__/control.test.ts +154 -0
- package/packages/contracts/src/control.ts +217 -0
- package/packages/contracts/src/index.ts +1 -0
- package/packages/daemon/dist/agent-explain.d.ts +8 -1
- package/packages/daemon/dist/agent-explain.js +19 -3
- package/packages/daemon/dist/control/client.d.ts +23 -0
- package/packages/daemon/dist/control/client.js +105 -0
- package/packages/daemon/dist/control/dispatch.d.ts +34 -0
- package/packages/daemon/dist/control/dispatch.js +83 -0
- package/packages/daemon/dist/control/fanout.d.ts +19 -0
- package/packages/daemon/dist/control/fanout.js +37 -0
- package/packages/daemon/dist/control/frames.d.ts +23 -0
- package/packages/daemon/dist/control/frames.js +37 -0
- package/packages/daemon/dist/control/lifecycle.d.ts +45 -0
- package/packages/daemon/dist/control/lifecycle.js +114 -0
- package/packages/daemon/dist/control/server.d.ts +16 -0
- package/packages/daemon/dist/control/server.js +214 -0
- package/packages/daemon/dist/control/verbs.d.ts +11 -0
- package/packages/daemon/dist/control/verbs.js +91 -0
- package/packages/daemon/dist/doctor.d.ts +18 -0
- package/packages/daemon/dist/doctor.js +105 -15
- package/packages/daemon/dist/lib/agent-discovery.d.ts +27 -2
- package/packages/daemon/dist/lib/agent-discovery.js +29 -14
- package/packages/daemon/dist/lib/app-config.d.ts +106 -0
- package/packages/daemon/dist/lib/app-config.js +104 -5
- package/packages/daemon/dist/lib/manifest-pack.d.ts +79 -0
- package/packages/daemon/dist/lib/manifest-pack.js +232 -0
- package/packages/daemon/dist/lib/state-home.d.ts +2 -0
- package/packages/daemon/dist/lib/state-home.js +12 -0
- package/packages/daemon/dist/lib/tui-binary.d.ts +57 -0
- package/packages/daemon/dist/lib/tui-binary.js +130 -0
- package/packages/daemon/dist/lib/update-check.js +5 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +34 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/PkgInfo +1 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/AppIcon.icns +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +139 -0
- package/packages/daemon/dist/restore.d.ts +35 -8
- package/packages/daemon/dist/restore.js +52 -15
- package/packages/daemon/dist/send.d.ts +33 -1
- package/packages/daemon/dist/send.js +32 -19
- package/packages/daemon/dist/widgets/explorer/breadcrumbs.d.ts +1 -1
- package/packages/daemon/dist/widgets/explorer/footer.d.ts +1 -1
- package/packages/daemon/dist/widgets/explorer/tree.d.ts +1 -1
- package/packages/daemon/dist/widgets/lib/help-overlay.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/agent-naming.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/config-tree.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/detect-panel.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/field-editor.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/footer.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/layout-picker.d.ts +1 -1
- package/packages/daemon/src/agent-explain.ts +34 -6
- package/packages/daemon/src/control/client.ts +128 -0
- package/packages/daemon/src/control/dispatch.ts +107 -0
- package/packages/daemon/src/control/fanout.ts +44 -0
- package/packages/daemon/src/control/frames.ts +40 -0
- package/packages/daemon/src/control/lifecycle.ts +151 -0
- package/packages/daemon/src/control/server.ts +237 -0
- package/packages/daemon/src/control/verbs.ts +118 -0
- package/packages/daemon/src/doctor.ts +113 -28
- package/packages/daemon/src/lib/agent-discovery.ts +53 -13
- package/packages/daemon/src/lib/app-config.ts +194 -5
- package/packages/daemon/src/lib/manifest-pack.ts +255 -0
- package/packages/daemon/src/lib/state-home.ts +13 -0
- package/packages/daemon/src/lib/tui-binary.ts +165 -0
- package/packages/daemon/src/lib/update-check.ts +5 -0
- package/packages/daemon/src/restore.ts +53 -15
- package/packages/daemon/src/send.ts +55 -21
- package/packages/daemon/src/tui/chrome/events.ts +4 -4
- package/packages/daemon/src/tui/chrome/front-door.ts +39 -0
- package/packages/daemon/src/tui/chrome/notify-prefs.ts +58 -0
- package/packages/daemon/src/tui/chrome/notify-state.ts +76 -0
- package/packages/daemon/src/tui/chrome/notify.ts +737 -52
- package/packages/daemon/src/tui/chrome/updater.ts +318 -30
- package/packages/daemon/src/tui/compiled.ts +11 -3
- package/packages/daemon/src/tui/detect/classify.ts +49 -0
- package/packages/daemon/src/tui/detect/manifest-loader.ts +56 -6
- package/packages/daemon/src/tui/detect/manifest.ts +39 -3
- package/packages/daemon/src/tui/detect/manifests.ts +395 -33
- package/packages/daemon/src/tui/detect/process-tree.ts +33 -3
- package/packages/daemon/src/tui/detect/session-id.ts +503 -0
- package/packages/daemon/src/tui/integrations/opencode.ts +121 -0
- package/packages/daemon/src/tui/main.ts +13 -1
- package/packages/daemon/src/tui/mirror/ack-writer.ts +77 -0
- package/packages/daemon/src/tui/mirror/agent-chip.ts +126 -0
- package/packages/daemon/src/tui/mirror/agent-lifecycle.ts +437 -0
- package/packages/daemon/src/tui/mirror/agent-rows.ts +155 -0
- package/packages/daemon/src/tui/mirror/app-state.ts +342 -0
- package/packages/daemon/src/tui/mirror/app.tsx +7048 -0
- package/packages/daemon/src/tui/mirror/attention.ts +110 -0
- package/packages/daemon/src/tui/mirror/blit.ts +186 -0
- package/packages/daemon/src/tui/mirror/control-client.ts +80 -9
- package/packages/daemon/src/tui/mirror/dialog-model.ts +298 -0
- package/packages/daemon/src/tui/mirror/dialog-stack.ts +367 -0
- package/packages/daemon/src/tui/mirror/diff-model.ts +387 -0
- package/packages/daemon/src/tui/mirror/editor-buffer.ts +117 -0
- package/packages/daemon/src/tui/mirror/file-tree.ts +322 -0
- package/packages/daemon/src/tui/mirror/focus-border.ts +57 -0
- package/packages/daemon/src/tui/mirror/folder-picker.ts +124 -0
- package/packages/daemon/src/tui/mirror/home-model.ts +174 -0
- package/packages/daemon/src/tui/mirror/host-terminal.ts +49 -0
- package/packages/daemon/src/tui/mirror/hosted.ts +205 -0
- package/packages/daemon/src/tui/mirror/input-coalescer.ts +105 -0
- package/packages/daemon/src/tui/mirror/layout-parse.ts +154 -0
- package/packages/daemon/src/tui/mirror/menu-model.ts +210 -0
- package/packages/daemon/src/tui/mirror/palette.ts +564 -0
- package/packages/daemon/src/tui/mirror/pane-mirror.ts +578 -20
- package/packages/daemon/src/tui/mirror/pane-surface.tsx +422 -0
- package/packages/daemon/src/tui/mirror/perf-tap.ts +186 -0
- package/packages/daemon/src/tui/mirror/resize-model.ts +85 -0
- package/packages/daemon/src/tui/mirror/scrollbar-model.ts +88 -0
- package/packages/daemon/src/tui/mirror/search-model.ts +70 -0
- package/packages/daemon/src/tui/mirror/selection.ts +376 -0
- package/packages/daemon/src/tui/mirror/session-mirror.ts +724 -0
- package/packages/daemon/src/tui/mirror/settings-model.ts +425 -0
- package/packages/daemon/src/tui/mirror/sidebar.tsx +218 -0
- package/packages/daemon/src/tui/mirror/size-truth.ts +130 -0
- package/packages/daemon/src/tui/mirror/spans.ts +46 -0
- package/packages/daemon/src/tui/mirror/status-grammar.ts +32 -0
- package/packages/daemon/src/tui/mirror/theme.ts +45 -0
- package/packages/daemon/src/tui/team/entry.ts +34 -7
- package/packages/daemon/src/tui/team/fuzzy.ts +20 -0
- package/packages/daemon/src/tui/team/report.ts +11 -1
- package/packages/daemon/src/tui/team/sessions.ts +184 -17
- package/packages/daemon/src/tui/team/wait.ts +144 -0
- package/scripts/build-macos-notifier.mjs +160 -0
- package/scripts/build-tui.mjs +11 -4
- package/scripts/perf-mirror.mjs +313 -0
- package/scripts/postinstall.js +8 -1
- package/scripts/prepublish-check.mjs +37 -1
- package/scripts/publish-tap.sh +55 -0
- package/skill/SKILL.md +110 -2
- package/templates/AGENTS.md +14 -7
- package/templates/agent-team-monorepo.yml +8 -0
- package/templates/agent-team-nextjs.yml +8 -0
- package/templates/agent-team.yml +10 -0
- package/templates/convex.yml +2 -0
- package/templates/default.yml +11 -5
- package/templates/go.yml +4 -0
- package/templates/missions.yml +6 -0
- package/templates/nextjs.yml +4 -0
- package/templates/python.yml +4 -0
- package/templates/skills/backend.md +5 -12
- package/templates/skills/frontend.md +5 -12
- package/templates/skills/general-worker.md +5 -12
- package/templates/skills/researcher.md +7 -12
- package/templates/skills/reviewer.md +7 -16
- package/templates/vite.yml +4 -0
- package/packages/daemon/src/tui/mirror/viewer.tsx +0 -166
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
* per-concern readers ({@link ../tui/chrome/notify.ts}, {@link ../restore.ts})
|
|
21
21
|
* now delegate here.
|
|
22
22
|
*/
|
|
23
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
23
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
24
24
|
import { homedir } from "node:os";
|
|
25
|
-
import { join } from "node:path";
|
|
25
|
+
import { dirname, join } from "node:path";
|
|
26
26
|
// ---------------------------------------------------------------------------
|
|
27
27
|
// Defaults
|
|
28
28
|
// ---------------------------------------------------------------------------
|
|
@@ -50,12 +50,19 @@ export const DEFAULT_APP_CONFIG = {
|
|
|
50
50
|
glyphs: { active: "●", inactive: "○" },
|
|
51
51
|
},
|
|
52
52
|
updater: { tickMs: 2000, snapshotEvery: 15 },
|
|
53
|
-
notifications: { toast: true, macos: false },
|
|
53
|
+
notifications: { toast: true, macos: false, terminal: true, delaySeconds: 2, sound: "blocked" },
|
|
54
54
|
restore: { resumeAgents: false },
|
|
55
|
-
updates: { check: true },
|
|
55
|
+
updates: { check: true, manifests: false },
|
|
56
56
|
welcome: { show: true },
|
|
57
57
|
integrations: { offer: true },
|
|
58
58
|
worktrees: { dir: "" },
|
|
59
|
+
app: {
|
|
60
|
+
frontDoor: false,
|
|
61
|
+
detachable: false,
|
|
62
|
+
dragSelect: "agents",
|
|
63
|
+
newAgentCwd: "pane",
|
|
64
|
+
kittyKeys: true,
|
|
65
|
+
},
|
|
59
66
|
};
|
|
60
67
|
/** The default theme tokens — the fallback threaded into the pure builders. */
|
|
61
68
|
export const DEFAULT_THEME = DEFAULT_APP_CONFIG.theme;
|
|
@@ -82,6 +89,16 @@ function pickBool(value, fallback) {
|
|
|
82
89
|
function pickPosInt(value, fallback) {
|
|
83
90
|
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : fallback;
|
|
84
91
|
}
|
|
92
|
+
/** A non-negative integer (0 allowed — e.g. "no delay"), else the default. */
|
|
93
|
+
function pickNonNegInt(value, fallback) {
|
|
94
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : fallback;
|
|
95
|
+
}
|
|
96
|
+
/** One of the allowed literals, else the default. */
|
|
97
|
+
function pickChoice(value, allowed, fallback) {
|
|
98
|
+
return typeof value === "string" && allowed.includes(value)
|
|
99
|
+
? value
|
|
100
|
+
: fallback;
|
|
101
|
+
}
|
|
85
102
|
/**
|
|
86
103
|
* PURE — merge an unknown (typically parsed JSON) over {@link DEFAULT_APP_CONFIG}.
|
|
87
104
|
* Every block/leaf is validated independently: a missing or mistyped field falls
|
|
@@ -103,6 +120,7 @@ export function parseAppConfig(input) {
|
|
|
103
120
|
const welcome = asObject(root.welcome);
|
|
104
121
|
const integrations = asObject(root.integrations);
|
|
105
122
|
const worktrees = asObject(root.worktrees);
|
|
123
|
+
const app = asObject(root.app);
|
|
106
124
|
return {
|
|
107
125
|
keys: {
|
|
108
126
|
popup: pickString(keys.popup, D.keys.popup),
|
|
@@ -139,12 +157,25 @@ export function parseAppConfig(input) {
|
|
|
139
157
|
notifications: {
|
|
140
158
|
toast: pickBool(notifications.toast, D.notifications.toast),
|
|
141
159
|
macos: pickBool(notifications.macos, D.notifications.macos),
|
|
160
|
+
terminal: pickBool(notifications.terminal, D.notifications.terminal),
|
|
161
|
+
delaySeconds: pickNonNegInt(notifications.delaySeconds, D.notifications.delaySeconds),
|
|
162
|
+
sound: pickChoice(notifications.sound, ["blocked", "all", "none"], D.notifications.sound),
|
|
142
163
|
},
|
|
143
164
|
restore: { resumeAgents: pickBool(restore.resumeAgents, D.restore.resumeAgents) },
|
|
144
|
-
updates: {
|
|
165
|
+
updates: {
|
|
166
|
+
check: pickBool(updates.check, D.updates.check),
|
|
167
|
+
manifests: pickBool(updates.manifests, D.updates.manifests),
|
|
168
|
+
},
|
|
145
169
|
welcome: { show: pickBool(welcome.show, D.welcome.show) },
|
|
146
170
|
integrations: { offer: pickBool(integrations.offer, D.integrations.offer) },
|
|
147
171
|
worktrees: { dir: pickString(worktrees.dir, D.worktrees.dir) },
|
|
172
|
+
app: {
|
|
173
|
+
frontDoor: pickBool(app.frontDoor, D.app.frontDoor),
|
|
174
|
+
detachable: pickBool(app.detachable, D.app.detachable),
|
|
175
|
+
dragSelect: pickChoice(app.dragSelect, ["agents", "always", "never"], D.app.dragSelect),
|
|
176
|
+
newAgentCwd: pickChoice(app.newAgentCwd, ["pane", "session"], D.app.newAgentCwd),
|
|
177
|
+
kittyKeys: pickBool(app.kittyKeys, D.app.kittyKeys),
|
|
178
|
+
},
|
|
148
179
|
};
|
|
149
180
|
}
|
|
150
181
|
// ---------------------------------------------------------------------------
|
|
@@ -189,3 +220,71 @@ export function getAppConfig() {
|
|
|
189
220
|
export function _resetForTests() {
|
|
190
221
|
cached = null;
|
|
191
222
|
}
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// Writes (M22.4 — the settings surface persists through here)
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
/**
|
|
227
|
+
* io — the RAW parsed config file as the user wrote it (`{}` when missing or
|
|
228
|
+
* malformed). Writers merge over THIS, not over {@link parseAppConfig}'s output:
|
|
229
|
+
* round-tripping through the parser would materialize every default into the
|
|
230
|
+
* hand-editable file and silently DROP fields the typed shape doesn't model yet
|
|
231
|
+
* (e.g. the notification polish fields `notifications.enabled` /
|
|
232
|
+
* `notifications.quietHours` that {@link ../tui/chrome/notify.ts} reads raw).
|
|
233
|
+
*/
|
|
234
|
+
export function loadRawAppConfig() {
|
|
235
|
+
const path = appConfigPath();
|
|
236
|
+
if (!existsSync(path))
|
|
237
|
+
return {};
|
|
238
|
+
try {
|
|
239
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
240
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
241
|
+
? parsed
|
|
242
|
+
: {};
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return {};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function isPlainObject(value) {
|
|
249
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* PURE — merge `patch` into `raw` without touching either input. Objects merge
|
|
253
|
+
* key-by-key (recursively), anything else replaces, and a key whose patch value
|
|
254
|
+
* is `undefined` is REMOVED from the result. Unknown user fields survive — the
|
|
255
|
+
* merge only visits keys the patch names.
|
|
256
|
+
*/
|
|
257
|
+
export function mergeConfigPatch(raw, patch) {
|
|
258
|
+
const out = { ...raw };
|
|
259
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
260
|
+
if (value === undefined) {
|
|
261
|
+
delete out[key];
|
|
262
|
+
}
|
|
263
|
+
else if (isPlainObject(value) && isPlainObject(out[key])) {
|
|
264
|
+
out[key] = mergeConfigPatch(out[key], value);
|
|
265
|
+
}
|
|
266
|
+
else if (isPlainObject(value)) {
|
|
267
|
+
out[key] = mergeConfigPatch({}, value);
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
out[key] = value;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return out;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* io — apply a patch to the config file ATOMICALLY (temp + rename, the same
|
|
277
|
+
* discipline as the editor save): read raw → {@link mergeConfigPatch} → write.
|
|
278
|
+
* Busts the {@link getAppConfig} process cache and returns the new parsed
|
|
279
|
+
* config. Honors `TMUX_IDE_CONFIG`, so tests never touch the real file.
|
|
280
|
+
*/
|
|
281
|
+
export function updateAppConfig(patch) {
|
|
282
|
+
const path = appConfigPath();
|
|
283
|
+
const merged = mergeConfigPatch(loadRawAppConfig(), patch);
|
|
284
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
285
|
+
const tmp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
286
|
+
writeFileSync(tmp, `${JSON.stringify(merged, null, 2)}\n`, "utf-8");
|
|
287
|
+
renameSync(tmp, path);
|
|
288
|
+
cached = null;
|
|
289
|
+
return parseAppConfig(merged);
|
|
290
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { AgentManifest } from "../tui/detect/manifest.ts";
|
|
2
|
+
/** The pack-format version this build understands. */
|
|
3
|
+
export declare const MANIFEST_PACK_SCHEMA = 1;
|
|
4
|
+
/** The release asset name a pack is published under. */
|
|
5
|
+
export declare const MANIFEST_PACK_ASSET = "agent-manifests.json";
|
|
6
|
+
/** Env override for the pack URL (tests/dev; may be `file://` or loopback http). */
|
|
7
|
+
export declare const MANIFEST_PACK_URL_ENV = "TMUX_IDE_MANIFEST_PACK_URL";
|
|
8
|
+
/** A fetched/validated manifest pack. */
|
|
9
|
+
export interface ManifestPack {
|
|
10
|
+
schema: number;
|
|
11
|
+
/** Pack version — informational (surfaced in logs), any non-empty string. */
|
|
12
|
+
pack: string;
|
|
13
|
+
manifests: AgentManifest[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* PURE — the default pack URL for a version:
|
|
17
|
+
* `https://github.com/<repo>/releases/download/v<version>/agent-manifests.json`
|
|
18
|
+
* (the same asset-URL construction as the TUI binary download).
|
|
19
|
+
*/
|
|
20
|
+
export declare function manifestPackUrl(version?: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* PURE — is `url` an acceptable pack source? `https:` always; `file:` and
|
|
23
|
+
* LOOPBACK `http:` (127.0.0.1 / localhost / [::1]) only so tests and local
|
|
24
|
+
* fixtures can drive the flow. Any other scheme/host is rejected.
|
|
25
|
+
*/
|
|
26
|
+
export declare function isAllowedPackUrl(url: string): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* PURE — validate an untrusted pack document. Returns the typed pack or a
|
|
29
|
+
* human-readable reason. Every manifest entry must pass the SAME structural
|
|
30
|
+
* validation user override files do ({@link validateManifestShape}) — one bad
|
|
31
|
+
* entry rejects the whole pack (a partial install would be harder to reason
|
|
32
|
+
* about than a loud failure). Never throws.
|
|
33
|
+
*/
|
|
34
|
+
export declare function validateManifestPack(value: unknown): {
|
|
35
|
+
ok: true;
|
|
36
|
+
pack: ManifestPack;
|
|
37
|
+
} | {
|
|
38
|
+
ok: false;
|
|
39
|
+
reason: string;
|
|
40
|
+
};
|
|
41
|
+
/** The installed pack's directory: `<agent-detection>/pack/`. */
|
|
42
|
+
export declare function packDir(): string;
|
|
43
|
+
/** The installed pack's path: `<agent-detection>/pack/manifest-pack.json`. */
|
|
44
|
+
export declare function packPath(): string;
|
|
45
|
+
/**
|
|
46
|
+
* io — fetch and validate a pack. `file://` URLs are read from disk (Node's
|
|
47
|
+
* fetch does not speak file:); everything else goes through fetch with a
|
|
48
|
+
* timeout. THROWS with an actionable message on a disallowed URL, a failed
|
|
49
|
+
* fetch, malformed JSON, or a schema-invalid pack — the caller decides whether
|
|
50
|
+
* that is loud (CLI) or swallowed (the background refresh).
|
|
51
|
+
*/
|
|
52
|
+
export declare function fetchManifestPack(url: string, timeoutMs?: number): Promise<ManifestPack>;
|
|
53
|
+
/**
|
|
54
|
+
* io — write a validated pack into place atomically (temp-in-same-dir →
|
|
55
|
+
* rename, so a crash never leaves a half-written pack the loader would then
|
|
56
|
+
* warn about every run). Returns the installed path.
|
|
57
|
+
*/
|
|
58
|
+
export declare function installManifestPack(pack: ManifestPack, dest?: string): string;
|
|
59
|
+
/**
|
|
60
|
+
* io — the `tmux-ide update --manifests` flow: resolve the URL (env override →
|
|
61
|
+
* the release-asset default), fetch, validate, install. Throws loudly on any
|
|
62
|
+
* failure (the CLI surfaces it); returns what was installed for reporting.
|
|
63
|
+
*/
|
|
64
|
+
export declare function updateManifestPack(opts?: {
|
|
65
|
+
url?: string;
|
|
66
|
+
log?: (msg: string) => void;
|
|
67
|
+
}): Promise<{
|
|
68
|
+
path: string;
|
|
69
|
+
packVersion: string;
|
|
70
|
+
count: number;
|
|
71
|
+
}>;
|
|
72
|
+
/**
|
|
73
|
+
* io — the GATED background refresh the daily update check invokes (see
|
|
74
|
+
* `runUpdateCheck` in {@link ./update-check.ts}). Opt-in via
|
|
75
|
+
* `updates.manifests: true` (default false); NEVER throws — a failed refresh
|
|
76
|
+
* just keeps the previous pack, exactly like a failed version check keeps the
|
|
77
|
+
* previous `latest`.
|
|
78
|
+
*/
|
|
79
|
+
export declare function maybeRefreshManifestPack(): Promise<void>;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote agent-detection manifest packs — `tmux-ide update --manifests` (M25.4).
|
|
3
|
+
*
|
|
4
|
+
* Detection breadth shouldn't wait for an npm release: agent TUIs change their
|
|
5
|
+
* chrome between our versions, and a re-tuned manifest is pure data. This
|
|
6
|
+
* module fetches a versioned MANIFEST PACK from a static URL and installs it
|
|
7
|
+
* where the manifest loader already hot-merges from.
|
|
8
|
+
*
|
|
9
|
+
* ## The pack format (publish contract)
|
|
10
|
+
*
|
|
11
|
+
* A single JSON document:
|
|
12
|
+
*
|
|
13
|
+
* ```json
|
|
14
|
+
* {
|
|
15
|
+
* "schema": 1, // this format's version — reject others
|
|
16
|
+
* "pack": "2026.07.12", // pack version (informational, any string)
|
|
17
|
+
* "manifests": [ // one AgentManifest per entry, exactly the
|
|
18
|
+
* { // override-file shape manifest-loader.ts
|
|
19
|
+
* "id": "claude", // documents (id, commands[], states,
|
|
20
|
+
* "commands": ["claude"], // optional confidence)
|
|
21
|
+
* "confidence": "tuned",
|
|
22
|
+
* "states": { "working": { "any": [{ "contains": "esc to interrupt" }] } }
|
|
23
|
+
* }
|
|
24
|
+
* ]
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* Publishing: attach the document as the `agent-manifests.json` asset on a
|
|
29
|
+
* GitHub release — {@link manifestPackUrl} constructs the same
|
|
30
|
+
* `releases/download/v<version>/<asset>` URL the TUI-binary download uses, so
|
|
31
|
+
* a pack rides every release and a NEWER pack can be re-attached to the
|
|
32
|
+
* CURRENT release tag at any time (the fetch always re-downloads; the pane
|
|
33
|
+
* option cache never pins it).
|
|
34
|
+
*
|
|
35
|
+
* ## Precedence (verified by tests)
|
|
36
|
+
*
|
|
37
|
+
* The pack installs into `<agent-detection>/pack/manifest-pack.json` — a
|
|
38
|
+
* SUBDIRECTORY of the user-override dir, so `readOverrideManifests` (which
|
|
39
|
+
* lists only `*.json` FILES in the top level) never sees it. The loader merges
|
|
40
|
+
* `bundled → pack → user`: a pack manifest replaces a bundled id or appends a
|
|
41
|
+
* new one, and a user's own `<agent-detection>/*.json` file ALWAYS beats both.
|
|
42
|
+
*
|
|
43
|
+
* ## Safety
|
|
44
|
+
*
|
|
45
|
+
* - Https only for remote URLs. `file://` and loopback `http://` are accepted
|
|
46
|
+
* ONLY so tests/dev can exercise the flow without the network — anything
|
|
47
|
+
* else is rejected before a byte is fetched.
|
|
48
|
+
* - The pack is schema-validated ({@link validateManifestPack}) BEFORE being
|
|
49
|
+
* written; an invalid pack fails loudly and leaves the previous one intact.
|
|
50
|
+
* - The gated periodic refresh ({@link maybeRefreshManifestPack}) rides the
|
|
51
|
+
* existing daily update check (see {@link ./update-check.ts}) — no timers or
|
|
52
|
+
* daemons of its own — and is double-gated on `updates.check` (the caller)
|
|
53
|
+
* and `updates.manifests` (read here, default false).
|
|
54
|
+
*/
|
|
55
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
56
|
+
import { dirname, join } from "node:path";
|
|
57
|
+
import { fileURLToPath } from "node:url";
|
|
58
|
+
import { overrideDir, validateManifestShape } from "../tui/detect/manifest-loader.js";
|
|
59
|
+
import { getAppConfig } from "./app-config.js";
|
|
60
|
+
import { getCurrentVersion } from "./update-check.js";
|
|
61
|
+
import { RELEASE_REPO } from "./tui-binary.js";
|
|
62
|
+
/** The pack-format version this build understands. */
|
|
63
|
+
export const MANIFEST_PACK_SCHEMA = 1;
|
|
64
|
+
/** The release asset name a pack is published under. */
|
|
65
|
+
export const MANIFEST_PACK_ASSET = "agent-manifests.json";
|
|
66
|
+
/** Env override for the pack URL (tests/dev; may be `file://` or loopback http). */
|
|
67
|
+
export const MANIFEST_PACK_URL_ENV = "TMUX_IDE_MANIFEST_PACK_URL";
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Pure
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
/**
|
|
72
|
+
* PURE — the default pack URL for a version:
|
|
73
|
+
* `https://github.com/<repo>/releases/download/v<version>/agent-manifests.json`
|
|
74
|
+
* (the same asset-URL construction as the TUI binary download).
|
|
75
|
+
*/
|
|
76
|
+
export function manifestPackUrl(version = getCurrentVersion()) {
|
|
77
|
+
const v = version.startsWith("v") ? version.slice(1) : version;
|
|
78
|
+
return `https://github.com/${RELEASE_REPO}/releases/download/v${v}/${MANIFEST_PACK_ASSET}`;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* PURE — is `url` an acceptable pack source? `https:` always; `file:` and
|
|
82
|
+
* LOOPBACK `http:` (127.0.0.1 / localhost / [::1]) only so tests and local
|
|
83
|
+
* fixtures can drive the flow. Any other scheme/host is rejected.
|
|
84
|
+
*/
|
|
85
|
+
export function isAllowedPackUrl(url) {
|
|
86
|
+
let parsed;
|
|
87
|
+
try {
|
|
88
|
+
parsed = new URL(url);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
if (parsed.protocol === "https:" || parsed.protocol === "file:")
|
|
94
|
+
return true;
|
|
95
|
+
if (parsed.protocol === "http:") {
|
|
96
|
+
return ["127.0.0.1", "localhost", "[::1]", "::1"].includes(parsed.hostname);
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* PURE — validate an untrusted pack document. Returns the typed pack or a
|
|
102
|
+
* human-readable reason. Every manifest entry must pass the SAME structural
|
|
103
|
+
* validation user override files do ({@link validateManifestShape}) — one bad
|
|
104
|
+
* entry rejects the whole pack (a partial install would be harder to reason
|
|
105
|
+
* about than a loud failure). Never throws.
|
|
106
|
+
*/
|
|
107
|
+
export function validateManifestPack(value) {
|
|
108
|
+
if (typeof value !== "object" || value === null)
|
|
109
|
+
return { ok: false, reason: "not an object" };
|
|
110
|
+
const v = value;
|
|
111
|
+
if (v.schema !== MANIFEST_PACK_SCHEMA) {
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
reason: `unsupported schema ${JSON.stringify(v.schema)} (want ${MANIFEST_PACK_SCHEMA})`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (typeof v.pack !== "string" || v.pack.trim().length === 0) {
|
|
118
|
+
return { ok: false, reason: "missing pack version string" };
|
|
119
|
+
}
|
|
120
|
+
if (!Array.isArray(v.manifests) || v.manifests.length === 0) {
|
|
121
|
+
return { ok: false, reason: "manifests must be a non-empty array" };
|
|
122
|
+
}
|
|
123
|
+
for (let i = 0; i < v.manifests.length; i++) {
|
|
124
|
+
if (!validateManifestShape(v.manifests[i])) {
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
reason: `manifests[${i}] is not a valid AgentManifest (need id, commands[], states)`,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
ok: true,
|
|
133
|
+
pack: { schema: MANIFEST_PACK_SCHEMA, pack: v.pack, manifests: v.manifests },
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// io
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
/** The installed pack's directory: `<agent-detection>/pack/`. */
|
|
140
|
+
export function packDir() {
|
|
141
|
+
return join(overrideDir(), "pack");
|
|
142
|
+
}
|
|
143
|
+
/** The installed pack's path: `<agent-detection>/pack/manifest-pack.json`. */
|
|
144
|
+
export function packPath() {
|
|
145
|
+
return join(packDir(), "manifest-pack.json");
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* io — fetch and validate a pack. `file://` URLs are read from disk (Node's
|
|
149
|
+
* fetch does not speak file:); everything else goes through fetch with a
|
|
150
|
+
* timeout. THROWS with an actionable message on a disallowed URL, a failed
|
|
151
|
+
* fetch, malformed JSON, or a schema-invalid pack — the caller decides whether
|
|
152
|
+
* that is loud (CLI) or swallowed (the background refresh).
|
|
153
|
+
*/
|
|
154
|
+
export async function fetchManifestPack(url, timeoutMs = 5000) {
|
|
155
|
+
if (!isAllowedPackUrl(url)) {
|
|
156
|
+
throw new Error(`refusing manifest-pack URL ${url} — https only (file:// and loopback http are allowed for local testing)`);
|
|
157
|
+
}
|
|
158
|
+
let body;
|
|
159
|
+
if (url.startsWith("file:")) {
|
|
160
|
+
body = readFileSync(fileURLToPath(url), "utf8");
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
const controller = new AbortController();
|
|
164
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
165
|
+
try {
|
|
166
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
167
|
+
if (!res.ok) {
|
|
168
|
+
throw new Error(`manifest-pack download failed (${url} → HTTP ${res.status} ${res.statusText})`);
|
|
169
|
+
}
|
|
170
|
+
body = await res.text();
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
clearTimeout(timer);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
let parsed;
|
|
177
|
+
try {
|
|
178
|
+
parsed = JSON.parse(body);
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
throw new Error(`manifest pack at ${url} is not valid JSON`);
|
|
182
|
+
}
|
|
183
|
+
const verdict = validateManifestPack(parsed);
|
|
184
|
+
if (!verdict.ok) {
|
|
185
|
+
throw new Error(`manifest pack at ${url} rejected: ${verdict.reason}`);
|
|
186
|
+
}
|
|
187
|
+
return verdict.pack;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* io — write a validated pack into place atomically (temp-in-same-dir →
|
|
191
|
+
* rename, so a crash never leaves a half-written pack the loader would then
|
|
192
|
+
* warn about every run). Returns the installed path.
|
|
193
|
+
*/
|
|
194
|
+
export function installManifestPack(pack, dest = packPath()) {
|
|
195
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
196
|
+
const tmp = `${dest}.${process.pid}.tmp`;
|
|
197
|
+
writeFileSync(tmp, JSON.stringify(pack, null, 2));
|
|
198
|
+
renameSync(tmp, dest);
|
|
199
|
+
return dest;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* io — the `tmux-ide update --manifests` flow: resolve the URL (env override →
|
|
203
|
+
* the release-asset default), fetch, validate, install. Throws loudly on any
|
|
204
|
+
* failure (the CLI surfaces it); returns what was installed for reporting.
|
|
205
|
+
*/
|
|
206
|
+
export async function updateManifestPack(opts = {}) {
|
|
207
|
+
const log = opts.log ?? (() => { });
|
|
208
|
+
const url = opts.url ?? process.env[MANIFEST_PACK_URL_ENV] ?? manifestPackUrl();
|
|
209
|
+
log(`fetching manifest pack from ${url}`);
|
|
210
|
+
const pack = await fetchManifestPack(url);
|
|
211
|
+
const path = installManifestPack(pack);
|
|
212
|
+
log(`installed pack ${pack.pack} (${pack.manifests.length} manifests) → ${path}`);
|
|
213
|
+
return { path, packVersion: pack.pack, count: pack.manifests.length };
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* io — the GATED background refresh the daily update check invokes (see
|
|
217
|
+
* `runUpdateCheck` in {@link ./update-check.ts}). Opt-in via
|
|
218
|
+
* `updates.manifests: true` (default false); NEVER throws — a failed refresh
|
|
219
|
+
* just keeps the previous pack, exactly like a failed version check keeps the
|
|
220
|
+
* previous `latest`.
|
|
221
|
+
*/
|
|
222
|
+
export async function maybeRefreshManifestPack() {
|
|
223
|
+
try {
|
|
224
|
+
if (!getAppConfig().updates.manifests)
|
|
225
|
+
return;
|
|
226
|
+
await updateManifestPack();
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// Offline, a missing release asset, or an invalid pack — all degrade to
|
|
230
|
+
// "keep what we have"; the explicit CLI path reports errors loudly instead.
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE tmux-ide state home — `TMUX_IDE_HOME` when set (tests / per-run
|
|
3
|
+
* overrides), else `~/.tmux-ide`. The same resolution the welcome marker,
|
|
4
|
+
* update cache, and app state each grew locally; new state files should
|
|
5
|
+
* resolve through here so none of them can drift from the override again.
|
|
6
|
+
*/
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
/** Absolute path to the state home directory (not created here). */
|
|
10
|
+
export function stateHome() {
|
|
11
|
+
return process.env.TMUX_IDE_HOME ?? join(homedir(), ".tmux-ide");
|
|
12
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** The `<os>-<arch>` tags we publish a prebuilt TUI binary for. */
|
|
2
|
+
export type TuiPlatformTag = "darwin-arm64" | "darwin-x64" | "linux-x64" | "linux-arm64";
|
|
3
|
+
/** The GitHub repo the release assets live under. */
|
|
4
|
+
export declare const RELEASE_REPO = "wavyrai/tmux-ide";
|
|
5
|
+
/** A downloaded binary smaller than this is treated as corrupt/truncated. */
|
|
6
|
+
export declare const MIN_TUI_BINARY_BYTES: number;
|
|
7
|
+
/**
|
|
8
|
+
* PURE — map a Node `process.platform`/`process.arch` pair to the release tag,
|
|
9
|
+
* or null when we don't publish a binary for it (e.g. windows, freebsd) so the
|
|
10
|
+
* caller can fall back to the "install bun" message.
|
|
11
|
+
*/
|
|
12
|
+
export declare function tuiPlatformTag(platform?: NodeJS.Platform, arch?: string): TuiPlatformTag | null;
|
|
13
|
+
/** PURE — the `bun build --compile --target` flag for a tag (`bun-<tag>`). */
|
|
14
|
+
export declare function bunTargetForTag(tag: TuiPlatformTag): string;
|
|
15
|
+
/** PURE — the release asset filename for a tag (gzip-compressed binary). */
|
|
16
|
+
export declare function releaseAssetName(tag: TuiPlatformTag): string;
|
|
17
|
+
/** PURE — strip a leading `v` from a version string; `2.6.1` and `v2.6.1` both → `2.6.1`. */
|
|
18
|
+
export declare function normalizeVersion(version: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* PURE — the GitHub download URL for a platform's asset at a given version:
|
|
21
|
+
* `https://github.com/<repo>/releases/download/v<version>/tmux-ide-tui-<tag>.gz`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function releaseAssetUrl(version: string, tag: TuiPlatformTag): string;
|
|
24
|
+
/**
|
|
25
|
+
* PURE — where a downloaded binary lives: `<home>/bin/tmux-ide-tui-<tag>-<version>`.
|
|
26
|
+
* The version is stamped INTO the name so a `tmux-ide update` to a new version
|
|
27
|
+
* misses the old download and re-fetches (rather than launching a stale binary).
|
|
28
|
+
*/
|
|
29
|
+
export declare function downloadedTuiPath(home: string, tag: TuiPlatformTag, version: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* io — the tmux-ide state home (`TMUX_IDE_HOME` override, else `~/.tmux-ide`),
|
|
32
|
+
* the same resolution the update-check cache and welcome marker use.
|
|
33
|
+
*/
|
|
34
|
+
export declare function tuiStateHome(): string;
|
|
35
|
+
/**
|
|
36
|
+
* io — locate a previously downloaded per-platform binary for THIS version and
|
|
37
|
+
* platform, or null. Feeds the resolution order in `tui/compiled.ts` after the
|
|
38
|
+
* shipped/local compiled binary and before the honest "unavailable" error.
|
|
39
|
+
*/
|
|
40
|
+
export declare function findDownloadedTui(version?: string): string | null;
|
|
41
|
+
/**
|
|
42
|
+
* io — download, verify, and install the per-platform TUI binary for the running
|
|
43
|
+
* version. Fetches the gzip asset, inflates it, rejects anything under
|
|
44
|
+
* {@link MIN_TUI_BINARY_BYTES} (a truncated download or an HTML error page),
|
|
45
|
+
* writes it `0o755`, and atomically renames it into place (temp-in-same-dir →
|
|
46
|
+
* rename, so a crashed download never leaves a half-written executable).
|
|
47
|
+
*
|
|
48
|
+
* Throws with an actionable message on an unsupported platform or a failed
|
|
49
|
+
* fetch. Returns the installed path and byte size.
|
|
50
|
+
*/
|
|
51
|
+
export declare function downloadTuiBinary(opts?: {
|
|
52
|
+
version?: string;
|
|
53
|
+
log?: (msg: string) => void;
|
|
54
|
+
}): Promise<{
|
|
55
|
+
path: string;
|
|
56
|
+
bytes: number;
|
|
57
|
+
}>;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-platform TUI binary: the runtime-download fallback that lets a clean
|
|
3
|
+
* `npm i -g tmux-ide` run the full OpenTUI/Solid cockpit WITHOUT `bun`.
|
|
4
|
+
*
|
|
5
|
+
* The dev checkout runs the `.tsx` surfaces via bun; an npm install with no bun
|
|
6
|
+
* needs a self-contained binary. We do NOT bundle that ~70MB blob in the npm
|
|
7
|
+
* tarball (a surprise on every install) — instead the release workflow
|
|
8
|
+
* (`.github/workflows/release-binaries.yml`) cross-compiles one per platform and
|
|
9
|
+
* uploads them as GitHub release assets, and this module downloads the right one
|
|
10
|
+
* on demand (explicit `tmux-ide update --tui-binary`, or a consented first run).
|
|
11
|
+
*
|
|
12
|
+
* The mapping/URL/path helpers are PURE (unit-tested); {@link downloadTuiBinary}
|
|
13
|
+
* and {@link findDownloadedTui} are the thin io that fetch and probe.
|
|
14
|
+
*/
|
|
15
|
+
import { chmodSync, existsSync, mkdirSync, renameSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import { gunzipSync } from "node:zlib";
|
|
19
|
+
import { getCurrentVersion } from "./update-check.js";
|
|
20
|
+
/** The GitHub repo the release assets live under. */
|
|
21
|
+
export const RELEASE_REPO = "wavyrai/tmux-ide";
|
|
22
|
+
/** A downloaded binary smaller than this is treated as corrupt/truncated. */
|
|
23
|
+
export const MIN_TUI_BINARY_BYTES = 10 * 1024 * 1024;
|
|
24
|
+
const SUPPORTED = {
|
|
25
|
+
"darwin-arm64": "darwin-arm64",
|
|
26
|
+
"darwin-x64": "darwin-x64",
|
|
27
|
+
"linux-x64": "linux-x64",
|
|
28
|
+
"linux-arm64": "linux-arm64",
|
|
29
|
+
};
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Pure
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
/**
|
|
34
|
+
* PURE — map a Node `process.platform`/`process.arch` pair to the release tag,
|
|
35
|
+
* or null when we don't publish a binary for it (e.g. windows, freebsd) so the
|
|
36
|
+
* caller can fall back to the "install bun" message.
|
|
37
|
+
*/
|
|
38
|
+
export function tuiPlatformTag(platform = process.platform, arch = process.arch) {
|
|
39
|
+
return SUPPORTED[`${platform}-${arch}`] ?? null;
|
|
40
|
+
}
|
|
41
|
+
/** PURE — the `bun build --compile --target` flag for a tag (`bun-<tag>`). */
|
|
42
|
+
export function bunTargetForTag(tag) {
|
|
43
|
+
return `bun-${tag}`;
|
|
44
|
+
}
|
|
45
|
+
/** PURE — the release asset filename for a tag (gzip-compressed binary). */
|
|
46
|
+
export function releaseAssetName(tag) {
|
|
47
|
+
return `tmux-ide-tui-${tag}.gz`;
|
|
48
|
+
}
|
|
49
|
+
/** PURE — strip a leading `v` from a version string; `2.6.1` and `v2.6.1` both → `2.6.1`. */
|
|
50
|
+
export function normalizeVersion(version) {
|
|
51
|
+
return version.startsWith("v") ? version.slice(1) : version;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* PURE — the GitHub download URL for a platform's asset at a given version:
|
|
55
|
+
* `https://github.com/<repo>/releases/download/v<version>/tmux-ide-tui-<tag>.gz`.
|
|
56
|
+
*/
|
|
57
|
+
export function releaseAssetUrl(version, tag) {
|
|
58
|
+
return `https://github.com/${RELEASE_REPO}/releases/download/v${normalizeVersion(version)}/${releaseAssetName(tag)}`;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* PURE — where a downloaded binary lives: `<home>/bin/tmux-ide-tui-<tag>-<version>`.
|
|
62
|
+
* The version is stamped INTO the name so a `tmux-ide update` to a new version
|
|
63
|
+
* misses the old download and re-fetches (rather than launching a stale binary).
|
|
64
|
+
*/
|
|
65
|
+
export function downloadedTuiPath(home, tag, version) {
|
|
66
|
+
return join(home, "bin", `tmux-ide-tui-${tag}-${normalizeVersion(version)}`);
|
|
67
|
+
}
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// io
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
/**
|
|
72
|
+
* io — the tmux-ide state home (`TMUX_IDE_HOME` override, else `~/.tmux-ide`),
|
|
73
|
+
* the same resolution the update-check cache and welcome marker use.
|
|
74
|
+
*/
|
|
75
|
+
export function tuiStateHome() {
|
|
76
|
+
return process.env.TMUX_IDE_HOME ?? join(homedir(), ".tmux-ide");
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* io — locate a previously downloaded per-platform binary for THIS version and
|
|
80
|
+
* platform, or null. Feeds the resolution order in `tui/compiled.ts` after the
|
|
81
|
+
* shipped/local compiled binary and before the honest "unavailable" error.
|
|
82
|
+
*/
|
|
83
|
+
export function findDownloadedTui(version = getCurrentVersion()) {
|
|
84
|
+
const tag = tuiPlatformTag();
|
|
85
|
+
if (!tag)
|
|
86
|
+
return null;
|
|
87
|
+
const path = downloadedTuiPath(tuiStateHome(), tag, version);
|
|
88
|
+
return existsSync(path) ? path : null;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* io — download, verify, and install the per-platform TUI binary for the running
|
|
92
|
+
* version. Fetches the gzip asset, inflates it, rejects anything under
|
|
93
|
+
* {@link MIN_TUI_BINARY_BYTES} (a truncated download or an HTML error page),
|
|
94
|
+
* writes it `0o755`, and atomically renames it into place (temp-in-same-dir →
|
|
95
|
+
* rename, so a crashed download never leaves a half-written executable).
|
|
96
|
+
*
|
|
97
|
+
* Throws with an actionable message on an unsupported platform or a failed
|
|
98
|
+
* fetch. Returns the installed path and byte size.
|
|
99
|
+
*/
|
|
100
|
+
export async function downloadTuiBinary(opts = {}) {
|
|
101
|
+
const log = opts.log ?? (() => { });
|
|
102
|
+
const version = normalizeVersion(opts.version ?? getCurrentVersion());
|
|
103
|
+
const tag = tuiPlatformTag();
|
|
104
|
+
if (!tag) {
|
|
105
|
+
throw new Error(`no prebuilt TUI binary is published for ${process.platform}-${process.arch} — ` +
|
|
106
|
+
`install bun (https://bun.sh) to run the TUI surfaces from source instead`);
|
|
107
|
+
}
|
|
108
|
+
const url = releaseAssetUrl(version, tag);
|
|
109
|
+
const dest = downloadedTuiPath(tuiStateHome(), tag, version);
|
|
110
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
111
|
+
log(`downloading ${url}`);
|
|
112
|
+
const res = await fetch(url);
|
|
113
|
+
if (!res.ok) {
|
|
114
|
+
throw new Error(`could not download the TUI binary (${url} → HTTP ${res.status} ${res.statusText}). ` +
|
|
115
|
+
`Check that release v${version} exists and published its assets.`);
|
|
116
|
+
}
|
|
117
|
+
const gz = Buffer.from(await res.arrayBuffer());
|
|
118
|
+
const bin = gunzipSync(gz);
|
|
119
|
+
if (bin.byteLength < MIN_TUI_BINARY_BYTES) {
|
|
120
|
+
throw new Error(`the downloaded TUI binary is only ${bin.byteLength} bytes (expected >10MB) — ` +
|
|
121
|
+
`treating it as corrupt and leaving the previous binary (if any) in place`);
|
|
122
|
+
}
|
|
123
|
+
const tmp = `${dest}.${process.pid}.tmp`;
|
|
124
|
+
writeFileSync(tmp, bin, { mode: 0o755 });
|
|
125
|
+
chmodSync(tmp, 0o755);
|
|
126
|
+
renameSync(tmp, dest);
|
|
127
|
+
const mb = (bin.byteLength / 1024 / 1024).toFixed(1);
|
|
128
|
+
log(`installed ${dest} (${mb} MB)`);
|
|
129
|
+
return { path: dest, bytes: bin.byteLength };
|
|
130
|
+
}
|