forgemap 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -7
- package/dist/bin/forgemap.mjs +1161 -453
- package/dist/bin/forgemap.mjs.map +1 -1
- package/package.json +4 -3
package/dist/bin/forgemap.mjs
CHANGED
|
@@ -3,12 +3,13 @@ import { defineCommand, runMain } from "citty";
|
|
|
3
3
|
import consola from "consola";
|
|
4
4
|
import { access, mkdir, readFile, readdir, rename, rm, rmdir, stat, writeFile } from "node:fs/promises";
|
|
5
5
|
import { colors, formatTree } from "consola/utils";
|
|
6
|
-
import { dirname, isAbsolute, join, resolve } from "pathe";
|
|
6
|
+
import { dirname, extname, isAbsolute, join, relative, resolve } from "pathe";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
9
9
|
import { loadConfig } from "c12";
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
11
11
|
import { spawn } from "node:child_process";
|
|
12
|
+
import { updateConfig } from "c12/update";
|
|
12
13
|
import Fuse from "fuse.js";
|
|
13
14
|
//#region src/commands/cd.ts
|
|
14
15
|
/**
|
|
@@ -81,6 +82,39 @@ function findGlobalConfig() {
|
|
|
81
82
|
if (existsSync(candidate)) return candidate;
|
|
82
83
|
}
|
|
83
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Every `forgemap.config.*` a change could be written to, in resolution order:
|
|
87
|
+
* one per directory walking up from `start` (nearest first, mirroring the
|
|
88
|
+
* loader's first-basename-wins rule), then the global config. Used by the
|
|
89
|
+
* `forge` command to let the user pick a target when more than one exists.
|
|
90
|
+
*/
|
|
91
|
+
function discoverConfigFiles(start = process.cwd()) {
|
|
92
|
+
const found = [];
|
|
93
|
+
const seen = /* @__PURE__ */ new Set();
|
|
94
|
+
let dir = resolve(start);
|
|
95
|
+
for (;;) {
|
|
96
|
+
for (const base of CONFIG_BASENAMES) {
|
|
97
|
+
const candidate = join(dir, base);
|
|
98
|
+
if (existsSync(candidate)) {
|
|
99
|
+
seen.add(candidate);
|
|
100
|
+
found.push({
|
|
101
|
+
path: candidate,
|
|
102
|
+
source: "walk-up"
|
|
103
|
+
});
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const parent = dirname(dir);
|
|
108
|
+
if (parent === dir) break;
|
|
109
|
+
dir = parent;
|
|
110
|
+
}
|
|
111
|
+
const global = findGlobalConfig();
|
|
112
|
+
if (global && !seen.has(global)) found.push({
|
|
113
|
+
path: global,
|
|
114
|
+
source: "global"
|
|
115
|
+
});
|
|
116
|
+
return found;
|
|
117
|
+
}
|
|
84
118
|
var DEFAULT_CONFIG$1 = {
|
|
85
119
|
root: ".",
|
|
86
120
|
defaultForge: "github",
|
|
@@ -117,6 +151,7 @@ async function loadForgeMapConfig(options = {}) {
|
|
|
117
151
|
}
|
|
118
152
|
}
|
|
119
153
|
}
|
|
154
|
+
if (explicit) explicit = resolve(startDir, explicit);
|
|
120
155
|
const cwd = explicit ? dirname(explicit) : startDir;
|
|
121
156
|
const { config, configFile } = await loadConfig({
|
|
122
157
|
name: "forgemap",
|
|
@@ -1335,146 +1370,6 @@ async function installRcBlock(shell, label, lines, legacyLabels = []) {
|
|
|
1335
1370
|
};
|
|
1336
1371
|
}
|
|
1337
1372
|
//#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
1373
|
//#region src/config/write.ts
|
|
1479
1374
|
var HEADER = `/**
|
|
1480
1375
|
* forgemap configuration.
|
|
@@ -1729,66 +1624,687 @@ var deleteCommand = defineCommand({
|
|
|
1729
1624
|
}
|
|
1730
1625
|
});
|
|
1731
1626
|
//#endregion
|
|
1732
|
-
//#region src/
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1627
|
+
//#region src/config/forges.ts
|
|
1628
|
+
/** Every forge `type` the config schema accepts, in prompt/display order. */
|
|
1629
|
+
var FORGE_TYPES = [
|
|
1630
|
+
"github",
|
|
1631
|
+
"gitlab",
|
|
1632
|
+
"gitea",
|
|
1633
|
+
"codeberg",
|
|
1634
|
+
"git"
|
|
1635
|
+
];
|
|
1636
|
+
/** Canonical host per forge type, offered as the host prompt's default. The
|
|
1637
|
+
* self-hosted flavors (`gitea`, plain `git`) have no universal host, so none
|
|
1638
|
+
* is suggested for them. */
|
|
1639
|
+
var DEFAULT_HOSTS = {
|
|
1640
|
+
github: "github.com",
|
|
1641
|
+
gitlab: "gitlab.com",
|
|
1642
|
+
codeberg: "codeberg.org"
|
|
1643
|
+
};
|
|
1644
|
+
/** Git clone protocols, in prompt order (`ssh` is the schema default). */
|
|
1645
|
+
var GIT_PROTOCOLS = ["ssh", "https"];
|
|
1646
|
+
/** Reject empty / whitespace-only keys; any other string is a valid map key.
|
|
1647
|
+
* Returns an error message, or `null` when the key is acceptable. */
|
|
1648
|
+
function validateForgeKey(raw) {
|
|
1649
|
+
if (raw.trim().length === 0) return "Forge key must not be empty.";
|
|
1650
|
+
return null;
|
|
1651
|
+
}
|
|
1652
|
+
/** Whether `value` is one of the schema's forge types (narrows a raw flag). */
|
|
1653
|
+
function isForgeType(value) {
|
|
1654
|
+
return FORGE_TYPES.includes(value);
|
|
1655
|
+
}
|
|
1656
|
+
/** Whether `value` is a supported git protocol. */
|
|
1657
|
+
function isGitProtocol(value) {
|
|
1658
|
+
return GIT_PROTOCOLS.includes(value);
|
|
1659
|
+
}
|
|
1660
|
+
/** Build a `ForgeConfig` from collected input, keeping `protocol` only when it
|
|
1661
|
+
* is the non-default (`https`) git protocol. */
|
|
1662
|
+
function buildForge(input) {
|
|
1663
|
+
if (input.type === "git") {
|
|
1664
|
+
const forge = {
|
|
1665
|
+
type: "git",
|
|
1666
|
+
host: input.host,
|
|
1667
|
+
dir: input.dir
|
|
1668
|
+
};
|
|
1669
|
+
if (input.protocol === "https") forge.protocol = "https";
|
|
1670
|
+
return forge;
|
|
1739
1671
|
}
|
|
1672
|
+
return {
|
|
1673
|
+
type: input.type,
|
|
1674
|
+
host: input.host,
|
|
1675
|
+
dir: input.dir
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
function addForge(config, key, forge) {
|
|
1679
|
+
if (!config.forges) config.forges = {};
|
|
1680
|
+
config.forges[key] = forge;
|
|
1681
|
+
}
|
|
1682
|
+
function removeForge(config, key) {
|
|
1683
|
+
if (config.forges) delete config.forges[key];
|
|
1684
|
+
}
|
|
1685
|
+
function setDefaultForge(config, key) {
|
|
1686
|
+
config.defaultForge = key;
|
|
1740
1687
|
}
|
|
1688
|
+
/** Apply a partial change to an existing forge in place. Clears `protocol`
|
|
1689
|
+
* whenever the resulting type is not `git`, since it is meaningless there. */
|
|
1690
|
+
function editForge(config, key, patch) {
|
|
1691
|
+
const forge = config.forges?.[key];
|
|
1692
|
+
if (!forge) return;
|
|
1693
|
+
if (patch.type !== void 0) forge.type = patch.type;
|
|
1694
|
+
if (patch.host !== void 0) forge.host = patch.host;
|
|
1695
|
+
if (patch.dir !== void 0) forge.dir = patch.dir;
|
|
1696
|
+
if (forge.type !== "git") delete forge.protocol;
|
|
1697
|
+
else if (patch.protocol === null) delete forge.protocol;
|
|
1698
|
+
else if (patch.protocol !== void 0) forge.protocol = patch.protocol;
|
|
1699
|
+
}
|
|
1700
|
+
//#endregion
|
|
1701
|
+
//#region src/config/mutate.ts
|
|
1741
1702
|
/**
|
|
1742
|
-
*
|
|
1743
|
-
*
|
|
1744
|
-
*
|
|
1703
|
+
* Apply an in-place mutation to a `forgemap.config.*` file, preserving its
|
|
1704
|
+
* formatting and comments.
|
|
1705
|
+
*
|
|
1706
|
+
* `.ts`/`.mts`/`.js`/… are round-tripped through c12's `updateConfig`, which
|
|
1707
|
+
* parses the module with magicast and edits the exported object literal — it
|
|
1708
|
+
* transparently unwraps a `defineForgeMapConfig(...)` call. Plain `.json`
|
|
1709
|
+
* configs, which magicast/updateConfig refuse, are read, mutated and written
|
|
1710
|
+
* back directly.
|
|
1711
|
+
*
|
|
1712
|
+
* Rejects when the source can't be edited safely (e.g. forges built dynamically
|
|
1713
|
+
* rather than declared as a literal); callers surface that as a manual-edit
|
|
1714
|
+
* fallback rather than crashing.
|
|
1745
1715
|
*/
|
|
1746
|
-
async function
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1716
|
+
async function mutateConfigFile(path, mutate) {
|
|
1717
|
+
if (extname(path) === ".json") {
|
|
1718
|
+
const current = JSON.parse(await readFile(path, "utf8"));
|
|
1719
|
+
mutate(current);
|
|
1720
|
+
await writeFile(path, `${JSON.stringify(current, null, 2)}\n`, "utf8");
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
await updateConfig({
|
|
1724
|
+
cwd: dirname(path),
|
|
1725
|
+
configFile: "forgemap.config",
|
|
1726
|
+
onUpdate: (config) => {
|
|
1727
|
+
mutate(config);
|
|
1758
1728
|
}
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1731
|
+
//#endregion
|
|
1732
|
+
//#region src/repos/picker.ts
|
|
1733
|
+
/**
|
|
1734
|
+
* Show the interactive repo picker and return the chosen local path
|
|
1735
|
+
* (undefined when the user cancels).
|
|
1736
|
+
*
|
|
1737
|
+
* `$(forgemap pick)` / `$(forgemap path <q>)` captures stdout, so the
|
|
1738
|
+
* interactive TUI must not go there. consola/clack writes the UI to stdout AND
|
|
1739
|
+
* reads stdout.rows/columns for layout — but a captured stdout is a pipe (no
|
|
1740
|
+
* rows → nothing renders). So for the duration of the prompt: route stdout
|
|
1741
|
+
* writes to stderr (the real TTY) and borrow stderr's dimensions, then
|
|
1742
|
+
* restore. stdout stays clean for the chosen path only.
|
|
1743
|
+
*
|
|
1744
|
+
* Callers must check {@link canPrompt} first — without a TTY on stdin there is
|
|
1745
|
+
* nobody to answer.
|
|
1746
|
+
*/
|
|
1747
|
+
async function promptRepoChoice(candidates) {
|
|
1748
|
+
const out = process.stdout;
|
|
1749
|
+
const realWrite = out.write;
|
|
1750
|
+
const saved = {
|
|
1751
|
+
rows: Object.getOwnPropertyDescriptor(out, "rows"),
|
|
1752
|
+
columns: Object.getOwnPropertyDescriptor(out, "columns"),
|
|
1753
|
+
isTTY: Object.getOwnPropertyDescriptor(out, "isTTY")
|
|
1754
|
+
};
|
|
1755
|
+
const fake = (key, value) => {
|
|
1756
|
+
Object.defineProperty(out, key, {
|
|
1757
|
+
configurable: true,
|
|
1758
|
+
value
|
|
1759
|
+
});
|
|
1760
|
+
};
|
|
1761
|
+
const restore = (key) => {
|
|
1762
|
+
if (saved[key]) Object.defineProperty(out, key, saved[key]);
|
|
1763
|
+
else delete out[key];
|
|
1764
|
+
};
|
|
1765
|
+
out.write = process.stderr.write.bind(process.stderr);
|
|
1766
|
+
fake("rows", process.stderr.rows ?? 24);
|
|
1767
|
+
fake("columns", process.stderr.columns ?? 80);
|
|
1768
|
+
fake("isTTY", true);
|
|
1769
|
+
let choice;
|
|
1770
|
+
try {
|
|
1771
|
+
choice = await consola.prompt("Select a repo", {
|
|
1772
|
+
type: "select",
|
|
1773
|
+
options: candidates.map((r) => ({
|
|
1774
|
+
label: `${colors.gray(`${r.forgeName}:`)}${r.slug}`,
|
|
1775
|
+
value: r.localPath,
|
|
1776
|
+
hint: r.localPath
|
|
1777
|
+
}))
|
|
1778
|
+
});
|
|
1779
|
+
} finally {
|
|
1780
|
+
out.write = realWrite;
|
|
1781
|
+
restore("rows");
|
|
1782
|
+
restore("columns");
|
|
1783
|
+
restore("isTTY");
|
|
1759
1784
|
}
|
|
1760
|
-
return
|
|
1785
|
+
return typeof choice === "string" && choice ? choice : void 0;
|
|
1761
1786
|
}
|
|
1762
|
-
|
|
1763
|
-
|
|
1787
|
+
/** Whether an interactive prompt can be shown at all. */
|
|
1788
|
+
function canPrompt() {
|
|
1789
|
+
return Boolean(process.stdin.isTTY);
|
|
1790
|
+
}
|
|
1791
|
+
//#endregion
|
|
1792
|
+
//#region src/commands/forge/shared.ts
|
|
1793
|
+
/** Whether interactive prompts can be shown (a TTY on stdin to answer them). */
|
|
1794
|
+
function interactive() {
|
|
1795
|
+
return canPrompt();
|
|
1796
|
+
}
|
|
1797
|
+
/** Prompt for free text. Returns the raw string, or `null` when cancelled. */
|
|
1798
|
+
async function promptText(message, placeholder) {
|
|
1799
|
+
const answer = await consola.prompt(message, {
|
|
1800
|
+
type: "text",
|
|
1801
|
+
placeholder,
|
|
1802
|
+
cancel: "null"
|
|
1803
|
+
});
|
|
1804
|
+
return typeof answer === "string" ? answer : null;
|
|
1805
|
+
}
|
|
1806
|
+
/** Prompt to pick one of `options`. Returns the value, or `null` when cancelled. */
|
|
1807
|
+
async function promptSelect(message, options) {
|
|
1808
|
+
const answer = await consola.prompt(message, {
|
|
1809
|
+
type: "select",
|
|
1810
|
+
options: [...options],
|
|
1811
|
+
cancel: "null"
|
|
1812
|
+
});
|
|
1813
|
+
return typeof answer === "string" && answer ? answer : null;
|
|
1814
|
+
}
|
|
1815
|
+
/** Yes/no confirmation. Returns `false` when declined or cancelled. */
|
|
1816
|
+
async function confirmChange(message) {
|
|
1817
|
+
return await consola.prompt(message, {
|
|
1818
|
+
type: "confirm",
|
|
1819
|
+
cancel: "null"
|
|
1820
|
+
}) === true;
|
|
1764
1821
|
}
|
|
1765
1822
|
/**
|
|
1766
|
-
*
|
|
1767
|
-
*
|
|
1768
|
-
*
|
|
1823
|
+
* Choose which config file `add` writes to.
|
|
1824
|
+
* - `--config <path>` always wins (created when it does not exist).
|
|
1825
|
+
* - otherwise discover candidates: one → use it; several with a TTY → present a
|
|
1826
|
+
* select (the final step before confirming); several without a TTY → nearest.
|
|
1827
|
+
* - nothing discovered → a fresh `forgemap.config.ts` in the cwd.
|
|
1828
|
+
*
|
|
1829
|
+
* Returns `null` when the user cancels the select.
|
|
1769
1830
|
*/
|
|
1770
|
-
function
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
if (list) list.push(report);
|
|
1777
|
-
else byServer.set(report.repo.serverDir, [report]);
|
|
1778
|
-
}
|
|
1779
|
-
for (const [serverDir, group] of byServer) {
|
|
1780
|
-
counts.set(serverDir, group.length);
|
|
1781
|
-
const hostTally = /* @__PURE__ */ new Map();
|
|
1782
|
-
for (const report of group) if (report.originHost) hostTally.set(report.originHost, (hostTally.get(report.originHost) ?? 0) + 1);
|
|
1783
|
-
const host = [...hostTally.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? "";
|
|
1784
|
-
forges[serverDir] = {
|
|
1785
|
-
type: host ? forgeTypeForHost(host) : "git",
|
|
1786
|
-
host,
|
|
1787
|
-
dir: serverDir
|
|
1831
|
+
async function resolveAddTarget(explicit) {
|
|
1832
|
+
if (explicit) {
|
|
1833
|
+
const path = resolve(process.cwd(), explicit);
|
|
1834
|
+
return {
|
|
1835
|
+
path,
|
|
1836
|
+
create: !existsSync(path)
|
|
1788
1837
|
};
|
|
1789
1838
|
}
|
|
1790
|
-
|
|
1791
|
-
|
|
1839
|
+
const candidates = discoverConfigFiles();
|
|
1840
|
+
if (candidates.length === 0) return {
|
|
1841
|
+
path: join(process.cwd(), "forgemap.config.ts"),
|
|
1842
|
+
create: true
|
|
1843
|
+
};
|
|
1844
|
+
if (candidates.length === 1 || !interactive()) return {
|
|
1845
|
+
path: candidates[0].path,
|
|
1846
|
+
create: false
|
|
1847
|
+
};
|
|
1848
|
+
const choice = await consola.prompt("Which config file should this change be written to?", {
|
|
1849
|
+
type: "select",
|
|
1850
|
+
options: candidates.map((c) => ({
|
|
1851
|
+
label: relative(process.cwd(), c.path) || c.path,
|
|
1852
|
+
value: c.path,
|
|
1853
|
+
hint: c.source
|
|
1854
|
+
})),
|
|
1855
|
+
cancel: "null"
|
|
1856
|
+
});
|
|
1857
|
+
if (typeof choice !== "string" || !choice) {
|
|
1858
|
+
consola.info("Aborted — nothing changed.");
|
|
1859
|
+
return null;
|
|
1860
|
+
}
|
|
1861
|
+
return {
|
|
1862
|
+
path: choice,
|
|
1863
|
+
create: false
|
|
1864
|
+
};
|
|
1865
|
+
}
|
|
1866
|
+
/**
|
|
1867
|
+
* The config file `edit`/`remove` operate on — the forge already lives in a real
|
|
1868
|
+
* file, so `--config` or the resolved config file is used. Prints an error and
|
|
1869
|
+
* returns `null` when only the built-in defaults are in effect (no file).
|
|
1870
|
+
*/
|
|
1871
|
+
function existingConfigFile(loaded, explicit) {
|
|
1872
|
+
if (explicit) return resolve(process.cwd(), explicit);
|
|
1873
|
+
if (!loaded.configFile) {
|
|
1874
|
+
consola.error("No forgemap config file found. Run `forgemap config init` or `forgemap forge add` first.");
|
|
1875
|
+
return null;
|
|
1876
|
+
}
|
|
1877
|
+
return loaded.configFile;
|
|
1878
|
+
}
|
|
1879
|
+
/**
|
|
1880
|
+
* Round-trip `mutate` into `path`; on failure (a config too dynamic to rewrite)
|
|
1881
|
+
* report it and print the change for manual application instead of crashing.
|
|
1882
|
+
* Returns whether the file was updated.
|
|
1883
|
+
*/
|
|
1884
|
+
async function applyChange(path, mutate, manualHint) {
|
|
1885
|
+
try {
|
|
1886
|
+
await mutateConfigFile(path, mutate);
|
|
1887
|
+
return true;
|
|
1888
|
+
} catch (error) {
|
|
1889
|
+
consola.error(`Could not update ${path} automatically: ${error.message}`);
|
|
1890
|
+
consola.info("Apply this change by hand instead:");
|
|
1891
|
+
manualHint();
|
|
1892
|
+
return false;
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
/** Print a forge as a `forgemap.config` block (the manual-edit fallback). */
|
|
1896
|
+
function printManualForge(key, forge) {
|
|
1897
|
+
consola.log(` ${key}: {`);
|
|
1898
|
+
consola.log(` type: '${forge.type}',`);
|
|
1899
|
+
consola.log(` host: '${forge.host}',`);
|
|
1900
|
+
consola.log(` dir: '${forge.dir}'${forge.protocol ? "," : ""}`);
|
|
1901
|
+
if (forge.protocol) consola.log(` protocol: '${forge.protocol}'`);
|
|
1902
|
+
consola.log(" }");
|
|
1903
|
+
}
|
|
1904
|
+
//#endregion
|
|
1905
|
+
//#region src/commands/forge/add.ts
|
|
1906
|
+
var forgeAddCommand = defineCommand({
|
|
1907
|
+
meta: {
|
|
1908
|
+
name: "add",
|
|
1909
|
+
description: "Add a forge to the config (prompts for anything not passed)"
|
|
1910
|
+
},
|
|
1911
|
+
args: {
|
|
1912
|
+
key: {
|
|
1913
|
+
type: "positional",
|
|
1914
|
+
required: false,
|
|
1915
|
+
description: "Forge key, e.g. github or work"
|
|
1916
|
+
},
|
|
1917
|
+
type: {
|
|
1918
|
+
type: "string",
|
|
1919
|
+
description: `Forge type (${FORGE_TYPES.join(", ")})`
|
|
1920
|
+
},
|
|
1921
|
+
host: {
|
|
1922
|
+
type: "string",
|
|
1923
|
+
description: "Forge host, e.g. github.com"
|
|
1924
|
+
},
|
|
1925
|
+
dir: {
|
|
1926
|
+
type: "string",
|
|
1927
|
+
description: "Directory under root, e.g. comGithub"
|
|
1928
|
+
},
|
|
1929
|
+
protocol: {
|
|
1930
|
+
type: "string",
|
|
1931
|
+
description: `Clone protocol for type=git (${GIT_PROTOCOLS.join(", ")})`
|
|
1932
|
+
},
|
|
1933
|
+
default: {
|
|
1934
|
+
type: "boolean",
|
|
1935
|
+
description: "Set this forge as the default",
|
|
1936
|
+
default: false
|
|
1937
|
+
},
|
|
1938
|
+
config: {
|
|
1939
|
+
type: "string",
|
|
1940
|
+
description: "Path to the forgemap config file to modify"
|
|
1941
|
+
},
|
|
1942
|
+
yes: {
|
|
1943
|
+
type: "boolean",
|
|
1944
|
+
description: "Skip the confirmation prompt",
|
|
1945
|
+
default: false
|
|
1946
|
+
}
|
|
1947
|
+
},
|
|
1948
|
+
async run({ args }) {
|
|
1949
|
+
const loaded = await loadForgeMapConfig({ configFile: args.config });
|
|
1950
|
+
const tty = interactive();
|
|
1951
|
+
let key = typeof args.key === "string" ? args.key.trim() : "";
|
|
1952
|
+
if (!key && tty) {
|
|
1953
|
+
const answer = await promptText("Forge key (e.g. github, work):");
|
|
1954
|
+
if (answer === null) return abort$2();
|
|
1955
|
+
key = answer.trim();
|
|
1956
|
+
}
|
|
1957
|
+
const keyError = validateForgeKey(key);
|
|
1958
|
+
if (keyError) return fail$2(keyError);
|
|
1959
|
+
if (loaded.configFile && key in loaded.config.forges) return fail$2(`Forge "${key}" already exists. Use \`forgemap forge edit ${key}\` to change it.`);
|
|
1960
|
+
let type;
|
|
1961
|
+
if (typeof args.type === "string") {
|
|
1962
|
+
if (!isForgeType(args.type)) return fail$2(invalidType$1(args.type));
|
|
1963
|
+
type = args.type;
|
|
1964
|
+
} else if (tty) {
|
|
1965
|
+
const answer = await promptSelect("Forge type:", FORGE_TYPES);
|
|
1966
|
+
if (answer === null || !isForgeType(answer)) return abort$2();
|
|
1967
|
+
type = answer;
|
|
1968
|
+
}
|
|
1969
|
+
if (!type) return fail$2("Missing forge type. Pass --type.");
|
|
1970
|
+
const suggestedHost = DEFAULT_HOSTS[type] ?? "";
|
|
1971
|
+
let host = typeof args.host === "string" ? args.host.trim() : "";
|
|
1972
|
+
if (!host && tty) {
|
|
1973
|
+
const answer = await promptText("Host:", suggestedHost);
|
|
1974
|
+
if (answer === null) return abort$2();
|
|
1975
|
+
host = answer.trim() || suggestedHost;
|
|
1976
|
+
} else if (!host) host = suggestedHost;
|
|
1977
|
+
if (!host) return fail$2("Missing host. Pass --host.");
|
|
1978
|
+
let dir = typeof args.dir === "string" ? args.dir.trim() : "";
|
|
1979
|
+
if (!dir && tty) {
|
|
1980
|
+
const answer = await promptText("Directory (under root):");
|
|
1981
|
+
if (answer === null) return abort$2();
|
|
1982
|
+
dir = answer.trim();
|
|
1983
|
+
}
|
|
1984
|
+
if (!dir) return fail$2("Missing directory. Pass --dir.");
|
|
1985
|
+
let protocol;
|
|
1986
|
+
if (type === "git") {
|
|
1987
|
+
if (typeof args.protocol === "string") {
|
|
1988
|
+
if (!isGitProtocol(args.protocol)) return fail$2(invalidProtocol$1(args.protocol));
|
|
1989
|
+
protocol = args.protocol;
|
|
1990
|
+
} else if (tty) {
|
|
1991
|
+
const answer = await promptSelect("Clone protocol:", GIT_PROTOCOLS);
|
|
1992
|
+
if (answer !== null && isGitProtocol(answer)) protocol = answer;
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
let makeDefault = args.default === true;
|
|
1996
|
+
if (!loaded.configFile) makeDefault = true;
|
|
1997
|
+
else if (!makeDefault && tty) makeDefault = await confirmChange(`Set "${key}" as the default forge?`);
|
|
1998
|
+
const forge = buildForge({
|
|
1999
|
+
type,
|
|
2000
|
+
host,
|
|
2001
|
+
dir,
|
|
2002
|
+
protocol
|
|
2003
|
+
});
|
|
2004
|
+
const target = await resolveAddTarget(args.config);
|
|
2005
|
+
if (!target) return;
|
|
2006
|
+
consola.info(`Add forge "${key}" (${type} → ${host}) into ${target.create ? "new " : ""}${target.path}`);
|
|
2007
|
+
if (tty && !args.yes && !await confirmChange("Apply this change?")) return abort$2();
|
|
2008
|
+
if (target.create) {
|
|
2009
|
+
const written = await writeConfigFile({
|
|
2010
|
+
root: loaded.config.root,
|
|
2011
|
+
defaultForge: key,
|
|
2012
|
+
forges: { [key]: forge }
|
|
2013
|
+
}, { outDir: dirname(target.path) });
|
|
2014
|
+
if (!written) return fail$2(`${target.path} already exists.`);
|
|
2015
|
+
consola.success(`Added forge "${key}" — wrote ${written.path}`);
|
|
2016
|
+
return;
|
|
2017
|
+
}
|
|
2018
|
+
if (await applyChange(target.path, (c) => {
|
|
2019
|
+
addForge(c, key, forge);
|
|
2020
|
+
if (makeDefault) setDefaultForge(c, key);
|
|
2021
|
+
}, () => printManualForge(key, forge))) consola.success(`Added forge "${key}" to ${target.path}`);
|
|
2022
|
+
else process.exitCode = 1;
|
|
2023
|
+
}
|
|
2024
|
+
});
|
|
2025
|
+
function fail$2(message) {
|
|
2026
|
+
consola.error(message);
|
|
2027
|
+
process.exitCode = 1;
|
|
2028
|
+
}
|
|
2029
|
+
function abort$2() {
|
|
2030
|
+
consola.info("Aborted — nothing changed.");
|
|
2031
|
+
}
|
|
2032
|
+
function invalidType$1(value) {
|
|
2033
|
+
return `Invalid type "${value}". Expected one of: ${FORGE_TYPES.join(", ")}.`;
|
|
2034
|
+
}
|
|
2035
|
+
function invalidProtocol$1(value) {
|
|
2036
|
+
return `Invalid protocol "${value}". Expected one of: ${GIT_PROTOCOLS.join(", ")}.`;
|
|
2037
|
+
}
|
|
2038
|
+
//#endregion
|
|
2039
|
+
//#region src/commands/forge/edit.ts
|
|
2040
|
+
var forgeEditCommand = defineCommand({
|
|
2041
|
+
meta: {
|
|
2042
|
+
name: "edit",
|
|
2043
|
+
description: "Edit an existing forge (prompts for fields when none are passed)"
|
|
2044
|
+
},
|
|
2045
|
+
args: {
|
|
2046
|
+
key: {
|
|
2047
|
+
type: "positional",
|
|
2048
|
+
required: false,
|
|
2049
|
+
description: "Forge key to edit"
|
|
2050
|
+
},
|
|
2051
|
+
type: {
|
|
2052
|
+
type: "string",
|
|
2053
|
+
description: `New forge type (${FORGE_TYPES.join(", ")})`
|
|
2054
|
+
},
|
|
2055
|
+
host: {
|
|
2056
|
+
type: "string",
|
|
2057
|
+
description: "New host"
|
|
2058
|
+
},
|
|
2059
|
+
dir: {
|
|
2060
|
+
type: "string",
|
|
2061
|
+
description: "New directory under root"
|
|
2062
|
+
},
|
|
2063
|
+
protocol: {
|
|
2064
|
+
type: "string",
|
|
2065
|
+
description: `New clone protocol for type=git (${GIT_PROTOCOLS.join(", ")})`
|
|
2066
|
+
},
|
|
2067
|
+
config: {
|
|
2068
|
+
type: "string",
|
|
2069
|
+
description: "Path to the forgemap config file to modify"
|
|
2070
|
+
},
|
|
2071
|
+
yes: {
|
|
2072
|
+
type: "boolean",
|
|
2073
|
+
description: "Skip the confirmation prompt",
|
|
2074
|
+
default: false
|
|
2075
|
+
}
|
|
2076
|
+
},
|
|
2077
|
+
async run({ args }) {
|
|
2078
|
+
const loaded = await loadForgeMapConfig({ configFile: args.config });
|
|
2079
|
+
const tty = interactive();
|
|
2080
|
+
const file = existingConfigFile(loaded, args.config);
|
|
2081
|
+
if (!file) {
|
|
2082
|
+
process.exitCode = 1;
|
|
2083
|
+
return;
|
|
2084
|
+
}
|
|
2085
|
+
const forges = loaded.config.forges;
|
|
2086
|
+
let key = typeof args.key === "string" ? args.key.trim() : "";
|
|
2087
|
+
if (!key && tty) {
|
|
2088
|
+
const answer = await promptSelect("Which forge should be edited?", Object.keys(forges));
|
|
2089
|
+
if (answer === null) return abort$1();
|
|
2090
|
+
key = answer;
|
|
2091
|
+
}
|
|
2092
|
+
if (!key) return fail$1("Missing forge key. Pass it as an argument.");
|
|
2093
|
+
const current = forges[key];
|
|
2094
|
+
if (!current) return fail$1(`No forge "${key}" in ${file}. Configured: ${Object.keys(forges).join(", ")}.`);
|
|
2095
|
+
const currentProtocol = current.type === "git" ? current.protocol : void 0;
|
|
2096
|
+
const patch = {};
|
|
2097
|
+
if (typeof args.type === "string") {
|
|
2098
|
+
if (!isForgeType(args.type)) return fail$1(invalidType(args.type));
|
|
2099
|
+
patch.type = args.type;
|
|
2100
|
+
} else if (tty) {
|
|
2101
|
+
const answer = await promptSelect(`Type (current: ${current.type}):`, FORGE_TYPES);
|
|
2102
|
+
if (answer === null) return abort$1();
|
|
2103
|
+
if (isForgeType(answer)) patch.type = answer;
|
|
2104
|
+
}
|
|
2105
|
+
const resultType = patch.type ?? current.type;
|
|
2106
|
+
if (typeof args.host === "string") patch.host = args.host.trim();
|
|
2107
|
+
else if (tty) {
|
|
2108
|
+
const answer = await promptText(`Host (current: ${current.host}):`, current.host);
|
|
2109
|
+
if (answer === null) return abort$1();
|
|
2110
|
+
if (answer.trim()) patch.host = answer.trim();
|
|
2111
|
+
}
|
|
2112
|
+
if (typeof args.dir === "string") patch.dir = args.dir.trim();
|
|
2113
|
+
else if (tty) {
|
|
2114
|
+
const answer = await promptText(`Directory (current: ${current.dir}):`, current.dir);
|
|
2115
|
+
if (answer === null) return abort$1();
|
|
2116
|
+
if (answer.trim()) patch.dir = answer.trim();
|
|
2117
|
+
}
|
|
2118
|
+
if (resultType === "git") {
|
|
2119
|
+
if (typeof args.protocol === "string") {
|
|
2120
|
+
if (!isGitProtocol(args.protocol)) return fail$1(invalidProtocol(args.protocol));
|
|
2121
|
+
patch.protocol = args.protocol;
|
|
2122
|
+
} else if (tty) {
|
|
2123
|
+
const answer = await promptSelect("Clone protocol:", GIT_PROTOCOLS);
|
|
2124
|
+
if (answer !== null && isGitProtocol(answer)) patch.protocol = answer;
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
if (!hasChanges(patch)) return fail$1("Nothing to change. Pass --type, --host, --dir or --protocol.");
|
|
2128
|
+
consola.info(`Edit forge "${key}" in ${file}`);
|
|
2129
|
+
if (tty && !args.yes && !await confirmChange("Apply this change?")) return abort$1();
|
|
2130
|
+
const merged = mergeForge(current, currentProtocol, patch, resultType);
|
|
2131
|
+
if (await applyChange(file, (c) => editForge(c, key, patch), () => {
|
|
2132
|
+
consola.log(`Update the "${key}" entry to:`);
|
|
2133
|
+
printManualForge(key, merged);
|
|
2134
|
+
})) consola.success(`Edited forge "${key}" in ${file}`);
|
|
2135
|
+
else process.exitCode = 1;
|
|
2136
|
+
}
|
|
2137
|
+
});
|
|
2138
|
+
function hasChanges(patch) {
|
|
2139
|
+
return patch.type !== void 0 || patch.host !== void 0 || patch.dir !== void 0 || patch.protocol !== void 0;
|
|
2140
|
+
}
|
|
2141
|
+
function mergeForge(current, currentProtocol, patch, resultType) {
|
|
2142
|
+
const protocol = resultType === "git" ? patch.protocol ?? currentProtocol : void 0;
|
|
2143
|
+
return {
|
|
2144
|
+
type: resultType,
|
|
2145
|
+
host: patch.host ?? current.host,
|
|
2146
|
+
dir: patch.dir ?? current.dir,
|
|
2147
|
+
...protocol ? { protocol } : {}
|
|
2148
|
+
};
|
|
2149
|
+
}
|
|
2150
|
+
function fail$1(message) {
|
|
2151
|
+
consola.error(message);
|
|
2152
|
+
process.exitCode = 1;
|
|
2153
|
+
}
|
|
2154
|
+
function abort$1() {
|
|
2155
|
+
consola.info("Aborted — nothing changed.");
|
|
2156
|
+
}
|
|
2157
|
+
function invalidType(value) {
|
|
2158
|
+
return `Invalid type "${value}". Expected one of: ${FORGE_TYPES.join(", ")}.`;
|
|
2159
|
+
}
|
|
2160
|
+
function invalidProtocol(value) {
|
|
2161
|
+
return `Invalid protocol "${value}". Expected one of: ${GIT_PROTOCOLS.join(", ")}.`;
|
|
2162
|
+
}
|
|
2163
|
+
//#endregion
|
|
2164
|
+
//#region src/commands/forge/remove.ts
|
|
2165
|
+
var LEAVE_UNSET = "— leave unset —";
|
|
2166
|
+
var forgeRemoveCommand = defineCommand({
|
|
2167
|
+
meta: {
|
|
2168
|
+
name: "remove",
|
|
2169
|
+
description: "Remove a forge from the config"
|
|
2170
|
+
},
|
|
2171
|
+
args: {
|
|
2172
|
+
key: {
|
|
2173
|
+
type: "positional",
|
|
2174
|
+
required: false,
|
|
2175
|
+
description: "Forge key to remove"
|
|
2176
|
+
},
|
|
2177
|
+
default: {
|
|
2178
|
+
type: "string",
|
|
2179
|
+
description: "When removing the default forge, reassign the default to this"
|
|
2180
|
+
},
|
|
2181
|
+
config: {
|
|
2182
|
+
type: "string",
|
|
2183
|
+
description: "Path to the forgemap config file to modify"
|
|
2184
|
+
},
|
|
2185
|
+
yes: {
|
|
2186
|
+
type: "boolean",
|
|
2187
|
+
description: "Skip the confirmation prompt",
|
|
2188
|
+
default: false
|
|
2189
|
+
}
|
|
2190
|
+
},
|
|
2191
|
+
async run({ args }) {
|
|
2192
|
+
const loaded = await loadForgeMapConfig({ configFile: args.config });
|
|
2193
|
+
const tty = interactive();
|
|
2194
|
+
const file = existingConfigFile(loaded, args.config);
|
|
2195
|
+
if (!file) {
|
|
2196
|
+
process.exitCode = 1;
|
|
2197
|
+
return;
|
|
2198
|
+
}
|
|
2199
|
+
const forges = loaded.config.forges;
|
|
2200
|
+
let key = typeof args.key === "string" ? args.key.trim() : "";
|
|
2201
|
+
if (!key && tty) {
|
|
2202
|
+
const answer = await promptSelect("Which forge should be removed?", Object.keys(forges));
|
|
2203
|
+
if (answer === null) return abort();
|
|
2204
|
+
key = answer;
|
|
2205
|
+
}
|
|
2206
|
+
if (!key) return fail("Missing forge key. Pass it as an argument.");
|
|
2207
|
+
if (!(key in forges)) return fail(`No forge "${key}" in ${file}. Configured: ${Object.keys(forges).join(", ")}.`);
|
|
2208
|
+
const remaining = Object.keys(forges).filter((k) => k !== key);
|
|
2209
|
+
let newDefault;
|
|
2210
|
+
if (loaded.config.defaultForge === key && remaining.length > 0) if (typeof args.default === "string") {
|
|
2211
|
+
if (!remaining.includes(args.default)) return fail(`Cannot set default to "${args.default}" — not a remaining forge (${remaining.join(", ")}).`);
|
|
2212
|
+
newDefault = args.default;
|
|
2213
|
+
} else if (tty) {
|
|
2214
|
+
const answer = await promptSelect(`"${key}" is the default forge. Pick a new default:`, [...remaining, LEAVE_UNSET]);
|
|
2215
|
+
if (answer === null) return abort();
|
|
2216
|
+
if (answer !== LEAVE_UNSET) newDefault = answer;
|
|
2217
|
+
} else consola.warn(`Removing the default forge "${key}"; defaultForge now points at a missing forge. Pass --default to reassign it.`);
|
|
2218
|
+
consola.info(`Remove forge "${key}" from ${file}${newDefault ? ` (new default: "${newDefault}")` : ""}`);
|
|
2219
|
+
if (tty && !args.yes && !await confirmChange("Apply this change?")) return abort();
|
|
2220
|
+
if (await applyChange(file, (c) => {
|
|
2221
|
+
removeForge(c, key);
|
|
2222
|
+
if (newDefault) setDefaultForge(c, newDefault);
|
|
2223
|
+
}, () => consola.log(`Remove the "${key}" entry from \`forges\` in ${file}.`))) consola.success(`Removed forge "${key}" from ${file}`);
|
|
2224
|
+
else process.exitCode = 1;
|
|
2225
|
+
}
|
|
2226
|
+
});
|
|
2227
|
+
function fail(message) {
|
|
2228
|
+
consola.error(message);
|
|
2229
|
+
process.exitCode = 1;
|
|
2230
|
+
}
|
|
2231
|
+
function abort() {
|
|
2232
|
+
consola.info("Aborted — nothing changed.");
|
|
2233
|
+
}
|
|
2234
|
+
//#endregion
|
|
2235
|
+
//#region src/commands/forge/index.ts
|
|
2236
|
+
var forgeCommand = defineCommand({
|
|
2237
|
+
meta: {
|
|
2238
|
+
name: "forge",
|
|
2239
|
+
description: "Add, remove or edit forges in the config"
|
|
2240
|
+
},
|
|
2241
|
+
subCommands: {
|
|
2242
|
+
add: forgeAddCommand,
|
|
2243
|
+
remove: forgeRemoveCommand,
|
|
2244
|
+
edit: forgeEditCommand
|
|
2245
|
+
}
|
|
2246
|
+
});
|
|
2247
|
+
//#endregion
|
|
2248
|
+
//#region src/repos/import.ts
|
|
2249
|
+
async function listDirs(path) {
|
|
2250
|
+
try {
|
|
2251
|
+
return (await readdir(path, { withFileTypes: true })).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
|
|
2252
|
+
} catch (error) {
|
|
2253
|
+
if (error.code === "ENOENT") return [];
|
|
2254
|
+
throw error;
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
/**
|
|
2258
|
+
* Structure-driven depth-3 walk of `<path>/<serverDir>/<owner>/<repo>`.
|
|
2259
|
+
* Unlike `scanRepos`, this is config-free: every top-level directory is a
|
|
2260
|
+
* candidate server dir, and the names are discovered rather than configured.
|
|
2261
|
+
*/
|
|
2262
|
+
async function discoverForgemapLayout(path) {
|
|
2263
|
+
const repos = [];
|
|
2264
|
+
for (const serverDir of await listDirs(path)) {
|
|
2265
|
+
const serverPath = join(path, serverDir);
|
|
2266
|
+
for (const owner of await listDirs(serverPath)) {
|
|
2267
|
+
const ownerPath = join(serverPath, owner);
|
|
2268
|
+
for (const repo of await listDirs(ownerPath)) repos.push({
|
|
2269
|
+
serverDir,
|
|
2270
|
+
owner,
|
|
2271
|
+
repo,
|
|
2272
|
+
localPath: join(ownerPath, repo)
|
|
2273
|
+
});
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
return repos;
|
|
2277
|
+
}
|
|
2278
|
+
function forgeTypeForHost(host) {
|
|
2279
|
+
return host === "github.com" ? "github" : "git";
|
|
2280
|
+
}
|
|
2281
|
+
/**
|
|
2282
|
+
* Derive a `root` + one forge per server dir from the analyzed reports.
|
|
2283
|
+
* Host (and therefore type) come from the dominant origin host of the repos
|
|
2284
|
+
* under each server dir.
|
|
2285
|
+
*/
|
|
2286
|
+
function deriveConfig(reports, path) {
|
|
2287
|
+
const forges = {};
|
|
2288
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2289
|
+
const byServer = /* @__PURE__ */ new Map();
|
|
2290
|
+
for (const report of reports) {
|
|
2291
|
+
const list = byServer.get(report.repo.serverDir);
|
|
2292
|
+
if (list) list.push(report);
|
|
2293
|
+
else byServer.set(report.repo.serverDir, [report]);
|
|
2294
|
+
}
|
|
2295
|
+
for (const [serverDir, group] of byServer) {
|
|
2296
|
+
counts.set(serverDir, group.length);
|
|
2297
|
+
const hostTally = /* @__PURE__ */ new Map();
|
|
2298
|
+
for (const report of group) if (report.originHost) hostTally.set(report.originHost, (hostTally.get(report.originHost) ?? 0) + 1);
|
|
2299
|
+
const host = [...hostTally.entries()].sort((a, b) => b[1] - a[1])[0]?.[0] ?? "";
|
|
2300
|
+
forges[serverDir] = {
|
|
2301
|
+
type: host ? forgeTypeForHost(host) : "git",
|
|
2302
|
+
host,
|
|
2303
|
+
dir: serverDir
|
|
2304
|
+
};
|
|
2305
|
+
}
|
|
2306
|
+
return {
|
|
2307
|
+
root: path,
|
|
1792
2308
|
defaultForge: Object.keys(forges).slice().sort((a, b) => {
|
|
1793
2309
|
const aGh = forges[a].type === "github" ? 1 : 0;
|
|
1794
2310
|
const bGh = forges[b].type === "github" ? 1 : 0;
|
|
@@ -2419,7 +2935,7 @@ var infoCommand = defineCommand({
|
|
|
2419
2935
|
async run({ args }) {
|
|
2420
2936
|
const binary = resolveBinary(process.argv[1]);
|
|
2421
2937
|
const info = {
|
|
2422
|
-
version: "0.
|
|
2938
|
+
version: "0.6.0",
|
|
2423
2939
|
build: detectBuild(binary.resolved),
|
|
2424
2940
|
binary,
|
|
2425
2941
|
node: process.version,
|
|
@@ -2433,89 +2949,176 @@ var infoCommand = defineCommand({
|
|
|
2433
2949
|
}
|
|
2434
2950
|
});
|
|
2435
2951
|
//#endregion
|
|
2436
|
-
//#region src/repos/
|
|
2952
|
+
//#region src/repos/filter.ts
|
|
2953
|
+
var FLAG = "--filter";
|
|
2437
2954
|
/**
|
|
2438
|
-
*
|
|
2439
|
-
*
|
|
2440
|
-
* what makes a query rank identically no matter which command runs it.
|
|
2955
|
+
* Shared `--filter` option for the commands that enumerate repos
|
|
2956
|
+
* (`status`, `sync`, `list`), so the flag reads identically everywhere.
|
|
2441
2957
|
*/
|
|
2442
|
-
var
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
"owner",
|
|
2446
|
-
"repo"
|
|
2447
|
-
],
|
|
2448
|
-
threshold: .3,
|
|
2449
|
-
ignoreLocation: true
|
|
2958
|
+
var filterArg = {
|
|
2959
|
+
type: "string",
|
|
2960
|
+
description: "Restrict to repos whose owner or forge name matches. Repeatable; a repo passes if it matches any value."
|
|
2450
2961
|
};
|
|
2451
|
-
function createRepoFuse(repos) {
|
|
2452
|
-
return new Fuse(repos, REPO_FUSE_OPTIONS);
|
|
2453
|
-
}
|
|
2454
|
-
/** Fuzzy-match `query` against scanned repos, best match first. */
|
|
2455
|
-
function matchRepos(repos, query, limit) {
|
|
2456
|
-
return createRepoFuse(repos).search(query, limit ? { limit } : void 0).map((r) => r.item);
|
|
2457
|
-
}
|
|
2458
|
-
//#endregion
|
|
2459
|
-
//#region src/repos/picker.ts
|
|
2460
2962
|
/**
|
|
2461
|
-
*
|
|
2462
|
-
* (undefined when the user cancels).
|
|
2463
|
-
*
|
|
2464
|
-
* `$(forgemap pick)` / `$(forgemap path <q>)` captures stdout, so the
|
|
2465
|
-
* interactive TUI must not go there. consola/clack writes the UI to stdout AND
|
|
2466
|
-
* reads stdout.rows/columns for layout — but a captured stdout is a pipe (no
|
|
2467
|
-
* rows → nothing renders). So for the duration of the prompt: route stdout
|
|
2468
|
-
* writes to stderr (the real TTY) and borrow stderr's dimensions, then
|
|
2469
|
-
* restore. stdout stays clean for the chosen path only.
|
|
2963
|
+
* Recover every `--filter` occurrence from the raw argv.
|
|
2470
2964
|
*
|
|
2471
|
-
*
|
|
2472
|
-
*
|
|
2965
|
+
* citty (0.2.2) parses through `node:util` `parseArgs` and never sets
|
|
2966
|
+
* `multiple: true`, so Node keeps only the **last** value of a repeated option:
|
|
2967
|
+
* `--filter a --filter b` reaches `args.filter` as `'b'`, silently dropping
|
|
2968
|
+
* `a`. The raw argv is the only place the full list survives.
|
|
2473
2969
|
*/
|
|
2474
|
-
|
|
2475
|
-
const
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
}
|
|
2487
|
-
|
|
2488
|
-
const restore = (key) => {
|
|
2489
|
-
if (saved[key]) Object.defineProperty(out, key, saved[key]);
|
|
2490
|
-
else delete out[key];
|
|
2491
|
-
};
|
|
2492
|
-
out.write = process.stderr.write.bind(process.stderr);
|
|
2493
|
-
fake("rows", process.stderr.rows ?? 24);
|
|
2494
|
-
fake("columns", process.stderr.columns ?? 80);
|
|
2495
|
-
fake("isTTY", true);
|
|
2496
|
-
let choice;
|
|
2497
|
-
try {
|
|
2498
|
-
choice = await consola.prompt("Select a repo", {
|
|
2499
|
-
type: "select",
|
|
2500
|
-
options: candidates.map((r) => ({
|
|
2501
|
-
label: `${colors.gray(`${r.forgeName}:`)}${r.slug}`,
|
|
2502
|
-
value: r.localPath,
|
|
2503
|
-
hint: r.localPath
|
|
2504
|
-
}))
|
|
2505
|
-
});
|
|
2506
|
-
} finally {
|
|
2507
|
-
out.write = realWrite;
|
|
2508
|
-
restore("rows");
|
|
2509
|
-
restore("columns");
|
|
2510
|
-
restore("isTTY");
|
|
2970
|
+
function collectFilterArgs(rawArgs) {
|
|
2971
|
+
const values = [];
|
|
2972
|
+
for (let i = 0; i < rawArgs.length; i++) {
|
|
2973
|
+
const arg = rawArgs[i];
|
|
2974
|
+
if (arg === "--") break;
|
|
2975
|
+
if (arg === FLAG) {
|
|
2976
|
+
const next = rawArgs[i + 1];
|
|
2977
|
+
if (next !== void 0 && !next.startsWith("-")) {
|
|
2978
|
+
values.push(next);
|
|
2979
|
+
i++;
|
|
2980
|
+
}
|
|
2981
|
+
continue;
|
|
2982
|
+
}
|
|
2983
|
+
if (arg.startsWith(`${FLAG}=`)) values.push(arg.slice(9));
|
|
2511
2984
|
}
|
|
2512
|
-
return
|
|
2985
|
+
return values;
|
|
2513
2986
|
}
|
|
2514
|
-
/**
|
|
2515
|
-
|
|
2516
|
-
|
|
2987
|
+
/**
|
|
2988
|
+
* Normalize the shapes a filter value arrives in — absent, a single string, or
|
|
2989
|
+
* a list — into a list, dropping blanks (`--filter ''`).
|
|
2990
|
+
*/
|
|
2991
|
+
function normalizeFilters(value) {
|
|
2992
|
+
if (value === void 0) return [];
|
|
2993
|
+
return (Array.isArray(value) ? value : [value]).map((v) => v.trim()).filter((v) => v.length > 0);
|
|
2994
|
+
}
|
|
2995
|
+
/**
|
|
2996
|
+
* The filter values for a run: the raw argv wins, since it is the only shape
|
|
2997
|
+
* that survives repetition. Fall back to the parsed value when the argv carries
|
|
2998
|
+
* no `--filter` at all, which is how the command is driven programmatically
|
|
2999
|
+
* (and in tests), where `rawArgs` may be empty.
|
|
3000
|
+
*/
|
|
3001
|
+
function resolveFilters(rawArgs, value) {
|
|
3002
|
+
const fromRawArgs = collectFilterArgs(rawArgs);
|
|
3003
|
+
return normalizeFilters(fromRawArgs.length > 0 ? fromRawArgs : value);
|
|
3004
|
+
}
|
|
3005
|
+
/**
|
|
3006
|
+
* Keep the repos matching any of `filters` (OR-combined). A value matches when
|
|
3007
|
+
* it equals the repo's owner or its forge name, compared case-insensitively —
|
|
3008
|
+
* forge and owner names are case-preserving but not case-significant. An empty
|
|
3009
|
+
* filter list is a no-op, so an unused flag never narrows the output.
|
|
3010
|
+
*/
|
|
3011
|
+
function filterRepos(repos, filters) {
|
|
3012
|
+
if (filters.length === 0) return repos;
|
|
3013
|
+
const wanted = new Set(filters.map((f) => f.toLowerCase()));
|
|
3014
|
+
return repos.filter((r) => wanted.has(r.owner.toLowerCase()) || wanted.has(r.forgeName.toLowerCase()));
|
|
2517
3015
|
}
|
|
2518
3016
|
//#endregion
|
|
3017
|
+
//#region src/repos/match.ts
|
|
3018
|
+
/**
|
|
3019
|
+
* The one Fuse configuration every fuzzy lookup shares — `list`, `pick`
|
|
3020
|
+
* and the fuzzy slug fallback in `path`/`open`. Keeping it in one place is
|
|
3021
|
+
* what makes a query rank identically no matter which command runs it.
|
|
3022
|
+
*/
|
|
3023
|
+
var REPO_FUSE_OPTIONS = {
|
|
3024
|
+
keys: [
|
|
3025
|
+
"slug",
|
|
3026
|
+
"owner",
|
|
3027
|
+
"repo"
|
|
3028
|
+
],
|
|
3029
|
+
threshold: .3,
|
|
3030
|
+
ignoreLocation: true
|
|
3031
|
+
};
|
|
3032
|
+
function createRepoFuse(repos) {
|
|
3033
|
+
return new Fuse(repos, REPO_FUSE_OPTIONS);
|
|
3034
|
+
}
|
|
3035
|
+
/** Fuzzy-match `query` against scanned repos, best match first. */
|
|
3036
|
+
function matchRepos(repos, query, limit) {
|
|
3037
|
+
return createRepoFuse(repos).search(query, limit ? { limit } : void 0).map((r) => r.item);
|
|
3038
|
+
}
|
|
3039
|
+
//#endregion
|
|
3040
|
+
//#region src/commands/list.ts
|
|
3041
|
+
function renderTree$1(repos) {
|
|
3042
|
+
const byForge = /* @__PURE__ */ new Map();
|
|
3043
|
+
for (const r of repos) {
|
|
3044
|
+
let owners = byForge.get(r.forgeName);
|
|
3045
|
+
if (!owners) {
|
|
3046
|
+
owners = /* @__PURE__ */ new Map();
|
|
3047
|
+
byForge.set(r.forgeName, owners);
|
|
3048
|
+
}
|
|
3049
|
+
const list = owners.get(r.owner);
|
|
3050
|
+
if (list) list.push(r);
|
|
3051
|
+
else owners.set(r.owner, [r]);
|
|
3052
|
+
}
|
|
3053
|
+
return formatTree(Array.from(byForge, ([forge, owners]) => ({
|
|
3054
|
+
text: colors.bold(forge),
|
|
3055
|
+
children: Array.from(owners, ([owner, items]) => ({
|
|
3056
|
+
text: owner,
|
|
3057
|
+
children: items.map((r) => ({ text: `${colors.cyan(r.repo)} ${colors.dim(r.localPath)}` }))
|
|
3058
|
+
}))
|
|
3059
|
+
})));
|
|
3060
|
+
}
|
|
3061
|
+
var listCommand = defineCommand({
|
|
3062
|
+
meta: {
|
|
3063
|
+
name: "list",
|
|
3064
|
+
description: "List cloned repos; with a query, fuzzy-match by owner/repo and print matches"
|
|
3065
|
+
},
|
|
3066
|
+
args: {
|
|
3067
|
+
query: {
|
|
3068
|
+
type: "positional",
|
|
3069
|
+
description: "Optional search term (matched fuzzily against <owner>/<repo>). Omit to list every repo.",
|
|
3070
|
+
required: false
|
|
3071
|
+
},
|
|
3072
|
+
format: {
|
|
3073
|
+
type: "string",
|
|
3074
|
+
description: "Output format: auto (default), pretty, path, or slug. auto picks pretty in a TTY, path when piped.",
|
|
3075
|
+
default: "auto"
|
|
3076
|
+
},
|
|
3077
|
+
filter: filterArg,
|
|
3078
|
+
limit: {
|
|
3079
|
+
type: "string",
|
|
3080
|
+
description: "Maximum number of matches to print (default: unlimited)"
|
|
3081
|
+
},
|
|
3082
|
+
config: {
|
|
3083
|
+
type: "string",
|
|
3084
|
+
description: "Path to forgemap.config.ts (overrides walk-up discovery)"
|
|
3085
|
+
}
|
|
3086
|
+
},
|
|
3087
|
+
async run({ args, rawArgs }) {
|
|
3088
|
+
const loaded = await loadForgeMapConfig({ configFile: args.config });
|
|
3089
|
+
const configDir = loaded.configFile ? dirname(loaded.configFile) : loaded.cwd;
|
|
3090
|
+
const repos = filterRepos(await scanRepos({
|
|
3091
|
+
config: loaded.config,
|
|
3092
|
+
configDir
|
|
3093
|
+
}), resolveFilters(rawArgs, args.filter));
|
|
3094
|
+
const limit = args.limit ? Number.parseInt(args.limit, 10) : void 0;
|
|
3095
|
+
const query = args.query ?? "";
|
|
3096
|
+
const items = matchRepos(repos, query, limit);
|
|
3097
|
+
const allowed = [
|
|
3098
|
+
"auto",
|
|
3099
|
+
"pretty",
|
|
3100
|
+
"path",
|
|
3101
|
+
"slug"
|
|
3102
|
+
];
|
|
3103
|
+
if (!allowed.includes(args.format)) {
|
|
3104
|
+
consola.error(`Invalid --format value "${args.format}". Allowed: ${allowed.join(", ")}.`);
|
|
3105
|
+
process.exitCode = 1;
|
|
3106
|
+
return;
|
|
3107
|
+
}
|
|
3108
|
+
const requested = args.format;
|
|
3109
|
+
const format = requested === "auto" ? process.stdout.isTTY ? "pretty" : "path" : requested;
|
|
3110
|
+
if (items.length === 0) {
|
|
3111
|
+
if (format === "pretty") consola.info(query ? `No matches for "${query}".` : "No repos found.");
|
|
3112
|
+
return;
|
|
3113
|
+
}
|
|
3114
|
+
if (format === "pretty") {
|
|
3115
|
+
process.stdout.write(`${renderTree$1(items)}\n`);
|
|
3116
|
+
return;
|
|
3117
|
+
}
|
|
3118
|
+
for (const item of items) process.stdout.write(`${format === "slug" ? item.slug : item.localPath}\n`);
|
|
3119
|
+
}
|
|
3120
|
+
});
|
|
3121
|
+
//#endregion
|
|
2519
3122
|
//#region src/slug/locate.ts
|
|
2520
3123
|
/**
|
|
2521
3124
|
* Turn user input into a repo location.
|
|
@@ -2712,7 +3315,7 @@ var pickCommand = defineCommand({
|
|
|
2712
3315
|
return;
|
|
2713
3316
|
}
|
|
2714
3317
|
if (!canPrompt()) {
|
|
2715
|
-
consola.error("pick requires an interactive terminal. Use `forgemap
|
|
3318
|
+
consola.error("pick requires an interactive terminal. Use `forgemap list` for non-interactive output.");
|
|
2716
3319
|
process.exitCode = 1;
|
|
2717
3320
|
return;
|
|
2718
3321
|
}
|
|
@@ -2721,152 +3324,6 @@ var pickCommand = defineCommand({
|
|
|
2721
3324
|
}
|
|
2722
3325
|
});
|
|
2723
3326
|
//#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
3327
|
//#region src/commands/shell-init.ts
|
|
2871
3328
|
/** Append (idempotently) a loader that, plus completion, sets up the shell so
|
|
2872
3329
|
* the user only has to re-source their rc file. */
|
|
@@ -2896,7 +3353,7 @@ ${name}() {
|
|
|
2896
3353
|
target=$(command forgemap pick) || return $?
|
|
2897
3354
|
else
|
|
2898
3355
|
local matches
|
|
2899
|
-
matches=$(command forgemap
|
|
3356
|
+
matches=$(command forgemap list "$1" --format path)
|
|
2900
3357
|
local count
|
|
2901
3358
|
count=$(printf '%s' "$matches" | grep -c '^/' || true)
|
|
2902
3359
|
if [ "$count" = "1" ]; then
|
|
@@ -2915,7 +3372,7 @@ ${name}() {
|
|
|
2915
3372
|
}
|
|
2916
3373
|
`;
|
|
2917
3374
|
}
|
|
2918
|
-
function renderFish(name) {
|
|
3375
|
+
function renderFish$1(name) {
|
|
2919
3376
|
return `# forgemap shell integration — drop into your ~/.config/fish/config.fish:
|
|
2920
3377
|
# forgemap shell-init fish | source
|
|
2921
3378
|
|
|
@@ -2926,7 +3383,7 @@ function ${name} --description "forgemap with cd interception"
|
|
|
2926
3383
|
if test (count $argv) -eq 0
|
|
2927
3384
|
set target (command forgemap pick); or return $status
|
|
2928
3385
|
else
|
|
2929
|
-
set matches (command forgemap
|
|
3386
|
+
set matches (command forgemap list $argv[1] --format path)
|
|
2930
3387
|
set count (count $matches)
|
|
2931
3388
|
if test $count -eq 1
|
|
2932
3389
|
set target $matches[1]
|
|
@@ -2978,7 +3435,7 @@ var shellInitCommand = defineCommand({
|
|
|
2978
3435
|
await install(requested, name);
|
|
2979
3436
|
return;
|
|
2980
3437
|
}
|
|
2981
|
-
const out = requested === "fish" ? renderFish(name) : renderPosix(name);
|
|
3438
|
+
const out = requested === "fish" ? renderFish$1(name) : renderPosix(name);
|
|
2982
3439
|
process.stdout.write(out);
|
|
2983
3440
|
}
|
|
2984
3441
|
});
|
|
@@ -3326,12 +3783,297 @@ function severitySymbol(severity) {
|
|
|
3326
3783
|
if (severity === "warn") return colors.yellow("!");
|
|
3327
3784
|
return colors.red("✗");
|
|
3328
3785
|
}
|
|
3786
|
+
var validateCommand = defineCommand({
|
|
3787
|
+
meta: {
|
|
3788
|
+
name: "validate",
|
|
3789
|
+
description: "Preflight: check the config schema, required CLI tools, and root directory"
|
|
3790
|
+
},
|
|
3791
|
+
args: {
|
|
3792
|
+
json: {
|
|
3793
|
+
type: "boolean",
|
|
3794
|
+
description: "Emit a machine-readable JSON report",
|
|
3795
|
+
default: false
|
|
3796
|
+
},
|
|
3797
|
+
config: {
|
|
3798
|
+
type: "string",
|
|
3799
|
+
description: "Path to forgemap.config.ts (overrides walk-up discovery)"
|
|
3800
|
+
}
|
|
3801
|
+
},
|
|
3802
|
+
async run({ args }) {
|
|
3803
|
+
const loaded = await loadForgeMapConfig({ configFile: args.config });
|
|
3804
|
+
const configDir = loaded.configFile ? dirname(loaded.configFile) : loaded.cwd;
|
|
3805
|
+
const checks = await runChecks(loaded.config, configDir);
|
|
3806
|
+
const ok = checks.every((c) => c.severity !== "fail");
|
|
3807
|
+
if (args.json) process.stdout.write(`${JSON.stringify({
|
|
3808
|
+
ok,
|
|
3809
|
+
checks
|
|
3810
|
+
}, null, 2)}\n`);
|
|
3811
|
+
else {
|
|
3812
|
+
for (const c of checks) process.stdout.write(`${severitySymbol(c.severity)} ${c.name.padEnd(22)} ${colors.dim(c.message)}\n`);
|
|
3813
|
+
process.stdout.write(`\n${ok ? colors.green("All checks passed.") : colors.red("Validation failed.")}\n`);
|
|
3814
|
+
}
|
|
3815
|
+
if (!ok) {
|
|
3816
|
+
process.exitCode = 1;
|
|
3817
|
+
return;
|
|
3818
|
+
}
|
|
3819
|
+
if (!loaded.configFile) consola.warn("No forgemap.config.ts found — using built-in defaults. Run `forgemap config init` to materialize one.");
|
|
3820
|
+
}
|
|
3821
|
+
});
|
|
3822
|
+
//#endregion
|
|
3823
|
+
//#region src/commands/completion.ts
|
|
3824
|
+
var SLUG_COMMANDS = [
|
|
3825
|
+
"clone",
|
|
3826
|
+
"cd",
|
|
3827
|
+
"path",
|
|
3828
|
+
"open",
|
|
3829
|
+
"list",
|
|
3830
|
+
"pick",
|
|
3831
|
+
"delete"
|
|
3832
|
+
];
|
|
3833
|
+
var STATIC_FLAG_VALUES = {
|
|
3834
|
+
list: { "--format": [
|
|
3835
|
+
"auto",
|
|
3836
|
+
"pretty",
|
|
3837
|
+
"path",
|
|
3838
|
+
"slug"
|
|
3839
|
+
] },
|
|
3840
|
+
status: { "--format": ["pretty", "json"] },
|
|
3841
|
+
import: {
|
|
3842
|
+
"--format": ["pretty", "json"],
|
|
3843
|
+
"--type": ["forgemap"]
|
|
3844
|
+
}
|
|
3845
|
+
};
|
|
3846
|
+
var SHELL_POSITIONAL = /* @__PURE__ */ new Set(["completion", "shell-init"]);
|
|
3847
|
+
/** Every forgemap command declares `args` as a plain object literal (or omits
|
|
3848
|
+
* it, like `config`), never a thunk — so no `Resolvable` unwrapping is needed. */
|
|
3849
|
+
function argsOf(cmd) {
|
|
3850
|
+
return cmd.args ?? {};
|
|
3851
|
+
}
|
|
3852
|
+
/** Flag names a command exposes, derived from its `defineCommand` args so a new
|
|
3853
|
+
* flag surfaces in completion automatically. Positionals are handled
|
|
3854
|
+
* separately; negatable booleans (those with a `negativeDescription`) also get
|
|
3855
|
+
* their `--no-<flag>` form. */
|
|
3856
|
+
function flagsOf(cmd) {
|
|
3857
|
+
const flags = [];
|
|
3858
|
+
for (const [name, def] of Object.entries(argsOf(cmd))) {
|
|
3859
|
+
if (def.type === "positional") continue;
|
|
3860
|
+
flags.push(`--${name}`);
|
|
3861
|
+
if (def.type === "boolean" && def.negativeDescription) flags.push(`--no-${name}`);
|
|
3862
|
+
}
|
|
3863
|
+
return flags;
|
|
3864
|
+
}
|
|
3865
|
+
/** The ordered subcommand registry, mirroring `rootCommand.subCommands` in
|
|
3866
|
+
* cli.ts. It is kept here rather than imported from cli.ts because that would
|
|
3867
|
+
* form a cycle (cli → completion → cli). Built lazily so the self-reference to
|
|
3868
|
+
* `completionCommand` resolves after the module finishes initializing. */
|
|
3869
|
+
function commandSpecs() {
|
|
3870
|
+
return [
|
|
3871
|
+
["clone", cloneCommand],
|
|
3872
|
+
["import", importCommand],
|
|
3873
|
+
["cleanup", cleanupCommand],
|
|
3874
|
+
["delete", deleteCommand],
|
|
3875
|
+
["cd", cdCommand],
|
|
3876
|
+
["path", pathCommand],
|
|
3877
|
+
["open", openCommand],
|
|
3878
|
+
["list", listCommand],
|
|
3879
|
+
["pick", pickCommand],
|
|
3880
|
+
["status", statusCommand],
|
|
3881
|
+
["sync", syncCommand],
|
|
3882
|
+
["validate", validateCommand],
|
|
3883
|
+
["info", infoCommand],
|
|
3884
|
+
["completion", completionCommand],
|
|
3885
|
+
["shell-init", shellInitCommand],
|
|
3886
|
+
["config", configCommand],
|
|
3887
|
+
["forge", forgeCommand]
|
|
3888
|
+
].map(([name, cmd]) => ({
|
|
3889
|
+
name,
|
|
3890
|
+
flags: flagsOf(cmd),
|
|
3891
|
+
flagValues: STATIC_FLAG_VALUES[name] ?? {},
|
|
3892
|
+
positionalValues: SHELL_POSITIONAL.has(name) ? [...SUPPORTED_SHELLS] : [],
|
|
3893
|
+
slugs: SLUG_COMMANDS.includes(name)
|
|
3894
|
+
}));
|
|
3895
|
+
}
|
|
3896
|
+
function flagValuePairs(specs) {
|
|
3897
|
+
return specs.flatMap((s) => Object.entries(s.flagValues).map(([flag, values]) => [
|
|
3898
|
+
s.name,
|
|
3899
|
+
flag,
|
|
3900
|
+
values
|
|
3901
|
+
]));
|
|
3902
|
+
}
|
|
3903
|
+
function renderBash(specs) {
|
|
3904
|
+
const names = specs.map((s) => s.name).join(" ");
|
|
3905
|
+
const valueArms = flagValuePairs(specs).map(([cmd, flag, values]) => ` ${cmd}:${flag}) COMPREPLY=( $(compgen -W "${values.join(" ")}" -- "$cur") ); return ;;`).join("\n");
|
|
3906
|
+
const flagArms = specs.filter((s) => s.flags.length > 0).map((s) => ` ${s.name}) flags="${s.flags.join(" ")}" ;;`).join("\n");
|
|
3907
|
+
const slugCmds = specs.filter((s) => s.slugs).map((s) => s.name);
|
|
3908
|
+
return `# forgemap bash completion — drop into your ~/.bashrc:
|
|
3909
|
+
# eval "$(forgemap completion bash)"
|
|
3910
|
+
_forgemap_completion() {
|
|
3911
|
+
local cur prev cmd flags
|
|
3912
|
+
COMPREPLY=()
|
|
3913
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
3914
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
3915
|
+
cmd="\${COMP_WORDS[1]}"
|
|
3916
|
+
|
|
3917
|
+
if [ "$COMP_CWORD" = "1" ]; then
|
|
3918
|
+
COMPREPLY=( $(compgen -W "${names}" -- "$cur") )
|
|
3919
|
+
return
|
|
3920
|
+
fi
|
|
3921
|
+
|
|
3922
|
+
# Values for flags with a fixed set (e.g. --format), keyed by "<cmd>:<flag>".
|
|
3923
|
+
case "$cmd:$prev" in
|
|
3924
|
+
${valueArms}
|
|
3925
|
+
esac
|
|
3926
|
+
|
|
3927
|
+
# Flag names for the current subcommand.
|
|
3928
|
+
if [[ "$cur" == -* ]]; then
|
|
3929
|
+
case "$cmd" in
|
|
3930
|
+
${flagArms}
|
|
3931
|
+
esac
|
|
3932
|
+
COMPREPLY=( $(compgen -W "$flags" -- "$cur") )
|
|
3933
|
+
return
|
|
3934
|
+
fi
|
|
3935
|
+
|
|
3936
|
+
# Positional values (repo slugs, or a shell name).
|
|
3937
|
+
case "$cmd" in
|
|
3938
|
+
${[slugCmds.length > 0 ? ` ${slugCmds.join("|")})
|
|
3939
|
+
local slugs
|
|
3940
|
+
slugs=$(forgemap list --format slug 2>/dev/null)
|
|
3941
|
+
COMPREPLY=( $(compgen -W "$slugs" -- "$cur") )
|
|
3942
|
+
;;` : "", ...specs.filter((s) => s.positionalValues.length > 0).map((s) => ` ${s.name}) COMPREPLY=( $(compgen -W "${s.positionalValues.join(" ")}" -- "$cur") ) ;;`)].filter(Boolean).join("\n")}
|
|
3943
|
+
esac
|
|
3944
|
+
}
|
|
3945
|
+
complete -F _forgemap_completion forgemap
|
|
3946
|
+
`;
|
|
3947
|
+
}
|
|
3948
|
+
function renderZsh(specs) {
|
|
3949
|
+
return `# forgemap zsh completion — drop into your ~/.zshrc:
|
|
3950
|
+
# eval "$(forgemap completion zsh)"
|
|
3951
|
+
_forgemap() {
|
|
3952
|
+
local -a subcommands
|
|
3953
|
+
subcommands=(${specs.map((s) => `'${s.name}'`).join(" ")})
|
|
3954
|
+
local cmd="\${words[2]}"
|
|
3955
|
+
local prev="\${words[CURRENT-1]}"
|
|
3956
|
+
local cur="\${words[CURRENT]}"
|
|
3957
|
+
|
|
3958
|
+
if (( CURRENT == 2 )); then
|
|
3959
|
+
_describe 'forgemap subcommand' subcommands
|
|
3960
|
+
return
|
|
3961
|
+
fi
|
|
3962
|
+
|
|
3963
|
+
# Values for flags with a fixed set (e.g. --format), keyed by "<cmd>:<flag>".
|
|
3964
|
+
case "$cmd:$prev" in
|
|
3965
|
+
${flagValuePairs(specs).map(([cmd, flag, values]) => ` ${cmd}:${flag}) compadd ${values.join(" ")}; return ;;`).join("\n")}
|
|
3966
|
+
esac
|
|
3967
|
+
|
|
3968
|
+
# Flag names for the current subcommand.
|
|
3969
|
+
if [[ "$cur" == -* ]]; then
|
|
3970
|
+
case "$cmd" in
|
|
3971
|
+
${specs.filter((s) => s.flags.length > 0).map((s) => ` ${s.name}) compadd -- ${s.flags.join(" ")}; return ;;`).join("\n")}
|
|
3972
|
+
esac
|
|
3973
|
+
return
|
|
3974
|
+
fi
|
|
3975
|
+
|
|
3976
|
+
# Positional values (repo slugs, or a shell name).
|
|
3977
|
+
case "$cmd" in
|
|
3978
|
+
${specs.filter((s) => s.slugs).map((s) => s.name).join("|")})
|
|
3979
|
+
local -a slugs
|
|
3980
|
+
slugs=("\${(@f)$(forgemap list --format slug 2>/dev/null)}")
|
|
3981
|
+
_describe 'slug' slugs
|
|
3982
|
+
;;
|
|
3983
|
+
${specs.filter((s) => s.positionalValues.length > 0).map((s) => ` ${s.name}) compadd ${s.positionalValues.join(" ")} ;;`).join("\n")}
|
|
3984
|
+
esac
|
|
3985
|
+
}
|
|
3986
|
+
compdef _forgemap forgemap
|
|
3987
|
+
`;
|
|
3988
|
+
}
|
|
3989
|
+
function renderFish(specs) {
|
|
3990
|
+
const names = specs.map((s) => s.name).join(" ");
|
|
3991
|
+
const flagLines = specs.flatMap((s) => s.flags.map((flag) => {
|
|
3992
|
+
const long = flag.replace(/^--/, "");
|
|
3993
|
+
const values = s.flagValues[flag];
|
|
3994
|
+
const valuePart = values ? ` -x -a '${values.join(" ")}'` : "";
|
|
3995
|
+
return `complete -c forgemap -n '__fish_seen_subcommand_from ${s.name}' -l ${long}${valuePart}`;
|
|
3996
|
+
})).join("\n");
|
|
3997
|
+
const shellCmds = specs.filter((s) => s.positionalValues.length > 0);
|
|
3998
|
+
return `# forgemap fish completion — drop into your ~/.config/fish/config.fish:
|
|
3999
|
+
# forgemap completion fish | source
|
|
4000
|
+
|
|
4001
|
+
# Subcommands (depth 1).
|
|
4002
|
+
complete -c forgemap -f -n '__fish_use_subcommand' -a '${names}'
|
|
4003
|
+
|
|
4004
|
+
# Flags per subcommand (with fixed value sets where applicable).
|
|
4005
|
+
${flagLines}
|
|
4006
|
+
|
|
4007
|
+
# Shell flavor for completion / shell-init (depth 2).
|
|
4008
|
+
function __forgemap_needs_shell
|
|
4009
|
+
set -l tokens (commandline -opc)
|
|
4010
|
+
set -l shell_cmds ${shellCmds.map((s) => `"${s.name}"`).join(" ")}
|
|
4011
|
+
if test (count $tokens) -ge 2; and contains $tokens[2] $shell_cmds
|
|
4012
|
+
return 0
|
|
4013
|
+
end
|
|
4014
|
+
return 1
|
|
4015
|
+
end
|
|
4016
|
+
complete -c forgemap -f -n '__forgemap_needs_shell' -a '${shellCmds[0]?.positionalValues.join(" ") ?? ""}'
|
|
4017
|
+
|
|
4018
|
+
# Slugs (depth 2) for commands that take one.
|
|
4019
|
+
function __forgemap_needs_slug
|
|
4020
|
+
set -l tokens (commandline -opc)
|
|
4021
|
+
set -l slug_cmds ${specs.filter((s) => s.slugs).map((s) => `"${s.name}"`).join(" ")}
|
|
4022
|
+
if test (count $tokens) -ge 2; and contains $tokens[2] $slug_cmds
|
|
4023
|
+
return 0
|
|
4024
|
+
end
|
|
4025
|
+
return 1
|
|
4026
|
+
end
|
|
4027
|
+
|
|
4028
|
+
complete -c forgemap -f -n '__forgemap_needs_slug' \\
|
|
4029
|
+
-a '(forgemap list --format slug 2>/dev/null)'
|
|
4030
|
+
`;
|
|
4031
|
+
}
|
|
4032
|
+
var completionCommand = defineCommand({
|
|
4033
|
+
meta: {
|
|
4034
|
+
name: "completion",
|
|
4035
|
+
description: "Print a shell completion script. Source via `eval \"$(forgemap completion)\"`."
|
|
4036
|
+
},
|
|
4037
|
+
args: {
|
|
4038
|
+
shell: {
|
|
4039
|
+
type: "positional",
|
|
4040
|
+
description: `Shell flavor (${SUPPORTED_SHELLS.join(", ")}). Auto-detected from $SHELL if omitted.`,
|
|
4041
|
+
required: false
|
|
4042
|
+
},
|
|
4043
|
+
install: {
|
|
4044
|
+
type: "boolean",
|
|
4045
|
+
description: "Append the completion loader to your shell's rc file (idempotent) instead of printing",
|
|
4046
|
+
default: false
|
|
4047
|
+
}
|
|
4048
|
+
},
|
|
4049
|
+
async run({ args }) {
|
|
4050
|
+
const requested = args.shell ?? detectShell();
|
|
4051
|
+
if (!SUPPORTED_SHELLS.includes(requested)) {
|
|
4052
|
+
consola.error(`Unsupported shell "${requested}". Supported: ${SUPPORTED_SHELLS.join(", ")}.`);
|
|
4053
|
+
process.exitCode = 1;
|
|
4054
|
+
return;
|
|
4055
|
+
}
|
|
4056
|
+
if (args.install) {
|
|
4057
|
+
const { status, rcFile } = await installRcBlock(requested, "completion", [requested === "fish" ? "forgemap completion fish | source" : `eval "$(forgemap completion ${requested})"`]);
|
|
4058
|
+
if (status === "present") consola.info(`forgemap completion already present in ${rcFile}.`);
|
|
4059
|
+
else {
|
|
4060
|
+
const verb = status === "updated" ? "Updated" : "Added";
|
|
4061
|
+
consola.success(`${verb} forgemap completion in ${rcFile}.`);
|
|
4062
|
+
consola.info(`Run \`source ${rcFile}\` or restart your shell to activate it.`);
|
|
4063
|
+
}
|
|
4064
|
+
return;
|
|
4065
|
+
}
|
|
4066
|
+
const specs = commandSpecs();
|
|
4067
|
+
const out = requested === "fish" ? renderFish(specs) : requested === "zsh" ? renderZsh(specs) : renderBash(specs);
|
|
4068
|
+
process.stdout.write(out);
|
|
4069
|
+
}
|
|
4070
|
+
});
|
|
3329
4071
|
//#endregion
|
|
3330
4072
|
//#region src/bin/forgemap.ts
|
|
3331
4073
|
runMain(defineCommand({
|
|
3332
4074
|
meta: {
|
|
3333
4075
|
name: "forgemap",
|
|
3334
|
-
version: "0.
|
|
4076
|
+
version: "0.6.0",
|
|
3335
4077
|
description: "Manage a local repo layout of the form <root>/<forge.dir>/<owner>/<repo>"
|
|
3336
4078
|
},
|
|
3337
4079
|
subCommands: {
|
|
@@ -3342,50 +4084,16 @@ runMain(defineCommand({
|
|
|
3342
4084
|
cd: cdCommand,
|
|
3343
4085
|
path: pathCommand,
|
|
3344
4086
|
open: openCommand,
|
|
3345
|
-
|
|
4087
|
+
list: listCommand,
|
|
3346
4088
|
pick: pickCommand,
|
|
3347
4089
|
status: statusCommand,
|
|
3348
4090
|
sync: syncCommand,
|
|
3349
|
-
validate:
|
|
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
|
-
}),
|
|
4091
|
+
validate: validateCommand,
|
|
3385
4092
|
info: infoCommand,
|
|
3386
4093
|
completion: completionCommand,
|
|
3387
4094
|
"shell-init": shellInitCommand,
|
|
3388
|
-
config: configCommand
|
|
4095
|
+
config: configCommand,
|
|
4096
|
+
forge: forgeCommand
|
|
3389
4097
|
}
|
|
3390
4098
|
}));
|
|
3391
4099
|
//#endregion
|