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,289 @@
1
+ /**
2
+ * Git worktree flow — one checkout per branch so parallel agents (or a fleet)
3
+ * each work on isolation. `tmux-ide worktree create <branch>` adds a git
4
+ * worktree in a SIBLING directory (never inside the repo) and opens a tmux
5
+ * session there; the CLI (`bin/cli.ts`, `worktree` case) wires the io.
6
+ *
7
+ * The pure helpers — {@link worktreeSessionName}, {@link worktreePath},
8
+ * {@link parseWorktreeList}, {@link mapWorktreeError} — carry no io so they're
9
+ * cheaply testable. The io wrappers ({@link createWorktree},
10
+ * {@link removeWorktree}, {@link listWorktrees}) shell out to `git` and map its
11
+ * stderr into a typed {@link WorktreeError} with an actionable message.
12
+ */
13
+ import { execFileSync } from "node:child_process";
14
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
15
+ import { IdeError } from "./errors.ts";
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Typed error
19
+ // ---------------------------------------------------------------------------
20
+
21
+ export type WorktreeErrorCode =
22
+ | "NOT_A_GIT_REPO"
23
+ | "BRANCH_EXISTS"
24
+ | "BRANCH_NOT_FOUND"
25
+ | "ALREADY_CHECKED_OUT"
26
+ | "WORKTREE_EXISTS"
27
+ | "WORKTREE_DIRTY"
28
+ | "WORKTREE_NOT_FOUND"
29
+ | "GIT_FAILED";
30
+
31
+ export class WorktreeError extends IdeError {
32
+ constructor(message: string, code: WorktreeErrorCode) {
33
+ super(message, { code, exitCode: 1 });
34
+ this.name = "WorktreeError";
35
+ }
36
+ }
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Pure helpers
40
+ // ---------------------------------------------------------------------------
41
+
42
+ /**
43
+ * PURE — sanitize a component for a tmux session name. tmux treats `:` as a
44
+ * target separator and silently rewrites `.` to `_`, so we normalize both (plus
45
+ * whitespace) to `-` up front, keeping the name stable and predictable. `/` is
46
+ * left intact — tmux accepts it in session names (verified live on tmux 3.6).
47
+ */
48
+ function sanitizeForTmux(part: string): string {
49
+ // Slashes too: `feat/x` would parse as a path-ish target in tmux commands
50
+ // and can't appear in the status bar's `#[range=user|sw<name>]` mouse ranges.
51
+ return part.replace(/[.:/\s]+/g, "-");
52
+ }
53
+
54
+ /**
55
+ * PURE — the tmux session name for a project's worktree on `branch`, e.g.
56
+ * `myapp@fix-auth`. Both sides are sanitized ({@link sanitizeForTmux}) so a
57
+ * dotted/colon'd branch (or project) stays a single inert session name; the
58
+ * `@` separator is preserved so the session reads as "project, this branch".
59
+ */
60
+ export function worktreeSessionName(project: string, branch: string): string {
61
+ return `${sanitizeForTmux(project)}@${sanitizeForTmux(branch)}`;
62
+ }
63
+
64
+ /**
65
+ * PURE — the default base directory for a repo's worktrees: a SIBLING of the
66
+ * repo named `<repo-name>-worktrees`. Kept outside the repo so a worktree is
67
+ * never nested inside its own parent checkout (which git rejects and which
68
+ * would pollute the project tree).
69
+ */
70
+ export function defaultWorktreeBaseDir(repoDir: string): string {
71
+ const abs = resolve(repoDir);
72
+ return join(dirname(abs), `${basename(abs)}-worktrees`);
73
+ }
74
+
75
+ /**
76
+ * PURE — the on-disk path for a worktree of `branch`. Defaults to
77
+ * `<repo>/../<repo-name>-worktrees/<branch>`; a non-empty `configuredDir` (the
78
+ * app-config `worktrees.dir` override) replaces the base, resolved relative to
79
+ * the repo when it isn't absolute. The branch is the final path segment (a
80
+ * slashed branch nests, which `git worktree add` creates).
81
+ */
82
+ export function worktreePath(
83
+ repoDir: string,
84
+ branch: string,
85
+ configuredDir?: string | null,
86
+ ): string {
87
+ const base =
88
+ configuredDir && configuredDir.length > 0
89
+ ? isAbsolute(configuredDir)
90
+ ? configuredDir
91
+ : resolve(repoDir, configuredDir)
92
+ : defaultWorktreeBaseDir(repoDir);
93
+ return join(base, branch);
94
+ }
95
+
96
+ /** A single record from `git worktree list --porcelain`. */
97
+ export interface WorktreeEntry {
98
+ /** Absolute path of the worktree. */
99
+ path: string;
100
+ /** The checked-out commit sha, or null (bare repo). */
101
+ head: string | null;
102
+ /** Short branch name (no `refs/heads/`), or null when detached/bare. */
103
+ branch: string | null;
104
+ /** The bare repository entry. */
105
+ bare: boolean;
106
+ /** A detached-HEAD worktree (no branch). */
107
+ detached: boolean;
108
+ }
109
+
110
+ /**
111
+ * PURE — parse `git worktree list --porcelain`. Records are separated by blank
112
+ * lines; each has a `worktree <path>` line, then optional `HEAD <sha>`,
113
+ * `branch refs/heads/<name>`, `bare`, `detached` attribute lines. Unknown
114
+ * attribute lines are ignored. Malformed/empty input yields `[]`.
115
+ */
116
+ export function parseWorktreeList(porcelain: string): WorktreeEntry[] {
117
+ const entries: WorktreeEntry[] = [];
118
+ let current: WorktreeEntry | null = null;
119
+
120
+ const flush = () => {
121
+ if (current) entries.push(current);
122
+ current = null;
123
+ };
124
+
125
+ for (const rawLine of porcelain.split("\n")) {
126
+ const line = rawLine.replace(/\r$/, "");
127
+ if (line.length === 0) {
128
+ flush();
129
+ continue;
130
+ }
131
+ if (line.startsWith("worktree ")) {
132
+ flush();
133
+ current = {
134
+ path: line.slice("worktree ".length),
135
+ head: null,
136
+ branch: null,
137
+ bare: false,
138
+ detached: false,
139
+ };
140
+ continue;
141
+ }
142
+ if (!current) continue;
143
+ if (line.startsWith("HEAD ")) {
144
+ current.head = line.slice("HEAD ".length);
145
+ } else if (line.startsWith("branch ")) {
146
+ current.branch = line.slice("branch ".length).replace(/^refs\/heads\//, "");
147
+ } else if (line === "bare") {
148
+ current.bare = true;
149
+ } else if (line === "detached") {
150
+ current.detached = true;
151
+ }
152
+ }
153
+ flush();
154
+ return entries;
155
+ }
156
+
157
+ /**
158
+ * PURE — map a failed `git` invocation's stderr to a typed
159
+ * {@link WorktreeError}. Recognizes the common worktree failures so the CLI can
160
+ * surface an actionable message instead of a raw git dump; anything unmatched
161
+ * falls back to `GIT_FAILED` carrying the trimmed stderr.
162
+ */
163
+ export function mapWorktreeError(stderr: string, fallbackMessage: string): WorktreeError {
164
+ const text = stderr.trim();
165
+ const lower = text.toLowerCase();
166
+
167
+ if (lower.includes("not a git repository")) {
168
+ return new WorktreeError(
169
+ "Not a git repository. Run `tmux-ide worktree` from inside a git repo.",
170
+ "NOT_A_GIT_REPO",
171
+ );
172
+ }
173
+ if (lower.includes("already exists") && lower.includes("branch")) {
174
+ return new WorktreeError(
175
+ `${text}\nUse \`tmux-ide worktree create <branch>\` without --from to check out the existing branch, or pick a new name.`,
176
+ "BRANCH_EXISTS",
177
+ );
178
+ }
179
+ if (lower.includes("is already checked out") || lower.includes("already used by worktree")) {
180
+ return new WorktreeError(text, "ALREADY_CHECKED_OUT");
181
+ }
182
+ if (lower.includes("already exists")) {
183
+ return new WorktreeError(text, "WORKTREE_EXISTS");
184
+ }
185
+ if (
186
+ (lower.includes("invalid reference") || lower.includes("not a valid ref")) &&
187
+ !lower.includes("already")
188
+ ) {
189
+ return new WorktreeError(text, "BRANCH_NOT_FOUND");
190
+ }
191
+ if (
192
+ lower.includes("contains modified or untracked files") ||
193
+ lower.includes("use --force") ||
194
+ lower.includes("use 'remove -f'")
195
+ ) {
196
+ return new WorktreeError(
197
+ `${text}\nRe-run with --force to discard those changes.`,
198
+ "WORKTREE_DIRTY",
199
+ );
200
+ }
201
+ if (lower.includes("is not a working tree") || lower.includes("not a working tree")) {
202
+ return new WorktreeError(text, "WORKTREE_NOT_FOUND");
203
+ }
204
+ return new WorktreeError(text.length > 0 ? text : fallbackMessage, "GIT_FAILED");
205
+ }
206
+
207
+ // ---------------------------------------------------------------------------
208
+ // io — git shell-outs (map stderr → WorktreeError)
209
+ // ---------------------------------------------------------------------------
210
+
211
+ /** Injectable git runner so io wrappers stay unit-testable. */
212
+ export type GitRunner = (repoDir: string, args: string[]) => string;
213
+
214
+ let gitRunner: GitRunner = (repoDir, args) =>
215
+ execFileSync("git", args, {
216
+ cwd: repoDir,
217
+ encoding: "utf-8",
218
+ stdio: ["ignore", "pipe", "pipe"],
219
+ });
220
+
221
+ /** Swap the git runner (tests). Returns a restore fn. */
222
+ export function _setGitRunnerForTests(fn: GitRunner): () => void {
223
+ const prev = gitRunner;
224
+ gitRunner = fn;
225
+ return () => {
226
+ gitRunner = prev;
227
+ };
228
+ }
229
+
230
+ interface GitExecError {
231
+ stderr?: Buffer | string;
232
+ }
233
+
234
+ function runGit(repoDir: string, args: string[], fallbackMessage: string): string {
235
+ try {
236
+ return gitRunner(repoDir, args);
237
+ } catch (error) {
238
+ const stderr = (error as GitExecError).stderr;
239
+ const text = stderr ? stderr.toString() : "";
240
+ throw mapWorktreeError(text, fallbackMessage);
241
+ }
242
+ }
243
+
244
+ export interface CreateWorktreeOptions {
245
+ /** Create a NEW branch (git worktree add -b). Off → check out an existing branch. */
246
+ newBranch?: boolean;
247
+ /** Base ref for the new branch (default: current HEAD). Only used with newBranch. */
248
+ from?: string | null;
249
+ }
250
+
251
+ /**
252
+ * Add a git worktree for `branch` at `worktreeAbsPath` and return that path.
253
+ * With `newBranch`, creates the branch (optionally off `from`); otherwise
254
+ * checks out an existing branch. Failures throw a typed {@link WorktreeError}.
255
+ */
256
+ export function createWorktree(
257
+ repoDir: string,
258
+ branch: string,
259
+ worktreeAbsPath: string,
260
+ options: CreateWorktreeOptions = {},
261
+ ): string {
262
+ const args = ["worktree", "add"];
263
+ if (options.newBranch) {
264
+ args.push("-b", branch, worktreeAbsPath);
265
+ if (options.from && options.from.length > 0) args.push(options.from);
266
+ } else {
267
+ args.push(worktreeAbsPath, branch);
268
+ }
269
+ runGit(repoDir, args, `Failed to create worktree for ${branch}`);
270
+ return worktreeAbsPath;
271
+ }
272
+
273
+ /** Remove a git worktree by path. `force` discards uncommitted changes. */
274
+ export function removeWorktree(
275
+ repoDir: string,
276
+ worktreeAbsPath: string,
277
+ options: { force?: boolean } = {},
278
+ ): void {
279
+ const args = ["worktree", "remove"];
280
+ if (options.force) args.push("--force");
281
+ args.push(worktreeAbsPath);
282
+ runGit(repoDir, args, `Failed to remove worktree ${worktreeAbsPath}`);
283
+ }
284
+
285
+ /** List the repo's worktrees. Throws a typed {@link WorktreeError} on failure. */
286
+ export function listWorktrees(repoDir: string): WorktreeEntry[] {
287
+ const out = runGit(repoDir, ["worktree", "list", "--porcelain"], "Failed to list worktrees");
288
+ return parseWorktreeList(out);
289
+ }
@@ -0,0 +1,27 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+ import { resolve, basename } from "node:path";
3
+ import yaml from "js-yaml";
4
+ import type { IdeConfig } from "../types.ts";
5
+
6
+ export function readConfig(dir: string): { config: IdeConfig; configPath: string } {
7
+ const configPath = resolve(dir, "ide.yml");
8
+ const raw = readFileSync(configPath, "utf-8");
9
+ const config = yaml.load(raw) as IdeConfig;
10
+ return { config, configPath };
11
+ }
12
+
13
+ export function writeConfig(dir: string, config: IdeConfig): string {
14
+ const configPath = resolve(dir, "ide.yml");
15
+ const out = yaml.dump(config, { lineWidth: -1, noRefs: true, quotingType: '"' });
16
+ writeFileSync(configPath, out);
17
+ return configPath;
18
+ }
19
+
20
+ export function getSessionName(dir: string): { name: string; source: "config" | "fallback" } {
21
+ try {
22
+ const { config } = readConfig(dir);
23
+ return { name: config.name ?? basename(dir), source: config.name ? "config" : "fallback" };
24
+ } catch {
25
+ return { name: basename(dir), source: "fallback" };
26
+ }
27
+ }
@@ -0,0 +1,40 @@
1
+ import { execSync } from "node:child_process";
2
+
3
+ export async function ls({ json }: { json?: boolean } = {}): Promise<void> {
4
+ let raw: string;
5
+ try {
6
+ raw = execSync(
7
+ 'tmux list-sessions -F "#{session_name}|#{session_created}|#{session_attached}"',
8
+ { encoding: "utf-8" },
9
+ ).trim();
10
+ } catch {
11
+ if (json) {
12
+ console.log(JSON.stringify({ sessions: [] }));
13
+ } else {
14
+ console.log("No tmux sessions running.");
15
+ }
16
+ return;
17
+ }
18
+
19
+ const sessions = raw.split("\n").map((line) => {
20
+ const [name, created, attached] = line.split("|");
21
+ return {
22
+ name,
23
+ created: new Date(parseInt(created!) * 1000).toISOString(),
24
+ attached: attached !== "0",
25
+ };
26
+ });
27
+
28
+ if (json) {
29
+ console.log(JSON.stringify({ sessions }, null, 2));
30
+ return;
31
+ }
32
+
33
+ // Table output
34
+ console.log("SESSION".padEnd(24) + "CREATED".padEnd(22) + "ATTACHED");
35
+ console.log("─".repeat(54));
36
+ for (const s of sessions) {
37
+ const date = new Date(s.created).toLocaleString();
38
+ console.log(s.name!.padEnd(24) + date.padEnd(22) + (s.attached ? "yes" : "no"));
39
+ }
40
+ }
@@ -0,0 +1,24 @@
1
+ import { resolve } from "node:path";
2
+ import { getSessionName } from "./lib/yaml-io.ts";
3
+ import { launch } from "./launch.ts";
4
+ import { killSession, stopSessionMonitor } from "@tmux-ide/tmux-bridge";
5
+
6
+ export async function restart(
7
+ targetDir: string | undefined,
8
+ { json, attach }: { json?: boolean; attach?: boolean } = {},
9
+ ): Promise<void> {
10
+ const dir = resolve(targetDir ?? ".");
11
+ const { name: session } = getSessionName(dir);
12
+
13
+ // Stop the session monitor first — this triggers orchestrator graceful
14
+ // shutdown (release tasks, save state) before we kill the tmux session.
15
+ stopSessionMonitor(session);
16
+
17
+ const result = killSession(session);
18
+
19
+ if (result.stopped) {
20
+ console.log(`Stopped session "${session}"`);
21
+ }
22
+
23
+ await launch(dir, { json, attach });
24
+ }