@libtmux/workspace-cli 0.1.0-alpha.11
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/LICENSE +21 -0
- package/README.md +330 -0
- package/THIRD-PARTY-NOTICES.md +183 -0
- package/dist/app-w7cbtf0n.js +747 -0
- package/dist/completions-2a9fqwq3.js +282 -0
- package/dist/extensions-8v6mfszj.js +268 -0
- package/dist/index-0j386v40.js +7112 -0
- package/dist/main-1de529jm.js +37 -0
- package/dist/main-bfnr2nkj.js +3106 -0
- package/dist/main-c8atq2pj.js +470 -0
- package/dist/main-gh3pafqb.js +369 -0
- package/dist/main.js +35 -0
- package/dist/progress-n08zapg2.js +1649 -0
- package/dist/reference-ebfxh6yp.js +90 -0
- package/dist/tmux-wa2zn8z1.js +893 -0
- package/docs/command-reference.md +227 -0
- package/docs/commands.json +1125 -0
- package/docs/completions/_tmux-workspace +239 -0
- package/docs/completions/tmux-workspace.bash +257 -0
- package/docs/completions/tmux-workspace.fish +430 -0
- package/package.json +49 -0
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
import {
|
|
2
|
+
expand,
|
|
3
|
+
normalize,
|
|
4
|
+
processRun,
|
|
5
|
+
tokenize
|
|
6
|
+
} from "./main-c8atq2pj.js";
|
|
7
|
+
import {
|
|
8
|
+
CliError,
|
|
9
|
+
OperationOutput,
|
|
10
|
+
colorEnabled,
|
|
11
|
+
discover,
|
|
12
|
+
emitJson,
|
|
13
|
+
expandPath,
|
|
14
|
+
home,
|
|
15
|
+
mapping,
|
|
16
|
+
privatePath,
|
|
17
|
+
readDocument,
|
|
18
|
+
resolveWorkspace,
|
|
19
|
+
saveDocument,
|
|
20
|
+
scalarText,
|
|
21
|
+
sequence,
|
|
22
|
+
styled,
|
|
23
|
+
write
|
|
24
|
+
} from "./main-gh3pafqb.js";
|
|
25
|
+
import {
|
|
26
|
+
CommanderError,
|
|
27
|
+
createParser,
|
|
28
|
+
package_default
|
|
29
|
+
} from "./main-bfnr2nkj.js";
|
|
30
|
+
import {
|
|
31
|
+
__require
|
|
32
|
+
} from "./main-1de529jm.js";
|
|
33
|
+
|
|
34
|
+
// src/app.ts
|
|
35
|
+
import { basename, dirname, extname, join as join2, resolve as resolve3 } from "node:path";
|
|
36
|
+
|
|
37
|
+
// src/imports.ts
|
|
38
|
+
import { join, resolve } from "node:path";
|
|
39
|
+
function fields(value, allowed, path) {
|
|
40
|
+
for (const key of Object.keys(value))
|
|
41
|
+
if (!allowed.includes(key))
|
|
42
|
+
throw new Error(`Unsupported import field: ${path}.${key}`);
|
|
43
|
+
}
|
|
44
|
+
function alias(value, current, legacy, path) {
|
|
45
|
+
if (value[current] !== undefined && value[current] !== null && value[legacy] !== undefined && value[legacy] !== null)
|
|
46
|
+
throw new Error(`Choose ${path}.${current} or ${path}.${legacy}`);
|
|
47
|
+
return value[current] ?? value[legacy];
|
|
48
|
+
}
|
|
49
|
+
function strings(value, path) {
|
|
50
|
+
if (value === undefined || value === null)
|
|
51
|
+
return [];
|
|
52
|
+
const entries = typeof value === "string" ? [value] : sequence(value, path);
|
|
53
|
+
return entries.map((entry) => {
|
|
54
|
+
if (typeof entry !== "string")
|
|
55
|
+
throw new Error(`${path} must contain strings`);
|
|
56
|
+
return entry;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function commands(value, path, separator) {
|
|
60
|
+
const entries = strings(value, path);
|
|
61
|
+
return (separator && entries.length ? [entries.join(separator)] : entries).map((cmd) => ({
|
|
62
|
+
cmd
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
function directory(value, base, context) {
|
|
66
|
+
if (value === undefined || value === null)
|
|
67
|
+
return base;
|
|
68
|
+
if (typeof value !== "string")
|
|
69
|
+
throw new Error("Imported root must be a string");
|
|
70
|
+
return resolve(base, expand(value, context));
|
|
71
|
+
}
|
|
72
|
+
function copy(source, target, names) {
|
|
73
|
+
for (const name of names)
|
|
74
|
+
if (source[name] !== undefined)
|
|
75
|
+
target[name] = structuredClone(source[name]);
|
|
76
|
+
}
|
|
77
|
+
function focusFirst(items) {
|
|
78
|
+
const selected = items.findIndex((item) => item.focus === true);
|
|
79
|
+
for (const [index, item] of items.entries()) {
|
|
80
|
+
if (item.focus !== undefined && typeof item.focus !== "boolean")
|
|
81
|
+
throw new Error("Imported focus must be boolean");
|
|
82
|
+
if (index === (selected < 0 ? 0 : selected))
|
|
83
|
+
item.focus = true;
|
|
84
|
+
else
|
|
85
|
+
delete item.focus;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function teamocil(source, context, fallbackName) {
|
|
89
|
+
if (source.session !== undefined)
|
|
90
|
+
fields(source, ["session"], "teamocil");
|
|
91
|
+
const session = mapping(source.session ?? source, "teamocil session");
|
|
92
|
+
fields(session, ["name", "root", "windows", "description"], "session");
|
|
93
|
+
const root = directory(session.root, context.cwd, context);
|
|
94
|
+
const windows = sequence(session.windows, "session.windows").map((value, ordinal) => {
|
|
95
|
+
const path = `session.windows[${ordinal}]`;
|
|
96
|
+
const window = mapping(value, path);
|
|
97
|
+
fields(window, ["name", "root", "layout", "focus", "options", "panes", "splits", "description"], path);
|
|
98
|
+
const result2 = { window_name: window.name ?? null };
|
|
99
|
+
copy(window, result2, ["layout", "focus", "options", "description"]);
|
|
100
|
+
if (window.root !== undefined)
|
|
101
|
+
result2.start_directory = directory(window.root, root, context);
|
|
102
|
+
const panes = sequence(alias(window, "panes", "splits", path) ?? [null], `${path}.panes`).map((value2, index) => {
|
|
103
|
+
const panePath = `${path}.panes[${index}]`;
|
|
104
|
+
if (typeof value2 === "string" || value2 === null)
|
|
105
|
+
return { shell_command: commands(value2, panePath) };
|
|
106
|
+
const pane = mapping(value2, panePath);
|
|
107
|
+
fields(pane, ["commands", "cmd", "focus", "description"], panePath);
|
|
108
|
+
const result3 = {
|
|
109
|
+
shell_command: commands(alias(pane, "commands", "cmd", panePath), panePath, "; ")
|
|
110
|
+
};
|
|
111
|
+
copy(pane, result3, ["focus", "description"]);
|
|
112
|
+
return result3;
|
|
113
|
+
});
|
|
114
|
+
focusFirst(panes);
|
|
115
|
+
result2.panes = panes;
|
|
116
|
+
return result2;
|
|
117
|
+
});
|
|
118
|
+
focusFirst(windows);
|
|
119
|
+
const result = {
|
|
120
|
+
session_name: session.name ?? fallbackName ?? null,
|
|
121
|
+
start_directory: root,
|
|
122
|
+
windows
|
|
123
|
+
};
|
|
124
|
+
copy(session, result, ["description"]);
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
function tmuxinator(source, context) {
|
|
128
|
+
fields(source, [
|
|
129
|
+
"name",
|
|
130
|
+
"project_name",
|
|
131
|
+
"root",
|
|
132
|
+
"project_root",
|
|
133
|
+
"windows",
|
|
134
|
+
"tabs",
|
|
135
|
+
"pre_window",
|
|
136
|
+
"description"
|
|
137
|
+
], "tmuxinator");
|
|
138
|
+
const root = directory(alias(source, "root", "project_root", "tmuxinator"), context.cwd, context);
|
|
139
|
+
const windows = sequence(alias(source, "windows", "tabs", "tmuxinator"), "windows").map((value, ordinal) => {
|
|
140
|
+
const path = `windows[${ordinal}]`;
|
|
141
|
+
const entries = Object.entries(mapping(value, path));
|
|
142
|
+
if (entries.length !== 1)
|
|
143
|
+
throw new Error(`${path} needs exactly one window name`);
|
|
144
|
+
const [name, definition] = entries[0];
|
|
145
|
+
const result2 = { window_name: name };
|
|
146
|
+
let panes;
|
|
147
|
+
if (typeof definition === "string" || definition === null || Array.isArray(definition)) {
|
|
148
|
+
panes = [{ shell_command: commands(definition, path) }];
|
|
149
|
+
} else {
|
|
150
|
+
const window = mapping(definition, path);
|
|
151
|
+
fields(window, ["root", "layout", "pre", "panes", "synchronize", "description"], path);
|
|
152
|
+
if (window.pre !== undefined && (!Array.isArray(window.panes) || window.panes.length === 0))
|
|
153
|
+
throw new Error(`${path}.pre requires explicit panes`);
|
|
154
|
+
copy(window, result2, ["layout", "description"]);
|
|
155
|
+
if (window.root !== undefined)
|
|
156
|
+
result2.start_directory = directory(window.root, root, context);
|
|
157
|
+
if (window.pre !== undefined)
|
|
158
|
+
result2.shell_command_before = commands(window.pre, `${path}.pre`, " && ");
|
|
159
|
+
if (window.synchronize !== undefined) {
|
|
160
|
+
const value2 = window.synchronize;
|
|
161
|
+
if (![true, false, "before", "after"].includes(value2))
|
|
162
|
+
throw new Error(`${path}.synchronize must be boolean, before, or after`);
|
|
163
|
+
result2[value2 === "after" ? "options_after" : "options"] = {
|
|
164
|
+
"synchronize-panes": value2 !== false
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
panes = sequence(window.panes ?? [null], `${path}.panes`).map((pane, index) => ({
|
|
168
|
+
shell_command: commands(pane, `${path}.panes[${index}]`)
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
focusFirst(panes);
|
|
172
|
+
result2.panes = panes;
|
|
173
|
+
return result2;
|
|
174
|
+
});
|
|
175
|
+
focusFirst(windows);
|
|
176
|
+
const result = {
|
|
177
|
+
session_name: alias(source, "name", "project_name", "tmuxinator") ?? null,
|
|
178
|
+
start_directory: root,
|
|
179
|
+
windows
|
|
180
|
+
};
|
|
181
|
+
if (source.pre_window !== undefined)
|
|
182
|
+
result.shell_command_before = commands(source.pre_window, "pre_window", "; ");
|
|
183
|
+
copy(source, result, ["description"]);
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
186
|
+
function importDocument(kind, source, context, fallbackName) {
|
|
187
|
+
if (kind === "tmuxinator" && JSON.stringify(source).includes("<%"))
|
|
188
|
+
throw new Error("tmuxinator ERB templates are unsupported; expand them before import");
|
|
189
|
+
const result = kind === "teamocil" ? teamocil(source, context, fallbackName) : tmuxinator(source, context);
|
|
190
|
+
normalize(result, join(context.cwd, "imported.json"), context);
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/search.ts
|
|
195
|
+
var aliases = {
|
|
196
|
+
name: "name",
|
|
197
|
+
n: "name",
|
|
198
|
+
session: "session_name",
|
|
199
|
+
session_name: "session_name",
|
|
200
|
+
s: "session_name",
|
|
201
|
+
path: "path",
|
|
202
|
+
p: "path",
|
|
203
|
+
window: "window",
|
|
204
|
+
w: "window",
|
|
205
|
+
pane: "pane"
|
|
206
|
+
};
|
|
207
|
+
var defaultFields = ["name", "session_name", "path", "window", "pane"];
|
|
208
|
+
function fields2(selected) {
|
|
209
|
+
if (!selected)
|
|
210
|
+
return defaultFields;
|
|
211
|
+
return [
|
|
212
|
+
...new Set(selected.map((item) => {
|
|
213
|
+
const key = item.toLowerCase();
|
|
214
|
+
if (!Object.hasOwn(aliases, key))
|
|
215
|
+
throw new CliError("usage", `Unknown search field: ${item}`, 2);
|
|
216
|
+
return aliases[key];
|
|
217
|
+
}))
|
|
218
|
+
];
|
|
219
|
+
}
|
|
220
|
+
function extract(record) {
|
|
221
|
+
const result = {
|
|
222
|
+
name: [record.name],
|
|
223
|
+
path: [record.path],
|
|
224
|
+
session_name: [pythonString(record.session_name ?? "")],
|
|
225
|
+
window: [],
|
|
226
|
+
pane: []
|
|
227
|
+
};
|
|
228
|
+
const windows = record.config?.windows;
|
|
229
|
+
if (!Array.isArray(windows))
|
|
230
|
+
return result;
|
|
231
|
+
for (const window of windows) {
|
|
232
|
+
if (!window || typeof window !== "object" || Array.isArray(window))
|
|
233
|
+
continue;
|
|
234
|
+
if (window.window_name)
|
|
235
|
+
result.window.push(pythonString(window.window_name));
|
|
236
|
+
if (Array.isArray(window.panes))
|
|
237
|
+
for (const pane of window.panes) {
|
|
238
|
+
if (typeof pane === "string")
|
|
239
|
+
result.pane.push(pane);
|
|
240
|
+
else if (pane && typeof pane === "object" && !Array.isArray(pane)) {
|
|
241
|
+
const commands2 = pane.shell_command;
|
|
242
|
+
if (typeof commands2 === "string")
|
|
243
|
+
result.pane.push(commands2);
|
|
244
|
+
else if (Array.isArray(commands2))
|
|
245
|
+
result.pane.push(...commands2.filter(Boolean).map(pythonString));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
function pythonString(value) {
|
|
252
|
+
if (value === null)
|
|
253
|
+
return "None";
|
|
254
|
+
if (value === true)
|
|
255
|
+
return "True";
|
|
256
|
+
if (value === false)
|
|
257
|
+
return "False";
|
|
258
|
+
if (Array.isArray(value))
|
|
259
|
+
return `[${value.map(pythonString).join(", ")}]`;
|
|
260
|
+
if (typeof value === "object")
|
|
261
|
+
return `{${Object.entries(value).map(([key, value2]) => `'${key}': ${typeof value2 === "string" ? `'${value2}'` : pythonString(value2)}`).join(", ")}}`;
|
|
262
|
+
return String(value);
|
|
263
|
+
}
|
|
264
|
+
async function search(values, context) {
|
|
265
|
+
const selected = fields2(values.field);
|
|
266
|
+
const queries = (values.query_terms ?? []).filter(Boolean).map((term) => {
|
|
267
|
+
const colon = term.indexOf(":");
|
|
268
|
+
const prefix = colon > 0 ? term.slice(0, colon).toLowerCase() : undefined;
|
|
269
|
+
const prefixed = prefix !== undefined && Object.hasOwn(aliases, prefix) ? aliases[prefix] : undefined;
|
|
270
|
+
const pattern = prefixed ? term.slice(colon + 1) : term;
|
|
271
|
+
return { fields: prefixed ? [prefixed] : selected, pattern };
|
|
272
|
+
}).filter((term) => term.pattern);
|
|
273
|
+
if (!queries.length)
|
|
274
|
+
throw new CliError("usage", "Provide at least one search pattern", 2);
|
|
275
|
+
const patterns = queries.map((query) => {
|
|
276
|
+
let expression = values.fixed_strings ? query.pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") : query.pattern;
|
|
277
|
+
if (values.word_regexp)
|
|
278
|
+
expression = `\\b(?:${expression})\\b`;
|
|
279
|
+
try {
|
|
280
|
+
return {
|
|
281
|
+
fields: query.fields,
|
|
282
|
+
regex: new RegExp(expression, values.ignore_case || values.smart_case && !/\p{Lu}/u.test(query.pattern) ? "iu" : "u")
|
|
283
|
+
};
|
|
284
|
+
} catch (error) {
|
|
285
|
+
throw new CliError("usage", `Invalid search pattern: ${error.message}`, 2);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
const { workspaces } = await discover(context, true);
|
|
289
|
+
const results = [];
|
|
290
|
+
for (const record of workspaces) {
|
|
291
|
+
const extracted = extract(record);
|
|
292
|
+
const matches = {};
|
|
293
|
+
const outcomes = patterns.map((pattern) => {
|
|
294
|
+
let found = false;
|
|
295
|
+
for (const field of pattern.fields)
|
|
296
|
+
for (const value of extracted[field] ?? []) {
|
|
297
|
+
const match = pattern.regex.exec(value);
|
|
298
|
+
if (match) {
|
|
299
|
+
found = true;
|
|
300
|
+
(matches[field] ??= []).push(match[0]);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return found;
|
|
304
|
+
});
|
|
305
|
+
const matched = values.match_any ? outcomes.some(Boolean) : outcomes.every(Boolean);
|
|
306
|
+
if (values.invert_match ? matched : !matched)
|
|
307
|
+
continue;
|
|
308
|
+
results.push({
|
|
309
|
+
name: record.name,
|
|
310
|
+
path: record.path,
|
|
311
|
+
session_name: record.session_name ?? "",
|
|
312
|
+
source: record.source,
|
|
313
|
+
matched_fields: Object.keys(matches),
|
|
314
|
+
matches
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
return results;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/commands.ts
|
|
321
|
+
import { platform, release } from "node:os";
|
|
322
|
+
function tty(context) {
|
|
323
|
+
return Boolean(context.stdin.isTTY);
|
|
324
|
+
}
|
|
325
|
+
async function edit(request, context) {
|
|
326
|
+
const file = await resolveWorkspace(scalarText(request.values.workspace_file), context);
|
|
327
|
+
const argv = [...tokenize(context.env.EDITOR || "vi"), file];
|
|
328
|
+
const child = await processRun(argv, {
|
|
329
|
+
cwd: context.cwd,
|
|
330
|
+
env: context.env,
|
|
331
|
+
terminal: tty(context),
|
|
332
|
+
...context.signal ? { signal: context.signal } : {},
|
|
333
|
+
...request.mode === "human" ? {
|
|
334
|
+
output: async (stream, text) => {
|
|
335
|
+
await write(context[stream], text, context.signal);
|
|
336
|
+
}
|
|
337
|
+
} : {}
|
|
338
|
+
});
|
|
339
|
+
if (request.mode !== "human")
|
|
340
|
+
await emitJson(context.stdout, {
|
|
341
|
+
schema_version: 1,
|
|
342
|
+
command: "edit",
|
|
343
|
+
status: child.code ? "error" : "ok",
|
|
344
|
+
file: privatePath(file, context),
|
|
345
|
+
child_status: child.code,
|
|
346
|
+
stdout: child.stdout,
|
|
347
|
+
stderr: child.stderr,
|
|
348
|
+
truncated: child.truncated,
|
|
349
|
+
encoding: "utf-8-with-replacement"
|
|
350
|
+
}, false, context.signal);
|
|
351
|
+
return child.code;
|
|
352
|
+
}
|
|
353
|
+
async function debugInfo(request, context) {
|
|
354
|
+
let tmux;
|
|
355
|
+
try {
|
|
356
|
+
const result = await processRun([context.env.TMUX_BIN || "tmux", "-V"], {
|
|
357
|
+
cwd: context.cwd,
|
|
358
|
+
env: context.env,
|
|
359
|
+
...context.signal ? { signal: context.signal } : {}
|
|
360
|
+
});
|
|
361
|
+
tmux = result.code ? { version: null, error: result.stderr.trim() } : { version: result.stdout.trim() };
|
|
362
|
+
} catch (error) {
|
|
363
|
+
tmux = { version: null, error: String(error) };
|
|
364
|
+
}
|
|
365
|
+
const document = {
|
|
366
|
+
port: "typescript",
|
|
367
|
+
version: package_default.version,
|
|
368
|
+
platform: platform(),
|
|
369
|
+
release: release(),
|
|
370
|
+
cwd: privatePath(context.cwd, context),
|
|
371
|
+
runtime: {
|
|
372
|
+
name: process.versions.bun ? "Bun" : "Node.js",
|
|
373
|
+
version: process.versions.bun ?? process.versions.node
|
|
374
|
+
},
|
|
375
|
+
tmux,
|
|
376
|
+
paths: Object.fromEntries(["TMUXP_CONFIGDIR", "XDG_CONFIG_HOME", "SHELL", "TMUX_BIN", "TMUX_WORKSPACE_PYTHON"].flatMap((key) => context.env[key] ? [[key, privatePath(context.env[key], context)]] : []))
|
|
377
|
+
};
|
|
378
|
+
if (request.mode !== "human")
|
|
379
|
+
await emitJson(context.stdout, document, request.mode === "json", context.signal);
|
|
380
|
+
else {
|
|
381
|
+
const color = colorEnabled(request.mode, request.values.color, context.env, Boolean(context.stdout.isTTY));
|
|
382
|
+
await write(context.stdout, styled("heading", "Workspace diagnostics", color) + `
|
|
383
|
+
`, context.signal);
|
|
384
|
+
for (const [label, value] of Object.entries(document))
|
|
385
|
+
await write(context.stdout, `${styled("subject", label, color)}: ${styled("info", typeof value === "string" ? value : JSON.stringify(value), color)}
|
|
386
|
+
`, context.signal);
|
|
387
|
+
}
|
|
388
|
+
return 0;
|
|
389
|
+
}
|
|
390
|
+
async function shell(request, context) {
|
|
391
|
+
const interactive = request.values.command === null || request.values.command === undefined;
|
|
392
|
+
if (interactive && !tty(context))
|
|
393
|
+
throw new CliError("usage", "An interactive Python shell needs a terminal; use -c for captured output", 2);
|
|
394
|
+
const python = context.env.TMUX_WORKSPACE_PYTHON || "python3";
|
|
395
|
+
const signal = context.signal ? AbortSignal.any([context.signal, AbortSignal.timeout(1e4)]) : AbortSignal.timeout(1e4);
|
|
396
|
+
try {
|
|
397
|
+
const check = await processRun([python, "-c", "import importlib.metadata; print(importlib.metadata.version('tmuxp'))"], { cwd: context.cwd, env: context.env, signal });
|
|
398
|
+
if (check.code || check.stdout.trim() !== "1.74.0")
|
|
399
|
+
throw new Error("Requires tmuxp 1.74.0");
|
|
400
|
+
} catch (error) {
|
|
401
|
+
if (context.signal?.aborted)
|
|
402
|
+
throw error;
|
|
403
|
+
throw new CliError("script_failed", "The Python bridge requires tmuxp 1.74.0; set TMUX_WORKSPACE_PYTHON to its interpreter");
|
|
404
|
+
}
|
|
405
|
+
const args = [
|
|
406
|
+
python,
|
|
407
|
+
"-u",
|
|
408
|
+
"-c",
|
|
409
|
+
"from tmuxp.cli import cli; cli()",
|
|
410
|
+
"--color",
|
|
411
|
+
"never",
|
|
412
|
+
"shell"
|
|
413
|
+
];
|
|
414
|
+
for (const [name, flag] of [
|
|
415
|
+
["socket_path", "-S"],
|
|
416
|
+
["socket_name", "-L"],
|
|
417
|
+
["command", "-c"]
|
|
418
|
+
]) {
|
|
419
|
+
const value = request.values[name];
|
|
420
|
+
if (value !== null && value !== undefined)
|
|
421
|
+
args.push(flag, scalarText(value));
|
|
422
|
+
}
|
|
423
|
+
if (request.values.shell)
|
|
424
|
+
args.push(`--${scalarText(request.values.shell)}`);
|
|
425
|
+
args.push(request.values.use_pythonrc ? "--use-pythonrc" : "--no-startup", request.values.use_vi_mode ? "--use-vi-mode" : "--no-vi-mode");
|
|
426
|
+
for (const key of ["session_name", "window_name"])
|
|
427
|
+
if (request.values[key])
|
|
428
|
+
args.push(scalarText(request.values[key]));
|
|
429
|
+
const output = new OperationOutput("shell", request.mode, context.stdout, context.signal);
|
|
430
|
+
const child = await processRun(args, {
|
|
431
|
+
cwd: context.cwd,
|
|
432
|
+
env: context.env,
|
|
433
|
+
terminal: interactive,
|
|
434
|
+
...context.signal ? { signal: context.signal } : {},
|
|
435
|
+
output: async (stream, text) => {
|
|
436
|
+
if (request.mode === "human")
|
|
437
|
+
await write(context[stream], text, context.signal);
|
|
438
|
+
else
|
|
439
|
+
await output.event("script-output", { stream, text, encoding: "utf-8-with-replacement" });
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
await output.result({
|
|
443
|
+
status: child.code ? "error" : "ok",
|
|
444
|
+
child_status: child.code,
|
|
445
|
+
stdout: child.stdout,
|
|
446
|
+
stderr: child.stderr,
|
|
447
|
+
truncated: child.truncated,
|
|
448
|
+
encoding: "utf-8-with-replacement"
|
|
449
|
+
});
|
|
450
|
+
return child.code;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// src/diagnostics.ts
|
|
454
|
+
import { constants } from "node:fs";
|
|
455
|
+
import { open } from "node:fs/promises";
|
|
456
|
+
import { resolve as resolve2 } from "node:path";
|
|
457
|
+
var levels = { debug: 10, info: 20, warning: 30, error: 40, critical: 50 };
|
|
458
|
+
|
|
459
|
+
class Diagnostics {
|
|
460
|
+
pending = Promise.resolve();
|
|
461
|
+
failed = false;
|
|
462
|
+
level;
|
|
463
|
+
mode;
|
|
464
|
+
stderr;
|
|
465
|
+
color;
|
|
466
|
+
file;
|
|
467
|
+
signal;
|
|
468
|
+
constructor(level, mode, stderr, color, file, signal) {
|
|
469
|
+
this.level = level;
|
|
470
|
+
this.mode = mode;
|
|
471
|
+
this.stderr = stderr;
|
|
472
|
+
this.color = color;
|
|
473
|
+
this.file = file;
|
|
474
|
+
this.signal = signal;
|
|
475
|
+
}
|
|
476
|
+
static async open(values, mode, context) {
|
|
477
|
+
context.signal?.throwIfAborted();
|
|
478
|
+
let file;
|
|
479
|
+
if (values.log_file) {
|
|
480
|
+
const path = resolve2(context.cwd, expandPath(scalarText(values.log_file), context));
|
|
481
|
+
file = await open(path, constants.O_WRONLY | constants.O_CREAT | constants.O_APPEND | constants.O_NONBLOCK, 384);
|
|
482
|
+
try {
|
|
483
|
+
if (!(await file.stat()).isFile())
|
|
484
|
+
throw new CliError("usage", "Log destination must be a regular file", 2);
|
|
485
|
+
} catch (error) {
|
|
486
|
+
await file.close();
|
|
487
|
+
throw error;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return new Diagnostics(values.log_level ?? "warning", mode, context.stderr, colorEnabled(mode, values.color, context.env, Boolean(context.stderr.isTTY)), file, context.signal);
|
|
491
|
+
}
|
|
492
|
+
accepts(level) {
|
|
493
|
+
return !this.failed && levels[level] >= levels[this.level];
|
|
494
|
+
}
|
|
495
|
+
async record(level, event, data = {}, echo = true) {
|
|
496
|
+
if (!this.accepts(level))
|
|
497
|
+
return;
|
|
498
|
+
const record = { ...data, schema_version: 1, level, event, time: new Date().toISOString() };
|
|
499
|
+
this.pending = this.pending.then(async () => {
|
|
500
|
+
this.signal?.throwIfAborted();
|
|
501
|
+
if (this.file)
|
|
502
|
+
await this.file.appendFile(JSON.stringify(record) + `
|
|
503
|
+
`);
|
|
504
|
+
if (!echo)
|
|
505
|
+
return;
|
|
506
|
+
if (this.mode !== "human")
|
|
507
|
+
await write(this.stderr, JSON.stringify(record) + `
|
|
508
|
+
`, this.signal);
|
|
509
|
+
else {
|
|
510
|
+
const role = level === "error" || level === "critical" ? "error" : level === "warning" ? "warning" : "info";
|
|
511
|
+
await write(this.stderr, `${styled(role, level, this.color)}: ${styled("subject", event, this.color)} ${styled("secondary", JSON.stringify(data), this.color)}
|
|
512
|
+
`, this.signal);
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
try {
|
|
516
|
+
await this.pending;
|
|
517
|
+
} catch (error) {
|
|
518
|
+
if (this.failed)
|
|
519
|
+
return;
|
|
520
|
+
this.failed = true;
|
|
521
|
+
this.signal?.throwIfAborted();
|
|
522
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
523
|
+
try {
|
|
524
|
+
await write(this.stderr, this.mode === "human" ? `tmux-workspace: logging stopped: ${message}
|
|
525
|
+
` : JSON.stringify({ schema_version: 1, code: "log_error", message }) + `
|
|
526
|
+
`, this.signal);
|
|
527
|
+
} catch {
|
|
528
|
+
this.signal?.throwIfAborted();
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
async close() {
|
|
533
|
+
await this.pending.catch(() => {});
|
|
534
|
+
await this.file?.close();
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// src/app.ts
|
|
539
|
+
async function run(argv, context) {
|
|
540
|
+
let diagnostics;
|
|
541
|
+
let help = "";
|
|
542
|
+
let parserError = "";
|
|
543
|
+
const parser = createParser({
|
|
544
|
+
stdout: (text) => {
|
|
545
|
+
help += text;
|
|
546
|
+
},
|
|
547
|
+
stderr: (text) => {
|
|
548
|
+
parserError += text;
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
const positional = argv.indexOf("--");
|
|
552
|
+
const options = positional < 0 ? argv : argv.slice(0, positional);
|
|
553
|
+
let mode = options.includes("--ndjson") ? "ndjson" : options.includes("--json") ? "json" : "human";
|
|
554
|
+
const report = async (code, message) => {
|
|
555
|
+
const text = mode === "human" ? `tmux-workspace: ${message}
|
|
556
|
+
` : JSON.stringify({ schema_version: 1, code, message }) + `
|
|
557
|
+
`;
|
|
558
|
+
await write(context.stderr, text, context.signal);
|
|
559
|
+
};
|
|
560
|
+
const reportLogFailure = async (error) => {
|
|
561
|
+
try {
|
|
562
|
+
await report("log_error", error instanceof Error ? error.message : String(error));
|
|
563
|
+
} catch {}
|
|
564
|
+
};
|
|
565
|
+
try {
|
|
566
|
+
try {
|
|
567
|
+
parser.command.parse(argv, { from: "user" });
|
|
568
|
+
} catch (error) {
|
|
569
|
+
if (error instanceof CommanderError && error.exitCode === 0) {
|
|
570
|
+
await write(context.stdout, help, context.signal);
|
|
571
|
+
return 0;
|
|
572
|
+
}
|
|
573
|
+
const first = parser.command.args[0];
|
|
574
|
+
const topLevel = new Set([...parser.commands.keys()].filter((name) => name && !name.includes(" ")));
|
|
575
|
+
if (error instanceof CommanderError && error.code === "commander.excessArguments" && first !== undefined && !topLevel.has(first)) {
|
|
576
|
+
parserError = "";
|
|
577
|
+
throw new CommanderError(2, "commander.unknownCommand", `error: unknown command '${first}'`);
|
|
578
|
+
}
|
|
579
|
+
throw error;
|
|
580
|
+
}
|
|
581
|
+
const request = parser.request();
|
|
582
|
+
mode = request.mode;
|
|
583
|
+
diagnostics = await Diagnostics.open(request.values, request.mode, context);
|
|
584
|
+
context = { ...context, diagnostics };
|
|
585
|
+
context.signal?.throwIfAborted();
|
|
586
|
+
await diagnostics.record("debug", "command-started", { command: request.command });
|
|
587
|
+
const color = colorEnabled(request.mode, request.values.color, context.env, Boolean(context.stdout.isTTY));
|
|
588
|
+
const render = (role, value) => styled(role, value, color);
|
|
589
|
+
if (!request.command || request.command === "import") {
|
|
590
|
+
if (mode !== "human")
|
|
591
|
+
throw new CliError("usage", request.command ? "Choose import teamocil or import tmuxinator." : "Choose a workspace command.", 2);
|
|
592
|
+
await write(context.stdout, parser.commands.get(request.command).helpInformation(), context.signal);
|
|
593
|
+
return 0;
|
|
594
|
+
}
|
|
595
|
+
if (request.command === "load" || request.command === "freeze") {
|
|
596
|
+
const operation = (await import("./tmux-wa2zn8z1.js"))[request.command];
|
|
597
|
+
context.signal?.throwIfAborted();
|
|
598
|
+
return await operation(request, context);
|
|
599
|
+
}
|
|
600
|
+
if (request.command === "edit")
|
|
601
|
+
return await edit(request, context);
|
|
602
|
+
if (request.command === "debug-info")
|
|
603
|
+
return await debugInfo(request, context);
|
|
604
|
+
if (request.command === "shell")
|
|
605
|
+
return await shell(request, context);
|
|
606
|
+
if (request.command === "completion") {
|
|
607
|
+
const [{ commandCatalog }, { completionScript }] = await Promise.all([
|
|
608
|
+
import("./reference-ebfxh6yp.js"),
|
|
609
|
+
import("./completions-2a9fqwq3.js")
|
|
610
|
+
]);
|
|
611
|
+
const shell2 = request.values.shell;
|
|
612
|
+
const script = completionScript(commandCatalog(parser), shell2);
|
|
613
|
+
if (mode === "human")
|
|
614
|
+
await write(context.stdout, script, context.signal);
|
|
615
|
+
else
|
|
616
|
+
await emitJson(context.stdout, {
|
|
617
|
+
schema_version: 1,
|
|
618
|
+
command: "completion",
|
|
619
|
+
status: "ok",
|
|
620
|
+
shell: shell2,
|
|
621
|
+
script
|
|
622
|
+
}, mode === "json", context.signal);
|
|
623
|
+
return 0;
|
|
624
|
+
}
|
|
625
|
+
if (request.command === "ls") {
|
|
626
|
+
const result = await discover(context, Boolean(request.values.full));
|
|
627
|
+
if (mode === "json")
|
|
628
|
+
await emitJson(context.stdout, result, true, context.signal);
|
|
629
|
+
else if (mode === "ndjson")
|
|
630
|
+
for (const record of result.workspaces)
|
|
631
|
+
await emitJson(context.stdout, record, false, context.signal);
|
|
632
|
+
else {
|
|
633
|
+
let heading = "";
|
|
634
|
+
for (const record of result.workspaces) {
|
|
635
|
+
const group = request.values.tree ? dirname(record.path) : `${record.source === "local" ? "Local" : "Global"} workspaces`;
|
|
636
|
+
if (group !== heading) {
|
|
637
|
+
await write(context.stdout, render("heading", group) + `:
|
|
638
|
+
`, context.signal);
|
|
639
|
+
heading = group;
|
|
640
|
+
}
|
|
641
|
+
await write(context.stdout, ` ${render("subject", record.name)} ${render("info", record.path)}
|
|
642
|
+
`, context.signal);
|
|
643
|
+
if (request.values.full && record.config)
|
|
644
|
+
await write(context.stdout, JSON.stringify(record.config, null, 2) + `
|
|
645
|
+
`, context.signal);
|
|
646
|
+
}
|
|
647
|
+
if (!result.workspaces.length)
|
|
648
|
+
await write(context.stdout, render("warning", "No workspaces found.") + `
|
|
649
|
+
`, context.signal);
|
|
650
|
+
await write(context.stdout, `
|
|
651
|
+
` + render("heading", "Global workspace directories:") + `
|
|
652
|
+
`, context.signal);
|
|
653
|
+
for (const entry of result.global_workspace_dirs)
|
|
654
|
+
await write(context.stdout, ` ${render("secondary", entry.source)}: ${render("info", entry.path)} (${render(entry.active ? "success" : "secondary", entry.exists ? `${entry.workspace_count} workspaces${entry.active ? ", active" : ""}` : "not found")})
|
|
655
|
+
`, context.signal);
|
|
656
|
+
}
|
|
657
|
+
return 0;
|
|
658
|
+
}
|
|
659
|
+
if (request.command === "search") {
|
|
660
|
+
const results = await search(request.values, context);
|
|
661
|
+
if (mode === "json")
|
|
662
|
+
await emitJson(context.stdout, results, true, context.signal);
|
|
663
|
+
else if (mode === "ndjson")
|
|
664
|
+
for (const result of results)
|
|
665
|
+
await emitJson(context.stdout, result, false, context.signal);
|
|
666
|
+
else
|
|
667
|
+
for (const result of results)
|
|
668
|
+
await write(context.stdout, `${render("subject", result.name)} ${render("info", result.path)} ${render("secondary", result.matched_fields.join(", "))}
|
|
669
|
+
`, context.signal);
|
|
670
|
+
return 0;
|
|
671
|
+
}
|
|
672
|
+
if (request.command === "convert" || request.command.startsWith("import ")) {
|
|
673
|
+
const kind = request.command.split(" ")[1];
|
|
674
|
+
let importerRoot;
|
|
675
|
+
if (kind === "teamocil")
|
|
676
|
+
importerRoot = join2(home(context), ".teamocil");
|
|
677
|
+
else if (kind === "tmuxinator")
|
|
678
|
+
importerRoot = expandPath(context.env.TMUXINATOR_CONFIG ?? join2(home(context), ".tmuxinator"), context);
|
|
679
|
+
const source = await resolveWorkspace(scalarText(request.values.workspace_file), context, importerRoot);
|
|
680
|
+
let document;
|
|
681
|
+
try {
|
|
682
|
+
const raw = await readDocument(source);
|
|
683
|
+
document = kind ? importDocument(kind, raw, context, basename(source, extname(source))) : raw;
|
|
684
|
+
} catch (error) {
|
|
685
|
+
if (error instanceof CliError)
|
|
686
|
+
throw error;
|
|
687
|
+
throw new CliError("invalid_workspace", error instanceof Error ? error.message : String(error));
|
|
688
|
+
}
|
|
689
|
+
let destination = request.values.save_to ? resolve3(context.cwd, expandPath(scalarText(request.values.save_to), context)) : undefined;
|
|
690
|
+
const format = scalarText(request.values.workspace_format ?? (extname(source) === ".json" ? "yaml" : "json"));
|
|
691
|
+
if (!destination && mode === "human") {
|
|
692
|
+
if (!request.values.answer_yes)
|
|
693
|
+
throw new CliError("usage", "Confirm conversion with --yes or provide --save-to", 2);
|
|
694
|
+
destination = join2(dirname(source), basename(source, extname(source)) + (format === "json" ? ".json" : ".yaml"));
|
|
695
|
+
}
|
|
696
|
+
if (destination) {
|
|
697
|
+
await saveDocument(document, destination, format, Boolean(request.values.force), context.signal);
|
|
698
|
+
const result = {
|
|
699
|
+
schema_version: 1,
|
|
700
|
+
command: request.command,
|
|
701
|
+
status: "ok",
|
|
702
|
+
destination: privatePath(destination, context),
|
|
703
|
+
format
|
|
704
|
+
};
|
|
705
|
+
if (mode === "human")
|
|
706
|
+
await write(context.stdout, `${render("success", "Saved")} ${render("info", destination)}
|
|
707
|
+
`, context.signal);
|
|
708
|
+
else
|
|
709
|
+
await emitJson(context.stdout, result, false, context.signal);
|
|
710
|
+
} else if (mode === "json")
|
|
711
|
+
await emitJson(context.stdout, document, false, context.signal);
|
|
712
|
+
else
|
|
713
|
+
await emitJson(context.stdout, {
|
|
714
|
+
schema_version: 1,
|
|
715
|
+
command: request.command,
|
|
716
|
+
status: "ok",
|
|
717
|
+
workspace: document
|
|
718
|
+
}, false, context.signal);
|
|
719
|
+
return 0;
|
|
720
|
+
}
|
|
721
|
+
throw new Error(`${request.command} has no handler`);
|
|
722
|
+
} catch (error) {
|
|
723
|
+
if (context.signal?.aborted)
|
|
724
|
+
return 130;
|
|
725
|
+
const usage = error instanceof CommanderError;
|
|
726
|
+
const code = usage ? "usage" : error instanceof CliError ? error.code : "tmux_failed";
|
|
727
|
+
const message = usage ? parserError.trim() || error.message : error instanceof Error ? error.message : String(error);
|
|
728
|
+
try {
|
|
729
|
+
await report(code, message);
|
|
730
|
+
} catch {}
|
|
731
|
+
try {
|
|
732
|
+
await diagnostics?.record("error", "command-failed", { code, message }, false);
|
|
733
|
+
} catch (logError) {
|
|
734
|
+
await reportLogFailure(logError);
|
|
735
|
+
}
|
|
736
|
+
return context.signal?.aborted ? 130 : usage ? 2 : error instanceof CliError ? error.exitCode : 1;
|
|
737
|
+
} finally {
|
|
738
|
+
try {
|
|
739
|
+
await diagnostics?.close();
|
|
740
|
+
} catch (error) {
|
|
741
|
+
await reportLogFailure(error);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
export {
|
|
746
|
+
run
|
|
747
|
+
};
|