@getmonoceros/workbench 1.8.0 → 1.9.1

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
@@ -251,25 +251,25 @@ function detectHelpRequest(argv, main2) {
251
251
  const separatorIdx = argv.indexOf("--");
252
252
  if (helpIdx === -1) return null;
253
253
  if (separatorIdx !== -1 && separatorIdx < helpIdx) return null;
254
- const path16 = [];
254
+ const path17 = [];
255
255
  const tokens = argv.slice(
256
256
  0,
257
257
  separatorIdx === -1 ? argv.length : separatorIdx
258
258
  );
259
259
  let cursor = main2;
260
260
  const mainName = (main2.meta ?? {}).name ?? "monoceros";
261
- path16.push(mainName);
261
+ path17.push(mainName);
262
262
  for (const tok of tokens) {
263
263
  if (tok.startsWith("-")) continue;
264
264
  const subs = cursor.subCommands ?? {};
265
265
  if (tok in subs) {
266
266
  cursor = subs[tok];
267
- path16.push(tok);
267
+ path17.push(tok);
268
268
  continue;
269
269
  }
270
270
  break;
271
271
  }
272
- return { path: path16, cmd: cursor };
272
+ return { path: path17, cmd: cursor };
273
273
  }
274
274
  async function maybeRenderHelp(argv, main2) {
275
275
  const hit = detectHelpRequest(argv, main2);
@@ -301,17 +301,17 @@ function getInnerArgs() {
301
301
  }
302
302
 
303
303
  // src/main.ts
304
- import { defineCommand as defineCommand28 } from "citty";
304
+ import { defineCommand as defineCommand29 } from "citty";
305
305
 
306
306
  // src/commands/add-apt-packages.ts
307
307
  import { defineCommand } from "citty";
308
308
  import { consola as consola2 } from "consola";
309
309
 
310
310
  // src/modify/index.ts
311
- import { promises as fs7 } from "fs";
311
+ import { promises as fs8 } from "fs";
312
312
  import { consola } from "consola";
313
313
  import { createPatch } from "diff";
314
- import path6 from "path";
314
+ import path7 from "path";
315
315
 
316
316
  // src/config/io.ts
317
317
  import { promises as fs } from "fs";
@@ -1161,10 +1161,175 @@ ${existing}` : leakedComment;
1161
1161
  }
1162
1162
  }
1163
1163
 
1164
+ // src/init/components.ts
1165
+ import { existsSync as existsSync2, promises as fs4 } from "fs";
1166
+ import path3 from "path";
1167
+ import { z as z3 } from "zod";
1168
+ import { parse as parseYaml } from "yaml";
1169
+ var CategorySchema = z3.enum(["language", "service", "feature"]);
1170
+ var FeatureContributionSchema = z3.object({
1171
+ ref: z3.string().regex(REGEX.featureRef),
1172
+ options: z3.record(z3.string(), FeatureOptionValueSchema).optional()
1173
+ });
1174
+ var ComponentFileSchema = z3.object({
1175
+ displayName: z3.string().min(1),
1176
+ description: z3.string().min(1),
1177
+ category: CategorySchema,
1178
+ contributes: z3.object({
1179
+ languages: z3.array(z3.string().min(1)).optional(),
1180
+ services: z3.array(z3.string().min(1)).optional(),
1181
+ features: z3.array(FeatureContributionSchema).optional()
1182
+ })
1183
+ }).superRefine((data, ctx) => {
1184
+ const c = data.contributes;
1185
+ const filled = [
1186
+ c.languages && c.languages.length > 0 ? "languages" : null,
1187
+ c.services && c.services.length > 0 ? "services" : null,
1188
+ c.features && c.features.length > 0 ? "features" : null
1189
+ ].filter((x) => x !== null);
1190
+ if (filled.length === 0) {
1191
+ ctx.addIssue({
1192
+ code: z3.ZodIssueCode.custom,
1193
+ message: "contributes must set at least one of languages/services/features"
1194
+ });
1195
+ return;
1196
+ }
1197
+ if (filled.length > 1) {
1198
+ ctx.addIssue({
1199
+ code: z3.ZodIssueCode.custom,
1200
+ message: `contributes must set exactly one of languages/services/features, got: ${filled.join(", ")}`
1201
+ });
1202
+ return;
1203
+ }
1204
+ const expected = data.category === "language" ? "languages" : data.category === "service" ? "services" : "features";
1205
+ if (filled[0] !== expected) {
1206
+ ctx.addIssue({
1207
+ code: z3.ZodIssueCode.custom,
1208
+ message: `category '${data.category}' requires contributes.${expected}, got contributes.${filled[0]}`
1209
+ });
1210
+ }
1211
+ });
1212
+ async function loadComponentCatalog(rootDir = componentsDir()) {
1213
+ if (!existsSync2(rootDir)) {
1214
+ return /* @__PURE__ */ new Map();
1215
+ }
1216
+ const out = /* @__PURE__ */ new Map();
1217
+ await walk(rootDir, rootDir, out);
1218
+ return out;
1219
+ }
1220
+ async function walk(baseDir, currentDir, out) {
1221
+ const entries = await fs4.readdir(currentDir, { withFileTypes: true });
1222
+ for (const entry2 of entries) {
1223
+ const full = path3.join(currentDir, entry2.name);
1224
+ if (entry2.isDirectory()) {
1225
+ await walk(baseDir, full, out);
1226
+ continue;
1227
+ }
1228
+ if (!entry2.isFile() || !entry2.name.endsWith(".yml")) continue;
1229
+ const relative = path3.relative(baseDir, full);
1230
+ const name = relative.replace(/\.yml$/, "").split(path3.sep).join("/");
1231
+ const text = await fs4.readFile(full, "utf8");
1232
+ let raw;
1233
+ try {
1234
+ raw = parseYaml(text);
1235
+ } catch (err) {
1236
+ throw new Error(
1237
+ `Failed to parse component ${name} (${full}): ${err.message}`
1238
+ );
1239
+ }
1240
+ const parsed = ComponentFileSchema.safeParse(raw);
1241
+ if (!parsed.success) {
1242
+ const issues = parsed.error.issues.map((issue) => {
1243
+ const where = issue.path.length > 0 ? issue.path.join(".") : "(root)";
1244
+ return ` - ${where}: ${issue.message}`;
1245
+ }).join("\n");
1246
+ throw new Error(`Invalid component ${name} (${full}):
1247
+ ${issues}`);
1248
+ }
1249
+ out.set(name, { name, sourcePath: full, file: parsed.data });
1250
+ }
1251
+ }
1252
+ function mergeComponents(resolved) {
1253
+ const languages = [];
1254
+ const services = [];
1255
+ const featureByRef = /* @__PURE__ */ new Map();
1256
+ for (const entry2 of resolved) {
1257
+ const c = isResolvedComponent(entry2) ? entry2.component : entry2;
1258
+ const version = isResolvedComponent(entry2) ? entry2.version : void 0;
1259
+ const ct = c.file.contributes;
1260
+ for (const lang of ct.languages ?? []) {
1261
+ const value = version !== void 0 ? `${lang}:${version}` : lang;
1262
+ if (!languages.includes(value)) languages.push(value);
1263
+ }
1264
+ for (const svc of ct.services ?? []) {
1265
+ if (!services.includes(svc)) services.push(svc);
1266
+ }
1267
+ for (const f of ct.features ?? []) {
1268
+ const existing = featureByRef.get(f.ref);
1269
+ if (!existing) {
1270
+ featureByRef.set(f.ref, {
1271
+ ref: f.ref,
1272
+ options: { ...f.options ?? {} }
1273
+ });
1274
+ continue;
1275
+ }
1276
+ existing.options = mergeFeatureOptions(existing.options, f.options ?? {});
1277
+ }
1278
+ }
1279
+ return {
1280
+ languages,
1281
+ services,
1282
+ features: [...featureByRef.values()]
1283
+ };
1284
+ }
1285
+ function isResolvedComponent(x) {
1286
+ return "component" in x;
1287
+ }
1288
+ function mergeFeatureOptions(a, b) {
1289
+ const result = { ...a };
1290
+ for (const [key, valueB] of Object.entries(b)) {
1291
+ const valueA = result[key];
1292
+ if (typeof valueA === "boolean" && typeof valueB === "boolean") {
1293
+ result[key] = valueA || valueB;
1294
+ continue;
1295
+ }
1296
+ result[key] = valueB;
1297
+ }
1298
+ return result;
1299
+ }
1300
+ function resolveComponents(catalog, names) {
1301
+ const unknown = [];
1302
+ const out = [];
1303
+ for (const raw of names) {
1304
+ const colon = raw.indexOf(":");
1305
+ const name = colon === -1 ? raw : raw.slice(0, colon);
1306
+ const version = colon === -1 ? void 0 : raw.slice(colon + 1);
1307
+ const c = catalog.get(name);
1308
+ if (!c) {
1309
+ unknown.push(raw);
1310
+ continue;
1311
+ }
1312
+ if (version !== void 0 && c.file.category !== "language") {
1313
+ throw new Error(
1314
+ `Component '${name}' is a ${c.file.category}, not a language \u2014 a ':${version}' suffix has no meaning here.`
1315
+ );
1316
+ }
1317
+ out.push({ component: c, ...version !== void 0 ? { version } : {} });
1318
+ }
1319
+ if (unknown.length > 0) {
1320
+ const available = [...catalog.keys()].sort();
1321
+ throw new Error(
1322
+ `Unknown component${unknown.length > 1 ? "s" : ""}: ${unknown.join(", ")}.
1323
+ Available: ${available.join(", ")}.`
1324
+ );
1325
+ }
1326
+ return out;
1327
+ }
1328
+
1164
1329
  // src/proxy/index.ts
1165
1330
  import { spawn as spawn3 } from "child_process";
1166
- import { promises as fs4 } from "fs";
1167
- import path3 from "path";
1331
+ import { promises as fs5 } from "fs";
1332
+ import path4 from "path";
1168
1333
  var PROXY_CONTAINER_NAME = "monoceros-proxy";
1169
1334
  var PROXY_NETWORK_NAME = "monoceros-proxy";
1170
1335
  var TRAEFIK_IMAGE = "traefik:v3.3";
@@ -1190,12 +1355,12 @@ var defaultDockerExec = (args) => {
1190
1355
  };
1191
1356
  var realDocker = defaultDockerExec;
1192
1357
  function proxyDynamicDir(home) {
1193
- return path3.join(home ?? monocerosHome(), "traefik", "dynamic");
1358
+ return path4.join(home ?? monocerosHome(), "traefik", "dynamic");
1194
1359
  }
1195
1360
  async function ensureProxy(opts = {}) {
1196
1361
  const docker = opts.docker ?? realDocker;
1197
1362
  const dyn = proxyDynamicDir(opts.monocerosHome);
1198
- await fs4.mkdir(dyn, { recursive: true });
1363
+ await fs5.mkdir(dyn, { recursive: true });
1199
1364
  const netInspect = await docker(["network", "inspect", PROXY_NETWORK_NAME]);
1200
1365
  if (netInspect.exitCode !== 0) {
1201
1366
  const create = await docker(["network", "create", PROXY_NETWORK_NAME]);
@@ -1281,8 +1446,8 @@ async function maybeStopProxy(opts = {}) {
1281
1446
  }
1282
1447
 
1283
1448
  // src/proxy/dynamic.ts
1284
- import { promises as fs5 } from "fs";
1285
- import path4 from "path";
1449
+ import { promises as fs6 } from "fs";
1450
+ import path5 from "path";
1286
1451
  async function writeDynamicConfig(name, ports, opts = {}) {
1287
1452
  if (ports.length === 0) {
1288
1453
  throw new Error(
@@ -1290,14 +1455,14 @@ async function writeDynamicConfig(name, ports, opts = {}) {
1290
1455
  );
1291
1456
  }
1292
1457
  const dir = proxyDynamicDir(opts.monocerosHome);
1293
- await fs5.mkdir(dir, { recursive: true });
1294
- const file = path4.join(dir, `${name}.yml`);
1295
- await fs5.writeFile(file, renderDynamicConfig(name, ports), "utf8");
1458
+ await fs6.mkdir(dir, { recursive: true });
1459
+ const file = path5.join(dir, `${name}.yml`);
1460
+ await fs6.writeFile(file, renderDynamicConfig(name, ports), "utf8");
1296
1461
  return file;
1297
1462
  }
1298
1463
  async function removeDynamicConfig(name, opts = {}) {
1299
- const file = path4.join(proxyDynamicDir(opts.monocerosHome), `${name}.yml`);
1300
- await fs5.rm(file, { force: true });
1464
+ const file = path5.join(proxyDynamicDir(opts.monocerosHome), `${name}.yml`);
1465
+ await fs6.rm(file, { force: true });
1301
1466
  }
1302
1467
  function renderDynamicConfig(name, ports) {
1303
1468
  const lines = [];
@@ -1498,8 +1663,8 @@ function knownServices() {
1498
1663
  }
1499
1664
 
1500
1665
  // src/create/scaffold.ts
1501
- import { existsSync as existsSync2, readFileSync, promises as fs6 } from "fs";
1502
- import path5 from "path";
1666
+ import { existsSync as existsSync3, readFileSync, promises as fs7 } from "fs";
1667
+ import path6 from "path";
1503
1668
 
1504
1669
  // src/util/ref.ts
1505
1670
  var FEATURE_NAME_CHARSET = "[a-z0-9._-]+";
@@ -1670,8 +1835,8 @@ function resolveFeatures(opts) {
1670
1835
  if (match) {
1671
1836
  const name = match.name;
1672
1837
  const checkout = workbenchCheckoutRoot();
1673
- const localSourceDir = checkout ? path5.join(checkout, "images", "features", name) : null;
1674
- if (localSourceDir && existsSync2(localSourceDir)) {
1838
+ const localSourceDir = checkout ? path6.join(checkout, "images", "features", name) : null;
1839
+ if (localSourceDir && existsSync3(localSourceDir)) {
1675
1840
  const { paths, files } = readPersistentHomeEntries(localSourceDir);
1676
1841
  resolved.push({
1677
1842
  devcontainerKey: `./features/${name}`,
@@ -1695,7 +1860,7 @@ function resolveFeatures(opts) {
1695
1860
  return resolved;
1696
1861
  }
1697
1862
  function readPersistentHomeEntries(localSourceDir) {
1698
- const manifestPath = path5.join(localSourceDir, "devcontainer-feature.json");
1863
+ const manifestPath = path6.join(localSourceDir, "devcontainer-feature.json");
1699
1864
  try {
1700
1865
  const text = readFileSync(manifestPath, "utf8");
1701
1866
  const parsed = JSON.parse(text);
@@ -1980,86 +2145,86 @@ function buildPostCreateScript(opts) {
1980
2145
  return lines.join("\n") + "\n";
1981
2146
  }
1982
2147
  async function writePostCreateScript(devcontainerDir, opts) {
1983
- const dest = path5.join(devcontainerDir, "post-create.sh");
1984
- await fs6.writeFile(dest, buildPostCreateScript(opts));
1985
- await fs6.chmod(dest, 493);
2148
+ const dest = path6.join(devcontainerDir, "post-create.sh");
2149
+ await fs7.writeFile(dest, buildPostCreateScript(opts));
2150
+ await fs7.chmod(dest, 493);
1986
2151
  }
1987
2152
  async function writeScaffold(opts, targetDir, scaffoldOpts = {}) {
1988
2153
  const dockerMode = scaffoldOpts.dockerMode ?? "rootful";
1989
- const devcontainerDir = path5.join(targetDir, ".devcontainer");
1990
- const monocerosDir = path5.join(targetDir, ".monoceros");
1991
- const projectsDir = path5.join(targetDir, "projects");
1992
- const homeDir = path5.join(targetDir, "home");
1993
- const dataDir = path5.join(targetDir, "data");
1994
- await fs6.mkdir(devcontainerDir, { recursive: true });
1995
- await fs6.mkdir(monocerosDir, { recursive: true });
1996
- await fs6.mkdir(projectsDir, { recursive: true });
1997
- await fs6.mkdir(homeDir, { recursive: true });
2154
+ const devcontainerDir = path6.join(targetDir, ".devcontainer");
2155
+ const monocerosDir = path6.join(targetDir, ".monoceros");
2156
+ const projectsDir = path6.join(targetDir, "projects");
2157
+ const homeDir = path6.join(targetDir, "home");
2158
+ const dataDir = path6.join(targetDir, "data");
2159
+ await fs7.mkdir(devcontainerDir, { recursive: true });
2160
+ await fs7.mkdir(monocerosDir, { recursive: true });
2161
+ await fs7.mkdir(projectsDir, { recursive: true });
2162
+ await fs7.mkdir(homeDir, { recursive: true });
1998
2163
  if (needsCompose(opts)) {
1999
- await fs6.mkdir(dataDir, { recursive: true });
2164
+ await fs7.mkdir(dataDir, { recursive: true });
2000
2165
  for (const svcId of opts.services) {
2001
2166
  const def = SERVICE_CATALOG[svcId];
2002
2167
  if (def?.dataMount) {
2003
- await fs6.mkdir(path5.join(dataDir, def.id), { recursive: true });
2168
+ await fs7.mkdir(path6.join(dataDir, def.id), { recursive: true });
2004
2169
  }
2005
2170
  }
2006
2171
  }
2007
- const containerGitignore = path5.join(targetDir, ".gitignore");
2008
- await fs6.writeFile(containerGitignore, "/home/\n/.monoceros/\n/data/\n");
2009
- const gitkeep = path5.join(projectsDir, ".gitkeep");
2010
- if (!existsSync2(gitkeep)) {
2011
- await fs6.writeFile(gitkeep, "");
2172
+ const containerGitignore = path6.join(targetDir, ".gitignore");
2173
+ await fs7.writeFile(containerGitignore, "/home/\n/.monoceros/\n/data/\n");
2174
+ const gitkeep = path6.join(projectsDir, ".gitkeep");
2175
+ if (!existsSync3(gitkeep)) {
2176
+ await fs7.writeFile(gitkeep, "");
2012
2177
  }
2013
- await fs6.writeFile(
2014
- path5.join(monocerosDir, ".gitignore"),
2178
+ await fs7.writeFile(
2179
+ path6.join(monocerosDir, ".gitignore"),
2015
2180
  "git-credentials*\ngitconfig\n"
2016
2181
  );
2017
2182
  const devcontainerJson = buildDevcontainerJson(opts, dockerMode);
2018
- await fs6.writeFile(
2019
- path5.join(devcontainerDir, "devcontainer.json"),
2183
+ await fs7.writeFile(
2184
+ path6.join(devcontainerDir, "devcontainer.json"),
2020
2185
  JSON.stringify(devcontainerJson, null, 2) + "\n"
2021
2186
  );
2022
- const featuresDir = path5.join(devcontainerDir, "features");
2023
- if (existsSync2(featuresDir)) {
2024
- await fs6.rm(featuresDir, { recursive: true, force: true });
2187
+ const featuresDir = path6.join(devcontainerDir, "features");
2188
+ if (existsSync3(featuresDir)) {
2189
+ await fs7.rm(featuresDir, { recursive: true, force: true });
2025
2190
  }
2026
2191
  const resolvedFeatures = resolveFeatures(opts);
2027
2192
  for (const f of resolvedFeatures) {
2028
2193
  if (!f.localSourceDir || !f.localName) continue;
2029
- const dest = path5.join(featuresDir, f.localName);
2030
- await fs6.mkdir(dest, { recursive: true });
2031
- await fs6.cp(f.localSourceDir, dest, { recursive: true });
2194
+ const dest = path6.join(featuresDir, f.localName);
2195
+ await fs7.mkdir(dest, { recursive: true });
2196
+ await fs7.cp(f.localSourceDir, dest, { recursive: true });
2032
2197
  }
2033
2198
  for (const f of resolvedFeatures) {
2034
2199
  for (const sub of f.persistentHomePaths) {
2035
- await fs6.mkdir(path5.join(homeDir, sub), { recursive: true });
2200
+ await fs7.mkdir(path6.join(homeDir, sub), { recursive: true });
2036
2201
  }
2037
2202
  for (const entry2 of f.persistentHomeFiles) {
2038
- const filePath = path5.join(homeDir, entry2.path);
2039
- await fs6.mkdir(path5.dirname(filePath), { recursive: true });
2040
- if (!existsSync2(filePath)) {
2041
- await fs6.writeFile(filePath, entry2.initialContent);
2203
+ const filePath = path6.join(homeDir, entry2.path);
2204
+ await fs7.mkdir(path6.dirname(filePath), { recursive: true });
2205
+ if (!existsSync3(filePath)) {
2206
+ await fs7.writeFile(filePath, entry2.initialContent);
2042
2207
  }
2043
2208
  }
2044
2209
  }
2045
2210
  await writePostCreateScript(devcontainerDir, opts);
2046
- const composePath = path5.join(devcontainerDir, "compose.yaml");
2211
+ const composePath = path6.join(devcontainerDir, "compose.yaml");
2047
2212
  if (needsCompose(opts)) {
2048
- await fs6.writeFile(composePath, buildComposeYaml(opts, dockerMode));
2049
- } else if (existsSync2(composePath)) {
2050
- await fs6.rm(composePath);
2213
+ await fs7.writeFile(composePath, buildComposeYaml(opts, dockerMode));
2214
+ } else if (existsSync3(composePath)) {
2215
+ await fs7.rm(composePath);
2051
2216
  }
2052
- const workspacePath = path5.join(targetDir, `${opts.name}.code-workspace`);
2217
+ const workspacePath = path6.join(targetDir, `${opts.name}.code-workspace`);
2053
2218
  let existingWorkspace;
2054
2219
  try {
2055
- const raw = await fs6.readFile(workspacePath, "utf8");
2220
+ const raw = await fs7.readFile(workspacePath, "utf8");
2056
2221
  existingWorkspace = JSON.parse(raw);
2057
2222
  } catch {
2058
2223
  existingWorkspace = void 0;
2059
2224
  }
2060
2225
  const generated = buildCodeWorkspaceJson(opts);
2061
2226
  const merged = mergeCodeWorkspace(existingWorkspace, generated);
2062
- await fs6.writeFile(workspacePath, JSON.stringify(merged, null, 2) + "\n");
2227
+ await fs7.writeFile(workspacePath, JSON.stringify(merged, null, 2) + "\n");
2063
2228
  }
2064
2229
 
2065
2230
  // src/modify/yml.ts
@@ -2417,8 +2582,8 @@ function removeRepoFromDoc(doc, urlOrPath) {
2417
2582
  if (!isMap2(item)) return false;
2418
2583
  const url = item.get("url");
2419
2584
  if (url === urlOrPath) return true;
2420
- const path16 = item.get("path");
2421
- const effectivePath = typeof path16 === "string" ? path16 : typeof url === "string" ? deriveRepoName(url) : void 0;
2585
+ const path17 = item.get("path");
2586
+ const effectivePath = typeof path17 === "string" ? path17 : typeof url === "string" ? deriveRepoName(url) : void 0;
2422
2587
  return effectivePath === urlOrPath;
2423
2588
  });
2424
2589
  if (idx < 0) return false;
@@ -2468,7 +2633,7 @@ async function runAddRepo(input) {
2468
2633
  "Missing repo URL. Usage: monoceros add-repo <containername> <url>."
2469
2634
  );
2470
2635
  }
2471
- const path16 = (input.path ?? deriveRepoName(url)).trim();
2636
+ const path17 = (input.path ?? deriveRepoName(url)).trim();
2472
2637
  const hasName = typeof input.gitName === "string" && input.gitName.trim().length > 0;
2473
2638
  const hasEmail = typeof input.gitEmail === "string" && input.gitEmail.trim().length > 0;
2474
2639
  if (hasName !== hasEmail) {
@@ -2497,7 +2662,7 @@ async function runAddRepo(input) {
2497
2662
  const providerToWrite = !canonical && explicitProvider ? explicitProvider : void 0;
2498
2663
  const entry2 = {
2499
2664
  url,
2500
- path: path16,
2665
+ path: path17,
2501
2666
  ...hasName && hasEmail ? {
2502
2667
  gitUser: {
2503
2668
  name: input.gitName.trim(),
@@ -2573,14 +2738,14 @@ async function tryCloneInRunningContainer(input, entry2) {
2573
2738
  );
2574
2739
  return;
2575
2740
  }
2576
- const containerName = input.name;
2741
+ const containerName2 = input.name;
2577
2742
  const targetRel = `projects/${entry2.path}`;
2578
2743
  const parentRel = entry2.path.includes("/") ? `projects/${entry2.path.split("/").slice(0, -1).join("/")}` : "projects";
2579
- const credentialsFile = `/workspaces/${containerName}/.monoceros/git-credentials`;
2744
+ const credentialsFile = `/workspaces/${containerName2}/.monoceros/git-credentials`;
2580
2745
  const credentialHelper = `store --file=${credentialsFile}`;
2581
2746
  const script = [
2582
2747
  `set -eu`,
2583
- `cd /workspaces/${containerName}`,
2748
+ `cd /workspaces/${containerName2}`,
2584
2749
  `if [ -d ${shquote(targetRel)} ]; then`,
2585
2750
  ` echo "[add-repo] ${targetRel} already exists \u2014 skipping clone."`,
2586
2751
  ` exit 0`,
@@ -2611,9 +2776,9 @@ async function tryCloneInRunningContainer(input, entry2) {
2611
2776
  return;
2612
2777
  }
2613
2778
  logger.info(
2614
- `Cloned ${entry2.url} into /workspaces/${containerName}/${targetRel} inside the running container.`
2779
+ `Cloned ${entry2.url} into /workspaces/${containerName2}/${targetRel} inside the running container.`
2615
2780
  );
2616
- void path6;
2781
+ void path7;
2617
2782
  }
2618
2783
  function shquote(value) {
2619
2784
  return `'${value.replace(/'/g, `'\\''`)}'`;
