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.
- package/README.md +14 -9
- package/bin/cli.js +1024 -519
- package/bin/cli.ts +63 -6
- package/bunfig.toml +4 -0
- package/package.json +18 -7
- package/packages/contracts/package.json +22 -0
- package/packages/contracts/src/__tests__/ide-config.test.ts +46 -0
- package/packages/contracts/src/__tests__/terminals.test.ts +87 -0
- package/packages/contracts/src/actions-contract.ts +310 -0
- package/packages/contracts/src/actions-errors.ts +41 -0
- package/packages/contracts/src/domain.ts +36 -0
- package/packages/contracts/src/ide-config.ts +170 -0
- package/packages/contracts/src/index.ts +24 -0
- package/packages/contracts/src/lib-internal/auth.ts +13 -0
- package/packages/contracts/src/lib-internal/hq.ts +38 -0
- package/packages/contracts/src/terminals.ts +116 -0
- package/packages/contracts/src/tmux.ts +60 -0
- package/packages/contracts/src/workspace.ts +67 -0
- package/packages/daemon/dist/agent-explain.d.ts +8 -1
- package/packages/daemon/dist/agent-explain.js +19 -3
- 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/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 +298 -0
- package/packages/daemon/src/attach.ts +20 -0
- package/packages/daemon/src/bin.ts +4 -0
- package/packages/daemon/src/canonical.ts +7 -0
- package/packages/daemon/src/cli.ts +499 -0
- package/packages/daemon/src/command-center/actions/contract.ts +2 -0
- package/packages/daemon/src/command-center/actions/dispatcher.ts +137 -0
- package/packages/daemon/src/command-center/actions/errors.ts +105 -0
- package/packages/daemon/src/command-center/actions/handlers/_project-context.ts +30 -0
- package/packages/daemon/src/command-center/actions/handlers/_resolve-project.ts +78 -0
- package/packages/daemon/src/command-center/actions/handlers/app-set-remote-access.ts +118 -0
- package/packages/daemon/src/command-center/actions/handlers/config-actions.ts +113 -0
- package/packages/daemon/src/command-center/actions/handlers/daemon-shutdown.ts +38 -0
- package/packages/daemon/src/command-center/actions/handlers/project-activate.ts +30 -0
- package/packages/daemon/src/command-center/actions/handlers/project-launch.ts +70 -0
- package/packages/daemon/src/command-center/actions/handlers/project-open-terminal.ts +87 -0
- package/packages/daemon/src/command-center/actions/handlers/project-restart.ts +38 -0
- package/packages/daemon/src/command-center/actions/handlers/project-stop.ts +62 -0
- package/packages/daemon/src/command-center/actions/handlers/terminal-respawn.ts +119 -0
- package/packages/daemon/src/command-center/actions/handlers/terminal-stop.ts +35 -0
- package/packages/daemon/src/command-center/actions/registry.ts +149 -0
- package/packages/daemon/src/command-center/discovery.ts +96 -0
- package/packages/daemon/src/command-center/index.ts +31 -0
- package/packages/daemon/src/command-center/schemas.ts +85 -0
- package/packages/daemon/src/command-center/server.ts +1260 -0
- package/packages/daemon/src/command-center/ws-events.ts +316 -0
- package/packages/daemon/src/config.ts +549 -0
- package/packages/daemon/src/detect.ts +248 -0
- package/packages/daemon/src/doctor.ts +242 -0
- package/packages/daemon/src/embed.ts +5 -0
- package/packages/daemon/src/index.ts +12 -0
- package/packages/daemon/src/init.ts +211 -0
- package/packages/daemon/src/inspect.ts +178 -0
- package/packages/daemon/src/js-yaml.d.ts +10 -0
- package/packages/daemon/src/launch.ts +349 -0
- package/packages/daemon/src/lib/active-projects.ts +49 -0
- package/packages/daemon/src/lib/agent-discovery.ts +121 -0
- package/packages/daemon/src/lib/app-config.ts +427 -0
- package/packages/daemon/src/lib/app-settings.ts +53 -0
- package/packages/daemon/src/lib/auth/auth-service.ts +227 -0
- package/packages/daemon/src/lib/auth/middleware.ts +56 -0
- package/packages/daemon/src/lib/auth/types.ts +2 -0
- package/packages/daemon/src/lib/auth-token.ts +5 -0
- package/packages/daemon/src/lib/authorship.ts +280 -0
- package/packages/daemon/src/lib/canonical-daemon.ts +122 -0
- package/packages/daemon/src/lib/cli-action-bridge.ts +216 -0
- package/packages/daemon/src/lib/daemon-embed.ts +782 -0
- package/packages/daemon/src/lib/daemon-watchdog.ts +111 -0
- package/packages/daemon/src/lib/daemon.ts +79 -0
- package/packages/daemon/src/lib/dot-path.ts +17 -0
- package/packages/daemon/src/lib/errors.ts +67 -0
- package/packages/daemon/src/lib/filesystem-browser.ts +292 -0
- package/packages/daemon/src/lib/launch-plan.ts +90 -0
- package/packages/daemon/src/lib/log.ts +134 -0
- package/packages/daemon/src/lib/output.ts +76 -0
- package/packages/daemon/src/lib/project-init-runner.ts +150 -0
- package/packages/daemon/src/lib/project-inspect.ts +80 -0
- package/packages/daemon/src/lib/project-onboard.ts +149 -0
- package/packages/daemon/src/lib/project-probe.ts +92 -0
- package/packages/daemon/src/lib/project-registry.ts +296 -0
- package/packages/daemon/src/lib/session-monitor.ts +122 -0
- package/packages/daemon/src/lib/session-options.ts +100 -0
- package/packages/daemon/src/lib/shell.ts +8 -0
- package/packages/daemon/src/lib/sizes.ts +36 -0
- package/packages/daemon/src/lib/skill-sync.ts +155 -0
- package/packages/daemon/src/lib/slugify.ts +10 -0
- package/packages/daemon/src/lib/terminals-store.ts +125 -0
- package/packages/daemon/src/lib/tui-binary.ts +165 -0
- package/packages/daemon/src/lib/update-check.ts +298 -0
- package/packages/daemon/src/lib/update.ts +158 -0
- package/packages/daemon/src/lib/workspace-registry.ts +229 -0
- package/packages/daemon/src/lib/worktree.ts +289 -0
- package/packages/daemon/src/lib/yaml-io.ts +27 -0
- package/packages/daemon/src/ls.ts +40 -0
- package/packages/daemon/src/restart.ts +24 -0
- package/packages/daemon/src/restore.ts +514 -0
- package/packages/daemon/src/schemas/domain.ts +2 -0
- package/packages/daemon/src/schemas/filesystem.ts +34 -0
- package/packages/daemon/src/schemas/ide-config.ts +2 -0
- package/packages/daemon/src/schemas/index.ts +59 -0
- package/packages/daemon/src/schemas/inspect.ts +67 -0
- package/packages/daemon/src/schemas/registry.ts +55 -0
- package/packages/daemon/src/schemas/ws-events.ts +135 -0
- package/packages/daemon/src/send.ts +171 -0
- package/packages/daemon/src/server/README.md +15 -0
- package/packages/daemon/src/server/index.ts +74 -0
- package/packages/daemon/src/server/pty-bridge.ts +532 -0
- package/packages/daemon/src/server/standalone.ts +18 -0
- package/packages/daemon/src/server/ws-route.ts +483 -0
- package/packages/daemon/src/status.ts +59 -0
- package/packages/daemon/src/stop.ts +28 -0
- package/packages/daemon/src/terminal/NodePtyAdapter.ts +271 -0
- package/packages/daemon/src/terminal/PtyAdapter.ts +140 -0
- package/packages/daemon/src/terminal/README.md +92 -0
- package/packages/daemon/src/tui/chrome/cheatsheet.ts +260 -0
- package/packages/daemon/src/tui/chrome/chip.ts +30 -0
- package/packages/daemon/src/tui/chrome/events.ts +119 -0
- package/packages/daemon/src/tui/chrome/kitty-keys.ts +55 -0
- package/packages/daemon/src/tui/chrome/menu.ts +289 -0
- package/packages/daemon/src/tui/chrome/notify.ts +382 -0
- package/packages/daemon/src/tui/chrome/panels.ts +111 -0
- package/packages/daemon/src/tui/chrome/sidebar.ts +222 -0
- package/packages/daemon/src/tui/chrome/snapshot.ts +425 -0
- package/packages/daemon/src/tui/chrome/statusline.ts +595 -0
- package/packages/daemon/src/tui/chrome/updater.ts +510 -0
- package/packages/daemon/src/tui/chrome/welcome.ts +124 -0
- package/packages/daemon/src/tui/compiled.ts +121 -0
- package/packages/daemon/src/tui/detect/classify.ts +208 -0
- package/packages/daemon/src/tui/detect/manifest-loader.ts +193 -0
- package/packages/daemon/src/tui/detect/manifest.ts +199 -0
- package/packages/daemon/src/tui/detect/manifests.ts +354 -0
- package/packages/daemon/src/tui/detect/process-tree.ts +217 -0
- package/packages/daemon/src/tui/detect/snapshot.ts +70 -0
- package/packages/daemon/src/tui/integrations/claude.ts +176 -0
- package/packages/daemon/src/tui/integrations/offer.ts +145 -0
- package/packages/daemon/src/tui/main.ts +82 -0
- package/packages/daemon/src/tui/mirror/ack-writer.ts +77 -0
- package/packages/daemon/src/tui/mirror/agent-chip.ts +97 -0
- package/packages/daemon/src/tui/mirror/agent-rows.ts +133 -0
- package/packages/daemon/src/tui/mirror/app-state.ts +179 -0
- package/packages/daemon/src/tui/mirror/app.tsx +5265 -0
- package/packages/daemon/src/tui/mirror/blit.ts +186 -0
- package/packages/daemon/src/tui/mirror/control-client.ts +214 -0
- package/packages/daemon/src/tui/mirror/control.ts +97 -0
- package/packages/daemon/src/tui/mirror/dialog-model.ts +298 -0
- package/packages/daemon/src/tui/mirror/dialog-stack.ts +354 -0
- package/packages/daemon/src/tui/mirror/diff-model.ts +112 -0
- package/packages/daemon/src/tui/mirror/editor-buffer.ts +117 -0
- package/packages/daemon/src/tui/mirror/file-tree.ts +97 -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/input-coalescer.ts +105 -0
- package/packages/daemon/src/tui/mirror/menu-model.ts +187 -0
- package/packages/daemon/src/tui/mirror/palette.ts +274 -0
- package/packages/daemon/src/tui/mirror/pane-mirror.ts +561 -0
- package/packages/daemon/src/tui/mirror/pane-surface.tsx +415 -0
- package/packages/daemon/src/tui/mirror/perf-tap.ts +160 -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 +262 -0
- package/packages/daemon/src/tui/mirror/session-mirror.ts +443 -0
- package/packages/daemon/src/tui/mirror/settings-model.ts +345 -0
- package/packages/daemon/src/tui/mirror/size-truth.ts +77 -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/team/CONTROL.md +50 -0
- package/packages/daemon/src/tui/team/entry.ts +38 -0
- package/packages/daemon/src/tui/team/fuzzy.ts +133 -0
- package/packages/daemon/src/tui/team/home.ts +170 -0
- package/packages/daemon/src/tui/team/index.tsx +1521 -0
- package/packages/daemon/src/tui/team/input.ts +34 -0
- package/packages/daemon/src/tui/team/keymap.ts +127 -0
- package/packages/daemon/src/tui/team/mouse.ts +29 -0
- package/packages/daemon/src/tui/team/nav.ts +31 -0
- package/packages/daemon/src/tui/team/preview.ts +34 -0
- package/packages/daemon/src/tui/team/projects.ts +191 -0
- package/packages/daemon/src/tui/team/report.ts +83 -0
- package/packages/daemon/src/tui/team/sessions.ts +433 -0
- package/packages/daemon/src/tui/team/tree.ts +62 -0
- package/packages/daemon/src/types.ts +13 -0
- package/packages/daemon/src/ui/index.ts +32 -0
- package/packages/daemon/src/ui/terminal/index.ts +9 -0
- package/packages/daemon/src/ui/types.ts +91 -0
- package/packages/daemon/src/ui/web/base.css +80 -0
- package/packages/daemon/src/ui/web/components/Box.tsx +59 -0
- package/packages/daemon/src/ui/web/components/Input.tsx +32 -0
- package/packages/daemon/src/ui/web/components/ScrollBox.tsx +60 -0
- package/packages/daemon/src/ui/web/components/Text.tsx +28 -0
- package/packages/daemon/src/ui/web/hooks.ts +106 -0
- package/packages/daemon/src/ui/web/index.ts +27 -0
- package/packages/daemon/src/ui/web/render.ts +77 -0
- package/packages/daemon/src/ui/web/utils/color.ts +27 -0
- package/packages/daemon/src/validate.ts +217 -0
- package/packages/daemon/src/widgets/changes/README.md +3 -0
- package/packages/daemon/src/widgets/changes/index.tsx +691 -0
- package/packages/daemon/src/widgets/config/README.md +3 -0
- package/packages/daemon/src/widgets/config/index.tsx +481 -0
- package/packages/daemon/src/widgets/explorer/README.md +3 -0
- package/packages/daemon/src/widgets/explorer/breadcrumbs.tsx +77 -0
- package/packages/daemon/src/widgets/explorer/footer.tsx +20 -0
- package/packages/daemon/src/widgets/explorer/header.tsx +23 -0
- package/packages/daemon/src/widgets/explorer/index.tsx +456 -0
- package/packages/daemon/src/widgets/explorer/tree-model.ts +103 -0
- package/packages/daemon/src/widgets/explorer/tree.tsx +165 -0
- package/packages/daemon/src/widgets/lib/config-model.ts +116 -0
- package/packages/daemon/src/widgets/lib/files.ts +88 -0
- package/packages/daemon/src/widgets/lib/git.ts +88 -0
- package/packages/daemon/src/widgets/lib/grammar.ts +126 -0
- package/packages/daemon/src/widgets/lib/help-overlay.tsx +101 -0
- package/packages/daemon/src/widgets/lib/pane-comms.ts +209 -0
- package/packages/daemon/src/widgets/lib/theme.ts +194 -0
- package/packages/daemon/src/widgets/lib/watcher.ts +132 -0
- package/packages/daemon/src/widgets/preview/README.md +3 -0
- package/packages/daemon/src/widgets/preview/index.tsx +416 -0
- package/packages/daemon/src/widgets/resolve.ts +121 -0
- package/packages/daemon/src/widgets/setup/README.md +3 -0
- package/packages/daemon/src/widgets/setup/agent-naming.tsx +112 -0
- package/packages/daemon/src/widgets/setup/config-tree.tsx +246 -0
- package/packages/daemon/src/widgets/setup/detect-panel.tsx +72 -0
- package/packages/daemon/src/widgets/setup/field-editor.tsx +265 -0
- package/packages/daemon/src/widgets/setup/footer.tsx +107 -0
- package/packages/daemon/src/widgets/setup/index.tsx +341 -0
- package/packages/daemon/src/widgets/setup/layout-picker.tsx +96 -0
- package/packages/daemon/src/widgets/setup/orchestrator-panel.tsx +200 -0
- package/packages/daemon/src/widgets/setup/review-panel.tsx +140 -0
- package/packages/daemon/src/widgets/setup/setup-model.ts +188 -0
- package/packages/daemon/src/widgets/sidebar/index.tsx +527 -0
- package/packages/tmux-bridge/package.json +22 -0
- package/packages/tmux-bridge/src/errors.ts +28 -0
- package/packages/tmux-bridge/src/index.ts +31 -0
- package/packages/tmux-bridge/src/monitor.ts +77 -0
- package/packages/tmux-bridge/src/panes.ts +136 -0
- package/packages/tmux-bridge/src/runner.test.ts +501 -0
- package/packages/tmux-bridge/src/runner.ts +91 -0
- package/packages/tmux-bridge/src/sessions.ts +126 -0
- package/packages/tmux-bridge/src/targeting.test.ts +107 -0
- package/packages/tmux-bridge/src/targeting.ts +90 -0
- package/scripts/build-tui.mjs +11 -4
- package/scripts/perf-mirror.mjs +313 -0
- package/scripts/postinstall.js +26 -2
- package/skill/SKILL.md +22 -0
- 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
|
@@ -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
|
+
}
|