@wolfstar/create-http-framework 1.0.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -0
- package/dist/index.js +704 -92
- package/package.json +8 -3
- package/template/.env.hbs +4 -1
- package/template/.gitignore.hbs +4 -1
- package/template/src/commands/ping.js.hbs +12 -0
- package/template/src/commands/ping.ts.hbs +13 -5
- package/template/src/index.js.hbs +10 -0
- package/template/src/index.ts.hbs +10 -5
- package/template/package.json.hbs +0 -7
- package/template/tsconfig.json.hbs +0 -3
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# `@wolfstar/create-http-framework`
|
|
2
|
+
|
|
3
|
+
A CLI scaffolding tool for creating new [WolfStar HTTP Framework](https://github.com/wolfstar-project/stars-components/tree/main/packages/http-framework) bot projects.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm create @wolfstar/http-framework@latest my-discord-bot
|
|
9
|
+
# or
|
|
10
|
+
pnpm create @wolfstar/http-framework my-discord-bot
|
|
11
|
+
# or
|
|
12
|
+
yarn create @wolfstar/http-framework my-discord-bot
|
|
13
|
+
# or
|
|
14
|
+
bun create @wolfstar/http-framework my-discord-bot
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The CLI will guide you through the following prompts:
|
|
18
|
+
|
|
19
|
+
- **Project name** — an npm-compatible name for your bot
|
|
20
|
+
- **Package manager** — npm, yarn, pnpm, or bun
|
|
21
|
+
- **Language** — TypeScript or JavaScript
|
|
22
|
+
- **Build tool** — tsdown, TypeScript 6, or the TypeScript 7 release candidate
|
|
23
|
+
- **Linter and formatter** — Oxlint / ESLint and Oxfmt / Prettier
|
|
24
|
+
- **Port** — the port the HTTP server will listen on (default: `3000`)
|
|
25
|
+
- **i18n support** — optionally add `@wolfstar/http-framework-i18n`
|
|
26
|
+
- **Auto-install** — install dependencies immediately after scaffolding
|
|
27
|
+
|
|
28
|
+
## Options
|
|
29
|
+
|
|
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 |
|
|
44
|
+
|
|
45
|
+
## Non-interactive / AI agent mode
|
|
46
|
+
|
|
47
|
+
When the CLI detects an AI agent is running it (via [`@vercel/detect-agent`](https://www.npmjs.com/package/@vercel/detect-agent)), it automatically enters non-interactive mode, equivalent to passing `--no-interactive`. A project name argument is required in this mode:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
create-http-framework my-discord-bot --no-interactive
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Generated project structure
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
my-discord-bot/
|
|
57
|
+
├── src/
|
|
58
|
+
│ ├── 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)
|
|
62
|
+
├── .gitignore
|
|
63
|
+
├── package.json
|
|
64
|
+
└── tsconfig.json
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Requirements
|
|
68
|
+
|
|
69
|
+
- Node.js `>=20`
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { cancel, confirm, intro, isCancel, outro, spinner, text } from "@clack/prompts";
|
|
2
|
+
import { cancel, confirm, intro, isCancel, log, outro, select, spinner, text } from "@clack/prompts";
|
|
3
|
+
import { determineAgent } from "@vercel/detect-agent";
|
|
4
|
+
import mri from "mri";
|
|
3
5
|
import { dirname, join, relative, resolve } from "node:path";
|
|
4
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
5
|
-
import
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
7
|
+
import spawn from "cross-spawn";
|
|
6
8
|
import { resolveCommand } from "package-manager-detector/commands";
|
|
7
9
|
import { detect } from "package-manager-detector/detect";
|
|
8
10
|
import Handlebars from "handlebars";
|
|
@@ -10,64 +12,395 @@ import { fileURLToPath } from "node:url";
|
|
|
10
12
|
|
|
11
13
|
//#region src/tools/fileSystem.ts
|
|
12
14
|
function directoryExists(path) {
|
|
13
|
-
return existsSync(path);
|
|
15
|
+
return existsSync(path) && statSync(path).isDirectory();
|
|
16
|
+
}
|
|
17
|
+
function isEmpty(path) {
|
|
18
|
+
try {
|
|
19
|
+
return readdirSync(path).length === 0;
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (error.code === "ENOENT") return true;
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function emptyDir(dir) {
|
|
26
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) rmSync(join(dir, entry.name), {
|
|
27
|
+
recursive: true,
|
|
28
|
+
force: true
|
|
29
|
+
});
|
|
14
30
|
}
|
|
15
31
|
function writeFile(filePath, content) {
|
|
16
32
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
17
33
|
writeFileSync(filePath, content, "utf-8");
|
|
18
34
|
}
|
|
19
35
|
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/tools/options.ts
|
|
38
|
+
const PACKAGE_MANAGERS = [
|
|
39
|
+
"npm",
|
|
40
|
+
"yarn",
|
|
41
|
+
"pnpm",
|
|
42
|
+
"bun"
|
|
43
|
+
];
|
|
44
|
+
const LANGUAGES = ["ts", "js"];
|
|
45
|
+
const BUILD_TOOLS = [
|
|
46
|
+
"tsc6",
|
|
47
|
+
"tsc7",
|
|
48
|
+
"tsdown"
|
|
49
|
+
];
|
|
50
|
+
const LINTERS = [
|
|
51
|
+
"none",
|
|
52
|
+
"eslint",
|
|
53
|
+
"oxlint"
|
|
54
|
+
];
|
|
55
|
+
const FORMATTERS = [
|
|
56
|
+
"none",
|
|
57
|
+
"prettier",
|
|
58
|
+
"oxfmt"
|
|
59
|
+
];
|
|
60
|
+
/**
|
|
61
|
+
* TypeScript 7.0 (the native compiler) ships under the main `typescript` package on the `rc`
|
|
62
|
+
* dist-tag. We pin it exactly because it is a prerelease (the old separate `@typescript/native-preview`
|
|
63
|
+
* package only has dev builds).
|
|
64
|
+
*/
|
|
65
|
+
const TYPESCRIPT_RC_VERSION = "7.0.1-rc";
|
|
66
|
+
|
|
20
67
|
//#endregion
|
|
21
68
|
//#region src/tools/npmHelpers.ts
|
|
22
|
-
function fetchVersion(packageName) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
69
|
+
async function fetchVersion(packageName) {
|
|
70
|
+
const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
|
|
71
|
+
if (!response.ok) throw new Error(`Failed to resolve latest version for ${packageName}: ${response.status} ${response.statusText}`);
|
|
72
|
+
return (await response.json()).version;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Resolves the latest versions of only the packages required by the chosen selections.
|
|
76
|
+
* `ts-node` is intentionally never included. TypeScript 7.0 (tsc7) is pinned to the rc instead
|
|
77
|
+
* of being fetched, because `typescript@latest` resolves to the 6.x line.
|
|
78
|
+
*/
|
|
79
|
+
async function fetchDependencyVersions(selections) {
|
|
80
|
+
const names = /* @__PURE__ */ new Set([
|
|
81
|
+
"@wolfstar/http-framework",
|
|
82
|
+
"discord-api-types",
|
|
83
|
+
"@discordjs/builders"
|
|
84
|
+
]);
|
|
85
|
+
if (selections.i18n) names.add("@wolfstar/http-framework-i18n");
|
|
86
|
+
if (selections.language === "ts") {
|
|
87
|
+
names.add("@types/node");
|
|
88
|
+
switch (selections.buildTool) {
|
|
89
|
+
case "tsc6":
|
|
90
|
+
names.add("typescript").add("tsc-watch");
|
|
91
|
+
break;
|
|
92
|
+
case "tsc7":
|
|
93
|
+
names.add("tsc-watch");
|
|
94
|
+
break;
|
|
95
|
+
case "tsdown":
|
|
96
|
+
names.add("tsdown").add("typescript");
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (selections.linter === "eslint") {
|
|
101
|
+
names.add("eslint");
|
|
102
|
+
names.add(selections.language === "ts" ? "typescript-eslint" : "@eslint/js");
|
|
103
|
+
}
|
|
104
|
+
if (selections.linter === "oxlint") names.add("oxlint");
|
|
105
|
+
if (selections.formatter === "prettier") names.add("prettier");
|
|
106
|
+
if (selections.formatter === "oxfmt") names.add("oxfmt");
|
|
107
|
+
const list = [...names];
|
|
108
|
+
const resolved = await Promise.all(list.map(fetchVersion));
|
|
109
|
+
const versions = {};
|
|
110
|
+
list.forEach((name, index) => {
|
|
111
|
+
versions[name] = resolved[index];
|
|
112
|
+
});
|
|
113
|
+
if (selections.language === "ts" && selections.buildTool === "tsc7") versions["typescript"] = TYPESCRIPT_RC_VERSION;
|
|
114
|
+
return versions;
|
|
46
115
|
}
|
|
47
116
|
|
|
48
117
|
//#endregion
|
|
49
118
|
//#region src/tools/packageManager.ts
|
|
50
|
-
async function detectPackageManager(
|
|
119
|
+
async function detectPackageManager() {
|
|
51
120
|
const detected = await detect({ cwd: process.cwd() });
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
if (
|
|
55
|
-
const
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
121
|
+
if (detected?.name) return detected.name;
|
|
122
|
+
const userAgent = process.env["npm_config_user_agent"];
|
|
123
|
+
if (userAgent) {
|
|
124
|
+
const name = userAgent.split("/")[0];
|
|
125
|
+
if ([
|
|
126
|
+
"npm",
|
|
127
|
+
"yarn",
|
|
128
|
+
"pnpm",
|
|
129
|
+
"bun",
|
|
130
|
+
"deno"
|
|
131
|
+
].includes(name)) return name;
|
|
61
132
|
}
|
|
62
|
-
return
|
|
133
|
+
return "npm";
|
|
134
|
+
}
|
|
135
|
+
function quoteToken(token) {
|
|
136
|
+
return /\s/.test(token) ? `"${token}"` : token;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Builds the correct "run a script" command string for the given package manager, e.g.
|
|
140
|
+
* `npm run build` / `pnpm run build`. Extra args are forwarded the way each manager expects
|
|
141
|
+
* (npm inserts the `--` separator automatically), and tokens containing spaces are quoted so
|
|
142
|
+
* nested commands (`--onSuccess "pnpm run start"`) survive being placed in a package.json script.
|
|
143
|
+
*/
|
|
144
|
+
function getRunScript(pm, script, args = []) {
|
|
145
|
+
const resolved = resolveCommand(pm, "run", [script, ...args]);
|
|
146
|
+
if (!resolved) throw new Error(`Could not resolve run command for ${pm}`);
|
|
147
|
+
return [resolved.command, ...resolved.args].map(quoteToken).join(" ");
|
|
148
|
+
}
|
|
149
|
+
/** Returns the install command string for the given package manager, e.g. `pnpm install`. */
|
|
150
|
+
function getInstallScript(pm) {
|
|
151
|
+
const resolved = resolveCommand(pm, "install", []);
|
|
152
|
+
if (!resolved) throw new Error(`Could not resolve install command for ${pm}`);
|
|
153
|
+
return [resolved.command, ...resolved.args].map(quoteToken).join(" ");
|
|
154
|
+
}
|
|
155
|
+
async function installDependencies(pm, dir) {
|
|
156
|
+
const agent = (await detect({ cwd: process.cwd() }))?.agent ?? pm;
|
|
157
|
+
const resolved = resolveCommand(agent, "install", []);
|
|
158
|
+
if (!resolved) throw new Error(`Could not resolve install command for ${agent}`);
|
|
159
|
+
const result = spawn.sync(resolved.command, resolved.args, {
|
|
160
|
+
cwd: dir,
|
|
161
|
+
stdio: process.env["DEBUG"] ? "inherit" : "pipe"
|
|
162
|
+
});
|
|
163
|
+
if (result.error) throw result.error;
|
|
164
|
+
if (result.status !== 0) throw new Error(`Dependency installation failed with exit code ${result.status ?? "unknown"}`);
|
|
165
|
+
}
|
|
166
|
+
/** Runs an arbitrary command in `dir` via cross-spawn (used for the post-scaffold formatter pass). */
|
|
167
|
+
function runCommand(command, args, dir) {
|
|
168
|
+
const result = spawn.sync(command, args, {
|
|
169
|
+
cwd: dir,
|
|
170
|
+
stdio: process.env["DEBUG"] ? "inherit" : "pipe"
|
|
171
|
+
});
|
|
172
|
+
if (result.error) throw result.error;
|
|
173
|
+
if (result.status !== 0) throw new Error(`Command "${command}" failed with exit code ${result.status ?? "unknown"}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region src/tools/projectFiles.ts
|
|
178
|
+
const caret = (version) => `^${version}`;
|
|
179
|
+
function sortKeys(record) {
|
|
180
|
+
return Object.fromEntries(Object.entries(record).sort(([a], [b]) => a.localeCompare(b)));
|
|
181
|
+
}
|
|
182
|
+
function json(value) {
|
|
183
|
+
return `${JSON.stringify(value, null, " ")}\n`;
|
|
184
|
+
}
|
|
185
|
+
function buildScripts(ctx) {
|
|
186
|
+
const { packageManager: pm, buildTool } = ctx;
|
|
187
|
+
const start = "node dist/index.js";
|
|
188
|
+
let scripts;
|
|
189
|
+
if (ctx.language === "js") scripts = {
|
|
190
|
+
start: "node src/index.js",
|
|
191
|
+
dev: "node --watch src/index.js"
|
|
192
|
+
};
|
|
193
|
+
else if (buildTool === "tsdown") scripts = {
|
|
194
|
+
build: "tsdown",
|
|
195
|
+
dev: getRunScript(pm, "build", ["--onSuccess", getRunScript(pm, "start")]),
|
|
196
|
+
watch: getRunScript(pm, "build", ["--watch"]),
|
|
197
|
+
"watch:start": getRunScript(pm, "build", [
|
|
198
|
+
"--watch",
|
|
199
|
+
"--onSuccess",
|
|
200
|
+
getRunScript(pm, "start")
|
|
201
|
+
]),
|
|
202
|
+
start
|
|
203
|
+
};
|
|
204
|
+
else scripts = {
|
|
205
|
+
build: "tsc -b src",
|
|
206
|
+
dev: `${getRunScript(pm, "build")} && ${getRunScript(pm, "start")}`,
|
|
207
|
+
watch: "tsc -b src -w",
|
|
208
|
+
"watch:start": `tsc-watch -b src --onSuccess "${getRunScript(pm, "start")}"`,
|
|
209
|
+
start
|
|
210
|
+
};
|
|
211
|
+
if (ctx.linter === "oxlint") {
|
|
212
|
+
scripts["lint"] = "oxlint src";
|
|
213
|
+
scripts["lint:fix"] = "oxlint --fix src";
|
|
214
|
+
} else if (ctx.linter === "eslint") {
|
|
215
|
+
scripts["lint"] = "eslint src";
|
|
216
|
+
scripts["lint:fix"] = "eslint src --fix";
|
|
217
|
+
}
|
|
218
|
+
if (ctx.formatter === "oxfmt") {
|
|
219
|
+
scripts["format"] = "oxfmt --write src";
|
|
220
|
+
scripts["format:check"] = "oxfmt --check src";
|
|
221
|
+
} else if (ctx.formatter === "prettier") {
|
|
222
|
+
scripts["format"] = "prettier --write src";
|
|
223
|
+
scripts["format:check"] = "prettier --check src";
|
|
224
|
+
}
|
|
225
|
+
return scripts;
|
|
226
|
+
}
|
|
227
|
+
function buildDependencies(ctx) {
|
|
228
|
+
const v = ctx.versions;
|
|
229
|
+
const dependencies = {
|
|
230
|
+
"@discordjs/builders": caret(v["@discordjs/builders"]),
|
|
231
|
+
"@wolfstar/http-framework": caret(v["@wolfstar/http-framework"]),
|
|
232
|
+
"discord-api-types": caret(v["discord-api-types"])
|
|
233
|
+
};
|
|
234
|
+
if (ctx.i18n) dependencies["@wolfstar/http-framework-i18n"] = caret(v["@wolfstar/http-framework-i18n"]);
|
|
235
|
+
return sortKeys(dependencies);
|
|
236
|
+
}
|
|
237
|
+
function buildDevDependencies(ctx) {
|
|
238
|
+
const v = ctx.versions;
|
|
239
|
+
const dev = {};
|
|
240
|
+
if (ctx.language === "ts") {
|
|
241
|
+
dev["@types/node"] = caret(v["@types/node"]);
|
|
242
|
+
switch (ctx.buildTool) {
|
|
243
|
+
case "tsc6":
|
|
244
|
+
dev["typescript"] = caret(v["typescript"]);
|
|
245
|
+
dev["tsc-watch"] = caret(v["tsc-watch"]);
|
|
246
|
+
break;
|
|
247
|
+
case "tsc7":
|
|
248
|
+
dev["typescript"] = v["typescript"];
|
|
249
|
+
dev["tsc-watch"] = caret(v["tsc-watch"]);
|
|
250
|
+
break;
|
|
251
|
+
case "tsdown":
|
|
252
|
+
dev["tsdown"] = caret(v["tsdown"]);
|
|
253
|
+
dev["typescript"] = caret(v["typescript"]);
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (ctx.linter === "eslint") {
|
|
258
|
+
dev["eslint"] = caret(v["eslint"]);
|
|
259
|
+
if (ctx.language === "ts") dev["typescript-eslint"] = caret(v["typescript-eslint"]);
|
|
260
|
+
else dev["@eslint/js"] = caret(v["@eslint/js"]);
|
|
261
|
+
} else if (ctx.linter === "oxlint") dev["oxlint"] = caret(v["oxlint"]);
|
|
262
|
+
if (ctx.formatter === "prettier") dev["prettier"] = caret(v["prettier"]);
|
|
263
|
+
else if (ctx.formatter === "oxfmt") dev["oxfmt"] = caret(v["oxfmt"]);
|
|
264
|
+
return sortKeys(dev);
|
|
265
|
+
}
|
|
266
|
+
function packageJson(ctx) {
|
|
267
|
+
const devDependencies = buildDevDependencies(ctx);
|
|
268
|
+
return json({
|
|
269
|
+
name: ctx.name,
|
|
270
|
+
version: "1.0.0",
|
|
271
|
+
description: "A Discord HTTP bot built with `@wolfstar/http-framework`",
|
|
272
|
+
type: "module",
|
|
273
|
+
scripts: buildScripts(ctx),
|
|
274
|
+
dependencies: buildDependencies(ctx),
|
|
275
|
+
...Object.keys(devDependencies).length > 0 ? { devDependencies } : {},
|
|
276
|
+
engines: { node: ">=20" }
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
const sharedCompilerOptions = {
|
|
280
|
+
target: "ES2022",
|
|
281
|
+
module: "Node16",
|
|
282
|
+
moduleResolution: "Node16",
|
|
283
|
+
strict: true,
|
|
284
|
+
esModuleInterop: true,
|
|
285
|
+
skipLibCheck: true,
|
|
286
|
+
declaration: true,
|
|
287
|
+
declarationMap: true,
|
|
288
|
+
sourceMap: true,
|
|
289
|
+
experimentalDecorators: true,
|
|
290
|
+
emitDecoratorMetadata: true
|
|
291
|
+
};
|
|
292
|
+
/** Writes the tsconfig(s). The tsc branches use a composite build so `tsc -b src` resolves `src/tsconfig.json`. */
|
|
293
|
+
function writeTsconfig(targetDir, ctx) {
|
|
294
|
+
if (ctx.language === "js") return;
|
|
295
|
+
if (ctx.buildTool === "tsdown") {
|
|
296
|
+
writeFile(join(targetDir, "tsconfig.json"), json({
|
|
297
|
+
compilerOptions: {
|
|
298
|
+
...sharedCompilerOptions,
|
|
299
|
+
outDir: "./dist",
|
|
300
|
+
rootDir: "./src"
|
|
301
|
+
},
|
|
302
|
+
include: ["src/**/*.ts"],
|
|
303
|
+
exclude: ["node_modules", "dist"]
|
|
304
|
+
}));
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
writeFile(join(targetDir, "tsconfig.json"), json({
|
|
308
|
+
files: [],
|
|
309
|
+
references: [{ path: "./src" }]
|
|
310
|
+
}));
|
|
311
|
+
writeFile(join(targetDir, "src", "tsconfig.json"), json({
|
|
312
|
+
compilerOptions: {
|
|
313
|
+
...sharedCompilerOptions,
|
|
314
|
+
rootDir: ".",
|
|
315
|
+
outDir: "../dist",
|
|
316
|
+
composite: true,
|
|
317
|
+
tsBuildInfoFile: "../dist/tsconfig.tsbuildinfo"
|
|
318
|
+
},
|
|
319
|
+
include: ["**/*.ts"]
|
|
320
|
+
}));
|
|
321
|
+
}
|
|
322
|
+
function writeBuildConfig(targetDir, ctx) {
|
|
323
|
+
if (ctx.language === "js" || ctx.buildTool !== "tsdown") return;
|
|
324
|
+
const content = [
|
|
325
|
+
"import { defineConfig } from 'tsdown';",
|
|
326
|
+
"",
|
|
327
|
+
"export default defineConfig({",
|
|
328
|
+
" entry: ['src/**/*.ts'],",
|
|
329
|
+
" format: ['esm'],",
|
|
330
|
+
" target: 'node20',",
|
|
331
|
+
" // Mirror the src/ structure so the framework can load command pieces from dist/commands at runtime.",
|
|
332
|
+
" unbundle: true,",
|
|
333
|
+
" // Emit dist/index.js so the shared `start` script (node dist/index.js) works.",
|
|
334
|
+
" outExtensions: () => ({ js: '.js' }),",
|
|
335
|
+
" clean: true,",
|
|
336
|
+
" sourcemap: true",
|
|
337
|
+
"});",
|
|
338
|
+
""
|
|
339
|
+
].join("\n");
|
|
340
|
+
writeFile(join(targetDir, "tsdown.config.ts"), content);
|
|
341
|
+
}
|
|
342
|
+
function writeLinterConfig(targetDir, ctx) {
|
|
343
|
+
if (ctx.linter === "oxlint") writeFile(join(targetDir, ".oxlintrc.json"), json({
|
|
344
|
+
$schema: "./node_modules/oxlint/configuration_schema.json",
|
|
345
|
+
...ctx.language === "ts" ? { plugins: ["typescript"] } : {},
|
|
346
|
+
categories: {
|
|
347
|
+
correctness: "error",
|
|
348
|
+
suspicious: "warn"
|
|
349
|
+
},
|
|
350
|
+
ignorePatterns: ["dist/**", "node_modules/**"]
|
|
351
|
+
}));
|
|
352
|
+
else if (ctx.linter === "eslint") {
|
|
353
|
+
const content = ctx.language === "ts" ? [
|
|
354
|
+
"import tseslint from 'typescript-eslint';",
|
|
355
|
+
"",
|
|
356
|
+
"export default tseslint.config(",
|
|
357
|
+
" { ignores: ['dist/**'] },",
|
|
358
|
+
" ...tseslint.configs.recommended",
|
|
359
|
+
");",
|
|
360
|
+
""
|
|
361
|
+
].join("\n") : [
|
|
362
|
+
"import js from '@eslint/js';",
|
|
363
|
+
"",
|
|
364
|
+
"export default [",
|
|
365
|
+
" { ignores: ['dist/**'] },",
|
|
366
|
+
" js.configs.recommended",
|
|
367
|
+
"];",
|
|
368
|
+
""
|
|
369
|
+
].join("\n");
|
|
370
|
+
writeFile(join(targetDir, "eslint.config.mjs"), content);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function writeFormatterConfig(targetDir, ctx) {
|
|
374
|
+
if (ctx.formatter === "oxfmt") writeFile(join(targetDir, ".oxfmtrc.json"), json({
|
|
375
|
+
$schema: "./node_modules/oxfmt/configuration_schema.json",
|
|
376
|
+
useTabs: true,
|
|
377
|
+
tabWidth: 4,
|
|
378
|
+
printWidth: 150,
|
|
379
|
+
singleQuote: true,
|
|
380
|
+
trailingComma: "none",
|
|
381
|
+
semi: true,
|
|
382
|
+
endOfLine: "lf"
|
|
383
|
+
}));
|
|
384
|
+
else if (ctx.formatter === "prettier") writeFile(join(targetDir, ".prettierrc.json"), json({
|
|
385
|
+
useTabs: true,
|
|
386
|
+
tabWidth: 4,
|
|
387
|
+
printWidth: 150,
|
|
388
|
+
singleQuote: true,
|
|
389
|
+
trailingComma: "none"
|
|
390
|
+
}));
|
|
391
|
+
}
|
|
392
|
+
/** Generates every config-style file in code so the output is always valid, formatted JSON/TS. */
|
|
393
|
+
function writeProjectFiles(targetDir, ctx) {
|
|
394
|
+
writeFile(join(targetDir, "package.json"), packageJson(ctx));
|
|
395
|
+
writeTsconfig(targetDir, ctx);
|
|
396
|
+
writeBuildConfig(targetDir, ctx);
|
|
397
|
+
writeLinterConfig(targetDir, ctx);
|
|
398
|
+
writeFormatterConfig(targetDir, ctx);
|
|
63
399
|
}
|
|
64
400
|
|
|
65
401
|
//#endregion
|
|
66
402
|
//#region src/tools/templateProcessor.ts
|
|
67
|
-
|
|
68
|
-
Handlebars.registerHelper("or", (...args) => args.slice(0, -1).some(Boolean));
|
|
69
|
-
Handlebars.registerHelper("and", (...args) => args.slice(0, -1).every(Boolean));
|
|
70
|
-
const templateDir = join(fileURLToPath(import.meta.url), "../../..", "template");
|
|
403
|
+
const templateDir = join(fileURLToPath(import.meta.url), "../..", "template");
|
|
71
404
|
function walkDir(dir) {
|
|
72
405
|
const entries = readdirSync(dir);
|
|
73
406
|
const files = [];
|
|
@@ -82,6 +415,8 @@ function processTemplate(outputDir, context) {
|
|
|
82
415
|
const allFiles = walkDir(templateDir);
|
|
83
416
|
for (const absoluteSource of allFiles) {
|
|
84
417
|
const relativePath = relative(templateDir, absoluteSource);
|
|
418
|
+
if (context.language === "js" && relativePath.endsWith(".ts.hbs")) continue;
|
|
419
|
+
if (context.language === "ts" && relativePath.endsWith(".js.hbs")) continue;
|
|
85
420
|
const rawContent = readFileSync(absoluteSource, "utf-8");
|
|
86
421
|
const isHandlebars = relativePath.endsWith(".hbs");
|
|
87
422
|
writeFile(join(outputDir, isHandlebars ? relativePath.slice(0, -4) : relativePath), isHandlebars ? Handlebars.compile(rawContent)(context) : rawContent);
|
|
@@ -90,75 +425,352 @@ function processTemplate(outputDir, context) {
|
|
|
90
425
|
|
|
91
426
|
//#endregion
|
|
92
427
|
//#region src/index.ts
|
|
93
|
-
|
|
428
|
+
function isValidPackageName(name) {
|
|
429
|
+
return /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(name);
|
|
430
|
+
}
|
|
431
|
+
function toValidPackageName(name) {
|
|
432
|
+
return name.trim().toLowerCase().replace(/\s+/g, "-").replace(/^[._]+/, "").replace(/[^a-z0-9-._~]/g, "-");
|
|
433
|
+
}
|
|
434
|
+
function printHelp() {
|
|
435
|
+
process.stdout.write(`
|
|
436
|
+
Usage: create-http-framework [project-name] [options]
|
|
437
|
+
|
|
438
|
+
Options:
|
|
439
|
+
--overwrite Overwrite target directory if it already exists
|
|
440
|
+
--no-interactive Skip all prompts and use defaults / flags
|
|
441
|
+
--interactive, -i Force interactive prompts even when an AI agent is detected
|
|
442
|
+
--package-manager <pm> npm | yarn | pnpm | bun (defaults to the detected one)
|
|
443
|
+
--language <lang> ts | js (default: ts)
|
|
444
|
+
--build <tool> tsc6 | tsc7 | tsdown — TypeScript only (default: tsdown)
|
|
445
|
+
--lint <linter> none | eslint | oxlint (default: oxlint)
|
|
446
|
+
--format <formatter> none | prettier | oxfmt (default: oxfmt)
|
|
447
|
+
--port <number> HTTP port (default: 3000)
|
|
448
|
+
--i18n / --no-i18n Toggle @wolfstar/http-framework-i18n (default: off)
|
|
449
|
+
--install / --no-install Toggle dependency installation (default: on)
|
|
450
|
+
--help, -h Print this message and exit
|
|
451
|
+
|
|
452
|
+
`);
|
|
453
|
+
process.exit(0);
|
|
454
|
+
}
|
|
455
|
+
function parseEnum(value, allowed, label) {
|
|
456
|
+
if (value === void 0) return void 0;
|
|
457
|
+
if (!allowed.includes(value)) {
|
|
458
|
+
cancel(`Invalid ${label} "${value}". Expected one of: ${allowed.join(", ")}.`);
|
|
459
|
+
process.exit(1);
|
|
460
|
+
}
|
|
461
|
+
return value;
|
|
462
|
+
}
|
|
94
463
|
async function main() {
|
|
464
|
+
const argv = mri(process.argv.slice(2), {
|
|
465
|
+
boolean: ["overwrite", "help"],
|
|
466
|
+
string: [
|
|
467
|
+
"package-manager",
|
|
468
|
+
"language",
|
|
469
|
+
"build",
|
|
470
|
+
"lint",
|
|
471
|
+
"format",
|
|
472
|
+
"port"
|
|
473
|
+
],
|
|
474
|
+
alias: {
|
|
475
|
+
h: "help",
|
|
476
|
+
i: "interactive"
|
|
477
|
+
},
|
|
478
|
+
default: { install: true }
|
|
479
|
+
});
|
|
480
|
+
if (argv["help"]) {
|
|
481
|
+
printHelp();
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const argProjectName = argv._[0];
|
|
485
|
+
const flagOverwrite = argv["overwrite"];
|
|
486
|
+
const flagInteractive = argv["interactive"];
|
|
487
|
+
const cliPackageManager = parseEnum(argv["package-manager"], PACKAGE_MANAGERS, "--package-manager");
|
|
488
|
+
const cliLanguage = parseEnum(argv["language"], LANGUAGES, "--language");
|
|
489
|
+
const cliBuild = parseEnum(argv["build"], BUILD_TOOLS, "--build");
|
|
490
|
+
const cliLint = parseEnum(argv["lint"], LINTERS, "--lint");
|
|
491
|
+
const cliFormat = parseEnum(argv["format"], FORMATTERS, "--format");
|
|
492
|
+
const cliPort = argv["port"];
|
|
493
|
+
const cliI18n = argv["i18n"];
|
|
494
|
+
const cliInstall = argv["install"];
|
|
495
|
+
const { isAgent, agent } = await determineAgent();
|
|
496
|
+
const agentMode = isAgent && flagInteractive !== true;
|
|
497
|
+
const nonInteractive = flagInteractive === false || agentMode;
|
|
95
498
|
intro("Welcome to the WolfStar HTTP Framework!");
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (!value) return "Project name cannot be empty.";
|
|
104
|
-
if (!NAME_PATTERN.test(value)) return "Use only lowercase letters, numbers, dashes, and underscores.";
|
|
105
|
-
if (directoryExists(resolve(value))) return `Directory "${value}" already exists.`;
|
|
499
|
+
if (agentMode && flagInteractive !== false) log.info(`AI agent detected (${agent.name}) — running in non-interactive mode`);
|
|
500
|
+
const detectedPackageManager = await detectPackageManager();
|
|
501
|
+
let projectName;
|
|
502
|
+
if (nonInteractive) {
|
|
503
|
+
if (!argProjectName) {
|
|
504
|
+
cancel("A project name argument is required in non-interactive mode.");
|
|
505
|
+
process.exit(1);
|
|
106
506
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
const portInput = await text({
|
|
113
|
-
message: "Which port should the HTTP server listen on?",
|
|
114
|
-
placeholder: "3000",
|
|
115
|
-
initialValue: "3000",
|
|
116
|
-
validate(value) {
|
|
117
|
-
const n = Number(value);
|
|
118
|
-
if (!value || Number.isNaN(n) || !Number.isInteger(n) || n < 1 || n > 65535) return "Enter a valid port number (1–65535).";
|
|
507
|
+
const normalized = isValidPackageName(argProjectName) ? argProjectName : toValidPackageName(argProjectName);
|
|
508
|
+
if (!normalized || !isValidPackageName(normalized)) {
|
|
509
|
+
cancel("Provide a valid npm-compatible project name in non-interactive mode.");
|
|
510
|
+
process.exit(1);
|
|
119
511
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
512
|
+
projectName = normalized;
|
|
513
|
+
} else {
|
|
514
|
+
const suggested = argProjectName ? toValidPackageName(argProjectName) : "";
|
|
515
|
+
const nameResult = await text({
|
|
516
|
+
message: "What is the name of your project?",
|
|
517
|
+
placeholder: "my-discord-bot",
|
|
518
|
+
initialValue: argProjectName && isValidPackageName(argProjectName) ? argProjectName : suggested,
|
|
519
|
+
validate(value) {
|
|
520
|
+
if (!value) return "Project name cannot be empty.";
|
|
521
|
+
if (!isValidPackageName(value)) return "Use lowercase letters, numbers, dashes, dots, or underscores (npm-compatible).";
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
if (isCancel(nameResult)) {
|
|
525
|
+
cancel("Operation cancelled.");
|
|
526
|
+
process.exit(0);
|
|
527
|
+
}
|
|
528
|
+
projectName = nameResult;
|
|
124
529
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
530
|
+
let packageManager;
|
|
531
|
+
if (nonInteractive) packageManager = cliPackageManager ?? detectedPackageManager;
|
|
532
|
+
else {
|
|
533
|
+
const pmResult = await select({
|
|
534
|
+
message: "Which package manager would you like to use?",
|
|
535
|
+
options: PACKAGE_MANAGERS.map((pm) => ({
|
|
536
|
+
value: pm,
|
|
537
|
+
label: pm
|
|
538
|
+
})),
|
|
539
|
+
initialValue: PACKAGE_MANAGERS.includes(detectedPackageManager) ? detectedPackageManager : "npm"
|
|
540
|
+
});
|
|
541
|
+
if (isCancel(pmResult)) {
|
|
542
|
+
cancel("Operation cancelled.");
|
|
543
|
+
process.exit(0);
|
|
544
|
+
}
|
|
545
|
+
packageManager = pmResult;
|
|
132
546
|
}
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
547
|
+
const targetDir = resolve(projectName);
|
|
548
|
+
if (directoryExists(targetDir) && !isEmpty(targetDir)) if (flagOverwrite) emptyDir(targetDir);
|
|
549
|
+
else if (nonInteractive) {
|
|
550
|
+
cancel(`"${projectName}" already exists. Use --overwrite to overwrite it.`);
|
|
551
|
+
process.exit(1);
|
|
552
|
+
} else {
|
|
553
|
+
const choice = await select({
|
|
554
|
+
message: `"${projectName}" already exists. What would you like to do?`,
|
|
555
|
+
options: [{
|
|
556
|
+
value: "overwrite",
|
|
557
|
+
label: "Overwrite the directory"
|
|
558
|
+
}, {
|
|
559
|
+
value: "cancel",
|
|
560
|
+
label: "Cancel"
|
|
561
|
+
}]
|
|
562
|
+
});
|
|
563
|
+
if (isCancel(choice) || choice === "cancel") {
|
|
564
|
+
cancel("Operation cancelled.");
|
|
565
|
+
process.exit(0);
|
|
566
|
+
}
|
|
567
|
+
emptyDir(targetDir);
|
|
568
|
+
}
|
|
569
|
+
let port;
|
|
570
|
+
if (nonInteractive) {
|
|
571
|
+
port = cliPort === void 0 ? 3e3 : Number(cliPort);
|
|
572
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
573
|
+
cancel("Provide a valid --port between 1 and 65535.");
|
|
574
|
+
process.exit(1);
|
|
575
|
+
}
|
|
576
|
+
} else {
|
|
577
|
+
const portResult = await text({
|
|
578
|
+
message: "Which port should the HTTP server listen on?",
|
|
579
|
+
placeholder: "3000",
|
|
580
|
+
initialValue: "3000",
|
|
581
|
+
validate(value) {
|
|
582
|
+
const n = Number(value);
|
|
583
|
+
if (!value || Number.isNaN(n) || !Number.isInteger(n) || n < 1 || n > 65535) return "Enter a valid port number (1–65535).";
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
if (isCancel(portResult)) {
|
|
587
|
+
cancel("Operation cancelled.");
|
|
588
|
+
process.exit(0);
|
|
589
|
+
}
|
|
590
|
+
port = Number(portResult);
|
|
591
|
+
}
|
|
592
|
+
let language;
|
|
593
|
+
if (nonInteractive) language = cliLanguage ?? "ts";
|
|
594
|
+
else {
|
|
595
|
+
const languageResult = await select({
|
|
596
|
+
message: "Which language would you like to use?",
|
|
597
|
+
options: [{
|
|
598
|
+
value: "ts",
|
|
599
|
+
label: "TypeScript"
|
|
600
|
+
}, {
|
|
601
|
+
value: "js",
|
|
602
|
+
label: "JavaScript"
|
|
603
|
+
}],
|
|
604
|
+
initialValue: "ts"
|
|
605
|
+
});
|
|
606
|
+
if (isCancel(languageResult)) {
|
|
607
|
+
cancel("Operation cancelled.");
|
|
608
|
+
process.exit(0);
|
|
609
|
+
}
|
|
610
|
+
language = languageResult;
|
|
611
|
+
}
|
|
612
|
+
let buildTool = "tsdown";
|
|
613
|
+
if (cliBuild !== void 0 && language === "js") log.warn("--build only applies to TypeScript projects and will be ignored.");
|
|
614
|
+
if (language === "ts") if (nonInteractive) buildTool = cliBuild ?? "tsdown";
|
|
615
|
+
else {
|
|
616
|
+
const buildResult = await select({
|
|
617
|
+
message: "Which build tool would you like to use?",
|
|
618
|
+
options: [
|
|
619
|
+
{
|
|
620
|
+
value: "tsdown",
|
|
621
|
+
label: "tsdown (bundler)"
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
value: "tsc6",
|
|
625
|
+
label: "TypeScript 6.0 (tsc)"
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
value: "tsc7",
|
|
629
|
+
label: "TypeScript 7.0 rc (tsc)"
|
|
630
|
+
}
|
|
631
|
+
],
|
|
632
|
+
initialValue: "tsdown"
|
|
633
|
+
});
|
|
634
|
+
if (isCancel(buildResult)) {
|
|
635
|
+
cancel("Operation cancelled.");
|
|
636
|
+
process.exit(0);
|
|
637
|
+
}
|
|
638
|
+
buildTool = buildResult;
|
|
639
|
+
}
|
|
640
|
+
let linter;
|
|
641
|
+
if (nonInteractive) linter = cliLint ?? "oxlint";
|
|
642
|
+
else {
|
|
643
|
+
const lintResult = await select({
|
|
644
|
+
message: "Which linter would you like to use?",
|
|
645
|
+
options: [
|
|
646
|
+
{
|
|
647
|
+
value: "oxlint",
|
|
648
|
+
label: "oxlint"
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
value: "eslint",
|
|
652
|
+
label: "ESLint"
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
value: "none",
|
|
656
|
+
label: "None"
|
|
657
|
+
}
|
|
658
|
+
],
|
|
659
|
+
initialValue: "oxlint"
|
|
660
|
+
});
|
|
661
|
+
if (isCancel(lintResult)) {
|
|
662
|
+
cancel("Operation cancelled.");
|
|
663
|
+
process.exit(0);
|
|
664
|
+
}
|
|
665
|
+
linter = lintResult;
|
|
666
|
+
}
|
|
667
|
+
let formatter;
|
|
668
|
+
if (nonInteractive) formatter = cliFormat ?? "oxfmt";
|
|
669
|
+
else {
|
|
670
|
+
const formatResult = await select({
|
|
671
|
+
message: "Which formatter would you like to use?",
|
|
672
|
+
options: [
|
|
673
|
+
{
|
|
674
|
+
value: "oxfmt",
|
|
675
|
+
label: "oxfmt"
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
value: "prettier",
|
|
679
|
+
label: "Prettier"
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
value: "none",
|
|
683
|
+
label: "None"
|
|
684
|
+
}
|
|
685
|
+
],
|
|
686
|
+
initialValue: "oxfmt"
|
|
687
|
+
});
|
|
688
|
+
if (isCancel(formatResult)) {
|
|
689
|
+
cancel("Operation cancelled.");
|
|
690
|
+
process.exit(0);
|
|
691
|
+
}
|
|
692
|
+
formatter = formatResult;
|
|
693
|
+
}
|
|
694
|
+
let wantsI18n;
|
|
695
|
+
if (nonInteractive) wantsI18n = cliI18n ?? false;
|
|
696
|
+
else {
|
|
697
|
+
const i18nResult = await confirm({
|
|
698
|
+
message: "Would you like to add i18n support? (@wolfstar/http-framework-i18n)",
|
|
699
|
+
initialValue: false
|
|
700
|
+
});
|
|
701
|
+
if (isCancel(i18nResult)) {
|
|
702
|
+
cancel("Operation cancelled.");
|
|
703
|
+
process.exit(0);
|
|
704
|
+
}
|
|
705
|
+
wantsI18n = Boolean(i18nResult);
|
|
706
|
+
}
|
|
707
|
+
let wantsInstall;
|
|
708
|
+
if (nonInteractive) wantsInstall = cliInstall;
|
|
709
|
+
else {
|
|
710
|
+
const installResult = await confirm({
|
|
711
|
+
message: `Would you like to automatically install dependencies with ${packageManager}?`,
|
|
712
|
+
initialValue: true
|
|
713
|
+
});
|
|
714
|
+
if (isCancel(installResult)) {
|
|
715
|
+
cancel("Operation cancelled.");
|
|
716
|
+
process.exit(0);
|
|
717
|
+
}
|
|
718
|
+
wantsInstall = Boolean(installResult);
|
|
140
719
|
}
|
|
141
720
|
const s = spinner();
|
|
142
721
|
s.start("Fetching latest dependency versions...");
|
|
143
|
-
const versions = fetchDependencyVersions(
|
|
722
|
+
const versions = await fetchDependencyVersions({
|
|
723
|
+
i18n: wantsI18n,
|
|
724
|
+
language,
|
|
725
|
+
buildTool,
|
|
726
|
+
linter,
|
|
727
|
+
formatter
|
|
728
|
+
});
|
|
144
729
|
s.stop("Versions fetched.");
|
|
145
730
|
s.start("Generating project files...");
|
|
146
|
-
|
|
147
|
-
|
|
731
|
+
processTemplate(targetDir, {
|
|
732
|
+
name: projectName,
|
|
733
|
+
port,
|
|
734
|
+
language
|
|
735
|
+
});
|
|
736
|
+
writeProjectFiles(targetDir, {
|
|
148
737
|
name: projectName,
|
|
149
|
-
port
|
|
150
|
-
i18n:
|
|
738
|
+
port,
|
|
739
|
+
i18n: wantsI18n,
|
|
151
740
|
packageManager,
|
|
152
|
-
|
|
741
|
+
language,
|
|
742
|
+
buildTool,
|
|
743
|
+
linter,
|
|
744
|
+
formatter,
|
|
153
745
|
versions
|
|
154
746
|
});
|
|
155
747
|
s.stop("Project files generated.");
|
|
748
|
+
if (formatter !== "none") try {
|
|
749
|
+
s.start("Formatting project files...");
|
|
750
|
+
if (formatter === "oxfmt") runCommand("npx", [
|
|
751
|
+
"--yes",
|
|
752
|
+
"oxfmt",
|
|
753
|
+
"--write",
|
|
754
|
+
"."
|
|
755
|
+
], targetDir);
|
|
756
|
+
else runCommand("npx", [
|
|
757
|
+
"--yes",
|
|
758
|
+
"prettier",
|
|
759
|
+
"--write",
|
|
760
|
+
".",
|
|
761
|
+
"--log-level",
|
|
762
|
+
"warn"
|
|
763
|
+
], targetDir);
|
|
764
|
+
s.stop("Project files formatted.");
|
|
765
|
+
} catch {
|
|
766
|
+
s.stop("Skipped formatting (formatter not available yet — it will run after install).");
|
|
767
|
+
}
|
|
156
768
|
if (wantsInstall) {
|
|
157
769
|
s.start(`Installing dependencies with ${packageManager}...`);
|
|
158
|
-
await
|
|
770
|
+
await installDependencies(packageManager, targetDir);
|
|
159
771
|
s.stop("Dependencies installed.");
|
|
160
772
|
}
|
|
161
|
-
outro(`Done! To get started:\n\n cd ${projectName}\n${wantsInstall ? "" : ` ${packageManager}
|
|
773
|
+
outro(`Done! To get started:\n\n cd ${projectName}\n${wantsInstall ? "" : ` ${getInstallScript(packageManager)}\n`} ${getRunScript(packageManager, "dev")}`);
|
|
162
774
|
}
|
|
163
775
|
main().catch((error) => {
|
|
164
776
|
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": "1.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Create a new WolfStar HTTP Framework bot project",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -33,12 +33,17 @@
|
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@clack/prompts": "^1.
|
|
36
|
+
"@clack/prompts": "^1.7.0",
|
|
37
|
+
"@vercel/detect-agent": "^1.2.3",
|
|
38
|
+
"cross-spawn": "^7.0.6",
|
|
37
39
|
"handlebars": "^4.7.9",
|
|
38
|
-
"
|
|
40
|
+
"mri": "^1.2.0",
|
|
41
|
+
"package-manager-detector": "^1.7.0"
|
|
39
42
|
},
|
|
40
43
|
"devDependencies": {
|
|
44
|
+
"@types/cross-spawn": "^6.0.6",
|
|
41
45
|
"@types/handlebars": "^4.1.0",
|
|
46
|
+
"@types/mri": "^1.2.0",
|
|
42
47
|
"@types/node": "22.15.21",
|
|
43
48
|
"tsdown": "^0.22.3",
|
|
44
49
|
"typescript": "~5.8.3"
|
package/template/.env.hbs
CHANGED
package/template/.gitignore.hbs
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Command } from '@wolfstar/http-framework';
|
|
2
|
+
import { MessageFlags } from 'discord-api-types/v10';
|
|
3
|
+
|
|
4
|
+
export class PingCommand extends Command {
|
|
5
|
+
chatInputRun(interaction) {
|
|
6
|
+
return interaction.reply({ content: 'Pong!', flags: MessageFlags.Ephemeral });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
static builder = (builder) => builder
|
|
10
|
+
.setName('ping')
|
|
11
|
+
.setDescription('Replies with pong!')
|
|
12
|
+
}
|
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
import { Command } from '@wolfstar/http-framework';
|
|
2
|
-
'discord-api-types/v10';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { Command, RegisterCommand } from '@wolfstar/http-framework';
|
|
2
|
+
import { MessageFlags } from 'discord-api-types/v10';
|
|
3
|
+
|
|
4
|
+
@RegisterCommand((builder) =>
|
|
5
|
+
builder //
|
|
6
|
+
.setName('ping')
|
|
7
|
+
.setDescription('Replies with pong!')
|
|
8
|
+
)
|
|
9
|
+
export class PingCommand extends Command {
|
|
10
|
+
public override chatInputRun(interaction: Command.ChatInputInteraction) {
|
|
11
|
+
return interaction.reply({ content: 'Pong!', flags: MessageFlags.Ephemeral });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { dirname, join } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { Client } from '@wolfstar/http-framework';
|
|
4
|
+
|
|
5
|
+
const client = new Client();
|
|
6
|
+
|
|
7
|
+
await client.load({ baseUserDirectory: join(dirname(fileURLToPath(import.meta.url)), 'commands') });
|
|
8
|
+
await client.listen({ port: {{port}} });
|
|
9
|
+
|
|
10
|
+
console.log('Bot is running on port {{port}}');
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import { dirname, join } from 'node:path';
|
|
2
|
-
|
|
3
|
-
{
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { dirname, join } from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { Client } from '@wolfstar/http-framework';
|
|
4
|
+
|
|
5
|
+
const client = new Client();
|
|
6
|
+
|
|
7
|
+
await client.load({ baseUserDirectory: join(dirname(fileURLToPath(import.meta.url)), 'commands') });
|
|
8
|
+
await client.listen({ port: {{port}} });
|
|
9
|
+
|
|
10
|
+
console.log('Bot is running on port {{port}}');
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{ "name": "{{name}}", "version": "1.0.0", "description": "A Discord HTTP bot built with `@wolfstar/http-framework`", "author": "`@wolfstar`",
|
|
2
|
-
"repository": { "url": "https://github.com/yourusername/your-repo.git" }, "bugs": { "url": "https://github.com/yourusername/your-repo/issues" },
|
|
3
|
-
"homepage": "https://github.com/yourusername/your-repo#readme", "keywords": ["discord", "bot", "http", "wolfstar"], "type": "module", "scripts": {
|
|
4
|
-
"build": "tsc", "dev": "node --watch --loader ts-node/esm src/index.ts", "start": "node dist/index.js" }, "dependencies": { "@discordjs/builders": "^{{versions.discordJsBuilders}}",
|
|
5
|
-
"@wolfstar/http-framework": "^{{versions.httpFramework}}"{{#if i18n}}, "@wolfstar/http-framework-i18n": "^{{versions.httpFrameworkI18n}}"{{/if}},
|
|
6
|
-
"discord-api-types": "^{{versions.discordApiTypes}}" }, "devDependencies": { "@types/node": "^{{versions.typesNode}}", "ts-node": "^{{versions.tsNode}}",
|
|
7
|
-
"typescript": "^{{versions.typescript}}" }, "engines": { "node": ">=20" } }
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
{ "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "outDir": "./dist", "rootDir": "./src", "strict": true,
|
|
2
|
-
"esModuleInterop": true, "skipLibCheck": true, "declaration": true, "declarationMap": true, "sourceMap": true, "experimentalDecorators": true,
|
|
3
|
-
"emitDecoratorMetadata": true }, "include": ["src/**/*.ts"], "exclude": ["node_modules", "dist"] }
|