create-sprint 0.0.38 → 0.0.44

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");
@@ -84,83 +93,15 @@ function parseArgs(args) {
84
93
  return options;
85
94
  }
86
95
  ;
87
- async function getProjectName() {
88
- const name = await input({
89
- message: "Enter project name:",
90
- validate: (value) => {
91
- return validateProjectName(value) || true;
92
- }
93
- });
94
- return name;
95
- }
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
- ;
116
- async function selectTelemetry() {
117
- const telemetry = await select({
118
- 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
- ]
141
- });
142
- return telemetry;
143
- }
144
- ;
145
- async function createProject(projectName, language, telemetryArg, useDockerArg) {
96
+ async function createProject(projectName, language, telemetry, useDocker) {
146
97
  const isCurrentDir = projectName === ".";
147
98
  const targetDir = isCurrentDir ? process.cwd() : join(process.cwd(), projectName);
148
99
  if (!isCurrentDir && existsSync(targetDir)) {
149
- console.error(`Error: Directory ${projectName} already exists`);
100
+ p.cancel(`Directory "${projectName}" already exists.`);
150
101
  process.exit(1);
151
102
  }
152
103
  if (!isCurrentDir)
153
104
  await mkdir(targetDir, { recursive: true });
154
- let telemetry = telemetryArg || "none";
155
- if (!telemetryArg)
156
- telemetry = await selectTelemetry();
157
- let useDocker = useDockerArg || false;
158
- if (!useDockerArg) {
159
- useDocker = await confirm({
160
- message: "Do you want to add Docker support?",
161
- default: false,
162
- });
163
- }
164
105
  let pkgJson;
165
106
  if (language === "typescript")
166
107
  pkgJson = getTypeScriptPackageJson(projectName, telemetry);
@@ -181,6 +122,15 @@ async function createProject(projectName, language, telemetryArg, useDockerArg)
181
122
  await mkdir(join(srcDir, "controllers"), { recursive: true });
182
123
  await mkdir(join(srcDir, "schemas"), { recursive: true });
183
124
  await mkdir(join(srcDir, "cronjobs"), { recursive: true });
125
+ await mkdir(join(srcDir, "config"), { recursive: true });
126
+ if (language === "typescript") {
127
+ await writeFile(join(srcDir, "config", "index.ts"), "");
128
+ await writeFile(join(srcDir, "config", "clients.ts"), "");
129
+ }
130
+ else {
131
+ await writeFile(join(srcDir, "config", "index.js"), "");
132
+ await writeFile(join(srcDir, "config", "clients.js"), "");
133
+ }
184
134
  await writeFile(join(srcDir, "middlewares", ".gitkeep"), "");
185
135
  await writeFile(join(srcDir, "app." + (language === "typescript" ? "ts" : "js")), getMainFile(language));
186
136
  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.44",
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,86 @@ 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
- }
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
+ );
41
62
 
42
- console.log(`\nāœ… Creating Sprint project: ${projectName === "." ? "current directory" : projectName} with ${language === "typescript" ? "TypeScript" : "JavaScript"}\n`);
63
+ const targetDir = config.projectName === "." ? process.cwd() : join(process.cwd(), config.projectName);
43
64
 
44
- await createProject(projectName, language, telemetry, useDocker);
65
+ const s = p.spinner();
66
+ s.start("Creating project");
67
+ await createProject(
68
+ config.projectName as string,
69
+ config.language as "typescript" | "javascript",
70
+ config.telemetry as string,
71
+ config.docker as boolean,
72
+ );
73
+ s.stop("Project created");
45
74
 
46
- console.log("\nāœ… Project created successfully!");
47
-
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
- }
75
+ const installDeps = await p.confirm({ message: "Install dependencies now?", initialValue: true });
57
76
 
58
77
  if (installDeps) {
59
- console.log("\nšŸ“¦ Installing dependencies...\n");
60
- const targetDir = projectName === "." ? process.cwd() : join(process.cwd(), projectName);
78
+ const s2 = p.spinner();
79
+ s2.start("Installing dependencies");
61
80
  try {
62
- execSync("npm install", { cwd: targetDir, stdio: "inherit" });
63
- console.log("\nāœ… Dependencies installed successfully!");
81
+ execSync("npm install", { cwd: targetDir, stdio: "pipe" });
82
+ s2.stop("Dependencies installed");
64
83
  } catch {
65
- console.error("\nāŒ Error installing dependencies. Please run 'npm install' manually.");
84
+ s2.stop("Install failed — run npm install manually");
66
85
  }
67
86
  }
