create-nubis 0.0.0 → 0.1.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +6 -2
- package/dist/create.d.ts +12 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +294 -0
- package/dist/project-name.d.ts +1 -0
- package/dist/templates.d.ts +9 -0
- package/package.json +37 -7
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 EYTAK LIMITED
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# create-nubis
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Start a [Nubis](https://eytak.com/nubis) Discord bot:
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
```sh
|
|
6
|
+
npx create-nubis my-bot
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
MIT. Built by [Eytak](https://eytak.com).
|
package/dist/create.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type PackageManager } from "./templates";
|
|
2
|
+
export interface CreateOptions {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly packageManager: PackageManager;
|
|
5
|
+
readonly nubisVersion: string;
|
|
6
|
+
}
|
|
7
|
+
/** Writes a new bot into `dir`, which must be missing or empty. Returns the files written. */
|
|
8
|
+
export declare function createProject(dir: string, options: CreateOptions): Promise<string[]>;
|
|
9
|
+
/** The package manager that ran create-nubis (npx, bunx, pnpm create…), from npm_config_user_agent. */
|
|
10
|
+
export declare function detectPackageManager(userAgent: string | undefined): PackageManager;
|
|
11
|
+
/** `^0.1.0` for released versions; "latest" while developing (0.0.0). */
|
|
12
|
+
export declare function nubisRange(version: string): string;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
// package.json
|
|
6
|
+
var package_default = {
|
|
7
|
+
name: "create-nubis",
|
|
8
|
+
version: "0.1.0-next.0",
|
|
9
|
+
description: "Scaffold a new Nubis Discord bot. Built by Eytak.",
|
|
10
|
+
keywords: [
|
|
11
|
+
"discord",
|
|
12
|
+
"discord-bot",
|
|
13
|
+
"nubis",
|
|
14
|
+
"create"
|
|
15
|
+
],
|
|
16
|
+
license: "MIT",
|
|
17
|
+
author: "Eytak",
|
|
18
|
+
homepage: "https://eytak.com/nubis",
|
|
19
|
+
repository: {
|
|
20
|
+
type: "git",
|
|
21
|
+
url: "git+https://github.com/Eytak-Technologies/nubis.git",
|
|
22
|
+
directory: "packages/create-nubis"
|
|
23
|
+
},
|
|
24
|
+
bugs: "https://github.com/Eytak-Technologies/nubis/issues",
|
|
25
|
+
type: "module",
|
|
26
|
+
bin: {
|
|
27
|
+
"create-nubis": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
files: [
|
|
30
|
+
"dist"
|
|
31
|
+
],
|
|
32
|
+
engines: {
|
|
33
|
+
node: ">=20"
|
|
34
|
+
},
|
|
35
|
+
scripts: {
|
|
36
|
+
build: "bun build ./src/index.ts --outdir dist --target node --format esm --packages external && tsc -p tsconfig.build.json",
|
|
37
|
+
"check-types": "tsc --noEmit",
|
|
38
|
+
test: "bun test",
|
|
39
|
+
lint: "biome lint ."
|
|
40
|
+
},
|
|
41
|
+
devDependencies: {
|
|
42
|
+
nubis: "workspace:*"
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// src/create.ts
|
|
47
|
+
import { mkdir, readdir, writeFile } from "node:fs/promises";
|
|
48
|
+
import { dirname, join } from "node:path";
|
|
49
|
+
|
|
50
|
+
// src/templates.ts
|
|
51
|
+
var json = (value) => `${JSON.stringify(value, null, "\t")}
|
|
52
|
+
`;
|
|
53
|
+
function templateFiles(options) {
|
|
54
|
+
const { name, packageManager, nubisVersion } = options;
|
|
55
|
+
const bun = packageManager === "bun";
|
|
56
|
+
const run = packageManager === "npm" ? "npm run" : packageManager;
|
|
57
|
+
const testImport = bun ? "bun:test" : "vitest";
|
|
58
|
+
return {
|
|
59
|
+
"package.json": json({
|
|
60
|
+
name,
|
|
61
|
+
private: true,
|
|
62
|
+
type: "module",
|
|
63
|
+
scripts: {
|
|
64
|
+
dev: "nubis dev",
|
|
65
|
+
start: "nubis start",
|
|
66
|
+
sync: "nubis sync --global",
|
|
67
|
+
test: bun ? "bun test" : "vitest run",
|
|
68
|
+
typecheck: "tsc --noEmit"
|
|
69
|
+
},
|
|
70
|
+
dependencies: { nubis: nubisVersion },
|
|
71
|
+
devDependencies: bun ? { "@types/bun": "^1.4.4", typescript: "^7.0.0" } : { "@types/node": "^22.0.0", typescript: "^7.0.0", vitest: "^5.1.0" }
|
|
72
|
+
}),
|
|
73
|
+
"tsconfig.json": json({
|
|
74
|
+
compilerOptions: {
|
|
75
|
+
target: "ES2022",
|
|
76
|
+
module: "Preserve",
|
|
77
|
+
moduleResolution: "bundler",
|
|
78
|
+
types: [bun ? "bun" : "node"],
|
|
79
|
+
strict: true,
|
|
80
|
+
noEmit: true,
|
|
81
|
+
skipLibCheck: true,
|
|
82
|
+
verbatimModuleSyntax: true,
|
|
83
|
+
noUncheckedIndexedAccess: true
|
|
84
|
+
},
|
|
85
|
+
include: ["**/*.ts"]
|
|
86
|
+
}),
|
|
87
|
+
"nubis.config.ts": `import { defineConfig } from "nubis";
|
|
88
|
+
|
|
89
|
+
// Settings for the whole bot. Every key is optional; see https://eytak.com/nubis/docs/configuration
|
|
90
|
+
export default defineConfig({
|
|
91
|
+
presence: { activity: { type: "watching", name: "for /ping" } },
|
|
92
|
+
});
|
|
93
|
+
`,
|
|
94
|
+
".gitignore": `node_modules
|
|
95
|
+
dist
|
|
96
|
+
.env
|
|
97
|
+
`,
|
|
98
|
+
".env.example": `# From https://discord.com/developers/applications, under your app.
|
|
99
|
+
DISCORD_TOKEN=
|
|
100
|
+
DISCORD_APPLICATION_ID=
|
|
101
|
+
DISCORD_PUBLIC_KEY=
|
|
102
|
+
`,
|
|
103
|
+
"README.md": `# ${name}
|
|
104
|
+
|
|
105
|
+
A Discord bot built with [Nubis](https://eytak.com/nubis). Docs: https://eytak.com/nubis/docs
|
|
106
|
+
|
|
107
|
+
## Getting started
|
|
108
|
+
|
|
109
|
+
1. Create an application at https://discord.com/developers/applications and add a bot.
|
|
110
|
+
2. Copy \`.env.example\` to \`.env\` and fill it in.
|
|
111
|
+
3. Run the tests: \`${run} test\`
|
|
112
|
+
4. Start the bot: \`${run} dev\`
|
|
113
|
+
|
|
114
|
+
## Deploying
|
|
115
|
+
|
|
116
|
+
\`${run} sync\` registers your commands everywhere the bot is installed; run it when commands change. \`${run} start\` runs the bot.
|
|
117
|
+
|
|
118
|
+
## Layout
|
|
119
|
+
|
|
120
|
+
| Path | What it is |
|
|
121
|
+
| --- | --- |
|
|
122
|
+
| \`commands/ping.ts\` | \`/ping\`, the smallest command |
|
|
123
|
+
| \`commands/echo.ts\` | \`/echo\`, with a required option |
|
|
124
|
+
| \`commands/fun/roll.ts\` | \`/fun roll\`: folders become subcommands; \`_folder.ts\` describes them |
|
|
125
|
+
| \`commands/counter.ts\`, \`components/counter.ts\` | A button that keeps its count in typed state |
|
|
126
|
+
| \`events/ready.ts\` | Runs when the bot connects |
|
|
127
|
+
| \`nubis.config.ts\` | Bot-wide settings: presence, intents, guards, error handling |
|
|
128
|
+
| \`tests/\` | Tests that run without Discord, using \`nubis/testing\` |
|
|
129
|
+
`,
|
|
130
|
+
"commands/ping.ts": `import { command } from "nubis";
|
|
131
|
+
|
|
132
|
+
export default command({
|
|
133
|
+
description: "Check that the bot is alive",
|
|
134
|
+
run: ({ reply }) => reply("Pong!"),
|
|
135
|
+
});
|
|
136
|
+
`,
|
|
137
|
+
"commands/echo.ts": `import { command, option } from "nubis";
|
|
138
|
+
|
|
139
|
+
export default command({
|
|
140
|
+
description: "Repeat something back to you",
|
|
141
|
+
ephemeral: true,
|
|
142
|
+
options: {
|
|
143
|
+
text: option.string({ description: "What to repeat", required: true, maxLength: 200 }),
|
|
144
|
+
},
|
|
145
|
+
// options.text is a string: required options are never undefined.
|
|
146
|
+
run: ({ options, reply }) => reply(options.text),
|
|
147
|
+
});
|
|
148
|
+
`,
|
|
149
|
+
"commands/fun/_folder.ts": `import { folder } from "nubis";
|
|
150
|
+
|
|
151
|
+
export default folder({ description: "Games and silly things" });
|
|
152
|
+
`,
|
|
153
|
+
"commands/fun/roll.ts": `import { command, option } from "nubis";
|
|
154
|
+
|
|
155
|
+
export default command({
|
|
156
|
+
description: "Roll a die",
|
|
157
|
+
options: {
|
|
158
|
+
sides: option.integer({
|
|
159
|
+
description: "How many sides",
|
|
160
|
+
choices: [
|
|
161
|
+
{ name: "Six", value: 6 },
|
|
162
|
+
{ name: "Twenty", value: 20 },
|
|
163
|
+
],
|
|
164
|
+
}),
|
|
165
|
+
},
|
|
166
|
+
// options.sides is 6 | 20 | undefined, from the choices.
|
|
167
|
+
run: ({ options, reply }) => {
|
|
168
|
+
const sides = options.sides ?? 6;
|
|
169
|
+
return reply(\`\uD83C\uDFB2 You rolled \${1 + Math.floor(Math.random() * sides)} (d\${sides})\`);
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
`,
|
|
173
|
+
"commands/counter.ts": `import { command } from "nubis";
|
|
174
|
+
import { counter } from "../components/counter";
|
|
175
|
+
|
|
176
|
+
export default command({
|
|
177
|
+
description: "A button that counts clicks",
|
|
178
|
+
run: ({ reply }) => reply({ content: "Clicked 0 times", components: [counter({ count: 0 })] }),
|
|
179
|
+
});
|
|
180
|
+
`,
|
|
181
|
+
"components/counter.ts": `import { button, stateOf } from "nubis";
|
|
182
|
+
|
|
183
|
+
// The count travels in the button's custom_id, so no database is needed.
|
|
184
|
+
// For state you act on (user ids, amounts), use a schema such as Zod instead
|
|
185
|
+
// of stateOf: anyone can craft a custom_id, and schemas validate every click.
|
|
186
|
+
export const counter = button({
|
|
187
|
+
id: "counter",
|
|
188
|
+
label: "Click me",
|
|
189
|
+
style: "primary",
|
|
190
|
+
state: stateOf<{ count: number }>(),
|
|
191
|
+
onClick: ({ state, update }) => {
|
|
192
|
+
const count = state.count + 1;
|
|
193
|
+
return update({ content: \`Clicked \${count} times\`, components: [counter({ count })] });
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
`,
|
|
197
|
+
"events/ready.ts": `import { event } from "nubis";
|
|
198
|
+
|
|
199
|
+
export default event("ready", (data) => {
|
|
200
|
+
console.log(\`Logged in as \${data.user.username}\`);
|
|
201
|
+
});
|
|
202
|
+
`,
|
|
203
|
+
"tests/commands.test.ts": `import { describe, expect, test } from "${testImport}";
|
|
204
|
+
import { fake, testCommand, testComponent } from "nubis/testing";
|
|
205
|
+
import { counter } from "../components/counter";
|
|
206
|
+
import echo from "../commands/echo";
|
|
207
|
+
import ping from "../commands/ping";
|
|
208
|
+
|
|
209
|
+
describe("commands", () => {
|
|
210
|
+
test("/ping answers", async () => {
|
|
211
|
+
const result = await testCommand(ping).run();
|
|
212
|
+
expect(result.replies).toEqual(["Pong!"]);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("/echo repeats privately", async () => {
|
|
216
|
+
const result = await testCommand(echo).as({ user: fake.user("alice") }).run({ text: "hello" });
|
|
217
|
+
expect(result.replies).toEqual(["hello"]);
|
|
218
|
+
expect(result.ephemeral).toBe(true);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("the counter counts", async () => {
|
|
222
|
+
const result = await testComponent(counter).click({ count: 41 });
|
|
223
|
+
expect(result.replies).toEqual(["Clicked 42 times"]);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
`
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/create.ts
|
|
231
|
+
async function createProject(dir, options) {
|
|
232
|
+
const existing = await readdir(dir).catch(() => []);
|
|
233
|
+
if (existing.length > 0) {
|
|
234
|
+
throw new Error(`${dir} isn't empty. Pick a new folder name, or empty this one first.`);
|
|
235
|
+
}
|
|
236
|
+
const files = templateFiles(options);
|
|
237
|
+
for (const [path, content] of Object.entries(files)) {
|
|
238
|
+
await mkdir(dirname(join(dir, path)), { recursive: true });
|
|
239
|
+
await writeFile(join(dir, path), content);
|
|
240
|
+
}
|
|
241
|
+
return Object.keys(files);
|
|
242
|
+
}
|
|
243
|
+
function detectPackageManager(userAgent) {
|
|
244
|
+
const name = userAgent?.split("/")[0];
|
|
245
|
+
return name === "bun" || name === "pnpm" || name === "yarn" ? name : "npm";
|
|
246
|
+
}
|
|
247
|
+
function nubisRange(version) {
|
|
248
|
+
return version === "0.0.0" ? "latest" : `^${version}`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/project-name.ts
|
|
252
|
+
var VALID_NAME = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
253
|
+
function resolveProjectName(args) {
|
|
254
|
+
const name = args.find((arg) => !arg.startsWith("-")) ?? "my-bot";
|
|
255
|
+
if (!VALID_NAME.test(name)) {
|
|
256
|
+
throw new Error(`"${name}" is not a valid package name. Use lowercase letters, digits and dashes.`);
|
|
257
|
+
}
|
|
258
|
+
return name;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/index.ts
|
|
262
|
+
var MANAGERS = ["bun", "npm", "pnpm", "yarn"];
|
|
263
|
+
function flag(args, name) {
|
|
264
|
+
const index = args.indexOf(name);
|
|
265
|
+
return index === -1 ? undefined : args[index + 1];
|
|
266
|
+
}
|
|
267
|
+
async function main(args) {
|
|
268
|
+
const pmFlag = flag(args, "--pm");
|
|
269
|
+
if (pmFlag !== undefined && !MANAGERS.includes(pmFlag)) {
|
|
270
|
+
throw new Error(`--pm must be one of ${MANAGERS.join(", ")}.`);
|
|
271
|
+
}
|
|
272
|
+
const packageManager = pmFlag ?? detectPackageManager(process.env.npm_config_user_agent);
|
|
273
|
+
const name = resolveProjectName(args.filter((arg, i) => !(args[i - 1] === "--pm") && arg !== "--pm"));
|
|
274
|
+
const dir = resolve(name);
|
|
275
|
+
await createProject(dir, {
|
|
276
|
+
name: name.split("/").at(-1) ?? name,
|
|
277
|
+
packageManager,
|
|
278
|
+
nubisVersion: nubisRange(package_default.version)
|
|
279
|
+
});
|
|
280
|
+
const run = packageManager === "npm" ? "npm run" : packageManager;
|
|
281
|
+
console.log(`
|
|
282
|
+
Created ${name} with Nubis.
|
|
283
|
+
|
|
284
|
+
cd ${name}
|
|
285
|
+
${packageManager} install
|
|
286
|
+
${run} test
|
|
287
|
+
|
|
288
|
+
Then add your bot's token to .env (see .env.example) and run \`${run} dev\`.
|
|
289
|
+
`);
|
|
290
|
+
}
|
|
291
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
292
|
+
console.error(error instanceof Error ? error.message : error);
|
|
293
|
+
process.exitCode = 1;
|
|
294
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function resolveProjectName(args: readonly string[]): string;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type PackageManager = "bun" | "npm" | "pnpm" | "yarn";
|
|
2
|
+
export interface TemplateOptions {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly packageManager: PackageManager;
|
|
5
|
+
/** The `nubis` version range to depend on. */
|
|
6
|
+
readonly nubisVersion: string;
|
|
7
|
+
}
|
|
8
|
+
/** The files of a new bot, by path. Bun projects test with bun:test, others with Vitest. */
|
|
9
|
+
export declare function templateFiles(options: TemplateOptions): Record<string, string>;
|
package/package.json
CHANGED
|
@@ -1,9 +1,39 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
"name": "create-nubis",
|
|
3
|
+
"version": "0.1.0-next.0",
|
|
4
|
+
"description": "Scaffold a new Nubis Discord bot. Built by Eytak.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"discord",
|
|
7
|
+
"discord-bot",
|
|
8
|
+
"nubis",
|
|
9
|
+
"create"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"author": "Eytak",
|
|
13
|
+
"homepage": "https://eytak.com/nubis",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/Eytak-Technologies/nubis.git",
|
|
17
|
+
"directory": "packages/create-nubis"
|
|
18
|
+
},
|
|
19
|
+
"bugs": "https://github.com/Eytak-Technologies/nubis/issues",
|
|
20
|
+
"type": "module",
|
|
21
|
+
"bin": {
|
|
22
|
+
"create-nubis": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=20"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "bun build ./src/index.ts --outdir dist --target node --format esm --packages external && tsc -p tsconfig.build.json",
|
|
32
|
+
"check-types": "tsc --noEmit",
|
|
33
|
+
"test": "bun test",
|
|
34
|
+
"lint": "biome lint ."
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"nubis": "0.1.0-next.0"
|
|
38
|
+
}
|
|
9
39
|
}
|