", "cap results (search/lookup/reports/discover)", "the API's own default"],
@@ -306,16 +306,16 @@ export const CORE_CLI_COMMANDS = [
["--provider ", "discover: analysis provider", "offline"],
],
examples: [
- ["moshcode ticker NVDA", "score, technicals, thesis, signals, sources"],
- ["moshcode ticker lookup rivian", "company name → RIVN"],
- ["moshcode ticker signals AAPL", "what was actually said, with sources"],
- ["moshcode ticker search 'data center'", "across every indexed transcript"],
- ["moshcode ticker reports --limit 10", "the stored index, best score first"],
+ ["moshcode stocks NVDA", "score, technicals, thesis, signals, sources"],
+ ["moshcode stocks lookup rivian", "company name → RIVN"],
+ ["moshcode stocks signals AAPL", "what was actually said, with sources"],
+ ["moshcode stocks search 'data center'", "across every indexed transcript"],
+ ["moshcode stocks reports --limit 10", "the stored index, best score first"],
],
seeAlso: ["trade", "plugin", "tools"],
note: "research aid, not advice — reports are stored snapshots and every one prints when it was generated. Set MOSHCODE_ADVISOR_URL to point at another instance.",
},
- { name: "advisor", aliasOf: "ticker", description: "alias for ticker" },
+ { name: "advisor", aliasOf: "stocks", description: "alias for stocks" },
{
name: "crypto",
group: "tools",
@@ -342,7 +342,7 @@ export const CORE_CLI_COMMANDS = [
["moshcode crypto bars ETH --timeframe 1Hour", "historical OHLCV"],
["moshcode crypto book BTC-USD --depth 5", "top of book, both sides"],
],
- seeAlso: ["ticker", "trade", "plugin"],
+ seeAlso: ["stocks", "trade", "plugin"],
note: "research aid, not advice — prices are Alpaca's US crypto venue alone and can differ materially from other exchanges. Crypto trades 24/7 with no circuit breakers. Set MOSHCODE_ADVISOR_URL to point at another instance.",
},
{ name: "coins", aliasOf: "crypto", description: "alias for crypto" },
@@ -357,7 +357,7 @@ export const CORE_CLI_COMMANDS = [
["moshcode plugin install", "add the marketplace and install ticker"],
["moshcode plugin list", "what this marketplace ships, and what is installed"],
],
- seeAlso: ["skill", "mcp", "ticker"],
+ seeAlso: ["skill", "mcp", "stocks"],
note: "Claude Code is the only engine with a plugin primitive; the others are reported as skipped, exactly as they are for skills.",
},
{ name: "plugins", aliasOf: "plugin", description: "alias for plugin" },
@@ -539,42 +539,42 @@ export const DNS_VERBS = [
];
/**
- * `ticker`'s verbs.
+ * `stocks`'s verbs.
*
* `report` exists so a symbol that collides with a verb name still has an
* unambiguous spelling; without it, the bare-symbol shortcut would have no
* escape hatch. src/advisor.mjs owns the parser and test/advisor.test.mjs
* fails when the two lists disagree.
*/
-export const TICKER_VERBS = [
- { name: "report", description: "the stored research report for one ticker", synopsis: [["moshcode ticker report ", "same as `moshcode ticker `"]] },
- { name: "signals", description: "every extracted signal for a ticker", synopsis: [["moshcode ticker signals ", ""]] },
+export const STOCKS_VERBS = [
+ { name: "report", description: "the stored research report for one ticker", synopsis: [["moshcode stocks report ", "same as `moshcode stocks `"]] },
+ { name: "signals", description: "every extracted signal for a ticker", synopsis: [["moshcode stocks signals ", ""]] },
{
name: "search", description: "full-text search across indexed transcripts",
- synopsis: [["moshcode ticker search [--limit n]", ""]],
+ synopsis: [["moshcode stocks search [--limit n]", ""]],
},
{
name: "lookup", description: "find a ticker by company name",
- synopsis: [["moshcode ticker lookup [--limit n]", "rivian → RIVN"]],
+ synopsis: [["moshcode stocks lookup [--limit n]", "rivian → RIVN"]],
},
{
name: "reports", description: "every stored report",
- synopsis: [["moshcode ticker reports [--sort recent|score|ticker] [--limit n]", ""]],
+ synopsis: [["moshcode stocks reports [--sort recent|score|ticker] [--limit n]", ""]],
},
{
name: "discover", description: "a ranked watchlist for a topic",
- synopsis: [["moshcode ticker discover [topic…] [--horizon 1|2] [--provider p] [--limit n]", ""]],
+ synopsis: [["moshcode stocks discover [topic…] [--horizon 1|2] [--provider p] [--limit n]", ""]],
note: "ranks by analyzing each candidate — this one takes minutes, not milliseconds.",
},
- { name: "tickers", description: "every ticker present in the index", synopsis: [["moshcode ticker tickers", ""]] },
- { name: "stats", description: "index coverage counts", synopsis: [["moshcode ticker stats", ""]] },
- { name: "open", description: "open the shareable report page in a browser", synopsis: [["moshcode ticker open ", ""]] },
+ { name: "tickers", description: "every ticker present in the index", synopsis: [["moshcode stocks tickers", ""]] },
+ { name: "stats", description: "index coverage counts", synopsis: [["moshcode stocks stats", ""]] },
+ { name: "open", description: "open the shareable report page in a browser", synopsis: [["moshcode stocks open ", ""]] },
];
/**
* `crypto`'s verbs.
*
- * `report` earns its place for the same reason ticker's does — a bare pair is
+ * `report` earns its place for the same reason stocks's does — a bare pair is
* the shortcut, so a pair that collides with a verb name needs a spelling that
* cannot be mistaken for one. src/crypto.mjs owns the parser and
* test/crypto.test.mjs fails when the two lists disagree.
@@ -626,7 +626,7 @@ export const VERB_TABLES = {
UPGRADE_TARGETS,
DNS_VERBS,
TRADE_VERBS,
- TICKER_VERBS,
+ STOCKS_VERBS,
CRYPTO_VERBS,
PLUGIN_VERBS,
};
@@ -655,7 +655,7 @@ export const PIT_COMMANDS = [
description: "list workflow tools, or run one" },
{ name: "trade", args: " [args…]", cli: "trade",
description: "look up markets and preview/place Alpaca orders" },
- { name: "ticker", aliases: ["advisor"], args: " [args…]", cli: "ticker",
+ { name: "stocks", aliases: ["advisor"], args: " [args…]", cli: "stocks",
description: "equity research from advis0r.com" },
{ name: "crypto", aliases: ["coins"], args: " [args…]", cli: "crypto",
description: "crypto market data from advis0r.com" },
diff --git a/src/crypto.mjs b/src/crypto.mjs
index 4649fc0..4d7ff87 100644
--- a/src/crypto.mjs
+++ b/src/crypto.mjs
@@ -5,8 +5,8 @@
// function of the decoded JSON. Every route is public and read-only, so there
// is no login verb and no write verb — `moshcode trade` is where orders live.
//
-// This is a *sibling* of `ticker`, not a mode of it, because the two answer
-// different questions from different data. A ticker report is a stored snapshot
+// This is a *sibling* of `stocks`, not a mode of it, because the two answer
+// different questions from different data. A stocks report is a stored snapshot
// built from transcripts, SEC fundamentals and extracted signals. A crypto
// report is a live read of Alpaca's US crypto venue: no transcripts, no
// filings, no signals, and a `fetchedAt` measured in seconds rather than days.
@@ -50,7 +50,7 @@ export const CRYPTO_VERB_NAMES = [
"report", "quote", "snapshot", "technicals", "bars", "book", "spark", "assets", "lookup", "open",
];
-// The same reasoning as ticker's alias table: `/crypto price BTC` and
+// The same reasoning as stocks's alias table: `/crypto price BTC` and
// `/crypto candles BTC` should not be errors when the intent is obvious.
// `search` maps to lookup rather than erroring — crypto has no transcript
// index to search, and a directory lookup is what the word means here.
@@ -83,7 +83,7 @@ export function resolveVerb(word) {
* the API resolves it to that asset's USD pair, and inventing the `-USD` here
* would silently break the day a base has no USD pair.
*
- * Deliberately narrow, like ticker's: the whole job of the check is to tell
+ * Deliberately narrow, like stocks's: the whole job of the check is to tell
* `BTC` from `bitcoin` and send the second one to lookup with a useful message
* instead of a 400. Bases run to five characters (SUSHI, MATIC, TRUMP), so six
* leaves room for the concatenated `BTCUSD` spelling without swallowing words.
@@ -293,7 +293,7 @@ export function cryptoUrl(request, { base = advisorBase() } = {}) {
* Execute a translated request. `fetchImpl` is injectable for tests.
*
* Every crypto route is a live venue read, so one timeout fits all of them —
- * unlike ticker, which has to budget separately for `discover`'s per-candidate
+ * unlike stocks, which has to budget separately for `discover`'s per-candidate
* analysis.
*/
export async function fetchCrypto(request, { fetchImpl = globalThis.fetch, base = advisorBase(), timeoutMs = 45_000 } = {}) {
diff --git a/src/help.mjs b/src/help.mjs
index 1780c6b..7fdad39 100644
--- a/src/help.mjs
+++ b/src/help.mjs
@@ -103,6 +103,18 @@ function distance(a, b) {
return prev[b.length];
}
+/**
+ * Commands that were renamed, and what they became.
+ *
+ * Edit distance cannot help here — `ticker` is nowhere near `stocks` — so a
+ * rename that isn't recorded leaves people with a bare "unknown command" for a
+ * verb that worked yesterday. This is deliberately *not* an alias table: the
+ * old name stays dead, it just says where its replacement went.
+ */
+export const RENAMED_COMMANDS = {
+ ticker: "stocks",
+};
+
/**
* The nearest command to something that is not one, or null.
*
@@ -114,6 +126,8 @@ function distance(a, b) {
export function suggest(input, extra = []) {
const typed = String(input ?? "").toLowerCase();
if (!typed) return null;
+ // A known rename beats any distance match — it is not a guess.
+ if (RENAMED_COMMANDS[typed]) return RENAMED_COMMANDS[typed];
const candidates = [...CORE_CLI_COMMANDS.map((c) => c.name), ...extra]
.filter((n) => !n.startsWith("-"));
diff --git a/src/plugins.mjs b/src/plugins.mjs
index 1b2ab3d..ccd392b 100644
--- a/src/plugins.mjs
+++ b/src/plugins.mjs
@@ -31,7 +31,7 @@ export const PLUGINS = [
{
name: "ticker",
description: "equity research slash commands backed by advis0r.com",
- commands: ["/ticker", "/signals", "/research", "/lookup", "/reports", "/discover"],
+ commands: ["/stocks", "/signals", "/research", "/lookup", "/reports", "/discover"],
},
{
name: "crypto",
diff --git a/src/tui.mjs b/src/tui.mjs
index a95d37a..5e716ef 100644
--- a/src/tui.mjs
+++ b/src/tui.mjs
@@ -20,12 +20,12 @@ import { fetchMotdAd } from "./ads.mjs";
import { runScript } from "./runtime.mjs";
import { moshVocabulary } from "./commands.mjs";
import { mcpCommand, pluginCommand, skillCommand } from "./integrations.mjs";
-import { tickerCommand } from "./advisor.mjs";
+import { stocksCommand } from "./advisor.mjs";
import { cryptoCommand } from "./crypto.mjs";
import { canOpenBrowser, openBrowser } from "./open-url.mjs";
import { banner, hr, acid, ash, bone, dim, ok, err, warn, info, moshcodeVersion } from "./ui.mjs";
import { CORE_CLI_COMMAND_NAMES } from "./cli-schema.mjs";
-import { findPitCommand, pitHelpModel, renderPitCommand, suggest, wantsHelp } from "./help.mjs";
+import { RENAMED_COMMANDS, findPitCommand, pitHelpModel, renderPitCommand, suggest, wantsHelp } from "./help.mjs";
import { openNewTab } from "./tabs.mjs";
const PROMPT = () => acid("mosh ") + dim("▸ ");
@@ -673,13 +673,13 @@ export async function tui() {
rl = mkrl();
continue;
}
- // `/ticker` renders in the pit rather than handing the terminal to a tool:
+ // `/stocks` renders in the pit rather than handing the terminal to a tool:
// there is no advis0r binary to launch, only a public read-only API.
- if (cmd === "ticker" || cmd === "advisor") {
- await tickerCommand(rest, { openUrl: (url) => canOpenBrowser() && openBrowser(url) });
+ if (cmd === "stocks" || cmd === "advisor") {
+ await stocksCommand(rest, { openUrl: (url) => canOpenBrowser() && openBrowser(url) });
continue;
}
- // `/crypto` renders in the pit for the same reason `/ticker` does: there is
+ // `/crypto` renders in the pit for the same reason `/stocks` does: there is
// no crypto binary to hand the terminal to, only a public read-only API.
if (cmd === "crypto" || cmd === "coins") {
await cryptoCommand(rest, { openUrl: (url) => canOpenBrowser() && openBrowser(url) });
@@ -722,7 +722,12 @@ export async function tui() {
rl = mkrl();
continue;
}
- console.log(err(`unknown command "${line}". /help for the list.`));
+ // A renamed verb gets pointed at its replacement; `/ticker` was a pit
+ // command for a release, so a bare "unknown command" is a dead end here.
+ const renamed = RENAMED_COMMANDS[cmd];
+ console.log(err(renamed
+ ? `unknown command "${line}" — /${cmd} is now /${renamed}.`
+ : `unknown command "${line}". /help for the list.`));
}
try { rl.close(); } catch { /* noop */ }
diff --git a/test/advisor.test.mjs b/test/advisor.test.mjs
index 413f206..05ca382 100644
--- a/test/advisor.test.mjs
+++ b/test/advisor.test.mjs
@@ -1,35 +1,35 @@
-// `moshcode ticker` — argument translation, request building, and the two
+// `moshcode stocks` — argument translation, request building, and the two
// things this command must never get wrong: presenting a stored snapshot as a
// live quote, and dropping the API's disclaimer.
import assert from "node:assert/strict";
import test from "node:test";
import {
- DEFAULT_ADVISOR_URL, TICKER_VERB_NAMES, advisorBase, advisorUrl, fetchAdvisor,
- normalizeSymbol, renderAdvisor, resolveVerb, tickerArgs, tickerCommand, tickerUsage,
+ DEFAULT_ADVISOR_URL, STOCKS_VERB_NAMES, advisorBase, advisorUrl, fetchAdvisor,
+ normalizeSymbol, renderAdvisor, resolveVerb, stocksArgs, stocksCommand, stocksUsage,
} from "../src/advisor.mjs";
-import { TICKER_VERBS } from "../src/cli-schema.mjs";
+import { STOCKS_VERBS } from "../src/cli-schema.mjs";
// --- the bare-symbol shortcut ------------------------------------------------
test("a bare symbol is the report, and is upper-cased on the way out", () => {
- assert.deepEqual(tickerArgs(["nvda"]), {
+ assert.deepEqual(stocksArgs(["nvda"]), {
verb: "report", symbol: "NVDA", path: "/api/ticker", query: { symbol: "NVDA" }, json: false,
});
- assert.deepEqual(tickerArgs(["report", "brk.b"]), {
+ assert.deepEqual(stocksArgs(["report", "brk.b"]), {
verb: "report", symbol: "BRK.B", path: "/api/ticker", query: { symbol: "BRK.B" }, json: false,
});
});
test("no arguments prints usage rather than guessing a symbol", () => {
- assert.deepEqual(tickerArgs([]), { usage: true });
- assert.match(tickerUsage(), /usage: moshcode ticker/);
+ assert.deepEqual(stocksArgs([]), { usage: true });
+ assert.match(stocksUsage(), /usage: moshcode stocks/);
});
test("a company name is refused with the lookup that resolves it", () => {
- const result = tickerArgs(["some very long company name"]);
+ const result = stocksArgs(["some very long company name"]);
assert.match(result.error, /is not a ticker symbol/);
- assert.match(result.error, /moshcode ticker lookup/);
+ assert.match(result.error, /moshcode stocks lookup/);
});
// --- verbs -------------------------------------------------------------------
@@ -37,8 +37,8 @@ test("a company name is refused with the lookup that resolves it", () => {
test("every verb the schema documents is one the parser resolves", () => {
// The schema drives help and completion; the parser drives behaviour. A verb
// in one and not the other is a command that completes and then fails.
- assert.deepEqual(TICKER_VERBS.map(({ name }) => name).sort(), [...TICKER_VERB_NAMES].sort());
- for (const { name } of TICKER_VERBS) assert.equal(resolveVerb(name), name, `${name} does not resolve`);
+ assert.deepEqual(STOCKS_VERBS.map(({ name }) => name).sort(), [...STOCKS_VERB_NAMES].sort());
+ for (const { name } of STOCKS_VERBS) assert.equal(resolveVerb(name), name, `${name} does not resolve`);
});
test("aliases resolve, and anything else is treated as a symbol", () => {
@@ -49,18 +49,18 @@ test("aliases resolve, and anything else is treated as a symbol", () => {
});
test("search and lookup take words, not symbols", () => {
- assert.deepEqual(tickerArgs(["search", "data", "center", "--limit", "5"]), {
+ assert.deepEqual(stocksArgs(["search", "data", "center", "--limit", "5"]), {
verb: "search", path: "/api/search", query: { q: "data center", limit: "5" }, json: false,
});
- assert.deepEqual(tickerArgs(["lookup", "rivian"]), {
+ assert.deepEqual(stocksArgs(["lookup", "rivian"]), {
verb: "lookup", path: "/api/lookup", query: { q: "rivian" }, json: false,
});
});
test("reports defaults to score order and discover to the offline provider", () => {
- assert.deepEqual(tickerArgs(["reports"]).query, { sort: "score" });
- assert.deepEqual(tickerArgs(["reports", "--sort", "recent"]).query, { sort: "recent" });
- assert.deepEqual(tickerArgs(["discover", "fusion"]).query, {
+ assert.deepEqual(stocksArgs(["reports"]).query, { sort: "score" });
+ assert.deepEqual(stocksArgs(["reports", "--sort", "recent"]).query, { sort: "recent" });
+ assert.deepEqual(stocksArgs(["discover", "fusion"]).query, {
topic: "fusion", provider: "offline", horizon: "2",
});
});
@@ -68,28 +68,28 @@ test("reports defaults to score order and discover to the offline provider", ()
test("discover is marked slow, because it analyzes every candidate", () => {
// The flag is what buys it the longer timeout in fetchAdvisor. Without it the
// route reliably aborts mid-ranking and looks like an outage.
- assert.equal(tickerArgs(["discover"]).slow, true);
- assert.equal(tickerArgs(["reports"]).slow, undefined);
+ assert.equal(stocksArgs(["discover"]).slow, true);
+ assert.equal(stocksArgs(["reports"]).slow, undefined);
});
// --- flags -------------------------------------------------------------------
test("flag values are validated rather than forwarded", () => {
- assert.match(tickerArgs(["reports", "--limit", "0"]).error, /positive number/);
- assert.match(tickerArgs(["reports", "--limit"]).error, /positive number/);
- assert.match(tickerArgs(["reports", "--sort", "sideways"]).error, /recent, score, ticker/);
- assert.match(tickerArgs(["discover", "--horizon", "9"]).error, /must be 1 or 2/);
+ assert.match(stocksArgs(["reports", "--limit", "0"]).error, /positive number/);
+ assert.match(stocksArgs(["reports", "--limit"]).error, /positive number/);
+ assert.match(stocksArgs(["reports", "--sort", "sideways"]).error, /recent, score, ticker/);
+ assert.match(stocksArgs(["discover", "--horizon", "9"]).error, /must be 1 or 2/);
});
test("an unknown flag is an error, not a search term", () => {
// Left alone it would join `q` and be searched for verbatim, which returns
// nothing and reads as "the index has no coverage".
- assert.match(tickerArgs(["search", "--depth", "3"]).error, /unknown ticker flag/);
+ assert.match(stocksArgs(["search", "--depth", "3"]).error, /unknown stocks flag/);
});
test("--json survives anywhere in the argument list", () => {
- assert.equal(tickerArgs(["--json", "nvda"]).json, true);
- assert.equal(tickerArgs(["nvda", "--json"]).json, true);
+ assert.equal(stocksArgs(["--json", "nvda"]).json, true);
+ assert.equal(stocksArgs(["nvda", "--json"]).json, true);
});
// --- symbols -----------------------------------------------------------------
@@ -109,12 +109,12 @@ test("the base URL is overridable, so a local instance is testable", () => {
});
test("query values are encoded, not concatenated", () => {
- const url = advisorUrl(tickerArgs(["search", "data center & more"]), { base: "https://example.test" });
+ const url = advisorUrl(stocksArgs(["search", "data center & more"]), { base: "https://example.test" });
assert.equal(url, "https://example.test/api/search?q=data+center+%26+more");
});
test("open builds the shareable page URL and makes no request", () => {
- const request = tickerArgs(["open", "nvda"]);
+ const request = stocksArgs(["open", "nvda"]);
assert.equal(request.path, undefined);
assert.equal(advisorUrl(request, { base: "https://example.test" }), "https://example.test/ticker/NVDA");
});
@@ -126,7 +126,7 @@ const jsonResponse = (body, { ok = true, status = 200 } = {}) => ({
});
test("a non-JSON body is a failure, not a silent empty render", () => {
- return fetchAdvisor(tickerArgs(["stats"]), {
+ return fetchAdvisor(stocksArgs(["stats"]), {
fetchImpl: async () => ({ ok: false, status: 502, text: async () => "bad gateway" }),
}).then((res) => {
assert.equal(res.ok, false);
@@ -135,7 +135,7 @@ test("a non-JSON body is a failure, not a silent empty render", () => {
});
test("a transport failure is reported, never thrown at the caller", async () => {
- const res = await fetchAdvisor(tickerArgs(["stats"]), {
+ const res = await fetchAdvisor(stocksArgs(["stats"]), {
fetchImpl: async () => { throw new Error("ECONNREFUSED"); },
});
assert.equal(res.ok, false);
@@ -214,7 +214,7 @@ function capture() {
test("--json prints the response verbatim and renders nothing", async () => {
const io = capture();
- const code = await tickerCommand(["stats", "--json"], {
+ const code = await stocksCommand(["stats", "--json"], {
...io, fetchImpl: async () => jsonResponse({ documents: 3 }),
});
assert.equal(code, 0);
@@ -223,7 +223,7 @@ test("--json prints the response verbatim and renders nothing", async () => {
test("the API's own error is surfaced, with its did-you-mean", async () => {
const io = capture();
- const code = await tickerCommand(["RIVIAN"], {
+ const code = await stocksCommand(["RIVIAN"], {
...io,
fetchImpl: async () => jsonResponse(
{ error: '"RIVIAN" is not a ticker — did you mean RIVN?', didYouMean: { symbol: "RIVN", name: "Rivian" } },
@@ -233,13 +233,13 @@ test("the API's own error is surfaced, with its did-you-mean", async () => {
assert.equal(code, 1, "a failed lookup must not exit 0");
const output = io.lines.join("\n");
assert.match(output, /did you mean RIVN/);
- assert.match(output, /moshcode ticker RIVN/);
+ assert.match(output, /moshcode stocks RIVN/);
});
test("a bad argument exits non-zero without touching the network", async () => {
const io = capture();
let called = false;
- const code = await tickerCommand(["reports", "--sort", "sideways"], {
+ const code = await stocksCommand(["reports", "--sort", "sideways"], {
...io, fetchImpl: async () => { called = true; return jsonResponse({}); },
});
assert.equal(code, 1);
@@ -249,7 +249,7 @@ test("a bad argument exits non-zero without touching the network", async () => {
test("open never makes a request, and prints the URL when no browser opens", async () => {
const io = capture();
let called = false;
- const code = await tickerCommand(["open", "nvda"], {
+ const code = await stocksCommand(["open", "nvda"], {
...io,
base: "https://example.test",
openUrl: () => false,
diff --git a/test/help.test.mjs b/test/help.test.mjs
index 6722392..4510054 100644
--- a/test/help.test.mjs
+++ b/test/help.test.mjs
@@ -24,7 +24,7 @@ import {
} from "../src/cli-schema.mjs";
import {
WIDTH, findCommand, findPitCommand, helpModel, pitHelpModel, renderCommand, renderOverview,
- renderPitCommand, renderScriptVerb, README_START, README_END, suggest, wantsHelp,
+ renderPitCommand, renderScriptVerb, README_START, README_END, RENAMED_COMMANDS, suggest, wantsHelp,
withCommandTable, wrap,
} from "../src/help.mjs";
import { ENGINES } from "../src/engines.mjs";
@@ -274,6 +274,16 @@ test("suggest is tight enough to stay useful", () => {
assert.equal(suggest(""), null);
});
+test("a renamed command points at its replacement, and stays gone", () => {
+ // Edit distance cannot reach `stocks` from `ticker`, so without the rename
+ // table a verb that worked one release ago gets a bare "unknown command".
+ assert.equal(suggest("ticker"), "stocks");
+ for (const [old, now] of Object.entries(RENAMED_COMMANDS)) {
+ assert.equal(findCommand(old), null, `${old} was renamed — it must not still dispatch`);
+ assert.ok(findCommand(now), `${old} points at ${now}, which must exist`);
+ }
+});
+
test("findCommand resolves aliases and rejects strangers", () => {
assert.equal(findCommand("where").name, "pwd");
assert.equal(findCommand("update").name, "upgrade");