aiwcli 0.15.1 → 0.15.3
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/dist/commands/launch.d.ts +1 -0
- package/dist/commands/launch.js +24 -8
- package/dist/templates/_shared/.claude/skills/codex/SKILL.md +19 -26
- package/dist/templates/_shared/.codex/workflows/codex.md +11 -0
- package/dist/templates/_shared/lib-ts/agent-exec/backends/tmux.ts +23 -49
- package/dist/templates/_shared/lib-ts/base/launchers/tmux-launcher.ts +173 -0
- package/dist/templates/_shared/lib-ts/base/launchers/window-launcher.ts +93 -0
- package/dist/templates/_shared/lib-ts/base/launchers/wt-launcher.ts +64 -0
- package/dist/templates/_shared/lib-ts/base/pane-launcher.ts +55 -0
- package/dist/templates/_shared/lib-ts/base/sentinel-ipc.ts +87 -0
- package/dist/templates/_shared/lib-ts/base/tmux-driver.ts +160 -200
- package/dist/templates/_shared/lib-ts/base/tmux-pane-placement.ts +78 -0
- package/dist/templates/_shared/lib-ts/context/context-formatter.ts +0 -3
- package/dist/templates/_shared/scripts/resolve-run.ts +1 -1
- package/dist/templates/_shared/skills/codex/CLAUDE.md +70 -0
- package/dist/templates/_shared/skills/codex/SKILL.md +71 -0
- package/dist/templates/_shared/skills/{prompt-codex/scripts/watch-codex.ts → codex/lib/codex-watcher.ts} +78 -63
- package/dist/templates/_shared/skills/{prompt-codex → codex}/scripts/launch-codex.ts +106 -61
- package/dist/templates/_shared/skills/codex/scripts/watch-codex.ts +42 -0
- package/oclif.manifest.json +2 -2
- package/package.json +1 -1
- package/dist/templates/_shared/.claude/skills/codex/prompt.md +0 -30
- package/dist/templates/_shared/skills/prompt-codex/CLAUDE.md +0 -71
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
|
|
5
|
+
export interface SentinelIpcPaths {
|
|
6
|
+
tmpDir: string;
|
|
7
|
+
inputPath: string;
|
|
8
|
+
stdoutPath: string;
|
|
9
|
+
stderrPath: string;
|
|
10
|
+
sentinelPath: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function sanitizePrefix(prefix: string): string {
|
|
14
|
+
return prefix.replaceAll(/[^a-zA-Z0-9_-]/g, "-");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createSentinelIpcPaths(prefix: string): SentinelIpcPaths {
|
|
18
|
+
const safePrefix = sanitizePrefix(prefix);
|
|
19
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), `${safePrefix}-`));
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
tmpDir,
|
|
23
|
+
inputPath: path.join(tmpDir, "input.txt"),
|
|
24
|
+
stdoutPath: path.join(tmpDir, "stdout.txt"),
|
|
25
|
+
stderrPath: path.join(tmpDir, "stderr.txt"),
|
|
26
|
+
sentinelPath: path.join(tmpDir, "sentinel.txt"),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function buildShellCaptureScript(
|
|
31
|
+
command: string,
|
|
32
|
+
paths: Pick<SentinelIpcPaths, "inputPath" | "stdoutPath" | "stderrPath" | "sentinelPath">,
|
|
33
|
+
quote: (input: string) => string,
|
|
34
|
+
): string {
|
|
35
|
+
return [
|
|
36
|
+
command,
|
|
37
|
+
`< ${quote(paths.inputPath)}`,
|
|
38
|
+
`> ${quote(paths.stdoutPath)}`,
|
|
39
|
+
`2> ${quote(paths.stderrPath)}`,
|
|
40
|
+
`; echo $? > ${quote(paths.sentinelPath)}`,
|
|
41
|
+
].join(" ");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function waitForSentinelFile(
|
|
45
|
+
sentinelPath: string,
|
|
46
|
+
timeoutMs: number,
|
|
47
|
+
pollIntervalMs = 250,
|
|
48
|
+
): Promise<boolean> {
|
|
49
|
+
const deadline = Date.now() + timeoutMs;
|
|
50
|
+
while (Date.now() < deadline) {
|
|
51
|
+
if (fs.existsSync(sentinelPath)) return true;
|
|
52
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return fs.existsSync(sentinelPath);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function readSentinelExitCode(sentinelPath: string, fallback = 1): number {
|
|
59
|
+
if (!fs.existsSync(sentinelPath)) return fallback;
|
|
60
|
+
|
|
61
|
+
const raw = fs.readFileSync(sentinelPath, "utf-8").trim();
|
|
62
|
+
const parsed = Number.parseInt(raw, 10);
|
|
63
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function readTextIfExists(filePath: string): string {
|
|
67
|
+
if (!filePath || !fs.existsSync(filePath)) return "";
|
|
68
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function cleanupSentinelIpc(paths: Pick<SentinelIpcPaths, "tmpDir">): void {
|
|
72
|
+
try {
|
|
73
|
+
fs.rmSync(paths.tmpDir, { recursive: true, force: true });
|
|
74
|
+
} catch {
|
|
75
|
+
// Best-effort cleanup.
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function cleanupSentinelPath(sentinelPath: string | undefined): void {
|
|
80
|
+
if (!sentinelPath) return;
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
fs.rmSync(path.dirname(sentinelPath), { recursive: true, force: true });
|
|
84
|
+
} catch {
|
|
85
|
+
// Best-effort cleanup.
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -1,20 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared
|
|
2
|
+
* Shared pane driver helpers for launching CLI tools in visible panes.
|
|
3
3
|
*
|
|
4
|
-
* This module
|
|
5
|
-
* - tmux/
|
|
6
|
-
* - split-pane
|
|
7
|
-
* -
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* This module now delegates pane creation to platform-specific launchers:
|
|
5
|
+
* - tmux (Linux/macOS in tmux sessions)
|
|
6
|
+
* - Windows Terminal split-pane
|
|
7
|
+
* - Windows new-window fallback
|
|
8
|
+
*
|
|
9
|
+
* Prompt delivery is handled at launch time by passing prompt text as a CLI
|
|
10
|
+
* argument for REPL mode, rather than injecting into an already-running pane.
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
import * as fs from "node:fs";
|
|
13
14
|
|
|
15
|
+
import { createPaneLauncher, type PaneBackend, type PaneSplitDirection } from "./pane-launcher.js";
|
|
16
|
+
import { cleanupSentinelIpc, createSentinelIpcPaths } from "./sentinel-ipc.js";
|
|
14
17
|
import { execFileAsync, findExecutable } from "./subprocess-utils.js";
|
|
18
|
+
import {
|
|
19
|
+
getTmuxAvailability,
|
|
20
|
+
normalizeSplitFlag,
|
|
21
|
+
quoteForSh,
|
|
22
|
+
type TmuxAvailability,
|
|
23
|
+
type TmuxSplitFlag,
|
|
24
|
+
} from "./launchers/tmux-launcher.js";
|
|
25
|
+
|
|
26
|
+
export type { TmuxAvailability, TmuxSplitFlag };
|
|
27
|
+
export { getTmuxAvailability, normalizeSplitFlag, quoteForSh };
|
|
15
28
|
|
|
16
29
|
export type DriverMode = "exec" | "repl";
|
|
17
|
-
export type
|
|
30
|
+
export type TmuxSplitOption = TmuxSplitFlag | "auto";
|
|
18
31
|
|
|
19
32
|
export interface DriverPreflightResult {
|
|
20
33
|
available: boolean;
|
|
@@ -24,27 +37,6 @@ export interface DriverPreflightResult {
|
|
|
24
37
|
export type DriverPreflight =
|
|
25
38
|
(toolPath: string) => Promise<DriverPreflightResult> | DriverPreflightResult;
|
|
26
39
|
|
|
27
|
-
export interface SendToPaneOptions {
|
|
28
|
-
waitForPromptMs?: number;
|
|
29
|
-
postPasteDelayMs?: number;
|
|
30
|
-
retryEnter?: boolean;
|
|
31
|
-
retryDelayMs?: number;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface SendToPaneResult {
|
|
35
|
-
success: boolean;
|
|
36
|
-
/** Which step failed, if any */
|
|
37
|
-
failedAt?: "prompt-wait" | "load-buffer" | "paste-buffer" | "send-enter";
|
|
38
|
-
/** ms spent waiting for REPL prompt */
|
|
39
|
-
promptWaitMs?: number;
|
|
40
|
-
/** Last 200 chars of pane content at time of prompt-wait timeout */
|
|
41
|
-
paneTailOnTimeout?: string;
|
|
42
|
-
/** stderr from the failed tmux command, if any */
|
|
43
|
-
tmuxStderr?: string;
|
|
44
|
-
/** Whether retry Enter was sent */
|
|
45
|
-
retrySent?: boolean;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
40
|
export interface LaunchDriverOptions {
|
|
49
41
|
toolName: string;
|
|
50
42
|
toolBin?: string;
|
|
@@ -52,8 +44,7 @@ export interface LaunchDriverOptions {
|
|
|
52
44
|
args?: string[];
|
|
53
45
|
env?: Record<string, string>;
|
|
54
46
|
promptPath?: string;
|
|
55
|
-
|
|
56
|
-
splitFlag?: string;
|
|
47
|
+
splitFlag?: TmuxSplitOption;
|
|
57
48
|
splitTarget?: string;
|
|
58
49
|
autoClose?: boolean;
|
|
59
50
|
holdPane?: boolean;
|
|
@@ -66,31 +57,15 @@ export interface LaunchDriverOptions {
|
|
|
66
57
|
export interface LaunchDriverResult {
|
|
67
58
|
launched: boolean;
|
|
68
59
|
usedTmux: boolean;
|
|
60
|
+
backend: PaneBackend;
|
|
69
61
|
mode: DriverMode;
|
|
70
62
|
toolPath?: string;
|
|
71
63
|
paneId?: string;
|
|
64
|
+
sentinelPath?: string;
|
|
72
65
|
exitCode?: number;
|
|
66
|
+
stdout?: string;
|
|
73
67
|
reason?: string;
|
|
74
68
|
stderr?: string;
|
|
75
|
-
sendDiagnostics?: SendToPaneResult;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export interface TmuxAvailability {
|
|
79
|
-
available: boolean;
|
|
80
|
-
tmuxPath?: string;
|
|
81
|
-
reason?: string;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const REPL_PROMPT_REGEX = /(^|\n)\s*[›>]\s/;
|
|
85
|
-
const REPL_ACTIVITY_HINT = "\n• ";
|
|
86
|
-
|
|
87
|
-
function sleep(ms: number): Promise<void> {
|
|
88
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function getLastLine(text: string): string {
|
|
92
|
-
const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
93
|
-
return lines.at(-1) ?? "";
|
|
94
69
|
}
|
|
95
70
|
|
|
96
71
|
function buildEnvPrefix(env: Record<string, string>): string {
|
|
@@ -99,7 +74,21 @@ function buildEnvPrefix(env: Record<string, string>): string {
|
|
|
99
74
|
.join(" ");
|
|
100
75
|
}
|
|
101
76
|
|
|
102
|
-
function
|
|
77
|
+
function quoteForPowerShell(input: string): string {
|
|
78
|
+
return `'${input.replaceAll("'", "''")}'`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function buildCommandArgs(
|
|
82
|
+
args: string[],
|
|
83
|
+
mode: DriverMode,
|
|
84
|
+
promptPath?: string,
|
|
85
|
+
): string[] {
|
|
86
|
+
if (mode !== "repl" || !promptPath) return args;
|
|
87
|
+
const promptText = fs.readFileSync(promptPath, "utf-8");
|
|
88
|
+
return [...args, promptText];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function buildShToolCommand(
|
|
103
92
|
toolPath: string,
|
|
104
93
|
args: string[],
|
|
105
94
|
env: Record<string, string>,
|
|
@@ -107,7 +96,8 @@ function buildToolCommand(
|
|
|
107
96
|
promptPath?: string,
|
|
108
97
|
): string {
|
|
109
98
|
const envPrefix = buildEnvPrefix(env);
|
|
110
|
-
const
|
|
99
|
+
const commandArgs = buildCommandArgs(args, mode, promptPath);
|
|
100
|
+
const argPart = commandArgs.map((arg) => quoteForSh(arg)).join(" ");
|
|
111
101
|
const base = [envPrefix, quoteForSh(toolPath), argPart]
|
|
112
102
|
.filter(Boolean)
|
|
113
103
|
.join(" ");
|
|
@@ -119,50 +109,63 @@ function buildToolCommand(
|
|
|
119
109
|
return base;
|
|
120
110
|
}
|
|
121
111
|
|
|
112
|
+
function buildPowerShellToolCommand(
|
|
113
|
+
toolPath: string,
|
|
114
|
+
args: string[],
|
|
115
|
+
env: Record<string, string>,
|
|
116
|
+
mode: DriverMode,
|
|
117
|
+
promptPath?: string,
|
|
118
|
+
): string {
|
|
119
|
+
const envPrefix = Object.entries(env)
|
|
120
|
+
.map(([key, value]) => `$env:${key}=${quoteForPowerShell(value)}`)
|
|
121
|
+
.join("; ");
|
|
122
|
+
|
|
123
|
+
const commandArgs = buildCommandArgs(args, mode, promptPath);
|
|
124
|
+
const argArray = commandArgs.map((arg) => quoteForPowerShell(arg)).join(", ");
|
|
125
|
+
const invocation = `& ${quoteForPowerShell(toolPath)}${argArray ? ` @(${argArray})` : ""}`;
|
|
126
|
+
|
|
127
|
+
const body = mode === "exec" && promptPath
|
|
128
|
+
? `Get-Content -Raw -Path ${quoteForPowerShell(promptPath)} | ${invocation}`
|
|
129
|
+
: invocation;
|
|
130
|
+
|
|
131
|
+
return [envPrefix, body].filter(Boolean).join("; ");
|
|
132
|
+
}
|
|
133
|
+
|
|
122
134
|
function wrapPaneCommand(
|
|
135
|
+
backend: PaneBackend,
|
|
123
136
|
command: string,
|
|
137
|
+
sentinelPath: string,
|
|
124
138
|
autoClose: boolean,
|
|
125
139
|
holdPane: boolean,
|
|
126
140
|
holdMessage: string,
|
|
127
141
|
): string {
|
|
128
|
-
if (
|
|
129
|
-
|
|
130
|
-
}
|
|
142
|
+
if (backend === "tmux") {
|
|
143
|
+
const base = `${command}; code=$?; printf '%s' "$code" > ${quoteForSh(sentinelPath)}`;
|
|
131
144
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
145
|
+
if (autoClose) {
|
|
146
|
+
return `${base}; tmux kill-pane -t "$TMUX_PANE" >/dev/null 2>&1 || true; exit $code`;
|
|
147
|
+
}
|
|
135
148
|
|
|
136
|
-
|
|
137
|
-
}
|
|
149
|
+
if (holdPane) {
|
|
150
|
+
return `${base}; echo; echo ${quoteForSh(holdMessage)}; exec bash`;
|
|
151
|
+
}
|
|
138
152
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
})
|
|
143
|
-
return result.exitCode === 0 ? result.stdout : "";
|
|
144
|
-
}
|
|
153
|
+
return `${base}; exit $code`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const base = `${command}; $code = $LASTEXITCODE; Set-Content -Path ${quoteForPowerShell(sentinelPath)} -Value $code -NoNewline`;
|
|
145
157
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
paneId: string,
|
|
149
|
-
timeoutMs: number,
|
|
150
|
-
): Promise<boolean> {
|
|
151
|
-
const deadline = Date.now() + timeoutMs;
|
|
152
|
-
while (Date.now() < deadline) {
|
|
153
|
-
const snapshot = await capturePaneText(tmuxPath, paneId);
|
|
154
|
-
if (REPL_PROMPT_REGEX.test(snapshot)) return true;
|
|
155
|
-
await sleep(250);
|
|
158
|
+
if (holdPane && !autoClose) {
|
|
159
|
+
return `${base}; Write-Host ''; Write-Host ${quoteForPowerShell(holdMessage)}; Read-Host -Prompt 'Press Enter to close' | Out-Null`;
|
|
156
160
|
}
|
|
157
|
-
return false;
|
|
158
|
-
}
|
|
159
161
|
|
|
160
|
-
|
|
161
|
-
return `'${input.replaceAll("'", "'\"'\"'")}'`;
|
|
162
|
+
return `${base}; exit $code`;
|
|
162
163
|
}
|
|
163
164
|
|
|
164
|
-
|
|
165
|
-
|
|
165
|
+
function mapSplitDirection(splitFlag: TmuxSplitOption | undefined): PaneSplitDirection {
|
|
166
|
+
if (splitFlag === "auto") return "auto";
|
|
167
|
+
if (splitFlag === "-v") return "v";
|
|
168
|
+
return "h";
|
|
166
169
|
}
|
|
167
170
|
|
|
168
171
|
export function isTruthy(value: string | undefined): boolean {
|
|
@@ -171,77 +174,24 @@ export function isTruthy(value: string | undefined): boolean {
|
|
|
171
174
|
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
172
175
|
}
|
|
173
176
|
|
|
174
|
-
export function getTmuxAvailability(): TmuxAvailability {
|
|
175
|
-
if (!process.env.TMUX) {
|
|
176
|
-
return { available: false, reason: "TMUX is not set" };
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const tmuxPath = findExecutable("tmux");
|
|
180
|
-
if (!tmuxPath) {
|
|
181
|
-
return { available: false, reason: "tmux not found on PATH" };
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return { available: true, tmuxPath };
|
|
185
|
-
}
|
|
186
|
-
|
|
187
177
|
export function resolveToolPath(toolName: string, toolBin?: string): string | null {
|
|
188
178
|
const bin = toolBin?.trim() || toolName;
|
|
189
179
|
return findExecutable(bin);
|
|
190
180
|
}
|
|
191
181
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const promptWaitMs = Date.now() - startTime;
|
|
203
|
-
|
|
204
|
-
if (!promptReady) {
|
|
205
|
-
const snapshot = await capturePaneText(tmuxPath, paneId);
|
|
206
|
-
const paneTailOnTimeout = snapshot.slice(-200);
|
|
207
|
-
return { success: false, failedAt: "prompt-wait", promptWaitMs, paneTailOnTimeout };
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const bufferName = `aiwcli-pane-${Date.now()}`;
|
|
211
|
-
const load = await execFileAsync(tmuxPath, ["load-buffer", "-b", bufferName, filePath], {
|
|
212
|
-
timeout: 3000,
|
|
213
|
-
});
|
|
214
|
-
if (load.exitCode !== 0) return { success: false, failedAt: "load-buffer", promptWaitMs, tmuxStderr: load.stderr };
|
|
215
|
-
|
|
216
|
-
const paste = await execFileAsync(tmuxPath, ["paste-buffer", "-d", "-p", "-b", bufferName, "-t", paneId], {
|
|
217
|
-
timeout: 3000,
|
|
218
|
-
});
|
|
219
|
-
if (paste.exitCode !== 0) return { success: false, failedAt: "paste-buffer", promptWaitMs, tmuxStderr: paste.stderr };
|
|
220
|
-
|
|
221
|
-
await sleep(options?.postPasteDelayMs ?? 500);
|
|
222
|
-
|
|
223
|
-
const firstEnter = await execFileAsync(tmuxPath, ["send-keys", "-t", paneId, "Enter"], {
|
|
224
|
-
timeout: 3000,
|
|
225
|
-
});
|
|
226
|
-
if (firstEnter.exitCode !== 0) return { success: false, failedAt: "send-enter", promptWaitMs, tmuxStderr: firstEnter.stderr };
|
|
227
|
-
|
|
228
|
-
if (options?.retryEnter === false) {
|
|
229
|
-
return { success: true, promptWaitMs, retrySent: false };
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
let retrySent = false;
|
|
233
|
-
await sleep(options?.retryDelayMs ?? 1200);
|
|
234
|
-
const snapshot = await capturePaneText(tmuxPath, paneId);
|
|
235
|
-
const lastFewLines = snapshot.split(/\r?\n/).slice(-5).join("\n");
|
|
236
|
-
if (REPL_PROMPT_REGEX.test(lastFewLines) && !lastFewLines.includes(REPL_ACTIVITY_HINT)) {
|
|
237
|
-
const secondEnter = await execFileAsync(tmuxPath, ["send-keys", "-t", paneId, "Enter"], {
|
|
238
|
-
timeout: 3000,
|
|
239
|
-
});
|
|
240
|
-
if (secondEnter.exitCode !== 0) return { success: false, failedAt: "send-enter", promptWaitMs, tmuxStderr: secondEnter.stderr };
|
|
241
|
-
retrySent = true;
|
|
182
|
+
function buildCommandForBackend(
|
|
183
|
+
backend: PaneBackend,
|
|
184
|
+
toolPath: string,
|
|
185
|
+
args: string[],
|
|
186
|
+
envVars: Record<string, string>,
|
|
187
|
+
mode: DriverMode,
|
|
188
|
+
promptPath?: string,
|
|
189
|
+
): string {
|
|
190
|
+
if (backend === "tmux") {
|
|
191
|
+
return buildShToolCommand(toolPath, args, envVars, mode, promptPath);
|
|
242
192
|
}
|
|
243
193
|
|
|
244
|
-
return
|
|
194
|
+
return buildPowerShellToolCommand(toolPath, args, envVars, mode, promptPath);
|
|
245
195
|
}
|
|
246
196
|
|
|
247
197
|
export async function launchDriverInTmuxOrFallback(
|
|
@@ -257,6 +207,7 @@ export async function launchDriverInTmuxOrFallback(
|
|
|
257
207
|
return {
|
|
258
208
|
launched: false,
|
|
259
209
|
usedTmux: false,
|
|
210
|
+
backend: "exec",
|
|
260
211
|
mode,
|
|
261
212
|
reason: `${options.toolBin ?? options.toolName} not found on PATH`,
|
|
262
213
|
};
|
|
@@ -268,6 +219,7 @@ export async function launchDriverInTmuxOrFallback(
|
|
|
268
219
|
return {
|
|
269
220
|
launched: false,
|
|
270
221
|
usedTmux: false,
|
|
222
|
+
backend: "exec",
|
|
271
223
|
mode,
|
|
272
224
|
toolPath,
|
|
273
225
|
reason: preflight.error ?? "driver preflight failed",
|
|
@@ -275,85 +227,89 @@ export async function launchDriverInTmuxOrFallback(
|
|
|
275
227
|
}
|
|
276
228
|
}
|
|
277
229
|
|
|
278
|
-
if (
|
|
230
|
+
if (options.promptPath && !fs.existsSync(options.promptPath)) {
|
|
279
231
|
return {
|
|
280
232
|
launched: false,
|
|
281
233
|
usedTmux: false,
|
|
234
|
+
backend: "exec",
|
|
282
235
|
mode,
|
|
283
236
|
toolPath,
|
|
284
237
|
reason: `prompt file not found: ${options.promptPath}`,
|
|
285
238
|
};
|
|
286
239
|
}
|
|
287
240
|
|
|
288
|
-
const
|
|
289
|
-
if (
|
|
290
|
-
const
|
|
291
|
-
const splitTarget = options.splitTarget?.trim();
|
|
292
|
-
const baseCmd = buildToolCommand(toolPath, args, envVars, mode, options.promptPath);
|
|
293
|
-
const holdMessage = options.holdMessage ?? "[aiwcli] Driver exited. Pane held open.";
|
|
294
|
-
const paneBody = wrapPaneCommand(
|
|
295
|
-
baseCmd,
|
|
296
|
-
Boolean(options.autoClose),
|
|
297
|
-
Boolean(options.holdPane) && !Boolean(options.autoClose),
|
|
298
|
-
holdMessage,
|
|
299
|
-
);
|
|
300
|
-
const paneCmd = `bash -lc ${quoteForSh(paneBody)}`;
|
|
301
|
-
|
|
302
|
-
const tmuxArgs = ["split-window", splitFlag, "-P", "-F", "#{pane_id}"];
|
|
303
|
-
if (splitTarget) tmuxArgs.push("-t", splitTarget);
|
|
304
|
-
tmuxArgs.push(paneCmd);
|
|
305
|
-
|
|
306
|
-
const split = await execFileAsync(tmux.tmuxPath, tmuxArgs, { timeout: 3000 });
|
|
307
|
-
if (split.exitCode !== 0) {
|
|
308
|
-
return {
|
|
309
|
-
launched: false,
|
|
310
|
-
usedTmux: true,
|
|
311
|
-
mode,
|
|
312
|
-
toolPath,
|
|
313
|
-
reason: "tmux split-window failed",
|
|
314
|
-
stderr: split.stderr.trim() || undefined,
|
|
315
|
-
};
|
|
316
|
-
}
|
|
241
|
+
const paneLauncher = await createPaneLauncher({ requireTmuxSession: true });
|
|
242
|
+
if (paneLauncher) {
|
|
243
|
+
const sentinel = createSentinelIpcPaths(`aiwcli-pane-${options.toolName}`);
|
|
317
244
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
245
|
+
try {
|
|
246
|
+
const baseCommand = buildCommandForBackend(
|
|
247
|
+
paneLauncher.backend,
|
|
248
|
+
toolPath,
|
|
249
|
+
args,
|
|
250
|
+
envVars,
|
|
251
|
+
mode,
|
|
252
|
+
options.promptPath,
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
const holdMessage = options.holdMessage ?? "[aiwcli] Driver exited. Pane held open.";
|
|
256
|
+
const paneCommand = wrapPaneCommand(
|
|
257
|
+
paneLauncher.backend,
|
|
258
|
+
baseCommand,
|
|
259
|
+
sentinel.sentinelPath,
|
|
260
|
+
Boolean(options.autoClose),
|
|
261
|
+
Boolean(options.holdPane) && !Boolean(options.autoClose),
|
|
262
|
+
holdMessage,
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
const paneResult = await paneLauncher.launch({
|
|
266
|
+
command: paneCommand,
|
|
267
|
+
splitDirection: mapSplitDirection(options.splitFlag),
|
|
268
|
+
splitTarget: options.splitTarget,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
if (!paneResult.launched) {
|
|
272
|
+
cleanupSentinelIpc(sentinel);
|
|
322
273
|
return {
|
|
323
|
-
launched:
|
|
324
|
-
usedTmux:
|
|
274
|
+
launched: false,
|
|
275
|
+
usedTmux: paneLauncher.backend === "tmux",
|
|
276
|
+
backend: paneLauncher.backend,
|
|
325
277
|
mode,
|
|
326
278
|
toolPath,
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
sendDiagnostics: sendResult,
|
|
279
|
+
reason: paneResult.reason ?? "pane launch failed",
|
|
280
|
+
stderr: paneResult.stderr,
|
|
330
281
|
};
|
|
331
282
|
}
|
|
283
|
+
|
|
332
284
|
return {
|
|
333
285
|
launched: true,
|
|
334
|
-
usedTmux:
|
|
286
|
+
usedTmux: paneLauncher.backend === "tmux",
|
|
287
|
+
backend: paneLauncher.backend,
|
|
288
|
+
mode,
|
|
289
|
+
toolPath,
|
|
290
|
+
paneId: paneResult.paneId,
|
|
291
|
+
sentinelPath: sentinel.sentinelPath,
|
|
292
|
+
};
|
|
293
|
+
} catch (error) {
|
|
294
|
+
cleanupSentinelIpc(sentinel);
|
|
295
|
+
return {
|
|
296
|
+
launched: false,
|
|
297
|
+
usedTmux: paneLauncher.backend === "tmux",
|
|
298
|
+
backend: paneLauncher.backend,
|
|
335
299
|
mode,
|
|
336
300
|
toolPath,
|
|
337
|
-
|
|
338
|
-
sendDiagnostics: sendResult,
|
|
301
|
+
reason: `pane launch failed: ${String(error)}`,
|
|
339
302
|
};
|
|
340
303
|
}
|
|
341
|
-
|
|
342
|
-
return {
|
|
343
|
-
launched: true,
|
|
344
|
-
usedTmux: true,
|
|
345
|
-
mode,
|
|
346
|
-
toolPath,
|
|
347
|
-
paneId: paneId || undefined,
|
|
348
|
-
};
|
|
349
304
|
}
|
|
350
305
|
|
|
351
|
-
if (
|
|
352
|
-
const
|
|
306
|
+
if (options.allowExecFallback) {
|
|
307
|
+
const commandArgs = buildCommandArgs(args, mode, options.promptPath);
|
|
308
|
+
const input = mode === "exec" && options.promptPath
|
|
353
309
|
? fs.readFileSync(options.promptPath, "utf-8")
|
|
354
310
|
: undefined;
|
|
355
311
|
|
|
356
|
-
const result = await execFileAsync(toolPath,
|
|
312
|
+
const result = await execFileAsync(toolPath, commandArgs, {
|
|
357
313
|
input,
|
|
358
314
|
timeout: timeoutMs,
|
|
359
315
|
env: { ...process.env, ...envVars },
|
|
@@ -363,19 +319,23 @@ export async function launchDriverInTmuxOrFallback(
|
|
|
363
319
|
return {
|
|
364
320
|
launched: true,
|
|
365
321
|
usedTmux: false,
|
|
322
|
+
backend: "exec",
|
|
366
323
|
mode,
|
|
367
324
|
toolPath,
|
|
368
325
|
exitCode: result.exitCode,
|
|
326
|
+
stdout: result.stdout,
|
|
369
327
|
stderr: result.stderr,
|
|
370
328
|
reason: result.exitCode === 0 ? undefined : `fallback exec exited ${result.exitCode}`,
|
|
371
329
|
};
|
|
372
330
|
}
|
|
373
331
|
|
|
332
|
+
const tmux = getTmuxAvailability({ requireSessionEnv: true });
|
|
374
333
|
return {
|
|
375
334
|
launched: false,
|
|
376
335
|
usedTmux: false,
|
|
336
|
+
backend: "exec",
|
|
377
337
|
mode,
|
|
378
338
|
toolPath,
|
|
379
|
-
reason: `${tmux.reason ?? "
|
|
339
|
+
reason: `${tmux.reason ?? "no available pane launcher"}; fallback disabled`,
|
|
380
340
|
};
|
|
381
341
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { execFileAsync } from "./subprocess-utils.js";
|
|
2
|
+
|
|
3
|
+
export type TmuxSplitFlag = "-h" | "-v";
|
|
4
|
+
|
|
5
|
+
export interface TmuxPaneInfo {
|
|
6
|
+
paneId: string;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
active: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface PlacementResult {
|
|
13
|
+
targetPane: string;
|
|
14
|
+
splitFlag: TmuxSplitFlag;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const LIST_PANES_FORMAT = "#{pane_id} #{pane_width} #{pane_height} #{pane_active}";
|
|
18
|
+
|
|
19
|
+
export async function listPanes(tmuxPath: string): Promise<TmuxPaneInfo[]> {
|
|
20
|
+
const result = await execFileAsync(tmuxPath, ["list-panes", "-F", LIST_PANES_FORMAT], {
|
|
21
|
+
timeout: 3000,
|
|
22
|
+
});
|
|
23
|
+
if (result.exitCode !== 0) return [];
|
|
24
|
+
|
|
25
|
+
const panes: TmuxPaneInfo[] = [];
|
|
26
|
+
for (const rawLine of result.stdout.split(/\r?\n/)) {
|
|
27
|
+
const line = rawLine.trim();
|
|
28
|
+
if (!line) continue;
|
|
29
|
+
|
|
30
|
+
const parts = line.split(/\s+/);
|
|
31
|
+
if (parts.length < 4) continue;
|
|
32
|
+
|
|
33
|
+
const paneId = parts[0] ?? "";
|
|
34
|
+
const width = Number.parseInt(parts[1] ?? "", 10);
|
|
35
|
+
const height = Number.parseInt(parts[2] ?? "", 10);
|
|
36
|
+
const activeRaw = parts[3] ?? "";
|
|
37
|
+
|
|
38
|
+
if (!paneId || !Number.isFinite(width) || !Number.isFinite(height)) continue;
|
|
39
|
+
panes.push({
|
|
40
|
+
paneId,
|
|
41
|
+
width,
|
|
42
|
+
height,
|
|
43
|
+
active: activeRaw === "1",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return panes;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function findBestSplit(panes: TmuxPaneInfo[]): PlacementResult | null {
|
|
51
|
+
if (panes.length === 0) return null;
|
|
52
|
+
|
|
53
|
+
let best = panes[0];
|
|
54
|
+
let bestArea = best.width * best.height;
|
|
55
|
+
|
|
56
|
+
for (let i = 1; i < panes.length; i++) {
|
|
57
|
+
const pane = panes[i];
|
|
58
|
+
if (!pane) continue;
|
|
59
|
+
const area = pane.width * pane.height;
|
|
60
|
+
if (area > bestArea) {
|
|
61
|
+
best = pane;
|
|
62
|
+
bestArea = area;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Terminal cells are ~2x taller than wide (typical monospace font aspect ratio).
|
|
67
|
+
// Correct for this so BSP splits the visually longer axis, not just the higher
|
|
68
|
+
// character count. Without this, a 77x68 pane looks "wider" in chars but is
|
|
69
|
+
// actually much taller in pixels, and should split top/bottom (-v), not left/right.
|
|
70
|
+
const CELL_ASPECT_RATIO = 2.0;
|
|
71
|
+
const visualWidth = best.width;
|
|
72
|
+
const visualHeight = best.height * CELL_ASPECT_RATIO;
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
targetPane: best.paneId,
|
|
76
|
+
splitFlag: visualWidth >= visualHeight ? "-h" : "-v",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -255,9 +255,6 @@ export function buildExternalAgentContext(
|
|
|
255
255
|
`- **Context folder:** ${displayPath(contextDir)}`,
|
|
256
256
|
`- **Notes folder:** ${displayPath(notesDir)}`,
|
|
257
257
|
];
|
|
258
|
-
if (ctx.summary) {
|
|
259
|
-
lines.push(`- **Summary:** ${ctx.summary}`);
|
|
260
|
-
}
|
|
261
258
|
return lines.join("\n");
|
|
262
259
|
}
|
|
263
260
|
|