forgemap 0.5.0-dev.89-5f2e4a1 → 0.5.0-dev.90-1fa1595

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.
@@ -1335,146 +1335,6 @@ async function installRcBlock(shell, label, lines, legacyLabels = []) {
1335
1335
  };
1336
1336
  }
1337
1337
  //#endregion
1338
- //#region src/commands/completion.ts
1339
- var SUBCOMMANDS = [
1340
- "clone",
1341
- "import",
1342
- "cleanup",
1343
- "delete",
1344
- "cd",
1345
- "path",
1346
- "open",
1347
- "search",
1348
- "pick",
1349
- "status",
1350
- "sync",
1351
- "validate",
1352
- "shell-init",
1353
- "completion",
1354
- "config"
1355
- ];
1356
- var SLUG_COMMANDS = [
1357
- "clone",
1358
- "cd",
1359
- "path",
1360
- "open",
1361
- "search",
1362
- "pick",
1363
- "delete"
1364
- ];
1365
- function renderBash() {
1366
- return `# forgemap bash completion — drop into your ~/.bashrc:
1367
- # eval "$(forgemap completion bash)"
1368
- _forgemap_completion() {
1369
- local cur prev cmd words
1370
- COMPREPLY=()
1371
- cur="\${COMP_WORDS[COMP_CWORD]}"
1372
- cmd="\${COMP_WORDS[1]}"
1373
-
1374
- if [ "$COMP_CWORD" = "1" ]; then
1375
- COMPREPLY=( $(compgen -W "${SUBCOMMANDS.join(" ")}" -- "$cur") )
1376
- return
1377
- fi
1378
-
1379
- case "$cmd" in
1380
- ${SLUG_COMMANDS.join("|")})
1381
- local slugs
1382
- slugs=$(forgemap search '' --format slug 2>/dev/null)
1383
- COMPREPLY=( $(compgen -W "$slugs" -- "$cur") )
1384
- ;;
1385
- esac
1386
- }
1387
- complete -F _forgemap_completion forgemap
1388
- `;
1389
- }
1390
- function renderZsh() {
1391
- return `# forgemap zsh completion — drop into your ~/.zshrc:
1392
- # eval "$(forgemap completion zsh)"
1393
- _forgemap() {
1394
- local context state line
1395
- local -a subcommands slug_cmds
1396
- subcommands=(${SUBCOMMANDS.map((s) => `'${s}'`).join(" ")})
1397
- slug_cmds=(${SLUG_COMMANDS.map((s) => `'${s}'`).join(" ")})
1398
-
1399
- _arguments -C \\
1400
- '1: :->cmd' \\
1401
- '*::arg:->args'
1402
-
1403
- case "$state" in
1404
- cmd) _describe 'forgemap subcommand' subcommands ;;
1405
- args)
1406
- if (( $slug_cmds[(I)$words[1]] )); then
1407
- local -a slugs
1408
- slugs=("\${(@f)$(forgemap search '' --format slug 2>/dev/null)}")
1409
- _describe 'slug' slugs
1410
- fi
1411
- ;;
1412
- esac
1413
- }
1414
- compdef _forgemap forgemap
1415
- `;
1416
- }
1417
- function renderFish$1() {
1418
- const slugCmdsList = SLUG_COMMANDS.map((s) => `"${s}"`).join(" ");
1419
- return `# forgemap fish completion — drop into your ~/.config/fish/config.fish:
1420
- # forgemap completion fish | source
1421
-
1422
- # Subcommands (depth 1).
1423
- complete -c forgemap -f -n '__fish_use_subcommand' -a '${SUBCOMMANDS.join(" ")}'
1424
-
1425
- # Slugs (depth 2) for commands that take one.
1426
- function __forgemap_needs_slug
1427
- set -l tokens (commandline -opc)
1428
- set -l slug_cmds ${slugCmdsList}
1429
- if test (count $tokens) -ge 2; and contains $tokens[2] $slug_cmds
1430
- return 0
1431
- end
1432
- return 1
1433
- end
1434
-
1435
- complete -c forgemap -f -n '__forgemap_needs_slug' \\
1436
- -a '(forgemap search "" --format slug 2>/dev/null)'
1437
- `;
1438
- }
1439
- var completionCommand = defineCommand({
1440
- meta: {
1441
- name: "completion",
1442
- description: "Print a shell completion script. Source via `eval \"$(forgemap completion)\"`."
1443
- },
1444
- args: {
1445
- shell: {
1446
- type: "positional",
1447
- description: `Shell flavor (${SUPPORTED_SHELLS.join(", ")}). Auto-detected from $SHELL if omitted.`,
1448
- required: false
1449
- },
1450
- install: {
1451
- type: "boolean",
1452
- description: "Append the completion loader to your shell's rc file (idempotent) instead of printing",
1453
- default: false
1454
- }
1455
- },
1456
- async run({ args }) {
1457
- const requested = args.shell ?? detectShell();
1458
- if (!SUPPORTED_SHELLS.includes(requested)) {
1459
- consola.error(`Unsupported shell "${requested}". Supported: ${SUPPORTED_SHELLS.join(", ")}.`);
1460
- process.exitCode = 1;
1461
- return;
1462
- }
1463
- if (args.install) {
1464
- const { status, rcFile } = await installRcBlock(requested, "completion", [requested === "fish" ? "forgemap completion fish | source" : `eval "$(forgemap completion ${requested})"`]);
1465
- if (status === "present") consola.info(`forgemap completion already present in ${rcFile}.`);
1466
- else {
1467
- const verb = status === "updated" ? "Updated" : "Added";
1468
- consola.success(`${verb} forgemap completion in ${rcFile}.`);
1469
- consola.info(`Run \`source ${rcFile}\` or restart your shell to activate it.`);
1470
- }
1471
- return;
1472
- }
1473
- const out = requested === "fish" ? renderFish$1() : requested === "zsh" ? renderZsh() : renderBash();
1474
- process.stdout.write(out);
1475
- }
1476
- });
1477
- //#endregion
1478
1338
  //#region src/config/write.ts
