@getmonoceros/workbench 1.32.0 → 1.33.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/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,398 @@ 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
+ return [
2821
+ `Host ${hostAlias}`,
2822
+ ` HostName 127.0.0.1`,
2823
+ ` Port ${port}`,
2824
+ ` User node`,
2825
+ ` IdentityFile ${keyWin}`,
2826
+ ` IdentitiesOnly yes`,
2827
+ ` StrictHostKeyChecking no`,
2828
+ ` UserKnownHostsFile NUL`
2829
+ ].join("\n");
2830
+ }
2831
+ function escapeRegExp2(s) {
2832
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2833
+ }
2834
+ function blockMarkers(hostAlias) {
2835
+ return {
2836
+ begin: `# >>> monoceros ${hostAlias} >>>`,
2837
+ end: `# <<< monoceros ${hostAlias} <<<`
2838
+ };
2839
+ }
2840
+ async function upsertMarkedBlock(configPath, hostAlias, body) {
2841
+ const { begin, end } = blockMarkers(hostAlias);
2842
+ const section = `${begin}
2843
+ ${body}
2844
+ ${end}`;
2845
+ await fs7.mkdir(path9.dirname(configPath), { recursive: true });
2846
+ let existing = "";
2847
+ try {
2848
+ existing = await fs7.readFile(configPath, "utf8");
2849
+ } catch {
2850
+ existing = "";
2851
+ }
2852
+ const re = new RegExp(`${escapeRegExp2(begin)}[\\s\\S]*?${escapeRegExp2(end)}`);
2853
+ if (re.test(existing)) {
2854
+ await fs7.writeFile(configPath, existing.replace(re, section));
2855
+ return;
2856
+ }
2857
+ const sep = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
2858
+ await fs7.writeFile(configPath, `${existing}${sep}${section}
2859
+ `);
2860
+ }
2861
+ async function removeMarkedBlock(configPath, hostAlias) {
2862
+ let existing = "";
2863
+ try {
2864
+ existing = await fs7.readFile(configPath, "utf8");
2865
+ } catch {
2866
+ return;
2867
+ }
2868
+ const { begin, end } = blockMarkers(hostAlias);
2869
+ const re = new RegExp(
2870
+ `\\n?${escapeRegExp2(begin)}[\\s\\S]*?${escapeRegExp2(end)}\\n?`,
2871
+ "g"
2872
+ );
2873
+ await fs7.writeFile(
2874
+ configPath,
2875
+ existing.replace(re, "\n").replace(/\n{3,}/g, "\n\n")
2876
+ );
2877
+ }
2878
+ async function setupWindowsBridge(name, hostAlias, privateKey, deps, logger) {
2879
+ if (!deps.isWsl()) return;
2880
+ const profile = await deps.resolveProfile();
2881
+ if (!profile) {
2882
+ logger.warn(
2883
+ "WSL detected but the Windows user profile could not be resolved; skipping the Windows SSH bridge."
2884
+ );
2885
+ return;
2886
+ }
2887
+ const monoDir = path9.join(profile.homeWsl, ".ssh", "monoceros");
2888
+ await fs7.mkdir(monoDir, { recursive: true });
2889
+ const keyDst = path9.join(monoDir, name);
2890
+ await fs7.rm(keyDst, { force: true });
2891
+ await fs7.copyFile(privateKey, keyDst);
2892
+ const keyWin = `${profile.homeWin}\\.ssh\\monoceros\\${name}`;
2893
+ await upsertMarkedBlock(
2894
+ path9.join(profile.homeWsl, ".ssh", "config"),
2895
+ hostAlias,
2896
+ windowsHostBlock(hostAlias, keyWin, windowsSshPort(name))
2897
+ );
2898
+ await deps.lockKey(keyWin, profile.user);
2899
+ logger.info(
2900
+ `Windows SSH bridge ready: \`ssh ${hostAlias}\` (Codium/Gateway too).`
2901
+ );
2902
+ }
2903
+ async function removeWindowsBridge(name, hostAlias, deps) {
2904
+ if (!deps.isWsl()) return;
2905
+ const profile = await deps.resolveProfile();
2906
+ if (!profile) return;
2907
+ await fs7.rm(path9.join(profile.homeWsl, ".ssh", "monoceros", name), {
2908
+ force: true
2909
+ });
2910
+ await removeMarkedBlock(
2911
+ path9.join(profile.homeWsl, ".ssh", "config"),
2912
+ hostAlias
2913
+ );
2914
+ }
2915
+ async function setupSshAttach(opts) {
2916
+ const hostAlias = `monoceros-${opts.name}`;
2917
+ const logger = opts.logger ?? { info: () => {
2918
+ }, warn: () => {
2919
+ } };
2920
+ const keygen = opts.keygen ?? realKeygen;
2921
+ const userSshDir = opts.userSshDir ?? path9.join(os2.homedir(), ".ssh");
2922
+ const keys = await ensureKeypair(opts.targetDir, opts.name, keygen, logger);
2923
+ if (!keys) return { hostAlias, configured: false };
2924
+ const proxyScript = sshProxyScriptPath(opts.home, opts.name);
2925
+ const configEntry = sshConfigEntryPath(opts.home, opts.name);
2926
+ await fs7.mkdir(path9.dirname(proxyScript), { recursive: true });
2927
+ await fs7.mkdir(path9.dirname(configEntry), { recursive: true });
2928
+ await fs7.writeFile(
2929
+ proxyScript,
2930
+ proxyScriptContent(opts.name, opts.targetDir),
2931
+ { mode: 493 }
2932
+ );
2933
+ await fs7.chmod(proxyScript, 493).catch(() => {
2934
+ });
2935
+ await fs7.writeFile(
2936
+ configEntry,
2937
+ configEntryContent(opts.name, hostAlias, keys.privateKey, proxyScript)
2938
+ );
2939
+ await ensureInclude(userSshDir, opts.home);
2940
+ try {
2941
+ await setupWindowsBridge(
2942
+ opts.name,
2943
+ hostAlias,
2944
+ keys.privateKey,
2945
+ resolveWindowsDeps(opts.windows),
2946
+ logger
2947
+ );
2948
+ } catch (err) {
2949
+ logger.warn(
2950
+ `Windows SSH bridge skipped: ${err instanceof Error ? err.message : String(err)}`
2951
+ );
2952
+ }
2953
+ return { hostAlias, configured: true };
2954
+ }
2955
+ async function upsertKnownHost(knownHostsPath, hostId, typeAndKey) {
2956
+ await fs7.mkdir(path9.dirname(knownHostsPath), { recursive: true });
2957
+ let existing = "";
2958
+ try {
2959
+ existing = await fs7.readFile(knownHostsPath, "utf8");
2960
+ } catch {
2961
+ existing = "";
2962
+ }
2963
+ const kept = existing.split("\n").filter((l) => l.trim().length > 0 && l.split(/\s+/)[0] !== hostId);
2964
+ const next = `${[...kept, `${hostId} ${typeAndKey}`].join("\n")}
2965
+ `;
2966
+ await fs7.writeFile(knownHostsPath, next);
2967
+ }
2968
+ async function recordHostKey(opts) {
2969
+ const pubPath = path9.join(
2970
+ opts.targetDir,
2971
+ ".monoceros",
2972
+ "ssh",
2973
+ "host",
2974
+ "ssh_host_ed25519_key.pub"
2975
+ );
2976
+ let line = "";
2977
+ try {
2978
+ line = (await fs7.readFile(pubPath, "utf8")).trim();
2979
+ } catch {
2980
+ return;
2981
+ }
2982
+ const parts = line.split(/\s+/);
2983
+ if (parts.length < 2) return;
2984
+ const typeAndKey = `${parts[0]} ${parts[1]}`;
2985
+ const userSshDir = opts.userSshDir ?? path9.join(os2.homedir(), ".ssh");
2986
+ await upsertKnownHost(
2987
+ path9.join(userSshDir, "known_hosts"),
2988
+ `monoceros-${opts.name}`,
2989
+ typeAndKey
2990
+ );
2991
+ const deps = resolveWindowsDeps(opts.windows);
2992
+ if (deps.isWsl()) {
2993
+ const profile = await deps.resolveProfile();
2994
+ if (profile) {
2995
+ await upsertKnownHost(
2996
+ path9.join(profile.homeWsl, ".ssh", "known_hosts"),
2997
+ `[127.0.0.1]:${windowsSshPort(opts.name)}`,
2998
+ typeAndKey
2999
+ );
3000
+ }
3001
+ }
3002
+ }
3003
+ async function removeSshAttach(home, name, windows) {
3004
+ await fs7.rm(sshProxyScriptPath(home, name), { force: true });
3005
+ await fs7.rm(sshConfigEntryPath(home, name), { force: true });
3006
+ try {
3007
+ await removeWindowsBridge(
3008
+ name,
3009
+ `monoceros-${name}`,
3010
+ resolveWindowsDeps(windows)
3011
+ );
3012
+ } catch {
3013
+ }
3014
+ }
3015
+ var realKeygen;
3016
+ var init_ssh_attach = __esm({
3017
+ "src/devcontainer/ssh-attach.ts"() {
3018
+ "use strict";
3019
+ realKeygen = (args) => {
3020
+ return new Promise((resolve, reject) => {
3021
+ const child = spawn4("ssh-keygen", args, {
3022
+ stdio: ["ignore", "ignore", "pipe"]
3023
+ });
3024
+ let stderr = "";
3025
+ child.stderr.on("data", (chunk) => {
3026
+ stderr += chunk.toString();
3027
+ });
3028
+ child.on("error", reject);
3029
+ child.on("exit", (code) => resolve({ exitCode: code ?? 0, stderr }));
3030
+ });
3031
+ };
3032
+ }
3033
+ });
3034
+
2641
3035
  // src/create/opencode-config.ts
