@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,369 @@
|
|
|
1
|
+
// src/output.ts
|
|
2
|
+
class CliError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
exitCode;
|
|
5
|
+
constructor(code, message, exitCode = 1) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.exitCode = exitCode;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
class OutputClosedError extends Error {
|
|
13
|
+
}
|
|
14
|
+
async function write(stream, text, signal) {
|
|
15
|
+
signal?.throwIfAborted();
|
|
16
|
+
if (stream.destroyed || stream.writableEnded)
|
|
17
|
+
throw stream.errored ?? new OutputClosedError("Output stream closed");
|
|
18
|
+
await new Promise((resolve, reject) => {
|
|
19
|
+
let settled = false;
|
|
20
|
+
const finish = (error) => {
|
|
21
|
+
stream.off("close", closed);
|
|
22
|
+
signal?.removeEventListener("abort", aborted);
|
|
23
|
+
if (error) {
|
|
24
|
+
setImmediate(() => {
|
|
25
|
+
stream.off("error", failed);
|
|
26
|
+
});
|
|
27
|
+
} else {
|
|
28
|
+
stream.off("error", failed);
|
|
29
|
+
}
|
|
30
|
+
if (settled)
|
|
31
|
+
return;
|
|
32
|
+
settled = true;
|
|
33
|
+
if (error)
|
|
34
|
+
reject(error);
|
|
35
|
+
else
|
|
36
|
+
resolve();
|
|
37
|
+
};
|
|
38
|
+
const failed = (error) => {
|
|
39
|
+
finish(error);
|
|
40
|
+
};
|
|
41
|
+
const closed = () => {
|
|
42
|
+
finish(new OutputClosedError("Output stream closed before the write completed"));
|
|
43
|
+
};
|
|
44
|
+
const aborted = () => {
|
|
45
|
+
if (settled)
|
|
46
|
+
return;
|
|
47
|
+
settled = true;
|
|
48
|
+
signal?.removeEventListener("abort", aborted);
|
|
49
|
+
reject(signal?.reason);
|
|
50
|
+
};
|
|
51
|
+
stream.once("error", failed);
|
|
52
|
+
stream.once("close", closed);
|
|
53
|
+
signal?.addEventListener("abort", aborted, { once: true });
|
|
54
|
+
try {
|
|
55
|
+
stream.write(text, (error) => {
|
|
56
|
+
finish(error);
|
|
57
|
+
});
|
|
58
|
+
} catch (error) {
|
|
59
|
+
finish(error);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function colorEnabled(mode, choice, env, tty) {
|
|
64
|
+
if (mode !== "human" || env.NO_COLOR || choice === "never")
|
|
65
|
+
return false;
|
|
66
|
+
if (choice === "always")
|
|
67
|
+
return true;
|
|
68
|
+
if (env.FORCE_COLOR === "0")
|
|
69
|
+
return false;
|
|
70
|
+
if (env.FORCE_COLOR)
|
|
71
|
+
return true;
|
|
72
|
+
if (env.CLICOLOR_FORCE && env.CLICOLOR_FORCE !== "0")
|
|
73
|
+
return true;
|
|
74
|
+
return env.CLICOLOR === "0" ? false : tty;
|
|
75
|
+
}
|
|
76
|
+
var styles = {
|
|
77
|
+
heading: "1;36",
|
|
78
|
+
subject: "1;35",
|
|
79
|
+
info: "36",
|
|
80
|
+
success: "32",
|
|
81
|
+
warning: "33",
|
|
82
|
+
error: "31",
|
|
83
|
+
secondary: "2"
|
|
84
|
+
};
|
|
85
|
+
function styled(role, value, color) {
|
|
86
|
+
const text = String(value).replace(/[\u0000-\u001f\u007f]/g, (character) => JSON.stringify(character).slice(1, -1));
|
|
87
|
+
return color ? `\x1B[${styles[role]}m${text}\x1B[0m` : text;
|
|
88
|
+
}
|
|
89
|
+
async function emitJson(stream, value, pretty = false, signal) {
|
|
90
|
+
await write(stream, JSON.stringify(value, null, pretty ? 2 : undefined) + `
|
|
91
|
+
`, signal);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
class OperationOutput {
|
|
95
|
+
sequence = 0;
|
|
96
|
+
finished = false;
|
|
97
|
+
command;
|
|
98
|
+
mode;
|
|
99
|
+
stdout;
|
|
100
|
+
signal;
|
|
101
|
+
get isFinished() {
|
|
102
|
+
return this.finished;
|
|
103
|
+
}
|
|
104
|
+
observe;
|
|
105
|
+
constructor(command, mode, stdout, signal, observe) {
|
|
106
|
+
this.command = command;
|
|
107
|
+
this.mode = mode;
|
|
108
|
+
this.stdout = stdout;
|
|
109
|
+
this.signal = signal;
|
|
110
|
+
if (observe !== undefined)
|
|
111
|
+
this.observe = observe;
|
|
112
|
+
}
|
|
113
|
+
async event(event, data = {}) {
|
|
114
|
+
if (this.finished)
|
|
115
|
+
throw new Error("Cannot emit after the terminal result");
|
|
116
|
+
const terminal = ["completed", "failed"].includes(event);
|
|
117
|
+
if (!terminal)
|
|
118
|
+
this.signal?.throwIfAborted();
|
|
119
|
+
const handled = !this.signal?.aborted && await this.observe?.(event, data);
|
|
120
|
+
if (terminal)
|
|
121
|
+
this.finished = true;
|
|
122
|
+
if (this.mode === "ndjson")
|
|
123
|
+
await emitJson(this.stdout, {
|
|
124
|
+
schema_version: 1,
|
|
125
|
+
command: this.command,
|
|
126
|
+
event,
|
|
127
|
+
sequence: ++this.sequence,
|
|
128
|
+
...data
|
|
129
|
+
}, false, this.completionSignal(terminal));
|
|
130
|
+
return handled === true;
|
|
131
|
+
}
|
|
132
|
+
async result(data) {
|
|
133
|
+
const event = data.status === "error" || data.status === "partial" ? "failed" : "completed";
|
|
134
|
+
if (this.mode === "ndjson") {
|
|
135
|
+
await this.event(event, data);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (this.finished)
|
|
139
|
+
throw new Error("Cannot emit after the terminal result");
|
|
140
|
+
if (!this.signal?.aborted)
|
|
141
|
+
await this.observe?.(event, data);
|
|
142
|
+
this.finished = true;
|
|
143
|
+
if (this.mode === "json")
|
|
144
|
+
await emitJson(this.stdout, { schema_version: 1, command: this.command, ...data }, false, this.completionSignal(true));
|
|
145
|
+
}
|
|
146
|
+
completionSignal(terminal) {
|
|
147
|
+
return terminal && this.signal?.aborted ? AbortSignal.timeout(100) : this.signal;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/documents.ts
|
|
152
|
+
import { randomUUID } from "node:crypto";
|
|
153
|
+
import { link, open, readFile, readdir, rename, stat, unlink } from "node:fs/promises";
|
|
154
|
+
import { homedir } from "node:os";
|
|
155
|
+
import { basename, dirname, extname, isAbsolute, join, resolve, sep } from "node:path";
|
|
156
|
+
var extensions = [".yaml", ".yml", ".json"];
|
|
157
|
+
function home(context) {
|
|
158
|
+
return context.env.HOME ?? context.env.USERPROFILE ?? homedir();
|
|
159
|
+
}
|
|
160
|
+
function expandPath(path, context) {
|
|
161
|
+
return path === "~" ? home(context) : path.startsWith("~/") ? join(home(context), path.slice(2)) : path;
|
|
162
|
+
}
|
|
163
|
+
function privatePath(path, context) {
|
|
164
|
+
const base = home(context);
|
|
165
|
+
return path === base ? "~" : path.startsWith(base + sep) ? "~" + path.slice(base.length) : path;
|
|
166
|
+
}
|
|
167
|
+
function mapping(value, label = "workspace") {
|
|
168
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
169
|
+
throw new Error(`${label} must be a mapping`);
|
|
170
|
+
return value;
|
|
171
|
+
}
|
|
172
|
+
function scalarText(value) {
|
|
173
|
+
if (typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value))
|
|
174
|
+
return String(value);
|
|
175
|
+
throw new Error("Expected a string, boolean, or finite number");
|
|
176
|
+
}
|
|
177
|
+
function sequence(value, label) {
|
|
178
|
+
if (!Array.isArray(value))
|
|
179
|
+
throw new Error(`${label} must be a list`);
|
|
180
|
+
return value;
|
|
181
|
+
}
|
|
182
|
+
function ambiguousScalar(text, yamlModule) {
|
|
183
|
+
if (text.includes(`
|
|
184
|
+
`))
|
|
185
|
+
return false;
|
|
186
|
+
if (text === "")
|
|
187
|
+
return true;
|
|
188
|
+
const resolves = (options) => {
|
|
189
|
+
try {
|
|
190
|
+
return yamlModule.parse(text, options) !== text;
|
|
191
|
+
} catch {
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
return resolves() || resolves({ version: "1.1" });
|
|
196
|
+
}
|
|
197
|
+
function stringifyYaml(document, yamlModule) {
|
|
198
|
+
const doc = new yamlModule.Document(document);
|
|
199
|
+
yamlModule.visit(doc, {
|
|
200
|
+
Scalar(_key, node) {
|
|
201
|
+
if (typeof node.value === "string" && ambiguousScalar(node.value, yamlModule))
|
|
202
|
+
node.type = yamlModule.Scalar.QUOTE_DOUBLE;
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
return doc.toString();
|
|
206
|
+
}
|
|
207
|
+
async function info(path) {
|
|
208
|
+
try {
|
|
209
|
+
return await stat(path);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (["ENOENT", "ENOTDIR"].includes(error.code ?? ""))
|
|
212
|
+
return;
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function jsonData(value, active = new Set) {
|
|
217
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
218
|
+
return;
|
|
219
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
220
|
+
return;
|
|
221
|
+
if (typeof value !== "object" || active.has(value) || value instanceof Date)
|
|
222
|
+
throw new Error("Workspace must contain finite, acyclic JSON values");
|
|
223
|
+
active.add(value);
|
|
224
|
+
for (const entry of Object.values(value))
|
|
225
|
+
jsonData(entry, active);
|
|
226
|
+
active.delete(value);
|
|
227
|
+
}
|
|
228
|
+
async function readDocument(path) {
|
|
229
|
+
const source = await readFile(path, "utf8");
|
|
230
|
+
const extension = extname(path).toLowerCase();
|
|
231
|
+
if (!extensions.includes(extension))
|
|
232
|
+
throw new Error(`Unsupported workspace extension: ${extension}`);
|
|
233
|
+
const value = extension === ".json" ? JSON.parse(source) : (await import("./index-0j386v40.js")).parse(source, { merge: true, maxAliasCount: 100 });
|
|
234
|
+
jsonData(value);
|
|
235
|
+
return mapping(value);
|
|
236
|
+
}
|
|
237
|
+
async function saveDocument(document, path, format, force, signal) {
|
|
238
|
+
signal?.throwIfAborted();
|
|
239
|
+
jsonData(document);
|
|
240
|
+
if (!["json", "yaml"].includes(format))
|
|
241
|
+
throw new Error(`Unsupported workspace format: ${format}`);
|
|
242
|
+
const contents = format === "json" ? JSON.stringify(document, null, 2) + `
|
|
243
|
+
` : stringifyYaml(document, await import("./index-0j386v40.js"));
|
|
244
|
+
signal?.throwIfAborted();
|
|
245
|
+
const temporary = join(dirname(path), `.${basename(path)}.${randomUUID()}.tmp`);
|
|
246
|
+
try {
|
|
247
|
+
const handle = await open(temporary, "wx", 384);
|
|
248
|
+
try {
|
|
249
|
+
await handle.writeFile(contents, { signal });
|
|
250
|
+
await handle.sync();
|
|
251
|
+
} finally {
|
|
252
|
+
await handle.close();
|
|
253
|
+
}
|
|
254
|
+
signal?.throwIfAborted();
|
|
255
|
+
if (force)
|
|
256
|
+
await rename(temporary, path);
|
|
257
|
+
else {
|
|
258
|
+
try {
|
|
259
|
+
await link(temporary, path);
|
|
260
|
+
} catch (error) {
|
|
261
|
+
if (error.code === "EEXIST")
|
|
262
|
+
throw new CliError("destination_exists", `Destination exists: ${path}; use --force to replace it`);
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
} finally {
|
|
267
|
+
await unlink(temporary).catch((error) => {
|
|
268
|
+
if (error.code !== "ENOENT")
|
|
269
|
+
throw error;
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
async function directories(context) {
|
|
274
|
+
const entries = [];
|
|
275
|
+
const candidates = [];
|
|
276
|
+
if (context.env.TMUXP_CONFIGDIR !== undefined)
|
|
277
|
+
candidates.push([context.env.TMUXP_CONFIGDIR, "$TMUXP_CONFIGDIR"]);
|
|
278
|
+
candidates.push(context.env.XDG_CONFIG_HOME !== undefined ? [join(context.env.XDG_CONFIG_HOME, "tmuxp"), "$XDG_CONFIG_HOME/tmuxp"] : ["~/.config/tmuxp", "XDG default"]);
|
|
279
|
+
candidates.push(["~/.tmuxp", "Legacy"]);
|
|
280
|
+
for (const [path, source] of candidates) {
|
|
281
|
+
const expanded = resolve(context.cwd, expandPath(path, context));
|
|
282
|
+
if (entries.some((entry) => entry.path === expanded))
|
|
283
|
+
continue;
|
|
284
|
+
entries.push({
|
|
285
|
+
path: expanded,
|
|
286
|
+
source,
|
|
287
|
+
exists: (await info(expanded))?.isDirectory() ?? false
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
const active = entries.find((entry) => entry.exists)?.path ?? entries.at(-1).path;
|
|
291
|
+
return { entries, active };
|
|
292
|
+
}
|
|
293
|
+
async function resolveWorkspace(input, context, workspaceDirectory) {
|
|
294
|
+
const expanded = expandPath(input, context);
|
|
295
|
+
const pureName = !isAbsolute(expanded) && dirname(expanded) === "." && !expanded.includes(sep) && !extname(expanded) && ![".", ""].includes(expanded);
|
|
296
|
+
const target = resolve(context.cwd, expanded);
|
|
297
|
+
let candidates;
|
|
298
|
+
if (pureName) {
|
|
299
|
+
const active = workspaceDirectory ?? (await directories(context)).active;
|
|
300
|
+
candidates = extensions.map((extension) => resolve(context.cwd, active, expanded + extension));
|
|
301
|
+
} else if ((await info(target))?.isDirectory() || !extname(expanded)) {
|
|
302
|
+
candidates = extensions.map((extension) => join(target, `.tmuxp${extension}`));
|
|
303
|
+
} else
|
|
304
|
+
candidates = [target];
|
|
305
|
+
for (const candidate of candidates)
|
|
306
|
+
if ((await info(candidate))?.isFile())
|
|
307
|
+
return candidate;
|
|
308
|
+
throw new CliError("workspace_not_found", `Workspace not found: ${input}`);
|
|
309
|
+
}
|
|
310
|
+
async function discover(context, full = false) {
|
|
311
|
+
const files = [];
|
|
312
|
+
let current = resolve(context.cwd);
|
|
313
|
+
while (true) {
|
|
314
|
+
for (const extension of extensions) {
|
|
315
|
+
const path = join(current, `.tmuxp${extension}`);
|
|
316
|
+
if ((await info(path))?.isFile()) {
|
|
317
|
+
files.push({ path, source: "local" });
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const parent = dirname(current);
|
|
322
|
+
if (current === parent || current === resolve(home(context)))
|
|
323
|
+
break;
|
|
324
|
+
current = parent;
|
|
325
|
+
}
|
|
326
|
+
const { entries, active } = await directories(context);
|
|
327
|
+
const global_workspace_dirs = [];
|
|
328
|
+
for (const entry of entries) {
|
|
329
|
+
const names = entry.exists ? (await readdir(entry.path)).sort() : [];
|
|
330
|
+
const eligible = names.filter((name) => !name.startsWith(".") && extensions.includes(extname(name).toLowerCase()));
|
|
331
|
+
global_workspace_dirs.push({
|
|
332
|
+
...entry,
|
|
333
|
+
path: privatePath(entry.path, context),
|
|
334
|
+
workspace_count: eligible.length,
|
|
335
|
+
active: entry.path === active
|
|
336
|
+
});
|
|
337
|
+
if (entry.path === active)
|
|
338
|
+
for (const name of eligible) {
|
|
339
|
+
const path = join(entry.path, name);
|
|
340
|
+
if ((await info(path))?.isFile())
|
|
341
|
+
files.push({ path, source: "global" });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const workspaces = [];
|
|
345
|
+
for (const file of files) {
|
|
346
|
+
const details = await info(file.path);
|
|
347
|
+
if (!details)
|
|
348
|
+
continue;
|
|
349
|
+
let config = null;
|
|
350
|
+
try {
|
|
351
|
+
config = await readDocument(file.path);
|
|
352
|
+
} catch {}
|
|
353
|
+
const record = {
|
|
354
|
+
name: basename(file.path, extname(file.path)),
|
|
355
|
+
path: privatePath(file.path, context),
|
|
356
|
+
format: extname(file.path).toLowerCase() === ".json" ? "json" : "yaml",
|
|
357
|
+
size: details.size,
|
|
358
|
+
mtime: details.mtime.toISOString(),
|
|
359
|
+
session_name: config?.session_name ?? null,
|
|
360
|
+
source: file.source
|
|
361
|
+
};
|
|
362
|
+
if (full)
|
|
363
|
+
record.config = config;
|
|
364
|
+
workspaces.push(record);
|
|
365
|
+
}
|
|
366
|
+
return { workspaces, global_workspace_dirs };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export { CliError, write, colorEnabled, styled, emitJson, OperationOutput, home, expandPath, privatePath, mapping, scalarText, sequence, readDocument, saveDocument, resolveWorkspace, discover };
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
__require
|
|
4
|
+
} from "./main-1de529jm.js";
|
|
5
|
+
|
|
6
|
+
// src/main.ts
|
|
7
|
+
if (process.env.NO_COLOR)
|
|
8
|
+
delete process.env.FORCE_COLOR;
|
|
9
|
+
else if (process.env.NO_COLOR === "")
|
|
10
|
+
delete process.env.NO_COLOR;
|
|
11
|
+
var controller = new AbortController;
|
|
12
|
+
var pipeClosed = false;
|
|
13
|
+
process.once("SIGINT", () => controller.abort());
|
|
14
|
+
process.once("SIGTERM", () => controller.abort());
|
|
15
|
+
for (const stream of [process.stdout, process.stderr])
|
|
16
|
+
stream.on("error", (error) => {
|
|
17
|
+
if (error.code === "EPIPE") {
|
|
18
|
+
pipeClosed = true;
|
|
19
|
+
controller.abort();
|
|
20
|
+
process.exitCode = 141;
|
|
21
|
+
} else
|
|
22
|
+
throw error;
|
|
23
|
+
});
|
|
24
|
+
var { run } = await import("./app-w7cbtf0n.js");
|
|
25
|
+
var exitCode = await run(process.argv.slice(2), {
|
|
26
|
+
cwd: process.cwd(),
|
|
27
|
+
env: process.env,
|
|
28
|
+
stdout: process.stdout,
|
|
29
|
+
stderr: process.stderr,
|
|
30
|
+
stdin: process.stdin,
|
|
31
|
+
signal: controller.signal
|
|
32
|
+
});
|
|
33
|
+
process.exitCode = pipeClosed ? 141 : controller.signal.aborted ? 130 : exitCode;
|
|
34
|
+
if (controller.signal.aborted)
|
|
35
|
+
process.exit(process.exitCode);
|