@getmonoceros/workbench 1.13.1 → 1.13.3

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) ?? []);
@@ -251,25 +254,25 @@ function detectHelpRequest(argv, main2) {
251
254
  const separatorIdx = argv.indexOf("--");
252
255
  if (helpIdx === -1) return null;
253
256
  if (separatorIdx !== -1 && separatorIdx < helpIdx) return null;
254
- const path21 = [];
257
+ const path20 = [];
255
258
  const tokens = argv.slice(
256
259
  0,
257
260
  separatorIdx === -1 ? argv.length : separatorIdx
258
261
  );
259
262
  let cursor = main2;
260
263
  const mainName = (main2.meta ?? {}).name ?? "monoceros";
261
- path21.push(mainName);
264
+ path20.push(mainName);
262
265
  for (const tok of tokens) {
263
266
  if (tok.startsWith("-")) continue;
264
267
  const subs = cursor.subCommands ?? {};
265
268
  if (tok in subs) {
266
269
  cursor = subs[tok];
267
- path21.push(tok);
270
+ path20.push(tok);
268
271
  continue;
269
272
  }
270
273
  break;
271
274
  }
272
- return { path: path21, cmd: cursor };
275
+ return { path: path20, cmd: cursor };
273
276
  }
274
277
  async function maybeRenderHelp(argv, main2) {
275
278
  const hit = detectHelpRequest(argv, main2);
@@ -356,13 +359,12 @@ var FeatureEntrySchema = z.object({
356
359
  options: z.record(z.string(), FeatureOptionValueSchema).optional()
357
360
  });
358
361
  var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
362
+ function isValidEmail(value) {
363
+ return EMAIL_RE.test(value);
364
+ }
359
365
  var GitUserSchema = z.object({
360
366
  name: z.union([z.literal(""), z.null(), z.string().min(1)]).nullish().transform((v) => typeof v === "string" && v.length > 0 ? v : void 0),
361
- email: z.union([
362
- z.literal(""),
363
- z.null(),
364
- z.string().regex(EMAIL_RE, "Invalid email")
365
- ]).nullish().transform((v) => typeof v === "string" && v.length > 0 ? v : void 0)
367
+ email: z.union([z.literal(""), z.null(), z.string().min(1)]).nullish().transform((v) => typeof v === "string" && v.length > 0 ? v : void 0)
366
368
  });
367
369
  var RepoEntrySchema = z.object({
368
370
  url: z.string().regex(
@@ -717,6 +719,23 @@ function interpolateServices(services, vars) {
717
719
  });
718
720
  return { services: resolved, missing };
719
721
  }
722
+ var GIT_IDENTITY_VAR = {
723
+ name: "GIT_USER_NAME",
724
+ email: "GIT_USER_EMAIL"
725
+ };
726
+ function hasVarPlaceholder(value) {
727
+ return /\$\{[A-Za-z_][A-Za-z0-9_]*\}/.test(value);
728
+ }
729
+ function resolveGitUserFields(user, vars) {
730
+ const resolve = (raw) => {
731
+ if (raw === void 0) return {};
732
+ const r = interpolate(raw, vars);
733
+ if (r.missing.length > 0) return {};
734
+ const trimmed = r.value.trim();
735
+ return trimmed.length > 0 ? { value: trimmed } : {};
736
+ };
737
+ return { name: resolve(user.name), email: resolve(user.email) };
738
+ }
720
739
  function interpolateFeatures(features, vars) {
721
740
  const missing = [];
722
741
  const out = {};
@@ -1362,7 +1381,15 @@ var MonocerosConfigSchema = z2.object({
1362
1381
  // `git: null` for an empty mapping, which zod's plain
1363
1382
  // `.optional()` would reject.
1364
1383
  git: z2.object({
1365
- user: GitUserSchema.optional()
1384
+ // Strict email here: monoceros-config defaults are not tied to
1385
+ // any container `<name>.env`, so `${VAR}` placeholders make no
1386
+ // sense and the format can (and should) be validated at load
1387
+ // time — unlike the container/repo `git.user`, which defers to
1388
+ // apply after interpolation.
1389
+ user: GitUserSchema.optional().refine(
1390
+ (u) => u?.email === void 0 || isValidEmail(u.email),
1391
+ { message: "Invalid email in defaults.git.user", path: ["email"] }
1392
+ )
1366
1393
  }).nullish(),
1367
1394
  // .nullish() for the same reason as `git` — the sample keeps
1368
1395
  // `features:` uncommented as a category marker.
@@ -2823,6 +2850,17 @@ function setContainerGitUserInDoc(doc, user) {
2823
2850
  relocateLeakedSectionComments(doc);
2824
2851
  return true;
2825
2852
  }
2853
+ function ensureContainerGitUserPlaceholder(doc) {
2854
+ const gitNode = doc.get("git", true);
2855
+ if (gitNode && isMap2(gitNode)) {
2856
+ const userNode = gitNode.get("user", true);
2857
+ if (userNode && isMap2(userNode)) return false;
2858
+ }
2859
+ return setContainerGitUserInDoc(doc, {
2860
+ name: `\${${GIT_IDENTITY_VAR.name}}`,
2861
+ email: `\${${GIT_IDENTITY_VAR.email}}`
2862
+ });
2863
+ }
2826
2864
  function relocateLeakedSectionComments(doc) {
2827
2865
  const root = doc.contents;
2828
2866
  if (!root || !isMap2(root)) return;
@@ -3132,8 +3170,8 @@ function removeRepoFromDoc(doc, urlOrPath) {
3132
3170
  if (!isMap2(item)) return false;
3133
3171
  const url = item.get("url");
3134
3172
  if (url === urlOrPath) return true;
3135
- const path21 = item.get("path");
3136
- const effectivePath = typeof path21 === "string" ? path21 : typeof url === "string" ? deriveRepoName(url) : void 0;
3173
+ const path20 = item.get("path");
3174
+ const effectivePath = typeof path20 === "string" ? path20 : typeof url === "string" ? deriveRepoName(url) : void 0;
3137
3175
  return effectivePath === urlOrPath;
3138
3176
  });
3139
3177
  if (idx < 0) return false;
@@ -3226,7 +3264,7 @@ async function runAddRepo(input) {
3226
3264
  "Missing repo URL. Usage: monoceros add-repo <containername> <url>."
3227
3265
  );
3228
3266
  }
3229
- const path21 = (input.path ?? deriveRepoName(url)).trim();
3267
+ const path20 = (input.path ?? deriveRepoName(url)).trim();
3230
3268
  const hasName = typeof input.gitName === "string" && input.gitName.trim().length > 0;
3231
3269
  const hasEmail = typeof input.gitEmail === "string" && input.gitEmail.trim().length > 0;
3232
3270
  if (hasName !== hasEmail) {
@@ -3234,6 +3272,14 @@ async function runAddRepo(input) {
3234
3272
  "--git-name and --git-email must be set together. Pass both, or neither."
3235
3273
  );
3236
3274
  }
3275
+ if (hasEmail) {
3276
+ const email = input.gitEmail.trim();
3277
+ if (!isValidEmail(email) && !hasVarPlaceholder(email)) {
3278
+ throw new Error(
3279
+ `Invalid --git-email '${email}': must be a valid email or a \${VAR} placeholder resolved from <name>.env.`
3280
+ );
3281
+ }
3282
+ }
3237
3283
  const explicitProvider = normalizeProvider(input.provider);
3238
3284
  let host;
3239
3285
  try {
@@ -3255,7 +3301,7 @@ async function runAddRepo(input) {
3255
3301
  const providerToWrite = !canonical && explicitProvider ? explicitProvider : void 0;
3256
3302
  const entry2 = {
3257
3303
  url,
3258
- path: path21,
3304
+ path: path20,
3259
3305
  ...hasName && hasEmail ? {
3260
3306
  gitUser: {
3261
3307
  name: input.gitName.trim(),
@@ -3264,7 +3310,23 @@ async function runAddRepo(input) {
3264
3310
  } : {},
3265
3311
  ...providerToWrite ? { provider: providerToWrite } : {}
3266
3312
  };
3267
- const result = await mutate(input, (doc) => addRepoToDoc(doc, entry2));
3313
+ let gitUserScaffolded = false;
3314
+ const result = await mutate(input, (doc) => {
3315
+ const repoAdded = addRepoToDoc(doc, entry2);
3316
+ if (repoAdded) gitUserScaffolded = ensureContainerGitUserPlaceholder(doc);
3317
+ return repoAdded;
3318
+ });
3319
+ if (result.status === "updated" && gitUserScaffolded) {
3320
+ const home = input.monocerosHome ?? monocerosHome();
3321
+ await ensureEnvGitignored(containerConfigsDir(home));
3322
+ await ensureEnvVars(containerEnvPath(input.name, home), input.name, [
3323
+ GIT_IDENTITY_VAR.name,
3324
+ GIT_IDENTITY_VAR.email
3325
+ ]);
3326
+ (input.logger ?? defaultLogger()).info(
3327
+ `Added a container git.user with \${${GIT_IDENTITY_VAR.name}}/\${${GIT_IDENTITY_VAR.email}} placeholders and seeded ${input.name}.env \u2014 fill them or leave blank to use your global git identity.`
3328
+ );
3329
+ }
3268
3330
  if (result.status === "updated") {
3269
3331
  await tryCloneInRunningContainer(input, entry2);
3270
3332
  }
@@ -4078,7 +4140,7 @@ var addServiceCommand = defineCommand7({
4078
4140
  import { defineCommand as defineCommand8 } from "citty";
4079
4141
 
4080
4142
  // src/apply/index.ts
4081
- import { existsSync as existsSync8, promises as fs12 } from "fs";
4143
+ import { existsSync as existsSync7, promises as fs11 } from "fs";
4082
4144
  import { consola as consola11 } from "consola";
4083
4145
 
4084
4146
  // src/config/state.ts
@@ -4520,230 +4582,11 @@ function runLogs(opts) {
4520
4582
  );
4521
4583
  }
4522
4584
 
4523
- // src/devcontainer/repo-reachability.ts
4524
- import { spawn as spawn6 } from "child_process";
4525
- var realGitLsRemote = (url) => {
4526
- return new Promise((resolve, reject) => {
4527
- const child = spawn6("git", ["ls-remote", "--heads", "--", url], {
4528
- stdio: ["ignore", "pipe", "pipe"],
4529
- env: {
4530
- ...process.env,
4531
- GIT_TERMINAL_PROMPT: "0"
4532
- }
4533
- });
4534
- let stdout = "";
4535
- let stderr = "";
4536
- child.stdout.on("data", (chunk) => {
4537
- stdout += chunk.toString();
4538
- });
4539
- child.stderr.on("data", (chunk) => {
4540
- stderr += chunk.toString();
4541
- });
4542
- child.on("error", reject);
4543
- child.on(
4544
- "exit",
4545
- (code) => resolve({ stdout, stderr, exitCode: code ?? 0 })
4546
- );
4547
- });
4548
- };
4549
- function classifyStderr(stderr) {
4550
- const s = stderr.toLowerCase();
4551
- 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")) {
4552
- return "dns";
4553
- }
4554
- 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")) {
4555
- return "not-found-or-no-access";
4556
- }
4557
- 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")) {
4558
- return "auth-failed";
4559
- }
4560
- return "unknown";
4561
- }
4562
- async function checkRepoReachability(repos, options = {}) {
4563
- const spawnFn = options.spawn ?? realGitLsRemote;
4564
- const results = [];
4565
- for (const repo of repos) {
4566
- let result;
4567
- try {
4568
- result = await spawnFn(repo.url);
4569
- } catch (err) {
4570
- results.push({
4571
- url: repo.url,
4572
- ok: false,
4573
- kind: "unknown",
4574
- detail: err instanceof Error ? err.message : String(err)
4575
- });
4576
- continue;
4577
- }
4578
- if (result.exitCode === 0) {
4579
- results.push({ url: repo.url, ok: true, detail: "" });
4580
- continue;
4581
- }
4582
- results.push({
4583
- url: repo.url,
4584
- ok: false,
4585
- kind: classifyStderr(result.stderr),
4586
- detail: result.stderr.trim()
4587
- });
4588
- }
4589
- return results;
4590
- }
4591
- function formatUnreachableReposError(failures) {
4592
- const byKind = /* @__PURE__ */ new Map();
4593
- for (const f of failures) {
4594
- const kind = f.kind ?? "unknown";
4595
- const list = byKind.get(kind) ?? [];
4596
- list.push(f);
4597
- byKind.set(kind, list);
4598
- }
4599
- const totalUrls = failures.length;
4600
- const lines = [
4601
- totalUrls === 1 ? `Cannot reach declared repo: ${failures[0].url}` : `Cannot reach ${totalUrls} declared repos:`,
4602
- ""
4603
- ];
4604
- const sectionOrder = [
4605
- "not-found-or-no-access",
4606
- "auth-failed",
4607
- "dns",
4608
- "unknown"
4609
- ];
4610
- for (const kind of sectionOrder) {
4611
- const entries = byKind.get(kind);
4612
- if (!entries || entries.length === 0) continue;
4613
- lines.push(headerForKind(kind));
4614
- for (const e of entries) {
4615
- lines.push(` \u2022 ${e.url}`);
4616
- if (e.detail) {
4617
- for (const detailLine of e.detail.split("\n")) {
4618
- const trimmed = detailLine.trim();
4619
- if (trimmed) lines.push(` git: ${trimmed}`);
4620
- }
4621
- }
4622
- }
4623
- for (const advice of adviceForKind(kind)) {
4624
- lines.push(` - ${advice}`);
4625
- }
4626
- lines.push("");
4627
- }
4628
- lines.push(`Then re-run ${cyan2("monoceros apply")}.`);
4629
- return lines.join("\n");
4630
- }
4631
- function headerForKind(kind) {
4632
- switch (kind) {
4633
- case "not-found-or-no-access":
4634
- return "Repository not found (or your credentials don't grant access):";
4635
- case "auth-failed":
4636
- return "Authentication failed (credentials are present but rejected):";
4637
- case "dns":
4638
- return "Host unreachable (DNS / VPN / offline \u2014 git couldn't resolve the hostname):";
4639
- case "unknown":
4640
- return "Unrecognised git error:";
4641
- }
4642
- }
4643
- function adviceForKind(kind) {
4644
- switch (kind) {
4645
- case "not-found-or-no-access":
4646
- return [
4647
- "Re-check the URL for typos (case-sensitive on most hosts).",
4648
- "Verify the repo still exists / is not archived in a way that hides it.",
4649
- "Ensure your token covers this org / workspace and has read scope (GitHub: `repo`; GitLab: `read_repository`; Bitbucket: repo read)."
4650
- ];
4651
- case "auth-failed":
4652
- return [
4653
- "Token may be expired or revoked \u2014 regenerate it from the provider UI.",
4654
- "Re-run the provider CLI login (gh auth login / glab auth login) \u2014 Monoceros picks up the refreshed token on the next apply."
4655
- ];
4656
- case "dns":
4657
- return [
4658
- "Check your internet / VPN \u2014 corporate Git hosts often require VPN.",
4659
- "Verify the hostname spelling in the yml."
4660
- ];
4661
- case "unknown":
4662
- return [
4663
- "Run `git ls-remote <url>` manually on the host to see the raw error."
4664
- ];
4665
- }
4666
- }
4667
-
4668
- // src/devcontainer/repo-clone.ts
4669
- import { spawn as spawn7 } from "child_process";
4670
- import { existsSync as existsSync7, promises as fs10 } from "fs";
4671
- import path13 from "path";
4672
- var realGitClone = (url, dest) => {
4673
- return new Promise((resolve, reject) => {
4674
- const child = spawn7("git", ["clone", "--", url, dest], {
4675
- stdio: ["ignore", "pipe", "pipe"],
4676
- env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
4677
- });
4678
- let stdout = "";
4679
- let stderr = "";
4680
- child.stdout.on("data", (c) => {
4681
- stdout += c.toString();
4682
- });
4683
- child.stderr.on("data", (c) => {
4684
- stderr += c.toString();
4685
- });
4686
- child.on("error", reject);
4687
- child.on(
4688
- "exit",
4689
- (code) => resolve({ stdout, stderr, exitCode: code ?? 0 })
4690
- );
4691
- });
4692
- };
4693
- async function cloneReposHostSide(containerRoot, repos, options = {}) {
4694
- const spawnFn = options.spawn ?? realGitClone;
4695
- const results = [];
4696
- for (const repo of repos) {
4697
- const dest = path13.join(containerRoot, "projects", repo.path);
4698
- if (existsSync7(dest)) {
4699
- results.push({ path: repo.path, url: repo.url, status: "skipped" });
4700
- continue;
4701
- }
4702
- await fs10.mkdir(path13.dirname(dest), { recursive: true });
4703
- let r;
4704
- try {
4705
- r = await spawnFn(repo.url, dest);
4706
- } catch (err) {
4707
- results.push({
4708
- path: repo.path,
4709
- url: repo.url,
4710
- status: "failed",
4711
- detail: err instanceof Error ? err.message : String(err)
4712
- });
4713
- continue;
4714
- }
4715
- results.push(
4716
- r.exitCode === 0 ? { path: repo.path, url: repo.url, status: "cloned" } : {
4717
- path: repo.path,
4718
- url: repo.url,
4719
- status: "failed",
4720
- detail: r.stderr.trim()
4721
- }
4722
- );
4723
- }
4724
- return results;
4725
- }
4726
- function formatCloneFailuresError(failures) {
4727
- const lines = failures.length === 1 ? [`Failed to clone declared repo: ${failures[0].url}`, ""] : [`Failed to clone ${failures.length} declared repos:`, ""];
4728
- for (const f of failures) {
4729
- lines.push(` \u2022 ${f.url} \u2192 projects/${f.path}`);
4730
- if (f.detail) lines.push(` ${f.detail}`);
4731
- }
4732
- lines.push("");
4733
- lines.push(
4734
- "Reachability was confirmed earlier, so this is usually a local issue"
4735
- );
4736
- lines.push(
4737
- "(disk space, a leftover non-empty target dir). Fix it and re-run " + cyan2("monoceros apply") + "."
4738
- );
4739
- return lines.join("\n");
4740
- }
4741
-
4742
4585
  // src/devcontainer/docker-mode.ts
4743
- import { spawn as spawn8 } from "child_process";
4586
+ import { spawn as spawn6 } from "child_process";
4744
4587
  var realDockerInfo = () => {
4745
4588
  return new Promise((resolve, reject) => {
4746
- const child = spawn8(
4589
+ const child = spawn6(
4747
4590
  "docker",
4748
4591
  ["info", "--format", "{{json .SecurityOptions}}"],
4749
4592
  {
@@ -4802,13 +4645,13 @@ function formatRootlessNotSupportedError() {
4802
4645
  }
4803
4646
 
4804
4647
  // src/devcontainer/identity.ts
4805
- import { spawn as spawn9 } from "child_process";
4806
- import { promises as fs11 } from "fs";
4807
- import path14 from "path";
4648
+ import { spawn as spawn7 } from "child_process";
4649
+ import { promises as fs10 } from "fs";
4650
+ import path13 from "path";
4808
4651
  import { consola as consola10 } from "consola";
4809
4652
  var realGitConfigGet = (key) => {
4810
4653
  return new Promise((resolve, reject) => {
4811
- const child = spawn9("git", ["config", "--global", "--get", key], {
4654
+ const child = spawn7("git", ["config", "--global", "--get", key], {
4812
4655
  stdio: ["ignore", "pipe", "inherit"]
4813
4656
  });
4814
4657
  let stdout = "";
@@ -4918,8 +4761,8 @@ async function resolveIdentityWithPrompt(options = {}) {
4918
4761
  };
4919
4762
  }
4920
4763
  async function collectGitIdentity(devContainerRoot, options = {}) {
4921
- const gitconfigDir = path14.join(devContainerRoot, ".monoceros");
4922
- const gitconfigPath = path14.join(gitconfigDir, "gitconfig");
4764
+ const gitconfigDir = path13.join(devContainerRoot, ".monoceros");
4765
+ const gitconfigPath = path13.join(gitconfigDir, "gitconfig");
4923
4766
  const logger = options.logger ?? { info: () => {
4924
4767
  }, warn: () => {
4925
4768
  } };
@@ -4932,8 +4775,8 @@ async function collectGitIdentity(devContainerRoot, options = {}) {
4932
4775
  const lines = ["[user]"];
4933
4776
  if (resolved.name !== void 0) lines.push(` name = ${resolved.name}`);
4934
4777
  if (resolved.email !== void 0) lines.push(` email = ${resolved.email}`);
4935
- await fs11.mkdir(gitconfigDir, { recursive: true });
4936
- await fs11.writeFile(gitconfigPath, lines.join("\n") + "\n");
4778
+ await fs10.mkdir(gitconfigDir, { recursive: true });
4779
+ await fs10.writeFile(gitconfigPath, lines.join("\n") + "\n");
4937
4780
  return {
4938
4781
  ...resolved.name !== void 0 ? { name: resolved.name } : {},
4939
4782
  ...resolved.email !== void 0 ? { email: resolved.email } : {},
@@ -4976,7 +4819,7 @@ async function readKeyFromHost(spawnFn, key, logger) {
4976
4819
  }
4977
4820
  async function readExistingGitconfig(filePath) {
4978
4821
  try {
4979
- const content = await fs11.readFile(filePath, "utf8");
4822
+ const content = await fs10.readFile(filePath, "utf8");
4980
4823
  const result = {};
4981
4824
  const nameMatch = /^\s*name\s*=\s*(.+?)\s*$/m.exec(content);
4982
4825
  const emailMatch = /^\s*email\s*=\s*(.+?)\s*$/m.exec(content);
@@ -5009,7 +4852,7 @@ ${sectionLine(label)}
5009
4852
  );
5010
4853
  }
5011
4854
  const ymlPath = containerConfigPath(opts.name, home);
5012
- if (!existsSync8(ymlPath)) {
4855
+ if (!existsSync7(ymlPath)) {
5013
4856
  throw new Error(
5014
4857
  `No such config: ${ymlPath}. Run \`monoceros init <template> ${opts.name}\` first.`
5015
4858
  );
@@ -5040,6 +4883,44 @@ ${sectionLine(label)}
5040
4883
  }
5041
4884
  createOpts.services = interpServices.services;
5042
4885
  if (createOpts.features) createOpts.features = interpFeatures.features;
4886
+ const gitUserErrors = [];
4887
+ let containerGitOverride;
4888
+ if (parsed.config.git?.user) {
4889
+ const f = resolveGitUserFields(parsed.config.git.user, envVars);
4890
+ if (f.email.value !== void 0 && !isValidEmail(f.email.value)) {
4891
+ gitUserErrors.push(
4892
+ `git.user.email resolved to "${f.email.value}", which is not a valid email`
4893
+ );
4894
+ }
4895
+ const override2 = {
4896
+ ...f.name.value !== void 0 ? { name: f.name.value } : {},
4897
+ ...f.email.value !== void 0 ? { email: f.email.value } : {}
4898
+ };
4899
+ if (Object.keys(override2).length > 0) containerGitOverride = override2;
4900
+ }
4901
+ for (const repo of createOpts.repos ?? []) {
4902
+ if (!repo.gitUser) continue;
4903
+ const f = resolveGitUserFields(repo.gitUser, envVars);
4904
+ if (f.name.value === void 0 || f.email.value === void 0) {
4905
+ delete repo.gitUser;
4906
+ continue;
4907
+ }
4908
+ if (!isValidEmail(f.email.value)) {
4909
+ gitUserErrors.push(
4910
+ `repos[${repo.path}].git.user.email resolved to "${f.email.value}", which is not a valid email`
4911
+ );
4912
+ continue;
4913
+ }
4914
+ repo.gitUser = { name: f.name.value, email: f.email.value };
4915
+ }
4916
+ if (gitUserErrors.length > 0) {
4917
+ throw new Error(
4918
+ `Invalid git identity after resolving ${prettyPath(envPath)}:
4919
+ ` + gitUserErrors.map((e) => ` - ${e}`).join("\n") + `
4920
+
4921
+ Fix the value in the env file (or the yml).`
4922
+ );
4923
+ }
5043
4924
  validateOptions(createOpts);
5044
4925
  logger.success(`yml validated ${dim(`(${prettyPath(ymlPath)})`)}`);
5045
4926
  const hasRepos = (createOpts.repos ?? []).length > 0;
@@ -5054,7 +4935,7 @@ ${sectionLine(label)}
5054
4935
  ...opts.identitySpawn ? { spawn: opts.identitySpawn } : {},
5055
4936
  ...opts.identityPrompt ? { prompt: opts.identityPrompt } : {},
5056
4937
  ...opts.identityScopePrompt ? { scopePrompt: opts.identityScopePrompt } : {},
5057
- ...parsed.config.git?.user ? { containerOverride: parsed.config.git.user } : {},
4938
+ ...containerGitOverride ? { containerOverride: containerGitOverride } : {},
5058
4939
  ...globalConfig?.defaults?.git?.user ? { defaults: globalConfig.defaults.git.user } : {},
5059
4940
  logger: idLogger
5060
4941
  });
@@ -5077,16 +4958,6 @@ ${sectionLine(label)}
5077
4958
  throw new Error(formatMissingCredentialsError(missing));
5078
4959
  }
5079
4960
  }
5080
- const declaredRepos = createOpts.repos ?? [];
5081
- if (declaredRepos.length > 0) {
5082
- const reachability = await checkRepoReachability(declaredRepos, {
5083
- ...opts.reachabilitySpawn ? { spawn: opts.reachabilitySpawn } : {}
5084
- });
5085
- const unreachable = reachability.filter((r) => !r.ok);
5086
- if (unreachable.length > 0) {
5087
- throw new Error(formatUnreachableReposError(unreachable));
5088
- }
5089
- }
5090
4961
  section("Scaffold");
5091
4962
  const dockerMode = await detectDockerMode({
5092
4963
  ...opts.dockerInfoSpawn ? { spawn: opts.dockerInfoSpawn } : {}
@@ -5094,7 +4965,7 @@ ${sectionLine(label)}
5094
4965
  if (dockerMode === "rootless") {
5095
4966
  throw new Error(formatRootlessNotSupportedError());
5096
4967
  }
5097
- await fs12.mkdir(targetDir, { recursive: true });
4968
+ await fs11.mkdir(targetDir, { recursive: true });
5098
4969
  await writeScaffold(createOpts, targetDir, { dockerMode });
5099
4970
  await writeStateFile(
5100
4971
  targetDir,
@@ -5105,23 +4976,6 @@ ${sectionLine(label)}
5105
4976
  })
5106
4977
  );
5107
4978
  logger.success(`materialized into ${prettyPath(targetDir)}`);
5108
- const reposToClone = createOpts.repos ?? [];
5109
- if (reposToClone.length > 0) {
5110
- const cloneResults = await cloneReposHostSide(targetDir, reposToClone, {
5111
- ...opts.cloneSpawn ? { spawn: opts.cloneSpawn } : {}
5112
- });
5113
- for (const r of cloneResults) {
5114
- if (r.status === "cloned") {
5115
- logger.success(`cloned ${cyan2(r.path)} ${dim(`(${r.url})`)}`);
5116
- } else if (r.status === "skipped") {
5117
- logger.info(`projects/${r.path} already present \u2014 skipped clone`);
5118
- }
5119
- }
5120
- const cloneFailures = cloneResults.filter((r) => r.status === "failed");
5121
- if (cloneFailures.length > 0) {
5122
- throw new Error(formatCloneFailuresError(cloneFailures));
5123
- }
5124
- }
5125
4979
  section("Container");
5126
4980
  const featureRefs = parsed.config.features.map((f) => f.ref);
5127
4981
  if (featureRefs.length > 0) {
@@ -5169,8 +5023,8 @@ ${sectionLine(label)}
5169
5023
  return { targetDir, configPath: ymlPath, containerExitCode: exitCode };
5170
5024
  }
5171
5025
  async function assertSafeTargetDir(targetDir, expectedOrigin) {
5172
- if (!existsSync8(targetDir)) return;
5173
- const entries = await fs12.readdir(targetDir);
5026
+ if (!existsSync7(targetDir)) return;
5027
+ const entries = await fs11.readdir(targetDir);
5174
5028
  if (entries.length === 0) return;
5175
5029
  const state = await readStateFile(targetDir);
5176
5030
  if (state) {
@@ -5240,7 +5094,7 @@ async function persistPromptedIdentity(prompted, ymlPath, home, logger) {
5240
5094
  }
5241
5095
  if (wantContainer) {
5242
5096
  try {
5243
- const text = await fs12.readFile(ymlPath, "utf8");
5097
+ const text = await fs11.readFile(ymlPath, "utf8");
5244
5098
  const parsed = parseConfig(text, ymlPath);
5245
5099
  const changed = setContainerGitUserInDoc(parsed.doc, {
5246
5100
  name: prompted.name,
@@ -5248,7 +5102,7 @@ async function persistPromptedIdentity(prompted, ymlPath, home, logger) {
5248
5102
  });
5249
5103
  if (changed) {
5250
5104
  const out = stringifyConfig(parsed.doc);
5251
- await fs12.writeFile(ymlPath, out, "utf8");
5105
+ await fs11.writeFile(ymlPath, out, "utf8");
5252
5106
  logger.info(
5253
5107
  `Saved identity in this container \u2014 wrote git.user into ${prettyPath(ymlPath)}.`
5254
5108
  );
@@ -5262,7 +5116,7 @@ async function persistPromptedIdentity(prompted, ymlPath, home, logger) {
5262
5116
  }
5263
5117
 
5264
5118
  // src/version.ts
5265
- var CLI_VERSION = true ? "1.13.1" : "dev";
5119
+ var CLI_VERSION = true ? "1.13.3" : "dev";
5266
5120
 
5267
5121
  // src/commands/_dispatch.ts
5268
5122
  import { consola as consola12 } from "consola";
@@ -5323,7 +5177,7 @@ function renderCompletionScript(shell) {
5323
5177
  ' COMPREPLY=( $(compgen -W "$candidates" -- "$cur") )',
5324
5178
  " # Suppress the trailing space when bash narrowed the candidate",
5325
5179
  " # set to a single token that ends with `=` \u2014 those are value-",
5326
- " # flags (`--with=`, `--with-ports=`, \u2026) where the user types the",
5180
+ " # flags (`--with-features=`, `--with-ports=`, \u2026) where the user types the",
5327
5181
  " # value immediately after.",
5328
5182
  ' if [[ ${#COMPREPLY[@]} -eq 1 && "${COMPREPLY[0]}" == *= ]]; then',
5329
5183
  " compopt -o nospace",
@@ -5396,6 +5250,10 @@ var completionCommand = defineCommand9({
5396
5250
  meta: {
5397
5251
  name: "completion",
5398
5252
  group: "tooling",
5253
+ // Hidden from `monoceros --help`: the install scripts wire up
5254
+ // completion automatically; manual setup is documented in
5255
+ // docs/commands/completion.md. Still runnable directly.
5256
+ hidden: true,
5399
5257
  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."
5400
5258
  },
5401
5259
  args: {
@@ -5422,8 +5280,8 @@ var completionCommand = defineCommand9({
5422
5280
  import { defineCommand as defineCommand10 } from "citty";
5423
5281
 
5424
5282
  // src/completion/resolve.ts
5425
- import { existsSync as existsSync9, promises as fs13 } from "fs";
5426
- import path15 from "path";
5283
+ import { existsSync as existsSync8, promises as fs12 } from "fs";
5284
+ import path14 from "path";
5427
5285
  async function resolveCompletions(line, point, opts = {}) {
5428
5286
  const { prev, current } = parseCompletionLine(line, point);
5429
5287
  const ctx = { prev, current, opts };
@@ -5571,9 +5429,9 @@ function filterPrefix(values, fragment) {
5571
5429
  }
5572
5430
  async function listContainerNames(ctx) {
5573
5431
  const home = ctx.opts.monocerosHome ?? monocerosHome();
5574
- const dir = path15.join(home, "container-configs");
5575
- if (!existsSync9(dir)) return [];
5576
- const entries = await fs13.readdir(dir);
5432
+ const dir = path14.join(home, "container-configs");
5433
+ if (!existsSync8(dir)) return [];
5434
+ const entries = await fs12.readdir(dir);
5577
5435
  return entries.filter((e) => e.endsWith(".yml")).map((e) => e.slice(0, -".yml".length)).sort();
5578
5436
  }
5579
5437
  async function listFeatureComponents() {
@@ -5791,6 +5649,8 @@ var __completeCommand = defineCommand10({
5791
5649
  meta: {
5792
5650
  name: "__complete",
5793
5651
  group: "internal",
5652
+ // Internal plumbing for the shell wrappers — never shown in help.
5653
+ hidden: true,
5794
5654
  description: "Internal \u2014 shell completion engine. Used by the wrappers emitted by `monoceros completion <shell>`. Output one candidate completion per line."
5795
5655
  },
5796
5656
  args: {
@@ -5828,8 +5688,8 @@ import { defineCommand as defineCommand11 } from "citty";
5828
5688
  import { consola as consola14 } from "consola";
5829
5689
 
5830
5690
  // src/init/index.ts
5831
- import { existsSync as existsSync10, promises as fs14 } from "fs";
5832
- import path16 from "path";
5691
+ import { existsSync as existsSync9, promises as fs13 } from "fs";
5692
+ import path15 from "path";
5833
5693
  import { consola as consola13 } from "consola";
5834
5694
 
5835
5695
  // src/init/generator.ts
@@ -5897,6 +5757,7 @@ function generateComposedYml(name, composed, lookupManifest, repoUrls = [], port
5897
5757
  lines.push("");
5898
5758
  }
5899
5759
  if (repoUrls.length > 0) {
5760
+ pushGitIdentityBlock(lines);
5900
5761
  pushSectionHeader(
5901
5762
  lines,
5902
5763
  REPOS_HEADER,
@@ -6014,6 +5875,7 @@ function generateDocumentedYml(name, catalog, lookupManifest, repoUrls = [], por
6014
5875
  lines.push("");
6015
5876
  }
6016
5877
  if (repoUrls.length > 0) {
5878
+ pushGitIdentityBlock(lines);
6017
5879
  pushSectionHeader(
6018
5880
  lines,
6019
5881
  REPOS_HEADER,
@@ -6093,6 +5955,20 @@ function pushServiceEntry(out, svc) {
6093
5955
  var FEATURES_HEADER_ACTIVE = "A Monoceros dev-container is shaped by features \u2014 pluggable units that drop tooling (AI assistants, language CLIs, cloud SDKs, \u2026) into the container and bring their own options. The features active for this container are listed below; adjust their options as needed. Shared credentials used across containers belong in monoceros-config.yml under `defaults.features.<ref>` rather than here. Full catalog: `monoceros list-components`.";
6094
5956
  var FEATURES_HEADER_DOCUMENTED = "A Monoceros dev-container is shaped by features \u2014 pluggable units that drop tooling (AI assistants, language CLIs, cloud SDKs, \u2026) into the container and bring their own options. Un-comment the blocks below for the features you want active. Shared credentials used across containers belong in monoceros-config.yml under `defaults.features.<ref>` rather than here. Full catalog: `monoceros list-components`.";
6095
5957
  var REPOS_HEADER = "Git repositories cloned into `projects/` on container start-up. HTTPS URLs only. The provider is auto-detected for github.com / gitlab.com / bitbucket.org; for any other host (self-hosted GitLab, Gitea, \u2026) declare `provider:` explicitly. Add more later with `monoceros add-repo`.";
5958
+ var GIT_IDENTITY_HEADER = "Git committer identity for commits made inside the container. The ${VAR} values resolve from <name>.env at apply time \u2014 fill them there, or leave them blank to fall back to your global git config (or a one-time prompt). Override per repo under repos[].git.user.";
5959
+ function pushGitIdentityBlock(lines) {
5960
+ pushSectionHeader(
5961
+ lines,
5962
+ GIT_IDENTITY_HEADER,
5963
+ /* commented */
5964
+ false
5965
+ );
5966
+ lines.push("git:");
5967
+ lines.push(" user:");
5968
+ lines.push(` name: \${${GIT_IDENTITY_VAR.name}}`);
5969
+ lines.push(` email: \${${GIT_IDENTITY_VAR.email}}`);
5970
+ lines.push("");
5971
+ }
6096
5972
  function routingHeader(name) {
6097
5973
  return `Container ports exposed to the host through Traefik. Reach them in your browser as ${name}-<port>.localhost (e.g. ${name}-3000.localhost). The first entry is the default route and is also reachable as the bare ${name}.localhost. Manage the list with \`monoceros add-port\`.`;
6098
5974
  }
@@ -6179,7 +6055,7 @@ async function runInit(opts) {
6179
6055
  );
6180
6056
  }
6181
6057
  const dest = containerConfigPath(opts.name, home);
6182
- if (existsSync10(dest)) {
6058
+ if (existsSync9(dest)) {
6183
6059
  throw new Error(
6184
6060
  `Config already exists: ${dest}. Delete it manually before re-running \`monoceros init\` \u2014 this protects any hand-edits.`
6185
6061
  );
@@ -6237,17 +6113,6 @@ async function runInit(opts) {
6237
6113
  seenPorts.add(raw);
6238
6114
  ports.push(raw);
6239
6115
  }
6240
- let promptedIdentity;
6241
- if (repos.length > 0) {
6242
- const globalConfig = await readMonocerosConfig({ monocerosHome: home });
6243
- promptedIdentity = await resolveIdentityWithPrompt({
6244
- ...opts.identitySpawn ? { spawn: opts.identitySpawn } : {},
6245
- ...opts.identityPrompt ? { prompt: opts.identityPrompt } : {},
6246
- ...opts.identityScopePrompt ? { scopePrompt: opts.identityScopePrompt } : {},
6247
- ...globalConfig?.defaults?.git?.user ? { defaults: globalConfig.defaults.git.user } : {},
6248
- logger: { info: logger.info, warn: logger.info }
6249
- });
6250
- }
6251
6116
  let text;
6252
6117
  const composed = resolveComposedInit(catalog, {
6253
6118
  languages: opts.languages ?? [],
@@ -6261,9 +6126,9 @@ async function runInit(opts) {
6261
6126
  } else {
6262
6127
  text = generateComposedYml(opts.name, composed, lookup, repos, ports);
6263
6128
  }
6264
- await fs14.mkdir(containerConfigsDir(home), { recursive: true });
6129
+ await fs13.mkdir(containerConfigsDir(home), { recursive: true });
6265
6130
  await ensureEnvGitignored(containerConfigsDir(home));
6266
- await fs14.writeFile(dest, text, "utf8");
6131
+ await fs13.writeFile(dest, text, "utf8");
6267
6132
  const envPath = containerEnvPath(opts.name, home);
6268
6133
  const seedVars = {};
6269
6134
  for (const f of composed.features) {
@@ -6280,55 +6145,14 @@ async function runInit(opts) {
6280
6145
  Object.assign(seedVars, curatedServiceEnvDefaults(svc.name));
6281
6146
  }
6282
6147
  }
6283
- await ensureEnvVars(envPath, opts.name, seedVars);
6284
- if (promptedIdentity?.prompted) {
6285
- const { name, email, scope } = promptedIdentity.prompted;
6286
- if (scope === "g" || scope === "b") {
6287
- try {
6288
- const result = await writeGlobalDefaultGitUser(
6289
- { name, email },
6290
- { monocerosHome: home }
6291
- );
6292
- if (result.alreadySet) {
6293
- logger.info(
6294
- `monoceros-config.yml already had a defaults.git.user \u2014 left it alone.`
6295
- );
6296
- } else if (result.created) {
6297
- logger.info(
6298
- `Saved identity globally \u2014 created ${prettyPath(result.filePath)} with defaults.git.user.`
6299
- );
6300
- } else {
6301
- logger.info(
6302
- `Saved identity globally to ${prettyPath(result.filePath)}.`
6303
- );
6304
- }
6305
- } catch (err) {
6306
- logger.info(
6307
- `Could not persist identity to monoceros-config.yml: ${err instanceof Error ? err.message : String(err)}. \`monoceros apply\` will re-prompt.`
6308
- );
6309
- }
6310
- }
6311
- if (scope === "c" || scope === "b") {
6312
- try {
6313
- const written = await fs14.readFile(dest, "utf8");
6314
- const parsed = parseConfig(written, dest);
6315
- const changed = setContainerGitUserInDoc(parsed.doc, { name, email });
6316
- if (changed) {
6317
- await fs14.writeFile(dest, stringifyConfig(parsed.doc), "utf8");
6318
- logger.info(
6319
- `Saved identity in ${prettyPath(dest)} (container-level git.user).`
6320
- );
6321
- }
6322
- } catch (err) {
6323
- logger.info(
6324
- `Could not persist identity into ${prettyPath(dest)}: ${err instanceof Error ? err.message : String(err)}. \`monoceros apply\` will re-prompt.`
6325
- );
6326
- }
6327
- }
6148
+ if (repos.length > 0) {
6149
+ seedVars[GIT_IDENTITY_VAR.name] = "";
6150
+ seedVars[GIT_IDENTITY_VAR.email] = "";
6328
6151
  }
6152
+ await ensureEnvVars(envPath, opts.name, seedVars);
6329
6153
  const documented = !anyComposed;
6330
- const ymlRel = path16.relative(home, dest);
6331
- const envRel = path16.relative(home, envPath);
6154
+ const ymlRel = path15.relative(home, dest);
6155
+ const envRel = path15.relative(home, envPath);
6332
6156
  if (documented) {
6333
6157
  logger.success(`Wrote documented default to ${ymlRel} and ${envRel}.`);
6334
6158
  logger.info(
@@ -6594,7 +6418,7 @@ var listComponentsCommand = defineCommand12({
6594
6418
  meta: {
6595
6419
  name: "list-components",
6596
6420
  group: "discovery",
6597
- 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."
6421
+ 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."
6598
6422
  },
6599
6423
  args: {},
6600
6424
  async run() {
@@ -6863,8 +6687,8 @@ import { consola as consola20 } from "consola";
6863
6687
  import { createInterface } from "readline/promises";
6864
6688
 
6865
6689
  // src/remove/index.ts
6866
- import { existsSync as existsSync11, promises as fs15 } from "fs";
6867
- import path17 from "path";
6690
+ import { existsSync as existsSync10, promises as fs14 } from "fs";
6691
+ import path16 from "path";
6868
6692
  import { consola as consola19 } from "consola";
6869
6693
  async function runRemove(opts) {
6870
6694
  const home = opts.monocerosHome ?? monocerosHome();
@@ -6881,9 +6705,9 @@ async function runRemove(opts) {
6881
6705
  const ymlPath = containerConfigPath(opts.name, home);
6882
6706
  const envPath = containerEnvPath(opts.name, home);
6883
6707
  const containerPath = containerDir(opts.name, home);
6884
- const hasYml = existsSync11(ymlPath);
6885
- const hasEnv = existsSync11(envPath);
6886
- const hasContainer = existsSync11(containerPath);
6708
+ const hasYml = existsSync10(ymlPath);
6709
+ const hasEnv = existsSync10(envPath);
6710
+ const hasContainer = existsSync10(containerPath);
6887
6711
  if (!hasYml && !hasContainer) {
6888
6712
  throw new Error(
6889
6713
  `Nothing to remove for '${opts.name}': neither ${ymlPath} nor ${containerPath} exists.`
@@ -6907,30 +6731,30 @@ async function runRemove(opts) {
6907
6731
  let backupPath = null;
6908
6732
  if (!opts.noBackup && (hasYml || hasContainer)) {
6909
6733
  const ts = (opts.now ?? /* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
6910
- backupPath = path17.join(home, "container-backups", `${opts.name}-${ts}`);
6911
- await fs15.mkdir(backupPath, { recursive: true });
6734
+ backupPath = path16.join(home, "container-backups", `${opts.name}-${ts}`);
6735
+ await fs14.mkdir(backupPath, { recursive: true });
6912
6736
  if (hasYml) {
6913
- await fs15.copyFile(ymlPath, path17.join(backupPath, `${opts.name}.yml`));
6737
+ await fs14.copyFile(ymlPath, path16.join(backupPath, `${opts.name}.yml`));
6914
6738
  }
6915
6739
  if (hasEnv) {
6916
- await fs15.copyFile(envPath, path17.join(backupPath, `${opts.name}.env`));
6740
+ await fs14.copyFile(envPath, path16.join(backupPath, `${opts.name}.env`));
6917
6741
  }
6918
6742
  if (hasContainer) {
6919
- await fs15.cp(containerPath, path17.join(backupPath, "container"), {
6743
+ await fs14.cp(containerPath, path16.join(backupPath, "container"), {
6920
6744
  recursive: true
6921
6745
  });
6922
6746
  }
6923
6747
  logger.info(`Backup written to ${prettyPath(backupPath)}.`);
6924
6748
  }
6925
6749
  if (hasYml) {
6926
- await fs15.rm(ymlPath, { force: true });
6750
+ await fs14.rm(ymlPath, { force: true });
6927
6751
  }
6928
6752
  if (hasEnv) {
6929
- await fs15.rm(envPath, { force: true });
6753
+ await fs14.rm(envPath, { force: true });
6930
6754
  }
6931
6755
  if (hasContainer) {
6932
6756
  try {
6933
- await fs15.rm(containerPath, { recursive: true, force: true });
6757
+ await fs14.rm(containerPath, { recursive: true, force: true });
6934
6758
  } catch (err) {
6935
6759
  const code = err.code;
6936
6760
  if (code !== "EACCES" && code !== "EPERM") {
@@ -6956,7 +6780,7 @@ async function runRemove(opts) {
6956
6780
  `docker-based cleanup of ${containerPath} exited ${exit}. Inspect with \`sudo ls -la ${containerPath}\` and clean manually.`
6957
6781
  );
6958
6782
  }
6959
- await fs15.rm(containerPath, { recursive: true, force: true });
6783
+ await fs14.rm(containerPath, { recursive: true, force: true });
6960
6784
  }
6961
6785
  }
6962
6786
  logger.success(
@@ -7059,8 +6883,8 @@ import { defineCommand as defineCommand18 } from "citty";
7059
6883
  import { consola as consola22 } from "consola";
7060
6884
 
7061
6885
  // src/restore/index.ts
7062
- import { existsSync as existsSync12, promises as fs16 } from "fs";
7063
- import path18 from "path";
6886
+ import { existsSync as existsSync11, promises as fs15 } from "fs";
6887
+ import path17 from "path";
7064
6888
  import { consola as consola21 } from "consola";
7065
6889
  async function runRestore(opts) {
7066
6890
  const home = opts.monocerosHome ?? monocerosHome();
@@ -7068,15 +6892,15 @@ async function runRestore(opts) {
7068
6892
  info: (msg) => consola21.info(msg),
7069
6893
  success: (msg) => consola21.success(msg)
7070
6894
  };
7071
- const backup = path18.resolve(opts.backupPath);
7072
- if (!existsSync12(backup)) {
6895
+ const backup = path17.resolve(opts.backupPath);
6896
+ if (!existsSync11(backup)) {
7073
6897
  throw new Error(`Backup not found: ${backup}.`);
7074
6898
  }
7075
- const stat = await fs16.stat(backup);
6899
+ const stat = await fs15.stat(backup);
7076
6900
  if (!stat.isDirectory()) {
7077
6901
  throw new Error(`Backup path is not a directory: ${backup}.`);
7078
6902
  }
7079
- const entries = await fs16.readdir(backup);
6903
+ const entries = await fs15.readdir(backup);
7080
6904
  const ymlFiles = entries.filter((f) => f.endsWith(".yml"));
7081
6905
  if (ymlFiles.length === 0) {
7082
6906
  throw new Error(
@@ -7090,29 +6914,29 @@ async function runRestore(opts) {
7090
6914
  }
7091
6915
  const ymlFile = ymlFiles[0];
7092
6916
  const name = ymlFile.replace(/\.yml$/, "");
7093
- const containerInBackup = path18.join(backup, "container");
7094
- const hasContainer = existsSync12(containerInBackup);
7095
- const envInBackup = path18.join(backup, `${name}.env`);
7096
- const hasEnv = existsSync12(envInBackup);
6917
+ const containerInBackup = path17.join(backup, "container");
6918
+ const hasContainer = existsSync11(containerInBackup);
6919
+ const envInBackup = path17.join(backup, `${name}.env`);
6920
+ const hasEnv = existsSync11(envInBackup);
7097
6921
  const destYml = containerConfigPath(name, home);
7098
6922
  const destContainer = containerDir(name, home);
7099
- if (existsSync12(destYml)) {
6923
+ if (existsSync11(destYml)) {
7100
6924
  throw new Error(
7101
6925
  `Refusing to restore: ${destYml} already exists. Remove the current container first (\`monoceros remove ${name}\`) or rename the existing config.`
7102
6926
  );
7103
6927
  }
7104
- if (hasContainer && existsSync12(destContainer)) {
6928
+ if (hasContainer && existsSync11(destContainer)) {
7105
6929
  throw new Error(
7106
6930
  `Refusing to restore: ${destContainer} already exists. Remove the current container first (\`monoceros remove ${name}\`).`
7107
6931
  );
7108
6932
  }
7109
- await fs16.mkdir(containerConfigsDir(home), { recursive: true });
7110
- await fs16.copyFile(path18.join(backup, ymlFile), destYml);
6933
+ await fs15.mkdir(containerConfigsDir(home), { recursive: true });
6934
+ await fs15.copyFile(path17.join(backup, ymlFile), destYml);
7111
6935
  if (hasEnv) {
7112
- await fs16.copyFile(envInBackup, containerEnvPath(name, home));
6936
+ await fs15.copyFile(envInBackup, containerEnvPath(name, home));
7113
6937
  }
7114
6938
  if (hasContainer) {
7115
- await fs16.cp(containerInBackup, destContainer, { recursive: true });
6939
+ await fs15.cp(containerInBackup, destContainer, { recursive: true });
7116
6940
  }
7117
6941
  logger.success(`Restored '${name}' from ${prettyPath(backup)}.`);
7118
6942
  logger.info(
@@ -7370,8 +7194,8 @@ import { defineCommand as defineCommand24 } from "citty";
7370
7194
  import { consola as consola28 } from "consola";
7371
7195
 
7372
7196
  // src/devcontainer/shell.ts
7373
- import { existsSync as existsSync13 } from "fs";
7374
- import path19 from "path";
7197
+ import { existsSync as existsSync12 } from "fs";
7198
+ import path18 from "path";
7375
7199
  async function runShell(opts) {
7376
7200
  assertContainerExists(opts.root);
7377
7201
  const spawnFn = opts.spawn ?? spawnDevcontainer;
@@ -7394,7 +7218,7 @@ async function runShell(opts) {
7394
7218
  );
7395
7219
  }
7396
7220
  function assertContainerExists(root) {
7397
- if (!existsSync13(path19.join(root, ".devcontainer"))) {
7221
+ if (!existsSync12(path18.join(root, ".devcontainer"))) {
7398
7222
  throw new Error(
7399
7223
  `No .devcontainer/ at ${root}. Run \`monoceros apply <name>\` first.`
7400
7224
  );
@@ -7606,15 +7430,15 @@ import { defineCommand as defineCommand29 } from "citty";
7606
7430
  import { consola as consola33 } from "consola";
7607
7431
 
7608
7432
  // src/tunnel/run.ts
7609
- import { spawn as spawn10 } from "child_process";
7433
+ import { spawn as spawn8 } from "child_process";
7610
7434
  import { consola as consola32 } from "consola";
7611
7435
 
7612
7436
  // src/tunnel/resolve.ts
7613
- import { existsSync as existsSync14 } from "fs";
7614
- import path20 from "path";
7437
+ import { existsSync as existsSync13 } from "fs";
7438
+ import path19 from "path";
7615
7439
  async function resolveTunnelTarget(opts) {
7616
7440
  const ymlPath = containerConfigPath(opts.name, opts.monocerosHome);
7617
- if (!existsSync14(ymlPath)) {
7441
+ if (!existsSync13(ymlPath)) {
7618
7442
  throw new Error(
7619
7443
  `No yml profile for '${opts.name}' at ${ymlPath}. Run \`monoceros init ${opts.name}\` first.`
7620
7444
  );
@@ -7622,13 +7446,13 @@ async function resolveTunnelTarget(opts) {
7622
7446
  const parsed = await readConfig(ymlPath);
7623
7447
  const config = parsed.config;
7624
7448
  const containerRoot = containerDir(opts.name, opts.monocerosHome);
7625
- if (!existsSync14(containerRoot)) {
7449
+ if (!existsSync13(containerRoot)) {
7626
7450
  throw new Error(
7627
7451
  `Container '${opts.name}' is not materialised at ${containerRoot}. Run \`monoceros apply ${opts.name}\` first.`
7628
7452
  );
7629
7453
  }
7630
- const composePath = path20.join(containerRoot, ".devcontainer", "compose.yaml");
7631
- const isCompose = existsSync14(composePath);
7454
+ const composePath = path19.join(containerRoot, ".devcontainer", "compose.yaml");
7455
+ const isCompose = existsSync13(composePath);
7632
7456
  const parsedTarget = parseTargetArg(opts.target, config);
7633
7457
  const docker = opts.docker ?? defaultDockerExec;
7634
7458
  if (isCompose) {
@@ -7849,7 +7673,7 @@ function formatLocalPortHeldError(port, address, result) {
7849
7673
  // src/tunnel/run.ts
7850
7674
  var SOCAT_IMAGE = "alpine/socat:1.8.0.3";
7851
7675
  var defaultDockerSpawn = (args) => {
7852
- const child = spawn10("docker", args, {
7676
+ const child = spawn8("docker", args, {
7853
7677
  stdio: "inherit"
7854
7678
  });
7855
7679
  const exited = new Promise((resolve, reject) => {