@getmonoceros/workbench 1.32.0 → 1.33.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/dist/bin.js CHANGED
@@ -2235,6 +2235,10 @@ function runtimeSupportsBrowserBridge(version) {
2235
2235
  if (!version) return false;
2236
2236
  return compareRuntimeVersions(version, MIN_RUNTIME_FOR_BROWSER_BRIDGE) >= 0;
2237
2237
  }
2238
+ function runtimeSupportsHostKeyPinning(version) {
2239
+ if (!version) return false;
2240
+ return compareRuntimeVersions(version, MIN_RUNTIME_FOR_HOST_KEY_PINNING) >= 0;
2241
+ }
2238
2242
  function descriptorOptionDefaults(options) {
2239
2243
  const out = {};
2240
2244
  for (const [key, spec] of Object.entries(options)) {
@@ -2361,7 +2365,7 @@ function deriveServiceName(image) {
2361
2365
  const noTag = lastSegment.split("@")[0].split(":")[0];
2362
2366
  return noTag.toLowerCase().replace(/[^a-z0-9_-]/g, "-");
2363
2367
  }
2364
- var DEFAULT_BASE_IMAGE, override, BASE_IMAGE, RUNTIME_IMAGE_REPO, DEFAULT_RUNTIME_VERSION, MIN_RUNTIME_FOR_SSH_ATTACH, MIN_RUNTIME_FOR_BROWSER_BRIDGE, DESCRIPTORS, BUILTIN_LANGUAGES, LANGUAGE_CATALOG, LANGUAGE_SPEC_RE, SERVICE_CATALOG, DEFERRED_SERVICE_PROFILE;
2368
+ var DEFAULT_BASE_IMAGE, override, BASE_IMAGE, RUNTIME_IMAGE_REPO, DEFAULT_RUNTIME_VERSION, MIN_RUNTIME_FOR_SSH_ATTACH, MIN_RUNTIME_FOR_HOST_KEY_PINNING, MIN_RUNTIME_FOR_BROWSER_BRIDGE, DESCRIPTORS, BUILTIN_LANGUAGES, LANGUAGE_CATALOG, LANGUAGE_SPEC_RE, SERVICE_CATALOG, DEFERRED_SERVICE_PROFILE;
2365
2369
  var init_catalog = __esm({
2366
2370
  "src/create/catalog.ts"() {
2367
2371
  "use strict";
@@ -2370,13 +2374,14 @@ var init_catalog = __esm({
2370
2374
  override = process.env.MONOCEROS_BASE_IMAGE_OVERRIDE?.trim();
2371
2375
  BASE_IMAGE = override && override.length > 0 ? override : DEFAULT_BASE_IMAGE;
2372
2376
  RUNTIME_IMAGE_REPO = "ghcr.io/getmonoceros/monoceros-runtime";
2373
- DEFAULT_RUNTIME_VERSION = true ? "1.3.3" : readFileSync3(
2377
+ DEFAULT_RUNTIME_VERSION = true ? "1.3.4" : readFileSync3(
2374
2378
  fileURLToPath2(
2375
2379
  new URL("../../../../images/runtime/VERSION", import.meta.url)
2376
2380
  ),
2377
2381
  "utf8"
2378
2382
  ).trim();
2379
2383
  MIN_RUNTIME_FOR_SSH_ATTACH = "1.2.0";
2384
+ MIN_RUNTIME_FOR_HOST_KEY_PINNING = "1.3.4";
2380
2385
  MIN_RUNTIME_FOR_BROWSER_BRIDGE = "1.3.3";
2381
2386
  DESCRIPTORS = loadDescriptorCatalogSync();
2382
2387
  BUILTIN_LANGUAGES = new Set(
@@ -2638,9 +2643,409 @@ var init_claude_settings = __esm({
2638
2643
  }
2639
2644
  });
2640
2645
 
2646
+ // src/devcontainer/ssh-attach.ts
2647
+ import { spawn as spawn4 } from "child_process";
2648
+ import { promises as fs7 } from "fs";
2649
+ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
2650
+ import os2 from "os";
2651
+ import path9 from "path";
2652
+ function sshHomeDir(home) {
2653
+ return path9.join(home, "ssh");
2654
+ }
2655
+ function sshConfigEntryPath(home, name) {
2656
+ return path9.join(sshHomeDir(home), "config.d", name);
2657
+ }
2658
+ function sshProxyScriptPath(home, name) {
2659
+ return path9.join(sshHomeDir(home), `exec-${name}.sh`);
2660
+ }
2661
+ function privateKeyPath(targetDir) {
2662
+ return path9.join(targetDir, ".monoceros", "ssh", "id_ed25519");
2663
+ }
2664
+ async function ensureKeypair(targetDir, name, keygen, logger) {
2665
+ const privateKey = privateKeyPath(targetDir);
2666
+ const publicKey = `${privateKey}.pub`;
2667
+ if (existsSync6(privateKey) && existsSync6(publicKey)) {
2668
+ return { privateKey, publicKey };
2669
+ }
2670
+ await fs7.mkdir(path9.dirname(privateKey), { recursive: true });
2671
+ try {
2672
+ const res = await keygen([
2673
+ "-t",
2674
+ "ed25519",
2675
+ "-N",
2676
+ "",
2677
+ "-f",
2678
+ privateKey,
2679
+ "-C",
2680
+ `monoceros-${name}`,
2681
+ "-q"
2682
+ ]);
2683
+ if (res.exitCode !== 0) {
2684
+ logger.warn(
2685
+ `ssh-keygen failed (exit ${res.exitCode})${res.stderr ? `: ${res.stderr.trim()}` : ""}; IDE SSH attach not set up.`
2686
+ );
2687
+ return null;
2688
+ }
2689
+ } catch (err) {
2690
+ logger.warn(
2691
+ `ssh-keygen not runnable (${err instanceof Error ? err.message : String(err)}); IDE SSH attach not set up.`
2692
+ );
2693
+ return null;
2694
+ }
2695
+ return { privateKey, publicKey };
2696
+ }
2697
+ function proxyScriptContent(name, targetDir) {
2698
+ return `#!/bin/sh
2699
+ # Generated by Monoceros (ADR 0022) - do not edit.
2700
+ # Portless SSH transport into the running dev container '${name}'.
2701
+ # Resolves the container by the devcontainer.local_folder label (the
2702
+ # same handle 'monoceros shell' uses), so it follows rebuilds.
2703
+ set -e
2704
+ # GUI launchers (IDEs, Claude Desktop) start a ProxyCommand with a
2705
+ # minimal PATH: on macOS launchd hands out only /usr/bin:/bin:/usr/sbin:
2706
+ # /sbin, so a bare \`docker\` (Docker Desktop installs it under
2707
+ # /usr/local/bin or /opt/homebrew/bin) isn't found. Prepend the common
2708
+ # Docker locations so the same script works from a login shell and a GUI
2709
+ # launcher alike.
2710
+ PATH="/usr/local/bin:/opt/homebrew/bin:/Applications/Docker.app/Contents/Resources/bin:$PATH"
2711
+ cid=$(docker ps -q \\
2712
+ --filter "label=devcontainer.local_folder=${targetDir}" \\
2713
+ --filter status=running | head -n1)
2714
+ if [ -z "$cid" ]; then
2715
+ echo "monoceros: dev container '${name}' is not running. Start it with: monoceros apply ${name}" >&2
2716
+ exit 1
2717
+ fi
2718
+ exec docker exec -i "$cid" socat - TCP:127.0.0.1:22
2719
+ `;
2720
+ }
2721
+ function configEntryContent(name, hostAlias, privateKey, proxyScript) {
2722
+ return `# Generated by Monoceros (ADR 0022) - do not edit.
2723
+ # Attach an SSH-capable IDE (Codium, IntelliJ, Zed) to host
2724
+ # '${hostAlias}', or run: ssh ${hostAlias}
2725
+ Host ${hostAlias}
2726
+ User node
2727
+ IdentityFile "${privateKey}"
2728
+ IdentitiesOnly yes
2729
+ StrictHostKeyChecking no
2730
+ UserKnownHostsFile /dev/null
2731
+ ProxyCommand "${proxyScript}"
2732
+ `;
2733
+ }
2734
+ async function ensureInclude(userSshDir, home) {
2735
+ const configPath = path9.join(userSshDir, "config");
2736
+ const includeTarget = path9.join(sshHomeDir(home), "config.d", "*");
2737
+ const includeLine = `Include "${includeTarget}"`;
2738
+ await fs7.mkdir(userSshDir, { recursive: true });
2739
+ let existing = "";
2740
+ try {
2741
+ existing = await fs7.readFile(configPath, "utf8");
2742
+ } catch {
2743
+ existing = "";
2744
+ }
2745
+ if (existing.includes(includeTarget)) return;
2746
+ const banner = "# Added by Monoceros (ADR 0022): per-container SSH attach entries.";
2747
+ const prefix = `${banner}
2748
+ ${includeLine}
2749
+ `;
2750
+ const next = existing.length > 0 ? `${prefix}
2751
+ ${existing}` : prefix;
2752
+ await fs7.writeFile(configPath, next, { mode: 384 });
2753
+ await fs7.chmod(configPath, 384).catch(() => {
2754
+ });
2755
+ }
2756
+ function runCapture(cmd, args) {
2757
+ return new Promise((resolve, reject) => {
2758
+ const child = spawn4(cmd, args, {
2759
+ stdio: ["ignore", "pipe", "ignore"]
2760
+ });
2761
+ let stdout = "";
2762
+ child.stdout.on("data", (c) => {
2763
+ stdout += c.toString();
2764
+ });
2765
+ child.on("error", reject);
2766
+ child.on("exit", (code) => resolve({ stdout, exitCode: code ?? 0 }));
2767
+ });
2768
+ }
2769
+ function isWsl() {
2770
+ try {
2771
+ if (process.env.WSL_DISTRO_NAME) return true;
2772
+ const rel = readFileSync4(
2773
+ "/proc/sys/kernel/osrelease",
2774
+ "utf8"
2775
+ ).toLowerCase();
2776
+ return rel.includes("microsoft") || rel.includes("wsl");
2777
+ } catch {
2778
+ return false;
2779
+ }
2780
+ }
2781
+ async function resolveWindowsProfile() {
2782
+ try {
2783
+ const r = await runCapture("cmd.exe", ["/c", "echo %USERPROFILE%"]);
2784
+ const homeWin = r.stdout.replace(/\r/g, "").trim();
2785
+ if (r.exitCode !== 0 || !homeWin || homeWin.includes("%USERPROFILE%")) {
2786
+ return null;
2787
+ }
2788
+ const w = await runCapture("wslpath", ["-u", homeWin]);
2789
+ const homeWsl = w.stdout.trim();
2790
+ const user = homeWin.split("\\").pop() ?? "";
2791
+ if (w.exitCode !== 0 || !homeWsl || !user) return null;
2792
+ return { homeWsl, homeWin, user };
2793
+ } catch {
2794
+ return null;
2795
+ }
2796
+ }
2797
+ async function realLockWindowsKey(winKeyPath, user) {
2798
+ await runCapture("icacls.exe", [
2799
+ winKeyPath,
2800
+ "/inheritance:r",
2801
+ "/grant:r",
2802
+ `${user}:R`
2803
+ ]);
2804
+ }
2805
+ function resolveWindowsDeps(d) {
2806
+ return {
2807
+ isWsl: d?.isWsl ?? isWsl,
2808
+ resolveProfile: d?.resolveProfile ?? resolveWindowsProfile,
2809
+ lockKey: d?.lockKey ?? realLockWindowsKey
2810
+ };
2811
+ }
2812
+ function windowsSshPort(name) {
2813
+ let h = 0;
2814
+ for (let i = 0; i < name.length; i++) {
2815
+ h = h * 31 + name.charCodeAt(i) >>> 0;
2816
+ }
2817
+ return 49152 + h % 16384;
2818
+ }
2819
+ function windowsHostBlock(hostAlias, keyWin, port) {
2820
+ const head = [
2821
+ `Host ${hostAlias}`,
2822
+ ` User node`,
2823
+ ` IdentityFile ${keyWin}`,
2824
+ ` IdentitiesOnly yes`,
2825
+ ` StrictHostKeyChecking no`,
2826
+ ` UserKnownHostsFile NUL`
2827
+ ];
2828
+ if (port !== null) {
2829
+ return [
2830
+ `Host ${hostAlias}`,
2831
+ ` HostName 127.0.0.1`,
2832
+ ` Port ${port}`,
2833
+ ...head.slice(1)
2834
+ ].join("\n");
2835
+ }
2836
+ return [
2837
+ ...head,
2838
+ ` ProxyCommand docker exec -i ${hostAlias} socat - TCP:127.0.0.1:22`
2839
+ ].join("\n");
2840
+ }
2841
+ function escapeRegExp2(s) {
2842
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2843
+ }
2844
+ function blockMarkers(hostAlias) {
2845
+ return {
2846
+ begin: `# >>> monoceros ${hostAlias} >>>`,
2847
+ end: `# <<< monoceros ${hostAlias} <<<`
2848
+ };
2849
+ }
2850
+ async function upsertMarkedBlock(configPath, hostAlias, body) {
2851
+ const { begin, end } = blockMarkers(hostAlias);
2852
+ const section = `${begin}
2853
+ ${body}
2854
+ ${end}`;
2855
+ await fs7.mkdir(path9.dirname(configPath), { recursive: true });
2856
+ let existing = "";
2857
+ try {
2858
+ existing = await fs7.readFile(configPath, "utf8");
2859
+ } catch {
2860
+ existing = "";
2861
+ }
2862
+ const re = new RegExp(`${escapeRegExp2(begin)}[\\s\\S]*?${escapeRegExp2(end)}`);
2863
+ if (re.test(existing)) {
2864
+ await fs7.writeFile(configPath, existing.replace(re, section));
2865
+ return;
2866
+ }
2867
+ const sep = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
2868
+ await fs7.writeFile(configPath, `${existing}${sep}${section}
2869
+ `);
2870
+ }
2871
+ async function removeMarkedBlock(configPath, hostAlias) {
2872
+ let existing = "";
2873
+ try {
2874
+ existing = await fs7.readFile(configPath, "utf8");
2875
+ } catch {
2876
+ return;
2877
+ }
2878
+ const { begin, end } = blockMarkers(hostAlias);
2879
+ const re = new RegExp(
2880
+ `\\n?${escapeRegExp2(begin)}[\\s\\S]*?${escapeRegExp2(end)}\\n?`,
2881
+ "g"
2882
+ );
2883
+ await fs7.writeFile(
2884
+ configPath,
2885
+ existing.replace(re, "\n").replace(/\n{3,}/g, "\n\n")
2886
+ );
2887
+ }
2888
+ async function setupWindowsBridge(name, hostAlias, privateKey, directPort, deps, logger) {
2889
+ if (!deps.isWsl()) return;
2890
+ const profile = await deps.resolveProfile();
2891
+ if (!profile) {
2892
+ logger.warn(
2893
+ "WSL detected but the Windows user profile could not be resolved; skipping the Windows SSH bridge."
2894
+ );
2895
+ return;
2896
+ }
2897
+ const monoDir = path9.join(profile.homeWsl, ".ssh", "monoceros");
2898
+ await fs7.mkdir(monoDir, { recursive: true });
2899
+ const keyDst = path9.join(monoDir, name);
2900
+ await fs7.rm(keyDst, { force: true });
2901
+ await fs7.copyFile(privateKey, keyDst);
2902
+ const keyWin = `${profile.homeWin}\\.ssh\\monoceros\\${name}`;
2903
+ await upsertMarkedBlock(
2904
+ path9.join(profile.homeWsl, ".ssh", "config"),
2905
+ hostAlias,
2906
+ windowsHostBlock(hostAlias, keyWin, directPort)
2907
+ );
2908
+ await deps.lockKey(keyWin, profile.user);
2909
+ logger.info(
2910
+ `Windows SSH bridge ready: \`ssh ${hostAlias}\` (Codium/Gateway too).`
2911
+ );
2912
+ }
2913
+ async function removeWindowsBridge(name, hostAlias, deps) {
2914
+ if (!deps.isWsl()) return;
2915
+ const profile = await deps.resolveProfile();
2916
+ if (!profile) return;
2917
+ await fs7.rm(path9.join(profile.homeWsl, ".ssh", "monoceros", name), {
2918
+ force: true
2919
+ });
2920
+ await removeMarkedBlock(
2921
+ path9.join(profile.homeWsl, ".ssh", "config"),
2922
+ hostAlias
2923
+ );
2924
+ }
2925
+ async function setupSshAttach(opts) {
2926
+ const hostAlias = `monoceros-${opts.name}`;
2927
+ const logger = opts.logger ?? { info: () => {
2928
+ }, warn: () => {
2929
+ } };
2930
+ const keygen = opts.keygen ?? realKeygen;
2931
+ const userSshDir = opts.userSshDir ?? path9.join(os2.homedir(), ".ssh");
2932
+ const keys = await ensureKeypair(opts.targetDir, opts.name, keygen, logger);
2933
+ if (!keys) return { hostAlias, configured: false };
2934
+ const proxyScript = sshProxyScriptPath(opts.home, opts.name);
2935
+ const configEntry = sshConfigEntryPath(opts.home, opts.name);
2936
+ await fs7.mkdir(path9.dirname(proxyScript), { recursive: true });
2937
+ await fs7.mkdir(path9.dirname(configEntry), { recursive: true });
2938
+ await fs7.writeFile(
2939
+ proxyScript,
2940
+ proxyScriptContent(opts.name, opts.targetDir),
2941
+ { mode: 493 }
2942
+ );
2943
+ await fs7.chmod(proxyScript, 493).catch(() => {
2944
+ });
2945
+ await fs7.writeFile(
2946
+ configEntry,
2947
+ configEntryContent(opts.name, hostAlias, keys.privateKey, proxyScript)
2948
+ );
2949
+ await ensureInclude(userSshDir, opts.home);
2950
+ try {
2951
+ await setupWindowsBridge(
2952
+ opts.name,
2953
+ hostAlias,
2954
+ keys.privateKey,
2955
+ opts.windowsDirectPort ?? null,
2956
+ resolveWindowsDeps(opts.windows),
2957
+ logger
2958
+ );
2959
+ } catch (err) {
2960
+ logger.warn(
2961
+ `Windows SSH bridge skipped: ${err instanceof Error ? err.message : String(err)}`
2962
+ );
2963
+ }
2964
+ return { hostAlias, configured: true };
2965
+ }
2966
+ async function upsertKnownHost(knownHostsPath, hostId, typeAndKey) {
2967
+ await fs7.mkdir(path9.dirname(knownHostsPath), { recursive: true });
2968
+ let existing = "";
2969
+ try {
2970
+ existing = await fs7.readFile(knownHostsPath, "utf8");
2971
+ } catch {
2972
+ existing = "";
2973
+ }
2974
+ const kept = existing.split("\n").filter((l) => l.trim().length > 0 && l.split(/\s+/)[0] !== hostId);
2975
+ const next = `${[...kept, `${hostId} ${typeAndKey}`].join("\n")}
2976
+ `;
2977
+ await fs7.writeFile(knownHostsPath, next);
2978
+ }
2979
+ async function recordHostKey(opts) {
2980
+ const pubPath = path9.join(
2981
+ opts.targetDir,
2982
+ ".monoceros",
2983
+ "ssh",
2984
+ "host",
2985
+ "ssh_host_ed25519_key.pub"
2986
+ );
2987
+ let line = "";
2988
+ try {
2989
+ line = (await fs7.readFile(pubPath, "utf8")).trim();
2990
+ } catch {
2991
+ return;
2992
+ }
2993
+ const parts = line.split(/\s+/);
2994
+ if (parts.length < 2) return;
2995
+ const typeAndKey = `${parts[0]} ${parts[1]}`;
2996
+ const userSshDir = opts.userSshDir ?? path9.join(os2.homedir(), ".ssh");
2997
+ await upsertKnownHost(
2998
+ path9.join(userSshDir, "known_hosts"),
2999
+ `monoceros-${opts.name}`,
3000
+ typeAndKey
3001
+ );
3002
+ const deps = resolveWindowsDeps(opts.windows);
3003
+ if (deps.isWsl()) {
3004
+ const profile = await deps.resolveProfile();
3005
+ if (profile) {
3006
+ await upsertKnownHost(
3007
+ path9.join(profile.homeWsl, ".ssh", "known_hosts"),
3008
+ `[127.0.0.1]:${windowsSshPort(opts.name)}`,
3009
+ typeAndKey
3010
+ );
3011
+ }
3012
+ }
3013
+ }
3014
+ async function removeSshAttach(home, name, windows) {
3015
+ await fs7.rm(sshProxyScriptPath(home, name), { force: true });
3016
+ await fs7.rm(sshConfigEntryPath(home, name), { force: true });
3017
+ try {
3018
+ await removeWindowsBridge(
3019
+ name,
3020
+ `monoceros-${name}`,
3021
+ resolveWindowsDeps(windows)
3022
+ );
3023
+ } catch {
3024
+ }
3025
+ }
3026
+ var realKeygen;
3027
+ var init_ssh_attach = __esm({
3028
+ "src/devcontainer/ssh-attach.ts"() {
3029
+ "use strict";
3030
+ realKeygen = (args) => {
3031
+ return new Promise((resolve, reject) => {
3032
+ const child = spawn4("ssh-keygen", args, {
3033
+ stdio: ["ignore", "ignore", "pipe"]
3034
+ });
3035
+ let stderr = "";
3036
+ child.stderr.on("data", (chunk) => {
3037
+ stderr += chunk.toString();
3038
+ });
3039
+ child.on("error", reject);
3040
+ child.on("exit", (code) => resolve({ exitCode: code ?? 0, stderr }));
3041
+ });
3042
+ };
3043
+ }
3044
+ });
3045
+
2641
3046
  // src/create/opencode-config.ts
2642
- import { existsSync as existsSync6, promises as fsp3 } from "fs";
2643
- import path9 from "path";
3047
+ import { existsSync as existsSync7, promises as fsp3 } from "fs";
3048
+ import path10 from "path";
2644
3049
  import { consola } from "consola";
2645
3050
  function parseOpencodeModel(model) {
2646
3051
  const idx = model.indexOf("/");
@@ -2659,16 +3064,16 @@ async function writeOpencodeConfig(targetDir, containerName2, features) {
2659
3064
  const apiToken = str("apiToken");
2660
3065
  const npm = str("npm");
2661
3066
  const baseUrl = str("baseUrl");
2662
- const file = path9.join(
3067
+ const file = path10.join(
2663
3068
  targetDir,
2664
3069
  "home",
2665
3070
  ".config",
2666
3071
  "opencode",
2667
3072
  "opencode.json"
2668
3073
  );
2669
- await fsp3.mkdir(path9.dirname(file), { recursive: true });
3074
+ await fsp3.mkdir(path10.dirname(file), { recursive: true });
2670
3075
  let config = {};
2671
- if (existsSync6(file)) {
3076
+ if (existsSync7(file)) {
2672
3077
  try {
2673
3078
  const txt = await fsp3.readFile(file, "utf8");
2674
3079
  if (txt.trim()) {
@@ -2799,8 +3204,8 @@ var init_opencode_config = __esm({
2799
3204
  });
2800
3205
 
2801
3206
  // src/create/scaffold.ts
2802
- import { existsSync as existsSync7, promises as fs7 } from "fs";
2803
- import path10 from "path";
3207
+ import { existsSync as existsSync8, promises as fs8 } from "fs";
3208
+ import path11 from "path";
2804
3209
  function deriveRepoName(url) {
2805
3210
  const lastSep = Math.max(url.lastIndexOf("/"), url.lastIndexOf(":"));
2806
3211
  const tail = url.slice(lastSep + 1);
@@ -2929,7 +3334,7 @@ function featuresSourceRoot() {
2929
3334
  const override2 = process.env.MONOCEROS_FEATURES_DIR_OVERRIDE?.trim();
2930
3335
  if (override2 && override2.length > 0) return override2;
2931
3336
  const checkout = workbenchCheckoutRoot();
2932
- return checkout ? path10.join(checkout, "components", "features") : null;
3337
+ return checkout ? path11.join(checkout, "components", "features") : null;
2933
3338
  }
2934
3339
  function resolveFeatures(opts) {
2935
3340
  const resolved = [];
@@ -2978,8 +3383,8 @@ function resolveFeatures(opts) {
2978
3383
  const { paths, files } = descriptorPersistentHome(descriptor);
2979
3384
  const workspaceEnv = descriptor?.feature?.workspaceEnv ?? [];
2980
3385
  const sourceRoot = featuresSourceRoot();
2981
- const localSourceDir = sourceRoot ? path10.join(sourceRoot, name) : null;
2982
- if (descriptor && localSourceDir && existsSync7(localSourceDir)) {
3386
+ const localSourceDir = sourceRoot ? path11.join(sourceRoot, name) : null;
3387
+ if (descriptor && localSourceDir && existsSync8(localSourceDir)) {
2983
3388
  resolved.push({
2984
3389
  devcontainerKey: `./features/${name}`,
2985
3390
  options,
@@ -3153,6 +3558,9 @@ function ideStateVolumesForRuntime(name, version) {
3153
3558
  (v) => compareRuntimeVersions(version, v.minRuntime) >= 0
3154
3559
  );
3155
3560
  }
3561
+ function windowsBridgePort(opts) {
3562
+ return runtimeSupportsHostKeyPinning(opts.runtimeVersion) && isWsl() ? windowsSshPort(opts.name) : null;
3563
+ }
3156
3564
  function buildDevcontainerJson(opts, dockerMode = "rootful") {
3157
3565
  const resolvedFeatures = resolveFeatures(opts);
3158
3566
  const features = {};
@@ -3185,7 +3593,11 @@ function buildDevcontainerJson(opts, dockerMode = "rootful") {
3185
3593
  }
3186
3594
  } : void 0;
3187
3595
  const sshPostStart = runtimeSupportsSshAttach(opts.runtimeVersion) ? { postStartCommand: "sudo /usr/local/bin/monoceros-sshd-up.sh" } : {};
3188
- const workspaceEnv = featureWorkspaceEnv(resolvedFeatures);
3596
+ const sshBridgePort = windowsBridgePort(opts);
3597
+ const workspaceEnv = {
3598
+ ...featureWorkspaceEnv(resolvedFeatures),
3599
+ ...sshBridgePort ? { MONOCEROS_SSH_PUBLISH_PORT: String(sshBridgePort) } : {}
3600
+ };
3189
3601
  const containerEnvField = Object.keys(workspaceEnv).length > 0 ? { containerEnv: workspaceEnv } : void 0;
3190
3602
  if (needsCompose(opts)) {
3191
3603
  const eagerServices = opts.services.filter((s) => !serviceDefersStart(s.name)).map((s) => s.name);
@@ -3218,6 +3630,9 @@ function buildDevcontainerJson(opts, dockerMode = "rootful") {
3218
3630
  runArgs.push("--network=monoceros-proxy");
3219
3631
  runArgs.push(`--network-alias=${opts.name}`);
3220
3632
  }
3633
+ if (sshBridgePort !== null) {
3634
+ runArgs.push("-p", `127.0.0.1:${sshBridgePort}:${sshBridgePort}`);
3635
+ }
3221
3636
  return {
3222
3637
  name: opts.name,
3223
3638
  image: resolveRuntimeImage(opts.runtimeVersion),
@@ -3248,6 +3663,7 @@ function composeVolumeSource(spec, serviceName) {
3248
3663
  function buildComposeYaml(opts, dockerMode = "rootful") {
3249
3664
  void dockerMode;
3250
3665
  const hasPorts = (opts.ports?.length ?? 0) > 0;
3666
+ const sshBridgePort = windowsBridgePort(opts);
3251
3667
  const lines = ["services:"];
3252
3668
  const ideVolumes = ideStateVolumesForRuntime(opts.name, opts.runtimeVersion);
3253
3669
  lines.push(" workspace:");
@@ -3256,6 +3672,10 @@ function buildComposeYaml(opts, dockerMode = "rootful") {
3256
3672
  lines.push(" command: 'sleep infinity'");
3257
3673
  lines.push(" cap_add:");
3258
3674
  lines.push(" - NET_ADMIN");
3675
+ if (sshBridgePort !== null) {
3676
+ lines.push(" ports:");
3677
+ lines.push(` - "127.0.0.1:${sshBridgePort}:${sshBridgePort}"`);
3678
+ }
3259
3679
  if (hasPorts) {
3260
3680
  lines.push(" networks:");
3261
3681
  lines.push(" default: {}");
@@ -3280,7 +3700,8 @@ function buildComposeYaml(opts, dockerMode = "rootful") {
3280
3700
  }
3281
3701
  const wsEnv = {
3282
3702
  ...serviceConnectionEnv(opts.services),
3283
- ...featureWorkspaceEnv(resolvedFeatures)
3703
+ ...featureWorkspaceEnv(resolvedFeatures),
3704
+ ...sshBridgePort ? { MONOCEROS_SSH_PUBLISH_PORT: String(sshBridgePort) } : {}
3284
3705
  };
3285
3706
  const wsEnvKeys = Object.keys(wsEnv);
3286
3707
  if (wsEnvKeys.length > 0) {
@@ -3558,125 +3979,125 @@ function buildPostCreateScript(opts) {
3558
3979
  return lines.join("\n") + "\n";
3559
3980
  }
3560
3981
  async function writePostCreateScript(devcontainerDir, opts) {
3561
- const dest = path10.join(devcontainerDir, "post-create.sh");
3562
- await fs7.writeFile(dest, buildPostCreateScript(opts));
3563
- await fs7.chmod(dest, 493);
3982
+ const dest = path11.join(devcontainerDir, "post-create.sh");
3983
+ await fs8.writeFile(dest, buildPostCreateScript(opts));
3984
+ await fs8.chmod(dest, 493);
3564
3985
  }
3565
3986
  async function writeIfChanged(filePath, content) {
3566
3987
  try {
3567
- if (await fs7.readFile(filePath, "utf8") === content) return false;
3988
+ if (await fs8.readFile(filePath, "utf8") === content) return false;
3568
3989
  } catch {
3569
3990
  }
3570
- await fs7.writeFile(filePath, content);
3991
+ await fs8.writeFile(filePath, content);
3571
3992
  return true;
3572
3993
  }
3573
3994
  async function writeScaffold(opts, targetDir, scaffoldOpts = {}) {
3574
3995
  const dockerMode = scaffoldOpts.dockerMode ?? "rootful";
3575
- const devcontainerDir = path10.join(targetDir, ".devcontainer");
3576
- const monocerosDir = path10.join(targetDir, ".monoceros");
3577
- const projectsDir = path10.join(targetDir, "projects");
3578
- const homeDir = path10.join(targetDir, "home");
3579
- const dataDir = path10.join(targetDir, "data");
3580
- await fs7.mkdir(devcontainerDir, { recursive: true });
3581
- await fs7.mkdir(monocerosDir, { recursive: true });
3582
- await fs7.mkdir(projectsDir, { recursive: true });
3583
- await fs7.mkdir(homeDir, { recursive: true });
3996
+ const devcontainerDir = path11.join(targetDir, ".devcontainer");
3997
+ const monocerosDir = path11.join(targetDir, ".monoceros");
3998
+ const projectsDir = path11.join(targetDir, "projects");
3999
+ const homeDir = path11.join(targetDir, "home");
4000
+ const dataDir = path11.join(targetDir, "data");
4001
+ await fs8.mkdir(devcontainerDir, { recursive: true });
4002
+ await fs8.mkdir(monocerosDir, { recursive: true });
4003
+ await fs8.mkdir(projectsDir, { recursive: true });
4004
+ await fs8.mkdir(homeDir, { recursive: true });
3584
4005
  if (needsCompose(opts)) {
3585
- await fs7.mkdir(dataDir, { recursive: true });
4006
+ await fs8.mkdir(dataDir, { recursive: true });
3586
4007
  for (const svc of opts.services) {
3587
4008
  const hasDataVolume = svc.volumes.some((v) => v.split(":")[0] === "data");
3588
4009
  if (hasDataVolume) {
3589
- await fs7.mkdir(path10.join(dataDir, svc.name), { recursive: true });
4010
+ await fs8.mkdir(path11.join(dataDir, svc.name), { recursive: true });
3590
4011
  }
3591
4012
  }
3592
4013
  }
3593
- const containerGitignore = path10.join(targetDir, ".gitignore");
3594
- await fs7.writeFile(
4014
+ const containerGitignore = path11.join(targetDir, ".gitignore");
4015
+ await fs8.writeFile(
3595
4016
  containerGitignore,
3596
4017
  "/home/\n/.monoceros/\n/data/\n/AGENTS.md\n/CLAUDE.md\n"
3597
4018
  );
3598
- const gitkeep = path10.join(projectsDir, ".gitkeep");
3599
- if (!existsSync7(gitkeep)) {
3600
- await fs7.writeFile(gitkeep, "");
4019
+ const gitkeep = path11.join(projectsDir, ".gitkeep");
4020
+ if (!existsSync8(gitkeep)) {
4021
+ await fs8.writeFile(gitkeep, "");
3601
4022
  }
3602
- await fs7.writeFile(
3603
- path10.join(monocerosDir, ".gitignore"),
4023
+ await fs8.writeFile(
4024
+ path11.join(monocerosDir, ".gitignore"),
3604
4025
  "git-credentials*\ngitconfig\n"
3605
4026
  );
3606
4027
  const devcontainerJson = buildDevcontainerJson(opts, dockerMode);
3607
4028
  await writeIfChanged(
3608
- path10.join(devcontainerDir, "devcontainer.json"),
4029
+ path11.join(devcontainerDir, "devcontainer.json"),
3609
4030
  JSON.stringify(devcontainerJson, null, 2) + "\n"
3610
4031
  );
3611
- const featuresDir = path10.join(devcontainerDir, "features");
3612
- if (existsSync7(featuresDir)) {
3613
- await fs7.rm(featuresDir, { recursive: true, force: true });
4032
+ const featuresDir = path11.join(devcontainerDir, "features");
4033
+ if (existsSync8(featuresDir)) {
4034
+ await fs8.rm(featuresDir, { recursive: true, force: true });
3614
4035
  }
3615
4036
  const resolvedFeatures = resolveFeatures(opts);
3616
4037
  for (const f of resolvedFeatures) {
3617
4038
  if (!f.localSourceDir || !f.localName) continue;
3618
- const dest = path10.join(featuresDir, f.localName);
3619
- await fs7.mkdir(dest, { recursive: true });
3620
- const entries = await fs7.readdir(f.localSourceDir, { withFileTypes: true });
4039
+ const dest = path11.join(featuresDir, f.localName);
4040
+ await fs8.mkdir(dest, { recursive: true });
4041
+ const entries = await fs8.readdir(f.localSourceDir, { withFileTypes: true });
3621
4042
  for (const entry2 of entries) {
3622
4043
  if (!entry2.isFile()) continue;
3623
4044
  if (entry2.name === "component.yml" || entry2.name === "devcontainer-feature.json") {
3624
4045
  continue;
3625
4046
  }
3626
- await fs7.cp(
3627
- path10.join(f.localSourceDir, entry2.name),
3628
- path10.join(dest, entry2.name)
4047
+ await fs8.cp(
4048
+ path11.join(f.localSourceDir, entry2.name),
4049
+ path11.join(dest, entry2.name)
3629
4050
  );
3630
4051
  }
3631
4052
  if (f.generatedManifest) {
3632
- await fs7.writeFile(
3633
- path10.join(dest, "devcontainer-feature.json"),
4053
+ await fs8.writeFile(
4054
+ path11.join(dest, "devcontainer-feature.json"),
3634
4055
  JSON.stringify(f.generatedManifest, null, 2) + "\n"
3635
4056
  );
3636
4057
  }
3637
4058
  }
3638
4059
  for (const f of resolvedFeatures) {
3639
4060
  for (const sub of f.persistentHomePaths) {
3640
- await fs7.mkdir(path10.join(homeDir, sub), { recursive: true });
4061
+ await fs8.mkdir(path11.join(homeDir, sub), { recursive: true });
3641
4062
  }
3642
4063
  for (const entry2 of f.persistentHomeFiles) {
3643
- const filePath = path10.join(homeDir, entry2.path);
3644
- await fs7.mkdir(path10.dirname(filePath), { recursive: true });
3645
- if (!existsSync7(filePath)) {
3646
- await fs7.writeFile(filePath, entry2.initialContent);
4064
+ const filePath = path11.join(homeDir, entry2.path);
4065
+ await fs8.mkdir(path11.dirname(filePath), { recursive: true });
4066
+ if (!existsSync8(filePath)) {
4067
+ await fs8.writeFile(filePath, entry2.initialContent);
3647
4068
  }
3648
4069
  }
3649
4070
  }
3650
4071
  await writeClaudePermissionMode(targetDir, opts.features);
3651
4072
  await writeOpencodeConfig(targetDir, opts.name, opts.features);
3652
4073
  await writePostCreateScript(devcontainerDir, opts);
3653
- const composePath = path10.join(devcontainerDir, "compose.yaml");
4074
+ const composePath = path11.join(devcontainerDir, "compose.yaml");
3654
4075
  if (needsCompose(opts)) {
3655
4076
  await writeIfChanged(composePath, buildComposeYaml(opts, dockerMode));
3656
- } else if (existsSync7(composePath)) {
3657
- await fs7.rm(composePath);
4077
+ } else if (existsSync8(composePath)) {
4078
+ await fs8.rm(composePath);
3658
4079
  }
3659
- const workspacePath = path10.join(targetDir, `${opts.name}.code-workspace`);
4080
+ const workspacePath = path11.join(targetDir, `${opts.name}.code-workspace`);
3660
4081
  let existingWorkspace;
3661
4082
  try {
3662
- const raw = await fs7.readFile(workspacePath, "utf8");
4083
+ const raw = await fs8.readFile(workspacePath, "utf8");
3663
4084
  existingWorkspace = JSON.parse(raw);
3664
4085
  } catch {
3665
4086
  existingWorkspace = void 0;
3666
4087
  }
3667
4088
  const generated = buildCodeWorkspaceJson(opts);
3668
4089
  const merged = mergeCodeWorkspace(existingWorkspace, generated);
3669
- await fs7.writeFile(workspacePath, JSON.stringify(merged, null, 2) + "\n");
3670
- const vscodeDir = path10.join(targetDir, ".vscode");
3671
- const settingsPath = path10.join(vscodeDir, "settings.json");
4090
+ await fs8.writeFile(workspacePath, JSON.stringify(merged, null, 2) + "\n");
4091
+ const vscodeDir = path11.join(targetDir, ".vscode");
4092
+ const settingsPath = path11.join(vscodeDir, "settings.json");
3672
4093
  let existingSettings;
3673
4094
  try {
3674
- existingSettings = JSON.parse(await fs7.readFile(settingsPath, "utf8"));
4095
+ existingSettings = JSON.parse(await fs8.readFile(settingsPath, "utf8"));
3675
4096
  } catch {
3676
4097
  existingSettings = void 0;
3677
4098
  }
3678
- await fs7.mkdir(vscodeDir, { recursive: true });
3679
- await fs7.writeFile(
4099
+ await fs8.mkdir(vscodeDir, { recursive: true });
4100
+ await fs8.writeFile(
3680
4101
  settingsPath,
3681
4102
  JSON.stringify(mergeVscodeSettings(existingSettings), null, 2) + "\n"
3682
4103
  );
@@ -3690,6 +4111,7 @@ var init_scaffold = __esm({
3690
4111
  init_load_sync();
3691
4112
  init_generate_manifest();
3692
4113
  init_claude_settings();
4114
+ init_ssh_attach();
3693
4115
  init_opencode_config();
3694
4116
  init_catalog();
3695
4117
  APT_PACKAGE_NAME_RE2 = /^[a-z0-9][a-z0-9.+-]*$/;
@@ -4193,10 +4615,10 @@ var init_yml = __esm({
4193
4615
  });
4194
4616
 
4195
4617
  // src/modify/index.ts
4196
- import { promises as fs8 } from "fs";
4618
+ import { promises as fs9 } from "fs";
4197
4619
  import { consola as consola2 } from "consola";
4198
4620
  import { createPatch } from "diff";
4199
- import path11 from "path";
4621
+ import path12 from "path";
4200
4622
  function runAddLanguage(input) {
4201
4623
  const spec = parseLanguageSpec(input.language);
4202
4624
  if (!spec || !BUILTIN_LANGUAGES.has(spec.name) && !LANGUAGE_CATALOG[spec.name]) {
@@ -4450,7 +4872,7 @@ async function tryCloneInRunningContainer(input, entry2) {
4450
4872
  logger.info(
4451
4873
  `Cloned ${entry2.url} into /workspaces/${containerName2}/${targetRel} inside the running container.`
4452
4874
  );
4453
- void path11;
4875
+ void path12;
4454
4876
  }
4455
4877
  function shquote(value) {
4456
4878
  return `'${value.replace(/'/g, `'\\''`)}'`;
@@ -4662,7 +5084,7 @@ async function mutate(opts, apply) {
4662
5084
  const logger = opts.logger ?? defaultLogger();
4663
5085
  let oldText;
4664
5086
  try {
4665
- oldText = await fs8.readFile(ymlPath, "utf8");
5087
+ oldText = await fs9.readFile(ymlPath, "utf8");
4666
5088
  } catch {
4667
5089
  throw new Error(
4668
5090
  `No such config: ${ymlPath}. Run \`monoceros init <template> ${opts.name}\` first.`
@@ -4687,7 +5109,7 @@ async function mutate(opts, apply) {
4687
5109
  return { status: "aborted" };
4688
5110
  }
4689
5111
  }
4690
- await fs8.writeFile(ymlPath, newText, "utf8");
5112
+ await fs9.writeFile(ymlPath, newText, "utf8");
4691
5113
  logger.success(`Updated ${ymlPath}.`);
4692
5114
  logger.info(
4693
5115
  `Run \`monoceros apply ${opts.name}\` to rebuild the dev-container and pick up the change.`
@@ -5235,8 +5657,8 @@ var init_add_service = __esm({
5235
5657
  });
5236
5658
 
5237
5659
  // src/config/state.ts
5238
- import { promises as fs9 } from "fs";
5239
- import path12 from "path";
5660
+ import { promises as fs10 } from "fs";
5661
+ import path13 from "path";
5240
5662
  function buildStateFile(opts) {
5241
5663
  return {
5242
5664
  schemaVersion: CONFIG_SCHEMA_VERSION,
@@ -5247,20 +5669,20 @@ function buildStateFile(opts) {
5247
5669
  };
5248
5670
  }
5249
5671
  function stateFilePath(targetDir) {
5250
- return path12.join(targetDir, ".monoceros", "state.json");
5672
+ return path13.join(targetDir, ".monoceros", "state.json");
5251
5673
  }
5252
5674
  async function readStateFile(targetDir) {
5253
5675
  try {
5254
- const content = await fs9.readFile(stateFilePath(targetDir), "utf8");
5676
+ const content = await fs10.readFile(stateFilePath(targetDir), "utf8");
5255
5677
  return JSON.parse(content);
5256
5678
  } catch {
5257
5679
  return void 0;
5258
5680
  }
5259
5681
  }
5260
5682
  async function writeStateFile(targetDir, state) {
5261
- const monocerosDir = path12.join(targetDir, ".monoceros");
5262
- await fs9.mkdir(monocerosDir, { recursive: true });
5263
- await fs9.writeFile(
5683
+ const monocerosDir = path13.join(targetDir, ".monoceros");
5684
+ await fs10.mkdir(monocerosDir, { recursive: true });
5685
+ await fs10.writeFile(
5264
5686
  stateFilePath(targetDir),
5265
5687
  JSON.stringify(state, null, 2) + "\n"
5266
5688
  );
@@ -5366,15 +5788,15 @@ var init_transform = __esm({
5366
5788
  });
5367
5789
 
5368
5790
  // src/devcontainer/browser-bridge.ts
5369
- import { spawn as spawn4 } from "child_process";
5370
- import { existsSync as existsSync8, promises as fsp4, readFileSync as readFileSync4 } from "fs";
5791
+ import { spawn as spawn5 } from "child_process";
5792
+ import { existsSync as existsSync9, promises as fsp4, readFileSync as readFileSync5 } from "fs";
5371
5793
  import http from "http";
5372
- import path13 from "path";
5794
+ import path14 from "path";
5373
5795
  function relayDir(root) {
5374
- return path13.join(root, RELAY_DIRNAME);
5796
+ return path14.join(root, RELAY_DIRNAME);
5375
5797
  }
5376
5798
  function relayUrlFile(root) {
5377
- return path13.join(relayDir(root), "url");
5799
+ return path14.join(relayDir(root), "url");
5378
5800
  }
5379
5801
  function parseCallbackTarget(authUrl) {
5380
5802
  try {
@@ -5416,7 +5838,7 @@ function openInBrowser(url) {
5416
5838
  const platform = process.platform;
5417
5839
  const [cmd, args] = platform === "darwin" ? ["open", [url]] : platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
5418
5840
  try {
5419
- const child = spawn4(cmd, args, {
5841
+ const child = spawn5(cmd, args, {
5420
5842
  stdio: "ignore",
5421
5843
  detached: true
5422
5844
  });
@@ -5428,7 +5850,7 @@ function openInBrowser(url) {
5428
5850
  }
5429
5851
  async function startBrowserBridge(opts) {
5430
5852
  const dir = relayDir(opts.root);
5431
- const relayScript = path13.join(dir, "xdg-open");
5853
+ const relayScript = path14.join(dir, "xdg-open");
5432
5854
  const urlFile = relayUrlFile(opts.root);
5433
5855
  await fsp4.mkdir(dir, { recursive: true });
5434
5856
  await fsp4.rm(urlFile, { force: true });
@@ -5487,10 +5909,10 @@ function watchRelayUrl(opts) {
5487
5909
  servers.push(server);
5488
5910
  };
5489
5911
  const poll = setInterval(() => {
5490
- if (!existsSync8(opts.urlFile)) return;
5912
+ if (!existsSync9(opts.urlFile)) return;
5491
5913
  let content = "";
5492
5914
  try {
5493
- content = readFileSync4(opts.urlFile, "utf8");
5915
+ content = readFileSync5(opts.urlFile, "utf8");
5494
5916
  } catch {
5495
5917
  return;
5496
5918
  }
@@ -5622,19 +6044,19 @@ var init_runtime_pull_hint = __esm({
5622
6044
  });
5623
6045
 
5624
6046
  // src/devcontainer/cli.ts
5625
- import { spawn as spawn5 } from "child_process";
5626
- import { readFileSync as readFileSync5 } from "fs";
6047
+ import { spawn as spawn6 } from "child_process";
6048
+ import { readFileSync as readFileSync6 } from "fs";
5627
6049
  import { createRequire } from "module";
5628
- import path14 from "path";
6050
+ import path15 from "path";
5629
6051
  function devcontainerCliPath() {
5630
6052
  if (cachedBinaryPath) return cachedBinaryPath;
5631
6053
  const pkgJsonPath = require_.resolve("@devcontainers/cli/package.json");
5632
- const pkg = JSON.parse(readFileSync5(pkgJsonPath, "utf8"));
6054
+ const pkg = JSON.parse(readFileSync6(pkgJsonPath, "utf8"));
5633
6055
  const binEntry = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.devcontainer ?? "";
5634
6056
  if (!binEntry) {
5635
6057
  throw new Error("Could not resolve @devcontainers/cli bin entry.");
5636
6058
  }
5637
- cachedBinaryPath = path14.resolve(path14.dirname(pkgJsonPath), binEntry);
6059
+ cachedBinaryPath = path15.resolve(path15.dirname(pkgJsonPath), binEntry);
5638
6060
  return cachedBinaryPath;
5639
6061
  }
5640
6062
  var require_, cachedBinaryPath, spawnDevcontainer;
@@ -5650,7 +6072,7 @@ var init_cli = __esm({
5650
6072
  const env = { ...process.env, DOCKER_CLI_HINTS: "false" };
5651
6073
  return new Promise((resolve, reject) => {
5652
6074
  if (options.interactive) {
5653
- const child2 = spawn5(process.execPath, [binPath, ...args], {
6075
+ const child2 = spawn6(process.execPath, [binPath, ...args], {
5654
6076
  cwd,
5655
6077
  env,
5656
6078
  stdio: "inherit"
@@ -5659,7 +6081,7 @@ var init_cli = __esm({
5659
6081
  child2.on("exit", (code) => resolve(code ?? 0));
5660
6082
  return;
5661
6083
  }
5662
- const child = spawn5(process.execPath, [binPath, ...args], {
6084
+ const child = spawn6(process.execPath, [binPath, ...args], {
5663
6085
  cwd,
5664
6086
  env,
5665
6087
  stdio: ["ignore", "pipe", "pipe"]
@@ -5707,14 +6129,14 @@ var init_cli = __esm({
5707
6129
  });
5708
6130
 
5709
6131
  // src/devcontainer/compose.ts
5710
- import { spawn as spawn6 } from "child_process";
5711
- import { existsSync as existsSync9 } from "fs";
5712
- import path15 from "path";
6132
+ import { spawn as spawn7 } from "child_process";
6133
+ import { existsSync as existsSync10 } from "fs";
6134
+ import path16 from "path";
5713
6135
  import { Writable } from "stream";
5714
6136
  import { consola as consola10 } from "consola";
5715
6137
  function spawnDockerComposeTo(opts) {
5716
6138
  return (args, cwd) => new Promise((resolve, reject) => {
5717
- const child = spawn6("docker", ["compose", ...args], {
6139
+ const child = spawn7("docker", ["compose", ...args], {
5718
6140
  cwd,
5719
6141
  stdio: ["inherit", "pipe", "pipe"]
5720
6142
  });
@@ -5778,22 +6200,22 @@ async function cleanupDockerObjects(opts) {
5778
6200
  return { exitCode: rmExit, removedIds: ids };
5779
6201
  }
5780
6202
  function composeProjectName(root) {
5781
- return `${path15.basename(root)}_devcontainer`;
6203
+ return `${path16.basename(root)}_devcontainer`;
5782
6204
  }
5783
6205
  function assertDevcontainer(root) {
5784
- if (!existsSync9(path15.join(root, ".devcontainer"))) {
6206
+ if (!existsSync10(path16.join(root, ".devcontainer"))) {
5785
6207
  throw new Error(
5786
6208
  `No .devcontainer/ at ${root}. Run \`monoceros apply <name>\` first.`
5787
6209
  );
5788
6210
  }
5789
6211
  }
5790
6212
  function isComposeMode(root) {
5791
- return existsSync9(path15.join(root, ".devcontainer", "compose.yaml"));
6213
+ return existsSync10(path16.join(root, ".devcontainer", "compose.yaml"));
5792
6214
  }
5793
6215
  function resolveCompose(root) {
5794
6216
  assertDevcontainer(root);
5795
- const composeFile = path15.join(root, ".devcontainer", "compose.yaml");
5796
- if (!existsSync9(composeFile)) {
6217
+ const composeFile = path16.join(root, ".devcontainer", "compose.yaml");
6218
+ if (!existsSync10(composeFile)) {
5797
6219
  throw new Error(
5798
6220
  `No compose.yaml at ${composeFile}. \`monoceros logs\` tails compose service logs, which require services configured via \`monoceros add-service <name> <svc>\`. For a bare container, use \`monoceros shell <name>\` or read logs/<app>.log.`
5799
6221
  );
@@ -6001,7 +6423,7 @@ function imageContainerFilter(root) {
6001
6423
  async function stopImageContainer(opts) {
6002
6424
  const exec = opts.dockerExec ?? spawnDocker;
6003
6425
  const logger = opts.logger ?? { info: (msg) => consola10.info(msg) };
6004
- const name = path15.basename(opts.root);
6426
+ const name = path16.basename(opts.root);
6005
6427
  const ps = await exec([
6006
6428
  "ps",
6007
6429
  "-q",
@@ -6022,7 +6444,7 @@ async function stopImageContainer(opts) {
6022
6444
  async function statusImageContainer(opts) {
6023
6445
  const exec = opts.dockerExec ?? spawnDocker;
6024
6446
  const logger = opts.logger ?? { info: (msg) => consola10.info(msg) };
6025
- const name = path15.basename(opts.root);
6447
+ const name = path16.basename(opts.root);
6026
6448
  const res = await exec([
6027
6449
  "ps",
6028
6450
  "-a",
@@ -6064,7 +6486,7 @@ var init_compose = __esm({
6064
6486
  init_proxy();
6065
6487
  spawnDockerCompose = (args, cwd) => {
6066
6488
  return new Promise((resolve, reject) => {
6067
- const child = spawn6("docker", ["compose", ...args], {
6489
+ const child = spawn7("docker", ["compose", ...args], {
6068
6490
  cwd,
6069
6491
  stdio: ["inherit", "pipe", "pipe"]
6070
6492
  });
@@ -6076,7 +6498,7 @@ var init_compose = __esm({
6076
6498
  };
6077
6499
  spawnDocker = (args) => {
6078
6500
  return new Promise((resolve, reject) => {
6079
- const child = spawn6("docker", args, {
6501
+ const child = spawn7("docker", args, {
6080
6502
  stdio: ["ignore", "pipe", "pipe"]
6081
6503
  });
6082
6504
  let stdout = "";
@@ -6090,454 +6512,121 @@ var init_compose = __esm({
6090
6512
  child.on("error", reject);
6091
6513
  child.on(
6092
6514
  "exit",
6093
- (code) => resolve({ exitCode: code ?? 0, stdout, stderr })
6094
- );
6095
- });
6096
- };
6097
- BIND_SOURCE_MISSING_RE = /bind source path does not exist/i;
6098
- BIND_RETRY_ATTEMPTS = 3;
6099
- BIND_RETRY_DELAY_MS = 500;
6100
- }
6101
- });
6102
-
6103
- // src/devcontainer/bridge-daemon.ts
6104
- import { spawn as spawn7 } from "child_process";
6105
- import {
6106
- existsSync as existsSync10,
6107
- mkdirSync,
6108
- promises as fsp5,
6109
- readFileSync as readFileSync6,
6110
- writeFileSync
6111
- } from "fs";
6112
- import path16 from "path";
6113
- function bridgePidFile(root) {
6114
- return path16.join(relayDir(root), "daemon.pid");
6115
- }
6116
- function pidAlive(pid) {
6117
- try {
6118
- process.kill(pid, 0);
6119
- return true;
6120
- } catch {
6121
- return false;
6122
- }
6123
- }
6124
- function runningBridgePid(root) {
6125
- const file = bridgePidFile(root);
6126
- if (!existsSync10(file)) return null;
6127
- let pid = NaN;
6128
- try {
6129
- pid = Number(readFileSync6(file, "utf8").trim());
6130
- } catch {
6131
- return null;
6132
- }
6133
- return Number.isInteger(pid) && pid > 0 && pidAlive(pid) ? pid : null;
6134
- }
6135
- function spawnBridgeDaemon(root) {
6136
- try {
6137
- if (runningBridgePid(root) !== null) return;
6138
- const self = process.argv[1];
6139
- if (!self) return;
6140
- mkdirSync(relayDir(root), { recursive: true });
6141
- const child = spawn7(process.execPath, [self, "__bridge", root], {
6142
- detached: true,
6143
- stdio: "ignore"
6144
- });
6145
- if (typeof child.pid === "number") {
6146
- try {
6147
- writeFileSync(bridgePidFile(root), String(child.pid));
6148
- } catch {
6149
- }
6150
- }
6151
- child.unref();
6152
- } catch {
6153
- }
6154
- }
6155
- async function stopBridgeDaemon(root) {
6156
- const pid = runningBridgePid(root);
6157
- if (pid !== null) {
6158
- try {
6159
- process.kill(pid, "SIGTERM");
6160
- } catch {
6161
- }
6162
- }
6163
- await fsp5.rm(bridgePidFile(root), { force: true });
6164
- }
6165
- async function runBridgeDaemon(opts) {
6166
- const { root } = opts;
6167
- const dockerExec = opts.dockerExec ?? spawnDocker;
6168
- const lifecheckMs = opts.lifecheckMs ?? 5e3;
6169
- await fsp5.mkdir(relayDir(root), { recursive: true });
6170
- await fsp5.writeFile(bridgePidFile(root), String(process.pid));
6171
- const watcher = watchRelayUrl({
6172
- urlFile: relayUrlFile(root),
6173
- root,
6174
- spawn: opts.spawn ?? spawnDevcontainer
6175
- });
6176
- await new Promise((resolve) => {
6177
- let done = false;
6178
- const finish = () => {
6179
- if (done) return;
6180
- done = true;
6181
- resolve();
6182
- };
6183
- const lifecheck = setInterval(() => {
6184
- void isWorkspaceRunning(root, dockerExec).then((up) => {
6185
- if (!up) {
6186
- clearInterval(lifecheck);
6187
- finish();
6188
- }
6189
- }).catch(() => {
6190
- });
6191
- }, lifecheckMs);
6192
- const onSignal = () => {
6193
- clearInterval(lifecheck);
6194
- finish();
6195
- };
6196
- process.once("SIGTERM", onSignal);
6197
- process.once("SIGINT", onSignal);
6198
- });
6199
- watcher.dispose();
6200
- await fsp5.rm(bridgePidFile(root), { force: true });
6201
- }
6202
- var init_bridge_daemon = __esm({
6203
- "src/devcontainer/bridge-daemon.ts"() {
6204
- "use strict";
6205
- init_browser_bridge();
6206
- init_cli();
6207
- init_compose();
6208
- }
6209
- });
6210
-
6211
- // src/devcontainer/ssh-attach.ts
6212
- import { spawn as spawn8 } from "child_process";
6213
- import { promises as fs10 } from "fs";
6214
- import { existsSync as existsSync11, readFileSync as readFileSync7 } from "fs";
6215
- import os2 from "os";
6216
- import path17 from "path";
6217
- function sshHomeDir(home) {
6218
- return path17.join(home, "ssh");
6219
- }
6220
- function sshConfigEntryPath(home, name) {
6221
- return path17.join(sshHomeDir(home), "config.d", name);
6222
- }
6223
- function sshProxyScriptPath(home, name) {
6224
- return path17.join(sshHomeDir(home), `exec-${name}.sh`);
6225
- }
6226
- function privateKeyPath(targetDir) {
6227
- return path17.join(targetDir, ".monoceros", "ssh", "id_ed25519");
6228
- }
6229
- async function ensureKeypair(targetDir, name, keygen, logger) {
6230
- const privateKey = privateKeyPath(targetDir);
6231
- const publicKey = `${privateKey}.pub`;
6232
- if (existsSync11(privateKey) && existsSync11(publicKey)) {
6233
- return { privateKey, publicKey };
6234
- }
6235
- await fs10.mkdir(path17.dirname(privateKey), { recursive: true });
6236
- try {
6237
- const res = await keygen([
6238
- "-t",
6239
- "ed25519",
6240
- "-N",
6241
- "",
6242
- "-f",
6243
- privateKey,
6244
- "-C",
6245
- `monoceros-${name}`,
6246
- "-q"
6247
- ]);
6248
- if (res.exitCode !== 0) {
6249
- logger.warn(
6250
- `ssh-keygen failed (exit ${res.exitCode})${res.stderr ? `: ${res.stderr.trim()}` : ""}; IDE SSH attach not set up.`
6251
- );
6252
- return null;
6253
- }
6254
- } catch (err) {
6255
- logger.warn(
6256
- `ssh-keygen not runnable (${err instanceof Error ? err.message : String(err)}); IDE SSH attach not set up.`
6257
- );
6258
- return null;
6259
- }
6260
- return { privateKey, publicKey };
6261
- }
6262
- function proxyScriptContent(name, targetDir) {
6263
- return `#!/bin/sh
6264
- # Generated by Monoceros (ADR 0022) - do not edit.
6265
- # Portless SSH transport into the running dev container '${name}'.
6266
- # Resolves the container by the devcontainer.local_folder label (the
6267
- # same handle 'monoceros shell' uses), so it follows rebuilds.
6268
- set -e
6269
- # GUI launchers (IDEs, Claude Desktop) start a ProxyCommand with a
6270
- # minimal PATH: on macOS launchd hands out only /usr/bin:/bin:/usr/sbin:
6271
- # /sbin, so a bare \`docker\` (Docker Desktop installs it under
6272
- # /usr/local/bin or /opt/homebrew/bin) isn't found. Prepend the common
6273
- # Docker locations so the same script works from a login shell and a GUI
6274
- # launcher alike.
6275
- PATH="/usr/local/bin:/opt/homebrew/bin:/Applications/Docker.app/Contents/Resources/bin:$PATH"
6276
- cid=$(docker ps -q \\
6277
- --filter "label=devcontainer.local_folder=${targetDir}" \\
6278
- --filter status=running | head -n1)
6279
- if [ -z "$cid" ]; then
6280
- echo "monoceros: dev container '${name}' is not running. Start it with: monoceros apply ${name}" >&2
6281
- exit 1
6282
- fi
6283
- exec docker exec -i "$cid" socat - TCP:127.0.0.1:22
6284
- `;
6285
- }
6286
- function configEntryContent(name, hostAlias, privateKey, proxyScript) {
6287
- return `# Generated by Monoceros (ADR 0022) - do not edit.
6288
- # Attach an SSH-capable IDE (Codium, IntelliJ, Zed) to host
6289
- # '${hostAlias}', or run: ssh ${hostAlias}
6290
- Host ${hostAlias}
6291
- User node
6292
- IdentityFile "${privateKey}"
6293
- IdentitiesOnly yes
6294
- StrictHostKeyChecking no
6295
- UserKnownHostsFile /dev/null
6296
- ProxyCommand "${proxyScript}"
6297
- `;
6298
- }
6299
- async function ensureInclude(userSshDir, home) {
6300
- const configPath = path17.join(userSshDir, "config");
6301
- const includeTarget = path17.join(sshHomeDir(home), "config.d", "*");
6302
- const includeLine = `Include "${includeTarget}"`;
6303
- await fs10.mkdir(userSshDir, { recursive: true });
6304
- let existing = "";
6305
- try {
6306
- existing = await fs10.readFile(configPath, "utf8");
6307
- } catch {
6308
- existing = "";
6309
- }
6310
- if (existing.includes(includeTarget)) return;
6311
- const banner = "# Added by Monoceros (ADR 0022): per-container SSH attach entries.";
6312
- const prefix = `${banner}
6313
- ${includeLine}
6314
- `;
6315
- const next = existing.length > 0 ? `${prefix}
6316
- ${existing}` : prefix;
6317
- await fs10.writeFile(configPath, next, { mode: 384 });
6318
- await fs10.chmod(configPath, 384).catch(() => {
6319
- });
6320
- }
6321
- function runCapture(cmd, args) {
6322
- return new Promise((resolve, reject) => {
6323
- const child = spawn8(cmd, args, {
6324
- stdio: ["ignore", "pipe", "ignore"]
6325
- });
6326
- let stdout = "";
6327
- child.stdout.on("data", (c) => {
6328
- stdout += c.toString();
6329
- });
6330
- child.on("error", reject);
6331
- child.on("exit", (code) => resolve({ stdout, exitCode: code ?? 0 }));
6332
- });
6515
+ (code) => resolve({ exitCode: code ?? 0, stdout, stderr })
6516
+ );
6517
+ });
6518
+ };
6519
+ BIND_SOURCE_MISSING_RE = /bind source path does not exist/i;
6520
+ BIND_RETRY_ATTEMPTS = 3;
6521
+ BIND_RETRY_DELAY_MS = 500;
6522
+ }
6523
+ });
6524
+
6525
+ // src/devcontainer/bridge-daemon.ts
6526
+ import { spawn as spawn8 } from "child_process";
6527
+ import {
6528
+ existsSync as existsSync11,
6529
+ mkdirSync,
6530
+ promises as fsp5,
6531
+ readFileSync as readFileSync7,
6532
+ writeFileSync
6533
+ } from "fs";
6534
+ import path17 from "path";
6535
+ function bridgePidFile(root) {
6536
+ return path17.join(relayDir(root), "daemon.pid");
6333
6537
  }
6334
- function isWsl() {
6538
+ function pidAlive(pid) {
6335
6539
  try {
6336
- if (process.env.WSL_DISTRO_NAME) return true;
6337
- const rel = readFileSync7(
6338
- "/proc/sys/kernel/osrelease",
6339
- "utf8"
6340
- ).toLowerCase();
6341
- return rel.includes("microsoft") || rel.includes("wsl");
6540
+ process.kill(pid, 0);
6541
+ return true;
6342
6542
  } catch {
6343
6543
  return false;
6344
6544
  }
6345
6545
  }
6346
- async function resolveWindowsProfile() {
6546
+ function runningBridgePid(root) {
6547
+ const file = bridgePidFile(root);
6548
+ if (!existsSync11(file)) return null;
6549
+ let pid = NaN;
6347
6550
  try {
6348
- const r = await runCapture("cmd.exe", ["/c", "echo %USERPROFILE%"]);
6349
- const homeWin = r.stdout.replace(/\r/g, "").trim();
6350
- if (r.exitCode !== 0 || !homeWin || homeWin.includes("%USERPROFILE%")) {
6351
- return null;
6352
- }
6353
- const w = await runCapture("wslpath", ["-u", homeWin]);
6354
- const homeWsl = w.stdout.trim();
6355
- const user = homeWin.split("\\").pop() ?? "";
6356
- if (w.exitCode !== 0 || !homeWsl || !user) return null;
6357
- return { homeWsl, homeWin, user };
6551
+ pid = Number(readFileSync7(file, "utf8").trim());
6358
6552
  } catch {
6359
6553
  return null;
6360
6554
  }
6555
+ return Number.isInteger(pid) && pid > 0 && pidAlive(pid) ? pid : null;
6361
6556
  }
6362
- async function realLockWindowsKey(winKeyPath, user) {
6363
- await runCapture("icacls.exe", [
6364
- winKeyPath,
6365
- "/inheritance:r",
6366
- "/grant:r",
6367
- `${user}:R`
6368
- ]);
6369
- }
6370
- function resolveWindowsDeps(d) {
6371
- return {
6372
- isWsl: d?.isWsl ?? isWsl,
6373
- resolveProfile: d?.resolveProfile ?? resolveWindowsProfile,
6374
- lockKey: d?.lockKey ?? realLockWindowsKey
6375
- };
6376
- }
6377
- function windowsHostBlock(hostAlias, keyWin) {
6378
- return [
6379
- `Host ${hostAlias}`,
6380
- ` User node`,
6381
- ` IdentityFile ${keyWin}`,
6382
- ` IdentitiesOnly yes`,
6383
- ` StrictHostKeyChecking no`,
6384
- ` UserKnownHostsFile NUL`,
6385
- ` ProxyCommand docker exec -i ${hostAlias} socat - TCP:127.0.0.1:22`
6386
- ].join("\n");
6387
- }
6388
- function escapeRegExp2(s) {
6389
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6390
- }
6391
- function blockMarkers(hostAlias) {
6392
- return {
6393
- begin: `# >>> monoceros ${hostAlias} >>>`,
6394
- end: `# <<< monoceros ${hostAlias} <<<`
6395
- };
6396
- }
6397
- async function upsertMarkedBlock(configPath, hostAlias, body) {
6398
- const { begin, end } = blockMarkers(hostAlias);
6399
- const section = `${begin}
6400
- ${body}
6401
- ${end}`;
6402
- await fs10.mkdir(path17.dirname(configPath), { recursive: true });
6403
- let existing = "";
6404
- try {
6405
- existing = await fs10.readFile(configPath, "utf8");
6406
- } catch {
6407
- existing = "";
6408
- }
6409
- const re = new RegExp(`${escapeRegExp2(begin)}[\\s\\S]*?${escapeRegExp2(end)}`);
6410
- if (re.test(existing)) {
6411
- await fs10.writeFile(configPath, existing.replace(re, section));
6412
- return;
6413
- }
6414
- const sep = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
6415
- await fs10.writeFile(configPath, `${existing}${sep}${section}
6416
- `);
6417
- }
6418
- async function removeMarkedBlock(configPath, hostAlias) {
6419
- let existing = "";
6557
+ function spawnBridgeDaemon(root) {
6420
6558
  try {
6421
- existing = await fs10.readFile(configPath, "utf8");
6559
+ if (runningBridgePid(root) !== null) return;
6560
+ const self = process.argv[1];
6561
+ if (!self) return;
6562
+ mkdirSync(relayDir(root), { recursive: true });
6563
+ const child = spawn8(process.execPath, [self, "__bridge", root], {
6564
+ detached: true,
6565
+ stdio: "ignore"
6566
+ });
6567
+ if (typeof child.pid === "number") {
6568
+ try {
6569
+ writeFileSync(bridgePidFile(root), String(child.pid));
6570
+ } catch {
6571
+ }
6572
+ }
6573
+ child.unref();
6422
6574
  } catch {
6423
- return;
6424
6575
  }
6425
- const { begin, end } = blockMarkers(hostAlias);
6426
- const re = new RegExp(
6427
- `\\n?${escapeRegExp2(begin)}[\\s\\S]*?${escapeRegExp2(end)}\\n?`,
6428
- "g"
6429
- );
6430
- await fs10.writeFile(
6431
- configPath,
6432
- existing.replace(re, "\n").replace(/\n{3,}/g, "\n\n")
6433
- );
6434
6576
  }
6435
- async function setupWindowsBridge(name, hostAlias, privateKey, deps, logger) {
6436
- if (!deps.isWsl()) return;
6437
- const profile = await deps.resolveProfile();
6438
- if (!profile) {
6439
- logger.warn(
6440
- "WSL detected but the Windows user profile could not be resolved; skipping the Windows SSH bridge."
6441
- );
6442
- return;
6577
+ async function stopBridgeDaemon(root) {
6578
+ const pid = runningBridgePid(root);
6579
+ if (pid !== null) {
6580
+ try {
6581
+ process.kill(pid, "SIGTERM");
6582
+ } catch {
6583
+ }
6443
6584
  }
6444
- const monoDir = path17.join(profile.homeWsl, ".ssh", "monoceros");
6445
- await fs10.mkdir(monoDir, { recursive: true });
6446
- const keyDst = path17.join(monoDir, name);
6447
- await fs10.rm(keyDst, { force: true });
6448
- await fs10.copyFile(privateKey, keyDst);
6449
- const keyWin = `${profile.homeWin}\\.ssh\\monoceros\\${name}`;
6450
- await upsertMarkedBlock(
6451
- path17.join(profile.homeWsl, ".ssh", "config"),
6452
- hostAlias,
6453
- windowsHostBlock(hostAlias, keyWin)
6454
- );
6455
- await deps.lockKey(keyWin, profile.user);
6456
- logger.info(
6457
- `Windows SSH bridge ready: \`ssh ${hostAlias}\` (Codium/Gateway too).`
6458
- );
6585
+ await fsp5.rm(bridgePidFile(root), { force: true });
6459
6586
  }
6460
- async function removeWindowsBridge(name, hostAlias, deps) {
6461
- if (!deps.isWsl()) return;
6462
- const profile = await deps.resolveProfile();
6463
- if (!profile) return;
6464
- await fs10.rm(path17.join(profile.homeWsl, ".ssh", "monoceros", name), {
6465
- force: true
6587
+ async function runBridgeDaemon(opts) {
6588
+ const { root } = opts;
6589
+ const dockerExec = opts.dockerExec ?? spawnDocker;
6590
+ const lifecheckMs = opts.lifecheckMs ?? 5e3;
6591
+ await fsp5.mkdir(relayDir(root), { recursive: true });
6592
+ await fsp5.writeFile(bridgePidFile(root), String(process.pid));
6593
+ const watcher = watchRelayUrl({
6594
+ urlFile: relayUrlFile(root),
6595
+ root,
6596
+ spawn: opts.spawn ?? spawnDevcontainer
6466
6597
  });
6467
- await removeMarkedBlock(
6468
- path17.join(profile.homeWsl, ".ssh", "config"),
6469
- hostAlias
6470
- );
6471
- }
6472
- async function setupSshAttach(opts) {
6473
- const hostAlias = `monoceros-${opts.name}`;
6474
- const logger = opts.logger ?? { info: () => {
6475
- }, warn: () => {
6476
- } };
6477
- const keygen = opts.keygen ?? realKeygen;
6478
- const userSshDir = opts.userSshDir ?? path17.join(os2.homedir(), ".ssh");
6479
- const keys = await ensureKeypair(opts.targetDir, opts.name, keygen, logger);
6480
- if (!keys) return { hostAlias, configured: false };
6481
- const proxyScript = sshProxyScriptPath(opts.home, opts.name);
6482
- const configEntry = sshConfigEntryPath(opts.home, opts.name);
6483
- await fs10.mkdir(path17.dirname(proxyScript), { recursive: true });
6484
- await fs10.mkdir(path17.dirname(configEntry), { recursive: true });
6485
- await fs10.writeFile(
6486
- proxyScript,
6487
- proxyScriptContent(opts.name, opts.targetDir),
6488
- { mode: 493 }
6489
- );
6490
- await fs10.chmod(proxyScript, 493).catch(() => {
6598
+ await new Promise((resolve) => {
6599
+ let done = false;
6600
+ const finish = () => {
6601
+ if (done) return;
6602
+ done = true;
6603
+ resolve();
6604
+ };
6605
+ const lifecheck = setInterval(() => {
6606
+ void isWorkspaceRunning(root, dockerExec).then((up) => {
6607
+ if (!up) {
6608
+ clearInterval(lifecheck);
6609
+ finish();
6610
+ }
6611
+ }).catch(() => {
6612
+ });
6613
+ }, lifecheckMs);
6614
+ const onSignal = () => {
6615
+ clearInterval(lifecheck);
6616
+ finish();
6617
+ };
6618
+ process.once("SIGTERM", onSignal);
6619
+ process.once("SIGINT", onSignal);
6491
6620
  });
6492
- await fs10.writeFile(
6493
- configEntry,
6494
- configEntryContent(opts.name, hostAlias, keys.privateKey, proxyScript)
6495
- );
6496
- await ensureInclude(userSshDir, opts.home);
6497
- try {
6498
- await setupWindowsBridge(
6499
- opts.name,
6500
- hostAlias,
6501
- keys.privateKey,
6502
- resolveWindowsDeps(opts.windows),
6503
- logger
6504
- );
6505
- } catch (err) {
6506
- logger.warn(
6507
- `Windows SSH bridge skipped: ${err instanceof Error ? err.message : String(err)}`
6508
- );
6509
- }
6510
- return { hostAlias, configured: true };
6511
- }
6512
- async function removeSshAttach(home, name, windows) {
6513
- await fs10.rm(sshProxyScriptPath(home, name), { force: true });
6514
- await fs10.rm(sshConfigEntryPath(home, name), { force: true });
6515
- try {
6516
- await removeWindowsBridge(
6517
- name,
6518
- `monoceros-${name}`,
6519
- resolveWindowsDeps(windows)
6520
- );
6521
- } catch {
6522
- }
6621
+ watcher.dispose();
6622
+ await fsp5.rm(bridgePidFile(root), { force: true });
6523
6623
  }
6524
- var realKeygen;
6525
- var init_ssh_attach = __esm({
6526
- "src/devcontainer/ssh-attach.ts"() {
6624
+ var init_bridge_daemon = __esm({
6625
+ "src/devcontainer/bridge-daemon.ts"() {
6527
6626
  "use strict";
6528
- realKeygen = (args) => {
6529
- return new Promise((resolve, reject) => {
6530
- const child = spawn8("ssh-keygen", args, {
6531
- stdio: ["ignore", "ignore", "pipe"]
6532
- });
6533
- let stderr = "";
6534
- child.stderr.on("data", (chunk) => {
6535
- stderr += chunk.toString();
6536
- });
6537
- child.on("error", reject);
6538
- child.on("exit", (code) => resolve({ exitCode: code ?? 0, stderr }));
6539
- });
6540
- };
6627
+ init_browser_bridge();
6628
+ init_cli();
6629
+ init_compose();
6541
6630
  }
6542
6631
  });
6543
6632
 
@@ -8018,6 +8107,11 @@ Fix the value in the env file (or the yml).`
8018
8107
  name: opts.name,
8019
8108
  targetDir,
8020
8109
  home,
8110
+ // Direct-port Windows block only when the runtime publishes the port
8111
+ // (>= 1.3.4); otherwise the Windows block keeps the ProxyCommand.
8112
+ windowsDirectPort: runtimeSupportsHostKeyPinning(
8113
+ createOpts.runtimeVersion
8114
+ ) ? windowsSshPort(opts.name) : null,
8021
8115
  ...opts.sshKeygen ? { keygen: opts.sshKeygen } : {},
8022
8116
  ...opts.sshUserSshDir ? { userSshDir: opts.sshUserSshDir } : {},
8023
8117
  logger: idLogger
@@ -8155,6 +8249,19 @@ Fix the value in the env file (or the yml).`
8155
8249
  if (runtimeSupportsBrowserBridge(createOpts.runtimeVersion)) {
8156
8250
  spawnBridgeDaemon(targetDir);
8157
8251
  }
8252
+ if (runtimeSupportsHostKeyPinning(createOpts.runtimeVersion)) {
8253
+ try {
8254
+ await recordHostKey({
8255
+ name: opts.name,
8256
+ targetDir,
8257
+ ...opts.sshUserSshDir ? { userSshDir: opts.sshUserSshDir } : {}
8258
+ });
8259
+ } catch (err) {
8260
+ (logger.warn ?? logger.info)(
8261
+ `Recording the SSH host key skipped: ${err instanceof Error ? err.message : String(err)}`
8262
+ );
8263
+ }
8264
+ }
8158
8265
  const summaryLines = buildApplySummary(createOpts);
8159
8266
  if (summaryLines.length > 0) {
8160
8267
  const formatted = formatApplySummary(summaryLines);
@@ -8502,7 +8609,7 @@ var CLI_VERSION;
8502
8609
  var init_version = __esm({
8503
8610
  "src/version.ts"() {
8504
8611
  "use strict";
8505
- CLI_VERSION = true ? "1.32.0" : "dev";
8612
+ CLI_VERSION = true ? "1.33.1" : "dev";
8506
8613
  }
8507
8614
  });
8508
8615