@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 +126 -65
- package/dist/bin.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
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.
|
|
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
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
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);
|