create-sprint 0.0.54 → 0.0.58

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.
@@ -9,7 +9,7 @@ export function generateJWTKeys() {
9
9
  }
10
10
  export function getTypeScriptPackageJson(name, telemetry) {
11
11
  const deps = {
12
- "sprint-es": "^0.0.38"
12
+ "sprint-es": "^0.0.48"
13
13
  };
14
14
  const devDeps = {
15
15
  "@types/node": "^22.0.0",
@@ -39,7 +39,7 @@ export function getTypeScriptPackageJson(name, telemetry) {
39
39
  }
40
40
  export function getJavaScriptPackageJson(name, telemetry) {
41
41
  const deps = {
42
- "sprint-es": "^0.0.38"
42
+ "sprint-es": "^0.0.48"
43
43
  };
44
44
  if (telemetry === "sentry" || telemetry === "glitchtip") {
45
45
  deps["@sentry/node"] = "^8.0.0";
@@ -135,7 +135,7 @@ import { homeController, jwtValidateController } from "@/controllers/home";
135
135
  const router = Router();
136
136
 
137
137
  router.get("/", homeSchema, homeController);
138
- router.post("/jwt/validate", jwtValidateController);
138
+ router.post("/me", jwtValidateController);
139
139
 
140
140
  export default router;
141
141
  `;
@@ -147,7 +147,7 @@ import { homeController, jwtValidateController } from "../controllers/home.js";
147
147
  const router = Router();
148
148
 
149
149
  router.get("/", homeSchema, homeController);
150
- router.post("/jwt/validate", jwtValidateController);
150
+ router.post("/me", jwtValidateController);
151
151
 
152
152
  export default router;
153
153
  `;
@@ -193,24 +193,7 @@ export const homeController: Handler = (req: SprintRequest, res: SprintResponse)
193
193
  };
194
194
 
195
195
  export const jwtValidateController: Handler = (req: SprintRequest, res: SprintResponse) => {
196
- const token = req.sprint?.getAuthorization() || req.headers.authorization;
197
-
198
- if (!token) {
199
- return res.status(401).json({ error: "No token provided" });
200
- }
201
-
202
- try {
203
- const { publicKey, encryptionSecret } = getJwtFromEnv();
204
- const decoded = verifyEncrypted(token, publicKey, encryptionSecret);
205
-
206
- if (!decoded) {
207
- return res.status(401).json({ error: "Invalid token" });
208
- }
209
-
210
- res.json({ valid: true, payload: decoded });
211
- } catch (error) {
212
- return res.status(500).json({ error: "JWT not configured" });
213
- }
196
+ return res.json(req.custom.user);
214
197
  };
215
198
  `;
216
199
  }
@@ -225,24 +208,7 @@ export const homeController = (req: SprintRequest, res: SprintResponse) => {
225
208
  };
226
209
 
227
210
  export const jwtValidateController = (req: SprintRequest, res: SprintResponse) => {
228
- const token = req.sprint?.getAuthorization() || req.headers.authorization;
229
-
230
- if (!token) {
231
- return res.status(401).json({ error: "No token provided" });
232
- }
233
-
234
- try {
235
- const { publicKey, encryptionSecret } = getJwtFromEnv();
236
- const decoded = verifyEncrypted(token, publicKey, encryptionSecret);
237
-
238
- if (!decoded) {
239
- return res.status(401).json({ error: "Invalid token" });
240
- }
241
-
242
- res.json({ valid: true, payload: decoded });
243
- } catch (error) {
244
- return res.status(500).json({ error: "JWT not configured" });
245
- }
211
+ return res.json(req.custom.user);
246
212
  };
247
213
  `;
248
214
  }
@@ -271,7 +237,7 @@ export const jwtGenerateController: Handler = (req: SprintRequest, res: SprintRe
271
237
  const { userId, role } = req.body || {};
272
238
 
273
239
  try {
274
- const { privateKey } = getJwtFromEnv();
240
+ const { privateKey, encryptionSecret } = getJwtFromEnv();
275
241
  const payload = { userId, role: role || "user" };
276
242
  const token = signEncrypted(payload, privateKey, encryptionSecret, { expiresIn: "1h" });
277
243
  res.json({ token });
@@ -281,7 +247,8 @@ export const jwtGenerateController: Handler = (req: SprintRequest, res: SprintRe
281
247
  };
282
248
  `;
283
249
  }
284
- return `import { Handler, SprintRequest, SprintResponse } from "sprint-es";
250
+ else {
251
+ return `import { Handler, SprintRequest, SprintResponse } from "sprint-es";
285
252
  import { signEncrypted, getJwtFromEnv } from "sprint-es/jwt";
286
253
 
287
254
  export const adminController = (req: SprintRequest, res: SprintResponse) => {
@@ -313,6 +280,7 @@ export const jwtGenerateController = (req: SprintRequest, res: SprintResponse) =
313
280
  }
314
281
  };
315
282
  `;
283
+ }
316
284
  }
317
285
  export function getHomeSchema(language) {
318
286
  if (language === "typescript") {
@@ -370,13 +338,13 @@ export const jwtGenerateSchema = defineRouteSchema({
370
338
  }
371
339
  export function getInternalAuthMiddleware(language) {
372
340
  if (language === "typescript") {
373
- return `import { defineMiddleware } from "sprint-es";
341
+ return `import { defineMiddleware, SprintRequest, SprintResponse, NextFunction } from "sprint-es";
374
342
 
375
343
  export default defineMiddleware({
376
344
  name: "adminAuth",
377
345
  priority: 10,
378
346
  include: "/admin/**",
379
- handler: (req, res, next) => {
347
+ handler: (req: SprintRequest, res: SprintResponse, next: NextFunction) => {
380
348
  const auth = req.sprint.getAuthorization();
381
349
  if (!auth) return res.status(401).json({ error: "No authorization header" });
382
350
 
@@ -410,7 +378,7 @@ export default defineMiddleware({
410
378
  }
411
379
  export function getUserAuthMiddleware(language) {
412
380
  if (language === "typescript") {
413
- return `import { defineMiddleware } from "sprint-es";
381
+ return `import { defineMiddleware, SprintRequest, SprintResponse, NextFunction } from "sprint-es";
414
382
  import { verifyEncrypted, getJwtFromEnv } from "sprint-es/jwt";
415
383
 
416
384
  const { publicKey, encryptionSecret } = getJwtFromEnv();
@@ -420,13 +388,13 @@ export default defineMiddleware({
420
388
  priority: 10,
421
389
  include: "/**",
422
390
  exclude: "/admin/**",
423
- handler: (req, res, next) => {
391
+ handler: (req: SprintRequest, res: SprintResponse, next: NextFunction) => {
424
392
  const auth = req.sprint.getAuthorization();
425
393
  if (!auth) return res.status(401).json({ error: "No authorization header" });
426
394
 
427
395
  const token = auth.replace("Bearer ", "");
428
396
 
429
- const decoded = verifyEncrypted(token, publicKey, encryptionSecret);cd .
397
+ const decoded = verifyEncrypted(token, publicKey, encryptionSecret);
430
398
 
431
399
  if (!decoded) return res.status(403).json({ error: "Invalid token" });
432
400
 
@@ -499,8 +467,7 @@ CMD ["npm", "start"]
499
467
  `;
500
468
  }
501
469
  export function getDockerCompose(language) {
502
- return `version: "3.8"
503
-
470
+ return `
504
471
  services:
505
472
  app:
506
473
  build: .
@@ -508,7 +475,7 @@ services:
508
475
  - "3000:3000"
509
476
  environment:
510
477
  - NODE_ENV=production
511
- - PORT=3000
478
+ - PORT=5000
512
479
  restart: unless-stopped
513
480
  `;
514
481
  }
@@ -600,7 +567,7 @@ initTelemetry({
600
567
 
601
568
  initTelemetry({
602
569
  provider: "discord",
603
- webhookUrl: process.env.DISCORD_WEBHOOK_URL || ""
570
+ webhookUrl: process.env.DISCORD_TELEMETRY_WEBHOOK_URL || ""
604
571
  });
605
572
  `;
606
573
  }
@@ -627,7 +594,7 @@ import { initTelemetry } from "sprint-es/telemetry";
627
594
 
628
595
  initTelemetry({
629
596
  provider: "discord",
630
- webhookUrl: process.env.DISCORD_WEBHOOK_URL || ""
597
+ webhookUrl: process.env.DISCORD_TELEMETRY_WEBHOOK_URL || ""
631
598
  });
632
599
  `;
633
600
  }
@@ -648,7 +615,7 @@ SENTRY_DSN=
648
615
  else if (telemetry === "discord") {
649
616
  env += `
650
617
  # Discord Webhook URL for error notifications
651
- DISCORD_WEBHOOK_URL=
618
+ DISCORD_TELEMETRY_WEBHOOK_URL=
652
619
  `;
653
620
  }
654
621
  return env;
@@ -673,7 +640,7 @@ SENTRY_DSN=
673
640
  else if (telemetry === "discord") {
674
641
  env += `
675
642
  # Discord Webhook URL
676
- DISCORD_WEBHOOK_URL=
643
+ DISCORD_TELEMETRY_WEBHOOK_URL=
677
644
  `;
678
645
  }
679
646
  return env;
@@ -695,7 +662,7 @@ SENTRY_DSN=
695
662
  else if (telemetry === "discord") {
696
663
  env += `
697
664
  # Discord Webhook URL
698
- DISCORD_WEBHOOK_URL=
665
+ DISCORD_TELEMETRY_WEBHOOK_URL=
699
666
  `;
700
667
  }
701
668
  return env;
package/dist/index.js CHANGED
@@ -16,50 +16,67 @@ export async function runCLI(args) {
16
16
  const options = parseArgs(args);
17
17
  p.intro("Sprint — Quickly API Framework");
18
18
  p.intro(`${color.bgCyan(color.black(' create-sprint-app '))}`);
19
- const config = await p.group({
20
- projectName: () => p.text({
21
- message: "Project name:",
22
- placeholder: "my-api",
23
- validate: (v) => validateProjectName(v) || undefined,
24
- }),
25
- language: () => p.select({
26
- message: "Language:",
27
- options: [
28
- { value: "typescript", label: "TypeScript", hint: "recommended" },
29
- { value: "javascript", label: "JavaScript" },
30
- ],
31
- }),
32
- telemetry: () => p.select({
33
- message: "Error tracking:",
34
- options: [
35
- { value: "none", label: "None" },
36
- { value: "sentry", label: "Sentry", hint: "free tier available" },
37
- { value: "glitchtip", label: "GlitchTip", hint: "self-hostable" },
38
- { value: "discord", label: "Discord Webhook", hint: "sends to a channel" },
39
- ],
40
- }),
41
- docker: () => p.confirm({ message: "Add Docker support?", initialValue: false }),
42
- }, {
43
- onCancel: () => {
44
- p.cancel("Cancelled.");
45
- process.exit(0);
46
- },
47
- });
19
+ let config;
20
+ if (options.skipPrompts) {
21
+ config = {
22
+ projectName: options.projectName || "sprint-app",
23
+ language: options.language || "typescript",
24
+ telemetry: options.telemetry || "none",
25
+ docker: options.docker || false,
26
+ };
27
+ }
28
+ else {
29
+ config = await p.group({
30
+ projectName: () => p.text({
31
+ message: "Project name:",
32
+ placeholder: "my-api",
33
+ validate: (v) => validateProjectName(v) || undefined,
34
+ }),
35
+ language: () => p.select({
36
+ message: "Language:",
37
+ options: [
38
+ { value: "typescript", label: "TypeScript", hint: "recommended" },
39
+ { value: "javascript", label: "JavaScript" },
40
+ ],
41
+ }),
42
+ telemetry: () => p.select({
43
+ message: "Error tracking:",
44
+ options: [
45
+ { value: "none", label: "None" },
46
+ { value: "sentry", label: "Sentry", hint: "free tier available" },
47
+ { value: "glitchtip", label: "GlitchTip", hint: "self-hostable" },
48
+ { value: "discord", label: "Discord Webhook", hint: "sends to a channel" },
49
+ ],
50
+ }),
51
+ docker: () => p.confirm({ message: "Add Docker support?", initialValue: false }),
52
+ }, {
53
+ onCancel: () => {
54
+ p.cancel("Cancelled.");
55
+ process.exit(0);
56
+ },
57
+ });
58
+ }
48
59
  const targetDir = config.projectName === "." ? process.cwd() : join(process.cwd(), config.projectName);
49
60
  const s = p.spinner();
50
61
  s.start("Creating project");
51
62
  await createProject(config.projectName, config.language, config.telemetry, config.docker);
52
63
  s.stop("Project created");
53
- const installDeps = await p.confirm({ message: "Install dependencies now?", initialValue: true });
54
- const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
64
+ let installDeps = true;
65
+ if (options.skipInstall) {
66
+ installDeps = false;
67
+ }
68
+ else if (!options.skipPrompts) {
69
+ installDeps = await p.confirm({ message: "Install dependencies now?", initialValue: true });
70
+ }
55
71
  if (installDeps) {
56
72
  const s2 = p.spinner();
57
73
  s2.start("Installing dependencies");
58
74
  try {
59
75
  await new Promise((resolve, reject) => {
60
- const child = spawn(npmCmd, ["install"], {
76
+ const child = spawn("npm", ["install"], {
61
77
  cwd: targetDir,
62
- stdio: "pipe"
78
+ stdio: "inherit",
79
+ shell: true
63
80
  });
64
81
  child.on("close", (code) => {
65
82
  if (code === 0)
@@ -67,12 +84,16 @@ export async function runCLI(args) {
67
84
  else
68
85
  reject(new Error(`npm install exited with code ${code}`));
69
86
  });
70
- child.on("error", reject);
87
+ child.on("error", (err) => {
88
+ p.cancel(`Failed to run npm install: ${err.message}`);
89
+ reject(err);
90
+ });
71
91
  });
72
92
  s2.stop("Dependencies installed");
73
93
  }
74
- catch {
94
+ catch (err) {
75
95
  s2.stop("Install failed — run npm install manually");
96
+ console.error(err);
76
97
  }
77
98
  }
78
99
  const cdCmd = config.projectName === "." ? "" : `cd ${config.projectName} && `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-sprint",
3
- "version": "0.0.54",
3
+ "version": "0.0.58",
4
4
  "description": "Create a new Sprint API project",
5
5
  "type": "module",
6
6
  "bin": {
package/src/generators.ts CHANGED
@@ -16,7 +16,7 @@ export function generateJWTKeys(): JWTKeys {
16
16
 
17
17
  export function getTypeScriptPackageJson(name: string, telemetry: string) {
18
18
  const deps: Record<string, string> = {
19
- "sprint-es": "^0.0.38"
19
+ "sprint-es": "^0.0.48"
20
20
  };
21
21
 
22
22
  const devDeps: Record<string, string> = {
@@ -49,7 +49,7 @@ export function getTypeScriptPackageJson(name: string, telemetry: string) {
49
49
 
50
50
  export function getJavaScriptPackageJson(name: string, telemetry: string) {
51
51
  const deps: Record<string, string> = {
52
- "sprint-es": "^0.0.38"
52
+ "sprint-es": "^0.0.48"
53
53
  };
54
54
 
55
55
  if (telemetry === "sentry" || telemetry === "glitchtip") {
@@ -151,7 +151,7 @@ import { homeController, jwtValidateController } from "@/controllers/home";
151
151
  const router = Router();
152
152
 
153
153
  router.get("/", homeSchema, homeController);
154
- router.post("/jwt/validate", jwtValidateController);
154
+ router.post("/me", jwtValidateController);
155
155
 
156
156
  export default router;
157
157
  `;
@@ -163,7 +163,7 @@ import { homeController, jwtValidateController } from "../controllers/home.js";
163
163
  const router = Router();
164
164
 
165
165
  router.get("/", homeSchema, homeController);
166
- router.post("/jwt/validate", jwtValidateController);
166
+ router.post("/me", jwtValidateController);
167
167
 
168
168
  export default router;
169
169
  `;
@@ -211,24 +211,7 @@ export const homeController: Handler = (req: SprintRequest, res: SprintResponse)
211
211
  };
212
212
 
213
213
  export const jwtValidateController: Handler = (req: SprintRequest, res: SprintResponse) => {
214
- const token = req.sprint?.getAuthorization() || req.headers.authorization;
215
-
216
- if (!token) {
217
- return res.status(401).json({ error: "No token provided" });
218
- }
219
-
220
- try {
221
- const { publicKey, encryptionSecret } = getJwtFromEnv();
222
- const decoded = verifyEncrypted(token, publicKey, encryptionSecret);
223
-
224
- if (!decoded) {
225
- return res.status(401).json({ error: "Invalid token" });
226
- }
227
-
228
- res.json({ valid: true, payload: decoded });
229
- } catch (error) {
230
- return res.status(500).json({ error: "JWT not configured" });
231
- }
214
+ return res.json(req.custom.user);
232
215
  };
233
216
  `;
234
217
  }
@@ -243,24 +226,7 @@ export const homeController = (req: SprintRequest, res: SprintResponse) => {
243
226
  };
244
227
 
245
228
  export const jwtValidateController = (req: SprintRequest, res: SprintResponse) => {
246
- const token = req.sprint?.getAuthorization() || req.headers.authorization;
247
-
248
- if (!token) {
249
- return res.status(401).json({ error: "No token provided" });
250
- }
251
-
252
- try {
253
- const { publicKey, encryptionSecret } = getJwtFromEnv();
254
- const decoded = verifyEncrypted(token, publicKey, encryptionSecret);
255
-
256
- if (!decoded) {
257
- return res.status(401).json({ error: "Invalid token" });
258
- }
259
-
260
- res.json({ valid: true, payload: decoded });
261
- } catch (error) {
262
- return res.status(500).json({ error: "JWT not configured" });
263
- }
229
+ return res.json(req.custom.user);
264
230
  };
265
231
  `;
266
232
  }
@@ -290,7 +256,7 @@ export const jwtGenerateController: Handler = (req: SprintRequest, res: SprintRe
290
256
  const { userId, role } = req.body || {};
291
257
 
292
258
  try {
293
- const { privateKey } = getJwtFromEnv();
259
+ const { privateKey, encryptionSecret } = getJwtFromEnv();
294
260
  const payload = { userId, role: role || "user" };
295
261
  const token = signEncrypted(payload, privateKey, encryptionSecret, { expiresIn: "1h" });
296
262
  res.json({ token });
@@ -299,8 +265,8 @@ export const jwtGenerateController: Handler = (req: SprintRequest, res: SprintRe
299
265
  }
300
266
  };
301
267
  `;
302
- }
303
- return `import { Handler, SprintRequest, SprintResponse } from "sprint-es";
268
+ } else {
269
+ return `import { Handler, SprintRequest, SprintResponse } from "sprint-es";
304
270
  import { signEncrypted, getJwtFromEnv } from "sprint-es/jwt";
305
271
 
306
272
  export const adminController = (req: SprintRequest, res: SprintResponse) => {
@@ -332,7 +298,7 @@ export const jwtGenerateController = (req: SprintRequest, res: SprintResponse) =
332
298
  }
333
299
  };
334
300
  `;
335
-
301
+ }
336
302
  }
337
303
 
338
304
  export function getHomeSchema(language: string) {
@@ -393,13 +359,13 @@ export const jwtGenerateSchema = defineRouteSchema({
393
359
 
394
360
  export function getInternalAuthMiddleware(language: string) {
395
361
  if (language === "typescript") {
396
- return `import { defineMiddleware } from "sprint-es";
362
+ return `import { defineMiddleware, SprintRequest, SprintResponse, NextFunction } from "sprint-es";
397
363
 
398
364
  export default defineMiddleware({
399
365
  name: "adminAuth",
400
366
  priority: 10,
401
367
  include: "/admin/**",
402
- handler: (req, res, next) => {
368
+ handler: (req: SprintRequest, res: SprintResponse, next: NextFunction) => {
403
369
  const auth = req.sprint.getAuthorization();
404
370
  if (!auth) return res.status(401).json({ error: "No authorization header" });
405
371
 
@@ -434,7 +400,7 @@ export default defineMiddleware({
434
400
 
435
401
  export function getUserAuthMiddleware(language: string) {
436
402
  if (language === "typescript") {
437
- return `import { defineMiddleware } from "sprint-es";
403
+ return `import { defineMiddleware, SprintRequest, SprintResponse, NextFunction } from "sprint-es";
438
404
  import { verifyEncrypted, getJwtFromEnv } from "sprint-es/jwt";
439
405
 
440
406
  const { publicKey, encryptionSecret } = getJwtFromEnv();
@@ -444,13 +410,13 @@ export default defineMiddleware({
444
410
  priority: 10,
445
411
  include: "/**",
446
412
  exclude: "/admin/**",
447
- handler: (req, res, next) => {
413
+ handler: (req: SprintRequest, res: SprintResponse, next: NextFunction) => {
448
414
  const auth = req.sprint.getAuthorization();
449
415
  if (!auth) return res.status(401).json({ error: "No authorization header" });
450
416
 
451
417
  const token = auth.replace("Bearer ", "");
452
418
 
453
- const decoded = verifyEncrypted(token, publicKey, encryptionSecret);cd .
419
+ const decoded = verifyEncrypted(token, publicKey, encryptionSecret);
454
420
 
455
421
  if (!decoded) return res.status(403).json({ error: "Invalid token" });
456
422
 
@@ -525,8 +491,7 @@ CMD ["npm", "start"]
525
491
  }
526
492
 
527
493
  export function getDockerCompose(language: string) {
528
- return `version: "3.8"
529
-
494
+ return `
530
495
  services:
531
496
  app:
532
497
  build: .
@@ -534,7 +499,7 @@ services:
534
499
  - "3000:3000"
535
500
  environment:
536
501
  - NODE_ENV=production
537
- - PORT=3000
502
+ - PORT=5000
538
503
  restart: unless-stopped
539
504
  `;
540
505
  }
@@ -629,7 +594,7 @@ initTelemetry({
629
594
 
630
595
  initTelemetry({
631
596
  provider: "discord",
632
- webhookUrl: process.env.DISCORD_WEBHOOK_URL || ""
597
+ webhookUrl: process.env.DISCORD_TELEMETRY_WEBHOOK_URL || ""
633
598
  });
634
599
  `;
635
600
  }
@@ -658,7 +623,7 @@ import { initTelemetry } from "sprint-es/telemetry";
658
623
 
659
624
  initTelemetry({
660
625
  provider: "discord",
661
- webhookUrl: process.env.DISCORD_WEBHOOK_URL || ""
626
+ webhookUrl: process.env.DISCORD_TELEMETRY_WEBHOOK_URL || ""
662
627
  });
663
628
  `;
664
629
  }
@@ -681,7 +646,7 @@ SENTRY_DSN=
681
646
  } else if (telemetry === "discord") {
682
647
  env += `
683
648
  # Discord Webhook URL for error notifications
684
- DISCORD_WEBHOOK_URL=
649
+ DISCORD_TELEMETRY_WEBHOOK_URL=
685
650
  `;
686
651
  }
687
652
 
@@ -709,7 +674,7 @@ SENTRY_DSN=
709
674
  } else if (telemetry === "discord") {
710
675
  env += `
711
676
  # Discord Webhook URL
712
- DISCORD_WEBHOOK_URL=
677
+ DISCORD_TELEMETRY_WEBHOOK_URL=
713
678
  `;
714
679
  }
715
680
 
@@ -733,7 +698,7 @@ SENTRY_DSN=
733
698
  } else if (telemetry === "discord") {
734
699
  env += `
735
700
  # Discord Webhook URL
736
- DISCORD_WEBHOOK_URL=
701
+ DISCORD_TELEMETRY_WEBHOOK_URL=
737
702
  `;
738
703
  }
739
704
 
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { spawn } from "child_process";
2
- import { existsSync, writeFileSync } from "fs";
2
+ import { existsSync } from "fs";
3
3
  import { mkdir, writeFile as fsWriteFile } from "fs/promises";
4
4
  import { join } from "path";
5
5
  import color from "picocolors";
@@ -28,79 +28,104 @@ export async function runCLI(args: string[]) {
28
28
 
29
29
  p.intro(`${color.bgCyan(color.black(' create-sprint-app '))}`);
30
30
 
31
- const config = await p.group(
32
- {
33
- projectName: () =>
34
- p.text({
35
- message: "Project name:",
36
- placeholder: "my-api",
37
- validate: (v) => validateProjectName(v) || undefined,
38
- }),
39
-
40
- language: () =>
41
- p.select({
42
- message: "Language:",
43
- options: [
44
- { value: "typescript", label: "TypeScript", hint: "recommended" },
45
- { value: "javascript", label: "JavaScript" },
46
- ],
47
- }),
48
-
49
- telemetry: () =>
50
- p.select({
51
- message: "Error tracking:",
52
- options: [
53
- { value: "none", label: "None" },
54
- { value: "sentry", label: "Sentry", hint: "free tier available" },
55
- { value: "glitchtip", label: "GlitchTip", hint: "self-hostable" },
56
- { value: "discord", label: "Discord Webhook", hint: "sends to a channel" },
57
- ],
58
- }),
59
-
60
- docker: () =>
61
- p.confirm({ message: "Add Docker support?", initialValue: false }),
62
- },
63
- {
64
- onCancel: () => {
65
- p.cancel("Cancelled.");
66
- process.exit(0);
31
+ let config: {
32
+ projectName: string;
33
+ language: "typescript" | "javascript";
34
+ telemetry: string;
35
+ docker: boolean;
36
+ };
37
+
38
+ if (options.skipPrompts) {
39
+ config = {
40
+ projectName: options.projectName || "sprint-app",
41
+ language: options.language || "typescript",
42
+ telemetry: options.telemetry || "none",
43
+ docker: options.docker || false,
44
+ };
45
+ } else {
46
+ config = await p.group(
47
+ {
48
+ projectName: () =>
49
+ p.text({
50
+ message: "Project name:",
51
+ placeholder: "my-api",
52
+ validate: (v) => validateProjectName(v) || undefined,
53
+ }),
54
+
55
+ language: () =>
56
+ p.select({
57
+ message: "Language:",
58
+ options: [
59
+ { value: "typescript", label: "TypeScript", hint: "recommended" },
60
+ { value: "javascript", label: "JavaScript" },
61
+ ],
62
+ }),
63
+
64
+ telemetry: () =>
65
+ p.select({
66
+ message: "Error tracking:",
67
+ options: [
68
+ { value: "none", label: "None" },
69
+ { value: "sentry", label: "Sentry", hint: "free tier available" },
70
+ { value: "glitchtip", label: "GlitchTip", hint: "self-hostable" },
71
+ { value: "discord", label: "Discord Webhook", hint: "sends to a channel" },
72
+ ],
73
+ }),
74
+
75
+ docker: () =>
76
+ p.confirm({ message: "Add Docker support?", initialValue: false }),
67
77
  },
68
- }
69
- );
78
+ {
79
+ onCancel: () => {
80
+ p.cancel("Cancelled.");
81
+ process.exit(0);
82
+ },
83
+ }
84
+ );
85
+ }
70
86
 
71
87
  const targetDir = config.projectName === "." ? process.cwd() : join(process.cwd(), config.projectName);
72
88
 
73
89
  const s = p.spinner();
74
90
  s.start("Creating project");
75
91
  await createProject(
76
- config.projectName as string,
77
- config.language as "typescript" | "javascript",
78
- config.telemetry as string,
79
- config.docker as boolean,
92
+ config.projectName,
93
+ config.language,
94
+ config.telemetry,
95
+ config.docker,
80
96
  );
81
97
  s.stop("Project created");
82
98
 
83
- const installDeps = await p.confirm({ message: "Install dependencies now?", initialValue: true });
84
- const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
99
+ let installDeps = true;
100
+ if (options.skipInstall) {
101
+ installDeps = false;
102
+ } else if (!options.skipPrompts) {
103
+ installDeps = await p.confirm({ message: "Install dependencies now?", initialValue: true }) as boolean;
104
+ }
85
105
 
86
106
  if (installDeps) {
87
107
  const s2 = p.spinner();
88
108
  s2.start("Installing dependencies");
89
109
  try {
90
110
  await new Promise<void>((resolve, reject) => {
91
- const child = spawn(npmCmd, ["install"], {
111
+ const child = spawn("npm", ["install"], {
92
112
  cwd: targetDir,
93
- stdio: "pipe"
113
+ stdio: "inherit",
114
+ shell: true
94
115
  });
95
116
  child.on("close", (code) => {
96
117
  if (code === 0) resolve();
97
118
  else reject(new Error(`npm install exited with code ${code}`));
98
119
  });
99
- child.on("error", reject);
120
+ child.on("error", (err) => {
121
+ p.cancel(`Failed to run npm install: ${err.message}`);
122
+ reject(err);
123
+ });
100
124
  });
101
125
  s2.stop("Dependencies installed");
102
- } catch {
126
+ } catch (err) {
103
127
  s2.stop("Install failed — run npm install manually");
128
+ console.error(err);
104
129
  }
105
130
  }
106
131