@nectar-js/create 0.1.0 → 0.2.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 CHANGED
@@ -1,22 +1,26 @@
1
1
  # @nectar-js/create
2
2
 
3
- Scaffolds a new [Nectar](https://github.com/nectar-js/nectar) project.
3
+ Creates a new [Nectar](https://nectar-js.github.io/nectar/) project.
4
4
 
5
5
  ```bash
6
6
  npm create @nectar-js
7
7
  ```
8
8
 
9
- Asks for a directory, TypeScript or JavaScript, and a package manager, then prints the steps to get the bot running.
9
+ It asks where to put the project, TypeScript or JavaScript, and which package manager to use. Then it asks for your bot's token, application ID, and test server ID, which go in `.env` and can be skipped. It can also install dependencies and create a git repository.
10
10
 
11
11
  ```
12
12
  npm create @nectar-js [directory] [options]
13
13
 
14
- --ts, --js Language. Asked when omitted.
15
- --pm <name> Package manager: npm, pnpm, yarn, or bun. Detected from the one running this.
16
- --yes, -y Take the defaults instead of asking.
17
- --help, -h Show this help.
14
+ --ts, --js Language. Asked when omitted.
15
+ --pm <name> Package manager: npm, pnpm, yarn, or bun. Detected from the one running this.
16
+ --install, --no-install Install dependencies. Asked when omitted.
17
+ --git, --no-git Create a git repository. Asked when omitted.
18
+ --yes, -y Use the defaults for anything not given, and skip the Discord details.
19
+ --help, -h Show this help.
18
20
  ```
19
21
 
22
+ With npm, put `--` before the options: `npm create @nectar-js my-bot -- --ts`.
23
+
20
24
  ## License
21
25
 
22
26
  MIT. See [LICENSE](https://github.com/nectar-js/nectar/blob/main/LICENSE).
package/dist/index.js CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
+ import { spawn } from "node:child_process";
3
4
  import path from "node:path";
4
- import { createInterface } from "node:readline/promises";
5
5
  import { parseArgs, styleText } from "node:util";
6
+ import * as p from "@clack/prompts";
6
7
  import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
7
8
  //#region src/scaffold.ts
8
9
  const PACKAGE_MANAGERS = [
@@ -11,24 +12,37 @@ const PACKAGE_MANAGERS = [
11
12
  "yarn",
12
13
  "bun"
13
14
  ];
15
+ const PORTAL_URL = "https://discord.com/developers/applications";
14
16
  /** The package manager that launched us, from `npm_config_user_agent`. npm when unknown. */
15
17
  function detectPackageManager(userAgent) {
16
18
  const name = userAgent?.split("/")[0];
17
19
  return PACKAGE_MANAGERS.includes(name) ? name : "npm";
18
20
  }
19
- function nextSteps(directory, { packageManager }, style = (_format, text) => text) {
21
+ /** Why `dir` can't hold a new project, or `undefined` if it can. */
22
+ function projectProblem(dir) {
23
+ const name = path.basename(path.resolve(dir));
24
+ if (!/^[a-z0-9][a-z0-9._-]*$/.test(name)) return `"${name}" isn't a valid package name. Use lowercase letters, digits, ".", "_", and "-".`;
25
+ if (existsSync(dir) && readdirSync(dir).length > 0) return `${dir} already has files in it.`;
26
+ }
27
+ /** Why `value` isn't a Discord ID, or `undefined` if it is one or is empty. */
28
+ function idProblem(value) {
29
+ if (value === void 0 || value === "" || /^\d{17,20}$/.test(value)) return void 0;
30
+ return "Discord IDs are 17 to 20 digits.";
31
+ }
32
+ /** What's left to do after creating the project, one step per line. */
33
+ function nextSteps(directory, { packageManager, credentials = {} }, installed, style = (_format, text) => text) {
20
34
  const run = packageManager === "npm" ? "npm run" : packageManager;
21
- const step = (command, note = "") => ` ${style("cyan", command)}${note === "" ? "" : ` ${style("dim", note)}`}`;
35
+ const missing = [
36
+ credentials.token ? null : "DISCORD_TOKEN",
37
+ credentials.applicationId ? null : "DISCORD_APPLICATION_ID",
38
+ credentials.guildId ? null : "DEV_GUILD_ID"
39
+ ].filter((key) => key !== null);
22
40
  return [
23
- style("bold", "Next:"),
24
- step(`cd ${directory}`),
25
- step(`${packageManager} install`),
26
- step("cp .env.example .env", "then fill in DISCORD_TOKEN and DISCORD_APPLICATION_ID"),
27
- ` ${style("dim", "add your test server's ID to dev.guilds in the config")}`,
28
- step(`${run} dev`),
29
- "",
30
- `${style("dim", "Tokens and IDs live in the Developer Portal:")} https://discord.com/developers/applications`
31
- ].join("\n");
41
+ directory === "." ? null : style("cyan", `cd ${directory}`),
42
+ installed ? null : style("cyan", `${packageManager} install`),
43
+ missing.length === 0 ? null : `Fill in ${missing.join(", ")} in .env`,
44
+ style("cyan", `${run} dev`)
45
+ ].filter((line) => line !== null).join("\n");
32
46
  }
33
47
  const { version } = createRequire(import.meta.url)("../package.json");
34
48
  const DISCORD_JS = "^14.27.0";
@@ -36,10 +50,12 @@ const DISCORD_JS = "^14.27.0";
36
50
  function templateFiles(options) {
37
51
  const ts = options.language === "ts";
38
52
  const ext = ts ? "ts" : "js";
53
+ const { token = "", applicationId = "", guildId = "" } = options.credentials ?? {};
39
54
  const files = {
40
55
  "package.json": packageJson(options),
41
56
  [`nectar.config.${ext}`]: config(),
42
- ".env.example": "DISCORD_TOKEN=\nDISCORD_APPLICATION_ID=\n",
57
+ ".env": `DISCORD_TOKEN=${token}\nDISCORD_APPLICATION_ID=${applicationId}\nDEV_GUILD_ID=${guildId}\n`,
58
+ ".env.example": "DISCORD_TOKEN=\nDISCORD_APPLICATION_ID=\nDEV_GUILD_ID=\n",
43
59
  ".gitignore": "node_modules/\n.nectar/\n.env\n",
44
60
  [`app/middleware.${ext}`]: middleware(ts),
45
61
  [`app/commands/ping/command.${ext}`]: pingCommand(ts),
@@ -51,7 +67,8 @@ function templateFiles(options) {
51
67
  }
52
68
  /** Writes the template into `dir`, which must not exist or be empty. Returns the written paths. */
53
69
  function scaffold(dir, options) {
54
- if (existsSync(dir) && readdirSync(dir).length > 0) throw new Error(`${dir} already exists and is not empty.`);
70
+ const problem = projectProblem(dir);
71
+ if (problem !== void 0) throw new Error(problem);
55
72
  const files = templateFiles(options);
56
73
  for (const [relative, content] of Object.entries(files)) {
57
74
  const file = path.join(dir, ...relative.split("/"));
@@ -94,9 +111,8 @@ export default defineConfig({
94
111
  applicationId: process.env.DISCORD_APPLICATION_ID,
95
112
  intents: ["Guilds"],
96
113
  dev: {
97
- // Commands register here instantly while you develop. Paste your test server's ID,
98
- // or read it from .env the same way as the token above.
99
- guilds: [],
114
+ // Commands register in your test server instantly while you develop.
115
+ guilds: process.env.DEV_GUILD_ID ? [process.env.DEV_GUILD_ID] : [],
100
116
  },
101
117
  });
102
118
  `;
@@ -179,15 +195,53 @@ export default defineEvent("clientReady", async (client) => {
179
195
  }
180
196
  //#endregion
181
197
  //#region src/index.ts
198
+ const DOCS_URL = "https://nectar-js.github.io/nectar/";
182
199
  const USAGE = `Usage: npm create @nectar-js [directory] [options]
183
200
 
184
201
  Options:
185
- --ts, --js Language. Asked when omitted.
186
- --pm <name> Package manager: npm, pnpm, yarn, or bun. Detected from the one running this.
187
- --yes, -y Take the defaults instead of asking.
188
- --help, -h Show this help.`;
189
- const colors = process.stdout.isTTY === true && !process.env.NO_COLOR;
190
- const style = (format, text) => colors ? styleText(format, text) : text;
202
+ --ts, --js Language. Asked when omitted.
203
+ --pm <name> Package manager: npm, pnpm, yarn, or bun. Detected from the one running this.
204
+ --install, --no-install Install dependencies. Asked when omitted.
205
+ --git, --no-git Create a git repository. Asked when omitted.
206
+ --yes, -y Use the defaults for anything not given, and skip the Discord details.
207
+ --help, -h Show this help.`;
208
+ /** The logo's honey behind dark text, where the terminal can show it. */
209
+ function badge(text) {
210
+ if (process.stdout.hasColors?.(2 ** 24)) return `\x1b[48;2;245;165;36m\x1b[38;2;27;20;14m${text}\x1b[0m`;
211
+ return styleText(["bgYellow", "black"], text);
212
+ }
213
+ /** Ends the run when the person presses Ctrl+C or Escape at a prompt. */
214
+ function answer(value) {
215
+ if (p.isCancel(value)) {
216
+ p.cancel("Nothing was created.");
217
+ process.exit(1);
218
+ }
219
+ return value;
220
+ }
221
+ /** Runs a command in `cwd`, collecting its output for when it fails. The arguments are fixed. */
222
+ function run(command, args, cwd) {
223
+ return new Promise((resolve) => {
224
+ const child = process.platform === "win32" ? spawn([command, ...args].join(" "), {
225
+ cwd,
226
+ shell: true
227
+ }) : spawn(command, args, { cwd });
228
+ let output = "";
229
+ child.stdout?.on("data", (chunk) => {
230
+ output += chunk;
231
+ });
232
+ child.stderr?.on("data", (chunk) => {
233
+ output += chunk;
234
+ });
235
+ child.on("error", (error) => resolve({
236
+ ok: false,
237
+ output: error.message
238
+ }));
239
+ child.on("close", (code) => resolve({
240
+ ok: code === 0,
241
+ output
242
+ }));
243
+ });
244
+ }
191
245
  async function main(argv) {
192
246
  let values;
193
247
  let positionals;
@@ -198,6 +252,8 @@ async function main(argv) {
198
252
  ts: { type: "boolean" },
199
253
  js: { type: "boolean" },
200
254
  pm: { type: "string" },
255
+ install: { type: "boolean" },
256
+ git: { type: "boolean" },
201
257
  yes: {
202
258
  type: "boolean",
203
259
  short: "y"
@@ -207,7 +263,8 @@ async function main(argv) {
207
263
  short: "h"
208
264
  }
209
265
  },
210
- allowPositionals: true
266
+ allowPositionals: true,
267
+ allowNegative: true
211
268
  }));
212
269
  } catch (error) {
213
270
  console.error(error instanceof Error ? error.message : String(error));
@@ -226,60 +283,92 @@ async function main(argv) {
226
283
  console.error(`Unknown package manager "${values.pm}". Use ${PACKAGE_MANAGERS.join(", ")}.`);
227
284
  return 2;
228
285
  }
229
- const rl = values.yes !== true && process.stdin.isTTY === true ? createInterface({
230
- input: process.stdin,
231
- output: process.stdout
232
- }) : null;
233
- const ask = async (question, fallback) => {
234
- if (rl === null) return fallback;
235
- const answer = (await rl.question(`${question} (${fallback}) `)).trim();
236
- return answer === "" ? fallback : answer;
286
+ const interactive = values.yes !== true && process.stdin.isTTY === true;
287
+ p.intro(`${badge(" Nectar ")} ${styleText("dim", `v${version}`)}`);
288
+ const directory = positionals[0] ?? (interactive ? answer(await p.text({
289
+ message: "Where should the project go?",
290
+ placeholder: "my-bot",
291
+ defaultValue: "my-bot",
292
+ validate: (value) => projectProblem(value || "my-bot")
293
+ })) : "my-bot");
294
+ const problem = projectProblem(directory);
295
+ if (problem !== void 0) {
296
+ p.cancel(problem);
297
+ return 2;
298
+ }
299
+ const language = values.js === true ? "js" : values.ts === true || !interactive ? "ts" : answer(await p.select({
300
+ message: "Language",
301
+ options: [{
302
+ value: "ts",
303
+ label: "TypeScript"
304
+ }, {
305
+ value: "js",
306
+ label: "JavaScript"
307
+ }]
308
+ }));
309
+ const detected = detectPackageManager(process.env.npm_config_user_agent);
310
+ const packageManager = values.pm ?? (interactive ? answer(await p.select({
311
+ message: "Package manager",
312
+ options: PACKAGE_MANAGERS.map((pm) => ({
313
+ value: pm,
314
+ label: pm,
315
+ ...pm === detected ? { hint: "detected" } : {}
316
+ })),
317
+ initialValue: detected
318
+ })) : detected);
319
+ const credentials = {};
320
+ if (interactive) {
321
+ p.log.message(`Your bot's details from ${PORTAL_URL}\nPress Enter to skip any of them and fill them in later.`);
322
+ credentials.token = answer(await p.password({
323
+ message: "Bot token",
324
+ mask: "•"
325
+ }));
326
+ credentials.applicationId = answer(await p.text({
327
+ message: "Application ID",
328
+ validate: idProblem
329
+ }));
330
+ credentials.guildId = answer(await p.text({
331
+ message: "Test server ID",
332
+ placeholder: "Commands register here instantly while you develop",
333
+ validate: idProblem
334
+ }));
335
+ }
336
+ const install = values.install ?? (interactive ? answer(await p.confirm({ message: `Install dependencies with ${packageManager}?` })) : true);
337
+ const git = values.git ?? (interactive ? answer(await p.confirm({ message: "Create a git repository?" })) : true);
338
+ const target = path.resolve(directory);
339
+ const name = path.basename(target);
340
+ const options = {
341
+ name,
342
+ language,
343
+ packageManager,
344
+ credentials
237
345
  };
238
- try {
239
- const directory = positionals[0] ?? await ask("Project directory:", "my-bot");
240
- const name = path.basename(path.resolve(directory));
241
- if (!/^[a-z0-9][a-z0-9._-]*$/.test(name)) {
242
- console.error(`"${name}" is not a valid package name. Use lowercase letters, digits, ".", "_", and "-".`);
243
- return 2;
244
- }
245
- let language = values.js === true ? "js" : "ts";
246
- if (values.ts !== true && values.js !== true) {
247
- const answer = await ask("TypeScript or JavaScript? [ts/js]", "ts");
248
- if (answer !== "ts" && answer !== "js") {
249
- console.error(`Answer ts or js, not "${answer}".`);
250
- return 2;
251
- }
252
- language = answer;
253
- }
254
- const detected = detectPackageManager(process.env.npm_config_user_agent);
255
- let packageManager = values.pm ?? detected;
256
- if (values.pm === void 0) {
257
- const answer = await ask(`Package manager? [${PACKAGE_MANAGERS.join("/")}]`, detected);
258
- if (!PACKAGE_MANAGERS.includes(answer)) {
259
- console.error(`Unknown package manager "${answer}".`);
260
- return 2;
261
- }
262
- packageManager = answer;
263
- }
264
- const options = {
265
- name,
266
- language,
267
- packageManager
268
- };
269
- const target = path.resolve(directory);
270
- let files;
271
- try {
272
- files = scaffold(target, options);
273
- } catch (error) {
274
- console.error(error instanceof Error ? error.message : String(error));
275
- return 1;
276
- }
277
- console.log(`\n${style("green", "✔")} Created ${style("bold", name)} with ${files.length} files.\n`);
278
- console.log(nextSteps(directory, options, style));
279
- return 0;
280
- } finally {
281
- rl?.close();
346
+ scaffold(target, options);
347
+ p.log.success(`Created ${styleText("bold", name)}`);
348
+ let installed = false;
349
+ if (install) {
350
+ const spin = process.stdout.isTTY ? p.spinner() : null;
351
+ const message = `Installing dependencies with ${packageManager}`;
352
+ if (spin === null) p.log.step(message);
353
+ else spin.start(message);
354
+ const result = await run(packageManager, ["install"], target);
355
+ installed = result.ok;
356
+ const failure = `${packageManager} install failed:\n${result.output.trim()}`;
357
+ if (spin === null) {
358
+ if (result.ok) p.log.success("Installed dependencies");
359
+ else p.log.error(failure);
360
+ } else if (result.ok) spin.stop("Installed dependencies");
361
+ else spin.error(failure);
362
+ }
363
+ if (git) {
364
+ const result = await run("git", ["init", "--quiet"], target);
365
+ if (result.ok) p.log.success("Created a git repository");
366
+ else p.log.warn(`Couldn't create a git repository: ${result.output.trim()}`);
282
367
  }
368
+ const style = (format, text) => styleText(format, text);
369
+ p.note(nextSteps(directory, options, installed, style), "Next steps");
370
+ p.outro(`Docs at ${styleText("underline", DOCS_URL)}`);
371
+ return 0;
283
372
  }
284
373
  process.exitCode = await main(process.argv.slice(2));
285
374
  //#endregion
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/scaffold.ts","../src/index.ts"],"sourcesContent":["import { existsSync, mkdirSync, readdirSync, writeFileSync } from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport path from \"node:path\";\n\nexport type Language = \"ts\" | \"js\";\nexport type PackageManager = \"npm\" | \"pnpm\" | \"yarn\" | \"bun\";\n\nexport interface ScaffoldOptions {\n name: string;\n language: Language;\n packageManager: PackageManager;\n}\n\nexport const PACKAGE_MANAGERS: PackageManager[] = [\"npm\", \"pnpm\", \"yarn\", \"bun\"];\n\n/** The package manager that launched us, from `npm_config_user_agent`. npm when unknown. */\nexport function detectPackageManager(userAgent: string | undefined): PackageManager {\n const name = userAgent?.split(\"/\")[0];\n return PACKAGE_MANAGERS.includes(name as PackageManager) ? (name as PackageManager) : \"npm\";\n}\n\ntype Style = (format: \"bold\" | \"green\" | \"dim\" | \"cyan\", text: string) => string;\n\nexport function nextSteps(\n directory: string,\n { packageManager }: ScaffoldOptions,\n style: Style = (_format, text) => text,\n): string {\n const run = packageManager === \"npm\" ? \"npm run\" : packageManager;\n const step = (command: string, note = \"\") =>\n ` ${style(\"cyan\", command)}${note === \"\" ? \"\" : ` ${style(\"dim\", note)}`}`;\n return [\n style(\"bold\", \"Next:\"),\n step(`cd ${directory}`),\n step(`${packageManager} install`),\n step(\"cp .env.example .env\", \"then fill in DISCORD_TOKEN and DISCORD_APPLICATION_ID\"),\n ` ${style(\"dim\", \"add your test server's ID to dev.guilds in the config\")}`,\n step(`${run} dev`),\n \"\",\n `${style(\"dim\", \"Tokens and IDs live in the Developer Portal:\")} https://discord.com/developers/applications`,\n ].join(\"\\n\");\n}\n\nconst { version } = createRequire(import.meta.url)(\"../package.json\") as { version: string };\nconst DISCORD_JS = \"^14.27.0\";\n\n/** Everything a new project needs, as file paths relative to the project root. */\nexport function templateFiles(options: ScaffoldOptions): Record<string, string> {\n const ts = options.language === \"ts\";\n const ext = ts ? \"ts\" : \"js\";\n const files: Record<string, string> = {\n \"package.json\": packageJson(options),\n [`nectar.config.${ext}`]: config(),\n \".env.example\": \"DISCORD_TOKEN=\\nDISCORD_APPLICATION_ID=\\n\",\n \".gitignore\": \"node_modules/\\n.nectar/\\n.env\\n\",\n [`app/middleware.${ext}`]: middleware(ts),\n [`app/commands/ping/command.${ext}`]: pingCommand(ts),\n [`app/components/counter/[count]/button.${ext}`]: counterButton(ts),\n [`app/events/clientReady/event.${ext}`]: readyEvent(),\n };\n if (ts) files[\"tsconfig.json\"] = tsconfig();\n return files;\n}\n\n/** Writes the template into `dir`, which must not exist or be empty. Returns the written paths. */\nexport function scaffold(dir: string, options: ScaffoldOptions): string[] {\n if (existsSync(dir) && readdirSync(dir).length > 0) {\n throw new Error(`${dir} already exists and is not empty.`);\n }\n const files = templateFiles(options);\n for (const [relative, content] of Object.entries(files)) {\n const file = path.join(dir, ...relative.split(\"/\"));\n mkdirSync(path.dirname(file), { recursive: true });\n writeFileSync(file, content);\n }\n return Object.keys(files);\n}\n\nfunction packageJson({ name, language }: ScaffoldOptions): string {\n const scripts: Record<string, string> = {\n dev: \"nectar dev\",\n build: \"nectar build\",\n start: \"nectar start\",\n check: \"nectar check\",\n sync: \"nectar sync\",\n };\n if (language === \"ts\") scripts.typecheck = \"nectar build && tsc --noEmit\";\n const pkg = {\n name,\n private: true,\n type: \"module\",\n engines: { node: \">=22.18\" },\n scripts,\n dependencies: { \"@nectar-js/nectar\": `^${version}`, \"discord.js\": DISCORD_JS },\n ...(language === \"ts\"\n ? { devDependencies: { \"@types/node\": \"^22.20.1\", typescript: \"^5.9.0\" } }\n : {}),\n };\n return `${JSON.stringify(pkg, null, 2)}\\n`;\n}\n\nfunction config(): string {\n return `import { defineConfig } from \"@nectar-js/nectar\";\n\nexport default defineConfig({\n token: process.env.DISCORD_TOKEN,\n applicationId: process.env.DISCORD_APPLICATION_ID,\n intents: [\"Guilds\"],\n dev: {\n // Commands register here instantly while you develop. Paste your test server's ID,\n // or read it from .env the same way as the token above.\n guilds: [],\n },\n});\n`;\n}\n\nfunction tsconfig(): string {\n return `${JSON.stringify(\n {\n compilerOptions: {\n target: \"ES2023\",\n module: \"NodeNext\",\n moduleResolution: \"NodeNext\",\n lib: [\"ES2023\"],\n types: [\"node\"],\n strict: true,\n noEmit: true,\n allowImportingTsExtensions: true,\n skipLibCheck: true,\n verbatimModuleSyntax: true,\n },\n include: [\"app\", \"nectar.config.ts\", \".nectar/types.d.ts\"],\n },\n null,\n 2,\n )}\\n`;\n}\n\nfunction middleware(ts: boolean): string {\n return `import { defineMiddleware } from \"@nectar-js/nectar\";\n\n// Runs before every interaction. Whatever you pass to next() is on ctx downstream${ts ? \", typed\" : \"\"}.\nexport default defineMiddleware(async (_ctx, next) => {\n return next({ startedAt: Date.now() });\n});\n`;\n}\n\nfunction pingCommand(ts: boolean): string {\n const meta = ts\n ? `export const meta: CommandMeta = {`\n : `/** @type {import(\"@nectar-js/nectar\").CommandMeta} */\nexport const meta = {`;\n const imports = ts\n ? `import { type CommandMeta, customId, defineCommand } from \"@nectar-js/nectar\";`\n : `import { customId, defineCommand } from \"@nectar-js/nectar\";`;\n return `${imports}\nimport { ActionRowBuilder, ButtonBuilder, ButtonStyle } from \"discord.js\";\n\n${meta}\n description: \"Check that the bot is alive\",\n};\n\nexport default defineCommand(\"ping\", async (ctx) => {\n const button = new ButtonBuilder()\n .setCustomId(customId(\"counter/[count]\", { count: \"0\" }))\n .setLabel(\"Clicked 0 times\")\n .setStyle(ButtonStyle.Primary);\n await ctx.interaction.reply({\n content: \\`Pong in \\${Date.now() - ctx.startedAt}ms\\`,\n components: [new ActionRowBuilder${ts ? \"<ButtonBuilder>\" : \"\"}().addComponents(button)],\n });\n});\n`;\n}\n\nfunction counterButton(ts: boolean): string {\n return `import { customId, defineComponent } from \"@nectar-js/nectar\";\nimport { ActionRowBuilder, ButtonBuilder, ButtonStyle } from \"discord.js\";\n\n// The directory name [count] makes ctx.params.count a string decoded from the custom ID.\nexport default defineComponent(\"counter/[count]\", async (ctx) => {\n const count = Number(ctx.params.count) + 1;\n const button = new ButtonBuilder()\n .setCustomId(customId(\"counter/[count]\", { count: String(count) }))\n .setLabel(\\`Clicked \\${count} time\\${count === 1 ? \"\" : \"s\"}\\`)\n .setStyle(ButtonStyle.Primary);\n await ctx.interaction.update({\n components: [new ActionRowBuilder${ts ? \"<ButtonBuilder>\" : \"\"}().addComponents(button)],\n });\n});\n`;\n}\n\nfunction readyEvent(): string {\n return `import { defineEvent } from \"@nectar-js/nectar\";\n\nexport default defineEvent(\"clientReady\", async (client) => {\n console.log(\\`Logged in as \\${client.user.tag}\\`);\n});\n`;\n}\n","#!/usr/bin/env node\nimport path from \"node:path\";\nimport { createInterface } from \"node:readline/promises\";\nimport { parseArgs, styleText } from \"node:util\";\nimport {\n detectPackageManager,\n type Language,\n nextSteps,\n PACKAGE_MANAGERS,\n type PackageManager,\n type ScaffoldOptions,\n scaffold,\n} from \"./scaffold.js\";\n\nconst USAGE = `Usage: npm create @nectar-js [directory] [options]\n\nOptions:\n --ts, --js Language. Asked when omitted.\n --pm <name> Package manager: npm, pnpm, yarn, or bun. Detected from the one running this.\n --yes, -y Take the defaults instead of asking.\n --help, -h Show this help.`;\n\nconst colors = process.stdout.isTTY === true && !process.env.NO_COLOR;\nconst style = (format: \"bold\" | \"green\" | \"dim\" | \"cyan\", text: string): string =>\n colors ? styleText(format, text) : text;\n\nasync function main(argv: string[]): Promise<number> {\n let values: Record<string, string | boolean | undefined>;\n let positionals: string[];\n try {\n ({ values, positionals } = parseArgs({\n args: argv,\n options: {\n ts: { type: \"boolean\" },\n js: { type: \"boolean\" },\n pm: { type: \"string\" },\n yes: { type: \"boolean\", short: \"y\" },\n help: { type: \"boolean\", short: \"h\" },\n },\n allowPositionals: true,\n }));\n } catch (error) {\n console.error(error instanceof Error ? error.message : String(error));\n console.error(USAGE);\n return 2;\n }\n if (values.help === true) {\n console.log(USAGE);\n return 0;\n }\n if (values.ts === true && values.js === true) {\n console.error(\"Pass --ts or --js, not both.\");\n return 2;\n }\n if (values.pm !== undefined && !PACKAGE_MANAGERS.includes(values.pm as PackageManager)) {\n console.error(`Unknown package manager \"${values.pm}\". Use ${PACKAGE_MANAGERS.join(\", \")}.`);\n return 2;\n }\n\n const interactive = values.yes !== true && process.stdin.isTTY === true;\n const rl = interactive ? createInterface({ input: process.stdin, output: process.stdout }) : null;\n const ask = async (question: string, fallback: string): Promise<string> => {\n if (rl === null) return fallback;\n const answer = (await rl.question(`${question} (${fallback}) `)).trim();\n return answer === \"\" ? fallback : answer;\n };\n\n try {\n const directory = positionals[0] ?? (await ask(\"Project directory:\", \"my-bot\"));\n const name = path.basename(path.resolve(directory));\n if (!/^[a-z0-9][a-z0-9._-]*$/.test(name)) {\n console.error(\n `\"${name}\" is not a valid package name. Use lowercase letters, digits, \".\", \"_\", and \"-\".`,\n );\n return 2;\n }\n\n let language: Language = values.js === true ? \"js\" : \"ts\";\n if (values.ts !== true && values.js !== true) {\n const answer = await ask(\"TypeScript or JavaScript? [ts/js]\", \"ts\");\n if (answer !== \"ts\" && answer !== \"js\") {\n console.error(`Answer ts or js, not \"${answer}\".`);\n return 2;\n }\n language = answer;\n }\n\n const detected = detectPackageManager(process.env.npm_config_user_agent);\n let packageManager = (values.pm as PackageManager | undefined) ?? detected;\n if (values.pm === undefined) {\n const answer = await ask(`Package manager? [${PACKAGE_MANAGERS.join(\"/\")}]`, detected);\n if (!PACKAGE_MANAGERS.includes(answer as PackageManager)) {\n console.error(`Unknown package manager \"${answer}\".`);\n return 2;\n }\n packageManager = answer as PackageManager;\n }\n\n const options: ScaffoldOptions = { name, language, packageManager };\n const target = path.resolve(directory);\n let files: string[];\n try {\n files = scaffold(target, options);\n } catch (error) {\n console.error(error instanceof Error ? error.message : String(error));\n return 1;\n }\n\n console.log(\n `\\n${style(\"green\", \"✔\")} Created ${style(\"bold\", name)} with ${files.length} files.\\n`,\n );\n console.log(nextSteps(directory, options, style));\n return 0;\n } finally {\n rl?.close();\n }\n}\n\nprocess.exitCode = await main(process.argv.slice(2));\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/scaffold.ts","../src/index.ts"],"sourcesContent":["import { existsSync, mkdirSync, readdirSync, writeFileSync } from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport path from \"node:path\";\n\nexport type Language = \"ts\" | \"js\";\nexport type PackageManager = \"npm\" | \"pnpm\" | \"yarn\" | \"bun\";\n\n/** What goes into `.env`. Anything left empty is filled in later. */\nexport interface Credentials {\n token?: string;\n applicationId?: string;\n guildId?: string;\n}\n\nexport interface ScaffoldOptions {\n name: string;\n language: Language;\n packageManager: PackageManager;\n credentials?: Credentials;\n}\n\nexport const PACKAGE_MANAGERS: PackageManager[] = [\"npm\", \"pnpm\", \"yarn\", \"bun\"];\n\nexport const PORTAL_URL = \"https://discord.com/developers/applications\";\n\n/** The package manager that launched us, from `npm_config_user_agent`. npm when unknown. */\nexport function detectPackageManager(userAgent: string | undefined): PackageManager {\n const name = userAgent?.split(\"/\")[0];\n return PACKAGE_MANAGERS.includes(name as PackageManager) ? (name as PackageManager) : \"npm\";\n}\n\n/** Why `dir` can't hold a new project, or `undefined` if it can. */\nexport function projectProblem(dir: string): string | undefined {\n const name = path.basename(path.resolve(dir));\n if (!/^[a-z0-9][a-z0-9._-]*$/.test(name)) {\n return `\"${name}\" isn't a valid package name. Use lowercase letters, digits, \".\", \"_\", and \"-\".`;\n }\n if (existsSync(dir) && readdirSync(dir).length > 0) return `${dir} already has files in it.`;\n return undefined;\n}\n\n/** Why `value` isn't a Discord ID, or `undefined` if it is one or is empty. */\nexport function idProblem(value: string | undefined): string | undefined {\n if (value === undefined || value === \"\" || /^\\d{17,20}$/.test(value)) return undefined;\n return \"Discord IDs are 17 to 20 digits.\";\n}\n\ntype Style = (format: \"cyan\" | \"dim\", text: string) => string;\n\n/** What's left to do after creating the project, one step per line. */\nexport function nextSteps(\n directory: string,\n { packageManager, credentials = {} }: ScaffoldOptions,\n installed: boolean,\n style: Style = (_format, text) => text,\n): string {\n const run = packageManager === \"npm\" ? \"npm run\" : packageManager;\n const missing = [\n credentials.token ? null : \"DISCORD_TOKEN\",\n credentials.applicationId ? null : \"DISCORD_APPLICATION_ID\",\n credentials.guildId ? null : \"DEV_GUILD_ID\",\n ].filter((key): key is string => key !== null);\n return [\n directory === \".\" ? null : style(\"cyan\", `cd ${directory}`),\n installed ? null : style(\"cyan\", `${packageManager} install`),\n missing.length === 0 ? null : `Fill in ${missing.join(\", \")} in .env`,\n style(\"cyan\", `${run} dev`),\n ]\n .filter((line): line is string => line !== null)\n .join(\"\\n\");\n}\n\nexport const { version } = createRequire(import.meta.url)(\"../package.json\") as {\n version: string;\n};\nconst DISCORD_JS = \"^14.27.0\";\n\n/** Everything a new project needs, as file paths relative to the project root. */\nexport function templateFiles(options: ScaffoldOptions): Record<string, string> {\n const ts = options.language === \"ts\";\n const ext = ts ? \"ts\" : \"js\";\n const { token = \"\", applicationId = \"\", guildId = \"\" } = options.credentials ?? {};\n const files: Record<string, string> = {\n \"package.json\": packageJson(options),\n [`nectar.config.${ext}`]: config(),\n \".env\": `DISCORD_TOKEN=${token}\\nDISCORD_APPLICATION_ID=${applicationId}\\nDEV_GUILD_ID=${guildId}\\n`,\n \".env.example\": \"DISCORD_TOKEN=\\nDISCORD_APPLICATION_ID=\\nDEV_GUILD_ID=\\n\",\n \".gitignore\": \"node_modules/\\n.nectar/\\n.env\\n\",\n [`app/middleware.${ext}`]: middleware(ts),\n [`app/commands/ping/command.${ext}`]: pingCommand(ts),\n [`app/components/counter/[count]/button.${ext}`]: counterButton(ts),\n [`app/events/clientReady/event.${ext}`]: readyEvent(),\n };\n if (ts) files[\"tsconfig.json\"] = tsconfig();\n return files;\n}\n\n/** Writes the template into `dir`, which must not exist or be empty. Returns the written paths. */\nexport function scaffold(dir: string, options: ScaffoldOptions): string[] {\n const problem = projectProblem(dir);\n if (problem !== undefined) throw new Error(problem);\n const files = templateFiles(options);\n for (const [relative, content] of Object.entries(files)) {\n const file = path.join(dir, ...relative.split(\"/\"));\n mkdirSync(path.dirname(file), { recursive: true });\n writeFileSync(file, content);\n }\n return Object.keys(files);\n}\n\nfunction packageJson({ name, language }: ScaffoldOptions): string {\n const scripts: Record<string, string> = {\n dev: \"nectar dev\",\n build: \"nectar build\",\n start: \"nectar start\",\n check: \"nectar check\",\n sync: \"nectar sync\",\n };\n if (language === \"ts\") scripts.typecheck = \"nectar build && tsc --noEmit\";\n const pkg = {\n name,\n private: true,\n type: \"module\",\n engines: { node: \">=22.18\" },\n scripts,\n dependencies: { \"@nectar-js/nectar\": `^${version}`, \"discord.js\": DISCORD_JS },\n ...(language === \"ts\"\n ? { devDependencies: { \"@types/node\": \"^22.20.1\", typescript: \"^5.9.0\" } }\n : {}),\n };\n return `${JSON.stringify(pkg, null, 2)}\\n`;\n}\n\nfunction config(): string {\n return `import { defineConfig } from \"@nectar-js/nectar\";\n\nexport default defineConfig({\n token: process.env.DISCORD_TOKEN,\n applicationId: process.env.DISCORD_APPLICATION_ID,\n intents: [\"Guilds\"],\n dev: {\n // Commands register in your test server instantly while you develop.\n guilds: process.env.DEV_GUILD_ID ? [process.env.DEV_GUILD_ID] : [],\n },\n});\n`;\n}\n\nfunction tsconfig(): string {\n return `${JSON.stringify(\n {\n compilerOptions: {\n target: \"ES2023\",\n module: \"NodeNext\",\n moduleResolution: \"NodeNext\",\n lib: [\"ES2023\"],\n types: [\"node\"],\n strict: true,\n noEmit: true,\n allowImportingTsExtensions: true,\n skipLibCheck: true,\n verbatimModuleSyntax: true,\n },\n include: [\"app\", \"nectar.config.ts\", \".nectar/types.d.ts\"],\n },\n null,\n 2,\n )}\\n`;\n}\n\nfunction middleware(ts: boolean): string {\n return `import { defineMiddleware } from \"@nectar-js/nectar\";\n\n// Runs before every interaction. Whatever you pass to next() is on ctx downstream${ts ? \", typed\" : \"\"}.\nexport default defineMiddleware(async (_ctx, next) => {\n return next({ startedAt: Date.now() });\n});\n`;\n}\n\nfunction pingCommand(ts: boolean): string {\n const meta = ts\n ? `export const meta: CommandMeta = {`\n : `/** @type {import(\"@nectar-js/nectar\").CommandMeta} */\nexport const meta = {`;\n const imports = ts\n ? `import { type CommandMeta, customId, defineCommand } from \"@nectar-js/nectar\";`\n : `import { customId, defineCommand } from \"@nectar-js/nectar\";`;\n return `${imports}\nimport { ActionRowBuilder, ButtonBuilder, ButtonStyle } from \"discord.js\";\n\n${meta}\n description: \"Check that the bot is alive\",\n};\n\nexport default defineCommand(\"ping\", async (ctx) => {\n const button = new ButtonBuilder()\n .setCustomId(customId(\"counter/[count]\", { count: \"0\" }))\n .setLabel(\"Clicked 0 times\")\n .setStyle(ButtonStyle.Primary);\n await ctx.interaction.reply({\n content: \\`Pong in \\${Date.now() - ctx.startedAt}ms\\`,\n components: [new ActionRowBuilder${ts ? \"<ButtonBuilder>\" : \"\"}().addComponents(button)],\n });\n});\n`;\n}\n\nfunction counterButton(ts: boolean): string {\n return `import { customId, defineComponent } from \"@nectar-js/nectar\";\nimport { ActionRowBuilder, ButtonBuilder, ButtonStyle } from \"discord.js\";\n\n// The directory name [count] makes ctx.params.count a string decoded from the custom ID.\nexport default defineComponent(\"counter/[count]\", async (ctx) => {\n const count = Number(ctx.params.count) + 1;\n const button = new ButtonBuilder()\n .setCustomId(customId(\"counter/[count]\", { count: String(count) }))\n .setLabel(\\`Clicked \\${count} time\\${count === 1 ? \"\" : \"s\"}\\`)\n .setStyle(ButtonStyle.Primary);\n await ctx.interaction.update({\n components: [new ActionRowBuilder${ts ? \"<ButtonBuilder>\" : \"\"}().addComponents(button)],\n });\n});\n`;\n}\n\nfunction readyEvent(): string {\n return `import { defineEvent } from \"@nectar-js/nectar\";\n\nexport default defineEvent(\"clientReady\", async (client) => {\n console.log(\\`Logged in as \\${client.user.tag}\\`);\n});\n`;\n}\n","#!/usr/bin/env node\nimport { spawn } from \"node:child_process\";\nimport path from \"node:path\";\nimport { parseArgs, styleText } from \"node:util\";\nimport * as p from \"@clack/prompts\";\nimport {\n type Credentials,\n detectPackageManager,\n idProblem,\n type Language,\n nextSteps,\n PACKAGE_MANAGERS,\n type PackageManager,\n PORTAL_URL,\n projectProblem,\n scaffold,\n version,\n} from \"./scaffold.js\";\n\nconst DOCS_URL = \"https://nectar-js.github.io/nectar/\";\n\nconst USAGE = `Usage: npm create @nectar-js [directory] [options]\n\nOptions:\n --ts, --js Language. Asked when omitted.\n --pm <name> Package manager: npm, pnpm, yarn, or bun. Detected from the one running this.\n --install, --no-install Install dependencies. Asked when omitted.\n --git, --no-git Create a git repository. Asked when omitted.\n --yes, -y Use the defaults for anything not given, and skip the Discord details.\n --help, -h Show this help.`;\n\n/** The logo's honey behind dark text, where the terminal can show it. */\nfunction badge(text: string): string {\n if (process.stdout.hasColors?.(2 ** 24)) {\n return `\\x1b[48;2;245;165;36m\\x1b[38;2;27;20;14m${text}\\x1b[0m`;\n }\n return styleText([\"bgYellow\", \"black\"], text);\n}\n\n/** Ends the run when the person presses Ctrl+C or Escape at a prompt. */\nfunction answer<T>(value: T | symbol): T {\n if (p.isCancel(value)) {\n p.cancel(\"Nothing was created.\");\n process.exit(1);\n }\n // isCancel narrows out clack's cancel symbol, but TypeScript can't carry that to a generic T.\n return value as T;\n}\n\n/** Runs a command in `cwd`, collecting its output for when it fails. The arguments are fixed. */\nfunction run(\n command: string,\n args: string[],\n cwd: string,\n): Promise<{ ok: boolean; output: string }> {\n return new Promise((resolve) => {\n // npm, pnpm, and yarn are .cmd scripts on Windows, which Node only starts through a shell.\n // A shell takes one command string; an argument array with `shell` is deprecated.\n const child =\n process.platform === \"win32\"\n ? spawn([command, ...args].join(\" \"), { cwd, shell: true })\n : spawn(command, args, { cwd });\n let output = \"\";\n child.stdout?.on(\"data\", (chunk) => {\n output += chunk;\n });\n child.stderr?.on(\"data\", (chunk) => {\n output += chunk;\n });\n child.on(\"error\", (error) => resolve({ ok: false, output: error.message }));\n child.on(\"close\", (code) => resolve({ ok: code === 0, output }));\n });\n}\n\nasync function main(argv: string[]): Promise<number> {\n let values: Record<string, string | boolean | undefined>;\n let positionals: string[];\n try {\n ({ values, positionals } = parseArgs({\n args: argv,\n options: {\n ts: { type: \"boolean\" },\n js: { type: \"boolean\" },\n pm: { type: \"string\" },\n install: { type: \"boolean\" },\n git: { type: \"boolean\" },\n yes: { type: \"boolean\", short: \"y\" },\n help: { type: \"boolean\", short: \"h\" },\n },\n allowPositionals: true,\n allowNegative: true,\n }));\n } catch (error) {\n console.error(error instanceof Error ? error.message : String(error));\n console.error(USAGE);\n return 2;\n }\n if (values.help === true) {\n console.log(USAGE);\n return 0;\n }\n if (values.ts === true && values.js === true) {\n console.error(\"Pass --ts or --js, not both.\");\n return 2;\n }\n if (values.pm !== undefined && !PACKAGE_MANAGERS.includes(values.pm as PackageManager)) {\n console.error(`Unknown package manager \"${values.pm}\". Use ${PACKAGE_MANAGERS.join(\", \")}.`);\n return 2;\n }\n\n const interactive = values.yes !== true && process.stdin.isTTY === true;\n p.intro(`${badge(\" Nectar \")} ${styleText(\"dim\", `v${version}`)}`);\n\n const directory =\n positionals[0] ??\n (interactive\n ? answer(\n await p.text({\n message: \"Where should the project go?\",\n placeholder: \"my-bot\",\n defaultValue: \"my-bot\",\n validate: (value) => projectProblem(value || \"my-bot\"),\n }),\n )\n : \"my-bot\");\n const problem = projectProblem(directory);\n if (problem !== undefined) {\n p.cancel(problem);\n return 2;\n }\n\n const language: Language =\n values.js === true\n ? \"js\"\n : values.ts === true || !interactive\n ? \"ts\"\n : answer(\n await p.select<Language>({\n message: \"Language\",\n options: [\n { value: \"ts\", label: \"TypeScript\" },\n { value: \"js\", label: \"JavaScript\" },\n ],\n }),\n );\n\n const detected = detectPackageManager(process.env.npm_config_user_agent);\n const packageManager =\n (values.pm as PackageManager | undefined) ??\n (interactive\n ? answer(\n await p.select<PackageManager>({\n message: \"Package manager\",\n options: PACKAGE_MANAGERS.map((pm) => ({\n value: pm,\n label: pm,\n ...(pm === detected ? { hint: \"detected\" } : {}),\n })),\n initialValue: detected,\n }),\n )\n : detected);\n\n const credentials: Credentials = {};\n if (interactive) {\n p.log.message(\n `Your bot's details from ${PORTAL_URL}\\nPress Enter to skip any of them and fill them in later.`,\n );\n credentials.token = answer(await p.password({ message: \"Bot token\", mask: \"•\" }));\n credentials.applicationId = answer(\n await p.text({ message: \"Application ID\", validate: idProblem }),\n );\n credentials.guildId = answer(\n await p.text({\n message: \"Test server ID\",\n placeholder: \"Commands register here instantly while you develop\",\n validate: idProblem,\n }),\n );\n }\n\n const install =\n (values.install as boolean | undefined) ??\n (interactive\n ? answer(await p.confirm({ message: `Install dependencies with ${packageManager}?` }))\n : true);\n const git =\n (values.git as boolean | undefined) ??\n (interactive ? answer(await p.confirm({ message: \"Create a git repository?\" })) : true);\n\n const target = path.resolve(directory);\n const name = path.basename(target);\n const options = { name, language, packageManager, credentials };\n scaffold(target, options);\n p.log.success(`Created ${styleText(\"bold\", name)}`);\n\n let installed = false;\n if (install) {\n // A spinner redraws in place; in a log it would print every frame.\n const spin = process.stdout.isTTY ? p.spinner() : null;\n const message = `Installing dependencies with ${packageManager}`;\n if (spin === null) p.log.step(message);\n else spin.start(message);\n const result = await run(packageManager, [\"install\"], target);\n installed = result.ok;\n const failure = `${packageManager} install failed:\\n${result.output.trim()}`;\n if (spin === null) {\n if (result.ok) p.log.success(\"Installed dependencies\");\n else p.log.error(failure);\n } else if (result.ok) spin.stop(\"Installed dependencies\");\n else spin.error(failure);\n }\n\n if (git) {\n const result = await run(\"git\", [\"init\", \"--quiet\"], target);\n if (result.ok) p.log.success(\"Created a git repository\");\n else p.log.warn(`Couldn't create a git repository: ${result.output.trim()}`);\n }\n\n const style = (format: \"cyan\" | \"dim\", text: string) => styleText(format, text);\n p.note(nextSteps(directory, options, installed, style), \"Next steps\");\n p.outro(`Docs at ${styleText(\"underline\", DOCS_URL)}`);\n return 0;\n}\n\nprocess.exitCode = await main(process.argv.slice(2));\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nectar-js/create",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Create a new Nectar project.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -13,6 +13,9 @@
13
13
  "files": [
14
14
  "dist"
15
15
  ],
16
+ "dependencies": {
17
+ "@clack/prompts": "^1.8.0"
18
+ },
16
19
  "scripts": {
17
20
  "build": "tsdown",
18
21
  "typecheck": "tsc --noEmit"