@getmonoceros/workbench 1.1.0 → 1.3.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/README.md CHANGED
@@ -17,16 +17,18 @@ das vorab und geben plattform-spezifische Anleitung aus.
17
17
  ## Installation
18
18
 
19
19
  ```sh
20
- npm install -g @getmonoceros/workbench
20
+ # macOS / Linux
21
+ curl -fsSL https://raw.githubusercontent.com/getmonoceros/workbench/main/install.sh | sh
21
22
  ```
22
23
 
23
- Oder über das Install-Skript:
24
-
25
- ```sh
26
- curl -fsSL https://raw.githubusercontent.com/getmonoceros/workbench/main/install.sh | sh
24
+ ```powershell
25
+ # Windows (PowerShell)
26
+ iwr -useb https://raw.githubusercontent.com/getmonoceros/workbench/main/install.ps1 | iex
27
27
  ```
28
28
 
29
- Windows: das passende `install.ps1` über PowerShell.
29
+ Das Skript prüft Docker + Node, installiert das Paket global via
30
+ `npm install -g`, und richtet die Shell-Completion für deine Shell
31
+ ein.
30
32
 
31
33
  ## Erste Schritte
32
34
 
package/dist/bin.js CHANGED
@@ -122,12 +122,13 @@ function renderCommandsBlock(entries) {
122
122
  const renderSection = (label, items) => {
123
123
  if (items.length === 0) return;
124
124
  lines.push("");
125
- lines.push(` ${grey(label)}`);
125
+ lines.push(underline(grey(label)));
126
+ lines.push("");
126
127
  const rows = items.map((e) => [
127
128
  cyan(e.name),
128
129
  e.description
129
130
  ]);
130
- lines.push(alignTable(rows, " "));
131
+ lines.push(alignTable(rows, ""));
131
132
  };
132
133
  for (const { key, label } of GROUPS) {
133
134
  renderSection(label, byGroup.get(key) ?? []);
@@ -202,25 +203,25 @@ function detectHelpRequest(argv, main2) {
202
203
  const separatorIdx = argv.indexOf("--");
203
204
  if (helpIdx === -1) return null;
204
205
  if (separatorIdx !== -1 && separatorIdx < helpIdx) return null;
205
- const path14 = [];
206
+ const path13 = [];
206
207
  const tokens = argv.slice(
207
208
  0,
208
209
  separatorIdx === -1 ? argv.length : separatorIdx
209
210
  );
210
211
  let cursor = main2;
211
212
  const mainName = (main2.meta ?? {}).name ?? "monoceros";
212
- path14.push(mainName);
213
+ path13.push(mainName);
213
214
  for (const tok of tokens) {
214
215
  if (tok.startsWith("-")) continue;
215
216
  const subs = cursor.subCommands ?? {};
216
217
  if (tok in subs) {
217
218
  cursor = subs[tok];
218
- path14.push(tok);
219
+ path13.push(tok);
219
220
  continue;
220
221
  }
221
222
  break;
222
223
  }
223
- return { path: path14, cmd: cursor };
224
+ return { path: path13, cmd: cursor };
224
225
  }
225
226
  async function maybeRenderHelp(argv, main2) {
226
227
  const hit = detectHelpRequest(argv, main2);
@@ -450,6 +451,9 @@ function workbenchCheckoutRoot() {
450
451
  function componentsDir(root = workbenchRoot()) {
451
452
  return path.join(root, "templates", "components");
452
453
  }
454
+ function bundledFeaturesDir(root = workbenchRoot()) {
455
+ return path.join(root, "features");
456
+ }
453
457
  function containerConfigsDir(home = monocerosHome()) {
454
458
  return path.join(home, "container-configs");
455
459
  }
@@ -465,6 +469,16 @@ function containerDir(name, home = monocerosHome()) {
465
469
  function monocerosConfigPath(home = monocerosHome()) {
466
470
  return path.join(home, "monoceros-config.yml");
467
471
  }
472
+ function prettyPath(p) {
473
+ const home = os.homedir();
474
+ if (!home) return p;
475
+ if (p === home) return "~";
476
+ const prefix = home.endsWith(path.sep) ? home : home + path.sep;
477
+ if (p.startsWith(prefix)) {
478
+ return "~" + path.sep + p.slice(prefix.length);
479
+ }
480
+ return p;
481
+ }
468
482
 
469
483
  // src/create/catalog.ts
470
484
  var DEFAULT_BASE_IMAGE = "ghcr.io/getmonoceros/monoceros-runtime:1";
@@ -2311,7 +2325,7 @@ async function runApply(opts) {
2311
2325
  });
2312
2326
  }
2313
2327
  logger.success(
2314
- `Materialized config '${opts.name}' into ${targetDir}. Starting container\u2026`
2328
+ `Materialized config '${opts.name}' into ${prettyPath(targetDir)}. Starting container\u2026`
2315
2329
  );
2316
2330
  const exitCode = await runContainerCycle(targetDir, {
2317
2331
  hasCompose: needsCompose(createOpts),
@@ -2362,7 +2376,7 @@ function warnOnDeprecatedFeatureRefs(containerFeatures, globalConfig, logger) {
2362
2376
  }
2363
2377
 
2364
2378
  // src/version.ts
2365
- var CLI_VERSION = "1.1.0";
2379
+ var CLI_VERSION = "1.3.0";
2366
2380
 
2367
2381
  // src/commands/_dispatch.ts
2368
2382
  import { consola as consola11 } from "consola";
@@ -2451,7 +2465,7 @@ var COMMANDS_WITH_CONTAINER_ARG = [
2451
2465
  "remove-from-url",
2452
2466
  "remove-repo"
2453
2467
  ];
2454
- var SHELLS = ["bash", "zsh"];
2468
+ var SHELLS = ["bash", "zsh", "pwsh"];
2455
2469
  function renderCompletionScript(shell) {
2456
2470
  const commands = ALL_COMMANDS.join(" ");
2457
2471
  const containerCommandsRegex = COMMANDS_WITH_CONTAINER_ARG.join("|");
@@ -2492,6 +2506,54 @@ function renderCompletionScript(shell) {
2492
2506
  ""
2493
2507
  ].join("\n");
2494
2508
  }
2509
+ if (shell === "pwsh") {
2510
+ return [
2511
+ "# PowerShell completion for monoceros",
2512
+ "# install: dot-source this file from your $PROFILE, e.g.",
2513
+ "# monoceros completion pwsh > $HOME/.config/monoceros/completion.ps1",
2514
+ "# Add-Content $PROFILE '. $HOME/.config/monoceros/completion.ps1'",
2515
+ "",
2516
+ "Register-ArgumentCompleter -Native -CommandName monoceros -ScriptBlock {",
2517
+ " param($wordToComplete, $commandAst, $cursorPosition)",
2518
+ "",
2519
+ " $commands = @(",
2520
+ ...ALL_COMMANDS.map((c) => ` '${c}'`),
2521
+ " )",
2522
+ ` $shells = @('${SHELLS.join("', '")}')`,
2523
+ " $containerCommands = @(",
2524
+ ...COMMANDS_WITH_CONTAINER_ARG.map((c) => ` '${c}'`),
2525
+ " )",
2526
+ "",
2527
+ " $tokens = $commandAst.CommandElements",
2528
+ " $position = $tokens.Count",
2529
+ " if ($wordToComplete) { $position-- }",
2530
+ "",
2531
+ " if ($position -eq 1) {",
2532
+ ' $commands | Where-Object { $_ -like "$wordToComplete*" } |',
2533
+ ' ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, "ParameterValue", $_) }',
2534
+ " return",
2535
+ " }",
2536
+ "",
2537
+ " if ($position -eq 2) {",
2538
+ " $cmd = $tokens[1].Value",
2539
+ " if ($containerCommands -contains $cmd) {",
2540
+ ' $home = if ($env:MONOCEROS_HOME) { $env:MONOCEROS_HOME } else { Join-Path $env:USERPROFILE ".monoceros" }',
2541
+ ' $configsDir = Join-Path $home "container-configs"',
2542
+ " if (Test-Path $configsDir) {",
2543
+ ' Get-ChildItem -Path $configsDir -Filter "*.yml" |',
2544
+ " ForEach-Object { $_.BaseName } |",
2545
+ ' Where-Object { $_ -like "$wordToComplete*" } |',
2546
+ ' ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, "ParameterValue", $_) }',
2547
+ " }",
2548
+ ' } elseif ($cmd -eq "completion") {',
2549
+ ' $shells | Where-Object { $_ -like "$wordToComplete*" } |',
2550
+ ' ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, "ParameterValue", $_) }',
2551
+ " }",
2552
+ " }",
2553
+ "}",
2554
+ ""
2555
+ ].join("\n");
2556
+ }
2495
2557
  return [
2496
2558
  "#compdef monoceros",
2497
2559
  "# zsh completion for monoceros",
@@ -2538,18 +2600,18 @@ var completionCommand = defineCommand8({
2538
2600
  meta: {
2539
2601
  name: "completion",
2540
2602
  group: "tooling",
2541
- description: "Print a shell completion script for bash or zsh to stdout. Pipe the output into a file your shell loads at startup."
2603
+ description: "Print a shell completion script for bash, zsh or PowerShell to stdout. Pipe the output into a file your shell loads at startup. The install scripts (install.sh / install.ps1) call this automatically."
2542
2604
  },
2543
2605
  args: {
2544
2606
  shell: {
2545
2607
  type: "positional",
2546
- description: "Target shell. One of: 'bash', 'zsh'.",
2608
+ description: "Target shell. One of: 'bash', 'zsh', 'pwsh'.",
2547
2609
  required: true
2548
2610
  }
2549
2611
  },
2550
2612
  run({ args }) {
2551
2613
  const shell = args.shell;
2552
- if (shell !== "bash" && shell !== "zsh") {
2614
+ if (shell !== "bash" && shell !== "zsh" && shell !== "pwsh") {
2553
2615
  process.stderr.write(
2554
2616
  `Unknown shell: ${JSON.stringify(shell)}. Supported: ${SHELLS.join(", ")}.
