editprompt 1.7.0 → 2.0.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 +132 -12
- package/dist/index.js +1154 -833
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,96 +1,411 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { configureSync, getAnsiColorFormatter, getLogger, getTextFormatter, parseLogLevel, withFilter } from "@logtape/logtape";
|
|
3
3
|
import { cli, define } from "gunshi";
|
|
4
|
-
import { appendFileSync } from "node:fs";
|
|
4
|
+
import { appendFileSync, existsSync } from "node:fs";
|
|
5
|
+
import { homedir, tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { exec, spawn } from "node:child_process";
|
|
8
|
+
import Conf from "conf";
|
|
5
9
|
import { createHash, randomBytes } from "node:crypto";
|
|
6
10
|
import { createConnection } from "node:net";
|
|
7
|
-
import Conf from "conf";
|
|
8
|
-
import { exec, spawn } from "node:child_process";
|
|
9
11
|
import { promisify } from "node:util";
|
|
10
12
|
import clipboardy from "clipboardy";
|
|
11
13
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
12
|
-
import { tmpdir } from "node:os";
|
|
13
|
-
import { join } from "node:path";
|
|
14
14
|
|
|
15
|
-
//#region
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
let target = {};
|
|
19
|
-
for (var name in all) {
|
|
20
|
-
__defProp(target, name, {
|
|
21
|
-
get: all[name],
|
|
22
|
-
enumerable: true
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
if (!no_symbols) {
|
|
26
|
-
__defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
27
|
-
}
|
|
28
|
-
return target;
|
|
29
|
-
};
|
|
15
|
+
//#region src/modules/conf.ts
|
|
16
|
+
const projectName = process.env.NODE_ENV === "test" ? "editprompt-test" : "editprompt";
|
|
17
|
+
const conf = new Conf({ projectName });
|
|
30
18
|
|
|
31
19
|
//#endregion
|
|
32
|
-
//#region src/
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
20
|
+
//#region src/backends/stores.ts
|
|
21
|
+
const VAR_EDITOR_PANE = "editor_pane";
|
|
22
|
+
const VAR_IS_EDITOR = "is_editor";
|
|
23
|
+
const VAR_TARGET_PANES = "target_panes";
|
|
24
|
+
const VAR_QUOTE = "quote";
|
|
25
|
+
/**
|
|
26
|
+
* State kept in variables on the panes themselves (tmux pane options, niwaterm
|
|
27
|
+
* tab variables). Variable names are `<prefix>editor_pane`, `<prefix>is_editor`,
|
|
28
|
+
* `<prefix>target_panes` and `<prefix>quote`.
|
|
29
|
+
*
|
|
30
|
+
* The variables disappear with their pane, so forgetting an editor pane is a
|
|
31
|
+
* no-op here: only the link on the target pane is cleared.
|
|
32
|
+
*/
|
|
33
|
+
function paneVariableStore(io, prefix) {
|
|
34
|
+
const name = (key) => `${prefix}${key}`;
|
|
35
|
+
const read = async (paneId, key) => (await io.get(paneId, name(key))).trim();
|
|
36
|
+
return {
|
|
37
|
+
getEditorPaneId: (targetPaneId) => read(targetPaneId, VAR_EDITOR_PANE),
|
|
38
|
+
setEditorPaneId: (targetPaneId, editorPaneId) => io.set(targetPaneId, name(VAR_EDITOR_PANE), editorPaneId),
|
|
39
|
+
removeEditorPaneId: (targetPaneId) => io.set(targetPaneId, name(VAR_EDITOR_PANE), ""),
|
|
40
|
+
isEditorPane: async (paneId) => await read(paneId, VAR_IS_EDITOR) === "1",
|
|
41
|
+
getTargetPaneIds: async (editorPaneId) => {
|
|
42
|
+
const value = await read(editorPaneId, VAR_TARGET_PANES);
|
|
43
|
+
if (value === "") return [];
|
|
44
|
+
return value.split(",").map((id) => id.trim());
|
|
45
|
+
},
|
|
46
|
+
setTargetPaneIds: async (editorPaneId, targetPaneIds) => {
|
|
47
|
+
await io.set(editorPaneId, name(VAR_IS_EDITOR), "1");
|
|
48
|
+
await io.set(editorPaneId, name(VAR_TARGET_PANES), targetPaneIds.join(","));
|
|
49
|
+
},
|
|
50
|
+
removeEditorPane: async () => {},
|
|
51
|
+
keepsEditorPaneMarks: true,
|
|
52
|
+
getQuote: (targetPaneId) => io.get(targetPaneId, name(VAR_QUOTE)),
|
|
53
|
+
setQuote: (targetPaneId, text) => io.set(targetPaneId, name(VAR_QUOTE), text),
|
|
54
|
+
removeQuote: (targetPaneId) => io.set(targetPaneId, name(VAR_QUOTE), "")
|
|
55
|
+
};
|
|
43
56
|
}
|
|
44
|
-
function
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const envLevel = process.env.EDITPROMPT_LOG_LEVEL;
|
|
48
|
-
if (envLevel) return parseLogLevel(envLevel);
|
|
49
|
-
return "info";
|
|
57
|
+
function readRecord(key) {
|
|
58
|
+
const data = conf.get(key);
|
|
59
|
+
return typeof data === "object" && data !== null && !Array.isArray(data) ? data : void 0;
|
|
50
60
|
}
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
level: "ABBR"
|
|
61
|
+
function setField(key, field, value) {
|
|
62
|
+
conf.set(key, {
|
|
63
|
+
...readRecord(key),
|
|
64
|
+
[field]: value
|
|
56
65
|
});
|
|
57
|
-
|
|
58
|
-
|
|
66
|
+
}
|
|
67
|
+
function removeField(key, field) {
|
|
68
|
+
const data = readRecord(key);
|
|
69
|
+
if (!data || !(field in data)) return;
|
|
70
|
+
const { [field]: _, ...remaining } = data;
|
|
71
|
+
if (Object.keys(remaining).length === 0) conf.delete(key);
|
|
72
|
+
else conf.set(key, remaining);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* State kept in editprompt's Conf storage, for backends whose panes cannot hold
|
|
76
|
+
* variables. The layout is:
|
|
77
|
+
*
|
|
78
|
+
* - `<namespace>.targetPane.pane_<id>`: `{ editorPaneId?, quote_text? }`
|
|
79
|
+
* - `<namespace>.editorPane.pane_<id>`: `{ targetPaneIds }`
|
|
80
|
+
*
|
|
81
|
+
* The namespace is resolved on every access so that it can depend on the
|
|
82
|
+
* environment (Herdr scopes it to the session socket).
|
|
83
|
+
*/
|
|
84
|
+
function confStore(namespace, options = {}) {
|
|
85
|
+
const targetKey = (paneId) => `${namespace()}.targetPane.pane_${paneId}`;
|
|
86
|
+
const editorKey = (paneId) => `${namespace()}.editorPane.pane_${paneId}`;
|
|
87
|
+
const store = {
|
|
88
|
+
getEditorPaneId: async (targetPaneId) => {
|
|
89
|
+
const data = readRecord(targetKey(targetPaneId));
|
|
90
|
+
return data && "editorPaneId" in data ? String(data.editorPaneId) : "";
|
|
91
|
+
},
|
|
92
|
+
setEditorPaneId: async (targetPaneId, editorPaneId) => {
|
|
93
|
+
setField(targetKey(targetPaneId), "editorPaneId", editorPaneId);
|
|
94
|
+
},
|
|
95
|
+
removeEditorPaneId: async (targetPaneId) => {
|
|
96
|
+
removeField(targetKey(targetPaneId), "editorPaneId");
|
|
97
|
+
},
|
|
98
|
+
isEditorPane: async (paneId) => conf.has(editorKey(paneId)),
|
|
99
|
+
getTargetPaneIds: async (editorPaneId) => {
|
|
100
|
+
const targetPaneIds = readRecord(editorKey(editorPaneId))?.targetPaneIds;
|
|
101
|
+
return Array.isArray(targetPaneIds) ? targetPaneIds.map((id) => String(id)) : [];
|
|
102
|
+
},
|
|
103
|
+
setTargetPaneIds: async (editorPaneId, targetPaneIds) => {
|
|
104
|
+
conf.set(editorKey(editorPaneId), { targetPaneIds });
|
|
105
|
+
},
|
|
106
|
+
removeEditorPane: async (editorPaneId) => {
|
|
107
|
+
conf.delete(editorKey(editorPaneId));
|
|
108
|
+
},
|
|
109
|
+
getQuote: async (targetPaneId) => {
|
|
110
|
+
const data = readRecord(targetKey(targetPaneId));
|
|
111
|
+
return data && "quote_text" in data ? String(data.quote_text) : "";
|
|
112
|
+
},
|
|
113
|
+
setQuote: async (targetPaneId, text) => {
|
|
114
|
+
setField(targetKey(targetPaneId), "quote_text", text);
|
|
115
|
+
},
|
|
116
|
+
removeQuote: async (targetPaneId) => {
|
|
117
|
+
removeField(targetKey(targetPaneId), "quote_text");
|
|
118
|
+
}
|
|
59
119
|
};
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
120
|
+
if (!options.wholeTargetRecord) return store;
|
|
121
|
+
const logger = getLogger([...options.logCategory ?? ["editprompt", "state"]]);
|
|
122
|
+
return swallowErrors(logger, {
|
|
123
|
+
...store,
|
|
124
|
+
setEditorPaneId: async (targetPaneId, editorPaneId) => {
|
|
125
|
+
logger.debug(`Saving editor pane ID to conf key: ${namespace()}.targetPane.pane_{targetPaneId}`, { targetPaneId });
|
|
126
|
+
conf.set(targetKey(targetPaneId), { editorPaneId });
|
|
127
|
+
},
|
|
128
|
+
removeEditorPaneId: async (targetPaneId) => {
|
|
129
|
+
conf.delete(targetKey(targetPaneId));
|
|
130
|
+
},
|
|
131
|
+
removeQuote: async (targetPaneId) => {
|
|
132
|
+
const key = `${targetKey(targetPaneId)}.quote_text`;
|
|
133
|
+
if (conf.has(key)) conf.delete(key);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
const FAILURE_RESULTS = {
|
|
138
|
+
getEditorPaneId: "",
|
|
139
|
+
setEditorPaneId: void 0,
|
|
140
|
+
removeEditorPaneId: void 0,
|
|
141
|
+
isEditorPane: false,
|
|
142
|
+
getTargetPaneIds: [],
|
|
143
|
+
setTargetPaneIds: void 0,
|
|
144
|
+
removeEditorPane: void 0,
|
|
145
|
+
getQuote: "",
|
|
146
|
+
setQuote: void 0,
|
|
147
|
+
removeQuote: void 0
|
|
148
|
+
};
|
|
149
|
+
const LEGACY_METHOD_NAMES = {
|
|
150
|
+
getEditorPaneId: "getEditorPaneId",
|
|
151
|
+
setEditorPaneId: "saveEditorPaneId",
|
|
152
|
+
removeEditorPaneId: "clearEditorPaneId",
|
|
153
|
+
isEditorPane: "isEditorPaneFromConf",
|
|
154
|
+
getTargetPaneIds: "getTargetPaneIds",
|
|
155
|
+
setTargetPaneIds: "markAsEditorPane",
|
|
156
|
+
removeEditorPane: "clearEditorPaneId",
|
|
157
|
+
getQuote: "getQuoteText",
|
|
158
|
+
setQuote: "appendToQuoteText",
|
|
159
|
+
removeQuote: "clearQuoteText"
|
|
160
|
+
};
|
|
161
|
+
function swallowErrors(logger, store) {
|
|
162
|
+
const wrapped = {};
|
|
163
|
+
for (const method of Object.keys(FAILURE_RESULTS)) {
|
|
164
|
+
const call = store[method];
|
|
165
|
+
wrapped[method] = async (...args) => {
|
|
166
|
+
try {
|
|
167
|
+
return await call(...args);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
logger.debug(`${LEGACY_METHOD_NAMES[method]} failed: {error}`, { error });
|
|
170
|
+
return FAILURE_RESULTS[method];
|
|
171
|
+
}
|
|
72
172
|
};
|
|
73
|
-
sinks.file = withFilter(rawFileSink, fileLogLevel);
|
|
74
|
-
loggerSinks.push("file");
|
|
75
|
-
} catch {
|
|
76
|
-
process.stderr.write(`Warning: Cannot write to log file '${logFilePath}', continuing without file logging.\n`);
|
|
77
173
|
}
|
|
78
|
-
|
|
79
|
-
sinks,
|
|
80
|
-
loggers: [{
|
|
81
|
-
category: ["editprompt"],
|
|
82
|
-
sinks: loggerSinks,
|
|
83
|
-
lowestLevel: "trace"
|
|
84
|
-
}, {
|
|
85
|
-
category: ["logtape", "meta"],
|
|
86
|
-
lowestLevel: null
|
|
87
|
-
}]
|
|
88
|
-
});
|
|
174
|
+
return wrapped;
|
|
89
175
|
}
|
|
90
176
|
|
|
91
177
|
//#endregion
|
|
92
|
-
//#region
|
|
93
|
-
|
|
178
|
+
//#region src/backends/types.ts
|
|
179
|
+
function canSplitPane(backend) {
|
|
180
|
+
return typeof backend.splitPane === "function" && typeof backend.runInPane === "function" && typeof backend.closePane === "function";
|
|
181
|
+
}
|
|
182
|
+
/** Identity helper that gives backend definitions full type checking. */
|
|
183
|
+
function defineBackend(backend) {
|
|
184
|
+
return backend;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
//#endregion
|
|
188
|
+
//#region src/backends/command.ts
|
|
189
|
+
const logger$16 = getLogger(["editprompt", "command-backend"]);
|
|
190
|
+
const COMMAND_VARIABLES = {
|
|
191
|
+
currentPane: [],
|
|
192
|
+
paneExists: ["EDITPROMPT_PANE"],
|
|
193
|
+
focus: ["EDITPROMPT_PANE"],
|
|
194
|
+
sendText: ["EDITPROMPT_PANE"],
|
|
195
|
+
sendKey: ["EDITPROMPT_PANE", "EDITPROMPT_KEY"],
|
|
196
|
+
splitPane: [
|
|
197
|
+
"EDITPROMPT_PANE",
|
|
198
|
+
"EDITPROMPT_ROWS",
|
|
199
|
+
"EDITPROMPT_CWD"
|
|
200
|
+
],
|
|
201
|
+
runInPane: ["EDITPROMPT_PANE", "EDITPROMPT_COMMAND"],
|
|
202
|
+
closePane: ["EDITPROMPT_PANE"]
|
|
203
|
+
};
|
|
204
|
+
const COMMAND_VARIABLE_NAMES = new Set(Object.values(COMMAND_VARIABLES).flat());
|
|
205
|
+
const REQUIRED_COMMANDS = [
|
|
206
|
+
"paneExists",
|
|
207
|
+
"focus",
|
|
208
|
+
"sendText",
|
|
209
|
+
"sendKey"
|
|
210
|
+
];
|
|
211
|
+
const OPTIONAL_COMMANDS = [
|
|
212
|
+
"splitPane",
|
|
213
|
+
"runInPane",
|
|
214
|
+
"closePane"
|
|
215
|
+
];
|
|
216
|
+
const KNOWN_FIELDS = new Set([
|
|
217
|
+
"detect",
|
|
218
|
+
"currentPane",
|
|
219
|
+
...REQUIRED_COMMANDS,
|
|
220
|
+
...OPTIONAL_COMMANDS,
|
|
221
|
+
"maxChunkBytes",
|
|
222
|
+
"submitKey",
|
|
223
|
+
"quoteSeparator",
|
|
224
|
+
"collectInput"
|
|
225
|
+
]);
|
|
226
|
+
const DEFAULT_MAX_CHUNK_BYTES = 8192;
|
|
227
|
+
const SHELL = "/bin/sh";
|
|
228
|
+
const VARIABLE_PATTERN = /\$\{?(EDITPROMPT_[A-Z_]+)/g;
|
|
229
|
+
var CommandBackendConfigError = class extends Error {
|
|
230
|
+
name = "CommandBackendConfigError";
|
|
231
|
+
};
|
|
232
|
+
function isRecord(value) {
|
|
233
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
234
|
+
}
|
|
235
|
+
function parseScript(value, path, field, errors) {
|
|
236
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
237
|
+
errors.push(`${path} must be a non-empty string`);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const allowed = COMMAND_VARIABLES[field];
|
|
241
|
+
const reported = /* @__PURE__ */ new Set();
|
|
242
|
+
for (const [, name] of value.matchAll(VARIABLE_PATTERN)) {
|
|
243
|
+
const variable = name;
|
|
244
|
+
if (COMMAND_VARIABLE_NAMES.has(variable) && !allowed.includes(variable)) {
|
|
245
|
+
if (!reported.has(variable)) {
|
|
246
|
+
reported.add(variable);
|
|
247
|
+
const hint = allowed.length > 0 ? ` (available: ${allowed.map((n) => `$${n}`).join(", ")})` : "";
|
|
248
|
+
errors.push(`${path} uses $${variable}, which is not set for this command${hint}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
path,
|
|
254
|
+
source: value
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function readEnvRef(value) {
|
|
258
|
+
if (isRecord(value) && Object.keys(value).length === 1 && typeof value.env === "string") return value.env;
|
|
259
|
+
}
|
|
260
|
+
function shellQuote(value) {
|
|
261
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
262
|
+
}
|
|
263
|
+
function runScript(script, values, input) {
|
|
264
|
+
return new Promise((resolve, reject) => {
|
|
265
|
+
const child = spawn(SHELL, ["-c", script.source], {
|
|
266
|
+
env: {
|
|
267
|
+
...process.env,
|
|
268
|
+
...values
|
|
269
|
+
},
|
|
270
|
+
stdio: [
|
|
271
|
+
input === void 0 ? "ignore" : "pipe",
|
|
272
|
+
"pipe",
|
|
273
|
+
"pipe"
|
|
274
|
+
]
|
|
275
|
+
});
|
|
276
|
+
let stdout = "";
|
|
277
|
+
let stderr = "";
|
|
278
|
+
child.stdout?.setEncoding("utf8");
|
|
279
|
+
child.stdout?.on("data", (chunk) => {
|
|
280
|
+
stdout += chunk;
|
|
281
|
+
});
|
|
282
|
+
child.stderr?.setEncoding("utf8");
|
|
283
|
+
child.stderr?.on("data", (chunk) => {
|
|
284
|
+
stderr += chunk;
|
|
285
|
+
});
|
|
286
|
+
child.on("error", (error) => {
|
|
287
|
+
reject(/* @__PURE__ */ new Error(`Failed to run ${script.path}: ${error.message}`));
|
|
288
|
+
});
|
|
289
|
+
child.on("close", (code) => {
|
|
290
|
+
resolve({
|
|
291
|
+
code,
|
|
292
|
+
stdout,
|
|
293
|
+
stderr
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
if (input !== void 0) {
|
|
297
|
+
child.stdin?.on("error", (error) => {
|
|
298
|
+
reject(/* @__PURE__ */ new Error(`Failed to write to ${script.path}: ${error.message}`));
|
|
299
|
+
});
|
|
300
|
+
child.stdin?.end(input);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
async function runChecked(script, values, input) {
|
|
305
|
+
const result = await runScript(script, values, input);
|
|
306
|
+
if (result.code !== 0) throw new Error(`${script.path} exited with ${result.code}: ${result.stderr.trim()}`);
|
|
307
|
+
return result.stdout;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Validates a backend declared in the configuration file and builds it.
|
|
311
|
+
* Throws CommandBackendConfigError listing every problem found.
|
|
312
|
+
*/
|
|
313
|
+
function createCommandBackend(name, spec) {
|
|
314
|
+
const path = `backends.${name}`;
|
|
315
|
+
const errors = [];
|
|
316
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(name)) errors.push(`${path}: the name may only contain letters, digits, "-" and "_"`);
|
|
317
|
+
if (!isRecord(spec)) throw new CommandBackendConfigError(`${path} must be an object`);
|
|
318
|
+
for (const key of Object.keys(spec)) if (!KNOWN_FIELDS.has(key)) errors.push(`${path}.${key} is not a known option`);
|
|
319
|
+
let detectEnv;
|
|
320
|
+
if (spec.detect !== void 0) {
|
|
321
|
+
detectEnv = readEnvRef(spec.detect);
|
|
322
|
+
if (detectEnv === void 0) errors.push(`${path}.detect must be { env = "VARIABLE_NAME" }`);
|
|
323
|
+
}
|
|
324
|
+
const currentPaneEnv = readEnvRef(spec.currentPane);
|
|
325
|
+
let currentPaneScript;
|
|
326
|
+
if (spec.currentPane === void 0) errors.push(`${path}.currentPane is required`);
|
|
327
|
+
else if (currentPaneEnv === void 0) currentPaneScript = parseScript(spec.currentPane, `${path}.currentPane`, "currentPane", errors);
|
|
328
|
+
const commands = {};
|
|
329
|
+
for (const field of REQUIRED_COMMANDS) if (spec[field] === void 0) errors.push(`${path}.${field} is required`);
|
|
330
|
+
else commands[field] = parseScript(spec[field], `${path}.${field}`, field, errors);
|
|
331
|
+
const optionalCount = OPTIONAL_COMMANDS.filter((field) => spec[field] !== void 0).length;
|
|
332
|
+
if (optionalCount > 0 && optionalCount < OPTIONAL_COMMANDS.length) errors.push(`${path}: splitPane, runInPane and closePane must be set together`);
|
|
333
|
+
for (const field of OPTIONAL_COMMANDS) if (spec[field] !== void 0) commands[field] = parseScript(spec[field], `${path}.${field}`, field, errors);
|
|
334
|
+
const maxChunkBytes = spec.maxChunkBytes ?? DEFAULT_MAX_CHUNK_BYTES;
|
|
335
|
+
if (typeof maxChunkBytes !== "number" || !Number.isInteger(maxChunkBytes) || maxChunkBytes < 4) errors.push(`${path}.maxChunkBytes must be an integer of at least 4`);
|
|
336
|
+
const submitKey = spec.submitKey ?? "Enter";
|
|
337
|
+
if (typeof submitKey !== "string" || submitKey === "") errors.push(`${path}.submitKey must be a non-empty string`);
|
|
338
|
+
const quoteSeparator = spec.quoteSeparator ?? "\n\n";
|
|
339
|
+
if (typeof quoteSeparator !== "string") errors.push(`${path}.quoteSeparator must be a string`);
|
|
340
|
+
const collectInput = spec.collectInput ?? "argument";
|
|
341
|
+
if (collectInput !== "stdin" && collectInput !== "argument") errors.push(`${path}.collectInput must be "stdin" or "argument"`);
|
|
342
|
+
if (errors.length > 0) throw new CommandBackendConfigError(errors.join("\n"));
|
|
343
|
+
const command = (field) => commands[field];
|
|
344
|
+
const namespace = () => `state.${name}`;
|
|
345
|
+
const backend = defineBackend({
|
|
346
|
+
name,
|
|
347
|
+
detect: detectEnv ? (env) => (env[detectEnv]?.trim() ?? "") !== "" : void 0,
|
|
348
|
+
getCurrentPaneId: async () => {
|
|
349
|
+
if (currentPaneEnv !== void 0) {
|
|
350
|
+
const paneId = process.env[currentPaneEnv]?.trim();
|
|
351
|
+
if (!paneId) throw new Error(`${currentPaneEnv} is not set`);
|
|
352
|
+
return paneId;
|
|
353
|
+
}
|
|
354
|
+
return (await runChecked(currentPaneScript, {})).trim();
|
|
355
|
+
},
|
|
356
|
+
paneExists: async (paneId) => {
|
|
357
|
+
try {
|
|
358
|
+
return (await runScript(command("paneExists"), { EDITPROMPT_PANE: paneId })).code === 0;
|
|
359
|
+
} catch (error) {
|
|
360
|
+
logger$16.debug("paneExists failed: {error}", { error });
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
focusPane: async (paneId) => {
|
|
365
|
+
await runChecked(command("focus"), { EDITPROMPT_PANE: paneId });
|
|
366
|
+
},
|
|
367
|
+
sendText: async (paneId, chunk) => {
|
|
368
|
+
await runChecked(command("sendText"), { EDITPROMPT_PANE: paneId }, chunk);
|
|
369
|
+
},
|
|
370
|
+
sendKey: async (paneId, key) => {
|
|
371
|
+
await runChecked(command("sendKey"), {
|
|
372
|
+
EDITPROMPT_PANE: paneId,
|
|
373
|
+
EDITPROMPT_KEY: key
|
|
374
|
+
});
|
|
375
|
+
},
|
|
376
|
+
maxChunkBytes,
|
|
377
|
+
defaultSubmitKey: submitKey,
|
|
378
|
+
quoteSeparator,
|
|
379
|
+
collectInput,
|
|
380
|
+
storageNamespace: namespace,
|
|
381
|
+
resumeAcceptsEditorPane: true,
|
|
382
|
+
keepNewerEditorLink: true,
|
|
383
|
+
quietResumeErrors: "all",
|
|
384
|
+
state: confStore(namespace)
|
|
385
|
+
});
|
|
386
|
+
if (commands.splitPane && commands.runInPane && commands.closePane) {
|
|
387
|
+
const { splitPane, runInPane, closePane } = commands;
|
|
388
|
+
backend.splitPane = async (targetPaneId, options) => {
|
|
389
|
+
const paneId = (await runChecked(splitPane, {
|
|
390
|
+
EDITPROMPT_PANE: targetPaneId,
|
|
391
|
+
EDITPROMPT_ROWS: String(options.rows),
|
|
392
|
+
EDITPROMPT_CWD: options.cwd ?? process.cwd()
|
|
393
|
+
})).trim();
|
|
394
|
+
if (paneId === "") throw new Error(`${path}.splitPane did not print the new pane ID`);
|
|
395
|
+
return paneId;
|
|
396
|
+
};
|
|
397
|
+
backend.runInPane = async (paneId, argv) => {
|
|
398
|
+
await runChecked(runInPane, {
|
|
399
|
+
EDITPROMPT_PANE: paneId,
|
|
400
|
+
EDITPROMPT_COMMAND: argv.map(shellQuote).join(" ")
|
|
401
|
+
});
|
|
402
|
+
};
|
|
403
|
+
backend.closePane = async (paneId) => {
|
|
404
|
+
await runChecked(closePane, { EDITPROMPT_PANE: paneId });
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
return backend;
|
|
408
|
+
}
|
|
94
409
|
|
|
95
410
|
//#endregion
|
|
96
411
|
//#region src/config/constants.ts
|
|
@@ -103,65 +418,12 @@ const WEZTERM_SEND_CHUNK_BYTES = TMUX_SEND_CHUNK_BYTES;
|
|
|
103
418
|
const HERDR_REQUEST_LIMIT_BYTES = 1024 * 1024;
|
|
104
419
|
const HERDR_SEND_CHUNK_BYTES = HERDR_REQUEST_LIMIT_BYTES / 8;
|
|
105
420
|
const NIWATERM_SEND_CHUNK_BYTES = WEZTERM_SEND_CHUNK_BYTES;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
//#region src/utils/contentChunker.ts
|
|
109
|
-
/**
|
|
110
|
-
* Split content into chunks not exceeding maxBytes (UTF-8 byte count),
|
|
111
|
-
* never breaking a Unicode code point across chunks.
|
|
112
|
-
*
|
|
113
|
-
* Content shorter than maxBytes is returned as a single chunk, so short
|
|
114
|
-
* input keeps the previous behavior of being sent in one shot.
|
|
115
|
-
*/
|
|
116
|
-
function splitByByteSize(content, maxBytes) {
|
|
117
|
-
if (content === "") return [];
|
|
118
|
-
const chunks = [];
|
|
119
|
-
let current = "";
|
|
120
|
-
let currentBytes = 0;
|
|
121
|
-
for (const char of content) {
|
|
122
|
-
const charBytes = Buffer.byteLength(char, "utf8");
|
|
123
|
-
if (current !== "" && currentBytes + charBytes > maxBytes) {
|
|
124
|
-
chunks.push(current);
|
|
125
|
-
current = "";
|
|
126
|
-
currentBytes = 0;
|
|
127
|
-
}
|
|
128
|
-
current += char;
|
|
129
|
-
currentBytes += charBytes;
|
|
130
|
-
}
|
|
131
|
-
if (current !== "") chunks.push(current);
|
|
132
|
-
return chunks;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
//#endregion
|
|
136
|
-
//#region src/modules/conf.ts
|
|
137
|
-
const projectName = process.env.NODE_ENV === "test" ? "editprompt-test" : "editprompt";
|
|
138
|
-
const conf = new Conf({ projectName });
|
|
421
|
+
const REX_CONTROL_LIMIT_BYTES = 64 * 1024;
|
|
422
|
+
const REX_SEND_CHUNK_BYTES = REX_CONTROL_LIMIT_BYTES / 8;
|
|
139
423
|
|
|
140
424
|
//#endregion
|
|
141
425
|
//#region src/modules/herdr.ts
|
|
142
|
-
|
|
143
|
-
appendToQuoteText: () => appendToQuoteText$1,
|
|
144
|
-
calculateEditorSplitRatio: () => calculateEditorSplitRatio,
|
|
145
|
-
checkPaneExists: () => checkPaneExists$3,
|
|
146
|
-
clearEditorPaneId: () => clearEditorPaneId$3,
|
|
147
|
-
clearQuoteText: () => clearQuoteText$1,
|
|
148
|
-
closePane: () => closePane,
|
|
149
|
-
focusPane: () => focusPane$3,
|
|
150
|
-
getCurrentPaneId: () => getCurrentPaneId$3,
|
|
151
|
-
getEditorPaneId: () => getEditorPaneId$3,
|
|
152
|
-
getQuoteText: () => getQuoteText$1,
|
|
153
|
-
getStorageNamespace: () => getStorageNamespace,
|
|
154
|
-
getTargetPaneIds: () => getTargetPaneIds$3,
|
|
155
|
-
inputToHerdrPane: () => inputToHerdrPane,
|
|
156
|
-
isEditorPaneFromConf: () => isEditorPaneFromConf$1,
|
|
157
|
-
markAsEditorPane: () => markAsEditorPane$3,
|
|
158
|
-
resumeEditorPane: () => resumeEditorPane,
|
|
159
|
-
runCommandInPane: () => runCommandInPane,
|
|
160
|
-
saveEditorPaneId: () => saveEditorPaneId$3,
|
|
161
|
-
sendKeyToHerdrPane: () => sendKeyToHerdrPane,
|
|
162
|
-
splitEditorPane: () => splitEditorPane
|
|
163
|
-
});
|
|
164
|
-
const logger$14 = getLogger(["editprompt", "herdr"]);
|
|
426
|
+
const logger$15 = getLogger(["editprompt", "herdr"]);
|
|
165
427
|
const HERDR_REQUEST_TIMEOUT_MS = 6e3;
|
|
166
428
|
let requestSequence = 0;
|
|
167
429
|
function getSocketPath() {
|
|
@@ -242,27 +504,21 @@ async function sendRequest(method, params) {
|
|
|
242
504
|
function getStorageNamespace() {
|
|
243
505
|
return `herdr.session_${createHash("sha256").update(getSocketPath()).digest("hex").slice(0, 16)}`;
|
|
244
506
|
}
|
|
245
|
-
function
|
|
246
|
-
return `${getStorageNamespace()}.targetPane.pane_${paneId}`;
|
|
247
|
-
}
|
|
248
|
-
function editorPaneKey(paneId) {
|
|
249
|
-
return `${getStorageNamespace()}.editorPane.pane_${paneId}`;
|
|
250
|
-
}
|
|
251
|
-
async function getCurrentPaneId$3() {
|
|
507
|
+
async function getCurrentPaneId$4() {
|
|
252
508
|
const paneId = process.env.HERDR_PANE_ID?.trim() || process.env.HERDR_ACTIVE_PANE_ID?.trim();
|
|
253
509
|
if (!paneId) throw new Error("HERDR_PANE_ID or HERDR_ACTIVE_PANE_ID is not set");
|
|
254
510
|
return paneId;
|
|
255
511
|
}
|
|
256
|
-
async function checkPaneExists$
|
|
512
|
+
async function checkPaneExists$4(paneId) {
|
|
257
513
|
try {
|
|
258
514
|
await sendRequest("pane.get", { pane_id: paneId });
|
|
259
515
|
return true;
|
|
260
516
|
} catch (error) {
|
|
261
|
-
logger$
|
|
517
|
+
logger$15.debug("checkPaneExists failed: {error}", { error });
|
|
262
518
|
return false;
|
|
263
519
|
}
|
|
264
520
|
}
|
|
265
|
-
async function focusPane$
|
|
521
|
+
async function focusPane$4(paneId) {
|
|
266
522
|
await sendRequest("pane.focus", { pane_id: paneId });
|
|
267
523
|
}
|
|
268
524
|
function calculateEditorSplitRatio(paneHeight, editorPaneRows) {
|
|
@@ -299,123 +555,73 @@ async function runCommandInPane(paneId, argv) {
|
|
|
299
555
|
async function closePane(paneId) {
|
|
300
556
|
await sendRequest("pane.close", { pane_id: paneId });
|
|
301
557
|
}
|
|
302
|
-
async function
|
|
303
|
-
|
|
304
|
-
for (const chunk of chunks) await sendRequest("pane.send_text", {
|
|
558
|
+
async function sendTextChunk$4(paneId, chunk) {
|
|
559
|
+
await sendRequest("pane.send_text", {
|
|
305
560
|
pane_id: paneId,
|
|
306
561
|
text: chunk
|
|
307
562
|
});
|
|
308
|
-
logger$14.debug("Content sent to Herdr pane: {paneId} ({chunkCount} chunks)", {
|
|
309
|
-
paneId,
|
|
310
|
-
chunkCount: chunks.length
|
|
311
|
-
});
|
|
312
563
|
}
|
|
313
|
-
async function
|
|
314
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
564
|
+
async function sendKey$4(paneId, key) {
|
|
315
565
|
await sendRequest("pane.send_keys", {
|
|
316
566
|
pane_id: paneId,
|
|
317
567
|
keys: [key]
|
|
318
568
|
});
|
|
319
569
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
...existing,
|
|
325
|
-
editorPaneId
|
|
570
|
+
function logContentSent$4(paneId, chunkCount) {
|
|
571
|
+
logger$15.debug("Content sent to Herdr pane: {paneId} ({chunkCount} chunks)", {
|
|
572
|
+
paneId,
|
|
573
|
+
chunkCount
|
|
326
574
|
});
|
|
327
575
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
async function clearEditorPaneId$3(targetPaneId, expectedEditorPaneId) {
|
|
334
|
-
const editorPaneId = await getEditorPaneId$3(targetPaneId);
|
|
335
|
-
if (expectedEditorPaneId && editorPaneId !== expectedEditorPaneId) {
|
|
336
|
-
conf.delete(editorPaneKey(expectedEditorPaneId));
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
const data = conf.get(targetPaneKey(targetPaneId));
|
|
340
|
-
if (typeof data === "object" && data !== null && "editorPaneId" in data) {
|
|
341
|
-
const { editorPaneId: _, ...remaining } = data;
|
|
342
|
-
if (Object.keys(remaining).length === 0) conf.delete(targetPaneKey(targetPaneId));
|
|
343
|
-
else conf.set(targetPaneKey(targetPaneId), remaining);
|
|
344
|
-
}
|
|
345
|
-
if (editorPaneId) conf.delete(editorPaneKey(editorPaneId));
|
|
346
|
-
}
|
|
347
|
-
async function markAsEditorPane$3(editorPaneId, targetPaneIds) {
|
|
348
|
-
const uniqueTargetPaneIds = [...new Set(targetPaneIds)];
|
|
349
|
-
conf.set(editorPaneKey(editorPaneId), { targetPaneIds: uniqueTargetPaneIds });
|
|
350
|
-
for (const targetPaneId of uniqueTargetPaneIds) await saveEditorPaneId$3(targetPaneId, editorPaneId);
|
|
351
|
-
}
|
|
352
|
-
async function getTargetPaneIds$3(editorPaneId) {
|
|
353
|
-
const data = conf.get(editorPaneKey(editorPaneId));
|
|
354
|
-
if (typeof data === "object" && data !== null && "targetPaneIds" in data) {
|
|
355
|
-
const targetPaneIds = data.targetPaneIds;
|
|
356
|
-
if (Array.isArray(targetPaneIds)) return targetPaneIds.map((id) => String(id));
|
|
357
|
-
}
|
|
358
|
-
return [];
|
|
359
|
-
}
|
|
360
|
-
function isEditorPaneFromConf$1(paneId) {
|
|
361
|
-
return conf.has(editorPaneKey(paneId));
|
|
362
|
-
}
|
|
363
|
-
async function resumeEditorPane(targetPaneId) {
|
|
364
|
-
const currentPaneId = await getCurrentPaneId$3();
|
|
365
|
-
const editorPaneId = isEditorPaneFromConf$1(currentPaneId) ? currentPaneId : isEditorPaneFromConf$1(targetPaneId) ? targetPaneId : "";
|
|
366
|
-
if (editorPaneId) {
|
|
367
|
-
const targetPaneIds = await getTargetPaneIds$3(editorPaneId);
|
|
368
|
-
for (const paneId of targetPaneIds) if (await checkPaneExists$3(paneId)) {
|
|
369
|
-
await focusPane$3(paneId);
|
|
370
|
-
return true;
|
|
371
|
-
}
|
|
372
|
-
return false;
|
|
373
|
-
}
|
|
374
|
-
const registeredEditorPaneId = await getEditorPaneId$3(targetPaneId);
|
|
375
|
-
if (registeredEditorPaneId === "") return false;
|
|
376
|
-
if (!await checkPaneExists$3(registeredEditorPaneId)) {
|
|
377
|
-
await clearEditorPaneId$3(targetPaneId);
|
|
378
|
-
return false;
|
|
379
|
-
}
|
|
380
|
-
await focusPane$3(registeredEditorPaneId);
|
|
381
|
-
return true;
|
|
576
|
+
|
|
577
|
+
//#endregion
|
|
578
|
+
//#region src/backends/env.ts
|
|
579
|
+
function hasValue(value) {
|
|
580
|
+
return typeof value === "string" && value.trim() !== "";
|
|
382
581
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const existing = typeof data === "object" && data !== null ? data : {};
|
|
386
|
-
const existingQuoteText = "quote_text" in existing ? String(existing.quote_text) : "";
|
|
387
|
-
const quoteText = existingQuoteText.trim() !== "" ? `${existingQuoteText}\n\n${content}` : content;
|
|
388
|
-
conf.set(targetPaneKey(paneId), {
|
|
389
|
-
...existing,
|
|
390
|
-
quote_text: quoteText
|
|
391
|
-
});
|
|
582
|
+
function isHerdrEnvironment(env = process.env) {
|
|
583
|
+
return hasValue(env.HERDR_SOCKET_PATH) && (hasValue(env.HERDR_PANE_ID) || hasValue(env.HERDR_ACTIVE_PANE_ID));
|
|
392
584
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
if (typeof data === "object" && data !== null && "quote_text" in data) return String(data.quote_text);
|
|
396
|
-
return "";
|
|
585
|
+
function isNiwatermEnvironment(env = process.env) {
|
|
586
|
+
return hasValue(env.NIWATERM_TAB_ID);
|
|
397
587
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
if (typeof data === "object" && data !== null && "quote_text" in data) {
|
|
401
|
-
const { quote_text: _, ...remaining } = data;
|
|
402
|
-
if (Object.keys(remaining).length === 0) conf.delete(targetPaneKey(paneId));
|
|
403
|
-
else conf.set(targetPaneKey(paneId), remaining);
|
|
404
|
-
}
|
|
588
|
+
function isRexEnvironment(env = process.env) {
|
|
589
|
+
return hasValue(env.REX_BLOCK);
|
|
405
590
|
}
|
|
406
591
|
|
|
592
|
+
//#endregion
|
|
593
|
+
//#region src/backends/herdr.ts
|
|
594
|
+
const herdrBackend = defineBackend({
|
|
595
|
+
name: "herdr",
|
|
596
|
+
detect: (env) => isHerdrEnvironment(env),
|
|
597
|
+
getCurrentPaneId: () => getCurrentPaneId$4(),
|
|
598
|
+
paneExists: (paneId) => checkPaneExists$4(paneId),
|
|
599
|
+
focusPane: (paneId) => focusPane$4(paneId),
|
|
600
|
+
sendText: (paneId, chunk) => sendTextChunk$4(paneId, chunk),
|
|
601
|
+
sendKey: (paneId, key) => sendKey$4(paneId, key),
|
|
602
|
+
logContentSent: (paneId, chunkCount) => logContentSent$4(paneId, chunkCount),
|
|
603
|
+
maxChunkBytes: HERDR_SEND_CHUNK_BYTES,
|
|
604
|
+
defaultSubmitKey: "enter",
|
|
605
|
+
quoteSeparator: "\n\n",
|
|
606
|
+
collectInput: "argument",
|
|
607
|
+
quietResumeErrors: "all",
|
|
608
|
+
resumeAcceptsEditorPane: true,
|
|
609
|
+
keepNewerEditorLink: true,
|
|
610
|
+
storageNamespace: () => getStorageNamespace(),
|
|
611
|
+
state: confStore(() => getStorageNamespace()),
|
|
612
|
+
splitPane: (targetPaneId, { rows, cwd, env }) => splitEditorPane(targetPaneId, rows, cwd ?? process.env.HERDR_ACTIVE_PANE_CWD, env),
|
|
613
|
+
runInPane: (paneId, argv) => runCommandInPane(paneId, argv),
|
|
614
|
+
closePane: (paneId) => closePane(paneId)
|
|
615
|
+
});
|
|
616
|
+
|
|
407
617
|
//#endregion
|
|
408
618
|
//#region src/modules/niwaterm.ts
|
|
409
619
|
const execAsync$2 = promisify(exec);
|
|
410
|
-
const logger$
|
|
411
|
-
const VAR_EDITOR_PANE = "editprompt_editor_pane";
|
|
412
|
-
const VAR_IS_EDITOR = "editprompt_is_editor";
|
|
413
|
-
const VAR_TARGET_PANES = "editprompt_target_panes";
|
|
414
|
-
const VAR_QUOTE = "editprompt_quote";
|
|
620
|
+
const logger$14 = getLogger(["editprompt", "niwaterm"]);
|
|
415
621
|
function quoteShellWord(value) {
|
|
416
622
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
417
623
|
}
|
|
418
|
-
async function getCurrentPaneId$
|
|
624
|
+
async function getCurrentPaneId$3() {
|
|
419
625
|
const tabId = process.env.NIWATERM_TAB_ID?.trim();
|
|
420
626
|
if (!tabId) throw new Error("NIWATERM_TAB_ID is not set");
|
|
421
627
|
return tabId;
|
|
@@ -425,71 +631,184 @@ async function getTabVar(tabId, key) {
|
|
|
425
631
|
const { stdout } = await execAsync$2(`niwa tab var get -t ${quoteShellWord(tabId)} ${quoteShellWord(key)}`);
|
|
426
632
|
return stdout.trim();
|
|
427
633
|
} catch (error) {
|
|
428
|
-
logger$
|
|
634
|
+
logger$14.debug("getTabVar failed: {error}", { error });
|
|
429
635
|
return "";
|
|
430
636
|
}
|
|
431
637
|
}
|
|
432
638
|
async function setTabVar(tabId, key, value) {
|
|
433
639
|
await execAsync$2(`niwa tab var set -t ${quoteShellWord(tabId)} ${quoteShellWord(key)} ${quoteShellWord(value)}`);
|
|
434
640
|
}
|
|
435
|
-
async function checkPaneExists$
|
|
641
|
+
async function checkPaneExists$3(paneId) {
|
|
436
642
|
try {
|
|
437
643
|
await execAsync$2(`niwa tab var list -t ${quoteShellWord(paneId)}`);
|
|
438
644
|
return true;
|
|
439
645
|
} catch (error) {
|
|
440
|
-
logger$
|
|
646
|
+
logger$14.debug("checkPaneExists failed: {error}", { error });
|
|
441
647
|
return false;
|
|
442
648
|
}
|
|
443
649
|
}
|
|
444
|
-
async function focusPane$
|
|
650
|
+
async function focusPane$3(paneId) {
|
|
445
651
|
await execAsync$2(`niwa tab focus -t ${quoteShellWord(paneId)}`);
|
|
446
652
|
}
|
|
447
|
-
async function
|
|
448
|
-
await
|
|
449
|
-
}
|
|
450
|
-
async function clearEditorPaneId$2(targetPaneId) {
|
|
451
|
-
await setTabVar(targetPaneId, VAR_EDITOR_PANE, "");
|
|
452
|
-
}
|
|
453
|
-
async function getEditorPaneId$2(targetPaneId) {
|
|
454
|
-
return getTabVar(targetPaneId, VAR_EDITOR_PANE);
|
|
455
|
-
}
|
|
456
|
-
async function markAsEditorPane$2(editorPaneId, targetPaneIds) {
|
|
457
|
-
const uniqueTargetPaneIds = [...new Set(targetPaneIds)];
|
|
458
|
-
await setTabVar(editorPaneId, VAR_IS_EDITOR, "1");
|
|
459
|
-
await setTabVar(editorPaneId, VAR_TARGET_PANES, uniqueTargetPaneIds.join(","));
|
|
460
|
-
for (const targetPaneId of uniqueTargetPaneIds) await saveEditorPaneId$2(targetPaneId, editorPaneId);
|
|
653
|
+
async function sendKey$3(paneId, key) {
|
|
654
|
+
await execAsync$2(`niwa tab send-keys -t ${quoteShellWord(paneId)} ${quoteShellWord(key)}`);
|
|
461
655
|
}
|
|
462
|
-
async function
|
|
463
|
-
|
|
464
|
-
if (value === "") return [];
|
|
465
|
-
return value.split(",").map((id) => id.trim());
|
|
656
|
+
async function sendTextChunk$3(paneId, chunk) {
|
|
657
|
+
await execAsync$2(`niwa tab send-keys -t ${quoteShellWord(paneId)} -- ${quoteShellWord(chunk)}`);
|
|
466
658
|
}
|
|
467
|
-
|
|
468
|
-
|
|
659
|
+
function logContentSent$3(paneId, chunkCount) {
|
|
660
|
+
logger$14.debug("Content sent to niwaterm pane: {paneId} ({chunks} chunk(s))", {
|
|
661
|
+
paneId,
|
|
662
|
+
chunks: chunkCount
|
|
663
|
+
});
|
|
469
664
|
}
|
|
470
|
-
|
|
471
|
-
|
|
665
|
+
|
|
666
|
+
//#endregion
|
|
667
|
+
//#region src/backends/niwaterm.ts
|
|
668
|
+
const niwatermBackend = defineBackend({
|
|
669
|
+
name: "niwaterm",
|
|
670
|
+
detect: (env) => isNiwatermEnvironment(env),
|
|
671
|
+
getCurrentPaneId: () => getCurrentPaneId$3(),
|
|
672
|
+
paneExists: (paneId) => checkPaneExists$3(paneId),
|
|
673
|
+
focusPane: (paneId) => focusPane$3(paneId),
|
|
674
|
+
sendText: (paneId, chunk) => sendTextChunk$3(paneId, chunk),
|
|
675
|
+
sendKey: (paneId, key) => sendKey$3(paneId, key),
|
|
676
|
+
logContentSent: (paneId, chunkCount) => logContentSent$3(paneId, chunkCount),
|
|
677
|
+
maxChunkBytes: NIWATERM_SEND_CHUNK_BYTES,
|
|
678
|
+
defaultSubmitKey: "C-m",
|
|
679
|
+
quoteSeparator: "\n",
|
|
680
|
+
collectInput: "argument",
|
|
681
|
+
quietResumeErrors: "editorFocus",
|
|
682
|
+
traceResume: true,
|
|
683
|
+
storageNamespace: () => "niwaterm",
|
|
684
|
+
state: paneVariableStore({
|
|
685
|
+
get: (paneId, name) => getTabVar(paneId, name),
|
|
686
|
+
set: (paneId, name, value) => setTabVar(paneId, name, value)
|
|
687
|
+
}, "editprompt_")
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
//#endregion
|
|
691
|
+
//#region src/modules/rex.ts
|
|
692
|
+
const logger$13 = getLogger(["editprompt", "rex"]);
|
|
693
|
+
const REX_COMMAND = "rex";
|
|
694
|
+
const REX_GLOBAL_ARGS = ["--autostart=false"];
|
|
695
|
+
function rexCommand() {
|
|
696
|
+
const binDir = process.env.REX_BIN_DIR?.trim();
|
|
697
|
+
return binDir ? join(binDir, REX_COMMAND) : REX_COMMAND;
|
|
698
|
+
}
|
|
699
|
+
function rexArgs(args) {
|
|
700
|
+
return [...REX_GLOBAL_ARGS, ...args];
|
|
701
|
+
}
|
|
702
|
+
function runRex(args, input) {
|
|
703
|
+
const command = rexCommand();
|
|
704
|
+
const argv = rexArgs(args);
|
|
705
|
+
return new Promise((resolve, reject) => {
|
|
706
|
+
const child = spawn(command, argv, { stdio: [
|
|
707
|
+
input === void 0 ? "ignore" : "pipe",
|
|
708
|
+
"pipe",
|
|
709
|
+
"pipe"
|
|
710
|
+
] });
|
|
711
|
+
let stdout = "";
|
|
712
|
+
let stderr = "";
|
|
713
|
+
child.stdout?.setEncoding("utf8");
|
|
714
|
+
child.stdout?.on("data", (chunk) => {
|
|
715
|
+
stdout += chunk;
|
|
716
|
+
});
|
|
717
|
+
child.stderr?.setEncoding("utf8");
|
|
718
|
+
child.stderr?.on("data", (chunk) => {
|
|
719
|
+
stderr += chunk;
|
|
720
|
+
});
|
|
721
|
+
child.on("error", (error) => {
|
|
722
|
+
reject(/* @__PURE__ */ new Error(`Failed to run rex: ${error.message}`));
|
|
723
|
+
});
|
|
724
|
+
child.on("close", (code) => {
|
|
725
|
+
if (code === 0) {
|
|
726
|
+
resolve(stdout);
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
reject(/* @__PURE__ */ new Error(`rex ${argv.join(" ")} exited with ${code}: ${stderr.trim()}`));
|
|
730
|
+
});
|
|
731
|
+
if (input !== void 0) {
|
|
732
|
+
if (!child.stdin) {
|
|
733
|
+
reject(/* @__PURE__ */ new Error("rex was started without a standard input to write to"));
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
child.stdin.on("error", (error) => {
|
|
737
|
+
reject(/* @__PURE__ */ new Error(`Failed to write to rex: ${error.message}`));
|
|
738
|
+
});
|
|
739
|
+
child.stdin.end(input);
|
|
740
|
+
}
|
|
741
|
+
});
|
|
472
742
|
}
|
|
473
|
-
async function
|
|
474
|
-
const
|
|
475
|
-
|
|
743
|
+
async function getCurrentPaneId$2() {
|
|
744
|
+
const paneId = process.env.REX_BLOCK?.trim();
|
|
745
|
+
if (!paneId) throw new Error("REX_BLOCK is not set");
|
|
746
|
+
return paneId;
|
|
476
747
|
}
|
|
477
|
-
async function
|
|
478
|
-
|
|
748
|
+
async function checkPaneExists$2(paneId) {
|
|
749
|
+
try {
|
|
750
|
+
await runRex([
|
|
751
|
+
"block",
|
|
752
|
+
"inspect",
|
|
753
|
+
paneId,
|
|
754
|
+
"--json"
|
|
755
|
+
]);
|
|
756
|
+
return true;
|
|
757
|
+
} catch (error) {
|
|
758
|
+
logger$13.debug("checkPaneExists failed: {error}", { error });
|
|
759
|
+
return false;
|
|
760
|
+
}
|
|
479
761
|
}
|
|
480
|
-
async function
|
|
481
|
-
await
|
|
482
|
-
|
|
762
|
+
async function focusPane$2(paneId) {
|
|
763
|
+
await runRex([
|
|
764
|
+
"block",
|
|
765
|
+
"focus",
|
|
766
|
+
paneId
|
|
767
|
+
]);
|
|
768
|
+
}
|
|
769
|
+
async function sendTextChunk$2(paneId, chunk) {
|
|
770
|
+
await runRex([
|
|
771
|
+
"send",
|
|
772
|
+
"-b",
|
|
773
|
+
paneId
|
|
774
|
+
], chunk);
|
|
775
|
+
}
|
|
776
|
+
async function sendKey$2(paneId, key) {
|
|
777
|
+
await runRex([
|
|
778
|
+
"send-key",
|
|
779
|
+
"-b",
|
|
780
|
+
paneId,
|
|
781
|
+
key
|
|
782
|
+
]);
|
|
483
783
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
for (const chunk of chunks) await execAsync$2(`niwa tab send-keys -t ${quoteShellWord(paneId)} ${quoteShellWord(chunk)}`);
|
|
487
|
-
logger$13.debug("Content sent to niwaterm pane: {paneId} ({chunks} chunk(s))", {
|
|
784
|
+
function logContentSent$2(paneId, chunkCount) {
|
|
785
|
+
logger$13.debug("Content sent to Rex pane: {paneId} ({chunks} chunk(s))", {
|
|
488
786
|
paneId,
|
|
489
|
-
chunks:
|
|
787
|
+
chunks: chunkCount
|
|
490
788
|
});
|
|
491
789
|
}
|
|
492
790
|
|
|
791
|
+
//#endregion
|
|
792
|
+
//#region src/backends/rex.ts
|
|
793
|
+
const rexBackend = defineBackend({
|
|
794
|
+
name: "rex",
|
|
795
|
+
detect: (env) => isRexEnvironment(env),
|
|
796
|
+
getCurrentPaneId: () => getCurrentPaneId$2(),
|
|
797
|
+
paneExists: (paneId) => checkPaneExists$2(paneId),
|
|
798
|
+
focusPane: (paneId) => focusPane$2(paneId),
|
|
799
|
+
sendText: (paneId, chunk) => sendTextChunk$2(paneId, chunk),
|
|
800
|
+
sendKey: (paneId, key) => sendKey$2(paneId, key),
|
|
801
|
+
logContentSent: (paneId, chunkCount) => logContentSent$2(paneId, chunkCount),
|
|
802
|
+
maxChunkBytes: REX_SEND_CHUNK_BYTES,
|
|
803
|
+
defaultSubmitKey: "enter",
|
|
804
|
+
quoteSeparator: "\n\n",
|
|
805
|
+
collectInput: "argument",
|
|
806
|
+
quietResumeErrors: "all",
|
|
807
|
+
resumeAcceptsEditorPane: true,
|
|
808
|
+
storageNamespace: () => "rex",
|
|
809
|
+
state: confStore(() => "rex")
|
|
810
|
+
});
|
|
811
|
+
|
|
493
812
|
//#endregion
|
|
494
813
|
//#region src/modules/tmux.ts
|
|
495
814
|
const execAsync$1 = promisify(exec);
|
|
@@ -500,20 +819,6 @@ async function getCurrentPaneId$1() {
|
|
|
500
819
|
const { stdout } = await execAsync$1("tmux display-message -p \"#{pane_id}\"");
|
|
501
820
|
return stdout.trim();
|
|
502
821
|
}
|
|
503
|
-
async function saveEditorPaneId$1(targetPaneId, editorPaneId) {
|
|
504
|
-
await execAsync$1(`tmux set-option -pt '${targetPaneId}' @editprompt_editor_pane '${editorPaneId}'`);
|
|
505
|
-
}
|
|
506
|
-
async function clearEditorPaneId$1(targetPaneId) {
|
|
507
|
-
await execAsync$1(`tmux set-option -pt '${targetPaneId}' @editprompt_editor_pane ""`);
|
|
508
|
-
}
|
|
509
|
-
async function getEditorPaneId$1(targetPaneId) {
|
|
510
|
-
try {
|
|
511
|
-
const { stdout } = await execAsync$1(`tmux show -pt '${targetPaneId}' -v @editprompt_editor_pane`);
|
|
512
|
-
return stdout.trim();
|
|
513
|
-
} catch {
|
|
514
|
-
return "";
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
822
|
async function checkPaneExists$1(paneId) {
|
|
518
823
|
try {
|
|
519
824
|
const { stdout } = await execAsync$1("tmux list-panes -a -F \"#{pane_id}\"");
|
|
@@ -525,62 +830,55 @@ async function checkPaneExists$1(paneId) {
|
|
|
525
830
|
async function focusPane$1(paneId) {
|
|
526
831
|
await execAsync$1(`tmux select-pane -t '${paneId}'`);
|
|
527
832
|
}
|
|
528
|
-
async function
|
|
529
|
-
await execAsync$1(`tmux set-option -pt '${editorPaneId}' @editprompt_is_editor 1`);
|
|
530
|
-
const uniqueTargetPaneIds = [...new Set(targetPaneIds)];
|
|
531
|
-
await execAsync$1(`tmux set-option -pt '${editorPaneId}' @editprompt_target_panes '${uniqueTargetPaneIds.join(",")}'`);
|
|
532
|
-
for (const targetPaneId of uniqueTargetPaneIds) await saveEditorPaneId$1(targetPaneId, editorPaneId);
|
|
533
|
-
}
|
|
534
|
-
async function getTargetPaneIds$1(editorPaneId) {
|
|
833
|
+
async function getPaneOption(paneId, name) {
|
|
535
834
|
try {
|
|
536
|
-
const { stdout } = await execAsync$1(`tmux show -pt '${
|
|
537
|
-
const value = stdout.trim();
|
|
538
|
-
if (value === "") return [];
|
|
539
|
-
return value.split(",").map((id) => id.trim());
|
|
540
|
-
} catch {
|
|
541
|
-
return [];
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
async function isEditorPane(paneId) {
|
|
545
|
-
try {
|
|
546
|
-
const { stdout } = await execAsync$1(`tmux show -pt '${paneId}' -v @editprompt_is_editor`);
|
|
547
|
-
return stdout.trim() === "1";
|
|
548
|
-
} catch {
|
|
549
|
-
return false;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
async function getQuoteVariableContent(paneId) {
|
|
553
|
-
try {
|
|
554
|
-
const { stdout } = await execAsync$1(`tmux show -pt '${paneId}' -v @editprompt_quote`);
|
|
835
|
+
const { stdout } = await execAsync$1(`tmux show -pt '${paneId}' -v ${name}`);
|
|
555
836
|
return stdout;
|
|
556
837
|
} catch {
|
|
557
838
|
return "";
|
|
558
839
|
}
|
|
559
840
|
}
|
|
560
|
-
async function
|
|
561
|
-
|
|
562
|
-
const existingContent = await getQuoteVariableContent(paneId);
|
|
563
|
-
if (existingContent.trim() !== "") newContent = `${existingContent}\n${content}`;
|
|
564
|
-
else newContent = content;
|
|
565
|
-
await execAsync$1(`tmux set-option -pt '${paneId}' @editprompt_quote '${newContent.replace(/'/g, "'\\''")}' `);
|
|
566
|
-
}
|
|
567
|
-
async function clearQuoteVariable(targetPaneId) {
|
|
568
|
-
await execAsync$1(`tmux set-option -pt '${targetPaneId}' @editprompt_quote ""`);
|
|
841
|
+
async function setPaneOption(paneId, name, value) {
|
|
842
|
+
await execAsync$1(`tmux set-option -pt '${paneId}' ${name} '${value.replace(/'/g, "'\\''")}'`);
|
|
569
843
|
}
|
|
570
|
-
async function
|
|
571
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
844
|
+
async function sendKey$1(paneId, key) {
|
|
572
845
|
await execAsync$1(`tmux send-keys -t '${paneId}' '${key}'`);
|
|
573
846
|
}
|
|
574
|
-
async function
|
|
847
|
+
async function exitCopyMode(paneId) {
|
|
575
848
|
await execAsync$1(`tmux if-shell -t '${paneId}' '[ "#{pane_in_mode}" = "1" ]' "copy-mode -q -t '${paneId}'"`);
|
|
576
|
-
|
|
577
|
-
|
|
849
|
+
}
|
|
850
|
+
async function sendTextChunk$1(paneId, chunk) {
|
|
851
|
+
await execAsync$1(`tmux send-keys -t '${paneId}' -- '${chunk.replace(/'/g, "'\\''")}'`);
|
|
852
|
+
}
|
|
853
|
+
function logContentSent$1(paneId, chunkCount) {
|
|
578
854
|
logger$12.debug("Content sent to tmux pane: {paneId} ({chunks} chunk(s))", {
|
|
579
855
|
paneId,
|
|
580
|
-
chunks:
|
|
856
|
+
chunks: chunkCount
|
|
581
857
|
});
|
|
582
858
|
}
|
|
583
859
|
|
|
860
|
+
//#endregion
|
|
861
|
+
//#region src/backends/tmux.ts
|
|
862
|
+
const tmuxBackend = defineBackend({
|
|
863
|
+
name: "tmux",
|
|
864
|
+
getCurrentPaneId: () => getCurrentPaneId$1(),
|
|
865
|
+
paneExists: (paneId) => checkPaneExists$1(paneId),
|
|
866
|
+
focusPane: (paneId) => focusPane$1(paneId),
|
|
867
|
+
beforeSendText: (paneId) => exitCopyMode(paneId),
|
|
868
|
+
sendText: (paneId, chunk) => sendTextChunk$1(paneId, chunk),
|
|
869
|
+
sendKey: (paneId, key) => sendKey$1(paneId, key),
|
|
870
|
+
logContentSent: (paneId, chunkCount) => logContentSent$1(paneId, chunkCount),
|
|
871
|
+
maxChunkBytes: TMUX_SEND_CHUNK_BYTES,
|
|
872
|
+
defaultSubmitKey: "C-m",
|
|
873
|
+
quoteSeparator: "\n",
|
|
874
|
+
collectInput: "stdin",
|
|
875
|
+
storageNamespace: () => "tmux",
|
|
876
|
+
state: paneVariableStore({
|
|
877
|
+
get: (paneId, name) => getPaneOption(paneId, name),
|
|
878
|
+
set: (paneId, name, value) => setPaneOption(paneId, name, value)
|
|
879
|
+
}, "@editprompt_")
|
|
880
|
+
});
|
|
881
|
+
|
|
584
882
|
//#endregion
|
|
585
883
|
//#region src/modules/wezterm.ts
|
|
586
884
|
const logger$11 = getLogger(["editprompt", "wezterm"]);
|
|
@@ -605,114 +903,270 @@ async function checkPaneExists(paneId) {
|
|
|
605
903
|
return false;
|
|
606
904
|
}
|
|
607
905
|
}
|
|
608
|
-
async function
|
|
609
|
-
|
|
610
|
-
try {
|
|
611
|
-
conf.set(`wezterm.targetPane.pane_${targetPaneId}`, { editorPaneId });
|
|
612
|
-
} catch (error) {
|
|
613
|
-
logger$11.debug("saveEditorPaneId failed: {error}", { error });
|
|
614
|
-
}
|
|
906
|
+
async function focusPane(paneId) {
|
|
907
|
+
await execAsync(`wezterm cli activate-pane --pane-id '${paneId}'`);
|
|
615
908
|
}
|
|
616
|
-
async function
|
|
617
|
-
|
|
618
|
-
const data = conf.get(`wezterm.targetPane.pane_${targetPaneId}`);
|
|
619
|
-
if (typeof data === "object" && data !== null && "editorPaneId" in data) return String(data.editorPaneId);
|
|
620
|
-
return "";
|
|
621
|
-
} catch (error) {
|
|
622
|
-
logger$11.debug("getEditorPaneId failed: {error}", { error });
|
|
623
|
-
return "";
|
|
624
|
-
}
|
|
909
|
+
async function sendKey(paneId, key) {
|
|
910
|
+
await execAsync(`wezterm cli send-text --no-paste --pane-id '${paneId}' $'${key}'`);
|
|
625
911
|
}
|
|
626
|
-
async function
|
|
627
|
-
|
|
628
|
-
const editorPaneId = await getEditorPaneId(targetPaneId);
|
|
629
|
-
conf.delete(`wezterm.targetPane.pane_${targetPaneId}`);
|
|
630
|
-
if (editorPaneId) conf.delete(`wezterm.editorPane.pane_${editorPaneId}`);
|
|
631
|
-
} catch (error) {
|
|
632
|
-
logger$11.debug("clearEditorPaneId failed: {error}", { error });
|
|
633
|
-
}
|
|
912
|
+
async function sendTextChunk(paneId, chunk) {
|
|
913
|
+
await execAsync(`wezterm cli send-text --no-paste --pane-id '${paneId}' -- '${chunk.replace(/'/g, "'\\''")}'`);
|
|
634
914
|
}
|
|
635
|
-
|
|
636
|
-
|
|
915
|
+
function logContentSent(paneId, chunkCount) {
|
|
916
|
+
logger$11.debug("Content sent to wezterm pane: {paneId} ({chunks} chunk(s))", {
|
|
917
|
+
paneId,
|
|
918
|
+
chunks: chunkCount
|
|
919
|
+
});
|
|
637
920
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
921
|
+
|
|
922
|
+
//#endregion
|
|
923
|
+
//#region src/backends/wezterm.ts
|
|
924
|
+
const weztermBackend = defineBackend({
|
|
925
|
+
name: "wezterm",
|
|
926
|
+
getCurrentPaneId: () => getCurrentPaneId(),
|
|
927
|
+
paneExists: (paneId) => checkPaneExists(paneId),
|
|
928
|
+
focusPane: (paneId) => focusPane(paneId),
|
|
929
|
+
sendText: (paneId, chunk) => sendTextChunk(paneId, chunk),
|
|
930
|
+
sendKey: (paneId, key) => sendKey(paneId, key),
|
|
931
|
+
logContentSent: (paneId, chunkCount) => logContentSent(paneId, chunkCount),
|
|
932
|
+
maxChunkBytes: WEZTERM_SEND_CHUNK_BYTES,
|
|
933
|
+
defaultSubmitKey: "\\r",
|
|
934
|
+
quoteSeparator: "\n\n",
|
|
935
|
+
collectInput: "argument",
|
|
936
|
+
quietResumeErrors: "editorFocus",
|
|
937
|
+
traceResume: true,
|
|
938
|
+
storageNamespace: () => "wezterm",
|
|
939
|
+
state: confStore(() => "wezterm", {
|
|
940
|
+
wholeTargetRecord: true,
|
|
941
|
+
logCategory: ["editprompt", "wezterm"]
|
|
942
|
+
})
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
//#endregion
|
|
946
|
+
//#region src/backends/registry.ts
|
|
947
|
+
/** Used when nothing is configured and no backend detects its environment. */
|
|
948
|
+
const FALLBACK_BACKEND_NAME = "tmux";
|
|
949
|
+
const BUILTIN_BACKENDS = [
|
|
950
|
+
tmuxBackend,
|
|
951
|
+
weztermBackend,
|
|
952
|
+
herdrBackend,
|
|
953
|
+
niwatermBackend,
|
|
954
|
+
rexBackend
|
|
955
|
+
];
|
|
956
|
+
const backends = new Map(BUILTIN_BACKENDS.map((b) => [b.name, b]));
|
|
957
|
+
function isBuiltinBackendName(name) {
|
|
958
|
+
return BUILTIN_BACKENDS.some((backend) => backend.name === name);
|
|
959
|
+
}
|
|
960
|
+
function registerBackend(backend) {
|
|
961
|
+
if (backends.has(backend.name)) throw new Error(`A backend named '${backend.name}' is already registered`);
|
|
962
|
+
backends.set(backend.name, backend);
|
|
963
|
+
}
|
|
964
|
+
function hasBackend(name) {
|
|
965
|
+
return typeof name === "string" && backends.has(name);
|
|
966
|
+
}
|
|
967
|
+
function listBackendNames() {
|
|
968
|
+
return [...backends.keys()];
|
|
969
|
+
}
|
|
970
|
+
function getBackend(name) {
|
|
971
|
+
const backend = backends.get(name);
|
|
972
|
+
if (!backend) throw new Error(`Invalid multiplexer type '${name}'. Supported values: ${listBackendNames().join(", ")}`);
|
|
973
|
+
return backend;
|
|
974
|
+
}
|
|
975
|
+
function detectBackendName(env = process.env) {
|
|
976
|
+
for (const backend of backends.values()) if (backend.detect?.(env)) return backend.name;
|
|
977
|
+
return FALLBACK_BACKEND_NAME;
|
|
978
|
+
}
|
|
979
|
+
/**
|
|
980
|
+
* Resolves the backend name from, in order: the explicit `--mux` value,
|
|
981
|
+
* `EDITPROMPT_MUX`, and auto-detection. Throws for an unknown name.
|
|
982
|
+
*/
|
|
983
|
+
function resolveBackendName(explicit, env = process.env) {
|
|
984
|
+
const value = explicit || env.EDITPROMPT_MUX || detectBackendName(env);
|
|
985
|
+
if (!hasBackend(value)) {
|
|
986
|
+
const displayValue = typeof value === "string" ? value : `<${typeof value}>`;
|
|
987
|
+
throw new Error(`Invalid multiplexer type '${displayValue}'. Supported values: ${listBackendNames().join(", ")}`);
|
|
645
988
|
}
|
|
989
|
+
return value;
|
|
646
990
|
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
991
|
+
|
|
992
|
+
//#endregion
|
|
993
|
+
//#region src/config/userConfig.ts
|
|
994
|
+
const RC_FILE = ".editpromptrc";
|
|
995
|
+
/**
|
|
996
|
+
* The directory holding the configuration file: `EDITPROMPT_CONFIG_DIR`, or
|
|
997
|
+
* `editprompt` under `XDG_CONFIG_HOME` (default `~/.config`).
|
|
998
|
+
*/
|
|
999
|
+
function getConfigDir(env = process.env) {
|
|
1000
|
+
const explicit = env.EDITPROMPT_CONFIG_DIR?.trim();
|
|
1001
|
+
if (explicit) return explicit;
|
|
1002
|
+
return join(env.XDG_CONFIG_HOME?.trim() || join(homedir(), ".config"), "editprompt");
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Loads the configuration with c12, so every format c12 supports works:
|
|
1006
|
+
* `config.{json,jsonc,json5,yaml,yml,toml,js,ts,mjs,cjs,mts,cts}` in the
|
|
1007
|
+
* config directory, and the `.editpromptrc` RC file there, in
|
|
1008
|
+
* `$XDG_CONFIG_HOME` or in the home directory.
|
|
1009
|
+
*
|
|
1010
|
+
* Configuration is read from the user's config directory only, never from the
|
|
1011
|
+
* current working directory: a repository must not be able to change how
|
|
1012
|
+
* editprompt talks to the terminal, or run code through a config file.
|
|
1013
|
+
*/
|
|
1014
|
+
async function loadUserConfig(configDir = getConfigDir()) {
|
|
1015
|
+
const { loadConfig } = await import("c12");
|
|
1016
|
+
const { config } = await loadConfig({
|
|
1017
|
+
name: "editprompt",
|
|
1018
|
+
cwd: configDir,
|
|
1019
|
+
configFile: "config",
|
|
1020
|
+
rcFile: RC_FILE,
|
|
1021
|
+
globalRc: true,
|
|
1022
|
+
dotenv: false,
|
|
1023
|
+
packageJson: false
|
|
1024
|
+
});
|
|
1025
|
+
return config ?? {};
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Registers the backends declared in the configuration. Returns one message
|
|
1029
|
+
* per backend that could not be registered, so that a broken entry does not
|
|
1030
|
+
* take the built-in backends down with it.
|
|
1031
|
+
*/
|
|
1032
|
+
function registerConfiguredBackends(config) {
|
|
1033
|
+
const errors = [];
|
|
1034
|
+
const { backends } = config;
|
|
1035
|
+
if (backends === void 0) return errors;
|
|
1036
|
+
if (typeof backends !== "object" || backends === null || Array.isArray(backends)) return ["backends must be an object keyed by backend name"];
|
|
1037
|
+
for (const [name, spec] of Object.entries(backends)) {
|
|
1038
|
+
if (isBuiltinBackendName(name)) {
|
|
1039
|
+
errors.push(`backends.${name}: '${name}' is a built-in backend and cannot be redefined`);
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
try {
|
|
1043
|
+
registerBackend(createCommandBackend(name, spec));
|
|
1044
|
+
} catch (error) {
|
|
1045
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
653
1046
|
}
|
|
654
|
-
return [];
|
|
655
|
-
} catch (error) {
|
|
656
|
-
logger$11.debug("getTargetPaneIds failed: {error}", { error });
|
|
657
|
-
return [];
|
|
658
1047
|
}
|
|
1048
|
+
return errors;
|
|
659
1049
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
1050
|
+
/**
|
|
1051
|
+
* Returns the `--mux` value given on the command line, ignoring anything after
|
|
1052
|
+
* `--` (content passed to input, collect or stash).
|
|
1053
|
+
*/
|
|
1054
|
+
function findMuxArgument(argv) {
|
|
1055
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1056
|
+
const arg = argv[i];
|
|
1057
|
+
if (arg === "--") return;
|
|
1058
|
+
if (arg === "--mux" || arg === "-m") return argv[i + 1];
|
|
1059
|
+
if (arg.startsWith("--mux=")) return arg.slice(6);
|
|
666
1060
|
}
|
|
667
1061
|
}
|
|
668
|
-
|
|
1062
|
+
/**
|
|
1063
|
+
* Decides whether this invocation can use a configured backend at all, so that
|
|
1064
|
+
* the common case skips loading c12. Configured backends cannot replace
|
|
1065
|
+
* built-in ones and are detected after them, so they only matter when an
|
|
1066
|
+
* explicit `--mux`/EDITPROMPT_MUX names a non-built-in backend, or when
|
|
1067
|
+
* auto-detection would otherwise fall back to tmux.
|
|
1068
|
+
*/
|
|
1069
|
+
function mayUseConfiguredBackend(argv, env = process.env) {
|
|
1070
|
+
const explicit = findMuxArgument(argv) || env.EDITPROMPT_MUX;
|
|
1071
|
+
if (explicit) return !isBuiltinBackendName(explicit);
|
|
1072
|
+
return detectBackendName(env) === FALLBACK_BACKEND_NAME;
|
|
1073
|
+
}
|
|
1074
|
+
/** Returns true when one of the files c12 would read exists. */
|
|
1075
|
+
function hasConfigFiles(configDir = getConfigDir(), env = process.env) {
|
|
1076
|
+
return existsSync(configDir) || existsSync(join(env.XDG_CONFIG_HOME?.trim() || homedir(), RC_FILE));
|
|
1077
|
+
}
|
|
1078
|
+
/** Loads the configuration and registers its backends, reporting problems on stderr. */
|
|
1079
|
+
async function setupUserConfig(configDir = getConfigDir()) {
|
|
1080
|
+
if (!hasConfigFiles(configDir)) return;
|
|
1081
|
+
let config;
|
|
669
1082
|
try {
|
|
670
|
-
|
|
671
|
-
let newData;
|
|
672
|
-
if (typeof data === "object" && data !== null) {
|
|
673
|
-
const existingQuoteText = "quote_text" in data ? String(data.quote_text) : "";
|
|
674
|
-
const newQuoteText = existingQuoteText.trim() !== "" ? `${existingQuoteText}\n\n${content}` : content;
|
|
675
|
-
newData = {
|
|
676
|
-
...data,
|
|
677
|
-
quote_text: newQuoteText
|
|
678
|
-
};
|
|
679
|
-
} else newData = { quote_text: content };
|
|
680
|
-
conf.set(`wezterm.targetPane.pane_${paneId}`, newData);
|
|
1083
|
+
config = await loadUserConfig(configDir);
|
|
681
1084
|
} catch (error) {
|
|
682
|
-
|
|
1085
|
+
console.error(`editprompt: failed to load the configuration: ${error instanceof Error ? error.message : String(error)}`);
|
|
1086
|
+
return;
|
|
683
1087
|
}
|
|
1088
|
+
for (const message of registerConfiguredBackends(config)) console.error(`editprompt: invalid configuration: ${message}`);
|
|
684
1089
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
return "";
|
|
693
|
-
}
|
|
1090
|
+
|
|
1091
|
+
//#endregion
|
|
1092
|
+
//#region src/modules/logger.ts
|
|
1093
|
+
function resolveLogFilePath(options) {
|
|
1094
|
+
if (options.logFile) return options.logFile;
|
|
1095
|
+
const envPath = process.env.EDITPROMPT_LOG_FILE;
|
|
1096
|
+
if (envPath) return envPath;
|
|
694
1097
|
}
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
logger$11.debug("clearQuoteText failed: {error}", { error });
|
|
701
|
-
}
|
|
1098
|
+
function resolveFileLogLevel(options) {
|
|
1099
|
+
if (options.verbose) return "debug";
|
|
1100
|
+
const envLevel = process.env.EDITPROMPT_LOG_LEVEL;
|
|
1101
|
+
if (envLevel) return parseLogLevel(envLevel);
|
|
1102
|
+
return "info";
|
|
702
1103
|
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
1104
|
+
function resolveLogLevel(options) {
|
|
1105
|
+
if (options.quiet) return null;
|
|
1106
|
+
if (options.verbose) return "debug";
|
|
1107
|
+
const envLevel = process.env.EDITPROMPT_LOG_LEVEL;
|
|
1108
|
+
if (envLevel) return parseLogLevel(envLevel);
|
|
1109
|
+
return "info";
|
|
706
1110
|
}
|
|
707
|
-
|
|
708
|
-
const
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
1111
|
+
function setupLogger(options = {}) {
|
|
1112
|
+
const stderrLevel = resolveLogLevel(options);
|
|
1113
|
+
const stderrFormatter = getAnsiColorFormatter({
|
|
1114
|
+
timestamp: "time-timezone",
|
|
1115
|
+
level: "ABBR"
|
|
1116
|
+
});
|
|
1117
|
+
const rawStderrSink = (record) => {
|
|
1118
|
+
process.stderr.write(stderrFormatter(record));
|
|
1119
|
+
};
|
|
1120
|
+
const sinks = { stderr: stderrLevel === null ? () => {} : withFilter(rawStderrSink, stderrLevel) };
|
|
1121
|
+
const loggerSinks = ["stderr"];
|
|
1122
|
+
const logFilePath = resolveLogFilePath(options);
|
|
1123
|
+
const fileLogLevel = resolveFileLogLevel(options);
|
|
1124
|
+
if (logFilePath) try {
|
|
1125
|
+
appendFileSync(logFilePath, "");
|
|
1126
|
+
const fileFormatter = getTextFormatter({
|
|
1127
|
+
timestamp: "date-time-timezone",
|
|
1128
|
+
level: "ABBR"
|
|
1129
|
+
});
|
|
1130
|
+
const rawFileSink = (record) => {
|
|
1131
|
+
appendFileSync(logFilePath, fileFormatter(record));
|
|
1132
|
+
};
|
|
1133
|
+
sinks.file = withFilter(rawFileSink, fileLogLevel);
|
|
1134
|
+
loggerSinks.push("file");
|
|
1135
|
+
} catch {
|
|
1136
|
+
process.stderr.write(`Warning: Cannot write to log file '${logFilePath}', continuing without file logging.\n`);
|
|
1137
|
+
}
|
|
1138
|
+
configureSync({
|
|
1139
|
+
sinks,
|
|
1140
|
+
loggers: [{
|
|
1141
|
+
category: ["editprompt"],
|
|
1142
|
+
sinks: loggerSinks,
|
|
1143
|
+
lowestLevel: "trace"
|
|
1144
|
+
}, {
|
|
1145
|
+
category: ["logtape", "meta"],
|
|
1146
|
+
lowestLevel: null
|
|
1147
|
+
}]
|
|
713
1148
|
});
|
|
714
1149
|
}
|
|
715
1150
|
|
|
1151
|
+
//#endregion
|
|
1152
|
+
//#region package.json
|
|
1153
|
+
var version = "2.0.0";
|
|
1154
|
+
|
|
1155
|
+
//#endregion
|
|
1156
|
+
//#region src/core/quote.ts
|
|
1157
|
+
/** Appends a quote to the target pane's quote buffer. */
|
|
1158
|
+
async function appendQuote(backend, targetPaneId, content) {
|
|
1159
|
+
const existing = await backend.state.getQuote(targetPaneId);
|
|
1160
|
+
const quote = existing.trim() !== "" ? `${existing}${backend.quoteSeparator}${content}` : content;
|
|
1161
|
+
await backend.state.setQuote(targetPaneId, quote);
|
|
1162
|
+
}
|
|
1163
|
+
/** Returns the target pane's quote buffer and clears it. */
|
|
1164
|
+
async function takeQuote(backend, targetPaneId) {
|
|
1165
|
+
const quote = await backend.state.getQuote(targetPaneId);
|
|
1166
|
+
await backend.state.removeQuote(targetPaneId);
|
|
1167
|
+
return quote;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
716
1170
|
//#endregion
|
|
717
1171
|
//#region src/utils/argumentParser.ts
|
|
718
1172
|
/**
|
|
@@ -852,31 +1306,8 @@ function processQuoteText(text, options) {
|
|
|
852
1306
|
|
|
853
1307
|
//#endregion
|
|
854
1308
|
//#region src/utils/mux.ts
|
|
855
|
-
const SUPPORTED_MUXES = [
|
|
856
|
-
"tmux",
|
|
857
|
-
"wezterm",
|
|
858
|
-
"herdr",
|
|
859
|
-
"niwaterm"
|
|
860
|
-
];
|
|
861
|
-
function isMuxType(value) {
|
|
862
|
-
return value === "tmux" || value === "wezterm" || value === "herdr" || value === "niwaterm";
|
|
863
|
-
}
|
|
864
|
-
function hasValue(value) {
|
|
865
|
-
return typeof value === "string" && value.trim() !== "";
|
|
866
|
-
}
|
|
867
|
-
function isHerdrEnvironment(env = process.env) {
|
|
868
|
-
return hasValue(env.HERDR_SOCKET_PATH) && (hasValue(env.HERDR_PANE_ID) || hasValue(env.HERDR_ACTIVE_PANE_ID));
|
|
869
|
-
}
|
|
870
|
-
function isNiwatermEnvironment(env = process.env) {
|
|
871
|
-
return hasValue(env.NIWATERM_TAB_ID);
|
|
872
|
-
}
|
|
873
1309
|
function resolveMux(explicitMux, env = process.env) {
|
|
874
|
-
|
|
875
|
-
if (!isMuxType(muxValue)) {
|
|
876
|
-
const displayValue = typeof muxValue === "string" ? muxValue : `<${typeof muxValue}>`;
|
|
877
|
-
throw new Error(`Invalid multiplexer type '${displayValue}'. Supported values: ${SUPPORTED_MUXES.join(", ")}`);
|
|
878
|
-
}
|
|
879
|
-
return muxValue;
|
|
1310
|
+
return resolveBackendName(explicitMux, env);
|
|
880
1311
|
}
|
|
881
1312
|
|
|
882
1313
|
//#endregion
|
|
@@ -884,7 +1315,7 @@ function resolveMux(explicitMux, env = process.env) {
|
|
|
884
1315
|
const logger$10 = getLogger(["editprompt"]);
|
|
885
1316
|
const ARG_MUX = {
|
|
886
1317
|
short: "m",
|
|
887
|
-
description: "Multiplexer type (tmux, wezterm, herdr, or
|
|
1318
|
+
description: "Multiplexer type (tmux, wezterm, herdr, niwaterm, or rex; auto-detects Herdr, niwaterm and Rex, otherwise tmux)",
|
|
888
1319
|
type: "string"
|
|
889
1320
|
};
|
|
890
1321
|
const ARG_TARGET_PANE_SINGLE = {
|
|
@@ -994,10 +1425,7 @@ async function runCollectMode(mux, targetPaneId, rawContent, outputs = ["buffer"
|
|
|
994
1425
|
if (rawContent !== void 0) selection = rawContent;
|
|
995
1426
|
else selection = await readStdin();
|
|
996
1427
|
const processedText = processQuoteText(selection, { withQuote });
|
|
997
|
-
for (const output of outputs) if (output === "buffer")
|
|
998
|
-
else if (mux === "wezterm") await appendToQuoteText(targetPaneId, processedText);
|
|
999
|
-
else if (mux === "niwaterm") await appendToQuoteVariable$1(targetPaneId, processedText);
|
|
1000
|
-
else await appendToQuoteText$1(targetPaneId, processedText);
|
|
1428
|
+
for (const output of outputs) if (output === "buffer") await appendQuote(getBackend(mux), targetPaneId, processedText);
|
|
1001
1429
|
else if (output === "stdout") process.stdout.write(processedText);
|
|
1002
1430
|
} catch (error) {
|
|
1003
1431
|
logger$9.error(`${error instanceof Error ? error.message : "Unknown error"}`);
|
|
@@ -1027,7 +1455,7 @@ const collectCommand = define({
|
|
|
1027
1455
|
const outputs = normalizeCollectOutputs(ctx.values.output);
|
|
1028
1456
|
const withQuote = !ctx.values["no-quote"];
|
|
1029
1457
|
let rawContent;
|
|
1030
|
-
if (mux === "
|
|
1458
|
+
if (getBackend(mux).collectInput === "argument") {
|
|
1031
1459
|
rawContent = extractRawContent(ctx.rest, ctx.positionals);
|
|
1032
1460
|
if (rawContent === void 0) {
|
|
1033
1461
|
logger$9.error(`Text content is required for collect mode with ${mux}. Use: editprompt collect --mux ${mux} --target-pane <id> -- "<text>"`);
|
|
@@ -1053,111 +1481,124 @@ function readSendConfig() {
|
|
|
1053
1481
|
}
|
|
1054
1482
|
|
|
1055
1483
|
//#endregion
|
|
1056
|
-
//#region src/
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
isEditor = await isEditorPane(currentPaneId);
|
|
1066
|
-
} else if (config.mux === "wezterm") {
|
|
1067
|
-
currentPaneId = await getCurrentPaneId();
|
|
1068
|
-
isEditor = isEditorPaneFromConf(currentPaneId);
|
|
1069
|
-
} else if (config.mux === "niwaterm") {
|
|
1070
|
-
currentPaneId = await getCurrentPaneId$2();
|
|
1071
|
-
isEditor = await isEditorPane$1(currentPaneId);
|
|
1072
|
-
} else {
|
|
1073
|
-
currentPaneId = await getCurrentPaneId$3();
|
|
1074
|
-
isEditor = isEditorPaneFromConf$1(currentPaneId);
|
|
1075
|
-
}
|
|
1076
|
-
if (!isEditor) {
|
|
1077
|
-
logger$8.error("Current pane is not an editor pane");
|
|
1078
|
-
process.exit(1);
|
|
1079
|
-
}
|
|
1080
|
-
let targetPanes;
|
|
1081
|
-
if (config.mux === "tmux") targetPanes = await getTargetPaneIds$1(currentPaneId);
|
|
1082
|
-
else if (config.mux === "wezterm") targetPanes = await getTargetPaneIds(currentPaneId);
|
|
1083
|
-
else if (config.mux === "niwaterm") targetPanes = await getTargetPaneIds$2(currentPaneId);
|
|
1084
|
-
else targetPanes = await getTargetPaneIds$3(currentPaneId);
|
|
1085
|
-
if (targetPanes.length === 0) {
|
|
1086
|
-
logger$8.error("No target panes registered for this editor pane");
|
|
1087
|
-
process.exit(1);
|
|
1088
|
-
}
|
|
1089
|
-
const quoteContents = [];
|
|
1090
|
-
for (const targetPane of targetPanes) {
|
|
1091
|
-
let content;
|
|
1092
|
-
if (config.mux === "tmux") {
|
|
1093
|
-
content = await getQuoteVariableContent(targetPane);
|
|
1094
|
-
await clearQuoteVariable(targetPane);
|
|
1095
|
-
} else if (config.mux === "wezterm") {
|
|
1096
|
-
content = await getQuoteText(targetPane);
|
|
1097
|
-
await clearQuoteText(targetPane);
|
|
1098
|
-
} else if (config.mux === "niwaterm") {
|
|
1099
|
-
content = await getQuoteVariableContent$1(targetPane);
|
|
1100
|
-
await clearQuoteVariable$1(targetPane);
|
|
1101
|
-
} else {
|
|
1102
|
-
content = await getQuoteText$1(targetPane);
|
|
1103
|
-
await clearQuoteText$1(targetPane);
|
|
1104
|
-
}
|
|
1105
|
-
if (content.trim() !== "") quoteContents.push(content);
|
|
1106
|
-
}
|
|
1107
|
-
const combinedContent = quoteContents.join("\n");
|
|
1108
|
-
process.stdout.write(combinedContent.replace(/\n{3,}$/, "\n\n"));
|
|
1109
|
-
process.exit(0);
|
|
1110
|
-
} catch (error) {
|
|
1111
|
-
logger$8.error(`${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1112
|
-
process.exit(1);
|
|
1113
|
-
}
|
|
1484
|
+
//#region src/core/link.ts
|
|
1485
|
+
/**
|
|
1486
|
+
* Marks a pane as an editor pane serving the target panes, and records the
|
|
1487
|
+
* editor pane on each target pane so that resume can find it.
|
|
1488
|
+
*/
|
|
1489
|
+
async function markAsEditorPane(backend, editorPaneId, targetPaneIds) {
|
|
1490
|
+
const uniqueTargetPaneIds = [...new Set(targetPaneIds)];
|
|
1491
|
+
await backend.state.setTargetPaneIds(editorPaneId, uniqueTargetPaneIds);
|
|
1492
|
+
for (const targetPaneId of uniqueTargetPaneIds) await backend.state.setEditorPaneId(targetPaneId, editorPaneId);
|
|
1114
1493
|
}
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1494
|
+
function isEditorPane(backend, paneId) {
|
|
1495
|
+
return backend.state.isEditorPane(paneId);
|
|
1496
|
+
}
|
|
1497
|
+
function getTargetPaneIds(backend, editorPaneId) {
|
|
1498
|
+
return backend.state.getTargetPaneIds(editorPaneId);
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1501
|
+
* Clears the editor pane registered on a target pane.
|
|
1502
|
+
*
|
|
1503
|
+
* When `expectedEditorPaneId` is given and the target pane has meanwhile been
|
|
1504
|
+
* linked to a newer editor pane, the newer link is kept and only the expected
|
|
1505
|
+
* editor pane is forgotten. This lets an editor that exits late leave a newer
|
|
1506
|
+
* editor's registration alone.
|
|
1507
|
+
*/
|
|
1508
|
+
async function clearEditorLink(backend, targetPaneId, expectedEditorPaneId) {
|
|
1509
|
+
if (!expectedEditorPaneId && backend.state.keepsEditorPaneMarks) {
|
|
1510
|
+
await backend.state.removeEditorPaneId(targetPaneId);
|
|
1511
|
+
return;
|
|
1130
1512
|
}
|
|
1131
|
-
|
|
1513
|
+
const editorPaneId = await backend.state.getEditorPaneId(targetPaneId);
|
|
1514
|
+
if (expectedEditorPaneId && editorPaneId !== expectedEditorPaneId) {
|
|
1515
|
+
await backend.state.removeEditorPane(expectedEditorPaneId);
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
await backend.state.removeEditorPaneId(targetPaneId);
|
|
1519
|
+
if (editorPaneId) await backend.state.removeEditorPane(editorPaneId);
|
|
1520
|
+
}
|
|
1521
|
+
var EditorContextError = class extends Error {
|
|
1522
|
+
name = "EditorContextError";
|
|
1523
|
+
};
|
|
1524
|
+
/**
|
|
1525
|
+
* Resolves the editor pane editprompt runs in and the target panes registered
|
|
1526
|
+
* on it. Throws EditorContextError when the current pane is not an editor pane
|
|
1527
|
+
* or has no target panes.
|
|
1528
|
+
*/
|
|
1529
|
+
async function resolveEditorContext(backend) {
|
|
1530
|
+
const editorPaneId = await backend.getCurrentPaneId();
|
|
1531
|
+
if (!await backend.state.isEditorPane(editorPaneId)) throw new EditorContextError("Current pane is not an editor pane");
|
|
1532
|
+
const targetPaneIds = await backend.state.getTargetPaneIds(editorPaneId);
|
|
1533
|
+
if (targetPaneIds.length === 0) throw new EditorContextError("No target panes registered for this editor pane");
|
|
1534
|
+
return {
|
|
1535
|
+
editorPaneId,
|
|
1536
|
+
targetPaneIds
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1132
1539
|
|
|
1133
1540
|
//#endregion
|
|
1134
|
-
//#region src/utils/
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1541
|
+
//#region src/utils/contentChunker.ts
|
|
1542
|
+
/**
|
|
1543
|
+
* Split content into chunks not exceeding maxBytes (UTF-8 byte count),
|
|
1544
|
+
* never breaking a Unicode code point across chunks.
|
|
1545
|
+
*
|
|
1546
|
+
* Content shorter than maxBytes is returned as a single chunk, so short
|
|
1547
|
+
* input keeps the previous behavior of being sent in one shot.
|
|
1548
|
+
*/
|
|
1549
|
+
function splitByByteSize(content, maxBytes) {
|
|
1550
|
+
if (content === "") return [];
|
|
1551
|
+
const chunks = [];
|
|
1552
|
+
let current = "";
|
|
1553
|
+
let currentBytes = 0;
|
|
1554
|
+
for (const char of content) {
|
|
1555
|
+
const charBytes = Buffer.byteLength(char, "utf8");
|
|
1556
|
+
if (current !== "" && currentBytes + charBytes > maxBytes) {
|
|
1557
|
+
chunks.push(current);
|
|
1558
|
+
current = "";
|
|
1559
|
+
currentBytes = 0;
|
|
1560
|
+
}
|
|
1561
|
+
current += char;
|
|
1562
|
+
currentBytes += charBytes;
|
|
1563
|
+
}
|
|
1564
|
+
if (current !== "") chunks.push(current);
|
|
1565
|
+
return chunks;
|
|
1139
1566
|
}
|
|
1140
1567
|
|
|
1141
1568
|
//#endregion
|
|
1142
|
-
//#region src/
|
|
1143
|
-
const logger$
|
|
1569
|
+
//#region src/core/delivery.ts
|
|
1570
|
+
const logger$8 = getLogger(["editprompt", "delivery"]);
|
|
1144
1571
|
async function copyToClipboard(content) {
|
|
1145
1572
|
await clipboardy.write(content);
|
|
1146
1573
|
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1574
|
+
/**
|
|
1575
|
+
* Types content into a pane without changing focus. Long content is split
|
|
1576
|
+
* into chunks the backend accepts and sent in order.
|
|
1577
|
+
*/
|
|
1578
|
+
async function sendContentToPane(backend, paneId, content) {
|
|
1579
|
+
await backend.beforeSendText?.(paneId);
|
|
1580
|
+
const chunks = splitByByteSize(content, backend.maxChunkBytes);
|
|
1581
|
+
for (const chunk of chunks) await backend.sendText(paneId, chunk);
|
|
1582
|
+
if (backend.logContentSent) backend.logContentSent(paneId, chunks.length);
|
|
1583
|
+
else logger$8.debug("Content sent to {backend} pane: {paneId} ({chunks} chunk(s))", {
|
|
1584
|
+
backend: backend.name,
|
|
1585
|
+
paneId,
|
|
1586
|
+
chunks: chunks.length
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
/**
|
|
1590
|
+
* Sends a key after a delay. The delay keeps some CLIs (e.g., codex) from
|
|
1591
|
+
* treating the key as part of a paste.
|
|
1592
|
+
*/
|
|
1593
|
+
async function sendKeyToPane(backend, paneId, key, delay = 1e3) {
|
|
1594
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1595
|
+
await backend.sendKey(paneId, key);
|
|
1152
1596
|
}
|
|
1153
|
-
async function focusFirstSuccessPane(
|
|
1597
|
+
async function focusFirstSuccessPane(backend, targetPanes, failedPanes) {
|
|
1154
1598
|
const firstSuccessPane = targetPanes.find((p) => !failedPanes.includes(p));
|
|
1155
|
-
if (firstSuccessPane)
|
|
1156
|
-
else if (mux === "wezterm") await focusPane(firstSuccessPane);
|
|
1157
|
-
else if (mux === "niwaterm") await focusPane$2(firstSuccessPane);
|
|
1158
|
-
else await focusPane$3(firstSuccessPane);
|
|
1599
|
+
if (firstSuccessPane) await backend.focusPane(firstSuccessPane);
|
|
1159
1600
|
}
|
|
1160
|
-
async function handleContentDelivery(content,
|
|
1601
|
+
async function handleContentDelivery(content, backend, targetPanes) {
|
|
1161
1602
|
if (!content) return {
|
|
1162
1603
|
successCount: 0,
|
|
1163
1604
|
totalCount: 0,
|
|
@@ -1168,9 +1609,9 @@ async function handleContentDelivery(content, mux, targetPanes) {
|
|
|
1168
1609
|
if (targetPanes.length === 0) {
|
|
1169
1610
|
try {
|
|
1170
1611
|
await copyToClipboard(content);
|
|
1171
|
-
logger$
|
|
1612
|
+
logger$8.info("Content copied to clipboard.");
|
|
1172
1613
|
} catch (error) {
|
|
1173
|
-
logger$
|
|
1614
|
+
logger$8.warn(`Failed to copy to clipboard: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1174
1615
|
}
|
|
1175
1616
|
return {
|
|
1176
1617
|
successCount: 0,
|
|
@@ -1182,13 +1623,13 @@ async function handleContentDelivery(content, mux, targetPanes) {
|
|
|
1182
1623
|
}
|
|
1183
1624
|
const results = [];
|
|
1184
1625
|
for (const targetPane of targetPanes) try {
|
|
1185
|
-
await
|
|
1626
|
+
await sendContentToPane(backend, targetPane, content);
|
|
1186
1627
|
results.push({
|
|
1187
1628
|
pane: targetPane,
|
|
1188
1629
|
success: true
|
|
1189
1630
|
});
|
|
1190
1631
|
} catch (error) {
|
|
1191
|
-
logger$
|
|
1632
|
+
logger$8.warn(`Failed to send to pane ${targetPane}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1192
1633
|
results.push({
|
|
1193
1634
|
pane: targetPane,
|
|
1194
1635
|
success: false
|
|
@@ -1198,13 +1639,13 @@ async function handleContentDelivery(content, mux, targetPanes) {
|
|
|
1198
1639
|
const failedPanes = results.filter((r) => !r.success).map((r) => r.pane);
|
|
1199
1640
|
const allSuccess = successCount === targetPanes.length;
|
|
1200
1641
|
const allFailed = successCount === 0;
|
|
1201
|
-
if (allSuccess) logger$
|
|
1642
|
+
if (allSuccess) logger$8.info("Content sent successfully to all panes!");
|
|
1202
1643
|
else if (allFailed) {
|
|
1203
|
-
logger$
|
|
1204
|
-
logger$
|
|
1644
|
+
logger$8.error("All target panes failed to receive content.");
|
|
1645
|
+
logger$8.info("Falling back to clipboard...");
|
|
1205
1646
|
await copyToClipboard(content);
|
|
1206
|
-
logger$
|
|
1207
|
-
} else logger$
|
|
1647
|
+
logger$8.info("Content copied to clipboard.");
|
|
1648
|
+
} else logger$8.warn(`Content sent to ${successCount}/${targetPanes.length} panes. Failed panes: ${failedPanes.join(", ")}`);
|
|
1208
1649
|
return {
|
|
1209
1650
|
successCount,
|
|
1210
1651
|
totalCount: targetPanes.length,
|
|
@@ -1214,6 +1655,71 @@ async function handleContentDelivery(content, mux, targetPanes) {
|
|
|
1214
1655
|
};
|
|
1215
1656
|
}
|
|
1216
1657
|
|
|
1658
|
+
//#endregion
|
|
1659
|
+
//#region src/modes/common.ts
|
|
1660
|
+
/**
|
|
1661
|
+
* Resolves the editor pane and its target panes for commands that run inside
|
|
1662
|
+
* an editor pane, exiting with an error (logged to the command's logger) when
|
|
1663
|
+
* there are none.
|
|
1664
|
+
*/
|
|
1665
|
+
async function requireEditorContext(backend, logger) {
|
|
1666
|
+
try {
|
|
1667
|
+
return await resolveEditorContext(backend);
|
|
1668
|
+
} catch (error) {
|
|
1669
|
+
if (error instanceof EditorContextError) {
|
|
1670
|
+
logger.error(error.message);
|
|
1671
|
+
process.exit(1);
|
|
1672
|
+
}
|
|
1673
|
+
throw error;
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
//#endregion
|
|
1678
|
+
//#region src/modes/dump.ts
|
|
1679
|
+
const logger$7 = getLogger(["editprompt", "dump"]);
|
|
1680
|
+
async function runDumpMode() {
|
|
1681
|
+
try {
|
|
1682
|
+
const backend = getBackend(readSendConfig().mux);
|
|
1683
|
+
const { targetPaneIds } = await requireEditorContext(backend, logger$7);
|
|
1684
|
+
const quoteContents = [];
|
|
1685
|
+
for (const targetPane of targetPaneIds) {
|
|
1686
|
+
const content = await takeQuote(backend, targetPane);
|
|
1687
|
+
if (content.trim() !== "") quoteContents.push(content);
|
|
1688
|
+
}
|
|
1689
|
+
const combinedContent = quoteContents.join("\n");
|
|
1690
|
+
process.stdout.write(combinedContent.replace(/\n{3,}$/, "\n\n"));
|
|
1691
|
+
process.exit(0);
|
|
1692
|
+
} catch (error) {
|
|
1693
|
+
logger$7.error(`${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1694
|
+
process.exit(1);
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
const dumpCommand = define({
|
|
1698
|
+
name: "dump",
|
|
1699
|
+
description: "Output and clear collected quoted text from environment variables",
|
|
1700
|
+
args: {
|
|
1701
|
+
"log-file": ARG_LOG_FILE,
|
|
1702
|
+
quiet: ARG_QUIET,
|
|
1703
|
+
verbose: ARG_VERBOSE
|
|
1704
|
+
},
|
|
1705
|
+
async run(ctx) {
|
|
1706
|
+
setupLogger({
|
|
1707
|
+
quiet: Boolean(ctx.values.quiet),
|
|
1708
|
+
verbose: Boolean(ctx.values.verbose),
|
|
1709
|
+
logFile: ctx.values["log-file"]
|
|
1710
|
+
});
|
|
1711
|
+
await runDumpMode();
|
|
1712
|
+
}
|
|
1713
|
+
});
|
|
1714
|
+
|
|
1715
|
+
//#endregion
|
|
1716
|
+
//#region src/utils/contentProcessor.ts
|
|
1717
|
+
function processContent(content) {
|
|
1718
|
+
let processed = content.replace(/\n$/, "");
|
|
1719
|
+
if (/@[^\n]*$/.test(processed)) processed += " ";
|
|
1720
|
+
return processed;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1217
1723
|
//#endregion
|
|
1218
1724
|
//#region src/modes/input.ts
|
|
1219
1725
|
const logger$6 = getLogger(["editprompt", "input"]);
|
|
@@ -1224,52 +1730,15 @@ async function runInputMode(rawContent, autoSend, sendKey, sendKeyDelay) {
|
|
|
1224
1730
|
return;
|
|
1225
1731
|
}
|
|
1226
1732
|
const config = readSendConfig();
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
if (config.mux === "tmux") {
|
|
1230
|
-
currentPaneId = await getCurrentPaneId$1();
|
|
1231
|
-
isEditor = await isEditorPane(currentPaneId);
|
|
1232
|
-
} else if (config.mux === "wezterm") {
|
|
1233
|
-
currentPaneId = await getCurrentPaneId();
|
|
1234
|
-
isEditor = isEditorPaneFromConf(currentPaneId);
|
|
1235
|
-
} else if (config.mux === "niwaterm") {
|
|
1236
|
-
currentPaneId = await getCurrentPaneId$2();
|
|
1237
|
-
isEditor = await isEditorPane$1(currentPaneId);
|
|
1238
|
-
} else {
|
|
1239
|
-
currentPaneId = await getCurrentPaneId$3();
|
|
1240
|
-
isEditor = isEditorPaneFromConf$1(currentPaneId);
|
|
1241
|
-
}
|
|
1242
|
-
if (!isEditor) {
|
|
1243
|
-
logger$6.error("Current pane is not an editor pane");
|
|
1244
|
-
process.exit(1);
|
|
1245
|
-
}
|
|
1246
|
-
let targetPanes;
|
|
1247
|
-
if (config.mux === "tmux") targetPanes = await getTargetPaneIds$1(currentPaneId);
|
|
1248
|
-
else if (config.mux === "wezterm") targetPanes = await getTargetPaneIds(currentPaneId);
|
|
1249
|
-
else if (config.mux === "niwaterm") targetPanes = await getTargetPaneIds$2(currentPaneId);
|
|
1250
|
-
else targetPanes = await getTargetPaneIds$3(currentPaneId);
|
|
1251
|
-
if (targetPanes.length === 0) {
|
|
1252
|
-
logger$6.error("No target panes registered for this editor pane");
|
|
1253
|
-
process.exit(1);
|
|
1254
|
-
}
|
|
1733
|
+
const backend = getBackend(config.mux);
|
|
1734
|
+
const { targetPaneIds: targetPanes } = await requireEditorContext(backend, logger$6);
|
|
1255
1735
|
if (autoSend) {
|
|
1256
|
-
const key = sendKey ||
|
|
1736
|
+
const key = sendKey || backend.defaultSubmitKey;
|
|
1257
1737
|
const delay = /\.(png|webp|avif|jpe?g|gif)\b/i.test(content) ? sendKeyDelay ?? 1e3 : 200;
|
|
1258
1738
|
let successCount = 0;
|
|
1259
1739
|
for (const targetPane of targetPanes) try {
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
await sendKeyToWeztermPane(targetPane, key, delay);
|
|
1263
|
-
} else if (config.mux === "herdr") {
|
|
1264
|
-
await inputToHerdrPane(targetPane, content);
|
|
1265
|
-
await sendKeyToHerdrPane(targetPane, key, delay);
|
|
1266
|
-
} else if (config.mux === "niwaterm") {
|
|
1267
|
-
await inputToNiwatermPane(targetPane, content);
|
|
1268
|
-
await sendKeyToNiwatermPane(targetPane, key, delay);
|
|
1269
|
-
} else {
|
|
1270
|
-
await inputToTmuxPane(targetPane, content);
|
|
1271
|
-
await sendKeyToTmuxPane(targetPane, key, delay);
|
|
1272
|
-
}
|
|
1740
|
+
await sendContentToPane(backend, targetPane, content);
|
|
1741
|
+
await sendKeyToPane(backend, targetPane, key, delay);
|
|
1273
1742
|
successCount++;
|
|
1274
1743
|
} catch (error) {
|
|
1275
1744
|
logger$6.error(`Failed to send to pane ${targetPane}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
@@ -1286,12 +1755,12 @@ async function runInputMode(rawContent, autoSend, sendKey, sendKeyDelay) {
|
|
|
1286
1755
|
return;
|
|
1287
1756
|
}
|
|
1288
1757
|
try {
|
|
1289
|
-
const result = await handleContentDelivery(content,
|
|
1758
|
+
const result = await handleContentDelivery(content, backend, targetPanes);
|
|
1290
1759
|
if (config.alwaysCopy && !result.allFailed) {
|
|
1291
1760
|
await copyToClipboard(content);
|
|
1292
1761
|
logger$6.info("Also copied to clipboard.");
|
|
1293
1762
|
}
|
|
1294
|
-
if (result.successCount > 0) await focusFirstSuccessPane(
|
|
1763
|
+
if (result.successCount > 0) await focusFirstSuccessPane(backend, targetPanes, result.failedPanes);
|
|
1295
1764
|
if (result.allFailed) process.exit(1);
|
|
1296
1765
|
} catch (error) {
|
|
1297
1766
|
logger$6.error(`${error instanceof Error ? error.message : "Unknown error"}`);
|
|
@@ -1424,22 +1893,11 @@ async function openEditorAndGetContent(editorOption, envVars, sendConfig) {
|
|
|
1424
1893
|
//#region src/modes/openEditor.ts
|
|
1425
1894
|
const logger$5 = getLogger(["editprompt", "open"]);
|
|
1426
1895
|
async function runOpenEditorMode(options) {
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
const currentPaneId = await getCurrentPaneId();
|
|
1433
|
-
await markAsEditorPane(currentPaneId, options.targetPanes);
|
|
1434
|
-
} catch {}
|
|
1435
|
-
else if (options.targetPanes.length > 0 && options.mux === "herdr") try {
|
|
1436
|
-
const currentPaneId = await getCurrentPaneId$3();
|
|
1437
|
-
herdrEditorPaneId = currentPaneId;
|
|
1438
|
-
await markAsEditorPane$3(currentPaneId, options.targetPanes);
|
|
1439
|
-
} catch {}
|
|
1440
|
-
else if (options.targetPanes.length > 0 && options.mux === "niwaterm") try {
|
|
1441
|
-
const currentPaneId = await getCurrentPaneId$2();
|
|
1442
|
-
await markAsEditorPane$2(currentPaneId, options.targetPanes);
|
|
1896
|
+
const backend = getBackend(options.mux);
|
|
1897
|
+
let editorPaneId;
|
|
1898
|
+
if (options.targetPanes.length > 0) try {
|
|
1899
|
+
editorPaneId = await backend.getCurrentPaneId();
|
|
1900
|
+
await markAsEditorPane(backend, editorPaneId, options.targetPanes);
|
|
1443
1901
|
} catch {}
|
|
1444
1902
|
try {
|
|
1445
1903
|
const sendConfig = {
|
|
@@ -1454,31 +1912,23 @@ async function runOpenEditorMode(options) {
|
|
|
1454
1912
|
return;
|
|
1455
1913
|
}
|
|
1456
1914
|
try {
|
|
1457
|
-
const result = await handleContentDelivery(content,
|
|
1915
|
+
const result = await handleContentDelivery(content, backend, options.targetPanes);
|
|
1458
1916
|
console.log("---");
|
|
1459
1917
|
console.log(content);
|
|
1460
1918
|
if (options.alwaysCopy && !result.allFailed) {
|
|
1461
1919
|
await copyToClipboard(content);
|
|
1462
1920
|
logger$5.info("Also copied to clipboard.");
|
|
1463
1921
|
}
|
|
1464
|
-
if (options.targetPanes.length > 0 && result.successCount > 0) await focusFirstSuccessPane(
|
|
1922
|
+
if (options.targetPanes.length > 0 && result.successCount > 0) await focusFirstSuccessPane(backend, options.targetPanes, result.failedPanes);
|
|
1465
1923
|
if (!result.allSuccess) process.exit(1);
|
|
1466
1924
|
} catch (error) {
|
|
1467
1925
|
logger$5.error(`${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1468
1926
|
process.exit(1);
|
|
1469
1927
|
}
|
|
1470
1928
|
} finally {
|
|
1471
|
-
if (options.targetPanes.length > 0
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
else if (options.targetPanes.length > 0 && options.mux === "wezterm") try {
|
|
1475
|
-
for (const targetPane of options.targetPanes) await clearEditorPaneId(targetPane);
|
|
1476
|
-
} catch {}
|
|
1477
|
-
else if (options.targetPanes.length > 0 && options.mux === "herdr") try {
|
|
1478
|
-
for (const targetPane of options.targetPanes) await clearEditorPaneId$3(targetPane, herdrEditorPaneId);
|
|
1479
|
-
} catch {}
|
|
1480
|
-
else if (options.targetPanes.length > 0 && options.mux === "niwaterm") try {
|
|
1481
|
-
for (const targetPane of options.targetPanes) await clearEditorPaneId$2(targetPane);
|
|
1929
|
+
if (options.targetPanes.length > 0) try {
|
|
1930
|
+
const expectedEditorPaneId = backend.keepNewerEditorLink ? editorPaneId : void 0;
|
|
1931
|
+
for (const targetPane of options.targetPanes) await clearEditorLink(backend, targetPane, expectedEditorPaneId);
|
|
1482
1932
|
} catch {}
|
|
1483
1933
|
}
|
|
1484
1934
|
}
|
|
@@ -1519,47 +1969,20 @@ async function runRegisterMode(options) {
|
|
|
1519
1969
|
logger$4.error("--target-pane is required for register command");
|
|
1520
1970
|
process.exit(1);
|
|
1521
1971
|
}
|
|
1972
|
+
const backend = getBackend(options.mux);
|
|
1522
1973
|
let editorPaneId;
|
|
1523
1974
|
if (options.editorPane) editorPaneId = options.editorPane;
|
|
1524
|
-
else
|
|
1525
|
-
editorPaneId = await getCurrentPaneId
|
|
1526
|
-
if (!await isEditorPane(editorPaneId)) {
|
|
1527
|
-
logger$4.error("Current pane is not an editor pane. Please run this command from an editor pane or specify --editor-pane.");
|
|
1528
|
-
process.exit(1);
|
|
1529
|
-
}
|
|
1530
|
-
} else if (options.mux === "wezterm") {
|
|
1531
|
-
editorPaneId = await getCurrentPaneId();
|
|
1532
|
-
if (!isEditorPaneFromConf(editorPaneId)) {
|
|
1533
|
-
logger$4.error("Current pane is not an editor pane. Please run this command from an editor pane or specify --editor-pane.");
|
|
1534
|
-
process.exit(1);
|
|
1535
|
-
}
|
|
1536
|
-
} else if (options.mux === "herdr") {
|
|
1537
|
-
editorPaneId = await getCurrentPaneId$3();
|
|
1538
|
-
if (!isEditorPaneFromConf$1(editorPaneId)) {
|
|
1539
|
-
logger$4.error("Current pane is not an editor pane. Please run this command from an editor pane or specify --editor-pane.");
|
|
1540
|
-
process.exit(1);
|
|
1541
|
-
}
|
|
1542
|
-
} else if (options.mux === "niwaterm") {
|
|
1543
|
-
editorPaneId = await getCurrentPaneId$2();
|
|
1544
|
-
if (!await isEditorPane$1(editorPaneId)) {
|
|
1975
|
+
else {
|
|
1976
|
+
editorPaneId = await backend.getCurrentPaneId();
|
|
1977
|
+
if (!await isEditorPane(backend, editorPaneId)) {
|
|
1545
1978
|
logger$4.error("Current pane is not an editor pane. Please run this command from an editor pane or specify --editor-pane.");
|
|
1546
1979
|
process.exit(1);
|
|
1547
1980
|
}
|
|
1548
|
-
} else {
|
|
1549
|
-
logger$4.error("Unsupported multiplexer");
|
|
1550
|
-
process.exit(1);
|
|
1551
1981
|
}
|
|
1552
1982
|
try {
|
|
1553
|
-
|
|
1554
|
-
if (options.mux === "tmux") existingPanes = await getTargetPaneIds$1(editorPaneId);
|
|
1555
|
-
else if (options.mux === "wezterm") existingPanes = await getTargetPaneIds(editorPaneId);
|
|
1556
|
-
else if (options.mux === "herdr") existingPanes = await getTargetPaneIds$3(editorPaneId);
|
|
1557
|
-
else if (options.mux === "niwaterm") existingPanes = await getTargetPaneIds$2(editorPaneId);
|
|
1983
|
+
const existingPanes = await getTargetPaneIds(backend, editorPaneId);
|
|
1558
1984
|
const mergedTargetPanes = [...new Set([...existingPanes, ...options.targetPanes])];
|
|
1559
|
-
|
|
1560
|
-
else if (options.mux === "wezterm") await markAsEditorPane(editorPaneId, mergedTargetPanes);
|
|
1561
|
-
else if (options.mux === "herdr") await markAsEditorPane$3(editorPaneId, mergedTargetPanes);
|
|
1562
|
-
else if (options.mux === "niwaterm") await markAsEditorPane$2(editorPaneId, mergedTargetPanes);
|
|
1985
|
+
await markAsEditorPane(backend, editorPaneId, mergedTargetPanes);
|
|
1563
1986
|
logger$4.info(`Editor pane ${editorPaneId} registered with target panes: ${mergedTargetPanes.join(", ")}`);
|
|
1564
1987
|
} catch (error) {
|
|
1565
1988
|
logger$4.error(`${error instanceof Error ? error.message : "Unknown error"}`);
|
|
@@ -1596,124 +2019,74 @@ const registerCommand = define({
|
|
|
1596
2019
|
});
|
|
1597
2020
|
|
|
1598
2021
|
//#endregion
|
|
1599
|
-
//#region src/
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
2022
|
+
//#region src/core/resume.ts
|
|
2023
|
+
/** Raised when focusing the editor pane from its target pane fails. */
|
|
2024
|
+
var EditorFocusError = class extends Error {
|
|
2025
|
+
name = "EditorFocusError";
|
|
2026
|
+
constructor(editorPaneId, cause) {
|
|
2027
|
+
super(`Can't focus editor pane ${editorPaneId}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
2028
|
+
this.editorPaneId = editorPaneId;
|
|
2029
|
+
this.cause = cause;
|
|
1607
2030
|
}
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
for (const paneId of originalTargetPaneIds) if (await checkPaneExists$2(paneId)) {
|
|
1619
|
-
await focusPane$2(paneId);
|
|
1620
|
-
focused = true;
|
|
1621
|
-
break;
|
|
1622
|
-
}
|
|
1623
|
-
if (!focused) {
|
|
1624
|
-
logger$3.debug("All target panes do not exist");
|
|
1625
|
-
process.exit(1);
|
|
1626
|
-
}
|
|
1627
|
-
process.exit(0);
|
|
1628
|
-
}
|
|
1629
|
-
logger$3.debug("Current pane is not an editor pane");
|
|
1630
|
-
const editorPaneId = await getEditorPaneId$2(targetPane);
|
|
1631
|
-
logger$3.debug("niwaterm editorPaneId: {editorPaneId}", { editorPaneId });
|
|
1632
|
-
if (editorPaneId === "") {
|
|
1633
|
-
logger$3.debug("Editor pane ID not found");
|
|
1634
|
-
process.exit(1);
|
|
1635
|
-
}
|
|
1636
|
-
if (!await checkPaneExists$2(editorPaneId)) {
|
|
1637
|
-
logger$3.debug("Editor pane does not exist");
|
|
1638
|
-
await clearEditorPaneId$2(targetPane);
|
|
1639
|
-
process.exit(1);
|
|
2031
|
+
};
|
|
2032
|
+
async function switchEditorFocus(backend, targetPaneId, trace = () => {}) {
|
|
2033
|
+
const currentPaneId = await backend.getCurrentPaneId();
|
|
2034
|
+
const editorPaneId = await backend.state.isEditorPane(currentPaneId) ? currentPaneId : backend.resumeAcceptsEditorPane && await backend.state.isEditorPane(targetPaneId) ? targetPaneId : "";
|
|
2035
|
+
if (editorPaneId) {
|
|
2036
|
+
trace("Current pane is an editor pane");
|
|
2037
|
+
const targetPaneIds = await backend.state.getTargetPaneIds(editorPaneId);
|
|
2038
|
+
if (targetPaneIds.length === 0) {
|
|
2039
|
+
trace("No target pane IDs found for editor pane");
|
|
2040
|
+
return false;
|
|
1640
2041
|
}
|
|
1641
|
-
|
|
1642
|
-
await focusPane
|
|
1643
|
-
|
|
1644
|
-
} catch (error) {
|
|
1645
|
-
logger$3.debug("Can't focus editorPaneId: {editorPaneId}, error: {error}", {
|
|
1646
|
-
editorPaneId,
|
|
1647
|
-
error
|
|
1648
|
-
});
|
|
1649
|
-
process.exit(1);
|
|
2042
|
+
for (const paneId of targetPaneIds) if (await backend.paneExists(paneId)) {
|
|
2043
|
+
await backend.focusPane(paneId);
|
|
2044
|
+
return true;
|
|
1650
2045
|
}
|
|
2046
|
+
trace("All target panes do not exist");
|
|
2047
|
+
return false;
|
|
1651
2048
|
}
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
logger$3.debug("No target pane IDs found for editor pane");
|
|
1659
|
-
process.exit(1);
|
|
1660
|
-
}
|
|
1661
|
-
let focused = false;
|
|
1662
|
-
for (const paneId of originalTargetPaneIds) if (await checkPaneExists(paneId)) {
|
|
1663
|
-
await focusPane(paneId);
|
|
1664
|
-
focused = true;
|
|
1665
|
-
break;
|
|
1666
|
-
}
|
|
1667
|
-
if (!focused) {
|
|
1668
|
-
logger$3.debug("All target panes do not exist");
|
|
1669
|
-
process.exit(1);
|
|
1670
|
-
}
|
|
1671
|
-
process.exit(0);
|
|
1672
|
-
}
|
|
1673
|
-
logger$3.debug("Current pane is not an editor pane");
|
|
1674
|
-
const editorPaneId = await getEditorPaneId(targetPane);
|
|
1675
|
-
logger$3.debug("wezterm editorPaneId: {editorPaneId}", { editorPaneId });
|
|
1676
|
-
if (editorPaneId === "") {
|
|
1677
|
-
logger$3.debug("Editor pane ID not found");
|
|
1678
|
-
process.exit(1);
|
|
1679
|
-
}
|
|
1680
|
-
if (!await checkPaneExists(editorPaneId)) {
|
|
1681
|
-
logger$3.debug("Editor pane does not exist");
|
|
1682
|
-
await clearEditorPaneId(targetPane);
|
|
1683
|
-
process.exit(1);
|
|
1684
|
-
}
|
|
1685
|
-
try {
|
|
1686
|
-
await focusPane(editorPaneId);
|
|
1687
|
-
process.exit(0);
|
|
1688
|
-
} catch (error) {
|
|
1689
|
-
logger$3.debug("Can't focus editorPaneId: {editorPaneId}, error: {error}", {
|
|
1690
|
-
editorPaneId,
|
|
1691
|
-
error
|
|
1692
|
-
});
|
|
1693
|
-
process.exit(1);
|
|
1694
|
-
}
|
|
2049
|
+
trace("Current pane is not an editor pane");
|
|
2050
|
+
const registeredEditorPaneId = await backend.state.getEditorPaneId(targetPaneId);
|
|
2051
|
+
trace(`${backend.name} editorPaneId: {editorPaneId}`, { editorPaneId: registeredEditorPaneId });
|
|
2052
|
+
if (registeredEditorPaneId === "") {
|
|
2053
|
+
trace("Editor pane ID not found");
|
|
2054
|
+
return false;
|
|
1695
2055
|
}
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
let focused = false;
|
|
1701
|
-
for (const paneId of originalTargetPaneIds) if (await checkPaneExists$1(paneId)) {
|
|
1702
|
-
await focusPane$1(paneId);
|
|
1703
|
-
focused = true;
|
|
1704
|
-
break;
|
|
1705
|
-
}
|
|
1706
|
-
if (!focused) process.exit(1);
|
|
1707
|
-
process.exit(0);
|
|
2056
|
+
if (!await backend.paneExists(registeredEditorPaneId)) {
|
|
2057
|
+
trace("Editor pane does not exist");
|
|
2058
|
+
await clearEditorLink(backend, targetPaneId);
|
|
2059
|
+
return false;
|
|
1708
2060
|
}
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
2061
|
+
try {
|
|
2062
|
+
await backend.focusPane(registeredEditorPaneId);
|
|
2063
|
+
} catch (error) {
|
|
2064
|
+
throw new EditorFocusError(registeredEditorPaneId, error);
|
|
2065
|
+
}
|
|
2066
|
+
return true;
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
//#endregion
|
|
2070
|
+
//#region src/modes/resume.ts
|
|
2071
|
+
const logger$3 = getLogger(["editprompt", "resume"]);
|
|
2072
|
+
async function runResumeMode(targetPane, mux) {
|
|
2073
|
+
const backend = getBackend(mux);
|
|
2074
|
+
let switched;
|
|
2075
|
+
try {
|
|
2076
|
+
switched = await switchEditorFocus(backend, targetPane, backend.traceResume ? (message, properties) => logger$3.debug(message, properties) : void 0);
|
|
2077
|
+
} catch (error) {
|
|
2078
|
+
if (!(backend.quietResumeErrors === "all" || backend.quietResumeErrors === "editorFocus" && error instanceof EditorFocusError)) throw error instanceof EditorFocusError ? error.cause : error;
|
|
2079
|
+
if (error instanceof EditorFocusError) logger$3.debug("Can't focus editorPaneId: {editorPaneId}, error: {error}", {
|
|
2080
|
+
editorPaneId: error.editorPaneId,
|
|
2081
|
+
error: error.cause
|
|
2082
|
+
});
|
|
2083
|
+
else {
|
|
2084
|
+
const label = backend.name.charAt(0).toUpperCase() + backend.name.slice(1);
|
|
2085
|
+
logger$3.debug(`${label} resume failed: {error}`, { error });
|
|
2086
|
+
}
|
|
1713
2087
|
process.exit(1);
|
|
1714
2088
|
}
|
|
1715
|
-
|
|
1716
|
-
process.exit(0);
|
|
2089
|
+
process.exit(switched ? 0 : 1);
|
|
1717
2090
|
}
|
|
1718
2091
|
const resumeCommand = define({
|
|
1719
2092
|
name: "resume",
|
|
@@ -1743,41 +2116,11 @@ async function runPressMode(key, delay = 0) {
|
|
|
1743
2116
|
logger$2.error("Key is required");
|
|
1744
2117
|
process.exit(1);
|
|
1745
2118
|
}
|
|
1746
|
-
const
|
|
1747
|
-
|
|
1748
|
-
let isEditor;
|
|
1749
|
-
if (config.mux === "tmux") {
|
|
1750
|
-
currentPaneId = await getCurrentPaneId$1();
|
|
1751
|
-
isEditor = await isEditorPane(currentPaneId);
|
|
1752
|
-
} else if (config.mux === "wezterm") {
|
|
1753
|
-
currentPaneId = await getCurrentPaneId();
|
|
1754
|
-
isEditor = isEditorPaneFromConf(currentPaneId);
|
|
1755
|
-
} else if (config.mux === "niwaterm") {
|
|
1756
|
-
currentPaneId = await getCurrentPaneId$2();
|
|
1757
|
-
isEditor = await isEditorPane$1(currentPaneId);
|
|
1758
|
-
} else {
|
|
1759
|
-
currentPaneId = await getCurrentPaneId$3();
|
|
1760
|
-
isEditor = isEditorPaneFromConf$1(currentPaneId);
|
|
1761
|
-
}
|
|
1762
|
-
if (!isEditor) {
|
|
1763
|
-
logger$2.error("Current pane is not an editor pane");
|
|
1764
|
-
process.exit(1);
|
|
1765
|
-
}
|
|
1766
|
-
let targetPanes;
|
|
1767
|
-
if (config.mux === "tmux") targetPanes = await getTargetPaneIds$1(currentPaneId);
|
|
1768
|
-
else if (config.mux === "wezterm") targetPanes = await getTargetPaneIds(currentPaneId);
|
|
1769
|
-
else if (config.mux === "niwaterm") targetPanes = await getTargetPaneIds$2(currentPaneId);
|
|
1770
|
-
else targetPanes = await getTargetPaneIds$3(currentPaneId);
|
|
1771
|
-
if (targetPanes.length === 0) {
|
|
1772
|
-
logger$2.error("No target panes registered for this editor pane");
|
|
1773
|
-
process.exit(1);
|
|
1774
|
-
}
|
|
2119
|
+
const backend = getBackend(readSendConfig().mux);
|
|
2120
|
+
const { targetPaneIds } = await requireEditorContext(backend, logger$2);
|
|
1775
2121
|
let successCount = 0;
|
|
1776
|
-
for (const targetPane of
|
|
1777
|
-
|
|
1778
|
-
else if (config.mux === "herdr") await sendKeyToHerdrPane(targetPane, key, delay);
|
|
1779
|
-
else if (config.mux === "niwaterm") await sendKeyToNiwatermPane(targetPane, key, delay);
|
|
1780
|
-
else await sendKeyToTmuxPane(targetPane, key, delay);
|
|
2122
|
+
for (const targetPane of targetPaneIds) try {
|
|
2123
|
+
await sendKeyToPane(backend, targetPane, key, delay);
|
|
1781
2124
|
successCount++;
|
|
1782
2125
|
} catch (error) {
|
|
1783
2126
|
logger$2.warn(`Failed to send key to pane ${targetPane}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
@@ -1820,7 +2163,7 @@ const pressCommand = define({
|
|
|
1820
2163
|
//#region src/modes/stash.ts
|
|
1821
2164
|
const logger$1 = getLogger(["editprompt", "stash"]);
|
|
1822
2165
|
function getStashKey(mux, targetPaneId) {
|
|
1823
|
-
return `${mux
|
|
2166
|
+
return `${getBackend(mux).storageNamespace()}.targetPane.pane_${targetPaneId}.stash`;
|
|
1824
2167
|
}
|
|
1825
2168
|
async function pushStash(mux, targetPaneId, content) {
|
|
1826
2169
|
const key = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -1862,37 +2205,10 @@ function dropStash(mux, targetPaneId, key) {
|
|
|
1862
2205
|
}
|
|
1863
2206
|
async function getTargetPaneForStash() {
|
|
1864
2207
|
const config = readSendConfig();
|
|
1865
|
-
|
|
1866
|
-
let isEditor;
|
|
1867
|
-
if (config.mux === "tmux") {
|
|
1868
|
-
currentPaneId = await getCurrentPaneId$1();
|
|
1869
|
-
isEditor = await isEditorPane(currentPaneId);
|
|
1870
|
-
} else if (config.mux === "wezterm") {
|
|
1871
|
-
currentPaneId = await getCurrentPaneId();
|
|
1872
|
-
isEditor = isEditorPaneFromConf(currentPaneId);
|
|
1873
|
-
} else if (config.mux === "niwaterm") {
|
|
1874
|
-
currentPaneId = await getCurrentPaneId$2();
|
|
1875
|
-
isEditor = await isEditorPane$1(currentPaneId);
|
|
1876
|
-
} else {
|
|
1877
|
-
currentPaneId = await getCurrentPaneId$3();
|
|
1878
|
-
isEditor = isEditorPaneFromConf$1(currentPaneId);
|
|
1879
|
-
}
|
|
1880
|
-
if (!isEditor) {
|
|
1881
|
-
logger$1.error("Current pane is not an editor pane");
|
|
1882
|
-
process.exit(1);
|
|
1883
|
-
}
|
|
1884
|
-
let targetPanes;
|
|
1885
|
-
if (config.mux === "tmux") targetPanes = await getTargetPaneIds$1(currentPaneId);
|
|
1886
|
-
else if (config.mux === "wezterm") targetPanes = await getTargetPaneIds(currentPaneId);
|
|
1887
|
-
else if (config.mux === "niwaterm") targetPanes = await getTargetPaneIds$2(currentPaneId);
|
|
1888
|
-
else targetPanes = await getTargetPaneIds$3(currentPaneId);
|
|
1889
|
-
if (targetPanes.length === 0) {
|
|
1890
|
-
logger$1.error("No target panes registered for this editor pane");
|
|
1891
|
-
process.exit(1);
|
|
1892
|
-
}
|
|
2208
|
+
const { targetPaneIds } = await requireEditorContext(getBackend(config.mux), logger$1);
|
|
1893
2209
|
return {
|
|
1894
2210
|
mux: config.mux,
|
|
1895
|
-
targetPaneId:
|
|
2211
|
+
targetPaneId: targetPaneIds[0]
|
|
1896
2212
|
};
|
|
1897
2213
|
}
|
|
1898
2214
|
async function runPush(rest, positionals) {
|
|
@@ -2034,18 +2350,21 @@ function buildOpenEditorArgv(options) {
|
|
|
2034
2350
|
if (options.verbose) argv.push("--verbose");
|
|
2035
2351
|
return argv;
|
|
2036
2352
|
}
|
|
2037
|
-
async function runToggleMode(options,
|
|
2038
|
-
if (
|
|
2353
|
+
async function runToggleMode(options, backend = getBackend(options.mux)) {
|
|
2354
|
+
if (!canSplitPane(backend)) throw new Error(isBuiltinBackendName(backend.name) ? "toggle currently supports only the Herdr multiplexer" : `toggle is not supported by the '${backend.name}' multiplexer because it cannot split panes`);
|
|
2039
2355
|
if (!Number.isInteger(options.paneRows) || options.paneRows <= 0) throw new Error("--pane-rows must be a positive integer");
|
|
2040
|
-
if (await
|
|
2356
|
+
if (await switchEditorFocus(backend, options.targetPane)) return;
|
|
2041
2357
|
const launchEnv = {};
|
|
2042
2358
|
if (process.env.NODE_NO_WARNINGS !== void 0) launchEnv.NODE_NO_WARNINGS = process.env.NODE_NO_WARNINGS;
|
|
2043
|
-
const editorPaneId = await
|
|
2359
|
+
const editorPaneId = await backend.splitPane(options.targetPane, {
|
|
2360
|
+
rows: options.paneRows,
|
|
2361
|
+
env: launchEnv
|
|
2362
|
+
});
|
|
2044
2363
|
try {
|
|
2045
|
-
await
|
|
2364
|
+
await backend.runInPane(editorPaneId, buildOpenEditorArgv(options));
|
|
2046
2365
|
} catch (error) {
|
|
2047
2366
|
try {
|
|
2048
|
-
await
|
|
2367
|
+
await backend.closePane(editorPaneId);
|
|
2049
2368
|
} catch (closeError) {
|
|
2050
2369
|
logger.warn("Failed to close editor pane after launch failure: {error}", { error: closeError });
|
|
2051
2370
|
}
|
|
@@ -2102,7 +2421,9 @@ const toggleCommand = define({
|
|
|
2102
2421
|
|
|
2103
2422
|
//#endregion
|
|
2104
2423
|
//#region src/index.ts
|
|
2105
|
-
|
|
2424
|
+
const argv = process.argv.slice(2);
|
|
2425
|
+
if (mayUseConfiguredBackend(argv)) await setupUserConfig();
|
|
2426
|
+
await cli(argv, {
|
|
2106
2427
|
name: "editprompt",
|
|
2107
2428
|
description: "A CLI tool that lets you write prompts for CLI tools using your favorite text editor",
|
|
2108
2429
|
args: {},
|