@seedcord/cli 0.2.1 → 0.3.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
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { n as SEEDCORD_CONFIG_FILENAMES, t as version } from "./src-
|
|
1
|
+
import { n as SEEDCORD_CONFIG_FILENAMES, t as version } from "./src-DP3zjFwE.mjs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { Command } from "@commander-js/extra-typings";
|
|
4
4
|
import { Logger, LoggerChannelRegistry, StrictEventEmitter } from "@seedcord/services";
|
|
5
5
|
import { SeedcordErrorCode, isSeedcordError } from "@seedcord/errors";
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
7
|
-
import { SeedcordError } from "@seedcord/errors/internal";
|
|
7
|
+
import { SeedcordError, validateDiscordToken } from "@seedcord/errors/internal";
|
|
8
8
|
import { dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
9
9
|
import { pathToFileURL } from "node:url";
|
|
10
10
|
import { createJiti } from "jiti";
|
|
@@ -13,14 +13,18 @@ import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
|
13
13
|
import { spawn } from "node:child_process";
|
|
14
14
|
import { SeedcordBrand } from "@seedcord/types/internal";
|
|
15
15
|
import { assertNever, formatFilePath, isTsOrJsFile } from "@seedcord/utils";
|
|
16
|
-
import { ApplicationCommandOptionType, ApplicationCommandType } from "discord-api-types/v10";
|
|
16
|
+
import { ApplicationCommandOptionType, ApplicationCommandType, Routes } from "discord-api-types/v10";
|
|
17
17
|
import { routeLeavesOf } from "@seedcord/utils/internal";
|
|
18
|
+
import { Envapter } from "envapt";
|
|
19
|
+
import { REST } from "@discordjs/rest";
|
|
20
|
+
import chalk from "chalk";
|
|
21
|
+
import { autocompleteMultiselect, cancel, confirm, intro, isCancel, log, multiselect, note, outro, select, spinner } from "@clack/prompts";
|
|
22
|
+
import { createInterface } from "node:readline";
|
|
18
23
|
import { Box, Text, measureElement, render, useAnimation, useInput, useWindowSize } from "ink";
|
|
19
24
|
import React, { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
20
25
|
import Spinner from "ink-spinner";
|
|
21
26
|
import { createServer, createServerModuleRunner, defineConfig, mergeConfig } from "vite";
|
|
22
27
|
import { EvaluatedModules } from "vite/module-runner";
|
|
23
|
-
import chalk from "chalk";
|
|
24
28
|
import { minimatch } from "minimatch";
|
|
25
29
|
|
|
26
30
|
//#region src/core/BaseCommand.ts
|
|
@@ -469,7 +473,7 @@ var BuildCommand = class extends BaseCommand {
|
|
|
469
473
|
};
|
|
470
474
|
|
|
471
475
|
//#endregion
|
|
472
|
-
//#region src/commands/codegen/
|
|
476
|
+
//#region src/commands/codegen/AugmentationBuilder.ts
|
|
473
477
|
const KIND_BY_TYPE = {
|
|
474
478
|
[ApplicationCommandOptionType.String]: "string",
|
|
475
479
|
[ApplicationCommandOptionType.Integer]: "integer",
|
|
@@ -481,17 +485,23 @@ const KIND_BY_TYPE = {
|
|
|
481
485
|
[ApplicationCommandOptionType.Mentionable]: "mentionable",
|
|
482
486
|
[ApplicationCommandOptionType.Attachment]: "attachment"
|
|
483
487
|
};
|
|
488
|
+
function mapEmojis(emojiConfig) {
|
|
489
|
+
const emojis = {};
|
|
490
|
+
for (const [key, value] of Object.entries(emojiConfig)) emojis[key] = Array.isArray(value) ? "tuple" : "string";
|
|
491
|
+
return emojis;
|
|
492
|
+
}
|
|
484
493
|
/**
|
|
485
|
-
* Builds the generated
|
|
486
|
-
* tables, context-menu commands contribute their name to the user or
|
|
487
|
-
*
|
|
494
|
+
* Builds the generated augmentations from each command's `toJSON()` plus the emoji config. Chat-input
|
|
495
|
+
* commands become the slash-option tables, context-menu commands contribute their name to the user or
|
|
496
|
+
* message set, and each emoji key becomes a kind tag. Reads the builder back because djs erases option
|
|
497
|
+
* names at the type level.
|
|
488
498
|
*/
|
|
489
|
-
var
|
|
499
|
+
var AugmentationBuilder = class {
|
|
490
500
|
logger;
|
|
491
501
|
constructor(logger) {
|
|
492
502
|
this.logger = logger;
|
|
493
503
|
}
|
|
494
|
-
generate(commands) {
|
|
504
|
+
generate(commands, emojiConfig) {
|
|
495
505
|
const slash = {};
|
|
496
506
|
const sourceByRoute = /* @__PURE__ */ new Map();
|
|
497
507
|
const sourceByUserName = /* @__PURE__ */ new Map();
|
|
@@ -504,11 +514,13 @@ var RegistryGenerator = class {
|
|
|
504
514
|
}
|
|
505
515
|
const userContextMenus = [...sourceByUserName.keys()];
|
|
506
516
|
const messageContextMenus = [...sourceByMessageName.keys()];
|
|
507
|
-
|
|
517
|
+
const emojis = mapEmojis(emojiConfig);
|
|
518
|
+
this.logger.debug(`Generated ${Object.keys(slash).length} slash route(s), ${userContextMenus.length} user and ${messageContextMenus.length} message context-menu command(s), ${Object.keys(emojis).length} emoji(s)`);
|
|
508
519
|
return {
|
|
509
520
|
slash,
|
|
510
521
|
userContextMenus,
|
|
511
|
-
messageContextMenus
|
|
522
|
+
messageContextMenus,
|
|
523
|
+
emojis
|
|
512
524
|
};
|
|
513
525
|
}
|
|
514
526
|
collectSlash(json, sourceFile, slash, sourceByRoute) {
|
|
@@ -555,24 +567,20 @@ var RegistryGenerator = class {
|
|
|
555
567
|
};
|
|
556
568
|
|
|
557
569
|
//#endregion
|
|
558
|
-
//#region src/commands/codegen/
|
|
570
|
+
//#region src/commands/codegen/renderAugmentation.ts
|
|
559
571
|
const BANNER = `// Generated by \`seedcord codegen\`. Do not edit by hand.
|
|
560
|
-
// Run \`seedcord codegen\` after changing
|
|
561
|
-
const DISCLAIMER = ` /**
|
|
562
|
-
* These option types come from your command source. Redeploy your commands to Discord after regenerating,
|
|
563
|
-
* or an interaction from a stale command can return null for an option this file types as non-null.
|
|
564
|
-
*/`;
|
|
572
|
+
// Run \`seedcord codegen\` after changing your commands or emoji config.`;
|
|
565
573
|
const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
566
574
|
const CONTROL_CHAR = 32;
|
|
567
575
|
const HEX_RADIX = 16;
|
|
568
576
|
/**
|
|
569
|
-
* Renders the
|
|
570
|
-
* tables
|
|
577
|
+
* Renders the built augmentations into the committed `declare module 'seedcord'` file, the slash option
|
|
578
|
+
* tables, the user and message context-menu name registries, and the emoji map, all in one block. Keys are
|
|
571
579
|
* sorted so the output is stable across filesystems. The result is byte-stable so `seedcord codegen --check`
|
|
572
580
|
* can diff it directly.
|
|
573
581
|
*/
|
|
574
|
-
function
|
|
575
|
-
return `${BANNER}\n\ndeclare module 'seedcord' {\n
|
|
582
|
+
function renderAugmentation(registry) {
|
|
583
|
+
return `${BANNER}\n\ndeclare module 'seedcord' {\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 interface EmojiMap {\n${renderEmojiRows(registry.emojis)}\n }\n}\n\nexport {};\n`;
|
|
576
584
|
}
|
|
577
585
|
function renderSlashRows(tables) {
|
|
578
586
|
return Object.entries(tables).sort(([first], [second]) => compare(first, second)).map(([route, options]) => ` ${renderKey(route)}: ${renderRow(options)};`).join("\n");
|
|
@@ -580,6 +588,9 @@ function renderSlashRows(tables) {
|
|
|
580
588
|
function renderContextMenuRows(names) {
|
|
581
589
|
return [...names].sort(compare).map((name) => ` ${renderName(name)}: true;`).join("\n");
|
|
582
590
|
}
|
|
591
|
+
function renderEmojiRows(emojis) {
|
|
592
|
+
return Object.entries(emojis).sort(([first], [second]) => compare(first, second)).map(([key, kind]) => ` ${renderName(key)}: '${kind === "tuple" ? "guild" : "application"}';`).join("\n");
|
|
593
|
+
}
|
|
583
594
|
function renderRow(options) {
|
|
584
595
|
const entries = Object.entries(options);
|
|
585
596
|
if (entries.length === 0) return "{}";
|
|
@@ -622,11 +633,11 @@ function compare(first, second) {
|
|
|
622
633
|
|
|
623
634
|
//#endregion
|
|
624
635
|
//#region src/commands/codegen/CodegenRunner.ts
|
|
625
|
-
const OUTPUT_FILENAME = "
|
|
636
|
+
const OUTPUT_FILENAME = "seedcord-gen.d.ts";
|
|
626
637
|
/**
|
|
627
638
|
* Orchestrates `seedcord codegen`. Locates and loads the CLI config, imports the user's Seedcord instance to
|
|
628
|
-
* read its commands directory, scans and instantiates each command for its `toJSON()`, then
|
|
629
|
-
*
|
|
639
|
+
* read its commands directory and emoji config, scans and instantiates each command for its `toJSON()`, then
|
|
640
|
+
* renders the augmentations and either writes them or, under `--check`, diffs against the committed file.
|
|
630
641
|
*/
|
|
631
642
|
var CodegenRunner = class CodegenRunner {
|
|
632
643
|
locator;
|
|
@@ -643,11 +654,12 @@ var CodegenRunner = class CodegenRunner {
|
|
|
643
654
|
}
|
|
644
655
|
static create(logger) {
|
|
645
656
|
const moduleLoader = new RuntimeModuleLoader();
|
|
646
|
-
return new CodegenRunner(new ConfigLocator(logger), new ConfigLoader(moduleLoader, logger), moduleLoader, new
|
|
657
|
+
return new CodegenRunner(new ConfigLocator(logger), new ConfigLoader(moduleLoader, logger), moduleLoader, new AugmentationBuilder(logger), logger);
|
|
647
658
|
}
|
|
648
659
|
async run(check) {
|
|
649
660
|
const config = await this.loadConfig();
|
|
650
|
-
const
|
|
661
|
+
const { commands, emojis } = await this.scan(config);
|
|
662
|
+
const rendered = renderAugmentation(this.generator.generate(commands, emojis));
|
|
651
663
|
const outputPath = resolve(config.root, OUTPUT_FILENAME);
|
|
652
664
|
if (check) {
|
|
653
665
|
await this.check(rendered, outputPath);
|
|
@@ -659,11 +671,13 @@ var CodegenRunner = class CodegenRunner {
|
|
|
659
671
|
return this.configLoader.load(this.locator.locate());
|
|
660
672
|
}
|
|
661
673
|
async scan(config) {
|
|
662
|
-
const commandsDir = await this.
|
|
663
|
-
if (!commandsDir) return [];
|
|
674
|
+
const { commandsDir, emojis } = await this.resolveInstance(config);
|
|
664
675
|
const commands = [];
|
|
665
|
-
await this.walk(commandsDir, commands, /* @__PURE__ */ new Set(), true);
|
|
666
|
-
return
|
|
676
|
+
if (commandsDir) await this.walk(commandsDir, commands, /* @__PURE__ */ new Set(), true);
|
|
677
|
+
return {
|
|
678
|
+
commands,
|
|
679
|
+
emojis
|
|
680
|
+
};
|
|
667
681
|
}
|
|
668
682
|
async walk(dir, commands, seen, isRoot) {
|
|
669
683
|
let entries;
|
|
@@ -692,12 +706,15 @@ var CodegenRunner = class CodegenRunner {
|
|
|
692
706
|
}
|
|
693
707
|
}
|
|
694
708
|
}
|
|
695
|
-
async
|
|
709
|
+
async resolveInstance(config) {
|
|
696
710
|
this.logger.info("Loading instance to resolve the commands directory");
|
|
697
711
|
const instance = resolveDefaultExport(await this.moduleLoader.importModule(config.instance));
|
|
698
712
|
if (!this.isSeedcordInstance(instance)) throw new SeedcordError(SeedcordErrorCode.CliInstanceInvalid);
|
|
699
713
|
const commandsPath = instance.config.bot.commands.path;
|
|
700
|
-
return
|
|
714
|
+
return {
|
|
715
|
+
commandsDir: commandsPath ? resolve(process.cwd(), commandsPath) : void 0,
|
|
716
|
+
emojis: instance.config.bot.emojis ?? {}
|
|
717
|
+
};
|
|
701
718
|
}
|
|
702
719
|
commandJsonOf(exported) {
|
|
703
720
|
if (typeof exported !== "function") return void 0;
|
|
@@ -729,11 +746,11 @@ var CodegenRunner = class CodegenRunner {
|
|
|
729
746
|
async write(rendered, outputPath) {
|
|
730
747
|
await mkdir(dirname(outputPath), { recursive: true });
|
|
731
748
|
await writeFile(outputPath, rendered, "utf8");
|
|
732
|
-
this.logger.info(`
|
|
749
|
+
this.logger.info(`Augmentations written to ${outputPath}`);
|
|
733
750
|
}
|
|
734
751
|
async check(rendered, outputPath) {
|
|
735
752
|
if ((existsSync(outputPath) ? await readFile(outputPath, "utf8") : "") === rendered) return;
|
|
736
|
-
this.logger.error(`
|
|
753
|
+
this.logger.error(`Augmentations are out of date. Run \`seedcord codegen\` and commit ${outputPath}.`);
|
|
737
754
|
process.exitCode = 1;
|
|
738
755
|
}
|
|
739
756
|
};
|
|
@@ -743,11 +760,11 @@ var CodegenRunner = class CodegenRunner {
|
|
|
743
760
|
var CodegenCommand = class extends BaseCommand {
|
|
744
761
|
runner;
|
|
745
762
|
constructor() {
|
|
746
|
-
super("codegen", "Generate
|
|
763
|
+
super("codegen", "Generate typed augmentations (slash options, context menus, emojis) from your commands and config", "CLI:Codegen");
|
|
747
764
|
this.runner = CodegenRunner.create(this.logger);
|
|
748
765
|
}
|
|
749
766
|
register(program) {
|
|
750
|
-
program.command(this.name).description(this.description).option("--check", "Verify the committed
|
|
767
|
+
program.command(this.name).description(this.description).option("--check", "Verify the committed augmentations are up to date instead of writing them").action(async (options) => {
|
|
751
768
|
try {
|
|
752
769
|
await this.runner.run(options.check ?? false);
|
|
753
770
|
} catch (error) {
|
|
@@ -759,6 +776,547 @@ var CodegenCommand = class extends BaseCommand {
|
|
|
759
776
|
}
|
|
760
777
|
};
|
|
761
778
|
|
|
779
|
+
//#endregion
|
|
780
|
+
//#region src/core/interactive.ts
|
|
781
|
+
function isInteractive(opts, hasActionFlags) {
|
|
782
|
+
return !hasActionFlags && process.stdin.isTTY === true && process.stdout.isTTY === true && !process.env.CI && !opts.yes;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
//#endregion
|
|
786
|
+
//#region src/commands/commands/classify.ts
|
|
787
|
+
/**
|
|
788
|
+
* Selects which deployed guild commands `commands --clean` would delete. An overlap is a guild command whose
|
|
789
|
+
* name also exists globally, so it renders twice in the picker. Under `purge` every guild command is selected.
|
|
790
|
+
* Global commands are never passed in here, so they are never deleted.
|
|
791
|
+
*/
|
|
792
|
+
function classifyGuildCommands(globalNames, guilds, purge) {
|
|
793
|
+
const flagged = [];
|
|
794
|
+
for (const { guildId, guildName, commands } of guilds) for (const command of commands) if (purge) flagged.push({
|
|
795
|
+
guildId,
|
|
796
|
+
guildName,
|
|
797
|
+
id: command.id,
|
|
798
|
+
name: command.name,
|
|
799
|
+
reason: "purge"
|
|
800
|
+
});
|
|
801
|
+
else if (globalNames.has(command.name)) flagged.push({
|
|
802
|
+
guildId,
|
|
803
|
+
guildName,
|
|
804
|
+
id: command.id,
|
|
805
|
+
name: command.name,
|
|
806
|
+
reason: "overlap"
|
|
807
|
+
});
|
|
808
|
+
return flagged;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
//#endregion
|
|
812
|
+
//#region src/commands/commands/CleanRunner.ts
|
|
813
|
+
const GUILD_PAGE = 200;
|
|
814
|
+
var CleanRunner = class CleanRunner {
|
|
815
|
+
makeRest;
|
|
816
|
+
constructor(makeRest) {
|
|
817
|
+
this.makeRest = makeRest;
|
|
818
|
+
}
|
|
819
|
+
static create() {
|
|
820
|
+
return new CleanRunner((token) => new REST({ version: "10" }).setToken(token));
|
|
821
|
+
}
|
|
822
|
+
async resolveTargets(scope, token) {
|
|
823
|
+
if (scope.purge && scope.allGuilds) throw new SeedcordError(SeedcordErrorCode.CliCleanPurgeAllGuilds);
|
|
824
|
+
if (!scope.allGuilds && scope.guildIds.length === 0) throw new SeedcordError(SeedcordErrorCode.CliCleanNoGuilds);
|
|
825
|
+
const rest = this.makeRest(token);
|
|
826
|
+
return {
|
|
827
|
+
appId: await this.resolveAppId(rest),
|
|
828
|
+
guilds: scope.allGuilds ? await this.fetchBotGuilds(rest) : scope.guildIds.map((id) => ({
|
|
829
|
+
id,
|
|
830
|
+
name: id
|
|
831
|
+
}))
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
async listBotGuilds(token) {
|
|
835
|
+
return this.fetchBotGuilds(this.makeRest(token));
|
|
836
|
+
}
|
|
837
|
+
async scanGuilds(token, appId, guilds, purge) {
|
|
838
|
+
const rest = this.makeRest(token);
|
|
839
|
+
const globalNames = await this.fetchGlobalNames(rest, appId);
|
|
840
|
+
const buckets = [];
|
|
841
|
+
const skipped = [];
|
|
842
|
+
let scannedCommandCount = 0;
|
|
843
|
+
for (const guild of guilds) try {
|
|
844
|
+
const commands = (await rest.get(Routes.applicationGuildCommands(appId, guild.id))).map((command) => ({
|
|
845
|
+
id: command.id,
|
|
846
|
+
name: command.name
|
|
847
|
+
}));
|
|
848
|
+
scannedCommandCount += commands.length;
|
|
849
|
+
buckets.push({
|
|
850
|
+
guildId: guild.id,
|
|
851
|
+
guildName: guild.name,
|
|
852
|
+
commands
|
|
853
|
+
});
|
|
854
|
+
} catch (error) {
|
|
855
|
+
skipped.push({
|
|
856
|
+
guildId: guild.id,
|
|
857
|
+
guildName: guild.name,
|
|
858
|
+
reason: error instanceof Error ? error.message : "Unknown error"
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
return {
|
|
862
|
+
flagged: classifyGuildCommands(globalNames, buckets, purge),
|
|
863
|
+
skipped,
|
|
864
|
+
scannedGuildCount: buckets.length,
|
|
865
|
+
scannedCommandCount,
|
|
866
|
+
globalCommandCount: globalNames.size
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
async applyDeletions(token, appId, flagged) {
|
|
870
|
+
const rest = this.makeRest(token);
|
|
871
|
+
let deleted = 0;
|
|
872
|
+
const failed = [];
|
|
873
|
+
for (const command of flagged) try {
|
|
874
|
+
await rest.delete(Routes.applicationGuildCommand(appId, command.guildId, command.id));
|
|
875
|
+
deleted++;
|
|
876
|
+
} catch (error) {
|
|
877
|
+
failed.push({
|
|
878
|
+
command,
|
|
879
|
+
reason: error instanceof Error ? error.message : "Unknown error"
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
return {
|
|
883
|
+
deleted,
|
|
884
|
+
failed
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
async resolveAppId(rest) {
|
|
888
|
+
try {
|
|
889
|
+
return (await rest.get(Routes.currentApplication())).id;
|
|
890
|
+
} catch (error) {
|
|
891
|
+
const reason = error instanceof Error ? error.message : "Unknown error";
|
|
892
|
+
throw new SeedcordError(SeedcordErrorCode.CliCleanAppFetchFailed, [reason]);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
async fetchBotGuilds(rest) {
|
|
896
|
+
const guilds = [];
|
|
897
|
+
let after;
|
|
898
|
+
for (;;) {
|
|
899
|
+
const query = new URLSearchParams({ limit: String(GUILD_PAGE) });
|
|
900
|
+
if (after) query.set("after", after);
|
|
901
|
+
const page = await rest.get(Routes.userGuilds(), { query });
|
|
902
|
+
for (const guild of page) guilds.push({
|
|
903
|
+
id: guild.id,
|
|
904
|
+
name: guild.name
|
|
905
|
+
});
|
|
906
|
+
after = page.at(-1)?.id;
|
|
907
|
+
if (page.length < GUILD_PAGE || !after) break;
|
|
908
|
+
}
|
|
909
|
+
return guilds;
|
|
910
|
+
}
|
|
911
|
+
async fetchGlobalNames(rest, appId) {
|
|
912
|
+
const global = await rest.get(Routes.applicationCommands(appId));
|
|
913
|
+
return new Set(global.map((command) => command.name));
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
|
|
917
|
+
//#endregion
|
|
918
|
+
//#region src/core/format.ts
|
|
919
|
+
function plural(count, singular, pluralForm = `${singular}s`) {
|
|
920
|
+
return `${count} ${count === 1 ? singular : pluralForm}`;
|
|
921
|
+
}
|
|
922
|
+
function includesIgnoreCase(text, search) {
|
|
923
|
+
return text.toLowerCase().includes(search.trim().toLowerCase());
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
//#endregion
|
|
927
|
+
//#region src/core/prompts/requireValue.ts
|
|
928
|
+
function requireValue(value) {
|
|
929
|
+
if (isCancel(value)) {
|
|
930
|
+
cancel("Cancelled.");
|
|
931
|
+
throw new SeedcordError(SeedcordErrorCode.CliCancelled);
|
|
932
|
+
}
|
|
933
|
+
return value;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
//#endregion
|
|
937
|
+
//#region src/core/prompts/pickFromList.ts
|
|
938
|
+
const DEFAULT_MAX_ITEMS = 10;
|
|
939
|
+
const SEARCH_THRESHOLD = 12;
|
|
940
|
+
/**
|
|
941
|
+
* Multi-pick from a set, sized to the list: a plain checkbox multiselect for a short list, search-as-you-type
|
|
942
|
+
* for a long one. Returns the selected ids, or throws CliCancelled if the prompt is cancelled.
|
|
943
|
+
*/
|
|
944
|
+
async function pickFromList(opts) {
|
|
945
|
+
const options = opts.items.map((item) => ({
|
|
946
|
+
value: item.id,
|
|
947
|
+
label: item.name
|
|
948
|
+
}));
|
|
949
|
+
const maxItems = opts.maxItems ?? DEFAULT_MAX_ITEMS;
|
|
950
|
+
if (opts.items.length <= SEARCH_THRESHOLD) return requireValue(await multiselect({
|
|
951
|
+
message: opts.message,
|
|
952
|
+
options,
|
|
953
|
+
maxItems,
|
|
954
|
+
required: false
|
|
955
|
+
}));
|
|
956
|
+
return requireValue(await autocompleteMultiselect({
|
|
957
|
+
message: opts.message,
|
|
958
|
+
options,
|
|
959
|
+
maxItems,
|
|
960
|
+
placeholder: "Type to search...",
|
|
961
|
+
filter: (search, option) => includesIgnoreCase(option.label ?? "", search) || includesIgnoreCase(String(option.value), search)
|
|
962
|
+
}));
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
//#endregion
|
|
966
|
+
//#region src/core/prompts/index.ts
|
|
967
|
+
async function select$1(opts) {
|
|
968
|
+
return requireValue(await select(opts));
|
|
969
|
+
}
|
|
970
|
+
async function confirm$1(opts) {
|
|
971
|
+
return requireValue(await confirm(opts));
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
//#endregion
|
|
975
|
+
//#region src/commands/commands/confirm.ts
|
|
976
|
+
async function readlineAsk(question) {
|
|
977
|
+
const rl = createInterface({
|
|
978
|
+
input: process.stdin,
|
|
979
|
+
output: process.stdout
|
|
980
|
+
});
|
|
981
|
+
try {
|
|
982
|
+
return await new Promise((resolve) => rl.question(question, resolve));
|
|
983
|
+
} finally {
|
|
984
|
+
rl.close();
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* Gates a destructive delete behind typing the exact count, so a mistyped, empty, or non-numeric answer
|
|
989
|
+
* aborts with nothing deleted.
|
|
990
|
+
*/
|
|
991
|
+
async function confirmCount(count, logger, ask = readlineAsk) {
|
|
992
|
+
logger.warn(`About to delete ${count} guild command(s). This cannot be undone.`);
|
|
993
|
+
return (await ask(`Type ${count} to confirm (anything else aborts) `)).trim() === String(count);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
//#endregion
|
|
997
|
+
//#region src/commands/commands/cleanPresenters.ts
|
|
998
|
+
function groupByGuild(flagged) {
|
|
999
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1000
|
+
for (const command of flagged) {
|
|
1001
|
+
const group = groups.get(command.guildId) ?? {
|
|
1002
|
+
guildName: command.guildName,
|
|
1003
|
+
commands: []
|
|
1004
|
+
};
|
|
1005
|
+
group.commands.push(command);
|
|
1006
|
+
groups.set(command.guildId, group);
|
|
1007
|
+
}
|
|
1008
|
+
return [...groups.values()];
|
|
1009
|
+
}
|
|
1010
|
+
function skippedSummary(skipped) {
|
|
1011
|
+
return `Could not read ${plural(skipped.length, "guild")} (${skipped.map((s) => s.guildName).join(", ")}).`;
|
|
1012
|
+
}
|
|
1013
|
+
function emptyLines(scan, outcome) {
|
|
1014
|
+
return {
|
|
1015
|
+
"all-skipped": {
|
|
1016
|
+
info: [],
|
|
1017
|
+
outro: `Could not read any of the ${plural(scan.skipped.length, "guild")}, nothing was scanned.`
|
|
1018
|
+
},
|
|
1019
|
+
"no-commands": {
|
|
1020
|
+
info: [],
|
|
1021
|
+
outro: `Scanned ${plural(scan.scannedGuildCount, "guild")} with no guild commands deployed. Nothing to clean.`
|
|
1022
|
+
},
|
|
1023
|
+
"no-globals": {
|
|
1024
|
+
info: ["This app has no global commands, so nothing can duplicate one.", "Re-run and choose a full reset to remove guild commands."],
|
|
1025
|
+
outro: "Nothing to clean."
|
|
1026
|
+
},
|
|
1027
|
+
"no-overlaps": {
|
|
1028
|
+
info: [`Scanned ${plural(scan.scannedCommandCount, "guild command")} across ${plural(scan.scannedGuildCount, "guild")}. None duplicate a global command.`, "Re-run and choose a full reset to remove all of them."],
|
|
1029
|
+
outro: "Nothing to clean."
|
|
1030
|
+
}
|
|
1031
|
+
}[outcome];
|
|
1032
|
+
}
|
|
1033
|
+
/** Renders the clean flow as clack framed output for an interactive terminal. */
|
|
1034
|
+
var InteractivePresenter = class {
|
|
1035
|
+
async status(message, task, done) {
|
|
1036
|
+
const status = spinner();
|
|
1037
|
+
status.start(message);
|
|
1038
|
+
try {
|
|
1039
|
+
return await task();
|
|
1040
|
+
} finally {
|
|
1041
|
+
status.stop(done ?? message);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
preview(scan) {
|
|
1045
|
+
note(groupByGuild(scan.flagged).map((group) => `${chalk.bold(group.guildName)}\n${group.commands.map((c) => ` ${c.name}`).join("\n")}`).join("\n\n"), `Will delete ${plural(scan.flagged.length, "guild command")} (nothing deleted yet)`);
|
|
1046
|
+
if (scan.skipped.length > 0) log.warn(skippedSummary(scan.skipped));
|
|
1047
|
+
}
|
|
1048
|
+
largeBotGuard(guildCount) {
|
|
1049
|
+
return confirm$1({
|
|
1050
|
+
message: `The bot is in ${guildCount} guilds. Scan all of them? This makes ${guildCount} requests.`,
|
|
1051
|
+
initialValue: false
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
confirmDelete(count, skippedCount) {
|
|
1055
|
+
const skips = skippedCount > 0 ? ` ${plural(skippedCount, "guild")} could not be read.` : "";
|
|
1056
|
+
return confirm$1({
|
|
1057
|
+
message: `Delete ${plural(count, "guild command")}? This cannot be undone.${skips}`,
|
|
1058
|
+
initialValue: false
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
result(deletion, skipped) {
|
|
1062
|
+
for (const failure of deletion.failed) log.warn(`Could not delete ${failure.command.name} in ${failure.command.guildName} (${failure.reason}).`);
|
|
1063
|
+
if (skipped.length > 0) log.warn(skippedSummary(skipped));
|
|
1064
|
+
outro(`Deleted ${plural(deletion.deleted, "guild command")}. Global commands untouched.`);
|
|
1065
|
+
}
|
|
1066
|
+
nothingToClean(scan, outcome) {
|
|
1067
|
+
const lines = emptyLines(scan, outcome);
|
|
1068
|
+
for (const line of lines.info) log.info(line);
|
|
1069
|
+
outro(lines.outro);
|
|
1070
|
+
}
|
|
1071
|
+
dryRunHint() {
|
|
1072
|
+
outro("Dry run, nothing was deleted.");
|
|
1073
|
+
}
|
|
1074
|
+
};
|
|
1075
|
+
/** Renders the clean flow as plain logger lines for the flag, CI, and non-TTY path. */
|
|
1076
|
+
var FlagPresenter = class {
|
|
1077
|
+
logger;
|
|
1078
|
+
yes;
|
|
1079
|
+
constructor(logger, yes) {
|
|
1080
|
+
this.logger = logger;
|
|
1081
|
+
this.yes = yes;
|
|
1082
|
+
}
|
|
1083
|
+
status(message, task) {
|
|
1084
|
+
this.logger.info(message);
|
|
1085
|
+
return task();
|
|
1086
|
+
}
|
|
1087
|
+
preview(scan) {
|
|
1088
|
+
this.logger.info(chalk.bold(`${plural(scan.flagged.length, "guild command")} selected for deletion`));
|
|
1089
|
+
for (const group of groupByGuild(scan.flagged)) {
|
|
1090
|
+
this.logger.info(chalk.cyan(group.guildName));
|
|
1091
|
+
for (const command of group.commands) this.logger.info(` ${command.name}`);
|
|
1092
|
+
}
|
|
1093
|
+
for (const skip of scan.skipped) this.logger.warn(`Skipped ${skip.guildName} (${skip.reason}).`);
|
|
1094
|
+
}
|
|
1095
|
+
largeBotGuard(guildCount) {
|
|
1096
|
+
if (this.yes) return Promise.resolve(true);
|
|
1097
|
+
return Promise.reject(new SeedcordError(SeedcordErrorCode.CliCleanLargeBotUnconfirmed, [guildCount]));
|
|
1098
|
+
}
|
|
1099
|
+
confirmDelete(count, skippedCount) {
|
|
1100
|
+
if (this.yes) return Promise.resolve(true);
|
|
1101
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return Promise.reject(new SeedcordError(SeedcordErrorCode.CliCleanApplyNeedsYes));
|
|
1102
|
+
if (skippedCount > 0) this.logger.warn(`${plural(skippedCount, "guild")} could not be read and were not scanned.`);
|
|
1103
|
+
return confirmCount(count, this.logger);
|
|
1104
|
+
}
|
|
1105
|
+
result(deletion, skipped) {
|
|
1106
|
+
for (const failure of deletion.failed) this.logger.warn(`Could not delete ${failure.command.name} in ${failure.command.guildName} (${failure.reason}).`);
|
|
1107
|
+
if (skipped.length > 0) this.logger.warn(skippedSummary(skipped));
|
|
1108
|
+
this.logger.info(chalk.green(`Deleted ${plural(deletion.deleted, "guild command")}. Global commands untouched.`));
|
|
1109
|
+
}
|
|
1110
|
+
nothingToClean(scan, outcome) {
|
|
1111
|
+
const lines = emptyLines(scan, outcome);
|
|
1112
|
+
for (const line of lines.info) this.logger.info(line);
|
|
1113
|
+
this.logger.info(chalk.green(lines.outro));
|
|
1114
|
+
}
|
|
1115
|
+
dryRunHint() {
|
|
1116
|
+
this.logger.info(chalk.italic("Dry run. Re-run with --apply to delete the above."));
|
|
1117
|
+
}
|
|
1118
|
+
};
|
|
1119
|
+
|
|
1120
|
+
//#endregion
|
|
1121
|
+
//#region src/commands/commands/executeClean.ts
|
|
1122
|
+
const LARGE_BOT_THRESHOLD = 200;
|
|
1123
|
+
/** Why a scan found nothing to delete, so the caller can explain it instead of a bare "nothing to clean". */
|
|
1124
|
+
function emptyOutcome(scan, purge) {
|
|
1125
|
+
if (scan.scannedGuildCount === 0 && scan.skipped.length > 0) return "all-skipped";
|
|
1126
|
+
if (scan.scannedCommandCount === 0) return "no-commands";
|
|
1127
|
+
if (!purge && scan.globalCommandCount === 0) return "no-globals";
|
|
1128
|
+
return "no-overlaps";
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* The dry-run-then-confirm flow for `commands --clean`. Resolves targets, scans, previews, and only deletes
|
|
1132
|
+
* once `apply` is set and the presenter confirms. The presenter owns all output and the two confirmations,
|
|
1133
|
+
* so the wizard and the flag path share this flow with different presenters.
|
|
1134
|
+
*/
|
|
1135
|
+
async function executeClean(request) {
|
|
1136
|
+
const { runner, scope, apply, token, presenter, knownGuilds } = request;
|
|
1137
|
+
const { appId, guilds } = await presenter.status("Connecting to Discord...", () => runner.resolveTargets(scope, token), "Connected to Discord.");
|
|
1138
|
+
if (scope.allGuilds && guilds.length > 200) {
|
|
1139
|
+
if (!await presenter.largeBotGuard(guilds.length)) return;
|
|
1140
|
+
}
|
|
1141
|
+
const scan = await presenter.status(`Scanning ${plural(guilds.length, "guild")} for commands...`, () => runner.scanGuilds(token, appId, overlayNames(guilds, knownGuilds), scope.purge), `Scanned ${plural(guilds.length, "guild")}.`);
|
|
1142
|
+
if (scan.flagged.length === 0) {
|
|
1143
|
+
presenter.nothingToClean(scan, emptyOutcome(scan, scope.purge));
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
presenter.preview(scan);
|
|
1147
|
+
if (!apply) {
|
|
1148
|
+
presenter.dryRunHint();
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
if (!await presenter.confirmDelete(scan.flagged.length, scan.skipped.length)) return;
|
|
1152
|
+
const deletion = await presenter.status(`Deleting ${plural(scan.flagged.length, "guild command")}...`, () => runner.applyDeletions(token, appId, scan.flagged), "Deletion complete.");
|
|
1153
|
+
presenter.result(deletion, scan.skipped);
|
|
1154
|
+
}
|
|
1155
|
+
function overlayNames(guilds, knownGuilds) {
|
|
1156
|
+
if (!knownGuilds?.length) return guilds;
|
|
1157
|
+
const known = new Map(knownGuilds.map((guild) => [guild.id, guild.name]));
|
|
1158
|
+
return guilds.map((guild) => ({
|
|
1159
|
+
id: guild.id,
|
|
1160
|
+
name: known.get(guild.id) ?? guild.name
|
|
1161
|
+
}));
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
//#endregion
|
|
1165
|
+
//#region src/commands/commands/flagClean.ts
|
|
1166
|
+
/** Runs `seedcord commands --clean ...` headlessly from parsed flags, reproducing any wizard run. */
|
|
1167
|
+
async function runCleanFromFlags(runner, flags, token, logger) {
|
|
1168
|
+
await executeClean({
|
|
1169
|
+
runner,
|
|
1170
|
+
scope: {
|
|
1171
|
+
guildIds: flags.guildIds,
|
|
1172
|
+
allGuilds: flags.allGuilds,
|
|
1173
|
+
purge: flags.purge
|
|
1174
|
+
},
|
|
1175
|
+
apply: flags.apply,
|
|
1176
|
+
token,
|
|
1177
|
+
presenter: new FlagPresenter(logger, flags.yes)
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
//#endregion
|
|
1182
|
+
//#region src/commands/commands/wizard.ts
|
|
1183
|
+
async function runCleanWizard(runner, token) {
|
|
1184
|
+
intro("seedcord commands");
|
|
1185
|
+
const plan = await buildScope(runner, token);
|
|
1186
|
+
if (!plan) return;
|
|
1187
|
+
await executeClean({
|
|
1188
|
+
runner,
|
|
1189
|
+
scope: plan.scope,
|
|
1190
|
+
apply: true,
|
|
1191
|
+
token,
|
|
1192
|
+
presenter: new InteractivePresenter(),
|
|
1193
|
+
knownGuilds: plan.knownGuilds
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
async function buildScope(runner, token) {
|
|
1197
|
+
if (await select$1({
|
|
1198
|
+
message: "Remove guild commands that duplicate a global command. Which guilds?",
|
|
1199
|
+
options: [{
|
|
1200
|
+
value: "pick",
|
|
1201
|
+
label: "Pick specific guilds"
|
|
1202
|
+
}, {
|
|
1203
|
+
value: "all",
|
|
1204
|
+
label: "All guilds the bot is in",
|
|
1205
|
+
hint: "duplicates only, pick specific guilds for a full reset"
|
|
1206
|
+
}]
|
|
1207
|
+
}) === "all") return { scope: {
|
|
1208
|
+
guildIds: [],
|
|
1209
|
+
allGuilds: true,
|
|
1210
|
+
purge: false
|
|
1211
|
+
} };
|
|
1212
|
+
const status = spinner();
|
|
1213
|
+
status.start("Fetching the guilds the bot is in...");
|
|
1214
|
+
let guilds;
|
|
1215
|
+
try {
|
|
1216
|
+
guilds = await runner.listBotGuilds(token);
|
|
1217
|
+
} catch (error) {
|
|
1218
|
+
status.stop("Could not fetch the guild list.");
|
|
1219
|
+
throw error;
|
|
1220
|
+
}
|
|
1221
|
+
status.stop(`Found ${plural(guilds.length, "guild")}.`);
|
|
1222
|
+
if (guilds.length === 0) {
|
|
1223
|
+
outro("The bot is in no guilds.");
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
let guildIds;
|
|
1227
|
+
if (guilds.length > 1) {
|
|
1228
|
+
guildIds = await pickFromList({
|
|
1229
|
+
message: "Select guilds to clean",
|
|
1230
|
+
items: guilds
|
|
1231
|
+
});
|
|
1232
|
+
if (guildIds.length === 0) {
|
|
1233
|
+
outro("No guilds selected.");
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
} else {
|
|
1237
|
+
const only = guilds[0];
|
|
1238
|
+
if (!only) return void 0;
|
|
1239
|
+
guildIds = [only.id];
|
|
1240
|
+
log.info(`Only one guild, using ${only.name}.`);
|
|
1241
|
+
}
|
|
1242
|
+
const mode = await select$1({
|
|
1243
|
+
message: "What should I remove?",
|
|
1244
|
+
options: [{
|
|
1245
|
+
value: "overlap",
|
|
1246
|
+
label: "Only duplicates of a global command (recommended)"
|
|
1247
|
+
}, {
|
|
1248
|
+
value: "purge",
|
|
1249
|
+
label: "Every command in these guilds (full reset)"
|
|
1250
|
+
}]
|
|
1251
|
+
});
|
|
1252
|
+
return {
|
|
1253
|
+
scope: {
|
|
1254
|
+
guildIds,
|
|
1255
|
+
allGuilds: false,
|
|
1256
|
+
purge: mode === "purge"
|
|
1257
|
+
},
|
|
1258
|
+
knownGuilds: guilds
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
//#endregion
|
|
1263
|
+
//#region src/commands/commands/CommandsCommand.ts
|
|
1264
|
+
function hasCleanFlags(options) {
|
|
1265
|
+
return [
|
|
1266
|
+
options.clean,
|
|
1267
|
+
options.allGuilds,
|
|
1268
|
+
options.apply,
|
|
1269
|
+
options.purge
|
|
1270
|
+
].some(Boolean) || options.guild.length > 0;
|
|
1271
|
+
}
|
|
1272
|
+
var CommandsCommand = class extends BaseCommand {
|
|
1273
|
+
cleanRunner;
|
|
1274
|
+
constructor() {
|
|
1275
|
+
super("commands", "Inspect and clean deployed application commands", "CLI:Commands");
|
|
1276
|
+
this.cleanRunner = CleanRunner.create();
|
|
1277
|
+
}
|
|
1278
|
+
register(program) {
|
|
1279
|
+
program.command(this.name).description(this.description).option("--clean", "Report guild commands that duplicate a global command (deletes only with --apply)").option("--guild <ids...>", "Guild ids to inspect").option("--all-guilds", "Scan every guild the bot is in (overlaps only, cannot combine with --purge)").option("--apply", "Delete the reported commands instead of running a dry run").option("--purge", "Select every command in the named guilds, not only global overlaps").option("--yes", "Skip prompts and the typed-count confirm (for scripts and CI)").action(async (options) => this.run({
|
|
1280
|
+
clean: options.clean ?? false,
|
|
1281
|
+
guild: options.guild ?? [],
|
|
1282
|
+
allGuilds: options.allGuilds ?? false,
|
|
1283
|
+
apply: options.apply ?? false,
|
|
1284
|
+
purge: options.purge ?? false,
|
|
1285
|
+
yes: options.yes ?? false
|
|
1286
|
+
}));
|
|
1287
|
+
}
|
|
1288
|
+
async run(options) {
|
|
1289
|
+
try {
|
|
1290
|
+
await this.dispatch(options);
|
|
1291
|
+
} catch (error) {
|
|
1292
|
+
if (isSeedcordError(error, "SeedcordError", SeedcordErrorCode.CliCancelled)) return;
|
|
1293
|
+
this.logger.error("seedcord commands failed", error);
|
|
1294
|
+
if (isSeedcordError(error)) process.exitCode = 1;
|
|
1295
|
+
else process.exit(1);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
async dispatch(options) {
|
|
1299
|
+
const flags = hasCleanFlags(options);
|
|
1300
|
+
const interactive = isInteractive(options, flags);
|
|
1301
|
+
if (!interactive && !flags) {
|
|
1302
|
+
this.logger.info("Nothing to do. Pass --clean to inspect deployed commands.");
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
const token = validateDiscordToken(Envapter.get("DISCORD_BOT_TOKEN"));
|
|
1306
|
+
if (interactive) {
|
|
1307
|
+
await runCleanWizard(this.cleanRunner, token);
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
await runCleanFromFlags(this.cleanRunner, {
|
|
1311
|
+
guildIds: options.guild,
|
|
1312
|
+
allGuilds: options.allGuilds,
|
|
1313
|
+
apply: options.apply,
|
|
1314
|
+
purge: options.purge,
|
|
1315
|
+
yes: options.yes
|
|
1316
|
+
}, token, this.logger);
|
|
1317
|
+
}
|
|
1318
|
+
};
|
|
1319
|
+
|
|
762
1320
|
//#endregion
|
|
763
1321
|
//#region src/ui/hooks/useDevState.ts
|
|
764
1322
|
function useDevState(store) {
|
|
@@ -1012,10 +1570,10 @@ function handlePrompt(ctx) {
|
|
|
1012
1570
|
function handleToggleMode(ctx) {
|
|
1013
1571
|
if (!ctx.showToggles) return false;
|
|
1014
1572
|
const channels = LogStore.instance.getChannels();
|
|
1015
|
-
if (ctx.key.escape || ctx.input === "c") ctx.setShowToggles(false);
|
|
1573
|
+
if (ctx.key.escape || ctx.key.return || ctx.input === "c") ctx.setShowToggles(false);
|
|
1016
1574
|
else if (ctx.key.upArrow && channels.length > 0) ctx.setCursor((ctx.cursor + channels.length - 1) % channels.length);
|
|
1017
1575
|
else if (ctx.key.downArrow && channels.length > 0) ctx.setCursor((ctx.cursor + 1) % channels.length);
|
|
1018
|
-
else if (ctx.input === " "
|
|
1576
|
+
else if (ctx.input === " ") {
|
|
1019
1577
|
const channel = channels[ctx.cursor];
|
|
1020
1578
|
if (channel !== void 0) ctx.setEnabled(toggleChannel(ctx.enabled, channel, channels));
|
|
1021
1579
|
}
|
|
@@ -1344,7 +1902,7 @@ function HotkeyBar({ phase, interactive, mode, following }) {
|
|
|
1344
1902
|
keyLabel: "space",
|
|
1345
1903
|
action: "toggle"
|
|
1346
1904
|
}), /* @__PURE__ */ React.createElement(Hotkey, {
|
|
1347
|
-
keyLabel: "esc",
|
|
1905
|
+
keyLabel: "↵/esc",
|
|
1348
1906
|
action: "done"
|
|
1349
1907
|
})), mode === "default" && /* @__PURE__ */ React.createElement(DefaultKeys, {
|
|
1350
1908
|
phase,
|
|
@@ -2276,13 +2834,14 @@ var DevCommand = class extends BaseCommand {
|
|
|
2276
2834
|
|
|
2277
2835
|
//#endregion
|
|
2278
2836
|
//#region src/cli.ts
|
|
2279
|
-
const LOGGER_LABEL = "
|
|
2837
|
+
const LOGGER_LABEL = "seedcord CLI";
|
|
2280
2838
|
async function main() {
|
|
2281
2839
|
if (!process.env.ENV && !process.env.ENVIRONMENT && !process.env.NODE_ENV) process.env.NODE_ENV = "development";
|
|
2282
|
-
const program = new Command().name("seedcord").description("
|
|
2840
|
+
const program = new Command().name("seedcord").description("seedcord CLI").version(version);
|
|
2283
2841
|
new DevCommand().register(program);
|
|
2284
2842
|
new BuildCommand().register(program);
|
|
2285
2843
|
new CodegenCommand().register(program);
|
|
2844
|
+
new CommandsCommand().register(program);
|
|
2286
2845
|
await program.parseAsync(process.argv);
|
|
2287
2846
|
}
|
|
2288
2847
|
main().catch((error) => {
|