@seedcord/cli 0.1.0-next.0 → 0.2.0-next.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,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="../../assets/banner.png" alt="seedcord" width="100%" />
2
+ <img src="https://raw.githubusercontent.com/seedcord/seedcord/main/assets/banner.png" alt="seedcord" width="100%" />
3
3
  </p>
4
4
 
5
5
  ---
@@ -10,4 +10,4 @@ _This repository is a work in progress._
10
10
  - Till a major v1.0.0 release for seedcord, expect breaking changes in minor versions.
11
11
  - Documentation will come soon as well!
12
12
 
13
- I'm planning to release the first major version by Q1 2026. But till then, if you'd like to try using it, you can check out the code in `mock`
13
+ If you'd like to try it out, you can check out the code in `mock`
package/dist/cli.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { n as SEEDCORD_CONFIG_FILENAMES, t as version } from "./src-CCX-T6t_.mjs";
1
+ import { n as SEEDCORD_CONFIG_FILENAMES, t as version } from "./src-CZDimuFi.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import { Command } from "@commander-js/extra-typings";
4
4
  import { Logger, LoggerChannelRegistry, SeedcordErrorCode, StrictEventEmitter, isSeedcordError } from "@seedcord/services";
@@ -8,13 +8,15 @@ import { dirname, extname, isAbsolute, join, relative, resolve } from "node:path
8
8
  import { pathToFileURL } from "node:url";
9
9
  import { createJiti } from "jiti";
10
10
  import { tsImport } from "tsx/esm/api";
11
- import { mkdir, writeFile } from "node:fs/promises";
11
+ import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
12
12
  import { spawn } from "node:child_process";
13
- import { assertNever, formatFilePath } from "@seedcord/utils";
13
+ import { SeedcordBrand } from "@seedcord/types/internal";
14
+ import { assertNever, formatFilePath, isTsOrJsFile } from "@seedcord/utils";
15
+ import { ApplicationCommandOptionType, ApplicationCommandType } from "discord-api-types/v10";
16
+ import { routeLeavesOf } from "@seedcord/utils/internal";
14
17
  import { Box, Text, measureElement, render, useAnimation, useInput, useWindowSize } from "ink";
15
18
  import React, { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
16
19
  import Spinner from "ink-spinner";
17
- import { SeedcordBrand } from "@seedcord/types/internal";
18
20
  import { createServer, createServerModuleRunner, defineConfig, mergeConfig } from "vite";
19
21
  import { EvaluatedModules } from "vite/module-runner";
20
22
  import chalk from "chalk";
@@ -465,6 +467,297 @@ var BuildCommand = class extends BaseCommand {
465
467
  }
466
468
  };
467
469
 
