create-sprint 0.0.36 → 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,8 +1,15 @@
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 };
9
+ }
1
10
  export function getTypeScriptPackageJson(name, telemetry) {
2
11
  const deps = {
3
- "sprint-es": "^0.0.35",
4
- "node-cron": "^3.0.3",
5
- dotenv: "^17.0.0",
12
+ "sprint-es": "^0.0.38"
6
13
  };
7
14
  const devDeps = {
8
15
  "@types/node": "^22.0.0",
@@ -24,6 +31,7 @@ export function getTypeScriptPackageJson(name, telemetry) {
24
31
  build: "sprint-es build",
25
32
  start: "sprint-es start",
26
33
  dev: "sprint-es dev",
34
+ "generate:keys": "sprint-es generate-keys"
27
35
  },
28
36
  dependencies: deps,
29
37
  devDependencies: devDeps,
@@ -31,9 +39,7 @@ export function getTypeScriptPackageJson(name, telemetry) {
31
39
  }
32
40
  export function getJavaScriptPackageJson(name, telemetry) {
33
41
  const deps = {
34
- "sprint-es": "^0.0.35",
35
- "node-cron": "^3.0.3",
36
- dotenv: "^17.0.0",
42
+ "sprint-es": "^0.0.38"
37
43
  };
38
44
  if (telemetry === "sentry" || telemetry === "glitchtip") {
39
45
  deps["@sentry/node"] = "^8.0.0";
@@ -51,8 +57,9 @@ export function getJavaScriptPackageJson(name, telemetry) {
51
57
  build: "sprint-es build",
52
58
  start: "sprint-es start",
53
59
  dev: "sprint-es dev",
60
+ "generate:keys": "sprint-es generate-keys"
54
61
  },
55
- dependencies: deps,
62
+ dependencies: deps
56
63
  };
57
64
  }
58
65
  export function getTsConfig() {
@@ -147,24 +154,28 @@ export function getAdminRoute(language) {
147
154
  if (language === "typescript") {
148
155
  return `import { Router } from "sprint-es";
149
156
  import { adminSchema } from "@/schemas/admin";
150
- import { adminController, adminUsersController } from "@/controllers/admin";
157
+ import { adminController, adminUsersController, jwtGenerateController, jwtValidateController } from "@/controllers/admin";
151
158
 
152
159
  const router = Router();
153
160
 
154
161
  router.get("/", adminSchema, adminController);
155
162
  router.get("/users", adminSchema, adminUsersController);
163
+ router.post("/jwt/generate", jwtGenerateController);
164
+ router.post("/jwt/validate", jwtValidateController);
156
165
 
157
166
  export default router;
158
167
  `;
159
168
  }
160
169
  return `import { Router } from "sprint-es";
161
170
  import { adminSchema } from "../schemas/admin.js";
162
- import { adminController, adminUsersController } from "../controllers/admin.js";
171
+ import { adminController, adminUsersController, jwtGenerateController, jwtValidateController } from "../controllers/admin.js";
163
172
 
164
173
  const router = Router();
165
174
 
166
175
  router.get("/", adminSchema, adminController);
167
176
  router.get("/users", adminSchema, adminUsersController);
177
+ router.post("/jwt/generate", jwtGenerateController);
178
+ router.post("/jwt/validate", jwtValidateController);
168
179
 
169
180
  export default router;
170
181
  `;
@@ -194,6 +205,7 @@ export const homeController = (req, res) => {
194
205
  export function getAdminController(language) {
195
206
  if (language === "typescript") {
196
207
  return `import { Handler } from "sprint-es";
208
+ import { signEncrypted, verifyEncrypted, getJwtFromEnv } from "sprint-es/jwt";
197
209
 
198
210
  export const adminController: Handler = (req, res) => {
199
211
  res.json({
@@ -210,9 +222,48 @@ export const adminUsersController: Handler = (req, res) => {
210
222
  ]
211
223
  });
212
224
  };
225
+
226
+ export const jwtGenerateController: Handler = (req, res) => {
227
+ const { userId, role } = req.body || {};
228
+
229
+ if (!userId) {
230
+ return res.status(400).json({ error: "userId is required" });
231
+ }
232
+
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
+ }
241
+ };
242
+
243
+ export const jwtValidateController: Handler = (req, res) => {
244
+ const token = req.sprint?.getAuthorization() || req.headers.authorization;
245
+
246
+ if (!token) {
247
+ return res.status(401).json({ error: "No token provided" });
248
+ }
249
+
250
+ try {
251
+ const { publicKey } = getJwtFromEnv();
252
+ const decoded = verifyEncrypted(token, publicKey);
253
+
254
+ if (!decoded) {
255
+ return res.status(401).json({ error: "Invalid token" });
256
+ }
257
+
258
+ res.json({ valid: true, payload: decoded });
259
+ } catch (error) {
260
+ return res.status(500).json({ error: "JWT not configured" });
261
+ }
262
+ };
213
263
  `;
214
264
  }
215
265
  return `import { Handler } from "sprint-es";
266
+ import { signEncrypted, verifyEncrypted, getJwtFromEnv } from "sprint-es/jwt";
216
267
 
217
268
  export const adminController = (req, res) => {
218
269
  res.json({
@@ -229,6 +280,44 @@ export const adminUsersController = (req, res) => {
229
280
  ]
230
281
  });
231
282
  };
283
+
284
+ export const jwtGenerateController = (req, res) => {
285
+ const { userId, role } = req.body || {};
286
+
287
+ if (!userId) {
288
+ return res.status(400).json({ error: "userId is required" });
289
+ }
290
+
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
+ }
299
+ };
300
+
301
+ export const jwtValidateController = (req, res) => {
302
+ const token = req.sprint?.getAuthorization() || req.headers.authorization;
303
+
304
+ if (!token) {
305
+ return res.status(401).json({ error: "No token provided" });
306
+ }
307
+
308
+ try {
309
+ const { publicKey } = getJwtFromEnv();
310
+ const decoded = verifyEncrypted(token, publicKey);
311
+
312
+ if (!decoded) {
313
+ return res.status(401).json({ error: "Invalid token" });
314
+ }
315
+
316
+ res.json({ valid: true, payload: decoded });
317
+ } catch (error) {
318
+ return res.status(500).json({ error: "JWT not configured" });
319
+ }
320
+ };
232
321
  `;
233
322
  }
234
323
  export function getHomeSchema(language) {
@@ -511,8 +600,11 @@ DISCORD_WEBHOOK_URL=
511
600
  return env;
512
601
  }
513
602
  export function getEnvDevelopment(telemetry) {
603
+ const { publicKey, privateKey } = generateJWTKeys();
514
604
  let env = `NODE_ENV=development
515
605
  PORT=3000
606
+ JWT_PUBLIC_KEY=${publicKey.replace(/\n/g, "\\n")}
607
+ JWT_PRIVATE_KEY=${privateKey.replace(/\n/g, "\\n")}
516
608
  `;
517
609
  if (telemetry === "sentry" || telemetry === "glitchtip") {
518
610
  env += `
@@ -529,8 +621,11 @@ DISCORD_WEBHOOK_URL=
529
621
  return env;
530
622
  }
531
623
  export function getEnvProduction(telemetry) {
624
+ const { publicKey, privateKey } = generateJWTKeys();
532
625
  let env = `NODE_ENV=production
533
626
  PORT=3000
627
+ JWT_PUBLIC_KEY=${publicKey.replace(/\n/g, "\\n")}
628
+ JWT_PRIVATE_KEY=${privateKey.replace(/\n/g, "\\n")}
534
629
  `;
535
630
  if (telemetry === "sentry" || telemetry === "glitchtip") {
536
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));
@@ -193,8 +184,8 @@ async function createProject(projectName, language, telemetryArg, useDockerArg)
193
184
  await writeFile(join(srcDir, "cronjobs", "example." + (language === "typescript" ? "ts" : "js")), getExampleCronJob(language));
194
185
  await writeFile(join(targetDir, ".env.development.example"), getEnvDevelopment(telemetry));
195
186
  await writeFile(join(targetDir, ".env.production.example"), getEnvProduction(telemetry));
196
- await writeFile(join(targetDir, ".env.development"), "");
197
- await writeFile(join(targetDir, ".env.production"), "");
187
+ await writeFile(join(targetDir, ".env.development"), getEnvDevelopment(telemetry));
188
+ await writeFile(join(targetDir, ".env.production"), getEnvProduction(telemetry));
198
189
  await writeFile(join(targetDir, ".gitignore"), getGitignore());
199
190
  if (useDocker) {
200
191
  await writeFile(join(targetDir, "Dockerfile"), getDockerfile(language));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-sprint",
3
- "version": "0.0.36",
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,8 +1,22 @@
1
+ import { generateKeyPairSync } from "node:crypto";
2
+
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 };
15
+ }
16
+
1
17
  export function getTypeScriptPackageJson(name: string, telemetry: string) {
2
18
  const deps: Record<string, string> = {
3
- "sprint-es": "^0.0.35",
4
- "node-cron": "^3.0.3",
5
- dotenv: "^17.0.0",
19
+ "sprint-es": "^0.0.38"
6
20
  };
7
21
 
8
22
  const devDeps: Record<string, string> = {
@@ -26,6 +40,7 @@ export function getTypeScriptPackageJson(name: string, telemetry: string) {
26
40
  build: "sprint-es build",
27
41
  start: "sprint-es start",
28
42
  dev: "sprint-es dev",
43
+ "generate:keys": "sprint-es generate-keys"
29
44
  },
30
45
  dependencies: deps,
31
46
  devDependencies: devDeps,
@@ -34,9 +49,7 @@ export function getTypeScriptPackageJson(name: string, telemetry: string) {
34
49
 
35
50
  export function getJavaScriptPackageJson(name: string, telemetry: string) {
36
51
  const deps: Record<string, string> = {
37
- "sprint-es": "^0.0.35",
38
- "node-cron": "^3.0.3",
39
- dotenv: "^17.0.0",
52
+ "sprint-es": "^0.0.38"
40
53
  };
41
54
 
42
55
  if (telemetry === "sentry" || telemetry === "glitchtip") {
@@ -55,8 +68,9 @@ export function getJavaScriptPackageJson(name: string, telemetry: string) {
55
68
  build: "sprint-es build",
56
69
  start: "sprint-es start",
57
70
  dev: "sprint-es dev",
71
+ "generate:keys": "sprint-es generate-keys"
58
72
  },
59
- dependencies: deps,
73
+ dependencies: deps
60
74
  };
61
75
  }
62
76
 
@@ -157,24 +171,28 @@ export function getAdminRoute(language: string) {
157
171
  if (language === "typescript") {
158
172
  return `import { Router } from "sprint-es";
159
173
  import { adminSchema } from "@/schemas/admin";
160
- import { adminController, adminUsersController } from "@/controllers/admin";
174
+ import { adminController, adminUsersController, jwtGenerateController, jwtValidateController } from "@/controllers/admin";
161
175
 
162
176
  const router = Router();
163
177
 
164
178
  router.get("/", adminSchema, adminController);
165
179
  router.get("/users", adminSchema, adminUsersController);
180
+ router.post("/jwt/generate", jwtGenerateController);
181
+ router.post("/jwt/validate", jwtValidateController);
166
182
 
167
183
  export default router;
168
184
  `;
169
185
  }
170
186
  return `import { Router } from "sprint-es";
171
187
  import { adminSchema } from "../schemas/admin.js";
172
- import { adminController, adminUsersController } from "../controllers/admin.js";
188
+ import { adminController, adminUsersController, jwtGenerateController, jwtValidateController } from "../controllers/admin.js";
173
189
 
174
190
  const router = Router();
175
191
 
176
192
  router.get("/", adminSchema, adminController);
177
193
  router.get("/users", adminSchema, adminUsersController);
194
+ router.post("/jwt/generate", jwtGenerateController);
195
+ router.post("/jwt/validate", jwtValidateController);
178
196
 
179
197
  export default router;
180
198
  `;
@@ -206,6 +224,7 @@ export const homeController = (req, res) => {
206
224
  export function getAdminController(language: string) {
207
225
  if (language === "typescript") {
208
226
  return `import { Handler } from "sprint-es";
227
+ import { signEncrypted, verifyEncrypted, getJwtFromEnv } from "sprint-es/jwt";
209
228
 
210
229
  export const adminController: Handler = (req, res) => {
211
230
  res.json({
@@ -222,9 +241,48 @@ export const adminUsersController: Handler = (req, res) => {
222
241
  ]
223
242
  });
224
243
  };
244
+
245
+ export const jwtGenerateController: Handler = (req, res) => {
246
+ const { userId, role } = req.body || {};
247
+
248
+ if (!userId) {
249
+ return res.status(400).json({ error: "userId is required" });
250
+ }
251
+
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
+ }
260
+ };
261
+
262
+ export const jwtValidateController: Handler = (req, res) => {
263
+ const token = req.sprint?.getAuthorization() || req.headers.authorization;
264
+
265
+ if (!token) {
266
+ return res.status(401).json({ error: "No token provided" });
267
+ }
268
+
269
+ try {
270
+ const { publicKey } = getJwtFromEnv();
271
+ const decoded = verifyEncrypted(token, publicKey);
272
+
273
+ if (!decoded) {
274
+ return res.status(401).json({ error: "Invalid token" });
275
+ }
276
+
277
+ res.json({ valid: true, payload: decoded });
278
+ } catch (error) {
279
+ return res.status(500).json({ error: "JWT not configured" });
280
+ }
281
+ };
225
282
  `;
226
283
  }
227
284
  return `import { Handler } from "sprint-es";
285
+ import { signEncrypted, verifyEncrypted, getJwtFromEnv } from "sprint-es/jwt";
228
286
 
229
287
  export const adminController = (req, res) => {
230
288
  res.json({
@@ -241,6 +299,44 @@ export const adminUsersController = (req, res) => {
241
299
  ]
242
300
  });
243
301
  };
302
+
303
+ export const jwtGenerateController = (req, res) => {
304
+ const { userId, role } = req.body || {};
305
+
306
+ if (!userId) {
307
+ return res.status(400).json({ error: "userId is required" });
308
+ }
309
+
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
+ }
318
+ };
319
+
320
+ export const jwtValidateController = (req, res) => {
321
+ const token = req.sprint?.getAuthorization() || req.headers.authorization;
322
+
323
+ if (!token) {
324
+ return res.status(401).json({ error: "No token provided" });
325
+ }
326
+
327
+ try {
328
+ const { publicKey } = getJwtFromEnv();
329
+ const decoded = verifyEncrypted(token, publicKey);
330
+
331
+ if (!decoded) {
332
+ return res.status(401).json({ error: "Invalid token" });
333
+ }
334
+
335
+ res.json({ valid: true, payload: decoded });
336
+ } catch (error) {
337
+ return res.status(500).json({ error: "JWT not configured" });
338
+ }
339
+ };
244
340
  `;
245
341
  }
246
342
 
@@ -537,8 +633,11 @@ DISCORD_WEBHOOK_URL=
537
633
  }
538
634
 
539
635
  export function getEnvDevelopment(telemetry: string) {
636
+ const { publicKey, privateKey } = generateJWTKeys();
540
637
  let env = `NODE_ENV=development
541
638
  PORT=3000
639
+ JWT_PUBLIC_KEY=${publicKey.replace(/\n/g, "\\n")}
640
+ JWT_PRIVATE_KEY=${privateKey.replace(/\n/g, "\\n")}
542
641
  `;
543
642
 
544
643
  if (telemetry === "sentry" || telemetry === "glitchtip") {
@@ -557,8 +656,11 @@ DISCORD_WEBHOOK_URL=
557
656
  }
558
657
 
559
658
  export function getEnvProduction(telemetry: string) {
659
+ const { publicKey, privateKey } = generateJWTKeys();
560
660
  let env = `NODE_ENV=production
561
661
  PORT=3000
662
+ JWT_PUBLIC_KEY=${publicKey.replace(/\n/g, "\\n")}
663
+ JWT_PRIVATE_KEY=${privateKey.replace(/\n/g, "\\n")}
562
664
  `;
563
665
 
564
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
 
@@ -232,8 +234,8 @@ async function createProject(
232
234
  await writeFile(join(targetDir, ".env.development.example"), getEnvDevelopment(telemetry));
233
235
  await writeFile(join(targetDir, ".env.production.example"), getEnvProduction(telemetry));
234
236
 
235
- await writeFile(join(targetDir, ".env.development"), "");
236
- await writeFile(join(targetDir, ".env.production"), "");
237
+ await writeFile(join(targetDir, ".env.development"), getEnvDevelopment(telemetry));
238
+ await writeFile(join(targetDir, ".env.production"), getEnvProduction(telemetry));
237
239
 
238
240
  await writeFile(join(targetDir, ".gitignore"), getGitignore());
239
241