@getmonoceros/workbench 1.13.2 → 1.14.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
@@ -129,19 +129,19 @@ function wrapText(text, width, continuationIndent) {
129
129
  if (current.length > 0) lines.push(current.replace(/\s+$/, ""));
130
130
  return lines.map((l, i) => i === 0 ? l : continuationIndent + l).join("\n");
131
131
  }
132
- function alignTable(rows, indent) {
132
+ function alignTable(rows, indent, opts = {}) {
133
133
  if (rows.length === 0) return "";
134
- const labelWidth = Math.max(...rows.map((r) => visibleLen(r[0])));
134
+ const labelWidth = opts.fixedLabelWidth ?? Math.max(...rows.map((r) => visibleLen(r[0])));
135
135
  const gutter = " ";
136
136
  const descWidth = terminalWidth() - indent.length - labelWidth - gutter.length;
137
137
  const continuationIndent = " ".repeat(
138
138
  indent.length + labelWidth + gutter.length
139
139
  );
140
140
  return rows.map(([left, right]) => {
141
- const pad = " ".repeat(labelWidth - visibleLen(left));
141
+ const pad = " ".repeat(Math.max(0, labelWidth - visibleLen(left)));
142
142
  const wrapped = wrapText(right, descWidth, continuationIndent);
143
143
  return `${indent}${left}${pad}${gutter}${wrapped}`;
144
- }).join("\n");
144
+ }).join(opts.rowGap ? "\n\n" : "\n");
145
145
  }
146
146
  function collectSubCommands(cmd) {
147
147
  const subs = cmd.subCommands ?? {};
@@ -167,6 +167,7 @@ function renderCommandsBlock(entries) {
167
167
  arr.push(entry2);
168
168
  byGroup.set(entry2.group, arr);
169
169
  }
170
+ const labelWidth = Math.max(...entries.map((e) => visibleLen(cyan(e.name))));
170
171
  const renderSection = (label, items) => {
171
172
  if (items.length === 0) return;
172
173
  lines.push("");
@@ -176,7 +177,9 @@ function renderCommandsBlock(entries) {
176
177
  cyan(e.name),
177
178
  e.description
178
179
  ]);
179
- lines.push(alignTable(rows, ""));
180
+ lines.push(
181
+ alignTable(rows, "", { fixedLabelWidth: labelWidth, rowGap: true })
182
+ );
180
183
  };
