seedcord 0.17.4 → 0.18.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/README.md CHANGED
@@ -1,18 +1,67 @@
1
- <p align="center">
1
+ <div align="center">
2
2
  <picture>
3
3
  <source media="(prefers-color-scheme: dark)" srcset="https://cdn.seedcord.org/assets/wordmark-dark.webp" />
4
4
  <img src="https://cdn.seedcord.org/assets/wordmark-light.webp" alt="seedcord" width="440" />
5
5
  </picture>
6
- </p>
6
+ </div>
7
7
 
8
- ---
8
+ <div align="center">
9
+ <h3>The whole Discord bot, wired and typed</h3>
10
+ <a href="https://seedcord.org">Website</a> ·
11
+ <a href="https://guide.seedcord.org">Guide</a> ·
12
+ <a href="https://docs.seedcord.org">Reference</a> ·
13
+ <a href="https://discord.gg/DzFxY58WXf">Discord</a>
14
+ </div>
9
15
 
10
- The seedcord CLI and dev tooling.
16
+ <br />
11
17
 
12
- _This repository is a work in progress._
18
+ <div align="center">
13
19
 
14
- - There are no stable releases yet but changes are being made actively.
15
- - Until a major v1.0.0 release for seedcord, expect breaking changes in minor versions.
16
- - Documentation will come soon as well!
20
+ [![npm](https://img.shields.io/npm/v/seedcord?style=flat-square&logo=npm&logoColor=c8341f&label=&labelColor=1f1f1f&color=c8341f)](https://www.npmjs.com/package/seedcord) [![node](https://img.shields.io/node/v/seedcord?style=flat-square&label=node&labelColor=1f1f1f&color=4d7d33)](https://nodejs.org) [![license](https://img.shields.io/npm/l/seedcord?style=flat-square&label=license&labelColor=1f1f1f&color=f8f6e8)](LICENSE)
17
21
 
18
- If you'd like to try it out, you can check out the code in `mock`
22
+ </div>
23
+
24
+ ## About
25
+
26
+ `seedcord` is the CLI a bot project runs during development and at build time. It reads `seedcord.config.ts` and gives you a dev server with hot reload, a production build, the codegen that types your commands, and a cleanup tool for stale guild commands.
27
+
28
+ Until v1.0.0, minor versions can break.
29
+
30
+ ## Installation
31
+
32
+ `pnpm create seedcord` adds it to a new project. To add it to an existing one:
33
+
34
+ ```sh
35
+ pnpm add -D seedcord
36
+ ```
37
+
38
+ ## Commands
39
+
40
+ <!-- prettier-ignore-start -->
41
+
42
+ | command | what it does |
43
+ | --- | --- |
44
+ | `seedcord dev` | runs the bot from the config file, reloading changed modules in place |
45
+ | `seedcord build` | compiles the project from the config file |
46
+ | `seedcord codegen` | writes the typed augmentations for your commands and config |
47
+ | `seedcord commands` | inspects and cleans commands already deployed to Discord |
48
+
49
+ <!-- prettier-ignore-end -->
50
+
51
+ `codegen --check` verifies the committed augmentations match your source. Run it in CI.
52
+
53
+ `commands --clean` reports guild commands that duplicate a global one. It runs as a dry run until you pass `--apply`.
54
+
55
+ On the http transport, `dev` also opens a cloudflared tunnel and points Discord's interactions URL at it.
56
+
57
+ ## Config
58
+
59
+ `defineConfig` types `seedcord.config.ts`:
60
+
61
+ ```ts
62
+ import { defineConfig } from 'seedcord';
63
+
64
+ export default defineConfig({
65
+ // ...
66
+ });
67
+ ```
package/dist/cli.mjs CHANGED
@@ -1,10 +1,10 @@
1
- import { n as SEEDCORD_CONFIG_FILENAMES, t as version } from "./src-i-qherg7.mjs";
1
+ import { n as SEEDCORD_CONFIG_FILENAMES, t as version } from "./src-duSEbkfe.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import { Command } from "@commander-js/extra-typings";
4
- import { LEVEL_COLOR, Logger, LoggerChannelRegistry, paint } from "@seedcord/logger";
4
+ import { LEVEL_COLOR, Logger, LoggerChannelRegistry } from "@seedcord/logger";
5
5
  import { formatBody, installNodeDefaults } from "@seedcord/logger/node";
6
6
  import { Envapter, Environment } from "envapt";
7
- import { SeedcordErrorCode, isSeedcordError } from "@seedcord/errors";
7
+ import { SeedcordErrorCode, isSeedcordError, paint } from "@seedcord/errors";
8
8
  import { existsSync, realpathSync } from "node:fs";
9
9
  import { BRAND, SeedcordError, validateDiscordToken } from "@seedcord/errors/internal";
10
10
  import { delimiter, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
@@ -18,7 +18,6 @@ import { isTsOrJsFile } from "@seedcord/utils/node";
18
18
  import { ApplicationCommandOptionType, ApplicationCommandType, RESTJSONErrorCodes, Routes } from "discord-api-types/v10";
19
19
  import { routeLeavesOf } from "@seedcord/utils/internal";
20
20
  import { DiscordAPIError, REST } from "@discordjs/rest";
21
- import chalk from "chalk";
22
21
  import { autocompleteMultiselect, cancel, confirm, intro, isCancel, log, multiselect, note, outro, select, spinner } from "@clack/prompts";
23
22
  import { createInterface } from "node:readline";
24
23
  import { Box, Text, measureElement, render, useAnimation, useInput, useWindowSize } from "ink";
@@ -32,6 +31,7 @@ import { assertNever } from "@seedcord/utils";
32
31
  import { settleWithin } from "@seedcord/core/node/internal";
33
32
  import { createServer, createServerModuleRunner, defineConfig, mergeConfig } from "vite";
34
33
  import { EvaluatedModules } from "vite/module-runner";
34
+ import tsconfigPaths from "vite-tsconfig-paths";
35
35
  import { wrapHot } from "@seedcord/core/internal";
36
36
  import { minimatch } from "minimatch";
37
37
  import { createServer as createServer$1 } from "node:net";
@@ -74,28 +74,31 @@ function isOptionalString(value) {
74
74
  function isStringArray(value) {
75
75
  return Array.isArray(value) && value.every((item) => typeof item === "string");
76
76
  }
77
+ function invalidField(field, expected) {
78
+ return new SeedcordError(SeedcordErrorCode.CliConfigInvalidField, [field, expected]);
79
+ }
77
80
  function validateBuild(value) {
78
81
  if (value === void 0) return;
79
- if (!isPlainObject(value)) throw new SeedcordError(SeedcordErrorCode.CliConfigInvalidBuild);
80
- if (!isOptionalString(value.outDir)) throw new SeedcordError(SeedcordErrorCode.CliConfigInvalidBuildOutDir);
81
- if (!isOptionalString(value.tsconfig)) throw new SeedcordError(SeedcordErrorCode.CliConfigInvalidBuildTsconfig);
82
- if (!isOptionalString(value.bootstrap)) throw new SeedcordError(SeedcordErrorCode.CliConfigInvalidBuildBootstrap);
82
+ if (!isPlainObject(value)) throw invalidField("build", "an object");
83
+ if (!isOptionalString(value.outDir)) throw invalidField("build.outDir", "a string");
84
+ if (!isOptionalString(value.tsconfig)) throw invalidField("build.tsconfig", "a string");
85
+ if (!isOptionalString(value.bootstrap)) throw invalidField("build.bootstrap", "a string");
83
86
  }
84
87
  function validateTypecheck(value) {
85
88
  if (value === void 0 || typeof value === "boolean") return;
86
- if (!isPlainObject(value)) throw new SeedcordError(SeedcordErrorCode.CliConfigInvalidHmrTypecheck);
87
- if (!isOptionalString(value.tsconfig)) throw new SeedcordError(SeedcordErrorCode.CliConfigInvalidTsconfig);
89
+ if (!isPlainObject(value)) throw invalidField("hmr.typecheck", "a boolean or an object");
90
+ if (!isOptionalString(value.tsconfig)) throw invalidField("hmr.typecheck.tsconfig", "a string");
88
91
  }
89
92
  function validateHmr(value) {
90
93
  if (value === void 0) return;
91
- if (!isPlainObject(value)) throw new SeedcordError(SeedcordErrorCode.CliConfigInvalidHmr);
92
- if (value.restart !== void 0 && !isStringArray(value.restart)) throw new SeedcordError(SeedcordErrorCode.CliConfigInvalidHmrRestart);
93
- if (value.rollback !== void 0 && typeof value.rollback !== "boolean") throw new SeedcordError(SeedcordErrorCode.CliConfigInvalidHmrRollback);
94
+ if (!isPlainObject(value)) throw invalidField("hmr", "an object");
95
+ if (value.restart !== void 0 && !isStringArray(value.restart)) throw invalidField("hmr.restart", "an array of strings");
96
+ if (value.rollback !== void 0 && typeof value.rollback !== "boolean") throw invalidField("hmr.rollback", "a boolean");
94
97
  validateTypecheck(value.typecheck);
95
98
  }
96
99
  function validateTunnel(value) {
97
100
  if (value === void 0 || typeof value === "boolean") return;
98
- if (typeof value !== "string" || URL.parse(value)?.protocol !== "https:") throw new SeedcordError(SeedcordErrorCode.CliConfigInvalidTunnel);
101
+ if (typeof value !== "string" || URL.parse(value)?.protocol !== "https:") throw invalidField("tunnel", "a boolean or an https URL");
99
102
  }
100
103
  function resolveTunnel(value) {
101
104
  if (value === false) return { mode: "off" };
@@ -117,8 +120,8 @@ function validateConfig(raw) {
117
120
  if (!isPlainObject(raw)) throw new SeedcordError(SeedcordErrorCode.CliConfigInvalidExport);
118
121
  if (typeof raw.instance !== "string" || raw.instance.length === 0) throw new SeedcordError(SeedcordErrorCode.CliConfigMissingInstance);
119
122
  if (typeof raw.entry !== "string" || raw.entry.length === 0) throw new SeedcordError(SeedcordErrorCode.CliConfigMissingEntry);
120
- if (!isOptionalString(raw.root)) throw new SeedcordError(SeedcordErrorCode.CliConfigInvalidRoot);
121
- if (raw.idleAnimation !== void 0 && typeof raw.idleAnimation !== "boolean") throw new SeedcordError(SeedcordErrorCode.CliConfigInvalidIdleAnimation);
123
+ if (!isOptionalString(raw.root)) throw invalidField("root", "a string");
124
+ if (raw.idleAnimation !== void 0 && typeof raw.idleAnimation !== "boolean") throw invalidField("idleAnimation", "a boolean");
122
125
  validateTunnel(raw.tunnel);
123
126
  validateBuild(raw.build);
124
127
  validateHmr(raw.hmr);
@@ -328,7 +331,7 @@ var TypeScriptProjectBuilder = class {
328
331
  this.logger.info(`Building Seedcord project via tsc using ${tsconfigPath}`);
329
332
  try {
330
333
  await this.runTsc(projectDir, tsconfigPath);
331
- await this.fixRelativeSpecifiers(config.build.outDir);
334
+ await this.rewriteSpecifiers(projectDir, tsconfigPath);
332
335
  } catch (error) {
333
336
  if (isSeedcordError(error)) throw error;
334
337
  const reason = Error.isError(error) ? error.message : "Unknown build error";
@@ -367,11 +370,17 @@ var TypeScriptProjectBuilder = class {
367
370
  return false;
368
371
  }
369
372
  }
370
- async fixRelativeSpecifiers(outDir) {
371
- const fixPath = this.resolveFixEsmImportPath();
372
- const result = await this.runNodeScript(process.cwd(), fixPath, [outDir]);
373
+ async rewriteSpecifiers(projectDir, tsconfigPath) {
374
+ const binPath = this.resolveTscAlias();
375
+ const result = await this.runNodeScript(projectDir, binPath, [
376
+ "-p",
377
+ tsconfigPath,
378
+ "--resolve-full-paths",
379
+ "--resolve-full-extension",
380
+ ".js"
381
+ ]);
373
382
  if (result.exitCode === 0) return;
374
- throw new SeedcordError(SeedcordErrorCode.CliBuildFailed, [`Failed to fix relative import specifiers:\n${this.truncateOutput(result.output)}`]);
383
+ throw new SeedcordError(SeedcordErrorCode.CliBuildFailed, [`Failed to rewrite import specifiers:\n${this.truncateOutput(result.output)}`]);
375
384
  }
376
385
  resolveProjectTsc(projectDir) {
377
386
  const projectRequire = this.createProjectRequire(projectDir);
@@ -382,13 +391,8 @@ var TypeScriptProjectBuilder = class {
382
391
  throw new SeedcordError(SeedcordErrorCode.CliBuildFailed, [`Unable to resolve typescript. Ensure it is installed in this project.\n${reason}`]);
383
392
  }
384
393
  }
385
- resolveFixEsmImportPath() {
386
- const selfRequire = createRequire(import.meta.url);
387
- try {
388
- return selfRequire.resolve("fix-esm-import-path/fix-esm-import-path.js");
389
- } catch {
390
- return selfRequire.resolve("fix-esm-import-path");
391
- }
394
+ resolveTscAlias() {
395
+ return createRequire(import.meta.url).resolve("tsc-alias/dist/bin/index.js");
392
396
  }
393
397
  createProjectRequire(projectDir) {
394
398
  const candidate = resolve(projectDir, "package.json");
@@ -1165,7 +1169,7 @@ var InteractivePresenter = class {
1165
1169
  }
1166
1170
  }
1167
1171
  preview(scan) {
1168
- const body = groupByGuild(scan.flagged).map((group) => `${chalk.bold(group.guildName)}\n${group.commands.map((c) => ` ${c.name}`).join("\n")}`).join("\n\n");
1172
+ const body = groupByGuild(scan.flagged).map((group) => `${paint.bold(group.guildName)}\n${group.commands.map((c) => ` ${c.name}`).join("\n")}`).join("\n\n");
1169
1173
  note(body, `Will delete ${plural(scan.flagged.length, "guild command")} (nothing deleted yet)`);
1170
1174
  if (scan.skipped.length > 0) log.warn(skippedSummary(scan.skipped));
1171
1175
  }
@@ -1209,9 +1213,9 @@ var FlagPresenter = class {
1209
1213
  return task();
1210
1214
  }
1211
1215
  preview(scan) {
1212
- this.logger.info(chalk.bold(`${plural(scan.flagged.length, "guild command")} selected for deletion`));
1216
+ this.logger.info(paint.bold(`${plural(scan.flagged.length, "guild command")} selected for deletion`));
1213
1217
  for (const group of groupByGuild(scan.flagged)) {
1214
- this.logger.info(chalk.cyan(group.guildName));
1218
+ this.logger.info(paint.sky(group.guildName));
1215
1219
  for (const command of group.commands) this.logger.info(` ${command.name}`);
1216
1220
  }
1217
1221
  for (const skip of scan.skipped) this.logger.warn(`Skipped ${skip.guildName} (${skip.reason}).`);
@@ -1229,15 +1233,15 @@ var FlagPresenter = class {
1229
1233
  result(deletion, skipped) {
1230
1234
  for (const failure of deletion.failed) this.logger.warn(`Could not delete ${failure.command.name} in ${failure.command.guildName} (${failure.reason}).`);
1231
1235
  if (skipped.length > 0) this.logger.warn(skippedSummary(skipped));
1232
- this.logger.info(chalk.green(`Deleted ${plural(deletion.deleted, "guild command")}. Global commands untouched.`));
1236
+ this.logger.info(paint.mint(`Deleted ${plural(deletion.deleted, "guild command")}. Global commands untouched.`));
1233
1237
  }
1234
1238
  nothingToClean(scan, outcome) {
1235
1239
  const lines = emptyLines(scan, outcome);
1236
1240
  for (const line of lines.info) this.logger.info(line);
1237
- this.logger.info(chalk.green(lines.outro));
1241
+ this.logger.info(paint.mint(lines.outro));
1238
1242
  }
1239
1243
  dryRunHint() {
1240
- this.logger.info(chalk.italic("Dry run. Re-run with --apply to delete the above."));
1244
+ this.logger.info(paint.italic("Dry run. Re-run with --apply to delete the above."));
1241
1245
  }
1242
1246
  };
1243
1247
 
@@ -2298,7 +2302,7 @@ function FilterKeys() {
2298
2302
  //#endregion
2299
2303
  //#region src/ui/components/primitives/Sidebar.tsx
2300
2304
  const MAX_RAIL = 40;
2301
- const META_LABEL_WIDTH = 7;
2305
+ const META_LABEL_WIDTH = 8;
2302
2306
  const LOG_DIR = "logs/";
2303
2307
  function Meta({ label, value }) {
2304
2308
  return /* @__PURE__ */ React.createElement(Text, null, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, label.padEnd(META_LABEL_WIDTH)), value);
@@ -2306,11 +2310,17 @@ function Meta({ label, value }) {
2306
2310
  function tunnelValue(status) {
2307
2311
  return status === "live" || status === "lost" ? status : `${status}…`;
2308
2312
  }
2313
+ function transportLabel({ name }) {
2314
+ return name.replace("@seedcord/", "");
2315
+ }
2309
2316
  function StatusBlock({ state, uptimeMs }) {
2310
2317
  return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, /* @__PURE__ */ React.createElement(StatusBadge, {
2311
2318
  phase: state.phase,
2312
2319
  animate: state.idleAnimation
2313
- }), state.status ? /* @__PURE__ */ React.createElement(Text, null, state.status) : null, uptimeMs === null ? null : /* @__PURE__ */ React.createElement(Meta, {
2320
+ }), state.status ? /* @__PURE__ */ React.createElement(Text, null, state.status) : null, state.transport === null ? null : /* @__PURE__ */ React.createElement(Meta, {
2321
+ label: transportLabel(state.transport),
2322
+ value: `v${state.transport.version}`
2323
+ }), uptimeMs === null ? null : /* @__PURE__ */ React.createElement(Meta, {
2314
2324
  label: "up",
2315
2325
  value: formatUptime(uptimeMs)
2316
2326
  }), state.port === null ? null : /* @__PURE__ */ React.createElement(Meta, {
@@ -2333,7 +2343,7 @@ function Sidebar({ state, enabled, enabledLevels, uptimeMs, following, interacti
2333
2343
  flexShrink: 0,
2334
2344
  paddingX: 1,
2335
2345
  overflow: "hidden"
2336
- }, /* @__PURE__ */ React.createElement(Box, { flexShrink: 0 }, /* @__PURE__ */ React.createElement(Banner, { version: state.frameworkVersion })), /* @__PURE__ */ React.createElement(Box, {
2346
+ }, /* @__PURE__ */ React.createElement(Box, { flexShrink: 0 }, /* @__PURE__ */ React.createElement(Banner, { version })), /* @__PURE__ */ React.createElement(Box, {
2337
2347
  flexShrink: 0,
2338
2348
  marginTop: 1
2339
2349
  }, /* @__PURE__ */ React.createElement(StatusBlock, {
@@ -2702,13 +2712,13 @@ function profileReport() {
2702
2712
 
2703
2713
  //#endregion
2704
2714
  //#region src/ui/tier.ts
2705
- const FULL_ROWS = 29;
2706
- const COMPACT_ROWS = 20;
2715
+ const FULL_ROWS = 30;
2716
+ const COMPACT_ROWS = 21;
2707
2717
  const RAIL_COLUMNS = 80;
2708
2718
  function tierFor(rows, columns) {
2709
2719
  if (columns < 80) return "logs";
2710
- if (rows >= 29) return "full";
2711
- if (rows >= 20) return "compact";
2720
+ if (rows >= 30) return "full";
2721
+ if (rows >= 21) return "compact";
2712
2722
  return "logs";
2713
2723
  }
2714
2724
 
@@ -2796,7 +2806,7 @@ const INITIAL = {
2796
2806
  status: "Initializing…",
2797
2807
  error: null,
2798
2808
  isBusy: true,
2799
- frameworkVersion: null,
2809
+ transport: null,
2800
2810
  restartRequired: false,
2801
2811
  commandUpdatePrompt: null,
2802
2812
  port: null,
@@ -2821,8 +2831,8 @@ var DevStore = class extends TypedEventEmitter {
2821
2831
  setError(error) {
2822
2832
  this.patch({ error });
2823
2833
  }
2824
- setFrameworkVersion(frameworkVersion) {
2825
- this.patch({ frameworkVersion });
2834
+ setTransport(transport) {
2835
+ this.patch({ transport });
2826
2836
  }
2827
2837
  setTunnel(tunnel) {
2828
2838
  this.patch({ tunnel });
@@ -3025,7 +3035,10 @@ var DevSession = class {
3025
3035
  const instance = await Promise.resolve(exported);
3026
3036
  if (!isSeedcordInstance(instance)) throw new SeedcordError(SeedcordErrorCode.CliInstanceInvalid);
3027
3037
  this.instance = instance;
3028
- this.store.setFrameworkVersion(instance.version);
3038
+ this.store.setTransport({
3039
+ name: instance.augmentTarget,
3040
+ version: instance.version
3041
+ });
3029
3042
  try {
3030
3043
  this.store.setPhase("starting");
3031
3044
  this.store.setStatus("Starting Seedcord instance…");
@@ -3033,7 +3046,7 @@ var DevSession = class {
3033
3046
  await this.startupPromise;
3034
3047
  if (this.isStopped) return;
3035
3048
  this.store.setPhase("running");
3036
- this.store.setStatus(`${chalk.bold(instance.username ?? "Bot")} is ready!`);
3049
+ this.store.setStatus(`${paint.bold(instance.username ?? "Bot")} is ready!`);
3037
3050
  profileMark("ready");
3038
3051
  onReady?.();
3039
3052
  this.startTypecheck();
@@ -3294,8 +3307,9 @@ var ViteDevRuntime = class {
3294
3307
  this.hmrPlugin = hmrPlugin;
3295
3308
  const config = mergeConfig(vite_config_default, {
3296
3309
  root: projectRoot,
3310
+ configFile: false,
3297
3311
  server: { watch: { ignored: [logsIgnore(projectRoot)] } },
3298
- plugins: [hmrPlugin.plugin]
3312
+ plugins: [tsconfigPaths({ root: dirname(this.context.config.configFile) }), hmrPlugin.plugin]
3299
3313
  });
3300
3314
  this.viteServer = await createServer(config);
3301
3315
  this.evaluatedModules = new EvaluatedModules();
@@ -3585,10 +3599,10 @@ var TunnelCoordinator = class {
3585
3599
  let patched = false;
3586
3600
  try {
3587
3601
  this.deps.onStatus("opening");
3588
- this.deps.logger.info(`${opening(this.deps.kind)} ${paint.iris(String(port))}`);
3602
+ this.deps.logger.info(`${opening(this.deps.kind)} ${paint.sky.bold(String(port))}`);
3589
3603
  const url = await tunnel.open(attempt.signal, port, () => this.deps.onStatus("resolving"));
3590
3604
  if (superseded(attempt)) return;
3591
- this.deps.logger.info(`Reachable at ${paint.sky.italic(url)} ${since()}`);
3605
+ this.deps.logger.info(`Reachable at ${paint.sky.bold(url)} ${since()}`);
3592
3606
  this.deps.onStatus("registering");
3593
3607
  patched = true;
3594
3608
  await this.deps.endpoint.set(url, attempt.signal);