zuro-cli 0.0.2-beta.0

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,841 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/index.ts
27
+ var import_commander = require("commander");
28
+
29
+ // src/commands/init.ts
30
+ var import_ora = __toESM(require("ora"));
31
+ var import_chalk2 = __toESM(require("chalk"));
32
+ var import_fs_extra4 = __toESM(require("fs-extra"));
33
+ var import_path4 = __toESM(require("path"));
34
+ var import_prompts = __toESM(require("prompts"));
35
+
36
+ // src/utils/registry.ts
37
+ var import_node_crypto = require("crypto");
38
+ var DEFAULT_REGISTRY_BASE_URL = "https://zuro-cli.devbybriyan.com/registry";
39
+ var DEFAULT_REGISTRY_ENTRY_URL = `${DEFAULT_REGISTRY_BASE_URL}/channels/stable.json`;
40
+ var REGISTRY_ENV_VAR = "ZURO_REGISTRY_URL";
41
+ var REQUEST_TIMEOUT_MS = 8e3;
42
+ var MAX_RETRIES = 2;
43
+ function withTrailingSlash(url) {
44
+ return url.endsWith("/") ? url : `${url}/`;
45
+ }
46
+ function resolveRegistryEntryUrl() {
47
+ const override = process.env[REGISTRY_ENV_VAR]?.trim();
48
+ if (!override) {
49
+ return DEFAULT_REGISTRY_ENTRY_URL;
50
+ }
51
+ let parsed;
52
+ try {
53
+ parsed = new URL(override);
54
+ } catch {
55
+ throw new Error(
56
+ `Invalid ${REGISTRY_ENV_VAR} value '${override}'. Expected a valid http(s) URL.`
57
+ );
58
+ }
59
+ if (parsed.pathname.endsWith(".json")) {
60
+ return parsed.toString();
61
+ }
62
+ return new URL("channels/stable.json", withTrailingSlash(parsed.toString())).toString();
63
+ }
64
+ function isRegistryManifest(data) {
65
+ if (!data || typeof data !== "object") {
66
+ return false;
67
+ }
68
+ const candidate = data;
69
+ return !!candidate.modules && typeof candidate.modules === "object";
70
+ }
71
+ function isChannelPointer(data) {
72
+ if (!data || typeof data !== "object") {
73
+ return false;
74
+ }
75
+ const candidate = data;
76
+ return typeof candidate.indexPath === "string" || typeof candidate.indexUrl === "string";
77
+ }
78
+ function delay(ms) {
79
+ return new Promise((resolve) => setTimeout(resolve, ms));
80
+ }
81
+ async function fetchWithRetry(url) {
82
+ let lastError;
83
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
84
+ const controller = new AbortController();
85
+ const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
86
+ try {
87
+ const response = await fetch(url, {
88
+ headers: { Accept: "application/json, text/plain, */*" },
89
+ signal: controller.signal
90
+ });
91
+ if (response.ok) {
92
+ return response;
93
+ }
94
+ const shouldRetry = response.status >= 500 && attempt < MAX_RETRIES;
95
+ if (shouldRetry) {
96
+ await delay(250 * (attempt + 1));
97
+ continue;
98
+ }
99
+ throw new Error(`Request failed (${response.status} ${response.statusText}) for ${url}`);
100
+ } catch (error) {
101
+ lastError = error;
102
+ if (attempt === MAX_RETRIES) {
103
+ break;
104
+ }
105
+ await delay(250 * (attempt + 1));
106
+ } finally {
107
+ clearTimeout(timeout);
108
+ }
109
+ }
110
+ if (lastError instanceof Error) {
111
+ throw lastError;
112
+ }
113
+ throw new Error(`Failed to fetch ${url}`);
114
+ }
115
+ async function fetchJson(url) {
116
+ const response = await fetchWithRetry(url);
117
+ const data = await response.json();
118
+ return {
119
+ data,
120
+ resolvedUrl: response.url || url
121
+ };
122
+ }
123
+ function resolveManifestUrl(pointer, pointerUrl) {
124
+ const explicit = pointer.indexUrl || pointer.indexPath;
125
+ if (!explicit) {
126
+ throw new Error("Registry channel pointer is missing indexUrl/indexPath");
127
+ }
128
+ return new URL(explicit, pointerUrl).toString();
129
+ }
130
+ async function fetchRegistry() {
131
+ const registryEntryUrl = resolveRegistryEntryUrl();
132
+ let entry;
133
+ try {
134
+ entry = await fetchJson(registryEntryUrl);
135
+ } catch (error) {
136
+ throw new Error(
137
+ `Unable to fetch registry from ${registryEntryUrl}. For local testing set ${REGISTRY_ENV_VAR}=http://127.0.0.1:8787.`,
138
+ { cause: error }
139
+ );
140
+ }
141
+ if (isRegistryManifest(entry.data)) {
142
+ return {
143
+ manifest: entry.data,
144
+ manifestUrl: entry.resolvedUrl,
145
+ fileBaseUrl: withTrailingSlash(new URL(".", entry.resolvedUrl).toString())
146
+ };
147
+ }
148
+ if (!isChannelPointer(entry.data)) {
149
+ throw new Error(
150
+ `Invalid registry payload at ${registryEntryUrl}. Expected manifest or channel pointer.`
151
+ );
152
+ }
153
+ const manifestUrl = resolveManifestUrl(entry.data, entry.resolvedUrl);
154
+ const manifestResult = await fetchJson(manifestUrl);
155
+ if (!isRegistryManifest(manifestResult.data)) {
156
+ throw new Error(`Invalid manifest payload at ${manifestUrl}.`);
157
+ }
158
+ return {
159
+ manifest: manifestResult.data,
160
+ manifestUrl: manifestResult.resolvedUrl,
161
+ fileBaseUrl: withTrailingSlash(new URL(".", manifestResult.resolvedUrl).toString())
162
+ };
163
+ }
164
+ async function fetchFile(filePath, options) {
165
+ const normalizedPath = filePath.replace(/^\//, "");
166
+ const fileUrl = new URL(normalizedPath, withTrailingSlash(options.baseUrl)).toString();
167
+ const response = await fetchWithRetry(fileUrl);
168
+ const content = await response.text();
169
+ if (typeof options.expectedSize === "number") {
170
+ const actualSize = Buffer.byteLength(content, "utf8");
171
+ if (actualSize !== options.expectedSize) {
172
+ throw new Error(
173
+ `Size mismatch for ${filePath}. Expected ${options.expectedSize}, got ${actualSize}.`
174
+ );
175
+ }
176
+ }
177
+ if (options.expectedSha256) {
178
+ const actualSha256 = (0, import_node_crypto.createHash)("sha256").update(content, "utf8").digest("hex");
179
+ if (actualSha256 !== options.expectedSha256) {
180
+ throw new Error(
181
+ `Checksum mismatch for ${filePath}. Expected ${options.expectedSha256}, got ${actualSha256}.`
182
+ );
183
+ }
184
+ }
185
+ return content;
186
+ }
187
+
188
+ // src/utils/pm.ts
189
+ var import_fs_extra = __toESM(require("fs-extra"));
190
+ var import_path = __toESM(require("path"));
191
+ var import_execa = require("execa");
192
+ async function initPackageJson(cwd, force = false) {
193
+ const pkgPath = import_path.default.join(cwd, "package.json");
194
+ if (force || !await import_fs_extra.default.pathExists(pkgPath)) {
195
+ await import_fs_extra.default.writeJson(pkgPath, {
196
+ name: "zuro-app",
197
+ version: "0.0.1",
198
+ private: true,
199
+ scripts: {
200
+ "dev": "tsx watch src/server.ts",
201
+ "build": "tsc",
202
+ "start": "node dist/server.js"
203
+ }
204
+ }, { spaces: 2 });
205
+ }
206
+ }
207
+ async function installDependencies(pm, deps, cwd, options = {}) {
208
+ const uniqueDeps = [...new Set(deps)].filter(Boolean);
209
+ if (uniqueDeps.length === 0) {
210
+ return;
211
+ }
212
+ const isDev = options.dev ?? false;
213
+ if (pm === "npm") {
214
+ const args = ["install", ...isDev ? ["--save-dev"] : [], ...uniqueDeps];
215
+ await (0, import_execa.execa)("npm", args, { cwd });
216
+ return;
217
+ }
218
+ if (pm === "pnpm") {
219
+ const args = ["add", ...isDev ? ["-D"] : [], ...uniqueDeps];
220
+ await (0, import_execa.execa)("pnpm", args, { cwd });
221
+ return;
222
+ }
223
+ if (pm === "yarn") {
224
+ const args = ["add", ...isDev ? ["-D"] : [], ...uniqueDeps];
225
+ await (0, import_execa.execa)("yarn", args, { cwd });
226
+ return;
227
+ }
228
+ if (pm === "bun") {
229
+ const args = ["add", ...isDev ? ["-d"] : [], ...uniqueDeps];
230
+ await (0, import_execa.execa)("bun", args, { cwd });
231
+ return;
232
+ }
233
+ throw new Error(`Unsupported package manager: ${pm}`);
234
+ }
235
+
236
+ // src/utils/env-manager.ts
237
+ var import_fs_extra2 = __toESM(require("fs-extra"));
238
+ var import_path2 = __toESM(require("path"));
239
+ var import_os = __toESM(require("os"));
240
+ var updateEnvFile = async (cwd, variables, createIfMissing = true) => {
241
+ const envPath = import_path2.default.join(cwd, ".env");
242
+ let content = "";
243
+ if (import_fs_extra2.default.existsSync(envPath)) {
244
+ content = await import_fs_extra2.default.readFile(envPath, "utf-8");
245
+ } else if (!createIfMissing) {
246
+ return;
247
+ }
248
+ let modified = false;
249
+ for (const [key, value] of Object.entries(variables)) {
250
+ const regex = new RegExp(`^${key}=`, "m");
251
+ if (!regex.test(content)) {
252
+ if (content && !content.endsWith("\n")) {
253
+ content += import_os.default.EOL;
254
+ }
255
+ content += `${key}=${value}${import_os.default.EOL}`;
256
+ modified = true;
257
+ }
258
+ }
259
+ if (modified || !import_fs_extra2.default.existsSync(envPath)) {
260
+ await import_fs_extra2.default.writeFile(envPath, content);
261
+ }
262
+ };
263
+ var updateEnvSchema = async (cwd, srcDir, fields) => {
264
+ const envPath = import_path2.default.join(cwd, srcDir, "env.ts");
265
+ if (!import_fs_extra2.default.existsSync(envPath)) {
266
+ return false;
267
+ }
268
+ let content = await import_fs_extra2.default.readFile(envPath, "utf-8");
269
+ let modified = false;
270
+ for (const field of fields) {
271
+ if (content.includes(`${field.name}:`)) {
272
+ continue;
273
+ }
274
+ const schemaEndRegex = /(\n\s*)(}\);?\s*\n\s*export const env)/;
275
+ const match = content.match(schemaEndRegex);
276
+ if (match) {
277
+ const indent = " ";
278
+ const newField = `${indent}${field.name}: ${field.schema},
279
+ `;
280
+ content = content.replace(
281
+ schemaEndRegex,
282
+ `
283
+ ${newField}$1$2`
284
+ );
285
+ modified = true;
286
+ }
287
+ }
288
+ if (modified) {
289
+ await import_fs_extra2.default.writeFile(envPath, content);
290
+ }
291
+ return modified;
292
+ };
293
+ var createInitialEnv = async (cwd) => {
294
+ const envPath = import_path2.default.join(cwd, ".env");
295
+ if (import_fs_extra2.default.existsSync(envPath)) {
296
+ return;
297
+ }
298
+ const content = `# Environment Variables
299
+ PORT=3000
300
+ NODE_ENV=development
301
+ `;
302
+ await import_fs_extra2.default.writeFile(envPath, content);
303
+ };
304
+ var ENV_CONFIGS = {
305
+ "database-pg": {
306
+ envVars: {
307
+ DATABASE_URL: "postgresql://postgres@localhost:5432/mydb"
308
+ },
309
+ schemaFields: [
310
+ { name: "DATABASE_URL", schema: "z.string().url()" }
311
+ ]
312
+ },
313
+ "database-mysql": {
314
+ envVars: {
315
+ DATABASE_URL: "mysql://root@localhost:3306/mydb"
316
+ },
317
+ schemaFields: [
318
+ { name: "DATABASE_URL", schema: "z.string().url()" }
319
+ ]
320
+ },
321
+ auth: {
322
+ envVars: {
323
+ BETTER_AUTH_SECRET: "your-secret-key-at-least-32-characters-long",
324
+ BETTER_AUTH_URL: "http://localhost:3000"
325
+ },
326
+ schemaFields: [
327
+ { name: "BETTER_AUTH_SECRET", schema: "z.string().min(32)" },
328
+ { name: "BETTER_AUTH_URL", schema: "z.string().url()" }
329
+ ]
330
+ }
331
+ };
332
+
333
+ // src/utils/config.ts
334
+ var import_fs_extra3 = __toESM(require("fs-extra"));
335
+ var import_path3 = __toESM(require("path"));
336
+ function sanitizeConfig(input) {
337
+ if (!input || typeof input !== "object") {
338
+ return {};
339
+ }
340
+ const raw = input;
341
+ const config = {};
342
+ if (typeof raw.name === "string") {
343
+ config.name = raw.name;
344
+ }
345
+ if (typeof raw.pm === "string") {
346
+ config.pm = raw.pm;
347
+ }
348
+ if (typeof raw.srcDir === "string") {
349
+ config.srcDir = raw.srcDir;
350
+ }
351
+ return config;
352
+ }
353
+ function getConfigPath(cwd) {
354
+ return import_path3.default.join(cwd, "zuro.json");
355
+ }
356
+ async function readZuroConfig(cwd) {
357
+ const configPath = getConfigPath(cwd);
358
+ if (!await import_fs_extra3.default.pathExists(configPath)) {
359
+ return null;
360
+ }
361
+ const raw = await import_fs_extra3.default.readJson(configPath);
362
+ return sanitizeConfig(raw);
363
+ }
364
+ async function writeZuroConfig(cwd, config) {
365
+ const configPath = getConfigPath(cwd);
366
+ await import_fs_extra3.default.writeJson(configPath, sanitizeConfig(config), { spaces: 2 });
367
+ }
368
+
369
+ // src/utils/project-guard.ts
370
+ var import_chalk = __toESM(require("chalk"));
371
+ function showNonZuroProjectMessage() {
372
+ console.log(import_chalk.default.yellow("This directory looks like an existing project that wasn't created with Zuro CLI."));
373
+ console.log("");
374
+ console.log(import_chalk.default.yellow("We stopped here because we don't want to make unnecessary changes to your project."));
375
+ console.log("");
376
+ console.log("Zuro CLI works in:");
377
+ console.log("- a fresh/empty directory, or");
378
+ console.log("- an existing project already managed by Zuro CLI.");
379
+ }
380
+
381
+ // src/commands/init.ts
382
+ function resolveSafeTargetPath(projectRoot, srcDir, file) {
383
+ const relativeTargetPath = import_path4.default.join(srcDir, file.target);
384
+ const targetPath = import_path4.default.resolve(projectRoot, relativeTargetPath);
385
+ const normalizedRoot = import_path4.default.resolve(projectRoot);
386
+ if (targetPath !== normalizedRoot && !targetPath.startsWith(`${normalizedRoot}${import_path4.default.sep}`)) {
387
+ throw new Error(`Refusing to write outside project directory: ${file.target}`);
388
+ }
389
+ return {
390
+ relativeTargetPath,
391
+ targetPath
392
+ };
393
+ }
394
+ async function init() {
395
+ const cwd = process.cwd();
396
+ const isExistingProject = await import_fs_extra4.default.pathExists(import_path4.default.join(cwd, "package.json"));
397
+ const existingZuroConfig = await readZuroConfig(cwd);
398
+ if (isExistingProject && !existingZuroConfig) {
399
+ showNonZuroProjectMessage();
400
+ return;
401
+ }
402
+ let targetDir = cwd;
403
+ let pm = "npm";
404
+ let srcDir = "src";
405
+ let projectName = import_path4.default.basename(cwd);
406
+ if (isExistingProject) {
407
+ console.log(import_chalk2.default.blue("\u2139 Existing project detected."));
408
+ projectName = import_path4.default.basename(cwd);
409
+ if (await import_fs_extra4.default.pathExists(import_path4.default.join(cwd, "pnpm-lock.yaml"))) {
410
+ pm = "pnpm";
411
+ } else if (await import_fs_extra4.default.pathExists(import_path4.default.join(cwd, "bun.lockb"))) {
412
+ pm = "bun";
413
+ } else if (await import_fs_extra4.default.pathExists(import_path4.default.join(cwd, "yarn.lock"))) {
414
+ pm = "yarn";
415
+ }
416
+ const response = await (0, import_prompts.default)({
417
+ type: "text",
418
+ name: "srcDir",
419
+ message: "Where is your source code located?",
420
+ initial: "src"
421
+ });
422
+ srcDir = response.srcDir || "src";
423
+ } else {
424
+ console.log(import_chalk2.default.dim(` Tip: Leave blank to use current folder (${import_path4.default.basename(cwd)})
425
+ `));
426
+ const response = await (0, import_prompts.default)([
427
+ {
428
+ type: "text",
429
+ name: "path",
430
+ message: "Project name (blank for current folder)",
431
+ initial: ""
432
+ },
433
+ {
434
+ type: "select",
435
+ name: "pm",
436
+ message: "Package Manager?",
437
+ choices: [
438
+ { title: "npm", value: "npm" },
439
+ { title: "pnpm", value: "pnpm" },
440
+ { title: "bun", value: "bun" }
441
+ ],
442
+ initial: 0
443
+ }
444
+ ]);
445
+ if (response.pm === void 0) {
446
+ console.log(import_chalk2.default.red("Operation cancelled."));
447
+ return;
448
+ }
449
+ pm = response.pm;
450
+ srcDir = "src";
451
+ if (!response.path || response.path.trim() === "") {
452
+ projectName = import_path4.default.basename(cwd);
453
+ targetDir = cwd;
454
+ console.log(import_chalk2.default.blue(`\u2139 Using current folder: ${projectName}`));
455
+ } else {
456
+ projectName = response.path.trim();
457
+ targetDir = import_path4.default.resolve(cwd, projectName);
458
+ await import_fs_extra4.default.ensureDir(targetDir);
459
+ }
460
+ }
461
+ const existingConfig = targetDir === cwd ? existingZuroConfig : await readZuroConfig(targetDir);
462
+ const zuroConfig = {
463
+ name: projectName,
464
+ pm,
465
+ srcDir: srcDir || existingConfig?.srcDir || "src"
466
+ };
467
+ await writeZuroConfig(targetDir, zuroConfig);
468
+ const spinner = (0, import_ora.default)("Connecting to Zuro Registry...").start();
469
+ try {
470
+ const registryContext = await fetchRegistry();
471
+ const coreModule = registryContext.manifest.modules.core;
472
+ if (!coreModule) {
473
+ spinner.fail("Core module not found in registry.");
474
+ return;
475
+ }
476
+ spinner.text = "Initializing project...";
477
+ const hasPackageJson = await import_fs_extra4.default.pathExists(import_path4.default.join(targetDir, "package.json"));
478
+ if (!hasPackageJson) {
479
+ await initPackageJson(targetDir, true);
480
+ }
481
+ spinner.text = `Installing dependencies using ${pm}...`;
482
+ let runtimeDeps = [];
483
+ let devDeps = [];
484
+ if (isExistingProject) {
485
+ const safeDeps = ["zod", "dotenv"];
486
+ const coreDeps = coreModule.dependencies || [];
487
+ runtimeDeps = coreDeps.filter((dependency) => safeDeps.includes(dependency));
488
+ devDeps = coreModule.devDependencies || [];
489
+ } else {
490
+ runtimeDeps = coreModule.dependencies || [];
491
+ devDeps = coreModule.devDependencies || [];
492
+ }
493
+ await installDependencies(pm, runtimeDeps, targetDir);
494
+ await installDependencies(pm, devDeps, targetDir, { dev: true });
495
+ spinner.text = "Fetching core module files...";
496
+ for (const file of coreModule.files) {
497
+ const { relativeTargetPath, targetPath } = resolveSafeTargetPath(targetDir, srcDir, file);
498
+ const fileName = import_path4.default.basename(targetPath);
499
+ if (isExistingProject) {
500
+ if (fileName === "app.ts" || fileName === "server.ts") {
501
+ continue;
502
+ }
503
+ const relativeParts = relativeTargetPath.split(import_path4.default.sep);
504
+ const isSafe = fileName === "env.ts" || relativeParts.includes("lib");
505
+ if (!isSafe) {
506
+ continue;
507
+ }
508
+ }
509
+ const content = await fetchFile(file.path, {
510
+ baseUrl: registryContext.fileBaseUrl,
511
+ expectedSha256: file.sha256,
512
+ expectedSize: file.size
513
+ });
514
+ await import_fs_extra4.default.ensureDir(import_path4.default.dirname(targetPath));
515
+ await import_fs_extra4.default.writeFile(targetPath, content);
516
+ }
517
+ await createInitialEnv(targetDir);
518
+ spinner.succeed(import_chalk2.default.green("Project initialized successfully!"));
519
+ console.log(`
520
+ ${import_chalk2.default.bold("Next steps:")}`);
521
+ if (targetDir !== cwd) {
522
+ console.log(import_chalk2.default.cyan(` cd ${projectName}`));
523
+ }
524
+ console.log(import_chalk2.default.cyan(` ${pm} run dev`));
525
+ console.log(`
526
+ ${import_chalk2.default.dim("Add modules: zuro-cli add database, zuro-cli add auth")}`);
527
+ } catch (error) {
528
+ spinner.fail(import_chalk2.default.red("Failed to initialize project."));
529
+ console.error(error);
530
+ }
531
+ }
532
+
533
+ // src/commands/add.ts
534
+ var import_prompts2 = __toESM(require("prompts"));
535
+ var import_ora2 = __toESM(require("ora"));
536
+ var import_path6 = __toESM(require("path"));
537
+ var import_fs_extra6 = __toESM(require("fs-extra"));
538
+
539
+ // src/utils/dependency.ts
540
+ var import_fs_extra5 = __toESM(require("fs-extra"));
541
+ var import_path5 = __toESM(require("path"));
542
+ var import_chalk3 = __toESM(require("chalk"));
543
+ var BLOCK_SIGNATURES = {
544
+ core: "env.ts",
545
+ "database-pg": "db/index.ts",
546
+ "database-mysql": "db/index.ts",
547
+ validator: "middleware/validate.ts",
548
+ "error-handler": "lib/errors.ts",
549
+ logger: "lib/logger.ts",
550
+ auth: "lib/auth.ts"
551
+ };
552
+ var resolveDependencies = async (moduleDependencies, cwd) => {
553
+ if (!moduleDependencies || moduleDependencies.length === 0) {
554
+ return;
555
+ }
556
+ const config = await readZuroConfig(cwd);
557
+ const srcDir = config?.srcDir || "src";
558
+ for (const dep of moduleDependencies) {
559
+ if (dep === "database") {
560
+ const pgExists = import_fs_extra5.default.existsSync(import_path5.default.join(cwd, srcDir, BLOCK_SIGNATURES["database-pg"]));
561
+ const mysqlExists = import_fs_extra5.default.existsSync(import_path5.default.join(cwd, srcDir, BLOCK_SIGNATURES["database-mysql"]));
562
+ if (pgExists || mysqlExists) {
563
+ continue;
564
+ }
565
+ console.log(import_chalk3.default.blue(`\u2139 Dependency '${dep}' is missing. Triggering install...`));
566
+ await add("database");
567
+ continue;
568
+ }
569
+ const signature = BLOCK_SIGNATURES[dep];
570
+ if (!signature) {
571
+ continue;
572
+ }
573
+ if (import_fs_extra5.default.existsSync(import_path5.default.join(cwd, srcDir, signature))) {
574
+ continue;
575
+ }
576
+ console.log(import_chalk3.default.blue(`\u2139 Installing missing dependency: ${dep}...`));
577
+ await add(dep);
578
+ }
579
+ };
580
+
581
+ // src/commands/add.ts
582
+ var import_chalk4 = __toESM(require("chalk"));
583
+ function resolveSafeTargetPath2(projectRoot, srcDir, file) {
584
+ const targetPath = import_path6.default.resolve(projectRoot, srcDir, file.target);
585
+ const normalizedRoot = import_path6.default.resolve(projectRoot);
586
+ if (targetPath !== normalizedRoot && !targetPath.startsWith(`${normalizedRoot}${import_path6.default.sep}`)) {
587
+ throw new Error(`Refusing to write outside project directory: ${file.target}`);
588
+ }
589
+ return targetPath;
590
+ }
591
+ async function injectErrorHandler(projectRoot, srcDir) {
592
+ const appPath = import_path6.default.join(projectRoot, srcDir, "app.ts");
593
+ if (!import_fs_extra6.default.existsSync(appPath)) {
594
+ return false;
595
+ }
596
+ let content = await import_fs_extra6.default.readFile(appPath, "utf-8");
597
+ if (content.includes("errorHandler")) {
598
+ return true;
599
+ }
600
+ const errorImport = `import { errorHandler, notFoundHandler } from "./middleware/error-handler";`;
601
+ const importRegex = /^import .+ from .+;?\s*$/gm;
602
+ let lastImportIndex = 0;
603
+ let match;
604
+ while ((match = importRegex.exec(content)) !== null) {
605
+ lastImportIndex = match.index + match[0].length;
606
+ }
607
+ if (lastImportIndex > 0) {
608
+ content = content.slice(0, lastImportIndex) + `
609
+ ${errorImport}` + content.slice(lastImportIndex);
610
+ }
611
+ const errorSetup = `
612
+ // Error handling (must be last)
613
+ app.use(notFoundHandler);
614
+ app.use(errorHandler);
615
+ `;
616
+ const exportMatch = content.match(/export default app;?\s*$/m);
617
+ if (exportMatch && exportMatch.index !== void 0) {
618
+ content = content.slice(0, exportMatch.index) + errorSetup + "\n" + content.slice(exportMatch.index);
619
+ }
620
+ await import_fs_extra6.default.writeFile(appPath, content);
621
+ return true;
622
+ }
623
+ async function injectAuthRoutes(projectRoot, srcDir) {
624
+ const appPath = import_path6.default.join(projectRoot, srcDir, "app.ts");
625
+ if (!import_fs_extra6.default.existsSync(appPath)) {
626
+ return false;
627
+ }
628
+ let content = await import_fs_extra6.default.readFile(appPath, "utf-8");
629
+ if (content.includes("routes/auth.routes")) {
630
+ return true;
631
+ }
632
+ const authImport = `import authRoutes from "./routes/auth.routes";`;
633
+ const userImport = `import userRoutes from "./routes/user.routes";`;
634
+ const routeSetup = `
635
+ // Auth routes
636
+ app.use(authRoutes);
637
+ app.use("/api/users", userRoutes);
638
+ `;
639
+ const importRegex = /^import .+ from .+;?\s*$/gm;
640
+ let lastImportIndex = 0;
641
+ let match;
642
+ while ((match = importRegex.exec(content)) !== null) {
643
+ lastImportIndex = match.index + match[0].length;
644
+ }
645
+ if (lastImportIndex > 0) {
646
+ content = content.slice(0, lastImportIndex) + `
647
+ ${authImport}
648
+ ${userImport}` + content.slice(lastImportIndex);
649
+ }
650
+ const exportMatch = content.match(/export default app;?\s*$/m);
651
+ if (exportMatch && exportMatch.index !== void 0) {
652
+ content = content.slice(0, exportMatch.index) + routeSetup + "\n" + content.slice(exportMatch.index);
653
+ }
654
+ await import_fs_extra6.default.writeFile(appPath, content);
655
+ return true;
656
+ }
657
+ var add = async (moduleName) => {
658
+ const projectRoot = process.cwd();
659
+ const projectConfig = await readZuroConfig(projectRoot);
660
+ if (!projectConfig) {
661
+ showNonZuroProjectMessage();
662
+ return;
663
+ }
664
+ let srcDir = projectConfig.srcDir || "src";
665
+ let customDbUrl;
666
+ const DEFAULT_URLS = {
667
+ "database-pg": "postgresql://root@localhost:5432/mydb",
668
+ "database-mysql": "mysql://root@localhost:3306/mydb"
669
+ };
670
+ if (moduleName === "database") {
671
+ const variantResponse = await (0, import_prompts2.default)({
672
+ type: "select",
673
+ name: "variant",
674
+ message: "Which database dialect?",
675
+ choices: [
676
+ { title: "PostgreSQL", value: "database-pg" },
677
+ { title: "MySQL", value: "database-mysql" }
678
+ ]
679
+ });
680
+ if (!variantResponse.variant) {
681
+ process.exit(0);
682
+ }
683
+ moduleName = variantResponse.variant;
684
+ const defaultUrl = DEFAULT_URLS[moduleName];
685
+ console.log(import_chalk4.default.dim(` Tip: Leave blank to use ${defaultUrl}
686
+ `));
687
+ const urlResponse = await (0, import_prompts2.default)({
688
+ type: "text",
689
+ name: "dbUrl",
690
+ message: "Database URL",
691
+ initial: ""
692
+ });
693
+ customDbUrl = urlResponse.dbUrl?.trim() || defaultUrl;
694
+ }
695
+ if ((moduleName === "database-pg" || moduleName === "database-mysql") && customDbUrl === void 0) {
696
+ const defaultUrl = DEFAULT_URLS[moduleName];
697
+ console.log(import_chalk4.default.dim(` Tip: Leave blank to use ${defaultUrl}
698
+ `));
699
+ const response = await (0, import_prompts2.default)({
700
+ type: "text",
701
+ name: "dbUrl",
702
+ message: "Database URL",
703
+ initial: ""
704
+ });
705
+ customDbUrl = response.dbUrl?.trim() || defaultUrl;
706
+ }
707
+ const spinner = (0, import_ora2.default)(`Checking registry for ${moduleName}...`).start();
708
+ try {
709
+ const registryContext = await fetchRegistry();
710
+ const module2 = registryContext.manifest.modules[moduleName];
711
+ if (!module2) {
712
+ spinner.fail(`Module '${moduleName}' not found.`);
713
+ return;
714
+ }
715
+ spinner.succeed(`Found module: ${import_chalk4.default.cyan(moduleName)}`);
716
+ const moduleDeps = module2.moduleDependencies || [];
717
+ await resolveDependencies(moduleDeps, projectRoot);
718
+ spinner.start("Installing dependencies...");
719
+ let pm = "npm";
720
+ if (import_fs_extra6.default.existsSync(import_path6.default.join(projectRoot, "pnpm-lock.yaml"))) {
721
+ pm = "pnpm";
722
+ } else if (import_fs_extra6.default.existsSync(import_path6.default.join(projectRoot, "bun.lockb"))) {
723
+ pm = "bun";
724
+ } else if (import_fs_extra6.default.existsSync(import_path6.default.join(projectRoot, "yarn.lock"))) {
725
+ pm = "yarn";
726
+ }
727
+ await installDependencies(pm, module2.dependencies || [], projectRoot);
728
+ await installDependencies(pm, module2.devDependencies || [], projectRoot, { dev: true });
729
+ spinner.succeed("Dependencies installed");
730
+ spinner.start("Scaffolding files...");
731
+ for (const file of module2.files) {
732
+ const content = await fetchFile(file.path, {
733
+ baseUrl: registryContext.fileBaseUrl,
734
+ expectedSha256: file.sha256,
735
+ expectedSize: file.size
736
+ });
737
+ const targetPath = resolveSafeTargetPath2(projectRoot, srcDir, file);
738
+ await import_fs_extra6.default.ensureDir(import_path6.default.dirname(targetPath));
739
+ await import_fs_extra6.default.writeFile(targetPath, content);
740
+ }
741
+ spinner.succeed("Files generated");
742
+ if (moduleName === "auth") {
743
+ spinner.start("Configuring routes in app.ts...");
744
+ const injected = await injectAuthRoutes(projectRoot, srcDir);
745
+ if (injected) {
746
+ spinner.succeed("Routes configured in app.ts");
747
+ } else {
748
+ spinner.warn("Could not find app.ts - routes need manual setup");
749
+ }
750
+ }
751
+ if (moduleName === "error-handler") {
752
+ spinner.start("Configuring error handler in app.ts...");
753
+ const injected = await injectErrorHandler(projectRoot, srcDir);
754
+ if (injected) {
755
+ spinner.succeed("Error handler configured in app.ts");
756
+ } else {
757
+ spinner.warn("Could not find app.ts - error handler needs manual setup");
758
+ }
759
+ }
760
+ const envConfig = ENV_CONFIGS[moduleName];
761
+ if (envConfig) {
762
+ spinner.start("Updating environment configuration...");
763
+ const envVars = { ...envConfig.envVars };
764
+ if (customDbUrl && (moduleName === "database-pg" || moduleName === "database-mysql")) {
765
+ envVars.DATABASE_URL = customDbUrl;
766
+ }
767
+ await updateEnvFile(projectRoot, envVars);
768
+ await updateEnvSchema(projectRoot, srcDir, envConfig.schemaFields);
769
+ spinner.succeed("Environment configured");
770
+ }
771
+ console.log(import_chalk4.default.green(`
772
+ \u2714 ${moduleName} added successfully!
773
+ `));
774
+ if (moduleName === "auth") {
775
+ console.log(import_chalk4.default.bold("\u{1F4CB} Next Steps:\n"));
776
+ console.log(import_chalk4.default.yellow("1. Update your .env file:"));
777
+ console.log(
778
+ import_chalk4.default.dim(" We added placeholder values. Update BETTER_AUTH_SECRET with a secure key.\n")
779
+ );
780
+ console.log(import_chalk4.default.yellow("2. Run database migrations:"));
781
+ console.log(import_chalk4.default.cyan(" npx drizzle-kit generate"));
782
+ console.log(import_chalk4.default.cyan(" npx drizzle-kit migrate\n"));
783
+ console.log(import_chalk4.default.yellow("3. Available endpoints:"));
784
+ console.log(import_chalk4.default.dim(" POST /auth/sign-up/email - Register"));
785
+ console.log(import_chalk4.default.dim(" POST /auth/sign-in/email - Login"));
786
+ console.log(import_chalk4.default.dim(" POST /auth/sign-out - Logout"));
787
+ console.log(import_chalk4.default.dim(" GET /api/users/me - Current user\n"));
788
+ } else if (moduleName === "error-handler") {
789
+ console.log(import_chalk4.default.bold("\u{1F4CB} Usage:\n"));
790
+ console.log(import_chalk4.default.yellow("Throw errors in your controllers:"));
791
+ console.log(import_chalk4.default.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
792
+ console.log(import_chalk4.default.white(` import { UnauthorizedError, NotFoundError } from "./lib/errors";`));
793
+ console.log("");
794
+ console.log(import_chalk4.default.white(` throw new UnauthorizedError("Invalid credentials");`));
795
+ console.log(import_chalk4.default.white(` throw new NotFoundError("User not found");`));
796
+ console.log(import_chalk4.default.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
797
+ console.log(import_chalk4.default.yellow("Available error classes:"));
798
+ console.log(import_chalk4.default.dim(" BadRequestError (400)"));
799
+ console.log(import_chalk4.default.dim(" UnauthorizedError (401)"));
800
+ console.log(import_chalk4.default.dim(" ForbiddenError (403)"));
801
+ console.log(import_chalk4.default.dim(" NotFoundError (404)"));
802
+ console.log(import_chalk4.default.dim(" ConflictError (409)"));
803
+ console.log(import_chalk4.default.dim(" ValidationError (422)"));
804
+ console.log(import_chalk4.default.dim(" InternalServerError (500)\n"));
805
+ console.log(import_chalk4.default.yellow("Wrap async handlers:"));
806
+ console.log(import_chalk4.default.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
807
+ console.log(import_chalk4.default.white(` import { asyncHandler } from "./middleware/error-handler";`));
808
+ console.log("");
809
+ console.log(import_chalk4.default.white(` router.get("/users", asyncHandler(async (req, res) => {`));
810
+ console.log(import_chalk4.default.white(" // errors auto-caught"));
811
+ console.log(import_chalk4.default.white(" }));"));
812
+ console.log(import_chalk4.default.dim(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
813
+ } else if (moduleName.includes("database")) {
814
+ console.log(import_chalk4.default.bold("\u{1F4CB} Next Steps:\n"));
815
+ let stepNum = 1;
816
+ if (!customDbUrl) {
817
+ console.log(import_chalk4.default.yellow(`${stepNum}. Update DATABASE_URL in .env:`));
818
+ console.log(
819
+ import_chalk4.default.dim(" We added a placeholder. Update with your actual database credentials.\n")
820
+ );
821
+ stepNum++;
822
+ }
823
+ console.log(import_chalk4.default.yellow(`${stepNum}. Create schemas in src/db/schema/:`));
824
+ console.log(import_chalk4.default.dim(" Add table files and export from index.ts\n"));
825
+ stepNum++;
826
+ console.log(import_chalk4.default.yellow(`${stepNum}. Run migrations:`));
827
+ console.log(import_chalk4.default.cyan(" npx drizzle-kit generate"));
828
+ console.log(import_chalk4.default.cyan(" npx drizzle-kit migrate\n"));
829
+ }
830
+ } catch (error) {
831
+ spinner.fail(`Failed to add module: ${error.message}`);
832
+ }
833
+ };
834
+
835
+ // src/index.ts
836
+ var program = new import_commander.Command();
837
+ program.name("zuro-cli").description("Zuro CLI tool").version("0.0.1");
838
+ program.command("init").description("Initialize a new Zuro project").action(init);
839
+ program.command("add <module>").description("Add a module to your project").action(add);
840
+ program.parse(process.argv);
841
+ //# sourceMappingURL=index.js.map