@seedcord/cli 0.1.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/dist/cli.mjs +359 -20
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +1 -3
- package/dist/index.mjs +1 -1
- package/dist/{src-BHkD3jCv.mjs → src-CZDimuFi.mjs} +2 -2
- package/dist/{src-BHkD3jCv.mjs.map → src-CZDimuFi.mjs.map} +1 -1
- package/package.json +6 -7
- package/dist/Hmr-BXZ-LFe5.d.mts +0 -33
- package/dist/api/vite-hmr.d.mts +0 -33
- package/dist/api/vite-hmr.mjs +0 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as SEEDCORD_CONFIG_FILENAMES, t as version } from "./src-
|
|
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 {
|
|
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
|
-
|
|
870
|
-
|
|
871
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) => {
|