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,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed errors for the v2 action dispatcher. Every error path through a
|
|
3
|
+
* handler funnels into one of these so the dispatcher can map to the
|
|
4
|
+
* `{ ok: false, error: { code, message, details? } }` envelope without
|
|
5
|
+
* stringly-typed catches.
|
|
6
|
+
*
|
|
7
|
+
* Codes are stable wire identifiers — never rename without a coordinated
|
|
8
|
+
* dashboard release. New codes go at the bottom of the union.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { TerminalCwdError, type TerminalCwdErrorReason } from "../../server/pty-bridge.ts";
|
|
12
|
+
|
|
13
|
+
export type ActionErrorCode =
|
|
14
|
+
| "project_not_found"
|
|
15
|
+
| "cwd_not_found"
|
|
16
|
+
| "cwd_not_directory"
|
|
17
|
+
| "cwd_stat_failed"
|
|
18
|
+
| "session_already_running"
|
|
19
|
+
| "session_not_running"
|
|
20
|
+
| "launch_failed"
|
|
21
|
+
| "stop_failed"
|
|
22
|
+
| "terminal_not_found"
|
|
23
|
+
| "validation_failed"
|
|
24
|
+
| "task_not_found"
|
|
25
|
+
| "goal_not_found"
|
|
26
|
+
| "milestone_not_found"
|
|
27
|
+
| "mission_not_set"
|
|
28
|
+
| "task_dependency_unmet"
|
|
29
|
+
| "task_already_assigned"
|
|
30
|
+
| "skill_invalid"
|
|
31
|
+
| "skill_not_found"
|
|
32
|
+
| "ide_yml_missing"
|
|
33
|
+
| "config_path_invalid"
|
|
34
|
+
| "config_validation_failed"
|
|
35
|
+
| "validation_assertion_not_found"
|
|
36
|
+
| "webhook_not_found"
|
|
37
|
+
| "webhook_test_failed"
|
|
38
|
+
| "remote_access_restart_failed"
|
|
39
|
+
| "shutdown_already_in_progress"
|
|
40
|
+
| "bad_request"
|
|
41
|
+
| "thread_not_found"
|
|
42
|
+
| "permission_request_not_found"
|
|
43
|
+
| "internal";
|
|
44
|
+
|
|
45
|
+
export class ActionError extends Error {
|
|
46
|
+
readonly code: ActionErrorCode;
|
|
47
|
+
readonly details: unknown | undefined;
|
|
48
|
+
|
|
49
|
+
constructor(args: {
|
|
50
|
+
code: ActionErrorCode;
|
|
51
|
+
message: string;
|
|
52
|
+
details?: unknown;
|
|
53
|
+
cause?: unknown;
|
|
54
|
+
}) {
|
|
55
|
+
super(
|
|
56
|
+
args.message,
|
|
57
|
+
args.cause !== undefined ? { cause: args.cause as Error | undefined } : undefined,
|
|
58
|
+
);
|
|
59
|
+
this.name = "ActionError";
|
|
60
|
+
this.code = args.code;
|
|
61
|
+
this.details = args.details;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
toEnvelope(): {
|
|
65
|
+
code: ActionErrorCode;
|
|
66
|
+
message: string;
|
|
67
|
+
details?: unknown;
|
|
68
|
+
} {
|
|
69
|
+
return this.details !== undefined
|
|
70
|
+
? { code: this.code, message: this.message, details: this.details }
|
|
71
|
+
: { code: this.code, message: this.message };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const CWD_REASON_TO_CODE: Record<TerminalCwdErrorReason, ActionErrorCode> = {
|
|
76
|
+
notFound: "cwd_not_found",
|
|
77
|
+
notDirectory: "cwd_not_directory",
|
|
78
|
+
statFailed: "cwd_stat_failed",
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Translate a {@link TerminalCwdError} into an {@link ActionError} with the
|
|
83
|
+
* matching wire code. The cwd is preserved in `details` so the dashboard can
|
|
84
|
+
* surface it without re-parsing the error message.
|
|
85
|
+
*/
|
|
86
|
+
export function actionErrorFromCwdError(err: TerminalCwdError): ActionError {
|
|
87
|
+
return new ActionError({
|
|
88
|
+
code: CWD_REASON_TO_CODE[err.reason],
|
|
89
|
+
message: err.message,
|
|
90
|
+
details: { cwd: err.cwd, reason: err.reason },
|
|
91
|
+
cause: err,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Wrap an unknown thrown value as an `internal` ActionError. Used as the
|
|
97
|
+
* last-resort branch in dispatcher catch blocks so handlers never leak raw
|
|
98
|
+
* Error stacks across the wire.
|
|
99
|
+
*/
|
|
100
|
+
export function wrapInternalError(err: unknown): ActionError {
|
|
101
|
+
if (err instanceof ActionError) return err;
|
|
102
|
+
if (err instanceof TerminalCwdError) return actionErrorFromCwdError(err);
|
|
103
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
104
|
+
return new ActionError({ code: "internal", message, cause: err });
|
|
105
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { getSessionName } from "../../../lib/yaml-io.ts";
|
|
3
|
+
import { resolveProject, type ProjectResolverDeps } from "./_resolve-project.ts";
|
|
4
|
+
|
|
5
|
+
export interface ProjectContextDeps extends ProjectResolverDeps {
|
|
6
|
+
cwd?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ProjectContextInput {
|
|
10
|
+
projectName?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ProjectContext {
|
|
14
|
+
dir: string;
|
|
15
|
+
sessionName: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function resolveProjectContext(
|
|
19
|
+
input: ProjectContextInput,
|
|
20
|
+
deps: ProjectContextDeps = {},
|
|
21
|
+
): ProjectContext {
|
|
22
|
+
if (input.projectName) {
|
|
23
|
+
const project = resolveProject(input.projectName, deps);
|
|
24
|
+
return { dir: project.dir, sessionName: project.sessionName };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const dir = deps.cwd ?? process.cwd();
|
|
28
|
+
const sessionName = getSessionName(dir).name || basename(dir);
|
|
29
|
+
return { dir, sessionName };
|
|
30
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helper: resolve a registered project by name and project the
|
|
3
|
+
* fields the action handlers actually consume.
|
|
4
|
+
*
|
|
5
|
+
* Lives in handlers/ so the registry stays the only public surface; tests
|
|
6
|
+
* import from here directly.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { getProject as getRegisteredProject } from "../../../lib/project-registry.ts";
|
|
10
|
+
import {
|
|
11
|
+
getSessionCwd as getSessionCwdDefault,
|
|
12
|
+
hasSession as hasSessionDefault,
|
|
13
|
+
} from "@tmux-ide/tmux-bridge";
|
|
14
|
+
import type { RegisteredProject } from "../../../schemas/registry.ts";
|
|
15
|
+
import { ActionError } from "../errors.ts";
|
|
16
|
+
|
|
17
|
+
export interface ResolvedProject {
|
|
18
|
+
name: string;
|
|
19
|
+
dir: string;
|
|
20
|
+
sessionName: string;
|
|
21
|
+
/**
|
|
22
|
+
* `true` when the project was resolved from a live tmux session
|
|
23
|
+
* (rather than the registry). Useful for handlers that want to
|
|
24
|
+
* auto-register on first action — out of scope for this slice but
|
|
25
|
+
* makes the intent explicit at the boundary.
|
|
26
|
+
*/
|
|
27
|
+
fromLiveSession: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ProjectResolverDeps {
|
|
31
|
+
/** Override for tests — must satisfy the same contract as `getProject`. */
|
|
32
|
+
getProject?: (name: string) => RegisteredProject | null;
|
|
33
|
+
/** Override the live-session existence probe. */
|
|
34
|
+
hasSession?: (session: string) => boolean;
|
|
35
|
+
/** Override the live-session cwd lookup. */
|
|
36
|
+
getSessionCwd?: (session: string) => string | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve a project by name. The registry is the canonical source of
|
|
41
|
+
* truth, but as a fallback we also accept a live tmux session by the
|
|
42
|
+
* same name — the dashboard merges registered projects + live sessions
|
|
43
|
+
* in its sidebar, so action handlers must accept either. Throws an
|
|
44
|
+
* {@link ActionError} with code `project_not_found` when neither path
|
|
45
|
+
* succeeds.
|
|
46
|
+
*
|
|
47
|
+
* Note: the registry does not currently persist a session name distinct
|
|
48
|
+
* from the project name, so the session name returned here mirrors the
|
|
49
|
+
* project name. If they diverge in the future, this is the only call site
|
|
50
|
+
* that needs to change.
|
|
51
|
+
*/
|
|
52
|
+
export function resolveProject(name: string, deps: ProjectResolverDeps = {}): ResolvedProject {
|
|
53
|
+
const lookup = deps.getProject ?? getRegisteredProject;
|
|
54
|
+
const project = lookup(name);
|
|
55
|
+
if (project) {
|
|
56
|
+
return {
|
|
57
|
+
name: project.name,
|
|
58
|
+
dir: project.dir,
|
|
59
|
+
sessionName: project.name,
|
|
60
|
+
fromLiveSession: false,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Registry miss — fall back to a live tmux session of the same name.
|
|
65
|
+
const hasSession = deps.hasSession ?? hasSessionDefault;
|
|
66
|
+
if (hasSession(name)) {
|
|
67
|
+
const cwd = (deps.getSessionCwd ?? getSessionCwdDefault)(name);
|
|
68
|
+
if (cwd) {
|
|
69
|
+
return { name, dir: cwd, sessionName: name, fromLiveSession: true };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
throw new ActionError({
|
|
74
|
+
code: "project_not_found",
|
|
75
|
+
message: `Project "${name}" not found in registry or as a live tmux session`,
|
|
76
|
+
details: { name },
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { hostname, networkInterfaces } from "node:os";
|
|
2
|
+
import { generateAuthToken } from "../../../lib/auth-token.ts";
|
|
3
|
+
import { readAppSettings, writeAppSettings, type AppSettings } from "../../../lib/app-settings.ts";
|
|
4
|
+
import type { ActionInput, ActionResult } from "../contract.ts";
|
|
5
|
+
|
|
6
|
+
export interface RemoteAccessRestartRequest {
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
bindHostname: "0.0.0.0" | "127.0.0.1";
|
|
9
|
+
token: string | null;
|
|
10
|
+
port?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface RemoteAccessRestartResult {
|
|
14
|
+
port?: number;
|
|
15
|
+
host?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface AppSetRemoteAccessDeps {
|
|
19
|
+
readSettings?: () => AppSettings;
|
|
20
|
+
writeSettings?: (next: AppSettings) => void;
|
|
21
|
+
generateToken?: () => string;
|
|
22
|
+
restartDaemon?: (
|
|
23
|
+
request: RemoteAccessRestartRequest,
|
|
24
|
+
) => Promise<RemoteAccessRestartResult> | RemoteAccessRestartResult;
|
|
25
|
+
deferRestart?: (restart: () => void) => void;
|
|
26
|
+
port?: number;
|
|
27
|
+
host?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let remoteAccessRestartBackend:
|
|
31
|
+
| ((
|
|
32
|
+
request: RemoteAccessRestartRequest,
|
|
33
|
+
) => Promise<RemoteAccessRestartResult> | RemoteAccessRestartResult)
|
|
34
|
+
| null = null;
|
|
35
|
+
|
|
36
|
+
export function setRemoteAccessRestartBackend(
|
|
37
|
+
backend:
|
|
38
|
+
| ((
|
|
39
|
+
request: RemoteAccessRestartRequest,
|
|
40
|
+
) => Promise<RemoteAccessRestartResult> | RemoteAccessRestartResult)
|
|
41
|
+
| null,
|
|
42
|
+
): void {
|
|
43
|
+
remoteAccessRestartBackend = backend;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function currentPort(deps: AppSetRemoteAccessDeps): number {
|
|
47
|
+
const envPort = Number(process.env.TMUX_IDE_DAEMON_PORT);
|
|
48
|
+
return deps.port ?? (Number.isInteger(envPort) && envPort > 0 ? envPort : 6060);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function primaryLanHost(): string {
|
|
52
|
+
const interfaces = networkInterfaces();
|
|
53
|
+
for (const entries of Object.values(interfaces)) {
|
|
54
|
+
for (const entry of entries ?? []) {
|
|
55
|
+
if (entry.family === "IPv4" && !entry.internal) return entry.address;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return hostname();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function buildUrl(host: string, port: number): string {
|
|
62
|
+
return `http://${host}:${port}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function defaultDeferRestart(restart: () => void): void {
|
|
66
|
+
setImmediate(restart);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function appSetRemoteAccessHandler(
|
|
70
|
+
input: ActionInput<"app.setRemoteAccess">,
|
|
71
|
+
deps: AppSetRemoteAccessDeps = {},
|
|
72
|
+
): Promise<ActionResult<"app.setRemoteAccess">> {
|
|
73
|
+
const readSettings = deps.readSettings ?? readAppSettings;
|
|
74
|
+
const writeSettings = deps.writeSettings ?? writeAppSettings;
|
|
75
|
+
const nextEnabled = input.enabled;
|
|
76
|
+
const current = readSettings();
|
|
77
|
+
const token = nextEnabled
|
|
78
|
+
? (current.remoteAccess.token ?? (deps.generateToken ?? generateAuthToken)())
|
|
79
|
+
: null;
|
|
80
|
+
const next: AppSettings = {
|
|
81
|
+
...current,
|
|
82
|
+
remoteAccess: { enabled: nextEnabled, token },
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
writeSettings(next);
|
|
86
|
+
|
|
87
|
+
const port = currentPort(deps);
|
|
88
|
+
const request: RemoteAccessRestartRequest = {
|
|
89
|
+
enabled: nextEnabled,
|
|
90
|
+
bindHostname: nextEnabled ? "0.0.0.0" : "127.0.0.1",
|
|
91
|
+
token,
|
|
92
|
+
port,
|
|
93
|
+
};
|
|
94
|
+
const restartDaemon = deps.restartDaemon ?? remoteAccessRestartBackend;
|
|
95
|
+
if (restartDaemon) {
|
|
96
|
+
(deps.deferRestart ?? defaultDeferRestart)(() => {
|
|
97
|
+
void Promise.resolve(restartDaemon(request)).catch((err) => {
|
|
98
|
+
console.error(
|
|
99
|
+
`[actions] Failed to restart daemon for remote access: ${(err as Error).message ?? String(err)}`,
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!nextEnabled) {
|
|
106
|
+
return { enabled: false, url: null, token: null, qrPayload: null };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const host = deps.host ?? primaryLanHost();
|
|
110
|
+
const url = buildUrl(host, port);
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
enabled: true,
|
|
114
|
+
url,
|
|
115
|
+
token,
|
|
116
|
+
qrPayload: `${url}?token=${encodeURIComponent(token ?? "")}`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
configAddPane,
|
|
5
|
+
configAddRow,
|
|
6
|
+
configDisableTeam,
|
|
7
|
+
configEnableTeam,
|
|
8
|
+
configRemovePane,
|
|
9
|
+
configSetValue,
|
|
10
|
+
} from "../../../config.ts";
|
|
11
|
+
import { broadcastConfigChanged as broadcastConfigChangedDefault } from "../../ws-events.ts";
|
|
12
|
+
import { ActionError } from "../errors.ts";
|
|
13
|
+
import type { ActionInput, ActionResult } from "../contract.ts";
|
|
14
|
+
import { resolveProjectContext, type ProjectContextDeps } from "./_project-context.ts";
|
|
15
|
+
|
|
16
|
+
interface ConfigActionDeps extends ProjectContextDeps {
|
|
17
|
+
broadcastConfigChanged?: (sessionName: string) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function mutateConfigAction<Result>(
|
|
21
|
+
input: { projectName?: string },
|
|
22
|
+
deps: ConfigActionDeps,
|
|
23
|
+
fn: (dir: string) => Result,
|
|
24
|
+
): Result {
|
|
25
|
+
const context = resolveProjectContext(input, deps);
|
|
26
|
+
if (!existsSync(join(context.dir, "ide.yml"))) {
|
|
27
|
+
throw new ActionError({
|
|
28
|
+
code: "ide_yml_missing",
|
|
29
|
+
message: "ide.yml was not found",
|
|
30
|
+
details: { dir: context.dir },
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const result = fn(context.dir);
|
|
35
|
+
(deps.broadcastConfigChanged ?? broadcastConfigChangedDefault)(context.sessionName);
|
|
36
|
+
return result;
|
|
37
|
+
} catch (err) {
|
|
38
|
+
const message = (err as Error).message ?? String(err);
|
|
39
|
+
throw new ActionError({
|
|
40
|
+
code: message.toLowerCase().includes("path")
|
|
41
|
+
? "config_path_invalid"
|
|
42
|
+
: "config_validation_failed",
|
|
43
|
+
message,
|
|
44
|
+
cause: err,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function configSetHandler(
|
|
50
|
+
input: ActionInput<"config.set">,
|
|
51
|
+
deps: ConfigActionDeps = {},
|
|
52
|
+
): ActionResult<"config.set"> {
|
|
53
|
+
const config = mutateConfigAction(input, deps, (dir) =>
|
|
54
|
+
configSetValue(dir, input.path, input.value),
|
|
55
|
+
);
|
|
56
|
+
return { config };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function configAddPaneHandler(
|
|
60
|
+
input: ActionInput<"config.addPane">,
|
|
61
|
+
deps: ConfigActionDeps = {},
|
|
62
|
+
): ActionResult<"config.addPane"> {
|
|
63
|
+
const pane = {
|
|
64
|
+
title: input.title,
|
|
65
|
+
command: input.command,
|
|
66
|
+
type: input.type,
|
|
67
|
+
target: input.target,
|
|
68
|
+
dir: input.dir,
|
|
69
|
+
size: input.size,
|
|
70
|
+
focus: input.focus,
|
|
71
|
+
env: input.env,
|
|
72
|
+
role: input.role,
|
|
73
|
+
task: input.task,
|
|
74
|
+
specialty: input.specialty,
|
|
75
|
+
skill: input.skill,
|
|
76
|
+
};
|
|
77
|
+
const config = mutateConfigAction(input, deps, (dir) => configAddPane(dir, input.rowIndex, pane));
|
|
78
|
+
return { config };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function configRemovePaneHandler(
|
|
82
|
+
input: ActionInput<"config.removePane">,
|
|
83
|
+
deps: ConfigActionDeps = {},
|
|
84
|
+
): ActionResult<"config.removePane"> {
|
|
85
|
+
const config = mutateConfigAction(input, deps, (dir) =>
|
|
86
|
+
configRemovePane(dir, input.rowIndex, input.paneIndex),
|
|
87
|
+
).config;
|
|
88
|
+
return { config };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function configAddRowHandler(
|
|
92
|
+
input: ActionInput<"config.addRow">,
|
|
93
|
+
deps: ConfigActionDeps = {},
|
|
94
|
+
): ActionResult<"config.addRow"> {
|
|
95
|
+
const config = mutateConfigAction(input, deps, (dir) => configAddRow(dir, input.size));
|
|
96
|
+
return { config };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function configEnableTeamHandler(
|
|
100
|
+
input: ActionInput<"config.enableTeam">,
|
|
101
|
+
deps: ConfigActionDeps = {},
|
|
102
|
+
): ActionResult<"config.enableTeam"> {
|
|
103
|
+
const config = mutateConfigAction(input, deps, (dir) => configEnableTeam(dir, input.name));
|
|
104
|
+
return { config };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function configDisableTeamHandler(
|
|
108
|
+
input: ActionInput<"config.disableTeam">,
|
|
109
|
+
deps: ConfigActionDeps = {},
|
|
110
|
+
): ActionResult<"config.disableTeam"> {
|
|
111
|
+
const config = mutateConfigAction(input, deps, (dir) => configDisableTeam(dir));
|
|
112
|
+
return { config };
|
|
113
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ActionError } from "../errors.ts";
|
|
2
|
+
import type { ActionInput, ActionResult } from "../contract.ts";
|
|
3
|
+
|
|
4
|
+
export interface DaemonShutdownDeps {
|
|
5
|
+
shutdown?: (reason: string | null) => Promise<void> | void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
let shutdownBackend: ((reason: string | null) => Promise<void> | void) | null = null;
|
|
9
|
+
let shutdownInProgress = false;
|
|
10
|
+
|
|
11
|
+
export function setDaemonShutdownBackend(
|
|
12
|
+
backend: ((reason: string | null) => Promise<void> | void) | null,
|
|
13
|
+
): void {
|
|
14
|
+
shutdownBackend = backend;
|
|
15
|
+
if (!backend) shutdownInProgress = false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function daemonShutdownHandler(
|
|
19
|
+
input: ActionInput<"daemon.shutdown">,
|
|
20
|
+
deps: DaemonShutdownDeps = {},
|
|
21
|
+
): ActionResult<"daemon.shutdown"> {
|
|
22
|
+
if (shutdownInProgress) {
|
|
23
|
+
throw new ActionError({
|
|
24
|
+
code: "shutdown_already_in_progress",
|
|
25
|
+
message: "Daemon shutdown is already in progress",
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
shutdownInProgress = true;
|
|
30
|
+
const shutdown = deps.shutdown ?? shutdownBackend;
|
|
31
|
+
process.nextTick(() => {
|
|
32
|
+
void Promise.resolve(shutdown?.(input.reason ?? null)).catch((err) => {
|
|
33
|
+
console.error("[daemon] shutdown action failed:", err);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return { stopping: true };
|
|
38
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { activateProject as activateProjectDefault } from "../../../lib/active-projects.ts";
|
|
2
|
+
import type { ProjectActivationOptions } from "../../../lib/active-projects.ts";
|
|
3
|
+
import { ActionError } from "../errors.ts";
|
|
4
|
+
import type { ActionInput, ActionResult } from "../contract.ts";
|
|
5
|
+
import { resolveProject, type ProjectResolverDeps } from "./_resolve-project.ts";
|
|
6
|
+
|
|
7
|
+
export interface ProjectActivateDeps extends ProjectResolverDeps {
|
|
8
|
+
activateProject?: (name: string, options?: ProjectActivationOptions) => Promise<void>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function projectActivateHandler(
|
|
12
|
+
input: ActionInput<"project.activate">,
|
|
13
|
+
deps: ProjectActivateDeps = {},
|
|
14
|
+
): Promise<ActionResult<"project.activate">> {
|
|
15
|
+
const project = resolveProject(input.name, deps);
|
|
16
|
+
const activateProject = deps.activateProject ?? activateProjectDefault;
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
await activateProject(project.name);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
throw new ActionError({
|
|
22
|
+
code: "internal",
|
|
23
|
+
message: `Failed to activate project "${project.name}": ${(err as Error).message ?? String(err)}`,
|
|
24
|
+
details: { projectName: project.name },
|
|
25
|
+
cause: err,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return { active: true, projectName: project.name };
|
|
30
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handler: `project.launch`.
|
|
3
|
+
*
|
|
4
|
+
* Idempotent programmatic launch. Resolves the project from the registry,
|
|
5
|
+
* checks whether a session is already running, and returns
|
|
6
|
+
* `{ started: false }` without invoking the launcher when so. Otherwise
|
|
7
|
+
* delegates to `src/launch.ts` with `attach: false`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { hasSession as hasSessionDefault } from "@tmux-ide/tmux-bridge";
|
|
11
|
+
import { launch as launchDefault } from "../../../launch.ts";
|
|
12
|
+
import { ActionError } from "../errors.ts";
|
|
13
|
+
import { type ActionInput, type ActionResult } from "../contract.ts";
|
|
14
|
+
import { resolveProject, type ProjectResolverDeps } from "./_resolve-project.ts";
|
|
15
|
+
import { getDefaultWorkspaceRegistry } from "../../../lib/workspace-registry.ts";
|
|
16
|
+
|
|
17
|
+
export interface ProjectLaunchDeps extends ProjectResolverDeps {
|
|
18
|
+
hasSession?: (session: string) => boolean;
|
|
19
|
+
launch?: (dir: string, options: { json: boolean; attach: boolean }) => Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Ensure the workspace registry knows about this project. The project
|
|
24
|
+
* registry (`registerProject`) only updates the project-list — but
|
|
25
|
+
* `discoverSessions` / `/api/project/:name` consult the WORKSPACE registry
|
|
26
|
+
* before returning anything. Without this sync, every dashboard fetch
|
|
27
|
+
* 404s for projects that weren't passed as the daemon's primary sessionName.
|
|
28
|
+
*/
|
|
29
|
+
function ensureWorkspaceRegistered(name: string, sessionName: string, dir: string): void {
|
|
30
|
+
const reg = getDefaultWorkspaceRegistry();
|
|
31
|
+
if (reg.has(name)) return;
|
|
32
|
+
try {
|
|
33
|
+
reg.add({ name, sessionName, projectDir: dir });
|
|
34
|
+
} catch {
|
|
35
|
+
// ALREADY_EXISTS or persistence error — non-fatal; the next discover
|
|
36
|
+
// pass will pick it up if persistence eventually succeeds.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function projectLaunchHandler(
|
|
41
|
+
input: ActionInput<"project.launch">,
|
|
42
|
+
deps: ProjectLaunchDeps = {},
|
|
43
|
+
): Promise<ActionResult<"project.launch">> {
|
|
44
|
+
const project = resolveProject(input.name, deps);
|
|
45
|
+
const hasSession = deps.hasSession ?? hasSessionDefault;
|
|
46
|
+
|
|
47
|
+
if (hasSession(project.sessionName)) {
|
|
48
|
+
// Session already running — but the workspace registry may still be
|
|
49
|
+
// stale (e.g. tmux session created via curl, or daemon restarted).
|
|
50
|
+
// Sync before returning so /api/project/:name resolves.
|
|
51
|
+
ensureWorkspaceRegistered(project.name, project.sessionName, project.dir);
|
|
52
|
+
return { sessionName: project.sessionName, started: false };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const launch = deps.launch ?? launchDefault;
|
|
56
|
+
try {
|
|
57
|
+
await launch(project.dir, { json: false, attach: false });
|
|
58
|
+
} catch (err) {
|
|
59
|
+
throw new ActionError({
|
|
60
|
+
code: "launch_failed",
|
|
61
|
+
message: `Failed to launch session "${project.sessionName}": ${(err as Error).message ?? String(err)}`,
|
|
62
|
+
details: { sessionName: project.sessionName, dir: project.dir },
|
|
63
|
+
cause: err,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Freshly launched — workspace registry definitely needs the entry.
|
|
68
|
+
ensureWorkspaceRegistered(project.name, project.sessionName, project.dir);
|
|
69
|
+
return { sessionName: project.sessionName, started: true };
|
|
70
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handler: `project.openTerminal`.
|
|
3
|
+
*
|
|
4
|
+
* Resolves the project's cwd from the registry, ensures the tmux session is
|
|
5
|
+
* running (launching it if necessary), and asserts the resolved cwd is a
|
|
6
|
+
* real directory. Returns the dashboard's stable terminal tab id together
|
|
7
|
+
* with the resolved cwd so the dashboard does not need to stitch together
|
|
8
|
+
* the registry / session / cwd paths itself.
|
|
9
|
+
*
|
|
10
|
+
* This is the action that swallows the seam where the dashboard previously
|
|
11
|
+
* juggled `/api/projects` + `/api/project/:name` to figure out where to
|
|
12
|
+
* open a terminal.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { hasSession as hasSessionDefault } from "@tmux-ide/tmux-bridge";
|
|
16
|
+
import { launch as launchDefault } from "../../../launch.ts";
|
|
17
|
+
import { activateProject as activateProjectDefault } from "../../../lib/active-projects.ts";
|
|
18
|
+
import { assertValidCwd, TerminalCwdError } from "../../../server/pty-bridge.ts";
|
|
19
|
+
import type { Stats } from "node:fs";
|
|
20
|
+
import { ActionError, actionErrorFromCwdError } from "../errors.ts";
|
|
21
|
+
import { type ActionInput, type ActionResult } from "../contract.ts";
|
|
22
|
+
import { resolveProject, type ProjectResolverDeps } from "./_resolve-project.ts";
|
|
23
|
+
|
|
24
|
+
const TERMINAL_TAB_ID_PREFIX = "terminal";
|
|
25
|
+
const TERMINAL_TAB_ID_SUFFIX = "default";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build the dashboard-facing terminal tab id for a session's default
|
|
29
|
+
* terminal. Mirrors the layout state convention `terminal:<session>:default`.
|
|
30
|
+
* Centralised here so both this handler and tests share the source of truth.
|
|
31
|
+
*/
|
|
32
|
+
export function defaultTerminalTabId(sessionName: string): string {
|
|
33
|
+
return `${TERMINAL_TAB_ID_PREFIX}:${sessionName}:${TERMINAL_TAB_ID_SUFFIX}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ProjectOpenTerminalDeps extends ProjectResolverDeps {
|
|
37
|
+
hasSession?: (session: string) => boolean;
|
|
38
|
+
launch?: (dir: string, options: { json: boolean; attach: boolean }) => Promise<void>;
|
|
39
|
+
statCwd?: (cwd: string) => Stats;
|
|
40
|
+
activateProject?: (name: string) => Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function projectOpenTerminalHandler(
|
|
44
|
+
input: ActionInput<"project.openTerminal">,
|
|
45
|
+
deps: ProjectOpenTerminalDeps = {},
|
|
46
|
+
): Promise<ActionResult<"project.openTerminal">> {
|
|
47
|
+
const project = resolveProject(input.name, deps);
|
|
48
|
+
const activateProject = deps.activateProject ?? activateProjectDefault;
|
|
49
|
+
await activateProject(project.name);
|
|
50
|
+
|
|
51
|
+
// Always validate cwd up-front. A missing/broken project dir is the most
|
|
52
|
+
// common cause of the bugs this slice exists to eliminate.
|
|
53
|
+
try {
|
|
54
|
+
assertValidCwd(project.dir, deps.statCwd);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
if (err instanceof TerminalCwdError) {
|
|
57
|
+
throw actionErrorFromCwdError(err);
|
|
58
|
+
}
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const hasSession = deps.hasSession ?? hasSessionDefault;
|
|
63
|
+
const launch = deps.launch ?? launchDefault;
|
|
64
|
+
|
|
65
|
+
let launched = false;
|
|
66
|
+
if (!hasSession(project.sessionName)) {
|
|
67
|
+
try {
|
|
68
|
+
// attach: false — we run inside the daemon HTTP server, not a TTY.
|
|
69
|
+
await launch(project.dir, { json: false, attach: false });
|
|
70
|
+
launched = true;
|
|
71
|
+
} catch (err) {
|
|
72
|
+
throw new ActionError({
|
|
73
|
+
code: "launch_failed",
|
|
74
|
+
message: `Failed to launch session "${project.sessionName}": ${(err as Error).message ?? String(err)}`,
|
|
75
|
+
details: { sessionName: project.sessionName, dir: project.dir },
|
|
76
|
+
cause: err,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
sessionName: project.sessionName,
|
|
83
|
+
cwd: project.dir,
|
|
84
|
+
terminalTabId: defaultTerminalTabId(project.sessionName),
|
|
85
|
+
launched,
|
|
86
|
+
};
|
|
87
|
+
}
|