@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,282 @@
|
|
|
1
|
+
import"./main-1de529jm.js";
|
|
2
|
+
|
|
3
|
+
// src/completions.ts
|
|
4
|
+
var quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
5
|
+
var fishQuote = (value) => `'${value.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}'`;
|
|
6
|
+
function entries(catalog) {
|
|
7
|
+
return catalog.commands.flatMap((command, node) => {
|
|
8
|
+
const flags = (option) => [option.short, option.long].filter((flag) => !!flag);
|
|
9
|
+
return [
|
|
10
|
+
...command.options.map((option) => ({
|
|
11
|
+
keys: flags(option).map((flag) => `${node}:${flag}`),
|
|
12
|
+
kind: option.value,
|
|
13
|
+
...option
|
|
14
|
+
})),
|
|
15
|
+
...command.arguments.map((argument, index) => ({
|
|
16
|
+
keys: [`${node}:@${index}`],
|
|
17
|
+
kind: "argument",
|
|
18
|
+
...argument
|
|
19
|
+
})),
|
|
20
|
+
...command.subcommands.map((name) => ({
|
|
21
|
+
keys: [`${node}:>${name}`],
|
|
22
|
+
kind: "command",
|
|
23
|
+
next: catalog.commands.findIndex((candidate) => candidate.path.join(" ") === [...command.path, name].join(" "))
|
|
24
|
+
})),
|
|
25
|
+
{ keys: [`${node}:flags`], kind: "flags", choices: command.options.flatMap(flags) },
|
|
26
|
+
{ keys: [`${node}:commands`], kind: "commands", choices: command.subcommands }
|
|
27
|
+
];
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function shScript(catalog, shell, name) {
|
|
31
|
+
const lookup = entries(catalog).map((entry) => ` ${entry.keys.map(quote).join("|")}) kind=${quote(entry.kind)}; files=${quote(entry.completion ?? "")}; many=${entry.variadic ? 1 : 0}; next=${entry.next ?? -1}; candidates=(${(entry.choices ?? []).map(quote).join(" ")});;`).join(`
|
|
32
|
+
`);
|
|
33
|
+
const setup = shell === "bash" ? `local -a words=()
|
|
34
|
+
local raw line=$COMP_LINE join_next=0 end=-1 index trim=
|
|
35
|
+
for ((index=0; index<=COMP_CWORD; index++)); do
|
|
36
|
+
raw=\${COMP_WORDS[index]}
|
|
37
|
+
if [[ -n $raw && $raw != *[!=:]* ]]; then
|
|
38
|
+
if [[ $end -lt 1 || $line == [[:blank:]]* ]]; then end=$((end + 1)); fi
|
|
39
|
+
words[end]=\${words[end]}$raw
|
|
40
|
+
line=\${line#*"$raw"}
|
|
41
|
+
join_next=1
|
|
42
|
+
[[ $line == [[:blank:]]* ]] && join_next=0
|
|
43
|
+
else
|
|
44
|
+
[[ $join_next == 1 ]] || end=$((end + 1))
|
|
45
|
+
words[end]=\${words[end]}$raw
|
|
46
|
+
line=\${line#*"$raw"}
|
|
47
|
+
join_next=0
|
|
48
|
+
fi
|
|
49
|
+
done
|
|
50
|
+
raw=\${COMP_WORDS[COMP_CWORD]}
|
|
51
|
+
if [[ -n $raw && $raw != *[!=:]* ]]; then trim=\${words[end]}; else trim=\${words[end]%"$raw"}; fi
|
|
52
|
+
COMPREPLY=()` : `setopt localoptions KSH_ARRAYS
|
|
53
|
+
local -a words=("\${words[@]}")
|
|
54
|
+
local end=$((CURRENT - 1))`;
|
|
55
|
+
const emit = shell === "bash" ? `for candidate in "\${candidates[@]}"; do
|
|
56
|
+
if [[ $candidate == "$cur"* ]]; then candidate=$prefix$candidate; COMPREPLY+=("\${candidate#"$trim"}"); fi
|
|
57
|
+
done
|
|
58
|
+
if [[ -n $files ]]; then
|
|
59
|
+
local action=file
|
|
60
|
+
[[ $files == directory ]] && action=directory
|
|
61
|
+
while IFS= read -r candidate; do candidate=$prefix$candidate; COMPREPLY+=("\${candidate#"$trim"}"); done < <(compgen -A "$action" -- "$cur")
|
|
62
|
+
fi` : `unsetopt KSH_ARRAYS
|
|
63
|
+
if (( \${#candidates[@]} )); then compadd -p "$prefix" -- "\${candidates[@]}"; fi
|
|
64
|
+
if [[ -n $files ]]; then
|
|
65
|
+
[[ -z $prefix ]] || compset -P "$prefix"
|
|
66
|
+
if [[ $files == directory ]]; then _files -/; else _files; fi
|
|
67
|
+
fi`;
|
|
68
|
+
return `${shell === "zsh" ? `#compdef ${catalog.program}
|
|
69
|
+
` : `# Bash 3.2 or newer.
|
|
70
|
+
`}${name}_lookup() {
|
|
71
|
+
kind=; files=; many=0; next=-1; candidates=()
|
|
72
|
+
case "$node:$1" in
|
|
73
|
+
${lookup}
|
|
74
|
+
esac
|
|
75
|
+
}
|
|
76
|
+
${name}() {
|
|
77
|
+
${setup}
|
|
78
|
+
local node=0 pos=0 stop=0 pending= kind= files= many=0 next=-1
|
|
79
|
+
local w key tail prefix= cur candidate i
|
|
80
|
+
local -a candidates=()
|
|
81
|
+
for ((i=1; i<end; i++)); do
|
|
82
|
+
w=\${words[i]}
|
|
83
|
+
if [[ -n $pending ]]; then
|
|
84
|
+
if [[ $pending == required || $w != -* || $w == - ]]; then
|
|
85
|
+
[[ $many == 1 ]] && pending=optional || pending=
|
|
86
|
+
continue
|
|
87
|
+
fi
|
|
88
|
+
pending=
|
|
89
|
+
fi
|
|
90
|
+
if [[ $stop == 0 && $w == -- ]]; then stop=1; continue; fi
|
|
91
|
+
if [[ $stop == 0 && $w == --* ]]; then
|
|
92
|
+
key=\${w%%=*}; ${name}_lookup "$key"
|
|
93
|
+
if [[ $kind == required || $kind == optional ]]; then
|
|
94
|
+
if [[ $w == *=* ]]; then [[ $many == 1 ]] && pending=optional; else pending=$kind; fi
|
|
95
|
+
fi
|
|
96
|
+
continue
|
|
97
|
+
fi
|
|
98
|
+
if [[ $stop == 0 && $w == -?* ]]; then
|
|
99
|
+
tail=\${w#-}
|
|
100
|
+
while [[ -n $tail ]]; do
|
|
101
|
+
key=-\${tail:0:1}; tail=\${tail:1}; ${name}_lookup "$key"
|
|
102
|
+
if [[ $kind == required || $kind == optional ]]; then
|
|
103
|
+
if [[ -n $tail ]]; then [[ $many == 1 ]] && pending=optional; else pending=$kind; fi
|
|
104
|
+
break
|
|
105
|
+
fi
|
|
106
|
+
done
|
|
107
|
+
continue
|
|
108
|
+
fi
|
|
109
|
+
if [[ $stop == 0 && $pos == 0 ]]; then
|
|
110
|
+
${name}_lookup ">$w"
|
|
111
|
+
if [[ $next -ge 0 ]]; then node=$next; pos=0; continue; fi
|
|
112
|
+
fi
|
|
113
|
+
${name}_lookup "@$pos"
|
|
114
|
+
[[ $many == 1 ]] || pos=$((pos + 1))
|
|
115
|
+
done
|
|
116
|
+
cur=\${words[end]}
|
|
117
|
+
if [[ $pending == optional && $cur == -?* ]]; then pending=; fi
|
|
118
|
+
if [[ -z $pending && $stop == 0 && $cur == --*=* ]]; then
|
|
119
|
+
key=\${cur%%=*}; ${name}_lookup "$key"
|
|
120
|
+
if [[ $kind == required || $kind == optional ]]; then prefix=$key=; cur=\${cur#*=}; pending=$kind; fi
|
|
121
|
+
elif [[ -z $pending && $stop == 0 && $cur == -?* && $cur != --* ]]; then
|
|
122
|
+
tail=\${cur#-}; prefix=-
|
|
123
|
+
while [[ -n $tail ]]; do
|
|
124
|
+
key=-\${tail:0:1}; prefix=$prefix\${tail:0:1}; tail=\${tail:1}; ${name}_lookup "$key"
|
|
125
|
+
if [[ $kind == required || $kind == optional ]]; then cur=$tail; pending=$kind; break; fi
|
|
126
|
+
done
|
|
127
|
+
[[ -n $pending ]] || prefix=
|
|
128
|
+
fi
|
|
129
|
+
if [[ -z $pending ]]; then
|
|
130
|
+
if [[ $stop == 0 && $cur == -* ]]; then
|
|
131
|
+
${name}_lookup flags
|
|
132
|
+
else
|
|
133
|
+
${name}_lookup "@$pos"
|
|
134
|
+
if [[ $stop == 0 && $pos == 0 ]]; then
|
|
135
|
+
local -a positional=("\${candidates[@]}")
|
|
136
|
+
local positional_files=$files
|
|
137
|
+
${name}_lookup commands
|
|
138
|
+
candidates+=("\${positional[@]}"); files=$positional_files
|
|
139
|
+
fi
|
|
140
|
+
fi
|
|
141
|
+
fi
|
|
142
|
+
${emit}
|
|
143
|
+
return 0
|
|
144
|
+
}
|
|
145
|
+
${shell === "bash" ? `complete -o filenames -F ${name} ${quote(catalog.program)}` : `compdef ${name} ${quote(catalog.program)}
|
|
146
|
+
if [[ \${funcstack[1]} == ${quote("_" + catalog.program)} ]]; then ${name}; fi`}
|
|
147
|
+
`;
|
|
148
|
+
}
|
|
149
|
+
function fishScript(catalog, name) {
|
|
150
|
+
const lookup = entries(catalog).map((entry) => ` case ${entry.keys.map(fishQuote).join(" ")}
|
|
151
|
+
printf '%s\\n' ${[entry.kind, entry.completion ?? "_", entry.variadic ? "1" : "0", String(entry.next ?? -1), ...entry.choices ?? []].map(fishQuote).join(" ")}`).join(`
|
|
152
|
+
`);
|
|
153
|
+
return `function ${name}_lookup
|
|
154
|
+
switch "$argv[1]:$argv[2]"
|
|
155
|
+
${lookup}
|
|
156
|
+
case '*'
|
|
157
|
+
printf '%s\\n' _ _ 0 -1
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
function ${name}
|
|
161
|
+
set -l words (commandline -opc)
|
|
162
|
+
set -l cur (string unescape -- (commandline -ct))
|
|
163
|
+
set -l node 0
|
|
164
|
+
set -l pos 0
|
|
165
|
+
set -l stop 0
|
|
166
|
+
set -l pending
|
|
167
|
+
set -l info _ _ 0 -1
|
|
168
|
+
set -l prefix ''
|
|
169
|
+
for w in $words[2..-1]
|
|
170
|
+
if test -n "$pending"
|
|
171
|
+
if test "$pending" = required; or not string match -rq -- '^-.' "$w"
|
|
172
|
+
set pending
|
|
173
|
+
if test "$info[3]" = 1; set pending optional; end
|
|
174
|
+
continue
|
|
175
|
+
end
|
|
176
|
+
set pending
|
|
177
|
+
end
|
|
178
|
+
if test $stop = 0; and test "$w" = --
|
|
179
|
+
set stop 1
|
|
180
|
+
continue
|
|
181
|
+
end
|
|
182
|
+
if test $stop = 0; and string match -q -- '--*' "$w"
|
|
183
|
+
set -l key (string split -m 1 = -- "$w")[1]
|
|
184
|
+
set info (${name}_lookup $node "$key")
|
|
185
|
+
if contains -- "$info[1]" required optional
|
|
186
|
+
if string match -q '*=*' -- "$w"
|
|
187
|
+
if test "$info[3]" = 1; set pending optional; end
|
|
188
|
+
else
|
|
189
|
+
set pending $info[1]
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
continue
|
|
193
|
+
end
|
|
194
|
+
if test $stop = 0; and string match -rq -- '^-.' "$w"
|
|
195
|
+
set -l tail (string sub -s 2 -- "$w")
|
|
196
|
+
while test -n "$tail"
|
|
197
|
+
set -l key -(string sub -l 1 -- "$tail")
|
|
198
|
+
set tail (string sub -s 2 -- "$tail")
|
|
199
|
+
set info (${name}_lookup $node "$key")
|
|
200
|
+
if contains -- "$info[1]" required optional
|
|
201
|
+
if test -n "$tail"
|
|
202
|
+
if test "$info[3]" = 1; set pending optional; end
|
|
203
|
+
else
|
|
204
|
+
set pending $info[1]
|
|
205
|
+
end
|
|
206
|
+
break
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
continue
|
|
210
|
+
end
|
|
211
|
+
if test $stop = 0; and test $pos = 0
|
|
212
|
+
set info (${name}_lookup $node ">$w")
|
|
213
|
+
if test "$info[4]" -ge 0
|
|
214
|
+
set node $info[4]
|
|
215
|
+
set pos 0
|
|
216
|
+
continue
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
set info (${name}_lookup $node "@$pos")
|
|
220
|
+
if test "$info[3]" != 1; set pos (math $pos + 1); end
|
|
221
|
+
end
|
|
222
|
+
if test "$pending" = optional; and string match -rq -- '^-.' "$cur"; set pending; end
|
|
223
|
+
if test -z "$pending"; and test $stop = 0; and string match -q -- '--*=*' "$cur"
|
|
224
|
+
set -l parts (string split -m 1 = -- "$cur")
|
|
225
|
+
set info (${name}_lookup $node "$parts[1]")
|
|
226
|
+
if contains -- "$info[1]" required optional
|
|
227
|
+
set prefix "$parts[1]="
|
|
228
|
+
set cur "$parts[2]"
|
|
229
|
+
set pending $info[1]
|
|
230
|
+
end
|
|
231
|
+
else if test -z "$pending"; and test $stop = 0; and string match -rq -- '^-.' "$cur"; and not string match -q -- '--*' "$cur"
|
|
232
|
+
set -l tail (string sub -s 2 -- "$cur")
|
|
233
|
+
set prefix -
|
|
234
|
+
while test -n "$tail"
|
|
235
|
+
set -l char (string sub -l 1 -- "$tail")
|
|
236
|
+
set prefix "$prefix$char"
|
|
237
|
+
set tail (string sub -s 2 -- "$tail")
|
|
238
|
+
set info (${name}_lookup $node "-$char")
|
|
239
|
+
if contains -- "$info[1]" required optional
|
|
240
|
+
set cur "$tail"
|
|
241
|
+
set pending $info[1]
|
|
242
|
+
break
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
if test -z "$pending"; set prefix ''; end
|
|
246
|
+
end
|
|
247
|
+
if test -z "$pending"
|
|
248
|
+
if test $stop = 0; and string match -q -- '-*' "$cur"
|
|
249
|
+
set info (${name}_lookup $node flags)
|
|
250
|
+
else
|
|
251
|
+
set info (${name}_lookup $node "@$pos")
|
|
252
|
+
if test $stop = 0; and test $pos = 0
|
|
253
|
+
set -l children (${name}_lookup $node commands)
|
|
254
|
+
set -a info $children[5..-1]
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
for candidate in $info[5..-1]
|
|
259
|
+
if test (string sub -l (string length -- "$cur") -- "$candidate") = "$cur"
|
|
260
|
+
printf '%s\\n' "$prefix$candidate"
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
if contains -- "$info[2]" file directory
|
|
264
|
+
set -l paths (__fish_complete_path (string escape -- "$cur"))
|
|
265
|
+
for path in $paths
|
|
266
|
+
set -l value (string split -m 1 \\t -- "$path")[1]
|
|
267
|
+
if test "$info[2]" = file; or string match -q '*/' -- "$value"
|
|
268
|
+
printf '%s\\n' "$prefix$value"
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
complete -c ${fishQuote(catalog.program)} -f -a '(${name})'
|
|
274
|
+
`;
|
|
275
|
+
}
|
|
276
|
+
function completionScript(catalog, shell) {
|
|
277
|
+
const name = `_${catalog.program.replaceAll(/[^a-zA-Z0-9_]/g, "_")}`;
|
|
278
|
+
return shell === "fish" ? fishScript(catalog, name) : shScript(catalog, shell, name);
|
|
279
|
+
}
|
|
280
|
+
export {
|
|
281
|
+
completionScript
|
|
282
|
+
};
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import {
|
|
2
|
+
expand,
|
|
3
|
+
normalize,
|
|
4
|
+
processRun,
|
|
5
|
+
workspaceName
|
|
6
|
+
} from "./main-c8atq2pj.js";
|
|
7
|
+
import {
|
|
8
|
+
CliError,
|
|
9
|
+
privatePath,
|
|
10
|
+
write
|
|
11
|
+
} from "./main-gh3pafqb.js";
|
|
12
|
+
import"./main-1de529jm.js";
|
|
13
|
+
|
|
14
|
+
// src/extensions.ts
|
|
15
|
+
import {
|
|
16
|
+
TmuxCommandError
|
|
17
|
+
} from "libtmux";
|
|
18
|
+
import { constants } from "node:fs";
|
|
19
|
+
import { mkdtemp, open, rm, stat } from "node:fs/promises";
|
|
20
|
+
import { tmpdir } from "node:os";
|
|
21
|
+
import { dirname, join, resolve } from "node:path";
|
|
22
|
+
async function extensionPlan(data, path, context, append, override) {
|
|
23
|
+
const plugins = data.plugins ?? [];
|
|
24
|
+
if (!Array.isArray(plugins) || plugins.some((item) => typeof item !== "string" || !/^\w+(?:\.\w+)+$/.test(item)))
|
|
25
|
+
throw new CliError("invalid_workspace", "plugins must be an array of Python dotted class names");
|
|
26
|
+
const builder = data.workspace_builder ?? "";
|
|
27
|
+
if (typeof builder !== "string" || builder.includes("\x00"))
|
|
28
|
+
throw new CliError("invalid_workspace", "workspace_builder must be a string");
|
|
29
|
+
const rawPaths = data.workspace_builder_paths ?? [];
|
|
30
|
+
const paths = typeof rawPaths === "string" ? [rawPaths] : rawPaths;
|
|
31
|
+
if (!Array.isArray(paths) || paths.some((entry) => typeof entry !== "string" || entry.includes("\x00")))
|
|
32
|
+
throw new CliError("invalid_workspace", "workspace_builder_paths must contain directory paths");
|
|
33
|
+
for (const entry of paths) {
|
|
34
|
+
const directory = resolve(dirname(path), expand(entry, context));
|
|
35
|
+
if (!(await stat(directory).catch(() => {
|
|
36
|
+
return;
|
|
37
|
+
}))?.isDirectory())
|
|
38
|
+
throw new CliError("invalid_workspace", `Builder import directory does not exist: ${privatePath(directory, context)}`);
|
|
39
|
+
}
|
|
40
|
+
if (!plugins.length && !builder.trim())
|
|
41
|
+
return;
|
|
42
|
+
if (append && Object.hasOwn(data, "before_script"))
|
|
43
|
+
throw new CliError("invalid_workspace", "Python extension append cannot use before_script; run the script separately");
|
|
44
|
+
const name = workspaceName(data, context, override);
|
|
45
|
+
if (!builder.trim())
|
|
46
|
+
normalize(data, path, context, override, { allowExtensionFields: true });
|
|
47
|
+
const config = structuredClone(data);
|
|
48
|
+
if (!builder.trim())
|
|
49
|
+
delete config.workspace_builder;
|
|
50
|
+
return { data: config, name };
|
|
51
|
+
}
|
|
52
|
+
var runtimeCheck = `
|
|
53
|
+
from importlib.metadata import version
|
|
54
|
+
import inspect
|
|
55
|
+
if version("tmuxp") != "1.74.0":
|
|
56
|
+
raise RuntimeError("tmuxp 1.74.0 is required")
|
|
57
|
+
from libtmux import Server
|
|
58
|
+
from tmuxp.workspace.builder import prepended_sys_path, resolve_builder_class, resolve_builder_paths
|
|
59
|
+
if "tmux_bin" not in inspect.signature(Server).parameters:
|
|
60
|
+
raise RuntimeError("libtmux Server must accept tmux_bin")
|
|
61
|
+
`;
|
|
62
|
+
async function extensionRuntime(context) {
|
|
63
|
+
const python = context.env.TMUX_WORKSPACE_PYTHON || "python3";
|
|
64
|
+
const timeout = AbortSignal.timeout(1e4);
|
|
65
|
+
try {
|
|
66
|
+
const result = await processRun([python, "-c", runtimeCheck], {
|
|
67
|
+
cwd: context.cwd,
|
|
68
|
+
env: context.env,
|
|
69
|
+
signal: context.signal ? AbortSignal.any([context.signal, timeout]) : timeout
|
|
70
|
+
});
|
|
71
|
+
if (result.code !== 0)
|
|
72
|
+
throw new Error(result.stderr.trim());
|
|
73
|
+
} catch (error) {
|
|
74
|
+
context.signal?.throwIfAborted();
|
|
75
|
+
throw new CliError("script_failed", `Python extensions require tmuxp 1.74.0 and its builder registry: ${error instanceof Error ? error.message : String(error)}`);
|
|
76
|
+
}
|
|
77
|
+
return python;
|
|
78
|
+
}
|
|
79
|
+
var adapter = String.raw`
|
|
80
|
+
import importlib
|
|
81
|
+
import json
|
|
82
|
+
import os
|
|
83
|
+
import re
|
|
84
|
+
import shutil
|
|
85
|
+
import sys
|
|
86
|
+
from pathlib import Path
|
|
87
|
+
|
|
88
|
+
request = json.load(sys.stdin)
|
|
89
|
+
executable = shutil.which(request["server"]["tmux_bin"])
|
|
90
|
+
if executable is None:
|
|
91
|
+
raise RuntimeError("selected tmux executable was not found")
|
|
92
|
+
binary = Path(request["state_path"]).parent / "tmux"
|
|
93
|
+
request["server"]["tmux_bin"] = str(Path(executable).resolve())
|
|
94
|
+
binary.symlink_to(request["server"]["tmux_bin"])
|
|
95
|
+
for key in ("socket_path", "config_file"):
|
|
96
|
+
if request["server"].get(key):
|
|
97
|
+
request["server"][key] = str(Path(request["server"][key]).absolute())
|
|
98
|
+
os.environ["PATH"] = str(binary.parent) + os.pathsep + os.environ.get("PATH", "")
|
|
99
|
+
|
|
100
|
+
from libtmux import Server
|
|
101
|
+
from tmuxp.workspace import loader
|
|
102
|
+
from tmuxp.workspace.builder import prepended_sys_path, resolve_builder_class, resolve_builder_paths
|
|
103
|
+
|
|
104
|
+
config = request["config"]
|
|
105
|
+
custom = bool((config.get("workspace_builder") or "").strip())
|
|
106
|
+
config["session_name"] = request["session_name"]
|
|
107
|
+
config = loader.expand(config, cwd=str(Path(request["source"]).parent))
|
|
108
|
+
if not custom or "windows" in config:
|
|
109
|
+
config = loader.trickle(config)
|
|
110
|
+
config["session_name"] = request["session_name"]
|
|
111
|
+
server = Server(**request["server"])
|
|
112
|
+
borrowed = request.get("borrowed")
|
|
113
|
+
builder = None
|
|
114
|
+
|
|
115
|
+
def authenticate():
|
|
116
|
+
if borrowed is None:
|
|
117
|
+
return None
|
|
118
|
+
identity = borrowed["daemon"]
|
|
119
|
+
expected = f"{identity['pid']}:{identity['startTime']}"
|
|
120
|
+
if server.cmd("display-message", "-p", "#{pid}:#{start_time}").stdout != [expected]:
|
|
121
|
+
raise RuntimeError("authenticated append daemon changed")
|
|
122
|
+
session = server.sessions.get(session_id=borrowed["session_id"], default=None)
|
|
123
|
+
if session is None:
|
|
124
|
+
raise RuntimeError("authenticated append session disappeared")
|
|
125
|
+
return session
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
authenticate()
|
|
129
|
+
with prepended_sys_path(resolve_builder_paths(config, request["source"])):
|
|
130
|
+
plugins = []
|
|
131
|
+
for target in config.get("plugins") or []:
|
|
132
|
+
module, _, name = target.rpartition(".")
|
|
133
|
+
plugins.append(getattr(importlib.import_module(module), name)())
|
|
134
|
+
builder = resolve_builder_class(config)(session_config=config, server=server, plugins=plugins)
|
|
135
|
+
session = authenticate()
|
|
136
|
+
builder.build(session=session, append=session is not None)
|
|
137
|
+
for plugin in builder.plugins:
|
|
138
|
+
plugin.before_script(builder.session)
|
|
139
|
+
finally:
|
|
140
|
+
session_id = borrowed["session_id"] if borrowed is not None else None
|
|
141
|
+
if builder is not None:
|
|
142
|
+
try:
|
|
143
|
+
session_id = builder.session.id
|
|
144
|
+
except Exception:
|
|
145
|
+
pass
|
|
146
|
+
if not isinstance(session_id, str) or not re.fullmatch(r"\$\d+", session_id):
|
|
147
|
+
session_id = None
|
|
148
|
+
Path(request["state_path"]).write_text(json.dumps({"session_id": session_id}), encoding="utf-8")
|
|
149
|
+
`;
|
|
150
|
+
async function sessionHint(path) {
|
|
151
|
+
const file = await open(path, constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW);
|
|
152
|
+
try {
|
|
153
|
+
if (!(await file.stat()).isFile())
|
|
154
|
+
throw new Error("Extension state is not a regular file");
|
|
155
|
+
const buffer = Buffer.alloc(4097);
|
|
156
|
+
const { bytesRead } = await file.read(buffer);
|
|
157
|
+
if (bytesRead > 4096)
|
|
158
|
+
throw new Error("Extension state exceeds 4 KiB");
|
|
159
|
+
const state = JSON.parse(buffer.subarray(0, bytesRead).toString("utf8"));
|
|
160
|
+
const id = state && typeof state === "object" && "session_id" in state ? state.session_id : undefined;
|
|
161
|
+
if (id === null)
|
|
162
|
+
return;
|
|
163
|
+
if (typeof id !== "string" || !/^\$\d+$/.test(id))
|
|
164
|
+
throw new Error("Invalid extension session ID");
|
|
165
|
+
return id;
|
|
166
|
+
} finally {
|
|
167
|
+
await file.close();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function sameDaemon(left, right) {
|
|
171
|
+
return left.pid === right.pid && left.startTime === right.startTime;
|
|
172
|
+
}
|
|
173
|
+
async function baseline(server, signal) {
|
|
174
|
+
try {
|
|
175
|
+
await server.cmd("list-sessions", [], signal ? { signal } : {});
|
|
176
|
+
} catch (error) {
|
|
177
|
+
if (error instanceof TmuxCommandError && error.exitCode === 1 && !error.stdout.length && error.stderr.length === 1 && /^(?:no server running on .+|error connecting to .+ \((?:No such file or directory|Connection refused)\))$/.test(error.stderr[0]))
|
|
178
|
+
return;
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
return server.snapshot(signal ? { signal } : {});
|
|
182
|
+
}
|
|
183
|
+
async function buildExtension(python, spec, source, server, context, output, result, borrowed) {
|
|
184
|
+
const before = await baseline(server, context.signal);
|
|
185
|
+
if (borrowed && (!before || !sameDaemon(borrowed.daemon, before.daemonIdentity) || !before.sessions.toArray().some((session2) => session2.id === borrowed.session.id)))
|
|
186
|
+
throw new CliError("tmux_failed", "The authenticated append session or daemon changed");
|
|
187
|
+
const existing = !borrowed && before?.sessions.toArray().find((session2) => session2.name === spec.name);
|
|
188
|
+
if (existing) {
|
|
189
|
+
result.reused = true;
|
|
190
|
+
return existing;
|
|
191
|
+
}
|
|
192
|
+
const directory = await mkdtemp(join(tmpdir(), "ltx-workspace-extension-"));
|
|
193
|
+
const statePath = join(directory, "state.json");
|
|
194
|
+
result.stage = "extension-build";
|
|
195
|
+
result.effects_scope = "observed";
|
|
196
|
+
result.observed_windows = [];
|
|
197
|
+
result.observed_panes = [];
|
|
198
|
+
let session;
|
|
199
|
+
let failed;
|
|
200
|
+
try {
|
|
201
|
+
await output.event("extension-started", { input_index: result.input_index });
|
|
202
|
+
result.effects_unknown = true;
|
|
203
|
+
try {
|
|
204
|
+
result.script_output = await processRun([python, "-u", "-c", adapter], {
|
|
205
|
+
cwd: context.cwd,
|
|
206
|
+
env: { ...context.env, TMUX: undefined, TMUX_PANE: undefined },
|
|
207
|
+
...context.signal ? { signal: context.signal } : {},
|
|
208
|
+
input: JSON.stringify({
|
|
209
|
+
config: spec.data,
|
|
210
|
+
source,
|
|
211
|
+
session_name: spec.name,
|
|
212
|
+
state_path: statePath,
|
|
213
|
+
server: {
|
|
214
|
+
tmux_bin: server.tmuxBin,
|
|
215
|
+
socket_path: server.socketPath,
|
|
216
|
+
socket_name: server.socketName,
|
|
217
|
+
config_file: server.configFile,
|
|
218
|
+
colors: server.colors
|
|
219
|
+
},
|
|
220
|
+
...borrowed ? { borrowed: { session_id: borrowed.session.id, daemon: borrowed.daemon } } : {}
|
|
221
|
+
}),
|
|
222
|
+
output: async (stream, text) => {
|
|
223
|
+
const handled = await output.event("script-output", {
|
|
224
|
+
input_index: result.input_index,
|
|
225
|
+
stream,
|
|
226
|
+
text
|
|
227
|
+
});
|
|
228
|
+
if (output.mode === "human" && !handled)
|
|
229
|
+
await write(context[stream], text, context.signal);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
if (result.script_output.code !== 0)
|
|
233
|
+
throw new CliError("script_failed", `Python extension exited with status ${result.script_output.code}`);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
failed = error;
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
const after = await server.snapshot({ signal: AbortSignal.timeout(1e4) });
|
|
239
|
+
if (before && !sameDaemon(before.daemonIdentity, after.daemonIdentity))
|
|
240
|
+
throw new Error("Daemon changed during extension execution");
|
|
241
|
+
const oldWindows = new Set(before?.windows.toArray().map((window) => window.id));
|
|
242
|
+
const oldPanes = new Set(before?.panes.toArray().map((pane) => pane.id));
|
|
243
|
+
result.observed_windows = after.windows.toArray().map((window) => window.id).filter((id) => !oldWindows.has(id));
|
|
244
|
+
result.observed_panes = after.panes.toArray().map((pane) => pane.id).filter((id) => !oldPanes.has(id));
|
|
245
|
+
const hint = borrowed?.session.id ?? await sessionHint(statePath);
|
|
246
|
+
session = after.sessions.toArray().find((item) => item.id === hint);
|
|
247
|
+
if (!session)
|
|
248
|
+
throw new Error("Extension did not identify a surviving session");
|
|
249
|
+
result.session_id = session.id;
|
|
250
|
+
result.session_name = session.name ?? spec.name;
|
|
251
|
+
} catch (error) {
|
|
252
|
+
result.observation_error = error instanceof Error ? error.message : String(error);
|
|
253
|
+
failed ??= new CliError("tmux_failed", result.observation_error);
|
|
254
|
+
}
|
|
255
|
+
if (failed !== undefined)
|
|
256
|
+
throw failed;
|
|
257
|
+
result.completed_stages.push("extension-built");
|
|
258
|
+
await output.event("extension-completed", { input_index: result.input_index });
|
|
259
|
+
return session;
|
|
260
|
+
} finally {
|
|
261
|
+
await rm(directory, { recursive: true, force: true });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
export {
|
|
265
|
+
buildExtension,
|
|
266
|
+
extensionPlan,
|
|
267
|
+
extensionRuntime
|
|
268
|
+
};
|