2555
2617
  `
@@ -2566,7 +2628,6 @@ import { consola as consola13 } from "consola";
2566
2628
 
2567
2629
  // src/init/index.ts
2568
2630
  import { existsSync as existsSync7, promises as fs10 } from "fs";
2569
- import path10 from "path";
2570
2631
  import { consola as consola12 } from "consola";
2571
2632
 
2572
2633
  // src/init/components.ts
@@ -2765,11 +2826,10 @@ function generateComposedYml(name, components, lookupManifest) {
2765
2826
  if (merged.features.length > 0) {
2766
2827
  lines.push("features:");
2767
2828
  for (const f of merged.features) {
2768
- const hints = lookupManifest(f.ref)?.optionHints ?? [];
2769
2829
  renderFeatureBlock(
2770
2830
  lines,
2771
2831
  f,
2772
- hints,
2832
+ lookupManifest(f.ref),
2773
2833
  /* commented */
2774
2834
  false
2775
2835
  );
@@ -2854,11 +2914,10 @@ function generateDocumentedYml(name, catalog, lookupManifest) {
2854
2914
  for (const f of c.file.contributes.features ?? []) {
2855
2915
  if (renderedRefs.has(f.ref)) continue;
2856
2916
  renderedRefs.add(f.ref);
2857
- const hints = lookupManifest(f.ref)?.optionHints ?? [];
2858
2917
  renderFeatureBlock(
2859
2918
  lines,
2860
2919
  f,
2861
- hints,
2920
+ lookupManifest(f.ref),
2862
2921
  /* commented */
2863
2922
  true
2864
2923
  );
@@ -2869,11 +2928,10 @@ function generateDocumentedYml(name, catalog, lookupManifest) {
2869
2928
  for (const f of c.file.contributes.features ?? []) {
2870
2929
  if (renderedRefs.has(f.ref)) continue;
2871
2930
  renderedRefs.add(f.ref);
2872
- const hints = lookupManifest(f.ref)?.optionHints ?? [];
2873
2931
  renderFeatureBlock(
2874
2932
  lines,
2875
2933
  f,
2876
- hints,
2934
+ lookupManifest(f.ref),
2877
2935
  /* commented */
2878
2936
  true
2879
2937
  );
@@ -2883,8 +2941,21 @@ function generateDocumentedYml(name, catalog, lookupManifest) {
2883
2941
  }
2884
2942
  return ensureTrailingNewline(lines.join("\n"));
2885
2943
  }
2886
- function renderFeatureBlock(out, feature, optionHints, commented) {
2944
+ var COMMENT_WIDTH = 72;
2945
+ function renderFeatureBlock(out, feature, summary, commented) {
2887
2946
  const c = commented ? "# " : " ";
2947
+ const optionHints = summary?.optionHints ?? [];
2948
+ const optionDescriptions = summary?.optionDescriptions ?? {};
2949
+ const usageNotes = summary?.usageNotes ?? [];
2950
+ for (let i = 0; i < usageNotes.length; i++) {
2951
+ if (i > 0) out.push(`${c}#`);
2952
+ for (const line of wrapToComment(
2953
+ usageNotes[i],
2954
+ COMMENT_WIDTH - c.length
2955
+ )) {
2956
+ out.push(`${c}# ${line}`);
2957
+ }
2958
+ }
2888
2959
  out.push(`${c}- ref: ${feature.ref}`);
