@nilejs/cli 0.0.2 → 0.0.3

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/README.md CHANGED
@@ -114,6 +114,43 @@ export const getUserAction: Action = createAction({
114
114
 
115
115
  Kebab-case names are converted to camelCase for variables and PascalCase for types.
116
116
 
117
+ ### `nile generate schema`
118
+
119
+ Alias: `nile g schema`
120
+
121
+ Extract Zod validation schemas from your action definitions and generate TypeScript files with schema exports and inferred types.
122
+
123
+ ```bash
124
+ nile g schema
125
+ ```
126
+
127
+ The command auto-detects `src/services/services.config.ts`. If the file is not found, it prompts for the path. It spawns a `bun` subprocess to import your services config, extracts JSON Schema from each action's validation field, and converts them back to Zod code strings.
128
+
129
+ Output (default `src/generated/`):
130
+
131
+ - `schemas.ts` — named Zod schema exports (`tasksCreateSchema`, `tasksUpdateSchema`, etc.)
132
+ - `types.ts` — TypeScript types via `z.infer` (`TasksCreatePayload`, `TasksUpdatePayload`, etc.)
133
+
134
+ Options:
135
+
136
+ | Flag | Description |
137
+ |---|---|
138
+ | `-e, --entry <path>` | Path to services config (auto-detected by default) |
139
+ | `-o, --output <path>` | Output directory (default: `src/generated`) |
140
+
141
+ Actions without a `validation` schema are skipped and listed in the CLI output.
142
+
143
+ Requires [Bun](https://bun.sh) installed for the extraction subprocess.
144
+
145
+ ### Quick Reference
146
+
147
+ | Command | Description |
148
+ |---|---|
149
+ | `nile new <name>` | Scaffold a new project |
150
+ | `nile g service <name>` | Add a service with a demo action |
151
+ | `nile g action <service> <name>` | Add an action to an existing service |
152
+ | `nile g schema` | Generate Zod schemas and TypeScript types |
153
+
117
154
  ## Generated Project Structure
118
155
 
119
156
  ```
package/dist/index.js CHANGED
@@ -130,8 +130,9 @@ var generateActionCommand = async (serviceName, actionName) => {
130
130
  outro();
131
131
  };
132
132
 
133
- // src/commands/generate-service.ts
134
- import { resolve as resolve2 } from "node:path";
133
+ // src/commands/generate-schema.ts
134
+ import { execFileSync } from "node:child_process";
135
+ import { resolve as resolve3 } from "node:path";
135
136
 
136
137
  // src/utils/prompt.ts
137
138
  import { createInterface } from "node:readline";
@@ -153,15 +154,273 @@ var confirmPrompt = (question, defaultYes = true) => {
153
154
  });
154
155
  });
155
156
  };
157
+ var inputPrompt = (question) => {
158
+ const rl = createInterface({
159
+ input: process.stdin,
160
+ output: process.stdout
161
+ });
162
+ return new Promise((resolve2) => {
163
+ rl.question(` ${question} `, (answer) => {
164
+ rl.close();
165
+ resolve2(answer.trim());
166
+ });
167
+ });
168
+ };
156
169
 
157
- // src/commands/generate-service.ts
170
+ // src/utils/schema-codegen.ts
171
+ import { jsonSchemaToZod } from "json-schema-to-zod";
172
+ var toSchemaExportName = (service, action) => {
173
+ const pascalAction = action.charAt(0).toUpperCase() + action.slice(1);
174
+ return `${toCamelCase2(service)}${pascalAction}Schema`;
175
+ };
176
+ var toTypeExportName = (service, action) => {
177
+ const pascalService = toCamelCase2(service).charAt(0).toUpperCase() + toCamelCase2(service).slice(1);
178
+ const pascalAction = action.charAt(0).toUpperCase() + action.slice(1);
179
+ return `${pascalService}${pascalAction}Payload`;
180
+ };
158
181
  var toCamelCase2 = (str) => str.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
182
+ var generateHeader = () => {
183
+ const timestamp = new Date().toISOString();
184
+ return [
185
+ `// Auto-generated by @nilejs/cli on ${timestamp}`,
186
+ "// Do not edit manually. Re-run `nile generate schema` to update.",
187
+ ""
188
+ ].join(`
189
+ `);
190
+ };
191
+ var buildSchemaEntries = (extraction) => {
192
+ const entries = [];
193
+ for (const [serviceName, actions] of Object.entries(extraction.schemas)) {
194
+ for (const [actionName, jsonSchema] of Object.entries(actions)) {
195
+ const zodCode = jsonSchemaToZod(jsonSchema, {
196
+ noImport: true
197
+ });
198
+ entries.push({
199
+ serviceName,
200
+ actionName,
201
+ exportName: toSchemaExportName(serviceName, actionName),
202
+ typeName: toTypeExportName(serviceName, actionName),
203
+ zodCode
204
+ });
205
+ }
206
+ }
207
+ return entries;
208
+ };
209
+ var generateSchemasFile = (extraction) => {
210
+ const entries = buildSchemaEntries(extraction);
211
+ if (entries.length === 0) {
212
+ return `${generateHeader()}
213
+ // No action schemas found.
214
+ `;
215
+ }
216
+ const lines = [generateHeader(), `import { z } from "zod";`, ""];
217
+ for (const entry of entries) {
218
+ lines.push(`export const ${entry.exportName} = ${entry.zodCode};`);
219
+ lines.push("");
220
+ }
221
+ if (extraction.skipped.length > 0) {
222
+ lines.push(`// Skipped (no validation): ${extraction.skipped.join(", ")}`);
223
+ lines.push("");
224
+ }
225
+ return lines.join(`
226
+ `);
227
+ };
228
+ var generateTypesFile = (extraction) => {
229
+ const entries = buildSchemaEntries(extraction);
230
+ if (entries.length === 0) {
231
+ return `${generateHeader()}
232
+ // No action types found.
233
+ `;
234
+ }
235
+ const schemaImports = entries.map((e) => e.exportName);
236
+ const lines = [
237
+ generateHeader(),
238
+ `import type { z } from "zod";`,
239
+ "import type {",
240
+ ...schemaImports.map((name, i) => i < schemaImports.length - 1 ? ` ${name},` : ` ${name},`),
241
+ `} from "./schemas";`,
242
+ ""
243
+ ];
244
+ for (const entry of entries) {
245
+ lines.push(`export type ${entry.typeName} = z.infer<typeof ${entry.exportName}>;`);
246
+ }
247
+ lines.push("");
248
+ return lines.join(`
249
+ `);
250
+ };
251
+
252
+ // src/utils/schema-extractor.ts
253
+ import { execFile } from "node:child_process";
254
+ import { unlink, writeFile as writeFile2 } from "node:fs/promises";
255
+ import { resolve as resolve2 } from "node:path";
256
+ var TEMP_SCRIPT_NAME = ".nile-schema-extract.ts";
257
+ var LEADING_SLASH = /^\//;
258
+ var TS_EXTENSION = /\.ts$/;
259
+ var buildExtractionScript = (configPath) => {
260
+ return `import z from "zod";
261
+ import { services } from "${configPath}";
262
+
263
+ const schemas: Record<string, Record<string, unknown>> = {};
264
+ const skipped: string[] = [];
265
+
266
+ for (const service of services) {
267
+ const serviceSchemas: Record<string, unknown> = {};
268
+
269
+ for (const action of service.actions) {
270
+ if (!action.validation) {
271
+ skipped.push(\`\${service.name}.\${action.name}\`);
272
+ continue;
273
+ }
274
+
275
+ try {
276
+ const jsonSchema = z.toJSONSchema(action.validation, { unrepresentable: "any" });
277
+ serviceSchemas[action.name] = jsonSchema;
278
+ } catch {
279
+ skipped.push(\`\${service.name}.\${action.name}\`);
280
+ }
281
+ }
282
+
283
+ if (Object.keys(serviceSchemas).length > 0) {
284
+ schemas[service.name] = serviceSchemas;
285
+ }
286
+ }
287
+
288
+ console.log(JSON.stringify({ schemas, skipped }));
289
+ `;
290
+ };
291
+ var resolveImportPath = (configPath, projectRoot) => {
292
+ const relative = configPath.replace(projectRoot, "").replace(LEADING_SLASH, "./").replace(TS_EXTENSION, "");
293
+ return relative.startsWith("./") ? relative : `./${relative}`;
294
+ };
295
+ var extractSchemas = (configPath, projectRoot) => {
296
+ const importPath = resolveImportPath(configPath, projectRoot);
297
+ const scriptContent = buildExtractionScript(importPath);
298
+ const tempScriptPath = resolve2(projectRoot, TEMP_SCRIPT_NAME);
299
+ return new Promise((resolve3, reject) => {
300
+ writeFile2(tempScriptPath, scriptContent, "utf-8").then(() => {
301
+ execFile("bun", ["run", TEMP_SCRIPT_NAME], { cwd: projectRoot, timeout: 30000 }, (err, stdout, stderr) => {
302
+ unlink(tempScriptPath).catch(() => {});
303
+ if (err) {
304
+ const message = stderr?.trim() || err.message;
305
+ reject(new Error(`Schema extraction failed:
306
+ ${message}`));
307
+ return;
308
+ }
309
+ try {
310
+ const result = JSON.parse(stdout.trim());
311
+ resolve3(result);
312
+ } catch {
313
+ reject(new Error(`Failed to parse extraction output. Raw output:
314
+ ${stdout.trim()}`));
315
+ }
316
+ });
317
+ }).catch((writeErr) => {
318
+ reject(new Error(`Failed to write temp extraction script: ${writeErr.message}`));
319
+ });
320
+ });
321
+ };
322
+ var DEFAULT_CONFIG_PATH = "src/services/services.config.ts";
323
+ var findServicesConfig = (projectRoot) => {
324
+ const defaultPath = resolve2(projectRoot, DEFAULT_CONFIG_PATH);
325
+ return pathExists(defaultPath) ? defaultPath : null;
326
+ };
327
+
328
+ // src/commands/generate-schema.ts
329
+ var DEFAULT_OUTPUT_DIR = "src/generated";
330
+ var LEADING_SLASH2 = /^\//;
331
+ var resolveConfigPath = async (projectRoot, entryFlag) => {
332
+ if (entryFlag) {
333
+ const absolute2 = resolve3(projectRoot, entryFlag);
334
+ if (!pathExists(absolute2)) {
335
+ error(`Config file not found: ${entryFlag}`);
336
+ return null;
337
+ }
338
+ return absolute2;
339
+ }
340
+ const detected = findServicesConfig(projectRoot);
341
+ if (detected) {
342
+ success("Found services config at src/services/services.config.ts");
343
+ return detected;
344
+ }
345
+ warn("Could not find src/services/services.config.ts");
346
+ const userPath = await inputPrompt("Enter the path to your services config file:");
347
+ if (!userPath) {
348
+ error("No config path provided.");
349
+ return null;
350
+ }
351
+ const absolute = resolve3(projectRoot, userPath);
352
+ if (!pathExists(absolute)) {
353
+ error(`Config file not found: ${userPath}`);
354
+ return null;
355
+ }
356
+ return absolute;
357
+ };
358
+ var checkBunAvailable = () => {
359
+ try {
360
+ execFileSync("bun", ["--version"], { stdio: "ignore" });
361
+ return true;
362
+ } catch {
363
+ return false;
364
+ }
365
+ };
366
+ var generateSchemaCommand = async (options) => {
367
+ const projectRoot = process.cwd();
368
+ brand();
369
+ if (!checkBunAvailable()) {
370
+ error("Bun is required for schema extraction but was not found.");
371
+ hint("Install bun: https://bun.sh");
372
+ process.exit(1);
373
+ }
374
+ const configPath = await resolveConfigPath(projectRoot, options.entry);
375
+ if (!configPath) {
376
+ process.exit(1);
377
+ }
378
+ const spinner = createSpinner("Extracting action schemas...");
379
+ let extraction;
380
+ try {
381
+ extraction = await extractSchemas(configPath, projectRoot);
382
+ } catch (err) {
383
+ spinner.stop("Extraction failed");
384
+ console.log("");
385
+ error(err instanceof Error ? err.message : String(err));
386
+ process.exit(1);
387
+ }
388
+ const schemaCount = Object.values(extraction.schemas).reduce((sum, actions) => sum + Object.keys(actions).length, 0);
389
+ if (schemaCount === 0) {
390
+ spinner.stop("No action schemas found");
391
+ warn("None of the actions have validation schemas defined.");
392
+ hint("Add a Zod validation schema to your actions to generate types.");
393
+ outro();
394
+ return;
395
+ }
396
+ spinner.stop(`Extracted ${schemaCount} action schema${schemaCount > 1 ? "s" : ""}`);
397
+ const outputDir = resolve3(projectRoot, options.output ?? DEFAULT_OUTPUT_DIR);
398
+ await ensureDir(outputDir);
399
+ const schemasContent = generateSchemasFile(extraction);
400
+ const typesContent = generateTypesFile(extraction);
401
+ const schemasPath = resolve3(outputDir, "schemas.ts");
402
+ const typesPath = resolve3(outputDir, "types.ts");
403
+ await writeFileSafe(schemasPath, schemasContent);
404
+ success(`Generated ${schemasPath.replace(projectRoot, "").replace(LEADING_SLASH2, "")}`);
405
+ await writeFileSafe(typesPath, typesContent);
406
+ success(`Generated ${typesPath.replace(projectRoot, "").replace(LEADING_SLASH2, "")}`);
407
+ if (extraction.skipped.length > 0) {
408
+ console.log("");
409
+ hint(`Skipped ${extraction.skipped.length} action(s) without validation:`);
410
+ hint(extraction.skipped.join(", "));
411
+ }
412
+ outro();
413
+ };
414
+
415
+ // src/commands/generate-service.ts
416
+ import { resolve as resolve4 } from "node:path";
417
+ var toCamelCase3 = (str) => str.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
159
418
  var toPascalCase2 = (str) => {
160
- const camel = toCamelCase2(str);
419
+ const camel = toCamelCase3(str);
161
420
  return camel.charAt(0).toUpperCase() + camel.slice(1);
162
421
  };
163
422
  var generateActionContent2 = (serviceName) => {
164
- const camel = toCamelCase2(serviceName);
423
+ const camel = toCamelCase3(serviceName);
165
424
  const pascal = toPascalCase2(serviceName);
166
425
  return `import { type Action, createAction } from "@nilejs/nile";
167
426
  import { Ok } from "slang-ts";
@@ -194,7 +453,7 @@ var generateConfigSnippet = (serviceName) => {
194
453
  const serviceEntry = ` {
195
454
  name: "${serviceName}",
196
455
  description: "${pascal} service",
197
- actions: createActions([sample${pascal}Action]),
456
+ actions: [sample${pascal}Action],
198
457
  },`;
199
458
  return `${importLine}
200
459
 
@@ -220,7 +479,7 @@ ${importLine}`);
220
479
  const serviceEntry = ` {
221
480
  name: "${serviceName}",
222
481
  description: "${pascal} service",
223
- actions: createActions([sample${pascal}Action]),
482
+ actions: [sample${pascal}Action],
224
483
  },`;
225
484
  const closingIndex = content.lastIndexOf("];");
226
485
  if (closingIndex === -1) {
@@ -235,13 +494,13 @@ ${importLine}`);
235
494
  }
