yantr-js 0.1.0-beta.2

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.
package/dist/index.js ADDED
@@ -0,0 +1,1023 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/init.ts
7
+ import * as p from "@clack/prompts";
8
+ import chalk from "chalk";
9
+ import path4 from "path";
10
+ import fs4 from "fs-extra";
11
+
12
+ // src/lib/installer.ts
13
+ import { execa } from "execa";
14
+ import fs from "fs-extra";
15
+ import path from "path";
16
+ async function detectPackageManager(cwd) {
17
+ const lockFiles = {
18
+ "pnpm-lock.yaml": "pnpm",
19
+ "yarn.lock": "yarn",
20
+ "package-lock.json": "npm",
21
+ "bun.lockb": "bun"
22
+ };
23
+ for (const [lockFile, pm] of Object.entries(lockFiles)) {
24
+ if (await fs.pathExists(path.join(cwd, lockFile))) {
25
+ return pm;
26
+ }
27
+ }
28
+ return null;
29
+ }
30
+ async function runPackageManager(pm, args, cwd) {
31
+ await execa(pm, args, { cwd, stdio: "inherit" });
32
+ }
33
+ async function installDependencies(pm, packages, cwd, isDev = false) {
34
+ const args = [];
35
+ switch (pm) {
36
+ case "npm":
37
+ args.push("install", isDev ? "-D" : "", ...packages);
38
+ break;
39
+ case "pnpm":
40
+ args.push("add", isDev ? "-D" : "", ...packages);
41
+ break;
42
+ case "yarn":
43
+ args.push("add", isDev ? "-D" : "", ...packages);
44
+ break;
45
+ case "bun":
46
+ args.push("add", isDev ? "-d" : "", ...packages);
47
+ break;
48
+ }
49
+ const filteredArgs = args.filter(Boolean);
50
+ await runPackageManager(pm, filteredArgs, cwd);
51
+ }
52
+
53
+ // src/lib/config.ts
54
+ import fs2 from "fs-extra";
55
+ import path2 from "path";
56
+ import { z } from "zod";
57
+ var SetuConfigSchema = z.object({
58
+ $schema: z.string().optional(),
59
+ projectName: z.string(),
60
+ srcDir: z.string().default("./src"),
61
+ packageManager: z.enum(["npm", "pnpm", "yarn", "bun"]),
62
+ installedComponents: z.array(z.string()).default([])
63
+ });
64
+ var CONFIG_FILE = "yantr.json";
65
+ async function configExists(cwd) {
66
+ return fs2.pathExists(path2.join(cwd, CONFIG_FILE));
67
+ }
68
+ async function readConfig(cwd) {
69
+ const configPath = path2.join(cwd, CONFIG_FILE);
70
+ if (!await fs2.pathExists(configPath)) {
71
+ throw new Error('yantr.json not found. Run "setu init" first.');
72
+ }
73
+ const content = await fs2.readJson(configPath);
74
+ return SetuConfigSchema.parse(content);
75
+ }
76
+ async function writeConfig(cwd, config) {
77
+ const configPath = path2.join(cwd, CONFIG_FILE);
78
+ await fs2.writeJson(configPath, config, { spaces: 2 });
79
+ }
80
+ function createConfig(projectName, srcDir, packageManager) {
81
+ return {
82
+ $schema: "https://raw.githubusercontent.com/SibilSoren/setu-js/main/cli/schema.json",
83
+ projectName,
84
+ srcDir,
85
+ packageManager,
86
+ installedComponents: []
87
+ };
88
+ }
89
+ async function addInstalledComponent(cwd, component) {
90
+ const config = await readConfig(cwd);
91
+ if (!config.installedComponents.includes(component)) {
92
+ config.installedComponents.push(component);
93
+ await writeConfig(cwd, config);
94
+ }
95
+ }
96
+ async function isComponentInstalled(cwd, component) {
97
+ const config = await readConfig(cwd);
98
+ return config.installedComponents.includes(component);
99
+ }
100
+
101
+ // src/utils/fs.ts
102
+ import fs3 from "fs-extra";
103
+ import path3 from "path";
104
+ async function ensureDir(dirPath) {
105
+ await fs3.ensureDir(dirPath);
106
+ }
107
+ async function writeFile(filePath, content) {
108
+ await fs3.ensureDir(path3.dirname(filePath));
109
+ await fs3.writeFile(filePath, content, "utf-8");
110
+ }
111
+ async function fileExists(filePath) {
112
+ return fs3.pathExists(filePath);
113
+ }
114
+ async function isNodeProject(cwd) {
115
+ return fs3.pathExists(path3.join(cwd, "package.json"));
116
+ }
117
+ async function getProjectName(cwd) {
118
+ const pkgPath = path3.join(cwd, "package.json");
119
+ if (await fs3.pathExists(pkgPath)) {
120
+ const pkg = await fs3.readJson(pkgPath);
121
+ return pkg.name || null;
122
+ }
123
+ return null;
124
+ }
125
+ async function hasSrcDirectory(cwd) {
126
+ return fs3.pathExists(path3.join(cwd, "src"));
127
+ }
128
+
129
+ // src/commands/init.ts
130
+ async function init(options) {
131
+ console.clear();
132
+ p.intro(chalk.bgCyan.black(" \u{1FA9B} yantr init "));
133
+ const cwd = process.cwd();
134
+ const isNode = await isNodeProject(cwd);
135
+ if (!isNode) {
136
+ p.log.error("No package.json found in current directory.");
137
+ p.log.info("Please run this command in an existing Node.js project, or run:");
138
+ p.log.info(chalk.cyan(" npm init -y"));
139
+ p.outro(chalk.red("Initialization cancelled."));
140
+ process.exit(1);
141
+ }
142
+ const hasConfig = await configExists(cwd);
143
+ if (hasConfig && !options.yes) {
144
+ const overwrite = await p.confirm({
145
+ message: "yantr.json already exists. Overwrite?",
146
+ initialValue: false
147
+ });
148
+ if (p.isCancel(overwrite) || !overwrite) {
149
+ p.outro(chalk.yellow("Initialization cancelled."));
150
+ process.exit(0);
151
+ }
152
+ }
153
+ let projectName;
154
+ let srcDir;
155
+ let packageManager;
156
+ if (options.yes) {
157
+ projectName = await getProjectName(cwd) || path4.basename(cwd);
158
+ srcDir = await hasSrcDirectory(cwd) ? "./src" : ".";
159
+ packageManager = await detectPackageManager(cwd) || "npm";
160
+ } else {
161
+ const detectedPm = await detectPackageManager(cwd);
162
+ const detectedName = await getProjectName(cwd) || path4.basename(cwd);
163
+ const hasSrc = await hasSrcDirectory(cwd);
164
+ const responses = await p.group(
165
+ {
166
+ projectName: () => p.text({
167
+ message: "Project name:",
168
+ defaultValue: detectedName,
169
+ placeholder: detectedName
170
+ }),
171
+ srcDir: () => p.select({
172
+ message: "Where should Yantr put generated files?",
173
+ initialValue: hasSrc ? "./src" : ".",
174
+ options: [
175
+ { value: "./src", label: "./src (recommended)" },
176
+ { value: ".", label: ". (project root)" },
177
+ { value: "./lib", label: "./lib" }
178
+ ]
179
+ }),
180
+ packageManager: () => p.select({
181
+ message: "Package manager:",
182
+ initialValue: detectedPm || "npm",
183
+ options: [
184
+ { value: "npm", label: "npm" },
185
+ { value: "pnpm", label: "pnpm" },
186
+ { value: "yarn", label: "yarn" },
187
+ { value: "bun", label: "bun" }
188
+ ]
189
+ })
190
+ },
191
+ {
192
+ onCancel: () => {
193
+ p.cancel("Initialization cancelled.");
194
+ process.exit(0);
195
+ }
196
+ }
197
+ );
198
+ projectName = responses.projectName;
199
+ srcDir = responses.srcDir;
200
+ packageManager = responses.packageManager;
201
+ }
202
+ const spinner4 = p.spinner();
203
+ spinner4.start("Creating configuration...");
204
+ const config = createConfig(projectName, srcDir, packageManager);
205
+ await writeConfig(cwd, config);
206
+ spinner4.stop("Created yantr.json");
207
+ spinner4.start("Setting up base templates...");
208
+ const templatesDir = path4.join(srcDir, "lib", "yantr");
209
+ await ensureDir(path4.join(cwd, templatesDir));
210
+ const cliDir = path4.dirname(new URL(import.meta.url).pathname);
211
+ const registryPath = path4.join(cliDir, "..", "..", "registry", "templates", "base");
212
+ const baseTemplatesPath = await fs4.pathExists(registryPath) ? registryPath : path4.join(cliDir, "registry", "templates", "base");
213
+ const errorHandlerContent = `import type { Request, Response, NextFunction } from 'express';
214
+
215
+ /**
216
+ * Custom error class for application errors
217
+ */
218
+ export class AppError extends Error {
219
+ public readonly statusCode: number;
220
+ public readonly isOperational: boolean;
221
+ public readonly code?: string;
222
+
223
+ constructor(
224
+ message: string,
225
+ statusCode: number = 500,
226
+ isOperational: boolean = true,
227
+ code?: string
228
+ ) {
229
+ super(message);
230
+ this.statusCode = statusCode;
231
+ this.isOperational = isOperational;
232
+ this.code = code;
233
+
234
+ Object.setPrototypeOf(this, AppError.prototype);
235
+ Error.captureStackTrace(this, this.constructor);
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Common HTTP error classes
241
+ */
242
+ export class NotFoundError extends AppError {
243
+ constructor(message: string = 'Resource not found') {
244
+ super(message, 404, true, 'NOT_FOUND');
245
+ }
246
+ }
247
+
248
+ export class BadRequestError extends AppError {
249
+ constructor(message: string = 'Bad request') {
250
+ super(message, 400, true, 'BAD_REQUEST');
251
+ }
252
+ }
253
+
254
+ export class UnauthorizedError extends AppError {
255
+ constructor(message: string = 'Unauthorized') {
256
+ super(message, 401, true, 'UNAUTHORIZED');
257
+ }
258
+ }
259
+
260
+ export class ForbiddenError extends AppError {
261
+ constructor(message: string = 'Forbidden') {
262
+ super(message, 403, true, 'FORBIDDEN');
263
+ }
264
+ }
265
+
266
+ export class ConflictError extends AppError {
267
+ constructor(message: string = 'Conflict') {
268
+ super(message, 409, true, 'CONFLICT');
269
+ }
270
+ }
271
+
272
+ export class ValidationError extends AppError {
273
+ public readonly errors: Record<string, string[]>;
274
+
275
+ constructor(errors: Record<string, string[]>) {
276
+ super('Validation failed', 422, true, 'VALIDATION_ERROR');
277
+ this.errors = errors;
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Global error handler middleware
283
+ */
284
+ export function errorHandler(
285
+ err: Error,
286
+ _req: Request,
287
+ res: Response,
288
+ _next: NextFunction
289
+ ): void {
290
+ console.error('[Error]', err);
291
+
292
+ if (err instanceof AppError) {
293
+ const response: Record<string, unknown> = {
294
+ success: false,
295
+ error: {
296
+ message: err.message,
297
+ code: err.code,
298
+ },
299
+ };
300
+
301
+ if (err instanceof ValidationError) {
302
+ response.error = {
303
+ ...response.error as object,
304
+ details: err.errors,
305
+ };
306
+ }
307
+
308
+ res.status(err.statusCode).json(response);
309
+ return;
310
+ }
311
+
312
+ const statusCode = 500;
313
+ const message = process.env.NODE_ENV === 'production'
314
+ ? 'Internal server error'
315
+ : err.message;
316
+
317
+ res.status(statusCode).json({
318
+ success: false,
319
+ error: {
320
+ message,
321
+ code: 'INTERNAL_ERROR',
322
+ },
323
+ });
324
+ }
325
+
326
+ /**
327
+ * Async handler wrapper to catch errors in async route handlers
328
+ */
329
+ export function asyncHandler(
330
+ fn: (req: Request, res: Response, next: NextFunction) => Promise<void>
331
+ ) {
332
+ return (req: Request, res: Response, next: NextFunction) => {
333
+ Promise.resolve(fn(req, res, next)).catch(next);
334
+ };
335
+ }
336
+ `;
337
+ const zodMiddlewareContent = `import type { Request, Response, NextFunction } from 'express';
338
+ import { z, ZodError, ZodSchema } from 'zod';
339
+ import { ValidationError } from './error-handler';
340
+
341
+ type RequestLocation = 'body' | 'query' | 'params';
342
+
343
+ interface ValidateOptions {
344
+ body?: ZodSchema;
345
+ query?: ZodSchema;
346
+ params?: ZodSchema;
347
+ }
348
+
349
+ /**
350
+ * Format Zod errors into a readable format
351
+ */
352
+ function formatZodErrors(error: ZodError): Record<string, string[]> {
353
+ const errors: Record<string, string[]> = {};
354
+
355
+ for (const issue of error.issues) {
356
+ const path = issue.path.join('.');
357
+ const key = path || 'root';
358
+
359
+ if (!errors[key]) {
360
+ errors[key] = [];
361
+ }
362
+
363
+ errors[key].push(issue.message);
364
+ }
365
+
366
+ return errors;
367
+ }
368
+
369
+ /**
370
+ * Validate request data against Zod schemas
371
+ */
372
+ export function validate(schemas: ValidateOptions) {
373
+ return async (req: Request, _res: Response, next: NextFunction) => {
374
+ const allErrors: Record<string, string[]> = {};
375
+ const locations: RequestLocation[] = ['body', 'query', 'params'];
376
+
377
+ for (const location of locations) {
378
+ const schema = schemas[location];
379
+
380
+ if (schema) {
381
+ const result = await schema.safeParseAsync(req[location]);
382
+
383
+ if (!result.success) {
384
+ const errors = formatZodErrors(result.error);
385
+
386
+ for (const [key, messages] of Object.entries(errors)) {
387
+ const prefixedKey = \`\${location}.\${key}\`;
388
+ allErrors[prefixedKey] = messages;
389
+ }
390
+ } else {
391
+ req[location] = result.data;
392
+ }
393
+ }
394
+ }
395
+
396
+ if (Object.keys(allErrors).length > 0) {
397
+ return next(new ValidationError(allErrors));
398
+ }
399
+
400
+ next();
401
+ };
402
+ }
403
+
404
+ export function validateBody<T extends ZodSchema>(schema: T) {
405
+ return validate({ body: schema });
406
+ }
407
+
408
+ export function validateQuery<T extends ZodSchema>(schema: T) {
409
+ return validate({ query: schema });
410
+ }
411
+
412
+ export function validateParams<T extends ZodSchema>(schema: T) {
413
+ return validate({ params: schema });
414
+ }
415
+
416
+ export { z };
417
+ `;
418
+ await writeFile(
419
+ path4.join(cwd, templatesDir, "error-handler.ts"),
420
+ errorHandlerContent
421
+ );
422
+ await writeFile(
423
+ path4.join(cwd, templatesDir, "zod-middleware.ts"),
424
+ zodMiddlewareContent
425
+ );
426
+ spinner4.stop("Base templates created");
427
+ spinner4.start("Installing dependencies...");
428
+ try {
429
+ await installDependencies(packageManager, ["zod"], cwd, false);
430
+ spinner4.stop("Dependencies installed");
431
+ } catch (error) {
432
+ spinner4.stop("Could not install dependencies automatically");
433
+ p.log.warning(`Please run: ${chalk.cyan(`${packageManager} add zod`)}`);
434
+ }
435
+ p.note(
436
+ `${chalk.green("\u2713")} yantr.json created
437
+ ${chalk.green("\u2713")} ${templatesDir}/error-handler.ts
438
+ ${chalk.green("\u2713")} ${templatesDir}/zod-middleware.ts`,
439
+ "Files created"
440
+ );
441
+ p.log.info("Next steps:");
442
+ p.log.step(1, `Add error handler to your Express app:`);
443
+ console.log(chalk.gray(` import { errorHandler } from '${templatesDir}/error-handler';`));
444
+ console.log(chalk.gray(` app.use(errorHandler);`));
445
+ p.log.step(2, `Add components: ${chalk.cyan("yantr add auth")}`);
446
+ p.log.step(3, `Generate routes: ${chalk.cyan("yantr generate route users")}`);
447
+ p.outro(chalk.green("Yantr initialized successfully! \u{1FA9B}"));
448
+ }
449
+
450
+ // src/commands/add.ts
451
+ import * as p2 from "@clack/prompts";
452
+ import chalk2 from "chalk";
453
+ import path6 from "path";
454
+
455
+ // src/lib/registry.ts
456
+ import { z as z2 } from "zod";
457
+ import fs5 from "fs-extra";
458
+ import path5 from "path";
459
+ import { fileURLToPath } from "url";
460
+ var ComponentSchema = z2.object({
461
+ name: z2.string(),
462
+ description: z2.string(),
463
+ files: z2.array(z2.string()),
464
+ dependencies: z2.array(z2.string()).default([]),
465
+ devDependencies: z2.array(z2.string()).default([])
466
+ });
467
+ var RegistrySchema = z2.object({
468
+ version: z2.string(),
469
+ baseUrl: z2.string(),
470
+ components: z2.record(z2.string(), ComponentSchema)
471
+ });
472
+ var REGISTRY_URL = "https://raw.githubusercontent.com/SibilSoren/setu-js/main/cli/registry/registry.json";
473
+ var CACHE_DIR = ".yantr-cache";
474
+ var CACHE_TTL_MS = 1e3 * 60 * 60;
475
+ function getCacheDir() {
476
+ const homeDir = process.env.HOME || process.env.USERPROFILE || ".";
477
+ return path5.join(homeDir, CACHE_DIR);
478
+ }
479
+ async function getCachedRegistry() {
480
+ const cacheDir = getCacheDir();
481
+ const cachePath = path5.join(cacheDir, "registry.json");
482
+ try {
483
+ if (!await fs5.pathExists(cachePath)) {
484
+ return null;
485
+ }
486
+ const stats = await fs5.stat(cachePath);
487
+ const age = Date.now() - stats.mtimeMs;
488
+ if (age > CACHE_TTL_MS) {
489
+ return null;
490
+ }
491
+ const content = await fs5.readJson(cachePath);
492
+ return RegistrySchema.parse(content);
493
+ } catch {
494
+ return null;
495
+ }
496
+ }
497
+ async function cacheRegistry(registry) {
498
+ const cacheDir = getCacheDir();
499
+ const cachePath = path5.join(cacheDir, "registry.json");
500
+ try {
501
+ await fs5.ensureDir(cacheDir);
502
+ await fs5.writeJson(cachePath, registry);
503
+ } catch {
504
+ }
505
+ }
506
+ async function loadLocalRegistry() {
507
+ const __filename2 = fileURLToPath(import.meta.url);
508
+ const __dirname2 = path5.dirname(__filename2);
509
+ const possiblePaths = [
510
+ // From source: cli/src/lib -> cli/registry
511
+ path5.join(__dirname2, "..", "..", "registry", "registry.json"),
512
+ // From dist: cli/dist -> cli/registry
513
+ path5.join(__dirname2, "..", "registry", "registry.json"),
514
+ // Direct path from built bundle
515
+ path5.resolve(__dirname2, "..", "registry", "registry.json")
516
+ ];
517
+ for (const registryPath of possiblePaths) {
518
+ if (await fs5.pathExists(registryPath)) {
519
+ const content = await fs5.readJson(registryPath);
520
+ return RegistrySchema.parse(content);
521
+ }
522
+ }
523
+ throw new Error(`Local registry not found. Tried: ${possiblePaths.join(", ")}`);
524
+ }
525
+ async function loadLocalTemplateFile(filePath) {
526
+ const __filename2 = fileURLToPath(import.meta.url);
527
+ const __dirname2 = path5.dirname(__filename2);
528
+ const possiblePaths = [
529
+ path5.join(__dirname2, "..", "..", "registry", "templates", filePath),
530
+ path5.join(__dirname2, "..", "registry", "templates", filePath)
531
+ ];
532
+ for (const templatePath of possiblePaths) {
533
+ if (await fs5.pathExists(templatePath)) {
534
+ return fs5.readFile(templatePath, "utf-8");
535
+ }
536
+ }
537
+ return null;
538
+ }
539
+ async function getRegistry() {
540
+ const cached = await getCachedRegistry();
541
+ if (cached) {
542
+ return cached;
543
+ }
544
+ try {
545
+ const local = await loadLocalRegistry();
546
+ return local;
547
+ } catch {
548
+ }
549
+ try {
550
+ const controller = new AbortController();
551
+ const timeoutId = setTimeout(() => controller.abort(), 5e3);
552
+ const response = await fetch(REGISTRY_URL, { signal: controller.signal });
553
+ clearTimeout(timeoutId);
554
+ if (!response.ok) {
555
+ throw new Error(`HTTP ${response.status}`);
556
+ }
557
+ const data = await response.json();
558
+ const remote = RegistrySchema.parse(data);
559
+ await cacheRegistry(remote);
560
+ return remote;
561
+ } catch (error) {
562
+ throw new Error(`Failed to load registry: ${error instanceof Error ? error.message : String(error)}`);
563
+ }
564
+ }
565
+ async function getComponent(name) {
566
+ const registry = await getRegistry();
567
+ return registry.components[name] || null;
568
+ }
569
+ async function listComponents() {
570
+ const registry = await getRegistry();
571
+ return Object.keys(registry.components);
572
+ }
573
+ async function fetchTemplateFile(filePath) {
574
+ const local = await loadLocalTemplateFile(filePath);
575
+ if (local) {
576
+ return local;
577
+ }
578
+ const registry = await getRegistry();
579
+ const url = `${registry.baseUrl}/${filePath}`;
580
+ const controller = new AbortController();
581
+ const timeoutId = setTimeout(() => controller.abort(), 5e3);
582
+ const response = await fetch(url, { signal: controller.signal });
583
+ clearTimeout(timeoutId);
584
+ if (!response.ok) {
585
+ throw new Error(`Failed to fetch template: ${url} (${response.status})`);
586
+ }
587
+ return response.text();
588
+ }
589
+ async function fetchComponentFiles(componentName) {
590
+ const component = await getComponent(componentName);
591
+ if (!component) {
592
+ throw new Error(`Component not found: ${componentName}`);
593
+ }
594
+ const files = /* @__PURE__ */ new Map();
595
+ for (const filePath of component.files) {
596
+ const content = await fetchTemplateFile(filePath);
597
+ files.set(filePath, content);
598
+ }
599
+ return files;
600
+ }
601
+
602
+ // src/commands/add.ts
603
+ async function add(componentName, options) {
604
+ console.clear();
605
+ p2.intro(chalk2.bgCyan.black(` \u{1FA9B} yantr add ${componentName} `));
606
+ try {
607
+ const cwd = process.cwd();
608
+ if (!await configExists(cwd)) {
609
+ p2.log.error("yantr.json not found.");
610
+ p2.log.info("Please run " + chalk2.cyan("yantr init") + " first.");
611
+ p2.outro(chalk2.red("Add cancelled."));
612
+ process.exit(1);
613
+ }
614
+ const config = await readConfig(cwd);
615
+ const spinner4 = p2.spinner();
616
+ spinner4.start("Fetching component registry...");
617
+ const availableComponents = await listComponents();
618
+ if (!availableComponents.includes(componentName)) {
619
+ spinner4.stop("Component not found");
620
+ p2.log.error(`Unknown component: ${chalk2.red(componentName)}`);
621
+ p2.log.info(`Available components: ${chalk2.cyan(availableComponents.join(", "))}`);
622
+ p2.outro(chalk2.red("Add cancelled."));
623
+ process.exit(1);
624
+ }
625
+ const alreadyInstalled = await isComponentInstalled(cwd, componentName);
626
+ if (alreadyInstalled && !options.overwrite) {
627
+ spinner4.stop("Component already installed");
628
+ const overwrite = await p2.confirm({
629
+ message: `${componentName} is already installed. Overwrite?`,
630
+ initialValue: false
631
+ });
632
+ if (p2.isCancel(overwrite) || !overwrite) {
633
+ p2.outro(chalk2.yellow("Add cancelled."));
634
+ process.exit(0);
635
+ }
636
+ }
637
+ spinner4.start(`Downloading ${componentName}...`);
638
+ const component = await getComponent(componentName);
639
+ if (!component) {
640
+ spinner4.stop("Failed to fetch component");
641
+ p2.log.error("Failed to get component metadata");
642
+ process.exit(1);
643
+ }
644
+ const files = await fetchComponentFiles(componentName);
645
+ spinner4.stop(`Downloaded ${files.size} files`);
646
+ spinner4.start("Installing files...");
647
+ const installedFiles = [];
648
+ const baseDir = path6.join(cwd, config.srcDir, "lib", "yantr");
649
+ for (const [filePath, content] of files) {
650
+ const fileName = path6.basename(filePath);
651
+ const targetPath = path6.join(baseDir, componentName, fileName);
652
+ if (await fileExists(targetPath) && !options.overwrite && !alreadyInstalled) {
653
+ const shouldOverwrite = await p2.confirm({
654
+ message: `File ${fileName} already exists. Overwrite?`,
655
+ initialValue: false
656
+ });
657
+ if (p2.isCancel(shouldOverwrite) || !shouldOverwrite) {
658
+ continue;
659
+ }
660
+ }
661
+ await writeFile(targetPath, content);
662
+ installedFiles.push(path6.relative(cwd, targetPath));
663
+ }
664
+ spinner4.stop("Files installed");
665
+ if (component.dependencies.length > 0 || component.devDependencies.length > 0) {
666
+ spinner4.start("Installing dependencies...");
667
+ try {
668
+ if (component.dependencies.length > 0) {
669
+ await installDependencies(
670
+ config.packageManager,
671
+ component.dependencies,
672
+ cwd,
673
+ false
674
+ );
675
+ }
676
+ if (component.devDependencies.length > 0) {
677
+ await installDependencies(
678
+ config.packageManager,
679
+ component.devDependencies,
680
+ cwd,
681
+ true
682
+ );
683
+ }
684
+ spinner4.stop("Dependencies installed");
685
+ } catch (error) {
686
+ spinner4.stop("Could not install dependencies automatically");
687
+ if (component.dependencies.length > 0) {
688
+ p2.log.warning(
689
+ `Please run: ${chalk2.cyan(`${config.packageManager} add ${component.dependencies.join(" ")}`)}`
690
+ );
691
+ }
692
+ if (component.devDependencies.length > 0) {
693
+ p2.log.warning(
694
+ `Please run: ${chalk2.cyan(`${config.packageManager} add -D ${component.devDependencies.join(" ")}`)}`
695
+ );
696
+ }
697
+ }
698
+ }
699
+ await addInstalledComponent(cwd, componentName);
700
+ const filesNote = installedFiles.map((f) => `${chalk2.green("\u2713")} ${f}`).join("\n");
701
+ p2.note(filesNote, "Files created");
702
+ if (component.dependencies.length > 0) {
703
+ p2.log.info(`Dependencies: ${chalk2.cyan(component.dependencies.join(", "))}`);
704
+ }
705
+ const usageHints = {
706
+ auth: [
707
+ `Import auth routes: ${chalk2.gray(`import authRoutes from '${config.srcDir}/lib/yantr/auth/auth.routes';`)}`,
708
+ `Add to app: ${chalk2.gray('app.use("/api/auth", authRoutes);')}`
709
+ ],
710
+ logger: [
711
+ `Import logger: ${chalk2.gray(`import { logger } from '${config.srcDir}/lib/yantr/logger/logger';`)}`,
712
+ `Use HTTP logging: ${chalk2.gray("app.use(httpLogger);")}`
713
+ ],
714
+ database: [
715
+ `Import prisma: ${chalk2.gray(`import { prisma } from '${config.srcDir}/lib/yantr/database/prisma';`)}`,
716
+ `Initialize: ${chalk2.gray("npx prisma init")}`
717
+ ],
718
+ security: [
719
+ `Import security: ${chalk2.gray(`import { rateLimiter, helmetConfig } from '${config.srcDir}/lib/yantr/security';`)}`,
720
+ `Add to app: ${chalk2.gray("app.use(helmetConfig); app.use(rateLimiter);")}`
721
+ ]
722
+ };
723
+ if (usageHints[componentName]) {
724
+ p2.log.info("Usage:");
725
+ usageHints[componentName].forEach((hint, i) => {
726
+ console.log(` ${i + 1}. ${hint}`);
727
+ });
728
+ }
729
+ p2.outro(chalk2.green(`${component.name} added successfully! \u{1F309}`));
730
+ } catch (error) {
731
+ p2.log.error(`Failed: ${error instanceof Error ? error.message : String(error)}`);
732
+ console.error(error);
733
+ process.exit(1);
734
+ }
735
+ }
736
+
737
+ // src/commands/generate.ts
738
+ import * as p3 from "@clack/prompts";
739
+ import chalk3 from "chalk";
740
+ import path7 from "path";
741
+ async function generate(type, name, _options) {
742
+ console.clear();
743
+ p3.intro(chalk3.bgCyan.black(` \u{1FA9B} yantr generate ${type} ${name} `));
744
+ try {
745
+ const cwd = process.cwd();
746
+ if (!await configExists(cwd)) {
747
+ p3.log.error("yantr.json not found.");
748
+ p3.log.info("Please run " + chalk3.cyan("yantr init") + " first.");
749
+ p3.outro(chalk3.red("Generate cancelled."));
750
+ process.exit(1);
751
+ }
752
+ const config = await readConfig(cwd);
753
+ const validTypes = ["route"];
754
+ if (!validTypes.includes(type)) {
755
+ p3.log.error(`Unknown type: ${chalk3.red(type)}`);
756
+ p3.log.info(`Available types: ${chalk3.cyan(validTypes.join(", "))}`);
757
+ p3.outro(chalk3.red("Generate cancelled."));
758
+ process.exit(1);
759
+ }
760
+ if (type === "route") {
761
+ await generateRoute(cwd, config.srcDir, name);
762
+ }
763
+ p3.outro(chalk3.green(`Generated ${type} "${name}" successfully! \u{1FA9B}`));
764
+ } catch (error) {
765
+ p3.log.error(`Failed: ${error instanceof Error ? error.message : String(error)}`);
766
+ process.exit(1);
767
+ }
768
+ }
769
+ async function generateRoute(cwd, srcDir, name) {
770
+ const spinner4 = p3.spinner();
771
+ const normalizedName = name.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
772
+ const pascalName = toPascalCase(normalizedName);
773
+ const camelName = toCamelCase(normalizedName);
774
+ const baseDir = path7.join(cwd, srcDir);
775
+ const routesDir = path7.join(baseDir, "routes");
776
+ const controllersDir = path7.join(baseDir, "controllers");
777
+ const servicesDir = path7.join(baseDir, "services");
778
+ const files = {
779
+ route: path7.join(routesDir, `${normalizedName}.routes.ts`),
780
+ controller: path7.join(controllersDir, `${normalizedName}.controller.ts`),
781
+ service: path7.join(servicesDir, `${normalizedName}.service.ts`)
782
+ };
783
+ for (const [type, filePath] of Object.entries(files)) {
784
+ if (await fileExists(filePath)) {
785
+ const overwrite = await p3.confirm({
786
+ message: `${type} file already exists. Overwrite?`,
787
+ initialValue: false
788
+ });
789
+ if (p3.isCancel(overwrite) || !overwrite) {
790
+ p3.outro(chalk3.yellow("Generate cancelled."));
791
+ process.exit(0);
792
+ }
793
+ }
794
+ }
795
+ spinner4.start("Generating files...");
796
+ const serviceContent = generateServiceTemplate(pascalName, camelName);
797
+ await writeFile(files.service, serviceContent);
798
+ const controllerContent = generateControllerTemplate(pascalName, camelName, normalizedName);
799
+ await writeFile(files.controller, controllerContent);
800
+ const routeContent = generateRouteTemplate(pascalName, camelName, normalizedName);
801
+ await writeFile(files.route, routeContent);
802
+ spinner4.stop("Files generated");
803
+ const createdFiles = Object.values(files).map(
804
+ (f) => `${chalk3.green("\u2713")} ${path7.relative(cwd, f)}`
805
+ ).join("\n");
806
+ p3.note(createdFiles, "Files created");
807
+ p3.log.info("Add to your app:");
808
+ console.log(chalk3.gray(` import ${camelName}Routes from './${srcDir}/routes/${normalizedName}.routes';`));
809
+ console.log(chalk3.gray(` app.use('/api/${normalizedName}', ${camelName}Routes);`));
810
+ }
811
+ function generateServiceTemplate(pascalName, _camelName) {
812
+ return `/**
813
+ * ${pascalName} Service
814
+ *
815
+ * Business logic for ${pascalName.toLowerCase()} operations.
816
+ */
817
+
818
+ export interface ${pascalName} {
819
+ id: string;
820
+ // Add your fields here
821
+ createdAt: Date;
822
+ updatedAt: Date;
823
+ }
824
+
825
+ export interface Create${pascalName}Input {
826
+ // Add your input fields here
827
+ }
828
+
829
+ export interface Update${pascalName}Input {
830
+ // Add your update fields here
831
+ }
832
+
833
+ /**
834
+ * Get all ${pascalName.toLowerCase()}s
835
+ */
836
+ export async function getAll(): Promise<${pascalName}[]> {
837
+ // TODO: Implement database query
838
+ // Example: return prisma.${_camelName}.findMany();
839
+ return [];
840
+ }
841
+
842
+ /**
843
+ * Get ${pascalName.toLowerCase()} by ID
844
+ */
845
+ export async function getById(id: string): Promise<${pascalName} | null> {
846
+ // TODO: Implement database query
847
+ // Example: return prisma.${_camelName}.findUnique({ where: { id } });
848
+ return null;
849
+ }
850
+
851
+ /**
852
+ * Create a new ${pascalName.toLowerCase()}
853
+ */
854
+ export async function create(input: Create${pascalName}Input): Promise<${pascalName}> {
855
+ // TODO: Implement database insert
856
+ // Example: return prisma.${_camelName}.create({ data: input });
857
+ throw new Error('Not implemented');
858
+ }
859
+
860
+ /**
861
+ * Update ${pascalName.toLowerCase()} by ID
862
+ */
863
+ export async function update(id: string, input: Update${pascalName}Input): Promise<${pascalName}> {
864
+ // TODO: Implement database update
865
+ // Example: return prisma.${_camelName}.update({ where: { id }, data: input });
866
+ throw new Error('Not implemented');
867
+ }
868
+
869
+ /**
870
+ * Delete ${pascalName.toLowerCase()} by ID
871
+ */
872
+ export async function remove(id: string): Promise<void> {
873
+ // TODO: Implement database delete
874
+ // Example: await prisma.${_camelName}.delete({ where: { id } });
875
+ throw new Error('Not implemented');
876
+ }
877
+ `;
878
+ }
879
+ function generateControllerTemplate(pascalName, camelName, normalizedName) {
880
+ return `import type { Request, Response } from 'express';
881
+ import { z } from 'zod';
882
+ import * as ${camelName}Service from '../services/${normalizedName}.service';
883
+ import { asyncHandler } from '../lib/yantr/error-handler';
884
+ import { NotFoundError } from '../lib/yantr/error-handler';
885
+
886
+ /**
887
+ * ${pascalName} Controller
888
+ *
889
+ * Request handlers for ${pascalName.toLowerCase()} endpoints.
890
+ */
891
+
892
+ // Validation schemas
893
+ export const create${pascalName}Schema = z.object({
894
+ // Add your validation rules here
895
+ });
896
+
897
+ export const update${pascalName}Schema = z.object({
898
+ // Add your validation rules here
899
+ });
900
+
901
+ /**
902
+ * GET /api/${normalizedName}
903
+ * Get all ${pascalName.toLowerCase()}s
904
+ */
905
+ export const getAll = asyncHandler(async (_req: Request, res: Response) => {
906
+ const items = await ${camelName}Service.getAll();
907
+
908
+ res.json({
909
+ success: true,
910
+ data: items,
911
+ });
912
+ });
913
+
914
+ /**
915
+ * GET /api/${normalizedName}/:id
916
+ * Get ${pascalName.toLowerCase()} by ID
917
+ */
918
+ export const getById = asyncHandler(async (req: Request, res: Response) => {
919
+ const { id } = req.params;
920
+ const item = await ${camelName}Service.getById(id);
921
+
922
+ if (!item) {
923
+ throw new NotFoundError('${pascalName} not found');
924
+ }
925
+
926
+ res.json({
927
+ success: true,
928
+ data: item,
929
+ });
930
+ });
931
+
932
+ /**
933
+ * POST /api/${normalizedName}
934
+ * Create a new ${pascalName.toLowerCase()}
935
+ */
936
+ export const create = asyncHandler(async (req: Request, res: Response) => {
937
+ const input = create${pascalName}Schema.parse(req.body);
938
+ const item = await ${camelName}Service.create(input);
939
+
940
+ res.status(201).json({
941
+ success: true,
942
+ data: item,
943
+ message: '${pascalName} created successfully',
944
+ });
945
+ });
946
+
947
+ /**
948
+ * PUT /api/${normalizedName}/:id
949
+ * Update ${pascalName.toLowerCase()} by ID
950
+ */
951
+ export const update = asyncHandler(async (req: Request, res: Response) => {
952
+ const { id } = req.params;
953
+ const input = update${pascalName}Schema.parse(req.body);
954
+ const item = await ${camelName}Service.update(id, input);
955
+
956
+ res.json({
957
+ success: true,
958
+ data: item,
959
+ message: '${pascalName} updated successfully',
960
+ });
961
+ });
962
+
963
+ /**
964
+ * DELETE /api/${normalizedName}/:id
965
+ * Delete ${pascalName.toLowerCase()} by ID
966
+ */
967
+ export const remove = asyncHandler(async (req: Request, res: Response) => {
968
+ const { id } = req.params;
969
+ await ${camelName}Service.remove(id);
970
+
971
+ res.json({
972
+ success: true,
973
+ message: '${pascalName} deleted successfully',
974
+ });
975
+ });
976
+ `;
977
+ }
978
+ function generateRouteTemplate(pascalName, camelName, normalizedName) {
979
+ return `import { Router } from 'express';
980
+ import * as ${camelName}Controller from '../controllers/${normalizedName}.controller';
981
+ import { validateBody } from '../lib/yantr/zod-middleware';
982
+ import {
983
+ create${pascalName}Schema,
984
+ update${pascalName}Schema
985
+ } from '../controllers/${normalizedName}.controller';
986
+
987
+ const router = Router();
988
+
989
+ /**
990
+ * ${pascalName} Routes
991
+ *
992
+ * GET /api/${normalizedName} - Get all ${pascalName.toLowerCase()}s
993
+ * GET /api/${normalizedName}/:id - Get ${pascalName.toLowerCase()} by ID
994
+ * POST /api/${normalizedName} - Create ${pascalName.toLowerCase()}
995
+ * PUT /api/${normalizedName}/:id - Update ${pascalName.toLowerCase()}
996
+ * DELETE /api/${normalizedName}/:id - Delete ${pascalName.toLowerCase()}
997
+ */
998
+
999
+ router.get('/', ${camelName}Controller.getAll);
1000
+ router.get('/:id', ${camelName}Controller.getById);
1001
+ router.post('/', validateBody(create${pascalName}Schema), ${camelName}Controller.create);
1002
+ router.put('/:id', validateBody(update${pascalName}Schema), ${camelName}Controller.update);
1003
+ router.delete('/:id', ${camelName}Controller.remove);
1004
+
1005
+ export default router;
1006
+ `;
1007
+ }
1008
+ function toPascalCase(str) {
1009
+ return str.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
1010
+ }
1011
+ function toCamelCase(str) {
1012
+ const pascal = toPascalCase(str);
1013
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
1014
+ }
1015
+
1016
+ // src/index.ts
1017
+ var program = new Command();
1018
+ program.name("yantr").description("A Shadcn for Backend - Production-grade backend scaffolding CLI").version("0.1.0-beta.2");
1019
+ program.command("init").description("Initialize Setu in your project").option("-y, --yes", "Skip prompts and use defaults").action(init);
1020
+ program.command("add").description("Add a component to your project").argument("<component>", "Component to add (auth, logger, database, security)").option("-o, --overwrite", "Overwrite existing files").action(add);
1021
+ program.command("generate").alias("g").description("Generate boilerplate code").argument("<type>", "Type of code to generate (route)").argument("<name>", "Name of the resource").action(generate);
1022
+ program.parse();
1023
+ //# sourceMappingURL=index.js.map