seedcord 0.23.0-next.2 → 0.23.0-next.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -50,6 +50,7 @@ Run it through your project's scripts, like `pnpm dev`. A global `seedcord` thro
50
50
  | `seedcord codegen` | writes the typed augmentations for your commands and config |
51
51
  | `seedcord commands` | deploys, lists and cleans up your commands on Discord |
52
52
  | `seedcord make` | writes new files from one of your Handlebars templates, or one an installed plugin ships |
53
+ | `seedcord ask` | searches the guide for a question, a pasted error, or an error code, and works outside a project |
53
54
 
54
55
  <!-- prettier-ignore-end -->
55
56
 
package/dist/cli.mjs CHANGED
@@ -1,18 +1,18 @@
1
- import { n as SEEDCORD_CONFIG_FILENAMES, t as version } from "./version-Cmdvyxh7.mjs";
1
+ import { n as SEEDCORD_CONFIG_FILENAMES, t as version } from "./version-Xg0cBcsP.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import { Command, Option } from "@commander-js/extra-typings";
4
4
  import { SeedcordErrorCode, isSeedcordError, paint } from "@seedcord/errors";
5
5
  import { formatBody, installNodeConsole, installNodeDefaults } from "@seedcord/logger/node";
6
+ import uFuzzy from "@leeoniya/ufuzzy";
7
+ import { BOT_FILES_KEY, BUILT_FILES_READY_KEY, BotLayout, asError, encodeFile, hasErrorCode, importFailed, importSpecifier, isInside, isInteractive, isPlainObject, isPortInUse, isStringArray, pluralize, posixRelative, recordThrownBy, routeLeavesOf, setCliFiles, setDevFiles, toPosixPath, unlessAborted, vitePluginThrew, withoutQuery, workerdPath } from "@seedcord/utils/internal";
6
8
  import wrapAnsi from "wrap-ansi";
7
9
  import { SeedcordError, SeedcordRangeError, SeedcordTypeError, WORDMARK, displayPath, throwSingleOrAggregate } from "@seedcord/errors/internal";
8
10
  import { Box, Text, render, useAnimation, useBoxMetrics, useInput, useWindowSize } from "ink";
9
11
  import React, { Fragment, useEffect, useEffectEvent, useMemo, useReducer, useRef, useState, useSyncExternalStore } from "react";
10
- import uFuzzy from "@leeoniya/ufuzzy";
11
12
  import cliSpinners from "cli-spinners";
12
13
  import { LEVEL_COLOR, Logger, LoggerChannelRegistry } from "@seedcord/logger";
13
14
  import chalk from "chalk";
14
15
  import { basename, delimiter, dirname, join, posix, relative, resolve, sep, win32 } from "node:path";
15
- import { BOT_FILES_KEY, BUILT_FILES_READY_KEY, BotLayout, asError, encodeFile, hasErrorCode, importFailed, importSpecifier, isInside, isInteractive, isPlainObject, isPortInUse, isStringArray, pluralize, posixRelative, recordThrownBy, routeLeavesOf, setCliFiles, setDevFiles, toPosixPath, unlessAborted, vitePluginThrew, withoutQuery, workerdPath } from "@seedcord/utils/internal";
16
16
  import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
17
17
  import { CONSOLE_LEVEL, CONSOLE_METHODS, consoleRecord, logsFolderIn, routeConsole, traceError } from "@seedcord/logger/internal";
18
18
  import { spawn } from "node:child_process";
@@ -42,6 +42,274 @@ import { Writable } from "node:stream";
42
42
  import { camelCase, debounce, kebabCase, pascalCase, snakeCase } from "es-toolkit";
43
43
  import Handlebars from "handlebars";
44
44
 
45
+ //#region ../../tooling/brand/dist/index.mjs
46
+ const BRAND_COLORS = {
47
+ pith: "#f8f6e8",
48
+ seedDark: "#2d3328",
49
+ flesh: "#f04e36",
50
+ fleshDeep: "#c8341f",
51
+ rind: "#6fab49",
52
+ rindDeep: "#4d7d33"
53
+ };
54
+ var SiteAddress$1 = class {
55
+ url;
56
+ constructor(url) {
57
+ this.url = url.replace(/\/+$/, "");
58
+ }
59
+ get label() {
60
+ return this.url.replace(/^[a-z]+:\/\//, "");
61
+ }
62
+ get path() {
63
+ return new URL(this.url).pathname.replace(/\/$/, "");
64
+ }
65
+ get exportDir() {
66
+ return `dist${this.path}`;
67
+ }
68
+ at(path) {
69
+ if (path === "") return this.url;
70
+ return `${this.url}/${path.replace(/^\/+/, "")}`;
71
+ }
72
+ };
73
+ const PRODUCTION_ORIGIN$1 = "https://seedcord.org";
74
+ const PRODUCTION_SITES$1 = {
75
+ home: new SiteAddress$1(PRODUCTION_ORIGIN$1),
76
+ guide: new SiteAddress$1(`${PRODUCTION_ORIGIN$1}/guide`),
77
+ docs: new SiteAddress$1(`${PRODUCTION_ORIGIN$1}/docs`)
78
+ };
79
+ const NEXT_GUIDE = new SiteAddress$1(`https://next.seedcord.org${PRODUCTION_SITES$1.guide.path}`);
80
+
81
+ //#endregion
82
+ //#region ../../tooling/docs-search/dist/index.mjs
83
+ const ASK_WAIT_STATUS = {
84
+ tooManySearches: 429,
85
+ outOfAllowance: 503
86
+ };
87
+ const WAIT_REASONS = {
88
+ [ASK_WAIT_STATUS.tooManySearches]: "Too many searches.",
89
+ [ASK_WAIT_STATUS.outOfAllowance]: "The guide search hit its daily limit."
90
+ };
91
+ const isAskWaitStatus = (status) => status in WAIT_REASONS;
92
+ const UNITS = [{
93
+ unit: "hour",
94
+ seconds: 3600
95
+ }, {
96
+ unit: "minute",
97
+ seconds: 60
98
+ }];
99
+ const relative$1 = new Intl.RelativeTimeFormat("en", { numeric: "always" });
100
+ function tryAgainIn(seconds) {
101
+ const fits = UNITS.find((size) => seconds >= size.seconds);
102
+ return fits ? relative$1.format(Math.ceil(seconds / fits.seconds), fits.unit) : relative$1.format(seconds, "second");
103
+ }
104
+ const waitMessage = (status, seconds) => `${WAIT_REASONS[status]} Try again ${tryAgainIn(seconds)}.`;
105
+ var SearchUnavailable = class extends Error {
106
+ constructor(status, retryAfterSeconds) {
107
+ super(waitMessage(status, retryAfterSeconds));
108
+ }
109
+ };
110
+ const DocKind = {
111
+ Project: 1,
112
+ Module: 2,
113
+ Namespace: 4,
114
+ Enum: 8,
115
+ EnumMember: 16,
116
+ Variable: 32,
117
+ Function: 64,
118
+ Class: 128,
119
+ Interface: 256,
120
+ Constructor: 512,
121
+ Property: 1024,
122
+ Method: 2048,
123
+ CallSignature: 4096,
124
+ IndexSignature: 8192,
125
+ ConstructorSignature: 16384,
126
+ Parameter: 32768,
127
+ TypeLiteral: 65536,
128
+ TypeParameter: 131072,
129
+ Accessor: 262144,
130
+ GetSignature: 524288,
131
+ SetSignature: 1048576,
132
+ TypeAlias: 2097152,
133
+ Reference: 4194304
134
+ };
135
+ const KIND_SCORE_TABLE = {
136
+ [DocKind.Class]: 18,
137
+ [DocKind.Interface]: 16,
138
+ [DocKind.TypeAlias]: 15,
139
+ [DocKind.Enum]: 14,
140
+ [DocKind.Function]: 13,
141
+ [DocKind.Constructor]: 12,
142
+ [DocKind.Method]: 11,
143
+ [DocKind.Accessor]: 10,
144
+ [DocKind.Property]: 9,
145
+ [DocKind.EnumMember]: 7,
146
+ [DocKind.Variable]: 7,
147
+ [DocKind.TypeParameter]: 6,
148
+ [DocKind.Project]: 1,
149
+ [DocKind.Module]: 6,
150
+ [DocKind.Namespace]: 6,
151
+ [DocKind.CallSignature]: 10,
152
+ [DocKind.ConstructorSignature]: 10,
153
+ [DocKind.IndexSignature]: 8,
154
+ [DocKind.GetSignature]: 10,
155
+ [DocKind.SetSignature]: 10
156
+ };
157
+ const nameOnly = (_digits, name) => name;
158
+ const digitsAndName = (digits, name) => `${digits} ${name}`;
159
+ const CERTAIN_CODES = [
160
+ {
161
+ pattern: /\[(\d{4})\]/g,
162
+ rewrite: nameOnly
163
+ },
164
+ {
165
+ pattern: /^(\d{4})$/g,
166
+ rewrite: nameOnly
167
+ },
168
+ {
169
+ pattern: /(?<=\berror(?: code)?\W{0,3})(\d{4})\b/gi,
170
+ rewrite: digitsAndName
171
+ }
172
+ ];
173
+ const FOUR_DIGITS_ON_THEIR_OWN = /(?<![\w.:/#-])\d{4}(?![\w:/-]|\.\d)/g;
174
+ var ErrorCodeReader = class {
175
+ codeNames;
176
+ constructor(codeNames) {
177
+ this.codeNames = codeNames;
178
+ }
179
+ read(question) {
180
+ const swapped = /* @__PURE__ */ new Map();
181
+ let withNames = question;
182
+ for (const { pattern, rewrite } of CERTAIN_CODES) withNames = withNames.replace(pattern, (written, digits) => {
183
+ const mention = this.mentionOf(digits);
184
+ if (mention === void 0) return written;
185
+ swapped.set(mention.code, mention);
186
+ return rewrite(digits, mention.name);
187
+ });
188
+ const hints = /* @__PURE__ */ new Map();
189
+ for (const [digits] of withNames.matchAll(FOUR_DIGITS_ON_THEIR_OWN)) {
190
+ const mention = this.mentionOf(digits);
191
+ if (mention !== void 0 && !swapped.has(mention.code)) hints.set(mention.code, mention);
192
+ }
193
+ return {
194
+ question: withNames,
195
+ swapped: [...swapped.values()],
196
+ hints: [...hints.values()]
197
+ };
198
+ }
199
+ mentionOf(digits) {
200
+ const code = Number(digits);
201
+ const name = this.codeNames[code];
202
+ return name === void 0 ? void 0 : {
203
+ code,
204
+ name
205
+ };
206
+ }
207
+ };
208
+ const errorCodeQuestion = (code) => `error ${code}`;
209
+ var SiteAddress = class {
210
+ url;
211
+ constructor(url) {
212
+ this.url = url.replace(/\/+$/, "");
213
+ }
214
+ get label() {
215
+ return this.url.replace(/^[a-z]+:\/\//, "");
216
+ }
217
+ get path() {
218
+ return new URL(this.url).pathname.replace(/\/$/, "");
219
+ }
220
+ get exportDir() {
221
+ return `dist${this.path}`;
222
+ }
223
+ at(path) {
224
+ if (path === "") return this.url;
225
+ return `${this.url}/${path.replace(/^\/+/, "")}`;
226
+ }
227
+ };
228
+ const PRODUCTION_ORIGIN = "https://seedcord.org";
229
+ const PRODUCTION_SITES = {
230
+ home: new SiteAddress(PRODUCTION_ORIGIN),
231
+ guide: new SiteAddress(`${PRODUCTION_ORIGIN}/guide`),
232
+ docs: new SiteAddress(`${PRODUCTION_ORIGIN}/docs`)
233
+ };
234
+ new SiteAddress(`https://next.seedcord.org${PRODUCTION_SITES.guide.path}`);
235
+ const EXTENSION = ".json";
236
+ const REFERENCE_SEARCH_URL = PRODUCTION_SITES.docs.at("/api/search");
237
+ const GUIDE_ASK_PATH = "/api/ask";
238
+ const searchFileName = (version) => `${version}${EXTENSION}`;
239
+ const PACKAGES_FILE_NAME = searchFileName("packages");
240
+ const guideResult = (hit, found, site) => ({
241
+ ...hit,
242
+ url: site.at(hit.path),
243
+ ...found
244
+ });
245
+ const fetchPath = (path) => fetch(path);
246
+ const withoutQuery$1 = (path) => path.replace(/\?.*$/, "");
247
+ var SiteUnreachable = class extends Error {
248
+ constructor(path, cause) {
249
+ super(`${withoutQuery$1(path)} failed to load because the request never reached the server`, { cause });
250
+ }
251
+ };
252
+ async function requestReachingServer(path, read) {
253
+ try {
254
+ return await read(path);
255
+ } catch (error) {
256
+ if (error instanceof TypeError) throw new SiteUnreachable(path, error);
257
+ throw error;
258
+ }
259
+ }
260
+ async function readAsset(path, read = fetchPath) {
261
+ const response = await requestReachingServer(path, read);
262
+ if (!response.ok) throw new Error(`${withoutQuery$1(path)} failed to load with status ${response.status}`);
263
+ return response;
264
+ }
265
+ async function readJson$1(path, read = fetchPath) {
266
+ return await (await readAsset(path, read)).json();
267
+ }
268
+ async function fetchAnswer(url, signal) {
269
+ const response = await fetch(url, { signal });
270
+ const retryAfter = Number(response.headers.get("retry-after"));
271
+ if (isAskWaitStatus(response.status) && Number.isSafeInteger(retryAfter) && retryAfter > 0) throw new SearchUnavailable(response.status, retryAfter);
272
+ return response;
273
+ }
274
+ var GuideSearch = class {
275
+ site;
276
+ constructor(site) {
277
+ this.site = site;
278
+ }
279
+ async search(question, signal) {
280
+ const url = new URL(this.site.at(GUIDE_ASK_PATH));
281
+ url.searchParams.set("q", question);
282
+ return (await readJson$1(url.href, (path) => fetchAnswer(path, signal))).map((hit) => guideResult(hit, { by: "meaning" }, this.site));
283
+ }
284
+ };
285
+ const QUESTION_LENGTH = {
286
+ min: 2,
287
+ max: 500
288
+ };
289
+ const QUESTION_RULE = `${QUESTION_LENGTH.min} to ${QUESTION_LENGTH.max} characters, with a letter or a number in them`;
290
+ const LETTER_OR_NUMBER = /[\p{L}\p{N}]/u;
291
+ const isQuestion = (text) => text.length >= QUESTION_LENGTH.min && text.length <= QUESTION_LENGTH.max && LETTER_OR_NUMBER.test(text);
292
+ const collapseSpaces = (text) => text.replaceAll(/\s+/g, " ");
293
+ function parseQuestion(input) {
294
+ const question = collapseSpaces(input.trim());
295
+ return isQuestion(question) ? question : void 0;
296
+ }
297
+ const STACK_FRAME$1 = /^\s*at .*(?::\d+:\d+\)?|<anonymous>\)?|\(index \d+\))\s*$/gm;
298
+ function cutAtWord(text, max) {
299
+ if (text.length <= max) return text;
300
+ const lastSpace = text.lastIndexOf(" ", max);
301
+ return text.slice(0, lastSpace > 0 ? lastSpace : max);
302
+ }
303
+ function questionFromText(text) {
304
+ const withoutFrames = collapseSpaces(text.replaceAll(STACK_FRAME$1, "").trim());
305
+ const kept = cutAtWord(withoutFrames, QUESTION_LENGTH.max);
306
+ return {
307
+ question: parseQuestion(kept),
308
+ shortened: kept.length < withoutFrames.length
309
+ };
310
+ }
311
+
312
+ //#endregion
45
313
  //#region src/core/output/wrapLines.ts
46
314
  function wrapLines(text, width, prefix) {
47
315
  return text.split("\n").flatMap((line) => {
@@ -817,12 +1085,14 @@ var StepPrinter = class {
817
1085
  command;
818
1086
  labelWidth;
819
1087
  verbose;
1088
+ rows;
820
1089
  stdout;
821
1090
  stderr;
822
- constructor({ command, labels, verbose, stdout = process.stdout, stderr = process.stderr }) {
1091
+ constructor({ command, labels, verbose, rows = true, stdout = process.stdout, stderr = process.stderr }) {
823
1092
  this.command = command;
824
1093
  this.labelWidth = Math.max(...labels.map((label) => label.length));
825
1094
  this.verbose = verbose;
1095
+ this.rows = rows;
826
1096
  this.stdout = stdout;
827
1097
  this.stderr = stderr;
828
1098
  }
@@ -830,6 +1100,7 @@ var StepPrinter = class {
830
1100
  this.stdout.write(`\n${WORDMARK} ${paint.mute(`v${version}`)} ${this.command}\n\n`);
831
1101
  }
832
1102
  async step(label, task, note, lines) {
1103
+ if (!this.rows) return task();
833
1104
  const write = this.stdout.write.bind(this.stdout);
834
1105
  const row = new StepRow({
835
1106
  write,
@@ -887,17 +1158,33 @@ function exitFor(error) {
887
1158
  var BaseCommand = class {
888
1159
  name;
889
1160
  description;
890
- constructor(name, description) {
1161
+ needsProject;
1162
+ constructor(name, description, { needsProject = true } = {}) {
891
1163
  this.name = name;
892
1164
  this.description = description;
1165
+ this.needsProject = needsProject;
893
1166
  }
894
- async runSteps(command, labels, run) {
1167
+ define(program) {
1168
+ return program.command(this.name).description(this.description);
1169
+ }
1170
+ runSteps(command, labels, run) {
895
1171
  const printer = new StepPrinter({
896
1172
  command: this.name,
897
1173
  labels,
898
1174
  verbose: isVerbose(command)
899
1175
  });
900
1176
  printer.header();
1177
+ return this.runOn(printer, run);
1178
+ }
1179
+ runQuietSteps(command, labels, run) {
1180
+ return this.runOn(new StepPrinter({
1181
+ command: this.name,
1182
+ labels,
1183
+ verbose: isVerbose(command),
1184
+ rows: false
1185
+ }), run);
1186
+ }
1187
+ async runOn(printer, run) {
901
1188
  try {
902
1189
  await run(printer);
903
1190
  } catch (error) {
@@ -918,6 +1205,162 @@ var BaseCommand = class {
918
1205
  }
919
1206
  };
920
1207
 
1208
+ //#endregion
1209
+ //#region ../../tooling/ui/dist/PageAsset.mjs
1210
+ const ROOT = "index";
1211
+ const toSegments = (path) => path.split("/").filter(Boolean);
1212
+ var PageAsset = class {
1213
+ extension;
1214
+ directory;
1215
+ constructor(extension, directory) {
1216
+ this.extension = extension;
1217
+ this.directory = directory;
1218
+ }
1219
+ static forPath(pathname) {
1220
+ return PAGE_ASSETS.find((asset) => pathname.endsWith(asset.extension));
1221
+ }
1222
+ fileSegments(pageSegments) {
1223
+ const last = pageSegments.at(-1) ?? ROOT;
1224
+ return [...pageSegments.slice(0, -1), `${last}${this.extension}`];
1225
+ }
1226
+ pageSegments(fileSegments) {
1227
+ const last = fileSegments.at(-1);
1228
+ if (last?.endsWith(this.extension) !== true) return void 0;
1229
+ const name = last.slice(0, -this.extension.length);
1230
+ const folders = fileSegments.slice(0, -1);
1231
+ return folders.length === 0 && name === ROOT ? [] : [...folders, name];
1232
+ }
1233
+ publicPath(pagePath) {
1234
+ const page = pagePath.endsWith(this.extension) ? pagePath.slice(0, -this.extension.length) : pagePath;
1235
+ return `/${this.fileSegments(toSegments(page)).join("/")}`;
1236
+ }
1237
+ exportPath(pagePath) {
1238
+ return `/${this.directory}${this.publicPath(pagePath)}`;
1239
+ }
1240
+ };
1241
+ const TWIN = new PageAsset(".md", "llms");
1242
+ const CARD = new PageAsset(".png", "og");
1243
+ const PAGE_ASSETS = [TWIN, CARD];
1244
+
1245
+ //#endregion
1246
+ //#region src/commands/commands/StepsPresenter.ts
1247
+ var StepsPresenter = class {
1248
+ output;
1249
+ constructor(output) {
1250
+ this.output = output;
1251
+ }
1252
+ step(label, task, note) {
1253
+ return this.output.step(label, task, note);
1254
+ }
1255
+ detail(key, value) {
1256
+ this.output.detail(key, value);
1257
+ }
1258
+ };
1259
+
1260
+ //#endregion
1261
+ //#region src/commands/ask/AskPresenter.ts
1262
+ const ASK_STEPS = ["search"];
1263
+ const SHORTENED = `Your text is over ${QUESTION_LENGTH.max} characters, so seedcord ask searched only the start of it.`;
1264
+ const FOR_PERSON = {
1265
+ pageLines: ({ title, sections }) => [paint.bold(title), ...sections.flatMap(({ heading, url }) => [...heading === void 0 ? [] : [` ${heading}`], ` ${paint.sky(url)}`])],
1266
+ wholeGuide: (site) => site.url
1267
+ };
1268
+ const FOR_AGENT = {
1269
+ pageLines: ({ title, path, sections }, site) => [`${paint.bold(title)}: ${paint.sky(site.at(TWIN.publicPath(path)))}`, ...sections.flatMap(({ heading }) => heading === void 0 ? [] : [` - ${heading}`])],
1270
+ wholeGuide: (site) => site.at("llms.txt")
1271
+ };
1272
+ function pagesOf(results) {
1273
+ const pages = /* @__PURE__ */ new Map();
1274
+ for (const result of results) {
1275
+ const path = result.path.replace(/#.*$/, "");
1276
+ const title = result.page === result.tab ? result.page : `${result.page} (${result.tab})`;
1277
+ const page = pages.get(path) ?? {
1278
+ title,
1279
+ path,
1280
+ sections: []
1281
+ };
1282
+ if (result.heading === void 0) page.sections.unshift(result);
1283
+ else page.sections.push(result);
1284
+ pages.set(path, page);
1285
+ }
1286
+ return [...pages.values()];
1287
+ }
1288
+ function codeNotes({ swapped, hints }) {
1289
+ return [...swapped.map(({ code, name }) => paint.italic(`${code} is ${paint.bold(name)}`)), ...hints.map(({ code, name }) => paint.italic(`${code} is also the error code ${paint.bold(name)}. If you meant that, run: ${paint.bold(`seedcord ask "${errorCodeQuestion(code)}"`)}`))];
1290
+ }
1291
+ var AskPresenter = class extends StepsPresenter {
1292
+ format;
1293
+ site;
1294
+ constructor(output, format, site) {
1295
+ super(output);
1296
+ this.format = format;
1297
+ this.site = site;
1298
+ }
1299
+ answer({ codes, shortened, results }) {
1300
+ const notes = [...codeNotes(codes), ...shortened ? [SHORTENED] : []];
1301
+ const pages = pagesOf(results).map((page) => this.format.pageLines(page, this.site));
1302
+ const answer = pages.length > 0 ? pages : [[this.noMatches()]];
1303
+ this.output.line();
1304
+ for (const block of [notes, ...answer]) {
1305
+ if (block.length === 0) continue;
1306
+ for (const line of block) this.output.line(line);
1307
+ this.output.line();
1308
+ }
1309
+ }
1310
+ noMatches() {
1311
+ return `Nothing in the guide matches. Try a full sentence, or read ${paint.sky(this.format.wholeGuide(this.site))}`;
1312
+ }
1313
+ };
1314
+
1315
+ //#endregion
1316
+ //#region src/commands/ask/AskRunner.ts
1317
+ const SEARCH_TIMEOUT_MS = 1e4;
1318
+ const CODE_READER = new ErrorCodeReader(SeedcordErrorCode);
1319
+ var AskRunner = class {
1320
+ site;
1321
+ guide;
1322
+ constructor(site, guide) {
1323
+ this.site = site;
1324
+ this.guide = guide;
1325
+ }
1326
+ async run(output, format, text) {
1327
+ const { question, shortened } = questionFromText(text);
1328
+ if (question === void 0) throw new SeedcordTypeError(SeedcordErrorCode.CliAskBadQuestion, [QUESTION_LENGTH.min]);
1329
+ const presenter = new AskPresenter(output, format, this.site);
1330
+ presenter.detail("site", this.site.url);
1331
+ const results = await presenter.step("search", () => this.search(question));
1332
+ presenter.answer({
1333
+ codes: CODE_READER.read(question),
1334
+ shortened,
1335
+ results
1336
+ });
1337
+ }
1338
+ async search(question) {
1339
+ try {
1340
+ return await this.guide.search(question, AbortSignal.timeout(SEARCH_TIMEOUT_MS));
1341
+ } catch (error) {
1342
+ if (error instanceof SearchUnavailable) throw new SeedcordError(SeedcordErrorCode.CliAskHasToWait, [error.message, this.site.url], { cause: error });
1343
+ throw new SeedcordError(SeedcordErrorCode.CliAskFailed, [asError(error).message, this.site.url], { cause: error });
1344
+ }
1345
+ }
1346
+ };
1347
+
1348
+ //#endregion
1349
+ //#region src/commands/ask/AskCommand.ts
1350
+ const GUIDE_SITE = "0.23.0-next.3".includes("-") ? NEXT_GUIDE : PRODUCTION_SITES$1.guide;
1351
+ var AskCommand = class extends BaseCommand {
1352
+ runner = new AskRunner(GUIDE_SITE, new GuideSearch(GUIDE_SITE));
1353
+ constructor() {
1354
+ super("ask", "Search the guide", { needsProject: false });
1355
+ }
1356
+ register(program) {
1357
+ return this.define(program).argument("<question...>", "What to search for, or an error code or message to look up").allowUnknownOption().action((words, _options, command) => {
1358
+ const question = words.join(" ");
1359
+ return isInteractive() ? this.runSteps(command, ASK_STEPS, (printer) => this.runner.run(printer, FOR_PERSON, question)) : this.runQuietSteps(command, ASK_STEPS, (printer) => this.runner.run(printer, FOR_AGENT, question));
1360
+ });
1361
+ }
1362
+ };
1363
+
921
1364
  //#endregion
922
1365
  //#region src/core/config/openProjectWithSteps.ts
923
1366
  async function openProjectWithSteps(steps, loader, { projectDir, check } = {}) {
@@ -3316,7 +3759,7 @@ var BuildCommand = class extends BaseCommand {
3316
3759
  super("build", "Type check the bot and bundle it from the config file");
3317
3760
  }
3318
3761
  register(program) {
3319
- program.command(this.name).description(this.description).action((_options, command) => this.runSteps(command, BUILD_STEPS, async (printer) => {
3762
+ return this.define(program).action((_options, command) => this.runSteps(command, BUILD_STEPS, async (printer) => {
3320
3763
  printBuildSummary(printer, await BuildRunner.create(printer).run());
3321
3764
  }));
3322
3765
  }
@@ -3814,7 +4257,7 @@ var CodegenCommand = class extends BaseCommand {
3814
4257
  super("codegen", "Generate typed augmentations from your commands and config");
3815
4258
  }
3816
4259
  register(program) {
3817
- program.command(this.name).description(this.description).option("--check", "Verify the committed augmentations are up to date instead of writing them").action((options, command) => this.runSteps(command, CODEGEN_STEPS, async (printer) => {
4260
+ return this.define(program).option("--check", "Verify the committed augmentations are up to date instead of writing them").action((options, command) => this.runSteps(command, CODEGEN_STEPS, async (printer) => {
3818
4261
  const check = options.check ?? false;
3819
4262
  const { outputPath, changed } = await CodegenRunner.create(printer, cliLogger("Codegen")).run(check);
3820
4263
  printer.line();
@@ -3945,21 +4388,6 @@ function printSections(output, sections) {
3945
4388
  }
3946
4389
  }
3947
4390
 
3948
- //#endregion
3949
- //#region src/commands/commands/StepsPresenter.ts
3950
- var StepsPresenter = class {
3951
- output;
3952
- constructor(output) {
3953
- this.output = output;
3954
- }
3955
- step(label, task, note) {
3956
- return this.output.step(label, task, note);
3957
- }
3958
- detail(key, value) {
3959
- this.output.detail(key, value);
3960
- }
3961
- };
3962
-
3963
4391
  //#endregion
3964
4392
  //#region src/commands/commands/deploy/DeployPresenter.ts
3965
4393
  function deployedNote(scopes) {
@@ -4664,7 +5092,7 @@ var CommandsCommand = class extends BaseCommand {
4664
5092
  this.prompter = prompter;
4665
5093
  }
4666
5094
  register(program) {
4667
- const commands = program.command(this.name).description(this.description).action((_options, command) => this.openMenu(command));
5095
+ const commands = this.define(program).action((_options, command) => this.openMenu(command));
4668
5096
  commands.command("deploy").description("Deploy the commands in your code to Discord").option("--yes", "Deploy without asking first").option("--force", "Also deploy the scopes that match the last deploy").action((options, command) => this.runSteps(command, DEPLOY_STEPS, (printer) => this.deploy(printer, command, options)));
4669
5097
  commands.command("list").description("List the commands Discord has for your bot").option("--guilds <ids...>", "Also list these guilds").addOption(allGuildsOption("Also list every guild the bot is in")).option("--json", "Print the listing as JSON for scripts").action((options, command) => {
4670
5098
  const scope = {
@@ -4676,6 +5104,7 @@ var CommandsCommand = class extends BaseCommand {
4676
5104
  });
4677
5105
  commands.command("clean").description("Delete guild commands that duplicate a global command").option("--guilds <ids...>", "Guild ids to scan").addOption(allGuildsOption("Scan every guild the bot is in")).option("--yes", "Delete the duplicates without asking").action((flags, command) => this.remove(command, flags, new CleanMode(), askClean));
4678
5106
  commands.command("purge").description("Delete every command globally or in guilds").option("--global", "Delete every global command").option("--guilds <ids...>", "Delete every command in these guilds").addOption(allGuildsOption("Delete every command in every guild the bot is in")).option("--yes", "Delete the commands without asking").action((flags, command) => this.remove(command, flags, new PurgeMode(flags.global ?? false), askPurge));
5107
+ return commands;
4679
5108
  }
4680
5109
  async openMenu(command) {
4681
5110
  if (!isInteractive()) {
@@ -5959,42 +6388,6 @@ function ScrollableLogView({ visible, layout, viewportHeight, measured }) {
5959
6388
  })));
5960
6389
  }
5961
6390
 
5962
- //#endregion
5963
- //#region ../../tooling/brand/dist/index.mjs
5964
- const BRAND_COLORS = {
5965
- pith: "#f8f6e8",
5966
- seedDark: "#2d3328",
5967
- flesh: "#f04e36",
5968
- fleshDeep: "#c8341f",
5969
- rind: "#6fab49",
5970
- rindDeep: "#4d7d33"
5971
- };
5972
- var SiteAddress = class {
5973
- url;
5974
- constructor(url) {
5975
- this.url = url.replace(/\/+$/, "");
5976
- }
5977
- get label() {
5978
- return this.url.replace(/^[a-z]+:\/\//, "");
5979
- }
5980
- get path() {
5981
- return new URL(this.url).pathname.replace(/\/$/, "");
5982
- }
5983
- get exportDir() {
5984
- return `dist${this.path}`;
5985
- }
5986
- at(path) {
5987
- if (path === "") return this.url;
5988
- return `${this.url}/${path.replace(/^\/+/, "")}`;
5989
- }
5990
- };
5991
- const PRODUCTION_ORIGIN = "https://seedcord.org";
5992
- const PRODUCTION_SITES = {
5993
- home: new SiteAddress(PRODUCTION_ORIGIN),
5994
- guide: new SiteAddress(`${PRODUCTION_ORIGIN}/guide`),
5995
- docs: new SiteAddress(`${PRODUCTION_ORIGIN}/docs`)
5996
- };
5997
-
5998
6391
  //#endregion
5999
6392
  //#region src/ui/components/Banner.tsx
6000
6393
  function Banner({ version }) {
@@ -8036,7 +8429,7 @@ var TunnelCoordinator = class {
8036
8429
  //#region src/commands/dev/tunnel/createTunnelCoordinator.ts
8037
8430
  const PASTE_HINT = "The tunnel is still up, so try pasting that URL into the dashboard.";
8038
8431
  const RESTART_HINT = "Restart for a fresh hostname, or set `tunnel` to an https URL you already serve.";
8039
- const NO_PORT_HINT = `seedcord didn't start a tunnel, since the bot runs with ${paint.bold("port: false")} and cloudflared doesn't have a port to forward to. Give the bot a port while you develop, or set ${paint.amber("tunnel")} to the URL of the server that serves your bot: ${paint.sky(PRODUCTION_SITES.guide.at("build/embed"))}`;
8432
+ const NO_PORT_HINT = `seedcord didn't start a tunnel, since the bot runs with ${paint.bold("port: false")} and cloudflared doesn't have a port to forward to. Give the bot a port while you develop, or set ${paint.amber("tunnel")} to the URL of the server that serves your bot: ${paint.sky(PRODUCTION_SITES$1.guide.at("build/embed"))}`;
8040
8433
  function missingCloudflaredHint(platform) {
8041
8434
  const arrow = paint.mute("→");
8042
8435
  return [
@@ -8357,7 +8750,7 @@ var DevCommand = class extends BaseCommand {
8357
8750
  super("dev", "Run the bot from the config file");
8358
8751
  }
8359
8752
  register(program) {
8360
- program.command(this.name).description(this.description).action(async () => {
8753
+ return this.define(program).action(async () => {
8361
8754
  profileStdout();
8362
8755
  const store = new DevStore();
8363
8756
  const stateFolder = new StateFolder(process.cwd(), this.logger);
@@ -8966,7 +9359,7 @@ var MakeCommand = class extends BaseCommand {
8966
9359
  super("make", "Write new files from one of your templates");
8967
9360
  }
8968
9361
  register(program) {
8969
- program.command(this.name).description(this.description).action((_options, command) => this.runSteps(command, MAKE_STEPS, (printer) => unlessCancelled(printer, () => MakeRunner.create(printer, new Prompter(), isInteractive()).run())));
9362
+ return this.define(program).action((_options, command) => this.runSteps(command, MAKE_STEPS, (printer) => unlessCancelled(printer, () => MakeRunner.create(printer, new Prompter(), isInteractive()).run())));
8970
9363
  }
8971
9364
  };
8972
9365
 
@@ -8988,14 +9381,20 @@ function assertProjectInstall(projectDir, cliDir) {
8988
9381
  async function main() {
8989
9382
  installNodeConsole();
8990
9383
  const program = addVerboseOption(new Command().name("seedcord").description("seedcord CLI").version(version));
8991
- program.hook("preAction", () => {
8992
- assertProjectInstall(process.cwd(), import.meta.dirname);
8993
- });
8994
- new DevCommand().register(program);
8995
- new BuildCommand().register(program);
8996
- new CodegenCommand().register(program);
8997
- new CommandsCommand().register(program);
8998
- new MakeCommand().register(program);
9384
+ const commands = [
9385
+ new AskCommand(),
9386
+ new DevCommand(),
9387
+ new BuildCommand(),
9388
+ new CodegenCommand(),
9389
+ new CommandsCommand(),
9390
+ new MakeCommand()
9391
+ ];
9392
+ for (const command of commands) {
9393
+ const registered = command.register(program);
9394
+ if (command.needsProject) registered.hook("preAction", () => {
9395
+ assertProjectInstall(process.cwd(), import.meta.dirname);
9396
+ });
9397
+ }
8999
9398
  try {
9000
9399
  await program.parseAsync(process.argv);
9001
9400
  } catch (error) {