@tlbx-ai/midterm 8.2.1 → 8.2.3

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.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/bin/midterm.js +273 -19
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -33,3 +33,4 @@ Notes:
33
33
  - If you do not pass `--bind`, the launcher forces `127.0.0.1`
34
34
  - If you do not pass `--port`, the launcher opens `https://127.0.0.1:2000`
35
35
  - The launcher sets `MIDTERM_LAUNCH_MODE=npx` for the child process
36
+ - If you invoke `npx` from WSL but it resolves to Windows `node/npm`, the launcher detects the WSL working directory and runs the Linux MidTerm build inside that distro
package/bin/midterm.js CHANGED
@@ -25,15 +25,12 @@ async function main() {
25
25
  return;
26
26
  }
27
27
 
28
- const target = getPlatformTarget();
28
+ const runtime = await detectRuntime();
29
+ const target = await getPlatformTarget(runtime);
29
30
  const release = await resolveRelease(launcher.channel);
30
- const installDir = await ensureInstalledRelease(release, target);
31
- const mtPath = path.join(installDir, target.binaryName);
32
- const mthostPath = path.join(installDir, target.hostBinaryName);
33
-
34
- if (!fs.existsSync(mtPath) || !fs.existsSync(mthostPath)) {
35
- throw new Error(`Downloaded release is incomplete: expected ${target.binaryName} and ${target.hostBinaryName}`);
36
- }
31
+ const install = runtime.kind === 'wsl-interop'
32
+ ? await ensureInstalledReleaseInWsl(release, target, runtime)
33
+ : await ensureInstalledRelease(release, target);
37
34
 
38
35
  const childArgs = passthrough.slice();
39
36
  const explicitBind = getArgValue(childArgs, '--bind');
@@ -49,13 +46,16 @@ async function main() {
49
46
  MIDTERM_LAUNCH_MODE: 'npx',
50
47
  MIDTERM_NPX: '1',
51
48
  MIDTERM_NPX_CHANNEL: launcher.channel,
52
- MIDTERM_NPX_PACKAGE_VERSION: PACKAGE_VERSION
49
+ MIDTERM_NPX_PACKAGE_VERSION: PACKAGE_VERSION,
50
+ MIDTERM_NPX_RUNTIME: runtime.kind
53
51
  };
54
52
 
55
- const child = spawn(mtPath, childArgs, {
56
- stdio: 'inherit',
57
- env: childEnv
58
- });
53
+ const child = runtime.kind === 'wsl-interop'
54
+ ? spawnMidTermInWsl(runtime, install.mtPath, childArgs, childEnv)
55
+ : spawn(install.mtPath, childArgs, {
56
+ stdio: 'inherit',
57
+ env: childEnv
58
+ });
59
59
 
