From 78c614c69f77bd8098b93f6b31ffcb7ee3c0283e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 15:09:18 +0000 Subject: [PATCH 1/6] Initial plan From b8afc1719120a78865ced1eaf1e5566d65cd6d5a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 15:12:15 +0000 Subject: [PATCH 2/6] Add user config feature support Co-authored-by: Cleboost <61158869+Cleboost@users.noreply.github.com> --- packages/dev/commands/build.ts | 8 +- .../dev/commands/generate-config-types.ts | 126 ++++++++++++++++++ packages/dev/index.ts | 2 + packages/dev/utils/common.ts | 18 ++- packages/runtime/DjsClient.ts | 6 +- packages/utils/types/config.d.ts | 1 + 6 files changed, 156 insertions(+), 5 deletions(-) create mode 100644 packages/dev/commands/generate-config-types.ts diff --git a/packages/dev/commands/build.ts b/packages/dev/commands/build.ts index 6764f9c..2cd7a3d 100644 --- a/packages/dev/commands/build.ts +++ b/packages/dev/commands/build.ts @@ -76,6 +76,7 @@ function buildGeneratedEntry(opts: { eventFiles: string[]; cronFiles: string[]; hasCronEnabled: boolean; + hasUserConfigEnabled: boolean; }): string { const { genDir, @@ -169,6 +170,7 @@ function buildGeneratedEntry(opts: { import config from "../djs.config.ts"; import { DjsClient, type Route } from "@djs-core/runtime"; import { Events } from "discord.js"; +${opts.hasUserConfigEnabled ? 'import type { UserConfig } from "../config.types.ts";\nimport userConfigData from "../config.json" with { type: "json" };' : ""} ${imports.join("\n")} @@ -222,7 +224,7 @@ ${sortedCrons.map((c) => ` [${JSON.stringify(c.id)}, ${c.varName}],`).join("\ : "" } - const client = new DjsClient({ djsConfig: config }); +${opts.hasUserConfigEnabled ? " const client = new DjsClient({ djsConfig: config, userConfig: userConfigData as UserConfig });" : " const client = new DjsClient({ djsConfig: config });"} client.eventsHandler.set(events); @@ -305,9 +307,10 @@ export function registerBuildCommand(cli: CAC) { const configModule = await import(path.join(botRoot, "djs.config.ts")); const config = configModule.default as { - experimental?: { cron?: boolean }; + experimental?: { cron?: boolean; userConfig?: boolean }; }; const hasCronEnabled = config.experimental?.cron === true; + const hasUserConfigEnabled = config.experimental?.userConfig === true; const code = buildGeneratedEntry({ genDir, @@ -322,6 +325,7 @@ export function registerBuildCommand(cli: CAC) { eventFiles, cronFiles, hasCronEnabled, + hasUserConfigEnabled, }); await fs.writeFile(entryPath, code, "utf8"); diff --git a/packages/dev/commands/generate-config-types.ts b/packages/dev/commands/generate-config-types.ts new file mode 100644 index 0000000..31036f4 --- /dev/null +++ b/packages/dev/commands/generate-config-types.ts @@ -0,0 +1,126 @@ +import type { CAC } from "cac"; +import fs from "fs/promises"; +import path from "path"; +import pc from "picocolors"; +import { banner } from "../utils/common"; + +async function generateTypesFromJson( + configJsonPath: string, + outputPath: string, +): Promise { + const jsonContent = await fs.readFile(configJsonPath, "utf-8"); + const config = JSON.parse(jsonContent); + + const typeDefinition = generateTypeDefinition(config, "UserConfig"); + + const fileContent = `// Auto-generated from config.json. Do not edit manually. + +${typeDefinition} + +export type { UserConfig }; +`; + + await fs.writeFile(outputPath, fileContent, "utf-8"); +} + +function generateTypeDefinition( + obj: unknown, + typeName: string, + indent = 0, +): string { + if (obj === null) return "null"; + if (obj === undefined) return "undefined"; + + const indentStr = " ".repeat(indent); + const nextIndentStr = " ".repeat(indent + 1); + + if (Array.isArray(obj)) { + if (obj.length === 0) return "unknown[]"; + const firstElement = obj[0]; + const elementType = inferType(firstElement); + return `${elementType}[]`; + } + + if (typeof obj === "object") { + const entries = Object.entries(obj); + if (entries.length === 0) return "Record"; + + const properties = entries + .map(([key, value]) => { + const valueType = inferType(value); + return `${nextIndentStr}${key}: ${valueType};`; + }) + .join("\n"); + + return `${indent === 0 ? "" : "\n"}${indentStr}interface ${typeName} {\n${properties}\n${indentStr}}`; + } + + return inferType(obj); +} + +function inferType(value: unknown): string { + if (value === null) return "null"; + if (value === undefined) return "undefined"; + if (typeof value === "string") return "string"; + if (typeof value === "number") return "number"; + if (typeof value === "boolean") return "boolean"; + + if (Array.isArray(value)) { + if (value.length === 0) return "unknown[]"; + const firstElement = value[0]; + const elementType = inferType(firstElement); + const allSameType = value.every((v) => inferType(v) === elementType); + return allSameType ? `${elementType}[]` : "unknown[]"; + } + + if (typeof value === "object") { + const entries = Object.entries(value); + if (entries.length === 0) return "Record"; + + const properties = entries + .map(([key, val]) => { + const valueType = inferType(val); + return `${key}: ${valueType}`; + }) + .join("; "); + + return `{ ${properties} }`; + } + + return "unknown"; +} + +export function registerGenerateConfigTypesCommand(cli: CAC) { + cli + .command("generate-config-types", "Generate TypeScript types from config.json") + .option("-p, --path ", "Custom project path", { default: "." }) + .action(async (options: { path: string }) => { + console.log(banner); + console.log(`${pc.cyan("ℹ")} Generating config types...`); + + const projectRoot = path.resolve(process.cwd(), options.path); + const configJsonPath = path.join(projectRoot, "config.json"); + const outputPath = path.join(projectRoot, "config.types.ts"); + + try { + await fs.access(configJsonPath); + } catch { + console.error( + pc.red( + `❌ config.json not found at ${configJsonPath}\n Create a config.json file first.`, + ), + ); + process.exit(1); + } + + try { + await generateTypesFromJson(configJsonPath, outputPath); + console.log( + pc.green(`✓ Types generated successfully at ${outputPath}`), + ); + } catch (error: unknown) { + console.error(pc.red("❌ Error generating types:"), error); + process.exit(1); + } + }); +} diff --git a/packages/dev/index.ts b/packages/dev/index.ts index bf87ddf..ec66766 100644 --- a/packages/dev/index.ts +++ b/packages/dev/index.ts @@ -3,6 +3,7 @@ import { cac } from "cac"; import type { Config } from "../utils/types/config"; import { registerBuildCommand } from "./commands/build"; import { registerDevCommand } from "./commands/dev"; +import { registerGenerateConfigTypesCommand } from "./commands/generate-config-types"; import { registerStartCommand } from "./commands/start"; export type { Config }; @@ -12,5 +13,6 @@ const cli = cac("djs-core").version("2.0.0").help(); registerStartCommand(cli); registerDevCommand(cli); registerBuildCommand(cli); +registerGenerateConfigTypesCommand(cli); cli.parse(); diff --git a/packages/dev/utils/common.ts b/packages/dev/utils/common.ts index 8b59b4c..912ad08 100644 --- a/packages/dev/utils/common.ts +++ b/packages/dev/utils/common.ts @@ -137,7 +137,23 @@ export async function runBot(projectPath: string) { console.log(`${pc.green("✓")} Loaded ${pc.bold(tasks.size)} cron tasks`); } - const client = new DjsClient({ djsConfig: config }); + let userConfig: unknown = undefined; + if (config.experimental?.userConfig) { + try { + const configJsonPath = path.join(root, "config.json"); + const configJsonContent = await fs.readFile(configJsonPath, "utf-8"); + userConfig = JSON.parse(configJsonContent); + console.log(`${pc.green("✓")} User config loaded`); + } catch (error) { + console.warn( + pc.yellow( + "⚠️ userConfig is enabled but config.json not found or invalid", + ), + ); + } + } + + const client = new DjsClient({ djsConfig: config, userConfig }); client.eventsHandler.set(events); diff --git a/packages/runtime/DjsClient.ts b/packages/runtime/DjsClient.ts index d5b7e40..7af229a 100644 --- a/packages/runtime/DjsClient.ts +++ b/packages/runtime/DjsClient.ts @@ -25,7 +25,7 @@ import ModalHandler from "./handler/ModalHandler"; import SelectMenuHandler from "./handler/SelectMenuHandler"; import { cleanupExpiredTokens } from "./store/DataStore"; -export default class DjsClient extends Client { +export default class DjsClient extends Client { public eventsHandler: EventHandler = new EventHandler(this); public commandsHandler: CommandHandler = new CommandHandler(this); public buttonsHandler: ButtonHandler = new ButtonHandler(this); @@ -36,8 +36,9 @@ export default class DjsClient extends Client { new ApplicationCommandHandler(this); public cronHandler: CronHandler = new CronHandler(this); private readonly djsConfig: Config; + public readonly conf: UserConfig; - constructor({ djsConfig }: { djsConfig: Config }) { + constructor({ djsConfig, userConfig }: { djsConfig: Config; userConfig?: UserConfig }) { super({ intents: [ IntentsBitField.Flags.Guilds, @@ -47,6 +48,7 @@ export default class DjsClient extends Client { ], }); this.djsConfig = djsConfig; + this.conf = userConfig as UserConfig; if (djsConfig.servers && djsConfig.servers.length > 0) { this.commandsHandler.setGuilds(djsConfig.servers); diff --git a/packages/utils/types/config.d.ts b/packages/utils/types/config.d.ts index 50c3511..39ab646 100644 --- a/packages/utils/types/config.d.ts +++ b/packages/utils/types/config.d.ts @@ -8,5 +8,6 @@ export interface Config { }; experimental?: { cron?: boolean; + userConfig?: boolean; }; } From 36e92a9781e0185c0e1c6d3761fc3cfe3c4000f2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 15:15:11 +0000 Subject: [PATCH 3/6] Add user config example and documentation Co-authored-by: Cleboost <61158869+Cleboost@users.noreply.github.com> --- app/config.json | 12 +++ app/config.types.ts | 9 ++ app/djs.config.ts | 1 + app/src/events/ready-with-config.ts.example | 20 ++++ docs/USER_CONFIG.md | 100 ++++++++++++++++++++ 5 files changed, 142 insertions(+) create mode 100644 app/config.json create mode 100644 app/config.types.ts create mode 100644 app/src/events/ready-with-config.ts.example create mode 100644 docs/USER_CONFIG.md diff --git a/app/config.json b/app/config.json new file mode 100644 index 0000000..8104e90 --- /dev/null +++ b/app/config.json @@ -0,0 +1,12 @@ +{ + "database": { + "host": "localhost", + "port": 5432, + "name": "mydb" + }, + "features": { + "premium": true, + "maxUsers": 100 + }, + "apiKeys": ["key1", "key2"] +} diff --git a/app/config.types.ts b/app/config.types.ts new file mode 100644 index 0000000..174dc01 --- /dev/null +++ b/app/config.types.ts @@ -0,0 +1,9 @@ +// Auto-generated from config.json. Do not edit manually. + +interface UserConfig { + database: { host: string; port: number; name: string }; + features: { premium: boolean; maxUsers: number }; + apiKeys: string[]; +} + +export type { UserConfig }; diff --git a/app/djs.config.ts b/app/djs.config.ts index ad0d4f6..46bfc69 100644 --- a/app/djs.config.ts +++ b/app/djs.config.ts @@ -13,5 +13,6 @@ export default { }, experimental: { cron: true, + userConfig: true, }, } satisfies Config; diff --git a/app/src/events/ready-with-config.ts.example b/app/src/events/ready-with-config.ts.example new file mode 100644 index 0000000..d2bdf7f --- /dev/null +++ b/app/src/events/ready-with-config.ts.example @@ -0,0 +1,20 @@ +import { EventListner } from "@djs-core/runtime"; +import type { UserConfig } from "../../config.types"; +import { ActivityType, Events } from "discord.js"; + +export default new EventListner() + .event(Events.ClientReady) + .run((client) => { + client.user?.setActivity({ name: "🥖 bread", type: ActivityType.Custom }); + + // Example: Access user config + if (client.conf) { + console.log("User config loaded:"); + console.log( + ` Database: ${client.conf.database.host}:${client.conf.database.port}`, + ); + console.log(` Features - Premium: ${client.conf.features.premium}`); + console.log(` Features - Max Users: ${client.conf.features.maxUsers}`); + console.log(` API Keys: ${client.conf.apiKeys.length} configured`); + } + }); diff --git a/docs/USER_CONFIG.md b/docs/USER_CONFIG.md new file mode 100644 index 0000000..9d470d5 --- /dev/null +++ b/docs/USER_CONFIG.md @@ -0,0 +1,100 @@ +# User Config Feature + +This feature allows you to define custom configuration in a `config.json` file and access it with full TypeScript type safety through `client.conf`. + +## Setup + +### 1. Enable User Config + +In your `djs.config.ts`, enable the experimental `userConfig` feature: + +```typescript +import type { Config } from "@djs-core/dev"; + +export default { + token: process.env.TOKEN, + servers: ["your-server-id"], + experimental: { + userConfig: true, // Enable user config + }, +} satisfies Config; +``` + +### 2. Create config.json + +Create a `config.json` file in your project root with your custom configuration: + +```json +{ + "database": { + "host": "localhost", + "port": 5432, + "name": "mydb" + }, + "features": { + "premium": true, + "maxUsers": 100 + }, + "apiKeys": ["key1", "key2"] +} +``` + +### 3. Generate TypeScript Types + +Run the CLI command to generate TypeScript types from your `config.json`: + +```bash +bun djs-core generate-config-types +# or with a custom path +bun djs-core generate-config-types -p ./path/to/your/bot +``` + +This will create a `config.types.ts` file with the inferred TypeScript types: + +```typescript +// Auto-generated from config.json. Do not edit manually. + +interface UserConfig { + database: { host: string; port: number; name: string }; + features: { premium: boolean; maxUsers: number }; + apiKeys: string[]; +} + +export type { UserConfig }; +``` + +### 4. Use the Config in Your Code + +Access your config through `client.conf` with full type safety: + +```typescript +import { EventListner } from "@djs-core/runtime"; +import type { UserConfig } from "../config.types"; +import { Events } from "discord.js"; + +export default new EventListner() + .event(Events.ClientReady) + .run((client) => { + // Access your config with full TypeScript autocomplete and type checking + console.log(`Database: ${client.conf.database.host}:${client.conf.database.port}`); + console.log(`Premium enabled: ${client.conf.features.premium}`); + console.log(`Max users: ${client.conf.features.maxUsers}`); + console.log(`API Keys: ${client.conf.apiKeys.length}`); + }); +``` + +## Notes + +- The `config.json` file is loaded at runtime +- The `config.types.ts` file is auto-generated and should not be edited manually +- Re-run `generate-config-types` whenever you update your `config.json` +- The `client.conf` property is typed as `UserConfig` when using the generic type parameter +- If `userConfig` is not enabled, `client.conf` will be `undefined` + +## Example Use Cases + +- Database connection settings +- Feature flags +- API keys and credentials (ensure proper security measures) +- Custom bot configuration per environment +- Application-specific settings From 9fe80303404c842a39e45838aa479fc15966c98d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 15:16:52 +0000 Subject: [PATCH 4/6] Fix code review issues - make conf optional and add documentation Co-authored-by: Cleboost <61158869+Cleboost@users.noreply.github.com> --- docs/USER_CONFIG.md | 1 + packages/dev/commands/build.ts | 4 ++++ packages/runtime/DjsClient.ts | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/USER_CONFIG.md b/docs/USER_CONFIG.md index 9d470d5..3c22c6a 100644 --- a/docs/USER_CONFIG.md +++ b/docs/USER_CONFIG.md @@ -90,6 +90,7 @@ export default new EventListner() - Re-run `generate-config-types` whenever you update your `config.json` - The `client.conf` property is typed as `UserConfig` when using the generic type parameter - If `userConfig` is not enabled, `client.conf` will be `undefined` +- Empty arrays in config.json are typed as `unknown[]` - add at least one element for better type inference ## Example Use Cases diff --git a/packages/dev/commands/build.ts b/packages/dev/commands/build.ts index 2cd7a3d..2495386 100644 --- a/packages/dev/commands/build.ts +++ b/packages/dev/commands/build.ts @@ -224,6 +224,10 @@ ${sortedCrons.map((c) => ` [${JSON.stringify(c.id)}, ${c.varName}],`).join("\ : "" } + // Load user config if enabled. The type assertion is safe because: + // 1. The config.json is parsed and validated at build time + // 2. The UserConfig type is auto-generated from config.json structure + // 3. Any runtime mismatch will be caught during bot initialization ${opts.hasUserConfigEnabled ? " const client = new DjsClient({ djsConfig: config, userConfig: userConfigData as UserConfig });" : " const client = new DjsClient({ djsConfig: config });"} client.eventsHandler.set(events); diff --git a/packages/runtime/DjsClient.ts b/packages/runtime/DjsClient.ts index 7af229a..b779404 100644 --- a/packages/runtime/DjsClient.ts +++ b/packages/runtime/DjsClient.ts @@ -36,7 +36,7 @@ export default class DjsClient extends Client { new ApplicationCommandHandler(this); public cronHandler: CronHandler = new CronHandler(this); private readonly djsConfig: Config; - public readonly conf: UserConfig; + public readonly conf?: UserConfig; constructor({ djsConfig, userConfig }: { djsConfig: Config; userConfig?: UserConfig }) { super({ From 73f72a1012d4200cfb5c106b9e1e754cd7c1e6f7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 17:28:25 +0000 Subject: [PATCH 5/6] Auto-generate config types on dev/build/start commands Co-authored-by: Cleboost <61158869+Cleboost@users.noreply.github.com> --- docs/USER_CONFIG.md | 27 +++- packages/dev/commands/build.ts | 6 + packages/dev/commands/dev.ts | 27 ++++ .../dev/commands/generate-config-types.ts | 87 +----------- packages/dev/utils/common.ts | 6 + packages/dev/utils/config-type-generator.ts | 128 ++++++++++++++++++ 6 files changed, 188 insertions(+), 93 deletions(-) create mode 100644 packages/dev/utils/config-type-generator.ts diff --git a/docs/USER_CONFIG.md b/docs/USER_CONFIG.md index 3c22c6a..703ea9a 100644 --- a/docs/USER_CONFIG.md +++ b/docs/USER_CONFIG.md @@ -39,17 +39,17 @@ Create a `config.json` file in your project root with your custom configuration: } ``` -### 3. Generate TypeScript Types +### 3. Types Auto-Generated -Run the CLI command to generate TypeScript types from your `config.json`: +When you run `dev`, `build`, or `start`, the TypeScript types are **automatically generated** from your `config.json`: ```bash -bun djs-core generate-config-types -# or with a custom path -bun djs-core generate-config-types -p ./path/to/your/bot +bun djs-core dev # Types auto-generated +bun djs-core build # Types auto-generated +bun djs-core start # Types auto-generated ``` -This will create a `config.types.ts` file with the inferred TypeScript types: +A `config.types.ts` file will be created automatically: ```typescript // Auto-generated from config.json. Do not edit manually. @@ -63,6 +63,8 @@ interface UserConfig { export type { UserConfig }; ``` +**In dev mode**, types are regenerated automatically when you modify `config.json`. + ### 4. Use the Config in Your Code Access your config through `client.conf` with full type safety: @@ -83,11 +85,22 @@ export default new EventListner() }); ``` +## Manual Type Generation (Optional) + +If you need to generate types manually (e.g., for IDE refresh), you can use: + +```bash +bun djs-core generate-config-types +``` + +This is optional since types are auto-generated when running dev/build/start. + ## Notes - The `config.json` file is loaded at runtime - The `config.types.ts` file is auto-generated and should not be edited manually -- Re-run `generate-config-types` whenever you update your `config.json` +- Types are **automatically regenerated** when you run `dev`, `build`, or `start` +- In **dev mode**, types regenerate when you modify `config.json` - The `client.conf` property is typed as `UserConfig` when using the generic type parameter - If `userConfig` is not enabled, `client.conf` will be `undefined` - Empty arrays in config.json are typed as `unknown[]` - add at least one element for better type inference diff --git a/packages/dev/commands/build.ts b/packages/dev/commands/build.ts index 2495386..bbc9f53 100644 --- a/packages/dev/commands/build.ts +++ b/packages/dev/commands/build.ts @@ -4,6 +4,7 @@ import fs from "fs/promises"; import path from "path"; import pc from "picocolors"; import { banner, PATH_ALIASES } from "../utils/common"; +import { autoGenerateConfigTypes } from "../utils/config-type-generator"; declare const Bun: typeof import("bun"); @@ -316,6 +317,11 @@ export function registerBuildCommand(cli: CAC) { const hasCronEnabled = config.experimental?.cron === true; const hasUserConfigEnabled = config.experimental?.userConfig === true; + // Auto-generate config types if userConfig is enabled + if (hasUserConfigEnabled) { + await autoGenerateConfigTypes(botRoot, true); + } + const code = buildGeneratedEntry({ genDir, commandsDir, diff --git a/packages/dev/commands/dev.ts b/packages/dev/commands/dev.ts index 557b23c..21da5be 100644 --- a/packages/dev/commands/dev.ts +++ b/packages/dev/commands/dev.ts @@ -17,6 +17,7 @@ import chokidar from "chokidar"; import path from "path"; import pc from "picocolors"; import { banner, PATH_ALIASES, runBot } from "../utils/common"; +import { autoGenerateConfigTypes } from "../utils/config-type-generator"; type SelectMenu = | StringSelectMenu @@ -392,6 +393,32 @@ export function registerDevCommand(cli: CAC) { .on("change", (p) => processFile("change", p)) .on("unlink", (p) => processFile("unlink", p)); + // Watch config.json for changes if userConfig is enabled + if (config.experimental?.userConfig) { + const configJsonPath = path.join(root, "config.json"); + const configWatcher = chokidar.watch(configJsonPath, { + ignoreInitial: true, + }); + + configWatcher.on("change", async () => { + console.log( + `${pc.cyan("ℹ")} config.json changed, regenerating types...`, + ); + await autoGenerateConfigTypes(root); + }); + + configWatcher.on("add", async () => { + console.log( + `${pc.green("✓")} config.json created, generating types...`, + ); + await autoGenerateConfigTypes(root); + }); + + process.on("SIGINT", async () => { + await configWatcher.close(); + }); + } + process.on("SIGINT", async () => { console.log(pc.dim("\nShutting down...")); await watcher.close(); diff --git a/packages/dev/commands/generate-config-types.ts b/packages/dev/commands/generate-config-types.ts index 31036f4..8457ddf 100644 --- a/packages/dev/commands/generate-config-types.ts +++ b/packages/dev/commands/generate-config-types.ts @@ -3,92 +3,7 @@ import fs from "fs/promises"; import path from "path"; import pc from "picocolors"; import { banner } from "../utils/common"; - -async function generateTypesFromJson( - configJsonPath: string, - outputPath: string, -): Promise { - const jsonContent = await fs.readFile(configJsonPath, "utf-8"); - const config = JSON.parse(jsonContent); - - const typeDefinition = generateTypeDefinition(config, "UserConfig"); - - const fileContent = `// Auto-generated from config.json. Do not edit manually. - -${typeDefinition} - -export type { UserConfig }; -`; - - await fs.writeFile(outputPath, fileContent, "utf-8"); -} - -function generateTypeDefinition( - obj: unknown, - typeName: string, - indent = 0, -): string { - if (obj === null) return "null"; - if (obj === undefined) return "undefined"; - - const indentStr = " ".repeat(indent); - const nextIndentStr = " ".repeat(indent + 1); - - if (Array.isArray(obj)) { - if (obj.length === 0) return "unknown[]"; - const firstElement = obj[0]; - const elementType = inferType(firstElement); - return `${elementType}[]`; - } - - if (typeof obj === "object") { - const entries = Object.entries(obj); - if (entries.length === 0) return "Record"; - - const properties = entries - .map(([key, value]) => { - const valueType = inferType(value); - return `${nextIndentStr}${key}: ${valueType};`; - }) - .join("\n"); - - return `${indent === 0 ? "" : "\n"}${indentStr}interface ${typeName} {\n${properties}\n${indentStr}}`; - } - - return inferType(obj); -} - -function inferType(value: unknown): string { - if (value === null) return "null"; - if (value === undefined) return "undefined"; - if (typeof value === "string") return "string"; - if (typeof value === "number") return "number"; - if (typeof value === "boolean") return "boolean"; - - if (Array.isArray(value)) { - if (value.length === 0) return "unknown[]"; - const firstElement = value[0]; - const elementType = inferType(firstElement); - const allSameType = value.every((v) => inferType(v) === elementType); - return allSameType ? `${elementType}[]` : "unknown[]"; - } - - if (typeof value === "object") { - const entries = Object.entries(value); - if (entries.length === 0) return "Record"; - - const properties = entries - .map(([key, val]) => { - const valueType = inferType(val); - return `${key}: ${valueType}`; - }) - .join("; "); - - return `{ ${properties} }`; - } - - return "unknown"; -} +import { generateTypesFromJson } from "../utils/config-type-generator"; export function registerGenerateConfigTypesCommand(cli: CAC) { cli diff --git a/packages/dev/utils/common.ts b/packages/dev/utils/common.ts index 912ad08..66d7eef 100644 --- a/packages/dev/utils/common.ts +++ b/packages/dev/utils/common.ts @@ -26,6 +26,7 @@ import fs from "fs/promises"; import path, { resolve } from "path"; import pc from "picocolors"; import type { Config } from "../../utils/types/config"; +import { autoGenerateConfigTypes } from "./config-type-generator"; export const banner = ` ${pc.bold(pc.blue("djs-core"))} ${pc.dim(`v1.0.0`)} @@ -54,6 +55,11 @@ export async function runBot(projectPath: string) { console.log(`${pc.green("✓")} Config loaded`); + // Auto-generate config types if userConfig is enabled + if (config.experimental?.userConfig) { + await autoGenerateConfigTypes(root); + } + const commands: Route[] = []; const buttons: Button[] = []; const contextMenus: ContextMenu[] = []; diff --git a/packages/dev/utils/config-type-generator.ts b/packages/dev/utils/config-type-generator.ts new file mode 100644 index 0000000..621e945 --- /dev/null +++ b/packages/dev/utils/config-type-generator.ts @@ -0,0 +1,128 @@ +import fs from "fs/promises"; +import path from "path"; +import pc from "picocolors"; + +function inferType(value: unknown): string { + if (value === null) return "null"; + if (value === undefined) return "undefined"; + if (typeof value === "string") return "string"; + if (typeof value === "number") return "number"; + if (typeof value === "boolean") return "boolean"; + + if (Array.isArray(value)) { + if (value.length === 0) return "unknown[]"; + const firstElement = value[0]; + const elementType = inferType(firstElement); + const allSameType = value.every((v) => inferType(v) === elementType); + return allSameType ? `${elementType}[]` : "unknown[]"; + } + + if (typeof value === "object") { + const entries = Object.entries(value); + if (entries.length === 0) return "Record"; + + const properties = entries + .map(([key, val]) => { + const valueType = inferType(val); + return `${key}: ${valueType}`; + }) + .join("; "); + + return `{ ${properties} }`; + } + + return "unknown"; +} + +function generateTypeDefinition( + obj: unknown, + typeName: string, + indent = 0, +): string { + if (obj === null) return "null"; + if (obj === undefined) return "undefined"; + + const indentStr = " ".repeat(indent); + const nextIndentStr = " ".repeat(indent + 1); + + if (Array.isArray(obj)) { + if (obj.length === 0) return "unknown[]"; + const firstElement = obj[0]; + const elementType = inferType(firstElement); + return `${elementType}[]`; + } + + if (typeof obj === "object") { + const entries = Object.entries(obj); + if (entries.length === 0) return "Record"; + + const properties = entries + .map(([key, value]) => { + const valueType = inferType(value); + return `${nextIndentStr}${key}: ${valueType};`; + }) + .join("\n"); + + return `${indent === 0 ? "" : "\n"}${indentStr}interface ${typeName} {\n${properties}\n${indentStr}}`; + } + + return inferType(obj); +} + +export async function generateTypesFromJson( + configJsonPath: string, + outputPath: string, +): Promise { + const jsonContent = await fs.readFile(configJsonPath, "utf-8"); + const config = JSON.parse(jsonContent); + + const typeDefinition = generateTypeDefinition(config, "UserConfig"); + + const fileContent = `// Auto-generated from config.json. Do not edit manually. + +${typeDefinition} + +export type { UserConfig }; +`; + + await fs.writeFile(outputPath, fileContent, "utf-8"); +} + +/** + * Auto-generate config types if userConfig is enabled and config.json exists + * This is called automatically by dev/build/start commands + */ +export async function autoGenerateConfigTypes( + projectRoot: string, + silent = false, +): Promise { + const configJsonPath = path.join(projectRoot, "config.json"); + const outputPath = path.join(projectRoot, "config.types.ts"); + + try { + await fs.access(configJsonPath); + } catch { + // config.json doesn't exist, skip generation + if (!silent) { + console.log( + pc.yellow( + "⚠️ userConfig enabled but config.json not found. Skipping type generation.", + ), + ); + } + return false; + } + + try { + await generateTypesFromJson(configJsonPath, outputPath); + if (!silent) { + console.log(pc.green("✓ Config types auto-generated")); + } + return true; + } catch (error: unknown) { + if (!silent) { + console.warn(pc.yellow("⚠️ Error generating config types:"), error); + } + return false; + } +} From 240367f837b5b03ebe1d2ccc4ba0eee4ddf68f7e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 17:30:02 +0000 Subject: [PATCH 6/6] Fix SIGINT handler and improve error messages Co-authored-by: Cleboost <61158869+Cleboost@users.noreply.github.com> --- packages/dev/commands/dev.ts | 10 +++++----- packages/dev/utils/config-type-generator.ts | 12 +++++++++++- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/dev/commands/dev.ts b/packages/dev/commands/dev.ts index 21da5be..cb6395d 100644 --- a/packages/dev/commands/dev.ts +++ b/packages/dev/commands/dev.ts @@ -394,9 +394,10 @@ export function registerDevCommand(cli: CAC) { .on("unlink", (p) => processFile("unlink", p)); // Watch config.json for changes if userConfig is enabled + let configWatcher: chokidar.FSWatcher | null = null; if (config.experimental?.userConfig) { const configJsonPath = path.join(root, "config.json"); - const configWatcher = chokidar.watch(configJsonPath, { + configWatcher = chokidar.watch(configJsonPath, { ignoreInitial: true, }); @@ -413,15 +414,14 @@ export function registerDevCommand(cli: CAC) { ); await autoGenerateConfigTypes(root); }); - - process.on("SIGINT", async () => { - await configWatcher.close(); - }); } process.on("SIGINT", async () => { console.log(pc.dim("\nShutting down...")); await watcher.close(); + if (configWatcher) { + await configWatcher.close(); + } await client.destroy(); process.exit(0); }); diff --git a/packages/dev/utils/config-type-generator.ts b/packages/dev/utils/config-type-generator.ts index 621e945..90bb654 100644 --- a/packages/dev/utils/config-type-generator.ts +++ b/packages/dev/utils/config-type-generator.ts @@ -121,7 +121,17 @@ export async function autoGenerateConfigTypes( return true; } catch (error: unknown) { if (!silent) { - console.warn(pc.yellow("⚠️ Error generating config types:"), error); + console.warn( + pc.yellow( + `⚠️ Error generating config types from ${configJsonPath}`, + ), + ); + console.warn( + pc.dim(" Possible causes: invalid JSON syntax, file permissions"), + ); + if (error instanceof Error) { + console.warn(pc.dim(` ${error.message}`)); + } } return false; }