1479
1339
  var HEADER = `/**
1480
1340
  * forgemap configuration.
@@ -2419,7 +2279,7 @@ var infoCommand = defineCommand({
2419
2279
  async run({ args }) {
2420
2280
  const binary = resolveBinary(process.argv[1]);
2421
2281
  const info = {
2422
- version: "0.5.0-dev.89-5f2e4a1",
2282
+ version: "0.5.0-dev.90-1fa1595",
2423
2283
  build: detectBuild(binary.resolved),
2424
2284
  binary,
2425
2285
  node: process.version,
@@ -2433,9 +2293,74 @@ var infoCommand = defineCommand({
2433
2293
  }
2434
2294
  });
2435
2295
  //#endregion
2296
+ //#region src/repos/filter.ts
2297
+ var FLAG = "--filter";
2298
+ /**
2299
+ * Shared `--filter` option for the commands that enumerate repos
2300
+ * (`status`, `sync`, `list`), so the flag reads identically everywhere.
2301
+ */
2302
+ var filterArg = {
2303
+ type: "string",
2304
+ description: "Restrict to repos whose owner or forge name matches. Repeatable; a repo passes if it matches any value."
2305
+ };
2306
+ /**
2307
+ * Recover every `--filter` occurrence from the raw argv.
2308
+ *
2309
+ * citty (0.2.2) parses through `node:util` `parseArgs` and never sets
2310
+ * `multiple: true`, so Node keeps only the **last** value of a repeated option:
2311
+ * `--filter a --filter b` reaches `args.filter` as `'b'`, silently dropping
2312
+ * `a`. The raw argv is the only place the full list survives.
2313
+ */
2314
+ function collectFilterArgs(rawArgs) {
2315
+ const values = [];
2316
+ for (let i = 0; i < rawArgs.length; i++) {
2317
+ const arg = rawArgs[i];
2318
+ if (arg === "--") break;
2319
+ if (arg === FLAG) {
2320
+ const next = rawArgs[i + 1];
2321
+ if (next !== void 0 && !next.startsWith("-")) {
2322
+ values.push(next);
2323
+ i++;
2324
+ }
2325
+ continue;
2326
+ }
2327
+ if (arg.startsWith(`${FLAG}=`)) values.push(arg.slice(9));
2328
+ }
2329
+ return values;
2330
+ }
2331
+ /**
2332
+ * Normalize the shapes a filter value arrives in — absent, a single string, or
2333
+ * a list — into a list, dropping blanks (`--filter ''`).
2334
+ */
2335
+ function normalizeFilters(value) {
2336
+ if (value === void 0) return [];
2337
+ return (Array.isArray(value) ? value : [value]).map((v) => v.trim()).filter((v) => v.length > 0);
2338
+ }
2339
+ /**
2340
+ * The filter values for a run: the raw argv wins, since it is the only shape
2341
+ * that survives repetition. Fall back to the parsed value when the argv carries
2342
+ * no `--filter` at all, which is how the command is driven programmatically
2343
+ * (and in tests), where `rawArgs` may be empty.
2344
+ */
2345
+ function resolveFilters(rawArgs, value) {
2346
+ const fromRawArgs = collectFilterArgs(rawArgs);
2347
+ return normalizeFilters(fromRawArgs.length > 0 ? fromRawArgs : value);
2348
+ }
2349
+ /**
2350
+ * Keep the repos matching any of `filters` (OR-combined). A value matches when
2351
+ * it equals the repo's owner or its forge name, compared case-insensitively —
2352
+ * forge and owner names are case-preserving but not case-significant. An empty
2353
+ * filter list is a no-op, so an unused flag never narrows the output.
2354
+ */
2355
+ function filterRepos(repos, filters) {
2356
+ if (filters.length === 0) return repos;
2357
+ const wanted = new Set(filters.map((f) => f.toLowerCase()));
2358
+ return repos.filter((r) => wanted.has(r.owner.toLowerCase()) || wanted.has(r.forgeName.toLowerCase()));
2359
+ }
2360
+ //#endregion
2436
2361
  //#region src/repos/match.ts
2437
2362
  /**
2438
- * The one Fuse configuration every fuzzy lookup shares — `search`, `pick`
2363
+ * The one Fuse configuration every fuzzy lookup shares — `list`, `pick`
2439
2364
  * and the fuzzy slug fallback in `path`/`open`. Keeping it in one place is
2440
2365
  * what makes a query rank identically no matter which command runs it.
2441
2366
  */
@@ -2456,6 +2381,88 @@ function matchRepos(repos, query, limit) {
2456
2381
  return createRepoFuse(repos).search(query, limit ? { limit } : void 0).map((r) => r.item);
2457
2382
  }
2458
2383
  //#endregion
2384
+ //#region src/commands/list.ts
2385
+ function renderTree$1(repos) {
2386
+ const byForge = /* @__PURE__ */ new Map();
2387
+ for (const r of repos) {
2388
+ let owners = byForge.get(r.forgeName);
2389
+ if (!owners) {
2390
+ owners = /* @__PURE__ */ new Map();
2391
+ byForge.set(r.forgeName, owners);
2392
+ }
2393
+ const list = owners.get(r.owner);
2394
+ if (list) list.push(r);
2395
+ else owners.set(r.owner, [r]);
2396
+ }
2397
+ return formatTree(Array.from(byForge, ([forge, owners]) => ({
2398
+ text: colors.bold(forge),
2399
+ children: Array.from(owners, ([owner, items]) => ({
2400
+ text: owner,
2401
+ children: items.map((r) => ({ text: `${colors.cyan(r.repo)} ${colors.dim(r.localPath)}` }))
2402
+ }))
2403
+ })));
2404
+ }
2405
+ var listCommand = defineCommand({
2406
+ meta: {
2407
+ name: "list",
2408
+ description: "List cloned repos; with a query, fuzzy-match by owner/repo and print matches"
2409
+ },
2410
+ args: {
2411
+ query: {
2412
+ type: "positional",
2413
+ description: "Optional search term (matched fuzzily against <owner>/<repo>). Omit to list every repo.",
2414
+ required: false
2415
+ },
2416
+ format: {
2417
+ type: "string",
2418
+ description: "Output format: auto (default), pretty, path, or slug. auto picks pretty in a TTY, path when piped.",
2419
+ default: "auto"
2420
+ },
2421
+ filter: filterArg,
2422
+ limit: {
2423
+ type: "string",
2424
+ description: "Maximum number of matches to print (default: unlimited)"
2425
+ },
2426
+ config: {
2427
+ type: "string",
2428
+ description: "Path to forgemap.config.ts (overrides walk-up discovery)"
2429
+ }
2430
+ },
2431
+ async run({ args, rawArgs }) {
2432
+ const loaded = await loadForgeMapConfig({ configFile: args.config });
2433
+ const configDir = loaded.configFile ? dirname(loaded.configFile) : loaded.cwd;
2434
+ const repos = filterRepos(await scanRepos({
2435
+ config: loaded.config,
2436
+ configDir
2437
+ }), resolveFilters(rawArgs, args.filter));
2438
+ const limit = args.limit ? Number.parseInt(args.limit, 10) : void 0;
2439
+ const query = args.query ?? "";
2440
+ const items = matchRepos(repos, query, limit);
2441
+ const allowed = [
2442
+ "auto",
2443
+ "pretty",
2444
+ "path",
2445
+ "slug"
2446
+ ];
2447
+ if (!allowed.includes(args.format)) {
2448
+ consola.error(`Invalid --format value "${args.format}". Allowed: ${allowed.join(", ")}.`);
2449
+ process.exitCode = 1;
2450
+ return;
2451
+ }
2452
+ const requested = args.format;
2453
+ const format = requested === "auto" ? process.stdout.isTTY ? "pretty" : "path" : requested;
2454
+ if (items.length === 0) {
2455
+ if (format === "pretty") consola.info(query ? `No matches for "${query}".` : "No repos found.");
2456
+ return;
2457
+ }
2458
+ if (format === "pretty") {
2459
+ process.stdout.write(`${renderTree$1(items)}\n`);
2460
+ return;
2461
+ }
2462
+ for (const item of items) process.stdout.write(`${format === "slug" ? item.slug : item.localPath}\n`);
2463
+ }
2464
+ });
2465
+ //#endregion
2459
2466
  //#region src/repos/picker.ts
2460
2467
  /**
2461
2468
  * Show the interactive repo picker and return the chosen local path
@@ -2712,7 +2719,7 @@ var pickCommand = defineCommand({
2712
2719
  return;
2713
2720
  }
2714
2721
  if (!canPrompt()) {
2715
- consola.error("pick requires an interactive terminal. Use `forgemap search` for non-interactive output.");
2722
+ consola.error("pick requires an interactive terminal. Use `forgemap list` for non-interactive output.");
2716
2723
  process.exitCode = 1;
2717
2724
  return;
2718
2725
  }
@@ -2721,152 +2728,6 @@ var pickCommand = defineCommand({
2721
2728
  }
2722
2729
  });
2723
2730
  //#endregion
2724
- //#region src/repos/filter.ts
2725
- var FLAG = "--filter";
2726
- /**
2727
- * Shared `--filter` option for the commands that enumerate repos
2728
- * (`status`, `sync`, `search`), so the flag reads identically everywhere.
2729
- */
2730
- var filterArg = {
2731
- type: "string",
2732
- description: "Restrict to repos whose owner or forge name matches. Repeatable; a repo passes if it matches any value."
2733
- };
2734
- /**
2735
- * Recover every `--filter` occurrence from the raw argv.
2736
- *
2737
- * citty (0.2.2) parses through `node:util` `parseArgs` and never sets
2738
- * `multiple: true`, so Node keeps only the **last** value of a repeated option:
2739
- * `--filter a --filter b` reaches `args.filter` as `'b'`, silently dropping
2740
- * `a`. The raw argv is the only place the full list survives.
2741
- */
2742
- function collectFilterArgs(rawArgs) {
2743
- const values = [];
2744
- for (let i = 0; i < rawArgs.length; i++) {
2745
- const arg = rawArgs[i];
2746
- if (arg === "--") break;
2747
- if (arg === FLAG) {
2748
- const next = rawArgs[i + 1];
2749
- if (next !== void 0 && !next.startsWith("-")) {
2750
- values.push(next);
2751
- i++;
2752
- }
2753
- continue;
2754
- }
2755
- if (arg.startsWith(`${FLAG}=`)) values.push(arg.slice(9));
2756
- }
2757
- return values;
2758
- }
2759
- /**
2760
- * Normalize the shapes a filter value arrives in — absent, a single string, or
2761
- * a list — into a list, dropping blanks (`--filter ''`).
2762
- */
2763
- function normalizeFilters(value) {
2764
- if (value === void 0) return [];
2765
- return (Array.isArray(value) ? value : [value]).map((v) => v.trim()).filter((v) => v.length > 0);
2766
- }
2767
- /**
2768
- * The filter values for a run: the raw argv wins, since it is the only shape
2769
- * that survives repetition. Fall back to the parsed value when the argv carries
2770
- * no `--filter` at all, which is how the command is driven programmatically
2771
- * (and in tests), where `rawArgs` may be empty.
2772
- */
2773
- function resolveFilters(rawArgs, value) {
2774
- const fromRawArgs = collectFilterArgs(rawArgs);
2775
- return normalizeFilters(fromRawArgs.length > 0 ? fromRawArgs : value);
2776
- }
2777
- /**
2778
- * Keep the repos matching any of `filters` (OR-combined). A value matches when
2779
- * it equals the repo's owner or its forge name, compared case-insensitively —
2780
- * forge and owner names are case-preserving but not case-significant. An empty
2781
- * filter list is a no-op, so an unused flag never narrows the output.
2782
- */
2783
- function filterRepos(repos, filters) {
2784
- if (filters.length === 0) return repos;
2785
- const wanted = new Set(filters.map((f) => f.toLowerCase()));
2786
- return repos.filter((r) => wanted.has(r.owner.toLowerCase()) || wanted.has(r.forgeName.toLowerCase()));
2787
- }
2788
- //#endregion
2789
- //#region src/commands/search.ts
2790
- function renderTree$1(repos) {
2791
- const byForge = /* @__PURE__ */ new Map();
2792
- for (const r of repos) {
2793
- let owners = byForge.get(r.forgeName);
2794
- if (!owners) {
2795
- owners = /* @__PURE__ */ new Map();
2796
- byForge.set(r.forgeName, owners);
2797
- }
2798
- const list = owners.get(r.owner);
2799
- if (list) list.push(r);
2800
- else owners.set(r.owner, [r]);
2801
- }
2802
- return formatTree(Array.from(byForge, ([forge, owners]) => ({
2803
- text: colors.bold(forge),
2804
- children: Array.from(owners, ([owner, items]) => ({
2805
- text: owner,
2806
- children: items.map((r) => ({ text: `${colors.cyan(r.repo)} ${colors.dim(r.localPath)}` }))
2807
- }))
2808
- })));
2809
- }
2810
- var searchCommand = defineCommand({
2811
- meta: {
2812
- name: "search",
2813
- description: "Fuzzy-search cloned repos by owner/repo and print matching repos"
2814
- },
2815
- args: {
2816
- query: {
2817
- type: "positional",
2818
- description: "Search term (matched fuzzily against <owner>/<repo>)",
2819
- required: true
2820
- },
2821
- format: {
2822
- type: "string",
2823
- description: "Output format: auto (default), pretty, path, or slug. auto picks pretty in a TTY, path when piped.",
2824
- default: "auto"
2825
- },
2826
- filter: filterArg,
2827
- limit: {
2828
- type: "string",
2829
- description: "Maximum number of matches to print (default: unlimited)"
2830
- },
2831
- config: {
2832
- type: "string",
2833
- description: "Path to forgemap.config.ts (overrides walk-up discovery)"
2834
- }
2835
- },
2836
- async run({ args, rawArgs }) {
2837
- const loaded = await loadForgeMapConfig({ configFile: args.config });
2838
- const configDir = loaded.configFile ? dirname(loaded.configFile) : loaded.cwd;
2839
- const repos = filterRepos(await scanRepos({
2840
- config: loaded.config,
2841
- configDir
2842
- }), resolveFilters(rawArgs, args.filter));
2843
- const limit = args.limit ? Number.parseInt(args.limit, 10) : void 0;
2844
- const items = matchRepos(repos, args.query, limit);
2845
- const allowed = [
2846
- "auto",
2847
- "pretty",
2848
- "path",
2849
- "slug"
2850
- ];
2851
- if (!allowed.includes(args.format)) {
2852
- consola.error(`Invalid --format value "${args.format}". Allowed: ${allowed.join(", ")}.`);
2853
- process.exitCode = 1;
2854
- return;
2855
- }
2856
- const requested = args.format;
2857
- const format = requested === "auto" ? process.stdout.isTTY ? "pretty" : "path" : requested;
2858
- if (items.length === 0) {
2859
- if (format === "pretty") consola.info(`No matches for "${args.query}".`);
2860
- return;
2861
- }
2862
- if (format === "pretty") {
2863
- process.stdout.write(`${renderTree$1(items)}\n`);
2864
- return;
2865
- }
2866
- for (const item of items) process.stdout.write(`${format === "slug" ? item.slug : item.localPath}\n`);
2867
- }
2868
- });
2869
- //#endregion
2870
2731
  //#region src/commands/shell-init.ts
2871
2732
  /** Append (idempotently) a loader that, plus completion, sets up the shell so
2872
2733
  * the user only has to re-source their rc file. */
@@ -2896,7 +2757,7 @@ ${name}() {
2896
2757
  target=$(command forgemap pick) || return $?
2897
2758
  else
2898
2759
  local matches
2899
- matches=$(command forgemap search "$1" --format path)
2760
+ matches=$(command forgemap list "$1" --format path)
2900
2761
  local count
2901
2762
  count=$(printf '%s' "$matches" | grep -c '^/' || true)
2902
2763
  if [ "$count" = "1" ]; then
@@ -2915,7 +2776,7 @@ ${name}() {
2915
2776
  }
2916
2777
  `;
