@getmonoceros/workbench 1.38.19 → 1.39.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
@@ -479,6 +479,18 @@ function expandEnvRefs(env) {
479
479
  }
480
480
  return out;
481
481
  }
482
+ function mergeEnvLayers(...layers) {
483
+ const out = {};
484
+ for (const layer of layers) {
485
+ for (const [key, value] of Object.entries(layer)) {
486
+ const incomingEmpty = value.trim() === "";
487
+ const haveNonEmpty = out[key] !== void 0 && out[key].trim() !== "";
488
+ if (incomingEmpty && haveNonEmpty) continue;
489
+ out[key] = value;
490
+ }
491
+ }
492
+ return out;
493
+ }
482
494
  function interpolateServices(services, vars) {
483
495
  const missing = [];
484
496
  const resolved = services.map((svc) => {
@@ -1439,10 +1451,10 @@ function resolveRepoTokens(config, catalog, envVars) {
1439
1451
  async function resolveContainerRepoTokens(name, home, catalog) {
1440
1452
  const ymlPath = containerConfigPath(name, home);
1441
1453
  const { config } = parseConfig(await fs4.readFile(ymlPath, "utf8"), ymlPath);
1442
- const envVars = {
1443
- ...readEnvFile(globalEnvPath(home)),
1444
- ...readEnvFile(containerEnvPath(name, home))
1445
- };
1454
+ const envVars = mergeEnvLayers(
1455
+ readEnvFile(globalEnvPath(home)),
1456
+ readEnvFile(containerEnvPath(name, home))
1457
+ );
1446
1458
  return resolveRepoTokens(config, catalog, envVars);
1447
1459
  }
1448
1460
  function formatTokenUse(use) {
@@ -4452,6 +4464,9 @@ function generateAgentsMd(input) {
4452
4464
  lines.push(sub ? ` ${sub}` : "");
4453
4465
  }
4454
4466
  }
4467
+ for (const mountLine of formatServiceMounts(svc)) {
4468
+ lines.push(mountLine);
4469
+ }
4455
4470
  }
4456
4471
  lines.push("");
4457
4472
  const connEnv = serviceConnectionEnv(input.services);
@@ -4630,6 +4645,7 @@ function generateAgentsMd(input) {
4630
4645
  );
4631
4646
  lines.push("");
4632
4647
  const examplePort = input.ports.length > 0 ? String(input.ports[0]) : "<port>";
4648
+ const secondPort = input.ports.length > 1 ? String(input.ports[1]) : "<port>";
4633
4649
  lines.push("## Running a long-running server");
4634
4650
  lines.push("");
4635
4651
  lines.push(
@@ -4664,7 +4680,10 @@ function generateAgentsMd(input) {
4664
4680
  lines.push("{");
4665
4681
  lines.push(' "targets": [');
4666
4682
  lines.push(
4667
- ` { "name": "web", "command": "<the project's start command>", "port": ${examplePort}, "default": true }`
4683
+ ` { "name": "api", "command": "<the API's start command>", "port": ${examplePort}, "default": true },`
4684
+ );
4685
+ lines.push(
4686
+ ` { "name": "web", "command": "<the web start command>", "port": ${secondPort}, "default": true }`
4668
4687
  );
4669
4688
  lines.push(" ]");
4670
4689
  lines.push("}");
@@ -4703,6 +4722,17 @@ function generateAgentsMd(input) {
4703
4722
  "not started. Pass `--target <name>` to start or stop a single one."
4704
4723
  );
4705
4724
  lines.push("");
4725
+ lines.push(
4726
+ "When you add a server in a later session, revisit the existing",
4727
+ "`launch.json` instead of assuming its current `default` set is complete.",
4728
+ "If the new server belongs to the app that should come up together (a",
4729
+ "backend the frontend calls, a worker the app relies on), give it",
4730
+ '`"default": true` too and place its entry before whatever depends on it.',
4731
+ "A single pre-existing default entry does not mean later servers should",
4732
+ "stay non-default - most servers that make up the running app belong in",
4733
+ "the default set."
4734
+ );
4735
+ lines.push("");
4706
4736
  lines.push(
4707
4737
  "The server must listen on `0.0.0.0` (not `127.0.0.1`) on the exposed",
4708
4738
  "port, or Traefik cannot reach it. You only have the ports already",
@@ -4740,6 +4770,42 @@ function formatServiceLine(svc) {
4740
4770
  }
4741
4771
  return `- **${svc.name}** (custom image \`${svc.image}\`) \u2014 reachable at \`${reach}\``;
4742
4772
  }
4773
+ function formatServiceMounts(svc) {
4774
+ const byProject = /* @__PURE__ */ new Map();
4775
+ for (const spec of svc.volumes) {
4776
+ const mount = parseBindMount(spec);
4777
+ if (!mount) continue;
4778
+ const project = projectOf(mount.source);
4779
+ if (!project) continue;
4780
+ const readOnly = mount.mode === "ro" ? " (read-only)" : "";
4781
+ const bucket = byProject.get(project) ?? [];
4782
+ bucket.push(` - \`${mount.source}\` \u2192 \`${mount.target}\`${readOnly}`);
4783
+ byProject.set(project, bucket);
4784
+ }
4785
+ if (byProject.size === 0) return [];
4786
+ const out = [" Workspace mounts (edit these on the host, then re-apply):"];
4787
+ for (const [project, entries] of byProject) {
4788
+ out.push(` - ${project}:`);
4789
+ out.push(...entries);
4790
+ }
4791
+ return out;
4792
+ }
4793
+ function parseBindMount(spec) {
4794
+ const parts = spec.split(":");
4795
+ if (parts.length < 2) return null;
4796
+ const source = parts[0];
4797
+ let mode;
4798
+ const last = parts[parts.length - 1];
4799
+ if (parts.length >= 3 && !last.includes("/")) {
4800
+ mode = parts.pop();
4801
+ }
4802
+ return { source, target: parts.slice(1).join(":"), mode };
4803
+ }
4804
+ function projectOf(source) {
4805
+ const segments = source.split("/");
4806
+ if (segments[0] !== "projects" || segments.length < 2) return null;
4807
+ return segments[1] ?? null;
4808
+ }
4743
4809
  function agentsMdInputFromCreateOptions(opts, featureDisplayMap2, manifestLoader, hostPort = 80) {
4744
4810
  const features = [];
4745
4811
  for (const [ref, userOptions] of Object.entries(opts.features ?? {})) {
@@ -8355,11 +8421,13 @@ ${sectionLine(label)}
8355
8421
  warnOnDeprecatedFeatureRefs(parsed.config.features, globalConfig, logger);
8356
8422
  const envPath = containerEnvPath(opts.name, home);
8357
8423
  await ensureEnvGitignored(containerConfigsDir(home));
8358
- const envVars = expandEnvRefs({
8359
- ...readEnvFile(globalEnvPath(home)),
8360
- ...readEnvFile(envPath),
8361
- ...opts.env ?? {}
8362
- });
8424
+ const envVars = expandEnvRefs(
8425
+ mergeEnvLayers(
8426
+ readEnvFile(globalEnvPath(home)),
8427
+ readEnvFile(envPath),
8428
+ opts.env ?? {}
8429
+ )
8430
+ );
8363
8431
  const catalog = await loadComponentCatalog();
8364
8432
  const repoTokens = resolveRepoTokens(parsed.config, catalog, envVars);
8365
8433
  const featureTokenPrompt = opts.featureTokenPrompt ?? defaultFeatureTokenPrompt;
@@ -9070,7 +9138,7 @@ var CLI_VERSION;
9070
9138
  var init_version = __esm({
9071
9139
  "src/version.ts"() {
9072
9140
  "use strict";
9073
- CLI_VERSION = true ? "1.38.19" : "dev";
9141
+ CLI_VERSION = true ? "1.39.0" : "dev";
9074
9142
  }
9075
9143
  });
9076
9144
 
@@ -9145,164 +9213,6 @@ var init_apply2 = __esm({
9145
9213
  }
9146
9214
  });
9147
9215
 
9148
- // src/commands/completion.ts
9149
- import { defineCommand as defineCommand9 } from "citty";
9150
- function renderCompletionScript(shell) {
9151
- if (shell === "bash") {
9152
- return [
9153
- "# bash completion for monoceros",
9154
- "# install: source this file from .bashrc, e.g.",
9155
- "# monoceros completion bash > ~/.bash_completion.d/monoceros",
9156
- '# echo "source ~/.bash_completion.d/monoceros" >> ~/.bashrc',
9157
- "#",
9158
- "# The work is done by `monoceros __complete --line --point`; this",
9159
- "# shell wrapper only forwards the cursor view.",
9160
- "",
9161
- "_monoceros() {",
9162
- " local IFS=$'\\n'",
9163
- " local candidates",
9164
- ' candidates=$(monoceros __complete --line "$COMP_LINE" --point "$COMP_POINT" 2>/dev/null)',
9165
- ' local cur="${COMP_WORDS[COMP_CWORD]}"',
9166
- ' COMPREPLY=( $(compgen -W "$candidates" -- "$cur") )',
9167
- " # Suppress the trailing space when bash narrowed the candidate",
9168
- " # set to a single token that ends with `=` \u2014 those are value-",
9169
- " # flags (`--with-features=`, `--with-ports=`, \u2026) where the user types the",
9170
- " # value immediately after.",
9171
- ' if [[ ${#COMPREPLY[@]} -eq 1 && "${COMPREPLY[0]}" == *= ]]; then',
9172
- " compopt -o nospace",
9173
- " fi",
9174
- "}",
9175
- "complete -F _monoceros monoceros",
9176
- ""
9177
- ].join("\n");
9178
- }
9179
- if (shell === "pwsh") {
9180
- return [
9181
- "# PowerShell completion for monoceros",
9182
- "# install: dot-source this file from your $PROFILE, e.g.",
9183
- "# monoceros completion pwsh > $HOME/.config/monoceros/completion.ps1",
9184
- "# Add-Content $PROFILE '. $HOME/.config/monoceros/completion.ps1'",
9185
- "#",
9186
- "# The work is done by `monoceros __complete --line --point`; this",
9187
- "# shell wrapper only forwards the cursor view.",
9188
- "",
9189
- "Register-ArgumentCompleter -Native -CommandName monoceros -ScriptBlock {",
9190
- " param($wordToComplete, $commandAst, $cursorPosition)",
9191
- " $line = $commandAst.Extent.Text",
9192
- " $point = $cursorPosition - $commandAst.Extent.StartOffset",
9193
- " if ($point -lt 0) { $point = 0 }",
9194
- " $raw = & monoceros __complete --line $line --point $point 2>$null",
9195
- " if (-not $raw) { return }",
9196
- ' $raw -split "`n" |',
9197
- ' Where-Object { $_.Length -gt 0 -and $_ -like "$wordToComplete*" } |',
9198
- ' ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, "ParameterValue", $_) }',
9199
- "}",
9200
- ""
9201
- ].join("\n");
9202
- }
9203
- return [
9204
- "#compdef monoceros",
9205
- "# zsh completion for monoceros",
9206
- "# install: drop this file somewhere on your $fpath as `_monoceros`,",
9207
- "# then start a new shell (or run `compinit`). Example:",
9208
- '# monoceros completion zsh > "${fpath[1]}/_monoceros"',
9209
- "#",
9210
- "# The work is done by `monoceros __complete --line --point`; this",
9211
- "# shell wrapper only forwards the cursor view.",
9212
- "",
9213
- "_monoceros() {",
9214
- ' local line="$BUFFER"',
9215
- ' local point="$CURSOR"',
9216
- " local -a candidates with_eq without_eq",
9217
- ' candidates=("${(@f)$(monoceros __complete --line "$line" --point "$point" 2>/dev/null)}")',
9218
- ' candidates=("${(@)candidates:#}")',
9219
- ' # Split candidates into "ends with `=`" (value-flags \u2014 no suffix',
9220
- " # space wanted because the user types the value immediately) and",
9221
- ' # "everything else" (positional values, boolean flags \u2014 default',
9222
- " # space behaviour).",
9223
- ' for cand in "${candidates[@]}"; do',
9224
- ' if [[ "$cand" == *= ]]; then',
9225
- ' with_eq+=("$cand")',
9226
- " else",
9227
- ' without_eq+=("$cand")',
9228
- " fi",
9229
- " done",
9230
- " (( ${#with_eq[@]} )) && compadd -S '' -- \"${with_eq[@]}\"",
9231
- ' (( ${#without_eq[@]} )) && compadd -- "${without_eq[@]}"',
9232
- "}",
9233
- "",
9234
- '_monoceros "$@"',
9235
- ""
9236
- ].join("\n");
9237
- }
9238
- var SHELLS, completionCommand;
9239
- var init_completion = __esm({
9240
- "src/commands/completion.ts"() {
9241
- "use strict";
9242
- SHELLS = ["bash", "zsh", "pwsh"];
9243
- completionCommand = defineCommand9({
9244
- meta: {
9245
- name: "completion",
9246
- group: "tooling",
9247
- // Hidden from `monoceros --help`: the install scripts wire up
9248
- // completion automatically; manual setup is documented at
9249
- // getmonoceros.build/docs/reference/utilities/completion. Still runnable directly.
9250
- hidden: true,
9251
- 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."
9252
- },
9253
- args: {
9254
- shell: {
9255
- type: "positional",
9256
- description: "Target shell. One of: 'bash', 'zsh', 'pwsh'.",
9257
- required: true
9258
- }
9259
- },
9260
- run({ args }) {
9261
- const shell = args.shell;
9262
- if (shell !== "bash" && shell !== "zsh" && shell !== "pwsh") {
9263
- process.stderr.write(
9264
- `Unknown shell: ${JSON.stringify(shell)}. Supported: ${SHELLS.join(", ")}.
9265
- `
9266
- );
9267
- process.exit(2);
9268
- }
9269
- process.stdout.write(renderCompletionScript(shell));
9270
- }
9271
- });
9272
- }
9273
- });
9274
-
9275
- // src/commands/__bridge.ts
9276
- import { defineCommand as defineCommand10 } from "citty";
9277
- var __bridgeCommand;
9278
- var init_bridge = __esm({
9279
- "src/commands/__bridge.ts"() {
9280
- "use strict";
9281
- init_bridge_daemon();
9282
- __bridgeCommand = defineCommand10({
9283
- meta: {
9284
- name: "__bridge",
9285
- group: "internal",
9286
- hidden: true,
9287
- description: "Internal: host-side browser-bridge daemon (background)."
9288
- },
9289
- args: {
9290
- root: {
9291
- type: "positional",
9292
- description: "Materialized container root ($MONOCEROS_HOME/container/<name>/).",
9293
- required: true
9294
- }
9295
- },
9296
- async run({ args }) {
9297
- try {
9298
- await runBridgeDaemon({ root: args.root });
9299
- } catch {
9300
- }
9301
- }
9302
- });
9303
- }
9304
- });
9305
-
9306
9216
  // src/config/launch-config.ts
9307
9217
  import { promises as fs16 } from "fs";
9308
9218
  import path25 from "path";
@@ -9501,6 +9411,14 @@ function tokenize(text) {
9501
9411
  function isShellWhitespace(ch) {
9502
9412
  return ch === " " || ch === " ";
9503
9413
  }
9414
+ function staticSource(fn) {
9415
+ pwshMeta.set(fn, { static: true });
9416
+ return fn;
9417
+ }
9418
+ function dynamicSource(kind, fn) {
9419
+ pwshMeta.set(fn, { static: false, kind });
9420
+ return fn;
9421
+ }
9504
9422
  function dispatchCommand(spec, argTokens, ctx) {
9505
9423
  const dashDashIdx = argTokens.indexOf("--");
9506
9424
  const preDash = dashDashIdx < 0 ? argTokens : argTokens.slice(0, dashDashIdx);
@@ -9747,7 +9665,36 @@ async function resolveFeatureRefForCompletion(token) {
9747
9665
  const f = c.file.contributes.features?.[0];
9748
9666
  return f?.ref;
9749
9667
  }
9750
- var WORKSPACE_DIR_SKIP, PROJECTS_DIR_MAX_DEPTH, ALL_COMMANDS, containerName, COMMAND_SPECS, COMPLETION_COMMAND_SPEC_KEYS;
9668
+ async function describeSource(src) {
9669
+ if (!src) return {};
9670
+ const meta = pwshMeta.get(src);
9671
+ if (!meta) return {};
9672
+ if (meta.static) return { values: await src(EMPTY_CTX) };
9673
+ return { kind: meta.kind };
9674
+ }
9675
+ async function buildPwshCompletionModel() {
9676
+ const specs = {};
9677
+ for (const [name, spec] of Object.entries(COMMAND_SPECS)) {
9678
+ const posSources = spec.positionals ?? [];
9679
+ const positionalCount = spec.positionalCount ?? posSources.length;
9680
+ const positionals = [];
9681
+ for (const src of posSources) positionals.push(await describeSource(src));
9682
+ const flags = {};
9683
+ for (const [flag, fspec] of Object.entries(spec.flags ?? {})) {
9684
+ const entry2 = {
9685
+ type: fspec.type,
9686
+ aliases: fspec.aliases ?? []
9687
+ };
9688
+ if (fspec.type === "value" && fspec.values) {
9689
+ entry2.value = await describeSource(fspec.values);
9690
+ }
9691
+ flags[flag] = entry2;
9692
+ }
9693
+ specs[name] = { positionals, positionalCount, flags };
9694
+ }
9695
+ return { commands: [...ALL_COMMANDS], specs };
9696
+ }
9697
+ var pwshMeta, WORKSPACE_DIR_SKIP, PROJECTS_DIR_MAX_DEPTH, ALL_COMMANDS, containerName, appCandidates, appOrServiceCandidates, targetCandidates, runInDirs, languageValues, serviceValues, featureValues, providerValues, shellValues, openToolValues, COMMAND_SPECS, COMPLETION_COMMAND_SPEC_KEYS, EMPTY_CTX;
9751
9698
  var init_resolve = __esm({
9752
9699
  "src/completion/resolve.ts"() {
9753
9700
  "use strict";
@@ -9760,6 +9707,7 @@ var init_resolve = __esm({
9760
9707
  init_catalog();
9761
9708
  init_schema();
9762
9709
  init_open();
9710
+ pwshMeta = /* @__PURE__ */ new WeakMap();
9763
9711
  WORKSPACE_DIR_SKIP = /* @__PURE__ */ new Set(["node_modules"]);
9764
9712
  PROJECTS_DIR_MAX_DEPTH = 4;
9765
9713
  ALL_COMMANDS = [
@@ -9796,7 +9744,26 @@ var init_resolve = __esm({
9796
9744
  "share",
9797
9745
  "completion"
9798
9746
  ];
9799
- containerName = (ctx) => listContainerNames(ctx);
9747
+ containerName = dynamicSource(
9748
+ "containerName",
9749
+ (ctx) => listContainerNames(ctx)
9750
+ );
9751
+ appCandidates = dynamicSource("app", (ctx) => listAppCandidates(ctx));
9752
+ appOrServiceCandidates = dynamicSource(
9753
+ "appOrService",
9754
+ (ctx) => listAppOrServiceCandidates(ctx)
9755
+ );
9756
+ targetCandidates = dynamicSource(
9757
+ "target",
9758
+ (ctx) => listTargetCandidates(ctx)
9759
+ );
9760
+ runInDirs = dynamicSource("runInDir", (ctx) => listRunInDirs(ctx));
9761
+ languageValues = staticSource(() => listLanguageNames());
9762
+ serviceValues = staticSource(() => listServiceNames());
9763
+ featureValues = staticSource(() => listFeatureComponents());
9764
+ providerValues = staticSource(() => listProviders());
9765
+ shellValues = staticSource(() => listShellNames());
9766
+ openToolValues = staticSource(() => [...OPEN_TOOLS]);
9800
9767
  COMMAND_SPECS = {
9801
9768
  init: {
9802
9769
  // First positional is a FRESH name → no suggestion source, but
@@ -9805,12 +9772,9 @@ var init_resolve = __esm({
9805
9772
  // flag suggestions.
9806
9773
  positionalCount: 1,
9807
9774
  flags: {
9808
- "--with-languages": { type: "value", values: () => listLanguageNames() },
9809
- "--with-features": {
9810
- type: "value",
9811
- values: () => listFeatureComponents()
9812
- },
9813
- "--with-services": { type: "value", values: () => listServiceNames() },
9775
+ "--with-languages": { type: "value", values: languageValues },
9776
+ "--with-features": { type: "value", values: featureValues },
9777
+ "--with-services": { type: "value", values: serviceValues },
9814
9778
  "--with-apt-packages": { type: "value" },
9815
9779
  "--with-repos": { type: "value" },
9816
9780
  "--with-ports": { type: "value" }
@@ -9820,7 +9784,7 @@ var init_resolve = __esm({
9820
9784
  positionals: [containerName],
9821
9785
  flags: {
9822
9786
  "--yes": { type: "boolean", aliases: ["-y"] },
9823
- "--open": { type: "value", values: () => [...OPEN_TOOLS] }
9787
+ "--open": { type: "value", values: openToolValues }
9824
9788
  }
9825
9789
  },
9826
9790
  upgrade: {
@@ -9837,39 +9801,39 @@ var init_resolve = __esm({
9837
9801
  }
9838
9802
  },
9839
9803
  shell: { positionals: [containerName] },
9840
- open: { positionals: [containerName, () => [...OPEN_TOOLS]] },
9804
+ open: { positionals: [containerName, openToolValues] },
9841
9805
  run: {
9842
9806
  positionals: [containerName],
9843
- flags: { "--in": { type: "value", values: (ctx) => listRunInDirs(ctx) } }
9807
+ flags: { "--in": { type: "value", values: runInDirs } }
9844
9808
  },
9845
9809
  logs: {
9846
- positionals: [containerName, (ctx) => listAppCandidates(ctx)],
9810
+ positionals: [containerName, appCandidates],
9847
9811
  flags: {
9848
- "--target": { type: "value", values: (ctx) => listTargetCandidates(ctx) }
9812
+ "--target": { type: "value", values: targetCandidates }
9849
9813
  }
9850
9814
  },
9851
9815
  start: {
9852
- positionals: [containerName, (ctx) => listAppCandidates(ctx)],
9816
+ positionals: [containerName, appCandidates],
9853
9817
  flags: {
9854
- "--target": { type: "value", values: (ctx) => listTargetCandidates(ctx) },
9855
- "--open": { type: "value", values: () => [...OPEN_TOOLS] }
9818
+ "--target": { type: "value", values: targetCandidates },
9819
+ "--open": { type: "value", values: openToolValues }
9856
9820
  }
9857
9821
  },
9858
9822
  stop: {
9859
- positionals: [containerName, (ctx) => listAppCandidates(ctx)],
9823
+ positionals: [containerName, appCandidates],
9860
9824
  flags: {
9861
- "--target": { type: "value", values: (ctx) => listTargetCandidates(ctx) }
9825
+ "--target": { type: "value", values: targetCandidates }
9862
9826
  }
9863
9827
  },
9864
9828
  status: {
9865
- positionals: [containerName, (ctx) => listAppOrServiceCandidates(ctx)]
9829
+ positionals: [containerName, appOrServiceCandidates]
9866
9830
  },
9867
9831
  "list-apps": { positionals: [containerName] },
9868
9832
  "add-language": {
9869
- positionals: [containerName, () => listLanguageNames()]
9833
+ positionals: [containerName, languageValues]
9870
9834
  },
9871
9835
  "add-service": {
9872
- positionals: [containerName, () => listServiceNames()]
9836
+ positionals: [containerName, serviceValues]
9873
9837
  },
9874
9838
  "add-apt-packages": {
9875
9839
  positionals: [containerName],
@@ -9877,7 +9841,7 @@ var init_resolve = __esm({
9877
9841
  // freeform package names — no useful suggestion list
9878
9842
  },
9879
9843
  "add-feature": {
9880
- positionals: [containerName, () => listFeatureComponents()],
9844
+ positionals: [containerName, featureValues],
9881
9845
  flags: { "--yes": { type: "boolean", aliases: ["-y"] } },
9882
9846
  innerArgs: (ctx) => listFeatureOptionInnerArgs(ctx)
9883
9847
  },
@@ -9888,7 +9852,7 @@ var init_resolve = __esm({
9888
9852
  "--path": { type: "value" },
9889
9853
  "--git-name": { type: "value" },
9890
9854
  "--git-email": { type: "value" },
9891
- "--provider": { type: "value", values: () => listProviders() },
9855
+ "--provider": { type: "value", values: providerValues },
9892
9856
  "--yes": { type: "boolean", aliases: ["-y"] }
9893
9857
  }
9894
9858
  },
@@ -9901,17 +9865,17 @@ var init_resolve = __esm({
9901
9865
  innerArgs: () => []
9902
9866
  },
9903
9867
  "remove-language": {
9904
- positionals: [containerName, () => listLanguageNames()]
9868
+ positionals: [containerName, languageValues]
9905
9869
  },
9906
9870
  "remove-service": {
9907
- positionals: [containerName, () => listServiceNames()]
9871
+ positionals: [containerName, serviceValues]
9908
9872
  },
9909
9873
  "remove-apt-packages": {
9910
9874
  positionals: [containerName],
9911
9875
  innerArgs: () => []
9912
9876
  },
9913
9877
  "remove-feature": {
9914
- positionals: [containerName, () => listFeatureComponents()],
9878
+ positionals: [containerName, featureValues],
9915
9879
  flags: { "--yes": { type: "boolean", aliases: ["-y"] } }
9916
9880
  },
9917
9881
  "remove-from-url": { positionals: [containerName] },
@@ -9927,17 +9891,17 @@ var init_resolve = __esm({
9927
9891
  innerArgs: () => []
9928
9892
  },
9929
9893
  tunnel: {
9930
- positionals: [containerName, () => listServiceNames()],
9894
+ positionals: [containerName, serviceValues],
9931
9895
  flags: {
9932
9896
  "--local-port": { type: "value" },
9933
9897
  "--local-address": { type: "value" }
9934
9898
  }
9935
9899
  },
9936
9900
  share: {
9937
- positionals: [containerName, (ctx) => listAppCandidates(ctx)]
9901
+ positionals: [containerName, appCandidates]
9938
9902
  },
9939
9903
  completion: {
9940
- positionals: [() => listShellNames()]
9904
+ positionals: [shellValues]
9941
9905
  },
9942
9906
  "list-components": {},
9943
9907
  restore: {
@@ -9948,6 +9912,401 @@ var init_resolve = __esm({
9948
9912
  }
9949
9913
  };
9950
9914
  COMPLETION_COMMAND_SPEC_KEYS = Object.keys(COMMAND_SPECS);
9915
+ EMPTY_CTX = { prev: [], current: "", opts: {} };
9916
+ }
9917
+ });
9918
+
9919
+ // src/commands/completion.ts
9920
+ import { defineCommand as defineCommand9 } from "citty";
9921
+ function renderBashScript() {
9922
+ return [
9923
+ "# bash completion for monoceros",
9924
+ "# install: source this file from .bashrc, e.g.",
9925
+ "# monoceros completion bash > ~/.bash_completion.d/monoceros",
9926
+ '# echo "source ~/.bash_completion.d/monoceros" >> ~/.bashrc',
9927
+ "#",
9928
+ "# The work is done by `monoceros __complete --line --point`; this",
9929
+ "# shell wrapper only forwards the cursor view.",
9930
+ "",
9931
+ "_monoceros() {",
9932
+ " local IFS=$'\\n'",
9933
+ " local candidates",
9934
+ ' candidates=$(monoceros __complete --line "$COMP_LINE" --point "$COMP_POINT" 2>/dev/null)',
9935
+ ' local cur="${COMP_WORDS[COMP_CWORD]}"',
9936
+ ' COMPREPLY=( $(compgen -W "$candidates" -- "$cur") )',
9937
+ " # Suppress the trailing space when bash narrowed the candidate",
9938
+ " # set to a single token that ends with `=` \u2014 those are value-",
9939
+ " # flags (`--with-features=`, `--with-ports=`, \u2026) where the user types the",
9940
+ " # value immediately after.",
9941
+ ' if [[ ${#COMPREPLY[@]} -eq 1 && "${COMPREPLY[0]}" == *= ]]; then',
9942
+ " compopt -o nospace",
9943
+ " fi",
9944
+ "}",
9945
+ "complete -F _monoceros monoceros",
9946
+ ""
9947
+ ].join("\n");
9948
+ }
9949
+ function renderZshScript() {
9950
+ return [
9951
+ "#compdef monoceros",
9952
+ "# zsh completion for monoceros",
9953
+ "# install: drop this file somewhere on your $fpath as `_monoceros`,",
9954
+ "# then start a new shell (or run `compinit`). Example:",
9955
+ '# monoceros completion zsh > "${fpath[1]}/_monoceros"',
9956
+ "#",
9957
+ "# The work is done by `monoceros __complete --line --point`; this",
9958
+ "# shell wrapper only forwards the cursor view.",
9959
+ "",
9960
+ "_monoceros() {",
9961
+ ' local line="$BUFFER"',
9962
+ ' local point="$CURSOR"',
9963
+ " local -a candidates with_eq without_eq",
9964
+ ' candidates=("${(@f)$(monoceros __complete --line "$line" --point "$point" 2>/dev/null)}")',
9965
+ ' candidates=("${(@)candidates:#}")',
9966
+ ' # Split candidates into "ends with `=`" (value-flags \u2014 no suffix',
9967
+ " # space wanted because the user types the value immediately) and",
9968
+ ' # "everything else" (positional values, boolean flags \u2014 default',
9969
+ " # space behaviour).",
9970
+ ' for cand in "${candidates[@]}"; do',
9971
+ ' if [[ "$cand" == *= ]]; then',
9972
+ ' with_eq+=("$cand")',
9973
+ " else",
9974
+ ' without_eq+=("$cand")',
9975
+ " fi",
9976
+ " done",
9977
+ " (( ${#with_eq[@]} )) && compadd -S '' -- \"${with_eq[@]}\"",
9978
+ ' (( ${#without_eq[@]} )) && compadd -- "${without_eq[@]}"',
9979
+ "}",
9980
+ "",
9981
+ '_monoceros "$@"',
9982
+ ""
9983
+ ].join("\n");
9984
+ }
9985
+ async function renderPwshScript() {
9986
+ const model = await buildPwshCompletionModel();
9987
+ const json = JSON.stringify(model);
9988
+ return `# PowerShell completion for monoceros (self-contained).
9989
+ # install: dot-source this file from your $PROFILE, e.g.
9990
+ # monoceros completion pwsh > $HOME/.config/monoceros/completion.ps1
9991
+ # Add-Content $PROFILE '. $HOME/.config/monoceros/completion.ps1'
9992
+ #
9993
+ # Unlike the bash/zsh wrappers this never calls back into the CLI per
9994
+ # Tab (on Windows that would be a WSL round-trip each keystroke).
9995
+ # The static candidates are baked in below; the dynamic ones (container
9996
+ # names, apps, workspace dirs, launch targets) are read straight off the
9997
+ # host filesystem via the %USERPROFILE%\\.monoceros symlink.
9998
+
9999
+ $global:MonocerosModel = @'
10000
+ ${json}
10001
+ '@ | ConvertFrom-Json
10002
+
10003
+ function __Monoceros_Home {
10004
+ if ($env:MONOCEROS_HOME) { return $env:MONOCEROS_HOME }
10005
+ return (Join-Path $env:USERPROFILE '.monoceros')
10006
+ }
10007
+
10008
+ function __Monoceros_ContainerNames {
10009
+ $dir = Join-Path (__Monoceros_Home) 'container-configs'
10010
+ if (-not (Test-Path -LiteralPath $dir)) { return @() }
10011
+ @(Get-ChildItem -LiteralPath $dir -Filter '*.yml' -File -ErrorAction SilentlyContinue |
10012
+ ForEach-Object { $_.BaseName } | Sort-Object)
10013
+ }
10014
+
10015
+ # Recursively collect directory paths under $at, relative to it, capped
10016
+ # at $maxDepth. Skips dot-dirs, node_modules, and symlinked dirs \u2014 the
10017
+ # host-side mirror of collectDirs() in completion/resolve.ts.
10018
+ function __Monoceros_CollectDirs($maxDepth, $acc, $at, $rel, $depth) {
10019
+ $entries = Get-ChildItem -LiteralPath $at -Directory -Force -ErrorAction SilentlyContinue
10020
+ foreach ($e in $entries) {
10021
+ if ($e.Name.StartsWith('.')) { continue }
10022
+ if ($e.Name -eq 'node_modules') { continue }
10023
+ if ($e.LinkType) { continue }
10024
+ $childRel = if ($rel) { "$rel/$($e.Name)" } else { $e.Name }
10025
+ [void]$acc.Add($childRel)
10026
+ if (($depth + 1) -lt $maxDepth) {
10027
+ __Monoceros_CollectDirs $maxDepth $acc $e.FullName $childRel ($depth + 1)
10028
+ }
10029
+ }
10030
+ }
10031
+
10032
+ function __Monoceros_WorkspaceDirs($name) {
10033
+ if (-not $name) { return @() }
10034
+ $root = Join-Path (Join-Path (__Monoceros_Home) 'container') $name
10035
+ if (-not (Test-Path -LiteralPath $root)) { return @() }
10036
+ $acc = New-Object System.Collections.Generic.List[string]
10037
+ __Monoceros_CollectDirs 1 $acc $root '' 0
10038
+ $projects = Join-Path $root 'projects'
10039
+ if (Test-Path -LiteralPath $projects) {
10040
+ $pacc = New-Object System.Collections.Generic.List[string]
10041
+ __Monoceros_CollectDirs 4 $pacc $projects '' 0
10042
+ foreach ($p in $pacc) { [void]$acc.Add("projects/$p") }
10043
+ }
10044
+ @($acc | Sort-Object -Unique)
10045
+ }
10046
+
10047
+ # App-relative paths under projects/ carrying .monoceros/launch.json.
10048
+ function __Monoceros_WalkApps($at, $rel, $depth, $acc) {
10049
+ if ($rel -and (Test-Path -LiteralPath (Join-Path (Join-Path $at '.monoceros') 'launch.json'))) {
10050
+ [void]$acc.Add($rel)
10051
+ }
10052
+ if ($depth -ge 4) { return }
10053
+ $entries = Get-ChildItem -LiteralPath $at -Directory -Force -ErrorAction SilentlyContinue
10054
+ foreach ($e in $entries) {
10055
+ if ($e.Name.StartsWith('.')) { continue }
10056
+ $childRel = if ($rel) { "$rel/$($e.Name)" } else { $e.Name }
10057
+ __Monoceros_WalkApps $e.FullName $childRel ($depth + 1) $acc
10058
+ }
10059
+ }
10060
+
10061
+ function __Monoceros_Apps($name) {
10062
+ if (-not $name) { return @() }
10063
+ $root = Join-Path (Join-Path (Join-Path (__Monoceros_Home) 'container') $name) 'projects'
10064
+ if (-not (Test-Path -LiteralPath $root)) { return @() }
10065
+ $acc = New-Object System.Collections.Generic.List[string]
10066
+ __Monoceros_WalkApps $root '' 0 $acc
10067
+ @($acc | Sort-Object)
10068
+ }
10069
+
10070
+ function __Monoceros_Targets($name, $app) {
10071
+ if (-not $name -or -not $app) { return @() }
10072
+ $appPath = $app -replace '/', '\\'
10073
+ $file = Join-Path (Join-Path (Join-Path (Join-Path (__Monoceros_Home) 'container') $name) 'projects') (Join-Path $appPath (Join-Path '.monoceros' 'launch.json'))
10074
+ if (-not (Test-Path -LiteralPath $file)) { return @() }
10075
+ try { $json = Get-Content -LiteralPath $file -Raw | ConvertFrom-Json } catch { return @() }
10076
+ $list = if ($json.targets) { $json.targets } elseif ($json.configurations) { $json.configurations } else { $null }
10077
+ if (-not $list) { return @() }
10078
+ @($list | ForEach-Object { $_.name } | Where-Object { $_ })
10079
+ }
10080
+
10081
+ # The positional tokens (skipping flags) after \`monoceros <cmd>\`.
10082
+ function __Monoceros_Positionals($argTokens) {
10083
+ $out = @()
10084
+ foreach ($t in $argTokens) {
10085
+ if ($t -eq '--') { break }
10086
+ if ($t.StartsWith('-')) { continue }
10087
+ $out += $t
10088
+ }
10089
+ ,$out
10090
+ }
10091
+
10092
+ # Resolve a value descriptor ({values:[...]} static, or {kind:...}
10093
+ # dynamic) to its candidate list.
10094
+ function __Monoceros_Values($desc, $argTokens) {
10095
+ if (-not $desc) { return @() }
10096
+ if ($null -ne $desc.values) { return @($desc.values) }
10097
+ if ($desc.kind) {
10098
+ $pos = __Monoceros_Positionals $argTokens
10099
+ $name = if ($pos.Count -gt 0) { $pos[0] } else { $null }
10100
+ $app = if ($pos.Count -gt 1) { $pos[1] } else { $null }
10101
+ switch ($desc.kind) {
10102
+ 'containerName' { return (__Monoceros_ContainerNames) }
10103
+ 'app' { return (__Monoceros_Apps $name) }
10104
+ 'appOrService' { return (__Monoceros_Apps $name) }
10105
+ 'runInDir' { return (__Monoceros_WorkspaceDirs $name) }
10106
+ 'target' { return (__Monoceros_Targets $name $app) }
10107
+ }
10108
+ }
10109
+ return @()
10110
+ }
10111
+
10112
+ # Comma-aware prefix filter \u2014 mirrors resolveValues() so
10113
+ # \`--with-features=a,b<TAB>\` completes after the last comma.
10114
+ function __Monoceros_Filter($values, $fragment) {
10115
+ if (-not $values) { return @() }
10116
+ $commaIdx = $fragment.LastIndexOf(',')
10117
+ if ($commaIdx -lt 0) {
10118
+ return @($values | Where-Object { $_.StartsWith($fragment) })
10119
+ }
10120
+ $prefix = $fragment.Substring(0, $commaIdx + 1)
10121
+ $tail = $fragment.Substring($commaIdx + 1)
10122
+ @($values | Where-Object { $_.StartsWith($tail) } | ForEach-Object { "$prefix$_" })
10123
+ }
10124
+
10125
+ function __Monoceros_LookupFlag($flags, $token) {
10126
+ if (-not $flags) { return $null }
10127
+ $direct = $flags.PSObject.Properties[$token]
10128
+ if ($direct) { return $direct.Value }
10129
+ foreach ($p in $flags.PSObject.Properties) {
10130
+ if ($p.Value.aliases -contains $token) { return $p.Value }
10131
+ }
10132
+ return $null
10133
+ }
10134
+
10135
+ # Value flags are offered with a trailing '=' (so no space is wanted
10136
+ # after Tab); boolean flags and aliases as bare names.
10137
+ function __Monoceros_FlagNames($flags, $fragment) {
10138
+ if (-not $flags) { return @() }
10139
+ $names = @()
10140
+ foreach ($p in $flags.PSObject.Properties) {
10141
+ if ($p.Value.type -eq 'value') { $names += "$($p.Name)=" } else { $names += $p.Name }
10142
+ foreach ($a in $p.Value.aliases) { $names += $a }
10143
+ }
10144
+ @($names | Where-Object { $_.StartsWith($fragment) })
10145
+ }
10146
+
10147
+ function __Monoceros_CountPositionals($argTokens, $flags) {
10148
+ $count = 0
10149
+ for ($i = 0; $i -lt $argTokens.Count; $i++) {
10150
+ $t = $argTokens[$i]
10151
+ if ($t -like '--*' -and $t.Contains('=')) { continue }
10152
+ if ($t.StartsWith('-')) {
10153
+ $flag = __Monoceros_LookupFlag $flags $t
10154
+ if ($flag -and $flag.type -eq 'value' -and ($i + 1) -lt $argTokens.Count) { $i++ }
10155
+ continue
10156
+ }
10157
+ $count++
10158
+ }
10159
+ $count
10160
+ }
10161
+
10162
+ function __Monoceros_Emit($candidates) {
10163
+ @($candidates | ForEach-Object {
10164
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
10165
+ })
10166
+ }
10167
+
10168
+ Register-ArgumentCompleter -Native -CommandName @('monoceros', 'monoceros.cmd', 'monoceros.exe') -ScriptBlock {
10169
+ param($wordToComplete, $commandAst, $cursorPosition)
10170
+
10171
+ $model = $global:MonocerosModel
10172
+ $current = if ($wordToComplete) { $wordToComplete } else { '' }
10173
+
10174
+ # Tokens already complete on the line. When the current word is
10175
+ # non-empty it is the last command element \u2014 drop it from "prev".
10176
+ $elements = @($commandAst.CommandElements | ForEach-Object { $_.Extent.Text })
10177
+ if ($current -ne '' -and $elements.Count -gt 0 -and $elements[-1] -eq $current) {
10178
+ $prev = if ($elements.Count -gt 1) { @($elements[0..($elements.Count - 2)]) } else { @() }
10179
+ } else {
10180
+ $prev = $elements
10181
+ }
10182
+ # Strip the program name (prev[0]).
10183
+ $tokens = if ($prev.Count -gt 1) { @($prev[1..($prev.Count - 1)]) } else { @() }
10184
+
10185
+ # No subcommand yet \u2192 complete the subcommand list.
10186
+ if ($tokens.Count -eq 0) {
10187
+ return (__Monoceros_Emit (@($model.commands | Where-Object { $_.StartsWith($current) })))
10188
+ }
10189
+
10190
+ $command = $tokens[0]
10191
+ $spec = $model.specs.PSObject.Properties[$command]
10192
+ if (-not $spec) { return }
10193
+ $spec = $spec.Value
10194
+ $argTokens = if ($tokens.Count -gt 1) { @($tokens[1..($tokens.Count - 1)]) } else { @() }
10195
+
10196
+ # Everything after \`--\` is the inner command \u2014 not ours to complete.
10197
+ if ($argTokens -contains '--') { return }
10198
+
10199
+ # Case A: current is \`--flag=fragment\` \u2192 that flag's values.
10200
+ if ($current -like '--*' -and $current.Contains('=')) {
10201
+ $eq = $current.IndexOf('=')
10202
+ $flagName = $current.Substring(0, $eq)
10203
+ $frag = $current.Substring($eq + 1)
10204
+ $flag = if ($spec.flags) { $spec.flags.PSObject.Properties[$flagName] } else { $null }
10205
+ if (-not $flag -or $flag.Value.type -ne 'value') { return }
10206
+ $vals = __Monoceros_Values $flag.Value.value $argTokens
10207
+ return (__Monoceros_Emit (@(__Monoceros_Filter $vals $frag | ForEach-Object { "$flagName=$_" })))
10208
+ }
10209
+
10210
+ # Case B: current is an incomplete flag name.
10211
+ if ($current.StartsWith('-')) {
10212
+ return (__Monoceros_Emit (__Monoceros_FlagNames $spec.flags $current))
10213
+ }
10214
+
10215
+ # Case C: previous token was a value flag expecting a value.
10216
+ $lastPrev = if ($argTokens.Count -gt 0) { $argTokens[-1] } else { $null }
10217
+ if ($lastPrev -and $lastPrev -like '--*' -and -not $lastPrev.Contains('=')) {
10218
+ $flag = if ($spec.flags) { $spec.flags.PSObject.Properties[$lastPrev] } else { $null }
10219
+ if ($flag -and $flag.Value.type -eq 'value' -and $flag.Value.value) {
10220
+ $vals = __Monoceros_Values $flag.Value.value $argTokens
10221
+ return (__Monoceros_Emit (__Monoceros_Filter $vals $current))
10222
+ }
10223
+ }
10224
+
10225
+ # Case D: a positional slot.
10226
+ $positionalIdx = __Monoceros_CountPositionals $argTokens $spec.flags
10227
+ $positionals = @($spec.positionals)
10228
+ $expected = $spec.positionalCount
10229
+ if ($positionalIdx -lt $positionals.Count) {
10230
+ $vals = __Monoceros_Values $positionals[$positionalIdx] $argTokens
10231
+ return (__Monoceros_Emit (__Monoceros_Filter $vals $current))
10232
+ }
10233
+ if ($positionalIdx -ge $expected) {
10234
+ return (__Monoceros_Emit (__Monoceros_FlagNames $spec.flags $current))
10235
+ }
10236
+ }
10237
+ `;
10238
+ }
10239
+ async function renderCompletionScript(shell) {
10240
+ if (shell === "bash") return renderBashScript();
10241
+ if (shell === "zsh") return renderZshScript();
10242
+ return renderPwshScript();
10243
+ }
10244
+ var SHELLS, completionCommand;
10245
+ var init_completion = __esm({
10246
+ "src/commands/completion.ts"() {
10247
+ "use strict";
10248
+ init_resolve();
10249
+ SHELLS = ["bash", "zsh", "pwsh"];
10250
+ completionCommand = defineCommand9({
10251
+ meta: {
10252
+ name: "completion",
10253
+ group: "tooling",
10254
+ // Hidden from `monoceros --help`: the install scripts wire up
10255
+ // completion automatically; manual setup is documented at
10256
+ // getmonoceros.build/docs/reference/utilities/completion. Still runnable directly.
10257
+ hidden: true,
10258
+ 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."
10259
+ },
10260
+ args: {
10261
+ shell: {
10262
+ type: "positional",
10263
+ description: "Target shell. One of: 'bash', 'zsh', 'pwsh'.",
10264
+ required: true
10265
+ }
10266
+ },
10267
+ async run({ args }) {
10268
+ const shell = args.shell;
10269
+ if (shell !== "bash" && shell !== "zsh" && shell !== "pwsh") {
10270
+ process.stderr.write(
10271
+ `Unknown shell: ${JSON.stringify(shell)}. Supported: ${SHELLS.join(", ")}.
10272
+ `
10273
+ );
10274
+ process.exit(2);
10275
+ }
10276
+ process.stdout.write(await renderCompletionScript(shell));
10277
+ }
10278
+ });
10279
+ }
10280
+ });
10281
+
10282
+ // src/commands/__bridge.ts
10283
+ import { defineCommand as defineCommand10 } from "citty";
10284
+ var __bridgeCommand;
10285
+ var init_bridge = __esm({
10286
+ "src/commands/__bridge.ts"() {
10287
+ "use strict";
10288
+ init_bridge_daemon();
10289
+ __bridgeCommand = defineCommand10({
10290
+ meta: {
10291
+ name: "__bridge",
10292
+ group: "internal",
10293
+ hidden: true,
10294
+ description: "Internal: host-side browser-bridge daemon (background)."
10295
+ },
10296
+ args: {
10297
+ root: {
10298
+ type: "positional",
10299
+ description: "Materialized container root ($MONOCEROS_HOME/container/<name>/).",
10300
+ required: true
10301
+ }
10302
+ },
10303
+ async run({ args }) {
10304
+ try {
10305
+ await runBridgeDaemon({ root: args.root });
10306
+ } catch {
10307
+ }
10308
+ }
10309
+ });
9951
10310
  }
9952
10311
  });
9953
10312