@@ -2678,14 +2843,56 @@ function normalizePorts(raw) {
2678
2843
  }
2679
2844
  return result;
2680
2845
  }
2681
- function runAddFeature(input) {
2682
- const ref = input.ref.trim();
2683
- if (ref.length === 0) {
2846
+ async function runAddFeature(input) {
2847
+ const raw = input.ref.trim();
2848
+ if (raw.length === 0) {
2849
+ throw new Error(
2850
+ "Missing feature ref. Usage: monoceros add-feature <containername> <feature>."
2851
+ );
2852
+ }
2853
+ const resolved = await resolveFeatureRefOrShortname(raw);
2854
+ const merged = {
2855
+ ...resolved.defaultOptions,
2856
+ ...input.options ?? {}
2857
+ };
2858
+ return mutate(input, (doc) => addFeatureToDoc(doc, resolved.ref, merged));
2859
+ }
2860
+ async function resolveFeatureRefOrShortname(input) {
2861
+ if (REGEX.featureRef.test(input)) {
2862
+ return { ref: input, defaultOptions: {} };
2863
+ }
2864
+ const catalog = await loadComponentCatalog();
2865
+ const component = catalog.get(input);
2866
+ if (!component) {
2867
+ const featureShorts = [...catalog.values()].filter((c) => c.file.category === "feature").map((c) => c.name).sort();
2868
+ const knownList = featureShorts.length > 0 ? featureShorts.join(", ") : "(none)";
2869
+ throw new Error(
2870
+ `Unknown feature: ${JSON.stringify(input)}. Pass either a catalog short-name (one of: ${knownList}) or a full OCI ref like 'ghcr.io/getmonoceros/monoceros-features/<name>:<tag>'.`
2871
+ );
2872
+ }
2873
+ if (component.file.category !== "feature") {
2874
+ throw new Error(
2875
+ `'${input}' is a ${component.file.category}, not a feature. Use 'monoceros add-${component.file.category} <name> ${input}' instead.`
2876
+ );
2877
+ }
2878
+ const features = component.file.contributes.features ?? [];
2879
+ if (features.length === 0) {
2684
2880
  throw new Error(
2685
- "Missing feature ref. Usage: monoceros add-feature <containername> <ref>."
2881
+ `Catalog entry '${input}' contributes no feature ref \u2014 bug or stale catalog.`
2686
2882
  );
2687
2883
  }
2688
- return mutate(input, (doc) => addFeatureToDoc(doc, ref, input.options ?? {}));
2884
+ if (features.length > 1) {
2885
+ throw new Error(
2886
+ `'${input}' bundles multiple feature refs (${features.map((f) => f.ref).join(
2887
+ ", "
2888
+ )}). add-feature handles one at a time \u2014 pass the OCI ref directly.`
2889
+ );
2890
+ }
2891
+ const [first] = features;
2892
+ return {
2893
+ ref: first.ref,
2894
+ defaultOptions: { ...first.options ?? {} }
2895
+ };
2689
2896
  }
2690
2897
  function runRemoveLanguage(input) {
2691
2898
  return mutate(input, (doc) => removeLanguageFromDoc(doc, input.language));
@@ -2752,7 +2959,7 @@ async function mutate(opts, apply) {
2752
2959
  const logger = opts.logger ?? defaultLogger();
2753
2960
  let oldText;
2754
2961
  try {
2755
- oldText = await fs7.readFile(ymlPath, "utf8");
2962
+ oldText = await fs8.readFile(ymlPath, "utf8");
2756
2963
  } catch {
2757
2964
  throw new Error(
2758
2965
  `No such config: ${ymlPath}. Run \`monoceros init <template> ${opts.name}\` first.`
@@ -2776,7 +2983,7 @@ async function mutate(opts, apply) {
2776
2983
  return { status: "aborted" };
2777
2984
  }
2778
2985
  }
2779
- await fs7.writeFile(ymlPath, newText, "utf8");
2986
+ await fs8.writeFile(ymlPath, newText, "utf8");
2780
2987
  logger.success(`Updated ${ymlPath}.`);
2781
2988
  logger.info(
2782
2989
  `Run \`monoceros apply ${opts.name}\` to rebuild the dev-container and pick up the change.`
@@ -2912,7 +3119,7 @@ var addFeatureCommand = defineCommand2({
2912
3119
  },
2913
3120
  ref: {
2914
3121
  type: "positional",
2915
- description: "Devcontainer feature ref (OCI image style, e.g. `ghcr.io/devcontainers/features/docker-in-docker:2`).",
3122
+ description: "Feature to add. Either a Monoceros catalog short-name (e.g. `atlassian`, `atlassian/twg`, `claude` \u2014 see `monoceros list-components`) or a full OCI feature ref (e.g. `ghcr.io/devcontainers/features/docker-in-docker:2`). The short-name brings its catalog-defined default options; `-- key=value` overrides them.",
2916
3123
  required: true
2917
3124
  },
2918
3125
  yes: {
@@ -3245,12 +3452,12 @@ var addServiceCommand = defineCommand7({
3245
3452
  import { defineCommand as defineCommand8 } from "citty";
3246
3453
 
3247
3454
  // src/apply/index.ts
3248
- import { existsSync as existsSync4, promises as fs10 } from "fs";
3455
+ import { existsSync as existsSync5, promises as fs11 } from "fs";
3249
3456
  import { consola as consola11 } from "consola";
3250
3457
 
3251
3458
  // src/config/state.ts
3252
- import { promises as fs8 } from "fs";
3253
- import path7 from "path";
3459
+ import { promises as fs9 } from "fs";
3460
+ import path8 from "path";
3254
3461
  function buildStateFile(opts) {
3255
3462
  return {
3256
3463
  schemaVersion: CONFIG_SCHEMA_VERSION,
@@ -3260,20 +3467,20 @@ function buildStateFile(opts) {
3260
3467
  };
3261
3468
  }
3262
3469
  function stateFilePath(targetDir) {
3263
- return path7.join(targetDir, ".monoceros", "state.json");
3470
+ return path8.join(targetDir, ".monoceros", "state.json");
3264
3471
  }
3265
3472
  async function readStateFile(targetDir) {
3266
3473
  try {
3267
- const content = await fs8.readFile(stateFilePath(targetDir), "utf8");
3474
+ const content = await fs9.readFile(stateFilePath(targetDir), "utf8");
3268
3475
  return JSON.parse(content);
3269
3476
  } catch {
3270
3477
  return void 0;
3271
3478
  }
3272
3479
  }
3273
3480
  async function writeStateFile(targetDir, state) {
3274
- const monocerosDir = path7.join(targetDir, ".monoceros");
3275
- await fs8.mkdir(monocerosDir, { recursive: true });
3276
- await fs8.writeFile(
3481
+ const monocerosDir = path8.join(targetDir, ".monoceros");
3482
+ await fs9.mkdir(monocerosDir, { recursive: true });
3483
+ await fs9.writeFile(
3277
3484
  stateFilePath(targetDir),
3278
3485
  JSON.stringify(state, null, 2) + "\n"
3279
3486
  );
@@ -3343,8 +3550,8 @@ function solutionConfigToCreateOptions(config, featureDefaults = {}) {
3343
3550
 
3344
3551
  // src/devcontainer/compose.ts
3345
3552
  import { spawn as spawn5 } from "child_process";
3346
- import { existsSync as existsSync3 } from "fs";
3347
- import path9 from "path";
3553
+ import { existsSync as existsSync4 } from "fs";
3554
+ import path10 from "path";
3348
3555
  import { consola as consola9 } from "consola";
3349
3556
 
3350
3557
  // src/util/mask-secrets.ts
@@ -3407,7 +3614,7 @@ function createSecretMaskStream() {
3407
3614
  import { spawn as spawn4 } from "child_process";
3408
3615
  import { readFileSync as readFileSync2 } from "fs";
3409
3616
  import { createRequire } from "module";
3410
- import path8 from "path";
3617
+ import path9 from "path";
3411
3618
  var require_ = createRequire(import.meta.url);
3412
3619
  var cachedBinaryPath = null;
3413
3620
  function devcontainerCliPath() {
@@ -3418,7 +3625,7 @@ function devcontainerCliPath() {
3418
3625
  if (!binEntry) {
3419
3626
  throw new Error("Could not resolve @devcontainers/cli bin entry.");
3420
3627
  }
3421
- cachedBinaryPath = path8.resolve(path8.dirname(pkgJsonPath), binEntry);
3628
+ cachedBinaryPath = path9.resolve(path9.dirname(pkgJsonPath), binEntry);
3422
3629
  return cachedBinaryPath;
3423
3630
  }
3424
3631
  var spawnDevcontainer = (args, cwd, options = {}) => {
@@ -3490,16 +3697,16 @@ var spawnBash = (args, cwd) => {
3490
3697
  });
3491
3698
  };
3492
3699
  function composeProjectName(root) {
3493
- return `${path9.basename(root)}_devcontainer`;
3700
+ return `${path10.basename(root)}_devcontainer`;
3494
3701
  }
3495
3702
  function resolveCompose(root) {
3496
- if (!existsSync3(path9.join(root, ".devcontainer"))) {
3703
+ if (!existsSync4(path10.join(root, ".devcontainer"))) {
3497
3704
  throw new Error(
3498
3705
  `No .devcontainer/ at ${root}. Run \`monoceros apply <name>\` first.`
3499
3706
  );
3500
3707
  }
3501
- const composeFile = path9.join(root, ".devcontainer", "compose.yaml");
3502
- if (!existsSync3(composeFile)) {
3708
+ const composeFile = path10.join(root, ".devcontainer", "compose.yaml");
3709
+ if (!existsSync4(composeFile)) {
3503
3710
  throw new Error(
3504
3711
  `No compose.yaml at ${composeFile}. \`start\` / \`stop\` / \`status\` / \`logs\` require services configured via \`monoceros add-service <name> <svc>\`. Use \`monoceros shell <name>\` to enter the container directly.`
3505
3712
  );
@@ -3793,8 +4000,8 @@ function formatRootlessNotSupportedError() {
3793
4000
 
3794
4001
  // src/devcontainer/identity.ts
3795
4002
  import { spawn as spawn8 } from "child_process";
3796
- import { promises as fs9 } from "fs";
3797
- import path10 from "path";
4003
+ import { promises as fs10 } from "fs";
4004
+ import path11 from "path";
3798
4005
  import { consola as consola10 } from "consola";
3799
4006
  var realGitConfigGet = (key) => {
3800
4007
  return new Promise((resolve, reject) => {
@@ -3908,8 +4115,8 @@ async function resolveIdentityWithPrompt(options = {}) {
3908
4115
  };
3909
4116
  }
3910
4117
  async function collectGitIdentity(devContainerRoot, options = {}) {
3911
- const gitconfigDir = path10.join(devContainerRoot, ".monoceros");
3912
- const gitconfigPath = path10.join(gitconfigDir, "gitconfig");
4118
+ const gitconfigDir = path11.join(devContainerRoot, ".monoceros");
4119
+ const gitconfigPath = path11.join(gitconfigDir, "gitconfig");
3913
4120
  const logger = options.logger ?? { info: () => {
3914
4121
  }, warn: () => {
3915
4122
  } };
@@ -3922,8 +4129,8 @@ async function collectGitIdentity(devContainerRoot, options = {}) {
3922
4129
  const lines = ["[user]"];
3923
4130
  if (resolved.name !== void 0) lines.push(` name = ${resolved.name}`);
3924
4131
  if (resolved.email !== void 0) lines.push(` email = ${resolved.email}`);
3925
- await fs9.mkdir(gitconfigDir, { recursive: true });
3926
- await fs9.writeFile(gitconfigPath, lines.join("\n") + "\n");
4132
+ await fs10.mkdir(gitconfigDir, { recursive: true });
4133
+ await fs10.writeFile(gitconfigPath, lines.join("\n") + "\n");
3927
4134
  return {
3928
4135
  ...resolved.name !== void 0 ? { name: resolved.name } : {},
3929
4136
  ...resolved.email !== void 0 ? { email: resolved.email } : {},
@@ -3966,7 +4173,7 @@ async function readKeyFromHost(spawnFn, key, logger) {
3966
4173
  }
3967
4174
  async function readExistingGitconfig(filePath) {
3968
4175
  try {
3969
- const content = await fs9.readFile(filePath, "utf8");
4176
+ const content = await fs10.readFile(filePath, "utf8");
3970
4177
  const result = {};
3971
4178
  const nameMatch = /^\s*name\s*=\s*(.+?)\s*$/m.exec(content);
3972
4179
  const emailMatch = /^\s*email\s*=\s*(.+?)\s*$/m.exec(content);
@@ -3999,7 +4206,7 @@ ${sectionLine(label)}
3999
4206
  );
4000
4207
  }
4001
4208
  const ymlPath = containerConfigPath(opts.name, home);
4002
- if (!existsSync4(ymlPath)) {
4209
+ if (!existsSync5(ymlPath)) {
4003
4210
  throw new Error(
4004
4211
  `No such config: ${ymlPath}. Run \`monoceros init <template> ${opts.name}\` first.`
4005
4212
  );
@@ -4070,7 +4277,7 @@ ${sectionLine(label)}
4070
4277
  if (dockerMode === "rootless") {
4071
4278
  throw new Error(formatRootlessNotSupportedError());
4072
4279
  }
4073
- await fs10.mkdir(targetDir, { recursive: true });
4280
+ await fs11.mkdir(targetDir, { recursive: true });
4074
4281
  await writeScaffold(createOpts, targetDir, { dockerMode });
4075
4282
  await writeStateFile(
4076
4283
  targetDir,
@@ -4128,8 +4335,8 @@ ${sectionLine(label)}
4128
4335
  return { targetDir, configPath: ymlPath, containerExitCode: exitCode };
4129
4336
  }
4130
4337
  async function assertSafeTargetDir(targetDir, expectedOrigin) {
4131
- if (!existsSync4(targetDir)) return;
4132
- const entries = await fs10.readdir(targetDir);
4338
+ if (!existsSync5(targetDir)) return;
4339
+ const entries = await fs11.readdir(targetDir);
4133
4340
  if (entries.length === 0) return;
4134
4341
  const state = await readStateFile(targetDir);
4135
4342
  if (state) {
@@ -4199,7 +4406,7 @@ async function persistPromptedIdentity(prompted, ymlPath, home, logger) {
4199
4406
  }
4200
4407
  if (wantContainer) {
4201
4408
  try {
4202
- const text = await fs10.readFile(ymlPath, "utf8");
4409
+ const text = await fs11.readFile(ymlPath, "utf8");
4203
4410
  const parsed = parseConfig(text, ymlPath);
4204
4411
  const changed = setContainerGitUserInDoc(parsed.doc, {
4205
4412
  name: prompted.name,
@@ -4207,7 +4414,7 @@ async function persistPromptedIdentity(prompted, ymlPath, home, logger) {
4207
4414
  });
4208
4415
  if (changed) {
4209
4416
  const out = stringifyConfig(parsed.doc);
4210
- await fs10.writeFile(ymlPath, out, "utf8");
4417
+ await fs11.writeFile(ymlPath, out, "utf8");
4211
4418
  logger.info(
4212
4419
  `Saved identity in this container \u2014 wrote git.user into ${prettyPath(ymlPath)}.`
4213
4420
  );
@@ -4221,7 +4428,7 @@ async function persistPromptedIdentity(prompted, ymlPath, home, logger) {
4221
4428
  }
4222
4429
 
4223
4430
  // src/version.ts
4224
- var CLI_VERSION = true ? "1.8.0" : "dev";
4431
+ var CLI_VERSION = true ? "1.9.1" : "dev";
4225
4432
 
4226
4433
  // src/commands/_dispatch.ts
4227
4434
  import { consola as consola12 } from "consola";
@@ -4262,96 +4469,24 @@ var applyCommand = defineCommand8({
4262
4469
 
4263
4470
  // src/commands/completion.ts
4264
4471
  import { defineCommand as defineCommand9 } from "citty";
4265
- var ALL_COMMANDS = [
4266
- "init",
4267
- "list-components",
4268
- "shell",
4269
- "run",
4270
- "logs",
4271
- "start",
4272
- "stop",
4273
- "status",
4274
- "apply",
4275
- "remove",
4276
- "restore",
4277
- "add-service",
4278
- "add-language",
4279
- "add-apt-packages",
4280
- "add-feature",
4281
- "add-from-url",
4282
- "add-repo",
4283
- "add-port",
4284
- "remove-service",
4285
- "remove-language",
4286
- "remove-apt-packages",
4287
- "remove-feature",
4288
- "remove-from-url",
4289
- "remove-repo",
4290
- "remove-port",
4291
- "port",
4292
- "completion"
4293
- ];
4294
- var COMMANDS_WITH_CONTAINER_ARG = [
4295
- "shell",
4296
- "run",
4297
- "logs",
4298
- "start",
4299
- "stop",
4300
- "status",
4301
- "apply",
4302
- "remove",
4303
- "add-service",
4304
- "add-language",
4305
- "add-apt-packages",
4306
- "add-feature",
4307
- "add-from-url",
4308
- "add-repo",
4309
- "add-port",
4310
- "remove-service",
4311
- "remove-language",
4312
- "remove-apt-packages",
4313
- "remove-feature",
4314
- "remove-from-url",
4315
- "remove-repo",
4316
- "remove-port",
4317
- "port"
4318
- ];
4319
4472
  var SHELLS = ["bash", "zsh", "pwsh"];
4320
4473
  function renderCompletionScript(shell) {
4321
- const commands = ALL_COMMANDS.join(" ");
4322
- const containerCommandsRegex = COMMANDS_WITH_CONTAINER_ARG.join("|");
4323
4474
  if (shell === "bash") {
4324
4475
  return [
4325
4476
  "# bash completion for monoceros",
4326
4477
  "# install: source this file from .bashrc, e.g.",
4327
4478
  "# monoceros completion bash > ~/.bash_completion.d/monoceros",
4328
4479
  '# echo "source ~/.bash_completion.d/monoceros" >> ~/.bashrc',
4480
+ "#",
4481
+ "# The work is done by `monoceros __complete --line --point`; this",
4482
+ "# shell wrapper only forwards the cursor view.",
4329
4483
  "",
4330
4484
  "_monoceros() {",
4331
- " local cur prev cmd home configs_dir names",
4332
- ' cur="${COMP_WORDS[COMP_CWORD]}"',
4333
- "",
4334
- " if [[ $COMP_CWORD -eq 1 ]]; then",
4335
- ` COMPREPLY=( $(compgen -W "${commands}" -- "$cur") )`,
4336
- " return",
4337
- " fi",
4338
- "",
4339
- ' cmd="${COMP_WORDS[1]}"',
4340
- " if [[ $COMP_CWORD -eq 2 ]]; then",
4341
- ' case "$cmd" in',
4342
- ` ${containerCommandsRegex})`,
4343
- ' home="${MONOCEROS_HOME:-$HOME/.monoceros}"',
4344
- ' configs_dir="$home/container-configs"',
4345
- ' if [[ -d "$configs_dir" ]]; then',
4346
- ` names=$(cd "$configs_dir" && ls *.yml 2>/dev/null | sed 's/\\.yml$//')`,
4347
- ' COMPREPLY=( $(compgen -W "$names" -- "$cur") )',
4348
- " fi",
4349
- " ;;",
4350
- " completion)",
4351
- ` COMPREPLY=( $(compgen -W "${SHELLS.join(" ")}" -- "$cur") )`,
4352
- " ;;",
4353
- " esac",
4354
- " fi",
4485
+ " local IFS=$'\\n'",
4486
+ " local candidates",
4487
+ ' candidates=$(monoceros __complete --line "$COMP_LINE" --point "$COMP_POINT" 2>/dev/null)',
4488
+ ' local cur="${COMP_WORDS[COMP_CWORD]}"',
4489
+ ' COMPREPLY=( $(compgen -W "$candidates" -- "$cur") )',
4355
4490
  "}",
4356
4491
  "complete -F _monoceros monoceros",
4357
4492
  ""
@@ -4363,44 +4498,20 @@ function renderCompletionScript(shell) {
4363
4498
  "# install: dot-source this file from your $PROFILE, e.g.",
4364
4499
  "# monoceros completion pwsh > $HOME/.config/monoceros/completion.ps1",
4365
4500
  "# Add-Content $PROFILE '. $HOME/.config/monoceros/completion.ps1'",
4501
+ "#",
4502
+ "# The work is done by `monoceros __complete --line --point`; this",
4503
+ "# shell wrapper only forwards the cursor view.",
4366
4504
  "",
4367
4505
  "Register-ArgumentCompleter -Native -CommandName monoceros -ScriptBlock {",
4368
4506
  " param($wordToComplete, $commandAst, $cursorPosition)",
4369
- "",
4370
- " $commands = @(",
4371
- ...ALL_COMMANDS.map((c) => ` '${c}'`),
4372
- " )",
4373
- ` $shells = @('${SHELLS.join("', '")}')`,
4374
- " $containerCommands = @(",
4375
- ...COMMANDS_WITH_CONTAINER_ARG.map((c) => ` '${c}'`),
4376
- " )",
4377
- "",
4378
- " $tokens = $commandAst.CommandElements",
4379
- " $position = $tokens.Count",
4380
- " if ($wordToComplete) { $position-- }",
4381
- "",
4382
- " if ($position -eq 1) {",
4383
- ' $commands | Where-Object { $_ -like "$wordToComplete*" } |',
4384
- ' ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, "ParameterValue", $_) }',
4385
- " return",
4386
- " }",
4387
- "",
4388
- " if ($position -eq 2) {",
4389
- " $cmd = $tokens[1].Value",
4390
- " if ($containerCommands -contains $cmd) {",
4391
- ' $home = if ($env:MONOCEROS_HOME) { $env:MONOCEROS_HOME } else { Join-Path $env:USERPROFILE ".monoceros" }',
4392
- ' $configsDir = Join-Path $home "container-configs"',
4393
- " if (Test-Path $configsDir) {",
4394
- ' Get-ChildItem -Path $configsDir -Filter "*.yml" |',
4395
- " ForEach-Object { $_.BaseName } |",
4396
- ' Where-Object { $_ -like "$wordToComplete*" } |',
4397
- ' ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, "ParameterValue", $_) }',
4398
- " }",
4399
- ' } elseif ($cmd -eq "completion") {',
4400
- ' $shells | Where-Object { $_ -like "$wordToComplete*" } |',
4401
- ' ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, "ParameterValue", $_) }',
4402
- " }",
4403
- " }",
4507
+ " $line = $commandAst.Extent.Text",
4508
+ " $point = $cursorPosition - $commandAst.Extent.StartOffset",
4509
+ " if ($point -lt 0) { $point = 0 }",
4510
+ " $raw = & monoceros __complete --line $line --point $point 2>$null",
4511
+ " if (-not $raw) { return }",
4512
+ ' $raw -split "`n" |',
4513
+ ' Where-Object { $_.Length -gt 0 -and $_ -like "$wordToComplete*" } |',
4514
+ ' ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, "ParameterValue", $_) }',
4404
4515
  "}",
4405
4516
  ""
4406
4517
  ].join("\n");
@@ -4411,36 +4522,17 @@ function renderCompletionScript(shell) {
4411
4522
  "# install: drop this file somewhere on your $fpath as `_monoceros`,",
4412
4523
  "# then start a new shell (or run `compinit`). Example:",
4413
4524
  '# monoceros completion zsh > "${fpath[1]}/_monoceros"',
4525
+ "#",
4526
+ "# The work is done by `monoceros __complete --line --point`; this",
4527
+ "# shell wrapper only forwards the cursor view.",
4414
4528
  "",
4415
4529
  "_monoceros() {",
4416
- " local -a commands shells",
4417
- " commands=(",
4418
- ...ALL_COMMANDS.map((c) => ` '${c}'`),
4419
- " )",
4420
- ` shells=(${SHELLS.map((s) => `'${s}'`).join(" ")})`,
4421
- "",
4422
- " if (( CURRENT == 2 )); then",
4423
- " _describe 'monoceros command' commands",
4424
- " return",
4425
- " fi",
4426
- "",
4427
- " local cmd=${words[2]}",
4428
- " if (( CURRENT == 3 )); then",
4429
- " case $cmd in",
4430
- ` ${containerCommandsRegex})`,
4431
- ' local home="${MONOCEROS_HOME:-$HOME/.monoceros}"',
4432
- ' local configs_dir="$home/container-configs"',
4433
- " if [[ -d $configs_dir ]]; then",
4434
- " local -a names",
4435
- " names=(${configs_dir}/*.yml(N:t:r))",
4436
- " _describe 'container' names",
4437
- " fi",
4438
- " ;;",
4439
- " completion)",
4440
- " _describe 'shell' shells",
4441
- " ;;",
4442
- " esac",
4443
- " fi",
4530
+ ' local line="$BUFFER"',
4531
+ ' local point="$CURSOR"',
4532
+ " local -a candidates",
4533
+ ' candidates=("${(@f)$(monoceros __complete --line "$line" --point "$point" 2>/dev/null)}")',
4534
+ ' candidates=("${(@)candidates:#}")',
4535
+ " _describe 'completion' candidates",
4444
4536
  "}",
4445
4537
  "",
4446
4538
  '_monoceros "$@"',
@@ -4473,178 +4565,484 @@ var completionCommand = defineCommand9({
4473
4565
  }
4474
4566
  });
4475
4567
 
4476
- // src/commands/init.ts
4568
+ // src/commands/__complete.ts
4477
4569
  import { defineCommand as defineCommand10 } from "citty";
4478
- import { consola as consola14 } from "consola";
4479
4570
 
4480
- // src/init/index.ts
4571
+ // src/completion/resolve.ts
4481
4572
  import { existsSync as existsSync7, promises as fs12 } from "fs";
4482
- import { consola as consola13 } from "consola";
4573
+ import path13 from "path";
4483
4574
 
4484
- // src/init/components.ts
4485
- import { existsSync as existsSync5, promises as fs11 } from "fs";
4486
- import path11 from "path";
4487
- import { z as z3 } from "zod";
4488
- import { parse as parseYaml } from "yaml";
4489
- var CategorySchema = z3.enum(["language", "service", "feature"]);
4490
- var FeatureContributionSchema = z3.object({
4491
- ref: z3.string().regex(REGEX.featureRef),
4492
- options: z3.record(z3.string(), FeatureOptionValueSchema).optional()
4493
- });
4494
- var ComponentFileSchema = z3.object({
4495
- displayName: z3.string().min(1),
4496
- description: z3.string().min(1),
4497
- category: CategorySchema,
4498
- contributes: z3.object({
4499
- languages: z3.array(z3.string().min(1)).optional(),
4500
- services: z3.array(z3.string().min(1)).optional(),
4501
- features: z3.array(FeatureContributionSchema).optional()
4502
- })
4503
- }).superRefine((data, ctx) => {
4504
- const c = data.contributes;
4505
- const filled = [
4506
- c.languages && c.languages.length > 0 ? "languages" : null,
4507
- c.services && c.services.length > 0 ? "services" : null,
4508
- c.features && c.features.length > 0 ? "features" : null
4509
- ].filter((x) => x !== null);
4510
- if (filled.length === 0) {
4511
- ctx.addIssue({
4512
- code: z3.ZodIssueCode.custom,
4513
- message: "contributes must set at least one of languages/services/features"
4514
- });
4515
- return;
4575
+ // src/init/manifest.ts
4576
+ import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
4577
+ import path12 from "path";
4578
+ function resolveManifestPath(name, checkoutRoot) {
4579
+ if (checkoutRoot) {
4580
+ const checkoutPath = path12.join(
4581
+ checkoutRoot,
4582
+ "images",
4583
+ "features",
4584
+ name,
4585
+ "devcontainer-feature.json"
4586
+ );
4587
+ if (existsSync6(checkoutPath)) return checkoutPath;
4516
4588
  }
4517
- if (filled.length > 1) {
4518
- ctx.addIssue({
4519
- code: z3.ZodIssueCode.custom,
4520
- message: `contributes must set exactly one of languages/services/features, got: ${filled.join(", ")}`
4521
- });
4522
- return;
4589
+ const bundlePath = path12.join(
4590
+ bundledFeaturesDir(),
4591
+ name,
4592
+ "devcontainer-feature.json"
4593
+ );
4594
+ if (existsSync6(bundlePath)) return bundlePath;
4595
+ return null;
4596
+ }
4597
+ function loadFeatureManifestSummary(ref, checkoutRoot = workbenchCheckoutRoot()) {
4598
+ const match = matchMonocerosFeature(ref);
4599
+ if (!match) return void 0;
4600
+ const manifestPath = resolveManifestPath(match.name, checkoutRoot);
4601
+ if (!manifestPath) return void 0;
4602
+ try {
4603
+ const text = readFileSync3(manifestPath, "utf8");
4604
+ const parsed = JSON.parse(text);
4605
+ const rawHints = parsed["x-monoceros"]?.optionHints;
4606
+ const optionHints = Array.isArray(rawHints) ? rawHints.filter(
4607
+ (x) => typeof x === "string" && x.length > 0
4608
+ ) : [];
4609
+ const rawNotes = parsed["x-monoceros"]?.usageNotes;
4610
+ const usageNotes = Array.isArray(rawNotes) ? rawNotes.filter(
4611
+ (x) => typeof x === "string" && x.length > 0
4612
+ ) : [];
4613
+ const optionDescriptions = {};
4614
+ const optionTypes = {};
4615
+ const optionNames = [];
4616
+ if (parsed.options) {
4617
+ for (const [key, opt] of Object.entries(parsed.options)) {
4618
+ if (!opt || typeof opt !== "object") continue;
4619
+ optionNames.push(key);
4620
+ if (typeof opt.description === "string" && opt.description.length > 0) {
4621
+ optionDescriptions[key] = opt.description;
4622
+ }
4623
+ if (opt.type === "boolean") {
4624
+ optionTypes[key] = "boolean";
4625
+ } else if (opt.type === "string") {
4626
+ optionTypes[key] = "string";
4627
+ }
4628
+ }
4629
+ }
4630
+ const name = typeof parsed.name === "string" ? parsed.name : "";
4631
+ const description = typeof parsed.description === "string" ? parsed.description : "";
4632
+ const rawUrl = typeof parsed.documentationURL === "string" ? parsed.documentationURL.trim() : "";
4633
+ const documentationURL = rawUrl.length > 0 && rawUrl.toLowerCase() !== "tbd" ? rawUrl : void 0;
4634
+ return {
4635
+ name,
4636
+ description,
4637
+ documentationURL,
4638
+ optionHints,
4639
+ optionDescriptions,
4640
+ optionNames,
4641
+ optionTypes,
4642
+ usageNotes
4643
+ };
4644
+ } catch {
4645
+ return void 0;
4523
4646
  }
4524
- const expected = data.category === "language" ? "languages" : data.category === "service" ? "services" : "features";
4525
- if (filled[0] !== expected) {
4526
- ctx.addIssue({
4527
- code: z3.ZodIssueCode.custom,
4528
- message: `category '${data.category}' requires contributes.${expected}, got contributes.${filled[0]}`
4529
- });
4647
+ }
4648
+
4649
+ // src/completion/resolve.ts
4650
+ async function resolveCompletions(line, point, opts = {}) {
4651
+ const { prev, current } = parseCompletionLine(line, point);
4652
+ const ctx = { prev, current, opts };
4653
+ const head = prev[0];
4654
+ const afterProgram = head === void 0 ? [] : prev.slice(1);
4655
+ if (afterProgram.length === 0) {
4656
+ return filterPrefix(ALL_COMMANDS, current);
4657
+ }
4658
+ const command = afterProgram[0];
4659
+ const spec = COMMAND_SPECS[command];
4660
+ if (!spec) {
4661
+ return [];
4662
+ }
4663
+ const argTokens = afterProgram.slice(1);
4664
+ return dispatchCommand(spec, argTokens, ctx);
4665
+ }
4666
+ function parseCompletionLine(line, point) {
4667
+ const before = line.slice(0, Math.max(0, Math.min(point, line.length)));
4668
+ const tokens = tokenize(before);
4669
+ const lastChar = before.length > 0 ? before[before.length - 1] : "";
4670
+ if (tokens.length === 0 || isShellWhitespace(lastChar)) {
4671
+ return { prev: tokens, current: "" };
4530
4672
  }
4531
- });
4532
- async function loadComponentCatalog(rootDir = componentsDir()) {
4533
- if (!existsSync5(rootDir)) {
4534
- return /* @__PURE__ */ new Map();
4673
+ return {
4674
+ prev: tokens.slice(0, -1),
4675
+ current: tokens[tokens.length - 1]
4676
+ };
4677
+ }
4678
+ function tokenize(text) {
4679
+ const out = [];
4680
+ let i = 0;
4681
+ while (i < text.length) {
4682
+ while (i < text.length && isShellWhitespace(text[i])) i++;
4683
+ if (i >= text.length) break;
4684
+ let token = "";
4685
+ let quote = null;
4686
+ while (i < text.length) {
4687
+ const ch = text[i];
4688
+ if (quote === null && isShellWhitespace(ch)) break;
4689
+ if (quote === null && (ch === '"' || ch === "'")) {
4690
+ quote = ch;
4691
+ i++;
4692
+ continue;
4693
+ }
4694
+ if (quote !== null && ch === quote) {
4695
+ quote = null;
4696
+ i++;
4697
+ continue;
4698
+ }
4699
+ token += ch;
4700
+ i++;
4701
+ }
4702
+ out.push(token);
4535
4703
  }
4536
- const out = /* @__PURE__ */ new Map();
4537
- await walk(rootDir, rootDir, out);
4538
4704
  return out;
4539
4705
  }
4540
- async function walk(baseDir, currentDir, out) {
4541
- const entries = await fs11.readdir(currentDir, { withFileTypes: true });
4542
- for (const entry2 of entries) {
4543
- const full = path11.join(currentDir, entry2.name);
4544
- if (entry2.isDirectory()) {
4545
- await walk(baseDir, full, out);
4706
+ function isShellWhitespace(ch) {
4707
+ return ch === " " || ch === " ";
4708
+ }
4709
+ function dispatchCommand(spec, argTokens, ctx) {
4710
+ const dashDashIdx = argTokens.indexOf("--");
4711
+ const preDash = dashDashIdx < 0 ? argTokens : argTokens.slice(0, dashDashIdx);
4712
+ const inPostDash = dashDashIdx >= 0;
4713
+ if (inPostDash && spec.innerArgs) {
4714
+ return resolveValues(spec.innerArgs, ctx, ctx.current);
4715
+ }
4716
+ return resolvePreDash(spec, preDash, ctx);
4717
+ }
4718
+ async function resolvePreDash(spec, preDash, ctx) {
4719
+ const current = ctx.current;
4720
+ if (current.startsWith("--") && current.includes("=")) {
4721
+ const eqIdx = current.indexOf("=");
4722
+ const flagName = current.slice(0, eqIdx);
4723
+ const valueFragment = current.slice(eqIdx + 1);
4724
+ const flag = spec.flags?.[flagName];
4725
+ if (!flag || flag.type !== "value" || !flag.values) return [];
4726
+ const completions = await resolveValues(flag.values, ctx, valueFragment);
4727
+ return completions.map((v) => `${flagName}=${v}`);
4728
+ }
4729
+ if (current.startsWith("-")) {
4730
+ return listFlagNames(spec.flags ?? {}, current);
4731
+ }
4732
+ const lastPrev = preDash[preDash.length - 1];
4733
+ if (lastPrev && lastPrev.startsWith("--") && !lastPrev.includes("=")) {
4734
+ const flag = spec.flags?.[lastPrev];
4735
+ if (flag && flag.type === "value" && flag.values) {
4736
+ return resolveValues(flag.values, ctx, current);
4737
+ }
4738
+ }
4739
+ const positionalIdx = countCompletedPositionals(preDash, spec.flags ?? {});
4740
+ const positionals = spec.positionals ?? [];
4741
+ const expectedPositionalCount = spec.positionalCount ?? positionals.length;
4742
+ if (positionalIdx < positionals.length) {
4743
+ const positional = positionals[positionalIdx];
4744
+ if (positional) return resolveValues(positional, ctx, current);
4745
+ }
4746
+ if (positionalIdx >= expectedPositionalCount) {
4747
+ return listFlagNames(spec.flags ?? {}, current);
4748
+ }
4749
+ return [];
4750
+ }
4751
+ function listFlagNames(flags, fragment) {
4752
+ const names = [];
4753
+ for (const [name, spec] of Object.entries(flags)) {
4754
+ names.push(name);
4755
+ for (const alias of spec.aliases ?? []) names.push(alias);
4756
+ }
4757
+ return filterPrefix(names, fragment);
4758
+ }
4759
+ function countCompletedPositionals(preDash, flags) {
4760
+ let count = 0;
4761
+ for (let i = 0; i < preDash.length; i++) {
4762
+ const t = preDash[i];
4763
+ if (t.startsWith("--") && t.includes("=")) continue;
4764
+ if (t.startsWith("-")) {
4765
+ const flag = flags[t] ?? aliasFlag(flags, t);
4766
+ if (flag && flag.type === "value" && i + 1 < preDash.length) {
4767
+ i++;
4768
+ }
4546
4769
  continue;
4547
4770
  }
4548
- if (!entry2.isFile() || !entry2.name.endsWith(".yml")) continue;
4549
- const relative = path11.relative(baseDir, full);
4550
- const name = relative.replace(/\.yml$/, "").split(path11.sep).join("/");
4551
- const text = await fs11.readFile(full, "utf8");
4552
- let raw;
4553
- try {
4554
- raw = parseYaml(text);
4555
- } catch (err) {
4556
- throw new Error(
4557
- `Failed to parse component ${name} (${full}): ${err.message}`
4558
- );
4559
- }
4560
- const parsed = ComponentFileSchema.safeParse(raw);
4561
- if (!parsed.success) {
4562
- const issues = parsed.error.issues.map((issue) => {
4563
- const where = issue.path.length > 0 ? issue.path.join(".") : "(root)";
4564
- return ` - ${where}: ${issue.message}`;
4565
- }).join("\n");
4566
- throw new Error(`Invalid component ${name} (${full}):
4567
- ${issues}`);
4568
- }
4569
- out.set(name, { name, sourcePath: full, file: parsed.data });
4771
+ count++;
4570
4772
  }
4773
+ return count;
4571
4774
  }
4572
- function mergeComponents(resolved) {
4573
- const languages = [];
4574
- const services = [];
4575
- const featureByRef = /* @__PURE__ */ new Map();
4576
- for (const entry2 of resolved) {
4577
- const c = isResolvedComponent(entry2) ? entry2.component : entry2;
4578
- const version = isResolvedComponent(entry2) ? entry2.version : void 0;
4579
- const ct = c.file.contributes;
4580
- for (const lang of ct.languages ?? []) {
4581
- const value = version !== void 0 ? `${lang}:${version}` : lang;
4582
- if (!languages.includes(value)) languages.push(value);
4583
- }
4584
- for (const svc of ct.services ?? []) {
4585
- if (!services.includes(svc)) services.push(svc);
4586
- }
4587
- for (const f of ct.features ?? []) {
4588
- const existing = featureByRef.get(f.ref);
4589
- if (!existing) {
4590
- featureByRef.set(f.ref, {
4591
- ref: f.ref,
4592
- options: { ...f.options ?? {} }
4593
- });
4594
- continue;
4595
- }
4596
- existing.options = mergeFeatureOptions(existing.options, f.options ?? {});
4597
- }
4775
+ function aliasFlag(flags, token) {
4776
+ for (const f of Object.values(flags)) {
4777
+ if (f.aliases?.includes(token)) return f;
4598
4778
  }
4599
- return {
4600
- languages,
4601
- services,
4602
- features: [...featureByRef.values()]
4603
- };
4779
+ return void 0;
4604
4780
  }
4605
- function isResolvedComponent(x) {
4606
- return "component" in x;
4781
+ async function resolveValues(source, ctx, fragment) {
4782
+ const values = await source(ctx);
4783
+ const commaIdx = fragment.lastIndexOf(",");
4784
+ if (commaIdx < 0) {
4785
+ return filterPrefix(values, fragment);
4786
+ }
4787
+ const prefix = fragment.slice(0, commaIdx + 1);
4788
+ const tail = fragment.slice(commaIdx + 1);
4789
+ return filterPrefix(values, tail).map((v) => `${prefix}${v}`);
4790
+ }
4791
+ function filterPrefix(values, fragment) {
4792
+ if (fragment.length === 0) return [...values];
4793
+ return values.filter((v) => v.startsWith(fragment));
4794
+ }
4795
+ async function listContainerNames(ctx) {
4796
+ const home = ctx.opts.monocerosHome ?? monocerosHome();
4797
+ const dir = path13.join(home, "container-configs");
4798
+ if (!existsSync7(dir)) return [];
4799
+ const entries = await fs12.readdir(dir);
4800
+ return entries.filter((e) => e.endsWith(".yml")).map((e) => e.slice(0, -".yml".length)).sort();
4801
+ }
4802
+ async function listAllCatalogComponents() {
4803
+ const catalog = await loadComponentCatalog();
4804
+ return [...catalog.keys()].sort();
4805
+ }
4806
+ async function listFeatureComponents() {
4807
+ const catalog = await loadComponentCatalog();
4808
+ return [...catalog.values()].filter((c) => c.file.category === "feature").map((c) => c.name).sort();
4809
+ }
4810
+ function listLanguageNames() {
4811
+ return knownLanguages().sort();
4812
+ }
4813
+ function listServiceNames() {
4814
+ return knownServices().sort();
4815
+ }
4816
+ function listProviders() {
4817
+ return [...PROVIDER_VALUES];
4818
+ }
4819
+ function listShellNames() {
4820
+ return ["bash", "zsh", "pwsh"];
4821
+ }
4822
+ async function listFeatureOptionInnerArgs(ctx) {
4823
+ const after = ctx.prev.slice(2);
4824
+ let positionalCount = 0;
4825
+ let featureToken;
4826
+ for (let i = 0; i < after.length; i++) {
4827
+ const t = after[i];
4828
+ if (t === "--") break;
4829
+ if (t.startsWith("-")) continue;
4830
+ positionalCount++;
4831
+ if (positionalCount === 2) {
4832
+ featureToken = t;
4833
+ break;
4834
+ }
4835
+ }
4836
+ if (!featureToken) return [];
4837
+ const ref = await resolveFeatureRefForCompletion(featureToken);
4838
+ if (!ref) return [];
4839
+ const summary = loadFeatureManifestSummary(ref);
4840
+ if (!summary) return [];
4841
+ const dashDash = ctx.prev.indexOf("--");
4842
+ const innerSoFar = dashDash >= 0 ? ctx.prev.slice(dashDash + 1) : [];
4843
+ const usedKeys = /* @__PURE__ */ new Set();
4844
+ for (const t of innerSoFar) {
4845
+ const eq = t.indexOf("=");
4846
+ if (eq > 0) usedKeys.add(t.slice(0, eq));
4847
+ }
4848
+ const eqIdx = ctx.current.indexOf("=");
4849
+ if (eqIdx >= 0) {
4850
+ const key = ctx.current.slice(0, eqIdx);
4851
+ const valueFragment = ctx.current.slice(eqIdx + 1);
4852
+ const type = summary.optionTypes[key];
4853
+ if (type === "boolean") {
4854
+ return ["true", "false"].filter((v) => v.startsWith(valueFragment)).map((v) => `${key}=${v}`);
4855
+ }
4856
+ return [];
4857
+ }
4858
+ return summary.optionNames.filter((n) => !usedKeys.has(n));
4859
+ }
4860
+ async function resolveFeatureRefForCompletion(token) {
4861
+ if (REGEX.featureRef.test(token)) return token;
4862
+ const catalog = await loadComponentCatalog();
4863
+ const c = catalog.get(token);
4864
+ if (!c || c.file.category !== "feature") return void 0;
4865
+ const f = c.file.contributes.features?.[0];
4866
+ return f?.ref;
4607
4867
  }
4608
- function mergeFeatureOptions(a, b) {
4609
- const result = { ...a };
4610
- for (const [key, valueB] of Object.entries(b)) {
4611
- const valueA = result[key];
4612
- if (typeof valueA === "boolean" && typeof valueB === "boolean") {
4613
- result[key] = valueA || valueB;
4614
- continue;
4868
+ var ALL_COMMANDS = [
4869
+ "init",
4870
+ "list-components",
4871
+ "shell",
4872
+ "run",
4873
+ "logs",
4874
+ "start",
4875
+ "stop",
4876
+ "status",
4877
+ "apply",
4878
+ "remove",
4879
+ "restore",
4880
+ "add-service",
4881
+ "add-language",
4882
+ "add-apt-packages",
4883
+ "add-feature",
4884
+ "add-from-url",
4885
+ "add-repo",
4886
+ "add-port",
4887
+ "remove-service",
4888
+ "remove-language",
4889
+ "remove-apt-packages",
4890
+ "remove-feature",
4891
+ "remove-from-url",
4892
+ "remove-repo",
4893
+ "remove-port",
4894
+ "port",
4895
+ "completion"
4896
+ ];
4897
+ var containerName = (ctx) => listContainerNames(ctx);
4898
+ var COMMAND_SPECS = {
4899
+ init: {
4900
+ // First positional is a FRESH name → no suggestion source, but
4901
+ // the slot exists. Once the cursor is past it (after the name +
4902
+ // space), `--with` / `--with-repo` / `--with-ports` surface as
4903
+ // flag suggestions.
4904
+ positionalCount: 1,
4905
+ flags: {
4906
+ "--with": { type: "value", values: () => listAllCatalogComponents() },
4907
+ "--with-repo": { type: "value" },
4908
+ "--with-ports": { type: "value" }
4615
4909
  }
4616
- result[key] = valueB;
4910
+ },
4911
+ apply: {
4912
+ positionals: [containerName],
4913
+ flags: { "--yes": { type: "boolean", aliases: ["-y"] } }
4914
+ },
4915
+ remove: {
4916
+ positionals: [containerName],
4917
+ flags: {
4918
+ "--yes": { type: "boolean", aliases: ["-y"] },
4919
+ "--no-backup": { type: "boolean" }
4920
+ }
4921
+ },
4922
+ shell: { positionals: [containerName] },
4923
+ run: { positionals: [containerName] },
4924
+ logs: { positionals: [containerName] },
4925
+ start: { positionals: [containerName] },
4926
+ stop: { positionals: [containerName] },
4927
+ status: { positionals: [containerName] },
4928
+ "add-language": {
4929
+ positionals: [containerName, () => listLanguageNames()]
4930
+ },
4931
+ "add-service": {
4932
+ positionals: [containerName, () => listServiceNames()]
4933
+ },
4934
+ "add-apt-packages": {
4935
+ positionals: [containerName],
4936
+ innerArgs: () => []
4937
+ // freeform package names — no useful suggestion list
4938
+ },
4939
+ "add-feature": {
4940
+ positionals: [containerName, () => listFeatureComponents()],
4941
+ flags: { "--yes": { type: "boolean", aliases: ["-y"] } },
4942
+ innerArgs: (ctx) => listFeatureOptionInnerArgs(ctx)
4943
+ },
4944
+ "add-from-url": { positionals: [containerName] },
4945
+ "add-repo": {
4946
+ positionals: [containerName],
4947
+ flags: {
4948
+ "--path": { type: "value" },
4949
+ "--git-name": { type: "value" },
4950
+ "--git-email": { type: "value" },
4951
+ "--provider": { type: "value", values: () => listProviders() },
4952
+ "--yes": { type: "boolean", aliases: ["-y"] }
4953
+ }
4954
+ },
4955
+ "add-port": {
4956
+ positionals: [containerName],
4957
+ flags: {
4958
+ "--default": { type: "boolean" },
4959
+ "--yes": { type: "boolean", aliases: ["-y"] }
4960
+ },
4961
+ innerArgs: () => []
4962
+ },
4963
+ "remove-language": {
4964
+ positionals: [containerName, () => listLanguageNames()]
4965
+ },
4966
+ "remove-service": {
4967
+ positionals: [containerName, () => listServiceNames()]
4968
+ },
4969
+ "remove-apt-packages": {
4970
+ positionals: [containerName],
4971
+ innerArgs: () => []
4972
+ },
4973
+ "remove-feature": {
4974
+ positionals: [containerName, () => listFeatureComponents()],
4975
+ flags: { "--yes": { type: "boolean", aliases: ["-y"] } }
4976
+ },
4977
+ "remove-from-url": { positionals: [containerName] },
4978
+ "remove-repo": { positionals: [containerName] },
4979
+ "remove-port": {
4980
+ positionals: [containerName],
4981
+ flags: { "--yes": { type: "boolean", aliases: ["-y"] } },
4982
+ innerArgs: () => []
4983
+ },
4984
+ port: {
4985
+ positionals: [containerName],
4986
+ flags: { "--default": { type: "boolean" } },
4987
+ innerArgs: () => []
4988
+ },
4989
+ completion: {
4990
+ positionals: [() => listShellNames()]
4991
+ },
4992
+ "list-components": {},
4993
+ restore: {
4994
+ // First positional is a backup-path; no value suggestions today
4995
+ // (could plug filesystem completion later). Slot still exists so
4996
+ // Tab is silent inside it rather than offering flags prematurely.
4997
+ positionalCount: 1
4617
4998
  }
4618
- return result;
4619
- }
4620
- function resolveComponents(catalog, names) {
4621
- const unknown = [];
4622
- const out = [];
4623
- for (const raw of names) {
4624
- const colon = raw.indexOf(":");
4625
- const name = colon === -1 ? raw : raw.slice(0, colon);
4626
- const version = colon === -1 ? void 0 : raw.slice(colon + 1);
4627
- const c = catalog.get(name);
4628
- if (!c) {
4629
- unknown.push(raw);
4630
- continue;
4999
+ };
5000
+ var COMPLETION_COMMAND_SPEC_KEYS = Object.keys(COMMAND_SPECS);
5001
+
5002
+ // src/commands/__complete.ts
5003
+ var __completeCommand = defineCommand10({
5004
+ meta: {
5005
+ name: "__complete",
5006
+ group: "internal",
5007
+ description: "Internal \u2014 shell completion engine. Used by the wrappers emitted by `monoceros completion <shell>`. Output one candidate completion per line."
5008
+ },
5009
+ args: {
5010
+ line: {
5011
+ type: "string",
5012
+ description: "Full command line buffer up to (and possibly past) the cursor.",
5013
+ default: ""
5014
+ },
5015
+ point: {
5016
+ type: "string",
5017
+ description: "Byte offset of the cursor within --line. Default: end of line.",
5018
+ default: ""
4631
5019
  }
4632
- if (version !== void 0 && c.file.category !== "language") {
4633
- throw new Error(
4634
- `Component '${name}' is a ${c.file.category}, not a language \u2014 a ':${version}' suffix has no meaning here.`
5020
+ },
5021
+ async run({ args }) {
5022
+ const line = String(args.line ?? "");
5023
+ const point = args.point && String(args.point).length > 0 ? Number.parseInt(String(args.point), 10) : line.length;
5024
+ let candidates;
5025
+ try {
5026
+ candidates = await resolveCompletions(
5027
+ line,
5028
+ Number.isFinite(point) ? point : line.length
4635
5029
  );
5030
+ } catch {
5031
+ candidates = [];
5032
+ }
5033
+ if (candidates.length > 0) {
5034
+ process.stdout.write(candidates.join("\n") + "\n");
4636
5035
  }
4637
- out.push({ component: c, ...version !== void 0 ? { version } : {} });
4638
- }
4639
- if (unknown.length > 0) {
4640
- const available = [...catalog.keys()].sort();
4641
- throw new Error(
4642
- `Unknown component${unknown.length > 1 ? "s" : ""}: ${unknown.join(", ")}.
4643
- Available: ${available.join(", ")}.`
4644
- );
4645
5036
  }
4646
- return out;
4647
- }
5037
+ });
5038
+
5039
+ // src/commands/init.ts
5040
+ import { defineCommand as defineCommand11 } from "citty";
5041
+ import { consola as consola14 } from "consola";
5042
+
5043
+ // src/init/index.ts
5044
+ import { existsSync as existsSync8, promises as fs13 } from "fs";
5045
+ import { consola as consola13 } from "consola";
4648
5046
 
4649
5047
  // src/init/generator.ts
4650
5048
  var 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.";
@@ -5011,69 +5409,6 @@ function ensureTrailingNewline(s) {
5011
5409
  return s.endsWith("\n") ? s : s + "\n";
5012
5410
  }
5013
5411
 
5014
- // src/init/manifest.ts
5015
- import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
5016
- import path12 from "path";
5017
- function resolveManifestPath(name, checkoutRoot) {
5018
- if (checkoutRoot) {
5019
- const checkoutPath = path12.join(
5020
- checkoutRoot,
5021
- "images",
5022
- "features",
5023
- name,
5024
- "devcontainer-feature.json"
5025
- );
5026
- if (existsSync6(checkoutPath)) return checkoutPath;
5027
- }
5028
- const bundlePath = path12.join(
5029
- bundledFeaturesDir(),
5030
- name,
5031
- "devcontainer-feature.json"
5032
- );
5033
- if (existsSync6(bundlePath)) return bundlePath;
5034
- return null;
5035
- }
5036
- function loadFeatureManifestSummary(ref, checkoutRoot = workbenchCheckoutRoot()) {
5037
- const match = matchMonocerosFeature(ref);
5038
- if (!match) return void 0;
5039
- const manifestPath = resolveManifestPath(match.name, checkoutRoot);
5040
- if (!manifestPath) return void 0;
5041
- try {
5042
- const text = readFileSync3(manifestPath, "utf8");
5043
- const parsed = JSON.parse(text);
5044
- const rawHints = parsed["x-monoceros"]?.optionHints;
5045
- const optionHints = Array.isArray(rawHints) ? rawHints.filter(
5046
- (x) => typeof x === "string" && x.length > 0
5047
- ) : [];
5048
- const rawNotes = parsed["x-monoceros"]?.usageNotes;
5049
- const usageNotes = Array.isArray(rawNotes) ? rawNotes.filter(
5050
- (x) => typeof x === "string" && x.length > 0
5051
- ) : [];
5052
- const optionDescriptions = {};
5053
- if (parsed.options) {
5054
- for (const [key, opt] of Object.entries(parsed.options)) {
5055
- if (opt && typeof opt === "object" && typeof opt.description === "string" && opt.description.length > 0) {
5056
- optionDescriptions[key] = opt.description;
5057
- }
5058
- }
5059
- }
5060
- const name = typeof parsed.name === "string" ? parsed.name : "";
5061
- const description = typeof parsed.description === "string" ? parsed.description : "";
5062
- const rawUrl = typeof parsed.documentationURL === "string" ? parsed.documentationURL.trim() : "";
5063
- const documentationURL = rawUrl.length > 0 && rawUrl.toLowerCase() !== "tbd" ? rawUrl : void 0;
5064
- return {
5065
- name,
5066
- description,
5067
- documentationURL,
5068
- optionHints,
5069
- optionDescriptions,
5070
- usageNotes
5071
- };
5072
- } catch {
5073
- return void 0;
5074
- }
5075
- }
5076
-
5077
5412
  // src/init/index.ts
5078
5413
  async function runInit(opts) {
5079
5414
  const workbench = opts.workbenchRoot ?? workbenchRoot();
@@ -5088,7 +5423,7 @@ async function runInit(opts) {
5088
5423
  );
5089
5424
  }
5090
5425
  const dest = containerConfigPath(opts.name, home);
5091
- if (existsSync7(dest)) {
5426
+ if (existsSync8(dest)) {
5092
5427
  throw new Error(
5093
5428
  `Config already exists: ${dest}. Delete it manually before re-running \`monoceros init\` \u2014 this protects any hand-edits.`
5094
5429
  );
@@ -5165,8 +5500,8 @@ async function runInit(opts) {
5165
5500
  const components = resolveComponents(catalog, requested);
5166
5501
  text = generateComposedYml(opts.name, components, lookup, repos, ports);
5167
5502
  }
5168
- await fs12.mkdir(containerConfigsDir(home), { recursive: true });
5169
- await fs12.writeFile(dest, text, "utf8");
5503
+ await fs13.mkdir(containerConfigsDir(home), { recursive: true });
5504
+ await fs13.writeFile(dest, text, "utf8");
5170
5505
  if (promptedIdentity?.prompted) {
5171
5506
  const { name, email, scope } = promptedIdentity.prompted;
5172
5507
  if (scope === "g" || scope === "b") {
@@ -5196,11 +5531,11 @@ async function runInit(opts) {
5196
5531
  }
5197
5532
  if (scope === "c" || scope === "b") {
5198
5533
  try {
5199
- const written = await fs12.readFile(dest, "utf8");
5534
+ const written = await fs13.readFile(dest, "utf8");
5200
5535
  const parsed = parseConfig(written, dest);
5201
5536
  const changed = setContainerGitUserInDoc(parsed.doc, { name, email });
5202
5537
  if (changed) {
5203
- await fs12.writeFile(dest, stringifyConfig(parsed.doc), "utf8");
5538
+ await fs13.writeFile(dest, stringifyConfig(parsed.doc), "utf8");
5204
5539
  logger.info(
5205
5540
  `Saved identity in ${prettyPath(dest)} (container-level git.user).`
5206
5541
  );
@@ -5230,7 +5565,7 @@ async function runInit(opts) {
5230
5565
  }
5231
5566
 
5232
5567
  // src/commands/init.ts
5233
- var initCommand = defineCommand10({
5568
+ var initCommand = defineCommand11({
5234
5569
  meta: {
5235
5570
  name: "init",
5236
5571
  group: "lifecycle",
@@ -5349,7 +5684,7 @@ function collectWithList(withArg, rawArgs) {
5349
5684
  }
5350
5685
 
5351
5686
  // src/commands/list-components.ts
5352
- import { defineCommand as defineCommand11 } from "citty";
5687
+ import { defineCommand as defineCommand12 } from "citty";
5353
5688
  import { consola as consola15 } from "consola";
5354
5689
  var CATEGORY_LABELS = {
5355
5690
  language: "Languages",
@@ -5361,7 +5696,7 @@ var CATEGORY_ORDER = [
5361
5696
  "service",
5362
5697
  "feature"
5363
5698
  ];
5364
- var listComponentsCommand = defineCommand11({
5699
+ var listComponentsCommand = defineCommand12({
5365
5700
  meta: {
5366
5701
  name: "list-components",
5367
5702
  group: "discovery",
@@ -5430,8 +5765,8 @@ var listComponentsCommand = defineCommand11({
5430
5765
  });
5431
5766
 
5432
5767
  // src/commands/logs.ts
5433
- import { defineCommand as defineCommand12 } from "citty";
5434
- var logsCommand = defineCommand12({
5768
+ import { defineCommand as defineCommand13 } from "citty";
5769
+ var logsCommand = defineCommand13({
5435
5770
  meta: {
5436
5771
  name: "logs",
5437
5772
  group: "run",
@@ -5466,7 +5801,7 @@ var logsCommand = defineCommand12({
5466
5801
  });
5467
5802
 
5468
5803
  // src/commands/port.ts
5469
- import { defineCommand as defineCommand13 } from "citty";
5804
+ import { defineCommand as defineCommand14 } from "citty";
5470
5805
  import { consola as consola16 } from "consola";
5471
5806
  async function runPortListing(opts) {
5472
5807
  const out = opts.out ?? process.stdout;
@@ -5518,7 +5853,7 @@ async function runPortListing(opts) {
5518
5853
  }
5519
5854
  return 0;
5520
5855
  }
5521
- var portCommand = defineCommand13({
5856
+ var portCommand = defineCommand14({
5522
5857
  meta: {
5523
5858
  name: "port",
5524
5859
  group: "discovery",
@@ -5543,9 +5878,9 @@ var portCommand = defineCommand13({
5543
5878
  });
5544
5879
 
5545
5880
  // src/commands/remove-apt-packages.ts
5546
- import { defineCommand as defineCommand14 } from "citty";
5881
+ import { defineCommand as defineCommand15 } from "citty";
5547
5882
  import { consola as consola17 } from "consola";
5548
- var removeAptPackagesCommand = defineCommand14({
5883
+ var removeAptPackagesCommand = defineCommand15({
5549
5884
  meta: {
5550
5885
  name: "remove-apt-packages",
5551
5886
  group: "edit",
@@ -5587,9 +5922,9 @@ var removeAptPackagesCommand = defineCommand14({
5587
5922
  });
5588
5923
 
5589
5924
  // src/commands/remove-feature.ts
5590
- import { defineCommand as defineCommand15 } from "citty";
5925
+ import { defineCommand as defineCommand16 } from "citty";
5591
5926
  import { consola as consola18 } from "consola";
5592
- var removeFeatureCommand = defineCommand15({
5927
+ var removeFeatureCommand = defineCommand16({
5593
5928
  meta: {
5594
5929
  name: "remove-feature",
5595
5930
  group: "edit",
@@ -5629,13 +5964,13 @@ var removeFeatureCommand = defineCommand15({
5629
5964
  });
5630
5965
 
5631
5966
  // src/commands/remove.ts
5632
- import { defineCommand as defineCommand16 } from "citty";
5967
+ import { defineCommand as defineCommand17 } from "citty";
5633
5968
  import { consola as consola20 } from "consola";
5634
5969
  import { createInterface } from "readline/promises";
5635
5970
 
5636
5971
  // src/remove/index.ts
5637
- import { existsSync as existsSync8, promises as fs13 } from "fs";
5638
- import path13 from "path";
5972
+ import { existsSync as existsSync9, promises as fs14 } from "fs";
5973
+ import path14 from "path";
5639
5974
  import { consola as consola19 } from "consola";
5640
5975
  async function runRemove(opts) {
5641
5976
  const home = opts.monocerosHome ?? monocerosHome();
@@ -5651,8 +5986,8 @@ async function runRemove(opts) {
5651
5986
  }
5652
5987
  const ymlPath = containerConfigPath(opts.name, home);
5653
5988
  const containerPath = containerDir(opts.name, home);
5654
- const hasYml = existsSync8(ymlPath);
5655
- const hasContainer = existsSync8(containerPath);
5989
+ const hasYml = existsSync9(ymlPath);
5990
+ const hasContainer = existsSync9(containerPath);
5656
5991
  if (!hasYml && !hasContainer) {
5657
5992
  throw new Error(
5658
5993
  `Nothing to remove for '${opts.name}': neither ${ymlPath} nor ${containerPath} exists.`
@@ -5686,23 +6021,23 @@ async function runRemove(opts) {
5686
6021
  let backupPath = null;
5687
6022
  if (!opts.noBackup && (hasYml || hasContainer)) {
5688
6023
  const ts = (opts.now ?? /* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
5689
- backupPath = path13.join(home, "container-backups", `${opts.name}-${ts}`);
5690
- await fs13.mkdir(backupPath, { recursive: true });
6024
+ backupPath = path14.join(home, "container-backups", `${opts.name}-${ts}`);
6025
+ await fs14.mkdir(backupPath, { recursive: true });
5691
6026
  if (hasYml) {
5692
- await fs13.copyFile(ymlPath, path13.join(backupPath, `${opts.name}.yml`));
6027
+ await fs14.copyFile(ymlPath, path14.join(backupPath, `${opts.name}.yml`));
5693
6028
  }
5694
6029
  if (hasContainer) {
5695
- await fs13.cp(containerPath, path13.join(backupPath, "container"), {
6030
+ await fs14.cp(containerPath, path14.join(backupPath, "container"), {
5696
6031
  recursive: true
5697
6032
  });
5698
6033
  }
5699
6034
  logger.info(`Backup written to ${prettyPath(backupPath)}.`);
5700
6035
  }
5701
6036
  if (hasYml) {
5702
- await fs13.rm(ymlPath, { force: true });
6037
+ await fs14.rm(ymlPath, { force: true });
5703
6038
  }
5704
6039
  if (hasContainer) {
5705
- await fs13.rm(containerPath, { recursive: true, force: true });
6040
+ await fs14.rm(containerPath, { recursive: true, force: true });
5706
6041
  }
5707
6042
  logger.success(
5708
6043
  `Removed '${opts.name}': docker objects gone, container-configs entry deleted, container directory deleted.`
@@ -5739,7 +6074,7 @@ async function runRemove(opts) {
5739
6074
  }
5740
6075
 
5741
6076
  // src/commands/remove.ts
5742
- var removeCommand = defineCommand16({
6077
+ var removeCommand = defineCommand17({
5743
6078
  meta: {
5744
6079
  name: "remove",
5745
6080
  group: "lifecycle",
@@ -5800,12 +6135,12 @@ var removeCommand = defineCommand16({
5800
6135
  });
5801
6136
 
5802
6137
  // src/commands/restore.ts
5803
- import { defineCommand as defineCommand17 } from "citty";
6138
+ import { defineCommand as defineCommand18 } from "citty";
5804
6139
  import { consola as consola22 } from "consola";
5805
6140
 
5806
6141
  // src/restore/index.ts
5807
- import { existsSync as existsSync9, promises as fs14 } from "fs";
5808
- import path14 from "path";
6142
+ import { existsSync as existsSync10, promises as fs15 } from "fs";
6143
+ import path15 from "path";
5809
6144
  import { consola as consola21 } from "consola";
5810
6145
  async function runRestore(opts) {
5811
6146
  const home = opts.monocerosHome ?? monocerosHome();
@@ -5813,15 +6148,15 @@ async function runRestore(opts) {
5813
6148
  info: (msg) => consola21.info(msg),
5814
6149
  success: (msg) => consola21.success(msg)
5815
6150
  };
5816
- const backup = path14.resolve(opts.backupPath);
5817
- if (!existsSync9(backup)) {
6151
+ const backup = path15.resolve(opts.backupPath);
6152
+ if (!existsSync10(backup)) {
5818
6153
  throw new Error(`Backup not found: ${backup}.`);
5819
6154
  }
5820
- const stat = await fs14.stat(backup);
6155
+ const stat = await fs15.stat(backup);
5821
6156
  if (!stat.isDirectory()) {
5822
6157
  throw new Error(`Backup path is not a directory: ${backup}.`);
5823
6158
  }
5824
- const entries = await fs14.readdir(backup);
6159
+ const entries = await fs15.readdir(backup);
5825
6160
  const ymlFiles = entries.filter((f) => f.endsWith(".yml"));
5826
6161
  if (ymlFiles.length === 0) {
5827
6162
  throw new Error(
@@ -5835,24 +6170,24 @@ async function runRestore(opts) {
5835
6170
  }
5836
6171
  const ymlFile = ymlFiles[0];
5837
6172
  const name = ymlFile.replace(/\.yml$/, "");
5838
- const containerInBackup = path14.join(backup, "container");
5839
- const hasContainer = existsSync9(containerInBackup);
6173
+ const containerInBackup = path15.join(backup, "container");
6174
+ const hasContainer = existsSync10(containerInBackup);
5840
6175
  const destYml = containerConfigPath(name, home);
5841
6176
  const destContainer = containerDir(name, home);
5842
- if (existsSync9(destYml)) {
6177
+ if (existsSync10(destYml)) {
5843
6178
  throw new Error(
5844
6179
  `Refusing to restore: ${destYml} already exists. Remove the current container first (\`monoceros remove ${name}\`) or rename the existing config.`
5845
6180
  );
5846
6181
  }
5847
- if (hasContainer && existsSync9(destContainer)) {
6182
+ if (hasContainer && existsSync10(destContainer)) {
5848
6183
  throw new Error(
5849
6184
  `Refusing to restore: ${destContainer} already exists. Remove the current container first (\`monoceros remove ${name}\`).`
5850
6185
  );
5851
6186
  }
5852
- await fs14.mkdir(containerConfigsDir(home), { recursive: true });
5853
- await fs14.copyFile(path14.join(backup, ymlFile), destYml);
6187
+ await fs15.mkdir(containerConfigsDir(home), { recursive: true });
6188
+ await fs15.copyFile(path15.join(backup, ymlFile), destYml);
5854
6189
  if (hasContainer) {
5855
- await fs14.cp(containerInBackup, destContainer, { recursive: true });
6190
+ await fs15.cp(containerInBackup, destContainer, { recursive: true });
5856
6191
  }
5857
6192
  logger.success(`Restored '${name}' from ${prettyPath(backup)}.`);
5858
6193
  logger.info(
@@ -5866,7 +6201,7 @@ async function runRestore(opts) {
5866
6201
  }
5867
6202
 
5868
6203
  // src/commands/restore.ts
5869
- var restoreCommand = defineCommand17({
6204
+ var restoreCommand = defineCommand18({
5870
6205
  meta: {
5871
6206
  name: "restore",
5872
6207
  group: "lifecycle",
@@ -5890,9 +6225,9 @@ var restoreCommand = defineCommand17({
5890
6225
  });
5891
6226
 
5892
6227
  // src/commands/remove-from-url.ts
5893
- import { defineCommand as defineCommand18 } from "citty";
6228
+ import { defineCommand as defineCommand19 } from "citty";
5894
6229
  import { consola as consola23 } from "consola";
5895
- var removeFromUrlCommand = defineCommand18({
6230
+ var removeFromUrlCommand = defineCommand19({
5896
6231
  meta: {
5897
6232
  name: "remove-from-url",
5898
6233
  group: "edit",
@@ -5932,9 +6267,9 @@ var removeFromUrlCommand = defineCommand18({
5932
6267
  });
5933
6268
 
5934
6269
  // src/commands/remove-language.ts
5935
- import { defineCommand as defineCommand19 } from "citty";
6270
+ import { defineCommand as defineCommand20 } from "citty";
5936
6271
  import { consola as consola24 } from "consola";
5937
- var removeLanguageCommand = defineCommand19({
6272
+ var removeLanguageCommand = defineCommand20({
5938
6273
  meta: {
5939
6274
  name: "remove-language",
5940
6275
  group: "edit",
@@ -5974,9 +6309,9 @@ var removeLanguageCommand = defineCommand19({
5974
6309
  });
5975
6310
 
5976
6311
  // src/commands/remove-port.ts
5977
- import { defineCommand as defineCommand20 } from "citty";
6312
+ import { defineCommand as defineCommand21 } from "citty";
5978
6313
  import { consola as consola25 } from "consola";
5979
- var removePortCommand = defineCommand20({
6314
+ var removePortCommand = defineCommand21({
5980
6315
  meta: {
5981
6316
  name: "remove-port",
5982
6317
  group: "edit",
@@ -6022,9 +6357,9 @@ function coerceToken2(raw) {
6022
6357
  }
6023
6358
 
6024
6359
  // src/commands/remove-repo.ts
6025
- import { defineCommand as defineCommand21 } from "citty";
6360
+ import { defineCommand as defineCommand22 } from "citty";
6026
6361
  import { consola as consola26 } from "consola";
6027
- var removeRepoCommand = defineCommand21({
6362
+ var removeRepoCommand = defineCommand22({
6028
6363
  meta: {
6029
6364
  name: "remove-repo",
6030
6365
  group: "edit",
@@ -6064,9 +6399,9 @@ var removeRepoCommand = defineCommand21({
6064
6399
  });
6065
6400
 
6066
6401
  // src/commands/remove-service.ts
6067
- import { defineCommand as defineCommand22 } from "citty";
6402
+ import { defineCommand as defineCommand23 } from "citty";
6068
6403
  import { consola as consola27 } from "consola";
6069
- var removeServiceCommand = defineCommand22({
6404
+ var removeServiceCommand = defineCommand23({
6070
6405
  meta: {
6071
6406
  name: "remove-service",
6072
6407
  group: "edit",
@@ -6106,12 +6441,12 @@ var removeServiceCommand = defineCommand22({
6106
6441
  });
6107
6442
 
6108
6443
  // src/commands/run.ts
6109
- import { defineCommand as defineCommand23 } from "citty";
6444
+ import { defineCommand as defineCommand24 } from "citty";
6110
6445
  import { consola as consola28 } from "consola";
6111
6446
 
6112
6447
  // src/devcontainer/shell.ts
6113
- import { existsSync as existsSync10 } from "fs";
6114
- import path15 from "path";
6448
+ import { existsSync as existsSync11 } from "fs";
6449
+ import path16 from "path";
6115
6450
  async function runShell(opts) {
6116
6451
  assertContainerExists(opts.root);
6117
6452
  const spawnFn = opts.spawn ?? spawnDevcontainer;
@@ -6134,7 +6469,7 @@ async function runShell(opts) {
6134
6469
  );
6135
6470
  }
6136
6471
  function assertContainerExists(root) {
6137
- if (!existsSync10(path15.join(root, ".devcontainer"))) {
6472
+ if (!existsSync11(path16.join(root, ".devcontainer"))) {
6138
6473
  throw new Error(
6139
6474
  `No .devcontainer/ at ${root}. Run \`monoceros apply <name>\` first.`
6140
6475
  );
@@ -6170,7 +6505,7 @@ async function runInContainer(opts) {
6170
6505
  }
6171
6506
 
6172
6507
  // src/commands/run.ts
6173
- var runCommand = defineCommand23({
6508
+ var runCommand = defineCommand24({
6174
6509
  meta: {
6175
6510
  name: "run",
6176
6511
  group: "run",
@@ -6205,9 +6540,9 @@ var runCommand = defineCommand23({
6205
6540
  });
6206
6541
 
6207
6542
  // src/commands/shell.ts
6208
- import { defineCommand as defineCommand24 } from "citty";
6543
+ import { defineCommand as defineCommand25 } from "citty";
6209
6544
  import { consola as consola29 } from "consola";
6210
- var shellCommand = defineCommand24({
6545
+ var shellCommand = defineCommand25({
6211
6546
  meta: {
6212
6547
  name: "shell",
6213
6548
  group: "run",
@@ -6232,9 +6567,9 @@ var shellCommand = defineCommand24({
6232
6567
  });
6233
6568
 
6234
6569
  // src/commands/start.ts
6235
- import { defineCommand as defineCommand25 } from "citty";
6570
+ import { defineCommand as defineCommand26 } from "citty";
6236
6571
  import { consola as consola30 } from "consola";
6237
- var startCommand = defineCommand25({
6572
+ var startCommand = defineCommand26({
6238
6573
  meta: {
6239
6574
  name: "start",
6240
6575
  group: "run",
@@ -6273,8 +6608,8 @@ var startCommand = defineCommand25({
6273
6608
  });
6274
6609
 
6275
6610
  // src/commands/status.ts
6276
- import { defineCommand as defineCommand26 } from "citty";
6277
- var statusCommand = defineCommand26({
6611
+ import { defineCommand as defineCommand27 } from "citty";
6612
+ var statusCommand = defineCommand27({
6278
6613
  meta: {
6279
6614
  name: "status",
6280
6615
  group: "run",
@@ -6302,9 +6637,9 @@ var statusCommand = defineCommand26({
6302
6637
  });
6303
6638
 
6304
6639
  // src/commands/stop.ts
6305
- import { defineCommand as defineCommand27 } from "citty";
6640
+ import { defineCommand as defineCommand28 } from "citty";
6306
6641
  import { consola as consola31 } from "consola";
6307
- var stopCommand = defineCommand27({
6642
+ var stopCommand = defineCommand28({
6308
6643
  meta: {
6309
6644
  name: "stop",
6310
6645
  group: "run",
@@ -6342,7 +6677,7 @@ var stopCommand = defineCommand27({
6342
6677
  });
6343
6678
 
6344
6679
  // src/main.ts
6345
- var main = defineCommand28({
6680
+ var main = defineCommand29({
6346
6681
  meta: {
6347
6682
  name: "monoceros",
6348
6683
  version: CLI_VERSION,
@@ -6375,7 +6710,8 @@ var main = defineCommand28({
6375
6710
  "remove-repo": removeRepoCommand,
6376
6711
  "remove-port": removePortCommand,
6377
6712
  port: portCommand,
6378
- completion: completionCommand
6713
+ completion: completionCommand,
6714
+ __complete: __completeCommand
6379
6715
  }
6380
6716
  });
6381
6717