181
184
  for (const { key, label } of GROUPS) {
182
185
  renderSection(label, byGroup.get(key) ?? []);
@@ -619,6 +622,9 @@ function containersDir(home = monocerosHome()) {
619
622
  function containerDir(name, home = monocerosHome()) {
620
623
  return path.join(containersDir(home), name);
621
624
  }
625
+ function containerLogsDir(name, home = monocerosHome()) {
626
+ return path.join(containerDir(name, home), "logs");
627
+ }
622
628
  function monocerosConfigPath(home = monocerosHome()) {
623
629
  return path.join(home, "monoceros-config.yml");
624
630
  }
@@ -982,6 +988,10 @@ var ANSI_UNDERLINE2 = `${ESC}4m`;
982
988
  var ANSI_CYAN2 = `${ESC}36m`;
983
989
  var ANSI_GREY2 = `${ESC}90m`;
984
990
  var ANSI_RESET2 = `${ESC}0m`;
991
+ var ANSI_RE2 = /\x1b\[[0-9;]*m/g;
992
+ function stripAnsi(s) {
993
+ return s.replace(ANSI_RE2, "");
994
+ }
985
995
  function makeWrap(isTty2) {
986
996
  return (s, ...codes) => isTty2 ? codes.join("") + s + ANSI_RESET2 : s;
987
997
  }
@@ -4137,7 +4147,7 @@ var addServiceCommand = defineCommand7({
4137
4147
  import { defineCommand as defineCommand8 } from "citty";
4138
4148
 
4139
4149
  // src/apply/index.ts
4140
- import { existsSync as existsSync8, promises as fs12 } from "fs";
4150
+ import { existsSync as existsSync7, promises as fs11 } from "fs";
4141
4151
  import { consola as consola11 } from "consola";
4142
4152
 
4143
4153
  // src/config/state.ts
@@ -4237,10 +4247,292 @@ function solutionConfigToCreateOptions(config, featureDefaults = {}) {
4237
4247
  return result;
4238
4248
  }
4239
4249
 
4250
+ // src/apply/apply-log.ts
4251
+ import { createWriteStream, mkdirSync } from "fs";
4252
+ import path11 from "path";
4253
+ import { Writable } from "stream";
4254
+ function safeIsoStamp(d) {
4255
+ return d.toISOString().replace(/[:.]/g, "-");
4256
+ }
4257
+ function createApplyLog(opts) {
4258
+ const now = opts.now ?? /* @__PURE__ */ new Date();
4259
+ const dir = containerLogsDir(opts.name, opts.home);
4260
+ mkdirSync(dir, { recursive: true });
4261
+ const file = `apply-${opts.name}-${safeIsoStamp(now)}.log`;
4262
+ const fullPath = path11.join(dir, file);
4263
+ const stream = createWriteStream(fullPath, { flags: "w" });
4264
+ const header = [
4265
+ `# monoceros apply log`,
4266
+ `# command: monoceros apply ${opts.name}`,
4267
+ `# started: ${now.toISOString()}`,
4268
+ `# cli-version: ${opts.cliVersion}`,
4269
+ `# config: ${opts.configPath}`,
4270
+ `# host: ${process.platform}/${process.arch} node ${process.version}`,
4271
+ ``,
4272
+ ``
4273
+ ].join("\n");
4274
+ stream.write(header);
4275
+ const sink = new Writable({
4276
+ write(chunk, _enc, cb) {
4277
+ const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
4278
+ stream.write(stripAnsi(text), cb);
4279
+ }
4280
+ });
4281
+ let closed = false;
4282
+ return {
4283
+ path: fullPath,
4284
+ stream,
4285
+ sink,
4286
+ close: () => new Promise((resolve) => {
4287
+ if (closed) {
4288
+ resolve();
4289
+ return;
4290
+ }
4291
+ closed = true;
4292
+ sink.end(() => {
4293
+ stream.end(() => resolve());
4294
+ });
4295
+ })
4296
+ };
4297
+ }
4298
+ function teeApplyLogger(base, sink) {
4299
+ const write = (level, msg) => {
4300
+ sink.write(`[${level}] ${msg}
4301
+ `);
4302
+ };
4303
+ const wrapped = {
4304
+ info: (msg) => {
4305
+ base.info(msg);
4306
+ write("info", msg);
4307
+ },
4308
+ success: (msg) => {
4309
+ base.success(msg);
4310
+ write("ok", msg);
4311
+ },
4312
+ warn: (msg) => {
4313
+ (base.warn ?? base.info)(msg);
4314
+ write("warn", msg);
4315
+ }
4316
+ };
4317
+ if (base.section) wrapped.section = base.section.bind(base);
4318
+ return wrapped;
4319
+ }
4320
+
4321
+ // src/apply/apply-progress.ts
4322
+ import { Writable as Writable2 } from "stream";
4323
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
4324
+ var FRAME_INTERVAL_MS = 80;
4325
+ var TAIL_LINES = 15;
4326
+ var PHASE_TRIGGERS = [
4327
+ // Compose mode triggers a feature/layer build before the container
4328
+ // is created — distinct phase, often the longest single step.
4329
+ { pattern: /Start: Run: docker build/i, label: "building feature layers\u2026" },
4330
+ // Image mode jumps straight from "preparing…" into the docker run
4331
+ // that pulls (if needed) + creates + starts the container.
4332
+ { pattern: /Start: Run: docker run/i, label: "starting container\u2026" },
4333
+ { pattern: /Running the postCreateCommand/i, label: "running postCreate\u2026" }
4334
+ ];
4335
+ function createApplyProgress(opts) {
4336
+ const out = opts.out;
4337
+ const now = opts.now ?? (() => Date.now());
4338
+ const startedAt = now();
4339
+ let phase = "preparing\u2026";
4340
+ let frameIdx = 0;
4341
+ let timer = null;
4342
+ let stopped = false;
4343
+ const tail = [];
4344
+ let lineBuf = "";
4345
+ const writeSpinner = () => {
4346
+ if (!opts.interactive || stopped) return;
4347
+ out.write(`\r\x1B[K${FRAMES[frameIdx]} ${phase}`);
4348
+ };
4349
+ const clearLine = () => {
4350
+ if (!opts.interactive) return;
4351
+ out.write("\r\x1B[K");
4352
+ };
4353
+ const setPhase = (label) => {
4354
+ if (phase === label) return;
4355
+ phase = label;
4356
+ if (opts.interactive) {
4357
+ writeSpinner();
4358
+ } else {
4359
+ out.write(`> ${label}
4360
+ `);
4361
+ }
4362
+ };
4363
+ const println = (line) => {
4364
+ clearLine();
4365
+ const withNewline = line.endsWith("\n") ? line : `${line}
4366
+ `;
4367
+ out.write(withNewline);
4368
+ writeSpinner();
4369
+ };
4370
+ const fmtElapsed = () => {
4371
+ const ms = now() - startedAt;
4372
+ const totalSec = Math.max(0, Math.round(ms / 1e3));
4373
+ const m = Math.floor(totalSec / 60);
4374
+ const s = totalSec % 60;
4375
+ return m > 0 ? `${m}m ${s}s` : `${s}s`;
4376
+ };
4377
+ const stop = () => {
4378
+ if (timer) {
4379
+ clearInterval(timer);
4380
+ timer = null;
4381
+ }
4382
+ if (!stopped) {
4383
+ stopped = true;
4384
+ clearLine();
4385
+ }
4386
+ };
4387
+ const succeed = (label) => {
4388
+ stop();
4389
+ const text = label ?? `container ready (${fmtElapsed()})`;
4390
+ out.write(`\u2714 ${text}
4391
+ `);
4392
+ };
4393
+ const fail = () => {
4394
+ stop();
4395
+ return { tailLines: [...tail] };
4396
+ };
4397
+ const streamSink = new Writable2({
4398
+ write(chunk, _enc, cb) {
4399
+ const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
4400
+ lineBuf += stripAnsi(text);
4401
+ let nl;
4402
+ while ((nl = lineBuf.indexOf("\n")) !== -1) {
4403
+ const line = lineBuf.slice(0, nl);
4404
+ lineBuf = lineBuf.slice(nl + 1);
4405
+ if (line.length === 0) continue;
4406
+ tail.push(line);
4407
+ if (tail.length > TAIL_LINES) tail.shift();
4408
+ for (const trig of PHASE_TRIGGERS) {
4409
+ if (trig.pattern.test(line)) {
4410
+ setPhase(trig.label);
4411
+ break;
4412
+ }
4413
+ }
4414
+ }
4415
+ cb();
4416
+ }
4417
+ });
4418
+ if (opts.interactive) {
4419
+ writeSpinner();
4420
+ timer = setInterval(() => {
4421
+ frameIdx = (frameIdx + 1) % FRAMES.length;
4422
+ writeSpinner();
4423
+ }, FRAME_INTERVAL_MS);
4424
+ timer.unref?.();
4425
+ }
4426
+ return {
4427
+ setPhase,
4428
+ println,
4429
+ succeed,
4430
+ fail,
4431
+ streamSink
4432
+ };
4433
+ }
4434
+ function progressTeeLogger(progress, sink) {
4435
+ const fileLine = (level, msg) => {
4436
+ sink.write(`[${level}] ${msg}
4437
+ `);
4438
+ };
4439
+ return {
4440
+ info: (msg) => {
4441
+ progress.println(msg);
4442
+ fileLine("info", msg);
4443
+ },
4444
+ success: (msg) => {
4445
+ progress.println(`\u2714 ${msg}`);
4446
+ fileLine("ok", msg);
4447
+ },
4448
+ warn: (msg) => {
4449
+ progress.println(`! ${msg}`);
4450
+ fileLine("warn", msg);
4451
+ }
4452
+ };
4453
+ }
4454
+ function logFileOnlyLogger(sink) {
4455
+ const fileLine = (level, msg) => {
4456
+ sink.write(`[${level}] ${msg}
4457
+ `);
4458
+ };
4459
+ return {
4460
+ info: (msg) => fileLine("info", msg),
4461
+ success: (msg) => fileLine("ok", msg),
4462
+ warn: (msg) => fileLine("warn", msg)
4463
+ };
4464
+ }
4465
+ function createSigintAbort(deps) {
4466
+ let aborted = false;
4467
+ return () => {
4468
+ if (aborted) return;
4469
+ aborted = true;
4470
+ if (deps.progress) deps.progress.fail();
4471
+ deps.out.write("\n\u23F9 aborted\n");
4472
+ deps.log.stream.write("\n[abort] SIGINT received\n");
4473
+ void deps.log.close().finally(() => {
4474
+ deps.out.write(`
4475
+ ${deps.formatLogPointer(deps.log.path)}
4476
+ `);
4477
+ deps.onExit();
4478
+ });
4479
+ };
4480
+ }
4481
+
4482
+ // src/apply/apply-summary.ts
4483
+ function shortFeatureName(ref) {
4484
+ const withoutTag = ref.replace(/:[^:/@]+$/, "");
4485
+ const idx = withoutTag.lastIndexOf("/");
4486
+ return idx >= 0 ? withoutTag.slice(idx + 1) : withoutTag;
4487
+ }
4488
+ function shortRepoName(repo) {
4489
+ const last = repo.path.split("/").filter(Boolean).pop();
4490
+ return last && last.length > 0 ? last : repo.url;
4491
+ }
4492
+ function buildApplySummary(opts) {
4493
+ const lines = [];
4494
+ if (opts.languages.length > 0) {
4495
+ lines.push({ label: "Languages", values: opts.languages });
4496
+ }
4497
+ if (opts.services.length > 0) {
4498
+ lines.push({
4499
+ label: "Services",
4500
+ values: opts.services.map((s) => s.name)
4501
+ });
4502
+ }
4503
+ if (opts.features && Object.keys(opts.features).length > 0) {
4504
+ lines.push({
4505
+ label: "Features",
4506
+ values: Object.keys(opts.features).map(shortFeatureName)
4507
+ });
4508
+ }
4509
+ if (opts.repos && opts.repos.length > 0) {
4510
+ lines.push({
4511
+ label: "Repositories",
4512
+ values: opts.repos.map(shortRepoName)
4513
+ });
4514
+ }
4515
+ if (opts.ports && opts.ports.length > 0) {
4516
+ lines.push({ label: "Ports", values: opts.ports.map(String) });
4517
+ }
4518
+ if (opts.aptPackages && opts.aptPackages.length > 0) {
4519
+ lines.push({ label: "APT packages", values: opts.aptPackages });
4520
+ }
4521
+ if (opts.installUrls && opts.installUrls.length > 0) {
4522
+ lines.push({ label: "Install URLs", values: opts.installUrls });
4523
+ }
4524
+ return lines;
4525
+ }
4526
+ function formatApplySummary(lines) {
4527
+ if (lines.length === 0) return "";
4528
+ const labelWidth = Math.max(...lines.map((l) => l.label.length));
4529
+ return lines.map((l) => ` ${l.label.padEnd(labelWidth)} ${cyan2(l.values.join(", "))}`).join("\n");
4530
+ }
4531
+
4240
4532
  // src/devcontainer/compose.ts
4241
4533
  import { spawn as spawn5 } from "child_process";
4242
4534
  import { existsSync as existsSync6 } from "fs";
4243
- import path12 from "path";
4535
+ import path13 from "path";
4244
4536
  import { consola as consola9 } from "consola";
4245
4537
 
4246
4538
  // src/util/mask-secrets.ts
@@ -4303,7 +4595,7 @@ function createSecretMaskStream() {
4303
4595
  import { spawn as spawn4 } from "child_process";
4304
4596
  import { readFileSync as readFileSync4 } from "fs";
4305
4597
  import { createRequire } from "module";
4306
- import path11 from "path";
4598
+ import path12 from "path";
4307
4599
 
4308
4600
  // src/devcontainer/runtime-pull-hint.ts
4309
4601
  import { Transform as Transform2 } from "stream";
@@ -4354,7 +4646,7 @@ function devcontainerCliPath() {
4354
4646
  if (!binEntry) {
4355
4647
  throw new Error("Could not resolve @devcontainers/cli bin entry.");
4356
4648
  }
4357
- cachedBinaryPath = path11.resolve(path11.dirname(pkgJsonPath), binEntry);
4649
+ cachedBinaryPath = path12.resolve(path12.dirname(pkgJsonPath), binEntry);
4358
4650
  return cachedBinaryPath;
4359
4651
  }
4360
4652
  var spawnDevcontainer = (args, cwd, options = {}) => {
@@ -4394,8 +4686,20 @@ var spawnDevcontainer = (args, cwd, options = {}) => {
4394
4686
  return;
4395
4687
  }
4396
4688
  const pullHint = { hinted: false };
4397
- child.stdout?.pipe(createSecretMaskStream()).pipe(createRuntimePullHintStream(pullHint)).pipe(process.stdout);
4398
- child.stderr?.pipe(createSecretMaskStream()).pipe(createRuntimePullHintStream(pullHint)).pipe(process.stderr);
4689
+ const stdoutPipe = child.stdout?.pipe(createSecretMaskStream()).pipe(createRuntimePullHintStream(pullHint));
4690
+ const stderrPipe = child.stderr?.pipe(createSecretMaskStream()).pipe(createRuntimePullHintStream(pullHint));
4691
+ if (!options.silent) {
4692
+ stdoutPipe?.pipe(process.stdout);
4693
+ stderrPipe?.pipe(process.stderr);
4694
+ }
4695
+ if (options.logSink) {
4696
+ stdoutPipe?.pipe(options.logSink, { end: false });
4697
+ stderrPipe?.pipe(options.logSink, { end: false });
4698
+ }
4699
+ if (options.progressSink) {
4700
+ stdoutPipe?.pipe(options.progressSink, { end: false });
4701
+ stderrPipe?.pipe(options.progressSink, { end: false });
4702
+ }
4399
4703
  child.on("error", reject);
4400
4704
  child.on("exit", (code) => resolve(code ?? 0));
4401
4705
  });
@@ -4472,15 +4776,15 @@ async function cleanupDockerObjects(opts) {
4472
4776
  return { exitCode: rmExit, removedIds: ids };
4473
4777
  }
4474
4778
  function composeProjectName(root) {
4475
- return `${path12.basename(root)}_devcontainer`;
4779
+ return `${path13.basename(root)}_devcontainer`;
4476
4780
  }
4477
4781
  function resolveCompose(root) {
4478
- if (!existsSync6(path12.join(root, ".devcontainer"))) {
4782
+ if (!existsSync6(path13.join(root, ".devcontainer"))) {
4479
4783
  throw new Error(
4480
4784
  `No .devcontainer/ at ${root}. Run \`monoceros apply <name>\` first.`
4481
4785
  );
4482
4786
  }
4483
- const composeFile = path12.join(root, ".devcontainer", "compose.yaml");
4787
+ const composeFile = path13.join(root, ".devcontainer", "compose.yaml");
4484
4788
  if (!existsSync6(composeFile)) {
4485
4789
  throw new Error(
4486
4790
  `No compose.yaml at ${composeFile}. \`start\` / \`stop\` / \`status\` / \`logs\` require services configured via \`monoceros add-service <name> <svc>\`. Use \`monoceros shell <name>\` to enter the container directly.`
@@ -4501,9 +4805,17 @@ async function runStart(opts) {
4501
4805
  logger.info(`Bringing devcontainer up at ${opts.root}\u2026`);
4502
4806
  return spawnFn(
4503
4807
  ["up", "--workspace-folder", opts.root, "--mount-workspace-git-root=false"],
4504
- opts.root
4808
+ opts.root,
4809
+ buildSpawnOptions(opts)
4505
4810
  );
4506
4811
  }
4812
+ function buildSpawnOptions(opts) {
4813
+ const out = {};
4814
+ if (opts.logSink) out.logSink = opts.logSink;
4815
+ if (opts.progressSink) out.progressSink = opts.progressSink;
4816
+ if (opts.silent) out.silent = true;
4817
+ return Object.keys(out).length > 0 ? out : void 0;
4818
+ }
4507
4819
  async function runContainerCycle(root, opts) {
4508
4820
  const { hasCompose, logger } = opts;
4509
4821
  if (hasCompose) {
@@ -4539,6 +4851,9 @@ and retry \`monoceros apply\`.`
4539
4851
  return runStart({
4540
4852
  root,
4541
4853
  ...opts.devcontainerSpawn ? { spawn: opts.devcontainerSpawn } : {},
4854
+ ...opts.logSink ? { logSink: opts.logSink } : {},
4855
+ ...opts.progressSink ? { progressSink: opts.progressSink } : {},
4856
+ ...opts.silent ? { silent: true } : {},
4542
4857
  logger
4543
4858
  });
4544
4859
  }
@@ -4552,7 +4867,8 @@ and retry \`monoceros apply\`.`
4552
4867
  "--mount-workspace-git-root=false",
4553
4868
  "--remove-existing-container"
4554
4869
  ],
4555
- root
4870
+ root,
4871
+ buildSpawnOptions(opts)
4556
4872
  );
4557
4873
  }
4558
4874
  function runStop(opts) {
@@ -4579,230 +4895,11 @@ function runLogs(opts) {
4579
4895
  );
4580
4896
  }
4581
4897
 
4582
- // src/devcontainer/repo-reachability.ts
4583
- import { spawn as spawn6 } from "child_process";
4584
- var realGitLsRemote = (url) => {
4585
- return new Promise((resolve, reject) => {
4586
- const child = spawn6("git", ["ls-remote", "--heads", "--", url], {
4587
- stdio: ["ignore", "pipe", "pipe"],
4588
- env: {
4589
- ...process.env,
4590
- GIT_TERMINAL_PROMPT: "0"
4591
- }
4592
- });
4593
- let stdout = "";
4594
- let stderr = "";
4595
- child.stdout.on("data", (chunk) => {
4596
- stdout += chunk.toString();
4597
- });
4598
- child.stderr.on("data", (chunk) => {
4599
- stderr += chunk.toString();
4600
- });
4601
- child.on("error", reject);
4602
- child.on(
4603
- "exit",
4604
- (code) => resolve({ stdout, stderr, exitCode: code ?? 0 })
4605
- );
4606
- });
4607
- };
4608
- function classifyStderr(stderr) {
4609
- const s = stderr.toLowerCase();
4610
- if (s.includes("could not resolve host") || s.includes("name or service not known") || s.includes("temporary failure in name resolution") || s.includes("no address associated with hostname")) {
4611
- return "dns";
4612
- }
4613
- if (s.includes("repository not found") || s.includes("may not have access") || s.includes("no longer exists") || s.includes("don't have permission") || s.includes("could not be found") || s.includes("the requested url returned error: 404")) {
4614
- return "not-found-or-no-access";
4615
- }
4616
- if (s.includes("authentication failed") || s.includes("could not read username") || s.includes("incorrect username or password") || s.includes("the requested url returned error: 401") || s.includes("the requested url returned error: 403")) {
4617
- return "auth-failed";
4618
- }
4619
- return "unknown";
4620
- }
4621
- async function checkRepoReachability(repos, options = {}) {
4622
- const spawnFn = options.spawn ?? realGitLsRemote;
4623
- const results = [];
4624
- for (const repo of repos) {
4625
- let result;
4626
- try {
4627
- result = await spawnFn(repo.url);
4628
- } catch (err) {
4629
- results.push({
4630
- url: repo.url,
4631
- ok: false,
4632
- kind: "unknown",
4633
- detail: err instanceof Error ? err.message : String(err)
4634
- });
4635
- continue;
4636
- }
4637
- if (result.exitCode === 0) {
4638
- results.push({ url: repo.url, ok: true, detail: "" });
4639
- continue;
4640
- }
4641
- results.push({
4642
- url: repo.url,
4643
- ok: false,
4644
- kind: classifyStderr(result.stderr),
4645
- detail: result.stderr.trim()
4646
- });
4647
- }
4648
- return results;
4649
- }
4650
- function formatUnreachableReposError(failures) {
4651
- const byKind = /* @__PURE__ */ new Map();
4652
- for (const f of failures) {
4653
- const kind = f.kind ?? "unknown";
4654
- const list = byKind.get(kind) ?? [];
4655
- list.push(f);
4656
- byKind.set(kind, list);
4657
- }
4658
- const totalUrls = failures.length;
4659
- const lines = [
4660
- totalUrls === 1 ? `Cannot reach declared repo: ${failures[0].url}` : `Cannot reach ${totalUrls} declared repos:`,
4661
- ""
4662
- ];
4663
- const sectionOrder = [
4664
- "not-found-or-no-access",
4665
- "auth-failed",
4666
- "dns",
4667
- "unknown"
4668
- ];
4669
- for (const kind of sectionOrder) {
4670
- const entries = byKind.get(kind);
4671
- if (!entries || entries.length === 0) continue;
4672
- lines.push(headerForKind(kind));
4673
- for (const e of entries) {
4674
- lines.push(` \u2022 ${e.url}`);
4675
- if (e.detail) {
4676
- for (const detailLine of e.detail.split("\n")) {
4677
- const trimmed = detailLine.trim();
4678
- if (trimmed) lines.push(` git: ${trimmed}`);
4679
- }
4680
- }
4681
- }
4682
- for (const advice of adviceForKind(kind)) {
4683
- lines.push(` - ${advice}`);
4684
- }
4685
- lines.push("");
4686
- }
4687
- lines.push(`Then re-run ${cyan2("monoceros apply")}.`);
4688
- return lines.join("\n");
4689
- }
4690
- function headerForKind(kind) {
4691
- switch (kind) {
4692
- case "not-found-or-no-access":
4693
- return "Repository not found (or your credentials don't grant access):";
4694
- case "auth-failed":
4695
- return "Authentication failed (credentials are present but rejected):";
4696
- case "dns":
4697
- return "Host unreachable (DNS / VPN / offline \u2014 git couldn't resolve the hostname):";
4698
- case "unknown":
4699
- return "Unrecognised git error:";
4700
- }
4701
- }
4702
- function adviceForKind(kind) {
4703
- switch (kind) {
4704
- case "not-found-or-no-access":
4705
- return [
4706
- "Re-check the URL for typos (case-sensitive on most hosts).",
4707
- "Verify the repo still exists / is not archived in a way that hides it.",
4708
- "Ensure your token covers this org / workspace and has read scope (GitHub: `repo`; GitLab: `read_repository`; Bitbucket: repo read)."
4709
- ];
4710
- case "auth-failed":
4711
- return [
4712
- "Token may be expired or revoked \u2014 regenerate it from the provider UI.",
4713
- "Re-run the provider CLI login (gh auth login / glab auth login) \u2014 Monoceros picks up the refreshed token on the next apply."
4714
- ];
4715
- case "dns":
4716
- return [
4717
- "Check your internet / VPN \u2014 corporate Git hosts often require VPN.",
4718
- "Verify the hostname spelling in the yml."
4719
- ];
4720
- case "unknown":
4721
- return [
4722
- "Run `git ls-remote <url>` manually on the host to see the raw error."
4723
- ];
4724
- }
4725
- }
4726
-
4727
- // src/devcontainer/repo-clone.ts
4728
- import { spawn as spawn7 } from "child_process";
4729
- import { existsSync as existsSync7, promises as fs10 } from "fs";
4730
- import path13 from "path";
4731
- var realGitClone = (url, dest) => {
4732
- return new Promise((resolve, reject) => {
4733
- const child = spawn7("git", ["clone", "--", url, dest], {
4734
- stdio: ["ignore", "pipe", "pipe"],
4735
- env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
4736
- });
4737
- let stdout = "";
4738
- let stderr = "";
4739
- child.stdout.on("data", (c) => {
4740
- stdout += c.toString();
4741
- });
4742
- child.stderr.on("data", (c) => {
4743
- stderr += c.toString();
4744
- });
4745
- child.on("error", reject);
4746
- child.on(
4747
- "exit",
4748
- (code) => resolve({ stdout, stderr, exitCode: code ?? 0 })
4749
- );
4750
- });
4751
- };
4752
- async function cloneReposHostSide(containerRoot, repos, options = {}) {
4753
- const spawnFn = options.spawn ?? realGitClone;
4754
- const results = [];
4755
- for (const repo of repos) {
4756
- const dest = path13.join(containerRoot, "projects", repo.path);
4757
- if (existsSync7(dest)) {
4758
- results.push({ path: repo.path, url: repo.url, status: "skipped" });
4759
- continue;
4760
- }
4761
- await fs10.mkdir(path13.dirname(dest), { recursive: true });
4762
- let r;
4763
- try {
4764
- r = await spawnFn(repo.url, dest);
4765
- } catch (err) {
4766
- results.push({
4767
- path: repo.path,
4768
- url: repo.url,
4769
- status: "failed",
4770
- detail: err instanceof Error ? err.message : String(err)
4771
- });
4772
- continue;
4773
- }
4774
- results.push(
4775
- r.exitCode === 0 ? { path: repo.path, url: repo.url, status: "cloned" } : {
4776
- path: repo.path,
4777
- url: repo.url,
4778
- status: "failed",
4779
- detail: r.stderr.trim()
4780
- }
4781
- );
4782
- }
4783
- return results;
4784
- }
4785
- function formatCloneFailuresError(failures) {
4786
- const lines = failures.length === 1 ? [`Failed to clone declared repo: ${failures[0].url}`, ""] : [`Failed to clone ${failures.length} declared repos:`, ""];
4787
- for (const f of failures) {
4788
- lines.push(` \u2022 ${f.url} \u2192 projects/${f.path}`);
4789
- if (f.detail) lines.push(` ${f.detail}`);
4790
- }
4791
- lines.push("");
4792
- lines.push(
4793
- "Reachability was confirmed earlier, so this is usually a local issue"
4794
- );
4795
- lines.push(
4796
- "(disk space, a leftover non-empty target dir). Fix it and re-run " + cyan2("monoceros apply") + "."
4797
- );
4798
- return lines.join("\n");
4799
- }
4800
-
4801
4898
  // src/devcontainer/docker-mode.ts
4802
- import { spawn as spawn8 } from "child_process";
4899
+ import { spawn as spawn6 } from "child_process";
4803
4900
  var realDockerInfo = () => {
4804
4901
  return new Promise((resolve, reject) => {
4805
- const child = spawn8(
4902
+ const child = spawn6(
4806
4903
  "docker",
4807
4904
  ["info", "--format", "{{json .SecurityOptions}}"],
4808
4905
  {
@@ -4861,13 +4958,13 @@ function formatRootlessNotSupportedError() {
4861
4958
  }
4862
4959
 
4863
4960
  // src/devcontainer/identity.ts
4864
- import { spawn as spawn9 } from "child_process";
4865
- import { promises as fs11 } from "fs";
4961
+ import { spawn as spawn7 } from "child_process";
4962
+ import { promises as fs10 } from "fs";
4866
4963
  import path14 from "path";
4867
4964
  import { consola as consola10 } from "consola";
4868
4965
  var realGitConfigGet = (key) => {
4869
4966
  return new Promise((resolve, reject) => {
4870
- const child = spawn9("git", ["config", "--global", "--get", key], {
4967
+ const child = spawn7("git", ["config", "--global", "--get", key], {
4871
4968
  stdio: ["ignore", "pipe", "inherit"]
4872
4969
  });
4873
4970
  let stdout = "";
@@ -4991,8 +5088,8 @@ async function collectGitIdentity(devContainerRoot, options = {}) {
4991
5088
  const lines = ["[user]"];
4992
5089
  if (resolved.name !== void 0) lines.push(` name = ${resolved.name}`);
4993
5090
  if (resolved.email !== void 0) lines.push(` email = ${resolved.email}`);
4994
- await fs11.mkdir(gitconfigDir, { recursive: true });
4995
- await fs11.writeFile(gitconfigPath, lines.join("\n") + "\n");
5091
+ await fs10.mkdir(gitconfigDir, { recursive: true });
5092
+ await fs10.writeFile(gitconfigPath, lines.join("\n") + "\n");
4996
5093
  return {
4997
5094
  ...resolved.name !== void 0 ? { name: resolved.name } : {},
4998
5095
  ...resolved.email !== void 0 ? { email: resolved.email } : {},
@@ -5035,7 +5132,7 @@ async function readKeyFromHost(spawnFn, key, logger) {
5035
5132
  }
5036
5133
  async function readExistingGitconfig(filePath) {
5037
5134
  try {
5038
- const content = await fs11.readFile(filePath, "utf8");
5135
+ const content = await fs10.readFile(filePath, "utf8");
5039
5136
  const result = {};
5040
5137
  const nameMatch = /^\s*name\s*=\s*(.+?)\s*$/m.exec(content);
5041
5138
  const emailMatch = /^\s*email\s*=\s*(.+?)\s*$/m.exec(content);
@@ -5068,7 +5165,7 @@ ${sectionLine(label)}
5068
5165
  );
5069
5166
  }
5070
5167
  const ymlPath = containerConfigPath(opts.name, home);
5071
- if (!existsSync8(ymlPath)) {
5168
+ if (!existsSync7(ymlPath)) {
5072
5169
  throw new Error(
5073
5170
  `No such config: ${ymlPath}. Run \`monoceros init <template> ${opts.name}\` first.`
5074
5171
  );
@@ -5174,16 +5271,6 @@ Fix the value in the env file (or the yml).`
5174
5271
  throw new Error(formatMissingCredentialsError(missing));
5175
5272
  }
5176
5273
  }
5177
- const declaredRepos = createOpts.repos ?? [];
5178
- if (declaredRepos.length > 0) {
5179
- const reachability = await checkRepoReachability(declaredRepos, {
5180
- ...opts.reachabilitySpawn ? { spawn: opts.reachabilitySpawn } : {}
5181
- });
5182
- const unreachable = reachability.filter((r) => !r.ok);
5183
- if (unreachable.length > 0) {
5184
- throw new Error(formatUnreachableReposError(unreachable));
5185
- }
5186
- }
5187
5274
  section("Scaffold");
5188
5275
  const dockerMode = await detectDockerMode({
5189
5276
  ...opts.dockerInfoSpawn ? { spawn: opts.dockerInfoSpawn } : {}
@@ -5191,7 +5278,7 @@ Fix the value in the env file (or the yml).`
5191
5278
  if (dockerMode === "rootless") {
5192
5279
  throw new Error(formatRootlessNotSupportedError());
5193
5280
  }
5194
- await fs12.mkdir(targetDir, { recursive: true });
5281
+ await fs11.mkdir(targetDir, { recursive: true });
5195
5282
  await writeScaffold(createOpts, targetDir, { dockerMode });
5196
5283
  await writeStateFile(
5197
5284
  targetDir,
@@ -5202,72 +5289,118 @@ Fix the value in the env file (or the yml).`
5202
5289
  })
5203
5290
  );
5204
5291
  logger.success(`materialized into ${prettyPath(targetDir)}`);
5205
- const reposToClone = createOpts.repos ?? [];
5206
- if (reposToClone.length > 0) {
5207
- const cloneResults = await cloneReposHostSide(targetDir, reposToClone, {
5208
- ...opts.cloneSpawn ? { spawn: opts.cloneSpawn } : {}
5209
- });
5210
- for (const r of cloneResults) {
5211
- if (r.status === "cloned") {
5212
- logger.success(`cloned ${cyan2(r.path)} ${dim(`(${r.url})`)}`);
5213
- } else if (r.status === "skipped") {
5214
- logger.info(`projects/${r.path} already present \u2014 skipped clone`);
5215
- }
5216
- }
5217
- const cloneFailures = cloneResults.filter((r) => r.status === "failed");
5218
- if (cloneFailures.length > 0) {
5219
- throw new Error(formatCloneFailuresError(cloneFailures));
5220
- }
5221
- }
5222
5292
  section("Container");
5223
- const featureRefs = parsed.config.features.map((f) => f.ref);
5224
- if (featureRefs.length > 0) {
5225
- logger.info(`Features: ${featureRefs.map((r) => cyan2(r)).join(", ")}`);
5226
- }
5227
- logger.info(
5228
- dim(
5229
- 'Pulling runtime image and building feature layers. First apply takes ~1\u20132 min (Docker downloads the multi-arch base); subsequent applies are cached and fast. devcontainer-cli may log a "No manifest found" line \u2014 harmless, the pull continues.'
5230
- )
5231
- );
5232
- const ports = createOpts.ports ?? [];
5233
- const hasPorts = ports.length > 0;
5234
- if (hasPorts) {
5235
- await preflightHostPort(proxyHostPort(globalConfig), {
5236
- ...opts.proxyDocker ? { docker: opts.proxyDocker } : {}
5237
- });
5238
- }
5293
+ const applyLog = createApplyLog({
5294
+ name: opts.name,
5295
+ home,
5296
+ cliVersion: opts.cliVersion,
5297
+ configPath: ymlPath,
5298
+ ...opts.now ? { now: opts.now } : {}
5299
+ });
5300
+ const progressOut = opts.progressOut ?? process.stderr;
5301
+ const interactive = (progressOut.isTTY ?? false) && !opts.verbose;
5302
+ const progress = interactive ? createApplyProgress({ out: progressOut, interactive: true }) : null;
5303
+ const containerLogger = progress ? progressTeeLogger(progress, applyLog.sink) : teeApplyLogger(logger, applyLog.sink);
5304
+ const internalLogger = progress ? logFileOnlyLogger(applyLog.sink) : containerLogger;
5305
+ const onSigint = createSigintAbort({
5306
+ progress,
5307
+ out: progressOut,
5308
+ log: applyLog,
5309
+ formatLogPointer: (p) => dim(`log: ${prettyPath(p)}`),
5310
+ onExit: () => process.exit(130)
5311
+ });
5312
+ process.on("SIGINT", onSigint);
5313
+ let exitCode;
5239
5314
  try {
5315
+ const pullWarning = 'Pulling runtime image and building feature layers. First apply takes ~1\u20132 min (Docker downloads the multi-arch base); subsequent applies are cached and fast. devcontainer-cli may log a "No manifest found" line \u2014 harmless, the pull continues.';
5316
+ if (progress) {
5317
+ applyLog.stream.write(`# note: ${pullWarning}
5318
+
5319
+ `);
5320
+ } else {
5321
+ containerLogger.info(dim(pullWarning));
5322
+ }
5323
+ const ports = createOpts.ports ?? [];
5324
+ const hasPorts = ports.length > 0;
5240
5325
  if (hasPorts) {
5241
- await writeDynamicConfig(opts.name, ports, { monocerosHome: home });
5242
- await ensureProxy({
5243
- ...opts.proxyDocker ? { docker: opts.proxyDocker } : {},
5244
- monocerosHome: home,
5245
- hostPort: proxyHostPort(globalConfig),
5246
- logger
5326
+ await preflightHostPort(proxyHostPort(globalConfig), {
5327
+ ...opts.proxyDocker ? { docker: opts.proxyDocker } : {}
5247
5328
  });
5248
- } else {
5249
- await removeDynamicConfig(opts.name, { monocerosHome: home });
5250
5329
  }
5251
- } catch (err) {
5252
- logger.warn?.(
5253
- `Could not sync Traefik routes: ${err instanceof Error ? err.message : String(err)}. The container will start, but \`<name>.localhost\` routing may not work until the next \`monoceros apply\`.`
5254
- );
5255
- }
5256
- const exitCode = await runContainerCycle(targetDir, {
5257
- hasCompose: needsCompose(createOpts),
5258
- ...opts.dockerExec !== void 0 ? { dockerExec: opts.dockerExec } : {},
5259
- ...opts.devcontainerSpawn !== void 0 ? { devcontainerSpawn: opts.devcontainerSpawn } : {},
5260
- logger
5261
- });
5262
- if (exitCode === 0) {
5263
- section("Next steps");
5264
- logger.info(` ${cyan2(`monoceros shell ${opts.name}`)}`);
5330
+ try {
5331
+ if (hasPorts) {
5332
+ await writeDynamicConfig(opts.name, ports, { monocerosHome: home });
5333
+ await ensureProxy({
5334
+ ...opts.proxyDocker ? { docker: opts.proxyDocker } : {},
5335
+ monocerosHome: home,
5336
+ hostPort: proxyHostPort(globalConfig),
5337
+ logger: containerLogger
5338
+ });
5339
+ } else {
5340
+ await removeDynamicConfig(opts.name, { monocerosHome: home });
5341
+ }
5342
+ } catch (err) {
5343
+ containerLogger.warn?.(
5344
+ `Could not sync Traefik routes: ${err instanceof Error ? err.message : String(err)}. The container will start, but \`<name>.localhost\` routing may not work until the next \`monoceros apply\`.`
5345
+ );
5346
+ }
5347
+ if (progress) {
5348
+ progress.setPhase(
5349
+ needsCompose(createOpts) ? "cleaning up previous containers\u2026" : "starting container\u2026"
5350
+ );
5351
+ }
5352
+ exitCode = await runContainerCycle(targetDir, {
5353
+ hasCompose: needsCompose(createOpts),
5354
+ ...opts.dockerExec !== void 0 ? { dockerExec: opts.dockerExec } : {},
5355
+ ...opts.devcontainerSpawn !== void 0 ? { devcontainerSpawn: opts.devcontainerSpawn } : {},
5356
+ logSink: applyLog.sink,
5357
+ ...progress ? { progressSink: progress.streamSink, silent: true } : {},
5358
+ logger: internalLogger
5359
+ });
5360
+ if (progress) {
5361
+ if (exitCode === 0) {
5362
+ progress.succeed();
5363
+ } else {
5364
+ const { tailLines } = progress.fail();
5365
+ progressOut.write(`
5366
+ \u2718 apply failed (exit ${exitCode})
5367
+
5368
+ `);
5369
+ for (const line of tailLines) {
5370
+ progressOut.write(` ${line}
5371
+ `);
5372
+ }
5373
+ if (tailLines.length > 0) progressOut.write("\n");
5374
+ }
5375
+ }
5376
+ if (exitCode === 0) {
5377
+ const summaryLines = buildApplySummary(createOpts);
5378
+ if (summaryLines.length > 0) {
5379
+ const formatted = formatApplySummary(summaryLines);
5380
+ progressOut.write(`
5381
+ ${formatted}
5382
+ `);
5383
+ applyLog.stream.write(`
5384
+ ${stripAnsi(formatted)}
5385
+ `);
5386
+ }
5387
+ }
5388
+ await applyLog.close();
5389
+ progressOut.write(`
5390
+ ${dim(`log: ${prettyPath(applyLog.path)}`)}
5391
+ `);
5392
+ if (exitCode === 0) {
5393
+ section("Next steps");
5394
+ logger.info(` ${cyan2(`monoceros shell ${opts.name}`)}`);
5395
+ }
5396
+ } finally {
5397
+ process.off("SIGINT", onSigint);
5265
5398
  }
5266
5399
  return { targetDir, configPath: ymlPath, containerExitCode: exitCode };
5267
5400
  }
5268
5401
  async function assertSafeTargetDir(targetDir, expectedOrigin) {
5269
- if (!existsSync8(targetDir)) return;
5270
- const entries = await fs12.readdir(targetDir);
5402
+ if (!existsSync7(targetDir)) return;
5403
+ const entries = await fs11.readdir(targetDir);
5271
5404
  if (entries.length === 0) return;
5272
5405
  const state = await readStateFile(targetDir);
5273
5406
  if (state) {
@@ -5337,7 +5470,7 @@ async function persistPromptedIdentity(prompted, ymlPath, home, logger) {
5337
5470
  }
5338
5471
  if (wantContainer) {
5339
5472
  try {
5340
- const text = await fs12.readFile(ymlPath, "utf8");
5473
+ const text = await fs11.readFile(ymlPath, "utf8");
5341
5474
  const parsed = parseConfig(text, ymlPath);
5342
5475
  const changed = setContainerGitUserInDoc(parsed.doc, {
5343
5476
  name: prompted.name,
@@ -5345,7 +5478,7 @@ async function persistPromptedIdentity(prompted, ymlPath, home, logger) {
5345
5478
  });
5346
5479
  if (changed) {
5347
5480
  const out = stringifyConfig(parsed.doc);
5348
- await fs12.writeFile(ymlPath, out, "utf8");
5481
+ await fs11.writeFile(ymlPath, out, "utf8");
5349
5482
  logger.info(
5350
5483
  `Saved identity in this container \u2014 wrote git.user into ${prettyPath(ymlPath)}.`
5351
5484
  );
@@ -5359,7 +5492,7 @@ async function persistPromptedIdentity(prompted, ymlPath, home, logger) {
5359
5492
  }
5360
5493
 
5361
5494
  // src/version.ts
5362
- var CLI_VERSION = true ? "1.13.2" : "dev";
5495
+ var CLI_VERSION = true ? "1.14.0" : "dev";
5363
5496
 
5364
5497
  // src/commands/_dispatch.ts
5365
5498
  import { consola as consola12 } from "consola";
@@ -5385,13 +5518,19 @@ var applyCommand = defineCommand8({
5385
5518
  type: "positional",
5386
5519
  description: "Config name. Resolves to $MONOCEROS_HOME/container-configs/<name>.yml.",
5387
5520
  required: true
5521
+ },
5522
+ verbose: {
5523
+ type: "boolean",
5524
+ description: "Stream the raw @devcontainers/cli output to stderr instead of showing a phase spinner. Auto-enabled when stderr is not a TTY.",
5525
+ default: false
5388
5526
  }
5389
5527
  },
5390
5528
  run({ args }) {
5391
5529
  return dispatch(async () => {
5392
5530
  const result = await runApply({
5393
5531
  name: args.name,
5394
- cliVersion: CLI_VERSION
5532
+ cliVersion: CLI_VERSION,
5533
+ verbose: args.verbose
5395
5534
  });
5396
5535
  return result.containerExitCode;
5397
5536
  });
@@ -5420,7 +5559,7 @@ function renderCompletionScript(shell) {
5420
5559
  ' COMPREPLY=( $(compgen -W "$candidates" -- "$cur") )',
5421
5560
  " # Suppress the trailing space when bash narrowed the candidate",
5422
5561
  " # set to a single token that ends with `=` \u2014 those are value-",
5423
- " # flags (`--with=`, `--with-ports=`, \u2026) where the user types the",
5562
+ " # flags (`--with-features=`, `--with-ports=`, \u2026) where the user types the",
5424
5563
  " # value immediately after.",
5425
5564
  ' if [[ ${#COMPREPLY[@]} -eq 1 && "${COMPREPLY[0]}" == *= ]]; then',
5426
5565
  " compopt -o nospace",
@@ -5493,6 +5632,10 @@ var completionCommand = defineCommand9({
5493
5632
  meta: {
5494
5633
  name: "completion",
5495
5634
  group: "tooling",
5635
+ // Hidden from `monoceros --help`: the install scripts wire up
5636
+ // completion automatically; manual setup is documented in
5637
+ // docs/commands/completion.md. Still runnable directly.
5638
+ hidden: true,
5496
5639
  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."
5497
5640
  },
5498
5641
  args: {
@@ -5519,7 +5662,7 @@ var completionCommand = defineCommand9({
5519
5662
  import { defineCommand as defineCommand10 } from "citty";
5520
5663
 
5521
5664
  // src/completion/resolve.ts
5522
- import { existsSync as existsSync9, promises as fs13 } from "fs";
5665
+ import { existsSync as existsSync8, promises as fs12 } from "fs";
5523
5666
  import path15 from "path";
5524
5667
  async function resolveCompletions(line, point, opts = {}) {
5525
5668
  const { prev, current } = parseCompletionLine(line, point);
@@ -5669,8 +5812,8 @@ function filterPrefix(values, fragment) {
5669
5812
  async function listContainerNames(ctx) {
5670
5813
  const home = ctx.opts.monocerosHome ?? monocerosHome();
5671
5814
  const dir = path15.join(home, "container-configs");
5672
- if (!existsSync9(dir)) return [];
5673
- const entries = await fs13.readdir(dir);
5815
+ if (!existsSync8(dir)) return [];
5816
+ const entries = await fs12.readdir(dir);
5674
5817
  return entries.filter((e) => e.endsWith(".yml")).map((e) => e.slice(0, -".yml".length)).sort();
5675
5818
  }
5676
5819
  async function listFeatureComponents() {
@@ -5888,6 +6031,8 @@ var __completeCommand = defineCommand10({
5888
6031
  meta: {
5889
6032
  name: "__complete",
5890
6033
  group: "internal",
6034
+ // Internal plumbing for the shell wrappers — never shown in help.
6035
+ hidden: true,
5891
6036
  description: "Internal \u2014 shell completion engine. Used by the wrappers emitted by `monoceros completion <shell>`. Output one candidate completion per line."
5892
6037
  },
5893
6038
  args: {
@@ -5925,7 +6070,7 @@ import { defineCommand as defineCommand11 } from "citty";
5925
6070
  import { consola as consola14 } from "consola";
5926
6071
 
5927
6072
  // src/init/index.ts
5928
- import { existsSync as existsSync10, promises as fs14 } from "fs";
6073
+ import { existsSync as existsSync9, promises as fs13 } from "fs";
5929
6074
  import path16 from "path";
5930
6075
  import { consola as consola13 } from "consola";
5931
6076
 
@@ -6292,7 +6437,7 @@ async function runInit(opts) {
6292
6437
  );
6293
6438
  }
6294
6439
  const dest = containerConfigPath(opts.name, home);
6295
- if (existsSync10(dest)) {
6440
+ if (existsSync9(dest)) {
6296
6441
  throw new Error(
6297
6442
  `Config already exists: ${dest}. Delete it manually before re-running \`monoceros init\` \u2014 this protects any hand-edits.`
6298
6443
  );
@@ -6363,9 +6508,9 @@ async function runInit(opts) {
6363
6508
  } else {
6364
6509
  text = generateComposedYml(opts.name, composed, lookup, repos, ports);
6365
6510
  }
6366
- await fs14.mkdir(containerConfigsDir(home), { recursive: true });
6511
+ await fs13.mkdir(containerConfigsDir(home), { recursive: true });
6367
6512
  await ensureEnvGitignored(containerConfigsDir(home));
6368
- await fs14.writeFile(dest, text, "utf8");
6513
+ await fs13.writeFile(dest, text, "utf8");
6369
6514
  const envPath = containerEnvPath(opts.name, home);
6370
6515
  const seedVars = {};
6371
6516
  for (const f of composed.features) {
@@ -6655,7 +6800,7 @@ var listComponentsCommand = defineCommand12({
6655
6800
  meta: {
6656
6801
  name: "list-components",
6657
6802
  group: "discovery",
6658
- description: "Print the components catalog used by `monoceros init --with=\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."
6803
+ 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."
6659
6804
  },
6660
6805
  args: {},
6661
6806
  async run() {
@@ -6924,7 +7069,7 @@ import { consola as consola20 } from "consola";
6924
7069
  import { createInterface } from "readline/promises";
6925
7070
 
6926
7071
  // src/remove/index.ts
6927
- import { existsSync as existsSync11, promises as fs15 } from "fs";
7072
+ import { existsSync as existsSync10, promises as fs14 } from "fs";
6928
7073
  import path17 from "path";
6929
7074
  import { consola as consola19 } from "consola";
6930
7075
  async function runRemove(opts) {
@@ -6942,9 +7087,9 @@ async function runRemove(opts) {
6942
7087
  const ymlPath = containerConfigPath(opts.name, home);
6943
7088
  const envPath = containerEnvPath(opts.name, home);
6944
7089
  const containerPath = containerDir(opts.name, home);
6945
- const hasYml = existsSync11(ymlPath);
6946
- const hasEnv = existsSync11(envPath);
6947
- const hasContainer = existsSync11(containerPath);
7090
+ const hasYml = existsSync10(ymlPath);
7091
+ const hasEnv = existsSync10(envPath);
7092
+ const hasContainer = existsSync10(containerPath);
6948
7093
  if (!hasYml && !hasContainer) {
6949
7094
  throw new Error(
6950
7095
  `Nothing to remove for '${opts.name}': neither ${ymlPath} nor ${containerPath} exists.`
@@ -6969,29 +7114,29 @@ async function runRemove(opts) {
6969
7114
  if (!opts.noBackup && (hasYml || hasContainer)) {
6970
7115
  const ts = (opts.now ?? /* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
6971
7116
  backupPath = path17.join(home, "container-backups", `${opts.name}-${ts}`);
6972
- await fs15.mkdir(backupPath, { recursive: true });
7117
+ await fs14.mkdir(backupPath, { recursive: true });
6973
7118
  if (hasYml) {
6974
- await fs15.copyFile(ymlPath, path17.join(backupPath, `${opts.name}.yml`));
7119
+ await fs14.copyFile(ymlPath, path17.join(backupPath, `${opts.name}.yml`));
6975
7120
  }
6976
7121
  if (hasEnv) {
6977
- await fs15.copyFile(envPath, path17.join(backupPath, `${opts.name}.env`));
7122
+ await fs14.copyFile(envPath, path17.join(backupPath, `${opts.name}.env`));
6978
7123
  }
6979
7124
  if (hasContainer) {
6980
- await fs15.cp(containerPath, path17.join(backupPath, "container"), {
7125
+ await fs14.cp(containerPath, path17.join(backupPath, "container"), {
6981
7126
  recursive: true
6982
7127
  });
6983
7128
  }
6984
7129
  logger.info(`Backup written to ${prettyPath(backupPath)}.`);
6985
7130
  }
6986
7131
  if (hasYml) {
6987
- await fs15.rm(ymlPath, { force: true });
7132
+ await fs14.rm(ymlPath, { force: true });
6988
7133
  }
6989
7134
  if (hasEnv) {
6990
- await fs15.rm(envPath, { force: true });
7135
+ await fs14.rm(envPath, { force: true });
6991
7136
  }
6992
7137
  if (hasContainer) {
6993
7138
  try {
6994
- await fs15.rm(containerPath, { recursive: true, force: true });
7139
+ await fs14.rm(containerPath, { recursive: true, force: true });
6995
7140
  } catch (err) {
6996
7141
  const code = err.code;
6997
7142
  if (code !== "EACCES" && code !== "EPERM") {
@@ -7017,7 +7162,7 @@ async function runRemove(opts) {
7017
7162
  `docker-based cleanup of ${containerPath} exited ${exit}. Inspect with \`sudo ls -la ${containerPath}\` and clean manually.`
7018
7163
  );
7019
7164
  }
7020
- await fs15.rm(containerPath, { recursive: true, force: true });
7165
+ await fs14.rm(containerPath, { recursive: true, force: true });
7021
7166
  }
7022
7167
  }
7023
7168
  logger.success(
@@ -7120,7 +7265,7 @@ import { defineCommand as defineCommand18 } from "citty";
7120
7265
  import { consola as consola22 } from "consola";
7121
7266
 
7122
7267
  // src/restore/index.ts
7123
- import { existsSync as existsSync12, promises as fs16 } from "fs";
7268
+ import { existsSync as existsSync11, promises as fs15 } from "fs";
7124
7269
  import path18 from "path";
7125
7270
  import { consola as consola21 } from "consola";
7126
7271
  async function runRestore(opts) {
@@ -7130,14 +7275,14 @@ async function runRestore(opts) {
7130
7275
  success: (msg) => consola21.success(msg)
7131
7276
  };
7132
7277
  const backup = path18.resolve(opts.backupPath);
7133
- if (!existsSync12(backup)) {
7278
+ if (!existsSync11(backup)) {
7134
7279
  throw new Error(`Backup not found: ${backup}.`);
7135
7280
  }
7136
- const stat = await fs16.stat(backup);
7281
+ const stat = await fs15.stat(backup);
7137
7282
  if (!stat.isDirectory()) {
7138
7283
  throw new Error(`Backup path is not a directory: ${backup}.`);
7139
7284
  }
7140
- const entries = await fs16.readdir(backup);
7285
+ const entries = await fs15.readdir(backup);
7141
7286
  const ymlFiles = entries.filter((f) => f.endsWith(".yml"));
7142
7287
  if (ymlFiles.length === 0) {
7143
7288
  throw new Error(
@@ -7152,28 +7297,28 @@ async function runRestore(opts) {
7152
7297
  const ymlFile = ymlFiles[0];
7153
7298
  const name = ymlFile.replace(/\.yml$/, "");
7154
7299
  const containerInBackup = path18.join(backup, "container");
7155
- const hasContainer = existsSync12(containerInBackup);
7300
+ const hasContainer = existsSync11(containerInBackup);
7156
7301
  const envInBackup = path18.join(backup, `${name}.env`);
7157
- const hasEnv = existsSync12(envInBackup);
7302
+ const hasEnv = existsSync11(envInBackup);
7158
7303
  const destYml = containerConfigPath(name, home);
7159
7304
  const destContainer = containerDir(name, home);
7160
- if (existsSync12(destYml)) {
7305
+ if (existsSync11(destYml)) {
7161
7306
  throw new Error(
7162
7307
  `Refusing to restore: ${destYml} already exists. Remove the current container first (\`monoceros remove ${name}\`) or rename the existing config.`
7163
7308
  );
7164
7309
  }
7165
- if (hasContainer && existsSync12(destContainer)) {
7310
+ if (hasContainer && existsSync11(destContainer)) {
7166
7311
  throw new Error(
7167
7312
  `Refusing to restore: ${destContainer} already exists. Remove the current container first (\`monoceros remove ${name}\`).`
7168
7313
  );
7169
7314
  }
7170
- await fs16.mkdir(containerConfigsDir(home), { recursive: true });
7171
- await fs16.copyFile(path18.join(backup, ymlFile), destYml);
7315
+ await fs15.mkdir(containerConfigsDir(home), { recursive: true });
7316
+ await fs15.copyFile(path18.join(backup, ymlFile), destYml);
7172
7317
  if (hasEnv) {
7173
- await fs16.copyFile(envInBackup, containerEnvPath(name, home));
7318
+ await fs15.copyFile(envInBackup, containerEnvPath(name, home));
7174
7319
  }
7175
7320
  if (hasContainer) {
7176
- await fs16.cp(containerInBackup, destContainer, { recursive: true });
7321
+ await fs15.cp(containerInBackup, destContainer, { recursive: true });
7177
7322
  }
7178
7323
  logger.success(`Restored '${name}' from ${prettyPath(backup)}.`);
7179
7324
  logger.info(
@@ -7431,7 +7576,7 @@ import { defineCommand as defineCommand24 } from "citty";
7431
7576
  import { consola as consola28 } from "consola";
7432
7577
 
7433
7578
  // src/devcontainer/shell.ts
7434
- import { existsSync as existsSync13 } from "fs";
7579
+ import { existsSync as existsSync12 } from "fs";
7435
7580
  import path19 from "path";
7436
7581
  async function runShell(opts) {
7437
7582
  assertContainerExists(opts.root);
@@ -7455,7 +7600,7 @@ async function runShell(opts) {
7455
7600
  );
7456
7601
  }
7457
7602
  function assertContainerExists(root) {
7458
- if (!existsSync13(path19.join(root, ".devcontainer"))) {
7603
+ if (!existsSync12(path19.join(root, ".devcontainer"))) {
7459
7604
  throw new Error(
7460
7605
  `No .devcontainer/ at ${root}. Run \`monoceros apply <name>\` first.`
7461
7606
  );
@@ -7667,15 +7812,15 @@ import { defineCommand as defineCommand29 } from "citty";
7667
7812
  import { consola as consola33 } from "consola";
7668
7813
 
7669
7814
  // src/tunnel/run.ts
7670
- import { spawn as spawn10 } from "child_process";
7815
+ import { spawn as spawn8 } from "child_process";
7671
7816
  import { consola as consola32 } from "consola";
7672
7817
 
7673
7818
  // src/tunnel/resolve.ts
7674
- import { existsSync as existsSync14 } from "fs";
7819
+ import { existsSync as existsSync13 } from "fs";
7675
7820
  import path20 from "path";
7676
7821
  async function resolveTunnelTarget(opts) {
7677
7822
  const ymlPath = containerConfigPath(opts.name, opts.monocerosHome);
7678
- if (!existsSync14(ymlPath)) {
7823
+ if (!existsSync13(ymlPath)) {
7679
7824
  throw new Error(
7680
7825
  `No yml profile for '${opts.name}' at ${ymlPath}. Run \`monoceros init ${opts.name}\` first.`
7681
7826
  );
@@ -7683,13 +7828,13 @@ async function resolveTunnelTarget(opts) {
7683
7828
  const parsed = await readConfig(ymlPath);
7684
7829
  const config = parsed.config;
7685
7830
  const containerRoot = containerDir(opts.name, opts.monocerosHome);
7686
- if (!existsSync14(containerRoot)) {
7831
+ if (!existsSync13(containerRoot)) {
7687
7832
  throw new Error(
7688
7833
  `Container '${opts.name}' is not materialised at ${containerRoot}. Run \`monoceros apply ${opts.name}\` first.`
7689
7834
  );
7690
7835
  }
7691
7836
  const composePath = path20.join(containerRoot, ".devcontainer", "compose.yaml");
7692
- const isCompose = existsSync14(composePath);
7837
+ const isCompose = existsSync13(composePath);
7693
7838
  const parsedTarget = parseTargetArg(opts.target, config);
7694
7839
  const docker = opts.docker ?? defaultDockerExec;
7695
7840
  if (isCompose) {
@@ -7910,7 +8055,7 @@ function formatLocalPortHeldError(port, address, result) {
7910
8055
  // src/tunnel/run.ts
7911
8056
  var SOCAT_IMAGE = "alpine/socat:1.8.0.3";
7912
8057
  var defaultDockerSpawn = (args) => {
7913
- const child = spawn10("docker", args, {
8058
+ const child = spawn8("docker", args, {
7914
8059
  stdio: "inherit"
7915
8060
  });
7916
8061
  const exited = new Promise((resolve, reject) => {