@getmonoceros/workbench 1.36.9 → 1.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -74,7 +74,7 @@ ${issues}`);
74
74
  }
75
75
  return result.data;
76
76
  }
77
- var SOLUTION_NAME_RE, APT_PACKAGE_NAME_RE, RUNTIME_VERSION_RE, FEATURE_REF_RE, INSTALL_URL_RE, REPO_URL_RE, REPO_PATH_RE, REGEX, PROVIDER_VALUES, KNOWN_PROVIDER_HOSTS, CONFIG_SCHEMA_VERSION, FeatureOptionValueSchema, FeatureEntrySchema, EMAIL_RE, GitUserSchema, RepoEntrySchema, PortEntrySchema, RoutingSchema, SERVICE_NAME_RE, ServiceEnvValueSchema, ServiceHealthcheckSchema, SERVICE_RESTART_VALUES, ServiceObjectSchema, LanguageOptionValueSchema, LanguageEntrySchema, SolutionConfigSchema;
77
+ var SOLUTION_NAME_RE, APT_PACKAGE_NAME_RE, RUNTIME_VERSION_RE, FEATURE_REF_RE, INSTALL_URL_RE, REPO_URL_RE, REPO_PATH_RE, REGEX, PROVIDER_VALUES, PROVIDER_LABEL, GIT_CREDENTIAL_USERNAME, REPO_DOCS_URL, KNOWN_PROVIDER_HOSTS, CONFIG_SCHEMA_VERSION, FeatureOptionValueSchema, FeatureEntrySchema, EMAIL_RE, GitUserSchema, RepoEntrySchema, PortEntrySchema, RoutingSchema, SERVICE_NAME_RE, ServiceEnvValueSchema, ServiceHealthcheckSchema, SERVICE_RESTART_VALUES, ServiceObjectSchema, LanguageOptionValueSchema, LanguageEntrySchema, SolutionConfigSchema;
78
78
  var init_schema = __esm({
79
79
  "src/config/schema.ts"() {
80
80
  "use strict";
@@ -93,12 +93,18 @@ var init_schema = __esm({
93
93
  repoUrl: REPO_URL_RE,
94
94
  repoPath: REPO_PATH_RE
95
95
  };
96
- PROVIDER_VALUES = [
97
- "github",
98
- "gitlab",
99
- "bitbucket",
100
- "gitea"
101
- ];
96
+ PROVIDER_VALUES = ["github", "gitlab", "bitbucket"];
97
+ PROVIDER_LABEL = {
98
+ github: "GitHub",
99
+ gitlab: "GitLab",
100
+ bitbucket: "Bitbucket"
101
+ };
102
+ GIT_CREDENTIAL_USERNAME = {
103
+ github: "oauth2",
104
+ gitlab: "oauth2",
105
+ bitbucket: "x-bitbucket-api-token-auth"
106
+ };
107
+ REPO_DOCS_URL = "https://getmonoceros.build/docs/concepts/git-and-repos/";
102
108
  KNOWN_PROVIDER_HOSTS = {
103
109
  "github.com": "github",
104
110
  "gitlab.com": "gitlab",
@@ -394,6 +400,9 @@ function containerLogsDir(name, home = monocerosHome()) {
394
400
  function monocerosConfigPath(home = monocerosHome()) {
395
401
  return path.join(home, "monoceros-config.yml");
396
402
  }
403
+ function globalEnvPath(home = monocerosHome()) {
404
+ return path.join(home, "monoceros-config.env");
405
+ }
397
406
  function prettyPath(p) {
398
407
  const home = os.homedir();
399
408
  if (!home) return p;
@@ -645,6 +654,9 @@ function wrapToComment(text, width) {
645
654
  function featureOptionVarName(ref, optionKey) {
646
655
  const leaf = ref.split("/").pop() ?? ref;
647
656
  const id = leaf.split("@")[0].split(":")[0];
657
+ if (optionKey === "apiToken" && PROVIDER_TOKEN_VAR[id]) {
658
+ return PROVIDER_TOKEN_VAR[id];
659
+ }
648
660
  const idSnake = id.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase();
649
661
  const optSnake = optionKey.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^A-Za-z0-9]+/g, "_").toUpperCase();
650
662
  return `${idSnake}_${optSnake}`;
@@ -655,11 +667,15 @@ function featureOptionHints(summary, ref, activeKeys = []) {
655
667
  return { key, envVar, placeholder: `\${${envVar}}` };
656
668
  });
657
669
  }
658
- var FEATURE_HEADER_WIDTH;
670
+ var FEATURE_HEADER_WIDTH, PROVIDER_TOKEN_VAR;
659
671
  var init_feature_doc = __esm({
660
672
  "src/init/feature-doc.ts"() {
661
673
  "use strict";
662
674
  FEATURE_HEADER_WIDTH = 76 - 2;
675
+ PROVIDER_TOKEN_VAR = {
676
+ "github-cli": "GITHUB_API_TOKEN",
677
+ "gitlab-cli": "GITLAB_API_TOKEN"
678
+ };
663
679
  }
664
680
  });
665
681
 
@@ -1167,6 +1183,7 @@ function makePalette(isTty2) {
1167
1183
  underline: (s) => wrap(s, ANSI_UNDERLINE2),
1168
1184
  cyan: (s) => wrap(s, ANSI_CYAN2),
1169
1185
  green: (s) => wrap(s, ANSI_GREEN),
1186
+ yellow: (s) => wrap(s, ANSI_YELLOW),
1170
1187
  dim: (s) => wrap(s, ANSI_GREY2),
1171
1188
  sectionLine: (label) => wrap(`\u25B8 ${label}`, ANSI_BOLD2, ANSI_UNDERLINE2)
1172
1189
  };
@@ -1174,7 +1191,7 @@ function makePalette(isTty2) {
1174
1191
  function colorsFor(stream) {
1175
1192
  return makePalette(stream.isTTY ?? false);
1176
1193
  }
1177
- var ESC, ANSI_BOLD2, ANSI_UNDERLINE2, ANSI_CYAN2, ANSI_GREEN, ANSI_GREY2, ANSI_RESET2, ANSI_RE2, stderrPalette, bold2, underline2, cyan2, green, dim, sectionLine;
1194
+ var ESC, ANSI_BOLD2, ANSI_UNDERLINE2, ANSI_CYAN2, ANSI_GREEN, ANSI_YELLOW, ANSI_GREY2, ANSI_RESET2, ANSI_RE2, stderrPalette, bold2, underline2, cyan2, green, yellow, dim, sectionLine;
1178
1195
  var init_format = __esm({
1179
1196
  "src/util/format.ts"() {
1180
1197
  "use strict";
@@ -1183,6 +1200,7 @@ var init_format = __esm({
1183
1200
  ANSI_UNDERLINE2 = `${ESC}4m`;
1184
1201
  ANSI_CYAN2 = `${ESC}36m`;
1185
1202
  ANSI_GREEN = `${ESC}32m`;
1203
+ ANSI_YELLOW = `${ESC}33m`;
1186
1204
  ANSI_GREY2 = `${ESC}90m`;
1187
1205
  ANSI_RESET2 = `${ESC}0m`;
1188
1206
  ANSI_RE2 = /\x1b\[[0-9;]*m/g;
@@ -1191,13 +1209,13 @@ var init_format = __esm({
1191
1209
  underline2 = stderrPalette.underline;
1192
1210
  cyan2 = stderrPalette.cyan;
1193
1211
  green = stderrPalette.green;
1212
+ yellow = stderrPalette.yellow;
1194
1213
  dim = stderrPalette.dim;
1195
1214
  sectionLine = stderrPalette.sectionLine;
1196
1215
  }
1197
1216
  });
1198
1217
 
1199
1218
  // src/devcontainer/credentials.ts
1200
- import { spawn } from "child_process";
1201
1219
  import { promises as fs3 } from "fs";
1202
1220
  import path5 from "path";
1203
1221
  function resolveProvider(host, explicit) {
@@ -1220,128 +1238,6 @@ function uniqueHttpsHosts(repos) {
1220
1238
  }
1221
1239
  return [...byHost.values()];
1222
1240
  }
1223
- function installCommandForOS(opts) {
1224
- const withBrewBootstrap = (cmd) => [
1225
- "",
1226
- cyan2(BREW_INSTALL_COMMAND),
1227
- cyan2(cmd),
1228
- "",
1229
- dim("(Skip the first line if you already have Homebrew.)")
1230
- ].join("\n");
1231
- if (process.platform === "darwin") return withBrewBootstrap(opts.brew);
1232
- if (opts.linuxBrew) return withBrewBootstrap(opts.linuxBrew);
1233
- return `See ${opts.linuxDocsUrl} for package instructions.`;
1234
- }
1235
- function providerSetupHint(host, provider) {
1236
- if (provider === "github") {
1237
- const isSaas = host.toLowerCase() === "github.com";
1238
- const hostArg = isSaas ? "" : ` --hostname ${host}`;
1239
- const install = installCommandForOS({
1240
- brew: "brew install gh",
1241
- linuxBrew: "brew install gh",
1242
- linuxDocsUrl: "https://github.com/cli/cli#installation"
1243
- });
1244
- return {
1245
- title: `${host} \u2014 GitHub`,
1246
- body: [
1247
- "Install the GitHub CLI:",
1248
- install,
1249
- "",
1250
- "Then run once:",
1251
- cyan2(`gh auth login${hostArg}`),
1252
- cyan2(`gh auth setup-git${hostArg}`),
1253
- "",
1254
- "`gh auth login` walks through OAuth in your browser.",
1255
- "`gh auth setup-git` wires gh into git as a credential helper."
1256
- ].join("\n")
1257
- };
1258
- }
1259
- if (provider === "gitlab") {
1260
- const isSaas = host.toLowerCase() === "gitlab.com";
1261
- const hostArg = isSaas ? "" : ` --hostname ${host}`;
1262
- const install = installCommandForOS({
1263
- brew: "brew install glab",
1264
- linuxBrew: "brew install glab",
1265
- linuxDocsUrl: "https://gitlab.com/gitlab-org/cli#installation"
1266
- });
1267
- return {
1268
- title: `${host} \u2014 GitLab`,
1269
- body: [
1270
- "Install the GitLab CLI (glab):",
1271
- install,
1272
- "",
1273
- "Then run once:",
1274
- cyan2(`glab auth login${hostArg}`),
1275
- "",
1276
- "Choose `HTTPS` when asked for git-protocol, then accept",
1277
- '"Authenticate Git with your GitLab credentials" \u2014 glab',
1278
- "configures itself as the git credential helper."
1279
- ].join("\n")
1280
- };
1281
- }
1282
- if (provider === "bitbucket") {
1283
- const isCloud = host.toLowerCase() === "bitbucket.org";
1284
- if (isCloud) {
1285
- return {
1286
- title: `${host} \u2014 Bitbucket Cloud`,
1287
- body: [
1288
- "Bitbucket has no first-party CLI for git-credentials, so this",
1289
- "is a manual one-time setup. Generate an Atlassian API token at",
1290
- "https://id.atlassian.com/manage-profile/security/api-tokens",
1291
- "",
1292
- "Then store it via your OS credential helper:",
1293
- cyan2(
1294
- `git credential approve <<< $'protocol=https\\nhost=${host}\\nusername=<your-atlassian-email>\\npassword=<token>\\n'`
1295
- )
1296
- ].join("\n")
1297
- };
1298
- }
1299
- return {
1300
- title: `${host} \u2014 Bitbucket Data Center`,
1301
- body: [
1302
- "Bitbucket has no first-party CLI for git-credentials, so this",
1303
- "is a manual one-time setup. Generate a personal HTTP access",
1304
- `token in your Bitbucket UI: profile picture (top right on ${host})`,
1305
- "\u2192 Manage account \u2192 HTTP access tokens \u2192 Create token. Give it",
1306
- "at least repo-read + repo-write scopes for the repos you need.",
1307
- "",
1308
- "Then store it via your OS credential helper:",
1309
- cyan2(
1310
- `git credential approve <<< $'protocol=https\\nhost=${host}\\nusername=<your-bitbucket-username>\\npassword=<token>\\n'`
1311
- )
1312
- ].join("\n")
1313
- };
1314
- }
1315
- return {
1316
- title: `${host} \u2014 Gitea`,
1317
- body: [
1318
- "Gitea has no first-party CLI helper for git-credentials (the",
1319
- "`tea` CLI logs into its own config, not into your git credential",
1320
- "helper), so this is a manual one-time setup. Generate an access",
1321
- `token in your Gitea UI: profile picture (top right on ${host}) \u2192`,
1322
- 'Settings \u2192 Applications \u2192 "Generate New Token". Give it at',
1323
- "least the `read:repository` scope (add `write:repository` if you",
1324
- "need push from the container).",
1325
- "",
1326
- "Then store it via your OS credential helper:",
1327
- cyan2(
1328
- `git credential approve <<< $'protocol=https\\nhost=${host}\\nusername=<your-gitea-username>\\npassword=<token>\\n'`
1329
- )
1330
- ].join("\n")
1331
- };
1332
- }
1333
- function parseCredentialFillOutput(output) {
1334
- const result = {};
1335
- for (const line of output.split("\n")) {
1336
- const eqIdx = line.indexOf("=");
1337
- if (eqIdx <= 0) continue;
1338
- const key = line.slice(0, eqIdx);
1339
- const value = line.slice(eqIdx + 1);
1340
- if (key === "username") result.username = value;
1341
- if (key === "password") result.password = value;
1342
- }
1343
- return result;
1344
- }
1345
1241
  function formatCredentialLine(host, username, password) {
1346
1242
  const encUser = encodeURIComponent(username);
1347
1243
  const encPass = encodeURIComponent(password);
@@ -1350,76 +1246,30 @@ function formatCredentialLine(host, username, password) {
1350
1246
  async function collectGitCredentials(devContainerRoot, hosts, options = {}) {
1351
1247
  const credsDir = path5.join(devContainerRoot, ".monoceros");
1352
1248
  const credentialsPath = path5.join(credsDir, "git-credentials");
1353
- const spawnFn = options.spawn ?? realGitCredentialFill;
1354
- const approveFn = options.approve ?? realGitCredentialApprove;
1355
- const logger = options.logger ?? { info: () => {
1356
- }, warn: () => {
1357
- } };
1358
1249
  const lines = [];
1359
1250
  const perHost = [];
1360
1251
  for (const { host, provider } of hosts) {
1361
- if (provider === "unknown") {
1362
- perHost.push({
1363
- host,
1364
- provider: "github",
1365
- // placeholder — never rendered because pre-flight already bailed
1366
- status: "no-credentials",
1367
- detail: "provider not declared (internal: should not reach here)"
1368
- });
1369
- continue;
1370
- }
1371
- logger.info(`Fetching credentials for ${host} from host git\u2026`);
1372
- const input = `protocol=https
1373
- host=${host}
1374
-
1375
- `;
1376
- let result;
1377
- try {
1378
- result = await spawnFn(input);
1379
- } catch (err) {
1380
- const detail = err instanceof Error ? err.message : String(err);
1381
- perHost.push({ host, provider, status: "spawn-error", detail });
1382
- continue;
1383
- }
1384
- if (result.exitCode !== 0) {
1385
- perHost.push({
1386
- host,
1387
- provider,
1388
- status: "non-zero-exit",
1389
- detail: `exit code ${result.exitCode}`
1390
- });
1391
- continue;
1392
- }
1393
- const { username, password } = parseCredentialFillOutput(result.stdout);
1394
- if (!username || !password) {
1252
+ const known = provider === "unknown" ? "github" : provider;
1253
+ const pat = options.patByHost?.get(host);
1254
+ if (pat) {
1255
+ lines.push(
1256
+ formatCredentialLine(host, GIT_CREDENTIAL_USERNAME[known], pat)
1257
+ );
1258
+ perHost.push({ host, provider: known, status: "ok", detail: "" });
1259
+ } else {
1395
1260
  perHost.push({
1396
1261
  host,
1397
- provider,
1398
- status: "no-credentials",
1399
- detail: "host credential helper returned no username/password"
1262
+ provider: known,
1263
+ status: "no-token",
1264
+ detail: "no personal access token configured"
1400
1265
  });
1401
- continue;
1402
- }
1403
- lines.push(formatCredentialLine(host, username, password));
1404
- perHost.push({ host, provider, status: "ok", detail: "" });
1405
- const approveInput = `protocol=https
1406
- host=${host}
1407
- username=${username}
1408
- password=${password}
1409
-
1410
- `;
1411
- try {
1412
- await approveFn(approveInput);
1413
- } catch {
1414
1266
  }
1415
1267
  }
1416
1268
  await fs3.mkdir(credsDir, { recursive: true });
1417
1269
  await fs3.writeFile(
1418
1270
  credentialsPath,
1419
1271
  lines.join("\n") + (lines.length > 0 ? "\n" : ""),
1420
- {
1421
- mode: 384
1422
- }
1272
+ { mode: 384 }
1423
1273
  );
1424
1274
  return {
1425
1275
  hostsWritten: lines.length,
@@ -1428,95 +1278,146 @@ password=${password}
1428
1278
  credentialsPath
1429
1279
  };
1430
1280
  }
1431
- function formatMissingCredentialsError(missing) {
1432
- if (missing.length === 1) {
1433
- const m = missing[0];
1434
- const hint = providerSetupHint(m.host, m.provider);
1435
- return [
1436
- `Missing Git credentials: ${hint.title}`,
1437
- "",
1438
- hint.body,
1439
- "",
1440
- `Then re-run ${cyan2("monoceros apply")}.`
1441
- ].join("\n");
1442
- }
1443
- const lines = [
1444
- `Missing Git credentials for ${missing.length} hosts:`,
1445
- ""
1446
- ];
1447
- for (const m of missing) {
1448
- const hint = providerSetupHint(m.host, m.provider);
1449
- lines.push(hint.title);
1450
- lines.push("");
1451
- lines.push(hint.body);
1452
- lines.push("");
1453
- }
1454
- lines.push(`Then re-run ${cyan2("monoceros apply")}.`);
1455
- return lines.join("\n");
1456
- }
1457
1281
  function formatUnknownProviderError(hosts) {
1458
1282
  const sorted = [...new Set(hosts)].sort();
1459
1283
  const lines = [
1460
1284
  sorted.length === 1 ? `Unknown Git provider for host ${sorted[0]}.` : `Unknown Git provider for ${sorted.length} hosts: ${sorted.join(", ")}.`,
1461
1285
  "",
1462
1286
  "Monoceros auto-detects only github.com / gitlab.com / bitbucket.org.",
1463
- "For any other host (self-hosted GitLab, Gitea, Bitbucket Server, \u2026)",
1464
- "declare the provider explicitly in the yml. Edit the repo entry:",
1287
+ "For any other host (self-hosted GitLab, GitHub Enterprise, Bitbucket",
1288
+ "Data Center) declare the provider explicitly in the yml. Edit the",
1289
+ "repo entry:",
1465
1290
  "",
1466
1291
  cyan2(" repos:"),
1467
1292
  cyan2(` - url: https://${sorted[0]}/\u2026`),