470
+ //#endregion
471
+ //#region src/commands/codegen/RegistryGenerator.ts
472
+ const KIND_BY_TYPE = {
473
+ [ApplicationCommandOptionType.String]: "string",
474
+ [ApplicationCommandOptionType.Integer]: "integer",
475
+ [ApplicationCommandOptionType.Number]: "number",
476
+ [ApplicationCommandOptionType.Boolean]: "boolean",
477
+ [ApplicationCommandOptionType.User]: "user",
478
+ [ApplicationCommandOptionType.Channel]: "channel",
479
+ [ApplicationCommandOptionType.Role]: "role",
480
+ [ApplicationCommandOptionType.Mentionable]: "mentionable",
481
+ [ApplicationCommandOptionType.Attachment]: "attachment"
482
+ };
483
+ /**
484
+ * Builds the generated registry from each command's `toJSON()`. Chat-input commands become the slash-option
485
+ * tables, context-menu commands contribute their name to the user or message set. Reads the builder back
486
+ * because djs erases option names at the type level.
487
+ */
488
+ var RegistryGenerator = class {
489
+ logger;
490
+ constructor(logger) {
491
+ this.logger = logger;
492
+ }
493
+ generate(commands) {
494
+ const slash = {};
495
+ const sourceByRoute = /* @__PURE__ */ new Map();
496
+ const sourceByUserName = /* @__PURE__ */ new Map();
497
+ const sourceByMessageName = /* @__PURE__ */ new Map();
498
+ for (const command of commands) {
499
+ const { json } = command;
500
+ if (json.type === ApplicationCommandType.User) this.collectContextMenu("user", json, command.sourceFile, sourceByUserName);
501
+ else if (json.type === ApplicationCommandType.Message) this.collectContextMenu("message", json, command.sourceFile, sourceByMessageName);
502
+ else if (json.type === void 0 || json.type === ApplicationCommandType.ChatInput) this.collectSlash(json, command.sourceFile, slash, sourceByRoute);
503
+ }
504
+ const userContextMenus = [...sourceByUserName.keys()];
505
+ const messageContextMenus = [...sourceByMessageName.keys()];
506
+ this.logger.debug(`Generated ${Object.keys(slash).length} slash route(s), ${userContextMenus.length} user and ${messageContextMenus.length} message context-menu command(s)`);
507
+ return {
508
+ slash,
509
+ userContextMenus,
510
+ messageContextMenus
511
+ };
512
+ }
513
+ collectSlash(json, sourceFile, slash, sourceByRoute) {
514
+ this.warnEmptyGroups(json);
515
+ for (const leaf of routeLeavesOf(json)) {
516
+ const firstFile = sourceByRoute.get(leaf.route);
517
+ if (firstFile !== void 0) throw new SeedcordError(SeedcordErrorCode.CliCodegenDuplicateRoute, [
518
+ leaf.route,
519
+ firstFile,
520
+ sourceFile
521
+ ]);
522
+ sourceByRoute.set(leaf.route, sourceFile);
523
+ slash[leaf.route] = this.mapOptions(leaf.options);
524
+ }
525
+ }
526
+ collectContextMenu(kind, json, sourceFile, sourceByName) {
527
+ const firstFile = sourceByName.get(json.name);
528
+ if (firstFile !== void 0) throw new SeedcordError(SeedcordErrorCode.CliCodegenDuplicateContextMenu, [
529
+ kind,
530
+ json.name,
531
+ firstFile,
532
+ sourceFile
533
+ ]);
534
+ sourceByName.set(json.name, sourceFile);
535
+ }
536
+ warnEmptyGroups(json) {
537
+ for (const option of json.options ?? []) if (option.type === ApplicationCommandOptionType.SubcommandGroup && (option.options ?? []).length === 0) this.logger.warn(`Slash group \`${json.name}/${option.name}\` has no subcommands and will not deploy.`);
538
+ }
539
+ mapOptions(options) {
540
+ const table = {};
541
+ for (const option of options) {
542
+ if (option.type === ApplicationCommandOptionType.Subcommand || option.type === ApplicationCommandOptionType.SubcommandGroup) continue;
543
+ const choices = "choices" in option && option.choices && option.choices.length > 0 ? option.choices.map((choice) => choice.value) : void 0;
544
+ const autocomplete = "autocomplete" in option && option.autocomplete === true ? true : void 0;
545
+ table[option.name] = {
546
+ kind: KIND_BY_TYPE[option.type],
547
+ required: option.required ?? false,
548
+ ...choices ? { choices } : {},
549
+ ...autocomplete ? { autocomplete } : {}
550
+ };
551
+ }
552
+ return table;
553
+ }
554
+ };
555
+
556
+ //#endregion
557
+ //#region src/commands/codegen/renderRegistry.ts
558
+ const BANNER = `// Generated by \`seedcord codegen\`. Do not edit by hand.
559
+ // Run \`seedcord codegen\` after changing a command's options.`;
560
+ const DISCLAIMER = ` /**
561
+ * These option types come from your command source. Redeploy your commands to Discord after regenerating,
562
+ * or an interaction from a stale command can return null for an option this file types as non-null.
563
+ */`;
564
+ const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
565
+ const CONTROL_CHAR = 32;
566
+ const HEX_RADIX = 16;
567
+ /**
568
+ * Renders the generated registry into the committed `declare module 'seedcord'` file, the slash option
569
+ * tables plus the user and message context-menu name registries, all in one augmentation block. Keys are
570
+ * sorted so the output is stable across filesystems. The result is byte-stable so `seedcord codegen --check`
571
+ * can diff it directly.
572
+ */
573
+ function renderRegistry(registry) {
574
+ return `${BANNER}\n\ndeclare module 'seedcord' {\n${DISCLAIMER}\n interface SlashOptionRegistry {\n${renderSlashRows(registry.slash)}\n }\n interface UserContextMenuRegistry {\n${renderContextMenuRows(registry.userContextMenus)}\n }\n interface MessageContextMenuRegistry {\n${renderContextMenuRows(registry.messageContextMenus)}\n }\n}\n\nexport {};\n`;
575
+ }
576
+ function renderSlashRows(tables) {
577
+ return Object.entries(tables).sort(([first], [second]) => compare(first, second)).map(([route, options]) => ` ${renderKey(route)}: ${renderRow(options)};`).join("\n");
578
+ }
579
+ function renderContextMenuRows(names) {
580
+ return [...names].sort(compare).map((name) => ` ${renderName(name)}: true;`).join("\n");
581
+ }
582
+ function renderRow(options) {
583
+ const entries = Object.entries(options);
584
+ if (entries.length === 0) return "{}";
585
+ return `{ ${entries.map(([name, opt]) => `${renderKey(name)}: ${renderOption(opt)}`).join("; ")} }`;
586
+ }
587
+ function renderOption(opt) {
588
+ const parts = [`kind: '${opt.kind}'`, `required: ${opt.required}`];
589
+ if (opt.choices && opt.choices.length > 0) parts.push(`choices: [${opt.choices.map(renderChoice).join(", ")}]`);
590
+ if (opt.autocomplete) parts.push("autocomplete: true");
591
+ return `{ ${parts.join("; ")} }`;
592
+ }
593
+ function renderChoice(value) {
594
+ if (typeof value !== "string") return String(value);
595
+ return `'${escapeLiteral(value)}'`;
596
+ }
597
+ function renderName(name) {
598
+ return IDENTIFIER.test(name) ? name : `'${escapeLiteral(name)}'`;
599
+ }
600
+ function renderKey(name) {
601
+ return IDENTIFIER.test(name) ? name : `'${name}'`;
602
+ }
603
+ function escapeLiteral(value) {
604
+ let escaped = "";
605
+ for (const char of value) {
606
+ const code = char.charCodeAt(0);
607
+ if (char === "\\") escaped += "\\\\";
608
+ else if (char === "'") escaped += "\\'";
609
+ else if (char === "\n") escaped += "\\n";
610
+ else if (char === "\r") escaped += "\\r";
611
+ else if (char === " ") escaped += "\\t";
612
+ else if (code < CONTROL_CHAR) escaped += `\\x${code.toString(HEX_RADIX).padStart(2, "0")}`;
613
+ else escaped += char;
614
+ }
615
+ return escaped;
616
+ }
617
+ function compare(first, second) {
618
+ if (first < second) return -1;
619
+ return first > second ? 1 : 0;
620
+ }
621
+
622
+ //#endregion
623
+ //#region src/commands/codegen/CodegenRunner.ts
624
+ const OUTPUT_FILENAME = "command-registry.gen.ts";
625
+ /**
626
+ * Orchestrates `seedcord codegen`. Locates and loads the CLI config, imports the user's Seedcord instance to
627
+ * read its commands directory, scans and instantiates each command for its `toJSON()`, then renders the
628
+ * registry and either writes it or, under `--check`, diffs it against the committed file.
629
+ */
630
+ var CodegenRunner = class CodegenRunner {
631
+ locator;
632
+ configLoader;
633
+ moduleLoader;
634
+ generator;
635
+ logger;
636
+ constructor(locator, configLoader, moduleLoader, generator, logger) {
637
+ this.locator = locator;
638
+ this.configLoader = configLoader;
639
+ this.moduleLoader = moduleLoader;
640
+ this.generator = generator;
641
+ this.logger = logger;
642
+ }
643
+ static create(logger) {
644
+ const moduleLoader = new RuntimeModuleLoader();
645
+ return new CodegenRunner(new ConfigLocator(logger), new ConfigLoader(moduleLoader, logger), moduleLoader, new RegistryGenerator(logger), logger);
646
+ }
647
+ async run(check) {
648
+ const config = await this.loadConfig();
649
+ const rendered = renderRegistry(this.generator.generate(await this.scan(config)));
650
+ const outputPath = resolve(config.root, OUTPUT_FILENAME);
651
+ if (check) {
652
+ await this.check(rendered, outputPath);
653
+ return;
654
+ }
655
+ await this.write(rendered, outputPath);
656
+ }
657
+ async loadConfig() {
658
+ return this.configLoader.load(this.locator.locate());
659
+ }
660
+ async scan(config) {
661
+ const commandsDir = await this.resolveCommandsDir(config);
662
+ if (!commandsDir) return [];
663
+ const commands = [];
664
+ await this.walk(commandsDir, commands, /* @__PURE__ */ new Set(), true);
665
+ return commands;
666
+ }
667
+ async walk(dir, commands, seen, isRoot) {
668
+ let entries;
669
+ try {
670
+ entries = await readdir(dir, { withFileTypes: true });
671
+ } catch (error) {
672
+ const reason = error instanceof Error ? error.message : "Unknown error";
673
+ if (isRoot) throw new SeedcordError(SeedcordErrorCode.CliCodegenCommandsDirUnreadable, [dir, reason]);
674
+ this.logger.warn(`Skipping unreadable directory ${dir}. ${reason}.`);
675
+ return;
676
+ }
677
+ for (const entry of entries) {
678
+ const fullPath = join(dir, entry.name);
679
+ if (entry.isDirectory()) await this.walk(fullPath, commands, seen, false);
680
+ else if (isTsOrJsFile(entry)) {
681
+ const imported = await this.moduleLoader.importModule(fullPath);
682
+ for (const exported of Object.values(imported)) {
683
+ if (seen.has(exported)) continue;
684
+ seen.add(exported);
685
+ const json = this.commandJsonOf(exported);
686
+ if (json) commands.push({
687
+ sourceFile: fullPath,
688
+ json
689
+ });
690
+ }
691
+ }
692
+ }
693
+ }
694
+ async resolveCommandsDir(config) {
695
+ this.logger.info("Loading instance to resolve the commands directory");
696
+ const instance = resolveDefaultExport(await this.moduleLoader.importModule(config.instance));
697
+ if (!this.isSeedcordInstance(instance)) throw new SeedcordError(SeedcordErrorCode.CliInstanceInvalid);
698
+ const commandsPath = instance.config.bot.commands.path;
699
+ return commandsPath ? resolve(process.cwd(), commandsPath) : void 0;
700
+ }
701
+ commandJsonOf(exported) {
702
+ if (typeof exported !== "function") return void 0;
703
+ let instance;
704
+ try {
705
+ instance = new exported();
706
+ } catch {
707
+ return;
708
+ }
709
+ if (!this.isBuilderLike(instance)) return void 0;
710
+ const json = instance.component.toJSON();
711
+ if (!this.isApplicationCommand(json)) return void 0;
712
+ return json;
713
+ }
714
+ isBuilderLike(value) {
715
+ if (typeof value !== "object" || value === null) return false;
716
+ const component = value.component;
717
+ return typeof component === "object" && component !== null && typeof component.toJSON === "function";
718
+ }
719
+ isApplicationCommand(json) {
720
+ if (typeof json !== "object" || json === null) return false;
721
+ const { name, type } = json;
722
+ if (typeof name !== "string") return false;
723
+ return type === void 0 || type === ApplicationCommandType.ChatInput || type === ApplicationCommandType.User || type === ApplicationCommandType.Message;
724
+ }
725
+ isSeedcordInstance(candidate) {
726
+ return typeof candidate === "object" && candidate !== null && candidate[SeedcordBrand] === true;
727
+ }
728
+ async write(rendered, outputPath) {
729
+ await mkdir(dirname(outputPath), { recursive: true });
730
+ await writeFile(outputPath, rendered, "utf8");
731
+ this.logger.info(`Command registry written to ${outputPath}`);
732
+ }
733
+ async check(rendered, outputPath) {
734
+ if ((existsSync(outputPath) ? await readFile(outputPath, "utf8") : "") === rendered) return;
735
+ this.logger.error(`Command registry is out of date. Run \`seedcord codegen\` and commit ${outputPath}.`);
736
+ process.exitCode = 1;
737
+ }
738
+ };
739
+
740
+ //#endregion
741
+ //#region src/commands/codegen/CodegenCommand.ts
742
+ var CodegenCommand = class extends BaseCommand {
743
+ runner;
744
+ constructor() {
745
+ super("codegen", "Generate the typed command registry (slash options and context menus) from your commands", "CLI:Codegen");
746
+ this.runner = CodegenRunner.create(this.logger);
747
+ }
748
+ register(program) {
749
+ program.command(this.name).description(this.description).option("--check", "Verify the committed registry is up to date instead of writing it").action(async (options) => {
750
+ try {
751
+ await this.runner.run(options.check ?? false);
752
+ } catch (error) {
753
+ this.logger.error("Seedcord codegen failed", error);
754
+ if (isSeedcordError(error)) process.exitCode = 1;
755
+ else process.exit(1);
756
+ }
757
+ });
758
+ }
759
+ };
760
+
468
761
  //#endregion