2917
2778
  }
2918
- function renderFish(name) {
2779
+ function renderFish$1(name) {
2919
2780
  return `# forgemap shell integration — drop into your ~/.config/fish/config.fish:
2920
2781
  # forgemap shell-init fish | source
2921
2782
 
@@ -2926,7 +2787,7 @@ function ${name} --description "forgemap with cd interception"
2926
2787
  if test (count $argv) -eq 0
2927
2788
  set target (command forgemap pick); or return $status
2928
2789
  else
2929
- set matches (command forgemap search $argv[1] --format path)
2790
+ set matches (command forgemap list $argv[1] --format path)
2930
2791
  set count (count $matches)
2931
2792
  if test $count -eq 1
2932
2793
  set target $matches[1]
@@ -2978,7 +2839,7 @@ var shellInitCommand = defineCommand({
2978
2839
  await install(requested, name);
2979
2840
  return;
2980
2841
  }
2981
- const out = requested === "fish" ? renderFish(name) : renderPosix(name);
2842
+ const out = requested === "fish" ? renderFish$1(name) : renderPosix(name);
2982
2843
  process.stdout.write(out);
2983
2844
  }
2984
2845
  });
@@ -3326,12 +3187,296 @@ function severitySymbol(severity) {
3326
3187
  if (severity === "warn") return colors.yellow("!");
3327
3188
  return colors.red("✗");
3328
3189
  }
3190
+ var validateCommand = defineCommand({
3191
+ meta: {
3192
+ name: "validate",
3193
+ description: "Preflight: check the config schema, required CLI tools, and root directory"
3194
+ },
3195
+ args: {
3196
+ json: {
3197
+ type: "boolean",
3198
+ description: "Emit a machine-readable JSON report",
3199
+ default: false
3200
+ },
3201
+ config: {
3202
+ type: "string",
3203
+ description: "Path to forgemap.config.ts (overrides walk-up discovery)"
3204
+ }
3205
+ },
3206
+ async run({ args }) {
3207
+ const loaded = await loadForgeMapConfig({ configFile: args.config });
3208
+ const configDir = loaded.configFile ? dirname(loaded.configFile) : loaded.cwd;
3209
+ const checks = await runChecks(loaded.config, configDir);
3210
+ const ok = checks.every((c) => c.severity !== "fail");
3211
+ if (args.json) process.stdout.write(`${JSON.stringify({
3212
+ ok,
3213
+ checks
3214
+ }, null, 2)}\n`);
3215
+ else {
3216
+ for (const c of checks) process.stdout.write(`${severitySymbol(c.severity)} ${c.name.padEnd(22)} ${colors.dim(c.message)}\n`);
3217
+ process.stdout.write(`\n${ok ? colors.green("All checks passed.") : colors.red("Validation failed.")}\n`);
3218
+ }
3219
+ if (!ok) {
3220
+ process.exitCode = 1;
3221
+ return;
3222
+ }
3223
+ if (!loaded.configFile) consola.warn("No forgemap.config.ts found — using built-in defaults. Run `forgemap config init` to materialize one.");
3224
+ }
3225
+ });
3226
+ //#endregion
3227
+ //#region src/commands/completion.ts
3228
+ var SLUG_COMMANDS = [
3229
+ "clone",
3230
+ "cd",
3231
+ "path",
3232
+ "open",
3233
+ "list",
3234
+ "pick",
3235
+ "delete"
3236
+ ];
3237
+ var STATIC_FLAG_VALUES = {
3238
+ list: { "--format": [
3239
+ "auto",
3240
+ "pretty",
3241
+ "path",
3242
+ "slug"
3243
+ ] },
3244
+ status: { "--format": ["pretty", "json"] },
3245
+ import: {
3246
+ "--format": ["pretty", "json"],
3247
+ "--type": ["forgemap"]
3248
+ }
3249
+ };
3250
+ var SHELL_POSITIONAL = /* @__PURE__ */ new Set(["completion", "shell-init"]);
3251
+ /** Every forgemap command declares `args` as a plain object literal (or omits
3252
+ * it, like `config`), never a thunk — so no `Resolvable` unwrapping is needed. */
3253
+ function argsOf(cmd) {
3254
+ return cmd.args ?? {};
3255
+ }
3256
+ /** Flag names a command exposes, derived from its `defineCommand` args so a new
3257
+ * flag surfaces in completion automatically. Positionals are handled
3258
+ * separately; negatable booleans (those with a `negativeDescription`) also get
3259
+ * their `--no-<flag>` form. */
3260
+ function flagsOf(cmd) {
3261
+ const flags = [];
3262
+ for (const [name, def] of Object.entries(argsOf(cmd))) {
3263
+ if (def.type === "positional") continue;
3264
+ flags.push(`--${name}`);
3265
+ if (def.type === "boolean" && def.negativeDescription) flags.push(`--no-${name}`);
3266
+ }
3267
+ return flags;
3268
+ }
3269
+ /** The ordered subcommand registry, mirroring `rootCommand.subCommands` in
3270
+ * cli.ts. It is kept here rather than imported from cli.ts because that would
3271
+ * form a cycle (cli → completion → cli). Built lazily so the self-reference to
3272
+ * `completionCommand` resolves after the module finishes initializing. */
3273
+ function commandSpecs() {
3274
+ return [
3275
+ ["clone", cloneCommand],
3276
+ ["import", importCommand],
3277
+ ["cleanup", cleanupCommand],
3278
+ ["delete", deleteCommand],
3279
+ ["cd", cdCommand],
3280
+ ["path", pathCommand],
3281
+ ["open", openCommand],
3282
+ ["list", listCommand],
3283
+ ["pick", pickCommand],
3284
+ ["status", statusCommand],
3285
+ ["sync", syncCommand],
3286
+ ["validate", validateCommand],
3287
+ ["info", infoCommand],
3288
+ ["completion", completionCommand],
3289
+ ["shell-init", shellInitCommand],
3290
+ ["config", configCommand]
3291
+ ].map(([name, cmd]) => ({
3292
+ name,
3293
+ flags: flagsOf(cmd),
3294
+ flagValues: STATIC_FLAG_VALUES[name] ?? {},
3295
+ positionalValues: SHELL_POSITIONAL.has(name) ? [...SUPPORTED_SHELLS] : [],
3296
+ slugs: SLUG_COMMANDS.includes(name)
3297
+ }));
3298
+ }
3299
+ function flagValuePairs(specs) {
3300
+ return specs.flatMap((s) => Object.entries(s.flagValues).map(([flag, values]) => [
3301
+ s.name,
3302
+ flag,
3303
+ values
3304
+ ]));
3305
+ }
3306
+ function renderBash(specs) {
3307
+ const names = specs.map((s) => s.name).join(" ");
3308
+ const valueArms = flagValuePairs(specs).map(([cmd, flag, values]) => ` ${cmd}:${flag}) COMPREPLY=( $(compgen -W "${values.join(" ")}" -- "$cur") ); return ;;`).join("\n");
3309
+ const flagArms = specs.filter((s) => s.flags.length > 0).map((s) => ` ${s.name}) flags="${s.flags.join(" ")}" ;;`).join("\n");
3310
+ const slugCmds = specs.filter((s) => s.slugs).map((s) => s.name);
3311
+ return `# forgemap bash completion — drop into your ~/.bashrc:
3312
+ # eval "$(forgemap completion bash)"
3313
+ _forgemap_completion() {
3314
+ local cur prev cmd flags
3315
+ COMPREPLY=()
3316
+ cur="\${COMP_WORDS[COMP_CWORD]}"
3317
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
3318
+ cmd="\${COMP_WORDS[1]}"
3319
+
3320
+ if [ "$COMP_CWORD" = "1" ]; then
3321
+ COMPREPLY=( $(compgen -W "${names}" -- "$cur") )
3322
+ return
3323
+ fi
3324
+
3325
+ # Values for flags with a fixed set (e.g. --format), keyed by "<cmd>:<flag>".
3326
+ case "$cmd:$prev" in
3327
+ ${valueArms}
3328
+ esac
3329
+
3330
+ # Flag names for the current subcommand.
3331
+ if [[ "$cur" == -* ]]; then
3332
+ case "$cmd" in
3333
+ ${flagArms}
3334
+ esac
3335
+ COMPREPLY=( $(compgen -W "$flags" -- "$cur") )
3336
+ return
3337
+ fi
3338
+
3339
+ # Positional values (repo slugs, or a shell name).
3340
+ case "$cmd" in
3341
+ ${[slugCmds.length > 0 ? ` ${slugCmds.join("|")})
3342
+ local slugs
3343
+ slugs=$(forgemap list --format slug 2>/dev/null)
3344
+ COMPREPLY=( $(compgen -W "$slugs" -- "$cur") )
3345
+ ;;` : "", ...specs.filter((s) => s.positionalValues.length > 0).map((s) => ` ${s.name}) COMPREPLY=( $(compgen -W "${s.positionalValues.join(" ")}" -- "$cur") ) ;;`)].filter(Boolean).join("\n")}
3346
+ esac
3347
+ }
3348
+ complete -F _forgemap_completion forgemap
3349
+ `;
3350
+ }
3351
+ function renderZsh(specs) {
3352
+ return `# forgemap zsh completion — drop into your ~/.zshrc:
3353
+ # eval "$(forgemap completion zsh)"
3354
+ _forgemap() {
3355
+ local -a subcommands
3356
+ subcommands=(${specs.map((s) => `'${s.name}'`).join(" ")})
3357
+ local cmd="\${words[2]}"
3358
+ local prev="\${words[CURRENT-1]}"
3359
+ local cur="\${words[CURRENT]}"
3360
+
3361
+ if (( CURRENT == 2 )); then
3362
+ _describe 'forgemap subcommand' subcommands
3363
+ return
3364
+ fi
3365
+
3366
+ # Values for flags with a fixed set (e.g. --format), keyed by "<cmd>:<flag>".
3367
+ case "$cmd:$prev" in
3368
+ ${flagValuePairs(specs).map(([cmd, flag, values]) => ` ${cmd}:${flag}) compadd ${values.join(" ")}; return ;;`).join("\n")}
3369
+ esac
3370
+
3371
+ # Flag names for the current subcommand.
3372
+ if [[ "$cur" == -* ]]; then
3373
+ case "$cmd" in
3374
+ ${specs.filter((s) => s.flags.length > 0).map((s) => ` ${s.name}) compadd -- ${s.flags.join(" ")}; return ;;`).join("\n")}
3375
+ esac
3376
+ return
3377
+ fi
3378
+
3379
+ # Positional values (repo slugs, or a shell name).
3380
+ case "$cmd" in
3381
+ ${specs.filter((s) => s.slugs).map((s) => s.name).join("|")})
3382
+ local -a slugs
3383
+ slugs=("\${(@f)$(forgemap list --format slug 2>/dev/null)}")
3384
+ _describe 'slug' slugs
3385
+ ;;
3386
+ ${specs.filter((s) => s.positionalValues.length > 0).map((s) => ` ${s.name}) compadd ${s.positionalValues.join(" ")} ;;`).join("\n")}
3387
+ esac
3388
+ }
3389
+ compdef _forgemap forgemap
3390
+ `;
3391
+ }
3392
+ function renderFish(specs) {
3393
+ const names = specs.map((s) => s.name).join(" ");
3394
+ const flagLines = specs.flatMap((s) => s.flags.map((flag) => {
3395
+ const long = flag.replace(/^--/, "");
3396
+ const values = s.flagValues[flag];
3397
+ const valuePart = values ? ` -x -a '${values.join(" ")}'` : "";
3398
+ return `complete -c forgemap -n '__fish_seen_subcommand_from ${s.name}' -l ${long}${valuePart}`;
3399
+ })).join("\n");
3400
+ const shellCmds = specs.filter((s) => s.positionalValues.length > 0);
3401
+ return `# forgemap fish completion — drop into your ~/.config/fish/config.fish:
3402
+ # forgemap completion fish | source
3403
+
3404
+ # Subcommands (depth 1).
3405
+ complete -c forgemap -f -n '__fish_use_subcommand' -a '${names}'
3406
+
3407
+ # Flags per subcommand (with fixed value sets where applicable).
3408
+ ${flagLines}
3409
+
3410
+ # Shell flavor for completion / shell-init (depth 2).
3411
+ function __forgemap_needs_shell
3412
+ set -l tokens (commandline -opc)
3413
+ set -l shell_cmds ${shellCmds.map((s) => `"${s.name}"`).join(" ")}
3414
+ if test (count $tokens) -ge 2; and contains $tokens[2] $shell_cmds
3415
+ return 0
3416
+ end
3417
+ return 1
3418
+ end
3419
+ complete -c forgemap -f -n '__forgemap_needs_shell' -a '${shellCmds[0]?.positionalValues.join(" ") ?? ""}'
3420
+
3421
+ # Slugs (depth 2) for commands that take one.
3422
+ function __forgemap_needs_slug
3423
+ set -l tokens (commandline -opc)
3424
+ set -l slug_cmds ${specs.filter((s) => s.slugs).map((s) => `"${s.name}"`).join(" ")}
3425
+ if test (count $tokens) -ge 2; and contains $tokens[2] $slug_cmds
3426
+ return 0
3427
+ end
3428
+ return 1
3429
+ end
3430
+
3431
+ complete -c forgemap -f -n '__forgemap_needs_slug' \\
3432
+ -a '(forgemap list --format slug 2>/dev/null)'
3433
+ `;
3434
+ }
3435
+ var completionCommand = defineCommand({
3436
+ meta: {
3437
+ name: "completion",
3438
+ description: "Print a shell completion script. Source via `eval \"$(forgemap completion)\"`."
3439
+ },
3440
+ args: {
3441
+ shell: {
3442
+ type: "positional",
3443
+ description: `Shell flavor (${SUPPORTED_SHELLS.join(", ")}). Auto-detected from $SHELL if omitted.`,
3444
+ required: false
3445
+ },
3446
+ install: {
3447
+ type: "boolean",
3448
+ description: "Append the completion loader to your shell's rc file (idempotent) instead of printing",
3449
+ default: false
3450
+ }
3451
+ },
3452
+ async run({ args }) {
3453
+ const requested = args.shell ?? detectShell();
3454
+ if (!SUPPORTED_SHELLS.includes(requested)) {
3455
+ consola.error(`Unsupported shell "${requested}". Supported: ${SUPPORTED_SHELLS.join(", ")}.`);
3456
+ process.exitCode = 1;
3457
+ return;
3458
+ }
3459
+ if (args.install) {
3460
+ const { status, rcFile } = await installRcBlock(requested, "completion", [requested === "fish" ? "forgemap completion fish | source" : `eval "$(forgemap completion ${requested})"`]);
3461
+ if (status === "present") consola.info(`forgemap completion already present in ${rcFile}.`);
3462
+ else {
3463
+ const verb = status === "updated" ? "Updated" : "Added";
3464
+ consola.success(`${verb} forgemap completion in ${rcFile}.`);
3465
+ consola.info(`Run \`source ${rcFile}\` or restart your shell to activate it.`);
3466
+ }
3467
+ return;
3468
+ }
3469
+ const specs = commandSpecs();
3470
+ const out = requested === "fish" ? renderFish(specs) : requested === "zsh" ? renderZsh(specs) : renderBash(specs);
3471
+ process.stdout.write(out);
3472
+ }
3473
+ });
3329
3474
  //#endregion