1468
- cyan2(" provider: gitlab # or: github, bitbucket, gitea"),
1293
+ cyan2(" provider: gitlab # or: github, bitbucket"),
1469
1294
  "",
1470
- `Or re-add with ${cyan2("monoceros add-repo <name> <url> --provider=<github|gitlab|bitbucket|gitea>")}.`
1295
+ `Or re-add with ${cyan2("monoceros add-repo <name> <url> --provider=<github|gitlab|bitbucket>")}.`
1471
1296
  ];
1472
1297
  return lines.join("\n");
1473
1298
  }
1474
- var realGitCredentialFill, realGitCredentialApprove, BREW_INSTALL_COMMAND;
1475
1299
  var init_credentials = __esm({
1476
1300
  "src/devcontainer/credentials.ts"() {
1477
1301
  "use strict";
1478
1302
  init_schema();
1479
1303
  init_format();
1480
- realGitCredentialFill = (input) => {
1481
- return new Promise((resolve, reject) => {
1482
- const child = spawn("git", ["credential", "fill"], {
1483
- stdio: ["pipe", "pipe", "inherit"],
1484
- env: {
1485
- ...process.env,
1486
- GIT_TERMINAL_PROMPT: "0"
1487
- }
1488
- });
1489
- let stdout = "";
1490
- child.stdout.on("data", (chunk) => {
1491
- stdout += chunk.toString();
1492
- });
1493
- child.on("error", reject);
1494
- child.on("exit", (code) => resolve({ stdout, exitCode: code ?? 0 }));
1495
- child.stdin.write(input);
1496
- child.stdin.end();
1497
- });
1498
- };
1499
- realGitCredentialApprove = (input) => {
1500
- return new Promise((resolve, reject) => {
1501
- const child = spawn("git", ["credential", "approve"], {
1502
- stdio: ["pipe", "ignore", "inherit"],
1503
- env: {
1504
- ...process.env,
1505
- GIT_TERMINAL_PROMPT: "0"
1506
- }
1507
- });
1508
- child.on("error", reject);
1509
- child.on("exit", () => resolve());
1510
- child.stdin.write(input);
1511
- child.stdin.end();
1512
- });
1513
- };
1514
- BREW_INSTALL_COMMAND = '/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"';
1304
+ }
1305
+ });
1306
+
1307
+ // src/apply/repo-token.ts
1308
+ import { promises as fs4 } from "fs";
1309
+ function workspaceSegment(url) {
1310
+ let ws;
1311
+ try {
1312
+ ws = new URL(url).pathname.split("/").filter(Boolean)[0];
1313
+ } catch {
1314
+ return void 0;
1315
+ }
1316
+ return ws ? ws.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase() : void 0;
1317
+ }
1318
+ function tokenVarCandidates(provider, url) {
1319
+ const p = provider.toUpperCase();
1320
+ const vars = [`${p}_API_TOKEN`];
1321
+ const segment = workspaceSegment(url);
1322
+ if (segment) vars.push(`GIT_TOKEN__${p}_${segment}`);
1323
+ vars.push(`GIT_TOKEN__${p}`);
1324
+ return vars;
1325
+ }
1326
+ function resolveRepoTokens(config, catalog, envVars) {
1327
+ const features = config.features.map((f) => ({
1328
+ ...f,
1329
+ ...f.options ? { options: { ...f.options } } : {}
1330
+ }));
1331
+ const hostTokens = /* @__PURE__ */ new Map();
1332
+ const used = [];
1333
+ const missing = [];
1334
+ for (const repo of config.repos ?? []) {
1335
+ if (!repo.url.startsWith("https://")) continue;
1336
+ let host;
1337
+ try {
1338
+ host = new URL(repo.url).hostname;
1339
+ } catch {
1340
+ continue;
1341
+ }
1342
+ const provider = resolveProvider(host, repo.provider);
1343
+ if (provider === "unknown") continue;
1344
+ const ref = catalog.get(provider)?.file.contributes.features?.[0]?.ref;
1345
+ const tried = tokenVarCandidates(provider, repo.url);
1346
+ const hit = tried.find((v) => (envVars[v] ?? "").trim().length > 0);
1347
+ if (!hit) {
1348
+ missing.push({ host, provider, tried });
1349
+ continue;
1350
+ }
1351
+ const token = envVars[hit].trim();
1352
+ hostTokens.set(host, token);
1353
+ used.push({ host, provider, varName: hit });
1354
+ if (ref) {
1355
+ const feature = features.find((f) => f.ref === ref);
1356
+ if (feature) feature.options = { ...feature.options, apiToken: token };
1357
+ }
1358
+ }
1359
+ return {
1360
+ features,
1361
+ hostTokens,
1362
+ used,
1363
+ missing
1364
+ };
1365
+ }
1366
+ async function resolveContainerRepoTokens(name, home, catalog) {
1367
+ const ymlPath = containerConfigPath(name, home);
1368
+ const { config } = parseConfig(await fs4.readFile(ymlPath, "utf8"), ymlPath);
1369
+ const envVars = {
1370
+ ...readEnvFile(globalEnvPath(home)),
1371
+ ...readEnvFile(containerEnvPath(name, home))
1372
+ };
1373
+ return resolveRepoTokens(config, catalog, envVars);
1374
+ }
1375
+ function formatTokenUse(use) {
1376
+ return `${PROVIDER_LABEL[use.provider]} (${use.host}) \u2192 ${use.varName}`;
1377
+ }
1378
+ function formatUnauthenticatedRepos(missing, containerName2) {
1379
+ const lines = [
1380
+ bold2(yellow("\u26A0 Repo access \u2014 action needed")),
1381
+ "",
1382
+ yellow(" Some repositories are UNAUTHENTICATED:")
1383
+ ];
1384
+ for (const m of missing) {
1385
+ lines.push(` \u2022 ${PROVIDER_LABEL[m.provider]} (${m.host})`);
1386
+ }
1387
+ lines.push(
1388
+ "",
1389
+ bold2(" Public repositories still clone (read-only). But:"),
1390
+ " \u2022 gh / glab in the container are not logged in.",
1391
+ " \u2022 pushing, and cloning/pulling PRIVATE repositories, fails.",
1392
+ " \u2022 branches, PRs/MRs \u2014 anything that writes to the remote \u2014 fails.",
1393
+ "",
1394
+ bold2(" Set a token, then re-apply:")
1395
+ );
1396
+ for (const m of missing) {
1397
+ const featureVar = m.tried.find((v) => !v.startsWith("GIT_TOKEN__"));
1398
+ const sharedVar = m.tried.find((v) => v.startsWith("GIT_TOKEN__"));
1399
+ lines.push(
1400
+ ` \u2022 ${PROVIDER_LABEL[m.provider]}: ${cyan2(featureVar)} in container-configs/${containerName2}.env,`,
1401
+ ` or ${cyan2(sharedVar)} in monoceros-config.env`
1402
+ );
1403
+ }
1404
+ lines.push("", ` Details: ${cyan2(REPO_DOCS_URL)}`);
1405
+ return lines.join("\n");
1406
+ }
1407
+ var init_repo_token = __esm({
1408
+ "src/apply/repo-token.ts"() {
1409
+ "use strict";
1410
+ init_schema();
1411
+ init_io();
1412
+ init_env_file();
1413
+ init_paths();
1414
+ init_credentials();
1415
+ init_format();
1515
1416
  }
1516
1417
  });
1517
1418
 
1518
1419
  // src/devcontainer/locate-running.ts
