@nilejs/cli 0.0.1 → 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 +37 -0
- package/dist/index.js +329 -39
- package/package.json +2 -1
- package/template/README.md +29 -4
- package/template/package.json +1 -1
- package/template/src/index.ts +3 -3
- package/template/src/services/services.config.ts +5 -5
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
|
@@ -18,7 +18,7 @@ var writeFileSafe = async (filePath, content) => {
|
|
|
18
18
|
await ensureDir(dirname(filePath));
|
|
19
19
|
await writeFile(filePath, content, "utf-8");
|
|
20
20
|
};
|
|
21
|
-
var readFileContent =
|
|
21
|
+
var readFileContent = (filePath) => {
|
|
22
22
|
return readFile(filePath, "utf-8");
|
|
23
23
|
};
|
|
24
24
|
var copyDir = async (src, dest) => {
|
|
@@ -49,14 +49,33 @@ var replaceInFile = async (filePath, replacements) => {
|
|
|
49
49
|
|
|
50
50
|
// src/utils/log.ts
|
|
51
51
|
import pc from "picocolors";
|
|
52
|
+
var brand = () => console.log(`
|
|
53
|
+
${pc.bold(pc.cyan("~ Nile"))}
|
|
54
|
+
`);
|
|
55
|
+
var outro = () => console.log(pc.dim(`
|
|
56
|
+
Happy hacking. Let code flow like river Nile.
|
|
57
|
+
`));
|
|
52
58
|
var success = (msg) => console.log(pc.green(` ✓ ${msg}`));
|
|
53
|
-
var info = (msg) => console.log(pc.cyan(` ${msg}`));
|
|
54
59
|
var warn = (msg) => console.log(pc.yellow(` ⚠ ${msg}`));
|
|
55
60
|
var error = (msg) => console.error(pc.red(` ✗ ${msg}`));
|
|
56
|
-
var header = (msg) => console.log(`
|
|
57
|
-
${pc.bold(msg)}
|
|
58
|
-
`);
|
|
59
61
|
var hint = (msg) => console.log(pc.dim(` ${msg}`));
|
|
62
|
+
var SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
63
|
+
var createSpinner = (msg) => {
|
|
64
|
+
let i = 0;
|
|
65
|
+
const stream = process.stdout;
|
|
66
|
+
const id = setInterval(() => {
|
|
67
|
+
const frame = SPINNER_FRAMES[i % SPINNER_FRAMES.length];
|
|
68
|
+
stream.write(`\r ${pc.cyan(frame)} ${msg}`);
|
|
69
|
+
i++;
|
|
70
|
+
}, 80);
|
|
71
|
+
return {
|
|
72
|
+
stop: (finalMsg) => {
|
|
73
|
+
clearInterval(id);
|
|
74
|
+
stream.write(`\r ${pc.green("✓")} ${finalMsg}
|
|
75
|
+
`);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
};
|
|
60
79
|
|
|
61
80
|
// src/commands/generate-action.ts
|
|
62
81
|
var toCamelCase = (str) => str.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
@@ -92,7 +111,7 @@ var generateActionCommand = async (serviceName, actionName) => {
|
|
|
92
111
|
const serviceDir = resolve(process.cwd(), "src/services", serviceName);
|
|
93
112
|
if (!pathExists(serviceDir)) {
|
|
94
113
|
error(`Service "${serviceName}" not found at src/services/${serviceName}/`);
|
|
95
|
-
hint(
|
|
114
|
+
hint(`Create the service first: nile generate service ${serviceName}`);
|
|
96
115
|
process.exit(1);
|
|
97
116
|
}
|
|
98
117
|
const actionFile = resolve(serviceDir, `${actionName}.ts`);
|
|
@@ -100,21 +119,24 @@ var generateActionCommand = async (serviceName, actionName) => {
|
|
|
100
119
|
error(`Action file "${actionName}.ts" already exists in src/services/${serviceName}/`);
|
|
101
120
|
process.exit(1);
|
|
102
121
|
}
|
|
103
|
-
|
|
122
|
+
brand();
|
|
123
|
+
const spinner = createSpinner(`Creating action ${actionName}...`);
|
|
104
124
|
await writeFileSafe(actionFile, generateActionContent(actionName, serviceName));
|
|
105
|
-
|
|
125
|
+
spinner.stop(`Action created at src/services/${serviceName}/${actionName}.ts`);
|
|
106
126
|
const camel = toCamelCase(actionName);
|
|
107
|
-
|
|
108
|
-
hint("
|
|
127
|
+
console.log("");
|
|
128
|
+
hint("Register the action in your service config:");
|
|
109
129
|
hint(` import { ${camel}Action } from "./${serviceName}/${actionName}";`);
|
|
130
|
+
outro();
|
|
110
131
|
};
|
|
111
132
|
|
|
112
|
-
// src/commands/generate-
|
|
113
|
-
import {
|
|
133
|
+
// src/commands/generate-schema.ts
|
|
134
|
+
import { execFileSync } from "node:child_process";
|
|
135
|
+
import { resolve as resolve3 } from "node:path";
|
|
114
136
|
|
|
115
137
|
// src/utils/prompt.ts
|
|
116
138
|
import { createInterface } from "node:readline";
|
|
117
|
-
var confirmPrompt =
|
|
139
|
+
var confirmPrompt = (question, defaultYes = true) => {
|
|
118
140
|
const suffix = defaultYes ? "[Y/n]" : "[y/N]";
|
|
119
141
|
const rl = createInterface({
|
|
120
142
|
input: process.stdin,
|
|
@@ -132,15 +154,273 @@ var confirmPrompt = async (question, defaultYes = true) => {
|
|
|
132
154
|
});
|
|
133
155
|
});
|
|
134
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
|
+
};
|
|
135
169
|
|
|
136
|
-
// src/
|
|
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
|
+
};
|
|
137
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());
|
|
138
418
|
var toPascalCase2 = (str) => {
|
|
139
|
-
const camel =
|
|
419
|
+
const camel = toCamelCase3(str);
|
|
140
420
|
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
|
141
421
|
};
|
|
142
422
|
var generateActionContent2 = (serviceName) => {
|
|
143
|
-
const camel =
|
|
423
|
+
const camel = toCamelCase3(serviceName);
|
|
144
424
|
const pascal = toPascalCase2(serviceName);
|
|
145
425
|
return `import { type Action, createAction } from "@nilejs/nile";
|
|
146
426
|
import { Ok } from "slang-ts";
|
|
@@ -173,7 +453,7 @@ var generateConfigSnippet = (serviceName) => {
|
|
|
173
453
|
const serviceEntry = ` {
|
|
174
454
|
name: "${serviceName}",
|
|
175
455
|
description: "${pascal} service",
|
|
176
|
-
actions:
|
|
456
|
+
actions: [sample${pascal}Action],
|
|
177
457
|
},`;
|
|
178
458
|
return `${importLine}
|
|
179
459
|
|
|
@@ -199,7 +479,7 @@ ${importLine}`);
|
|
|
199
479
|
const serviceEntry = ` {
|
|
200
480
|
name: "${serviceName}",
|
|
201
481
|
description: "${pascal} service",
|
|
202
|
-
actions:
|
|
482
|
+
actions: [sample${pascal}Action],
|
|
203
483
|
},`;
|
|
204
484
|
const closingIndex = content.lastIndexOf("];");
|
|
205
485
|
if (closingIndex === -1) {
|
|
@@ -214,36 +494,37 @@ ${importLine}`);
|
|
|
214
494
|
}
|
|
215
495
|
};
|
|
216
496
|
var generateServiceCommand = async (serviceName) => {
|
|
217
|
-
const servicesDir =
|
|
497
|
+
const servicesDir = resolve4(process.cwd(), "src/services");
|
|
218
498
|
if (!pathExists(servicesDir)) {
|
|
219
499
|
error("Could not find src/services/ directory.");
|
|
220
500
|
hint("Make sure you're in a Nile project root.");
|
|
221
501
|
process.exit(1);
|
|
222
502
|
}
|
|
223
|
-
const serviceDir =
|
|
503
|
+
const serviceDir = resolve4(servicesDir, serviceName);
|
|
224
504
|
if (pathExists(serviceDir)) {
|
|
225
505
|
error(`Service "${serviceName}" already exists at src/services/${serviceName}/`);
|
|
226
506
|
process.exit(1);
|
|
227
507
|
}
|
|
228
|
-
|
|
508
|
+
brand();
|
|
509
|
+
const spinner = createSpinner(`Creating service ${serviceName}...`);
|
|
229
510
|
await ensureDir(serviceDir);
|
|
230
|
-
|
|
231
|
-
await writeFileSafe(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
success(`Service "${serviceName}" created at src/services/${serviceName}/`);
|
|
235
|
-
const configPath = resolve2(servicesDir, "services.config.ts");
|
|
511
|
+
await writeFileSafe(resolve4(serviceDir, "sample.ts"), generateActionContent2(serviceName));
|
|
512
|
+
await writeFileSafe(resolve4(serviceDir, "index.ts"), generateBarrelContent(serviceName));
|
|
513
|
+
spinner.stop(`Service created at src/services/${serviceName}/`);
|
|
514
|
+
const configPath = resolve4(servicesDir, "services.config.ts");
|
|
236
515
|
if (!pathExists(configPath)) {
|
|
237
516
|
warn("Could not find services.config.ts");
|
|
238
|
-
|
|
517
|
+
console.log("");
|
|
518
|
+
hint("Add this to your services config:");
|
|
239
519
|
console.log(generateConfigSnippet(serviceName));
|
|
520
|
+
outro();
|
|
240
521
|
return;
|
|
241
522
|
}
|
|
242
523
|
const shouldRegister = await confirmPrompt("Register this service in services.config.ts?");
|
|
243
524
|
if (shouldRegister) {
|
|
244
525
|
const registered = await autoRegisterService(configPath, serviceName);
|
|
245
526
|
if (registered) {
|
|
246
|
-
success("
|
|
527
|
+
success("Registered in services.config.ts");
|
|
247
528
|
} else {
|
|
248
529
|
warn("Could not auto-register. Add manually:");
|
|
249
530
|
console.log(`
|
|
@@ -251,37 +532,38 @@ ${generateConfigSnippet(serviceName)}
|
|
|
251
532
|
`);
|
|
252
533
|
}
|
|
253
534
|
} else {
|
|
254
|
-
|
|
535
|
+
console.log("");
|
|
536
|
+
hint("Add this to your services config:");
|
|
255
537
|
console.log(generateConfigSnippet(serviceName));
|
|
256
538
|
}
|
|
539
|
+
outro();
|
|
257
540
|
};
|
|
258
541
|
|
|
259
542
|
// src/commands/new.ts
|
|
260
|
-
import { resolve as
|
|
543
|
+
import { resolve as resolve5 } from "node:path";
|
|
261
544
|
import { fileURLToPath } from "node:url";
|
|
262
545
|
var __dirname2 = fileURLToPath(new URL(".", import.meta.url));
|
|
263
546
|
var resolveTemplateDir = () => {
|
|
264
|
-
const devPath =
|
|
547
|
+
const devPath = resolve5(__dirname2, "../../template");
|
|
265
548
|
if (pathExists(devPath)) {
|
|
266
549
|
return devPath;
|
|
267
550
|
}
|
|
268
|
-
const distPath =
|
|
551
|
+
const distPath = resolve5(__dirname2, "../template");
|
|
269
552
|
if (pathExists(distPath)) {
|
|
270
553
|
return distPath;
|
|
271
554
|
}
|
|
272
555
|
throw new Error("Template directory not found. The CLI package may be corrupted.");
|
|
273
556
|
};
|
|
274
557
|
var newCommand = async (projectName) => {
|
|
275
|
-
const targetDir =
|
|
558
|
+
const targetDir = resolve5(process.cwd(), projectName);
|
|
276
559
|
if (pathExists(targetDir)) {
|
|
277
560
|
error(`Directory "${projectName}" already exists.`);
|
|
278
561
|
process.exit(1);
|
|
279
562
|
}
|
|
280
|
-
|
|
563
|
+
brand();
|
|
564
|
+
const spinner = createSpinner(`Creating ${projectName}...`);
|
|
281
565
|
const templateDir = resolveTemplateDir();
|
|
282
|
-
info("Copying project files...");
|
|
283
566
|
await copyDir(templateDir, targetDir);
|
|
284
|
-
info("Configuring project...");
|
|
285
567
|
const allFiles = await getFilesRecursive(targetDir);
|
|
286
568
|
const replacements = { "{{projectName}}": projectName };
|
|
287
569
|
for (const filePath of allFiles) {
|
|
@@ -289,12 +571,19 @@ var newCommand = async (projectName) => {
|
|
|
289
571
|
await replaceInFile(filePath, replacements);
|
|
290
572
|
}
|
|
291
573
|
}
|
|
292
|
-
|
|
293
|
-
|
|
574
|
+
spinner.stop("Project ready.");
|
|
575
|
+
success(`Created ${projectName}`);
|
|
576
|
+
console.log("");
|
|
294
577
|
hint(`cd ${projectName}`);
|
|
295
578
|
hint("bun install");
|
|
296
579
|
hint("cp .env.example .env");
|
|
297
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");
|
|
586
|
+
outro();
|
|
298
587
|
};
|
|
299
588
|
|
|
300
589
|
// src/index.ts
|
|
@@ -304,4 +593,5 @@ program.command("new").argument("<project-name>", "Name of the project to create
|
|
|
304
593
|
var generate = program.command("generate").alias("g").description("Generate services and actions");
|
|
305
594
|
generate.command("service").argument("<name>", "Service name (kebab-case recommended)").description("Generate a new service with a demo action").action(generateServiceCommand);
|
|
306
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);
|
|
307
597
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nilejs/cli",
|
|
3
|
-
"version": "0.0.
|
|
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": {
|
package/template/README.md
CHANGED
|
@@ -10,7 +10,7 @@ cp .env.example .env
|
|
|
10
10
|
bun run dev
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
The server starts at `http://localhost:
|
|
13
|
+
The server starts at `http://localhost:8000`. PGLite creates an embedded Postgres database automatically, no external database required.
|
|
14
14
|
|
|
15
15
|
## Scripts
|
|
16
16
|
|
|
@@ -51,7 +51,7 @@ All requests go through a single POST endpoint. The `intent` field determines th
|
|
|
51
51
|
### Explore available services
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
|
-
curl -X POST http://localhost:
|
|
54
|
+
curl -X POST http://localhost:8000/api/services \
|
|
55
55
|
-H "Content-Type: application/json" \
|
|
56
56
|
-d '{"intent":"explore","service":"*","action":"*","payload":{}}'
|
|
57
57
|
```
|
|
@@ -59,7 +59,7 @@ curl -X POST http://localhost:3000/api/services \
|
|
|
59
59
|
### Execute an action
|
|
60
60
|
|
|
61
61
|
```bash
|
|
62
|
-
curl -X POST http://localhost:
|
|
62
|
+
curl -X POST http://localhost:8000/api/services \
|
|
63
63
|
-H "Content-Type: application/json" \
|
|
64
64
|
-d '{"intent":"execute","service":"tasks","action":"create","payload":{"title":"My first task"}}'
|
|
65
65
|
```
|
|
@@ -67,7 +67,7 @@ curl -X POST http://localhost:3000/api/services \
|
|
|
67
67
|
### Get action schemas
|
|
68
68
|
|
|
69
69
|
```bash
|
|
70
|
-
curl -X POST http://localhost:
|
|
70
|
+
curl -X POST http://localhost:8000/api/services \
|
|
71
71
|
-H "Content-Type: application/json" \
|
|
72
72
|
-d '{"intent":"schema","service":"tasks","action":"*","payload":{}}'
|
|
73
73
|
```
|
|
@@ -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.
|
package/template/package.json
CHANGED
package/template/src/index.ts
CHANGED
|
@@ -36,8 +36,8 @@ const server = createNileServer({
|
|
|
36
36
|
rest: {
|
|
37
37
|
baseUrl: "/api",
|
|
38
38
|
host: "localhost",
|
|
39
|
-
port:
|
|
40
|
-
allowedOrigins: ["http://localhost:
|
|
39
|
+
port: 8000,
|
|
40
|
+
allowedOrigins: ["http://localhost:8000"],
|
|
41
41
|
enableStatus: true,
|
|
42
42
|
},
|
|
43
43
|
onBoot: {
|
|
@@ -52,7 +52,7 @@ const server = createNileServer({
|
|
|
52
52
|
});
|
|
53
53
|
|
|
54
54
|
if (server.rest) {
|
|
55
|
-
const port = server.config.rest?.port ??
|
|
55
|
+
const port = server.config.rest?.port ?? 8000;
|
|
56
56
|
const { fetch } = server.rest.app;
|
|
57
57
|
|
|
58
58
|
Bun.serve({ port, fetch });
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import {
|
|
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:
|
|
13
|
+
actions: [
|
|
14
14
|
createTaskAction,
|
|
15
15
|
listTaskAction,
|
|
16
16
|
getTaskAction,
|
|
17
17
|
updateTaskAction,
|
|
18
18
|
deleteTaskAction,
|
|
19
|
-
]
|
|
19
|
+
],
|
|
20
20
|
},
|
|
21
|
-
];
|
|
21
|
+
]);
|