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,483 @@
|
|
|
1
|
+
import type { RawData, WebSocket } from "ws";
|
|
2
|
+
import {
|
|
3
|
+
PtyBridge,
|
|
4
|
+
TerminalCwdError,
|
|
5
|
+
type PtyExit,
|
|
6
|
+
type PtySpawnOptions,
|
|
7
|
+
type TerminalCwdErrorReason,
|
|
8
|
+
} from "./pty-bridge.ts";
|
|
9
|
+
|
|
10
|
+
const WS_OPEN = 1;
|
|
11
|
+
const DEFAULT_BACKPRESSURE_BYTES = 1 << 20;
|
|
12
|
+
const DRAIN_POLL_MS = 16;
|
|
13
|
+
const DEFAULT_BRIDGE_IDLE_MS = 300000;
|
|
14
|
+
|
|
15
|
+
export interface PtyBridgeLike {
|
|
16
|
+
spawn(cols: number, rows: number, options?: PtySpawnOptions): void;
|
|
17
|
+
restartWith?(cols: number, rows: number, options?: PtySpawnOptions): void;
|
|
18
|
+
getCwd?(): string | null;
|
|
19
|
+
readonly running?: boolean;
|
|
20
|
+
getReplayBuffer?(): Buffer;
|
|
21
|
+
flushReplayBuffer?(): void;
|
|
22
|
+
write(bytes: Buffer): void;
|
|
23
|
+
resize(cols: number, rows: number): void;
|
|
24
|
+
pause(): void;
|
|
25
|
+
resume(): void;
|
|
26
|
+
kill(signal?: NodeJS.Signals): void;
|
|
27
|
+
on(event: "output", listener: (bytes: Buffer) => void): this;
|
|
28
|
+
on(event: "exit", listener: (exit: PtyExit) => void): this;
|
|
29
|
+
off(event: "output", listener: (bytes: Buffer) => void): this;
|
|
30
|
+
off(event: "exit", listener: (exit: PtyExit) => void): this;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Wire reason codes for terminal-cwd errors. Mirrors the bridge's
|
|
35
|
+
* {@link TerminalCwdErrorReason} discriminator into the kebab-case the
|
|
36
|
+
* WS protocol speaks. Adding a new reason is a single-line change in
|
|
37
|
+
* both directions.
|
|
38
|
+
*/
|
|
39
|
+
export type CwdErrorWireReason = "cwd-not-found" | "cwd-not-directory" | "cwd-stat-failed";
|
|
40
|
+
|
|
41
|
+
const CWD_ERROR_WIRE_REASON: Record<TerminalCwdErrorReason, CwdErrorWireReason> = {
|
|
42
|
+
notFound: "cwd-not-found",
|
|
43
|
+
notDirectory: "cwd-not-directory",
|
|
44
|
+
statFailed: "cwd-stat-failed",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
interface WsLike {
|
|
48
|
+
readyState: number;
|
|
49
|
+
bufferedAmount?: number;
|
|
50
|
+
send(data: string | Buffer, options?: { binary?: boolean }): void;
|
|
51
|
+
close(code?: number, reason?: string): void;
|
|
52
|
+
on(event: "message", listener: (data: RawData | string, isBinary: boolean) => void): this;
|
|
53
|
+
on(event: "close", listener: () => void): this;
|
|
54
|
+
on(event: "error", listener: () => void): this;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface PtyWebSocketOptions {
|
|
58
|
+
createBridge?: (id: string) => PtyBridgeLike;
|
|
59
|
+
registry?: PtyBridgeRegistry;
|
|
60
|
+
idleMs?: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface PtyWebSocketConnection {
|
|
64
|
+
getBridge(): PtyBridgeLike | null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface InitFrame {
|
|
68
|
+
type: "init";
|
|
69
|
+
cols: number;
|
|
70
|
+
rows: number;
|
|
71
|
+
cwd?: string;
|
|
72
|
+
cmd?: string[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface ResizeFrame {
|
|
76
|
+
type: "resize";
|
|
77
|
+
cols: number;
|
|
78
|
+
rows: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isPositiveInteger(value: unknown): value is number {
|
|
82
|
+
return Number.isInteger(value) && Number(value) > 0;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function rawDataToBuffer(data: RawData | string): Buffer {
|
|
86
|
+
if (typeof data === "string") return Buffer.from(data, "utf8");
|
|
87
|
+
if (Buffer.isBuffer(data)) return data;
|
|
88
|
+
if (data instanceof ArrayBuffer) return Buffer.from(data);
|
|
89
|
+
if (Array.isArray(data)) return Buffer.concat(data);
|
|
90
|
+
return Buffer.from(data);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function rawDataToText(data: RawData | string): string {
|
|
94
|
+
return typeof data === "string" ? data : rawDataToBuffer(data).toString("utf8");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isJsonControlFrame(data: RawData | string, isBinary: boolean): boolean {
|
|
98
|
+
return !isBinary && rawDataToText(data).startsWith("{");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function parseJsonObject(data: RawData | string): Record<string, unknown> {
|
|
102
|
+
const parsed: unknown = JSON.parse(rawDataToText(data));
|
|
103
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
104
|
+
throw new Error("control frame must be a JSON object");
|
|
105
|
+
}
|
|
106
|
+
return parsed as Record<string, unknown>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function parseInitFrame(data: RawData | string): InitFrame {
|
|
110
|
+
const frame = parseJsonObject(data);
|
|
111
|
+
if (frame.type !== "init") {
|
|
112
|
+
throw new Error("first frame must be init");
|
|
113
|
+
}
|
|
114
|
+
if (!isPositiveInteger(frame.cols) || !isPositiveInteger(frame.rows)) {
|
|
115
|
+
throw new Error("init requires positive integer cols and rows");
|
|
116
|
+
}
|
|
117
|
+
if (frame.cwd !== undefined && typeof frame.cwd !== "string") {
|
|
118
|
+
throw new Error("init cwd must be a string");
|
|
119
|
+
}
|
|
120
|
+
if (
|
|
121
|
+
frame.cmd !== undefined &&
|
|
122
|
+
(!Array.isArray(frame.cmd) ||
|
|
123
|
+
frame.cmd.length === 0 ||
|
|
124
|
+
!frame.cmd.every((part) => typeof part === "string"))
|
|
125
|
+
) {
|
|
126
|
+
throw new Error("init cmd must be a non-empty string array");
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
type: "init",
|
|
130
|
+
cols: frame.cols,
|
|
131
|
+
rows: frame.rows,
|
|
132
|
+
...(frame.cwd !== undefined ? { cwd: frame.cwd } : {}),
|
|
133
|
+
...(frame.cmd !== undefined ? { cmd: frame.cmd } : {}),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function parseResizeFrame(data: RawData | string): ResizeFrame {
|
|
138
|
+
const frame = parseJsonObject(data);
|
|
139
|
+
if (frame.type !== "resize") {
|
|
140
|
+
throw new Error(`unsupported control frame: ${String(frame.type)}`);
|
|
141
|
+
}
|
|
142
|
+
if (!isPositiveInteger(frame.cols) || !isPositiveInteger(frame.rows)) {
|
|
143
|
+
throw new Error("resize requires positive integer cols and rows");
|
|
144
|
+
}
|
|
145
|
+
return { type: "resize", cols: frame.cols, rows: frame.rows };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
interface ErrorFrameExtras {
|
|
149
|
+
reason?: CwdErrorWireReason;
|
|
150
|
+
cwd?: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function sendError(ws: WsLike, message: string, extras?: ErrorFrameExtras): void {
|
|
154
|
+
if (ws.readyState !== WS_OPEN) return;
|
|
155
|
+
const frame: Record<string, unknown> = { type: "error", message };
|
|
156
|
+
if (extras?.reason !== undefined) frame.reason = extras.reason;
|
|
157
|
+
if (extras?.cwd !== undefined) frame.cwd = extras.cwd;
|
|
158
|
+
ws.send(JSON.stringify(frame));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function sendCwdError(ws: WsLike, err: TerminalCwdError): void {
|
|
162
|
+
sendError(ws, err.message, {
|
|
163
|
+
reason: CWD_ERROR_WIRE_REASON[err.reason],
|
|
164
|
+
cwd: err.cwd,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function closeWs(ws: WsLike): void {
|
|
169
|
+
if (ws.readyState === WS_OPEN) {
|
|
170
|
+
ws.close();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function backpressureBytes(): number {
|
|
175
|
+
const parsed = Number.parseInt(process.env.TMUX_IDE_PTY_BACKPRESSURE_BYTES ?? "", 10);
|
|
176
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_BACKPRESSURE_BYTES;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function bridgeIdleMs(options?: { idleMs?: number }): number {
|
|
180
|
+
if (options?.idleMs !== undefined) return options.idleMs;
|
|
181
|
+
const parsed = Number.parseInt(process.env.TMUX_IDE_BRIDGE_IDLE_MS ?? "", 10);
|
|
182
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_BRIDGE_IDLE_MS;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
interface BridgeEntry {
|
|
186
|
+
bridge: PtyBridgeLike;
|
|
187
|
+
clients: number;
|
|
188
|
+
idleTimer: ReturnType<typeof setTimeout> | null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export class PtyBridgeRegistry {
|
|
192
|
+
private entries = new Map<string, BridgeEntry>();
|
|
193
|
+
|
|
194
|
+
acquire(
|
|
195
|
+
id: string,
|
|
196
|
+
createBridge: (id: string) => PtyBridgeLike,
|
|
197
|
+
options: { idleMs?: number } = {},
|
|
198
|
+
): { bridge: PtyBridgeLike; reused: boolean; release: () => void } {
|
|
199
|
+
let entry = this.entries.get(id);
|
|
200
|
+
const reused = !!entry && entry.bridge.running !== false;
|
|
201
|
+
if (!entry || entry.bridge.running === false) {
|
|
202
|
+
entry = { bridge: createBridge(id), clients: 0, idleTimer: null };
|
|
203
|
+
this.entries.set(id, entry);
|
|
204
|
+
entry.bridge.on("exit", () => {
|
|
205
|
+
this.entries.delete(id);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (entry.idleTimer) {
|
|
210
|
+
clearTimeout(entry.idleTimer);
|
|
211
|
+
entry.idleTimer = null;
|
|
212
|
+
}
|
|
213
|
+
entry.clients++;
|
|
214
|
+
|
|
215
|
+
let released = false;
|
|
216
|
+
const release = () => {
|
|
217
|
+
if (released) return;
|
|
218
|
+
released = true;
|
|
219
|
+
const current = this.entries.get(id);
|
|
220
|
+
if (!current) return;
|
|
221
|
+
current.clients = Math.max(0, current.clients - 1);
|
|
222
|
+
if (current.clients > 0 || current.bridge.running === false) return;
|
|
223
|
+
const idleMs = bridgeIdleMs(options);
|
|
224
|
+
current.idleTimer = setTimeout(() => {
|
|
225
|
+
const latest = this.entries.get(id);
|
|
226
|
+
if (!latest || latest.clients > 0) return;
|
|
227
|
+
latest.bridge.kill("SIGTERM");
|
|
228
|
+
this.entries.delete(id);
|
|
229
|
+
}, idleMs);
|
|
230
|
+
current.idleTimer.unref?.();
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
return { bridge: entry.bridge, reused, release };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Look up a bridge by id without acquiring it. Returns `null` when no
|
|
238
|
+
* bridge is registered for the id. Used by server-side action handlers
|
|
239
|
+
* (terminal.respawn, terminal.stop) that need to operate on an existing
|
|
240
|
+
* bridge — they should not bump the client refcount.
|
|
241
|
+
*/
|
|
242
|
+
peek(id: string): PtyBridgeLike | null {
|
|
243
|
+
const entry = this.entries.get(id);
|
|
244
|
+
if (!entry) return null;
|
|
245
|
+
if (entry.bridge.running === false) return null;
|
|
246
|
+
return entry.bridge;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Drop the bridge for `id`, killing it synchronously. Used by the
|
|
251
|
+
* terminal.stop action handler to release a sticky bridge whose owner
|
|
252
|
+
* has explicitly asked to terminate it.
|
|
253
|
+
*/
|
|
254
|
+
delete(id: string): boolean {
|
|
255
|
+
const entry = this.entries.get(id);
|
|
256
|
+
if (!entry) return false;
|
|
257
|
+
if (entry.idleTimer) {
|
|
258
|
+
clearTimeout(entry.idleTimer);
|
|
259
|
+
entry.idleTimer = null;
|
|
260
|
+
}
|
|
261
|
+
try {
|
|
262
|
+
entry.bridge.kill("SIGTERM");
|
|
263
|
+
} catch {
|
|
264
|
+
// Best-effort — the process may already have exited.
|
|
265
|
+
}
|
|
266
|
+
this.entries.delete(id);
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
shutdown(): void {
|
|
271
|
+
for (const entry of this.entries.values()) {
|
|
272
|
+
if (entry.idleTimer) clearTimeout(entry.idleTimer);
|
|
273
|
+
entry.bridge.kill("SIGTERM");
|
|
274
|
+
}
|
|
275
|
+
this.entries.clear();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
size(): number {
|
|
279
|
+
return this.entries.size;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export const defaultPtyBridgeRegistry = new PtyBridgeRegistry();
|
|
284
|
+
|
|
285
|
+
export function shutdownPtyBridges(): void {
|
|
286
|
+
defaultPtyBridgeRegistry.shutdown();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function handlePtyWebSocket(
|
|
290
|
+
ws: WebSocket | WsLike,
|
|
291
|
+
id: string,
|
|
292
|
+
options: PtyWebSocketOptions = {},
|
|
293
|
+
): PtyWebSocketConnection {
|
|
294
|
+
const socket = ws as WsLike;
|
|
295
|
+
let bridge: PtyBridgeLike | null = null;
|
|
296
|
+
let initialized = false;
|
|
297
|
+
let ptyExited = false;
|
|
298
|
+
let killTimer: ReturnType<typeof setTimeout> | null = null;
|
|
299
|
+
let drainTimer: ReturnType<typeof setTimeout> | null = null;
|
|
300
|
+
let releaseBridge: (() => void) | null = null;
|
|
301
|
+
let outputListener: ((bytes: Buffer) => void) | null = null;
|
|
302
|
+
let exitListener: ((exit: PtyExit) => void) | null = null;
|
|
303
|
+
const backpressureThreshold = backpressureBytes();
|
|
304
|
+
const resumeThreshold = Math.floor(backpressureThreshold / 2);
|
|
305
|
+
|
|
306
|
+
const clearKillTimer = () => {
|
|
307
|
+
if (!killTimer) return;
|
|
308
|
+
clearTimeout(killTimer);
|
|
309
|
+
killTimer = null;
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const clearDrainTimer = () => {
|
|
313
|
+
if (!drainTimer) return;
|
|
314
|
+
clearTimeout(drainTimer);
|
|
315
|
+
drainTimer = null;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const scheduleDrainCheck = () => {
|
|
319
|
+
if (drainTimer || !bridge) return;
|
|
320
|
+
drainTimer = setTimeout(() => {
|
|
321
|
+
drainTimer = null;
|
|
322
|
+
if (!bridge || socket.readyState !== WS_OPEN) return;
|
|
323
|
+
if ((socket.bufferedAmount ?? 0) <= resumeThreshold) {
|
|
324
|
+
bridge.resume();
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
scheduleDrainCheck();
|
|
328
|
+
}, DRAIN_POLL_MS);
|
|
329
|
+
drainTimer.unref?.();
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const maybePauseForBackpressure = () => {
|
|
333
|
+
if (!bridge || (socket.bufferedAmount ?? 0) <= backpressureThreshold) return;
|
|
334
|
+
bridge.pause();
|
|
335
|
+
scheduleDrainCheck();
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const closeWithError = (message: string) => {
|
|
339
|
+
sendError(socket, message);
|
|
340
|
+
closeWs(socket);
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const closeWithCwdError = (err: TerminalCwdError) => {
|
|
344
|
+
sendCwdError(socket, err);
|
|
345
|
+
closeWs(socket);
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const attachBridgeEvents = (ptyBridge: PtyBridgeLike) => {
|
|
349
|
+
outputListener = (bytes) => {
|
|
350
|
+
if (socket.readyState === WS_OPEN) {
|
|
351
|
+
maybePauseForBackpressure();
|
|
352
|
+
socket.send(bytes, { binary: true });
|
|
353
|
+
maybePauseForBackpressure();
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
exitListener = (exit) => {
|
|
358
|
+
ptyExited = true;
|
|
359
|
+
clearKillTimer();
|
|
360
|
+
clearDrainTimer();
|
|
361
|
+
if (socket.readyState === WS_OPEN) {
|
|
362
|
+
socket.send(JSON.stringify({ type: "exit", code: exit.code, signal: exit.signal }));
|
|
363
|
+
socket.close();
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
ptyBridge.on("output", outputListener);
|
|
368
|
+
ptyBridge.on("exit", exitListener);
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const detachBridgeEvents = () => {
|
|
372
|
+
if (bridge && outputListener) bridge.off("output", outputListener);
|
|
373
|
+
if (bridge && exitListener) bridge.off("exit", exitListener);
|
|
374
|
+
outputListener = null;
|
|
375
|
+
exitListener = null;
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
socket.on("message", (data, isBinary) => {
|
|
379
|
+
if (!initialized) {
|
|
380
|
+
if (!isJsonControlFrame(data, isBinary)) {
|
|
381
|
+
closeWithError("init frame required before input");
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
let init: InitFrame;
|
|
386
|
+
try {
|
|
387
|
+
init = parseInitFrame(data);
|
|
388
|
+
} catch (err) {
|
|
389
|
+
closeWithError(err instanceof Error ? err.message : String(err));
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const registry = options.registry ?? defaultPtyBridgeRegistry;
|
|
394
|
+
const acquired = registry.acquire(
|
|
395
|
+
id,
|
|
396
|
+
options.createBridge ?? ((bridgeId) => new PtyBridge({ id: bridgeId })),
|
|
397
|
+
{
|
|
398
|
+
idleMs: options.idleMs,
|
|
399
|
+
},
|
|
400
|
+
);
|
|
401
|
+
bridge = acquired.bridge;
|
|
402
|
+
releaseBridge = acquired.release;
|
|
403
|
+
attachBridgeEvents(bridge);
|
|
404
|
+
|
|
405
|
+
try {
|
|
406
|
+
const spawnOptions: PtySpawnOptions = {};
|
|
407
|
+
if (init.cwd !== undefined) spawnOptions.cwd = init.cwd;
|
|
408
|
+
if (init.cmd !== undefined) spawnOptions.cmd = init.cmd;
|
|
409
|
+
|
|
410
|
+
const currentCwd = bridge.getCwd?.() ?? null;
|
|
411
|
+
const cwdChanged = init.cwd !== undefined && currentCwd !== null && currentCwd !== init.cwd;
|
|
412
|
+
|
|
413
|
+
if (!acquired.reused) {
|
|
414
|
+
bridge.spawn(init.cols, init.rows, spawnOptions);
|
|
415
|
+
} else if (cwdChanged && bridge.restartWith) {
|
|
416
|
+
// Sticky-bridge cwd mismatch: stop the running shell and
|
|
417
|
+
// respawn in the new cwd. Replay is meaningless since the
|
|
418
|
+
// prior process is gone — emit an empty replay-end so the
|
|
419
|
+
// client's reconnect bookkeeping stays consistent.
|
|
420
|
+
bridge.restartWith(init.cols, init.rows, spawnOptions);
|
|
421
|
+
if (socket.readyState === WS_OPEN) {
|
|
422
|
+
socket.send(JSON.stringify({ type: "replay-end", bytes: 0 }));
|
|
423
|
+
}
|
|
424
|
+
} else {
|
|
425
|
+
try {
|
|
426
|
+
bridge.resize(init.cols, init.rows);
|
|
427
|
+
} catch {
|
|
428
|
+
// Reconnect replay is still useful if the PTY is exiting between
|
|
429
|
+
// the registry running check and the resize request.
|
|
430
|
+
}
|
|
431
|
+
const replay = bridge.getReplayBuffer?.() ?? Buffer.alloc(0);
|
|
432
|
+
if (replay.byteLength > 0 && socket.readyState === WS_OPEN) {
|
|
433
|
+
socket.send(replay, { binary: true });
|
|
434
|
+
}
|
|
435
|
+
if (socket.readyState === WS_OPEN) {
|
|
436
|
+
socket.send(JSON.stringify({ type: "replay-end", bytes: replay.byteLength }));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
initialized = true;
|
|
440
|
+
} catch (err) {
|
|
441
|
+
detachBridgeEvents();
|
|
442
|
+
releaseBridge?.();
|
|
443
|
+
if (err instanceof TerminalCwdError) {
|
|
444
|
+
closeWithCwdError(err);
|
|
445
|
+
} else {
|
|
446
|
+
closeWithError(`spawn failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (isJsonControlFrame(data, isBinary)) {
|
|
453
|
+
try {
|
|
454
|
+
const resize = parseResizeFrame(data);
|
|
455
|
+
bridge?.resize(resize.cols, resize.rows);
|
|
456
|
+
} catch (err) {
|
|
457
|
+
closeWithError(err instanceof Error ? err.message : String(err));
|
|
458
|
+
}
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
try {
|
|
463
|
+
bridge?.write(rawDataToBuffer(data));
|
|
464
|
+
} catch (err) {
|
|
465
|
+
closeWithError(err instanceof Error ? err.message : String(err));
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
socket.on("close", () => {
|
|
470
|
+
clearDrainTimer();
|
|
471
|
+
detachBridgeEvents();
|
|
472
|
+
if (!bridge || ptyExited) return;
|
|
473
|
+
releaseBridge?.();
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
socket.on("error", () => {
|
|
477
|
+
closeWs(socket);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
return {
|
|
481
|
+
getBridge: () => bridge,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { getSessionName } from "./lib/yaml-io.ts";
|
|
4
|
+
import { getSessionState, isProcessAlive, listPanes } from "@tmux-ide/tmux-bridge";
|
|
5
|
+
import { isCanonicalDaemonAlive, readCanonicalDaemonInfo } from "./lib/canonical-daemon.ts";
|
|
6
|
+
|
|
7
|
+
export async function status(
|
|
8
|
+
targetDir: string | undefined,
|
|
9
|
+
{ json }: { json?: boolean } = {},
|
|
10
|
+
): Promise<void> {
|
|
11
|
+
const dir = resolve(targetDir ?? ".");
|
|
12
|
+
const { name: session } = getSessionName(dir);
|
|
13
|
+
const configExists = existsSync(resolve(dir, "ide.yml"));
|
|
14
|
+
|
|
15
|
+
const state = getSessionState(session);
|
|
16
|
+
const running = state.running;
|
|
17
|
+
let panes: ReturnType<typeof listPanes> = [];
|
|
18
|
+
|
|
19
|
+
if (running) panes = listPanes(session);
|
|
20
|
+
|
|
21
|
+
const daemonInfo = readCanonicalDaemonInfo();
|
|
22
|
+
const healthy = daemonInfo ? await isCanonicalDaemonAlive(daemonInfo) : false;
|
|
23
|
+
|
|
24
|
+
const data = {
|
|
25
|
+
session,
|
|
26
|
+
running,
|
|
27
|
+
configExists,
|
|
28
|
+
panes,
|
|
29
|
+
daemon: {
|
|
30
|
+
pid: daemonInfo?.pid ?? null,
|
|
31
|
+
alive: daemonInfo ? isProcessAlive(daemonInfo.pid) : false,
|
|
32
|
+
port: daemonInfo?.port ?? null,
|
|
33
|
+
healthy,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
if (json) {
|
|
38
|
+
console.log(JSON.stringify(data, null, 2));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log(`Session: ${session}`);
|
|
43
|
+
console.log(`Running: ${running ? "yes" : "no"}`);
|
|
44
|
+
console.log(`Config: ${configExists ? "ide.yml found" : "no ide.yml"}`);
|
|
45
|
+
|
|
46
|
+
if (running) {
|
|
47
|
+
console.log(
|
|
48
|
+
`Daemon: ${data.daemon.alive ? "running" : "not running"}${data.daemon.port ? ` (port ${data.daemon.port})` : ""}`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (panes.length > 0) {
|
|
53
|
+
console.log(`\nPanes:`);
|
|
54
|
+
for (const p of panes) {
|
|
55
|
+
const active = p.active ? " (active)" : "";
|
|
56
|
+
console.log(` ${p.index}: ${p.title} [${p.width}x${p.height}]${active}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { getSessionName } from "./lib/yaml-io.ts";
|
|
3
|
+
import { outputError } from "./lib/output.ts";
|
|
4
|
+
import { killSession, stopSessionMonitor } from "@tmux-ide/tmux-bridge";
|
|
5
|
+
|
|
6
|
+
export async function stop(
|
|
7
|
+
targetDir: string | undefined,
|
|
8
|
+
{ json }: { json?: boolean } = {},
|
|
9
|
+
): Promise<void> {
|
|
10
|
+
const dir = resolve(targetDir ?? ".");
|
|
11
|
+
const { name: session } = getSessionName(dir);
|
|
12
|
+
|
|
13
|
+
// Stop the session monitor before killing the session
|
|
14
|
+
stopSessionMonitor(session);
|
|
15
|
+
|
|
16
|
+
const result = killSession(session);
|
|
17
|
+
|
|
18
|
+
if (result.stopped) {
|
|
19
|
+
if (json) {
|
|
20
|
+
console.log(JSON.stringify({ stopped: session }));
|
|
21
|
+
} else {
|
|
22
|
+
console.log(`Stopped session "${session}"`);
|
|
23
|
+
}
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
outputError(`No active session "${session}" found`, "NOT_RUNNING");
|
|
28
|
+
}
|