@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.
- package/README.md +56 -18
- package/dist/index.js +241 -41
- package/package.json +5 -2
- package/template/{.env.hbs → base/.env.hbs} +2 -0
- package/template/base/README.md.hbs +33 -0
- package/template/base/src/commands/ping.js.hbs +18 -0
- package/template/{src → base/src}/commands/ping.ts.hbs +3 -1
- package/template/base/src/lib/setup/all.js.hbs +8 -0
- package/template/base/src/lib/setup/all.ts.hbs +8 -0
- package/template/base/src/lib/setup/logger.js.hbs +4 -0
- package/template/base/src/lib/setup/logger.ts.hbs +10 -0
- package/template/base/src/lib/types/augments.ts.hbs +13 -0
- package/template/base/src/main.js.hbs +31 -0
- package/template/base/src/main.ts.hbs +31 -0
- package/template/features/i18n/src/commands/ping.js.hbs +18 -0
- package/template/features/i18n/src/commands/ping.ts.hbs +15 -0
- package/template/features/i18n/src/locales/en-US/commands/ping.json +5 -0
- package/template/features/i18n/src/main.js.hbs +37 -0
- package/template/features/i18n/src/main.ts.hbs +37 -0
- package/template/features/subcommands/src/commands/math.js.hbs +41 -0
- package/template/features/subcommands/src/commands/math.ts.hbs +38 -0
- package/template/features/subcommands-advanced/src/commands/settings.js.hbs +53 -0
- package/template/features/subcommands-advanced/src/commands/settings.ts.hbs +70 -0
- package/template/features/subcommands-advanced-i18n/src/commands/settings.js.hbs +78 -0
- package/template/features/subcommands-advanced-i18n/src/commands/settings.ts.hbs +62 -0
- package/template/features/subcommands-advanced-i18n/src/locales/en-US/commands/settings.json +22 -0
- package/template/features/subcommands-i18n/src/commands/math.js.hbs +46 -0
- package/template/features/subcommands-i18n/src/commands/math.ts.hbs +43 -0
- package/template/features/subcommands-i18n/src/locales/en-US/commands/math.json +14 -0
- package/template/features/testing/tests/ping.test.js.hbs +33 -0
- package/template/features/testing/tests/ping.test.ts.hbs +33 -0
- package/template/features/testing/vitest.config.js.hbs +9 -0
- package/template/features/testing/vitest.config.ts.hbs +9 -0
- package/template/features/testing/vitest.setup.js.hbs +4 -0
- package/template/features/testing/vitest.setup.ts.hbs +4 -0
- package/template/features/testing-i18n/tests/ping.test.js.hbs +42 -0
- package/template/features/testing-i18n/tests/ping.test.ts.hbs +42 -0
- package/template/src/commands/ping.js.hbs +0 -12
- package/template/src/index.js.hbs +0 -8
- package/template/src/index.ts.hbs +0 -8
- /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
|
-
- **
|
|
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
|
|
31
|
-
|
|
|
32
|
-
| `--overwrite`
|
|
33
|
-
| `--no-interactive`
|
|
34
|
-
| `--interactive`
|
|
35
|
-
| `--package-manager <pm>`
|
|
36
|
-
| `--language <lang>`
|
|
37
|
-
| `--build <tool>`
|
|
38
|
-
| `--lint <linter>`
|
|
39
|
-
| `--format <formatter>`
|
|
40
|
-
| `--port <number>`
|
|
41
|
-
| `--i18n` / `--no-i18n`
|
|
42
|
-
| `--
|
|
43
|
-
| `--
|
|
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
|
-
│ │
|
|
60
|
-
│ └──
|
|
61
|
-
├──
|
|
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
|
-
"@
|
|
84
|
+
"@wolfstar/env-utilities",
|
|
85
|
+
"@wolfstar/logger",
|
|
86
|
+
"@wolfstar/start-banner",
|
|
87
|
+
"gradient-string"
|
|
84
88
|
]);
|
|
85
|
-
if (selections.i18n) names.add("@wolfstar/
|
|
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/
|
|
190
|
+
const start = "node dist/main.js";
|
|
186
191
|
let scripts;
|
|
187
192
|
if (ctx.language === "js") scripts = {
|
|
188
|
-
start: "node src/
|
|
189
|
-
dev: "node --watch src/
|
|
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
|
-
"
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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 =
|
|
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/
|
|
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: [
|
|
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
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
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(
|
|
873
|
+
if (isCancel(featuresResult)) {
|
|
708
874
|
cancel("Operation cancelled.");
|
|
709
875
|
process.exit(0);
|
|
710
876
|
}
|
|
711
|
-
|
|
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
|
-
|
|
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.
|
|
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"
|
|
@@ -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
|
+
}
|