60
60
  if (launcher.openBrowser) {
61
61
  void openBrowserWhenReady(browserUrl);
@@ -130,7 +130,79 @@ function printHelp() {
130
130
  console.log('All other arguments are passed to mt.');
131
131
  }
132
132
 
133
- function getPlatformTarget() {
133
+ async function detectRuntime() {
134
+ const wslContext = await detectWslInteropContext();
135
+ if (wslContext) {
136
+ return wslContext;
137
+ }
138
+
139
+ return {
140
+ kind: 'native',
141
+ platform: process.platform,
142
+ arch: process.arch
143
+ };
144
+ }
145
+
146
+ async function detectWslInteropContext() {
147
+ if (process.platform !== 'win32') {
148
+ return null;
149
+ }
150
+
151
+ const parsed = parseWslUncPath(process.cwd());
152
+ if (!parsed) {
153
+ return null;
154
+ }
155
+
156
+ const linuxHome = getWslCommandOutput(parsed.distroName, ['pwd'], '~');
157
+ if (!linuxHome.startsWith('/')) {
158
+ throw new Error(`Failed to determine WSL home directory for ${parsed.distroName}`);
159
+ }
160
+
161
+ const archRaw = getWslCommandOutput(parsed.distroName, ['uname', '-m'], '/');
162
+
163
+ return {
164
+ kind: 'wsl-interop',
165
+ distroName: parsed.distroName,
166
+ linuxCwd: parsed.linuxPath,
167
+ linuxHome,
168
+ uncRoot: parsed.uncRoot,
169
+ arch: normalizeWslArchitecture(archRaw)
170
+ };
171
+ }
172
+
173
+ function parseWslUncPath(value) {
174
+ const normalized = String(value || '');
175
+ const match = normalized.match(/^\\\\wsl(?:\.localhost|\$)\\([^\\]+)(\\.*)?$/i);
176
+ if (!match) {
177
+ return null;
178
+ }
179
+
180
+ const distroName = match[1];
181
+ const suffix = match[2] || '';
182
+ const linuxPath = suffix
183
+ ? suffix.replace(/\\/g, '/')
184
+ : '/';
185
+
186
+ return {
187
+ distroName,
188
+ linuxPath,
189
+ uncRoot: `\\\\wsl.localhost\\${distroName}`
190
+ };
191
+ }
192
+
193
+ async function getPlatformTarget(runtime) {
194
+ if (runtime.kind === 'wsl-interop') {
195
+ if (runtime.arch === 'x64') {
196
+ return {
197
+ assetName: 'mt-linux-x64.tar.gz',
198
+ binaryName: 'mt',
199
+ hostBinaryName: 'mthost'
200
+ };
201
+ }
202
+
203
+ throw new Error(`Unsupported WSL platform: linux ${runtime.arch}`);
204
+ }
205
+
134
206
  if (process.platform === 'win32' && process.arch === 'x64') {
135
207
  return {
136
208
  assetName: 'mt-win-x64.zip',
@@ -208,8 +280,12 @@ async function ensureInstalledRelease(release, target) {
208
280
  throw new Error(`Release ${release.tag} does not contain ${target.assetName}`);
209
281
  }
210
282
 
283
+ const mtPath = path.join(versionDir, target.binaryName);
284
+ const mthostPath = path.join(versionDir, target.hostBinaryName);
285
+
211
286
  if (fs.existsSync(completeMarker)) {
212
- return versionDir;
287
+ ensureInstalledFilesExist(mtPath, mthostPath, target);
288
+ return { mtPath, mthostPath };
213
289
  }
214
290
 
215
291
  await fsp.mkdir(cacheRoot, { recursive: true });
@@ -228,7 +304,8 @@ async function ensureInstalledRelease(release, target) {
228
304
  await fsp.rm(versionDir, { recursive: true, force: true });
229
305
  await fsp.rename(extractDir, versionDir);
230
306
  await fsp.writeFile(completeMarker, `${release.tag}\n`, 'utf8');
231
- return versionDir;
307
+ ensureInstalledFilesExist(mtPath, mthostPath, target);
308
+ return { mtPath, mthostPath };
232
309
  } catch (error) {
233
310
  await fsp.rm(versionDir, { recursive: true, force: true }).catch(() => {});
234
311
  throw error;
@@ -237,6 +314,58 @@ async function ensureInstalledRelease(release, target) {
237
314
  }
238
315
  }
239
316
 
317
+ async function ensureInstalledReleaseInWsl(release, target, runtime) {
318
+ const cacheRootLinux = path.posix.join(runtime.linuxHome, '.cache', 'midterm', 'npx-cache');
319
+ const cacheRootUnc = toWslUncPath(runtime, cacheRootLinux);
320
+ const versionDirLinux = path.posix.join(cacheRootLinux, sanitizeTag(release.tag));
321
+ const versionDirUnc = toWslUncPath(runtime, versionDirLinux);
322
+ const completeMarkerUnc = toWslUncPath(runtime, path.posix.join(versionDirLinux, '.complete'));
323
+ const targetAsset = release.assets.find((asset) => asset.name === target.assetName);
324
+
325
+ if (!targetAsset || !targetAsset.browser_download_url) {
326
+ throw new Error(`Release ${release.tag} does not contain ${target.assetName}`);
327
+ }
328
+
329
+ const mtPath = path.posix.join(versionDirLinux, target.binaryName);
330
+ const mthostPath = path.posix.join(versionDirLinux, target.hostBinaryName);
331
+ const mtPathUnc = toWslUncPath(runtime, mtPath);
332
+ const mthostPathUnc = toWslUncPath(runtime, mthostPath);
333
+
334
+ if (fs.existsSync(completeMarkerUnc)) {
335
+ ensureInstalledFilesExist(mtPathUnc, mthostPathUnc, target);
336
+ return { mtPath, mthostPath };
337
+ }
338
+
339
+ await fsp.mkdir(cacheRootUnc, { recursive: true });
340
+
341
+ const tempName = `staging-${Date.now()}-${process.pid}-${Math.random().toString(16).slice(2)}`;
342
+ const tempRootLinux = path.posix.join(cacheRootLinux, tempName);
343
+ const tempRootUnc = toWslUncPath(runtime, tempRootLinux);
344
+ const archiveLinux = path.posix.join(tempRootLinux, target.assetName);
345
+ const archiveUnc = toWslUncPath(runtime, archiveLinux);
346
+ const extractLinux = path.posix.join(tempRootLinux, 'extract');
347
+ const extractUnc = toWslUncPath(runtime, extractLinux);
348
+
349
+ try {
350
+ await fsp.mkdir(extractUnc, { recursive: true });
351
+ console.error(`MidTerm ${release.tag}: downloading ${target.assetName}`);
352
+ await downloadFile(targetAsset.browser_download_url, archiveUnc);
353
+ console.error(`MidTerm ${release.tag}: extracting`);
354
+ runWslCommand(runtime, ['tar', '-xzf', archiveLinux, '-C', extractLinux], '/');
355
+ runWslCommand(runtime, ['chmod', '755', path.posix.join(extractLinux, target.binaryName), path.posix.join(extractLinux, target.hostBinaryName)], '/');
356
+ await fsp.rm(versionDirUnc, { recursive: true, force: true });
357
+ await fsp.rename(extractUnc, versionDirUnc);
358
+ await fsp.writeFile(completeMarkerUnc, `${release.tag}\n`, 'utf8');
359
+ ensureInstalledFilesExist(mtPathUnc, mthostPathUnc, target);
360
+ return { mtPath, mthostPath };
361
+ } catch (error) {
362
+ await fsp.rm(versionDirUnc, { recursive: true, force: true }).catch(() => {});
363
+ throw error;
364
+ } finally {
365
+ await fsp.rm(tempRootUnc, { recursive: true, force: true }).catch(() => {});
366
+ }
367
+ }
368
+
240
369
  function getCacheRoot() {
241
370
  if (process.platform === 'win32') {
242
371
  const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
@@ -286,14 +415,20 @@ function extractArchive(archivePath, destinationPath) {
286
415
  '-Command',
287
416
  `Expand-Archive -LiteralPath '${escapePowerShell(archivePath)}' -DestinationPath '${escapePowerShell(destinationPath)}' -Force`
288
417
  ];
289
- const result = spawnSync('powershell', command, { stdio: 'inherit' });
418
+ const result = spawnSync('powershell', command, {
419
+ stdio: 'inherit',
420
+ cwd: getWindowsSubprocessCwd()
421
+ });
290
422
  if (result.status !== 0) {
291
423
  throw new Error(`Failed to extract ${path.basename(archivePath)} with PowerShell`);
292
424
  }
293
425
  return;
294
426
  }
295
427
 
296
- const result = spawnSync('tar', ['-xzf', archivePath, '-C', destinationPath], { stdio: 'inherit' });
428
+ const result = spawnSync('tar', ['-xzf', archivePath, '-C', destinationPath], {
429
+ stdio: 'inherit',
430
+ cwd: getWindowsSubprocessCwd()
431
+ });
297
432
  if (result.status !== 0) {
298
433
  throw new Error(`Failed to extract ${path.basename(archivePath)} with tar`);
299
434
  }
@@ -427,7 +562,8 @@ function openUrl(url) {
427
562
  const result = spawn(command, args, {
428
563
  detached: true,
429
564
  stdio: 'ignore',
430
- windowsHide: true
565
+ windowsHide: true,
566
+ cwd: getWindowsSubprocessCwd()
431
567
  });
432
568
  result.on('error', (error) => {
433
569
  console.error(`@tlbx-ai/midterm: failed to open browser automatically: ${error.message}`);
@@ -435,6 +571,37 @@ function openUrl(url) {
435
571
  result.unref();
436
572
  }
437
573
 
574
+ function spawnMidTermInWsl(runtime, mtPath, childArgs, childEnv) {
575
+ const envArgs = ['env'];
576
+ const passthroughEnv = [
577
+ 'MIDTERM_LAUNCH_MODE',
578
+ 'MIDTERM_NPX',
579
+ 'MIDTERM_NPX_CHANNEL',
580
+ 'MIDTERM_NPX_PACKAGE_VERSION',
581
+ 'MIDTERM_NPX_RUNTIME'
582
+ ];
583
+
584
+ for (const key of passthroughEnv) {
585
+ if (childEnv[key]) {
586
+ envArgs.push(`${key}=${childEnv[key]}`);
587
+ }
588
+ }
589
+
590
+ envArgs.push(mtPath, ...childArgs);
591
+
592
+ return spawn('wsl.exe', [
593
+ '--distribution',
594
+ runtime.distroName,
595
+ '--cd',
596
+ runtime.linuxCwd,
597
+ '--exec',
598
+ ...envArgs
599
+ ], {
600
+ stdio: 'inherit',
601
+ cwd: getWindowsSubprocessCwd()
602
+ });
603
+ }
604
+
438
605
  function forwardSignal(child, signal) {
439
606
  process.on(signal, () => {
440
607
  if (!child.killed) {
@@ -447,6 +614,93 @@ function escapePowerShell(value) {
447
614
  return value.replace(/'/g, "''");
448
615
  }
449
616
 
617
+ function ensureInstalledFilesExist(mtPath, mthostPath, target) {
618
+ if (!fs.existsSync(mtPath) || !fs.existsSync(mthostPath)) {
619
+ throw new Error(`Downloaded release is incomplete: expected ${target.binaryName} and ${target.hostBinaryName}`);
620
+ }
621
+ }
622
+
623
+ function toWslUncPath(runtime, linuxPath) {
624
+ const normalized = String(linuxPath || '/');
625
+ const suffix = normalized === '/'
626
+ ? ''
627
+ : normalized.replace(/\//g, '\\');
628
+ return `${runtime.uncRoot}${suffix}`;
629
+ }
630
+
631
+ function getWslCommandOutput(distroName, commandArgs, cwd) {
632
+ const result = spawnSync('wsl.exe', [
633
+ '--distribution',
634
+ distroName,
635
+ '--cd',
636
+ cwd,
637
+ '--exec',
638
+ ...commandArgs
639
+ ], {
640
+ encoding: 'utf8',
641
+ cwd: getWindowsSubprocessCwd()
642
+ });
643
+
644
+ if (result.error) {
645
+ throw result.error;
646
+ }
647
+
648
+ if (result.status !== 0) {
649
+ const stderr = String(result.stderr || '').trim();
650
+ throw new Error(`WSL command failed: ${stderr || commandArgs.join(' ')}`);
651
+ }
652
+
653
+ return String(result.stdout || '').trim();
654
+ }
655
+
656
+ function runWslCommand(runtime, commandArgs, cwd) {
657
+ const result = spawnSync('wsl.exe', [
658
+ '--distribution',
659
+ runtime.distroName,
660
+ '--cd',
661
+ cwd,
662
+ '--exec',
663
+ ...commandArgs
664
+ ], {
665
+ stdio: 'inherit',
666
+ cwd: getWindowsSubprocessCwd()
667
+ });
668
+
669
+ if (result.error) {
670
+ throw result.error;
671
+ }
672
+
673
+ if (result.status !== 0) {
674
+ throw new Error(`WSL command failed: ${commandArgs.join(' ')}`);
675
+ }
676
+ }
677
+
678
+ function normalizeWslArchitecture(value) {
679
+ const normalized = String(value || '').trim().toLowerCase();
680
+ if (normalized === 'x86_64' || normalized === 'amd64') {
681
+ return 'x64';
682
+ }
683
+
684
+ if (normalized === 'aarch64' || normalized === 'arm64') {
685
+ return 'arm64';
686
+ }
687
+
688
+ return normalized || process.arch;
689
+ }
690
+
691
+ function getWindowsSubprocessCwd() {
692
+ if (process.platform !== 'win32') {
693
+ return undefined;
694
+ }
695
+
696
+ const cwd = process.cwd();
697
+ if (/^\\\\/.test(cwd)) {
698
+ return process.env.SystemRoot || 'C:\\Windows';
699
+ }
700
+
701
+ return undefined;
702
+ }
703
+
450
704
  function compareVersions(leftTag, rightTag) {
451
705
  const left = parseVersion(leftTag);
452
706
  const right = parseVersion(rightTag);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tlbx-ai/midterm",
3
- "version": "8.2.1",
3
+ "version": "8.2.3",
4
4
  "description": "Launch MidTerm via npx by downloading the native binary for your platform",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {