@getmonoceros/workbench 1.29.2 → 1.31.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
@@ -665,7 +665,16 @@ var init_feature_doc = __esm({
665
665
 
666
666
  // src/catalog/descriptor.ts
667
667
  import { z as z2 } from "zod";
668
- var DESCRIPTOR_ID_RE, CategorySchema, OptionTypeSchema, SurfaceSchema, OptionValueSchema, OptionSpecSchema, BriefingLineSchema, HealthcheckSchema, LanguageBlockSchema, ServiceBlockSchema, PersistentHomeFileSchema, FeatureBlockSchema, DescriptorSchema;
668
+ function workspaceEnvTokens(vars) {
669
+ const tokens = [];
670
+ for (const template of Object.values(vars)) {
671
+ for (const m of template.matchAll(/\$\{([A-Za-z0-9_]+)\}/g)) {
672
+ tokens.push(m[1]);
673
+ }
674
+ }
675
+ return tokens;
676
+ }
677
+ var DESCRIPTOR_ID_RE, CategorySchema, OptionTypeSchema, SurfaceSchema, OptionValueSchema, OptionSpecSchema, BriefingLineSchema, HealthcheckSchema, LanguageBlockSchema, ServiceBlockSchema, PersistentHomeFileSchema, WorkspaceEnvBlockSchema, FeatureBlockSchema, DescriptorSchema;
669
678
  var init_descriptor = __esm({
670
679
  "src/catalog/descriptor.ts"() {
671
680
  "use strict";
@@ -718,11 +727,28 @@ var init_descriptor = __esm({
718
727
  * Coerced to string so authors can write bare YAML numbers
719
728
  * (`versions: [latest, 21, 17]`) without quoting.
720
729
  */
721
- versions: z2.array(z2.coerce.string()).optional()
730
+ versions: z2.array(z2.coerce.string()).optional(),
731
+ /**
732
+ * VS Code extensions to *recommend* (not auto-install) when this language
733
+ * is present, written to the `.code-workspace` `extensions.recommendations`
734
+ * (ADR 0016). List every editor variant where editors diverge: VS Code and
735
+ * VSCodium each resolve recommendation IDs against their own registry (MS
736
+ * Marketplace / Open VSX) and silently skip what they can't find, so an
737
+ * ID that only exists for one editor is simply ignored by the other. E.g.
738
+ * `[ms-python.python, ms-python.vscode-pylance]` — Codium drops Pylance.
739
+ */
740
+ vscodeExtensions: z2.array(z2.string()).optional()
722
741
  });
723
742
  ServiceBlockSchema = z2.object({
724
743
  image: z2.string().min(1),
725
744
  defaultPort: z2.number().int().positive().optional(),
745
+ /**
746
+ * Compose `command:` for the service container — the process to run
747
+ * instead of the image's default CMD. Baked into the expanded yml
748
+ * (visible + editable, unlike `deferStart`). E.g. Keycloak needs
749
+ * `start-dev --import-realm` because its image has no auto-start default.
750
+ */
751
+ command: z2.string().optional(),
726
752
  dataMount: z2.string().optional(),
727
753
  /**
728
754
  * Compose `user:` for the service container (e.g. `"0:0"`). Needed for
@@ -760,18 +786,55 @@ var init_descriptor = __esm({
760
786
  apt: z2.array(z2.string()).optional(),
761
787
  npm: z2.array(z2.string()).optional()
762
788
  }).optional(),
763
- vscodeExtensions: z2.array(z2.string()).optional()
789
+ vscodeExtensions: z2.array(z2.string()).optional(),
790
+ /**
791
+ * Example bind-mounts rendered as a COMMENTED `volumes:` scaffold in the
792
+ * generated yml (init / add-service), for the builder to uncomment and
793
+ * edit. NOT active volumes — the catalog can't know the builder's repo
794
+ * path. Used by services that need a project file but can't auto-wire it
795
+ * (e.g. Keycloak's realm.json / theme). Each entry is a compose volume
796
+ * spec, e.g. `projects/<app>/keycloak/realm.json:/opt/keycloak/data/import/<app>.json:ro`.
797
+ */
798
+ exampleVolumes: z2.array(z2.string()).optional(),
799
+ /**
800
+ * Start this service in a SECOND WAVE, host-side, *after* `devcontainer
801
+ * up` (and thus the in-container repo clone in post-create) has
802
+ * finished — instead of together with the workspace at `compose up`.
803
+ * For a service that bind-mounts a file from a cloned repo (e.g.
804
+ * Keycloak's `realm.json`, a Postgres `init.sql`): the file does not
805
+ * exist at the normal parallel start, but is on disk by the time the
806
+ * second wave runs. See ADR 0025.
807
+ *
808
+ * Deliberately a HIDDEN, descriptor-only field: it is NOT exposed in the
809
+ * user-facing yml schema and not baked into the expanded service object.
810
+ * The start paths resolve it by catalog lookup on the service name
811
+ * (`serviceDefersStart`). Caveat: a deferred service is NOT reachable
812
+ * during the workspace's post-create.
813
+ */
814
+ deferStart: z2.boolean().optional()
764
815
  });
765
816
  PersistentHomeFileSchema = z2.object({
766
817
  path: z2.string().min(1),
767
818
  initialContent: z2.string().optional()
768
819
  });
820
+ WorkspaceEnvBlockSchema = z2.object({
821
+ whenOption: z2.string().optional(),
822
+ vars: z2.record(z2.string(), z2.string())
823
+ });
769
824
  FeatureBlockSchema = z2.object({
770
825
  /** Publishable feature version (devcontainer-feature.json `version`). */
771
826
  version: z2.string().min(1),
772
827
  persistentHomePaths: z2.array(z2.string().min(1)).optional(),
773
828
  persistentHomeFiles: z2.array(PersistentHomeFileSchema).optional(),
774
- vscodeExtensions: z2.array(z2.string()).optional()
829
+ vscodeExtensions: z2.array(z2.string()).optional(),
830
+ /**
831
+ * Named runtime env injected into the workspace container (compose
832
+ * `environment:` / image-mode `containerEnv`). Catalog/CLI-side only — not
833
+ * emitted into the published devcontainer-feature.json (like `presets`),
834
+ * because it drives how the workbench wires the container, not the feature
835
+ * install. See `featureWorkspaceEnv` in create/scaffold.ts.
836
+ */
837
+ workspaceEnv: z2.array(WorkspaceEnvBlockSchema).optional()
775
838
  });
776
839
  DescriptorSchema = z2.object({
777
840
  id: z2.string().regex(DESCRIPTOR_ID_RE, "id must be lowercase letters/digits/hyphens"),
@@ -833,6 +896,24 @@ var init_descriptor = __esm({
833
896
  });
834
897
  }
835
898
  });
899
+ data.feature?.workspaceEnv?.forEach((block, i) => {
900
+ if (block.whenOption !== void 0 && !optionKeys.has(block.whenOption)) {
901
+ ctx.addIssue({
902
+ code: z2.ZodIssueCode.custom,
903
+ path: ["feature", "workspaceEnv", i, "whenOption"],
904
+ message: `whenOption '${block.whenOption}' is not a declared option`
905
+ });
906
+ }
907
+ for (const token of workspaceEnvTokens(block.vars)) {
908
+ if (!optionKeys.has(token)) {
909
+ ctx.addIssue({
910
+ code: z2.ZodIssueCode.custom,
911
+ path: ["feature", "workspaceEnv", i, "vars"],
912
+ message: `workspaceEnv template references '\${${token}}', which is not a declared option`
913
+ });
914
+ }
915
+ }
916
+ });
836
917
  if (data.presets) {
837
918
  if (data.category !== "feature") {
838
919
  ctx.addIssue({
@@ -2180,6 +2261,12 @@ function knownLanguages() {
2180
2261
  function knownServices() {
2181
2262
  return Object.keys(SERVICE_CATALOG).sort();
2182
2263
  }
2264
+ function serviceDefersStart(name) {
2265
+ return SERVICE_CATALOG[name]?.deferStart === true;
2266
+ }
2267
+ function curatedServiceExampleVolumes(name) {
2268
+ return SERVICE_CATALOG[name]?.exampleVolumes ?? [];
2269
+ }
2183
2270
  function resolveService(entry2) {
2184
2271
  return {
2185
2272
  name: entry2.name,
@@ -2215,6 +2302,7 @@ function expandCuratedService(name) {
2215
2302
  } : {},
2216
2303
  ...def.dataMount ? { volumes: [`data:${def.dataMount}`] } : {},
2217
2304
  ...def.user ? { user: def.user } : {},
2305
+ ...def.command ? { command: def.command } : {},
2218
2306
  ...def.healthcheck ? { healthcheck: def.healthcheck } : {},
2219
2307
  // Bake the connection-env templates into the yml (suffix → template) so
2220
2308
  // they travel with the service: a renamed/duplicated instance keeps them,
@@ -2269,7 +2357,7 @@ function deriveServiceName(image) {
2269
2357
  const noTag = lastSegment.split("@")[0].split(":")[0];
2270
2358
  return noTag.toLowerCase().replace(/[^a-z0-9_-]/g, "-");
2271
2359
  }
2272
- var DEFAULT_BASE_IMAGE, override, BASE_IMAGE, RUNTIME_IMAGE_REPO, DEFAULT_RUNTIME_VERSION, MIN_RUNTIME_FOR_SSH_ATTACH, DESCRIPTORS, BUILTIN_LANGUAGES, LANGUAGE_CATALOG, LANGUAGE_SPEC_RE, SERVICE_CATALOG;
2360
+ var DEFAULT_BASE_IMAGE, override, BASE_IMAGE, RUNTIME_IMAGE_REPO, DEFAULT_RUNTIME_VERSION, MIN_RUNTIME_FOR_SSH_ATTACH, DESCRIPTORS, BUILTIN_LANGUAGES, LANGUAGE_CATALOG, LANGUAGE_SPEC_RE, SERVICE_CATALOG, DEFERRED_SERVICE_PROFILE;
2273
2361
  var init_catalog = __esm({
2274
2362
  "src/create/catalog.ts"() {
2275
2363
  "use strict";
@@ -2299,7 +2387,8 @@ var init_catalog = __esm({
2299
2387
  feature: c.descriptor.language.feature,
2300
2388
  ...Object.keys(defaults).length > 0 ? { defaultOptions: defaults } : {},
2301
2389
  ...Object.keys(ymlOptions).length > 0 ? { ymlOptions } : {},
2302
- ...c.descriptor.language.defaultVersion !== void 0 ? { defaultVersion: c.descriptor.language.defaultVersion } : {}
2390
+ ...c.descriptor.language.defaultVersion !== void 0 ? { defaultVersion: c.descriptor.language.defaultVersion } : {},
2391
+ ...c.descriptor.language.vscodeExtensions ? { vscodeExtensions: c.descriptor.language.vscodeExtensions } : {}
2303
2392
  };
2304
2393
  return [key, entry2];
2305
2394
  })
@@ -2325,11 +2414,15 @@ var init_catalog = __esm({
2325
2414
  defaultPort: svc.defaultPort,
2326
2415
  ...svc.vscodeExtensions ? { vscodeExtensions: svc.vscodeExtensions } : {},
2327
2416
  ...svc.connectionEnv ? { connectionEnv: svc.connectionEnv } : {},
2328
- ...svc.client ? { client: svc.client } : {}
2417
+ ...svc.client ? { client: svc.client } : {},
2418
+ ...svc.command ? { command: svc.command } : {},
2419
+ ...svc.exampleVolumes ? { exampleVolumes: svc.exampleVolumes } : {},
2420
+ ...svc.deferStart ? { deferStart: true } : {}
2329
2421
  };
2330
2422
  return [key, entry2];
2331
2423
  })
2332
2424
  );
2425
+ DEFERRED_SERVICE_PROFILE = "monoceros-deferred";
2333
2426
  }
2334
2427
  });
2335
2428
 
@@ -2389,6 +2482,13 @@ function renderCustomService(name, image) {
2389
2482
  ].join("\n");
2390
2483
  return { bodyLines, comment };
2391
2484
  }
2485
+ function exampleVolumesComment(exampleVolumes) {
2486
+ if (exampleVolumes.length === 0) return void 0;
2487
+ return [
2488
+ " volumes: # uncomment + edit to mount your realm/theme",
2489
+ ...exampleVolumes.map((v) => ` - ${v}`)
2490
+ ].join("\n");
2491
+ }
2392
2492
  function customServiceHint(name) {
2393
2493
  return `'${name}' is a custom image \u2014 Monoceros doesn't know its env, ports or volumes. Review the commented block under services[].${name} in the yml and fill in what the image needs.`;
2394
2494
  }
@@ -2845,7 +2945,8 @@ function resolveFeatures(opts) {
2845
2945
  devcontainerKey: entry2.feature,
2846
2946
  options,
2847
2947
  persistentHomePaths: [],
2848
- persistentHomeFiles: []
2948
+ persistentHomeFiles: [],
2949
+ workspaceEnv: []
2849
2950
  });
2850
2951
  }
2851
2952
  const aptPackages = [
@@ -2859,7 +2960,8 @@ function resolveFeatures(opts) {
2859
2960
  devcontainerKey: "ghcr.io/devcontainers-contrib/features/apt-packages:1",
2860
2961
  options: { packages: aptPackages.join(",") },
2861
2962
  persistentHomePaths: [],
2862
- persistentHomeFiles: []
2963
+ persistentHomeFiles: [],
2964
+ workspaceEnv: []
2863
2965
  });
2864
2966
  }
2865
2967
  if (opts.features) {
@@ -2869,6 +2971,7 @@ function resolveFeatures(opts) {
2869
2971
  const name = match.name;
2870
2972
  const descriptor = featureDescriptor(name);
2871
2973
  const { paths, files } = descriptorPersistentHome(descriptor);
2974
+ const workspaceEnv = descriptor?.feature?.workspaceEnv ?? [];
2872
2975
  const sourceRoot = featuresSourceRoot();
2873
2976
  const localSourceDir = sourceRoot ? path10.join(sourceRoot, name) : null;
2874
2977
  if (descriptor && localSourceDir && existsSync7(localSourceDir)) {
@@ -2879,7 +2982,8 @@ function resolveFeatures(opts) {
2879
2982
  localName: name,
2880
2983
  generatedManifest: descriptorToFeatureManifest(descriptor),
2881
2984
  persistentHomePaths: paths,
2882
- persistentHomeFiles: files
2985
+ persistentHomeFiles: files,
2986
+ workspaceEnv
2883
2987
  });
2884
2988
  continue;
2885
2989
  }
@@ -2887,7 +2991,8 @@ function resolveFeatures(opts) {
2887
2991
  devcontainerKey: rawRef,
2888
2992
  options,
2889
2993
  persistentHomePaths: paths,
2890
- persistentHomeFiles: files
2994
+ persistentHomeFiles: files,
2995
+ workspaceEnv
2891
2996
  });
2892
2997
  continue;
2893
2998
  }
@@ -2895,7 +3000,8 @@ function resolveFeatures(opts) {
2895
3000
  devcontainerKey: rawRef,
2896
3001
  options,
2897
3002
  persistentHomePaths: [],
2898
- persistentHomeFiles: []
3003
+ persistentHomeFiles: [],
3004
+ workspaceEnv: []
2899
3005
  });
2900
3006
  }
2901
3007
  }
