forgemap 0.5.0-dev.95-3a3ea5e → 0.5.0-dev.97-def4d25

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.
@@ -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",
@@ -1589,6 +1624,627 @@ var deleteCommand = defineCommand({
1589
1624
  }
1590
1625
  });
1591
1626
  //#endregion
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;
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;
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
1702
+ /**
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.
1715
+ */
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);
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");
1784
+ }
1785
+ return typeof choice === "string" && choice ? choice : void 0;
1786
+ }
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;
1821
+ }
1822
+ /**
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.
1830
+ */
1831
+ async function resolveAddTarget(explicit) {
1832
+ if (explicit) {
1833
+ const path = resolve(process.cwd(), explicit);
1834
+ return {
1835
+ path,
1836
+ create: !existsSync(path)
1837
+ };
1838
+ }
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
1592
2248
  //#region src/repos/import.ts
1593
2249
  async function listDirs(path) {
1594
2250
  try {
@@ -2279,7 +2935,7 @@ var infoCommand = defineCommand({
2279
2935
  async run({ args }) {
2280
2936
  const binary = resolveBinary(process.argv[1]);
2281
2937
  const info = {
2282
- version: "0.5.0-dev.95-3a3ea5e",
2938
+ version: "0.5.0-dev.97-def4d25",
2283
2939
  build: detectBuild(binary.resolved),
2284
2940
  binary,
2285
2941
  node: process.version,
@@ -2463,66 +3119,6 @@ var listCommand = defineCommand({
2463
3119
  }
2464
3120
  });
2465
3121
  //#endregion
2466
- //#region src/repos/picker.ts
2467
- /**
2468
- * Show the interactive repo picker and return the chosen local path
2469
- * (undefined when the user cancels).
2470
- *
2471
- * `$(forgemap pick)` / `$(forgemap path <q>)` captures stdout, so the
2472
- * interactive TUI must not go there. consola/clack writes the UI to stdout AND
2473
- * reads stdout.rows/columns for layout — but a captured stdout is a pipe (no
2474
- * rows → nothing renders). So for the duration of the prompt: route stdout
2475
- * writes to stderr (the real TTY) and borrow stderr's dimensions, then
2476
- * restore. stdout stays clean for the chosen path only.
2477
- *
2478
- * Callers must check {@link canPrompt} first — without a TTY on stdin there is
2479
- * nobody to answer.
2480
- */
2481
- async function promptRepoChoice(candidates) {
2482
- const out = process.stdout;
2483
- const realWrite = out.write;
2484
- const saved = {
2485
- rows: Object.getOwnPropertyDescriptor(out, "rows"),
2486
- columns: Object.getOwnPropertyDescriptor(out, "columns"),
2487
- isTTY: Object.getOwnPropertyDescriptor(out, "isTTY")
2488
- };
2489
- const fake = (key, value) => {
2490
- Object.defineProperty(out, key, {
2491
- configurable: true,
2492
- value
2493
- });
2494
- };
2495
- const restore = (key) => {
2496
- if (saved[key]) Object.defineProperty(out, key, saved[key]);
2497
- else delete out[key];
2498
- };
2499
- out.write = process.stderr.write.bind(process.stderr);
2500
- fake("rows", process.stderr.rows ?? 24);
2501
- fake("columns", process.stderr.columns ?? 80);
2502
- fake("isTTY", true);
2503
- let choice;
2504
- try {
2505
- choice = await consola.prompt("Select a repo", {
2506
- type: "select",
2507
- options: candidates.map((r) => ({
2508
- label: `${colors.gray(`${r.forgeName}:`)}${r.slug}`,
2509
- value: r.localPath,
2510
- hint: r.localPath
2511
- }))
2512
- });
2513
- } finally {
2514
- out.write = realWrite;
2515
- restore("rows");
2516
- restore("columns");
2517
- restore("isTTY");
2518
- }
2519
- return typeof choice === "string" && choice ? choice : void 0;
2520
- }
2521
- /** Whether an interactive prompt can be shown at all. */
2522
- function canPrompt() {
2523
- return Boolean(process.stdin.isTTY);
2524
- }
2525
- //#endregion
2526
3122
  //#region src/slug/locate.ts
2527
3123
  /**
2528
3124
  * Turn user input into a repo location.
@@ -3287,7 +3883,8 @@ function commandSpecs() {
3287
3883
  ["info", infoCommand],
3288
3884
  ["completion", completionCommand],
3289
3885
  ["shell-init", shellInitCommand],
3290
- ["config", configCommand]
3886
+ ["config", configCommand],
3887
+ ["forge", forgeCommand]
3291
3888
  ].map(([name, cmd]) => ({
3292
3889
  name,
3293
3890
  flags: flagsOf(cmd),
@@ -3476,7 +4073,7 @@ var completionCommand = defineCommand({
3476
4073
  runMain(defineCommand({
3477
4074
  meta: {
3478
4075
  name: "forgemap",
3479
- version: "0.5.0-dev.95-3a3ea5e",
4076
+ version: "0.5.0-dev.97-def4d25",
3480
4077
  description: "Manage a local repo layout of the form <root>/<forge.dir>/<owner>/<repo>"
3481
4078
  },
3482
4079
  subCommands: {
@@ -3495,7 +4092,8 @@ runMain(defineCommand({
3495
4092
  info: infoCommand,
3496
4093
  completion: completionCommand,
3497
4094
  "shell-init": shellInitCommand,
3498
- config: configCommand
4095
+ config: configCommand,
4096
+ forge: forgeCommand
3499
4097
  }
3500
4098
  }));
3501
4099
  //#endregion