@getmonoceros/workbench 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
@@ -203,25 +203,25 @@ function detectHelpRequest(argv, main2) {
203
203
  const separatorIdx = argv.indexOf("--");
204
204
  if (helpIdx === -1) return null;
205
205
  if (separatorIdx !== -1 && separatorIdx < helpIdx) return null;
206
- const path14 = [];
206
+ const path13 = [];
207
207
  const tokens = argv.slice(
208
208
  0,
209
209
  separatorIdx === -1 ? argv.length : separatorIdx
210
210
  );
211
211
  let cursor = main2;
212
212
  const mainName = (main2.meta ?? {}).name ?? "monoceros";
213
- path14.push(mainName);
213
+ path13.push(mainName);
214
214
  for (const tok of tokens) {
215
215
  if (tok.startsWith("-")) continue;
216
216
  const subs = cursor.subCommands ?? {};
217
217
  if (tok in subs) {
218
218
  cursor = subs[tok];
219
- path14.push(tok);
219
+ path13.push(tok);
220
220
  continue;
221
221
  }
222
222
  break;
223
223
  }
224
- return { path: path14, cmd: cursor };
224
+ return { path: path13, cmd: cursor };
225
225
  }
226
226
  async function maybeRenderHelp(argv, main2) {
227
227
  const hit = detectHelpRequest(argv, main2);
@@ -451,6 +451,9 @@ function workbenchCheckoutRoot() {
451
451
  function componentsDir(root = workbenchRoot()) {
452
452
  return path.join(root, "templates", "components");
453
453
  }
454
+ function bundledFeaturesDir(root = workbenchRoot()) {
455
+ return path.join(root, "features");
456
+ }
454
457
  function containerConfigsDir(home = monocerosHome()) {
455
458
  return path.join(home, "container-configs");
456
459
  }
@@ -466,6 +469,16 @@ function containerDir(name, home = monocerosHome()) {
466
469
  function monocerosConfigPath(home = monocerosHome()) {
467
470
  return path.join(home, "monoceros-config.yml");
468
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
+ }
469
482
 
470
483
  // src/create/catalog.ts
471
484
  var DEFAULT_BASE_IMAGE = "ghcr.io/getmonoceros/monoceros-runtime:1";
@@ -2312,7 +2325,10 @@ async function runApply(opts) {
2312
2325
  });
2313
2326
  }
2314
2327
  logger.success(
2315
- `Materialized config '${opts.name}' into ${targetDir}. Starting container\u2026`
2328
+ `Materialized config '${opts.name}' into ${prettyPath(targetDir)}. Starting container\u2026`
2329
+ );
2330
+ logger.info(
2331
+ 'Pulling runtime image and building feature layers. First apply takes ~1\u20132 min (Docker downloads the multi-arch base); subsequent applies are cached and fast. devcontainer-cli may log a "No manifest found" line \u2014 harmless, the pull continues.'
2316
2332
  );
2317
2333
  const exitCode = await runContainerCycle(targetDir, {
2318
2334
  hasCompose: needsCompose(createOpts),
@@ -2363,7 +2379,7 @@ function warnOnDeprecatedFeatureRefs(containerFeatures, globalConfig, logger) {
2363
2379
  }
2364
2380
 
2365
2381
  // src/version.ts
2366
- var CLI_VERSION = "1.2.0";
2382
+ var CLI_VERSION = "1.3.1";
2367
2383
 
2368
2384
  // src/commands/_dispatch.ts
2369
2385
  import { consola as consola11 } from "consola";
@@ -2615,7 +2631,6 @@ import { consola as consola13 } from "consola";
2615
2631
 
2616
2632
  // src/init/index.ts
2617
2633
  import { existsSync as existsSync7, promises as fs10 } from "fs";
2618
- import path10 from "path";
2619
2634
  import { consola as consola12 } from "consola";
2620
2635
 
2621
2636
  // src/init/components.ts
@@ -2814,11 +2829,10 @@ function generateComposedYml(name, components, lookupManifest) {
2814
2829
  if (merged.features.length > 0) {
2815
2830
  lines.push("features:");
2816
2831
  for (const f of merged.features) {
2817
- const hints = lookupManifest(f.ref)?.optionHints ?? [];
2818
2832
  renderFeatureBlock(
2819
2833
  lines,
2820
2834
  f,
2821
- hints,
2835
+ lookupManifest(f.ref),
2822
2836
  /* commented */
2823
2837
  false
2824
2838
  );
@@ -2903,11 +2917,10 @@ function generateDocumentedYml(name, catalog, lookupManifest) {
2903
2917
  for (const f of c.file.contributes.features ?? []) {
2904
2918
  if (renderedRefs.has(f.ref)) continue;
2905
2919
  renderedRefs.add(f.ref);
2906
- const hints = lookupManifest(f.ref)?.optionHints ?? [];
2907
2920
  renderFeatureBlock(
2908
2921
  lines,
2909
2922
  f,
2910
- hints,
2923
+ lookupManifest(f.ref),
2911
2924
  /* commented */
2912
2925
  true
2913
2926
  );
@@ -2918,11 +2931,10 @@ function generateDocumentedYml(name, catalog, lookupManifest) {
2918
2931
  for (const f of c.file.contributes.features ?? []) {
2919
2932
  if (renderedRefs.has(f.ref)) continue;
2920
2933
  renderedRefs.add(f.ref);
2921
- const hints = lookupManifest(f.ref)?.optionHints ?? [];
2922
2934
  renderFeatureBlock(
2923
2935
  lines,
2924
2936
  f,
2925
- hints,
2937
+ lookupManifest(f.ref),
2926
2938
  /* commented */
2927
2939
  true
2928
2940
  );
@@ -2932,8 +2944,21 @@ function generateDocumentedYml(name, catalog, lookupManifest) {
2932
2944
  }
2933
2945
  return ensureTrailingNewline(lines.join("\n"));
2934
2946
  }
2935
- function renderFeatureBlock(out, feature, optionHints, commented) {
2947
+ var COMMENT_WIDTH = 72;
2948
+ function renderFeatureBlock(out, feature, summary, commented) {
2936
2949
  const c = commented ? "# " : " ";
2950
+ const optionHints = summary?.optionHints ?? [];
2951
+ const optionDescriptions = summary?.optionDescriptions ?? {};
2952
+ const usageNotes = summary?.usageNotes ?? [];
2953
+ for (let i = 0; i < usageNotes.length; i++) {
2954
+ if (i > 0) out.push(`${c}#`);
2955
+ for (const line of wrapToComment(
2956
+ usageNotes[i],
2957
+ COMMENT_WIDTH - c.length
2958
+ )) {
2959
+ out.push(`${c}# ${line}`);
2960
+ }
2961
+ }
2937
2962
  out.push(`${c}- ref: ${feature.ref}`);
2938
2963
  const options = feature.options ?? {};
2939
2964
  const activeOptions = Object.entries(options);
@@ -2948,7 +2973,7 @@ function renderFeatureBlock(out, feature, optionHints, commented) {
2948
2973
  `${c} # Optional \u2014 override monoceros-config.yml defaults.features:`
2949
2974
  );
2950
2975
  for (const hint of remainingHints) {
2951
- out.push(`${c} # ${hint}:`);
2976
+ emitHint(out, hint, optionDescriptions[hint], `${c} `);
2952
2977
  }
2953
2978
  }
2954
2979
  } else if (remainingHints.length > 0) {
@@ -2957,9 +2982,41 @@ function renderFeatureBlock(out, feature, optionHints, commented) {
2957
2982
  );
2958
2983
  out.push(`${c} # options:`);
2959
2984
  for (const hint of remainingHints) {
2960
- out.push(`${c} # ${hint}:`);
2985
+ emitHint(out, hint, optionDescriptions[hint], `${c} # `);
2986
+ }
2987
+ }
2988
+ }
2989
+ function emitHint(out, hint, description, linePrefix) {
2990
+ if (description) {
2991
+ for (const line of wrapToComment(
2992
+ description,
2993
+ COMMENT_WIDTH - linePrefix.length
2994
+ )) {
2995
+ out.push(`${linePrefix}# ${line}`);
2961
2996
  }
2962
2997
  }
2998
+ out.push(`${linePrefix}${hint}:`);
2999
+ }
3000
+ function wrapToComment(text, width) {
3001
+ const words = text.split(/\s+/).filter((w) => w.length > 0);
3002
+ if (words.length === 0) return [""];
3003
+ const usable = Math.max(width, 20);
3004
+ const lines = [];
3005
+ let current = "";
3006
+ for (const w of words) {
3007
+ if (current.length === 0) {
3008
+ current = w;
3009
+ continue;
3010
+ }
3011
+ if (current.length + 1 + w.length <= usable) {
3012
+ current += " " + w;
3013
+ } else {
3014
+ lines.push(current);
3015
+ current = w;
3016
+ }
3017
+ }
3018
+ if (current.length > 0) lines.push(current);
3019
+ return lines;
2963
3020
  }
2964
3021
  function renderScalarValue(value) {
2965
3022
  if (typeof value === "string") {
@@ -2988,28 +3045,50 @@ function ensureTrailingNewline(s) {
2988
3045
  // src/init/manifest.ts
2989
3046
  import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
2990
3047
  import path9 from "path";
2991
- function loadFeatureManifestSummary(ref, checkoutRoot = workbenchCheckoutRoot()) {
2992
- if (!checkoutRoot) return void 0;
2993
- const match = matchMonocerosFeature(ref);
2994
- if (!match) return void 0;
2995
- const name = match.name;
2996
- const manifestPath = path9.join(
2997
- checkoutRoot,
2998
- "images",
2999
- "features",
3048
+ function resolveManifestPath(name, checkoutRoot) {
3049
+ if (checkoutRoot) {
3050
+ const checkoutPath = path9.join(
3051
+ checkoutRoot,
3052
+ "images",
3053
+ "features",
3054
+ name,
3055
+ "devcontainer-feature.json"
3056
+ );
3057
+ if (existsSync6(checkoutPath)) return checkoutPath;
3058
+ }
3059
+ const bundlePath = path9.join(
3060
+ bundledFeaturesDir(),
3000
3061
  name,
3001
3062
  "devcontainer-feature.json"
3002
3063
  );
3003
- if (!existsSync6(manifestPath)) return void 0;
3064
+ if (existsSync6(bundlePath)) return bundlePath;
3065
+ return null;
3066
+ }
3067
+ function loadFeatureManifestSummary(ref, checkoutRoot = workbenchCheckoutRoot()) {
3068
+ const match = matchMonocerosFeature(ref);
3069
+ if (!match) return void 0;
3070
+ const manifestPath = resolveManifestPath(match.name, checkoutRoot);
3071
+ if (!manifestPath) return void 0;
3004
3072
  try {
3005
3073
  const text = readFileSync3(manifestPath, "utf8");
3006
3074
  const parsed = JSON.parse(text);
3007
- const raw = parsed["x-monoceros"]?.optionHints;
3008
- if (!Array.isArray(raw)) return { optionHints: [] };
3009
- const hints = raw.filter(
3075
+ const rawHints = parsed["x-monoceros"]?.optionHints;
3076
+ const optionHints = Array.isArray(rawHints) ? rawHints.filter(
3010
3077
  (x) => typeof x === "string" && x.length > 0
3011
- );
3012
- return { optionHints: hints };
3078
+ ) : [];
3079
+ const rawNotes = parsed["x-monoceros"]?.usageNotes;
3080
+ const usageNotes = Array.isArray(rawNotes) ? rawNotes.filter(
3081
+ (x) => typeof x === "string" && x.length > 0
3082
+ ) : [];
3083
+ const optionDescriptions = {};
3084
+ if (parsed.options) {
3085
+ for (const [key, opt] of Object.entries(parsed.options)) {
3086
+ if (opt && typeof opt === "object" && typeof opt.description === "string" && opt.description.length > 0) {
3087
+ optionDescriptions[key] = opt.description;
3088
+ }
3089
+ }
3090
+ }
3091
+ return { optionHints, optionDescriptions, usageNotes };
3013
3092
  } catch {
3014
3093
  return void 0;
3015
3094
  }
@@ -3053,14 +3132,14 @@ async function runInit(opts) {
3053
3132
  await fs10.mkdir(containerConfigsDir(home), { recursive: true });
3054
3133
  await fs10.writeFile(dest, text, "utf8");
3055
3134
  const documented = requested.length === 0;
3056
- const rel = path10.relative(home, dest) || dest;
3135
+ const displayPath = prettyPath(dest);
3057
3136
  if (documented) {
3058
3137
  logger.success(
3059
- `Wrote documented default to ${rel}. Un-comment what you need, then \`monoceros apply ${opts.name}\`.`
3138
+ `Wrote documented default to ${displayPath}. Un-comment what you need, then \`monoceros apply ${opts.name}\`.`
3060
3139
  );
3061
3140
  } else {
3062
3141
  logger.success(
3063
- `Composed ${requested.length} component(s) into ${rel}: ${requested.join(", ")}`
3142
+ `Composed ${requested.length} component(s) into ${displayPath}: ${requested.join(", ")}`
3064
3143
  );
3065
3144
  logger.info(
3066
3145
  `Edit the file if you need to tweak, then \`monoceros apply ${opts.name}\`.`
@@ -3297,7 +3376,7 @@ import { createInterface } from "readline/promises";
3297
3376
 
3298
3377
  // src/remove/index.ts
3299
3378
  import { existsSync as existsSync8, promises as fs11 } from "fs";
3300
- import path11 from "path";
3379
+ import path10 from "path";
3301
3380
  import { consola as consola17 } from "consola";
3302
3381
  async function runRemove(opts) {
3303
3382
  const home = opts.monocerosHome ?? monocerosHome();
@@ -3340,19 +3419,17 @@ async function runRemove(opts) {
3340
3419
  let backupPath = null;
3341
3420
  if (!opts.noBackup && (hasYml || hasContainer)) {
3342
3421
  const ts = (opts.now ?? /* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3343
- backupPath = path11.join(home, "container-backups", `${opts.name}-${ts}`);
3422
+ backupPath = path10.join(home, "container-backups", `${opts.name}-${ts}`);
3344
3423
  await fs11.mkdir(backupPath, { recursive: true });
3345
3424
  if (hasYml) {
3346
- await fs11.copyFile(ymlPath, path11.join(backupPath, `${opts.name}.yml`));
3425
+ await fs11.copyFile(ymlPath, path10.join(backupPath, `${opts.name}.yml`));
3347
3426
  }
3348
3427
  if (hasContainer) {
3349
- await fs11.cp(containerPath, path11.join(backupPath, "container"), {
3428
+ await fs11.cp(containerPath, path10.join(backupPath, "container"), {
3350
3429
  recursive: true
3351
3430
  });
3352
3431
  }
3353
- logger.info(
3354
- `Backup written to ${path11.relative(home, backupPath) || backupPath}.`
3355
- );
3432
+ logger.info(`Backup written to ${prettyPath(backupPath)}.`);
3356
3433
  }
3357
3434
  if (hasYml) {
3358
3435
  await fs11.rm(ymlPath, { force: true });
@@ -3443,7 +3520,7 @@ import { consola as consola20 } from "consola";
3443
3520
 
3444
3521
  // src/restore/index.ts
3445
3522
  import { existsSync as existsSync9, promises as fs12 } from "fs";
3446
- import path12 from "path";
3523
+ import path11 from "path";
3447
3524
  import { consola as consola19 } from "consola";
3448
3525
  async function runRestore(opts) {
3449
3526
  const home = opts.monocerosHome ?? monocerosHome();
@@ -3451,7 +3528,7 @@ async function runRestore(opts) {
3451
3528
  info: (msg) => consola19.info(msg),
3452
3529
  success: (msg) => consola19.success(msg)
3453
3530
  };
3454
- const backup = path12.resolve(opts.backupPath);
3531
+ const backup = path11.resolve(opts.backupPath);
3455
3532
  if (!existsSync9(backup)) {
3456
3533
  throw new Error(`Backup not found: ${backup}.`);
3457
3534
  }
@@ -3473,7 +3550,7 @@ async function runRestore(opts) {
3473
3550
  }
3474
3551
  const ymlFile = ymlFiles[0];
3475
3552
  const name = ymlFile.replace(/\.yml$/, "");
3476
- const containerInBackup = path12.join(backup, "container");
3553
+ const containerInBackup = path11.join(backup, "container");
3477
3554
  const hasContainer = existsSync9(containerInBackup);
3478
3555
  const destYml = containerConfigPath(name, home);
3479
3556
  const destContainer = containerDir(name, home);
@@ -3488,13 +3565,11 @@ async function runRestore(opts) {
3488
3565
  );
3489
3566
  }
3490
3567
  await fs12.mkdir(containerConfigsDir(home), { recursive: true });
3491
- await fs12.copyFile(path12.join(backup, ymlFile), destYml);
3568
+ await fs12.copyFile(path11.join(backup, ymlFile), destYml);
3492
3569
  if (hasContainer) {
3493
3570
  await fs12.cp(containerInBackup, destContainer, { recursive: true });
3494
3571
  }
3495
- logger.success(
3496
- `Restored '${name}' from ${path12.relative(home, backup) || backup}.`
3497
- );
3572
+ logger.success(`Restored '${name}' from ${prettyPath(backup)}.`);
3498
3573
  logger.info(
3499
3574
  `Run \`monoceros apply ${name}\` to bring the container back up.`
3500
3575
  );
@@ -3703,7 +3778,7 @@ import { consola as consola25 } from "consola";
3703
3778
 
3704
3779
  // src/devcontainer/shell.ts
3705
3780
  import { existsSync as existsSync10 } from "fs";
3706
- import path13 from "path";
3781
+ import path12 from "path";
3707
3782
  async function runShell(opts) {
3708
3783
  assertContainerExists(opts.root);
3709
3784
  const spawnFn = opts.spawn ?? spawnDevcontainer;
@@ -3718,7 +3793,7 @@ async function runShell(opts) {
3718
3793
  });
3719
3794
  }
3720
3795
  function assertContainerExists(root) {
3721
- if (!existsSync10(path13.join(root, ".devcontainer"))) {
3796
+ if (!existsSync10(path12.join(root, ".devcontainer"))) {
3722
3797
  throw new Error(
3723
3798
  `No .devcontainer/ at ${root}. Run \`monoceros apply <name>\` first.`
3724
3799
  );