@wolfstar/create-http-framework 2.1.4-next-20260827143053 → 2.2.0-next-20260831114920

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.
Files changed (41) hide show
  1. package/README.md +56 -18
  2. package/dist/index.js +241 -41
  3. package/package.json +5 -2
  4. package/template/{.env.hbs → base/.env.hbs} +2 -0
  5. package/template/base/README.md.hbs +33 -0
  6. package/template/base/src/commands/ping.js.hbs +18 -0
  7. package/template/{src → base/src}/commands/ping.ts.hbs +3 -1
  8. package/template/base/src/lib/setup/all.js.hbs +8 -0
  9. package/template/base/src/lib/setup/all.ts.hbs +8 -0
  10. package/template/base/src/lib/setup/logger.js.hbs +4 -0
  11. package/template/base/src/lib/setup/logger.ts.hbs +10 -0
  12. package/template/base/src/lib/types/augments.ts.hbs +13 -0
  13. package/template/base/src/main.js.hbs +31 -0
  14. package/template/base/src/main.ts.hbs +31 -0
  15. package/template/features/i18n/src/commands/ping.js.hbs +18 -0
  16. package/template/features/i18n/src/commands/ping.ts.hbs +15 -0
  17. package/template/features/i18n/src/locales/en-US/commands/ping.json +5 -0
  18. package/template/features/i18n/src/main.js.hbs +37 -0
  19. package/template/features/i18n/src/main.ts.hbs +37 -0
  20. package/template/features/subcommands/src/commands/math.js.hbs +41 -0
  21. package/template/features/subcommands/src/commands/math.ts.hbs +38 -0
  22. package/template/features/subcommands-advanced/src/commands/settings.js.hbs +53 -0
  23. package/template/features/subcommands-advanced/src/commands/settings.ts.hbs +70 -0
  24. package/template/features/subcommands-advanced-i18n/src/commands/settings.js.hbs +78 -0
  25. package/template/features/subcommands-advanced-i18n/src/commands/settings.ts.hbs +62 -0
  26. package/template/features/subcommands-advanced-i18n/src/locales/en-US/commands/settings.json +22 -0
  27. package/template/features/subcommands-i18n/src/commands/math.js.hbs +46 -0
  28. package/template/features/subcommands-i18n/src/commands/math.ts.hbs +43 -0
  29. package/template/features/subcommands-i18n/src/locales/en-US/commands/math.json +14 -0
  30. package/template/features/testing/tests/ping.test.js.hbs +33 -0
  31. package/template/features/testing/tests/ping.test.ts.hbs +33 -0
  32. package/template/features/testing/vitest.config.js.hbs +9 -0
  33. package/template/features/testing/vitest.config.ts.hbs +9 -0
  34. package/template/features/testing/vitest.setup.js.hbs +4 -0
  35. package/template/features/testing/vitest.setup.ts.hbs +4 -0
  36. package/template/features/testing-i18n/tests/ping.test.js.hbs +42 -0
  37. package/template/features/testing-i18n/tests/ping.test.ts.hbs +42 -0
  38. package/template/src/commands/ping.js.hbs +0 -12
  39. package/template/src/index.js.hbs +0 -8
  40. package/template/src/index.ts.hbs +0 -8
  41. /package/template/{.gitignore.hbs → base/.gitignore.hbs} +0 -0
package/README.md CHANGED
@@ -22,25 +22,30 @@ The CLI will guide you through the following prompts:
22
22
  - **Build tool** — tsdown, TypeScript 6, or the TypeScript 7 release candidate
23
23
  - **Linter and formatter** — Oxlint / ESLint and Oxfmt / Prettier
24
24
  - **Port** — the port the HTTP server will listen on (default: `3000`)