1519
- import { spawn as spawn2 } from "child_process";
1420
+ import { spawn } from "child_process";
1520
1421
  async function findRunningContainerByLocalFolder(containerPath, opts = {}) {
1521
1422
  const docker = opts.docker ?? realDockerLookup;
1522
1423
  const result = await docker([
@@ -1537,7 +1438,7 @@ var init_locate_running = __esm({
1537
1438
  "use strict";
1538
1439
  realDockerLookup = (args) => {
1539
1440
  return new Promise((resolve, reject) => {
1540
- const child = spawn2("docker", args, {
1441
+ const child = spawn("docker", args, {
1541
1442
  stdio: ["ignore", "pipe", "pipe"]
1542
1443
  });
1543
1444
  let stdout = "";
@@ -1557,7 +1458,7 @@ var init_locate_running = __esm({
1557
1458
  };
1558
1459
  realContainerExec = (containerId, argv) => {
1559
1460
  return new Promise((resolve, reject) => {
1560
- const child = spawn2("docker", ["exec", containerId, ...argv], {
1461
+ const child = spawn("docker", ["exec", containerId, ...argv], {
1561
1462
  // Inherit stdio so live git output reaches the user.
1562
1463
  stdio: ["ignore", "inherit", "inherit"]
1563
1464
  });
@@ -1569,7 +1470,7 @@ var init_locate_running = __esm({
1569
1470
  });
1570
1471
 
1571
1472
  // src/config/global.ts
1572
- import { promises as fs4 } from "fs";
1473
+ import { promises as fs5 } from "fs";
1573
1474
  import { z as z3 } from "zod";
1574
1475
  import { isMap, Pair, parseDocument as parseDocument2, Scalar, YAMLMap } from "yaml";
1575
1476
  async function readMonocerosConfig(opts = {}) {
@@ -1577,7 +1478,7 @@ async function readMonocerosConfig(opts = {}) {
1577
1478
  const filePath = monocerosConfigPath(home);
1578
1479
  let text;
1579
1480
  try {
1580
- text = await fs4.readFile(filePath, "utf8");
1481
+ text = await fs5.readFile(filePath, "utf8");
1581
1482
  } catch {
1582
1483
  return void 0;
1583
1484
  }
@@ -1613,7 +1514,7 @@ async function writeGlobalDefaultGitUser(user, opts = {}) {
1613
1514
  const filePath = monocerosConfigPath(home);
1614
1515
  let text;
1615
1516
  try {
1616
- text = await fs4.readFile(filePath, "utf8");
1517
+ text = await fs5.readFile(filePath, "utf8");
1617
1518
  } catch {
1618
1519
  text = void 0;
1619
1520
  }
@@ -1630,8 +1531,8 @@ async function writeGlobalDefaultGitUser(user, opts = {}) {
1630
1531
  ` email: ${user.email}`,
1631
1532
  ""
1632
1533
  ].join("\n");
1633
- await fs4.mkdir(home, { recursive: true });
1634
- await fs4.writeFile(filePath, fresh, "utf8");
1534
+ await fs5.mkdir(home, { recursive: true });
1535
+ await fs5.writeFile(filePath, fresh, "utf8");
1635
1536
  return { filePath, created: true, alreadySet: false };
1636
1537
  }
1637
1538
  const doc = parseDocument2(text, { prettyErrors: true });
@@ -1652,7 +1553,7 @@ async function writeGlobalDefaultGitUser(user, opts = {}) {
1652
1553
  userMap.set("name", user.name);
1653
1554
  userMap.set("email", user.email);
1654
1555
  const newText = String(doc);
1655
- await fs4.writeFile(filePath, newText, "utf8");
1556
+ await fs5.writeFile(filePath, newText, "utf8");
1656
1557
  return { filePath, created: false, alreadySet: false };
1657
1558
  }
1658
1559
  function ensureMap(doc, key) {
@@ -1913,8 +1814,8 @@ var init_components = __esm({
1913
1814
  });
1914
1815
 
1915
1816
  // src/proxy/index.ts
1916
- import { spawn as spawn3 } from "child_process";
1917
- import { promises as fs5 } from "fs";
1817
+ import { spawn as spawn2 } from "child_process";
1818
+ import { promises as fs6 } from "fs";
1918
1819
  import path6 from "path";
1919
1820
  function proxyDynamicDir(home) {
1920
1821
  return path6.join(home ?? monocerosHome(), "traefik", "dynamic");
@@ -1922,7 +1823,7 @@ function proxyDynamicDir(home) {
1922
1823
  async function ensureProxy(opts = {}) {
1923
1824
  const docker = opts.docker ?? realDocker;
1924
1825
  const dyn = proxyDynamicDir(opts.monocerosHome);
1925
- await fs5.mkdir(dyn, { recursive: true });
1826
+ await fs6.mkdir(dyn, { recursive: true });
1926
1827
  const netInspect = await docker(["network", "inspect", PROXY_NETWORK_NAME]);
1927
1828
  if (netInspect.exitCode !== 0) {
1928
1829
  const create = await docker(["network", "create", PROXY_NETWORK_NAME]);
@@ -2028,7 +1929,7 @@ var init_proxy = __esm({
2028
1929
  PROXY_RESTART_POLICY = "unless-stopped";
2029
1930
  defaultDockerExec = (args) => {
2030
1931
  return new Promise((resolve, reject) => {
2031
- const child = spawn3("docker", args, {
1932
+ const child = spawn2("docker", args, {
2032
1933
  stdio: ["ignore", "pipe", "pipe"]
2033
1934
  });
2034
1935
  let stdout = "";
@@ -2051,7 +1952,7 @@ var init_proxy = __esm({
2051
1952
  });
2052
1953
 
2053
1954
  // src/proxy/dynamic.ts
2054
- import { promises as fs6 } from "fs";
1955
+ import { promises as fs7 } from "fs";
2055
1956
  import path7 from "path";
2056
1957
  async function writeDynamicConfig(name, ports, opts = {}) {
2057
1958
  if (ports.length === 0) {
@@ -2060,14 +1961,14 @@ async function writeDynamicConfig(name, ports, opts = {}) {
2060
1961
  );
2061
1962
  }
2062
1963
  const dir = proxyDynamicDir(opts.monocerosHome);
2063
- await fs6.mkdir(dir, { recursive: true });
1964
+ await fs7.mkdir(dir, { recursive: true });
2064
1965
  const file = path7.join(dir, `${name}.yml`);
2065
- await fs6.writeFile(file, renderDynamicConfig(name, ports), "utf8");
1966
+ await fs7.writeFile(file, renderDynamicConfig(name, ports), "utf8");
2066
1967
  return file;
2067
1968
  }
2068
1969
  async function removeDynamicConfig(name, opts = {}) {
2069
1970
  const file = path7.join(proxyDynamicDir(opts.monocerosHome), `${name}.yml`);
2070
- await fs6.rm(file, { force: true });
1971
+ await fs7.rm(file, { force: true });
2071
1972
  }
2072
1973
  function renderDynamicConfig(name, ports) {
2073
1974
  const lines = [];
@@ -2712,8 +2613,8 @@ var init_claude_settings = __esm({
2712
2613
  });
2713
2614
 
2714
2615
  // src/devcontainer/ssh-attach.ts
2715
- import { spawn as spawn4 } from "child_process";
2716
- import { promises as fs7 } from "fs";
2616
+ import { spawn as spawn3 } from "child_process";
2617
+ import { promises as fs8 } from "fs";
2717
2618
  import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
2718
2619
  import os2 from "os";
2719
2620
  import path9 from "path";
@@ -2735,7 +2636,7 @@ async function ensureKeypair(targetDir, name, keygen, logger) {
2735
2636
  if (existsSync6(privateKey) && existsSync6(publicKey)) {
2736
2637
  return { privateKey, publicKey };
2737
2638
  }
2738
- await fs7.mkdir(path9.dirname(privateKey), { recursive: true });
2639
+ await fs8.mkdir(path9.dirname(privateKey), { recursive: true });
2739
2640
  try {
2740
2641
  const res = await keygen([
2741
2642
  "-t",
@@ -2812,10 +2713,10 @@ async function ensureInclude(userSshDir, home) {
2812
2713
  const configPath = path9.join(userSshDir, "config");
2813
2714
  const includeTarget = path9.join(sshHomeDir(home), "config.d", "*");
2814
2715
  const includeLine = `Include "${includeTarget}"`;
2815
- await fs7.mkdir(userSshDir, { recursive: true });
2716
+ await fs8.mkdir(userSshDir, { recursive: true });
2816
2717
  let existing = "";
2817
2718
  try {
2818
- existing = await fs7.readFile(configPath, "utf8");
2719
+ existing = await fs8.readFile(configPath, "utf8");
2819
2720
  } catch {
2820
2721
  existing = "";
2821
2722
  }
@@ -2826,13 +2727,13 @@ ${includeLine}
2826
2727
  `;
2827
2728
  const next = existing.length > 0 ? `${prefix}
2828
2729
  ${existing}` : prefix;
2829
- await fs7.writeFile(configPath, next, { mode: 384 });
2830
- await fs7.chmod(configPath, 384).catch(() => {
2730
+ await fs8.writeFile(configPath, next, { mode: 384 });
2731
+ await fs8.chmod(configPath, 384).catch(() => {
2831
2732
  });
2832
2733
  }
2833
2734
  function runCapture(cmd, args) {
2834
2735
  return new Promise((resolve, reject) => {
2835
- const child = spawn4(cmd, args, {
2736
+ const child = spawn3(cmd, args, {
2836
2737
  stdio: ["ignore", "pipe", "ignore"]
2837
2738
  });
2838
2739
  let stdout = "";
@@ -2929,26 +2830,26 @@ async function upsertMarkedBlock(configPath, hostAlias, body) {
2929
2830
  const section = `${begin}
2930
2831
  ${body}
2931
2832
  ${end}`;
2932
- await fs7.mkdir(path9.dirname(configPath), { recursive: true });
2833
+ await fs8.mkdir(path9.dirname(configPath), { recursive: true });
2933
2834
  let existing = "";
2934
2835
  try {
2935
- existing = await fs7.readFile(configPath, "utf8");
2836
+ existing = await fs8.readFile(configPath, "utf8");
2936
2837
  } catch {
2937
2838
  existing = "";
2938
2839
  }
2939
2840
  const re = new RegExp(`${escapeRegExp2(begin)}[\\s\\S]*?${escapeRegExp2(end)}`);
2940
2841
  if (re.test(existing)) {
2941
- await fs7.writeFile(configPath, existing.replace(re, section));
2842
+ await fs8.writeFile(configPath, existing.replace(re, section));
2942
2843
  return;
2943
2844
  }
2944
2845
  const sep = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
2945
- await fs7.writeFile(configPath, `${existing}${sep}${section}
2846
+ await fs8.writeFile(configPath, `${existing}${sep}${section}
2946
2847
  `);
2947
2848
  }
2948
2849
  async function removeMarkedBlock(configPath, hostAlias) {
2949
2850
  let existing = "";
2950
2851
  try {
2951
- existing = await fs7.readFile(configPath, "utf8");
2852
+ existing = await fs8.readFile(configPath, "utf8");
2952
2853
  } catch {
2953
2854
  return;
2954
2855
  }
@@ -2957,7 +2858,7 @@ async function removeMarkedBlock(configPath, hostAlias) {
2957
2858
  `\\n?${escapeRegExp2(begin)}[\\s\\S]*?${escapeRegExp2(end)}\\n?`,
2958
2859
  "g"
2959
2860
  );
2960
- await fs7.writeFile(
2861
+ await fs8.writeFile(
2961
2862
  configPath,
2962
2863
  existing.replace(re, "\n").replace(/\n{3,}/g, "\n\n")
2963
2864
  );
@@ -2972,10 +2873,10 @@ async function setupWindowsBridge(name, hostAlias, privateKey, directPort, deps,
2972
2873
  return;
2973
2874
  }
2974
2875
  const monoDir = path9.join(profile.homeWsl, ".ssh", "monoceros");
2975
- await fs7.mkdir(monoDir, { recursive: true });
2876
+ await fs8.mkdir(monoDir, { recursive: true });
2976
2877
  const keyDst = path9.join(monoDir, name);
2977
- await fs7.rm(keyDst, { force: true });
2978
- await fs7.copyFile(privateKey, keyDst);
2878
+ await fs8.rm(keyDst, { force: true });
2879
+ await fs8.copyFile(privateKey, keyDst);
2979
2880
  const keyWin = `${profile.homeWin}\\.ssh\\monoceros\\${name}`;
2980
2881
  await upsertMarkedBlock(
2981
2882
  path9.join(profile.homeWsl, ".ssh", "config"),
@@ -2991,7 +2892,7 @@ async function removeWindowsBridge(name, hostAlias, deps) {
2991
2892
  if (!deps.isWsl()) return;
2992
2893
  const profile = await deps.resolveProfile();
2993
2894
  if (!profile) return;
2994
- await fs7.rm(path9.join(profile.homeWsl, ".ssh", "monoceros", name), {
2895
+ await fs8.rm(path9.join(profile.homeWsl, ".ssh", "monoceros", name), {
2995
2896
  force: true
2996
2897
  });
2997
2898
  await removeMarkedBlock(
@@ -3012,16 +2913,16 @@ async function setupSshAttach(opts) {
3012
2913
  const configEntry = sshConfigEntryPath(opts.home, opts.name);
3013
2914
  const winDeps = resolveWindowsDeps(opts.windows);
3014
2915
  const directPort = winDeps.isWsl() ? opts.windowsDirectPort ?? null : null;
3015
- await fs7.mkdir(path9.dirname(proxyScript), { recursive: true });
3016
- await fs7.mkdir(path9.dirname(configEntry), { recursive: true });
3017
- await fs7.writeFile(
2916
+ await fs8.mkdir(path9.dirname(proxyScript), { recursive: true });
2917
+ await fs8.mkdir(path9.dirname(configEntry), { recursive: true });
2918
+ await fs8.writeFile(
3018
2919
  proxyScript,
3019
2920
  proxyScriptContent(opts.name, opts.targetDir),
3020
2921
  { mode: 493 }
3021
2922
  );
3022
- await fs7.chmod(proxyScript, 493).catch(() => {
2923
+ await fs8.chmod(proxyScript, 493).catch(() => {
3023
2924
  });
3024
- await fs7.writeFile(
2925
+ await fs8.writeFile(
3025
2926
  configEntry,
3026
2927
  configEntryContent(
3027
2928
  opts.name,
@@ -3049,17 +2950,17 @@ async function setupSshAttach(opts) {
3049
2950
  return { hostAlias, configured: true };
3050
2951
  }
3051
2952
  async function upsertKnownHost(knownHostsPath, hostId, typeAndKey) {
3052
- await fs7.mkdir(path9.dirname(knownHostsPath), { recursive: true });
2953
+ await fs8.mkdir(path9.dirname(knownHostsPath), { recursive: true });
3053
2954
  let existing = "";
3054
2955
  try {
3055
- existing = await fs7.readFile(knownHostsPath, "utf8");
2956
+ existing = await fs8.readFile(knownHostsPath, "utf8");
3056
2957
  } catch {
3057
2958
  existing = "";
3058
2959
  }
3059
2960
  const kept = existing.split("\n").filter((l) => l.trim().length > 0 && l.split(/\s+/)[0] !== hostId);
3060
2961
  const next = `${[...kept, `${hostId} ${typeAndKey}`].join("\n")}
3061
2962
  `;
3062
- await fs7.writeFile(knownHostsPath, next);
2963
+ await fs8.writeFile(knownHostsPath, next);
3063
2964
  }
3064
2965
  async function recordHostKey(opts) {
3065
2966
  const pubPath = path9.join(
@@ -3071,7 +2972,7 @@ async function recordHostKey(opts) {
3071
2972
  );
3072
2973
  let line = "";
3073
2974
  try {
3074
- line = (await fs7.readFile(pubPath, "utf8")).trim();
2975
+ line = (await fs8.readFile(pubPath, "utf8")).trim();
3075
2976
  } catch {
3076
2977
  return;
3077
2978
  }
@@ -3097,8 +2998,8 @@ async function recordHostKey(opts) {
3097
2998
  }
3098
2999
  }
3099
3000
  async function removeSshAttach(home, name, windows) {
3100
- await fs7.rm(sshProxyScriptPath(home, name), { force: true });
3101
- await fs7.rm(sshConfigEntryPath(home, name), { force: true });
3001
+ await fs8.rm(sshProxyScriptPath(home, name), { force: true });
3002
+ await fs8.rm(sshConfigEntryPath(home, name), { force: true });
3102
3003
  try {
3103
3004
  await removeWindowsBridge(
3104
3005
  name,
@@ -3114,7 +3015,7 @@ var init_ssh_attach = __esm({
3114
3015
  "use strict";
3115
3016
  realKeygen = (args) => {
3116
3017
  return new Promise((resolve, reject) => {
3117
- const child = spawn4("ssh-keygen", args, {
3018
+ const child = spawn3("ssh-keygen", args, {
3118
3019
  stdio: ["ignore", "ignore", "pipe"]
3119
3020
  });
3120
3021
  let stderr = "";
@@ -3289,13 +3190,26 @@ var init_opencode_config = __esm({
3289
3190
  });
3290
3191
 
3291
3192
  // src/create/scaffold.ts
3292
- import { existsSync as existsSync8, promises as fs8 } from "fs";
3193
+ import { existsSync as existsSync8, promises as fs9 } from "fs";
3293
3194
  import path11 from "path";
3294
3195
  function deriveRepoName(url) {
3295
3196
  const lastSep = Math.max(url.lastIndexOf("/"), url.lastIndexOf(":"));
3296
3197
  const tail = url.slice(lastSep + 1);
3297
3198
  return tail.replace(/\.git$/, "");
3298
3199
  }
3200
+ function cloneUrl(url) {
3201
+ try {
3202
+ const u = new URL(url);
3203
+ if (u.username || u.password) {
3204
+ u.username = "";
3205
+ u.password = "";
3206
+ return u.toString();
3207
+ }
3208
+ return url;
3209
+ } catch {
3210
+ return url;
3211
+ }
3212
+ }
3299
3213
  function validateOptions(opts) {
3300
3214
  if (!opts.name || !/^[a-zA-Z0-9._-]+$/.test(opts.name)) {
3301
3215
  throw new Error(
@@ -4075,10 +3989,11 @@ function buildPostCreateScript(opts) {
4075
3989
  if (parent) {
4076
3990
  lines.push(`mkdir -p "projects/${parent}"`);
4077
3991
  }
3992
+ const url = cloneUrl(repo.url);
4078
3993
  lines.push(
4079
3994
  `if [ ! -d "projects/${repo.path}" ]; then`,
4080
- ` echo "\u2192 Cloning ${repo.path} from ${repo.url}\u2026"`,
4081
- ` git clone "${repo.url}" "projects/${repo.path}"`,
3995
+ ` echo "\u2192 Cloning ${repo.path} from ${url}\u2026"`,
3996
+ ` git clone "${url}" "projects/${repo.path}"`,
4082
3997
  `else`,
4083
3998
  ` echo "\u2192 projects/${repo.path} already exists, skipping clone"`,
4084
3999
  `fi`
@@ -4097,15 +4012,15 @@ function buildPostCreateScript(opts) {
4097
4012
  }
4098
4013
  async function writePostCreateScript(devcontainerDir, opts) {
4099
4014
  const dest = path11.join(devcontainerDir, "post-create.sh");
4100
- await fs8.writeFile(dest, buildPostCreateScript(opts));
4101
- await fs8.chmod(dest, 493);
4015
+ await fs9.writeFile(dest, buildPostCreateScript(opts));
4016
+ await fs9.chmod(dest, 493);
4102
4017
  }
4103
4018
  async function writeIfChanged(filePath, content) {
4104
4019
  try {
4105
- if (await fs8.readFile(filePath, "utf8") === content) return false;
4020
+ if (await fs9.readFile(filePath, "utf8") === content) return false;
4106
4021
  } catch {
4107
4022
  }
4108
- await fs8.writeFile(filePath, content);
4023
+ await fs9.writeFile(filePath, content);
4109
4024
  return true;
4110
4025
  }
4111
4026
  async function writeScaffold(opts, targetDir, scaffoldOpts = {}) {
@@ -4115,36 +4030,36 @@ async function writeScaffold(opts, targetDir, scaffoldOpts = {}) {
4115
4030
  const projectsDir = path11.join(targetDir, "projects");
4116
4031
  const homeDir = path11.join(targetDir, "home");
4117
4032
  const dataDir = path11.join(targetDir, "data");
4118
- await fs8.mkdir(devcontainerDir, { recursive: true });
4119
- await fs8.mkdir(monocerosDir, { recursive: true });
4120
- await fs8.mkdir(projectsDir, { recursive: true });
4121
- await fs8.mkdir(homeDir, { recursive: true });
4033
+ await fs9.mkdir(devcontainerDir, { recursive: true });
4034
+ await fs9.mkdir(monocerosDir, { recursive: true });
4035
+ await fs9.mkdir(projectsDir, { recursive: true });
4036
+ await fs9.mkdir(homeDir, { recursive: true });
4122
4037
  if (needsCompose(opts)) {
4123
- await fs8.mkdir(dataDir, { recursive: true });
4038
+ await fs9.mkdir(dataDir, { recursive: true });
4124
4039
  for (const svc of opts.services) {
4125
4040
  const hasDataVolume = svc.volumes.some((v) => v.split(":")[0] === "data");
4126
4041
  if (hasDataVolume) {
4127
- await fs8.mkdir(path11.join(dataDir, svc.name), { recursive: true });
4042
+ await fs9.mkdir(path11.join(dataDir, svc.name), { recursive: true });
4128
4043
  }
4129
4044
  }
4130
4045
  for (const dir of serviceVolumeHostDirs(opts.services, opts.repos)) {
4131
- await fs8.mkdir(path11.join(targetDir, dir), { recursive: true });
4046
+ await fs9.mkdir(path11.join(targetDir, dir), { recursive: true });
4132
4047
  }
4133
4048
  }
4134
4049
  const containerGitignore = path11.join(targetDir, ".gitignore");
4135
- await fs8.writeFile(
4050
+ await fs9.writeFile(
4136
4051
  containerGitignore,
4137
4052
  "/home/\n/.monoceros/\n/data/\n/AGENTS.md\n/CLAUDE.md\n"
4138
4053
  );
4139
4054
  const gitkeep = path11.join(projectsDir, ".gitkeep");
4140
4055
  if (!existsSync8(gitkeep)) {
4141
- await fs8.writeFile(gitkeep, "");
4056
+ await fs9.writeFile(gitkeep, "");
4142
4057
  }
4143
- await fs8.writeFile(
4058
+ await fs9.writeFile(
4144
4059
  path11.join(monocerosDir, ".gitignore"),
4145
4060
  "git-credentials*\ngitconfig\nglobal-gitignore\n"
4146
4061
  );
4147
- await fs8.writeFile(
4062
+ await fs9.writeFile(
4148
4063
  path11.join(monocerosDir, "global-gitignore"),
4149
4064
  ".monoceros/\n"
4150
4065
  );
@@ -4155,26 +4070,26 @@ async function writeScaffold(opts, targetDir, scaffoldOpts = {}) {
4155
4070
  );
4156
4071
  const featuresDir = path11.join(devcontainerDir, "features");
4157
4072
  if (existsSync8(featuresDir)) {
4158
- await fs8.rm(featuresDir, { recursive: true, force: true });
4073
+ await fs9.rm(featuresDir, { recursive: true, force: true });
4159
4074
  }
4160
4075
  const resolvedFeatures = resolveFeatures(opts);
4161
4076
  for (const f of resolvedFeatures) {
4162
4077
  if (!f.localSourceDir || !f.localName) continue;
4163
4078
  const dest = path11.join(featuresDir, f.localName);
4164
- await fs8.mkdir(dest, { recursive: true });
4165
- const entries = await fs8.readdir(f.localSourceDir, { withFileTypes: true });
4079
+ await fs9.mkdir(dest, { recursive: true });
4080
+ const entries = await fs9.readdir(f.localSourceDir, { withFileTypes: true });
4166
4081
  for (const entry2 of entries) {
4167
4082
  if (!entry2.isFile()) continue;
4168
4083
  if (entry2.name === "component.yml" || entry2.name === "devcontainer-feature.json") {
4169
4084
  continue;
4170
4085
  }
4171
- await fs8.cp(
4086
+ await fs9.cp(
4172
4087
  path11.join(f.localSourceDir, entry2.name),
4173
4088
  path11.join(dest, entry2.name)
4174
4089
  );
4175
4090
  }
4176
4091
  if (f.generatedManifest) {
4177
- await fs8.writeFile(
4092
+ await fs9.writeFile(
4178
4093
  path11.join(dest, "devcontainer-feature.json"),
4179
4094
  JSON.stringify(f.generatedManifest, null, 2) + "\n"
4180
4095
  );
@@ -4182,13 +4097,13 @@ async function writeScaffold(opts, targetDir, scaffoldOpts = {}) {
4182
4097
  }
4183
4098
  for (const f of resolvedFeatures) {
4184
4099
  for (const sub of f.persistentHomePaths) {
4185
- await fs8.mkdir(path11.join(homeDir, sub), { recursive: true });
4100
+ await fs9.mkdir(path11.join(homeDir, sub), { recursive: true });
4186
4101
  }
4187
4102
  for (const entry2 of f.persistentHomeFiles) {
4188
4103
  const filePath = path11.join(homeDir, entry2.path);
4189
- await fs8.mkdir(path11.dirname(filePath), { recursive: true });
4104
+ await fs9.mkdir(path11.dirname(filePath), { recursive: true });
4190
4105
  if (!existsSync8(filePath)) {
4191
- await fs8.writeFile(filePath, entry2.initialContent);
4106
+ await fs9.writeFile(filePath, entry2.initialContent);
4192
4107
  }
4193
4108
  }
4194
4109
  }
@@ -4199,29 +4114,29 @@ async function writeScaffold(opts, targetDir, scaffoldOpts = {}) {
4199
4114
  if (needsCompose(opts)) {
4200
4115
  await writeIfChanged(composePath, buildComposeYaml(opts, dockerMode));
4201
4116
  } else if (existsSync8(composePath)) {
4202
- await fs8.rm(composePath);
4117
+ await fs9.rm(composePath);
4203
4118
  }
4204
4119
  const workspacePath = path11.join(targetDir, `${opts.name}.code-workspace`);
4205
4120
  let existingWorkspace;
4206
4121
  try {
4207
- const raw = await fs8.readFile(workspacePath, "utf8");
4122
+ const raw = await fs9.readFile(workspacePath, "utf8");
4208
4123
  existingWorkspace = JSON.parse(raw);
4209
4124
  } catch {
4210
4125
  existingWorkspace = void 0;
4211
4126
  }
4212
4127
  const generated = buildCodeWorkspaceJson(opts);
4213
4128
  const merged = mergeCodeWorkspace(existingWorkspace, generated);
4214
- await fs8.writeFile(workspacePath, JSON.stringify(merged, null, 2) + "\n");
4129
+ await fs9.writeFile(workspacePath, JSON.stringify(merged, null, 2) + "\n");
4215
4130
  const vscodeDir = path11.join(targetDir, ".vscode");
4216
4131
  const settingsPath = path11.join(vscodeDir, "settings.json");
4217
4132
  let existingSettings;
4218
4133
  try {
4219
- existingSettings = JSON.parse(await fs8.readFile(settingsPath, "utf8"));
4134
+ existingSettings = JSON.parse(await fs9.readFile(settingsPath, "utf8"));
4220
4135
  } catch {
4221
4136
  existingSettings = void 0;
4222
4137
  }
4223
- await fs8.mkdir(vscodeDir, { recursive: true });
4224
- await fs8.writeFile(
4138
+ await fs9.mkdir(vscodeDir, { recursive: true });
4139
+ await fs9.writeFile(
4225
4140
  settingsPath,
4226
4141
  JSON.stringify(mergeVscodeSettings(existingSettings), null, 2) + "\n"
4227
4142
  );
@@ -4976,7 +4891,7 @@ var init_markers = __esm({
4976
4891
  });
4977
4892
 
4978
4893
  // src/briefing/index.ts
4979
- import { promises as fs9 } from "fs";
4894
+ import { promises as fs10 } from "fs";
4980
4895
  import path12 from "path";
4981
4896
  async function writeBriefing(input) {
4982
4897
  const subCommands = input.subCommands ?? await loadSubCommandsDynamic();
@@ -4994,8 +4909,8 @@ async function writeBriefing(input) {
4994
4909
  await writeMarkerAware(path12.join(input.targetDir, "AGENTS.md"), agentsBody);
4995
4910
  await writeMarkerAware(path12.join(input.targetDir, "CLAUDE.md"), claudeBody);
4996
4911
  const monocerosDir = path12.join(input.targetDir, ".monoceros");
4997
- await fs9.mkdir(monocerosDir, { recursive: true });
4998
- await fs9.writeFile(
4912
+ await fs10.mkdir(monocerosDir, { recursive: true });
4913
+ await fs10.writeFile(
4999
4914
  path12.join(monocerosDir, "commands.md"),
5000
4915
  commandsBody,
5001
4916
  "utf8"
@@ -5016,18 +4931,18 @@ function featureDisplayMap(components) {
5016
4931
  async function writeMarkerAware(filePath, body) {
5017
4932
  let existing = null;
5018
4933
  try {
5019
- existing = await fs9.readFile(filePath, "utf8");
4934
+ existing = await fs10.readFile(filePath, "utf8");
5020
4935
  } catch (err) {
5021
4936
  if (err.code !== "ENOENT") throw err;
5022
4937
  }
5023
4938
  if (existing) {
5024
4939
  const updated = replaceMarkerBlock(existing, body);
5025
4940
  if (updated !== null) {
5026
- await fs9.writeFile(filePath, updated, "utf8");
4941
+ await fs10.writeFile(filePath, updated, "utf8");
5027
4942
  return;
5028
4943
  }
5029
4944
  }
5030
- await fs9.writeFile(filePath, wrapWithMarkers(body), "utf8");
4945
+ await fs10.writeFile(filePath, wrapWithMarkers(body), "utf8");
5031
4946
  }
5032
4947
  async function loadSubCommandsDynamic() {
5033
4948
  const mod = await Promise.resolve().then(() => (init_main(), main_exports));
@@ -5519,7 +5434,7 @@ var init_yml = __esm({
5519
5434
  });
5520
5435
 
5521
5436
  // src/modify/index.ts
5522
- import { promises as fs10 } from "fs";
5437
+ import { promises as fs11 } from "fs";
5523
5438
  import { consola as consola2 } from "consola";
5524
5439
  import { createPatch } from "diff";
5525
5440
  import path13 from "path";
@@ -5603,7 +5518,7 @@ function runAddAptPackages(input) {
5603
5518
  async function refreshBriefingFromYml(name, home) {
5604
5519
  const targetDir = containerDir(name, home);
5605
5520
  try {
5606
- await fs10.access(targetDir);
5521
+ await fs11.access(targetDir);
5607
5522
  } catch {
5608
5523
  return;
5609
5524
  }
@@ -5691,6 +5606,23 @@ async function runAddRepo(input) {
5691
5606
  `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.`
5692
5607
  );
5693
5608
  }
5609
+ if (result.status === "updated" && host) {
5610
+ const home = input.monocerosHome ?? monocerosHome();
5611
+ const prov = resolveProvider(host, entry2.provider);
5612
+ const featureShort = prov === "github" ? "github" : prov === "gitlab" ? "gitlab" : void 0;
5613
+ if (featureShort) {
5614
+ const yml = await fs11.readFile(
5615
+ containerConfigPath(input.name, home),
5616
+ "utf8"
5617
+ );
5618
+ const cfg = parseConfig(yml).config;
5619
+ const ref = (await resolveFeatureRefOrShortname(featureShort)).ref;
5620
+ const present = (cfg.features ?? []).some((f) => f.ref === ref);
5621
+ if (!present) {
5622
+ await runAddFeature({ ...input, ref: featureShort });
5623
+ }
5624
+ }
5625
+ }
5694
5626
  if (result.status === "updated") {
5695
5627
  await tryCloneInRunningContainer(input, entry2);
5696
5628
  await refreshBriefingFromYml(
@@ -5738,26 +5670,25 @@ async function tryCloneInRunningContainer(input, entry2) {
5738
5670
  return;
5739
5671
  }
5740
5672
  try {
5741
- const credsResult = await collectGitCredentials(
5742
- root,
5743
- [{ host: urlHost, provider }],
5744
- {
5745
- ...input.credentialsSpawn ? { spawn: input.credentialsSpawn } : {},
5673
+ const { hostTokens } = await resolveContainerRepoTokens(
5674
+ input.name,
5675
+ home,
5676
+ await loadComponentCatalog()
5677
+ );
5678
+ if (hostTokens.get(urlHost)) {
5679
+ await collectGitCredentials(root, [{ host: urlHost, provider }], {
5680
+ patByHost: hostTokens,
5746
5681
  logger: { info: () => {
5747
5682
  }, warn: (m) => logger.warn(m) }
5748
- }
5749
- );
5750
- const status = credsResult.perHost.find((h) => h.host === urlHost);
5751
- if (!status || status.status !== "ok") {
5752
- const detail = status?.detail ? `: ${status.detail}` : "";
5683
+ });
5684
+ } else {
5753
5685
  logger.warn(
5754
- `No HTTPS credentials available for ${urlHost}${detail}. The yml is updated; set up credentials (e.g. \`gh auth login\`) and re-run \`monoceros apply ${input.name}\` or rerun this add-repo.`
5686
+ `No access token set for ${urlHost} \u2014 cloning will fail if this repo is private. Set one (see ${REPO_DOCS_URL}) and re-run.`
5755
5687
  );
5756
- return;
5757
5688
  }
5758
5689
  } catch (err) {
5759
5690
  logger.warn(
5760
- `Credential fetch for ${urlHost} failed: ${err instanceof Error ? err.message : String(err)}. The yml is updated.`
5691
+ `Could not resolve a token for ${urlHost}: ${err instanceof Error ? err.message : String(err)}. The yml is updated; run \`monoceros apply ${input.name}\` to clone.`
5761
5692
  );
5762
5693
  return;
5763
5694
  }
@@ -5774,7 +5705,7 @@ async function tryCloneInRunningContainer(input, entry2) {
5774
5705
  ` exit 0`,
5775
5706
  `fi`,
5776
5707
  `mkdir -p ${shquote(parentRel)}`,
5777
- `git -c ${shquote(`credential.helper=${credentialHelper}`)} clone ${shquote(entry2.url)} ${shquote(targetRel)}`
5708
+ `git -c ${shquote(`credential.helper=${credentialHelper}`)} clone ${shquote(cloneUrl(entry2.url))} ${shquote(targetRel)}`
5778
5709
  ];
5779
5710
  if (entry2.gitUser) {
5780
5711
  script.push(
@@ -6021,7 +5952,7 @@ async function mutate(opts, apply) {
6021
5952
  const logger = opts.logger ?? defaultLogger();
6022
5953
  let oldText;
6023
5954
  try {
6024
- oldText = await fs10.readFile(ymlPath, "utf8");
5955
+ oldText = await fs11.readFile(ymlPath, "utf8");
6025
5956
  } catch {
6026
5957
  throw new Error(
6027
5958
  `No such config: ${ymlPath}. Run \`monoceros init <template> ${opts.name}\` first.`
@@ -6046,7 +5977,7 @@ async function mutate(opts, apply) {
6046
5977
  return { status: "aborted" };
6047
5978
  }
6048
5979
  }
6049
- await fs10.writeFile(ymlPath, newText, "utf8");
5980
+ await fs11.writeFile(ymlPath, newText, "utf8");
6050
5981
  logger.success(`Updated ${ymlPath}.`);
6051
5982
  logger.info(
6052
5983
  `Run \`monoceros apply ${opts.name}\` to rebuild the dev-container and pick up the change.`
@@ -6125,6 +6056,8 @@ var init_modify = __esm({
6125
6056
  init_feature_doc();
6126
6057
  init_manifest();
6127
6058
  init_credentials();
6059
+ init_repo_token();
6060
+ init_schema();
6128
6061
  init_locate_running();
6129
6062
  init_global();
6130
6063
  init_schema();
@@ -6400,7 +6333,7 @@ var init_add_repo = __esm({
6400
6333
  },
6401
6334
  provider: {
6402
6335
  type: "string",
6403
- description: "Git provider for credential-helper guidance: github | gitlab | bitbucket | gitea. Required when the URL host is not github.com, gitlab.com, or bitbucket.org \u2014 Monoceros uses this to suggest the right auth (gh / glab / a provider token) on missing credentials."
6336
+ description: "Git provider: github | gitlab | bitbucket. Required when the URL host is not github.com, gitlab.com, or bitbucket.org, so Monoceros knows which provider token to use for cloning."
6404
6337
  },
6405
6338
  yes: {
6406
6339
  type: "boolean",
@@ -6596,7 +6529,7 @@ var init_add_service = __esm({
6596
6529
  });
6597
6530
 
6598
6531
  // src/config/state.ts
6599
- import { promises as fs11 } from "fs";
6532
+ import { promises as fs12 } from "fs";
6600
6533
  import path14 from "path";
6601
6534
  function buildStateFile(opts) {
6602
6535
  return {
@@ -6612,7 +6545,7 @@ function stateFilePath(targetDir) {
6612
6545
  }
6613
6546
  async function readStateFile(targetDir) {
6614
6547
  try {
6615
- const content = await fs11.readFile(stateFilePath(targetDir), "utf8");
6548
+ const content = await fs12.readFile(stateFilePath(targetDir), "utf8");
6616
6549
  return JSON.parse(content);
6617
6550
  } catch {
6618
6551
  return void 0;
@@ -6620,8 +6553,8 @@ async function readStateFile(targetDir) {
6620
6553
  }
6621
6554
  async function writeStateFile(targetDir, state) {
6622
6555
  const monocerosDir = path14.join(targetDir, ".monoceros");
6623
- await fs11.mkdir(monocerosDir, { recursive: true });
6624
- await fs11.writeFile(
6556
+ await fs12.mkdir(monocerosDir, { recursive: true });
6557
+ await fs12.writeFile(
6625
6558
  stateFilePath(targetDir),
6626
6559
  JSON.stringify(state, null, 2) + "\n"
6627
6560
  );
@@ -6634,7 +6567,7 @@ var init_state = __esm({
6634
6567
  });
6635
6568
 
6636
6569
  // src/devcontainer/app-control.ts
6637
- import { promises as fs12 } from "fs";
6570
+ import { promises as fs13 } from "fs";
6638
6571
  import path15 from "path";
6639
6572
  function findRunningContainer(name, opts = {}) {
6640
6573
  return findRunningContainerByLocalFolder(containerDir(name), {
@@ -6657,7 +6590,7 @@ async function hasWantedApps(name, home) {
6657
6590
  async function anyPid(dir) {
6658
6591
  let entries;
6659
6592
  try {
6660
- entries = await fs12.readdir(dir, { withFileTypes: true });
6593
+ entries = await fs13.readdir(dir, { withFileTypes: true });
6661
6594
  } catch {
6662
6595
  return false;
6663
6596
  }
@@ -6684,7 +6617,7 @@ var init_app_control = __esm({
6684
6617
  });
6685
6618
 
6686
6619
  // src/devcontainer/browser-bridge.ts
6687
- import { spawn as spawn5 } from "child_process";
6620
+ import { spawn as spawn4 } from "child_process";
6688
6621
  import { existsSync as existsSync9, promises as fsp4, readFileSync as readFileSync5 } from "fs";
6689
6622
  import http from "http";
6690
6623
  import path16 from "path";
@@ -6742,7 +6675,7 @@ function openInBrowser(url) {
6742
6675
  const psOpen = `Start-Process '${url.replace(/'/g, "''")}'`;
6743
6676
  const [cmd, args] = platform === "darwin" ? ["open", [url]] : platform === "win32" || wsl ? ["powershell.exe", ["-NoProfile", "-Command", psOpen]] : ["xdg-open", [url]];
6744
6677
  try {
6745
- const child = spawn5(cmd, args, {
6678
+ const child = spawn4(cmd, args, {
6746
6679
  stdio: "ignore",
6747
6680
  detached: true
6748
6681
  });
@@ -6948,7 +6881,7 @@ var init_runtime_pull_hint = __esm({
6948
6881
  });
6949
6882
 
6950
6883
  // src/devcontainer/cli.ts
6951
- import { spawn as spawn6 } from "child_process";
6884
+ import { spawn as spawn5 } from "child_process";
6952
6885
  import { readFileSync as readFileSync6 } from "fs";
6953
6886
  import { createRequire } from "module";
6954
6887
  import path17 from "path";
@@ -6976,7 +6909,7 @@ var init_cli = __esm({
6976
6909
  const env = { ...process.env, DOCKER_CLI_HINTS: "false" };
6977
6910
  return new Promise((resolve, reject) => {
6978
6911
  if (options.interactive) {
6979
- const child2 = spawn6(process.execPath, [binPath, ...args], {
6912
+ const child2 = spawn5(process.execPath, [binPath, ...args], {
6980
6913
  cwd,
6981
6914
  env,
6982
6915
  stdio: "inherit"
@@ -6985,7 +6918,7 @@ var init_cli = __esm({
6985
6918
  child2.on("exit", (code) => resolve(code ?? 0));
6986
6919
  return;
6987
6920
  }
6988
- const child = spawn6(process.execPath, [binPath, ...args], {
6921
+ const child = spawn5(process.execPath, [binPath, ...args], {
6989
6922
  cwd,
6990
6923
  env,
6991
6924
  stdio: ["ignore", "pipe", "pipe"]
@@ -7033,14 +6966,14 @@ var init_cli = __esm({
7033
6966
  });
7034
6967
 
7035
6968
  // src/devcontainer/compose.ts
7036
- import { spawn as spawn7 } from "child_process";
6969
+ import { spawn as spawn6 } from "child_process";
7037
6970
  import { existsSync as existsSync10 } from "fs";
7038
6971
  import path18 from "path";
7039
6972
  import { Writable } from "stream";
7040
6973
  import { consola as consola10 } from "consola";
7041
6974
  function spawnDockerComposeTo(opts) {
7042
6975
  return (args, cwd) => new Promise((resolve, reject) => {
7043
- const child = spawn7("docker", ["compose", ...args], {
6976
+ const child = spawn6("docker", ["compose", ...args], {
7044
6977
  cwd,
7045
6978
  stdio: ["inherit", "pipe", "pipe"]
7046
6979
  });
@@ -7368,7 +7301,7 @@ var init_compose = __esm({
7368
7301
  init_proxy();
7369
7302
  spawnDockerCompose = (args, cwd) => {
7370
7303
  return new Promise((resolve, reject) => {
7371
- const child = spawn7("docker", ["compose", ...args], {
7304
+ const child = spawn6("docker", ["compose", ...args], {
7372
7305
  cwd,
7373
7306
  stdio: ["inherit", "pipe", "pipe"]
7374
7307
  });
@@ -7380,7 +7313,7 @@ var init_compose = __esm({
7380
7313
  };
7381
7314
  spawnDocker = (args) => {
7382
7315
  return new Promise((resolve, reject) => {
7383
- const child = spawn7("docker", args, {
7316
+ const child = spawn6("docker", args, {
7384
7317
  stdio: ["ignore", "pipe", "pipe"]
7385
7318
  });
7386
7319
  let stdout = "";
@@ -7405,7 +7338,7 @@ var init_compose = __esm({
7405
7338
  });
7406
7339
 
7407
7340
  // src/devcontainer/bridge-daemon.ts
7408
- import { spawn as spawn8 } from "child_process";
7341
+ import { spawn as spawn7 } from "child_process";
7409
7342
  import {
7410
7343
  existsSync as existsSync11,
7411
7344
  mkdirSync,
@@ -7442,7 +7375,7 @@ function spawnBridgeDaemon(root) {
7442
7375
  const self = process.argv[1];
7443
7376
  if (!self) return;
7444
7377
  mkdirSync(relayDir(root), { recursive: true });
7445
- const child = spawn8(
7378
+ const child = spawn7(
7446
7379
  process.execPath,
7447
7380
  [...process.execArgv, self, "__bridge", root],
7448
7381
  {
@@ -7671,8 +7604,11 @@ function createApplyProgress(opts) {
7671
7604
  tail.push(line);
7672
7605
  if (tail.length > TAIL_LINES) tail.shift();
7673
7606
  for (const trig of PHASE_TRIGGERS) {
7674
- if (trig.pattern.test(line)) {
7675
- setPhase(trig.label);
7607
+ const m = line.match(trig.pattern);
7608
+ if (m) {
7609
+ setPhase(
7610
+ typeof trig.label === "function" ? trig.label(m) : trig.label
7611
+ );
7676
7612
  break;
7677
7613
  }
7678
7614
  }
@@ -7768,6 +7704,15 @@ var init_apply_progress = __esm({
7768
7704
  pattern: /Start: Run: docker (?:run\b|compose\b.* up\b)/i,
7769
7705
  label: "starting container\u2026"
7770
7706
  },
7707
+ // post-create.sh sub-steps. Repo clones are the long, network-bound
7708
+ // part; surface which repo is being cloned instead of an opaque
7709
+ // "running postCreate…". The marker is the `→ Cloning <path> from …`
7710
+ // line post-create.sh echoes (see create/scaffold.ts). A devcontainer
7711
+ // timestamp prefix is fine — the pattern is unanchored.
7712
+ {
7713
+ pattern: /→ Cloning (\S+) from /,
7714
+ label: (m) => `cloning ${m[1]}\u2026`
7715
+ },
7771
7716
  { pattern: /Running the postCreateCommand/i, label: "running postCreate\u2026" }
7772
7717
  ];
7773
7718
  }
@@ -7941,7 +7886,7 @@ var init_machine_state = __esm({
7941
7886
  });
7942
7887
 
7943
7888
  // src/devcontainer/docker-mode.ts
7944
- import { spawn as spawn9 } from "child_process";
7889
+ import { spawn as spawn8 } from "child_process";
7945
7890
  async function detectDockerMode(options = {}) {
7946
7891
  const spawnFn = options.spawn ?? realDockerInfo;
7947
7892
  try {
@@ -7991,7 +7936,7 @@ var init_docker_mode = __esm({
7991
7936
  init_format();
7992
7937
  realDockerInfo = () => {
7993
7938
  return new Promise((resolve, reject) => {
7994
- const child = spawn9(
7939
+ const child = spawn8(
7995
7940
  "docker",
7996
7941
  ["info", "--format", "{{json .SecurityOptions}}"],
7997
7942
  {
@@ -8010,8 +7955,8 @@ var init_docker_mode = __esm({
8010
7955
  });
8011
7956
 
8012
7957
  // src/devcontainer/identity.ts
8013
- import { spawn as spawn10 } from "child_process";
8014
- import { promises as fs13 } from "fs";
7958
+ import { spawn as spawn9 } from "child_process";
7959
+ import { promises as fs14 } from "fs";
8015
7960
  import path22 from "path";
8016
7961
  import { consola as consola11 } from "consola";
8017
7962
  async function resolveIdentityWithPrompt(options = {}) {
@@ -8082,8 +8027,8 @@ async function collectGitIdentity(devContainerRoot, options = {}) {
8082
8027
  const lines = ["[user]"];
8083
8028
  if (resolved.name !== void 0) lines.push(` name = ${resolved.name}`);
8084
8029
  if (resolved.email !== void 0) lines.push(` email = ${resolved.email}`);
8085
- await fs13.mkdir(gitconfigDir, { recursive: true });
8086
- await fs13.writeFile(gitconfigPath, lines.join("\n") + "\n");
8030
+ await fs14.mkdir(gitconfigDir, { recursive: true });
8031
+ await fs14.writeFile(gitconfigPath, lines.join("\n") + "\n");
8087
8032
  return {
8088
8033
  ...resolved.name !== void 0 ? { name: resolved.name } : {},
8089
8034
  ...resolved.email !== void 0 ? { email: resolved.email } : {},
@@ -8126,7 +8071,7 @@ async function readKeyFromHost(spawnFn, key, logger) {
8126
8071
  }
8127
8072
  async function readExistingGitconfig(filePath) {
8128
8073
  try {
8129
- const content = await fs13.readFile(filePath, "utf8");
8074
+ const content = await fs14.readFile(filePath, "utf8");
8130
8075
  const result = {};
8131
8076
  const nameMatch = /^\s*name\s*=\s*(.+?)\s*$/m.exec(content);
8132
8077
  const emailMatch = /^\s*email\s*=\s*(.+?)\s*$/m.exec(content);
@@ -8143,7 +8088,7 @@ var init_identity = __esm({
8143
8088
  "use strict";
8144
8089
  realGitConfigGet = (key) => {
8145
8090
  return new Promise((resolve, reject) => {
8146
- const child = spawn10("git", ["config", "--global", "--get", key], {
8091
+ const child = spawn9("git", ["config", "--global", "--get", key], {
8147
8092
  stdio: ["ignore", "pipe", "inherit"]
8148
8093
  });
8149
8094
  let stdout = "";
@@ -8203,7 +8148,7 @@ var init_identity = __esm({
8203
8148
  });
8204
8149
 
8205
8150
  // src/apply/index.ts
8206
- import { existsSync as existsSync12, promises as fs14 } from "fs";
8151
+ import { existsSync as existsSync12, promises as fs15 } from "fs";
8207
8152
  import { consola as consola12 } from "consola";
8208
8153
  async function runApply(opts) {
8209
8154
  const home = opts.monocerosHome ?? monocerosHome();
@@ -8243,9 +8188,18 @@ ${sectionLine(label)}
8243
8188
  warnOnDeprecatedFeatureRefs(parsed.config.features, globalConfig, logger);
8244
8189
  const envPath = containerEnvPath(opts.name, home);
8245
8190
  await ensureEnvGitignored(containerConfigsDir(home));
8246
- const envVars = readEnvFile(envPath);
8191
+ const envVars = {
8192
+ ...readEnvFile(globalEnvPath(home)),
8193
+ ...readEnvFile(envPath),
8194
+ ...opts.env ?? {}
8195
+ };
8196
+ const catalog = await loadComponentCatalog();
8197
+ const repoTokens = resolveRepoTokens(parsed.config, catalog, envVars);
8198
+ for (const use of repoTokens.used) {
8199
+ logger.info(`Repo token: ${formatTokenUse(use)}`);
8200
+ }
8247
8201
  const resolvedFeatures = interpolateFeatureOptions(
8248
- parsed.config.features,
8202
+ repoTokens.features,
8249
8203
  envVars
8250
8204
  );
8251
8205
  const createOpts = normalizeOptions(
@@ -8327,13 +8281,11 @@ Fix the value in the env file (or the yml).`
8327
8281
  throw new Error(formatUnknownProviderError(unknownProviderHosts));
8328
8282
  }
8329
8283
  if (hostsToFetch.length > 0) {
8330
- const credResult = await collectGitCredentials(targetDir, hostsToFetch, {
8331
- ...opts.credentialsSpawn ? { spawn: opts.credentialsSpawn } : {},
8332
- logger: idLogger
8333
- });
8334
- const missing = credResult.perHost.filter((p) => p.status !== "ok");
8335
- if (missing.length > 0) {
8336
- throw new Error(formatMissingCredentialsError(missing));
8284
+ if (repoTokens.hostTokens.size > 0) {
8285
+ await collectGitCredentials(targetDir, hostsToFetch, {
8286
+ patByHost: repoTokens.hostTokens,
8287
+ logger: idLogger
8288
+ });
8337
8289
  }
8338
8290
  }
8339
8291
  section("Scaffold");
@@ -8343,7 +8295,7 @@ Fix the value in the env file (or the yml).`
8343
8295
  if (dockerMode === "rootless") {
8344
8296
  throw new Error(formatRootlessNotSupportedError());
8345
8297
  }
8346
- await fs14.mkdir(targetDir, { recursive: true });
8298
+ await fs15.mkdir(targetDir, { recursive: true });
8347
8299
  await writeScaffold(createOpts, targetDir, { dockerMode });
8348
8300
  await writeStateFile(
8349
8301
  targetDir,
@@ -8567,6 +8519,15 @@ ${stripAnsi(formatted)}
8567
8519
  `);
8568
8520
  }
8569
8521
  }
8522
+ if (repoTokens.missing.length > 0) {
8523
+ const warning = formatUnauthenticatedRepos(repoTokens.missing, opts.name);
8524
+ progressOut.write(`
8525
+ ${warning}
8526
+ `);
8527
+ applyLog.stream.write(`
8528
+ ${stripAnsi(warning)}
8529
+ `);
8530
+ }
8570
8531
  await applyLog.close();
8571
8532
  progressOut.write(`
8572
8533
  ${dim(`log: ${prettyPath(applyLog.path)}`)}
@@ -8582,7 +8543,7 @@ ${stripAnsi(formatted)}
8582
8543
  }
8583
8544
  async function assertSafeTargetDir(targetDir, expectedOrigin) {
8584
8545
  if (!existsSync12(targetDir)) return;
8585
- const entries = await fs14.readdir(targetDir);
8546
+ const entries = await fs15.readdir(targetDir);
8586
8547
  if (entries.length === 0) return;
8587
8548
  const state = await readStateFile(targetDir);
8588
8549
  if (state) {
@@ -8652,7 +8613,7 @@ async function persistPromptedIdentity(prompted, ymlPath, home, logger) {
8652
8613
  }
8653
8614
  if (wantContainer) {
8654
8615
  try {
8655
- const text = await fs14.readFile(ymlPath, "utf8");
8616
+ const text = await fs15.readFile(ymlPath, "utf8");
8656
8617
  const parsed = parseConfig(text, ymlPath);
8657
8618
  const changed = setContainerGitUserInDoc(parsed.doc, {
8658
8619
  name: prompted.name,
@@ -8660,7 +8621,7 @@ async function persistPromptedIdentity(prompted, ymlPath, home, logger) {
8660
8621
  });
8661
8622
  if (changed) {
8662
8623
  const out = stringifyConfig(parsed.doc);
8663
- await fs14.writeFile(ymlPath, out, "utf8");
8624
+ await fs15.writeFile(ymlPath, out, "utf8");
8664
8625
  logger.info(
8665
8626
  `Saved identity in this container \u2014 wrote git.user into ${prettyPath(ymlPath)}.`
8666
8627
  );
@@ -8706,6 +8667,7 @@ var init_apply = __esm({
8706
8667
  init_identity();
8707
8668
  init_global();
8708
8669
  init_yml();
8670
+ init_repo_token();
8709
8671
  }
8710
8672
  });
8711
8673
 
@@ -8760,7 +8722,7 @@ var init_shell = __esm({
8760
8722
  });
8761
8723
 
8762
8724
  // src/open/index.ts
8763
- import { spawn as spawn11 } from "child_process";
8725
+ import { spawn as spawn10 } from "child_process";
8764
8726
  import { accessSync, constants, existsSync as existsSync14 } from "fs";
8765
8727
  import path24 from "path";
8766
8728
  import { consola as consola13 } from "consola";
@@ -8796,7 +8758,7 @@ async function resolveEditorBinary(tool) {
8796
8758
  return null;
8797
8759
  }
8798
8760
  function realLaunch(bin, args) {
8799
- const child = spawn11(bin, args, {
8761
+ const child = spawn10(bin, args, {
8800
8762
  detached: true,
8801
8763
  stdio: "ignore"
8802
8764
  });
@@ -8880,7 +8842,7 @@ var CLI_VERSION;
8880
8842
  var init_version = __esm({
8881
8843
  "src/version.ts"() {
8882
8844
  "use strict";
8883
- CLI_VERSION = true ? "1.36.9" : "dev";
8845
+ CLI_VERSION = true ? "1.37.0" : "dev";
8884
8846
  }
8885
8847
  });
8886
8848
 
@@ -9114,7 +9076,7 @@ var init_bridge = __esm({
9114
9076
  });
9115
9077
 
9116
9078
  // src/config/launch-config.ts
9117
- import { promises as fs15 } from "fs";
9079
+ import { promises as fs16 } from "fs";
9118
9080
  import path25 from "path";
9119
9081
  function launchConfigPath(name, appRel, home) {
9120
9082
  return path25.join(
@@ -9167,7 +9129,7 @@ async function readLaunchConfig(name, appRel, home) {
9167
9129
  const file = launchConfigPath(name, appRel, home);
9168
9130
  let content;
9169
9131
  try {
9170
- content = await fs15.readFile(file, "utf8");
9132
+ content = await fs16.readFile(file, "utf8");
9171
9133
  } catch {
9172
9134
  return void 0;
9173
9135
  }
@@ -9209,7 +9171,7 @@ async function listApps(name, home) {
9209
9171
  async function walk(at, rel, depth) {
9210
9172
  let entries;
9211
9173
  try {
9212
- entries = await fs15.readdir(at, { withFileTypes: true });
9174
+ entries = await fs16.readdir(at, { withFileTypes: true });
9213
9175
  } catch {
9214
9176
  return;
9215
9177
  }
@@ -9218,7 +9180,7 @@ async function listApps(name, home) {
9218
9180
  );
9219
9181
  if (hasLaunch) {
9220
9182
  try {
9221
- await fs15.access(path25.join(at, LAUNCH_DIRNAME, LAUNCH_FILENAME));
9183
+ await fs16.access(path25.join(at, LAUNCH_DIRNAME, LAUNCH_FILENAME));
9222
9184
  if (rel) out.push(rel);
9223
9185
  } catch {
9224
9186
  }
@@ -9249,7 +9211,7 @@ var init_launch_config = __esm({
9249
9211
  });
9250
9212
 
9251
9213
  // src/completion/resolve.ts
9252
- import { existsSync as existsSync15, promises as fs16 } from "fs";
9214
+ import { existsSync as existsSync15, promises as fs17 } from "fs";
9253
9215
  import path26 from "path";
9254
9216
  async function resolveCompletions(line, point, opts = {}) {
9255
9217
  const { prev, current } = parseCompletionLine(line, point);
@@ -9401,7 +9363,7 @@ async function listContainerNames(ctx) {
9401
9363
  const home = ctx.opts.monocerosHome ?? monocerosHome();
9402
9364
  const dir = path26.join(home, "container-configs");
9403
9365
  if (!existsSync15(dir)) return [];
9404
- const entries = await fs16.readdir(dir);
9366
+ const entries = await fs17.readdir(dir);
9405
9367
  return entries.filter((e) => e.endsWith(".yml")).map((e) => e.slice(0, -".yml".length)).sort();
9406
9368
  }
9407
9369
  function containerNameFromCtx(ctx) {
@@ -9464,7 +9426,7 @@ async function collectDirs(dir, maxDepth) {
9464
9426
  async function walk(at, rel, depth) {
9465
9427
  let entries;
9466
9428
  try {
9467
- entries = await fs16.readdir(at, { withFileTypes: true });
9429
+ entries = await fs17.readdir(at, { withFileTypes: true });
9468
9430
  } catch {
9469
9431
  return;
9470
9432
  }
@@ -9808,7 +9770,7 @@ var init_complete = __esm({
9808
9770
  });
9809
9771
 
9810
9772
  // src/update/notifier.ts
9811
- import { spawn as spawn12 } from "child_process";
9773
+ import { spawn as spawn11 } from "child_process";
9812
9774
  function isNewerVersion(latest, current) {
9813
9775
  const parse = (v) => {
9814
9776
  const m = /^(\d+)\.(\d+)\.(\d+)/.exec(v.trim());
@@ -9867,7 +9829,7 @@ function spawnBackgroundCheck() {
9867
9829
  const self = process.argv[1];
9868
9830
  if (!self) return;
9869
9831
  try {
9870
- const child = spawn12(process.execPath, [self, "__update-check"], {
9832
+ const child = spawn11(process.execPath, [self, "__update-check"], {
9871
9833
  detached: true,
9872
9834
  stdio: "ignore"
9873
9835
  });
@@ -10061,162 +10023,6 @@ function generateComposedYml(name, composed, lookupManifest, repoUrls = [], port
10061
10023
  }
10062
10024
  return ensureTrailingNewline(lines.join("\n"));
10063
10025
  }
10064
- function generateDocumentedYml(name, catalog, lookupManifest, repoUrls = [], ports = []) {
10065
- const byCategory = groupByCategory(catalog);
10066
- const lines = [];
10067
- pushHeader(lines, SCHEMA_HEADER_DOCUMENTED, name);
10068
- lines.push("");
10069
- lines.push("schemaVersion: 1");
10070
- lines.push(`name: ${name}`);
10071
- lines.push(
10072
- "# Pinned runtime base image, reused on every apply (never auto-bumped)."
10073
- );
10074
- lines.push(
10075
- "# `monoceros upgrade <name>` refreshes the tooling and moves this to the"
10076
- );
10077
- lines.push("# latest runtime when a newer one exists.");
10078
- lines.push(`runtimeVersion: ${DEFAULT_RUNTIME_VERSION}`);
10079
- lines.push("");
10080
- if (byCategory.language.length > 0) {
10081
- pushSectionHeader(
10082
- lines,
10083
- LANGUAGES_HEADER,
10084
- /* commented */
10085
- true
10086
- );
10087
- lines.push("# languages:");
10088
- for (const c of byCategory.language) {
10089
- for (const lang of c.file.contributes.languages ?? []) {
10090
- const ver = LANGUAGE_CATALOG[lang]?.defaultVersion;
10091
- lines.push(`# - ${ver ? `${lang}:${ver}` : lang}`);
10092
- }
10093
- }
10094
- lines.push("");
10095
- }
10096
- if (byCategory.service.length > 0) {
10097
- pushSectionHeader(
10098
- lines,
10099
- SERVICES_HEADER,
10100
- /* commented */
10101
- true
10102
- );
10103
- lines.push("# services:");
10104
- for (const c of byCategory.service) {
10105
- for (const svc of c.file.contributes.services ?? []) {
10106
- const body = renderServiceObjectBody(expandCuratedService(svc));
10107
- lines.push(`# - ${body[0]}`);
10108
- for (const line of body.slice(1)) lines.push(`# ${line}`);
10109
- const exComment = exampleVolumesComment(
10110
- curatedServiceExampleVolumes(svc)
10111
- );
10112
- if (exComment) {
10113
- for (const cl of exComment.split("\n")) lines.push(`# ${cl}`);
10114
- }
10115
- }
10116
- }
10117
- lines.push("");
10118
- }
10119
- if (byCategory.feature.length > 0) {
10120
- pushSectionHeader(
10121
- lines,
10122
- FEATURES_HEADER_DOCUMENTED,
10123
- /* commented */
10124
- true
10125
- );
10126
- lines.push("# features:");
10127
- const renderedRefs = /* @__PURE__ */ new Set();
10128
- const topLevel = byCategory.feature.filter((c) => !c.name.includes("/"));
10129
- for (const c of topLevel) {
10130
- for (const f of c.file.contributes.features ?? []) {
10131
- if (renderedRefs.has(f.ref)) continue;
10132
- renderedRefs.add(f.ref);
10133
- lines.push("#");
10134
- renderFeatureBlock(
10135
- lines,
10136
- f,
10137
- lookupManifest(f.ref),
10138
- /* commented */
10139
- true
10140
- );
10141
- }
10142
- }
10143
- for (const c of byCategory.feature) {
10144
- if (!c.name.includes("/")) continue;
10145
- for (const f of c.file.contributes.features ?? []) {
10146
- if (renderedRefs.has(f.ref)) continue;
10147
- renderedRefs.add(f.ref);
10148
- lines.push("#");
10149
- renderFeatureBlock(
10150
- lines,
10151
- f,
10152
- lookupManifest(f.ref),
10153
- /* commented */
10154
- true
10155
- );
10156
- }
10157
- }
10158
- lines.push("");
10159
- }
10160
- if (repoUrls.length > 0) {
10161
- pushGitIdentityBlock(lines);
10162
- pushSectionHeader(
10163
- lines,
10164
- REPOS_HEADER,
10165
- /* commented */
10166
- false
10167
- );
10168
- lines.push("repos:");
10169
- for (const url of repoUrls) {
10170
- lines.push(` - url: ${url}`);
10171
- }
10172
- lines.push("");
10173
- } else {
10174
- pushSectionHeader(
10175
- lines,
10176
- REPOS_HEADER,
10177
- /* commented */
10178
- true
10179
- );
10180
- lines.push("# repos:");
10181
- lines.push("# - url: https://github.com/<org>/<repo>.git");
10182
- lines.push("# path: <folder>");
10183
- lines.push("# provider: github");
10184
- lines.push("# git:");
10185
- lines.push("# user:");
10186
- lines.push("# name: Your Name");
10187
- lines.push("# email: you@example.com");
10188
- lines.push("");
10189
- }
10190
- if (ports.length > 0) {
10191
- pushSectionHeader(
10192
- lines,
10193
- routingHeader(name),
10194
- /* commented */
10195
- false
10196
- );
10197
- lines.push("routing:");
10198
- lines.push(" ports:");
10199
- for (const port of ports) {
10200
- lines.push(` - ${port}`);
10201
- }
10202
- lines.push(" # vscodeAutoForward: false");
10203
- lines.push("");
10204
- } else {
10205
- pushSectionHeader(
10206
- lines,
10207
- routingHeader(name),
10208
- /* commented */
10209
- true
10210
- );
10211
- lines.push("# routing:");
10212
- lines.push("# ports:");
10213
- lines.push("# - 3000");
10214
- lines.push("# - 5173");
10215
- lines.push("# vscodeAutoForward: false");
10216
- lines.push("");
10217
- }
10218
- return ensureTrailingNewline(lines.join("\n"));
10219
- }
10220
10026
  function pushServiceEntry(out, svc) {
10221
10027
  if (svc.kind === "custom") {
10222
10028
  const { bodyLines, comment } = renderCustomService(
@@ -10300,24 +10106,10 @@ function renderScalarValue(value) {
10300
10106
  }
10301
10107
  return String(value);
10302
10108
  }
10303
- function groupByCategory(catalog) {
10304
- const out = {
10305
- language: [],
10306
- service: [],
10307
- feature: []
10308
- };
10309
- const sorted = [...catalog.values()].sort(
10310
- (a, b) => a.name.localeCompare(b.name)
10311
- );
10312
- for (const c of sorted) {
10313
- out[c.file.category].push(c);
10314
- }
10315
- return out;
10316
- }
10317
10109
  function ensureTrailingNewline(s) {
10318
10110
  return s.endsWith("\n") ? s : s + "\n";
10319
10111
  }
10320
- var SCHEMA_HEADER_ACTIVE, SCHEMA_HEADER_DOCUMENTED, COMMENT_WIDTH, LANGUAGES_HEADER, SERVICES_HEADER, APT_PACKAGES_HEADER, FEATURES_HEADER_ACTIVE, FEATURES_HEADER_DOCUMENTED, REPOS_HEADER, GIT_IDENTITY_HEADER;
10112
+ var SCHEMA_HEADER_ACTIVE, COMMENT_WIDTH, LANGUAGES_HEADER, SERVICES_HEADER, APT_PACKAGES_HEADER, FEATURES_HEADER_ACTIVE, REPOS_HEADER, GIT_IDENTITY_HEADER;
10321
10113
  var init_generator = __esm({
10322
10114
  "src/init/generator.ts"() {
10323
10115
  "use strict";
@@ -10325,21 +10117,19 @@ var init_generator = __esm({
10325
10117
  init_catalog();
10326
10118
  init_service_doc();
10327
10119
  init_env_file();
10328
- SCHEMA_HEADER_ACTIVE = "# Solution-config \u2014 describes what should be inside your dev-container.\n# Edit any section, then run `monoceros apply <name>` to (re-)build.";
10329
- SCHEMA_HEADER_DOCUMENTED = "# Solution-config \u2014 describes what should be inside your dev-container.\n# Every section is commented out by default; un-comment what you need\n# (strip one `#` per line of the block), then run `monoceros apply <name>`.";
10120
+ SCHEMA_HEADER_ACTIVE = "# Solution-config: what should be inside your dev-container.\n# Starts minimal; add languages, features, services, or repos with\n# `monoceros add-feature/add-service/add-repo <name>` (see\n# `monoceros list-components`), then `monoceros apply <name>`.";
10330
10121
  COMMENT_WIDTH = 76;
10331
10122
  LANGUAGES_HEADER = "Language runtimes installed inside the dev-container. Pick the ones your projects build against. The catalog of available runtimes is shown by `monoceros list-components`.";
10332
10123
  SERVICES_HEADER = "Sibling containers that run alongside the dev-container (databases, caches, message queues, \u2026). Each service is reachable from inside the dev-container by its name as hostname (e.g. `postgres://postgres:5432`). Activating any service switches the container to docker-compose mode automatically.";
10333
10124
  APT_PACKAGES_HEADER = "Debian/Ubuntu apt packages installed in the dev-container at build time. No curated list \u2014 any apt package name works; an invalid name surfaces as an apt error during build.";
10334
10125
  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`.";
10335
- 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`.";
10336
- 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`.";
10126
+ 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, GitHub Enterprise, \u2026) declare `provider:` explicitly. Add more later with `monoceros add-repo`.";
10337
10127
  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.";
10338
10128
  }
10339
10129
  });
10340
10130
 
10341
10131
  // src/init/index.ts
10342
- import { existsSync as existsSync16, promises as fs17 } from "fs";
10132
+ import { existsSync as existsSync16, promises as fs18 } from "fs";
10343
10133
  import path27 from "path";
10344
10134
  import { consola as consola16 } from "consola";
10345
10135
  async function runInit(opts) {
@@ -10412,22 +10202,24 @@ async function runInit(opts) {
10412
10202
  seenPorts.add(raw);
10413
10203
  ports.push(raw);
10414
10204
  }
10415
- let text;
10205
+ const explicitFeatures = opts.features ?? [];
10206
+ const repoFeatures = [
10207
+ ...new Set(
10208
+ repos.map((u) => KNOWN_PROVIDER_HOSTS[new URL(u).hostname.toLowerCase()]).filter(
10209
+ (p) => p === "github" || p === "gitlab"
10210
+ ).filter((p) => !explicitFeatures.includes(p)).map((p) => catalog.get(p)?.file.contributes.features?.[0]?.ref).filter((ref) => !!ref)
10211
+ )
10212
+ ].filter((ref) => !explicitFeatures.includes(ref));
10416
10213
  const composed = resolveComposedInit(catalog, {
10417
10214
  languages: opts.languages ?? [],
10418
- features: opts.features ?? [],
10215
+ features: [...explicitFeatures, ...repoFeatures],
10419
10216
  services: opts.services ?? [],
10420
10217
  aptPackages: opts.aptPackages ?? []
10421
10218
  });
10422
- const anyComposed = composed.languages.length > 0 || composed.features.length > 0 || composed.services.length > 0 || composed.aptPackages.length > 0;
10423
- if (!anyComposed) {
10424
- text = generateDocumentedYml(opts.name, catalog, lookup, repos, ports);
10425
- } else {
10426
- text = generateComposedYml(opts.name, composed, lookup, repos, ports);
10427
- }
10428
- await fs17.mkdir(containerConfigsDir(home), { recursive: true });
10219
+ const text = generateComposedYml(opts.name, composed, lookup, repos, ports);
10220
+ await fs18.mkdir(containerConfigsDir(home), { recursive: true });
10429
10221
  await ensureEnvGitignored(containerConfigsDir(home));
10430
- await fs17.writeFile(dest, text, "utf8");
10222
+ await fs18.writeFile(dest, text, "utf8");
10431
10223
  const envPath = containerEnvPath(opts.name, home);
10432
10224
  const seedVars = {};
10433
10225
  for (const f of composed.features) {
@@ -10449,21 +10241,13 @@ async function runInit(opts) {
10449
10241
  seedVars[GIT_IDENTITY_VAR.email] = "";
10450
10242
  }
10451
10243
  await ensureEnvVars(envPath, opts.name, seedVars);
10452
- const documented = !anyComposed;
10453
10244
  const ymlRel = path27.relative(home, dest);
10454
10245
  const envRel = path27.relative(home, envPath);
10455
- if (documented) {
10456
- logger.success(`Wrote documented default to ${ymlRel} and ${envRel}.`);
10457
- logger.info(
10458
- `Un-comment what you need, then \`monoceros apply ${opts.name}\`.`
10459
- );
10460
- } else {
10461
- logger.success(`Composed into ${ymlRel} and ${envRel}.`);
10462
- logger.info(
10463
- `Edit the files if you need to tweak, then \`monoceros apply ${opts.name}\`.`
10464
- );
10465
- }
10466
- return { configPath: dest, documented };
10246
+ logger.success(`Wrote ${ymlRel} and ${envRel}.`);
10247
+ logger.info(
10248
+ `Add components with \`monoceros add-feature/add-service/add-repo ${opts.name}\` (see \`monoceros list-components\`), then \`monoceros apply ${opts.name}\`.`
10249
+ );
10250
+ return { configPath: dest };
10467
10251
  }
10468
10252
  function resolveComposedInit(catalog, raw) {
10469
10253
  return {
@@ -11010,14 +10794,14 @@ var init_list_components = __esm({
11010
10794
  });
11011
10795
 
11012
10796
  // src/commands/logs.ts
11013
- import { spawn as spawn13 } from "child_process";
10797
+ import { spawn as spawn12 } from "child_process";
11014
10798
  import path28 from "path";
11015
10799
  import { defineCommand as defineCommand16 } from "citty";
11016
10800
  import { consola as consola20 } from "consola";
11017
10801
  function tailLogFile(file, follow) {
11018
10802
  const [cmd, args] = follow ? ["tail", ["-F", file]] : ["cat", [file]];
11019
10803
  return new Promise((resolve, reject) => {
11020
- const child = spawn13(cmd, args, { stdio: "inherit" });
10804
+ const child = spawn12(cmd, args, { stdio: "inherit" });
11021
10805
  child.on("error", reject);
11022
10806
  child.on("exit", (code) => resolve(code ?? 0));
11023
10807
  });
@@ -11315,7 +11099,7 @@ var init_remove_feature = __esm({
11315
11099
  });
11316
11100
 
11317
11101
  // src/remove/index.ts
11318
- import { existsSync as existsSync17, promises as fs18 } from "fs";
11102
+ import { existsSync as existsSync17, promises as fs19 } from "fs";
11319
11103
  import path29 from "path";
11320
11104
  import { consola as consola24 } from "consola";
11321
11105
  async function runRemove(opts) {
@@ -11362,17 +11146,17 @@ async function runRemove(opts) {
11362
11146
  if (!opts.noBackup && (hasYml || hasContainer)) {
11363
11147
  const ts = (opts.now ?? /* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
11364
11148
  backupPath = path29.join(home, "container-backups", `${opts.name}-${ts}`);
11365
- await fs18.mkdir(backupPath, { recursive: true });
11149
+ await fs19.mkdir(backupPath, { recursive: true });
11366
11150
  if (hasYml) {
11367
- await fs18.copyFile(ymlPath, path29.join(backupPath, `${opts.name}.yml`));
11151
+ await fs19.copyFile(ymlPath, path29.join(backupPath, `${opts.name}.yml`));
11368
11152
  }
11369
11153
  if (hasEnv) {
11370
- await fs18.copyFile(envPath, path29.join(backupPath, `${opts.name}.env`));
11154
+ await fs19.copyFile(envPath, path29.join(backupPath, `${opts.name}.env`));
11371
11155
  }
11372
11156
  if (hasContainer) {
11373
11157
  const dst = path29.join(backupPath, "container");
11374
11158
  try {
11375
- await fs18.cp(containerPath, dst, { recursive: true });
11159
+ await fs19.cp(containerPath, dst, { recursive: true });
11376
11160
  } catch (err) {
11377
11161
  const code = err.code;
11378
11162
  if (code !== "EACCES" && code !== "EPERM") {
@@ -11381,7 +11165,7 @@ async function runRemove(opts) {
11381
11165
  logger.info(
11382
11166
  `[remove] backup hit ${code} on root-owned files; copying via a throw-away alpine container\u2026`
11383
11167
  );
11384
- await fs18.mkdir(dst, { recursive: true });
11168
+ await fs19.mkdir(dst, { recursive: true });
11385
11169
  const { exitCode, stderr } = await dockerExec([
11386
11170
  "run",
11387
11171
  "--rm",
@@ -11407,14 +11191,14 @@ async function runRemove(opts) {
11407
11191
  await stopBridgeDaemon(containerPath);
11408
11192
  }
11409
11193
  if (hasYml) {
11410
- await fs18.rm(ymlPath, { force: true });
11194
+ await fs19.rm(ymlPath, { force: true });
11411
11195
  }
11412
11196
  if (hasEnv) {
11413
- await fs18.rm(envPath, { force: true });
11197
+ await fs19.rm(envPath, { force: true });
11414
11198
  }
11415
11199
  if (hasContainer) {
11416
11200
  try {
11417
- await fs18.rm(containerPath, { recursive: true, force: true });
11201
+ await fs19.rm(containerPath, { recursive: true, force: true });
11418
11202
  } catch (err) {
11419
11203
  const code = err.code;
11420
11204
  if (code !== "EACCES" && code !== "EPERM") {
@@ -11436,7 +11220,7 @@ async function runRemove(opts) {
11436
11220
  "-delete"
11437
11221
  ]);
11438
11222
  try {
11439
- await fs18.rm(containerPath, { recursive: true, force: true });
11223
+ await fs19.rm(containerPath, { recursive: true, force: true });
11440
11224
  } catch (err2) {
11441
11225
  const code2 = err2.code;
11442
11226
  throw new Error(
@@ -11571,7 +11355,7 @@ var init_remove2 = __esm({
11571
11355
  });
11572
11356
 
11573
11357
  // src/restore/index.ts
11574
- import { existsSync as existsSync18, promises as fs19 } from "fs";
11358
+ import { existsSync as existsSync18, promises as fs20 } from "fs";
11575
11359
  import path30 from "path";
11576
11360
  import { consola as consola26 } from "consola";
11577
11361
  async function runRestore(opts) {
@@ -11584,11 +11368,11 @@ async function runRestore(opts) {
11584
11368
  if (!existsSync18(backup)) {
11585
11369
  throw new Error(`Backup not found: ${backup}.`);
11586
11370
  }
11587
- const stat = await fs19.stat(backup);
11371
+ const stat = await fs20.stat(backup);
11588
11372
  if (!stat.isDirectory()) {
11589
11373
  throw new Error(`Backup path is not a directory: ${backup}.`);
11590
11374
  }
11591
- const entries = await fs19.readdir(backup);
11375
+ const entries = await fs20.readdir(backup);
11592
11376
  const ymlFiles = entries.filter((f) => f.endsWith(".yml"));
11593
11377
  if (ymlFiles.length === 0) {
11594
11378
  throw new Error(
@@ -11618,13 +11402,13 @@ async function runRestore(opts) {
11618
11402
  `Refusing to restore: ${destContainer} already exists. Remove the current container first (\`monoceros remove ${name}\`).`
11619
11403
  );
11620
11404
  }
11621
- await fs19.mkdir(containerConfigsDir(home), { recursive: true });
11622
- await fs19.copyFile(path30.join(backup, ymlFile), destYml);
11405
+ await fs20.mkdir(containerConfigsDir(home), { recursive: true });
11406
+ await fs20.copyFile(path30.join(backup, ymlFile), destYml);
11623
11407
  if (hasEnv) {
11624
- await fs19.copyFile(envInBackup, containerEnvPath(name, home));
11408
+ await fs20.copyFile(envInBackup, containerEnvPath(name, home));
11625
11409
  }
11626
11410
  if (hasContainer) {
11627
- await fs19.cp(containerInBackup, destContainer, { recursive: true });
11411
+ await fs20.cp(containerInBackup, destContainer, { recursive: true });
11628
11412
  }
11629
11413
  logger.success(`Restored '${name}' from ${prettyPath(backup)}.`);
11630
11414
  logger.info(
@@ -12334,7 +12118,7 @@ var init_port_check2 = __esm({
12334
12118
  });
12335
12119
 
12336
12120
  // src/tunnel/run.ts
12337
- import { spawn as spawn14 } from "child_process";
12121
+ import { spawn as spawn13 } from "child_process";
12338
12122
  import { consola as consola34 } from "consola";
12339
12123
  function signalNumber(signal) {
12340
12124
  switch (signal) {
@@ -12424,7 +12208,7 @@ var init_run3 = __esm({
12424
12208
  init_port_check2();
12425
12209
  SOCAT_IMAGE = "alpine/socat:1.8.0.3";
12426
12210
  defaultDockerSpawn = (args) => {
12427
- const child = spawn14("docker", args, {
12211
+ const child = spawn13("docker", args, {
12428
12212
  stdio: "inherit"
12429
12213
  });
12430
12214
  const exited = new Promise((resolve, reject) => {
@@ -13351,7 +13135,7 @@ var init_prune = __esm({
13351
13135
  });
13352
13136
 
13353
13137
  // src/upgrade/index.ts
13354
- import { existsSync as existsSync21, promises as fs20 } from "fs";
13138
+ import { existsSync as existsSync21, promises as fs21 } from "fs";
13355
13139
  import { consola as consola41 } from "consola";
13356
13140
  async function fetchRuntimeVersions() {
13357
13141
  const tokenUrl = `https://ghcr.io/token?service=ghcr.io&scope=repository:${RUNTIME_REPO}:pull`;
@@ -13468,10 +13252,10 @@ async function runUpgrade(opts) {
13468
13252
  for (const name of targets) {
13469
13253
  const ymlPath = containerConfigPath(name, home);
13470
13254
  if (!existsSync21(ymlPath)) continue;
13471
- const raw = await fs20.readFile(ymlPath, "utf8");
13255
+ const raw = await fs21.readFile(ymlPath, "utf8");
13472
13256
  const updated = setRuntimeVersion(raw, pinVersion);
13473
13257
  if (updated !== raw) {
13474
- await fs20.writeFile(ymlPath, updated);
13258
+ await fs21.writeFile(ymlPath, updated);
13475
13259
  bumped += 1;
13476
13260
  logger.info(`Pinned '${name}' to runtime ${pinVersion}.`);
13477
13261
  }
@@ -13513,7 +13297,7 @@ function formatPruneLine(prune) {
13513
13297
  }
13514
13298
  async function listContainerNames2(home) {
13515
13299
  try {
13516
- const entries = await fs20.readdir(containerConfigsDir(home));
13300
+ const entries = await fs21.readdir(containerConfigsDir(home));
13517
13301
  return entries.filter((e) => e.endsWith(".yml")).map((e) => e.slice(0, -".yml".length));
13518
13302
  } catch {
13519
13303
  return [];