236
495
  };
237
496
  var generateServiceCommand = async (serviceName) => {
238
- const servicesDir = resolve2(process.cwd(), "src/services");
497
+ const servicesDir = resolve4(process.cwd(), "src/services");
239
498
  if (!pathExists(servicesDir)) {
240
499
  error("Could not find src/services/ directory.");
241
500
  hint("Make sure you're in a Nile project root.");
242
501
  process.exit(1);
243
502
  }
244
- const serviceDir = resolve2(servicesDir, serviceName);
503
+ const serviceDir = resolve4(servicesDir, serviceName);
245
504
  if (pathExists(serviceDir)) {
246
505
  error(`Service "${serviceName}" already exists at src/services/${serviceName}/`);
247
506
  process.exit(1);
@@ -249,10 +508,10 @@ var generateServiceCommand = async (serviceName) => {
249
508
  brand();
250
509
  const spinner = createSpinner(`Creating service ${serviceName}...`);
251
510
  await ensureDir(serviceDir);
252
- await writeFileSafe(resolve2(serviceDir, "sample.ts"), generateActionContent2(serviceName));
253
- await writeFileSafe(resolve2(serviceDir, "index.ts"), generateBarrelContent(serviceName));
511
+ await writeFileSafe(resolve4(serviceDir, "sample.ts"), generateActionContent2(serviceName));
512
+ await writeFileSafe(resolve4(serviceDir, "index.ts"), generateBarrelContent(serviceName));
254
513
  spinner.stop(`Service created at src/services/${serviceName}/`);
255
- const configPath = resolve2(servicesDir, "services.config.ts");
514
+ const configPath = resolve4(servicesDir, "services.config.ts");
256
515
  if (!pathExists(configPath)) {
257
516
  warn("Could not find services.config.ts");
258
517
  console.log("");
@@ -281,22 +540,22 @@ ${generateConfigSnippet(serviceName)}
281
540
  };
282
541
 
283
542
  // src/commands/new.ts
284
- import { resolve as resolve3 } from "node:path";
543
+ import { resolve as resolve5 } from "node:path";
285
544
  import { fileURLToPath } from "node:url";
286
545
  var __dirname2 = fileURLToPath(new URL(".", import.meta.url));
287
546
  var resolveTemplateDir = () => {
288
- const devPath = resolve3(__dirname2, "../../template");
547
+ const devPath = resolve5(__dirname2, "../../template");
289
548
  if (pathExists(devPath)) {
290
549
  return devPath;
291
550
  }
292
- const distPath = resolve3(__dirname2, "../template");
551
+ const distPath = resolve5(__dirname2, "../template");
293
552
  if (pathExists(distPath)) {
294
553
  return distPath;
295
554
  }
296
555
  throw new Error("Template directory not found. The CLI package may be corrupted.");
297
556
  };
298
557
  var newCommand = async (projectName) => {
299
- const targetDir = resolve3(process.cwd(), projectName);
558
+ const targetDir = resolve5(process.cwd(), projectName);
300
559
  if (pathExists(targetDir)) {
301
560
  error(`Directory "${projectName}" already exists.`);
302
561
  process.exit(1);
@@ -315,10 +574,15 @@ var newCommand = async (projectName) => {
315
574
  spinner.stop("Project ready.");
316
575
  success(`Created ${projectName}`);
317
576
  console.log("");
318
- hint("cd " + projectName);
577
+ hint(`cd ${projectName}`);
319
578
  hint("bun install");
320
579
  hint("cp .env.example .env");
321
580
  hint("bun run dev");
581
+ console.log("");
582
+ success("Generate with the CLI:");
583
+ hint("nile g service <name> Add a new service");
584
+ hint("nile g action <service> <name> Add an action to a service");
585
+ hint("nile g schema Generate Zod schemas & types");
322
586
  outro();
323
587
  };
324
588
 
@@ -329,4 +593,5 @@ program.command("new").argument("<project-name>", "Name of the project to create
329
593
  var generate = program.command("generate").alias("g").description("Generate services and actions");
330
594
  generate.command("service").argument("<name>", "Service name (kebab-case recommended)").description("Generate a new service with a demo action").action(generateServiceCommand);
331
595
  generate.command("action").argument("<service-name>", "Name of the existing service").argument("<action-name>", "Name of the action to create").description("Generate a new action in an existing service").action(generateActionCommand);
596
+ generate.command("schema").option("-e, --entry <path>", "Path to services config file").option("-o, --output <path>", "Output directory for generated files").description("Generate Zod schemas and TypeScript types from action validations").action(generateSchemaCommand);
332
597
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nilejs/cli",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "CLI for scaffolding and generating Nile backend projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,6 +16,7 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "commander": "^13.1.0",
19
+ "json-schema-to-zod": "^2.7.0",
19
20
  "picocolors": "^1.1.1"
20
21
  },
21
22
  "devDependencies": {
@@ -78,6 +78,7 @@ Generate a new service with the CLI:
78
78
 
79
79
  ```bash
80
80
  nile generate service users
81
+ # or: nile g service users
81
82
  ```
82
83
 
83
84
  Or manually create a directory under `src/services/` with action files and register it in `src/services/services.config.ts`.
@@ -88,10 +89,34 @@ Generate a new action in an existing service:
88
89
 
89
90
  ```bash
90
91
  nile generate action users get-user
92
+ # or: nile g action users get-user
91
93
  ```
92
94
 
93
95
  Each action file exports a single action created with `createAction`, which takes a Zod validation schema and a handler function that returns `Ok(data)` or `Err(message)`.
94
96
 
97
+ ## Generating Schemas & Types
98
+
99
+ Extract Zod validation schemas from your actions and generate TypeScript types:
100
+
101
+ ```bash
102
+ nile generate schema
103
+ # or: nile g schema
104
+ ```
105
+
106
+ This auto-detects `src/services/services.config.ts`, reads validation schemas from all actions, and outputs two files:
107
+
108
+ - `src/generated/schemas.ts` — named Zod schema exports
109
+ - `src/generated/types.ts` — inferred TypeScript types via `z.infer`
110
+
111
+ Options:
112
+
113
+ | Flag | Description |
114
+ |---|---|
115
+ | `-e, --entry <path>` | Path to services config (auto-detected by default) |
116
+ | `-o, --output <path>` | Output directory (default: `src/generated`) |
117
+
118
+ Actions without a validation schema are skipped and listed in the CLI output.
119
+
95
120
  ## Database
96
121
 
97
122
  This project ships with [PGLite](https://electric-sql.com/product/pglite) for zero-setup local development. For production, you can swap it for Postgres, MySQL, SQLite, or any other database supported by Drizzle. Update `src/db/client.ts` with your connection and driver.
@@ -10,7 +10,7 @@
10
10
  "db:studio": "bunx drizzle-kit studio"
11
11
  },
12
12
  "dependencies": {
13
- "@nilejs/nile": "^0.0.1",
13
+ "@nilejs/nile": "latest",
14
14
  "@electric-sql/pglite": "^0.2.5",
15
15
  "drizzle-orm": "^0.39.2",
16
16
  "pino": "^10.3.1",
@@ -1,21 +1,21 @@
1
- import { createActions, type Services } from "@nilejs/nile";
1
+ import { createServices, type Services } from "@nilejs/nile";
2
2
  import { createTaskAction } from "./tasks/create";
3
3
  import { deleteTaskAction } from "./tasks/delete";
4
4
  import { getTaskAction } from "./tasks/get";
5
5
  import { listTaskAction } from "./tasks/list";
6
6
  import { updateTaskAction } from "./tasks/update";
7
7
 
8
- export const services: Services = [
8
+ export const services: Services = createServices([
9
9
  {
10
10
  name: "tasks",
11
11
  description: "Task management with CRUD operations",
12
12
  meta: { version: "1.0.0" },
13
- actions: createActions([
13
+ actions: [
14
14
  createTaskAction,
15
15
  listTaskAction,
16
16
  getTaskAction,
17
17
  updateTaskAction,
18
18
  deleteTaskAction,
19
- ]),
19
+ ],
20
20
  },
21
- ];
21
+ ]);