25
- - **i18n support** — optionally add `@wolfstar/http-framework-i18n`
25
+ - **Optional features** (multiselect) —
26
+ - **i18n** — add [`@wolfstar/plugin-i18next`](https://github.com/wolfstar-project/stars-components/tree/main/packages/plugin-i18next)
27
+ - **Subcommands** — add an example command that uses subcommands
28
+ - **Testing** — set up Vitest with [`@wolfstar/http-framework-test-utils`](https://github.com/wolfstar-project/stars-components/tree/main/packages/http-framework-test-utils)
26
29
  - **Auto-install** — install dependencies immediately after scaffolding
27
30
 
28
31
  ## Options
29
32
 
30
- | Flag | Alias | Description |
31
- | ---------------------------- | ----- | ----------------------------------------------------------- |
32
- | `--overwrite` | | Overwrite the target directory if it already exists |
33
- | `--no-interactive` | | Skip all prompts and use defaults / flags |
34
- | `--interactive` | `-i` | Force interactive prompts even when an AI agent is detected |
35
- | `--package-manager <pm>` | | Choose npm, yarn, pnpm, or bun |
36
- | `--language <lang>` | | Choose TypeScript (`ts`) or JavaScript (`js`) |
37
- | `--build <tool>` | | Choose `tsc6`, `tsc7`, or `tsdown` for TypeScript |
38
- | `--lint <linter>` | | Choose `none`, `eslint`, or `oxlint` |
39
- | `--format <formatter>` | | Choose `none`, `prettier`, or `oxfmt` |
40
- | `--port <number>` | | Set the HTTP port (default: `3000`) |
41
- | `--i18n` / `--no-i18n` | | Enable or disable i18n support |
42
- | `--install` / `--no-install` | | Enable or disable dependency installation |
43
- | `--help` | `-h` | Print usage and exit |
33
+ | Flag | Alias | Description |
34
+ | ------------------------------------ | ----- | -------------------------------------------------------------------------- |
35
+ | `--overwrite` | | Overwrite the target directory if it already exists |
36
+ | `--no-interactive` | | Skip all prompts and use defaults / flags |
37
+ | `--interactive` | `-i` | Force interactive prompts even when an AI agent is detected |
38
+ | `--package-manager <pm>` | | Choose npm, yarn, pnpm, or bun |
39
+ | `--language <lang>` | | Choose TypeScript (`ts`) or JavaScript (`js`) |
40
+ | `--build <tool>` | | Choose `tsc6`, `tsc7`, or `tsdown` for TypeScript |
41
+ | `--lint <linter>` | | Choose `none`, `eslint`, or `oxlint` |
42
+ | `--format <formatter>` | | Choose `none`, `prettier`, or `oxfmt` |
43
+ | `--port <number>` | | Set the HTTP port (default: `3000`) |
44
+ | `--i18n` / `--no-i18n` | | Enable or disable `@wolfstar/plugin-i18next` scaffolding |
45
+ | `--subcommands` / `--no-subcommands` | | Enable or disable the example subcommand command |
46
+ | `--testing` / `--no-testing` | | Enable or disable the Vitest + `@wolfstar/http-framework-test-utils` setup |
47
+ | `--install` / `--no-install` | | Enable or disable dependency installation |
48
+ | `--help` | `-h` | Print usage and exit |
44
49
 
45
50
  ## Non-interactive / AI agent mode
46
51
 
@@ -56,14 +61,47 @@ create-http-framework my-discord-bot --no-interactive
56
61
  my-discord-bot/
57
62
  ├── src/
58
63
  │ ├── commands/
59
- │ │ └── ping.ts # Example ping command
60
- │ └── index.ts # Entry point — starts the HTTP server
61
- ├── .env # Environment variables (DISCORD_TOKEN, DISCORD_PUBLIC_KEY)
64
+ │ │ ├── ping.ts # Example ping command
65
+ │ │ └── math.ts # Example command with subcommands (--subcommands)
66
+ │ ├── lib/
67
+ │ │ ├── setup/
68
+ │ │ │ ├── all.ts # Aggregates setup imports
69
+ │ │ │ └── logger.ts # Logger configuration
70
+ │ │ └── types/
71
+ │ │ └── augments.ts # Module augmentations (TypeScript only)
72
+ │ ├── locales/
73
+ │ │ └── en-US/
74
+ │ │ └── commands/
75
+ │ │ └── ping.json # Example locale resource (--i18n)
76
+ │ ├── @types/
77
+ │ │ └── i18next.d.ts # Generated i18next augmentation (--i18n)
78
+ │ └── main.ts # Entry point — starts the HTTP server
79
+ ├── tests/
80
+ │ └── ping.test.ts # Example test (--testing)
81
+ ├── vitest.config.ts # Vitest configuration (--testing)
82
+ ├── vitest.setup.ts # Vitest setup file (--testing)
83
+ ├── README.md # Generated project README
84
+ ├── .env # Environment variables (DISCORD_TOKEN, DISCORD_PUBLIC_KEY)
62
85
  ├── .gitignore
63
86
  ├── package.json
64
87
  └── tsconfig.json
65
88
  ```
66
89
 
90
+ - `src/locales/en-US/commands/ping.json` and `src/@types/i18next.d.ts` are only generated when **i18n** is enabled.
91
+ - `src/@types/i18next.d.ts` is produced by `@wolfstar/i18next-type-generator`; the CLI runs it automatically at the end of scaffolding, and it can be re-run any time locale files change (see [`generate:i18n`](#i18n-type-generation) below).
92
+ - `src/commands/math.ts` is only generated when **Subcommands** is enabled.
93
+ - `tests/ping.test.ts`, `vitest.config.ts`, and `vitest.setup.ts` are only generated when **Testing** is enabled.
94
+
95
+ ### i18n type generation
96
+
97
+ When i18n is enabled, the generated `package.json` includes a `generate:i18n` script that (re)generates `src/@types/i18next.d.ts` from the JSON files under `src/locales/`:
98
+
99
+ ```bash
100
+ pnpm generate:i18n
101
+ ```
102
+
103
+ The CLI runs this script automatically once at the end of scaffolding; re-run it manually whenever you add or edit locale keys so the generated types stay in sync.
104
+
67
105
  ## Requirements
68
106
 
69
107
  - Node.js `>=20`
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { cancel, confirm, intro, isCancel, log, outro, select, spinner, text } from "@clack/prompts";
2
+ import { cancel, confirm, intro, isCancel, log, multiselect, outro, select, spinner, text } from "@clack/prompts";
3
3
  import { determineAgent } from "@vercel/detect-agent";
4
4
  import mri from "mri";
5
5
  import { dirname, join, relative, resolve } from "node:path";
@@ -79,10 +79,15 @@ async function fetchVersion(packageName) {
79
79
  async function fetchDependencyVersions(selections) {
80
80
  const names = /* @__PURE__ */ new Set([
81
81
  "@wolfstar/http-framework",
82
+ "@sapphire/pieces",
82
83
  "discord-api-types",
83
- "@discordjs/builders"
84
+ "@wolfstar/env-utilities",
85
+ "@wolfstar/logger",
86
+ "@wolfstar/start-banner",
87
+ "gradient-string"
84
88
  ]);
85
- if (selections.i18n) names.add("@wolfstar/http-framework-i18n");
89
+ if (selections.i18n) names.add("@wolfstar/plugin-i18next").add("@wolfstar/i18next-type-generator");
90
+ if (selections.testing) names.add("vitest").add("@wolfstar/http-framework-test-utils");
86
91
  if (selections.language === "ts") {
87
92
  names.add("@types/node");
88
93
  switch (selections.buildTool) {
@@ -182,11 +187,11 @@ function json(value) {
182
187
  }
183
188
  function buildScripts(ctx) {
184
189
  const { packageManager: pm, buildTool } = ctx;
185
- const start = "node dist/index.js";
190
+ const start = "node dist/main.js";
186
191
  let scripts;
187
192
  if (ctx.language === "js") scripts = {
188
- start: "node src/index.js",
189
- dev: "node --watch src/index.js"
193
+ start: "node src/main.js",
194
+ dev: "node --watch src/main.js"
190
195
  };
191
196
  else if (buildTool === "tsdown") scripts = {
192
197
  build: "tsdown",
@@ -220,16 +225,22 @@ function buildScripts(ctx) {
220
225
  scripts["format"] = "prettier --write src";
221
226
  scripts["format:check"] = "prettier --check src";
222
227
  }
228
+ if (ctx.i18n) scripts["generate:i18n"] = "i18next-type-generator ./src/locales/en-US/ ./src/@types/i18next.d.ts";
229
+ if (ctx.testing) scripts["test"] = "vitest run";
223
230
  return scripts;
224
231
  }
225
232
  function buildDependencies(ctx) {
226
233
  const v = ctx.versions;
227
234
  const dependencies = {
228
- "@discordjs/builders": caret(v["@discordjs/builders"]),
229
235
  "@wolfstar/http-framework": caret(v["@wolfstar/http-framework"]),
230
- "discord-api-types": caret(v["discord-api-types"])
236
+ "@sapphire/pieces": caret(v["@sapphire/pieces"]),
237
+ "discord-api-types": caret(v["discord-api-types"]),
238
+ "@wolfstar/env-utilities": caret(v["@wolfstar/env-utilities"]),
239
+ "@wolfstar/logger": caret(v["@wolfstar/logger"]),
240
+ "@wolfstar/start-banner": caret(v["@wolfstar/start-banner"]),
241
+ "gradient-string": caret(v["gradient-string"])
231
242
  };
232
- if (ctx.i18n) dependencies["@wolfstar/http-framework-i18n"] = caret(v["@wolfstar/http-framework-i18n"]);
243
+ if (ctx.i18n) dependencies["@wolfstar/plugin-i18next"] = caret(v["@wolfstar/plugin-i18next"]);
233
244
  return sortKeys(dependencies);
234
245
  }
235
246
  function buildDevDependencies(ctx) {
@@ -258,11 +269,16 @@ function buildDevDependencies(ctx) {
258
269
  } else if (ctx.linter === "oxlint") dev["oxlint"] = caret(v["oxlint"]);
259
270
  if (ctx.formatter === "prettier") dev["prettier"] = caret(v["prettier"]);
260
271
  else if (ctx.formatter === "oxfmt") dev["oxfmt"] = caret(v["oxfmt"]);
272
+ if (ctx.i18n) dev["@wolfstar/i18next-type-generator"] = caret(v["@wolfstar/i18next-type-generator"]);
273
+ if (ctx.testing) {
274
+ dev["vitest"] = caret(v["vitest"]);
275
+ dev["@wolfstar/http-framework-test-utils"] = caret(v["@wolfstar/http-framework-test-utils"]);
276
+ }
261
277
  return sortKeys(dev);
262
278
  }
263
279
  function packageJson(ctx) {
264
280
  const devDependencies = buildDevDependencies(ctx);
265
- const main = ctx.language === "js" ? "src/index.js" : "dist/index.js";
281
+ const main = ctx.language === "js" ? "src/main.js" : "dist/main.js";
266
282
  return json({
267
283
  name: ctx.name,
268
284
  version: "1.0.0",
@@ -329,7 +345,7 @@ function writeBuildConfig(targetDir, ctx) {
329
345
  " target: 'node20',",
330
346
  " // Mirror the src/ structure so the framework can load command pieces from dist/commands at runtime.",
331
347
  " unbundle: true,",
332
- " // Emit dist/index.js so the shared `start` script (node dist/index.js) works.",
348
+ " // Emit dist/main.js so the shared `start` script (node dist/main.js) works.",
333
349
  " outExtensions: () => ({ js: '.js' }),",
334
350
  " clean: true,",
335
351
  " sourcemap: true",
@@ -400,6 +416,8 @@ function writeProjectFiles(targetDir, ctx) {
400
416
  //#endregion
401
417
  //#region src/tools/templateProcessor.ts
402
418
  const templateDir = join(fileURLToPath(import.meta.url), "../..", "template");
419
+ const baseDir = join(templateDir, "base");
420
+ const featuresDir = join(templateDir, "features");
403
421
  function walkDir(dir) {
404
422
  const entries = readdirSync(dir);
405
423
  const files = [];
@@ -410,20 +428,123 @@ function walkDir(dir) {
410
428
  }
411
429
  return files;
412
430
  }
413
- function processTemplate(outputDir, context) {
414
- const allFiles = walkDir(templateDir);
415
- for (const absoluteSource of allFiles) {
416
- const relativePath = relative(templateDir, absoluteSource);
417
- if (context.language === "js" && relativePath.endsWith(".ts.hbs")) continue;
418
- if (context.language === "ts" && relativePath.endsWith(".js.hbs")) continue;
431
+ /**
432
+ * Resolves which `template/features/*` directories should be layered on top of `template/base/`,
433
+ * in application order. Later entries overwrite earlier ones (and `base/`) on path collisions.
434
+ */
435
+ function resolveFeatureDirs(ctx) {
436
+ const dirs = [];
437
+ if (ctx.i18n) dirs.push("i18n");
438
+ if (ctx.subcommandsAdvanced) dirs.push(ctx.i18n ? "subcommands-advanced-i18n" : "subcommands-advanced");
439
+ else if (ctx.subcommands) dirs.push(ctx.i18n ? "subcommands-i18n" : "subcommands");
440
+ if (ctx.testing) {
441
+ dirs.push("testing");
442
+ if (ctx.i18n) dirs.push("testing-i18n");
443
+ }
444
+ return dirs;
445
+ }
446
+ /** The output-relative path a source file under a template root resolves to for the given language, or `null` if the file doesn't apply to that language. */
447
+ function toOutputRelative(root, absoluteSource, language) {
448
+ const relativePath = relative(root, absoluteSource);
449
+ if (language === "js" && relativePath.endsWith(".ts.hbs")) return null;
450
+ if (language === "ts" && relativePath.endsWith(".js.hbs")) return null;
451
+ return relativePath.endsWith(".hbs") ? relativePath.slice(0, -4) : relativePath;
452
+ }
453
+ /** The set of output-relative paths that writing `root` for `language` would produce. */
454
+ function collectOutputPaths(root, language) {
455
+ const paths = /* @__PURE__ */ new Set();
456
+ for (const absoluteSource of walkDir(root)) {
457
+ const outputRelative = toOutputRelative(root, absoluteSource, language);
458
+ if (outputRelative !== null) paths.add(outputRelative);
459
+ }
460
+ return paths;
461
+ }
462
+ function renderSource(absoluteSource, context) {
463
+ const rawContent = readFileSync(absoluteSource, "utf-8");
464
+ return absoluteSource.endsWith(".hbs") ? Handlebars.compile(rawContent)(context) : rawContent;
465
+ }
466
+ /**
467
+ * Maps every output-relative path `root` could ever produce — for either language — to the source
468
+ * file(s) that render it. Unlike {@link collectOutputPaths} this ignores `context.language`, so it
469
+ * also surfaces paths left behind by a previous run under the *other* language (e.g. `src/main.ts`
470
+ * after rerunning with `--language js`).
471
+ */
472
+ function collectCandidateSources(root) {
473
+ const candidates = /* @__PURE__ */ new Map();
474
+ for (const absoluteSource of walkDir(root)) {
475
+ const relativePath = relative(root, absoluteSource);
476
+ const outputRelative = relativePath.endsWith(".hbs") ? relativePath.slice(0, -4) : relativePath;
477
+ const sources = candidates.get(outputRelative);
478
+ if (sources) sources.push(absoluteSource);
479
+ else candidates.set(outputRelative, [absoluteSource]);
480
+ }
481
+ return candidates;
482
+ }
483
+ /**
484
+ * When re-running against a non-empty `outputDir` (`--ignore`), files written by a previous run
485
+ * survive even after the feature that produced them is disabled, or the project's language is
486
+ * switched — `processTemplate` only ever (re)writes the paths the *current* selections produce, it
487
+ * never deletes. Left behind, those files can keep importing packages that `writeProjectFiles` just
488
+ * dropped from `package.json` (e.g. `@wolfstar/plugin-i18next`), breaking install/build.
489
+ *
490
+ * Deletes any known generator output path that isn't produced by `base/` or an active feature this
491
+ * run, but only when the file on disk still matches byte-for-byte what the generator would have
492
+ * written for it — i.e. it wasn't hand-edited since. Hand-edited files are left in place (and their
493
+ * paths returned) so the caller can warn instead of silently discarding user work.
494
+ */
495
+ function removeStaleGeneratedFiles(outputDir, context) {
496
+ const keepPaths = collectOutputPaths(baseDir, context.language);
497
+ for (const feature of resolveFeatureDirs(context)) for (const path of collectOutputPaths(join(featuresDir, feature), context.language)) keepPaths.add(path);
498
+ const candidates = collectCandidateSources(baseDir);
499
+ for (const feature of readdirSync(featuresDir)) for (const [path, sources] of collectCandidateSources(join(featuresDir, feature))) {
500
+ const existing = candidates.get(path);
501
+ if (existing) existing.push(...sources);
502
+ else candidates.set(path, sources);
503
+ }
504
+ const preserved = [];
505
+ for (const [path, sources] of candidates) {
506
+ if (keepPaths.has(path)) continue;
507
+ const target = join(outputDir, path);
508
+ if (!existsSync(target)) continue;
509
+ const actual = readFileSync(target, "utf-8");
510
+ if (sources.some((source) => renderSource(source, context) === actual)) rmSync(target);
511
+ else preserved.push(path);
512
+ }
513
+ return preserved;
514
+ }
515
+ /**
516
+ * Walks `root` and writes every file into `outputDir`, preserving the relative path.
517
+ *
518
+ * - `*.ts.hbs` / `*.js.hbs` files are skipped unless they match `context.language`.
519
+ * - Other `*.hbs` files (e.g. `.env.hbs`, `.gitignore.hbs`, `README.md.hbs`) are compiled unconditionally.
520
+ * - Non-`.hbs` files (e.g. static locale JSON under a feature's `src/locales/**`) are copied verbatim —
521
+ * no extension stripped, no Handlebars compilation.
522
+ */
523
+ function processDir(root, outputDir, context) {
524
+ for (const absoluteSource of walkDir(root)) {
525
+ const outputRelative = toOutputRelative(root, absoluteSource, context.language);
526
+ if (outputRelative === null) continue;
419
527
  const rawContent = readFileSync(absoluteSource, "utf-8");
420
- const isHandlebars = relativePath.endsWith(".hbs");
421
- const outputRelative = isHandlebars ? relativePath.slice(0, -4) : relativePath;
528
+ const isHandlebars = absoluteSource.endsWith(".hbs");
422
529
  const outputPath = join(outputDir, outputRelative);
423
530
  const content = isHandlebars ? Handlebars.compile(rawContent)(context) : rawContent;
424
531
  writeFile(outputPath, content);
425
532
  }
426
533
  }
534
+ /**
535
+ * Renders `template/base/` into `outputDir`, then layers the feature directories resolved by
536
+ * {@link resolveFeatureDirs} on top, in order — feature files overwrite base files at the same
537
+ * output-relative path (e.g. `features/i18n/src/main.ts.hbs` overwrites `base/src/main.ts.hbs`).
538
+ *
539
+ * @returns Output-relative paths that looked stale (belong to a disabled feature or the other
540
+ * language) but were left in place because they'd been hand-edited since the last run.
541
+ */
542
+ async function processTemplate(outputDir, context) {
543
+ const preserved = removeStaleGeneratedFiles(outputDir, context);
544
+ processDir(baseDir, outputDir, context);
545
+ for (const feature of resolveFeatureDirs(context)) processDir(join(featuresDir, feature), outputDir, context);
546
+ return preserved;
547
+ }
427
548
 
428
549
  //#endregion
429
550
  //#region src/index.ts
@@ -447,8 +568,12 @@ Options:
447
568
  --lint <linter> none | eslint | oxlint (default: oxlint)
448
569
  --format <formatter> none | prettier | oxfmt (default: oxfmt)
449
570
  --port <number> HTTP port (default: 3000)
450
- --i18n / --no-i18n Toggle @wolfstar/http-framework-i18n (default: off)
571
+ --i18n / --no-i18n Toggle @wolfstar/plugin-i18next (default: off)
572
+ --subcommands / --no-subcommands Toggle the subcommands example command (default: off)
573
+ --subcommands-advanced / --no-subcommands-advanced Toggle the subcommand groups example command (default: off; overrides --subcommands)
574
+ --testing / --no-testing Toggle the testing setup (vitest) (default: off)
451
575
  --install / --no-install Toggle dependency installation (default: on)
576
+ --ignore Write into an existing, non-empty directory without clearing it
452
577
  --help, -h Print this message and exit
453
578
 
454
579
  `);
@@ -464,7 +589,11 @@ function parseEnum(value, allowed, label) {
464
589
  }
465
590
  async function main() {
466
591
  const argv = mri(process.argv.slice(2), {
467
- boolean: ["overwrite", "help"],
592
+ boolean: [
593
+ "overwrite",
594
+ "ignore",
595
+ "help"
596
+ ],
468
597
  string: [
469
598
  "package-manager",
470
599
  "language",
@@ -485,6 +614,7 @@ async function main() {
485
614
  }
486
615
  const argProjectName = argv._[0];
487
616
  const flagOverwrite = argv["overwrite"];
617
+ const flagIgnore = argv["ignore"];
488
618
  const flagInteractive = argv["interactive"];
489
619
  const cliPackageManager = parseEnum(argv["package-manager"], PACKAGE_MANAGERS, "--package-manager");
490
620
  const cliLanguage = parseEnum(argv["language"], LANGUAGES, "--language");
@@ -493,6 +623,9 @@ async function main() {
493
623
  const cliFormat = parseEnum(argv["format"], FORMATTERS, "--format");
494
624
  const cliPort = argv["port"];
495
625
  const cliI18n = argv["i18n"];
626
+ const cliSubcommands = argv["subcommands"];
627
+ const cliSubcommandsAdvanced = argv["subcommands-advanced"];
628
+ const cliTesting = argv["testing"];
496
629
  const cliInstall = argv["install"];
497
630
  const { isAgent, agent } = await determineAgent();
498
631
  const agentMode = isAgent && flagInteractive !== true;
@@ -549,25 +682,32 @@ async function main() {
549
682
  const targetDir = resolve(projectName);
550
683
  if (directoryExists(targetDir) && !isEmpty(targetDir)) {
551
684
  if (flagOverwrite) emptyDir(targetDir);
552
- else if (nonInteractive) {
553
- cancel(`"${projectName}" already exists. Use --overwrite to overwrite it.`);
685
+ else if (flagIgnore) {} else if (nonInteractive) {
686
+ cancel(`"${projectName}" already exists. Use --overwrite to overwrite it, or --ignore to write into it without clearing it first.`);
554
687
  process.exit(1);
555
688
  } else {
556
689
  const choice = await select({
557
690
  message: `"${projectName}" already exists. What would you like to do?`,
558
- options: [{
559
- value: "overwrite",
560
- label: "Overwrite the directory"
561
- }, {
562
- value: "cancel",
563
- label: "Cancel"
564
- }]
691
+ options: [
692
+ {
693
+ value: "overwrite",
694
+ label: "Overwrite the directory (empties it first)"
695
+ },
696
+ {
697
+ value: "ignore",
698
+ label: "Write into it without clearing existing files"
699
+ },
700
+ {
701
+ value: "cancel",
702
+ label: "Cancel"
703
+ }
704
+ ]
565
705
  });
566
706
  if (isCancel(choice) || choice === "cancel") {
567
707
  cancel("Operation cancelled.");
568
708
  process.exit(0);
569
709
  }
570
- emptyDir(targetDir);
710
+ if (choice === "overwrite") emptyDir(targetDir);
571
711
  }
572
712
  }
573
713
  let port;
@@ -698,17 +838,51 @@ async function main() {
698
838
  formatter = formatResult;
699
839
  }
700
840
  let wantsI18n;
701
- if (nonInteractive) wantsI18n = cliI18n ?? false;
702
- else {
703
- const i18nResult = await confirm({
704
- message: "Would you like to add i18n support? (@wolfstar/http-framework-i18n)",
705
- initialValue: false
841
+ let wantsSubcommands;
842
+ let wantsSubcommandsAdvanced;
843
+ let wantsTesting;
844
+ if (nonInteractive) {
845
+ wantsI18n = cliI18n ?? false;
846
+ wantsSubcommands = cliSubcommands ?? false;
847
+ wantsSubcommandsAdvanced = cliSubcommandsAdvanced ?? false;
848
+ wantsTesting = cliTesting ?? false;
849
+ } else {
850
+ const featuresResult = await multiselect({
851
+ message: "Which optional features would you like to add?",
852
+ options: [
853
+ {
854
+ value: "i18n",
855
+ label: "i18n support (@wolfstar/plugin-i18next)"
856
+ },
857
+ {
858
+ value: "subcommands",
859
+ label: "Subcommands example command"
860
+ },
861
+ {
862
+ value: "subcommands-advanced",
863
+ label: "Subcommands example command (with groups)"
864
+ },
865
+ {
866
+ value: "testing",
867
+ label: "Testing setup (vitest)"
868
+ }
869
+ ],
870
+ initialValues: [],
871
+ required: false
706
872
  });
707
- if (isCancel(i18nResult)) {
873
+ if (isCancel(featuresResult)) {
708
874
  cancel("Operation cancelled.");
709
875
  process.exit(0);
710
876
  }
711
- wantsI18n = Boolean(i18nResult);
877
+ const features = new Set(featuresResult);
878
+ wantsI18n = features.has("i18n");
879
+ wantsSubcommands = features.has("subcommands");
880
+ wantsSubcommandsAdvanced = features.has("subcommands-advanced");
881
+ wantsTesting = features.has("testing");
882
+ }
883
+ if (wantsSubcommands && wantsSubcommandsAdvanced) {
884
+ log.warn("Both --subcommands and --subcommands-advanced were selected; using the advanced example (with groups).");
885
+ wantsSubcommands = false;
712
886
  }
713
887
  let wantsInstall;
714
888
  if (nonInteractive) wantsInstall = cliInstall;
@@ -727,6 +901,9 @@ async function main() {
727
901
  s.start("Fetching latest dependency versions...");
728
902
  const versions = await fetchDependencyVersions({
729
903
  i18n: wantsI18n,
904
+ subcommands: wantsSubcommands,
905
+ subcommandsAdvanced: wantsSubcommandsAdvanced,
906
+ testing: wantsTesting,
730
907
  language,
731
908
  buildTool,
732
909
  linter,
@@ -734,15 +911,22 @@ async function main() {
734
911
  });
735
912
  s.stop("Versions fetched.");
736
913
  s.start("Generating project files...");
737
- processTemplate(targetDir, {
914
+ const preservedFiles = await processTemplate(targetDir, {
738
915
  name: projectName,
739
916
  port,
740
- language
917
+ language,
918
+ i18n: wantsI18n,
919
+ subcommands: wantsSubcommands,
920
+ subcommandsAdvanced: wantsSubcommandsAdvanced,
921
+ testing: wantsTesting
741
922
  });
742
923
  writeProjectFiles(targetDir, {
743
924
  name: projectName,
744
925
  port,
745
926
  i18n: wantsI18n,
927
+ subcommands: wantsSubcommands,
928
+ subcommandsAdvanced: wantsSubcommandsAdvanced,
929
+ testing: wantsTesting,
746
930
  packageManager,
747
931
  language,
748
932
  buildTool,
@@ -751,6 +935,19 @@ async function main() {
751
935
  versions
752
936
  });
753
937
  s.stop("Project files generated.");
938
+ for (const file of preservedFiles) log.warn(`Kept hand-edited "${file}" even though its feature is now disabled — remove it manually if it's no longer needed.`);
939
+ if (wantsI18n) try {
940
+ s.start("Generating i18n types...");
941
+ runCommand("npx", [
942
+ "--yes",
943
+ "@wolfstar/i18next-type-generator",
944
+ "./src/locales/en-US",
945
+ "./src/@types/i18next.d.ts"
946
+ ], targetDir);
947
+ s.stop("i18n types generated.");
948
+ } catch {
949
+ s.stop("Skipped i18n type generation (run it manually once dependencies are installed).");
950
+ }
754
951
  if (formatter !== "none") try {
755
952
  s.start("Formatting project files...");
756
953
  if (formatter === "oxfmt") runCommand("npx", [
@@ -776,7 +973,10 @@ async function main() {
776
973
  await installDependencies(packageManager, targetDir);
777
974
  s.stop("Dependencies installed.");
778
975
  }
779
- outro(`Done! To get started:\n\n cd ${projectName}\n${wantsInstall ? "" : ` ${getInstallScript(packageManager)}\n`} ${getRunScript(packageManager, "dev")}`);
976
+ const extraNotes = [];
977
+ if (wantsI18n) extraNotes.push(` After editing locale files, regenerate i18next types with: ${getRunScript(packageManager, "generate:i18n")}`);
978
+ if (wantsTesting) extraNotes.push(` Run the test suite with: ${getRunScript(packageManager, "test")}`);
979
+ outro(`Done! To get started:\n\n cd ${projectName}\n${wantsInstall ? "" : ` ${getInstallScript(packageManager)}\n`} ${getRunScript(packageManager, "dev")}${extraNotes.length ? `\n\n${extraNotes.join("\n")}` : ""}`);
780
980
  }
781
981
  main().catch((error) => {
782
982
  cancel(error instanceof Error ? error.message : String(error));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wolfstar/create-http-framework",
3
- "version": "2.1.4-next-20260827143053",
3
+ "version": "2.2.0-next-20260831114920",
4
4
  "description": "Create a new WolfStar HTTP Framework bot project",
5
5
  "keywords": [
6
6
  "cli",
@@ -46,13 +46,16 @@
46
46
  "@types/handlebars": "^4.1.0",
47
47
  "@types/mri": "^1.2.0",
48
48
  "@types/node": "22.15.21",
49
+ "@vitest/coverage-v8": "^4.1.8",
49
50
  "tsdown": "^0.22.14",
50
- "typescript": "~5.8.3"
51
+ "typescript": "~5.8.3",
52
+ "vitest": "^4.1.8"
51
53
  },
52
54
  "engines": {
53
55
  "node": ">=20"
54
56
  },
55
57
  "scripts": {
58
+ "test": "vitest run",
56
59
  "build": "tsdown --config-loader unrun",
57
60
  "typecheck": "tsc -p ../../tsconfig.json",
58
61
  "lint": "oxlint src --fix"
@@ -1,4 +1,6 @@
1
1
  DISCORD_TOKEN=your_bot_token_here
2
2
  DISCORD_PUBLIC_KEY=your_public_key_here
3
3
  DISCORD_CLIENT_ID=your_client_id_here
4
+ HTTP_ADDRESS=0.0.0.0
5
+ HTTP_PORT={{port}}
4
6
  HTTP_POST_PATH=/
@@ -0,0 +1,33 @@
1
+ # {{name}}
2
+
3
+ A Discord bot built with [`@wolfstar/http-framework`](https://www.npmjs.com/package/@wolfstar/http-framework), an HTTP-interactions framework for Discord bots.
4
+
5
+ ## Setup
6
+
7
+ 1. Install dependencies (skip this if the generator already installed them for you).
8
+
9
+ 2. This project was generated with a `.env` file already in place — it's written with placeholder values directly as `.env` (there is no separate `.env.example`), and `.gitignore` keeps it out of version control. Open `.env` and replace the placeholders with your bot's real `DISCORD_TOKEN`, `DISCORD_PUBLIC_KEY`, and `DISCORD_CLIENT_ID`.
10
+ {{#if i18n}}
11
+ 3. This project uses [`@wolfstar/plugin-i18next`](https://www.npmjs.com/package/@wolfstar/plugin-i18next). Locale resources live under `src/locales/`; after editing them, regenerate the typed i18next resources with `npm run generate:i18n`.
12
+ {{/if}}
13
+
14
+ ## Scripts
15
+
16
+ - `npm run dev` — run the bot in watch mode.
17
+ - `npm run build` — compile the project (TypeScript projects only).
18
+ - `npm start` — run the bot.
19
+ {{#if testing}}
20
+ - `npm test` — run the test suite with vitest.
21
+ {{/if}}
22
+
23
+ ## Commands
24
+
25
+ - `/ping` — replies with `Pong!`.
26
+ {{#if subcommands}}
27
+ - `/math add` / `/math subtract` — basic arithmetic on two integers.
28
+ {{/if}}
29
+ {{#if subcommandsAdvanced}}
30
+ - `/settings profile get` / `/settings profile set` / `/settings notifications enable` / `/settings notifications disable` — subcommand groups example.
31
+ {{/if}}
32
+
33
+ The HTTP server listens on `HTTP_ADDRESS`:`HTTP_PORT` from `.env` — `{{port}}` by default.
@@ -0,0 +1,18 @@
1
+ import { Command } from '@wolfstar/http-framework';
2
+ import { ApplicationIntegrationType, InteractionContextType, MessageFlags } from 'discord-api-types/v10';
3
+
4
+ export class PingCommand extends Command {
5
+ registerApplicationCommands(registry) {
6
+ registry.registerChatInputCommand((builder) =>
7
+ builder
8
+ .setName('ping')
9
+ .setDescription('Replies with pong!')
10
+ .setIntegrationTypes(ApplicationIntegrationType.GuildInstall, ApplicationIntegrationType.UserInstall)
11
+ .setContexts(InteractionContextType.Guild, InteractionContextType.BotDM, InteractionContextType.PrivateChannel)
12
+ );
13
+ }
14
+
15
+ chatInputRun(interaction) {
16
+ return interaction.reply({ content: 'Pong!', flags: MessageFlags.Ephemeral });
17
+ }
18
+ }
@@ -1,10 +1,12 @@
1
1
  import { Command, RegisterCommand } from '@wolfstar/http-framework';
2
- import { MessageFlags } from 'discord-api-types/v10';
2
+ import { ApplicationIntegrationType, InteractionContextType, MessageFlags } from 'discord-api-types/v10';
3
3
 
4
4
  @RegisterCommand((builder) =>
5
5
  builder //
6
6
  .setName('ping')
7
7
  .setDescription('Replies with pong!')
8
+ .setIntegrationTypes(ApplicationIntegrationType.GuildInstall, ApplicationIntegrationType.UserInstall)
9
+ .setContexts(InteractionContextType.Guild, InteractionContextType.BotDM, InteractionContextType.PrivateChannel)
8
10
  )
9
11
  export class PingCommand extends Command {
10
12
  public override chatInputRun(interaction: Command.ChatInputInteraction) {
@@ -0,0 +1,8 @@
1
+ import { setup as envRun } from '@wolfstar/env-utilities';
2
+ /* oxlint-disable import/first -- side-effect setup modules must run after env load preparation */
3
+ import './logger.js';
4
+
5
+ export function setup() {
6
+ // From src/lib/setup → .env at the project root
7
+ envRun(new URL('../../../.env', import.meta.url));
8
+ }
@@ -0,0 +1,8 @@
1
+ import { setup as envRun } from '@wolfstar/env-utilities';
2
+ /* oxlint-disable import/first -- side-effect setup modules must run after env load preparation */
3
+ import './logger.js';
4
+
5
+ export function setup() {
6
+ // From dist/lib/setup → .env at the project root
7
+ envRun(new URL('../../../.env', import.meta.url));
8
+ }