@getmonoceros/workbench 1.13.1 → 1.13.2

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
@@ -356,13 +356,12 @@ var FeatureEntrySchema = z.object({
356
356
  options: z.record(z.string(), FeatureOptionValueSchema).optional()
357
357
  });
358
358
  var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
359
+ function isValidEmail(value) {
360
+ return EMAIL_RE.test(value);
361
+ }
359
362
  var GitUserSchema = z.object({
360
363
  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)
364
+ email: z.union([z.literal(""), z.null(), z.string().min(1)]).nullish().transform((v) => typeof v === "string" && v.length > 0 ? v : void 0)
366
365
  });
367
366
  var RepoEntrySchema = z.object({
368
367
  url: z.string().regex(
@@ -717,6 +716,23 @@ function interpolateServices(services, vars) {
717
716
  });
718
717
  return { services: resolved, missing };
719
718
  }
719
+ var GIT_IDENTITY_VAR = {
720
+ name: "GIT_USER_NAME",
721
+ email: "GIT_USER_EMAIL"
722
+ };
723
+ function hasVarPlaceholder(value) {
724
+ return /\$\{[A-Za-z_][A-Za-z0-9_]*\}/.test(value);
725
+ }
726
+ function resolveGitUserFields(user, vars) {
727
+ const resolve = (raw) => {
728
+ if (raw === void 0) return {};
729
+ const r = interpolate(raw, vars);
730
+ if (r.missing.length > 0) return {};
731
+ const trimmed = r.value.trim();
732
+ return trimmed.length > 0 ? { value: trimmed } : {};
733
+ };
734
+ return { name: resolve(user.name), email: resolve(user.email) };
735
+ }
720
736
  function interpolateFeatures(features, vars) {
721
737
  const missing = [];
722
738
  const out = {};
@@ -1362,7 +1378,15 @@ var MonocerosConfigSchema = z2.object({
1362
1378
  // `git: null` for an empty mapping, which zod's plain
1363
1379
  // `.optional()` would reject.
1364
1380
  git: z2.object({
1365
- user: GitUserSchema.optional()
1381
+ // Strict email here: monoceros-config defaults are not tied to
1382
+ // any container `<name>.env`, so `${VAR}` placeholders make no
1383
+ // sense and the format can (and should) be validated at load
1384
+ // time — unlike the container/repo `git.user`, which defers to
1385
+ // apply after interpolation.
1386
+ user: GitUserSchema.optional().refine(
1387
+ (u) => u?.email === void 0 || isValidEmail(u.email),
1388
+ { message: "Invalid email in defaults.git.user", path: ["email"] }
1389
+ )
1366
1390
  }).nullish(),
1367
1391
  // .nullish() for the same reason as `git` — the sample keeps
1368
1392
  // `features:` uncommented as a category marker.
@@ -2823,6 +2847,17 @@ function setContainerGitUserInDoc(doc, user) {
2823
2847
  relocateLeakedSectionComments(doc);
2824
2848
  return true;
2825
2849
  }
2850
+ function ensureContainerGitUserPlaceholder(doc) {
2851
+ const gitNode = doc.get("git", true);
2852
+ if (gitNode && isMap2(gitNode)) {
2853
+ const userNode = gitNode.get("user", true);
2854
+ if (userNode && isMap2(userNode)) return false;
2855
+ }
2856
+ return setContainerGitUserInDoc(doc, {
2857
+ name: `\${${GIT_IDENTITY_VAR.name}}`,
2858
+ email: `\${${GIT_IDENTITY_VAR.email}}`
2859
+ });
2860
+ }
2826
2861
  function relocateLeakedSectionComments(doc) {
2827
2862
  const root = doc.contents;
2828
2863
  if (!root || !isMap2(root)) return;
@@ -3234,6 +3269,14 @@ async function runAddRepo(input) {
3234
3269
  "--git-name and --git-email must be set together. Pass both, or neither."
3235
3270
  );
3236
3271
  }
3272
+ if (hasEmail) {
3273
+ const email = input.gitEmail.trim();
3274
+ if (!isValidEmail(email) && !hasVarPlaceholder(email)) {
3275
+ throw new Error(
3276
+ `Invalid --git-email '${email}': must be a valid email or a \${VAR} placeholder resolved from <name>.env.`
3277
+ );
3278
+ }
3279
+ }
3237
3280
  const explicitProvider = normalizeProvider(input.provider);
3238
3281
  let host;
3239
3282
  try {
@@ -3264,7 +3307,23 @@ async function runAddRepo(input) {
3264
3307
  } : {},
3265
3308
  ...providerToWrite ? { provider: providerToWrite } : {}
3266
3309
  };
3267
- const result = await mutate(input, (doc) => addRepoToDoc(doc, entry2));
3310
+ let gitUserScaffolded = false;
3311
+ const result = await mutate(input, (doc) => {
3312
+ const repoAdded = addRepoToDoc(doc, entry2);
3313
+ if (repoAdded) gitUserScaffolded = ensureContainerGitUserPlaceholder(doc);
3314
+ return repoAdded;
3315
+ });
3316
+ if (result.status === "updated" && gitUserScaffolded) {
3317
+ const home = input.monocerosHome ?? monocerosHome();
3318
+ await ensureEnvGitignored(containerConfigsDir(home));
3319
+ await ensureEnvVars(containerEnvPath(input.name, home), input.name, [
3320
+ GIT_IDENTITY_VAR.name,
3321
+ GIT_IDENTITY_VAR.email
3322
+ ]);
3323
+ (input.logger ?? defaultLogger()).info(
3324
+ `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.`
3325
+ );
3326
+ }
3268
3327
  if (result.status === "updated") {
3269
3328
  await tryCloneInRunningContainer(input, entry2);
3270
3329
  }
@@ -5040,6 +5099,44 @@ ${sectionLine(label)}
5040
5099
  }
5041
5100
  createOpts.services = interpServices.services;
5042
5101
  if (createOpts.features) createOpts.features = interpFeatures.features;
5102
+ const gitUserErrors = [];
5103
+ let containerGitOverride;
5104
+ if (parsed.config.git?.user) {
5105
+ const f = resolveGitUserFields(parsed.config.git.user, envVars);
5106
+ if (f.email.value !== void 0 && !isValidEmail(f.email.value)) {
5107
+ gitUserErrors.push(
5108
+ `git.user.email resolved to "${f.email.value}", which is not a valid email`
5109
+ );
5110
+ }
5111
+ const override2 = {
5112
+ ...f.name.value !== void 0 ? { name: f.name.value } : {},
5113
+ ...f.email.value !== void 0 ? { email: f.email.value } : {}
5114
+ };
5115
+ if (Object.keys(override2).length > 0) containerGitOverride = override2;
5116
+ }
5117
+ for (const repo of createOpts.repos ?? []) {
5118
+ if (!repo.gitUser) continue;
5119
+ const f = resolveGitUserFields(repo.gitUser, envVars);
5120
+ if (f.name.value === void 0 || f.email.value === void 0) {
5121
+ delete repo.gitUser;
5122
+ continue;
5123
+ }
5124
+ if (!isValidEmail(f.email.value)) {
5125
+ gitUserErrors.push(
5126
+ `repos[${repo.path}].git.user.email resolved to "${f.email.value}", which is not a valid email`
5127
+ );
5128
+ continue;
5129
+ }
5130
+ repo.gitUser = { name: f.name.value, email: f.email.value };
5131
+ }
5132
+ if (gitUserErrors.length > 0) {
5133
+ throw new Error(
5134
+ `Invalid git identity after resolving ${prettyPath(envPath)}:
5135
+ ` + gitUserErrors.map((e) => ` - ${e}`).join("\n") + `
5136
+
5137
+ Fix the value in the env file (or the yml).`
5138
+ );
5139
+ }
5043
5140
  validateOptions(createOpts);
5044
5141
  logger.success(`yml validated ${dim(`(${prettyPath(ymlPath)})`)}`);
5045
5142
  const hasRepos = (createOpts.repos ?? []).length > 0;
@@ -5054,7 +5151,7 @@ ${sectionLine(label)}
5054
5151
  ...opts.identitySpawn ? { spawn: opts.identitySpawn } : {},
5055
5152
  ...opts.identityPrompt ? { prompt: opts.identityPrompt } : {},
5056
5153
  ...opts.identityScopePrompt ? { scopePrompt: opts.identityScopePrompt } : {},
5057
- ...parsed.config.git?.user ? { containerOverride: parsed.config.git.user } : {},
5154
+ ...containerGitOverride ? { containerOverride: containerGitOverride } : {},
5058
5155
  ...globalConfig?.defaults?.git?.user ? { defaults: globalConfig.defaults.git.user } : {},
5059
5156
  logger: idLogger
5060
5157
  });
@@ -5262,7 +5359,7 @@ async function persistPromptedIdentity(prompted, ymlPath, home, logger) {
5262
5359
  }
5263
5360
 
5264
5361
  // src/version.ts
5265
- var CLI_VERSION = true ? "1.13.1" : "dev";
5362
+ var CLI_VERSION = true ? "1.13.2" : "dev";
5266
5363
 
5267
5364
  // src/commands/_dispatch.ts
5268
5365
  import { consola as consola12 } from "consola";
@@ -5897,6 +5994,7 @@ function generateComposedYml(name, composed, lookupManifest, repoUrls = [], port
5897
5994
  lines.push("");
5898
5995
  }
5899
5996
  if (repoUrls.length > 0) {
5997
+ pushGitIdentityBlock(lines);
5900
5998
  pushSectionHeader(
5901
5999
  lines,
5902
6000
  REPOS_HEADER,
@@ -6014,6 +6112,7 @@ function generateDocumentedYml(name, catalog, lookupManifest, repoUrls = [], por
6014
6112
  lines.push("");
6015
6113
  }
6016
6114
  if (repoUrls.length > 0) {
6115
+ pushGitIdentityBlock(lines);
6017
6116
  pushSectionHeader(
6018
6117
  lines,
6019
6118
  REPOS_HEADER,
@@ -6093,6 +6192,20 @@ function pushServiceEntry(out, svc) {
6093
6192
  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
6193
  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
6194
  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`.";
6195
+ 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.";
6196
+ function pushGitIdentityBlock(lines) {
6197
+ pushSectionHeader(
6198
+ lines,
6199
+ GIT_IDENTITY_HEADER,
6200
+ /* commented */
6201
+ false
6202
+ );
6203
+ lines.push("git:");
6204
+ lines.push(" user:");
6205
+ lines.push(` name: \${${GIT_IDENTITY_VAR.name}}`);
6206
+ lines.push(` email: \${${GIT_IDENTITY_VAR.email}}`);
6207
+ lines.push("");
6208
+ }
6096
6209
  function routingHeader(name) {
6097
6210
  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
6211
  }
@@ -6237,17 +6350,6 @@ async function runInit(opts) {
6237
6350
  seenPorts.add(raw);
6238
6351
  ports.push(raw);
6239
6352
  }
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
6353
  let text;
6252
6354
  const composed = resolveComposedInit(catalog, {
6253
6355
  languages: opts.languages ?? [],
@@ -6280,52 +6382,11 @@ async function runInit(opts) {
6280
6382
  Object.assign(seedVars, curatedServiceEnvDefaults(svc.name));
6281
6383
  }
6282
6384
  }
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
- }
6385
+ if (repos.length > 0) {
6386
+ seedVars[GIT_IDENTITY_VAR.name] = "";
6387
+ seedVars[GIT_IDENTITY_VAR.email] = "";
6328
6388
  }
6389
+ await ensureEnvVars(envPath, opts.name, seedVars);
6329
6390
  const documented = !anyComposed;
6330
6391
  const ymlRel = path16.relative(home, dest);
6331
6392
  const envRel = path16.relative(home, envPath);