@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 +641 -534
- package/dist/bin.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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
|
|
2643
|
-
import
|
|
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 =
|
|
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(
|
|
3074
|
+
await fsp3.mkdir(path10.dirname(file), { recursive: true });
|
|
2670
3075
|
let config = {};
|
|
2671
|
-
if (
|
|
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
|
|
2803
|
-
import
|
|
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 ?
|
|
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 ?
|
|
2982
|
-
if (descriptor && 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
|
|
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 =
|
|
3562
|
-
await
|
|
3563
|
-
await
|
|
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
|
|
3988
|
+
if (await fs8.readFile(filePath, "utf8") === content) return false;
|
|
3568
3989
|
} catch {
|
|
3569
3990
|
}
|
|
3570
|
-
await
|
|
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 =
|
|
3576
|
-
const monocerosDir =
|
|
3577
|
-
const projectsDir =
|
|
3578
|
-
const homeDir =
|
|
3579
|
-
const dataDir =
|
|
3580
|
-
await
|
|
3581
|
-
await
|
|
3582
|
-
await
|
|
3583
|
-
await
|
|
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
|
|
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
|
|
4010
|
+
await fs8.mkdir(path11.join(dataDir, svc.name), { recursive: true });
|
|
3590
4011
|
}
|
|
3591
4012
|
}
|
|
3592
4013
|
}
|
|
3593
|
-
const containerGitignore =
|
|
3594
|
-
await
|
|
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 =
|
|
3599
|
-
if (!
|
|
3600
|
-
await
|
|
4019
|
+
const gitkeep = path11.join(projectsDir, ".gitkeep");
|
|
4020
|
+
if (!existsSync8(gitkeep)) {
|
|
4021
|
+
await fs8.writeFile(gitkeep, "");
|
|
3601
4022
|
}
|
|
3602
|
-
await
|
|
3603
|
-
|
|
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
|
-
|
|
4029
|
+
path11.join(devcontainerDir, "devcontainer.json"),
|
|
3609
4030
|
JSON.stringify(devcontainerJson, null, 2) + "\n"
|
|
3610
4031
|
);
|
|
3611
|
-
const featuresDir =
|
|
3612
|
-
if (
|
|
3613
|
-
await
|
|
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 =
|
|
3619
|
-
await
|
|
3620
|
-
const entries = await
|
|
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
|
|
3627
|
-
|
|
3628
|
-
|
|
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
|
|
3633
|
-
|
|
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
|
|
4061
|
+
await fs8.mkdir(path11.join(homeDir, sub), { recursive: true });
|
|
3641
4062
|
}
|
|
3642
4063
|
for (const entry2 of f.persistentHomeFiles) {
|
|
3643
|
-
const filePath =
|
|
3644
|
-
await
|
|
3645
|
-
if (!
|
|
3646
|
-
await
|
|
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 =
|
|
4074
|
+
const composePath = path11.join(devcontainerDir, "compose.yaml");
|
|
3654
4075
|
if (needsCompose(opts)) {
|
|
3655
4076
|
await writeIfChanged(composePath, buildComposeYaml(opts, dockerMode));
|
|
3656
|
-
} else if (
|
|
3657
|
-
await
|
|
4077
|
+
} else if (existsSync8(composePath)) {
|
|
4078
|
+
await fs8.rm(composePath);
|
|
3658
4079
|
}
|
|
3659
|
-
const workspacePath =
|
|
4080
|
+
const workspacePath = path11.join(targetDir, `${opts.name}.code-workspace`);
|
|
3660
4081
|
let existingWorkspace;
|
|
3661
4082
|
try {
|
|
3662
|
-
const raw = await
|
|
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
|
|
3670
|
-
const vscodeDir =
|
|
3671
|
-
const settingsPath =
|
|
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
|
|
4095
|
+
existingSettings = JSON.parse(await fs8.readFile(settingsPath, "utf8"));
|
|
3675
4096
|
} catch {
|
|
3676
4097
|
existingSettings = void 0;
|
|
3677
4098
|
}
|
|
3678
|
-
await
|
|
3679
|
-
await
|
|
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
|
|
4618
|
+
import { promises as fs9 } from "fs";
|
|
4197
4619
|
import { consola as consola2 } from "consola";
|
|
4198
4620
|
import { createPatch } from "diff";
|
|
4199
|
-
import
|
|
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
|
|
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
|
|
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
|
|
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
|
|
5239
|
-
import
|
|
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
|
|
5672
|
+
return path13.join(targetDir, ".monoceros", "state.json");
|
|
5251
5673
|
}
|
|
5252
5674
|
async function readStateFile(targetDir) {
|
|
5253
5675
|
try {
|
|
5254
|
-
const content = await
|
|
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 =
|
|
5262
|
-
await
|
|
5263
|
-
await
|
|
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
|
|
5370
|
-
import { existsSync as
|
|
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
|
|
5794
|
+
import path14 from "path";
|
|
5373
5795
|
function relayDir(root) {
|
|
5374
|
-
return
|
|
5796
|
+
return path14.join(root, RELAY_DIRNAME);
|
|
5375
5797
|
}
|
|
5376
5798
|
function relayUrlFile(root) {
|
|
5377
|
-
return
|
|
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 =
|
|
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 =
|
|
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 (!
|
|
5912
|
+
if (!existsSync9(opts.urlFile)) return;
|
|
5491
5913
|
let content = "";
|
|
5492
5914
|
try {
|
|
5493
|
-
content =
|
|
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
|
|
5626
|
-
import { readFileSync as
|
|
6047
|
+
import { spawn as spawn6 } from "child_process";
|
|
6048
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
5627
6049
|
import { createRequire } from "module";
|
|
5628
|
-
import
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
5711
|
-
import { existsSync as
|
|
5712
|
-
import
|
|
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 =
|
|
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 `${
|
|
6203
|
+
return `${path16.basename(root)}_devcontainer`;
|
|
5782
6204
|
}
|
|
5783
6205
|
function assertDevcontainer(root) {
|
|
5784
|
-
if (!
|
|
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
|
|
6213
|
+
return existsSync10(path16.join(root, ".devcontainer", "compose.yaml"));
|
|
5792
6214
|
}
|
|
5793
6215
|
function resolveCompose(root) {
|
|
5794
6216
|
assertDevcontainer(root);
|
|
5795
|
-
const composeFile =
|
|
5796
|
-
if (!
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
6105
|
-
import {
|
|
6106
|
-
existsSync as
|
|
6107
|
-
mkdirSync,
|
|
6108
|
-
promises as fsp5,
|
|
6109
|
-
readFileSync as
|
|
6110
|
-
writeFileSync
|
|
6111
|
-
} from "fs";
|
|
6112
|
-
import
|
|
6113
|
-
function bridgePidFile(root) {
|
|
6114
|
-
return
|
|
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
|
|
6538
|
+
function pidAlive(pid) {
|
|
6335
6539
|
try {
|
|
6336
|
-
|
|
6337
|
-
|
|
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
|
-
|
|
6546
|
+
function runningBridgePid(root) {
|
|
6547
|
+
const file = bridgePidFile(root);
|
|
6548
|
+
if (!existsSync11(file)) return null;
|
|
6549
|
+
let pid = NaN;
|
|
6347
6550
|
try {
|
|
6348
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
|
|
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
|
-
|
|
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
|
|
6461
|
-
|
|
6462
|
-
const
|
|
6463
|
-
|
|
6464
|
-
await
|
|
6465
|
-
|
|
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
|
|
6468
|
-
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
|
|
6485
|
-
|
|
6486
|
-
|
|
6487
|
-
|
|
6488
|
-
|
|
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
|
-
|
|
6493
|
-
|
|
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
|
|
6525
|
-
|
|
6526
|
-
"src/devcontainer/ssh-attach.ts"() {
|
|
6624
|
+
var init_bridge_daemon = __esm({
|
|
6625
|
+
"src/devcontainer/bridge-daemon.ts"() {
|
|
6527
6626
|
"use strict";
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
|
|
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.
|
|
8612
|
+
CLI_VERSION = true ? "1.33.1" : "dev";
|
|
8506
8613
|
}
|
|
8507
8614
|
});
|
|
8508
8615
|
|