@tlbx-ai/midterm 0.1.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 +33 -0
- package/bin/midterm.js +351 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# @tlbx-ai/midterm
|
|
2
|
+
|
|
3
|
+
Launch MidTerm through `npx`.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx @tlbx-ai/midterm
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
The launcher downloads the native MidTerm release for your platform, caches it in your user profile, and runs it locally.
|
|
10
|
+
|
|
11
|
+
Supported platforms:
|
|
12
|
+
|
|
13
|
+
- Windows x64
|
|
14
|
+
- macOS x64
|
|
15
|
+
- macOS ARM64
|
|
16
|
+
- Linux x64
|
|
17
|
+
|
|
18
|
+
Extra arguments are passed through to `mt`:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx @tlbx-ai/midterm -- --port 2001 --bind 127.0.0.1
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Launcher-only options:
|
|
25
|
+
|
|
26
|
+
- `--channel stable|dev`
|
|
27
|
+
- `--help-launcher`
|
|
28
|
+
|
|
29
|
+
Notes:
|
|
30
|
+
|
|
31
|
+
- Default channel is `stable`
|
|
32
|
+
- If you do not pass `--bind`, the launcher forces `127.0.0.1`
|
|
33
|
+
- The launcher sets `MIDTERM_LAUNCH_MODE=npx` for the child process
|
package/bin/midterm.js
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const fsp = require('node:fs/promises');
|
|
7
|
+
const os = require('node:os');
|
|
8
|
+
const path = require('node:path');
|
|
9
|
+
const { spawn, spawnSync } = require('node:child_process');
|
|
10
|
+
|
|
11
|
+
const PACKAGE_VERSION = '0.1.0';
|
|
12
|
+
const REPO_OWNER = 'tlbx-ai';
|
|
13
|
+
const REPO_NAME = 'MidTerm';
|
|
14
|
+
const GITHUB_API = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}`;
|
|
15
|
+
|
|
16
|
+
async function main() {
|
|
17
|
+
const { launcher, passthrough } = parseArgs(process.argv.slice(2));
|
|
18
|
+
|
|
19
|
+
if (launcher.help) {
|
|
20
|
+
printHelp();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const target = getPlatformTarget();
|
|
25
|
+
const release = await resolveRelease(launcher.channel);
|
|
26
|
+
const installDir = await ensureInstalledRelease(release, target);
|
|
27
|
+
const mtPath = path.join(installDir, target.binaryName);
|
|
28
|
+
const mthostPath = path.join(installDir, target.hostBinaryName);
|
|
29
|
+
|
|
30
|
+
if (!fs.existsSync(mtPath) || !fs.existsSync(mthostPath)) {
|
|
31
|
+
throw new Error(`Downloaded release is incomplete: expected ${target.binaryName} and ${target.hostBinaryName}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const childArgs = passthrough.slice();
|
|
35
|
+
if (!hasArg(childArgs, '--bind')) {
|
|
36
|
+
childArgs.push('--bind', '127.0.0.1');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const childEnv = {
|
|
40
|
+
...process.env,
|
|
41
|
+
MIDTERM_LAUNCH_MODE: 'npx',
|
|
42
|
+
MIDTERM_NPX: '1',
|
|
43
|
+
MIDTERM_NPX_CHANNEL: launcher.channel,
|
|
44
|
+
MIDTERM_NPX_PACKAGE_VERSION: PACKAGE_VERSION
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const child = spawn(mtPath, childArgs, {
|
|
48
|
+
stdio: 'inherit',
|
|
49
|
+
env: childEnv
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
forwardSignal(child, 'SIGINT');
|
|
53
|
+
forwardSignal(child, 'SIGTERM');
|
|
54
|
+
forwardSignal(child, 'SIGHUP');
|
|
55
|
+
|
|
56
|
+
child.on('exit', (code, signal) => {
|
|
57
|
+
if (signal) {
|
|
58
|
+
process.kill(process.pid, signal);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
process.exit(code ?? 0);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function parseArgs(args) {
|
|
67
|
+
const launcher = {
|
|
68
|
+
help: false,
|
|
69
|
+
channel: 'stable'
|
|
70
|
+
};
|
|
71
|
+
const passthrough = [];
|
|
72
|
+
|
|
73
|
+
for (let i = 0; i < args.length; i++) {
|
|
74
|
+
const arg = args[i];
|
|
75
|
+
|
|
76
|
+
if (arg === '--') {
|
|
77
|
+
passthrough.push(...args.slice(i + 1));
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (arg === '--help-launcher') {
|
|
82
|
+
launcher.help = true;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (arg === '--channel') {
|
|
87
|
+
const value = args[i + 1];
|
|
88
|
+
if (value !== 'stable' && value !== 'dev') {
|
|
89
|
+
throw new Error('--channel must be stable or dev');
|
|
90
|
+
}
|
|
91
|
+
launcher.channel = value;
|
|
92
|
+
i++;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
passthrough.push(arg);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { launcher, passthrough };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function printHelp() {
|
|
103
|
+
console.log('@tlbx-ai/midterm launcher');
|
|
104
|
+
console.log('');
|
|
105
|
+
console.log('Usage: npx @tlbx-ai/midterm [--channel stable|dev] [-- <mt args...>]');
|
|
106
|
+
console.log('');
|
|
107
|
+
console.log('Launcher options:');
|
|
108
|
+
console.log(' --channel stable|dev Choose the release channel (default: stable)');
|
|
109
|
+
console.log(' --help-launcher Show launcher help');
|
|
110
|
+
console.log('');
|
|
111
|
+
console.log('All other arguments are passed to mt.');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function getPlatformTarget() {
|
|
115
|
+
if (process.platform === 'win32' && process.arch === 'x64') {
|
|
116
|
+
return {
|
|
117
|
+
assetName: 'mt-win-x64.zip',
|
|
118
|
+
binaryName: 'mt.exe',
|
|
119
|
+
hostBinaryName: 'mthost.exe'
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (process.platform === 'darwin' && process.arch === 'arm64') {
|
|
124
|
+
return {
|
|
125
|
+
assetName: 'mt-osx-arm64.tar.gz',
|
|
126
|
+
binaryName: 'mt',
|
|
127
|
+
hostBinaryName: 'mthost'
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (process.platform === 'darwin' && process.arch === 'x64') {
|
|
132
|
+
return {
|
|
133
|
+
assetName: 'mt-osx-x64.tar.gz',
|
|
134
|
+
binaryName: 'mt',
|
|
135
|
+
hostBinaryName: 'mthost'
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (process.platform === 'linux' && process.arch === 'x64') {
|
|
140
|
+
return {
|
|
141
|
+
assetName: 'mt-linux-x64.tar.gz',
|
|
142
|
+
binaryName: 'mt',
|
|
143
|
+
hostBinaryName: 'mthost'
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
throw new Error(`Unsupported platform: ${process.platform} ${process.arch}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function resolveRelease(channel) {
|
|
151
|
+
const headers = {
|
|
152
|
+
'User-Agent': '@tlbx-ai/midterm',
|
|
153
|
+
'Accept': 'application/vnd.github+json'
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
if (channel === 'stable') {
|
|
157
|
+
const release = await fetchJson(`${GITHUB_API}/releases/latest`, headers);
|
|
158
|
+
return mapRelease(release);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const releases = await fetchJson(`${GITHUB_API}/releases?per_page=50`, headers);
|
|
162
|
+
const prereleases = Array.isArray(releases) ? releases.filter((release) => release.prerelease) : [];
|
|
163
|
+
if (prereleases.length === 0) {
|
|
164
|
+
throw new Error('No dev releases found on GitHub');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
prereleases.sort((left, right) => compareVersions(right.tag_name, left.tag_name));
|
|
168
|
+
return mapRelease(prereleases[0]);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function mapRelease(release) {
|
|
172
|
+
if (!release || !release.tag_name || !Array.isArray(release.assets)) {
|
|
173
|
+
throw new Error('Unexpected GitHub release payload');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
tag: release.tag_name,
|
|
178
|
+
assets: release.assets
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function ensureInstalledRelease(release, target) {
|
|
183
|
+
const cacheRoot = getCacheRoot();
|
|
184
|
+
const versionDir = path.join(cacheRoot, sanitizeTag(release.tag));
|
|
185
|
+
const completeMarker = path.join(versionDir, '.complete');
|
|
186
|
+
const targetAsset = release.assets.find((asset) => asset.name === target.assetName);
|
|
187
|
+
|
|
188
|
+
if (!targetAsset || !targetAsset.browser_download_url) {
|
|
189
|
+
throw new Error(`Release ${release.tag} does not contain ${target.assetName}`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (fs.existsSync(completeMarker)) {
|
|
193
|
+
return versionDir;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
await fsp.mkdir(cacheRoot, { recursive: true });
|
|
197
|
+
|
|
198
|
+
const tempRoot = await fsp.mkdtemp(path.join(cacheRoot, 'staging-'));
|
|
199
|
+
const archivePath = path.join(tempRoot, target.assetName);
|
|
200
|
+
const extractDir = path.join(tempRoot, 'extract');
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
await fsp.mkdir(extractDir, { recursive: true });
|
|
204
|
+
console.error(`MidTerm ${release.tag}: downloading ${target.assetName}`);
|
|
205
|
+
await downloadFile(targetAsset.browser_download_url, archivePath);
|
|
206
|
+
console.error(`MidTerm ${release.tag}: extracting`);
|
|
207
|
+
extractArchive(archivePath, extractDir);
|
|
208
|
+
await ensureExecutableBits(extractDir, target);
|
|
209
|
+
await fsp.rm(versionDir, { recursive: true, force: true });
|
|
210
|
+
await fsp.rename(extractDir, versionDir);
|
|
211
|
+
await fsp.writeFile(completeMarker, `${release.tag}\n`, 'utf8');
|
|
212
|
+
return versionDir;
|
|
213
|
+
} catch (error) {
|
|
214
|
+
await fsp.rm(versionDir, { recursive: true, force: true }).catch(() => {});
|
|
215
|
+
throw error;
|
|
216
|
+
} finally {
|
|
217
|
+
await fsp.rm(tempRoot, { recursive: true, force: true }).catch(() => {});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function getCacheRoot() {
|
|
222
|
+
if (process.platform === 'win32') {
|
|
223
|
+
const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
224
|
+
return path.join(localAppData, 'MidTerm', 'npx-cache');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (process.platform === 'darwin') {
|
|
228
|
+
return path.join(os.homedir(), 'Library', 'Caches', 'MidTerm', 'npx-cache');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const xdgCache = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache');
|
|
232
|
+
return path.join(xdgCache, 'midterm', 'npx-cache');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function sanitizeTag(tag) {
|
|
236
|
+
return String(tag).replace(/^v/, '');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function fetchJson(url, headers) {
|
|
240
|
+
const response = await fetch(url, { headers });
|
|
241
|
+
if (!response.ok) {
|
|
242
|
+
throw new Error(`GitHub API request failed: ${response.status} ${response.statusText}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return response.json();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function downloadFile(url, filePath) {
|
|
249
|
+
const response = await fetch(url, {
|
|
250
|
+
headers: {
|
|
251
|
+
'User-Agent': '@tlbx-ai/midterm'
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
if (!response.ok) {
|
|
256
|
+
throw new Error(`Download failed: ${response.status} ${response.statusText}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
260
|
+
await fsp.writeFile(filePath, Buffer.from(arrayBuffer));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function extractArchive(archivePath, destinationPath) {
|
|
264
|
+
if (archivePath.endsWith('.zip')) {
|
|
265
|
+
const command = [
|
|
266
|
+
'-NoProfile',
|
|
267
|
+
'-Command',
|
|
268
|
+
`Expand-Archive -LiteralPath '${escapePowerShell(archivePath)}' -DestinationPath '${escapePowerShell(destinationPath)}' -Force`
|
|
269
|
+
];
|
|
270
|
+
const result = spawnSync('powershell', command, { stdio: 'inherit' });
|
|
271
|
+
if (result.status !== 0) {
|
|
272
|
+
throw new Error(`Failed to extract ${path.basename(archivePath)} with PowerShell`);
|
|
273
|
+
}
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const result = spawnSync('tar', ['-xzf', archivePath, '-C', destinationPath], { stdio: 'inherit' });
|
|
278
|
+
if (result.status !== 0) {
|
|
279
|
+
throw new Error(`Failed to extract ${path.basename(archivePath)} with tar`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function ensureExecutableBits(installDir, target) {
|
|
284
|
+
if (process.platform === 'win32') {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
await Promise.all([
|
|
289
|
+
fsp.chmod(path.join(installDir, target.binaryName), 0o755),
|
|
290
|
+
fsp.chmod(path.join(installDir, target.hostBinaryName), 0o755)
|
|
291
|
+
]);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function hasArg(args, name) {
|
|
295
|
+
return args.some((arg) => arg === name || arg.startsWith(`${name}=`));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function forwardSignal(child, signal) {
|
|
299
|
+
process.on(signal, () => {
|
|
300
|
+
if (!child.killed) {
|
|
301
|
+
child.kill(signal);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function escapePowerShell(value) {
|
|
307
|
+
return value.replace(/'/g, "''");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function compareVersions(leftTag, rightTag) {
|
|
311
|
+
const left = parseVersion(leftTag);
|
|
312
|
+
const right = parseVersion(rightTag);
|
|
313
|
+
|
|
314
|
+
for (let i = 0; i < 3; i++) {
|
|
315
|
+
if (left.base[i] !== right.base[i]) {
|
|
316
|
+
return left.base[i] - right.base[i];
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (left.prerelease === null && right.prerelease !== null) {
|
|
321
|
+
return 1;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (left.prerelease !== null && right.prerelease === null) {
|
|
325
|
+
return -1;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (left.prerelease === null && right.prerelease === null) {
|
|
329
|
+
return 0;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return left.prerelease - right.prerelease;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function parseVersion(tag) {
|
|
336
|
+
const clean = String(tag).replace(/^v/, '');
|
|
337
|
+
const [basePart, prereleasePart] = clean.split('-', 2);
|
|
338
|
+
const base = basePart.split('.').map((value) => Number.parseInt(value, 10) || 0);
|
|
339
|
+
const prereleaseMatch = prereleasePart ? prereleasePart.match(/\.(\d+)$/) : null;
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
base: [base[0] || 0, base[1] || 0, base[2] || 0],
|
|
343
|
+
prerelease: prereleaseMatch ? Number.parseInt(prereleaseMatch[1], 10) : null
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
main().catch((error) => {
|
|
348
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
349
|
+
console.error(`@tlbx-ai/midterm: ${message}`);
|
|
350
|
+
process.exit(1);
|
|
351
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tlbx-ai/midterm",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Launch MidTerm via npx by downloading the native binary for your platform",
|
|
5
|
+
"license": "AGPL-3.0-only",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/tlbx-ai/MidTerm.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/tlbx-ai/MidTerm",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/tlbx-ai/MidTerm/issues"
|
|
13
|
+
},
|
|
14
|
+
"bin": {
|
|
15
|
+
"midterm": "bin/midterm.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"bin",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"midterm",
|
|
29
|
+
"terminal",
|
|
30
|
+
"remote-terminal",
|
|
31
|
+
"browser-terminal",
|
|
32
|
+
"npx"
|
|
33
|
+
]
|
|
34
|
+
}
|