create-sprint 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/bin/cli.js +650 -0
  2. package/bin/cli.ts +639 -221
  3. package/package.json +35 -32
package/bin/cli.js ADDED
@@ -0,0 +1,650 @@
1
+ #!/usr/bin/env node
2
+ import { select, input, confirm } from "@inquirer/prompts";
3
+ import { mkdir, writeFile } from "fs/promises";
4
+ import { existsSync } from "fs";
5
+ import { join } from "path";
6
+ import { execSync } from "child_process";
7
+ const args = process.argv.slice(2);
8
+ const hasHelp = args.includes("--help") || args.includes("-h");
9
+ const hasTs = args.includes("--ts") || args.includes("--typescript");
10
+ const hasJs = args.includes("--js") || args.includes("--javascript");
11
+ const hasName = args.indexOf("--name");
12
+ const projectNameArg = hasName !== -1 ? args[hasName + 1] : null;
13
+ const useCurrentDirArg = args.includes("--current");
14
+ const skipInstallArg = args.includes("--no-install");
15
+ const telemetryArg = args.includes("--telemetry") ? args[args.indexOf("--telemetry") + 1] : null;
16
+ const useDockerArg = args.includes("--docker");
17
+ if (hasHelp) {
18
+ console.log("\nšŸš€ Sprint - Quickly API Framework\n");
19
+ console.log("Usage: sprint [options]");
20
+ console.log("\nOptions:");
21
+ console.log(" --ts, --typescript Create TypeScript project");
22
+ console.log(" --js, --javascript Create JavaScript project");
23
+ console.log(" --name <name> Project name (use '.' for current directory)");
24
+ console.log(" --no-install Skip automatic dependency installation");
25
+ console.log(" --telemetry <type> Telemetry: none, sentry, glitchtip, discord");
26
+ console.log(" --docker Add Docker support");
27
+ console.log(" --help, -h Show this help message");
28
+ console.log("\nExamples:");
29
+ console.log(" sprint Interactive mode");
30
+ console.log(" sprint --ts Create TypeScript project");
31
+ console.log(" sprint --ts --name my-api");
32
+ console.log(" sprint --js --name . --telemetry sentry --docker\n");
33
+ process.exit(0);
34
+ }
35
+ async function main() {
36
+ console.log("\nšŸš€ Welcome to Sprint - Quickly API Framework\n");
37
+ let projectName;
38
+ let language;
39
+ if (projectNameArg) {
40
+ projectName = projectNameArg;
41
+ }
42
+ else if (useCurrentDirArg) {
43
+ projectName = ".";
44
+ }
45
+ else {
46
+ projectName = await getProjectName();
47
+ }
48
+ if (hasTs) {
49
+ language = "typescript";
50
+ }
51
+ else if (hasJs) {
52
+ language = "javascript";
53
+ }
54
+ else {
55
+ language = await selectLanguage();
56
+ }
57
+ console.log(`\nāœ… Creating Sprint project: ${projectName === "." ? "current directory" : projectName} with ${language === "typescript" ? "TypeScript" : "JavaScript"}\n`);
58
+ await createProject(projectName, language);
59
+ console.log("\nāœ… Project created successfully!");
60
+ let installDeps = true;
61
+ if (!skipInstallArg) {
62
+ installDeps = await confirm({
63
+ message: "Do you want to install dependencies now?",
64
+ default: true,
65
+ });
66
+ }
67
+ if (installDeps) {
68
+ console.log("\nšŸ“¦ Installing dependencies...\n");
69
+ const targetDir = projectName === "." ? process.cwd() : join(process.cwd(), projectName);
70
+ try {
71
+ execSync("npm install", { cwd: targetDir, stdio: "inherit" });
72
+ console.log("\nāœ… Dependencies installed successfully!");
73
+ }
74
+ catch (error) {
75
+ console.error("\nāŒ Error installing dependencies. Please run 'npm install' manually.");
76
+ }
77
+ }
78
+ console.log("\nšŸ“¦ Next steps:");
79
+ const cdCmd = projectName === "." ? "" : `cd ${projectName} && `;
80
+ if (!installDeps) {
81
+ console.log(` ${cdCmd}npm install`);
82
+ }
83
+ console.log(` ${cdCmd}npm run dev`);
84
+ console.log("\n");
85
+ }
86
+ async function getProjectName() {
87
+ const name = await input({
88
+ message: "Enter project name:",
89
+ validate: (value) => {
90
+ if (!value.trim())
91
+ return "Please enter a project name";
92
+ return true;
93
+ },
94
+ });
95
+ return name;
96
+ }
97
+ async function selectLanguage() {
98
+ const language = await select({
99
+ message: "Select your preferred language:",
100
+ choices: [
101
+ {
102
+ name: "TypeScript",
103
+ value: "typescript",
104
+ description: "Recommended - Type safety and better developer experience",
105
+ },
106
+ {
107
+ name: "JavaScript",
108
+ value: "javascript",
109
+ description: "Vanilla JavaScript for simpler projects",
110
+ },
111
+ ],
112
+ });
113
+ return language;
114
+ }
115
+ async function selectTelemetry() {
116
+ const telemetry = await select({
117
+ message: "Select error tracking/telemetry solution:",
118
+ choices: [
119
+ {
120
+ name: "None",
121
+ value: "none",
122
+ description: "No error tracking integration",
123
+ },
124
+ {
125
+ name: "Sentry",
126
+ value: "sentry",
127
+ description: "Full-featured error tracking (free tier available)",
128
+ },
129
+ {
130
+ name: "GlitchTip",
131
+ value: "glitchtip",
132
+ description: "Simple error tracking, can be self-hosted",
133
+ },
134
+ {
135
+ name: "Discord Webhook",
136
+ value: "discord",
137
+ description: "Send error notifications to Discord channel",
138
+ },
139
+ ],
140
+ });
141
+ return telemetry;
142
+ }
143
+ async function createProject(projectName, language) {
144
+ const isCurrentDir = projectName === ".";
145
+ const targetDir = isCurrentDir ? process.cwd() : join(process.cwd(), projectName);
146
+ if (!isCurrentDir && existsSync(targetDir)) {
147
+ console.error(`Error: Directory ${projectName} already exists`);
148
+ process.exit(1);
149
+ }
150
+ if (!isCurrentDir) {
151
+ await mkdir(targetDir, { recursive: true });
152
+ }
153
+ let telemetry = "none";
154
+ if (telemetryArg && ["sentry", "glitchtip", "discord", "none"].includes(telemetryArg)) {
155
+ telemetry = telemetryArg;
156
+ }
157
+ else if (!hasTs && !hasJs) {
158
+ telemetry = await selectTelemetry();
159
+ }
160
+ let useDocker = useDockerArg;
161
+ if (!useDocker && !hasTs && !hasJs) {
162
+ useDocker = await confirm({
163
+ message: "Do you want to add Docker support?",
164
+ default: false,
165
+ });
166
+ }
167
+ let pkgJson;
168
+ if (language === "typescript") {
169
+ pkgJson = getTypeScriptPackageJson(projectName, telemetry);
170
+ }
171
+ else {
172
+ pkgJson = getJavaScriptPackageJson(projectName, telemetry);
173
+ }
174
+ await writeFile(join(targetDir, "package.json"), JSON.stringify(pkgJson, null, 2));
175
+ if (language === "typescript") {
176
+ await writeFile(join(targetDir, "tsconfig.json"), getTsConfig());
177
+ await writeFile(join(targetDir, "vite.config.ts"), getViteConfig());
178
+ }
179
+ const srcDir = join(targetDir, "src");
180
+ await mkdir(srcDir, { recursive: true });
181
+ await mkdir(join(srcDir, "middlewares"), { recursive: true });
182
+ await mkdir(join(srcDir, "routes"), { recursive: true });
183
+ await mkdir(join(srcDir, "controllers"), { recursive: true });
184
+ await writeFile(join(srcDir, "middlewares", ".gitkeep"), "");
185
+ await writeFile(join(srcDir, "controllers", ".gitkeep"), "");
186
+ await writeFile(join(srcDir, "index." + (language === "typescript" ? "ts" : "js")), getMainFile(language, telemetry));
187
+ await writeFile(join(srcDir, "routes", "home." + (language === "typescript" ? "ts" : "js")), getHomeRoute(language));
188
+ if (language === "typescript") {
189
+ await writeFile(join(srcDir, "app." + (language === "typescript" ? "ts" : "js")), getAppFile(language, telemetry));
190
+ }
191
+ await writeFile(join(targetDir, ".env.example"), getEnvExample(telemetry));
192
+ await writeFile(join(targetDir, ".env"), "");
193
+ if (telemetry !== "none") {
194
+ await writeFile(join(targetDir, "sprint.config.js"), getSprintConfig(telemetry));
195
+ }
196
+ await writeFile(join(targetDir, ".gitignore"), getGitignore(language));
197
+ if (useDocker) {
198
+ await writeFile(join(targetDir, "Dockerfile"), getDockerfile(language));
199
+ await writeFile(join(targetDir, "docker-compose.yml"), getDockerCompose(language));
200
+ await writeFile(join(targetDir, ".dockerignore"), getDockerIgnore());
201
+ }
202
+ }
203
+ function getTypeScriptPackageJson(name, telemetry) {
204
+ const deps = {
205
+ "sprint-es": "^0.0.24",
206
+ dotenv: "^17.0.0",
207
+ };
208
+ const devDeps = {
209
+ "@types/node": "^22.0.0",
210
+ "tsx": "^4.19.0",
211
+ typescript: "^5.6.0",
212
+ vite: "^6.0.0",
213
+ };
214
+ if (telemetry === "sentry") {
215
+ deps["@sentry/node"] = "^8.0.0";
216
+ }
217
+ else if (telemetry === "glitchtip") {
218
+ deps["@sentry/node"] = "^8.0.0";
219
+ }
220
+ else if (telemetry === "discord") {
221
+ deps["axios"] = "^1.6.0";
222
+ }
223
+ return {
224
+ name: name === "." ? "sprint-app" : name,
225
+ version: "0.0.1",
226
+ description: "Sprint API",
227
+ main: "dist/index.js",
228
+ scripts: {
229
+ build: "vite build",
230
+ start: "node dist/index.js --prod",
231
+ dev: "vite --dev",
232
+ },
233
+ dependencies: deps,
234
+ devDependencies: devDeps,
235
+ };
236
+ }
237
+ function getJavaScriptPackageJson(name, telemetry) {
238
+ const deps = {
239
+ "sprint-es": "^0.0.24",
240
+ dotenv: "^17.0.0",
241
+ };
242
+ if (telemetry === "sentry") {
243
+ deps["@sentry/node"] = "^8.0.0";
244
+ }
245
+ else if (telemetry === "glitchtip") {
246
+ deps["@sentry/node"] = "^8.0.0";
247
+ }
248
+ else if (telemetry === "discord") {
249
+ deps["axios"] = "^1.6.0";
250
+ }
251
+ return {
252
+ name: name === "." ? "sprint-app" : name,
253
+ version: "0.0.1",
254
+ description: "Sprint API",
255
+ main: "src/index.js",
256
+ type: "module",
257
+ scripts: {
258
+ start: "node src/index.js --prod",
259
+ dev: "node --watch src/index.js --dev",
260
+ },
261
+ dependencies: deps,
262
+ };
263
+ }
264
+ function getTsConfig() {
265
+ return JSON.stringify({
266
+ compilerOptions: {
267
+ target: "ES2020",
268
+ module: "ESNext",
269
+ moduleResolution: "bundler",
270
+ lib: ["ES2020"],
271
+ outDir: "./dist",
272
+ rootDir: "./src",
273
+ strict: true,
274
+ esModuleInterop: true,
275
+ skipLibCheck: true,
276
+ forceConsistentCasingInFileNames: true,
277
+ resolveJsonModule: true,
278
+ declaration: true,
279
+ declarationMap: true,
280
+ sourceMap: true,
281
+ tabWidth: 4,
282
+ },
283
+ include: ["src/**/*"],
284
+ exclude: ["node_modules", "dist"],
285
+ }, null, 2);
286
+ }
287
+ function getViteConfig() {
288
+ return `import { defineConfig } from "vite";
289
+ import { resolve } from "path";
290
+
291
+ export default defineConfig({
292
+ build: {
293
+ lib: {
294
+ entry: resolve(__dirname, "src/index.ts"),
295
+ formats: ["es"],
296
+ fileName: "index",
297
+ },
298
+ outDir: "dist",
299
+ rollupOptions: {
300
+ external: ["sprint-es", "express", "cors", "morgan", "serve-favicon", "dotenv"],
301
+ },
302
+ target: "ES2020",
303
+ },
304
+ resolve: {
305
+ alias: {
306
+ "@": resolve(__dirname, "src"),
307
+ },
308
+ },
309
+ });
310
+ `;
311
+ }
312
+ function getMainFile(language, telemetry) {
313
+ const hasTelemetry = telemetry !== "none";
314
+ if (language === "typescript") {
315
+ let code = `import Sprint from "sprint-es";
316
+ import dotenv from "dotenv";
317
+ import homeRouter from "./routes/home";
318
+
319
+ dotenv.config();
320
+
321
+ `;
322
+ if (hasTelemetry) {
323
+ code += getTelemetryImport(language, telemetry);
324
+ }
325
+ code += `const app = new Sprint({
326
+ port: process.env.PORT ? parseInt(process.env.PORT) : 3000,
327
+ favicon: process.env.FAVICON || undefined,
328
+ bodyParser: {
329
+ limit: "10mb"
330
+ }
331
+ });
332
+
333
+ app.use(homeRouter);
334
+
335
+ app.listen();
336
+ `;
337
+ return code;
338
+ }
339
+ let code = `import Sprint from "sprint-es";
340
+ import dotenv from "dotenv";
341
+ import homeRouter from "./routes/home";
342
+
343
+ dotenv.config();
344
+
345
+ `;
346
+ if (hasTelemetry) {
347
+ code += getTelemetryImport(language, telemetry);
348
+ }
349
+ code += `const app = new Sprint({
350
+ port: process.env.PORT ? parseInt(process.env.PORT) : 3000,
351
+ favicon: process.env.FAVICON || undefined,
352
+ bodyParser: {
353
+ limit: "10mb"
354
+ }
355
+ });
356
+
357
+ app.use(homeRouter);
358
+
359
+ app.listen();
360
+ `;
361
+ return code;
362
+ }
363
+ function getTelemetryImport(language, telemetry) {
364
+ if (telemetry === "sentry" || telemetry === "glitchtip") {
365
+ return `import * as Sentry from "@sentry/node";
366
+
367
+ Sentry.init({
368
+ dsn: process.env.SENTRY_DSN,
369
+ integrations: [
370
+ Sentry.httpIntegration(),
371
+ ],
372
+ });
373
+
374
+ `;
375
+ }
376
+ else if (telemetry === "discord") {
377
+ return `import axios from "axios";
378
+
379
+ const discordWebhook = process.env.DISCORD_WEBHOOK_URL;
380
+
381
+ async function sendDiscordError(error, req) {
382
+ if (!discordWebhook) return;
383
+
384
+ const embed = {
385
+ title: "🚨 Error in Sprint API",
386
+ description: \`\${error.message}\`,
387
+ color: 16711680,
388
+ fields: [
389
+ {
390
+ name: "Method",
391
+ value: req.method,
392
+ inline: true,
393
+ },
394
+ {
395
+ name: "URL",
396
+ value: req.url,
397
+ inline: true,
398
+ },
399
+ {
400
+ name: "Stack",
401
+ value: error.stack?.split("\\n").slice(0, 5).join("\\n") || "No stack trace",
402
+ },
403
+ ],
404
+ timestamp: new Date().toISOString(),
405
+ };
406
+
407
+ try {
408
+ await axios.post(discordWebhook, {
409
+ embeds: [embed],
410
+ });
411
+ } catch (e) {
412
+ console.error("Failed to send Discord webhook:", e.message);
413
+ }
414
+ }
415
+
416
+ `;
417
+ }
418
+ return "";
419
+ }
420
+ function getAppFile(language, telemetry) {
421
+ if (language === "typescript") {
422
+ return `import type { SprintOptions } from "sprint-es";
423
+
424
+ export const options: SprintOptions = {
425
+ port: 3000,
426
+ bodyParser: {
427
+ limit: "10mb"
428
+ }
429
+ };
430
+ `;
431
+ }
432
+ return "";
433
+ }
434
+ function getEnvExample(telemetry) {
435
+ let env = `PORT=3000
436
+ FAVICON=./public/favicon.ico
437
+ NODE_ENV=development
438
+ `;
439
+ if (telemetry === "sentry" || telemetry === "glitchtip") {
440
+ env += `
441
+ # Sentry / GlitchTip (use GlitchTip DSN for self-hosted)
442
+ SENTRY_DSN=
443
+ `;
444
+ }
445
+ else if (telemetry === "discord") {
446
+ env += `
447
+ # Discord Webhook URL for error notifications
448
+ DISCORD_WEBHOOK_URL=
449
+ `;
450
+ }
451
+ return env;
452
+ }
453
+ function getSprintConfig(telemetry) {
454
+ if (telemetry === "discord") {
455
+ return `export default {
456
+ errorHandler: async (error, req, res) => {
457
+ console.error(error);
458
+
459
+ if (process.env.DISCORD_WEBHOOK_URL && res.socket.server) {
460
+ const axios = await import("axios");
461
+ const embed = {
462
+ title: "🚨 Error in Sprint API",
463
+ description: error.message,
464
+ color: 16711680,
465
+ fields: [
466
+ {
467
+ name: "Method",
468
+ value: req.method,
469
+ inline: true,
470
+ },
471
+ {
472
+ name: "URL",
473
+ value: req.url,
474
+ inline: true,
475
+ },
476
+ {
477
+ name: "Stack",
478
+ value: error.stack?.split("\\n").slice(0, 5).join("\\n") || "No stack",
479
+ },
480
+ ],
481
+ timestamp: new Date().toISOString(),
482
+ };
483
+
484
+ try {
485
+ await axios.default.post(process.env.DISCORD_WEBHOOK_URL, {
486
+ embeds: [embed],
487
+ });
488
+ } catch (e) {
489
+ console.error("Discord webhook error:", e.message);
490
+ }
491
+ }
492
+
493
+ res.status(500).json({ error: "Internal server error" });
494
+ }
495
+ };
496
+ `;
497
+ }
498
+ return "";
499
+ }
500
+ function getHomeRoute(language) {
501
+ if (language === "typescript") {
502
+ return `import { Router } from "sprint-es";
503
+
504
+ const router = Router();
505
+
506
+ router.get("/", (req, res) => {
507
+ res.json({
508
+ message: "Hello World",
509
+ status: "ok"
510
+ });
511
+ });
512
+
513
+ export default router;
514
+ `;
515
+ }
516
+ return `import { Router } from "sprint-es";
517
+
518
+ const router = Router();
519
+
520
+ router.get("/", (req, res) => {
521
+ res.json({
522
+ message: "Hello World",
523
+ status: "ok"
524
+ });
525
+ });
526
+
527
+ export default router;
528
+ `;
529
+ }
530
+ function getDockerfile(language) {
531
+ if (language === "typescript") {
532
+ return `FROM node:20-alpine
533
+
534
+ WORKDIR /app
535
+
536
+ COPY package*.json ./
537
+
538
+ RUN npm install
539
+
540
+ COPY . .
541
+
542
+ RUN npm run build
543
+
544
+ EXPOSE 3000
545
+
546
+ CMD ["npm", "start"]
547
+ `;
548
+ }
549
+ return `FROM node:20-alpine
550
+
551
+ WORKDIR /app
552
+
553
+ COPY package*.json ./
554
+
555
+ RUN npm install
556
+
557
+ COPY . .
558
+
559
+ EXPOSE 3000
560
+
561
+ CMD ["npm", "start"]
562
+ `;
563
+ }
564
+ function getDockerCompose(language) {
565
+ if (language === "typescript") {
566
+ return `version: "3.8"
567
+
568
+ services:
569
+ app:
570
+ build: .
571
+ ports:
572
+ - "3000:3000"
573
+ environment:
574
+ - NODE_ENV=production
575
+ - PORT=3000
576
+ restart: unless-stopped
577
+ `;
578
+ }
579
+ return `version: "3.8"
580
+
581
+ services:
582
+ app:
583
+ build: .
584
+ ports:
585
+ - "3000:3000"
586
+ environment:
587
+ - NODE_ENV=production
588
+ - PORT=3000
589
+ restart: unless-stopped
590
+ `;
591
+ }
592
+ function getGitignore(language) {
593
+ return `# Dependencies
594
+ node_modules/
595
+ npm-debug.log*
596
+ yarn-debug.log*
597
+ yarn-error.log*
598
+
599
+ # Build
600
+ dist/
601
+ build/
602
+ *.tsbuildinfo
603
+
604
+ # Environment
605
+ .env
606
+ .env.local
607
+ .env.*.local
608
+
609
+ # IDE
610
+ .vscode/
611
+ .idea/
612
+ *.swp
613
+ *.swo
614
+ *~
615
+
616
+ # OS
617
+ .DS_Store
618
+ Thumbs.db
619
+
620
+ # Logs
621
+ logs/
622
+ *.log
623
+
624
+ # Test
625
+ coverage/
626
+
627
+ # Temporary
628
+ tmp/
629
+ temp/
630
+ `;
631
+ }
632
+ function getDockerIgnore() {
633
+ return `node_modules
634
+ npm-debug.log
635
+ .env
636
+ .env.local
637
+ .git
638
+ .gitignore
639
+ README.md
640
+ dist
641
+ build
642
+ coverage
643
+ .vscode
644
+ .idea
645
+ *.log
646
+ tmp
647
+ temp
648
+ `;
649
+ }
650
+ main().catch(console.error);