469
762
  //#region src/ui/hooks/useDevState.ts
470
763
  function useDevState(store) {
@@ -865,10 +1158,16 @@ const PALETTE = [
865
1158
  "blue",
866
1159
  "cyanBright"
867
1160
  ];
1161
+ const assigned = /* @__PURE__ */ new Map();
868
1162
  function channelColor(channel) {
869
- let hash = 0;
870
- for (const char of channel) hash = (hash + char.charCodeAt(0)) % PALETTE.length;
871
- return PALETTE[hash] ?? PALETTE[0];
1163
+ const existing = assigned.get(channel);
1164
+ if (existing) return existing;
1165
+ const color = PALETTE[assigned.size % PALETTE.length] ?? PALETTE[0];
1166
+ assigned.set(channel, color);
1167
+ return color;
1168
+ }
1169
+ function resetChannelColors() {
1170
+ assigned.clear();
872
1171
  }
873
1172
 
874
1173
  //#endregion
@@ -929,7 +1228,7 @@ function Banner({ config, compact = false }) {
929
1228
  return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, /* @__PURE__ */ React.createElement(Wordmark, null), /* @__PURE__ */ React.createElement(Box, {
930
1229
  flexDirection: "column",
931
1230
  paddingTop: 1
932
- }, /* @__PURE__ */ React.createElement(Text, { wrap: "truncate" }, /* @__PURE__ */ React.createElement(Text, { color: "blue" }, "➜"), " Interactions: ", /* @__PURE__ */ React.createElement(ConfigPath, { path: config.bot.interactions.path })), /* @__PURE__ */ React.createElement(Text, { wrap: "truncate" }, /* @__PURE__ */ React.createElement(Text, { color: "blue" }, "➜"), " Events: ", /* @__PURE__ */ React.createElement(ConfigPath, { path: config.bot.events.path })), /* @__PURE__ */ React.createElement(Text, { wrap: "truncate" }, /* @__PURE__ */ React.createElement(Text, { color: "blue" }, "➜"), " Pub/Sub: ", /* @__PURE__ */ React.createElement(ConfigPath, { path: config.subscribers.path }))));
1231
+ }, /* @__PURE__ */ React.createElement(Text, null, /* @__PURE__ */ React.createElement(Text, { color: "blue" }, "➜"), " Interactions: ", /* @__PURE__ */ React.createElement(ConfigPath, { path: config.bot.interactions.path })), /* @__PURE__ */ React.createElement(Text, null, /* @__PURE__ */ React.createElement(Text, { color: "blue" }, "➜"), " Events: ", /* @__PURE__ */ React.createElement(ConfigPath, { path: config.bot.events.path })), /* @__PURE__ */ React.createElement(Text, null, /* @__PURE__ */ React.createElement(Text, { color: "blue" }, "➜"), " Pub/Sub: ", /* @__PURE__ */ React.createElement(ConfigPath, { path: config.subscribers.path }))));
933
1232
  }
