create-sprint 0.0.38 → 0.0.42

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.
@@ -1,10 +1,15 @@
1
- import { randomBytes } from "node:crypto";
2
- export function generateJWT_SECRET() {
3
- return randomBytes(32).toString("hex");
1
+ import { generateKeyPairSync } from "node:crypto";
2
+ export function generateJWTKeys() {
3
+ const { publicKey, privateKey } = generateKeyPairSync("ec", {
4
+ namedCurve: "prime256v1",
5
+ publicKeyEncoding: { type: "spki", format: "pem" },
6
+ privateKeyEncoding: { type: "pkcs8", format: "pem" }
7
+ });
8
+ return { publicKey, privateKey };
4
9
  }
5
10
  export function getTypeScriptPackageJson(name, telemetry) {
6
11
  const deps = {
7
- "sprint-es": "^0.0.37",
12
+ "sprint-es": "^0.0.38"
8
13
  };
9
14
  const devDeps = {
10
15
  "@types/node": "^22.0.0",
@@ -34,7 +39,7 @@ export function getTypeScriptPackageJson(name, telemetry) {
34
39
  }
35
40
  export function getJavaScriptPackageJson(name, telemetry) {
36
41
  const deps = {
37
- "sprint-es": "^0.0.37",
42
+ "sprint-es": "^0.0.38"
38
43
  };
39
44
  if (telemetry === "sentry" || telemetry === "glitchtip") {
40
45
  deps["@sentry/node"] = "^8.0.0";
@@ -54,7 +59,7 @@ export function getJavaScriptPackageJson(name, telemetry) {
54
59
  dev: "sprint-es dev",
55
60
  "generate:keys": "sprint-es generate-keys"
56
61
  },
57
- dependencies: deps,
62
+ dependencies: deps
58
63
  };
59
64
  }
60
65
  export function getTsConfig() {
@@ -200,9 +205,7 @@ export const homeController = (req, res) => {
200
205
  export function getAdminController(language) {
201
206
  if (language === "typescript") {
202
207
  return `import { Handler } from "sprint-es";
203
- import { signEncrypted, verifyEncrypted } from "sprint-es/jwt";
204
-
205
- const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key-change-in-production";
208
+ import { signEncrypted, verifyEncrypted, getJwtFromEnv } from "sprint-es/jwt";
206
209
 
207
210
  export const adminController: Handler = (req, res) => {
208
211
  res.json({
@@ -227,10 +230,14 @@ export const jwtGenerateController: Handler = (req, res) => {
227
230
  return res.status(400).json({ error: "userId is required" });
228
231
  }
229
232
 
230
- const payload = { userId, role: role || "user" };
231
- const token = signEncrypted(payload, JWT_SECRET, { expiresIn: "1h" });
232
-
233
- res.json({ token });
233
+ try {
234
+ const { privateKey } = getJwtFromEnv();
235
+ const payload = { userId, role: role || "user" };
236
+ const token = signEncrypted(payload, privateKey, { expiresIn: "1h" });
237
+ res.json({ token });
238
+ } catch (error) {
239
+ return res.status(500).json({ error: "JWT not configured" });
240
+ }
234
241
  };
235
242
 
236
243
  export const jwtValidateController: Handler = (req, res) => {
@@ -240,20 +247,23 @@ export const jwtValidateController: Handler = (req, res) => {
240
247
  return res.status(401).json({ error: "No token provided" });
241
248
  }
242
249
 
243
- const decoded = verifyEncrypted(token, JWT_SECRET);
250
+ try {
251
+ const { publicKey } = getJwtFromEnv();
252
+ const decoded = verifyEncrypted(token, publicKey);
244
253
 
245
- if (!decoded) {
246
- return res.status(401).json({ error: "Invalid token" });
247
- }
254
+ if (!decoded) {
255
+ return res.status(401).json({ error: "Invalid token" });
256
+ }
248
257
 
249
- res.json({ valid: true, payload: decoded });
258
+ res.json({ valid: true, payload: decoded });
259
+ } catch (error) {
260
+ return res.status(500).json({ error: "JWT not configured" });
261
+ }
250
262
  };
251
263
  `;
252
264
  }
253
265
  return `import { Handler } from "sprint-es";
254
- import { signEncrypted, verifyEncrypted } from "sprint-es/jwt";
255
-
256
- const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key-change-in-production";
266
+ import { signEncrypted, verifyEncrypted, getJwtFromEnv } from "sprint-es/jwt";
257
267
 
258
268
  export const adminController = (req, res) => {
259
269
  res.json({
@@ -278,10 +288,14 @@ export const jwtGenerateController = (req, res) => {
278
288
  return res.status(400).json({ error: "userId is required" });
279
289
  }
280
290
 
281
- const payload = { userId, role: role || "user" };
282
- const token = signEncrypted(payload, JWT_SECRET, { expiresIn: "1h" });
283
-
284
- res.json({ token });
291
+ try {
292
+ const { privateKey } = getJwtFromEnv();
293
+ const payload = { userId, role: role || "user" };
294
+ const token = signEncrypted(payload, privateKey, { expiresIn: "1h" });
295
+ res.json({ token });
296
+ } catch (error) {
297
+ return res.status(500).json({ error: "JWT not configured" });
298
+ }
285
299
  };
286
300
 
287
301
  export const jwtValidateController = (req, res) => {
@@ -291,13 +305,18 @@ export const jwtValidateController = (req, res) => {
291
305
  return res.status(401).json({ error: "No token provided" });
292
306
  }
293
307
 
294
- const decoded = verifyEncrypted(token, JWT_SECRET);
308
+ try {
309
+ const { publicKey } = getJwtFromEnv();
310
+ const decoded = verifyEncrypted(token, publicKey);
295
311
 
296
- if (!decoded) {
297
- return res.status(401).json({ error: "Invalid token" });
298
- }
312
+ if (!decoded) {
313
+ return res.status(401).json({ error: "Invalid token" });
314
+ }
299
315
 
300
- res.json({ valid: true, payload: decoded });
316
+ res.json({ valid: true, payload: decoded });
317
+ } catch (error) {
318
+ return res.status(500).json({ error: "JWT not configured" });
319
+ }
301
320
  };
302
321
  `;
303
322
  }
@@ -581,10 +600,11 @@ DISCORD_WEBHOOK_URL=
581
600
  return env;
582
601
  }
583
602
  export function getEnvDevelopment(telemetry) {
584
- const jwtSecret = generateJWT_SECRET();
603
+ const { publicKey, privateKey } = generateJWTKeys();
585
604
  let env = `NODE_ENV=development
586
605
  PORT=3000
587
- JWT_SECRET=${jwtSecret}
606
+ JWT_PUBLIC_KEY=${publicKey.replace(/\n/g, "\\n")}
607
+ JWT_PRIVATE_KEY=${privateKey.replace(/\n/g, "\\n")}
588
608
  `;
589
609
  if (telemetry === "sentry" || telemetry === "glitchtip") {
590
610
  env += `
@@ -601,10 +621,11 @@ DISCORD_WEBHOOK_URL=
601
621
  return env;
602
622
  }
603
623
  export function getEnvProduction(telemetry) {
604
- const jwtSecret = generateJWT_SECRET();
624
+ const { publicKey, privateKey } = generateJWTKeys();
605
625
  let env = `NODE_ENV=production
606
626
  PORT=3000
607
- JWT_SECRET=${jwtSecret}
627
+ JWT_PUBLIC_KEY=${publicKey.replace(/\n/g, "\\n")}
628
+ JWT_PRIVATE_KEY=${privateKey.replace(/\n/g, "\\n")}
608
629
  `;
609
630
  if (telemetry === "sentry" || telemetry === "glitchtip") {
610
631
  env += `
package/dist/index.js CHANGED
@@ -2,59 +2,68 @@ import { execSync } from "child_process";
2
2
  import { existsSync } from "fs";
3
3
  import { mkdir, writeFile } from "fs/promises";
4
4
  import { join } from "path";
5
- import { input, select, confirm } from "@inquirer/prompts";
5
+ import * as p from "@clack/prompts";
6
6
  import { validateProjectName } from "./validators.js";
7
7
  import { getTypeScriptPackageJson, getJavaScriptPackageJson, getTsConfig, getViteConfig, getMainFile, getHomeRoute, getAdminRoute, getHomeController, getAdminController, getAuthMiddleware, getHomeSchema, getAdminSchema, getDockerfile, getDockerCompose, getGitignore, getDockerIgnore, getSprintConfigFile, getEnvDevelopment, getEnvProduction, getExampleCronJob } from "./generators.js";
8
8
  export async function runCLI(args) {
9
9
  const options = parseArgs(args);
10
- console.log("\nšŸš€ Welcome to Sprint - Quickly API Framework\n");
11
- let projectName = options.projectName;
12
- let language = options.language;
13
- const telemetry = options.telemetry;
14
- const useDocker = options.docker;
15
- if (!projectName) {
16
- projectName = await getProjectName();
17
- }
18
- const error = validateProjectName(projectName);
19
- if (error) {
20
- console.error(`\nāŒ Error: ${error}\n`);
21
- process.exit(1);
22
- }
23
- if (!language) {
24
- language = await selectLanguage();
25
- }
26
- console.log(`\nāœ… Creating Sprint project: ${projectName === "." ? "current directory" : projectName} with ${language === "typescript" ? "TypeScript" : "JavaScript"}\n`);
27
- await createProject(projectName, language, telemetry, useDocker);
28
- console.log("\nāœ… Project created successfully!");
29
- let installDeps = true;
30
- if (options.skipInstall) {
31
- installDeps = false;
32
- }
33
- else {
34
- installDeps = await confirm({
35
- message: "Do you want to install dependencies now?",
36
- default: true,
37
- });
38
- }
10
+ p.intro("Sprint — Quickly API Framework");
11
+ const config = await p.group({
12
+ projectName: () => p.text({
13
+ message: "Project name:",
14
+ placeholder: "my-api",
15
+ validate: (v) => validateProjectName(v) || undefined,
16
+ }),
17
+ language: () => p.select({
18
+ message: "Language:",
19
+ options: [
20
+ { value: "typescript", label: "TypeScript", hint: "recommended" },
21
+ { value: "javascript", label: "JavaScript" },
22
+ ],
23
+ }),
24
+ telemetry: () => p.select({
25
+ message: "Error tracking:",
26
+ options: [
27
+ { value: "none", label: "None" },
28
+ { value: "sentry", label: "Sentry", hint: "free tier available" },
29
+ { value: "glitchtip", label: "GlitchTip", hint: "self-hostable" },
30
+ { value: "discord", label: "Discord Webhook", hint: "sends to a channel" },
31
+ ],
32
+ }),
33
+ docker: () => p.confirm({ message: "Add Docker support?", initialValue: false }),
34
+ }, {
35
+ onCancel: () => {
36
+ p.cancel("Cancelled.");
37
+ process.exit(0);
38
+ },
39
+ });
40
+ const targetDir = config.projectName === "." ? process.cwd() : join(process.cwd(), config.projectName);
41
+ const s = p.spinner();
42
+ s.start("Creating project");
43
+ await createProject(config.projectName, config.language, config.telemetry, config.docker);
44
+ s.stop("Project created");
45
+ const installDeps = await p.confirm({ message: "Install dependencies now?", initialValue: true });
39
46
  if (installDeps) {
40
- console.log("\nšŸ“¦ Installing dependencies...\n");
41
- const targetDir = projectName === "." ? process.cwd() : join(process.cwd(), projectName);
47
+ const s2 = p.spinner();
48
+ s2.start("Installing dependencies");
42
49
  try {
43
- execSync("npm install", { cwd: targetDir, stdio: "inherit" });
44
- console.log("\nāœ… Dependencies installed successfully!");
50
+ execSync("npm install", { cwd: targetDir, stdio: "pipe" });
51
+ s2.stop("Dependencies installed");
45
52
  }
46
53
  catch {
47
- console.error("\nāŒ Error installing dependencies. Please run 'npm install' manually.");
54
+ s2.stop("Install failed — run npm install manually");
48
55
  }
49
56
  }
50
- console.log("\nšŸ“¦ Next steps:");
51
- const cdCmd = projectName === "." ? "" : `cd ${projectName} && `;
52
- if (!installDeps) {
53
- console.log(` ${cdCmd}npm install`);
54
- }
55
- console.log(` ${cdCmd}npm run dev`);
56
- console.log("\n");
57
+ const cdCmd = config.projectName === "." ? "" : `cd ${config.projectName} && `;
58
+ p.note([
59
+ !installDeps ? `${cdCmd}npm install` : "",
60
+ `${cdCmd}npm run dev`,
61
+ ]
62
+ .filter(Boolean)
63
+ .join("\n"), "Next steps");
64
+ p.outro("Ready. Happy shipping.");
57
65
  }
66
+ ;
58
67
  function parseArgs(args) {
59
68
  const options = {};
60
69
  const hasTs = args.includes("--ts") || args.includes("--typescript");
@@ -85,63 +94,36 @@ function parseArgs(args) {
85
94
  }
86
95
  ;
87
96
  async function getProjectName() {
88
- const name = await input({
97
+ const name = await p.text({
89
98
  message: "Enter project name:",
90
- validate: (value) => {
91
- return validateProjectName(value) || true;
92
- }
99
+ validate: (value) => validateProjectName(value) || undefined,
93
100
  });
94
101
  return name;
95
102
  }
96
103
  ;
97
104
  async function selectLanguage() {
98
- const language = await select({
105
+ const language = await p.select({
99
106
  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
- ]
107
+ options: [
108
+ { value: "typescript", label: "TypeScript", hint: "recommended" },
109
+ { value: "javascript", label: "JavaScript" },
110
+ ],
112
111
  });
113
112
  return language;
114
113
  }
115
114
  ;
116
115
  async function selectTelemetry() {
117
- const telemetry = await select({
116
+ const telemetry = await p.select({
118
117
  message: "Select error tracking/telemetry solution:",
119
- choices: [
120
- {
121
- name: "None",
122
- value: "none",
123
- description: "No error tracking integration",
124
- },
125
- {
126
- name: "Sentry",
127
- value: "sentry",
128
- description: "Full-featured error tracking (free tier available)",
129
- },
130
- {
131
- name: "GlitchTip",
132
- value: "glitchtip",
133
- description: "Simple error tracking, can be self-hosted",
134
- },
135
- {
136
- name: "Discord Webhook",
137
- value: "discord",
138
- description: "Send error notifications to Discord channel",
139
- }
140
- ]
118
+ options: [
119
+ { value: "none", label: "None" },
120
+ { value: "sentry", label: "Sentry", hint: "free tier available" },
121
+ { value: "glitchtip", label: "GlitchTip", hint: "self-hostable" },
122
+ { value: "discord", label: "Discord Webhook", hint: "sends to a channel" },
123
+ ],
141
124
  });
142
125
  return telemetry;
143
126
  }
144
- ;
145
127
  async function createProject(projectName, language, telemetryArg, useDockerArg) {
146
128
  const isCurrentDir = projectName === ".";
147
129
  const targetDir = isCurrentDir ? process.cwd() : join(process.cwd(), projectName);
@@ -156,9 +138,9 @@ async function createProject(projectName, language, telemetryArg, useDockerArg)
156
138
  telemetry = await selectTelemetry();
157
139
  let useDocker = useDockerArg || false;
158
140
  if (!useDockerArg) {
159
- useDocker = await confirm({
141
+ useDocker = await p.confirm({
160
142
  message: "Do you want to add Docker support?",
161
- default: false,
143
+ initialValue: false,
162
144
  });
163
145
  }
164
146
  let pkgJson;
@@ -181,6 +163,15 @@ async function createProject(projectName, language, telemetryArg, useDockerArg)
181
163
  await mkdir(join(srcDir, "controllers"), { recursive: true });
182
164
  await mkdir(join(srcDir, "schemas"), { recursive: true });
183
165
  await mkdir(join(srcDir, "cronjobs"), { recursive: true });
166
+ await mkdir(join(srcDir, "config"), { recursive: true });
167
+ if (language === "typescript") {
168
+ await writeFile(join(srcDir, "config", "index.ts"), "");
169
+ await writeFile(join(srcDir, "config", "clients.ts"), "");
170
+ }
171
+ else {
172
+ await writeFile(join(srcDir, "config", "index.js"), "");
173
+ await writeFile(join(srcDir, "config", "clients.js"), "");
174
+ }
184
175
  await writeFile(join(srcDir, "middlewares", ".gitkeep"), "");
185
176
  await writeFile(join(srcDir, "app." + (language === "typescript" ? "ts" : "js")), getMainFile(language));
186
177
  await writeFile(join(srcDir, "routes", "home." + (language === "typescript" ? "ts" : "js")), getHomeRoute(language));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-sprint",
3
- "version": "0.0.38",
3
+ "version": "0.0.42",
4
4
  "description": "Create a new Sprint API project",
5
5
  "type": "module",
6
6
  "bin": {
@@ -26,7 +26,7 @@
26
26
  "author": "TPEOficial LLC",
27
27
  "license": "Apache-2.0",
28
28
  "dependencies": {
29
- "@inquirer/prompts": "^7.10.1"
29
+ "@clack/prompts": "^1.0.1"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@types/node": "^25.3.3",
package/src/generators.ts CHANGED
@@ -1,12 +1,22 @@
1
- import { randomBytes } from "node:crypto";
1
+ import { generateKeyPairSync } from "node:crypto";
2
2
 
3
- export function generateJWT_SECRET(): string {
4
- return randomBytes(32).toString("hex");
3
+ export interface JWTKeys {
4
+ publicKey: string;
5
+ privateKey: string;
6
+ }
7
+
8
+ export function generateJWTKeys(): JWTKeys {
9
+ const { publicKey, privateKey } = generateKeyPairSync("ec", {
10
+ namedCurve: "prime256v1",
11
+ publicKeyEncoding: { type: "spki", format: "pem" },
12
+ privateKeyEncoding: { type: "pkcs8", format: "pem" }
13
+ });
14
+ return { publicKey, privateKey };
5
15
  }
6
16
 
7
17
  export function getTypeScriptPackageJson(name: string, telemetry: string) {
8
18
  const deps: Record<string, string> = {
9
- "sprint-es": "^0.0.37",
19
+ "sprint-es": "^0.0.38"
10
20
  };
11
21
 
12
22
  const devDeps: Record<string, string> = {
@@ -39,7 +49,7 @@ export function getTypeScriptPackageJson(name: string, telemetry: string) {
39
49
 
40
50
  export function getJavaScriptPackageJson(name: string, telemetry: string) {
41
51
  const deps: Record<string, string> = {
42
- "sprint-es": "^0.0.37",
52
+ "sprint-es": "^0.0.38"
43
53
  };
44
54
 
45
55
  if (telemetry === "sentry" || telemetry === "glitchtip") {
@@ -60,7 +70,7 @@ export function getJavaScriptPackageJson(name: string, telemetry: string) {
60
70
  dev: "sprint-es dev",
61
71
  "generate:keys": "sprint-es generate-keys"
62
72
  },
63
- dependencies: deps,
73
+ dependencies: deps
64
74
  };
65
75
  }
66
76
 
@@ -214,9 +224,7 @@ export const homeController = (req, res) => {
214
224
  export function getAdminController(language: string) {
215
225
  if (language === "typescript") {
216
226
  return `import { Handler } from "sprint-es";
217
- import { signEncrypted, verifyEncrypted } from "sprint-es/jwt";
218
-
219
- const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key-change-in-production";
227
+ import { signEncrypted, verifyEncrypted, getJwtFromEnv } from "sprint-es/jwt";
220
228
 
221
229
  export const adminController: Handler = (req, res) => {
222
230
  res.json({
@@ -241,10 +249,14 @@ export const jwtGenerateController: Handler = (req, res) => {
241
249
  return res.status(400).json({ error: "userId is required" });
242
250
  }
243
251
 
244
- const payload = { userId, role: role || "user" };
245
- const token = signEncrypted(payload, JWT_SECRET, { expiresIn: "1h" });
246
-
247
- res.json({ token });
252
+ try {
253
+ const { privateKey } = getJwtFromEnv();
254
+ const payload = { userId, role: role || "user" };
255
+ const token = signEncrypted(payload, privateKey, { expiresIn: "1h" });
256
+ res.json({ token });
257
+ } catch (error) {
258
+ return res.status(500).json({ error: "JWT not configured" });
259
+ }
248
260
  };
249
261
 
250
262
  export const jwtValidateController: Handler = (req, res) => {
@@ -254,20 +266,23 @@ export const jwtValidateController: Handler = (req, res) => {
254
266
  return res.status(401).json({ error: "No token provided" });
255
267
  }
256
268
 
257
- const decoded = verifyEncrypted(token, JWT_SECRET);
269
+ try {
270
+ const { publicKey } = getJwtFromEnv();
271
+ const decoded = verifyEncrypted(token, publicKey);
258
272
 
259
- if (!decoded) {
260
- return res.status(401).json({ error: "Invalid token" });
261
- }
273
+ if (!decoded) {
274
+ return res.status(401).json({ error: "Invalid token" });
275
+ }
262
276
 
263
- res.json({ valid: true, payload: decoded });
277
+ res.json({ valid: true, payload: decoded });
278
+ } catch (error) {
279
+ return res.status(500).json({ error: "JWT not configured" });
280
+ }
264
281
  };
265
282
  `;
266
283
  }
267
284
  return `import { Handler } from "sprint-es";
268
- import { signEncrypted, verifyEncrypted } from "sprint-es/jwt";
269
-
270
- const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key-change-in-production";
285
+ import { signEncrypted, verifyEncrypted, getJwtFromEnv } from "sprint-es/jwt";
271
286
 
272
287
  export const adminController = (req, res) => {
273
288
  res.json({
@@ -292,10 +307,14 @@ export const jwtGenerateController = (req, res) => {
292
307
  return res.status(400).json({ error: "userId is required" });
293
308
  }
294
309
 
295
- const payload = { userId, role: role || "user" };
296
- const token = signEncrypted(payload, JWT_SECRET, { expiresIn: "1h" });
297
-
298
- res.json({ token });
310
+ try {
311
+ const { privateKey } = getJwtFromEnv();
312
+ const payload = { userId, role: role || "user" };
313
+ const token = signEncrypted(payload, privateKey, { expiresIn: "1h" });
314
+ res.json({ token });
315
+ } catch (error) {
316
+ return res.status(500).json({ error: "JWT not configured" });
317
+ }
299
318
  };
300
319
 
301
320
  export const jwtValidateController = (req, res) => {
@@ -305,13 +324,18 @@ export const jwtValidateController = (req, res) => {
305
324
  return res.status(401).json({ error: "No token provided" });
306
325
  }
307
326
 
308
- const decoded = verifyEncrypted(token, JWT_SECRET);
327
+ try {
328
+ const { publicKey } = getJwtFromEnv();
329
+ const decoded = verifyEncrypted(token, publicKey);
309
330
 
310
- if (!decoded) {
311
- return res.status(401).json({ error: "Invalid token" });
312
- }
331
+ if (!decoded) {
332
+ return res.status(401).json({ error: "Invalid token" });
333
+ }
313
334
 
314
- res.json({ valid: true, payload: decoded });
335
+ res.json({ valid: true, payload: decoded });
336
+ } catch (error) {
337
+ return res.status(500).json({ error: "JWT not configured" });
338
+ }
315
339
  };
316
340
  `;
317
341
  }
@@ -609,10 +633,11 @@ DISCORD_WEBHOOK_URL=
609
633
  }
610
634
 
611
635
  export function getEnvDevelopment(telemetry: string) {
612
- const jwtSecret = generateJWT_SECRET();
636
+ const { publicKey, privateKey } = generateJWTKeys();
613
637
  let env = `NODE_ENV=development
614
638
  PORT=3000
615
- JWT_SECRET=${jwtSecret}
639
+ JWT_PUBLIC_KEY=${publicKey.replace(/\n/g, "\\n")}
640
+ JWT_PRIVATE_KEY=${privateKey.replace(/\n/g, "\\n")}
616
641
  `;
617
642
 
618
643
  if (telemetry === "sentry" || telemetry === "glitchtip") {
@@ -631,10 +656,11 @@ DISCORD_WEBHOOK_URL=
631
656
  }
632
657
 
633
658
  export function getEnvProduction(telemetry: string) {
634
- const jwtSecret = generateJWT_SECRET();
659
+ const { publicKey, privateKey } = generateJWTKeys();
635
660
  let env = `NODE_ENV=production
636
661
  PORT=3000
637
- JWT_SECRET=${jwtSecret}
662
+ JWT_PUBLIC_KEY=${publicKey.replace(/\n/g, "\\n")}
663
+ JWT_PRIVATE_KEY=${privateKey.replace(/\n/g, "\\n")}
638
664
  `;
639
665
 
640
666
  if (telemetry === "sentry" || telemetry === "glitchtip") {
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@ import { execSync } from "child_process";
2
2
  import { existsSync } from "fs";
3
3
  import { mkdir, writeFile } from "fs/promises";
4
4
  import { join } from "path";
5
- import { input, select, confirm } from "@inquirer/prompts";
5
+ import * as p from "@clack/prompts";
6
6
  import { validateProjectName } from "./validators.js";
7
7
  import { getTypeScriptPackageJson, getJavaScriptPackageJson, getTsConfig, getViteConfig, getMainFile, getHomeRoute, getAdminRoute, getHomeController, getAdminController, getAuthMiddleware, getHomeSchema, getAdminSchema, getDockerfile, getDockerCompose, getGitignore, getDockerIgnore, getSprintConfigFile, getEnvDevelopment, getEnvProduction, getExampleCronJob } from "./generators.js";
8
8
 
@@ -18,62 +18,81 @@ export interface CLIOptions {
18
18
  export async function runCLI(args: string[]) {
19
19
  const options = parseArgs(args);
20
20
 
21
- console.log("\nšŸš€ Welcome to Sprint - Quickly API Framework\n");
22
-
23
- let projectName = options.projectName;
24
- let language = options.language;
25
- const telemetry = options.telemetry;
26
- const useDocker = options.docker;
27
-
28
- if (!projectName) {
29
- projectName = await getProjectName();
30
- }
31
-
32
- const error = validateProjectName(projectName);
33
- if (error) {
34
- console.error(`\nāŒ Error: ${error}\n`);
35
- process.exit(1);
36
- }
37
-
38
- if (!language) {
39
- language = await selectLanguage();
40
- }
41
-
42
- console.log(`\nāœ… Creating Sprint project: ${projectName === "." ? "current directory" : projectName} with ${language === "typescript" ? "TypeScript" : "JavaScript"}\n`);
21
+ p.intro("Sprint — Quickly API Framework");
22
+
23
+ const config = await p.group(
24
+ {
25
+ projectName: () =>
26
+ p.text({
27
+ message: "Project name:",
28
+ placeholder: "my-api",
29
+ validate: (v) => validateProjectName(v) || undefined,
30
+ }),
31
+
32
+ language: () =>
33
+ p.select({
34
+ message: "Language:",
35
+ options: [
36
+ { value: "typescript", label: "TypeScript", hint: "recommended" },
37
+ { value: "javascript", label: "JavaScript" },
38
+ ],
39
+ }),
40
+
41
+ telemetry: () =>
42
+ 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
+
52
+ docker: () =>
53
+ p.confirm({ message: "Add Docker support?", initialValue: false }),
54
+ },
55
+ {
56
+ onCancel: () => {
57
+ p.cancel("Cancelled.");
58
+ process.exit(0);
59
+ },
60
+ }
61
+ );
43
62
 
44
- await createProject(projectName, language, telemetry, useDocker);
63
+ const targetDir = config.projectName === "." ? process.cwd() : join(process.cwd(), config.projectName);
45
64
 
46
- console.log("\nāœ… Project created successfully!");
65
+ const s = p.spinner();
66
+ s.start("Creating project");
67
+ await createProject(config.projectName, config.language as "typescript" | "javascript", config.telemetry, config.docker);
68
+ s.stop("Project created");
47
69
 
48
- let installDeps = true;
49
- if (options.skipInstall) {
50
- installDeps = false;
51
- } else {
52
- installDeps = await confirm({
53
- message: "Do you want to install dependencies now?",
54
- default: true,
55
- });
56
- }
70
+ const installDeps = await p.confirm({ message: "Install dependencies now?", initialValue: true });
57
71
 
58
72
  if (installDeps) {
59
- console.log("\nšŸ“¦ Installing dependencies...\n");
60
- const targetDir = projectName === "." ? process.cwd() : join(process.cwd(), projectName);
73
+ const s2 = p.spinner();
74
+ s2.start("Installing dependencies");
61
75
  try {
62
- execSync("npm install", { cwd: targetDir, stdio: "inherit" });
63
- console.log("\nāœ… Dependencies installed successfully!");
76
+ execSync("npm install", { cwd: targetDir, stdio: "pipe" });
77
+ s2.stop("Dependencies installed");
64
78
  } catch {
65
- console.error("\nāŒ Error installing dependencies. Please run 'npm install' manually.");
79
+ s2.stop("Install failed — run npm install manually");
66
80
  }
67
81
  }
68
82
 
69
- console.log("\nšŸ“¦ Next steps:");
70
- const cdCmd = projectName === "." ? "" : `cd ${projectName} && `;
71
- if (!installDeps) {
72
- console.log(` ${cdCmd}npm install`);
73
- }
74
- console.log(` ${cdCmd}npm run dev`);
75
- console.log("\n");
76
- }
83
+ const cdCmd = config.projectName === "." ? "" : `cd ${config.projectName} && `;
84
+ p.note(
85
+ [
86
+ !installDeps ? `${cdCmd}npm install` : "",
87
+ `${cdCmd}npm run dev`,
88
+ ]
89
+ .filter(Boolean)
90
+ .join("\n"),
91
+ "Next steps"
92
+ );
93
+
94
+ p.outro("Ready. Happy shipping.");
95
+ };
77
96
 
78
97
  function parseArgs(args: string[]): CLIOptions {
79
98
  const options: CLIOptions = {};
@@ -104,65 +123,39 @@ function parseArgs(args: string[]): CLIOptions {
104
123
  };
105
124
 
106
125
  async function getProjectName(): Promise<string> {
107
- const name = await input({
126
+ const name = await p.text({
108
127
  message: "Enter project name:",
109
- validate: (value) => {
110
- return validateProjectName(value) || true;
111
- }
128
+ validate: (value) => validateProjectName(value) || undefined,
112
129
  });
113
130
 
114
- return name;
131
+ return name as string;
115
132
  };
116
133
 
117
134
  async function selectLanguage(): Promise<"typescript" | "javascript"> {
118
- const language = await select({
135
+ const language = await p.select({
119
136
  message: "Select your preferred language:",
120
- choices: [
121
- {
122
- name: "TypeScript",
123
- value: "typescript",
124
- description: "Recommended - Type safety and better developer experience",
125
- },
126
- {
127
- name: "JavaScript",
128
- value: "javascript",
129
- description: "Vanilla JavaScript for simpler projects",
130
- }
131
- ]
137
+ options: [
138
+ { value: "typescript", label: "TypeScript", hint: "recommended" },
139
+ { value: "javascript", label: "JavaScript" },
140
+ ],
132
141
  });
133
142
 
134
143
  return language as "typescript" | "javascript";
135
144
  };
136
145
 
137
146
  async function selectTelemetry(): Promise<"none" | "sentry" | "glitchtip" | "discord"> {
138
- const telemetry = await select({
147
+ const telemetry = await p.select({
139
148
  message: "Select error tracking/telemetry solution:",
140
- choices: [
141
- {
142
- name: "None",
143
- value: "none",
144
- description: "No error tracking integration",
145
- },
146
- {
147
- name: "Sentry",
148
- value: "sentry",
149
- description: "Full-featured error tracking (free tier available)",
150
- },
151
- {
152
- name: "GlitchTip",
153
- value: "glitchtip",
154
- description: "Simple error tracking, can be self-hosted",
155
- },
156
- {
157
- name: "Discord Webhook",
158
- value: "discord",
159
- description: "Send error notifications to Discord channel",
160
- }
161
- ]
149
+ options: [
150
+ { value: "none", label: "None" },
151
+ { value: "sentry", label: "Sentry", hint: "free tier available" },
152
+ { value: "glitchtip", label: "GlitchTip", hint: "self-hostable" },
153
+ { value: "discord", label: "Discord Webhook", hint: "sends to a channel" },
154
+ ],
162
155
  });
163
156
 
164
157
  return telemetry as "none" | "sentry" | "glitchtip" | "discord";
165
- };
158
+ }
166
159
 
167
160
  async function createProject(
168
161
  projectName: string,
@@ -185,10 +178,10 @@ async function createProject(
185
178
 
186
179
  let useDocker = useDockerArg || false;
187
180
  if (!useDockerArg) {
188
- useDocker = await confirm({
181
+ useDocker = await p.confirm({
189
182
  message: "Do you want to add Docker support?",
190
- default: false,
191
- });
183
+ initialValue: false,
184
+ }) as boolean;
192
185
  }
193
186
 
194
187
  let pkgJson;
@@ -211,6 +204,15 @@ async function createProject(
211
204
  await mkdir(join(srcDir, "controllers"), { recursive: true });
212
205
  await mkdir(join(srcDir, "schemas"), { recursive: true });
213
206
  await mkdir(join(srcDir, "cronjobs"), { recursive: true });
207
+ await mkdir(join(srcDir, "config"), { recursive: true });
208
+
209
+ if (language === "typescript") {
210
+ await writeFile(join(srcDir, "config", "index.ts"), "");
211
+ await writeFile(join(srcDir, "config", "clients.ts"), "");
212
+ } else {
213
+ await writeFile(join(srcDir, "config", "index.js"), "");
214
+ await writeFile(join(srcDir, "config", "clients.js"), "");
215
+ }
214
216
 
215
217
  await writeFile(join(srcDir, "middlewares", ".gitkeep"), "");
216
218