2889
2960
  const options = feature.options ?? {};
2890
2961
  const activeOptions = Object.entries(options);
@@ -2899,7 +2970,7 @@ function renderFeatureBlock(out, feature, optionHints, commented) {
2899
2970
  `${c} # Optional \u2014 override monoceros-config.yml defaults.features:`
2900
2971
  );
2901
2972
  for (const hint of remainingHints) {
2902
- out.push(`${c} # ${hint}:`);
2973
+ emitHint(out, hint, optionDescriptions[hint], `${c} `);
2903
2974
  }
2904
2975
  }
2905
2976
  } else if (remainingHints.length > 0) {
@@ -2908,10 +2979,42 @@ function renderFeatureBlock(out, feature, optionHints, commented) {
2908
2979
  );
2909
2980
  out.push(`${c} # options:`);
2910
2981
  for (const hint of remainingHints) {
2911
- out.push(`${c} # ${hint}:`);
2982
+ emitHint(out, hint, optionDescriptions[hint], `${c} # `);
2912
2983
  }
2913
2984
  }
2914
2985
  }
2986
+ function emitHint(out, hint, description, linePrefix) {
2987
+ if (description) {
2988
+ for (const line of wrapToComment(
2989
+ description,
2990
+ COMMENT_WIDTH - linePrefix.length
2991
+ )) {
2992
+ out.push(`${linePrefix}# ${line}`);
2993
+ }
2994
+ }
2995
+ out.push(`${linePrefix}${hint}:`);
2996
+ }
2997
+ function wrapToComment(text, width) {
2998
+ const words = text.split(/\s+/).filter((w) => w.length > 0);
2999
+ if (words.length === 0) return [""];
3000
+ const usable = Math.max(width, 20);
3001
+ const lines = [];
3002
+ let current = "";
3003
+ for (const w of words) {
3004
+ if (current.length === 0) {
3005
+ current = w;
3006
+ continue;
3007
+ }
3008
+ if (current.length + 1 + w.length <= usable) {
3009
+ current += " " + w;
3010
+ } else {
3011
+ lines.push(current);
3012
+ current = w;
3013
+ }
3014
+ }
3015
+ if (current.length > 0) lines.push(current);
3016
+ return lines;
3017
+ }
2915
3018
  function renderScalarValue(value) {
2916
3019
  if (typeof value === "string") {
2917
3020
  return /^[A-Za-z_][A-Za-z0-9._-]*$/.test(value) ? value : JSON.stringify(value);
@@ -2939,28 +3042,50 @@ function ensureTrailingNewline(s) {
2939
3042
  // src/init/manifest.ts
2940
3043
  import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
2941
3044
  import path9 from "path";
2942
- function loadFeatureManifestSummary(ref, checkoutRoot = workbenchCheckoutRoot()) {
2943
- if (!checkoutRoot) return void 0;
2944
- const match = matchMonocerosFeature(ref);
2945
- if (!match) return void 0;
2946
- const name = match.name;
2947
- const manifestPath = path9.join(
2948
- checkoutRoot,
2949
- "images",
2950
- "features",
3045
+ function resolveManifestPath(name, checkoutRoot) {
3046
+ if (checkoutRoot) {
3047
+ const checkoutPath = path9.join(
3048
+ checkoutRoot,
3049
+ "images",
3050
+ "features",
3051
+ name,
3052
+ "devcontainer-feature.json"
3053
+ );
3054
+ if (existsSync6(checkoutPath)) return checkoutPath;
3055
+ }
3056
+ const bundlePath = path9.join(
3057
+ bundledFeaturesDir(),
2951
3058
  name,
2952
3059
  "devcontainer-feature.json"
2953
3060
  );
2954
- if (!existsSync6(manifestPath)) return void 0;
3061
+ if (existsSync6(bundlePath)) return bundlePath;
3062
+ return null;
3063
+ }
3064
+ function loadFeatureManifestSummary(ref, checkoutRoot = workbenchCheckoutRoot()) {
3065
+ const match = matchMonocerosFeature(ref);
3066
+ if (!match) return void 0;
3067
+ const manifestPath = resolveManifestPath(match.name, checkoutRoot);
3068
+ if (!manifestPath) return void 0;
2955
3069
  try {
2956
3070
  const text = readFileSync3(manifestPath, "utf8");
2957
3071
  const parsed = JSON.parse(text);
2958
- const raw = parsed["x-monoceros"]?.optionHints;
2959
- if (!Array.isArray(raw)) return { optionHints: [] };
2960
- const hints = raw.filter(
3072
+ const rawHints = parsed["x-monoceros"]?.optionHints;
3073
+ const optionHints = Array.isArray(rawHints) ? rawHints.filter(
2961
3074
  (x) => typeof x === "string" && x.length > 0
2962
- );
2963
- return { optionHints: hints };
3075
+ ) : [];
3076
+ const rawNotes = parsed["x-monoceros"]?.usageNotes;
3077
+ const usageNotes = Array.isArray(rawNotes) ? rawNotes.filter(
3078
+ (x) => typeof x === "string" && x.length > 0
3079
+ ) : [];
3080
+ const optionDescriptions = {};
3081
+ if (parsed.options) {
3082
+ for (const [key, opt] of Object.entries(parsed.options)) {
3083
+ if (opt && typeof opt === "object" && typeof opt.description === "string" && opt.description.length > 0) {
3084
+ optionDescriptions[key] = opt.description;
3085
+ }
3086
+ }
3087
+ }
3088
+ return { optionHints, optionDescriptions, usageNotes };
2964
3089
  } catch {
2965
3090
  return void 0;
2966
3091
  }
@@ -3004,14 +3129,14 @@ async function runInit(opts) {
3004
3129
  await fs10.mkdir(containerConfigsDir(home), { recursive: true });
3005
3130
  await fs10.writeFile(dest, text, "utf8");
3006
3131
  const documented = requested.length === 0;
3007
- const rel = path10.relative(home, dest) || dest;
3132
+ const displayPath = prettyPath(dest);
3008
3133
  if (documented) {
3009
3134
  logger.success(
3010
- `Wrote documented default to ${rel}. Un-comment what you need, then \`monoceros apply ${opts.name}\`.`
3135
+ `Wrote documented default to ${displayPath}. Un-comment what you need, then \`monoceros apply ${opts.name}\`.`
3011
3136
  );
3012
3137
  } else {
3013
3138
  logger.success(
3014
- `Composed ${requested.length} component(s) into ${rel}: ${requested.join(", ")}`
3139
+ `Composed ${requested.length} component(s) into ${displayPath}: ${requested.join(", ")}`
3015
3140
  );
3016
3141
  logger.info(
3017
3142
  `Edit the file if you need to tweak, then \`monoceros apply ${opts.name}\`.`
@@ -3248,7 +3373,7 @@ import { createInterface } from "readline/promises";
3248
3373
 
3249
3374
  // src/remove/index.ts
3250
3375
  import { existsSync as existsSync8, promises as fs11 } from "fs";
3251
- import path11 from "path";
3376
+ import path10 from "path";
3252
3377
  import { consola as consola17 } from "consola";
3253
3378
  async function runRemove(opts) {
3254
3379
  const home = opts.monocerosHome ?? monocerosHome();
@@ -3291,19 +3416,17 @@ async function runRemove(opts) {
3291
3416
  let backupPath = null;
3292
3417
  if (!opts.noBackup && (hasYml || hasContainer)) {
3293
3418
  const ts = (opts.now ?? /* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3294
- backupPath = path11.join(home, "container-backups", `${opts.name}-${ts}`);
3419
+ backupPath = path10.join(home, "container-backups", `${opts.name}-${ts}`);
3295
3420
  await fs11.mkdir(backupPath, { recursive: true });
3296
3421
  if (hasYml) {
3297
- await fs11.copyFile(ymlPath, path11.join(backupPath, `${opts.name}.yml`));
3422
+ await fs11.copyFile(ymlPath, path10.join(backupPath, `${opts.name}.yml`));
3298
3423
  }
3299
3424
  if (hasContainer) {
3300
- await fs11.cp(containerPath, path11.join(backupPath, "container"), {
3425
+ await fs11.cp(containerPath, path10.join(backupPath, "container"), {
3301
3426
  recursive: true
3302
3427
  });
3303
3428
  }
3304
- logger.info(
3305
- `Backup written to ${path11.relative(home, backupPath) || backupPath}.`
3306
- );
3429
+ logger.info(`Backup written to ${prettyPath(backupPath)}.`);
3307
3430
  }
3308
3431
  if (hasYml) {
3309
3432
  await fs11.rm(ymlPath, { force: true });
@@ -3394,7 +3517,7 @@ import { consola as consola20 } from "consola";
3394
3517
 
3395
3518
  // src/restore/index.ts
3396
3519
  import { existsSync as existsSync9, promises as fs12 } from "fs";
3397
- import path12 from "path";
3520
+ import path11 from "path";
3398
3521
  import { consola as consola19 } from "consola";
3399
3522
  async function runRestore(opts) {
3400
3523
  const home = opts.monocerosHome ?? monocerosHome();
@@ -3402,7 +3525,7 @@ async function runRestore(opts) {
3402
3525
  info: (msg) => consola19.info(msg),
3403
3526
  success: (msg) => consola19.success(msg)
3404
3527
  };
3405
- const backup = path12.resolve(opts.backupPath);
3528
+ const backup = path11.resolve(opts.backupPath);
3406
3529
  if (!existsSync9(backup)) {
3407
3530
  throw new Error(`Backup not found: ${backup}.`);
3408
3531
  }
@@ -3424,7 +3547,7 @@ async function runRestore(opts) {
3424
3547
  }
3425
3548
  const ymlFile = ymlFiles[0];
3426
3549
  const name = ymlFile.replace(/\.yml$/, "");
3427
- const containerInBackup = path12.join(backup, "container");
3550
+ const containerInBackup = path11.join(backup, "container");
3428
3551
  const hasContainer = existsSync9(containerInBackup);
3429
3552
  const destYml = containerConfigPath(name, home);
3430
3553
  const destContainer = containerDir(name, home);
@@ -3439,13 +3562,11 @@ async function runRestore(opts) {
3439
3562
  );
3440
3563
  }
3441
3564
  await fs12.mkdir(containerConfigsDir(home), { recursive: true });
3442
- await fs12.copyFile(path12.join(backup, ymlFile), destYml);
3565
+ await fs12.copyFile(path11.join(backup, ymlFile), destYml);
3443
3566
  if (hasContainer) {
3444
3567
  await fs12.cp(containerInBackup, destContainer, { recursive: true });
3445
3568
  }
3446
- logger.success(
3447
- `Restored '${name}' from ${path12.relative(home, backup) || backup}.`
3448
- );
3569
+ logger.success(`Restored '${name}' from ${prettyPath(backup)}.`);
3449
3570
  logger.info(
3450
3571
  `Run \`monoceros apply ${name}\` to bring the container back up.`
3451
3572
  );
@@ -3654,7 +3775,7 @@ import { consola as consola25 } from "consola";
3654
3775
 
3655
3776
  // src/devcontainer/shell.ts
3656
3777
  import { existsSync as existsSync10 } from "fs";
3657
- import path13 from "path";
3778
+ import path12 from "path";
3658
3779
  async function runShell(opts) {
3659
3780
  assertContainerExists(opts.root);
3660
3781
  const spawnFn = opts.spawn ?? spawnDevcontainer;
@@ -3669,7 +3790,7 @@ async function runShell(opts) {
3669
3790
  });
3670
3791
  }
3671
3792
  function assertContainerExists(root) {
3672
- if (!existsSync10(path13.join(root, ".devcontainer"))) {
3793
+ if (!existsSync10(path12.join(root, ".devcontainer"))) {
3673
3794
  throw new Error(
3674
3795
  `No .devcontainer/ at ${root}. Run \`monoceros apply <name>\` first.`
3675
3796
  );