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,514 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tmux-ide restore` — rebuild the fleet from the last snapshot.
|
|
3
|
+
*
|
|
4
|
+
* The chrome updater continuously writes `~/.tmux-ide/snapshot.json`
|
|
5
|
+
* ({@link ./tui/chrome/snapshot.ts}); when the tmux server dies for real, this
|
|
6
|
+
* rebuilds every session it recorded — windows, split layouts, per-pane cwds,
|
|
7
|
+
* and titles — and re-adopts the sessions that were adopted.
|
|
8
|
+
*
|
|
9
|
+
* Safety first: an already-live session is NEVER clobbered (it's skipped), and
|
|
10
|
+
* recorded pane commands are NOT auto-run unless `--run-commands` is passed —
|
|
11
|
+
* a restored agent pane is a plain shell in the right directory with its title
|
|
12
|
+
* restored.
|
|
13
|
+
*
|
|
14
|
+
* `--resume-agents` (or `{ "restore": { "resumeAgents": true } }` in
|
|
15
|
+
* `~/.tmux-ide/config.json`) layers native agent-resume on top: a rebuilt
|
|
16
|
+
* claude pane that carries a recorded `@agent_session_id` relaunches as
|
|
17
|
+
* `claude --resume <id>`, reviving the actual conversation rather than opening
|
|
18
|
+
* a fresh shell. See {@link paneResumeCommand} for the exact decision table.
|
|
19
|
+
*
|
|
20
|
+
* {@link buildRestorePlan} + {@link paneResumeCommand} + {@link restorePrefs}
|
|
21
|
+
* are PURE (unit-tested); {@link restore} is the thin io wrapper that reads the
|
|
22
|
+
* snapshot + registry + live tmux + config, then executes the plan.
|
|
23
|
+
*/
|
|
24
|
+
import { runTmux } from "@tmux-ide/tmux-bridge";
|
|
25
|
+
import { appConfigPath, loadAppConfig, parseAppConfig } from "./lib/app-config.ts";
|
|
26
|
+
import { IdeError } from "./lib/errors.ts";
|
|
27
|
+
import { listProjects } from "./lib/project-registry.ts";
|
|
28
|
+
import { adoptSession } from "./tui/chrome/statusline.ts";
|
|
29
|
+
import {
|
|
30
|
+
readSnapshot,
|
|
31
|
+
type FleetSnapshot,
|
|
32
|
+
type PaneSnapshot,
|
|
33
|
+
type SessionSnapshot,
|
|
34
|
+
} from "./tui/chrome/snapshot.ts";
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Pure plan
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* One restore action per snapshot session:
|
|
42
|
+
* - `skip` — a live session already owns the name; never clobber it.
|
|
43
|
+
* - `launch` — the session maps to a registry project WITH an ide.yml; the
|
|
44
|
+
* config is the source of truth, so relaunch it instead of a raw
|
|
45
|
+
* rebuild.
|
|
46
|
+
* - `rebuild` — raw reconstruction from the snapshot (windows/panes/layout).
|
|
47
|
+
*/
|
|
48
|
+
export type RestoreAction =
|
|
49
|
+
| { kind: "skip"; session: string }
|
|
50
|
+
| { kind: "launch"; session: string; dir: string }
|
|
51
|
+
| { kind: "rebuild"; session: SessionSnapshot };
|
|
52
|
+
|
|
53
|
+
export interface RestorePlan {
|
|
54
|
+
actions: RestoreAction[];
|
|
55
|
+
/** Total panes across `rebuild` actions (what restore directly creates). */
|
|
56
|
+
paneCount: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* PURE — decide what to do with each snapshot session.
|
|
61
|
+
*
|
|
62
|
+
* Sessions are processed in snapshot order. A name that's already live is
|
|
63
|
+
* skipped. Otherwise, when `ideProjects` maps the name to a directory (a
|
|
64
|
+
* registry project whose dir has an ide.yml), we prefer relaunching from that
|
|
65
|
+
* config; failing that, we raw-rebuild from the recorded windows/panes.
|
|
66
|
+
*/
|
|
67
|
+
export function buildRestorePlan(
|
|
68
|
+
snapshot: FleetSnapshot,
|
|
69
|
+
liveSessionNames: Iterable<string>,
|
|
70
|
+
ideProjects: Map<string, string> = new Map(),
|
|
71
|
+
): RestorePlan {
|
|
72
|
+
const live = new Set(liveSessionNames);
|
|
73
|
+
const actions: RestoreAction[] = [];
|
|
74
|
+
let paneCount = 0;
|
|
75
|
+
for (const session of snapshot.sessions) {
|
|
76
|
+
if (live.has(session.name)) {
|
|
77
|
+
actions.push({ kind: "skip", session: session.name });
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const dir = ideProjects.get(session.name);
|
|
81
|
+
if (dir) {
|
|
82
|
+
actions.push({ kind: "launch", session: session.name, dir });
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
actions.push({ kind: "rebuild", session });
|
|
86
|
+
for (const window of session.windows) paneCount += window.panes.length;
|
|
87
|
+
}
|
|
88
|
+
return { actions, paneCount };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Pure — agent resume decision
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
/** A session id is trusted only if it's the uuid alphabet — nothing shell-active. */
|
|
96
|
+
const SAFE_SESSION_ID = /^[A-Za-z0-9-]+$/;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* PURE — the command to relaunch a pane so its agent conversation is revived,
|
|
100
|
+
* or `null` for "no resume — leave it a plain shell (or replay `command`)".
|
|
101
|
+
*
|
|
102
|
+
* Decision table (only fires when `resumeAgents` is on):
|
|
103
|
+
* - claude + a recorded `@agent_session_id` → `claude --resume <id>`. The id is
|
|
104
|
+
* a uuid, but we don't trust the snapshot: unless it matches
|
|
105
|
+
* {@link SAFE_SESSION_ID} (`[A-Za-z0-9-]`) we bail to `null` rather than risk
|
|
106
|
+
* feeding shell metacharacters into send-keys.
|
|
107
|
+
* - claude with NO session id → `null`. `claude --continue` would be too
|
|
108
|
+
* magical here (it resumes the most-recent conversation in the cwd, which may
|
|
109
|
+
* belong to a different pane); a fresh shell is the safe, predictable default.
|
|
110
|
+
* - any other agent → `null`. No native resume story yet.
|
|
111
|
+
* - `resumeAgents` off → always `null`.
|
|
112
|
+
*/
|
|
113
|
+
export function paneResumeCommand(
|
|
114
|
+
pane: PaneSnapshot,
|
|
115
|
+
opts: { resumeAgents: boolean },
|
|
116
|
+
): string | null {
|
|
117
|
+
if (!opts.resumeAgents) return null;
|
|
118
|
+
if (pane.agent !== "claude") return null;
|
|
119
|
+
const id = pane.agentSessionId;
|
|
120
|
+
if (!id || !SAFE_SESSION_ID.test(id)) return null;
|
|
121
|
+
return `claude --resume ${id}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** PURE — how many of a session's panes would resume under `resumeAgents`. */
|
|
125
|
+
export function countResumableAgents(session: SessionSnapshot, resumeAgents: boolean): number {
|
|
126
|
+
let n = 0;
|
|
127
|
+
for (const window of session.windows) {
|
|
128
|
+
for (const pane of window.panes) {
|
|
129
|
+
if (paneResumeCommand(pane, { resumeAgents })) n++;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return n;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Pure — restore preferences (~/.tmux-ide/config.json)
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
/** Restore behaviour toggles (minimal, pre-M14). */
|
|
140
|
+
export interface RestorePrefs {
|
|
141
|
+
resumeAgents: boolean;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Default: don't resume agents unless asked (flag or config). */
|
|
145
|
+
export const DEFAULT_RESTORE_PREFS: RestorePrefs = { resumeAgents: false };
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* PURE — read `{ restore: { resumeAgents } }` out of a parsed config, falling
|
|
149
|
+
* back to {@link DEFAULT_RESTORE_PREFS} for anything missing or mistyped.
|
|
150
|
+
* Delegates to the shared {@link parseAppConfig} so restore parsing can't drift
|
|
151
|
+
* from the rest of the config.
|
|
152
|
+
*/
|
|
153
|
+
export function restorePrefs(parsedConfig: unknown): RestorePrefs {
|
|
154
|
+
return parseAppConfig(parsedConfig).restore;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Absolute path to the shared config (honors `TMUX_IDE_CONFIG`). */
|
|
158
|
+
export function restoreConfigPath(): string {
|
|
159
|
+
return appConfigPath();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** io — resolve restore prefs from the shared app config; missing/invalid → defaults. */
|
|
163
|
+
export function readRestorePrefs(): RestorePrefs {
|
|
164
|
+
return loadAppConfig().restore;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// io — execute a rebuild action against live tmux
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
function tmuxCapture(args: string[]): string {
|
|
172
|
+
return runTmux(args, { encoding: "utf-8" }).toString().trim();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Options for one raw rebuild (see {@link rebuildSession}). */
|
|
176
|
+
interface RebuildOptions {
|
|
177
|
+
/** Replay each pane's recorded command (`--run-commands`). */
|
|
178
|
+
runCommands: boolean;
|
|
179
|
+
/** Relaunch resumable agent panes as `claude --resume <id>` (`--resume-agents`). */
|
|
180
|
+
resumeAgents: boolean;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Raw-rebuild one session from its snapshot: create the session + its first
|
|
185
|
+
* window, add the remaining windows, split each window's panes (with their
|
|
186
|
+
* cwds), apply the recorded layout, restore titles, set the active window, and
|
|
187
|
+
* relaunch each pane per {@link RebuildOptions}.
|
|
188
|
+
*
|
|
189
|
+
* Per pane, at most ONE command is ever sent: an agent-resume command (when
|
|
190
|
+
* eligible under {@link paneResumeCommand}) takes precedence and, if it fires,
|
|
191
|
+
* suppresses the `--run-commands` replay for that pane. Returns the titles of
|
|
192
|
+
* the panes that were resumed (for the report).
|
|
193
|
+
*/
|
|
194
|
+
function rebuildSession(session: SessionSnapshot, opts: RebuildOptions): string[] {
|
|
195
|
+
const { runCommands, resumeAgents } = opts;
|
|
196
|
+
const resumedTitles: string[] = [];
|
|
197
|
+
const windows = session.windows;
|
|
198
|
+
if (windows.length === 0) {
|
|
199
|
+
runTmux(["new-session", "-d", "-s", session.name, "-c", session.cwd]);
|
|
200
|
+
return resumedTitles;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
windows.forEach((window, w) => {
|
|
204
|
+
const windowCwd = window.panes[0]?.cwd || session.cwd;
|
|
205
|
+
// First window comes from new-session; the rest are added detached.
|
|
206
|
+
const windowId =
|
|
207
|
+
w === 0
|
|
208
|
+
? tmuxCapture([
|
|
209
|
+
"new-session",
|
|
210
|
+
"-d",
|
|
211
|
+
"-P",
|
|
212
|
+
"-F",
|
|
213
|
+
"#{window_id}",
|
|
214
|
+
"-s",
|
|
215
|
+
session.name,
|
|
216
|
+
"-c",
|
|
217
|
+
windowCwd,
|
|
218
|
+
])
|
|
219
|
+
: tmuxCapture([
|
|
220
|
+
"new-window",
|
|
221
|
+
"-d",
|
|
222
|
+
"-P",
|
|
223
|
+
"-F",
|
|
224
|
+
"#{window_id}",
|
|
225
|
+
"-t",
|
|
226
|
+
`${session.name}:`,
|
|
227
|
+
"-c",
|
|
228
|
+
windowCwd,
|
|
229
|
+
]);
|
|
230
|
+
|
|
231
|
+
// The window opens with one pane (snapshot pane 0). Create the rest; the
|
|
232
|
+
// layout string below fixes geometry, so split direction is irrelevant.
|
|
233
|
+
const paneIds = [tmuxCapture(["display-message", "-p", "-t", windowId, "#{pane_id}"])];
|
|
234
|
+
for (let p = 1; p < window.panes.length; p++) {
|
|
235
|
+
const paneCwd = window.panes[p]!.cwd || windowCwd;
|
|
236
|
+
paneIds.push(
|
|
237
|
+
tmuxCapture([
|
|
238
|
+
"split-window",
|
|
239
|
+
"-d",
|
|
240
|
+
"-P",
|
|
241
|
+
"-F",
|
|
242
|
+
"#{pane_id}",
|
|
243
|
+
"-t",
|
|
244
|
+
paneIds[p - 1]!,
|
|
245
|
+
"-c",
|
|
246
|
+
paneCwd,
|
|
247
|
+
]),
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Restore geometry from the recorded layout (tmux accepts the checksummed
|
|
252
|
+
// #{window_layout} string verbatim), then name the window.
|
|
253
|
+
if (window.layout) runTmux(["select-layout", "-t", windowId, window.layout]);
|
|
254
|
+
if (window.name) runTmux(["rename-window", "-t", windowId, window.name]);
|
|
255
|
+
|
|
256
|
+
// Restore per-pane titles (and optionally replay commands). Titles are
|
|
257
|
+
// paired by creation order, which matches the snapshot's index order.
|
|
258
|
+
window.panes.forEach((pane, p) => {
|
|
259
|
+
const paneId = paneIds[p];
|
|
260
|
+
if (!paneId) return;
|
|
261
|
+
if (pane.title) runTmux(["select-pane", "-t", paneId, "-T", pane.title]);
|
|
262
|
+
// Re-stamp agent identity so the next snapshot keeps it and 0037's
|
|
263
|
+
// agent-resume can find the conversation to revive.
|
|
264
|
+
if (pane.agentSessionId) {
|
|
265
|
+
runTmux(["set-option", "-p", "-t", paneId, "@agent_session_id", pane.agentSessionId]);
|
|
266
|
+
}
|
|
267
|
+
if (pane.agent) {
|
|
268
|
+
runTmux(["set-option", "-p", "-t", paneId, "@agent_hint", pane.agent]);
|
|
269
|
+
}
|
|
270
|
+
// Resume the agent conversation if eligible; that takes precedence over
|
|
271
|
+
// the recorded-command replay so a pane is never sent two commands.
|
|
272
|
+
const resumeCmd = paneResumeCommand(pane, { resumeAgents });
|
|
273
|
+
if (resumeCmd) {
|
|
274
|
+
runTmux(["send-keys", "-t", paneId, "-l", "--", resumeCmd]);
|
|
275
|
+
runTmux(["send-keys", "-t", paneId, "Enter"]);
|
|
276
|
+
resumedTitles.push(pane.title || paneId);
|
|
277
|
+
} else if (runCommands && pane.command) {
|
|
278
|
+
runTmux(["send-keys", "-t", paneId, "-l", "--", pane.command]);
|
|
279
|
+
runTmux(["send-keys", "-t", paneId, "Enter"]);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Set the active window to whatever the snapshot had focused.
|
|
285
|
+
const activeIndex = windows.findIndex((w) => w.active);
|
|
286
|
+
if (activeIndex >= 0) {
|
|
287
|
+
runTmux(["select-window", "-t", `${session.name}:${windows[activeIndex]!.index}`]);
|
|
288
|
+
}
|
|
289
|
+
return resumedTitles;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
// io — the command
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
/** Registry sessions whose dir has an ide.yml → name -> dir (the launch path). */
|
|
297
|
+
function ideBackedProjects(): Map<string, string> {
|
|
298
|
+
const map = new Map<string, string>();
|
|
299
|
+
try {
|
|
300
|
+
for (const project of listProjects()) {
|
|
301
|
+
if (project.hasIdeYml && project.dir) map.set(project.name, project.dir);
|
|
302
|
+
}
|
|
303
|
+
} catch {
|
|
304
|
+
// no registry → nothing prefers a launch; everything raw-rebuilds
|
|
305
|
+
}
|
|
306
|
+
return map;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function liveSessions(): string[] {
|
|
310
|
+
try {
|
|
311
|
+
const raw = tmuxCapture(["list-sessions", "-F", "#{session_name}"]);
|
|
312
|
+
return raw ? raw.split("\n").filter(Boolean) : [];
|
|
313
|
+
} catch {
|
|
314
|
+
// no server / no sessions → nothing is live, rebuild everything
|
|
315
|
+
return [];
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export interface RestoreOptions {
|
|
320
|
+
json?: boolean;
|
|
321
|
+
dryRun?: boolean;
|
|
322
|
+
runCommands?: boolean;
|
|
323
|
+
/** Relaunch resumable agent panes (`--resume-agents`); ORed with config default. */
|
|
324
|
+
resumeAgents?: boolean;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** One rebuilt session's resumed agent panes (for the report). */
|
|
328
|
+
interface ResumedSession {
|
|
329
|
+
session: string;
|
|
330
|
+
panes: string[];
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Rebuild the fleet from the last snapshot. Missing snapshot → friendly exit 1.
|
|
335
|
+
* `dryRun` prints the plan without touching tmux; `runCommands` opts into
|
|
336
|
+
* replaying recorded pane commands (off by default for safety); `resumeAgents`
|
|
337
|
+
* (flag ORed with the `~/.tmux-ide/config.json` default) revives agent
|
|
338
|
+
* conversations via `claude --resume <id>`.
|
|
339
|
+
*/
|
|
340
|
+
export async function restore({
|
|
341
|
+
json = false,
|
|
342
|
+
dryRun = false,
|
|
343
|
+
runCommands = false,
|
|
344
|
+
resumeAgents = false,
|
|
345
|
+
}: RestoreOptions = {}): Promise<void> {
|
|
346
|
+
const snapshot = readSnapshot();
|
|
347
|
+
if (!snapshot) {
|
|
348
|
+
throw new IdeError(
|
|
349
|
+
"no snapshot yet — the updater writes one every ~30s while any session is adopted",
|
|
350
|
+
{ code: "NO_SNAPSHOT", exitCode: 1 },
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// The flag forces resume on; config makes it the default. No negative flag.
|
|
355
|
+
const resume = resumeAgents || readRestorePrefs().resumeAgents;
|
|
356
|
+
const plan = buildRestorePlan(snapshot, liveSessions(), ideBackedProjects());
|
|
357
|
+
|
|
358
|
+
if (dryRun) {
|
|
359
|
+
reportPlan(plan, snapshot, {
|
|
360
|
+
json,
|
|
361
|
+
dryRun: true,
|
|
362
|
+
restored: [],
|
|
363
|
+
launched: [],
|
|
364
|
+
resumed: [],
|
|
365
|
+
resumeAgents: resume,
|
|
366
|
+
});
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const restored: string[] = [];
|
|
371
|
+
const launched: string[] = [];
|
|
372
|
+
const resumed: ResumedSession[] = [];
|
|
373
|
+
const recordResumed = (session: string, panes: string[]) => {
|
|
374
|
+
if (panes.length) resumed.push({ session, panes });
|
|
375
|
+
};
|
|
376
|
+
for (const action of plan.actions) {
|
|
377
|
+
if (action.kind === "skip") continue;
|
|
378
|
+
if (action.kind === "launch") {
|
|
379
|
+
const ok = await launchProject(action.dir, json);
|
|
380
|
+
if (ok) launched.push(action.session);
|
|
381
|
+
else {
|
|
382
|
+
// Launch failed — fall back to a raw rebuild from the snapshot so the
|
|
383
|
+
// session still comes back.
|
|
384
|
+
const snap = snapshot.sessions.find((s) => s.name === action.session);
|
|
385
|
+
if (snap) {
|
|
386
|
+
recordResumed(snap.name, rebuildSession(snap, { runCommands, resumeAgents: resume }));
|
|
387
|
+
if (snap.adopted) safeAdopt(snap.name);
|
|
388
|
+
restored.push(action.session);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
recordResumed(
|
|
394
|
+
action.session.name,
|
|
395
|
+
rebuildSession(action.session, { runCommands, resumeAgents: resume }),
|
|
396
|
+
);
|
|
397
|
+
if (action.session.adopted) safeAdopt(action.session.name);
|
|
398
|
+
restored.push(action.session.name);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
reportPlan(plan, snapshot, {
|
|
402
|
+
json,
|
|
403
|
+
dryRun: false,
|
|
404
|
+
restored,
|
|
405
|
+
launched,
|
|
406
|
+
resumed,
|
|
407
|
+
resumeAgents: resume,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Adopt a restored session into the chrome; never let a chrome failure abort restore. */
|
|
412
|
+
function safeAdopt(session: string): void {
|
|
413
|
+
try {
|
|
414
|
+
adoptSession(session);
|
|
415
|
+
} catch {
|
|
416
|
+
// chrome is optional — the session is already rebuilt
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Relaunch a project from its ide.yml (config is the source of truth). Returns
|
|
422
|
+
* false on any failure so the caller can fall back to a raw rebuild. Launch's
|
|
423
|
+
* own stdout is swallowed in `--json` mode so the report stays parseable.
|
|
424
|
+
*/
|
|
425
|
+
async function launchProject(dir: string, json: boolean): Promise<boolean> {
|
|
426
|
+
const restoreLog = console.log;
|
|
427
|
+
if (json) console.log = () => {};
|
|
428
|
+
try {
|
|
429
|
+
const { launch } = await import("./launch.ts");
|
|
430
|
+
await launch(dir, { attach: false });
|
|
431
|
+
return true;
|
|
432
|
+
} catch {
|
|
433
|
+
return false;
|
|
434
|
+
} finally {
|
|
435
|
+
console.log = restoreLog;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
interface ReportContext {
|
|
440
|
+
json: boolean;
|
|
441
|
+
dryRun: boolean;
|
|
442
|
+
restored: string[];
|
|
443
|
+
launched: string[];
|
|
444
|
+
/** Per-session resumed agent panes (empty on dry-run — see `resumeAgents`). */
|
|
445
|
+
resumed: ResumedSession[];
|
|
446
|
+
/** Whether resume is on (drives the dry-run `(would resume N agents)` note). */
|
|
447
|
+
resumeAgents: boolean;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function reportPlan(
|
|
451
|
+
plan: RestorePlan,
|
|
452
|
+
snapshot: FleetSnapshot,
|
|
453
|
+
{ json, dryRun, restored, launched, resumed, resumeAgents }: ReportContext,
|
|
454
|
+
): void {
|
|
455
|
+
const skipped = plan.actions.filter((a) => a.kind === "skip").map((a) => a.session);
|
|
456
|
+
const willLaunch = plan.actions.filter((a) => a.kind === "launch").map((a) => a.session);
|
|
457
|
+
const willRebuild = plan.actions.filter((a) => a.kind === "rebuild").map((a) => a.session.name);
|
|
458
|
+
const resumedPanes = resumed.reduce((n, r) => n + r.panes.length, 0);
|
|
459
|
+
|
|
460
|
+
if (json) {
|
|
461
|
+
console.log(
|
|
462
|
+
JSON.stringify(
|
|
463
|
+
{
|
|
464
|
+
dryRun,
|
|
465
|
+
savedAt: snapshot.savedAt,
|
|
466
|
+
skipped,
|
|
467
|
+
launched: dryRun ? willLaunch : launched,
|
|
468
|
+
restored: dryRun ? willRebuild : restored,
|
|
469
|
+
panes: plan.paneCount,
|
|
470
|
+
resumeAgents,
|
|
471
|
+
resumedPanes,
|
|
472
|
+
resumed,
|
|
473
|
+
},
|
|
474
|
+
null,
|
|
475
|
+
2,
|
|
476
|
+
),
|
|
477
|
+
);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (dryRun) {
|
|
482
|
+
console.log(`Restore plan (snapshot from ${snapshot.savedAt}):`);
|
|
483
|
+
for (const action of plan.actions) {
|
|
484
|
+
if (action.kind === "skip") {
|
|
485
|
+
console.log(` skip ${action.session} (already running)`);
|
|
486
|
+
} else if (action.kind === "launch") {
|
|
487
|
+
console.log(` launch ${action.session} (ide.yml at ${action.dir})`);
|
|
488
|
+
} else {
|
|
489
|
+
const w = action.session.windows.length;
|
|
490
|
+
const p = action.session.windows.reduce((n, win) => n + win.panes.length, 0);
|
|
491
|
+
const wouldResume = countResumableAgents(action.session, resumeAgents);
|
|
492
|
+
const resumeNote = wouldResume
|
|
493
|
+
? `, would resume ${wouldResume} agent${wouldResume === 1 ? "" : "s"}`
|
|
494
|
+
: "";
|
|
495
|
+
console.log(
|
|
496
|
+
` rebuild ${action.session.name} (${w} window${w === 1 ? "" : "s"}, ${p} pane${p === 1 ? "" : "s"}${resumeNote})`,
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const resumedBySession = new Map(resumed.map((r) => [r.session, r.panes.length]));
|
|
504
|
+
const resumeSuffix = (name: string) => {
|
|
505
|
+
const n = resumedBySession.get(name) ?? 0;
|
|
506
|
+
return n ? ` (resumed ${n} agent${n === 1 ? "" : "s"})` : "";
|
|
507
|
+
};
|
|
508
|
+
const parts: string[] = [];
|
|
509
|
+
if (restored.length)
|
|
510
|
+
parts.push(`rebuilt ${restored.map((s) => `${s}${resumeSuffix(s)}`).join(", ")}`);
|
|
511
|
+
if (launched.length) parts.push(`launched ${launched.join(", ")}`);
|
|
512
|
+
if (skipped.length) parts.push(`skipped ${skipped.join(", ")} (already running)`);
|
|
513
|
+
console.log(parts.length ? `Restored: ${parts.join("; ")}` : "Nothing to restore.");
|
|
514
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem browsing contracts. Powers the dashboard's directory picker
|
|
3
|
+
* (used in AddProjectDialog and elsewhere). The shape mirrors t3code's
|
|
4
|
+
* `FilesystemBrowseResult` so the consuming pattern (browse on partial
|
|
5
|
+
* path, render entries, click to navigate) ports straight across.
|
|
6
|
+
*
|
|
7
|
+
* The shape is FROZEN — the dashboard imports these via `@tmux-ide/schemas`.
|
|
8
|
+
* Add new fields by appending optional properties; do not rename existing
|
|
9
|
+
* fields without bumping a major.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
|
|
14
|
+
export const FilesystemEntrySchemaZ = z.object({
|
|
15
|
+
/** Basename only (e.g. "src", "package.json"). Never contains a slash. */
|
|
16
|
+
name: z.string(),
|
|
17
|
+
/** Absolute, canonical path to the entry. */
|
|
18
|
+
fullPath: z.string(),
|
|
19
|
+
/** True if the entry is a directory (or a symlink pointing at one). */
|
|
20
|
+
isDir: z.boolean(),
|
|
21
|
+
/** True if the dirent itself is a symlink (regardless of target type). */
|
|
22
|
+
isSymlink: z.boolean(),
|
|
23
|
+
});
|
|
24
|
+
export type FilesystemEntry = z.infer<typeof FilesystemEntrySchemaZ>;
|
|
25
|
+
|
|
26
|
+
export const FilesystemBrowseResultSchemaZ = z.object({
|
|
27
|
+
/** Canonical absolute path of the directory listed. */
|
|
28
|
+
path: z.string(),
|
|
29
|
+
/** Parent directory path, or null if at filesystem / sandbox root. */
|
|
30
|
+
parentPath: z.string().nullable(),
|
|
31
|
+
/** Directories first, then files; alphabetical (case-insensitive) within group. */
|
|
32
|
+
entries: z.array(FilesystemEntrySchemaZ),
|
|
33
|
+
});
|
|
34
|
+
export type FilesystemBrowseResult = z.infer<typeof FilesystemBrowseResultSchemaZ>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export {
|
|
2
|
+
PaneSchema,
|
|
3
|
+
RowSchema,
|
|
4
|
+
ThemeConfigSchema,
|
|
5
|
+
OrchestratorYamlConfigSchema,
|
|
6
|
+
IdeConfigSchema,
|
|
7
|
+
PaneActionSchema,
|
|
8
|
+
SessionStateSchema,
|
|
9
|
+
} from "./ide-config.ts";
|
|
10
|
+
|
|
11
|
+
export type {
|
|
12
|
+
Pane,
|
|
13
|
+
Row,
|
|
14
|
+
ThemeConfig,
|
|
15
|
+
OrchestratorYamlConfig,
|
|
16
|
+
IdeConfig,
|
|
17
|
+
PaneAction,
|
|
18
|
+
SessionState,
|
|
19
|
+
} from "./ide-config.ts";
|
|
20
|
+
|
|
21
|
+
export { SessionOverviewSchemaZ, PaneInfoSchemaZ } from "./domain.ts";
|
|
22
|
+
|
|
23
|
+
export type { SessionOverview, PaneInfo } from "./domain.ts";
|
|
24
|
+
|
|
25
|
+
export { ClientFrameSchemaZ, ServerFrameSchemaZ, SessionSnapshotSchemaZ } from "./ws-events.ts";
|
|
26
|
+
|
|
27
|
+
export type { ClientFrame, ServerFrame, SessionSnapshot } from "./ws-events.ts";
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
RegisteredProjectSchemaZ,
|
|
31
|
+
RegisterProjectRequestSchemaZ,
|
|
32
|
+
InitProjectRequestSchemaZ,
|
|
33
|
+
ProjectTemplateSchemaZ,
|
|
34
|
+
} from "./registry.ts";
|
|
35
|
+
|
|
36
|
+
export type {
|
|
37
|
+
RegisteredProject,
|
|
38
|
+
RegisterProjectRequest,
|
|
39
|
+
InitProjectRequest,
|
|
40
|
+
ProjectTemplate,
|
|
41
|
+
} from "./registry.ts";
|
|
42
|
+
|
|
43
|
+
export { FilesystemEntrySchemaZ, FilesystemBrowseResultSchemaZ } from "./filesystem.ts";
|
|
44
|
+
|
|
45
|
+
export type { FilesystemEntry, FilesystemBrowseResult } from "./filesystem.ts";
|
|
46
|
+
|
|
47
|
+
export {
|
|
48
|
+
ProjectInspectDetectedSchemaZ,
|
|
49
|
+
ProjectInspectSchemaZ,
|
|
50
|
+
InspectFilesystemRequestSchemaZ,
|
|
51
|
+
OnboardProjectRequestSchemaZ,
|
|
52
|
+
} from "./inspect.ts";
|
|
53
|
+
|
|
54
|
+
export type {
|
|
55
|
+
ProjectInspectDetected,
|
|
56
|
+
ProjectInspect,
|
|
57
|
+
InspectFilesystemRequest,
|
|
58
|
+
OnboardProjectRequest,
|
|
59
|
+
} from "./inspect.ts";
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project inspect + onboard contracts. Used by the dashboard's "Add project"
|
|
3
|
+
* dialog to inspect an arbitrary directory (without registering it) and to
|
|
4
|
+
* onboard a directory that has no `ide.yml` yet (compose, write, register).
|
|
5
|
+
*
|
|
6
|
+
* The shape is FROZEN — the dashboard imports these via `@tmux-ide/schemas`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
|
|
11
|
+
export const ProjectInspectDetectedSchemaZ = z.object({
|
|
12
|
+
/** Detected package manager from lockfile, or `null`. */
|
|
13
|
+
packageManager: z.enum(["pnpm", "npm", "yarn", "bun"]).nullable(),
|
|
14
|
+
/** Detected frameworks (e.g. `["next", "convex"]`). Empty array when none. */
|
|
15
|
+
frameworks: z.array(z.string()),
|
|
16
|
+
/** Suggested dev command (e.g. `pnpm dev`). `null` if no dev script found. */
|
|
17
|
+
devCommand: z.string().nullable(),
|
|
18
|
+
/** Suggested test command (e.g. `pnpm test`). `null` if no test script found. */
|
|
19
|
+
testCommand: z.string().nullable(),
|
|
20
|
+
});
|
|
21
|
+
export type ProjectInspectDetected = z.infer<typeof ProjectInspectDetectedSchemaZ>;
|
|
22
|
+
|
|
23
|
+
export const ProjectInspectSchemaZ = z.object({
|
|
24
|
+
/** Sanitized basename of the directory — safe to use as a tmux session name. */
|
|
25
|
+
name: z.string(),
|
|
26
|
+
/** Absolute, canonical path to the directory. */
|
|
27
|
+
dir: z.string(),
|
|
28
|
+
/** Whether `<dir>/ide.yml` exists. */
|
|
29
|
+
hasIdeYml: z.boolean(),
|
|
30
|
+
/** Git remote origin URL, or `null` if not a git repo / no origin / probe failed. */
|
|
31
|
+
gitOrigin: z.string().nullable(),
|
|
32
|
+
/** Current git branch, or `null` if not a git repo / detached HEAD / probe failed. */
|
|
33
|
+
gitBranch: z.string().nullable(),
|
|
34
|
+
/** Detected stack signals (reuses `tmux-ide detect` logic). */
|
|
35
|
+
detected: ProjectInspectDetectedSchemaZ,
|
|
36
|
+
});
|
|
37
|
+
export type ProjectInspect = z.infer<typeof ProjectInspectSchemaZ>;
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// REST request bodies
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
export const InspectFilesystemRequestSchemaZ = z.object({
|
|
44
|
+
dir: z.string().min(1),
|
|
45
|
+
});
|
|
46
|
+
export type InspectFilesystemRequest = z.infer<typeof InspectFilesystemRequestSchemaZ>;
|
|
47
|
+
|
|
48
|
+
export const OnboardProjectRequestSchemaZ = z.object({
|
|
49
|
+
dir: z.string().min(1),
|
|
50
|
+
/** Optional override for the project name — defaults to inspect.name. */
|
|
51
|
+
name: z.string().min(1).optional(),
|
|
52
|
+
/** 1, 2, or 3 — how many Claude panes to scaffold in the top row. */
|
|
53
|
+
agents: z.number().int().min(1).max(3),
|
|
54
|
+
/**
|
|
55
|
+
* Optional per-agent pane titles. When provided, length must equal
|
|
56
|
+
* `agents`; the server uses these as `title:` for the Claude panes
|
|
57
|
+
* instead of the canonical `Lead`/`Teammate N`/`Claude N` defaults.
|
|
58
|
+
*/
|
|
59
|
+
agentNames: z.array(z.string().min(1)).optional(),
|
|
60
|
+
/** Dev server command (e.g. `pnpm dev`). Omit / null to skip the dev pane. */
|
|
61
|
+
devCommand: z.string().min(1).nullable().optional(),
|
|
62
|
+
/** Test command (e.g. `pnpm test`). Currently informational; stored for later. */
|
|
63
|
+
testCommand: z.string().min(1).nullable().optional(),
|
|
64
|
+
/** Lint command (e.g. `pnpm lint`). Currently informational; stored for later. */
|
|
65
|
+
lintCommand: z.string().min(1).nullable().optional(),
|
|
66
|
+
});
|
|
67
|
+
export type OnboardProjectRequest = z.infer<typeof OnboardProjectRequestSchemaZ>;
|