3330
3475
  //#region src/bin/forgemap.ts
3331
3476
  runMain(defineCommand({
3332
3477
  meta: {
3333
3478
  name: "forgemap",
3334
- version: "0.5.0-dev.89-5f2e4a1",
3479
+ version: "0.5.0-dev.90-1fa1595",
3335
3480
  description: "Manage a local repo layout of the form <root>/<forge.dir>/<owner>/<repo>"
3336
3481
  },
3337
3482
  subCommands: {
@@ -3342,46 +3487,11 @@ runMain(defineCommand({
3342
3487
  cd: cdCommand,
3343
3488
  path: pathCommand,
3344
3489
  open: openCommand,
3345
- search: searchCommand,
3490
+ list: listCommand,
3346
3491
  pick: pickCommand,
3347
3492
  status: statusCommand,
3348
3493
  sync: syncCommand,
3349
- validate: defineCommand({
3350
- meta: {
3351
- name: "validate",
3352
- description: "Preflight: check the config schema, required CLI tools, and root directory"
3353
- },
3354
- args: {
3355
- json: {
3356
- type: "boolean",
3357
- description: "Emit a machine-readable JSON report",
3358
- default: false
3359
- },
3360
- config: {
3361
- type: "string",
3362
- description: "Path to forgemap.config.ts (overrides walk-up discovery)"
3363
- }
3364
- },
3365
- async run({ args }) {
3366
- const loaded = await loadForgeMapConfig({ configFile: args.config });
3367
- const configDir = loaded.configFile ? dirname(loaded.configFile) : loaded.cwd;
3368
- const checks = await runChecks(loaded.config, configDir);
3369
- const ok = checks.every((c) => c.severity !== "fail");
3370
- if (args.json) process.stdout.write(`${JSON.stringify({
3371
- ok,
3372
- checks
3373
- }, null, 2)}\n`);
3374
- else {
3375
- for (const c of checks) process.stdout.write(`${severitySymbol(c.severity)} ${c.name.padEnd(22)} ${colors.dim(c.message)}\n`);
3376
- process.stdout.write(`\n${ok ? colors.green("All checks passed.") : colors.red("Validation failed.")}\n`);
3377
- }
3378
- if (!ok) {
3379
- process.exitCode = 1;
3380
- return;
3381
- }
3382
- if (!loaded.configFile) consola.warn("No forgemap.config.ts found — using built-in defaults. Run `forgemap config init` to materialize one.");
3383
- }
3384
- }),
3494
+ validate: validateCommand,
3385
3495
  info: infoCommand,
3386
3496
  completion: completionCommand,
3387
3497
  "shell-init": shellInitCommand,