@@ -2915,6 +3021,33 @@ function descriptorPersistentHome(descriptor) {
2915
3021
  files: filterFileEntries(descriptor?.feature?.persistentHomeFiles)
2916
3022
  };
2917
3023
  }
3024
+ function isOptionEnabled(value) {
3025
+ if (typeof value === "boolean") return value;
3026
+ if (typeof value === "number") return value !== 0;
3027
+ if (typeof value === "string") return value !== "" && value !== "false";
3028
+ return false;
3029
+ }
3030
+ function featureWorkspaceEnv(features) {
3031
+ const env = {};
3032
+ for (const f of features) {
3033
+ for (const block of f.workspaceEnv) {
3034
+ if (block.whenOption !== void 0 && !isOptionEnabled(f.options[block.whenOption])) {
3035
+ continue;
3036
+ }
3037
+ for (const [name, template] of Object.entries(block.vars)) {
3038
+ const value = template.replace(
3039
+ /\$\{([A-Za-z0-9_]+)\}/g,
3040
+ (_, token) => {
3041
+ const v = f.options[token];
3042
+ return v === void 0 || v === null ? "" : String(v);
3043
+ }
3044
+ );
3045
+ if (value !== "") env[name] = value;
3046
+ }
3047
+ }
3048
+ }
3049
+ return env;
3050
+ }
2918
3051
  function filterSubpaths(raw) {
2919
3052
  if (!Array.isArray(raw)) return [];
2920
3053
  return raw.filter(
@@ -3034,12 +3167,15 @@ function buildDevcontainerJson(opts, dockerMode = "rootful") {
3034
3167
  }
3035
3168
  } : void 0;
3036
3169
  const sshPostStart = runtimeSupportsSshAttach(opts.runtimeVersion) ? { postStartCommand: "sudo /usr/local/bin/monoceros-sshd-up.sh" } : {};
3170
+ const workspaceEnv = featureWorkspaceEnv(resolvedFeatures);
3171
+ const containerEnvField = Object.keys(workspaceEnv).length > 0 ? { containerEnv: workspaceEnv } : void 0;
3037
3172
  if (needsCompose(opts)) {
3173
+ const eagerServices = opts.services.filter((s) => !serviceDefersStart(s.name)).map((s) => s.name);
3038
3174
  return {
3039
3175
  name: opts.name,
3040
3176
  dockerComposeFile: "compose.yaml",
3041
3177
  service: "workspace",
3042
- ...opts.services.length > 0 ? { runServices: opts.services.map((s) => s.name) } : {},
3178
+ ...eagerServices.length > 0 ? { runServices: eagerServices } : {},
3043
3179
  workspaceFolder: `/workspaces/${opts.name}`,
3044
3180
  remoteUser: "node",
3045
3181
  forwardPorts: ports,
@@ -3074,6 +3210,7 @@ function buildDevcontainerJson(opts, dockerMode = "rootful") {
3074
3210
  forwardPorts: ports,
3075
3211
  postCreateCommand: ".devcontainer/post-create.sh",
3076
3212
  ...sshPostStart,
3213
+ ...containerEnvField ?? {},
3077
3214
  ...featuresField ?? {},
3078
3215
  ...customizationsField ?? {}
3079
3216
  };
@@ -3123,7 +3260,10 @@ function buildComposeYaml(opts, dockerMode = "rootful") {
3123
3260
  for (const v of ideVolumes) {
3124
3261
  lines.push(` - ${v.volume}:${v.target}`);
3125
3262
  }
3126
- const wsEnv = serviceConnectionEnv(opts.services);
3263
+ const wsEnv = {
3264
+ ...serviceConnectionEnv(opts.services),
3265
+ ...featureWorkspaceEnv(resolvedFeatures)
3266
+ };
3127
3267
  const wsEnvKeys = Object.keys(wsEnv);
3128
3268
  if (wsEnvKeys.length > 0) {
3129
3269
  lines.push(" environment:");
@@ -3134,6 +3274,10 @@ function buildComposeYaml(opts, dockerMode = "rootful") {
3134
3274
  for (const svc of opts.services) {
3135
3275
  lines.push(` ${svc.name}:`);
3136
3276
  lines.push(` image: ${svc.image}`);
3277
+ if (serviceDefersStart(svc.name)) {
3278
+ lines.push(" profiles:");
3279
+ lines.push(` - ${DEFERRED_SERVICE_PROFILE}`);
3280
+ }
3137
3281
  if (svc.user !== void 0) {
3138
3282
  lines.push(` user: ${composeScalar(svc.user)}`);
3139
3283
  }
@@ -3195,9 +3339,22 @@ function extractRepoHost(url) {
3195
3339
  }
3196
3340
  function computeExtensionRecommendations(opts) {
3197
3341
  const recs = /* @__PURE__ */ new Set();
3342
+ for (const langSpec of opts.languages) {
3343
+ const parsed = parseLanguageSpec(langSpec);
3344
+ if (!parsed) continue;
3345
+ for (const ext of LANGUAGE_CATALOG[parsed.name]?.vscodeExtensions ?? []) {
3346
+ recs.add(ext);
3347
+ }
3348
+ }
3198
3349
  for (const svc of opts.services) {
3199
- const catalogEntry = SERVICE_CATALOG[svc.name];
3200
- for (const ext of catalogEntry?.vscodeExtensions ?? []) {
3350
+ for (const ext of SERVICE_CATALOG[svc.name]?.vscodeExtensions ?? []) {
3351
+ recs.add(ext);
3352
+ }
3353
+ }
3354
+ for (const rawRef of Object.keys(opts.features ?? {})) {
3355
+ const match = matchMonocerosFeature(rawRef);
3356
+ const descriptor = match ? featureDescriptor(match.name) : void 0;
3357
+ for (const ext of descriptor?.feature?.vscodeExtensions ?? []) {
3201
3358
  recs.add(ext);
3202
3359
  }
3203
3360
  }
@@ -3812,26 +3969,39 @@ function addInstallUrlToDoc(doc, url) {
3812
3969
  seq.add(url);
3813
3970
  return true;
3814
3971
  }
3815
- function addFeatureToDoc(doc, ref, options = {}, displayName) {
3972
+ function addFeatureToDoc(doc, ref, options = {}, opts = {}) {
3816
3973
  const seq = ensureSeq(doc, "features");
3817
- const label = displayName ?? ref;
3974
+ const label = opts.displayName ?? ref;
3818
3975
  const summary = loadFeatureManifestSummary(ref);
3819
- const hints = featureOptionHints(summary, ref, Object.keys(options));
3820
- const mergedOptions = { ...options };
3821
- for (const h of hints) mergedOptions[h.key] = h.placeholder;
3976
+ const withHints = (o) => {
3977
+ const out = { ...o };
3978
+ for (const h of featureOptionHints(summary, ref, Object.keys(out))) {
3979
+ out[h.key] = h.placeholder;
3980
+ }
3981
+ return out;
3982
+ };
3822
3983
  for (const item of seq.items) {
3823
3984
  if (!isMap2(item)) continue;
3824
3985
  const itemRef = item.get("ref");
3825
3986
  if (itemRef !== ref) continue;
3826
3987
  const itemJs = item.toJS(doc);
3827
3988
  const existingJs = itemJs.options ?? {};
3828
- if (JSON.stringify(existingJs) === JSON.stringify(mergedOptions)) {
3989
+ if (opts.isPreset) {
3990
+ const merged = withHints(mergeFeatureOptions(existingJs, options));
3991
+ if (JSON.stringify(existingJs) === JSON.stringify(merged)) {
3992
+ return false;
3993
+ }
3994
+ item.set("options", merged);
3995
+ return true;
3996
+ }
3997
+ if (JSON.stringify(existingJs) === JSON.stringify(withHints(options))) {
3829
3998
  return false;
3830
3999
  }
3831
4000
  throw new Error(
3832
4001
  `Feature ${label} is already configured with different options. Remove it first (\`monoceros remove-feature ${label}\`) before re-adding.`
3833
4002
  );
3834
4003
  }
4004
+ const mergedOptions = withHints(options);
3835
4005
  const entry2 = new YAMLMap2();
3836
4006
  entry2.set("ref", ref);
3837
4007
  if (Object.keys(mergedOptions).length > 0) {
@@ -3993,6 +4163,7 @@ var init_yml = __esm({
3993
4163
  "use strict";
3994
4164
  init_scaffold();
3995
4165
  init_manifest();
4166
+ init_components();
3996
4167
  init_feature_doc();
3997
4168
  init_env_file();
3998
4169
  GIT_USER_HEADER_COMMENT = [
@@ -4038,7 +4209,7 @@ async function runAddService(input) {
4038
4209
  const image = curated ? expandCuratedService(arg).image : arg;
4039
4210
  const custom = curated ? null : renderCustomService(name, arg);
4040
4211
  const bodyLines = curated ? renderServiceObjectBody({ ...expandCuratedService(arg), name }) : custom.bodyLines;
4041
- const scaffoldComment = curated ? void 0 : custom.comment;
4212
+ const scaffoldComment = curated ? exampleVolumesComment(curatedServiceExampleVolumes(arg)) : custom.comment;
4042
4213
  const result = await mutate(input, (doc) => {
4043
4214
  const r = addServiceEntryToDoc(
4044
4215
  doc,
@@ -4340,7 +4511,10 @@ async function runAddFeature(input) {
4340
4511
  };
4341
4512
  const result = await mutate(
4342
4513
  input,
4343
- (doc) => addFeatureToDoc(doc, resolved.ref, merged, raw)
4514
+ (doc) => addFeatureToDoc(doc, resolved.ref, merged, {
4515
+ isPreset: resolved.isPreset,
4516
+ displayName: raw
4517
+ })
4344
4518
  );
4345
4519
  if (result.status === "updated") {
4346
4520
  const summary = loadFeatureManifestSummary(resolved.ref);
@@ -4368,7 +4542,7 @@ async function runAddFeature(input) {
4368
4542
  }
4369
4543
  async function resolveFeatureRefOrShortname(input) {
4370
4544
  if (REGEX.featureRef.test(input)) {
4371
- return { ref: input, defaultOptions: {} };
4545
+ return { ref: input, defaultOptions: {}, isPreset: false };
4372
4546
  }
4373
4547
  const catalog = await loadComponentCatalog();
4374
4548
  const component = catalog.get(input);
@@ -4400,7 +4574,8 @@ async function resolveFeatureRefOrShortname(input) {
4400
4574
  const [first] = features;
4401
4575
  return {
4402
4576
  ref: first.ref,
4403
- defaultOptions: { ...first.options ?? {} }
4577
+ defaultOptions: { ...first.options ?? {} },
4578
+ isPreset: input.includes("/")
4404
4579
  };
4405
4580
  }
4406
4581
  function runRemoveLanguage(input) {
@@ -4683,7 +4858,7 @@ var init_add_feature = __esm({
4683
4858
  meta: {
4684
4859
  name: "add-feature",
4685
4860
  group: "edit",
4686
- description: "Add a devcontainer feature by ref to the container config. Options follow `--` as `key=value` pairs. Idempotent (same ref + same options is a no-op). Adding the same ref with different options is an error."
4861
+ description: "Add a devcontainer feature by ref to the container config. Options follow `--` as `key=value` pairs. Adding a sub-tool selector (e.g. `atlassian/forge`) to an already-present feature merges it in additively, keeping the other sub-tools on. A plain feature already present with different options is an error (remove + re-add to change it)."
4687
4862
  },
4688
4863
  args: {
4689
4864
  name: {
@@ -4846,7 +5021,7 @@ var init_add_repo = __esm({
4846
5021
  },
4847
5022
  provider: {
4848
5023
  type: "string",
4849
- description: "Git provider for credential-helper guidance: github | gitlab | bitbucket. Required when the URL host is not github.com, gitlab.com, or bitbucket.org \u2014 Monoceros uses this to suggest the right CLI (gh / glab / Atlassian token) on missing credentials."
5024
+ description: "Git provider for credential-helper guidance: github | gitlab | bitbucket | gitea. Required when the URL host is not github.com, gitlab.com, or bitbucket.org \u2014 Monoceros uses this to suggest the right auth (gh / glab / a provider token) on missing credentials."
4850
5025
  },
4851
5026
  yes: {
4852
5027
  type: "boolean",
@@ -6642,6 +6817,25 @@ import { existsSync as existsSync9 } from "fs";
6642
6817
  import path17 from "path";
6643
6818
  import { Writable as Writable3 } from "stream";
6644
6819
  import { consola as consola10 } from "consola";
6820
+ function spawnDockerComposeTo(opts) {
6821
+ return (args, cwd) => new Promise((resolve, reject) => {
6822
+ const child = spawn6("docker", ["compose", ...args], {
6823
+ cwd,
6824
+ stdio: ["inherit", "pipe", "pipe"]
6825
+ });
6826
+ const route = (src, screen) => {
6827
+ if (!src) return;
6828
+ src.pipe(createSecretMaskStream()).on("data", (chunk) => {
6829
+ opts.logSink?.write(chunk);
6830
+ if (!opts.silent) screen.write(chunk);
6831
+ });
6832
+ };
6833
+ route(child.stdout, process.stdout);
6834
+ route(child.stderr, process.stderr);
6835
+ child.on("error", reject);
6836
+ child.on("exit", (code) => resolve(code ?? 0));
6837
+ });
6838
+ }
6645
6839
  async function findContainerIds(filters, exec = spawnDocker) {
6646
6840
  const ids = /* @__PURE__ */ new Set();
6647
6841
  for (const filter of filters) {
@@ -6717,6 +6911,32 @@ async function runComposeAction(buildSubArgs, opts) {
6717
6911
  const subArgs = buildSubArgs(opts.service);
6718
6912
  return spawnFn(["-f", composeFile, "-p", projectName, ...subArgs], opts.root);
6719
6913
  }
6914
+ async function startDeferredServices(opts) {
6915
+ if (opts.services.length === 0) return 0;
6916
+ const { composeFile, projectName } = resolveCompose(opts.root);
6917
+ const spawnFn = opts.spawn ?? spawnDockerComposeTo({
6918
+ ...opts.logSink ? { logSink: opts.logSink } : {},
6919
+ ...opts.silent ? { silent: true } : {}
6920
+ });
6921
+ opts.logger?.info(
6922
+ `Starting deferred service(s): ${opts.services.join(", ")}\u2026`
6923
+ );
6924
+ return spawnFn(
6925
+ [
6926
+ "-f",
6927
+ composeFile,
6928
+ "-p",
6929
+ projectName,
6930
+ "--profile",
6931
+ DEFERRED_SERVICE_PROFILE,
6932
+ "up",
6933
+ "-d",
6934
+ "--quiet-pull",
6935
+ ...opts.services
6936
+ ],
6937
+ opts.root
6938
+ );
6939
+ }
6720
6940
  async function runStart(opts) {
6721
6941
  assertDevcontainer(opts.root);
6722
6942
  const logger = opts.logger ?? { info: (msg) => consola10.info(msg) };
@@ -6944,6 +7164,7 @@ var init_compose = __esm({
6944
7164
  "use strict";
6945
7165
  init_proxy();
6946
7166
  init_mask_secrets();
7167
+ init_catalog();
6947
7168
  init_cli();
6948
7169
  init_proxy();
6949
7170
  spawnDockerCompose = (args, cwd) => {
@@ -7020,7 +7241,7 @@ var init_images = __esm({
7020
7241
  });
7021
7242
 
7022
7243
  // src/config/machine-state.ts
7023
- import { promises as fsp4 } from "fs";
7244
+ import { promises as fsp4, readFileSync as readFileSync6 } from "fs";
7024
7245
  import path18 from "path";
7025
7246
  function machineStatePath(home = monocerosHome()) {
7026
7247
  return path18.join(home, ".machine-state.json");
@@ -7036,6 +7257,18 @@ async function readMachineState(home = monocerosHome()) {
7036
7257
  }
7037
7258
  return {};
7038
7259
  }
7260
+ function readMachineStateSync(home = monocerosHome()) {
7261
+ try {
7262
+ const parsed = JSON.parse(
7263
+ readFileSync6(machineStatePath(home), "utf8")
7264
+ );
7265
+ if (typeof parsed === "object" && parsed !== null) {
7266
+ return parsed;
7267
+ }
7268
+ } catch {
7269
+ }
7270
+ return {};
7271
+ }
7039
7272
  async function writeMachineState(state, home = monocerosHome()) {
7040
7273
  await fsp4.writeFile(
7041
7274
  machineStatePath(home),
@@ -7051,6 +7284,12 @@ async function recordBuiltImage(record, home = monocerosHome()) {
7051
7284
  state.builtImages = [...rest, record];
7052
7285
  await writeMachineState(state, home);
7053
7286
  }
7287
+ async function recordVersionCheck(opts, home = monocerosHome()) {
7288
+ const state = await readMachineState(home);
7289
+ state.lastVersionCheckAt = opts.nowIso;
7290
+ if (opts.latestVersion) state.latestVersion = opts.latestVersion;
7291
+ await writeMachineState(state, home);
7292
+ }
7054
7293
  async function markUpgraded(nowIso, home = monocerosHome()) {
7055
7294
  const state = await readMachineState(home);
7056
7295
  state.lastUpgradeAt = nowIso;
@@ -7590,6 +7829,30 @@ Fix the value in the env file (or the yml).`
7590
7829
  ...progress ? { progressSink: progress.streamSink, silent: true } : {},
7591
7830
  logger: internalLogger
7592
7831
  });
7832
+ if (exitCode === 0) {
7833
+ const deferred = createOpts.services.filter((s) => serviceDefersStart(s.name)).map((s) => s.name);
7834
+ if (deferred.length > 0) {
7835
+ if (progress) progress.setPhase("starting services\u2026");
7836
+ try {
7837
+ const deferExit = await startDeferredServices({
7838
+ root: targetDir,
7839
+ services: deferred,
7840
+ logSink: applyLog.sink,
7841
+ silent: progress !== null,
7842
+ logger: internalLogger
7843
+ });
7844
+ if (deferExit !== 0) {
7845
+ containerLogger.warn?.(
7846
+ `Deferred service(s) ${deferred.join(", ")} did not start cleanly (exit ${deferExit}). The workspace is up; bring them up with \`monoceros start ${opts.name}\`.`
7847
+ );
7848
+ }
7849
+ } catch (err) {
7850
+ containerLogger.warn?.(
7851
+ `Could not start deferred service(s) ${deferred.join(", ")}: ${err instanceof Error ? err.message : String(err)}. The workspace is up.`
7852
+ );
7853
+ }
7854
+ }
7855
+ }
7593
7856
  if (progress) {
7594
7857
  if (exitCode === 0) {
7595
7858
  progress.succeed();
@@ -7784,7 +8047,7 @@ var init_apply = __esm({
7784
8047
 
7785
8048
  // src/devcontainer/browser-bridge.ts
7786
8049
  import { spawn as spawn9 } from "child_process";
7787
- import { existsSync as existsSync11, promises as fsp5, readFileSync as readFileSync6 } from "fs";
8050
+ import { existsSync as existsSync11, promises as fsp5, readFileSync as readFileSync7 } from "fs";
7788
8051
  import http from "http";
7789
8052
  import path20 from "path";
7790
8053
  function parseCallbackTarget(authUrl) {
@@ -7887,7 +8150,7 @@ exit 0
7887
8150
  if (!existsSync11(urlFile)) return;
7888
8151
  let content = "";
7889
8152
  try {
7890
- content = readFileSync6(urlFile, "utf8");
8153
+ content = readFileSync7(urlFile, "utf8");
7891
8154
  } catch {
7892
8155
  return;
7893
8156
  }
@@ -8084,7 +8347,7 @@ var CLI_VERSION;
8084
8347
  var init_version = __esm({
8085
8348
  "src/version.ts"() {
8086
8349
  "use strict";
8087
- CLI_VERSION = true ? "1.29.2" : "dev";
8350
+ CLI_VERSION = true ? "1.31.0" : "dev";
8088
8351
  }
8089
8352
  });
8090
8353
 
@@ -8729,6 +8992,140 @@ var init_complete = __esm({
8729
8992
  }
8730
8993
  });
8731
8994
 
8995
+ // src/update/notifier.ts
8996
+ import { spawn as spawn11 } from "child_process";
8997
+ function isNewerVersion(latest, current) {
8998
+ const parse = (v) => {
8999
+ const m = /^(\d+)\.(\d+)\.(\d+)/.exec(v.trim());
9000
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
9001
+ };
9002
+ const a = parse(latest);
9003
+ const b = parse(current);
9004
+ if (!a || !b) return false;
9005
+ for (let i = 0; i < 3; i++) {
9006
+ if (a[i] !== b[i]) return a[i] > b[i];
9007
+ }
9008
+ return false;
9009
+ }
9010
+ function formatUpdateNotice(latest, current) {
9011
+ return `
9012
+ \u2B06 Monoceros ${latest} is available (you have ${current}).
9013
+ Update: ${INSTALL_COMMAND}
9014
+ `;
9015
+ }
9016
+ function decideUpdateAction(state, currentVersion, now, intervalMs = CHECK_INTERVAL_MS) {
9017
+ const notice = state.latestVersion && isNewerVersion(state.latestVersion, currentVersion) ? formatUpdateNotice(state.latestVersion, currentVersion) : null;
9018
+ const last = state.lastVersionCheckAt ? new Date(state.lastVersionCheckAt).getTime() : NaN;
9019
+ const refresh = !Number.isFinite(last) || now.getTime() - last > intervalMs;
9020
+ return { notice, refresh };
9021
+ }
9022
+ function scheduleUpdateNotice(opts) {
9023
+ try {
9024
+ if (process.env[OPT_OUT_ENV]) return;
9025
+ if (opts.currentVersion === "dev") return;
9026
+ if (!process.stdout.isTTY) return;
9027
+ if (opts.commandName === void 0) return;
9028
+ if (SKIP_COMMANDS.has(opts.commandName)) return;
9029
+ const argv = opts.argv ?? process.argv.slice(2);
9030
+ if (argv.some((a) => ["-h", "--help", "-v", "--version"].includes(a))) {
9031
+ return;
9032
+ }
9033
+ const state = readMachineStateSync();
9034
+ const { notice, refresh } = decideUpdateAction(
9035
+ state,
9036
+ opts.currentVersion,
9037
+ opts.now ?? /* @__PURE__ */ new Date()
9038
+ );
9039
+ if (notice) {
9040
+ process.on("exit", () => {
9041
+ try {
9042
+ process.stderr.write(notice);
9043
+ } catch {
9044
+ }
9045
+ });
9046
+ }
9047
+ if (refresh) spawnBackgroundCheck();
9048
+ } catch {
9049
+ }
9050
+ }
9051
+ function spawnBackgroundCheck() {
9052
+ const self = process.argv[1];
9053
+ if (!self) return;
9054
+ try {
9055
+ const child = spawn11(process.execPath, [self, "__update-check"], {
9056
+ detached: true,
9057
+ stdio: "ignore"
9058
+ });
9059
+ child.unref();
9060
+ } catch {
9061
+ }
9062
+ }
9063
+ async function runUpdateCheck(now = /* @__PURE__ */ new Date(), home) {
9064
+ let latestVersion;
9065
+ try {
9066
+ const controller = new AbortController();
9067
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
9068
+ try {
9069
+ const res = await fetch(REGISTRY_URL, { signal: controller.signal });
9070
+ if (res.ok) {
9071
+ const json = await res.json();
9072
+ if (typeof json.version === "string") latestVersion = json.version;
9073
+ }
9074
+ } finally {
9075
+ clearTimeout(timer);
9076
+ }
9077
+ } catch {
9078
+ }
9079
+ try {
9080
+ await recordVersionCheck(
9081
+ {
9082
+ ...latestVersion ? { latestVersion } : {},
9083
+ nowIso: now.toISOString()
9084
+ },
9085
+ home
9086
+ );
9087
+ } catch {
9088
+ }
9089
+ }
9090
+ var PACKAGE, REGISTRY_URL, INSTALL_COMMAND, CHECK_INTERVAL_MS, OPT_OUT_ENV, FETCH_TIMEOUT_MS, SKIP_COMMANDS;
9091
+ var init_notifier = __esm({
9092
+ "src/update/notifier.ts"() {
9093
+ "use strict";
9094
+ init_machine_state();
9095
+ PACKAGE = "@getmonoceros/workbench";
9096
+ REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE}/latest`;
9097
+ INSTALL_COMMAND = "curl -fsSL https://raw.githubusercontent.com/getmonoceros/workbench/main/install.sh | bash";
9098
+ CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
9099
+ OPT_OUT_ENV = "MONOCEROS_NO_UPDATE_NOTIFIER";
9100
+ FETCH_TIMEOUT_MS = 5e3;
9101
+ SKIP_COMMANDS = /* @__PURE__ */ new Set(["__complete", "__update-check", "completion"]);
9102
+ }
9103
+ });
9104
+
9105
+ // src/commands/__update-check.ts
9106
+ import { defineCommand as defineCommand11 } from "citty";
9107
+ var __updateCheckCommand;
9108
+ var init_update_check = __esm({
9109
+ "src/commands/__update-check.ts"() {
9110
+ "use strict";
9111
+ init_notifier();
9112
+ __updateCheckCommand = defineCommand11({
9113
+ meta: {
9114
+ name: "__update-check",
9115
+ group: "internal",
9116
+ hidden: true,
9117
+ description: "Internal: refresh the cached latest-version (background)."
9118
+ },
9119
+ async run() {
9120
+ try {
9121
+ await runUpdateCheck();
9122
+ } catch {
9123
+ }
9124
+ }
9125
+ });
9126
+ }
9127
+ });
9128
+
8732
9129
  // src/init/generator.ts
8733
9130
  function generateComposedYml(name, composed, lookupManifest, repoUrls = [], ports = []) {
8734
9131
  const lines = [];
@@ -8894,6 +9291,12 @@ function generateDocumentedYml(name, catalog, lookupManifest, repoUrls = [], por
8894
9291
  const body = renderServiceObjectBody(expandCuratedService(svc));
8895
9292
  lines.push(`# - ${body[0]}`);
8896
9293
  for (const line of body.slice(1)) lines.push(`# ${line}`);
9294
+ const exComment = exampleVolumesComment(
9295
+ curatedServiceExampleVolumes(svc)
9296
+ );
9297
+ if (exComment) {
9298
+ for (const cl of exComment.split("\n")) lines.push(`# ${cl}`);
9299
+ }
8897
9300
  }
8898
9301
  }
8899
9302
  lines.push("");
@@ -9013,6 +9416,12 @@ function pushServiceEntry(out, svc) {
9013
9416
  const body = renderServiceObjectBody(expandCuratedService(svc.name));
9014
9417
  out.push(` - ${body[0]}`);
9015
9418
  for (const line of body.slice(1)) out.push(` ${line}`);
9419
+ const exComment = exampleVolumesComment(
9420
+ curatedServiceExampleVolumes(svc.name)
9421
+ );
9422
+ if (exComment) {
9423
+ for (const cl of exComment.split("\n")) out.push(` #${cl}`);
9424
+ }
9016
9425
  }
9017
9426
  function pushGitIdentityBlock(lines) {
9018
9427
  pushSectionHeader(
@@ -9302,19 +9711,19 @@ function resolveInitAptPackages(entries) {
9302
9711
  }
9303
9712
  function resolveInitServices(entries) {
9304
9713
  const out = [];
9305
- const byName = /* @__PURE__ */ new Map();
9714
+ const byName2 = /* @__PURE__ */ new Map();
9306
9715
  for (const raw of entries) {
9307
9716
  const e = raw.trim();
9308
9717
  if (!e) continue;
9309
9718
  const svc = isCuratedService(e) ? { kind: "curated", name: e } : { kind: "custom", name: deriveServiceName(e), image: e };
9310
- const existing = byName.get(svc.name);
9719
+ const existing = byName2.get(svc.name);
9311
9720
  if (existing) {
9312
9721
  if (existing.kind === svc.kind && existing.image === svc.image) continue;
9313
9722
  throw new Error(
9314
9723
  `Two --with-services entries resolve to the service name '${svc.name}'. Add one after init with \`monoceros add-service ${"<name>"} <image> --as=<other>\`.`
9315
9724
  );
9316
9725
  }
9317
- byName.set(svc.name, svc);
9726
+ byName2.set(svc.name, svc);
9318
9727
  out.push(svc);
9319
9728
  }
9320
9729
  return out;
@@ -9370,7 +9779,7 @@ var init_init = __esm({
9370
9779
  });
9371
9780
 
9372
9781
  // src/commands/init.ts
9373
- import { defineCommand as defineCommand11 } from "citty";
9782
+ import { defineCommand as defineCommand12 } from "citty";
9374
9783
  import { consola as consola17 } from "consola";
9375
9784
  function collectListFlag(flag, rawArgs) {
9376
9785
  const eq = `${flag}=`;
@@ -9436,7 +9845,7 @@ var init_init2 = __esm({
9436
9845
  "src/commands/init.ts"() {
9437
9846
  "use strict";
9438
9847
  init_init();
9439
- initCommand = defineCommand11({
9848
+ initCommand = defineCommand12({
9440
9849
  meta: {
9441
9850
  name: "init",
9442
9851
  group: "lifecycle",
@@ -9544,8 +9953,76 @@ var init_expand = __esm({
9544
9953
  }
9545
9954
  });
9546
9955
 
9956
+ // src/catalog/catalog-json.ts
9957
+ function projectOption(key, spec) {
9958
+ if (spec.surface === "silent") return null;
9959
+ const out = {
9960
+ key,
9961
+ type: spec.type,
9962
+ surface: spec.surface
9963
+ };
9964
+ if (spec.default !== void 0) out.default = spec.default;
9965
+ if (spec.description !== void 0) out.description = spec.description;
9966
+ if (spec.proposals !== void 0) out.proposals = spec.proposals;
9967
+ return out;
9968
+ }
9969
+ function projectOptions(options) {
9970
+ return Object.entries(options).map(([key, spec]) => projectOption(key, spec)).filter((o) => o !== null).sort((a, b) => a.key.localeCompare(b.key));
9971
+ }
9972
+ function byName(a, b) {
9973
+ return a.name.localeCompare(b.name);
9974
+ }
9975
+ function buildCatalogJson(catalog, cliVersion) {
9976
+ const languages = [];
9977
+ const services = [];
9978
+ const features = [];
9979
+ for (const { descriptor: d } of catalog.values()) {
9980
+ const base = {
9981
+ name: d.name ?? d.id,
9982
+ displayName: d.displayName,
9983
+ description: d.description,
9984
+ options: projectOptions(d.options)
9985
+ };
9986
+ if (d.documentationURL !== void 0) {
9987
+ base.documentationURL = d.documentationURL;
9988
+ }
9989
+ if (d.category === "language" && d.language) {
9990
+ const lang = { ...base };
9991
+ if (d.language.defaultVersion !== void 0) {
9992
+ lang.defaultVersion = d.language.defaultVersion;
9993
+ }
9994
+ if (d.language.versions !== void 0) {
9995
+ lang.versions = d.language.versions;
9996
+ }
9997
+ languages.push(lang);
9998
+ } else if (d.category === "service" && d.service) {
9999
+ const svc = { ...base };
10000
+ if (d.service.defaultPort !== void 0) {
10001
+ svc.defaultPort = d.service.defaultPort;
10002
+ }
10003
+ services.push(svc);
10004
+ } else if (d.category === "feature") {
10005
+ features.push({ ...base, presets: Object.keys(d.presets ?? {}).sort() });
10006
+ }
10007
+ }
10008
+ return {
10009
+ schemaVersion: CATALOG_JSON_SCHEMA_VERSION,
10010
+ cliVersion,
10011
+ languages: languages.sort(byName),
10012
+ services: services.sort(byName),
10013
+ features: features.sort(byName)
10014
+ };
10015
+ }
10016
+ var CATALOG_JSON_SCHEMA_VERSION;
10017
+ var init_catalog_json = __esm({
10018
+ "src/catalog/catalog-json.ts"() {
10019
+ "use strict";
10020
+ CATALOG_JSON_SCHEMA_VERSION = 1;
10021
+ }
10022
+ });
10023
+
9547
10024
  // src/commands/list-components.ts
9548
- import { defineCommand as defineCommand12 } from "citty";
10025
+ import { defineCommand as defineCommand13 } from "citty";
9549
10026
  import { consola as consola18 } from "consola";
9550
10027
  var CATEGORY_LABELS, CATEGORY_ORDER, listComponentsCommand;
9551
10028
  var init_list_components = __esm({
@@ -9553,6 +10030,8 @@ var init_list_components = __esm({
9553
10030
  "use strict";
9554
10031
  init_load();
9555
10032
  init_expand();
10033
+ init_catalog_json();
10034
+ init_version();
9556
10035
  init_format();
9557
10036
  CATEGORY_LABELS = {
9558
10037
  language: "Languages",
@@ -9564,16 +10043,28 @@ var init_list_components = __esm({
9564
10043
  "service",
9565
10044
  "feature"
9566
10045
  ];
9567
- listComponentsCommand = defineCommand12({
10046
+ listComponentsCommand = defineCommand13({
9568
10047
  meta: {
9569
10048
  name: "list-components",
9570
10049
  group: "discovery",
9571
- description: "Print the components catalog used by `monoceros init --with-languages=\u2026 / --with-services=\u2026 / --with-features=\u2026`, grouped by category (Languages, Services, Features). Component names render in cyan, descriptions in default colour; when piped, the formatting drops out and lines become `name<TAB>description` for grep/awk-friendly consumption."
10050
+ description: "Print the components catalog used by `monoceros init --with-languages=\u2026 / --with-services=\u2026 / --with-features=\u2026`, grouped by category (Languages, Services, Features). Component names render in cyan, descriptions in default colour; when piped, the formatting drops out and lines become `name<TAB>description` for grep/awk-friendly consumption. `--json` emits the full catalog (options, versions, presets) as a machine-readable document \u2014 the same shape published at getmonoceros.build/catalog.json."
9572
10051
  },
9573
- args: {},
9574
- async run() {
10052
+ args: {
10053
+ json: {
10054
+ type: "boolean",
10055
+ description: "Emit the catalog as a JSON document (name, options, versions, presets per component) instead of the human-readable listing."
10056
+ }
10057
+ },
10058
+ async run({ args }) {
9575
10059
  try {
9576
- const catalog = expandSelectable(await loadDescriptorCatalog());
10060
+ const descriptors = await loadDescriptorCatalog();
10061
+ if (args.json) {
10062
+ const doc = buildCatalogJson(descriptors, CLI_VERSION);
10063
+ process.stdout.write(`${JSON.stringify(doc, null, 2)}
10064
+ `);
10065
+ process.exit(0);
10066
+ }
10067
+ const catalog = expandSelectable(descriptors);
9577
10068
  if (catalog.size === 0) {
9578
10069
  consola18.warn(
9579
10070
  "No components found. The workbench checkout looks incomplete."
@@ -9635,14 +10126,14 @@ var init_list_components = __esm({
9635
10126
  });
9636
10127
 
9637
10128
  // src/commands/logs.ts
9638
- import { spawn as spawn11 } from "child_process";
10129
+ import { spawn as spawn12 } from "child_process";
9639
10130
  import { existsSync as existsSync16 } from "fs";
9640
10131
  import path25 from "path";
9641
- import { defineCommand as defineCommand13 } from "citty";
10132
+ import { defineCommand as defineCommand14 } from "citty";
9642
10133
  function tailLogFile(file, follow) {
9643
10134
  const [cmd, args] = follow ? ["tail", ["-F", file]] : ["cat", [file]];
9644
10135
  return new Promise((resolve, reject) => {
9645
- const child = spawn11(cmd, args, { stdio: "inherit" });
10136
+ const child = spawn12(cmd, args, { stdio: "inherit" });
9646
10137
  child.on("error", reject);
9647
10138
  child.on("exit", (code) => resolve(code ?? 0));
9648
10139
  });
@@ -9654,7 +10145,7 @@ var init_logs = __esm({
9654
10145
  init_paths();
9655
10146
  init_compose();
9656
10147
  init_dispatch();
9657
- logsCommand = defineCommand13({
10148
+ logsCommand = defineCommand14({
9658
10149
  meta: {
9659
10150
  name: "logs",
9660
10151
  group: "run",
@@ -9698,14 +10189,14 @@ var init_logs = __esm({
9698
10189
  });
9699
10190
 
9700
10191
  // src/commands/open.ts
9701
- import { defineCommand as defineCommand14 } from "citty";
10192
+ import { defineCommand as defineCommand15 } from "citty";
9702
10193
  var openCommand;
9703
10194
  var init_open2 = __esm({
9704
10195
  "src/commands/open.ts"() {
9705
10196
  "use strict";
9706
10197
  init_open();
9707
10198
  init_dispatch();
9708
- openCommand = defineCommand14({
10199
+ openCommand = defineCommand15({
9709
10200
  meta: {
9710
10201
  name: "open",
9711
10202
  group: "run",
@@ -9731,7 +10222,7 @@ var init_open2 = __esm({
9731
10222
  });
9732
10223
 
9733
10224
  // src/commands/port.ts
9734
- import { defineCommand as defineCommand15 } from "citty";
10225
+ import { defineCommand as defineCommand16 } from "citty";
9735
10226
  import { consola as consola19 } from "consola";
9736
10227
  async function runPortListing(opts) {
9737
10228
  const out = opts.out ?? process.stdout;
@@ -9793,7 +10284,7 @@ var init_port = __esm({
9793
10284
  init_schema();
9794
10285
  init_dynamic();
9795
10286
  init_format();
9796
- portCommand = defineCommand15({
10287
+ portCommand = defineCommand16({
9797
10288
  meta: {
9798
10289
  name: "port",
9799
10290
  group: "discovery",
@@ -9820,7 +10311,7 @@ var init_port = __esm({
9820
10311
  });
9821
10312
 
9822
10313
  // src/commands/remove-apt-packages.ts
9823
- import { defineCommand as defineCommand16 } from "citty";
10314
+ import { defineCommand as defineCommand17 } from "citty";
9824
10315
  import { consola as consola20 } from "consola";
9825
10316
  var removeAptPackagesCommand;
9826
10317
  var init_remove_apt_packages = __esm({
@@ -9828,7 +10319,7 @@ var init_remove_apt_packages = __esm({
9828
10319
  "use strict";
9829
10320
  init_inner_args();
9830
10321
  init_modify();
9831
- removeAptPackagesCommand = defineCommand16({
10322
+ removeAptPackagesCommand = defineCommand17({
9832
10323
  meta: {
9833
10324
  name: "remove-apt-packages",
9834
10325
  group: "edit",
@@ -9872,14 +10363,14 @@ var init_remove_apt_packages = __esm({
9872
10363
  });
9873
10364
 
9874
10365
  // src/commands/remove-feature.ts
9875
- import { defineCommand as defineCommand17 } from "citty";
10366
+ import { defineCommand as defineCommand18 } from "citty";
9876
10367
  import { consola as consola21 } from "consola";
9877
10368
  var removeFeatureCommand;
9878
10369
  var init_remove_feature = __esm({
9879
10370
  "src/commands/remove-feature.ts"() {
9880
10371
  "use strict";
9881
10372
  init_modify();
9882
- removeFeatureCommand = defineCommand17({
10373
+ removeFeatureCommand = defineCommand18({
9883
10374
  meta: {
9884
10375
  name: "remove-feature",
9885
10376
  group: "edit",
@@ -10075,7 +10566,7 @@ var init_remove = __esm({
10075
10566
  });
10076
10567
 
10077
10568
  // src/commands/remove.ts
10078
- import { defineCommand as defineCommand18 } from "citty";
10569
+ import { defineCommand as defineCommand19 } from "citty";
10079
10570
  import { consola as consola23 } from "consola";
10080
10571
  import { createInterface } from "readline/promises";
10081
10572
  var removeCommand;
@@ -10083,7 +10574,7 @@ var init_remove2 = __esm({
10083
10574
  "src/commands/remove.ts"() {
10084
10575
  "use strict";
10085
10576
  init_remove();
10086
- removeCommand = defineCommand18({
10577
+ removeCommand = defineCommand19({
10087
10578
  meta: {
10088
10579
  name: "remove",
10089
10580
  group: "lifecycle",
@@ -10219,14 +10710,14 @@ var init_restore = __esm({
10219
10710
  });
10220
10711
 
10221
10712
  // src/commands/restore.ts
10222
- import { defineCommand as defineCommand19 } from "citty";
10713
+ import { defineCommand as defineCommand20 } from "citty";
10223
10714
  import { consola as consola25 } from "consola";
10224
10715
  var restoreCommand;
10225
10716
  var init_restore2 = __esm({
10226
10717
  "src/commands/restore.ts"() {
10227
10718
  "use strict";
10228
10719
  init_restore();
10229
- restoreCommand = defineCommand19({
10720
+ restoreCommand = defineCommand20({
10230
10721
  meta: {
10231
10722
  name: "restore",
10232
10723
  group: "lifecycle",
@@ -10252,14 +10743,14 @@ var init_restore2 = __esm({
10252
10743
  });
10253
10744
 
10254
10745
  // src/commands/remove-from-url.ts
10255
- import { defineCommand as defineCommand20 } from "citty";
10746
+ import { defineCommand as defineCommand21 } from "citty";
10256
10747
  import { consola as consola26 } from "consola";
10257
10748
  var removeFromUrlCommand;
10258
10749
  var init_remove_from_url = __esm({
10259
10750
  "src/commands/remove-from-url.ts"() {
10260
10751
  "use strict";
10261
10752
  init_modify();
10262
- removeFromUrlCommand = defineCommand20({
10753
+ removeFromUrlCommand = defineCommand21({
10263
10754
  meta: {
10264
10755
  name: "remove-from-url",
10265
10756
  group: "edit",
@@ -10301,14 +10792,14 @@ var init_remove_from_url = __esm({
10301
10792
  });
10302
10793
 
10303
10794
  // src/commands/remove-language.ts
10304
- import { defineCommand as defineCommand21 } from "citty";
10795
+ import { defineCommand as defineCommand22 } from "citty";
10305
10796
  import { consola as consola27 } from "consola";
10306
10797
  var removeLanguageCommand;
10307
10798
  var init_remove_language = __esm({
10308
10799
  "src/commands/remove-language.ts"() {
10309
10800
  "use strict";
10310
10801
  init_modify();
10311
- removeLanguageCommand = defineCommand21({
10802
+ removeLanguageCommand = defineCommand22({
10312
10803
  meta: {
10313
10804
  name: "remove-language",
10314
10805
  group: "edit",
@@ -10350,7 +10841,7 @@ var init_remove_language = __esm({
10350
10841
  });
10351
10842
 
10352
10843
  // src/commands/remove-port.ts
10353
- import { defineCommand as defineCommand22 } from "citty";
10844
+ import { defineCommand as defineCommand23 } from "citty";
10354
10845
  import { consola as consola28 } from "consola";
10355
10846
  function coerceToken2(raw) {
10356
10847
  const n = Number(raw);
@@ -10362,7 +10853,7 @@ var init_remove_port = __esm({
10362
10853
  "use strict";
10363
10854
  init_inner_args();
10364
10855
  init_modify();
10365
- removePortCommand = defineCommand22({
10856
+ removePortCommand = defineCommand23({
10366
10857
  meta: {
10367
10858
  name: "remove-port",
10368
10859
  group: "edit",
@@ -10406,14 +10897,14 @@ var init_remove_port = __esm({
10406
10897
  });
10407
10898
 
10408
10899
  // src/commands/remove-repo.ts
10409
- import { defineCommand as defineCommand23 } from "citty";
10900
+ import { defineCommand as defineCommand24 } from "citty";
10410
10901
  import { consola as consola29 } from "consola";
10411
10902
  var removeRepoCommand;
10412
10903
  var init_remove_repo = __esm({
10413
10904
  "src/commands/remove-repo.ts"() {
10414
10905
  "use strict";
10415
10906
  init_modify();
10416
- removeRepoCommand = defineCommand23({
10907
+ removeRepoCommand = defineCommand24({
10417
10908
  meta: {
10418
10909
  name: "remove-repo",
10419
10910
  group: "edit",
@@ -10455,14 +10946,14 @@ var init_remove_repo = __esm({
10455
10946
  });
10456
10947
 
10457
10948
  // src/commands/remove-service.ts
10458
- import { defineCommand as defineCommand24 } from "citty";
10949
+ import { defineCommand as defineCommand25 } from "citty";
10459
10950
  import { consola as consola30 } from "consola";
10460
10951
  var removeServiceCommand;
10461
10952
  var init_remove_service = __esm({
10462
10953
  "src/commands/remove-service.ts"() {
10463
10954
  "use strict";
10464
10955
  init_modify();
10465
- removeServiceCommand = defineCommand24({
10956
+ removeServiceCommand = defineCommand25({
10466
10957
  meta: {
10467
10958
  name: "remove-service",
10468
10959
  group: "edit",
@@ -10604,7 +11095,7 @@ var init_run = __esm({
10604
11095
  });
10605
11096
 
10606
11097
  // src/commands/run.ts
10607
- import { defineCommand as defineCommand25 } from "citty";
11098
+ import { defineCommand as defineCommand26 } from "citty";
10608
11099
  import { consola as consola31 } from "consola";
10609
11100
  var runCommand;
10610
11101
  var init_run2 = __esm({
@@ -10613,7 +11104,7 @@ var init_run2 = __esm({
10613
11104
  init_paths();
10614
11105
  init_run();
10615
11106
  init_inner_args();
10616
- runCommand = defineCommand25({
11107
+ runCommand = defineCommand26({
10617
11108
  meta: {
10618
11109
  name: "run",
10619
11110
  group: "run",
@@ -10656,7 +11147,7 @@ var init_run2 = __esm({
10656
11147
  });
10657
11148
 
10658
11149
  // src/commands/shell.ts
10659
- import { defineCommand as defineCommand26 } from "citty";
11150
+ import { defineCommand as defineCommand27 } from "citty";
10660
11151
  import { consola as consola32 } from "consola";
10661
11152
  var shellCommand;
10662
11153
  var init_shell2 = __esm({
@@ -10664,7 +11155,7 @@ var init_shell2 = __esm({
10664
11155
  "use strict";
10665
11156
  init_paths();
10666
11157
  init_shell();
10667
- shellCommand = defineCommand26({
11158
+ shellCommand = defineCommand27({
10668
11159
  meta: {
10669
11160
  name: "shell",
10670
11161
  group: "run",
@@ -10694,7 +11185,7 @@ var init_shell2 = __esm({
10694
11185
  });
10695
11186
 
10696
11187
  // src/commands/start.ts
10697
- import { defineCommand as defineCommand27 } from "citty";
11188
+ import { defineCommand as defineCommand28 } from "citty";
10698
11189
  import { consola as consola33 } from "consola";
10699
11190
  var startCommand;
10700
11191
  var init_start = __esm({
@@ -10704,11 +11195,12 @@ var init_start = __esm({
10704
11195
  init_io();
10705
11196
  init_paths();
10706
11197
  init_compose();
11198
+ init_catalog();
10707
11199
  init_open();
10708
11200
  init_proxy();
10709
11201
  init_port_check();
10710
11202
  init_dispatch();
10711
- startCommand = defineCommand27({
11203
+ startCommand = defineCommand28({
10712
11204
  meta: {
10713
11205
  name: "start",
10714
11206
  group: "run",
@@ -10729,6 +11221,7 @@ var init_start = __esm({
10729
11221
  return dispatch(async () => {
10730
11222
  let needsProxy = false;
10731
11223
  let hostPort = 80;
11224
+ let deferred = [];
10732
11225
  try {
10733
11226
  const parsed = await readConfig(containerConfigPath(args.name));
10734
11227
  if ((parsed.config.routing?.ports ?? []).length > 0) {
@@ -10736,6 +11229,7 @@ var init_start = __esm({
10736
11229
  const global = await readMonocerosConfig();
10737
11230
  hostPort = proxyHostPort(global);
10738
11231
  }
11232
+ deferred = (parsed.config.services ?? []).filter((s) => serviceDefersStart(s.name)).map((s) => s.name);
10739
11233
  } catch (err) {
10740
11234
  consola33.warn(
10741
11235
  `Could not read container yml ahead of start: ${err instanceof Error ? err.message : String(err)}. Skipping Traefik pre-flight.`
@@ -10746,6 +11240,24 @@ var init_start = __esm({
10746
11240
  await ensureProxy({ hostPort });
10747
11241
  }
10748
11242
  const exitCode = await runStart({ root: containerDir(args.name) });
11243
+ if (exitCode === 0 && deferred.length > 0) {
11244
+ try {
11245
+ const deferExit = await startDeferredServices({
11246
+ root: containerDir(args.name),
11247
+ services: deferred,
11248
+ logger: consola33
11249
+ });
11250
+ if (deferExit !== 0) {
11251
+ consola33.warn(
11252
+ `Deferred service(s) ${deferred.join(", ")} did not start cleanly (exit ${deferExit}).`
11253
+ );
11254
+ }
11255
+ } catch (err) {
11256
+ consola33.warn(
11257
+ `Could not start deferred service(s) ${deferred.join(", ")}: ${err instanceof Error ? err.message : String(err)}`
11258
+ );
11259
+ }
11260
+ }
10749
11261
  if (args.open && exitCode === 0) {
10750
11262
  try {
10751
11263
  await runOpen({ name: args.name, tool: args.open });
@@ -10761,7 +11273,7 @@ var init_start = __esm({
10761
11273
  });
10762
11274
 
10763
11275
  // src/commands/status.ts
10764
- import { defineCommand as defineCommand28 } from "citty";
11276
+ import { defineCommand as defineCommand29 } from "citty";
10765
11277
  var statusCommand;
10766
11278
  var init_status = __esm({
10767
11279
  "src/commands/status.ts"() {
@@ -10769,7 +11281,7 @@ var init_status = __esm({
10769
11281
  init_paths();
10770
11282
  init_compose();
10771
11283
  init_dispatch();
10772
- statusCommand = defineCommand28({
11284
+ statusCommand = defineCommand29({
10773
11285
  meta: {
10774
11286
  name: "status",
10775
11287
  group: "run",
@@ -10799,7 +11311,7 @@ var init_status = __esm({
10799
11311
  });
10800
11312
 
10801
11313
  // src/commands/stop.ts
10802
- import { defineCommand as defineCommand29 } from "citty";
11314
+ import { defineCommand as defineCommand30 } from "citty";
10803
11315
  import { consola as consola34 } from "consola";
10804
11316
  var stopCommand;
10805
11317
  var init_stop = __esm({
@@ -10809,7 +11321,7 @@ var init_stop = __esm({
10809
11321
  init_compose();
10810
11322
  init_proxy();
10811
11323
  init_dispatch();
10812
- stopCommand = defineCommand29({
11324
+ stopCommand = defineCommand30({
10813
11325
  meta: {
10814
11326
  name: "stop",
10815
11327
  group: "run",
@@ -11102,7 +11614,7 @@ var init_port_check2 = __esm({
11102
11614
  });
11103
11615
 
11104
11616
  // src/tunnel/run.ts
11105
- import { spawn as spawn12 } from "child_process";
11617
+ import { spawn as spawn13 } from "child_process";
11106
11618
  import { consola as consola35 } from "consola";
11107
11619
  function signalNumber(signal) {
11108
11620
  switch (signal) {
@@ -11192,7 +11704,7 @@ var init_run3 = __esm({
11192
11704
  init_port_check2();
11193
11705
  SOCAT_IMAGE = "alpine/socat:1.8.0.3";
11194
11706
  defaultDockerSpawn = (args) => {
11195
- const child = spawn12("docker", args, {
11707
+ const child = spawn13("docker", args, {
11196
11708
  stdio: "inherit"
11197
11709
  });
11198
11710
  const exited = new Promise((resolve, reject) => {
@@ -11223,7 +11735,7 @@ var init_run3 = __esm({
11223
11735
  });
11224
11736
 
11225
11737
  // src/commands/tunnel.ts
11226
- import { defineCommand as defineCommand30 } from "citty";
11738
+ import { defineCommand as defineCommand31 } from "citty";
11227
11739
  import { consola as consola36 } from "consola";
11228
11740
  function parseLocalPort(raw) {
11229
11741
  if (raw === void 0) return void 0;
@@ -11240,7 +11752,7 @@ var init_tunnel = __esm({
11240
11752
  "src/commands/tunnel.ts"() {
11241
11753
  "use strict";
11242
11754
  init_run3();
11243
- tunnelCommand = defineCommand30({
11755
+ tunnelCommand = defineCommand31({
11244
11756
  meta: {
11245
11757
  name: "tunnel",
11246
11758
  group: "discovery",
@@ -11532,7 +12044,7 @@ var init_upgrade = __esm({
11532
12044
  });
11533
12045
 
11534
12046
  // src/commands/upgrade.ts
11535
- import { defineCommand as defineCommand31 } from "citty";
12047
+ import { defineCommand as defineCommand32 } from "citty";
11536
12048
  var upgradeCommand;
11537
12049
  var init_upgrade2 = __esm({
11538
12050
  "src/commands/upgrade.ts"() {
@@ -11540,7 +12052,7 @@ var init_upgrade2 = __esm({
11540
12052
  init_upgrade();
11541
12053
  init_version();
11542
12054
  init_dispatch();
11543
- upgradeCommand = defineCommand31({
12055
+ upgradeCommand = defineCommand32({
11544
12056
  meta: {
11545
12057
  name: "upgrade",
11546
12058
  group: "lifecycle",
@@ -11582,7 +12094,7 @@ var main_exports = {};
11582
12094
  __export(main_exports, {
11583
12095
  main: () => main
11584
12096
  });
11585
- import { defineCommand as defineCommand32 } from "citty";
12097
+ import { defineCommand as defineCommand33 } from "citty";
11586
12098
  var main;
11587
12099
  var init_main = __esm({
11588
12100
  "src/main.ts"() {
@@ -11597,6 +12109,7 @@ var init_main = __esm({
11597
12109
  init_apply2();
11598
12110
  init_completion();
11599
12111
  init_complete();
12112
+ init_update_check();
11600
12113
  init_init2();
11601
12114
  init_list_components();
11602
12115
  init_logs();
@@ -11619,7 +12132,7 @@ var init_main = __esm({
11619
12132
  init_tunnel();
11620
12133
  init_upgrade2();
11621
12134
  init_version();
11622
- main = defineCommand32({
12135
+ main = defineCommand33({
11623
12136
  meta: {
11624
12137
  name: "monoceros",
11625
12138
  version: CLI_VERSION,
@@ -11656,7 +12169,8 @@ var init_main = __esm({
11656
12169
  port: portCommand,
11657
12170
  tunnel: tunnelCommand,
11658
12171
  completion: completionCommand,
11659
- __complete: __completeCommand
12172
+ __complete: __completeCommand,
12173
+ "__update-check": __updateCheckCommand
11660
12174
  }
11661
12175
  });
11662
12176
  }
@@ -11946,9 +12460,15 @@ async function maybeRenderHelp(argv, main2) {
11946
12460
  // src/bin.ts
11947
12461
  init_inner_args();
11948
12462
  init_main();
12463
+ init_notifier();
12464
+ init_version();
11949
12465
  bootstrapDockerGroup();
11950
12466
  consumeInnerArgsFromProcessArgv();
11951
12467
  async function entry() {
12468
+ scheduleUpdateNotice({
12469
+ currentVersion: CLI_VERSION,
12470
+ commandName: process.argv.slice(2).find((a) => !a.startsWith("-"))
12471
+ });
11952
12472
  if (await maybeRenderHelp(process.argv.slice(2), main)) {
11953
12473
  return;
11954
12474
  }