@kaynooo/cli-doctor 0.9.0 → 0.11.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/dist/cli-bin.js +104 -34
- package/dist/cli.js +104 -34
- package/package.json +2 -2
package/dist/cli-bin.js
CHANGED
|
@@ -481,7 +481,6 @@ var DASHES = /^-+/;
|
|
|
481
481
|
function jsonOption(holds) {
|
|
482
482
|
return { type: "boolean", description: holds === undefined ? "print JSON" : `print JSON: ${holds}` };
|
|
483
483
|
}
|
|
484
|
-
|
|
485
484
|
class Exit extends Error {
|
|
486
485
|
code;
|
|
487
486
|
hint;
|
|
@@ -1582,9 +1581,20 @@ var KEY_SEQUENCES = /\x1B\[[0-9;]*[A-Z~]|\x1BO[A-Z]|[\s\S]/giu;
|
|
|
1582
1581
|
var CONTROL = /^[\x00-\x1F\x7F]/;
|
|
1583
1582
|
var WORDS = /\s+/;
|
|
1584
1583
|
var MAX_ROWS = 10;
|
|
1585
|
-
var
|
|
1586
|
-
|
|
1587
|
-
|
|
1584
|
+
var PASTE_START = "\x1B[200~";
|
|
1585
|
+
var PASTE_END = "\x1B[201~";
|
|
1586
|
+
var KEYS = { "\x1B[A": "up", "\x1BOA": "up", "\x1B[B": "down", "\x1BOB": "down", "\x1B[5~": "pageup", "\x1B[6~": "pagedown", "\x1B[H": "home", "\x1B[1~": "home", "\x1BOH": "home", "\x1B[F": "end", "\x1B[4~": "end", "\x1BOF": "end", "\r": "enter", "\n": "enter", "\x03": "cancel", "\x1B": "cancel", "\x7F": "erase", "\b": "erase" };
|
|
1587
|
+
function readKeys(input, paste = { active: false }) {
|
|
1588
|
+
return (input.match(KEY_SEQUENCES) ?? []).flatMap((sequence) => {
|
|
1589
|
+
if (sequence === PASTE_START || sequence === PASTE_END) {
|
|
1590
|
+
paste.active = sequence === PASTE_START;
|
|
1591
|
+
return [];
|
|
1592
|
+
}
|
|
1593
|
+
if (paste.active && (sequence === "\r" || sequence === `
|
|
1594
|
+
`))
|
|
1595
|
+
return [];
|
|
1596
|
+
return KEYS[sequence] ?? (CONTROL.test(sequence) ? [] : [{ text: sequence }]);
|
|
1597
|
+
});
|
|
1588
1598
|
}
|
|
1589
1599
|
function flip(checked, value) {
|
|
1590
1600
|
const next = new Set(checked);
|
|
@@ -1615,6 +1625,22 @@ async function checklist(question, choices, { initial = new Set, min = 0, toggle
|
|
|
1615
1625
|
});
|
|
1616
1626
|
return accepted ? checked : undefined;
|
|
1617
1627
|
}
|
|
1628
|
+
async function confirm(question, { default: fallback, ...settings } = {}) {
|
|
1629
|
+
let answered = fallback;
|
|
1630
|
+
const choices = { true: "Y/n", false: "y/N", undefined: "y/n" }[String(fallback)];
|
|
1631
|
+
const accepted = await ask(question, settings, (colors, state) => [state === "asking" ? asking(colors, question, "", choices) : answer(colors, question, state, answered ? "yes" : "no")], (key) => {
|
|
1632
|
+
if (key === "cancel")
|
|
1633
|
+
return "cancel";
|
|
1634
|
+
if (key === "enter")
|
|
1635
|
+
return answered === undefined ? undefined : "enter";
|
|
1636
|
+
const letter = typeof key === "object" ? key.text.toLowerCase() : "";
|
|
1637
|
+
if (letter !== "y" && letter !== "n")
|
|
1638
|
+
return;
|
|
1639
|
+
answered = letter === "y";
|
|
1640
|
+
return "enter";
|
|
1641
|
+
});
|
|
1642
|
+
return accepted ? answered : undefined;
|
|
1643
|
+
}
|
|
1618
1644
|
async function input2(question, { default: fallback, mask = false, validate, ...settings } = {}) {
|
|
1619
1645
|
let value = "";
|
|
1620
1646
|
let invalid;
|
|
@@ -1667,6 +1693,12 @@ class List {
|
|
|
1667
1693
|
this.cursor = (this.cursor + (key === "up" ? this.matches.length - 1 : 1)) % this.matches.length;
|
|
1668
1694
|
return;
|
|
1669
1695
|
}
|
|
1696
|
+
if (key === "home" || key === "end" || key === "pageup" || key === "pagedown") {
|
|
1697
|
+
const last = this.matches.length - 1;
|
|
1698
|
+
const to = { home: 0, end: last, pageup: this.cursor - MAX_ROWS, pagedown: this.cursor + MAX_ROWS }[key];
|
|
1699
|
+
this.cursor = Math.max(0, Math.min(last, to));
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1670
1702
|
if (key === "erase")
|
|
1671
1703
|
this.filter([...this.query].slice(0, -1).join(""));
|
|
1672
1704
|
else if (typeof key === "object")
|
|
@@ -1708,22 +1740,29 @@ async function ask(question, { hint, terminal = process15 }, render, press) {
|
|
|
1708
1740
|
if (!isInteractive(terminal))
|
|
1709
1741
|
fail(`cannot ask "${question}" off a terminal`, 2, hint);
|
|
1710
1742
|
const colors = paint(stderr);
|
|
1711
|
-
let drawn =
|
|
1743
|
+
let drawn = [];
|
|
1744
|
+
let current = "asking";
|
|
1712
1745
|
let outcome;
|
|
1746
|
+
const columns = () => stderr.columns || 80;
|
|
1713
1747
|
const draw = (state) => {
|
|
1714
|
-
const frame = render(colors, state, { columns:
|
|
1715
|
-
|
|
1748
|
+
const frame = render(colors, state, { columns: columns(), rows: stderr.rows || 24 });
|
|
1749
|
+
const rows = drawn.reduce((sum, line) => sum + Math.max(1, Math.ceil(Bun.stringWidth(line) / columns())), 0);
|
|
1750
|
+
stderr.write(`${rows > 1 ? `\x1B[${rows - 1}A` : ""}\r\x1B[0J${frame.join(`
|
|
1716
1751
|
`)}`);
|
|
1717
|
-
drawn = frame
|
|
1752
|
+
drawn = frame;
|
|
1753
|
+
current = state;
|
|
1718
1754
|
};
|
|
1755
|
+
const redraw = () => draw(current);
|
|
1756
|
+
const paste = { active: false };
|
|
1719
1757
|
try {
|
|
1758
|
+
stderr.on?.("resize", redraw);
|
|
1720
1759
|
stdin.setRawMode(true);
|
|
1721
1760
|
stdin.resume();
|
|
1722
|
-
stderr.write("\x1B[?25l");
|
|
1761
|
+
stderr.write("\x1B[?25l\x1B[?2004h");
|
|
1723
1762
|
draw("asking");
|
|
1724
1763
|
return await new Promise((resolve) => {
|
|
1725
1764
|
function onData(data) {
|
|
1726
|
-
for (const key of readKeys(data.toString())) {
|
|
1765
|
+
for (const key of readKeys(data.toString(), paste)) {
|
|
1727
1766
|
const pressed = press(key);
|
|
1728
1767
|
if (pressed !== undefined) {
|
|
1729
1768
|
settle(pressed);
|
|
@@ -1745,9 +1784,10 @@ async function ask(question, { hint, terminal = process15 }, render, press) {
|
|
|
1745
1784
|
stdin.on("end", onEnd);
|
|
1746
1785
|
});
|
|
1747
1786
|
} finally {
|
|
1787
|
+
stderr.off?.("resize", redraw);
|
|
1748
1788
|
draw(outcome ?? "cancel");
|
|
1749
1789
|
stderr.write(`
|
|
1750
|
-
\x1B[?25h`);
|
|
1790
|
+
\x1B[?25h\x1B[?2004l`);
|
|
1751
1791
|
stdin.setRawMode(false);
|
|
1752
1792
|
stdin.pause();
|
|
1753
1793
|
}
|
|
@@ -1755,18 +1795,19 @@ async function ask(question, { hint, terminal = process15 }, render, press) {
|
|
|
1755
1795
|
|
|
1756
1796
|
// src/install.ts
|
|
1757
1797
|
var HOOKS = "hooks";
|
|
1798
|
+
var PLUGIN_UNINSTALL = ["claude", "plugin", "uninstall", "cli-hooks"];
|
|
1758
1799
|
var PLUGIN_INSTALL = ["claude", "plugin", "install", "cli-hooks", "--marketplace", `https://${FORGE}/kaynooo/cli.git`];
|
|
1759
1800
|
var CLI_PREFIX = /^cli-/;
|
|
1760
1801
|
var shortName = (name) => name.slice(name.indexOf("/") + 1).replace(CLI_PREFIX, "");
|
|
1761
1802
|
function candidates2(host) {
|
|
1762
|
-
const packages = Object.entries(TOOLS).
|
|
1803
|
+
const packages = Object.entries(TOOLS).map(([name, bins]) => {
|
|
1763
1804
|
const needs = (NEEDS[name] ?? []).map((need) => need.bin);
|
|
1764
|
-
return { id: name, names: [shortName(name), name, `cli-${shortName(name)}`, ...bins], label: shortName(name), hint: `${bins.join(" ")}: ${ABOUT[name] ?? ""}${needs.length === 0 ? "" : `, needs ${needs.join(", ")}`}`, packages: [name] };
|
|
1805
|
+
return { id: name, names: [shortName(name), name, `cli-${shortName(name)}`, ...bins], label: shortName(name), hint: `${bins.join(" ")}: ${ABOUT[name] ?? ""}${needs.length === 0 ? "" : `, needs ${needs.join(", ")}`}`, packages: [name], installed: isInstalled(host, name) };
|
|
1765
1806
|
});
|
|
1766
1807
|
const { plugin, mod } = hooksWiring(host);
|
|
1767
|
-
if (
|
|
1808
|
+
if (mod !== undefined && !plugin)
|
|
1768
1809
|
return packages;
|
|
1769
|
-
return [...packages, { id: HOOKS, names: [HOOKS, "plugin", "cli-hooks"], label: "hooks", hint: `the Claude Code plugin running guard on every tool call, needs ${HOOKS_REQUIRES}`, packages: isInstalled(host, HOOKS_REQUIRES) ? [] : [HOOKS_REQUIRES] }];
|
|
1810
|
+
return [...packages, { id: HOOKS, names: [HOOKS, "plugin", "cli-hooks"], label: "hooks", hint: `the Claude Code plugin running guard on every tool call, needs ${HOOKS_REQUIRES}`, packages: isInstalled(host, HOOKS_REQUIRES) ? [] : [HOOKS_REQUIRES], installed: plugin }];
|
|
1770
1811
|
}
|
|
1771
1812
|
function choose(pool, names) {
|
|
1772
1813
|
const chosen = new Set;
|
|
@@ -1803,12 +1844,21 @@ function plan(host, picked, force) {
|
|
|
1803
1844
|
function checksFor(packages, plugin) {
|
|
1804
1845
|
return [...packages.map((name) => name.slice(name.indexOf("/") + 1)), ...plugin ? ["hooks"] : [], "needs", "completions", ...packages.includes("@kaynooo/cli-forgejo") ? ["tea"] : []];
|
|
1805
1846
|
}
|
|
1806
|
-
async function ask2(
|
|
1847
|
+
async function ask2(all) {
|
|
1807
1848
|
if (!isInteractive())
|
|
1808
1849
|
badUsage("name what to install, or all: doctor --install forgejo check hooks (doctor --install --list shows the choices)");
|
|
1809
|
-
const choices =
|
|
1810
|
-
const picked = await checklist("
|
|
1811
|
-
|
|
1850
|
+
const choices = all.map((each) => ({ value: each, label: each.label, hint: `${each.installed ? "installed, " : ""}${each.hint}` }));
|
|
1851
|
+
const picked = await checklist("installed packages", choices, { initial: new Set(all.filter((each) => each.installed)), hint: "check to install, uncheck to remove: doctor --install forgejo check names them" });
|
|
1852
|
+
if (picked === undefined)
|
|
1853
|
+
return;
|
|
1854
|
+
const remove = all.filter((each) => !picked.has(each) && each.installed);
|
|
1855
|
+
if (remove.length > 0 && await confirm(`remove ${remove.map((each) => each.label).join(", ")}?`, { default: false }) !== true)
|
|
1856
|
+
return { add: all.filter((each) => picked.has(each) && !each.installed), remove: [] };
|
|
1857
|
+
return { add: all.filter((each) => picked.has(each) && !each.installed), remove };
|
|
1858
|
+
}
|
|
1859
|
+
function removal(all, remove) {
|
|
1860
|
+
const keepsGuard = all.some((each) => each.id === HOOKS && each.installed && !remove.includes(each));
|
|
1861
|
+
return { packages: [...new Set(remove.flatMap((each) => each.id === HOOKS ? [] : each.packages))].filter((name) => !(keepsGuard && name === HOOKS_REQUIRES)), plugin: remove.some((each) => each.id === HOOKS) };
|
|
1812
1862
|
}
|
|
1813
1863
|
async function askWorkspace(host) {
|
|
1814
1864
|
if (!isInteractive() || workspaceChosen({ home: host.home }))
|
|
@@ -1817,8 +1867,9 @@ async function askWorkspace(host) {
|
|
|
1817
1867
|
if (folder !== undefined && folder !== "" && !saveWorkspace(folder, { home: host.home }))
|
|
1818
1868
|
console.error(`could not write the workspace into ${userConfigPath("config", { home: host.home })}: it does not parse, fix it by hand`);
|
|
1819
1869
|
}
|
|
1820
|
-
async function install(host, options, recheck) {
|
|
1821
|
-
const
|
|
1870
|
+
async function install(host, options, recheck, choosing = ask2) {
|
|
1871
|
+
const all = candidates2(host);
|
|
1872
|
+
const pool = all.filter((each) => !each.installed);
|
|
1822
1873
|
const { dim, green, red, yellow } = paint(process16.stdout);
|
|
1823
1874
|
if (options.list) {
|
|
1824
1875
|
for (const each of pool)
|
|
@@ -1827,20 +1878,23 @@ async function install(host, options, recheck) {
|
|
|
1827
1878
|
console.log("everything is installed");
|
|
1828
1879
|
return 0;
|
|
1829
1880
|
}
|
|
1830
|
-
if (pool.length === 0) {
|
|
1881
|
+
if (pool.length === 0 && (options.names.length > 0 || !isInteractive())) {
|
|
1831
1882
|
console.log("everything is installed");
|
|
1832
1883
|
return 0;
|
|
1833
1884
|
}
|
|
1834
|
-
const unknown = options.names.filter((name) => name !== "all" && !
|
|
1885
|
+
const unknown = options.names.filter((name) => name !== "all" && !all.some((each) => each.names.includes(name)));
|
|
1835
1886
|
if (unknown.length > 0)
|
|
1836
1887
|
badUsage(`nothing to install is named ${unknown.join(", ")}: doctor --install --list`);
|
|
1837
|
-
const
|
|
1888
|
+
const chosen = options.names.length === 0 ? await choosing(all) : { add: choose(pool, options.names), remove: [] };
|
|
1889
|
+
const picked = chosen?.add ?? [];
|
|
1838
1890
|
const skipped = options.names.filter((name) => name !== "all" && !pool.some((each) => each.names.includes(name)));
|
|
1839
1891
|
for (const name of skipped)
|
|
1840
1892
|
console.log(`${dim("already")} ${name} is installed`);
|
|
1841
|
-
|
|
1893
|
+
const removing = removal(all, chosen?.remove ?? []);
|
|
1894
|
+
const removes = removing.packages.length > 0 || removing.plugin;
|
|
1895
|
+
if (picked.length === 0 && !removes) {
|
|
1842
1896
|
if (skipped.length === 0)
|
|
1843
|
-
console.log("nothing
|
|
1897
|
+
console.log("nothing changed");
|
|
1844
1898
|
return 0;
|
|
1845
1899
|
}
|
|
1846
1900
|
const planned = plan(host, picked, options.force);
|
|
@@ -1848,7 +1902,7 @@ async function install(host, options, recheck) {
|
|
|
1848
1902
|
console.log(`${red("blocked")} ${each.package} needs ${each.need} (${each.why}): ${each.how}; --force installs it anyway`);
|
|
1849
1903
|
for (const each of planned.warnings)
|
|
1850
1904
|
console.log(`${yellow("warn ")} ${each.package}: ${each.need} is missing, ${each.why}`);
|
|
1851
|
-
if (planned.packages.length === 0 && !planned.plugin)
|
|
1905
|
+
if (planned.packages.length === 0 && !planned.plugin && !removes)
|
|
1852
1906
|
return 1;
|
|
1853
1907
|
if (!options.dryRun && planned.packages.some((name) => name === "@kaynooo/cli-workspace" || name === "@kaynooo/cli-pkg"))
|
|
1854
1908
|
await askWorkspace(host);
|
|
@@ -1857,6 +1911,11 @@ async function install(host, options, recheck) {
|
|
|
1857
1911
|
console.log(`${options.dryRun ? dim("would ") : green("install")} ${command.join(" ")}`);
|
|
1858
1912
|
if (planned.plugin)
|
|
1859
1913
|
console.log(`${options.dryRun ? dim("would ") : green("install")} ${PLUGIN_INSTALL.join(" ")}`);
|
|
1914
|
+
const uninstall = ["bun", "remove", "-g", ...removing.packages];
|
|
1915
|
+
if (removing.packages.length > 0)
|
|
1916
|
+
console.log(`${options.dryRun ? dim("would ") : green("remove ")} ${uninstall.join(" ")}`);
|
|
1917
|
+
if (removing.plugin)
|
|
1918
|
+
console.log(`${options.dryRun ? dim("would ") : green("remove ")} ${PLUGIN_UNINSTALL.join(" ")}`);
|
|
1860
1919
|
if (options.dryRun)
|
|
1861
1920
|
return planned.blocked.length > 0 ? 1 : 0;
|
|
1862
1921
|
if (planned.packages.length > 0) {
|
|
@@ -1875,16 +1934,27 @@ async function install(host, options, recheck) {
|
|
|
1875
1934
|
console.error(`${red("failed")} ${PLUGIN_INSTALL.join(" ")}: exit ${ran.code}: ${firstLine(ran.stderr || ran.stdout)}`);
|
|
1876
1935
|
}
|
|
1877
1936
|
}
|
|
1937
|
+
if (removing.packages.length > 0) {
|
|
1938
|
+
const ran = await host.run(uninstall);
|
|
1939
|
+
if (ran.code !== 0) {
|
|
1940
|
+
console.error(`${red("failed")} bun remove -g: exit ${ran.code}: ${firstLine(ran.stderr || ran.stdout)}`);
|
|
1941
|
+
return 1;
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
if (removing.plugin && host.which("claude") === null) {
|
|
1945
|
+
console.error(`${yellow("not run")} ${PLUGIN_UNINSTALL.join(" ")}: claude is not on PATH`);
|
|
1946
|
+
} else if (removing.plugin) {
|
|
1947
|
+
const ran = await host.run(PLUGIN_UNINSTALL);
|
|
1948
|
+
if (ran.code !== 0) {
|
|
1949
|
+
console.error(`${red("failed")} ${PLUGIN_UNINSTALL.join(" ")}: exit ${ran.code}: ${firstLine(ran.stderr || ran.stdout)}`);
|
|
1950
|
+
return 1;
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1878
1953
|
console.log(`
|
|
1879
1954
|
after the install:`);
|
|
1880
1955
|
const code = await recheck(checksFor(planned.packages, planned.plugin));
|
|
1881
1956
|
return planned.blocked.length > 0 ? 1 : code;
|
|
1882
1957
|
}
|
|
1883
|
-
function candidatesAll(host) {
|
|
1884
|
-
const open = candidates2(host);
|
|
1885
|
-
const done = Object.entries(TOOLS).filter(([name]) => isInstalled(host, name)).map(([name, bins]) => ({ id: name, names: [shortName(name), name, `cli-${shortName(name)}`, ...bins], label: shortName(name), hint: "", packages: [name] }));
|
|
1886
|
-
return [...open, ...done];
|
|
1887
|
-
}
|
|
1888
1958
|
|
|
1889
1959
|
// src/doctor.ts
|
|
1890
1960
|
var USAGE = `doctor [--only <check>] [--quiet] [--fix | --install [<name>...|all]] [--list] [--dry-run] [--force] [--json]: checks this host's @kaynooo/cli-* install, one line per check, the command that fixes a failing one beside it, and installs the packages it picks
|
|
@@ -1901,7 +1971,7 @@ var OPTIONS = {
|
|
|
1901
1971
|
quiet: { type: "boolean", short: "q", description: "print only the failing checks" },
|
|
1902
1972
|
fix: { type: "boolean", description: "run the fix: lines doctor may run, then check again" },
|
|
1903
1973
|
"dry-run": { type: "boolean", description: "with --fix or --install, show what would run without running it (alone, it implies --fix)" },
|
|
1904
|
-
install: { type: "boolean", description: "install the packages named, or
|
|
1974
|
+
install: { type: "boolean", description: "install the packages named, or managed from a checklist, installed ones checked and unchecking removing them: forgejo, ft, hooks, all" },
|
|
1905
1975
|
list: { type: "boolean", description: "with --install, print the choices" },
|
|
1906
1976
|
force: { type: "boolean", description: "with --install, install a package even though a tool it requires is missing" }
|
|
1907
1977
|
};
|
|
@@ -1924,7 +1994,7 @@ function printOutcomes(outcomes, log) {
|
|
|
1924
1994
|
log(`${marks[status]} ${command}${detail === undefined ? "" : ` ${dim(detail)}`}`);
|
|
1925
1995
|
}
|
|
1926
1996
|
async function main(args, host = hostHere()) {
|
|
1927
|
-
const { values, positionals } = parse(args, USAGE, OPTIONS, { positionals: ["[name...]"], exits: EXITS, replaces: "checking each bin's version, tea's login and the hooks by hand", examples: ["doctor", ["doctor --json", "the checks for a script"], ["doctor -q", "silent unless something fails"], ["doctor --fix --dry-run", "what --fix would run"], ["doctor --install", "
|
|
1997
|
+
const { values, positionals } = parse(args, USAGE, OPTIONS, { positionals: ["[name...]"], exits: EXITS, replaces: "checking each bin's version, tea's login and the hooks by hand", examples: ["doctor", ["doctor --json", "the checks for a script"], ["doctor -q", "silent unless something fails"], ["doctor --fix --dry-run", "what --fix would run"], ["doctor --install", "check to install, uncheck to remove"], ["doctor --install forgejo check hooks", "three named ones"], ["doctor --install all --dry-run", "what installing everything would run"]] });
|
|
1928
1998
|
const only = values.only ?? [];
|
|
1929
1999
|
if (values.install !== true && (positionals.length > 0 || values.list === true || values.force === true))
|
|
1930
2000
|
badUsage("names, --list and --force go with --install");
|
package/dist/cli.js
CHANGED
|
@@ -481,7 +481,6 @@ var DASHES = /^-+/;
|
|
|
481
481
|
function jsonOption(holds) {
|
|
482
482
|
return { type: "boolean", description: holds === undefined ? "print JSON" : `print JSON: ${holds}` };
|
|
483
483
|
}
|
|
484
|
-
|
|
485
484
|
class Exit extends Error {
|
|
486
485
|
code;
|
|
487
486
|
hint;
|
|
@@ -1580,9 +1579,20 @@ var KEY_SEQUENCES = /\x1B\[[0-9;]*[A-Z~]|\x1BO[A-Z]|[\s\S]/giu;
|
|
|
1580
1579
|
var CONTROL = /^[\x00-\x1F\x7F]/;
|
|
1581
1580
|
var WORDS = /\s+/;
|
|
1582
1581
|
var MAX_ROWS = 10;
|
|
1583
|
-
var
|
|
1584
|
-
|
|
1585
|
-
|
|
1582
|
+
var PASTE_START = "\x1B[200~";
|
|
1583
|
+
var PASTE_END = "\x1B[201~";
|
|
1584
|
+
var KEYS = { "\x1B[A": "up", "\x1BOA": "up", "\x1B[B": "down", "\x1BOB": "down", "\x1B[5~": "pageup", "\x1B[6~": "pagedown", "\x1B[H": "home", "\x1B[1~": "home", "\x1BOH": "home", "\x1B[F": "end", "\x1B[4~": "end", "\x1BOF": "end", "\r": "enter", "\n": "enter", "\x03": "cancel", "\x1B": "cancel", "\x7F": "erase", "\b": "erase" };
|
|
1585
|
+
function readKeys(input, paste = { active: false }) {
|
|
1586
|
+
return (input.match(KEY_SEQUENCES) ?? []).flatMap((sequence) => {
|
|
1587
|
+
if (sequence === PASTE_START || sequence === PASTE_END) {
|
|
1588
|
+
paste.active = sequence === PASTE_START;
|
|
1589
|
+
return [];
|
|
1590
|
+
}
|
|
1591
|
+
if (paste.active && (sequence === "\r" || sequence === `
|
|
1592
|
+
`))
|
|
1593
|
+
return [];
|
|
1594
|
+
return KEYS[sequence] ?? (CONTROL.test(sequence) ? [] : [{ text: sequence }]);
|
|
1595
|
+
});
|
|
1586
1596
|
}
|
|
1587
1597
|
function flip(checked, value) {
|
|
1588
1598
|
const next = new Set(checked);
|
|
@@ -1613,6 +1623,22 @@ async function checklist(question, choices, { initial = new Set, min = 0, toggle
|
|
|
1613
1623
|
});
|
|
1614
1624
|
return accepted ? checked : undefined;
|
|
1615
1625
|
}
|
|
1626
|
+
async function confirm(question, { default: fallback, ...settings } = {}) {
|
|
1627
|
+
let answered = fallback;
|
|
1628
|
+
const choices = { true: "Y/n", false: "y/N", undefined: "y/n" }[String(fallback)];
|
|
1629
|
+
const accepted = await ask(question, settings, (colors, state) => [state === "asking" ? asking(colors, question, "", choices) : answer(colors, question, state, answered ? "yes" : "no")], (key) => {
|
|
1630
|
+
if (key === "cancel")
|
|
1631
|
+
return "cancel";
|
|
1632
|
+
if (key === "enter")
|
|
1633
|
+
return answered === undefined ? undefined : "enter";
|
|
1634
|
+
const letter = typeof key === "object" ? key.text.toLowerCase() : "";
|
|
1635
|
+
if (letter !== "y" && letter !== "n")
|
|
1636
|
+
return;
|
|
1637
|
+
answered = letter === "y";
|
|
1638
|
+
return "enter";
|
|
1639
|
+
});
|
|
1640
|
+
return accepted ? answered : undefined;
|
|
1641
|
+
}
|
|
1616
1642
|
async function input2(question, { default: fallback, mask = false, validate, ...settings } = {}) {
|
|
1617
1643
|
let value = "";
|
|
1618
1644
|
let invalid;
|
|
@@ -1665,6 +1691,12 @@ class List {
|
|
|
1665
1691
|
this.cursor = (this.cursor + (key === "up" ? this.matches.length - 1 : 1)) % this.matches.length;
|
|
1666
1692
|
return;
|
|
1667
1693
|
}
|
|
1694
|
+
if (key === "home" || key === "end" || key === "pageup" || key === "pagedown") {
|
|
1695
|
+
const last = this.matches.length - 1;
|
|
1696
|
+
const to = { home: 0, end: last, pageup: this.cursor - MAX_ROWS, pagedown: this.cursor + MAX_ROWS }[key];
|
|
1697
|
+
this.cursor = Math.max(0, Math.min(last, to));
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1668
1700
|
if (key === "erase")
|
|
1669
1701
|
this.filter([...this.query].slice(0, -1).join(""));
|
|
1670
1702
|
else if (typeof key === "object")
|
|
@@ -1706,22 +1738,29 @@ async function ask(question, { hint, terminal = process15 }, render, press) {
|
|
|
1706
1738
|
if (!isInteractive(terminal))
|
|
1707
1739
|
fail(`cannot ask "${question}" off a terminal`, 2, hint);
|
|
1708
1740
|
const colors = paint(stderr);
|
|
1709
|
-
let drawn =
|
|
1741
|
+
let drawn = [];
|
|
1742
|
+
let current = "asking";
|
|
1710
1743
|
let outcome;
|
|
1744
|
+
const columns = () => stderr.columns || 80;
|
|
1711
1745
|
const draw = (state) => {
|
|
1712
|
-
const frame = render(colors, state, { columns:
|
|
1713
|
-
|
|
1746
|
+
const frame = render(colors, state, { columns: columns(), rows: stderr.rows || 24 });
|
|
1747
|
+
const rows = drawn.reduce((sum, line) => sum + Math.max(1, Math.ceil(Bun.stringWidth(line) / columns())), 0);
|
|
1748
|
+
stderr.write(`${rows > 1 ? `\x1B[${rows - 1}A` : ""}\r\x1B[0J${frame.join(`
|
|
1714
1749
|
`)}`);
|
|
1715
|
-
drawn = frame
|
|
1750
|
+
drawn = frame;
|
|
1751
|
+
current = state;
|
|
1716
1752
|
};
|
|
1753
|
+
const redraw = () => draw(current);
|
|
1754
|
+
const paste = { active: false };
|
|
1717
1755
|
try {
|
|
1756
|
+
stderr.on?.("resize", redraw);
|
|
1718
1757
|
stdin.setRawMode(true);
|
|
1719
1758
|
stdin.resume();
|
|
1720
|
-
stderr.write("\x1B[?25l");
|
|
1759
|
+
stderr.write("\x1B[?25l\x1B[?2004h");
|
|
1721
1760
|
draw("asking");
|
|
1722
1761
|
return await new Promise((resolve) => {
|
|
1723
1762
|
function onData(data) {
|
|
1724
|
-
for (const key of readKeys(data.toString())) {
|
|
1763
|
+
for (const key of readKeys(data.toString(), paste)) {
|
|
1725
1764
|
const pressed = press(key);
|
|
1726
1765
|
if (pressed !== undefined) {
|
|
1727
1766
|
settle(pressed);
|
|
@@ -1743,9 +1782,10 @@ async function ask(question, { hint, terminal = process15 }, render, press) {
|
|
|
1743
1782
|
stdin.on("end", onEnd);
|
|
1744
1783
|
});
|
|
1745
1784
|
} finally {
|
|
1785
|
+
stderr.off?.("resize", redraw);
|
|
1746
1786
|
draw(outcome ?? "cancel");
|
|
1747
1787
|
stderr.write(`
|
|
1748
|
-
\x1B[?25h`);
|
|
1788
|
+
\x1B[?25h\x1B[?2004l`);
|
|
1749
1789
|
stdin.setRawMode(false);
|
|
1750
1790
|
stdin.pause();
|
|
1751
1791
|
}
|
|
@@ -1753,18 +1793,19 @@ async function ask(question, { hint, terminal = process15 }, render, press) {
|
|
|
1753
1793
|
|
|
1754
1794
|
// src/install.ts
|
|
1755
1795
|
var HOOKS = "hooks";
|
|
1796
|
+
var PLUGIN_UNINSTALL = ["claude", "plugin", "uninstall", "cli-hooks"];
|
|
1756
1797
|
var PLUGIN_INSTALL = ["claude", "plugin", "install", "cli-hooks", "--marketplace", `https://${FORGE}/kaynooo/cli.git`];
|
|
1757
1798
|
var CLI_PREFIX = /^cli-/;
|
|
1758
1799
|
var shortName = (name) => name.slice(name.indexOf("/") + 1).replace(CLI_PREFIX, "");
|
|
1759
1800
|
function candidates2(host) {
|
|
1760
|
-
const packages = Object.entries(TOOLS).
|
|
1801
|
+
const packages = Object.entries(TOOLS).map(([name, bins]) => {
|
|
1761
1802
|
const needs = (NEEDS[name] ?? []).map((need) => need.bin);
|
|
1762
|
-
return { id: name, names: [shortName(name), name, `cli-${shortName(name)}`, ...bins], label: shortName(name), hint: `${bins.join(" ")}: ${ABOUT[name] ?? ""}${needs.length === 0 ? "" : `, needs ${needs.join(", ")}`}`, packages: [name] };
|
|
1803
|
+
return { id: name, names: [shortName(name), name, `cli-${shortName(name)}`, ...bins], label: shortName(name), hint: `${bins.join(" ")}: ${ABOUT[name] ?? ""}${needs.length === 0 ? "" : `, needs ${needs.join(", ")}`}`, packages: [name], installed: isInstalled(host, name) };
|
|
1763
1804
|
});
|
|
1764
1805
|
const { plugin, mod } = hooksWiring(host);
|
|
1765
|
-
if (
|
|
1806
|
+
if (mod !== undefined && !plugin)
|
|
1766
1807
|
return packages;
|
|
1767
|
-
return [...packages, { id: HOOKS, names: [HOOKS, "plugin", "cli-hooks"], label: "hooks", hint: `the Claude Code plugin running guard on every tool call, needs ${HOOKS_REQUIRES}`, packages: isInstalled(host, HOOKS_REQUIRES) ? [] : [HOOKS_REQUIRES] }];
|
|
1808
|
+
return [...packages, { id: HOOKS, names: [HOOKS, "plugin", "cli-hooks"], label: "hooks", hint: `the Claude Code plugin running guard on every tool call, needs ${HOOKS_REQUIRES}`, packages: isInstalled(host, HOOKS_REQUIRES) ? [] : [HOOKS_REQUIRES], installed: plugin }];
|
|
1768
1809
|
}
|
|
1769
1810
|
function choose(pool, names) {
|
|
1770
1811
|
const chosen = new Set;
|
|
@@ -1801,12 +1842,21 @@ function plan(host, picked, force) {
|
|
|
1801
1842
|
function checksFor(packages, plugin) {
|
|
1802
1843
|
return [...packages.map((name) => name.slice(name.indexOf("/") + 1)), ...plugin ? ["hooks"] : [], "needs", "completions", ...packages.includes("@kaynooo/cli-forgejo") ? ["tea"] : []];
|
|
1803
1844
|
}
|
|
1804
|
-
async function ask2(
|
|
1845
|
+
async function ask2(all) {
|
|
1805
1846
|
if (!isInteractive())
|
|
1806
1847
|
badUsage("name what to install, or all: doctor --install forgejo check hooks (doctor --install --list shows the choices)");
|
|
1807
|
-
const choices =
|
|
1808
|
-
const picked = await checklist("
|
|
1809
|
-
|
|
1848
|
+
const choices = all.map((each) => ({ value: each, label: each.label, hint: `${each.installed ? "installed, " : ""}${each.hint}` }));
|
|
1849
|
+
const picked = await checklist("installed packages", choices, { initial: new Set(all.filter((each) => each.installed)), hint: "check to install, uncheck to remove: doctor --install forgejo check names them" });
|
|
1850
|
+
if (picked === undefined)
|
|
1851
|
+
return;
|
|
1852
|
+
const remove = all.filter((each) => !picked.has(each) && each.installed);
|
|
1853
|
+
if (remove.length > 0 && await confirm(`remove ${remove.map((each) => each.label).join(", ")}?`, { default: false }) !== true)
|
|
1854
|
+
return { add: all.filter((each) => picked.has(each) && !each.installed), remove: [] };
|
|
1855
|
+
return { add: all.filter((each) => picked.has(each) && !each.installed), remove };
|
|
1856
|
+
}
|
|
1857
|
+
function removal(all, remove) {
|
|
1858
|
+
const keepsGuard = all.some((each) => each.id === HOOKS && each.installed && !remove.includes(each));
|
|
1859
|
+
return { packages: [...new Set(remove.flatMap((each) => each.id === HOOKS ? [] : each.packages))].filter((name) => !(keepsGuard && name === HOOKS_REQUIRES)), plugin: remove.some((each) => each.id === HOOKS) };
|
|
1810
1860
|
}
|
|
1811
1861
|
async function askWorkspace(host) {
|
|
1812
1862
|
if (!isInteractive() || workspaceChosen({ home: host.home }))
|
|
@@ -1815,8 +1865,9 @@ async function askWorkspace(host) {
|
|
|
1815
1865
|
if (folder !== undefined && folder !== "" && !saveWorkspace(folder, { home: host.home }))
|
|
1816
1866
|
console.error(`could not write the workspace into ${userConfigPath("config", { home: host.home })}: it does not parse, fix it by hand`);
|
|
1817
1867
|
}
|
|
1818
|
-
async function install(host, options, recheck) {
|
|
1819
|
-
const
|
|
1868
|
+
async function install(host, options, recheck, choosing = ask2) {
|
|
1869
|
+
const all = candidates2(host);
|
|
1870
|
+
const pool = all.filter((each) => !each.installed);
|
|
1820
1871
|
const { dim, green, red, yellow } = paint(process16.stdout);
|
|
1821
1872
|
if (options.list) {
|
|
1822
1873
|
for (const each of pool)
|
|
@@ -1825,20 +1876,23 @@ async function install(host, options, recheck) {
|
|
|
1825
1876
|
console.log("everything is installed");
|
|
1826
1877
|
return 0;
|
|
1827
1878
|
}
|
|
1828
|
-
if (pool.length === 0) {
|
|
1879
|
+
if (pool.length === 0 && (options.names.length > 0 || !isInteractive())) {
|
|
1829
1880
|
console.log("everything is installed");
|
|
1830
1881
|
return 0;
|
|
1831
1882
|
}
|
|
1832
|
-
const unknown = options.names.filter((name) => name !== "all" && !
|
|
1883
|
+
const unknown = options.names.filter((name) => name !== "all" && !all.some((each) => each.names.includes(name)));
|
|
1833
1884
|
if (unknown.length > 0)
|
|
1834
1885
|
badUsage(`nothing to install is named ${unknown.join(", ")}: doctor --install --list`);
|
|
1835
|
-
const
|
|
1886
|
+
const chosen = options.names.length === 0 ? await choosing(all) : { add: choose(pool, options.names), remove: [] };
|
|
1887
|
+
const picked = chosen?.add ?? [];
|
|
1836
1888
|
const skipped = options.names.filter((name) => name !== "all" && !pool.some((each) => each.names.includes(name)));
|
|
1837
1889
|
for (const name of skipped)
|
|
1838
1890
|
console.log(`${dim("already")} ${name} is installed`);
|
|
1839
|
-
|
|
1891
|
+
const removing = removal(all, chosen?.remove ?? []);
|
|
1892
|
+
const removes = removing.packages.length > 0 || removing.plugin;
|
|
1893
|
+
if (picked.length === 0 && !removes) {
|
|
1840
1894
|
if (skipped.length === 0)
|
|
1841
|
-
console.log("nothing
|
|
1895
|
+
console.log("nothing changed");
|
|
1842
1896
|
return 0;
|
|
1843
1897
|
}
|
|
1844
1898
|
const planned = plan(host, picked, options.force);
|
|
@@ -1846,7 +1900,7 @@ async function install(host, options, recheck) {
|
|
|
1846
1900
|
console.log(`${red("blocked")} ${each.package} needs ${each.need} (${each.why}): ${each.how}; --force installs it anyway`);
|
|
1847
1901
|
for (const each of planned.warnings)
|
|
1848
1902
|
console.log(`${yellow("warn ")} ${each.package}: ${each.need} is missing, ${each.why}`);
|
|
1849
|
-
if (planned.packages.length === 0 && !planned.plugin)
|
|
1903
|
+
if (planned.packages.length === 0 && !planned.plugin && !removes)
|
|
1850
1904
|
return 1;
|
|
1851
1905
|
if (!options.dryRun && planned.packages.some((name) => name === "@kaynooo/cli-workspace" || name === "@kaynooo/cli-pkg"))
|
|
1852
1906
|
await askWorkspace(host);
|
|
@@ -1855,6 +1909,11 @@ async function install(host, options, recheck) {
|
|
|
1855
1909
|
console.log(`${options.dryRun ? dim("would ") : green("install")} ${command.join(" ")}`);
|
|
1856
1910
|
if (planned.plugin)
|
|
1857
1911
|
console.log(`${options.dryRun ? dim("would ") : green("install")} ${PLUGIN_INSTALL.join(" ")}`);
|
|
1912
|
+
const uninstall = ["bun", "remove", "-g", ...removing.packages];
|
|
1913
|
+
if (removing.packages.length > 0)
|
|
1914
|
+
console.log(`${options.dryRun ? dim("would ") : green("remove ")} ${uninstall.join(" ")}`);
|
|
1915
|
+
if (removing.plugin)
|
|
1916
|
+
console.log(`${options.dryRun ? dim("would ") : green("remove ")} ${PLUGIN_UNINSTALL.join(" ")}`);
|
|
1858
1917
|
if (options.dryRun)
|
|
1859
1918
|
return planned.blocked.length > 0 ? 1 : 0;
|
|
1860
1919
|
if (planned.packages.length > 0) {
|
|
@@ -1873,16 +1932,27 @@ async function install(host, options, recheck) {
|
|
|
1873
1932
|
console.error(`${red("failed")} ${PLUGIN_INSTALL.join(" ")}: exit ${ran.code}: ${firstLine(ran.stderr || ran.stdout)}`);
|
|
1874
1933
|
}
|
|
1875
1934
|
}
|
|
1935
|
+
if (removing.packages.length > 0) {
|
|
1936
|
+
const ran = await host.run(uninstall);
|
|
1937
|
+
if (ran.code !== 0) {
|
|
1938
|
+
console.error(`${red("failed")} bun remove -g: exit ${ran.code}: ${firstLine(ran.stderr || ran.stdout)}`);
|
|
1939
|
+
return 1;
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
if (removing.plugin && host.which("claude") === null) {
|
|
1943
|
+
console.error(`${yellow("not run")} ${PLUGIN_UNINSTALL.join(" ")}: claude is not on PATH`);
|
|
1944
|
+
} else if (removing.plugin) {
|
|
1945
|
+
const ran = await host.run(PLUGIN_UNINSTALL);
|
|
1946
|
+
if (ran.code !== 0) {
|
|
1947
|
+
console.error(`${red("failed")} ${PLUGIN_UNINSTALL.join(" ")}: exit ${ran.code}: ${firstLine(ran.stderr || ran.stdout)}`);
|
|
1948
|
+
return 1;
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1876
1951
|
console.log(`
|
|
1877
1952
|
after the install:`);
|
|
1878
1953
|
const code = await recheck(checksFor(planned.packages, planned.plugin));
|
|
1879
1954
|
return planned.blocked.length > 0 ? 1 : code;
|
|
1880
1955
|
}
|
|
1881
|
-
function candidatesAll(host) {
|
|
1882
|
-
const open = candidates2(host);
|
|
1883
|
-
const done = Object.entries(TOOLS).filter(([name]) => isInstalled(host, name)).map(([name, bins]) => ({ id: name, names: [shortName(name), name, `cli-${shortName(name)}`, ...bins], label: shortName(name), hint: "", packages: [name] }));
|
|
1884
|
-
return [...open, ...done];
|
|
1885
|
-
}
|
|
1886
1956
|
|
|
1887
1957
|
// src/doctor.ts
|
|
1888
1958
|
var USAGE = `doctor [--only <check>] [--quiet] [--fix | --install [<name>...|all]] [--list] [--dry-run] [--force] [--json]: checks this host's @kaynooo/cli-* install, one line per check, the command that fixes a failing one beside it, and installs the packages it picks
|
|
@@ -1899,7 +1969,7 @@ var OPTIONS = {
|
|
|
1899
1969
|
quiet: { type: "boolean", short: "q", description: "print only the failing checks" },
|
|
1900
1970
|
fix: { type: "boolean", description: "run the fix: lines doctor may run, then check again" },
|
|
1901
1971
|
"dry-run": { type: "boolean", description: "with --fix or --install, show what would run without running it (alone, it implies --fix)" },
|
|
1902
|
-
install: { type: "boolean", description: "install the packages named, or
|
|
1972
|
+
install: { type: "boolean", description: "install the packages named, or managed from a checklist, installed ones checked and unchecking removing them: forgejo, ft, hooks, all" },
|
|
1903
1973
|
list: { type: "boolean", description: "with --install, print the choices" },
|
|
1904
1974
|
force: { type: "boolean", description: "with --install, install a package even though a tool it requires is missing" }
|
|
1905
1975
|
};
|
|
@@ -1922,7 +1992,7 @@ function printOutcomes(outcomes, log) {
|
|
|
1922
1992
|
log(`${marks[status]} ${command}${detail === undefined ? "" : ` ${dim(detail)}`}`);
|
|
1923
1993
|
}
|
|
1924
1994
|
async function main(args, host = hostHere()) {
|
|
1925
|
-
const { values, positionals } = parse(args, USAGE, OPTIONS, { positionals: ["[name...]"], exits: EXITS, replaces: "checking each bin's version, tea's login and the hooks by hand", examples: ["doctor", ["doctor --json", "the checks for a script"], ["doctor -q", "silent unless something fails"], ["doctor --fix --dry-run", "what --fix would run"], ["doctor --install", "
|
|
1995
|
+
const { values, positionals } = parse(args, USAGE, OPTIONS, { positionals: ["[name...]"], exits: EXITS, replaces: "checking each bin's version, tea's login and the hooks by hand", examples: ["doctor", ["doctor --json", "the checks for a script"], ["doctor -q", "silent unless something fails"], ["doctor --fix --dry-run", "what --fix would run"], ["doctor --install", "check to install, uncheck to remove"], ["doctor --install forgejo check hooks", "three named ones"], ["doctor --install all --dry-run", "what installing everything would run"]] });
|
|
1926
1996
|
const only = values.only ?? [];
|
|
1927
1997
|
if (values.install !== true && (positionals.length > 0 || values.list === true || values.force === true))
|
|
1928
1998
|
badUsage("names, --list and --force go with --install");
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kaynooo/cli-doctor",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.11.0",
|
|
5
5
|
"description": "doctor: checks the @kaynooo/cli-* install of a host: each bin's version against the registry, tea's login to Forgejo, the Claude Code hooks and the Bun floor; cli list: every bin in one line, read from its own help.",
|
|
6
6
|
"author": "Kaynooo <contact@kaynooo.fr>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -37,6 +37,6 @@
|
|
|
37
37
|
"doctor": "$npm_execpath src/cli.ts"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@kaynooo/cli-kit": "0.
|
|
40
|
+
"@kaynooo/cli-kit": "0.24.0"
|
|
41
41
|
}
|
|
42
42
|
}
|