@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 +625 -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,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
|
|
2643
|
-
import
|
|
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 =
|
|
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(
|
|
3063
|
+
await fsp3.mkdir(path10.dirname(file), { recursive: true });
|
|
2670
3064
|
let config = {};
|
|
2671
|
-
if (
|
|
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
|
|
2803
|
-
import
|
|
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 ?
|
|
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 ?
|
|
2982
|
-
if (descriptor && 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
|
|
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 =
|
|
3562
|
-
await
|
|
3563
|
-
await
|
|
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
|
|
3977
|
+
if (await fs8.readFile(filePath, "utf8") === content) return false;
|
|
3568
3978
|
} catch {
|
|
3569
3979
|
}
|
|
3570
|
-
await
|
|
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 =
|
|
3576
|
-
const monocerosDir =
|
|
3577
|
-
const projectsDir =
|
|
3578
|
-
const homeDir =
|
|
3579
|
-
const dataDir =
|
|
3580
|
-
await
|
|
3581
|
-
await
|
|
3582
|
-
await
|
|
3583
|
-
await
|
|
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
|
|
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
|
|
3999
|
+
await fs8.mkdir(path11.join(dataDir, svc.name), { recursive: true });
|
|
3590
4000
|
}
|
|
3591
4001
|
}
|
|
3592
4002
|
}
|
|
3593
|
-
const containerGitignore =
|
|
3594
|
-
await
|
|
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 =
|
|
3599
|
-
if (!
|
|
3600
|
-
await
|
|
4008
|
+
const gitkeep = path11.join(projectsDir, ".gitkeep");
|
|
4009
|
+
if (!existsSync8(gitkeep)) {
|
|
4010
|
+
await fs8.writeFile(gitkeep, "");
|
|
3601
4011
|
}
|
|
3602
|
-
await
|
|
3603
|
-
|
|
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
|
-
|
|
4018
|
+
path11.join(devcontainerDir, "devcontainer.json"),
|
|
3609
4019
|
JSON.stringify(devcontainerJson, null, 2) + "\n"
|
|
3610
4020
|
);
|
|
3611
|
-
const featuresDir =
|
|
3612
|
-
if (
|
|
3613
|
-
await
|
|
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 =
|
|
3619
|
-
await
|
|
3620
|
-
const entries = await
|
|
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
|
|
3627
|
-
|
|
3628
|
-
|
|
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
|
|
3633
|
-
|
|
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
|
|
4050
|
+
await fs8.mkdir(path11.join(homeDir, sub), { recursive: true });
|
|
3641
4051
|
}
|
|
3642
4052
|
for (const entry2 of f.persistentHomeFiles) {
|
|
3643
|
-
const filePath =
|
|
3644
|
-
await
|
|
3645
|
-
if (!
|
|
3646
|
-
await
|
|
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 =
|
|
4063
|
+
const composePath = path11.join(devcontainerDir, "compose.yaml");
|
|
3654
4064
|
if (needsCompose(opts)) {
|
|
3655
4065
|
await writeIfChanged(composePath, buildComposeYaml(opts, dockerMode));
|
|
3656
|
-
} else if (
|
|
3657
|
-
await
|
|
4066
|
+
} else if (existsSync8(composePath)) {
|
|
4067
|
+
await fs8.rm(composePath);
|
|
3658
4068
|
}
|
|
3659
|
-
const workspacePath =
|
|
4069
|
+
const workspacePath = path11.join(targetDir, `${opts.name}.code-workspace`);
|
|
3660
4070
|
let existingWorkspace;
|
|
3661
4071
|
try {
|
|
3662
|
-
const raw = await
|
|
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
|
|
3670
|
-
const vscodeDir =
|
|
3671
|
-
const settingsPath =
|
|
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
|
|
4084
|
+
existingSettings = JSON.parse(await fs8.readFile(settingsPath, "utf8"));
|
|
3675
4085
|
} catch {
|
|
3676
4086
|
existingSettings = void 0;
|
|
3677
4087
|
}
|
|
3678
|
-
await
|
|
3679
|
-
await
|
|
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
|
|
4607
|
+
import { promises as fs9 } from "fs";
|
|
4197
4608
|
import { consola as consola2 } from "consola";
|
|
4198
4609
|
import { createPatch } from "diff";
|
|
4199
|
-
import
|
|
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
|
|
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
|
|
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
|
|
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
|
|
5239
|
-
import
|
|
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
|
|
5661
|
+
return path13.join(targetDir, ".monoceros", "state.json");
|
|
5251
5662
|
}
|
|
5252
5663
|
async function readStateFile(targetDir) {
|
|
5253
5664
|
try {
|
|
5254
|
-
const content = await
|
|
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 =
|
|
5262
|
-
await
|
|
5263
|
-
await
|
|
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
|
|
5370
|
-
import { existsSync as
|
|
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
|
|
5783
|
+
import path14 from "path";
|
|
5373
5784
|
function relayDir(root) {
|
|
5374
|
-
return
|
|
5785
|
+
return path14.join(root, RELAY_DIRNAME);
|
|
5375
5786
|
}
|
|
5376
5787
|
function relayUrlFile(root) {
|
|
5377
|
-
return
|
|
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 =
|
|
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 =
|
|
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 (!
|
|
5901
|
+
if (!existsSync9(opts.urlFile)) return;
|
|
5491
5902
|
let content = "";
|
|
5492
5903
|
try {
|
|
5493
|
-
content =
|
|
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
|
|
5626
|
-
import { readFileSync as
|
|
6036
|
+
import { spawn as spawn6 } from "child_process";
|
|
6037
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
5627
6038
|
import { createRequire } from "module";
|
|
5628
|
-
import
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
5711
|
-
import { existsSync as
|
|
5712
|
-
import
|
|
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 =
|
|
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 `${
|
|
6192
|
+
return `${path16.basename(root)}_devcontainer`;
|
|
5782
6193
|
}
|
|
5783
6194
|
function assertDevcontainer(root) {
|
|
5784
|
-
if (!
|
|
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
|
|
6202
|
+
return existsSync10(path16.join(root, ".devcontainer", "compose.yaml"));
|
|
5792
6203
|
}
|
|
5793
6204
|
function resolveCompose(root) {
|
|
5794
6205
|
assertDevcontainer(root);
|
|
5795
|
-
const composeFile =
|
|
5796
|
-
if (!
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
});
|
|
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
|
|
6527
|
+
function pidAlive(pid) {
|
|
6335
6528
|
try {
|
|
6336
|
-
|
|
6337
|
-
|
|
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
|
-
|
|
6535
|
+
function runningBridgePid(root) {
|
|
6536
|
+
const file = bridgePidFile(root);
|
|
6537
|
+
if (!existsSync11(file)) return null;
|
|
6538
|
+
let pid = NaN;
|
|
6347
6539
|
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 };
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
|
|
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
|
-
|
|
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
|
|
6461
|
-
|
|
6462
|
-
const
|
|
6463
|
-
|
|
6464
|
-
await
|
|
6465
|
-
|
|
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
|
|
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(() => {
|
|
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
|
-
|
|
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
|
-
}
|
|
6610
|
+
watcher.dispose();
|
|
6611
|
+
await fsp5.rm(bridgePidFile(root), { force: true });
|
|
6523
6612
|
}
|
|
6524
|
-
var
|
|
6525
|
-
|
|
6526
|
-
"src/devcontainer/ssh-attach.ts"() {
|
|
6613
|
+
var init_bridge_daemon = __esm({
|
|
6614
|
+
"src/devcontainer/bridge-daemon.ts"() {
|
|
6527
6615
|
"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
|
-
};
|
|
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.
|
|
8596
|
+
CLI_VERSION = true ? "1.33.0" : "dev";
|
|
8506
8597
|
}
|
|
8507
8598
|
});
|
|
8508
8599
|
|