@getmonoceros/workbench 1.27.0 → 1.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -2278,7 +2278,7 @@ var init_catalog = __esm({
2278
2278
  override = process.env.MONOCEROS_BASE_IMAGE_OVERRIDE?.trim();
2279
2279
  BASE_IMAGE = override && override.length > 0 ? override : DEFAULT_BASE_IMAGE;
2280
2280
  RUNTIME_IMAGE_REPO = "ghcr.io/getmonoceros/monoceros-runtime";
2281
- DEFAULT_RUNTIME_VERSION = true ? "1.2.0" : readFileSync3(
2281
+ DEFAULT_RUNTIME_VERSION = true ? "1.3.2" : readFileSync3(
2282
2282
  fileURLToPath2(
2283
2283
  new URL("../../../../images/runtime/VERSION", import.meta.url)
2284
2284
  ),
@@ -2964,6 +2964,35 @@ function ideStateVolumes(name) {
2964
2964
  volume: `monoceros-${name}-vscodium-userdata`,
2965
2965
  target: "/home/node/.vscodium-server/data/User",
2966
2966
  minRuntime: "1.2.0"
2967
+ },
2968
+ // JetBrains (ADR 0022). Only the backend DISTRIBUTION
2969
+ // (`~/.cache/JetBrains/RemoteDev/dist`, ~3 GB, identical across
2970
+ // containers) is shared machine-wide - downloaded once. The sibling
2971
+ // RemoteDev state (`active/`, `recent/`, `remote-dev-worker/`) is
2972
+ // per-user session state and stays per-container, inside the
2973
+ // per-container `~/.cache/JetBrains` volume; sharing it pooled every
2974
+ // container's recent-projects/active-sessions (the "wild" Gateway
2975
+ // list). The shared `dist` mount nests two levels into that volume.
2976
+ {
2977
+ volume: "monoceros-jetbrains-dist",
2978
+ target: "/home/node/.cache/JetBrains/RemoteDev/dist",
2979
+ minRuntime: "1.3.2",
2980
+ shared: true
2981
+ },
2982
+ {
2983
+ volume: `monoceros-${name}-jetbrains-cache`,
2984
+ target: "/home/node/.cache/JetBrains",
2985
+ minRuntime: "1.3.0"
2986
+ },
2987
+ {
2988
+ volume: `monoceros-${name}-jetbrains-config`,
2989
+ target: "/home/node/.config/JetBrains",
2990
+ minRuntime: "1.3.0"
2991
+ },
2992
+ {
2993
+ volume: `monoceros-${name}-jetbrains-data`,
2994
+ target: "/home/node/.local/share/JetBrains",
2995
+ minRuntime: "1.3.0"
2967
2996
  }
2968
2997
  ];
2969
2998
  }
@@ -5146,7 +5175,7 @@ var init_transform = __esm({
5146
5175
  // src/devcontainer/ssh-attach.ts
5147
5176
  import { spawn as spawn4 } from "child_process";
5148
5177
  import { promises as fs10 } from "fs";
5149
- import { existsSync as existsSync8 } from "fs";
5178
+ import { existsSync as existsSync8, readFileSync as readFileSync4 } from "fs";
5150
5179
  import os2 from "os";
5151
5180
  import path13 from "path";
5152
5181
  function sshHomeDir(home) {
@@ -5246,6 +5275,155 @@ ${existing}` : prefix;
5246
5275
  await fs10.chmod(configPath, 384).catch(() => {
5247
5276
  });
5248
5277
  }
5278
+ function runCapture(cmd, args) {
5279
+ return new Promise((resolve, reject) => {
5280
+ const child = spawn4(cmd, args, {
5281
+ stdio: ["ignore", "pipe", "ignore"]
5282
+ });
5283
+ let stdout = "";
5284
+ child.stdout.on("data", (c) => {
5285
+ stdout += c.toString();
5286
+ });
5287
+ child.on("error", reject);
5288
+ child.on("exit", (code) => resolve({ stdout, exitCode: code ?? 0 }));
5289
+ });
5290
+ }
5291
+ function realIsWsl() {
5292
+ try {
5293
+ if (process.env.WSL_DISTRO_NAME) return true;
5294
+ const rel = readFileSync4(
5295
+ "/proc/sys/kernel/osrelease",
5296
+ "utf8"
5297
+ ).toLowerCase();
5298
+ return rel.includes("microsoft") || rel.includes("wsl");
5299
+ } catch {
5300
+ return false;
5301
+ }
5302
+ }
5303
+ async function realResolveWindowsProfile() {
5304
+ try {
5305
+ const r = await runCapture("cmd.exe", ["/c", "echo %USERPROFILE%"]);
5306
+ const homeWin = r.stdout.replace(/\r/g, "").trim();
5307
+ if (r.exitCode !== 0 || !homeWin || homeWin.includes("%USERPROFILE%")) {
5308
+ return null;
5309
+ }
5310
+ const w = await runCapture("wslpath", ["-u", homeWin]);
5311
+ const homeWsl = w.stdout.trim();
5312
+ const user = homeWin.split("\\").pop() ?? "";
5313
+ if (w.exitCode !== 0 || !homeWsl || !user) return null;
5314
+ return { homeWsl, homeWin, user };
5315
+ } catch {
5316
+ return null;
5317
+ }
5318
+ }
5319
+ async function realLockWindowsKey(winKeyPath, user) {
5320
+ await runCapture("icacls.exe", [
5321
+ winKeyPath,
5322
+ "/inheritance:r",
5323
+ "/grant:r",
5324
+ `${user}:R`
5325
+ ]);
5326
+ }
5327
+ function resolveWindowsDeps(d) {
5328
+ return {
5329
+ isWsl: d?.isWsl ?? realIsWsl,
5330
+ resolveProfile: d?.resolveProfile ?? realResolveWindowsProfile,
5331
+ lockKey: d?.lockKey ?? realLockWindowsKey
5332
+ };
5333
+ }
5334
+ function windowsHostBlock(hostAlias, keyWin) {
5335
+ return [
5336
+ `Host ${hostAlias}`,
5337
+ ` User node`,
5338
+ ` IdentityFile ${keyWin}`,
5339
+ ` IdentitiesOnly yes`,
5340
+ ` StrictHostKeyChecking no`,
5341
+ ` UserKnownHostsFile NUL`,
5342
+ ` ProxyCommand docker exec -i ${hostAlias} socat - TCP:127.0.0.1:22`
5343
+ ].join("\n");
5344
+ }
5345
+ function escapeRegExp2(s) {
5346
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5347
+ }
5348
+ function blockMarkers(hostAlias) {
5349
+ return {
5350
+ begin: `# >>> monoceros ${hostAlias} >>>`,
5351
+ end: `# <<< monoceros ${hostAlias} <<<`
5352
+ };
5353
+ }
5354
+ async function upsertMarkedBlock(configPath, hostAlias, body) {
5355
+ const { begin, end } = blockMarkers(hostAlias);
5356
+ const section = `${begin}
5357
+ ${body}
5358
+ ${end}`;
5359
+ await fs10.mkdir(path13.dirname(configPath), { recursive: true });
5360
+ let existing = "";
5361
+ try {
5362
+ existing = await fs10.readFile(configPath, "utf8");
5363
+ } catch {
5364
+ existing = "";
5365
+ }
5366
+ const re = new RegExp(`${escapeRegExp2(begin)}[\\s\\S]*?${escapeRegExp2(end)}`);
5367
+ if (re.test(existing)) {
5368
+ await fs10.writeFile(configPath, existing.replace(re, section));
5369
+ return;
5370
+ }
5371
+ const sep = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
5372
+ await fs10.writeFile(configPath, `${existing}${sep}${section}
5373
+ `);
5374
+ }
5375
+ async function removeMarkedBlock(configPath, hostAlias) {
5376
+ let existing = "";
5377
+ try {
5378
+ existing = await fs10.readFile(configPath, "utf8");
5379
+ } catch {
5380
+ return;
5381
+ }
5382
+ const { begin, end } = blockMarkers(hostAlias);
5383
+ const re = new RegExp(
5384
+ `\\n?${escapeRegExp2(begin)}[\\s\\S]*?${escapeRegExp2(end)}\\n?`,
5385
+ "g"
5386
+ );
5387
+ await fs10.writeFile(
5388
+ configPath,
5389
+ existing.replace(re, "\n").replace(/\n{3,}/g, "\n\n")
5390
+ );
5391
+ }
5392
+ async function setupWindowsBridge(name, hostAlias, privateKey, deps, logger) {
5393
+ if (!deps.isWsl()) return;
5394
+ const profile = await deps.resolveProfile();
5395
+ if (!profile) {
5396
+ logger.warn(
5397
+ "WSL detected but the Windows user profile could not be resolved; skipping the Windows SSH bridge."
5398
+ );
5399
+ return;
5400
+ }
5401
+ const monoDir = path13.join(profile.homeWsl, ".ssh", "monoceros");
5402
+ await fs10.mkdir(monoDir, { recursive: true });
5403
+ await fs10.copyFile(privateKey, path13.join(monoDir, name));
5404
+ const keyWin = `${profile.homeWin}\\.ssh\\monoceros\\${name}`;
5405
+ await upsertMarkedBlock(
5406
+ path13.join(profile.homeWsl, ".ssh", "config"),
5407
+ hostAlias,
5408
+ windowsHostBlock(hostAlias, keyWin)
5409
+ );
5410
+ await deps.lockKey(keyWin, profile.user);
5411
+ logger.info(
5412
+ `Windows SSH bridge ready: \`ssh ${hostAlias}\` (Codium/Gateway too).`
5413
+ );
5414
+ }
5415
+ async function removeWindowsBridge(name, hostAlias, deps) {
5416
+ if (!deps.isWsl()) return;
5417
+ const profile = await deps.resolveProfile();
5418
+ if (!profile) return;
5419
+ await fs10.rm(path13.join(profile.homeWsl, ".ssh", "monoceros", name), {
5420
+ force: true
5421
+ });
5422
+ await removeMarkedBlock(
5423
+ path13.join(profile.homeWsl, ".ssh", "config"),
5424
+ hostAlias
5425
+ );
5426
+ }
5249
5427
  async function setupSshAttach(opts) {
5250
5428
  const hostAlias = `monoceros-${opts.name}`;
5251
5429
  const logger = opts.logger ?? { info: () => {
@@ -5271,11 +5449,32 @@ async function setupSshAttach(opts) {
5271
5449
  configEntryContent(opts.name, hostAlias, keys.privateKey, proxyScript)
5272
5450
  );
5273
5451
  await ensureInclude(userSshDir, opts.home);
5452
+ try {
5453
+ await setupWindowsBridge(
5454
+ opts.name,
5455
+ hostAlias,
5456
+ keys.privateKey,
5457
+ resolveWindowsDeps(opts.windows),
5458
+ logger
5459
+ );
5460
+ } catch (err) {
5461
+ logger.warn(
5462
+ `Windows SSH bridge skipped: ${err instanceof Error ? err.message : String(err)}`
5463
+ );
5464
+ }
5274
5465
  return { hostAlias, configured: true };
5275
5466
  }
5276
- async function removeSshAttach(home, name) {
5467
+ async function removeSshAttach(home, name, windows) {
5277
5468
  await fs10.rm(sshProxyScriptPath(home, name), { force: true });
5278
5469
  await fs10.rm(sshConfigEntryPath(home, name), { force: true });
5470
+ try {
5471
+ await removeWindowsBridge(
5472
+ name,
5473
+ `monoceros-${name}`,
5474
+ resolveWindowsDeps(windows)
5475
+ );
5476
+ } catch {
5477
+ }
5279
5478
  }
5280
5479
  var realKeygen;
5281
5480
  var init_ssh_attach = __esm({
@@ -6352,13 +6551,13 @@ var init_runtime_pull_hint = __esm({
6352
6551
 
6353
6552
  // src/devcontainer/cli.ts
6354
6553
  import { spawn as spawn5 } from "child_process";
6355
- import { readFileSync as readFileSync4 } from "fs";
6554
+ import { readFileSync as readFileSync5 } from "fs";
6356
6555
  import { createRequire } from "module";
6357
6556
  import path16 from "path";
6358
6557
  function devcontainerCliPath() {
6359
6558
  if (cachedBinaryPath) return cachedBinaryPath;
6360
6559
  const pkgJsonPath = require_.resolve("@devcontainers/cli/package.json");
6361
- const pkg = JSON.parse(readFileSync4(pkgJsonPath, "utf8"));
6560
+ const pkg = JSON.parse(readFileSync5(pkgJsonPath, "utf8"));
6362
6561
  const binEntry = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.devcontainer ?? "";
6363
6562
  if (!binEntry) {
6364
6563
  throw new Error("Could not resolve @devcontainers/cli bin entry.");
@@ -7583,7 +7782,7 @@ var init_apply = __esm({
7583
7782
 
7584
7783
  // src/devcontainer/browser-bridge.ts
7585
7784
  import { spawn as spawn9 } from "child_process";
7586
- import { existsSync as existsSync11, promises as fsp5, readFileSync as readFileSync5 } from "fs";
7785
+ import { existsSync as existsSync11, promises as fsp5, readFileSync as readFileSync6 } from "fs";
7587
7786
  import http from "http";
7588
7787
  import path20 from "path";
7589
7788
  function parseCallbackTarget(authUrl) {
@@ -7686,7 +7885,7 @@ exit 0
7686
7885
  if (!existsSync11(urlFile)) return;
7687
7886
  let content = "";
7688
7887
  try {
7689
- content = readFileSync5(urlFile, "utf8");
7888
+ content = readFileSync6(urlFile, "utf8");
7690
7889
  } catch {
7691
7890
  return;
7692
7891
  }
@@ -7869,7 +8068,7 @@ var CLI_VERSION;
7869
8068
  var init_version = __esm({
7870
8069
  "src/version.ts"() {
7871
8070
  "use strict";
7872
- CLI_VERSION = true ? "1.27.0" : "dev";
8071
+ CLI_VERSION = true ? "1.29.0" : "dev";
7873
8072
  }
7874
8073
  });
7875
8074
 
@@ -9747,7 +9946,7 @@ async function runRemove(opts) {
9747
9946
  logger,
9748
9947
  exec: dockerExec
9749
9948
  });
9750
- const ideVolumes = ideStateVolumes(opts.name).map((v) => v.volume);
9949
+ const ideVolumes = ideStateVolumes(opts.name).filter((v) => !v.shared).map((v) => v.volume);
9751
9950
  await dockerExec(["volume", "rm", "-f", ...ideVolumes]);
9752
9951
  let backupPath = null;
9753
9952
  if (!opts.noBackup && (hasYml || hasContainer)) {