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.
- package/dist/bin/forgemap.mjs +663 -65
- package/dist/bin/forgemap.mjs.map +1 -1
- package/package.json +2 -1
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",
|
|
@@ -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.
|
|
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.
|
|
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
|