68
87
 
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
- }
88
+ const cdCmd = config.projectName === "." ? "" : `cd ${config.projectName} && `;
89
+ p.note(
90
+ [
91
+ !installDeps ? `${cdCmd}npm install` : "",
92
+ `${cdCmd}npm run dev`,
93
+ ]
94
+ .filter(Boolean)
95
+ .join("\n"),
96
+ "Next steps"
97
+ );
98
+
99
+ p.outro("Ready. Happy shipping.");
100
+ };
77
101
 
78
102
  function parseArgs(args: string[]): CLIOptions {
79
103
  const options: CLIOptions = {};
@@ -103,94 +127,22 @@ function parseArgs(args: string[]): CLIOptions {
103
127
  return options;
104
128
  };
105
129
 
106
- async function getProjectName(): Promise<string> {
107
- const name = await input({
108
- message: "Enter project name:",
109
- validate: (value) => {
110
- return validateProjectName(value) || true;
111
- }
112
- });
113
-
114
- return name;
115
- };
116
-
117
- async function selectLanguage(): Promise<"typescript" | "javascript"> {
118
- const language = await select({
119
- 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
- ]
132
- });
133
-
134
- return language as "typescript" | "javascript";
135
- };
136
-
137
- async function selectTelemetry(): Promise<"none" | "sentry" | "glitchtip" | "discord"> {
138
- const telemetry = await select({
139
- 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
- ]
162
- });
163
-
164
- return telemetry as "none" | "sentry" | "glitchtip" | "discord";
165
- };
166
-
167
130
  async function createProject(
168
131
  projectName: string,
169
132
  language: "typescript" | "javascript",
170
- telemetryArg?: string,
171
- useDockerArg?: boolean
133
+ telemetry: string,
134
+ useDocker: boolean
172
135
  ) {
173
136
  const isCurrentDir = projectName === ".";
174
137
  const targetDir = isCurrentDir ? process.cwd() : join(process.cwd(), projectName);
175
138
 
176
139
  if (!isCurrentDir && existsSync(targetDir)) {
177
- console.error(`Error: Directory ${projectName} already exists`);
140
+ p.cancel(`Directory "${projectName}" already exists.`);
178
141
  process.exit(1);
179
142
  }
180
143
 
181
144
  if (!isCurrentDir) await mkdir(targetDir, { recursive: true });
182
145
 
183
- let telemetry = telemetryArg || "none";
184
- if (!telemetryArg) telemetry = await selectTelemetry();
185
-
186
- let useDocker = useDockerArg || false;
187
- if (!useDockerArg) {
188
- useDocker = await confirm({
189
- message: "Do you want to add Docker support?",
190
- default: false,
191
- });
192
- }
193
-
194
146
  let pkgJson;
195
147
  if (language === "typescript") pkgJson = getTypeScriptPackageJson(projectName, telemetry);
196
148
  else pkgJson = getJavaScriptPackageJson(projectName, telemetry);
@@ -211,6 +163,15 @@ async function createProject(
211
163
  await mkdir(join(srcDir, "controllers"), { recursive: true });
212
164
  await mkdir(join(srcDir, "schemas"), { recursive: true });
213
165
  await mkdir(join(srcDir, "cronjobs"), { recursive: true });
166
+ await mkdir(join(srcDir, "config"), { recursive: true });
167
+
168
+ if (language === "typescript") {
169
+ await writeFile(join(srcDir, "config", "index.ts"), "");
170
+ await writeFile(join(srcDir, "config", "clients.ts"), "");
171
+ } else {
172
+ await writeFile(join(srcDir, "config", "index.js"), "");
173
+ await writeFile(join(srcDir, "config", "clients.js"), "");
174
+ }
214
175
 
215
176
  await writeFile(join(srcDir, "middlewares", ".gitkeep"), "");
216
177