tmux-ide 2.6.0 → 2.7.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 (271) hide show
  1. package/README.md +14 -9
  2. package/bin/cli.js +1024 -519
  3. package/bin/cli.ts +63 -6
  4. package/bunfig.toml +4 -0
  5. package/package.json +18 -7
  6. package/packages/contracts/package.json +22 -0
  7. package/packages/contracts/src/__tests__/ide-config.test.ts +46 -0
  8. package/packages/contracts/src/__tests__/terminals.test.ts +87 -0
  9. package/packages/contracts/src/actions-contract.ts +310 -0
  10. package/packages/contracts/src/actions-errors.ts +41 -0
  11. package/packages/contracts/src/domain.ts +36 -0
  12. package/packages/contracts/src/ide-config.ts +170 -0
  13. package/packages/contracts/src/index.ts +24 -0
  14. package/packages/contracts/src/lib-internal/auth.ts +13 -0
  15. package/packages/contracts/src/lib-internal/hq.ts +38 -0
  16. package/packages/contracts/src/terminals.ts +116 -0
  17. package/packages/contracts/src/tmux.ts +60 -0
  18. package/packages/contracts/src/workspace.ts +67 -0
  19. package/packages/daemon/dist/agent-explain.d.ts +8 -1
  20. package/packages/daemon/dist/agent-explain.js +19 -3
  21. package/packages/daemon/dist/lib/tui-binary.d.ts +57 -0
  22. package/packages/daemon/dist/lib/tui-binary.js +130 -0
  23. package/packages/daemon/dist/widgets/explorer/breadcrumbs.d.ts +1 -1
  24. package/packages/daemon/dist/widgets/explorer/footer.d.ts +1 -1
  25. package/packages/daemon/dist/widgets/explorer/tree.d.ts +1 -1
  26. package/packages/daemon/dist/widgets/lib/help-overlay.d.ts +1 -1
  27. package/packages/daemon/dist/widgets/setup/agent-naming.d.ts +1 -1
  28. package/packages/daemon/dist/widgets/setup/config-tree.d.ts +1 -1
  29. package/packages/daemon/dist/widgets/setup/detect-panel.d.ts +1 -1
  30. package/packages/daemon/dist/widgets/setup/field-editor.d.ts +1 -1
  31. package/packages/daemon/dist/widgets/setup/footer.d.ts +1 -1
  32. package/packages/daemon/dist/widgets/setup/layout-picker.d.ts +1 -1
  33. package/packages/daemon/src/agent-explain.ts +298 -0
  34. package/packages/daemon/src/attach.ts +20 -0
  35. package/packages/daemon/src/bin.ts +4 -0
  36. package/packages/daemon/src/canonical.ts +7 -0
  37. package/packages/daemon/src/cli.ts +499 -0
  38. package/packages/daemon/src/command-center/actions/contract.ts +2 -0
  39. package/packages/daemon/src/command-center/actions/dispatcher.ts +137 -0
  40. package/packages/daemon/src/command-center/actions/errors.ts +105 -0
  41. package/packages/daemon/src/command-center/actions/handlers/_project-context.ts +30 -0
  42. package/packages/daemon/src/command-center/actions/handlers/_resolve-project.ts +78 -0
  43. package/packages/daemon/src/command-center/actions/handlers/app-set-remote-access.ts +118 -0
  44. package/packages/daemon/src/command-center/actions/handlers/config-actions.ts +113 -0
  45. package/packages/daemon/src/command-center/actions/handlers/daemon-shutdown.ts +38 -0
  46. package/packages/daemon/src/command-center/actions/handlers/project-activate.ts +30 -0
  47. package/packages/daemon/src/command-center/actions/handlers/project-launch.ts +70 -0
  48. package/packages/daemon/src/command-center/actions/handlers/project-open-terminal.ts +87 -0
  49. package/packages/daemon/src/command-center/actions/handlers/project-restart.ts +38 -0
  50. package/packages/daemon/src/command-center/actions/handlers/project-stop.ts +62 -0
  51. package/packages/daemon/src/command-center/actions/handlers/terminal-respawn.ts +119 -0
  52. package/packages/daemon/src/command-center/actions/handlers/terminal-stop.ts +35 -0
  53. package/packages/daemon/src/command-center/actions/registry.ts +149 -0
  54. package/packages/daemon/src/command-center/discovery.ts +96 -0
  55. package/packages/daemon/src/command-center/index.ts +31 -0
  56. package/packages/daemon/src/command-center/schemas.ts +85 -0
  57. package/packages/daemon/src/command-center/server.ts +1260 -0
  58. package/packages/daemon/src/command-center/ws-events.ts +316 -0
  59. package/packages/daemon/src/config.ts +549 -0
  60. package/packages/daemon/src/detect.ts +248 -0
  61. package/packages/daemon/src/doctor.ts +242 -0
  62. package/packages/daemon/src/embed.ts +5 -0
  63. package/packages/daemon/src/index.ts +12 -0
  64. package/packages/daemon/src/init.ts +211 -0
  65. package/packages/daemon/src/inspect.ts +178 -0
  66. package/packages/daemon/src/js-yaml.d.ts +10 -0
  67. package/packages/daemon/src/launch.ts +349 -0
  68. package/packages/daemon/src/lib/active-projects.ts +49 -0
  69. package/packages/daemon/src/lib/agent-discovery.ts +121 -0
  70. package/packages/daemon/src/lib/app-config.ts +427 -0
  71. package/packages/daemon/src/lib/app-settings.ts +53 -0
  72. package/packages/daemon/src/lib/auth/auth-service.ts +227 -0
  73. package/packages/daemon/src/lib/auth/middleware.ts +56 -0
  74. package/packages/daemon/src/lib/auth/types.ts +2 -0
  75. package/packages/daemon/src/lib/auth-token.ts +5 -0
  76. package/packages/daemon/src/lib/authorship.ts +280 -0
  77. package/packages/daemon/src/lib/canonical-daemon.ts +122 -0
  78. package/packages/daemon/src/lib/cli-action-bridge.ts +216 -0
  79. package/packages/daemon/src/lib/daemon-embed.ts +782 -0
  80. package/packages/daemon/src/lib/daemon-watchdog.ts +111 -0
  81. package/packages/daemon/src/lib/daemon.ts +79 -0
  82. package/packages/daemon/src/lib/dot-path.ts +17 -0
  83. package/packages/daemon/src/lib/errors.ts +67 -0
  84. package/packages/daemon/src/lib/filesystem-browser.ts +292 -0
  85. package/packages/daemon/src/lib/launch-plan.ts +90 -0
  86. package/packages/daemon/src/lib/log.ts +134 -0
  87. package/packages/daemon/src/lib/output.ts +76 -0
  88. package/packages/daemon/src/lib/project-init-runner.ts +150 -0
  89. package/packages/daemon/src/lib/project-inspect.ts +80 -0
  90. package/packages/daemon/src/lib/project-onboard.ts +149 -0
  91. package/packages/daemon/src/lib/project-probe.ts +92 -0
  92. package/packages/daemon/src/lib/project-registry.ts +296 -0
  93. package/packages/daemon/src/lib/session-monitor.ts +122 -0
  94. package/packages/daemon/src/lib/session-options.ts +100 -0
  95. package/packages/daemon/src/lib/shell.ts +8 -0
  96. package/packages/daemon/src/lib/sizes.ts +36 -0
  97. package/packages/daemon/src/lib/skill-sync.ts +155 -0
  98. package/packages/daemon/src/lib/slugify.ts +10 -0
  99. package/packages/daemon/src/lib/terminals-store.ts +125 -0
  100. package/packages/daemon/src/lib/tui-binary.ts +165 -0
  101. package/packages/daemon/src/lib/update-check.ts +298 -0
  102. package/packages/daemon/src/lib/update.ts +158 -0
  103. package/packages/daemon/src/lib/workspace-registry.ts +229 -0
  104. package/packages/daemon/src/lib/worktree.ts +289 -0
  105. package/packages/daemon/src/lib/yaml-io.ts +27 -0
  106. package/packages/daemon/src/ls.ts +40 -0
  107. package/packages/daemon/src/restart.ts +24 -0
  108. package/packages/daemon/src/restore.ts +514 -0
  109. package/packages/daemon/src/schemas/domain.ts +2 -0
  110. package/packages/daemon/src/schemas/filesystem.ts +34 -0
  111. package/packages/daemon/src/schemas/ide-config.ts +2 -0
  112. package/packages/daemon/src/schemas/index.ts +59 -0
  113. package/packages/daemon/src/schemas/inspect.ts +67 -0
  114. package/packages/daemon/src/schemas/registry.ts +55 -0
  115. package/packages/daemon/src/schemas/ws-events.ts +135 -0
  116. package/packages/daemon/src/send.ts +171 -0
  117. package/packages/daemon/src/server/README.md +15 -0
  118. package/packages/daemon/src/server/index.ts +74 -0
  119. package/packages/daemon/src/server/pty-bridge.ts +532 -0
  120. package/packages/daemon/src/server/standalone.ts +18 -0
  121. package/packages/daemon/src/server/ws-route.ts +483 -0
  122. package/packages/daemon/src/status.ts +59 -0
  123. package/packages/daemon/src/stop.ts +28 -0
  124. package/packages/daemon/src/terminal/NodePtyAdapter.ts +271 -0
  125. package/packages/daemon/src/terminal/PtyAdapter.ts +140 -0
  126. package/packages/daemon/src/terminal/README.md +92 -0
  127. package/packages/daemon/src/tui/chrome/cheatsheet.ts +260 -0
  128. package/packages/daemon/src/tui/chrome/chip.ts +30 -0
  129. package/packages/daemon/src/tui/chrome/events.ts +119 -0
  130. package/packages/daemon/src/tui/chrome/kitty-keys.ts +55 -0
  131. package/packages/daemon/src/tui/chrome/menu.ts +289 -0
  132. package/packages/daemon/src/tui/chrome/notify.ts +382 -0
  133. package/packages/daemon/src/tui/chrome/panels.ts +111 -0
  134. package/packages/daemon/src/tui/chrome/sidebar.ts +222 -0
  135. package/packages/daemon/src/tui/chrome/snapshot.ts +425 -0
  136. package/packages/daemon/src/tui/chrome/statusline.ts +595 -0
  137. package/packages/daemon/src/tui/chrome/updater.ts +510 -0
  138. package/packages/daemon/src/tui/chrome/welcome.ts +124 -0
  139. package/packages/daemon/src/tui/compiled.ts +121 -0
  140. package/packages/daemon/src/tui/detect/classify.ts +208 -0
  141. package/packages/daemon/src/tui/detect/manifest-loader.ts +193 -0
  142. package/packages/daemon/src/tui/detect/manifest.ts +199 -0
  143. package/packages/daemon/src/tui/detect/manifests.ts +354 -0
  144. package/packages/daemon/src/tui/detect/process-tree.ts +217 -0
  145. package/packages/daemon/src/tui/detect/snapshot.ts +70 -0
  146. package/packages/daemon/src/tui/integrations/claude.ts +176 -0
  147. package/packages/daemon/src/tui/integrations/offer.ts +145 -0
  148. package/packages/daemon/src/tui/main.ts +82 -0
  149. package/packages/daemon/src/tui/mirror/ack-writer.ts +77 -0
  150. package/packages/daemon/src/tui/mirror/agent-chip.ts +97 -0
  151. package/packages/daemon/src/tui/mirror/agent-rows.ts +133 -0
  152. package/packages/daemon/src/tui/mirror/app-state.ts +179 -0
  153. package/packages/daemon/src/tui/mirror/app.tsx +5265 -0
  154. package/packages/daemon/src/tui/mirror/blit.ts +186 -0
  155. package/packages/daemon/src/tui/mirror/control-client.ts +214 -0
  156. package/packages/daemon/src/tui/mirror/control.ts +97 -0
  157. package/packages/daemon/src/tui/mirror/dialog-model.ts +298 -0
  158. package/packages/daemon/src/tui/mirror/dialog-stack.ts +354 -0
  159. package/packages/daemon/src/tui/mirror/diff-model.ts +112 -0
  160. package/packages/daemon/src/tui/mirror/editor-buffer.ts +117 -0
  161. package/packages/daemon/src/tui/mirror/file-tree.ts +97 -0
  162. package/packages/daemon/src/tui/mirror/focus-border.ts +57 -0
  163. package/packages/daemon/src/tui/mirror/folder-picker.ts +124 -0
  164. package/packages/daemon/src/tui/mirror/home-model.ts +174 -0
  165. package/packages/daemon/src/tui/mirror/input-coalescer.ts +105 -0
  166. package/packages/daemon/src/tui/mirror/menu-model.ts +187 -0
  167. package/packages/daemon/src/tui/mirror/palette.ts +274 -0
  168. package/packages/daemon/src/tui/mirror/pane-mirror.ts +561 -0
  169. package/packages/daemon/src/tui/mirror/pane-surface.tsx +415 -0
  170. package/packages/daemon/src/tui/mirror/perf-tap.ts +160 -0
  171. package/packages/daemon/src/tui/mirror/resize-model.ts +85 -0
  172. package/packages/daemon/src/tui/mirror/scrollbar-model.ts +88 -0
  173. package/packages/daemon/src/tui/mirror/search-model.ts +70 -0
  174. package/packages/daemon/src/tui/mirror/selection.ts +262 -0
  175. package/packages/daemon/src/tui/mirror/session-mirror.ts +443 -0
  176. package/packages/daemon/src/tui/mirror/settings-model.ts +345 -0
  177. package/packages/daemon/src/tui/mirror/size-truth.ts +77 -0
  178. package/packages/daemon/src/tui/mirror/spans.ts +46 -0
  179. package/packages/daemon/src/tui/mirror/status-grammar.ts +32 -0
  180. package/packages/daemon/src/tui/team/CONTROL.md +50 -0
  181. package/packages/daemon/src/tui/team/entry.ts +38 -0
  182. package/packages/daemon/src/tui/team/fuzzy.ts +133 -0
  183. package/packages/daemon/src/tui/team/home.ts +170 -0
  184. package/packages/daemon/src/tui/team/index.tsx +1521 -0
  185. package/packages/daemon/src/tui/team/input.ts +34 -0
  186. package/packages/daemon/src/tui/team/keymap.ts +127 -0
  187. package/packages/daemon/src/tui/team/mouse.ts +29 -0
  188. package/packages/daemon/src/tui/team/nav.ts +31 -0
  189. package/packages/daemon/src/tui/team/preview.ts +34 -0
  190. package/packages/daemon/src/tui/team/projects.ts +191 -0
  191. package/packages/daemon/src/tui/team/report.ts +83 -0
  192. package/packages/daemon/src/tui/team/sessions.ts +433 -0
  193. package/packages/daemon/src/tui/team/tree.ts +62 -0
  194. package/packages/daemon/src/types.ts +13 -0
  195. package/packages/daemon/src/ui/index.ts +32 -0
  196. package/packages/daemon/src/ui/terminal/index.ts +9 -0
  197. package/packages/daemon/src/ui/types.ts +91 -0
  198. package/packages/daemon/src/ui/web/base.css +80 -0
  199. package/packages/daemon/src/ui/web/components/Box.tsx +59 -0
  200. package/packages/daemon/src/ui/web/components/Input.tsx +32 -0
  201. package/packages/daemon/src/ui/web/components/ScrollBox.tsx +60 -0
  202. package/packages/daemon/src/ui/web/components/Text.tsx +28 -0
  203. package/packages/daemon/src/ui/web/hooks.ts +106 -0
  204. package/packages/daemon/src/ui/web/index.ts +27 -0
  205. package/packages/daemon/src/ui/web/render.ts +77 -0
  206. package/packages/daemon/src/ui/web/utils/color.ts +27 -0
  207. package/packages/daemon/src/validate.ts +217 -0
  208. package/packages/daemon/src/widgets/changes/README.md +3 -0
  209. package/packages/daemon/src/widgets/changes/index.tsx +691 -0
  210. package/packages/daemon/src/widgets/config/README.md +3 -0
  211. package/packages/daemon/src/widgets/config/index.tsx +481 -0
  212. package/packages/daemon/src/widgets/explorer/README.md +3 -0
  213. package/packages/daemon/src/widgets/explorer/breadcrumbs.tsx +77 -0
  214. package/packages/daemon/src/widgets/explorer/footer.tsx +20 -0
  215. package/packages/daemon/src/widgets/explorer/header.tsx +23 -0
  216. package/packages/daemon/src/widgets/explorer/index.tsx +456 -0
  217. package/packages/daemon/src/widgets/explorer/tree-model.ts +103 -0
  218. package/packages/daemon/src/widgets/explorer/tree.tsx +165 -0
  219. package/packages/daemon/src/widgets/lib/config-model.ts +116 -0
  220. package/packages/daemon/src/widgets/lib/files.ts +88 -0
  221. package/packages/daemon/src/widgets/lib/git.ts +88 -0
  222. package/packages/daemon/src/widgets/lib/grammar.ts +126 -0
  223. package/packages/daemon/src/widgets/lib/help-overlay.tsx +101 -0
  224. package/packages/daemon/src/widgets/lib/pane-comms.ts +209 -0
  225. package/packages/daemon/src/widgets/lib/theme.ts +194 -0
  226. package/packages/daemon/src/widgets/lib/watcher.ts +132 -0
  227. package/packages/daemon/src/widgets/preview/README.md +3 -0
  228. package/packages/daemon/src/widgets/preview/index.tsx +416 -0
  229. package/packages/daemon/src/widgets/resolve.ts +121 -0
  230. package/packages/daemon/src/widgets/setup/README.md +3 -0
  231. package/packages/daemon/src/widgets/setup/agent-naming.tsx +112 -0
  232. package/packages/daemon/src/widgets/setup/config-tree.tsx +246 -0
  233. package/packages/daemon/src/widgets/setup/detect-panel.tsx +72 -0
  234. package/packages/daemon/src/widgets/setup/field-editor.tsx +265 -0
  235. package/packages/daemon/src/widgets/setup/footer.tsx +107 -0
  236. package/packages/daemon/src/widgets/setup/index.tsx +341 -0
  237. package/packages/daemon/src/widgets/setup/layout-picker.tsx +96 -0
  238. package/packages/daemon/src/widgets/setup/orchestrator-panel.tsx +200 -0
  239. package/packages/daemon/src/widgets/setup/review-panel.tsx +140 -0
  240. package/packages/daemon/src/widgets/setup/setup-model.ts +188 -0
  241. package/packages/daemon/src/widgets/sidebar/index.tsx +527 -0
  242. package/packages/tmux-bridge/package.json +22 -0
  243. package/packages/tmux-bridge/src/errors.ts +28 -0
  244. package/packages/tmux-bridge/src/index.ts +31 -0
  245. package/packages/tmux-bridge/src/monitor.ts +77 -0
  246. package/packages/tmux-bridge/src/panes.ts +136 -0
  247. package/packages/tmux-bridge/src/runner.test.ts +501 -0
  248. package/packages/tmux-bridge/src/runner.ts +91 -0
  249. package/packages/tmux-bridge/src/sessions.ts +126 -0
  250. package/packages/tmux-bridge/src/targeting.test.ts +107 -0
  251. package/packages/tmux-bridge/src/targeting.ts +90 -0
  252. package/scripts/build-tui.mjs +11 -4
  253. package/scripts/perf-mirror.mjs +313 -0
  254. package/scripts/postinstall.js +26 -2
  255. package/skill/SKILL.md +22 -0
  256. package/templates/AGENTS.md +14 -7
  257. package/templates/agent-team-monorepo.yml +8 -0
  258. package/templates/agent-team-nextjs.yml +8 -0
  259. package/templates/agent-team.yml +10 -0
  260. package/templates/convex.yml +2 -0
  261. package/templates/default.yml +11 -5
  262. package/templates/go.yml +4 -0
  263. package/templates/missions.yml +6 -0
  264. package/templates/nextjs.yml +4 -0
  265. package/templates/python.yml +4 -0
  266. package/templates/skills/backend.md +5 -12
  267. package/templates/skills/frontend.md +5 -12
  268. package/templates/skills/general-worker.md +5 -12
  269. package/templates/skills/researcher.md +7 -12
  270. package/templates/skills/reviewer.md +7 -16
  271. package/templates/vite.yml +4 -0
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Managed sync of the bundled Claude Code skill → `~/.claude/skills/tmux-ide`.
3
+ *
4
+ * `skill/SKILL.md` is the manual a coding agent loads to drive tmux-ide. It ships
5
+ * IN the package, but the copy that agents actually read lives in the user's
6
+ * `~/.claude/skills/` — so every install AND every update has to keep that copy
7
+ * fresh, or agents drive an old CLI. This module is that refresh: it copies the
8
+ * bundled SKILL.md into the tmux-ide skill dir with the version marker rewritten
9
+ * to the installed package version, and reports whether that was an install, an
10
+ * update, or a no-op.
11
+ *
12
+ * The dir is FULLY MANAGED: overwriting our own `~/.claude/skills/tmux-ide` is
13
+ * correct, and this NEVER touches anything outside it (user-authored skills in
14
+ * sibling dirs are safe). The version marker is line 2 of SKILL.md, an HTML
15
+ * comment ({@link VERSION_MARKER_RE}); {@link parseSkillVersion} reads it and
16
+ * {@link rewriteVersionMarker} substitutes it at copy time.
17
+ *
18
+ * The parse/render helpers are PURE and tested against fixtures; {@link syncSkill}
19
+ * is the thin io wrapper (tested with tmp dirs). Wired from three places — the
20
+ * npm `postinstall` (which re-implements the marker rewrite in plain JS since it
21
+ * can't import TS — see scripts/postinstall.js), `tmux-ide skill-sync` / the dev
22
+ * path of `tmux-ide update`, and `tmux-ide integration install claude`.
23
+ */
24
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
25
+ import { homedir } from "node:os";
26
+ import { dirname, join } from "node:path";
27
+ import { fileURLToPath } from "node:url";
28
+ import { getCurrentVersion } from "./update-check.ts";
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Paths
32
+ // ---------------------------------------------------------------------------
33
+
34
+ /**
35
+ * Claude Code's config dir: `TMUX_IDE_CLAUDE_DIR` when set (tests / per-run
36
+ * overrides), else `~/.claude`. Mirrors the {@link ../tui/integrations/claude.ts}
37
+ * settings override so both halves of the agent setup honor the same scratch dir.
38
+ */
39
+ export function claudeDir(): string {
40
+ return process.env.TMUX_IDE_CLAUDE_DIR ?? join(homedir(), ".claude");
41
+ }
42
+
43
+ /** The fully-managed tmux-ide skill dir: `<claudeDir>/skills/tmux-ide`. */
44
+ export function skillTargetDir(): string {
45
+ return join(claudeDir(), "skills", "tmux-ide");
46
+ }
47
+
48
+ /** The installed SKILL.md path (inside {@link skillTargetDir}). */
49
+ export function skillTargetFile(): string {
50
+ return join(skillTargetDir(), "SKILL.md");
51
+ }
52
+
53
+ /**
54
+ * io — the bundled SKILL.md that ships in the package. Resolved relative to this
55
+ * module, handling BOTH the esbuild-bundled CLI (this file inlines into
56
+ * `bin/cli.js`, so `here` is `bin/` and the skill is one level up) AND the dev
57
+ * source tree (`packages/daemon/src/lib/`, four levels below the repo root) — the
58
+ * same dual-candidate trick as {@link getCurrentVersion}. Returns the first that
59
+ * exists, else the bundled-layout guess.
60
+ */
61
+ export function defaultSkillSource(): string {
62
+ const here = dirname(fileURLToPath(import.meta.url));
63
+ const candidates = [
64
+ join(here, "../skill/SKILL.md"), // bundled bin/cli.js → repo root
65
+ join(here, "../../../../skill/SKILL.md"), // dev src/lib → repo root
66
+ ];
67
+ return candidates.find((c) => existsSync(c)) ?? candidates[0]!;
68
+ }
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Pure — version marker
72
+ // ---------------------------------------------------------------------------
73
+
74
+ /**
75
+ * The version marker line (an HTML comment on line 2 of SKILL.md). The sync
76
+ * machinery reads it to know which version an installed copy is, and rewrites it
77
+ * to the installed package version at copy time.
78
+ */
79
+ export const VERSION_MARKER_RE = /<!--\s*tmux-ide-skill-version:\s*([^\s]+)\s*-->/;
80
+
81
+ /** PURE — render the marker line for a given version. */
82
+ export function versionMarker(version: string): string {
83
+ return `<!-- tmux-ide-skill-version: ${version} -->`;
84
+ }
85
+
86
+ /** PURE — the version in a SKILL.md body, or null when the marker is absent. */
87
+ export function parseSkillVersion(content: string): string | null {
88
+ const match = content.match(VERSION_MARKER_RE);
89
+ return match ? match[1]! : null;
90
+ }
91
+
92
+ /**
93
+ * PURE — return `content` with its version marker rewritten to `version`. When no
94
+ * marker is present the content is returned unchanged (the bundled SKILL.md
95
+ * always carries one; this just never corrupts a hand-stripped file).
96
+ */
97
+ export function rewriteVersionMarker(content: string, version: string): string {
98
+ if (!VERSION_MARKER_RE.test(content)) return content;
99
+ return content.replace(VERSION_MARKER_RE, versionMarker(version));
100
+ }
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // io
104
+ // ---------------------------------------------------------------------------
105
+
106
+ /** The version marker read off an installed SKILL.md, or null if absent/unreadable. */
107
+ export function installedSkillVersion(dir: string = skillTargetDir()): string | null {
108
+ const file = join(dir, "SKILL.md");
109
+ if (!existsSync(file)) return null;
110
+ try {
111
+ return parseSkillVersion(readFileSync(file, "utf-8"));
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
116
+
117
+ /** What a {@link syncSkill} did. */
118
+ export interface SyncResult {
119
+ /** `installed` = target was absent; `updated` = content changed; `unchanged` = no-op. */
120
+ action: "installed" | "updated" | "unchanged";
121
+ /** The written SKILL.md path. */
122
+ path: string;
123
+ /** The installed version before an `updated` (the marker we replaced), if any. */
124
+ from?: string | null;
125
+ /** The version written into the copy's marker. */
126
+ to: string;
127
+ }
128
+
129
+ /**
130
+ * io — copy the bundled SKILL.md into the managed skill dir with its version
131
+ * marker rewritten to `version`. Content-driven and idempotent: the RENDERED
132
+ * source (marker substituted) is compared byte-for-byte against the installed
133
+ * copy — identical → `unchanged` (no write), absent → `installed`, otherwise
134
+ * `updated`. Only ever writes inside {@link skillTargetDir}; nothing outside it
135
+ * is read or touched.
136
+ */
137
+ export function syncSkill({
138
+ source = defaultSkillSource(),
139
+ version = getCurrentVersion(),
140
+ }: { source?: string; version?: string } = {}): SyncResult {
141
+ const rendered = rewriteVersionMarker(readFileSync(source, "utf-8"), version);
142
+ const dir = skillTargetDir();
143
+ const target = join(dir, "SKILL.md");
144
+
145
+ const existing = existsSync(target) ? readFileSync(target, "utf-8") : null;
146
+ if (existing === rendered) {
147
+ return { action: "unchanged", path: target, to: version };
148
+ }
149
+
150
+ mkdirSync(dir, { recursive: true });
151
+ writeFileSync(target, rendered, "utf-8");
152
+
153
+ if (existing === null) return { action: "installed", path: target, to: version };
154
+ return { action: "updated", path: target, from: parseSkillVersion(existing), to: version };
155
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Convert text to a URL/filename-safe slug.
3
+ */
4
+ export function slugify(text: string, maxLen = 40): string {
5
+ return text
6
+ .toLowerCase()
7
+ .replace(/[^a-z0-9]+/g, "-")
8
+ .replace(/^-|-$/g, "")
9
+ .slice(0, maxLen);
10
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Terminals registry (G20-P1).
3
+ *
4
+ * JSON-backed persistence for tab-strip metadata — names, scopes, and
5
+ * kind. The actual PTY process lives in `PtyBridgeRegistry`; this
6
+ * store only persists what the UI needs to restore tab labels across
7
+ * reloads. Atomic writes (temp + rename) so a crash mid-write leaves
8
+ * the previous list intact.
9
+ */
10
+
11
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
12
+ import { dirname, join } from "node:path";
13
+ import type { Terminal, TerminalKind } from "@tmux-ide/contracts";
14
+
15
+ const TERMINALS_FILE = ".tmux-ide/terminals.json";
16
+ const SAFE_ID = /^[A-Za-z0-9_-]+$/u;
17
+
18
+ function path(dir: string): string {
19
+ return join(dir, TERMINALS_FILE);
20
+ }
21
+
22
+ function ensureDir(dir: string): void {
23
+ mkdirSync(dirname(path(dir)), { recursive: true });
24
+ }
25
+
26
+ /** Load the full list for a session. Missing file → empty list. */
27
+ export function loadTerminals(dir: string): Terminal[] {
28
+ const file = path(dir);
29
+ if (!existsSync(file)) return [];
30
+ try {
31
+ const body = readFileSync(file, "utf-8");
32
+ const parsed = JSON.parse(body) as { terminals?: unknown };
33
+ if (!parsed.terminals || !Array.isArray(parsed.terminals)) return [];
34
+ return parsed.terminals.filter((t): t is Terminal => isTerminal(t)).map((t) => ({ ...t }));
35
+ } catch {
36
+ return [];
37
+ }
38
+ }
39
+
40
+ function isTerminal(value: unknown): value is Terminal {
41
+ if (!value || typeof value !== "object") return false;
42
+ const v = value as Record<string, unknown>;
43
+ return (
44
+ typeof v.id === "string" &&
45
+ SAFE_ID.test(v.id) &&
46
+ typeof v.projectId === "string" &&
47
+ typeof v.scopeId === "string" &&
48
+ typeof v.name === "string" &&
49
+ (v.kind === "shell" || v.kind === "setup" || v.kind === "run" || v.kind === "teardown") &&
50
+ typeof v.createdAt === "string" &&
51
+ typeof v.updatedAt === "string"
52
+ );
53
+ }
54
+
55
+ function writeAtomic(dir: string, terminals: Terminal[]): void {
56
+ ensureDir(dir);
57
+ const file = path(dir);
58
+ const tmp = `${file}.tmp`;
59
+ writeFileSync(tmp, JSON.stringify({ terminals }, null, 2) + "\n");
60
+ renameSync(tmp, file);
61
+ }
62
+
63
+ export interface CreateInput {
64
+ id: string;
65
+ projectId: string;
66
+ scopeId: string;
67
+ name: string;
68
+ kind: TerminalKind;
69
+ scripted?: boolean;
70
+ }
71
+
72
+ /** Insert OR replace a terminal record by id. Idempotent — calling
73
+ * `createTerminal` twice with the same deterministic id is a no-op
74
+ * on the canonical fields and updates `updatedAt`. */
75
+ export function upsertTerminal(dir: string, input: CreateInput): Terminal {
76
+ if (!SAFE_ID.test(input.id)) {
77
+ throw new Error(`invalid terminal id "${input.id}"`);
78
+ }
79
+ const existing = loadTerminals(dir);
80
+ const idx = existing.findIndex((t) => t.id === input.id);
81
+ const now = new Date().toISOString();
82
+ const next: Terminal = {
83
+ id: input.id,
84
+ projectId: input.projectId,
85
+ scopeId: input.scopeId,
86
+ name: input.name,
87
+ kind: input.kind,
88
+ createdAt: existing[idx]?.createdAt ?? now,
89
+ updatedAt: now,
90
+ ...(input.scripted ? { scripted: true } : {}),
91
+ };
92
+ if (idx === -1) existing.push(next);
93
+ else existing[idx] = next;
94
+ writeAtomic(dir, existing);
95
+ return next;
96
+ }
97
+
98
+ export function renameTerminal(dir: string, id: string, name: string): Terminal | null {
99
+ const all = loadTerminals(dir);
100
+ const idx = all.findIndex((t) => t.id === id);
101
+ if (idx === -1) return null;
102
+ const trimmed = name.trim();
103
+ if (!trimmed) throw new Error("name required");
104
+ const next: Terminal = {
105
+ ...all[idx]!,
106
+ name: trimmed,
107
+ updatedAt: new Date().toISOString(),
108
+ };
109
+ all[idx] = next;
110
+ writeAtomic(dir, all);
111
+ return next;
112
+ }
113
+
114
+ export function deleteTerminal(dir: string, id: string): boolean {
115
+ const all = loadTerminals(dir);
116
+ const next = all.filter((t) => t.id !== id);
117
+ if (next.length === all.length) return false;
118
+ writeAtomic(dir, next);
119
+ return true;
120
+ }
121
+
122
+ export function findTerminal(dir: string, id: string): Terminal | null {
123
+ const all = loadTerminals(dir);
124
+ return all.find((t) => t.id === id) ?? null;
125
+ }
@@ -0,0 +1,165 @@
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.ts";
20
+
21
+ /** The `<os>-<arch>` tags we publish a prebuilt TUI binary for. */
22
+ export type TuiPlatformTag = "darwin-arm64" | "darwin-x64" | "linux-x64" | "linux-arm64";
23
+
24
+ /** The GitHub repo the release assets live under. */
25
+ export const RELEASE_REPO = "wavyrai/tmux-ide";
26
+
27
+ /** A downloaded binary smaller than this is treated as corrupt/truncated. */
28
+ export const MIN_TUI_BINARY_BYTES = 10 * 1024 * 1024;
29
+
30
+ const SUPPORTED: Record<string, TuiPlatformTag> = {
31
+ "darwin-arm64": "darwin-arm64",
32
+ "darwin-x64": "darwin-x64",
33
+ "linux-x64": "linux-x64",
34
+ "linux-arm64": "linux-arm64",
35
+ };
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Pure
39
+ // ---------------------------------------------------------------------------
40
+
41
+ /**
42
+ * PURE — map a Node `process.platform`/`process.arch` pair to the release tag,
43
+ * or null when we don't publish a binary for it (e.g. windows, freebsd) so the
44
+ * caller can fall back to the "install bun" message.
45
+ */
46
+ export function tuiPlatformTag(
47
+ platform: NodeJS.Platform = process.platform,
48
+ arch: string = process.arch,
49
+ ): TuiPlatformTag | null {
50
+ return SUPPORTED[`${platform}-${arch}`] ?? null;
51
+ }
52
+
53
+ /** PURE — the `bun build --compile --target` flag for a tag (`bun-<tag>`). */
54
+ export function bunTargetForTag(tag: TuiPlatformTag): string {
55
+ return `bun-${tag}`;
56
+ }
57
+
58
+ /** PURE — the release asset filename for a tag (gzip-compressed binary). */
59
+ export function releaseAssetName(tag: TuiPlatformTag): string {
60
+ return `tmux-ide-tui-${tag}.gz`;
61
+ }
62
+
63
+ /** PURE — strip a leading `v` from a version string; `2.6.1` and `v2.6.1` both → `2.6.1`. */
64
+ export function normalizeVersion(version: string): string {
65
+ return version.startsWith("v") ? version.slice(1) : version;
66
+ }
67
+
68
+ /**
69
+ * PURE — the GitHub download URL for a platform's asset at a given version:
70
+ * `https://github.com/<repo>/releases/download/v<version>/tmux-ide-tui-<tag>.gz`.
71
+ */
72
+ export function releaseAssetUrl(version: string, tag: TuiPlatformTag): string {
73
+ return `https://github.com/${RELEASE_REPO}/releases/download/v${normalizeVersion(version)}/${releaseAssetName(tag)}`;
74
+ }
75
+
76
+ /**
77
+ * PURE — where a downloaded binary lives: `<home>/bin/tmux-ide-tui-<tag>-<version>`.
78
+ * The version is stamped INTO the name so a `tmux-ide update` to a new version
79
+ * misses the old download and re-fetches (rather than launching a stale binary).
80
+ */
81
+ export function downloadedTuiPath(home: string, tag: TuiPlatformTag, version: string): string {
82
+ return join(home, "bin", `tmux-ide-tui-${tag}-${normalizeVersion(version)}`);
83
+ }
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // io
87
+ // ---------------------------------------------------------------------------
88
+
89
+ /**
90
+ * io — the tmux-ide state home (`TMUX_IDE_HOME` override, else `~/.tmux-ide`),
91
+ * the same resolution the update-check cache and welcome marker use.
92
+ */
93
+ export function tuiStateHome(): string {
94
+ return process.env.TMUX_IDE_HOME ?? join(homedir(), ".tmux-ide");
95
+ }
96
+
97
+ /**
98
+ * io — locate a previously downloaded per-platform binary for THIS version and
99
+ * platform, or null. Feeds the resolution order in `tui/compiled.ts` after the
100
+ * shipped/local compiled binary and before the honest "unavailable" error.
101
+ */
102
+ export function findDownloadedTui(version: string = getCurrentVersion()): string | null {
103
+ const tag = tuiPlatformTag();
104
+ if (!tag) return null;
105
+ const path = downloadedTuiPath(tuiStateHome(), tag, version);
106
+ return existsSync(path) ? path : null;
107
+ }
108
+
109
+ /**
110
+ * io — download, verify, and install the per-platform TUI binary for the running
111
+ * version. Fetches the gzip asset, inflates it, rejects anything under
112
+ * {@link MIN_TUI_BINARY_BYTES} (a truncated download or an HTML error page),
113
+ * writes it `0o755`, and atomically renames it into place (temp-in-same-dir →
114
+ * rename, so a crashed download never leaves a half-written executable).
115
+ *
116
+ * Throws with an actionable message on an unsupported platform or a failed
117
+ * fetch. Returns the installed path and byte size.
118
+ */
119
+ export async function downloadTuiBinary(
120
+ opts: {
121
+ version?: string;
122
+ log?: (msg: string) => void;
123
+ } = {},
124
+ ): Promise<{ path: string; bytes: number }> {
125
+ const log = opts.log ?? (() => {});
126
+ const version = normalizeVersion(opts.version ?? getCurrentVersion());
127
+ const tag = tuiPlatformTag();
128
+ if (!tag) {
129
+ throw new Error(
130
+ `no prebuilt TUI binary is published for ${process.platform}-${process.arch} — ` +
131
+ `install bun (https://bun.sh) to run the TUI surfaces from source instead`,
132
+ );
133
+ }
134
+
135
+ const url = releaseAssetUrl(version, tag);
136
+ const dest = downloadedTuiPath(tuiStateHome(), tag, version);
137
+ mkdirSync(dirname(dest), { recursive: true });
138
+
139
+ log(`downloading ${url}`);
140
+ const res = await fetch(url);
141
+ if (!res.ok) {
142
+ throw new Error(
143
+ `could not download the TUI binary (${url} → HTTP ${res.status} ${res.statusText}). ` +
144
+ `Check that release v${version} exists and published its assets.`,
145
+ );
146
+ }
147
+
148
+ const gz = Buffer.from(await res.arrayBuffer());
149
+ const bin = gunzipSync(gz);
150
+ if (bin.byteLength < MIN_TUI_BINARY_BYTES) {
151
+ throw new Error(
152
+ `the downloaded TUI binary is only ${bin.byteLength} bytes (expected >10MB) — ` +
153
+ `treating it as corrupt and leaving the previous binary (if any) in place`,
154
+ );
155
+ }
156
+
157
+ const tmp = `${dest}.${process.pid}.tmp`;
158
+ writeFileSync(tmp, bin, { mode: 0o755 });
159
+ chmodSync(tmp, 0o755);
160
+ renameSync(tmp, dest);
161
+
162
+ const mb = (bin.byteLength / 1024 / 1024).toFixed(1);
163
+ log(`installed ${dest} (${mb} MB)`);
164
+ return { path: dest, bytes: bin.byteLength };
165
+ }