2642
- import { existsSync as existsSync6, promises as fsp3 } from "fs";
2643
- import path9 from "path";
3036
+ import { existsSync as existsSync7, promises as fsp3 } from "fs";
3037
+ import path10 from "path";
2644
3038
  import { consola } from "consola";
2645
3039
  function parseOpencodeModel(model) {
2646
3040
  const idx = model.indexOf("/");
@@ -2659,16 +3053,16 @@ async function writeOpencodeConfig(targetDir, containerName2, features) {
2659
3053
  const apiToken = str("apiToken");
2660
3054
  const npm = str("npm");
2661
3055
  const baseUrl = str("baseUrl");
2662
- const file = path9.join(
3056
+ const file = path10.join(
2663
3057
  targetDir,
2664
3058
  "home",
2665
3059
  ".config",
2666
3060
  "opencode",
2667
3061
  "opencode.json"
2668
3062
  );
2669
- await fsp3.mkdir(path9.dirname(file), { recursive: true });
3063
+ await fsp3.mkdir(path10.dirname(file), { recursive: true });
2670
3064
  let config = {};
2671
- if (existsSync6(file)) {
3065
+ if (existsSync7(file)) {
2672
3066
  try {
2673
3067
  const txt = await fsp3.readFile(file, "utf8");
2674
3068
  if (txt.trim()) {
@@ -2799,8 +3193,8 @@ var init_opencode_config = __esm({
2799
3193
  });
2800
3194
 
2801
3195
  // src/create/scaffold.ts
2802
- import { existsSync as existsSync7, promises as fs7 } from "fs";
2803
- import path10 from "path";
3196
+ import { existsSync as existsSync8, promises as fs8 } from "fs";
3197
+ import path11 from "path";
2804
3198
  function deriveRepoName(url) {
2805
3199
  const lastSep = Math.max(url.lastIndexOf("/"), url.lastIndexOf(":"));
2806
3200
  const tail = url.slice(lastSep + 1);
@@ -2929,7 +3323,7 @@ function featuresSourceRoot() {
2929
3323
  const override2 = process.env.MONOCEROS_FEATURES_DIR_OVERRIDE?.trim();
2930
3324
  if (override2 && override2.length > 0) return override2;
2931
3325
  const checkout = workbenchCheckoutRoot();
2932
- return checkout ? path10.join(checkout, "components", "features") : null;
3326
+ return checkout ? path11.join(checkout, "components", "features") : null;
2933
3327
  }
2934
3328
  function resolveFeatures(opts) {
2935
3329
  const resolved = [];
@@ -2978,8 +3372,8 @@ function resolveFeatures(opts) {
2978
3372
  const { paths, files } = descriptorPersistentHome(descriptor);
2979
3373
  const workspaceEnv = descriptor?.feature?.workspaceEnv ?? [];
2980
3374
  const sourceRoot = featuresSourceRoot();
2981
- const localSourceDir = sourceRoot ? path10.join(sourceRoot, name) : null;
2982
- if (descriptor && localSourceDir && existsSync7(localSourceDir)) {
3375
+ const localSourceDir = sourceRoot ? path11.join(sourceRoot, name) : null;
3376
+ if (descriptor && localSourceDir && existsSync8(localSourceDir)) {
2983
3377
  resolved.push({
2984
3378
  devcontainerKey: `./features/${name}`,
2985
3379
  options,
@@ -3153,6 +3547,9 @@ function ideStateVolumesForRuntime(name, version) {
3153
3547
  (v) => compareRuntimeVersions(version, v.minRuntime) >= 0
3154
3548
  );
3155
3549
  }
3550
+ function windowsBridgePort(opts) {
3551
+ return runtimeSupportsHostKeyPinning(opts.runtimeVersion) && isWsl() ? windowsSshPort(opts.name) : null;
3552
+ }
3156
3553
  function buildDevcontainerJson(opts, dockerMode = "rootful") {
3157
3554
  const resolvedFeatures = resolveFeatures(opts);
3158
3555
  const features = {};
@@ -3185,7 +3582,11 @@ function buildDevcontainerJson(opts, dockerMode = "rootful") {
3185
3582
  }
3186
3583
  } : void 0;
3187
3584
  const sshPostStart = runtimeSupportsSshAttach(opts.runtimeVersion) ? { postStartCommand: "sudo /usr/local/bin/monoceros-sshd-up.sh" } : {};
3188
- const workspaceEnv = featureWorkspaceEnv(resolvedFeatures);
3585
+ const sshBridgePort = windowsBridgePort(opts);
3586
+ const workspaceEnv = {
3587
+ ...featureWorkspaceEnv(resolvedFeatures),
3588
+ ...sshBridgePort ? { MONOCEROS_SSH_PUBLISH_PORT: String(sshBridgePort) } : {}
3589
+ };
3189
3590
  const containerEnvField = Object.keys(workspaceEnv).length > 0 ? { containerEnv: workspaceEnv } : void 0;
3190
3591
  if (needsCompose(opts)) {
3191
3592
  const eagerServices = opts.services.filter((s) => !serviceDefersStart(s.name)).map((s) => s.name);
@@ -3218,6 +3619,9 @@ function buildDevcontainerJson(opts, dockerMode = "rootful") {
3218
3619
  runArgs.push("--network=monoceros-proxy");
3219
3620
  runArgs.push(`--network-alias=${opts.name}`);
3220
3621
  }
3622
+ if (sshBridgePort !== null) {
3623
+ runArgs.push("-p", `127.0.0.1:${sshBridgePort}:${sshBridgePort}`);
3624
+ }
3221
3625
  return {
3222
3626
  name: opts.name,
3223
3627
  image: resolveRuntimeImage(opts.runtimeVersion),
@@ -3248,6 +3652,7 @@ function composeVolumeSource(spec, serviceName) {
3248
3652
  function buildComposeYaml(opts, dockerMode = "rootful") {
3249
3653
  void dockerMode;
3250
3654
  const hasPorts = (opts.ports?.length ?? 0) > 0;
3655
+ const sshBridgePort = windowsBridgePort(opts);
3251
3656
  const lines = ["services:"];
3252
3657
  const ideVolumes = ideStateVolumesForRuntime(opts.name, opts.runtimeVersion);
3253
3658
  lines.push(" workspace:");
@@ -3256,6 +3661,10 @@ function buildComposeYaml(opts, dockerMode = "rootful") {
3256
3661
  lines.push(" command: 'sleep infinity'");
3257
3662
  lines.push(" cap_add:");
3258
3663
  lines.push(" - NET_ADMIN");
3664
+ if (sshBridgePort !== null) {
3665
+ lines.push(" ports:");
3666
+ lines.push(` - "127.0.0.1:${sshBridgePort}:${sshBridgePort}"`);
3667
+ }
3259
3668
  if (hasPorts) {
3260
3669
  lines.push(" networks:");
3261
3670
  lines.push(" default: {}");
@@ -3280,7 +3689,8 @@ function buildComposeYaml(opts, dockerMode = "rootful") {
3280
3689
  }
3281
3690
  const wsEnv = {
3282
3691
  ...serviceConnectionEnv(opts.services),
3283
- ...featureWorkspaceEnv(resolvedFeatures)
3692
+ ...featureWorkspaceEnv(resolvedFeatures),
3693
+ ...sshBridgePort ? { MONOCEROS_SSH_PUBLISH_PORT: String(sshBridgePort) } : {}
3284
3694
  };
3285
3695
  const wsEnvKeys = Object.keys(wsEnv);
3286
3696
  if (wsEnvKeys.length > 0) {
@@ -3558,125 +3968,125 @@ function buildPostCreateScript(opts) {
3558
3968
  return lines.join("\n") + "\n";
3559
3969
  }
3560
3970
  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);
3971
+ const dest = path11.join(devcontainerDir, "post-create.sh");
3972
+ await fs8.writeFile(dest, buildPostCreateScript(opts));
3973
+ await fs8.chmod(dest, 493);
3564
3974
  }
3565
3975
  async function writeIfChanged(filePath, content) {
3566
3976
  try {
3567
- if (await fs7.readFile(filePath, "utf8") === content) return false;
3977
+ if (await fs8.readFile(filePath, "utf8") === content) return false;
3568
3978
  } catch {
3569
3979
  }
3570
- await fs7.writeFile(filePath, content);
3980
+ await fs8.writeFile(filePath, content);
3571
3981
  return true;
3572
3982
  }
3573
3983
  async function writeScaffold(opts, targetDir, scaffoldOpts = {}) {
3574
3984
  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 });
3985
+ const devcontainerDir = path11.join(targetDir, ".devcontainer");
3986
+ const monocerosDir = path11.join(targetDir, ".monoceros");
3987
+ const projectsDir = path11.join(targetDir, "projects");
3988
+ const homeDir = path11.join(targetDir, "home");
3989
+ const dataDir = path11.join(targetDir, "data");
3990
+ await fs8.mkdir(devcontainerDir, { recursive: true });
3991
+ await fs8.mkdir(monocerosDir, { recursive: true });
3992
+ await fs8.mkdir(projectsDir, { recursive: true });
3993
+ await fs8.mkdir(homeDir, { recursive: true });
3584
3994
  if (needsCompose(opts)) {
3585
- await fs7.mkdir(dataDir, { recursive: true });
3995
+ await fs8.mkdir(dataDir, { recursive: true });
3586
3996
  for (const svc of opts.services) {
3587
3997
  const hasDataVolume = svc.volumes.some((v) => v.split(":")[0] === "data");
3588
3998
  if (hasDataVolume) {
3589
- await fs7.mkdir(path10.join(dataDir, svc.name), { recursive: true });
3999
+ await fs8.mkdir(path11.join(dataDir, svc.name), { recursive: true });
3590
4000
  }
3591
4001
  }
3592
4002
  }
3593
- const containerGitignore = path10.join(targetDir, ".gitignore");
3594
- await fs7.writeFile(
4003
+ const containerGitignore = path11.join(targetDir, ".gitignore");
4004
+ await fs8.writeFile(
3595
4005
  containerGitignore,
3596
4006
  "/home/\n/.monoceros/\n/data/\n/AGENTS.md\n/CLAUDE.md\n"
3597
4007
  );
3598
- const gitkeep = path10.join(projectsDir, ".gitkeep");
3599
- if (!existsSync7(gitkeep)) {
3600
- await fs7.writeFile(gitkeep, "");
4008
+ const gitkeep = path11.join(projectsDir, ".gitkeep");
4009
+ if (!existsSync8(gitkeep)) {
4010
+ await fs8.writeFile(gitkeep, "");
3601
4011
  }
3602
- await fs7.writeFile(
3603
- path10.join(monocerosDir, ".gitignore"),
4012
+ await fs8.writeFile(
4013
+ path11.join(monocerosDir, ".gitignore"),
3604
4014
  "git-credentials*\ngitconfig\n"
3605
4015
  );
3606
4016
  const devcontainerJson = buildDevcontainerJson(opts, dockerMode);
3607
4017
  await writeIfChanged(
3608
- path10.join(devcontainerDir, "devcontainer.json"),
4018
+ path11.join(devcontainerDir, "devcontainer.json"),
3609
4019
  JSON.stringify(devcontainerJson, null, 2) + "\n"
3610
4020
  );
3611
- const featuresDir = path10.join(devcontainerDir, "features");
3612
- if (existsSync7(featuresDir)) {
3613
- await fs7.rm(featuresDir, { recursive: true, force: true });
4021
+ const featuresDir = path11.join(devcontainerDir, "features");
4022
+ if (existsSync8(featuresDir)) {
4023
+ await fs8.rm(featuresDir, { recursive: true, force: true });
3614
4024
  }
3615
4025
  const resolvedFeatures = resolveFeatures(opts);
3616
4026
  for (const f of resolvedFeatures) {
3617
4027
  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 });
4028
+ const dest = path11.join(featuresDir, f.localName);
4029
+ await fs8.mkdir(dest, { recursive: true });
4030
+ const entries = await fs8.readdir(f.localSourceDir, { withFileTypes: true });
3621
4031
  for (const entry2 of entries) {
3622
4032
  if (!entry2.isFile()) continue;
3623
4033
  if (entry2.name === "component.yml" || entry2.name === "devcontainer-feature.json") {
3624
4034
  continue;
3625
4035
  }
3626
- await fs7.cp(
3627
- path10.join(f.localSourceDir, entry2.name),
3628
- path10.join(dest, entry2.name)
4036
+ await fs8.cp(
4037
+ path11.join(f.localSourceDir, entry2.name),
4038
+ path11.join(dest, entry2.name)
3629
4039
  );
3630
4040
  }
3631
4041
  if (f.generatedManifest) {
3632
- await fs7.writeFile(
3633
- path10.join(dest, "devcontainer-feature.json"),
4042
+ await fs8.writeFile(
4043
+ path11.join(dest, "devcontainer-feature.json"),
3634
4044
  JSON.stringify(f.generatedManifest, null, 2) + "\n"
3635
4045
  );
3636
4046
  }
3637
4047
  }
3638
4048
  for (const f of resolvedFeatures) {
3639
4049
  for (const sub of f.persistentHomePaths) {
3640
- await fs7.mkdir(path10.join(homeDir, sub), { recursive: true });
4050
+ await fs8.mkdir(path11.join(homeDir, sub), { recursive: true });
3641
4051
  }
3642
4052
  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);
4053
+ const filePath = path11.join(homeDir, entry2.path);
4054
+ await fs8.mkdir(path11.dirname(filePath), { recursive: true });
4055
+ if (!existsSync8(filePath)) {
4056
+ await fs8.writeFile(filePath, entry2.initialContent);
3647
4057
  }
3648
4058
  }
3649
4059
  }
3650
4060
  await writeClaudePermissionMode(targetDir, opts.features);
3651
4061
  await writeOpencodeConfig(targetDir, opts.name, opts.features);
3652
4062
  await writePostCreateScript(devcontainerDir, opts);
3653
- const composePath = path10.join(devcontainerDir, "compose.yaml");
4063
+ const composePath = path11.join(devcontainerDir, "compose.yaml");
3654
4064
  if (needsCompose(opts)) {
3655
4065
  await writeIfChanged(composePath, buildComposeYaml(opts, dockerMode));
3656
- } else if (existsSync7(composePath)) {
3657
- await fs7.rm(composePath);
4066
+ } else if (existsSync8(composePath)) {
4067
+ await fs8.rm(composePath);
3658
4068
  }
3659
- const workspacePath = path10.join(targetDir, `${opts.name}.code-workspace`);
4069
+ const workspacePath = path11.join(targetDir, `${opts.name}.code-workspace`);
3660
4070
  let existingWorkspace;
3661
4071
  try {
3662
- const raw = await fs7.readFile(workspacePath, "utf8");
4072
+ const raw = await fs8.readFile(workspacePath, "utf8");
3663
4073
  existingWorkspace = JSON.parse(raw);
3664
4074
  } catch {
3665
4075
  existingWorkspace = void 0;
3666
4076
  }
3667
4077
  const generated = buildCodeWorkspaceJson(opts);
3668
4078
  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");
4079
+ await fs8.writeFile(workspacePath, JSON.stringify(merged, null, 2) + "\n");
4080
+ const vscodeDir = path11.join(targetDir, ".vscode");
4081
+ const settingsPath = path11.join(vscodeDir, "settings.json");
3672
4082
  let existingSettings;
3673
4083
  try {
3674
- existingSettings = JSON.parse(await fs7.readFile(settingsPath, "utf8"));
4084
+ existingSettings = JSON.parse(await fs8.readFile(settingsPath, "utf8"));
3675
4085
  } catch {
3676
4086
  existingSettings = void 0;
3677
4087
  }
3678
- await fs7.mkdir(vscodeDir, { recursive: true });
3679
- await fs7.writeFile(
4088
+ await fs8.mkdir(vscodeDir, { recursive: true });
4089
+ await fs8.writeFile(
3680
4090
  settingsPath,
3681
4091
  JSON.stringify(mergeVscodeSettings(existingSettings), null, 2) + "\n"
3682
4092
  );
@@ -3690,6 +4100,7 @@ var init_scaffold = __esm({
3690
4100
  init_load_sync();
3691
4101
  init_generate_manifest();
3692
4102
  init_claude_settings();
4103
+ init_ssh_attach();
3693
4104
  init_opencode_config();
3694
4105
  init_catalog();
3695
4106
  APT_PACKAGE_NAME_RE2 = /^[a-z0-9][a-z0-9.+-]*$/;
@@ -4193,10 +4604,10 @@ var init_yml = __esm({
4193
4604
  });
4194
4605
 
4195
4606
  // src/modify/index.ts
4196
- import { promises as fs8 } from "fs";
4607
+ import { promises as fs9 } from "fs";
4197
4608
  import { consola as consola2 } from "consola";
4198
4609
  import { createPatch } from "diff";
4199
- import path11 from "path";
4610
+ import path12 from "path";
4200
4611
  function runAddLanguage(input) {
4201
4612
  const spec = parseLanguageSpec(input.language);
4202
4613
  if (!spec || !BUILTIN_LANGUAGES.has(spec.name) && !LANGUAGE_CATALOG[spec.name]) {
@@ -4450,7 +4861,7 @@ async function tryCloneInRunningContainer(input, entry2) {
4450
4861
  logger.info(
4451
4862
  `Cloned ${entry2.url} into /workspaces/${containerName2}/${targetRel} inside the running container.`
4452
4863
  );
4453
- void path11;
4864
+ void path12;
4454
4865
  }
4455
4866
  function shquote(value) {
4456
4867
  return `'${value.replace(/'/g, `'\\''`)}'`;
@@ -4662,7 +5073,7 @@ async function mutate(opts, apply) {
4662
5073
  const logger = opts.logger ?? defaultLogger();
4663
5074
  let oldText;
4664
5075
  try {
4665
- oldText = await fs8.readFile(ymlPath, "utf8");
5076
+ oldText = await fs9.readFile(ymlPath, "utf8");
4666
5077
  } catch {
4667
5078
  throw new Error(
4668
5079
  `No such config: ${ymlPath}. Run \`monoceros init <template> ${opts.name}\` first.`
@@ -4687,7 +5098,7 @@ async function mutate(opts, apply) {
4687
5098
  return { status: "aborted" };
4688
5099
  }
4689
5100
  }
4690
- await fs8.writeFile(ymlPath, newText, "utf8");
5101
+ await fs9.writeFile(ymlPath, newText, "utf8");
4691
5102
  logger.success(`Updated ${ymlPath}.`);
4692
5103
  logger.info(
4693
5104
  `Run \`monoceros apply ${opts.name}\` to rebuild the dev-container and pick up the change.`
@@ -5235,8 +5646,8 @@ var init_add_service = __esm({
5235
5646
  });
5236
5647
 
5237
5648
  // src/config/state.ts
5238
- import { promises as fs9 } from "fs";
5239
- import path12 from "path";
5649
+ import { promises as fs10 } from "fs";
5650
+ import path13 from "path";
5240
5651
  function buildStateFile(opts) {
5241
5652
  return {
5242
5653
  schemaVersion: CONFIG_SCHEMA_VERSION,
@@ -5247,20 +5658,20 @@ function buildStateFile(opts) {
5247
5658
  };
5248
5659
  }
5249
5660
  function stateFilePath(targetDir) {
5250
- return path12.join(targetDir, ".monoceros", "state.json");
5661
+ return path13.join(targetDir, ".monoceros", "state.json");
5251
5662
  }
5252
5663
  async function readStateFile(targetDir) {
5253
5664
  try {
5254
- const content = await fs9.readFile(stateFilePath(targetDir), "utf8");
5665
+ const content = await fs10.readFile(stateFilePath(targetDir), "utf8");
5255
5666
  return JSON.parse(content);
5256
5667
  } catch {
5257
5668
  return void 0;
5258
5669
  }
5259
5670
  }
5260
5671
  async function writeStateFile(targetDir, state) {
5261
- const monocerosDir = path12.join(targetDir, ".monoceros");
5262
- await fs9.mkdir(monocerosDir, { recursive: true });
5263
- await fs9.writeFile(
5672
+ const monocerosDir = path13.join(targetDir, ".monoceros");
5673
+ await fs10.mkdir(monocerosDir, { recursive: true });
5674
+ await fs10.writeFile(
5264
5675
  stateFilePath(targetDir),
5265
5676
  JSON.stringify(state, null, 2) + "\n"
5266
5677
  );
@@ -5366,15 +5777,15 @@ var init_transform = __esm({
5366
5777
  });
5367
5778
 
5368
5779
  // 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";
5780
+ import { spawn as spawn5 } from "child_process";
5781
+ import { existsSync as existsSync9, promises as fsp4, readFileSync as readFileSync5 } from "fs";
5371
5782
  import http from "http";
5372
- import path13 from "path";
5783
+ import path14 from "path";
5373
5784
  function relayDir(root) {
5374
- return path13.join(root, RELAY_DIRNAME);
5785
+ return path14.join(root, RELAY_DIRNAME);
5375
5786
  }
5376
5787
  function relayUrlFile(root) {
5377
- return path13.join(relayDir(root), "url");
5788
+ return path14.join(relayDir(root), "url");
5378
5789
  }
5379
5790
  function parseCallbackTarget(authUrl) {
5380
5791
  try {
@@ -5416,7 +5827,7 @@ function openInBrowser(url) {
5416
5827
  const platform = process.platform;
5417
5828
  const [cmd, args] = platform === "darwin" ? ["open", [url]] : platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
5418
5829
  try {
5419
- const child = spawn4(cmd, args, {
5830
+ const child = spawn5(cmd, args, {
5420
5831
  stdio: "ignore",
5421
5832
  detached: true
5422
5833
  });
@@ -5428,7 +5839,7 @@ function openInBrowser(url) {
5428
5839
  }
5429
5840
  async function startBrowserBridge(opts) {
5430
5841
  const dir = relayDir(opts.root);
5431
- const relayScript = path13.join(dir, "xdg-open");
5842
+ const relayScript = path14.join(dir, "xdg-open");
5432
5843
  const urlFile = relayUrlFile(opts.root);
5433
5844
  await fsp4.mkdir(dir, { recursive: true });
5434
5845
  await fsp4.rm(urlFile, { force: true });
@@ -5487,10 +5898,10 @@ function watchRelayUrl(opts) {
5487
5898
  servers.push(server);
5488
5899
  };
5489
5900
  const poll = setInterval(() => {
5490
- if (!existsSync8(opts.urlFile)) return;
5901
+ if (!existsSync9(opts.urlFile)) return;
5491
5902
  let content = "";
5492
5903
  try {
5493
- content = readFileSync4(opts.urlFile, "utf8");
5904
+ content = readFileSync5(opts.urlFile, "utf8");
5494
5905
  } catch {
5495
5906
  return;
5496
5907
  }
@@ -5622,19 +6033,19 @@ var init_runtime_pull_hint = __esm({
5622
6033
  });
5623
6034
 
5624
6035
  // src/devcontainer/cli.ts
5625
- import { spawn as spawn5 } from "child_process";
5626
- import { readFileSync as readFileSync5 } from "fs";
6036
+ import { spawn as spawn6 } from "child_process";
6037
+ import { readFileSync as readFileSync6 } from "fs";
5627
6038
  import { createRequire } from "module";
5628
- import path14 from "path";
6039
+ import path15 from "path";
5629
6040
  function devcontainerCliPath() {
5630
6041
  if (cachedBinaryPath) return cachedBinaryPath;
5631
6042
  const pkgJsonPath = require_.resolve("@devcontainers/cli/package.json");
5632
- const pkg = JSON.parse(readFileSync5(pkgJsonPath, "utf8"));
6043
+ const pkg = JSON.parse(readFileSync6(pkgJsonPath, "utf8"));
5633
6044
  const binEntry = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.devcontainer ?? "";
5634
6045
  if (!binEntry) {
5635
6046
  throw new Error("Could not resolve @devcontainers/cli bin entry.");
5636
6047
  }
5637
- cachedBinaryPath = path14.resolve(path14.dirname(pkgJsonPath), binEntry);
6048
+ cachedBinaryPath = path15.resolve(path15.dirname(pkgJsonPath), binEntry);
5638
6049
  return cachedBinaryPath;
5639
6050
  }
5640
6051
  var require_, cachedBinaryPath, spawnDevcontainer;
@@ -5650,7 +6061,7 @@ var init_cli = __esm({
5650
6061
  const env = { ...process.env, DOCKER_CLI_HINTS: "false" };
5651
6062
  return new Promise((resolve, reject) => {
5652
6063
  if (options.interactive) {
5653
- const child2 = spawn5(process.execPath, [binPath, ...args], {
6064
+ const child2 = spawn6(process.execPath, [binPath, ...args], {
5654
6065
  cwd,
5655
6066
  env,
5656
6067
  stdio: "inherit"
@@ -5659,7 +6070,7 @@ var init_cli = __esm({
5659
6070
  child2.on("exit", (code) => resolve(code ?? 0));
5660
6071
  return;
5661
6072
  }
5662
- const child = spawn5(process.execPath, [binPath, ...args], {
6073
+ const child = spawn6(process.execPath, [binPath, ...args], {
5663
6074
  cwd,
5664
6075
  env,
5665
6076
  stdio: ["ignore", "pipe", "pipe"]
@@ -5707,14 +6118,14 @@ var init_cli = __esm({
5707
6118
  });
5708
6119
 
5709
6120
  // src/devcontainer/compose.ts
5710
- import { spawn as spawn6 } from "child_process";
5711
- import { existsSync as existsSync9 } from "fs";
5712
- import path15 from "path";
6121
+ import { spawn as spawn7 } from "child_process";
6122
+ import { existsSync as existsSync10 } from "fs";
6123
+ import path16 from "path";
5713
6124
  import { Writable } from "stream";
5714
6125
  import { consola as consola10 } from "consola";
5715
6126
  function spawnDockerComposeTo(opts) {
5716
6127
  return (args, cwd) => new Promise((resolve, reject) => {
5717
- const child = spawn6("docker", ["compose", ...args], {
6128
+ const child = spawn7("docker", ["compose", ...args], {
5718
6129
  cwd,
5719
6130
  stdio: ["inherit", "pipe", "pipe"]
5720
6131
  });
@@ -5778,22 +6189,22 @@ async function cleanupDockerObjects(opts) {
5778
6189
  return { exitCode: rmExit, removedIds: ids };
5779
6190
  }
5780
6191
  function composeProjectName(root) {
5781
- return `${path15.basename(root)}_devcontainer`;
6192
+ return `${path16.basename(root)}_devcontainer`;
5782
6193
  }
5783
6194
  function assertDevcontainer(root) {
5784
- if (!existsSync9(path15.join(root, ".devcontainer"))) {
6195
+ if (!existsSync10(path16.join(root, ".devcontainer"))) {
5785
6196
  throw new Error(
5786
6197
  `No .devcontainer/ at ${root}. Run \`monoceros apply <name>\` first.`
5787
6198
  );
5788
6199
  }
5789
6200
  }
5790
6201
  function isComposeMode(root) {
5791
- return existsSync9(path15.join(root, ".devcontainer", "compose.yaml"));
6202
+ return existsSync10(path16.join(root, ".devcontainer", "compose.yaml"));
5792
6203
  }
5793
6204
  function resolveCompose(root) {
5794
6205
  assertDevcontainer(root);
5795
- const composeFile = path15.join(root, ".devcontainer", "compose.yaml");
5796
- if (!existsSync9(composeFile)) {
6206
+ const composeFile = path16.join(root, ".devcontainer", "compose.yaml");
6207
+ if (!existsSync10(composeFile)) {
5797
6208
  throw new Error(
5798
6209
  `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
6210
  );
@@ -6001,7 +6412,7 @@ function imageContainerFilter(root) {
6001
6412
  async function stopImageContainer(opts) {
6002
6413
  const exec = opts.dockerExec ?? spawnDocker;
6003
6414
  const logger = opts.logger ?? { info: (msg) => consola10.info(msg) };
6004
- const name = path15.basename(opts.root);
6415
+ const name = path16.basename(opts.root);
6005
6416
  const ps = await exec([
6006
6417
  "ps",
6007
6418
  "-q",
@@ -6022,7 +6433,7 @@ async function stopImageContainer(opts) {
6022
6433
  async function statusImageContainer(opts) {
6023
6434
  const exec = opts.dockerExec ?? spawnDocker;
6024
6435
  const logger = opts.logger ?? { info: (msg) => consola10.info(msg) };
6025
- const name = path15.basename(opts.root);
6436
+ const name = path16.basename(opts.root);
6026
6437
  const res = await exec([
6027
6438
  "ps",
6028
6439
  "-a",
@@ -6064,7 +6475,7 @@ var init_compose = __esm({
6064
6475
  init_proxy();
6065
6476
  spawnDockerCompose = (args, cwd) => {
6066
6477
  return new Promise((resolve, reject) => {
6067
- const child = spawn6("docker", ["compose", ...args], {
6478
+ const child = spawn7("docker", ["compose", ...args], {
6068
6479
  cwd,
6069
6480
  stdio: ["inherit", "pipe", "pipe"]
6070
6481
  });
@@ -6076,7 +6487,7 @@ var init_compose = __esm({
6076
6487
  };
6077
6488
  spawnDocker = (args) => {
6078
6489
  return new Promise((resolve, reject) => {
6079
- const child = spawn6("docker", args, {
6490
+ const child = spawn7("docker", args, {
6080
6491
  stdio: ["ignore", "pipe", "pipe"]
6081
6492
  });
6082
6493
  let stdout = "";
@@ -6090,454 +6501,121 @@ var init_compose = __esm({
6090
6501
  child.on("error", reject);
6091
6502
  child.on(
6092
6503
  "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
- });
6504
+ (code) => resolve({ exitCode: code ?? 0, stdout, stderr })
6505
+ );
6506
+ });
6507
+ };
6508
+ BIND_SOURCE_MISSING_RE = /bind source path does not exist/i;
6509
+ BIND_RETRY_ATTEMPTS = 3;
6510
+ BIND_RETRY_DELAY_MS = 500;
6511
+ }
6512
+ });
6513
+
6514
+ // src/devcontainer/bridge-daemon.ts
6515
+ import { spawn as spawn8 } from "child_process";
6516
+ import {
6517
+ existsSync as existsSync11,
6518
+ mkdirSync,
6519
+ promises as fsp5,
6520
+ readFileSync as readFileSync7,
6521
+ writeFileSync
6522
+ } from "fs";
6523
+ import path17 from "path";
6524
+ function bridgePidFile(root) {
6525
+ return path17.join(relayDir(root), "daemon.pid");
6333
6526
  }
6334
- function isWsl() {
6527
+ function pidAlive(pid) {
6335
6528
  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");
6529
+ process.kill(pid, 0);
6530
+ return true;
6342
6531
  } catch {
6343
6532
  return false;
6344
6533
  }
6345
6534
  }
6346
- async function resolveWindowsProfile() {
6535
+ function runningBridgePid(root) {
6536
+ const file = bridgePidFile(root);
6537
+ if (!existsSync11(file)) return null;
6538
+ let pid = NaN;
6347
6539
  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 };
6540
+ pid = Number(readFileSync7(file, "utf8").trim());
6358
6541
  } catch {
6359
6542
  return null;
6360
6543
  }
6544
+ return Number.isInteger(pid) && pid > 0 && pidAlive(pid) ? pid : null;
6361
6545
  }
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 = "";
6546
+ function spawnBridgeDaemon(root) {
6420
6547
  try {
6421
- existing = await fs10.readFile(configPath, "utf8");
6548
+ if (runningBridgePid(root) !== null) return;
6549
+ const self = process.argv[1];
6550
+ if (!self) return;
6551
+ mkdirSync(relayDir(root), { recursive: true });
6552
+ const child = spawn8(process.execPath, [self, "__bridge", root], {
6553
+ detached: true,
6554
+ stdio: "ignore"
6555
+ });
6556
+ if (typeof child.pid === "number") {
6557
+ try {
6558
+ writeFileSync(bridgePidFile(root), String(child.pid));
6559
+ } catch {
6560
+ }
6561
+ }
6562
+ child.unref();
6422
6563
  } catch {
6423
- return;
6424
6564
  }
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
6565
  }
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;
6566
+ async function stopBridgeDaemon(root) {
6567
+ const pid = runningBridgePid(root);
6568
+ if (pid !== null) {
6569
+ try {
6570
+ process.kill(pid, "SIGTERM");
6571
+ } catch {
6572
+ }
6443
6573
  }
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
- );
6574
+ await fsp5.rm(bridgePidFile(root), { force: true });
6459
6575
  }
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
6576
+ async function runBridgeDaemon(opts) {
6577
+ const { root } = opts;
6578
+ const dockerExec = opts.dockerExec ?? spawnDocker;
6579
+ const lifecheckMs = opts.lifecheckMs ?? 5e3;
6580
+ await fsp5.mkdir(relayDir(root), { recursive: true });
6581
+ await fsp5.writeFile(bridgePidFile(root), String(process.pid));
6582
+ const watcher = watchRelayUrl({
6583
+ urlFile: relayUrlFile(root),
6584
+ root,
6585
+ spawn: opts.spawn ?? spawnDevcontainer
6466
6586
  });
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(() => {
6587
+ await new Promise((resolve) => {
6588
+ let done = false;
6589
+ const finish = () => {
6590
+ if (done) return;
6591
+ done = true;
6592
+ resolve();
6593
+ };
6594
+ const lifecheck = setInterval(() => {
6595
+ void isWorkspaceRunning(root, dockerExec).then((up) => {
6596
+ if (!up) {
6597
+ clearInterval(lifecheck);
6598
+ finish();
6599
+ }
6600
+ }).catch(() => {
6601
+ });
6602
+ }, lifecheckMs);
6603
+ const onSignal = () => {
6604
+ clearInterval(lifecheck);
6605
+ finish();
6606
+ };
6607
+ process.once("SIGTERM", onSignal);
6608
+ process.once("SIGINT", onSignal);
6491
6609
  });
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
- }
6610
+ watcher.dispose();
6611
+ await fsp5.rm(bridgePidFile(root), { force: true });
6523
6612
  }
6524
- var realKeygen;
6525
- var init_ssh_attach = __esm({
6526
- "src/devcontainer/ssh-attach.ts"() {
6613
+ var init_bridge_daemon = __esm({
6614
+ "src/devcontainer/bridge-daemon.ts"() {
6527
6615
  "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
- };
6616
+ init_browser_bridge();
6617
+ init_cli();
6618
+ init_compose();
6541
6619
  }
6542
6620
  });
6543
6621
 
@@ -8155,6 +8233,19 @@ Fix the value in the env file (or the yml).`
8155
8233
  if (runtimeSupportsBrowserBridge(createOpts.runtimeVersion)) {
8156
8234
  spawnBridgeDaemon(targetDir);
8157
8235
  }
8236
+ if (runtimeSupportsHostKeyPinning(createOpts.runtimeVersion)) {
8237
+ try {
8238
+ await recordHostKey({
8239
+ name: opts.name,
8240
+ targetDir,
8241
+ ...opts.sshUserSshDir ? { userSshDir: opts.sshUserSshDir } : {}
8242
+ });
8243
+ } catch (err) {
8244
+ (logger.warn ?? logger.info)(
8245
+ `Recording the SSH host key skipped: ${err instanceof Error ? err.message : String(err)}`
8246
+ );
8247
+ }
8248
+ }
8158
8249
  const summaryLines = buildApplySummary(createOpts);
8159
8250
  if (summaryLines.length > 0) {
8160
8251
  const formatted = formatApplySummary(summaryLines);
@@ -8502,7 +8593,7 @@ var CLI_VERSION;
8502
8593
  var init_version = __esm({
8503
8594
  "src/version.ts"() {
8504
8595
  "use strict";
8505
- CLI_VERSION = true ? "1.32.0" : "dev";
8596
+ CLI_VERSION = true ? "1.33.0" : "dev";
8506
8597
  }
8507
8598
  });
8508
8599