@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 +221 -397
- package/dist/bin.js.map +1 -1
- package/package.json +1 -1
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
270
|
+
path20.push(tok);
|
|
268
271
|
continue;
|
|
269
272
|
}
|
|
270
273
|
break;
|
|
271
274
|
}
|
|
272
|
-
return { path:
|
|
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
|
-
|
|
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
|
|
3136
|
-
const effectivePath = typeof
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
4586
|
+
import { spawn as spawn6 } from "child_process";
|
|
4744
4587
|
var realDockerInfo = () => {
|
|
4745
4588
|
return new Promise((resolve, reject) => {
|
|
4746
|
-
const child =
|
|
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
|
|
4806
|
-
import { promises as
|
|
4807
|
-
import
|
|
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 =
|
|
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 =
|
|
4922
|
-
const gitconfigPath =
|
|
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
|
|
4936
|
-
await
|
|
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
|
|
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 (!
|
|
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
|
-
...
|
|
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
|
|
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 (!
|
|
5173
|
-
const entries = await
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
5426
|
-
import
|
|
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 =
|
|
5575
|
-
if (!
|
|
5576
|
-
const entries = await
|
|
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
|
|
5832
|
-
import
|
|
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 (
|
|
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
|
|
6129
|
+
await fs13.mkdir(containerConfigsDir(home), { recursive: true });
|
|
6265
6130
|
await ensureEnvGitignored(containerConfigsDir(home));
|
|
6266
|
-
await
|
|
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
|
-
|
|
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
|
-
}
|
|
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 =
|
|
6331
|
-
const envRel =
|
|
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
|
|
6867
|
-
import
|
|
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 =
|
|
6885
|
-
const hasEnv =
|
|
6886
|
-
const hasContainer =
|
|
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 =
|
|
6911
|
-
await
|
|
6734
|
+
backupPath = path16.join(home, "container-backups", `${opts.name}-${ts}`);
|
|
6735
|
+
await fs14.mkdir(backupPath, { recursive: true });
|
|
6912
6736
|
if (hasYml) {
|
|
6913
|
-
await
|
|
6737
|
+
await fs14.copyFile(ymlPath, path16.join(backupPath, `${opts.name}.yml`));
|
|
6914
6738
|
}
|
|
6915
6739
|
if (hasEnv) {
|
|
6916
|
-
await
|
|
6740
|
+
await fs14.copyFile(envPath, path16.join(backupPath, `${opts.name}.env`));
|
|
6917
6741
|
}
|
|
6918
6742
|
if (hasContainer) {
|
|
6919
|
-
await
|
|
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
|
|
6750
|
+
await fs14.rm(ymlPath, { force: true });
|
|
6927
6751
|
}
|
|
6928
6752
|
if (hasEnv) {
|
|
6929
|
-
await
|
|
6753
|
+
await fs14.rm(envPath, { force: true });
|
|
6930
6754
|
}
|
|
6931
6755
|
if (hasContainer) {
|
|
6932
6756
|
try {
|
|
6933
|
-
await
|
|
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
|
|
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
|
|
7063
|
-
import
|
|
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 =
|
|
7072
|
-
if (!
|
|
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
|
|
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
|
|
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 =
|
|
7094
|
-
const hasContainer =
|
|
7095
|
-
const envInBackup =
|
|
7096
|
-
const hasEnv =
|
|
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 (
|
|
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 &&
|
|
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
|
|
7110
|
-
await
|
|
6933
|
+
await fs15.mkdir(containerConfigsDir(home), { recursive: true });
|
|
6934
|
+
await fs15.copyFile(path17.join(backup, ymlFile), destYml);
|
|
7111
6935
|
if (hasEnv) {
|
|
7112
|
-
await
|
|
6936
|
+
await fs15.copyFile(envInBackup, containerEnvPath(name, home));
|
|
7113
6937
|
}
|
|
7114
6938
|
if (hasContainer) {
|
|
7115
|
-
await
|
|
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
|
|
7374
|
-
import
|
|
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 (!
|
|
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
|
|
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
|
|
7614
|
-
import
|
|
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 (!
|
|
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 (!
|
|
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 =
|
|
7631
|
-
const isCompose =
|
|
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 =
|
|
7676
|
+
const child = spawn8("docker", args, {
|
|
7853
7677
|
stdio: "inherit"
|
|
7854
7678
|
});
|
|
7855
7679
|
const exited = new Promise((resolve, reject) => {
|