934
1233
 
935
1234
  //#endregion
@@ -1056,6 +1355,7 @@ function HotkeyBar({ phase, interactive, mode, following }) {
1056
1355
  //#endregion
1057
1356
  //#region src/ui/components/primitives/Sidebar.tsx
1058
1357
  const COMPACT_ROWS = 26;
1358
+ const MAX_RAIL = 40;
1059
1359
  const META_LABEL_WIDTH = 5;
1060
1360
  function logDir() {
1061
1361
  const registry = LoggerChannelRegistry.instance;
@@ -1063,11 +1363,11 @@ function logDir() {
1063
1363
  return path ? formatFilePath(path, { onlyDir: true }) : null;
1064
1364
  }
1065
1365
  function Meta({ label, value }) {
1066
- return /* @__PURE__ */ React.createElement(Text, { wrap: "truncate" }, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, label.padEnd(META_LABEL_WIDTH)), value);
1366
+ return /* @__PURE__ */ React.createElement(Text, null, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, label.padEnd(META_LABEL_WIDTH)), value);
1067
1367
  }
1068
1368
  function StatusBlock({ state, uptimeMs }) {
1069
1369
  const dir = logDir();
1070
- return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, /* @__PURE__ */ React.createElement(StatusBadge, { phase: state.phase }), state.status ? /* @__PURE__ */ React.createElement(Text, { wrap: "truncate" }, state.status) : null, uptimeMs === null ? null : /* @__PURE__ */ React.createElement(Meta, {
1370
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, /* @__PURE__ */ React.createElement(StatusBadge, { phase: state.phase }), state.status ? /* @__PURE__ */ React.createElement(Text, null, state.status) : null, uptimeMs === null ? null : /* @__PURE__ */ React.createElement(Meta, {
1071
1371
  label: "up",
1072
1372
  value: formatUptime(uptimeMs)
1073
1373
  }), dir === null ? null : /* @__PURE__ */ React.createElement(Meta, {
@@ -1075,12 +1375,15 @@ function StatusBlock({ state, uptimeMs }) {
1075
1375
  value: dir
1076
1376
  }));
1077
1377
  }
1078
- function Sidebar({ state, enabled, uptimeMs, following, interactive, showToggles, cursor, width }) {
1378
+ function Sidebar({ state, enabled, uptimeMs, following, interactive, showToggles, cursor, width, ref }) {
1079
1379
  const { rows } = useWindowSize();
1080
1380
  const compact = rows < COMPACT_ROWS;
1081
1381
  return /* @__PURE__ */ React.createElement(Box, {
1382
+ ref,
1082
1383
  flexDirection: "column",
1083
- width,
1384
+ width: width ?? void 0,
1385
+ maxWidth: MAX_RAIL,
1386
+ flexShrink: 0,
1084
1387
  paddingX: 1,
1085
1388
  overflow: "hidden"
1086
1389
  }, /* @__PURE__ */ React.createElement(Box, { flexShrink: 0 }, /* @__PURE__ */ React.createElement(Banner, {
@@ -1115,14 +1418,11 @@ function Sidebar({ state, enabled, uptimeMs, following, interactive, showToggles
1115
1418
 
1116
1419
  //#endregion
1117
1420
  //#region src/ui/layout/DevLayout.tsx
1118
- const MAX_RAIL = 40;
1119
- const MIN_RAIL = 26;
1120
- const RAIL_FRACTION = .32;
1121
1421
  function DevLayout(props) {
1122
- const { state, columns, logBoxRef, scroll, viewportHeight, measured } = props;
1422
+ const { state, railRef, railWidth, logBoxRef, scroll, viewportHeight, measured } = props;
1123
1423
  const { enabled, showToggles, cursor, interactive, uptimeMs } = props;
1124
- const railWidth = Math.min(MAX_RAIL, Math.max(MIN_RAIL, Math.floor(columns * RAIL_FRACTION)));
1125
1424
  return /* @__PURE__ */ React.createElement(Box, { flexGrow: 1 }, /* @__PURE__ */ React.createElement(Sidebar, {
1425
+ ref: railRef,
1126
1426
  state,
1127
1427
  enabled,
1128
1428
  uptimeMs,
@@ -1134,6 +1434,7 @@ function DevLayout(props) {
1134
1434
  }), /* @__PURE__ */ React.createElement(Box, {
1135
1435
  flexDirection: "column",
1136
1436
  flexGrow: 1,
1437
+ minWidth: 0,
1137
1438
  borderStyle: "single",
1138
1439
  borderColor: "gray",
1139
1440
  borderTop: false,
@@ -1174,6 +1475,9 @@ function DevApp(props) {
1174
1475
  const [cursor, setCursor] = useState(0);
1175
1476
  const logBoxRef = useRef(null);
1176
1477
  const [logBoxHeight, setLogBoxHeight] = useState(0);
1478
+ const railRef = useRef(null);
1479
+ const [railWidth, setRailWidth] = useState(null);
1480
+ const measuredConfig = useRef(null);
1177
1481
  const logs = useLogs(enabled);
1178
1482
  const viewportHeight = Math.max(1, logBoxHeight);
1179
1483
  const scroll = useScroll(logs, viewportHeight, logKey);
@@ -1190,6 +1494,18 @@ function DevApp(props) {
1190
1494
  state.restartRequired,
1191
1495
  state.commandUpdatePrompt
1192
1496
  ]);
1497
+ useEffect(() => {
1498
+ if (!railRef.current || !state.config || measuredConfig.current === state.config) return;
1499
+ const measured = measureElement(railRef.current).width;
1500
+ if (measured > 0) {
1501
+ measuredConfig.current = state.config;
1502
+ setRailWidth(measured);
1503
+ }
1504
+ }, [
1505
+ rows,
1506
+ columns,
1507
+ state.config
1508
+ ]);
1193
1509
  useInput((input, key) => {
1194
1510
  dispatchHotkey({
1195
1511
  input,
@@ -1228,7 +1544,8 @@ function DevApp(props) {
1228
1544
  overflow: "hidden"
1229
1545
  }, /* @__PURE__ */ React.createElement(DevLayout, {
1230
1546
  state,
1231
- columns,
1547
+ railRef,
1548
+ railWidth,
1232
1549
  logBoxRef,
1233
1550
  scroll,
1234
1551
  viewportHeight,
@@ -1782,19 +2099,27 @@ var DevRunner = class DevRunner {
1782
2099
  locator;
1783
2100
  configLoader;
1784
2101
  store;
2102
+ codegen;
2103
+ codegenLogger;
1785
2104
  currentSession = null;
1786
2105
  signalResolve;
1787
2106
  shouldQuit = false;
1788
2107
  isDisconnected = false;
1789
2108
  isRunning = false;
1790
- constructor(locator, configLoader, store) {
2109
+ isRegenerating = false;
2110
+ constructor(locator, configLoader, store, codegen, codegenLogger) {
1791
2111
  this.locator = locator;
1792
2112
  this.configLoader = configLoader;
1793
2113
  this.store = store;
2114
+ this.codegen = codegen;
2115
+ this.codegenLogger = codegenLogger;
1794
2116
  }
1795
2117
  static create(logger, store) {
1796
2118
  const moduleLoader = new RuntimeModuleLoader();
1797
- return new DevRunner(new ConfigLocator(logger), new ConfigLoader(moduleLoader, logger), store);
2119
+ const locator = new ConfigLocator(logger);
2120
+ const configLoader = new ConfigLoader(moduleLoader, logger);
2121
+ const codegenLogger = new Logger("CLI:Codegen");
2122
+ return new DevRunner(locator, configLoader, store, CodegenRunner.create(codegenLogger), codegenLogger);
1798
2123
  }
1799
2124
  async run() {
1800
2125
  if (this.isRunning) return;
@@ -1823,6 +2148,7 @@ var DevRunner = class DevRunner {
1823
2148
  if (!this.shouldQuit) this.isDisconnected = false;
1824
2149
  }
1825
2150
  async runSession() {
2151
+ resetChannelColors();
1826
2152
  this.store.setPhase("starting");
1827
2153
  this.store.setBusy(true);
1828
2154
  const config = await this.loadConfig();
@@ -1864,8 +2190,20 @@ var DevRunner = class DevRunner {
1864
2190
  this.signalResolve?.();
1865
2191
  }
1866
2192
  refreshCommands(shouldRefresh) {
2193
+ if (shouldRefresh) this.regenerateRegistry();
1867
2194
  this.currentSession?.refreshCommands(shouldRefresh);
1868
2195
  }
2196
+ async regenerateRegistry() {
2197
+ if (this.isRegenerating) return;
2198
+ this.isRegenerating = true;
2199
+ try {
2200
+ await this.codegen.run(false);
2201
+ } catch (error) {
2202
+ this.codegenLogger.error("Command registry regeneration failed", error);
2203
+ } finally {
2204
+ this.isRegenerating = false;
2205
+ }
2206
+ }
1869
2207
  async waitForSignal() {
1870
2208
  return new Promise((resolve) => {
1871
2209
  this.signalResolve = resolve;
@@ -1943,6 +2281,7 @@ async function main() {
1943
2281
  const program = new Command().name("seedcord").description("Seedcord CLI").version(version);
1944
2282
  new DevCommand().register(program);
1945
2283
  new BuildCommand().register(program);
2284
+ new CodegenCommand().register(program);
1946
2285
  await program.parseAsync(process.argv);
1947
2286
  }
1948
2287
  main().catch((error) => {