zerogterm 0.3.0-alpha.1 → 0.5.0-alpha.1
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 +144 -29
- package/dist/main/main/clipboard.js +30 -0
- package/dist/main/main/main.js +19 -4
- package/dist/main/main/preload.cjs +1 -1
- package/dist/main/main/session-service.js +80 -49
- package/dist/main/main/shell-catalog.js +193 -0
- package/dist/renderer/assets/index-BJsoL2QL.js +17 -0
- package/dist/renderer/assets/index-BPfYUrvm.css +1 -0
- package/dist/renderer/assets/voice-worker-DoNT0zEM.js +37 -0
- package/dist/renderer/index.html +3 -3
- package/package.json +1 -1
- package/dist/renderer/assets/index-5pgRaxBt.css +0 -1
- package/dist/renderer/assets/index-Cs9gfbAs.js +0 -17
- package/dist/renderer/assets/voice-worker-DWb1dVxY.js +0 -37
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// Which shells this machine can actually start, and what to spawn for each.
|
|
2
|
+
//
|
|
3
|
+
// Ported forward from the cross-platform discovery work in 690f01d on
|
|
4
|
+
// fix/cross-platform-and-review-fixes, which never reached dev. What shipped
|
|
5
|
+
// instead knew only `pwsh.exe`, so a Windows machine without PowerShell 7 — the
|
|
6
|
+
// common case, since 7 is a separate install — was offered no PowerShell at
|
|
7
|
+
// all, and the two shells Windows always has, Windows PowerShell and Command
|
|
8
|
+
// Prompt, could not be chosen. bash was offered unconditionally on every
|
|
9
|
+
// platform, including where it cannot start.
|
|
10
|
+
//
|
|
11
|
+
// Candidates are resolved against PATH rather than probed by spawning them.
|
|
12
|
+
// Spawning was slow and wrong in two ways: it cold-started PowerShell on CI,
|
|
13
|
+
// which intermittently blew vitest's timeout, and it made `--version` the test
|
|
14
|
+
// of existence, which Command Prompt does not answer. Resolving also yields an
|
|
15
|
+
// absolute path, which is what node-pty needs on Windows — it does not apply
|
|
16
|
+
// PATHEXT, so a bare `bash` fails there with "File not found:".
|
|
17
|
+
import { statSync } from 'node:fs';
|
|
18
|
+
/** Distro names reach `wsl.exe -d <name>`; a leading '-' would read as a flag. */
|
|
19
|
+
const WSL_DISTRIBUTION = /^[A-Za-z0-9][A-Za-z0-9 ._-]{0,63}$/;
|
|
20
|
+
const LOCAL_SHELL_BACKENDS = [
|
|
21
|
+
'bash', 'zsh', 'fish', 'sh', 'powershell', 'pwsh', 'cmd', 'wsl'
|
|
22
|
+
];
|
|
23
|
+
/**
|
|
24
|
+
* Does this value name a shell?
|
|
25
|
+
*
|
|
26
|
+
* A stored session's backend can also be a transport — 'screen' for a session
|
|
27
|
+
* inside screen, 'ssh' for a remote one — and it arrives from the history file
|
|
28
|
+
* on disk, so it is narrowed rather than asserted.
|
|
29
|
+
*/
|
|
30
|
+
export function isLocalShellBackend(value) {
|
|
31
|
+
return typeof value === 'string' && LOCAL_SHELL_BACKENDS.includes(value);
|
|
32
|
+
}
|
|
33
|
+
function defaultIsFile(candidate) {
|
|
34
|
+
try {
|
|
35
|
+
return statSync(candidate).isFile();
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function resolved(options) {
|
|
42
|
+
const platform = options.platform ?? process.platform;
|
|
43
|
+
return {
|
|
44
|
+
platform,
|
|
45
|
+
windows: platform === 'win32',
|
|
46
|
+
path: options.path ?? process.env.PATH ?? '',
|
|
47
|
+
pathExt: options.pathExt ?? process.env.PATHEXT ?? '.EXE;.CMD;.BAT;.COM',
|
|
48
|
+
loginShell: options.loginShell ?? process.env.SHELL,
|
|
49
|
+
isFile: options.isFile ?? defaultIsFile
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Path arithmetic for the *target* platform, not the host.
|
|
54
|
+
*
|
|
55
|
+
* node:path's `delimiter`, `sep` and `join` follow whichever platform this code
|
|
56
|
+
* is running on, which is right in production and wrong for reasoning about a
|
|
57
|
+
* platform passed in — splitting a Unix PATH on ';' yields one long entry that
|
|
58
|
+
* matches nothing. Doing it explicitly keeps the module honest either way.
|
|
59
|
+
*/
|
|
60
|
+
function pathParts(value, windows) {
|
|
61
|
+
return value.split(windows ? ';' : ':').filter(Boolean);
|
|
62
|
+
}
|
|
63
|
+
function joinPath(directory, name, windows) {
|
|
64
|
+
const separator = windows ? '\\' : '/';
|
|
65
|
+
const trimmed = directory.replace(/[\\/]+$/, '');
|
|
66
|
+
return `${trimmed}${separator}${name}`;
|
|
67
|
+
}
|
|
68
|
+
function fileName(value) {
|
|
69
|
+
return value.split(/[\\/]/).pop() ?? value;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Resolve a bare command name against PATH without spawning anything.
|
|
73
|
+
*
|
|
74
|
+
* On Windows the extension is usually part of the command already, but PATHEXT
|
|
75
|
+
* is honoured so `pwsh` finds `pwsh.exe` — the empty extension is tried first so
|
|
76
|
+
* an exact name still wins.
|
|
77
|
+
*
|
|
78
|
+
* `accept` lets a caller reject a match and keep searching later PATH entries,
|
|
79
|
+
* which is what finding Git Bash behind System32's shim requires.
|
|
80
|
+
*/
|
|
81
|
+
export function findExecutable(command, options = {}, accept = () => true) {
|
|
82
|
+
const { windows, path, pathExt, isFile } = resolved(options);
|
|
83
|
+
const extensions = windows ? ['', ...pathExt.split(';').filter(Boolean)] : [''];
|
|
84
|
+
for (const directory of pathParts(path, windows)) {
|
|
85
|
+
for (const extension of extensions) {
|
|
86
|
+
const candidate = joinPath(directory, command + extension, windows);
|
|
87
|
+
if (isFile(candidate) && accept(candidate))
|
|
88
|
+
return candidate;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Is this the `bash.exe` shim Windows installs for WSL?
|
|
95
|
+
*
|
|
96
|
+
* System32's bash.exe is present on every Windows 10 and 11 machine whether or
|
|
97
|
+
* not a distribution is installed, and it launches WSL rather than Git Bash.
|
|
98
|
+
* Offering it would mean a "Git Bash" entry that starts something else, or
|
|
99
|
+
* nothing at all — and WSL already has its own backend. Rejecting it as a
|
|
100
|
+
* candidate rather than abandoning the search lets Git for Windows' own
|
|
101
|
+
* bash.exe, further along PATH, still be found.
|
|
102
|
+
*/
|
|
103
|
+
function isWslBashShim(file) {
|
|
104
|
+
return /[\\/]windows[\\/]system32[\\/]/i.test(file);
|
|
105
|
+
}
|
|
106
|
+
/** The shells worth offering on this platform, in the order they are preferred. */
|
|
107
|
+
function shellCandidates(windows) {
|
|
108
|
+
if (windows) {
|
|
109
|
+
return [
|
|
110
|
+
{ backend: 'powershell', label: 'Windows PowerShell', command: 'powershell.exe' },
|
|
111
|
+
{ backend: 'pwsh', label: 'PowerShell 7', command: 'pwsh.exe' },
|
|
112
|
+
{ backend: 'cmd', label: 'Command Prompt', command: 'cmd.exe' },
|
|
113
|
+
{ backend: 'wsl', label: 'WSL', command: 'wsl.exe' },
|
|
114
|
+
// Git for Windows ships bash; useful, but not a login shell for the OS.
|
|
115
|
+
{ backend: 'bash', label: 'Git Bash', command: 'bash.exe' }
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
return [
|
|
119
|
+
{ backend: 'bash', label: 'Bash', command: 'bash' },
|
|
120
|
+
{ backend: 'zsh', label: 'Zsh', command: 'zsh' },
|
|
121
|
+
{ backend: 'fish', label: 'Fish', command: 'fish' },
|
|
122
|
+
{ backend: 'sh', label: 'sh', command: 'sh' }
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Move the user's login shell to the front, and say so in its label.
|
|
127
|
+
*
|
|
128
|
+
* $SHELL is what the user chose for themselves, so it belongs first — but only
|
|
129
|
+
* as an ordering: a login shell that is not one of the known backends is left
|
|
130
|
+
* alone rather than spawned under a name the rest of the app cannot describe.
|
|
131
|
+
*/
|
|
132
|
+
function preferLoginShell(shells, loginShell) {
|
|
133
|
+
if (!loginShell)
|
|
134
|
+
return shells;
|
|
135
|
+
const name = fileName(loginShell);
|
|
136
|
+
const index = shells.findIndex((shell) => shell.backend === name);
|
|
137
|
+
if (index <= 0)
|
|
138
|
+
return shells;
|
|
139
|
+
const preferred = { ...shells[index], label: `${shells[index].label} (login shell)` };
|
|
140
|
+
return [preferred, ...shells.filter((_, position) => position !== index)];
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Every shell that exists on this machine, most preferred first.
|
|
144
|
+
*
|
|
145
|
+
* The first entry is the platform default: a native shell on Windows, the login
|
|
146
|
+
* shell or bash on Unix.
|
|
147
|
+
*/
|
|
148
|
+
export function discoverShellBackends(options = {}) {
|
|
149
|
+
const context = resolved(options);
|
|
150
|
+
const found = [];
|
|
151
|
+
for (const candidate of shellCandidates(context.windows)) {
|
|
152
|
+
const skipShim = context.windows && candidate.backend === 'bash';
|
|
153
|
+
const file = findExecutable(candidate.command, options, skipShim ? (match) => !isWslBashShim(match) : undefined);
|
|
154
|
+
if (!file)
|
|
155
|
+
continue;
|
|
156
|
+
found.push({
|
|
157
|
+
backend: candidate.backend,
|
|
158
|
+
executable: file,
|
|
159
|
+
args: candidate.args ?? [],
|
|
160
|
+
label: candidate.label
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return context.windows ? found : preferLoginShell(found, context.loginShell);
|
|
164
|
+
}
|
|
165
|
+
/** The backend a new session gets when the caller names none. */
|
|
166
|
+
export function defaultShellBackend(options = {}) {
|
|
167
|
+
const shells = discoverShellBackends(options);
|
|
168
|
+
if (!shells.length)
|
|
169
|
+
throw new Error('No usable shell was found on PATH.');
|
|
170
|
+
return shells[0];
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* What to spawn for a requested backend.
|
|
174
|
+
*
|
|
175
|
+
* The backend name crosses the IPC boundary from the renderer, so it is looked
|
|
176
|
+
* up in the catalogue rather than treated as a command: the renderer can name a
|
|
177
|
+
* shell, never an executable. A backend that is not installed fails here, by
|
|
178
|
+
* name, instead of as a pty that dies on spawn.
|
|
179
|
+
*/
|
|
180
|
+
export function resolveShellBackend(backend, distribution, options = {}) {
|
|
181
|
+
const match = discoverShellBackends(options).find((shell) => shell.backend === backend);
|
|
182
|
+
if (!match) {
|
|
183
|
+
const label = shellCandidates(resolved(options).windows).find((candidate) => candidate.backend === backend)?.label ?? backend;
|
|
184
|
+
throw new Error(`${label} is not installed or unavailable.`);
|
|
185
|
+
}
|
|
186
|
+
if (backend !== 'wsl' || !distribution)
|
|
187
|
+
return match;
|
|
188
|
+
const trimmed = distribution.trim();
|
|
189
|
+
if (!WSL_DISTRIBUTION.test(trimmed)) {
|
|
190
|
+
throw new Error('WSL distribution names may contain letters, numbers, spaces, _, ., and - only.');
|
|
191
|
+
}
|
|
192
|
+
return { ...match, args: ['-d', trimmed], label: `WSL · ${trimmed}`, wslDistribution: trimmed };
|
|
193
|
+
}
|