@nilejs/cli 0.0.1
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 +152 -0
- package/dist/index.js +307 -0
- package/package.json +25 -0
- package/template/.env.example +1 -0
- package/template/README.md +107 -0
- package/template/drizzle.config.ts +7 -0
- package/template/package.json +25 -0
- package/template/src/db/client.ts +13 -0
- package/template/src/db/index.ts +3 -0
- package/template/src/db/models/index.ts +1 -0
- package/template/src/db/models/tasks.ts +6 -0
- package/template/src/db/schema.ts +17 -0
- package/template/src/db/types.ts +4 -0
- package/template/src/index.ts +67 -0
- package/template/src/services/services.config.ts +21 -0
- package/template/src/services/tasks/create.ts +34 -0
- package/template/src/services/tasks/delete.ts +23 -0
- package/template/src/services/tasks/get.ts +23 -0
- package/template/src/services/tasks/list.ts +17 -0
- package/template/src/services/tasks/update.ts +31 -0
- package/template/tsconfig.json +18 -0
package/README.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# @nilejs/cli
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@nilejs/cli)
|
|
4
|
+
|
|
5
|
+
CLI for scaffolding and generating [Nile](https://www.npmjs.com/package/@nilejs/nile) backend projects.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun add -g @nilejs/cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g @nilejs/cli
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or use without installing:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bunx @nilejs/cli new my-app
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx @nilejs/cli new my-app
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Commands
|
|
28
|
+
|
|
29
|
+
### `nile new <project-name>`
|
|
30
|
+
|
|
31
|
+
Scaffold a new Nile project. Copies the project template, replaces placeholders with your project name, and prints next steps.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
nile new my-app
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Output:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
Creating project: my-app
|
|
41
|
+
|
|
42
|
+
Copying project files...
|
|
43
|
+
Configuring project...
|
|
44
|
+
Project "my-app" created.
|
|
45
|
+
|
|
46
|
+
Next steps:
|
|
47
|
+
|
|
48
|
+
cd my-app
|
|
49
|
+
bun install
|
|
50
|
+
cp .env.example .env
|
|
51
|
+
bun run dev
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The scaffolded project includes:
|
|
55
|
+
|
|
56
|
+
- `src/index.ts`, server entry with PGLite and Drizzle
|
|
57
|
+
- `src/db/`, database client, schema, types, and a `tasks` model using `createModel`
|
|
58
|
+
- `src/services/`, a `tasks` service with five CRUD actions
|
|
59
|
+
- `drizzle.config.ts`, `.env.example`, `tsconfig.json`, `package.json`
|
|
60
|
+
|
|
61
|
+
### `nile generate service <name>`
|
|
62
|
+
|
|
63
|
+
Alias: `nile g service <name>`
|
|
64
|
+
|
|
65
|
+
Generate a new service directory under `src/services/` with a demo action and barrel export. Run this from the project root.
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
nile g service users
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Creates:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
src/services/users/
|
|
75
|
+
sample.ts # Demo action with Zod schema, handler, and createAction
|
|
76
|
+
index.ts # Barrel export
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
After creating the files, the CLI asks whether to auto-register the service in `src/services/services.config.ts`. If you accept, it adds the import and service entry. If you decline, it prints the snippet to add manually.
|
|
80
|
+
|
|
81
|
+
### `nile generate action <service-name> <action-name>`
|
|
82
|
+
|
|
83
|
+
Alias: `nile g action <service-name> <action-name>`
|
|
84
|
+
|
|
85
|
+
Generate a new action file in an existing service directory.
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
nile g action users get-user
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Creates `src/services/users/get-user.ts` with:
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { type Action, createAction } from "@nilejs/nile";
|
|
95
|
+
import { Ok } from "slang-ts";
|
|
96
|
+
import z from "zod";
|
|
97
|
+
|
|
98
|
+
const getUserSchema = z.object({
|
|
99
|
+
// Define your validation schema here
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const getUserHandler = async (data: Record<string, unknown>) => {
|
|
103
|
+
// Implement your users.get-user logic here
|
|
104
|
+
return Ok({ result: data });
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const getUserAction: Action = createAction({
|
|
108
|
+
name: "get-user",
|
|
109
|
+
description: "GetUser action for users",
|
|
110
|
+
handler: getUserHandler,
|
|
111
|
+
validation: getUserSchema,
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Kebab-case names are converted to camelCase for variables and PascalCase for types.
|
|
116
|
+
|
|
117
|
+
## Generated Project Structure
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
my-app/
|
|
121
|
+
package.json
|
|
122
|
+
tsconfig.json
|
|
123
|
+
drizzle.config.ts
|
|
124
|
+
.env.example
|
|
125
|
+
src/
|
|
126
|
+
index.ts
|
|
127
|
+
db/
|
|
128
|
+
client.ts
|
|
129
|
+
schema.ts
|
|
130
|
+
types.ts
|
|
131
|
+
index.ts
|
|
132
|
+
models/
|
|
133
|
+
tasks.ts
|
|
134
|
+
index.ts
|
|
135
|
+
services/
|
|
136
|
+
services.config.ts
|
|
137
|
+
tasks/
|
|
138
|
+
create.ts
|
|
139
|
+
list.ts
|
|
140
|
+
get.ts
|
|
141
|
+
update.ts
|
|
142
|
+
delete.ts
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Requirements
|
|
146
|
+
|
|
147
|
+
- Node.js 18+ or Bun
|
|
148
|
+
- The scaffolded project uses Bun as its runtime (`Bun.serve`, `bun run`)
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/generate-action.ts
|
|
7
|
+
import { resolve } from "node:path";
|
|
8
|
+
|
|
9
|
+
// src/utils/files.ts
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
import { cp, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
var pathExists = (path) => existsSync(path);
|
|
14
|
+
var ensureDir = async (path) => {
|
|
15
|
+
await mkdir(path, { recursive: true });
|
|
16
|
+
};
|
|
17
|
+
var writeFileSafe = async (filePath, content) => {
|
|
18
|
+
await ensureDir(dirname(filePath));
|
|
19
|
+
await writeFile(filePath, content, "utf-8");
|
|
20
|
+
};
|
|
21
|
+
var readFileContent = async (filePath) => {
|
|
22
|
+
return readFile(filePath, "utf-8");
|
|
23
|
+
};
|
|
24
|
+
var copyDir = async (src, dest) => {
|
|
25
|
+
await cp(src, dest, { recursive: true });
|
|
26
|
+
};
|
|
27
|
+
var getFilesRecursive = async (dir, root) => {
|
|
28
|
+
const base = root ?? dir;
|
|
29
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
30
|
+
const files = [];
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
const fullPath = join(dir, entry.name);
|
|
33
|
+
if (entry.isDirectory()) {
|
|
34
|
+
const nested = await getFilesRecursive(fullPath, base);
|
|
35
|
+
files.push(...nested);
|
|
36
|
+
} else {
|
|
37
|
+
files.push(fullPath);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return files;
|
|
41
|
+
};
|
|
42
|
+
var replaceInFile = async (filePath, replacements) => {
|
|
43
|
+
let content = await readFileContent(filePath);
|
|
44
|
+
for (const [placeholder, value] of Object.entries(replacements)) {
|
|
45
|
+
content = content.replaceAll(placeholder, value);
|
|
46
|
+
}
|
|
47
|
+
await writeFile(filePath, content, "utf-8");
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// src/utils/log.ts
|
|
51
|
+
import pc from "picocolors";
|
|
52
|
+
var success = (msg) => console.log(pc.green(` ✓ ${msg}`));
|
|
53
|
+
var info = (msg) => console.log(pc.cyan(` ${msg}`));
|
|
54
|
+
var warn = (msg) => console.log(pc.yellow(` ⚠ ${msg}`));
|
|
55
|
+
var error = (msg) => console.error(pc.red(` ✗ ${msg}`));
|
|
56
|
+
var header = (msg) => console.log(`
|
|
57
|
+
${pc.bold(msg)}
|
|
58
|
+
`);
|
|
59
|
+
var hint = (msg) => console.log(pc.dim(` ${msg}`));
|
|
60
|
+
|
|
61
|
+
// src/commands/generate-action.ts
|
|
62
|
+
var toCamelCase = (str) => str.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
63
|
+
var toPascalCase = (str) => {
|
|
64
|
+
const camel = toCamelCase(str);
|
|
65
|
+
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
|
66
|
+
};
|
|
67
|
+
var generateActionContent = (actionName, serviceName) => {
|
|
68
|
+
const camel = toCamelCase(actionName);
|
|
69
|
+
const pascal = toPascalCase(actionName);
|
|
70
|
+
return `import { type Action, createAction } from "@nilejs/nile";
|
|
71
|
+
import { Ok } from "slang-ts";
|
|
72
|
+
import z from "zod";
|
|
73
|
+
|
|
74
|
+
const ${camel}Schema = z.object({
|
|
75
|
+
// Define your validation schema here
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const ${camel}Handler = async (data: Record<string, unknown>) => {
|
|
79
|
+
// Implement your ${serviceName}.${actionName} logic here
|
|
80
|
+
return Ok({ result: data });
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const ${camel}Action: Action = createAction({
|
|
84
|
+
name: "${actionName}",
|
|
85
|
+
description: "${pascal} action for ${serviceName}",
|
|
86
|
+
handler: ${camel}Handler,
|
|
87
|
+
validation: ${camel}Schema,
|
|
88
|
+
});
|
|
89
|
+
`;
|
|
90
|
+
};
|
|
91
|
+
var generateActionCommand = async (serviceName, actionName) => {
|
|
92
|
+
const serviceDir = resolve(process.cwd(), "src/services", serviceName);
|
|
93
|
+
if (!pathExists(serviceDir)) {
|
|
94
|
+
error(`Service "${serviceName}" not found at src/services/${serviceName}/`);
|
|
95
|
+
hint("Create the service first: nile generate service " + serviceName);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
const actionFile = resolve(serviceDir, `${actionName}.ts`);
|
|
99
|
+
if (pathExists(actionFile)) {
|
|
100
|
+
error(`Action file "${actionName}.ts" already exists in src/services/${serviceName}/`);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
header(`Generating action: ${serviceName}/${actionName}`);
|
|
104
|
+
await writeFileSafe(actionFile, generateActionContent(actionName, serviceName));
|
|
105
|
+
success(`Action created at src/services/${serviceName}/${actionName}.ts`);
|
|
106
|
+
const camel = toCamelCase(actionName);
|
|
107
|
+
header("Next steps:");
|
|
108
|
+
hint("Import and register the action in your service config:");
|
|
109
|
+
hint(` import { ${camel}Action } from "./${serviceName}/${actionName}";`);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// src/commands/generate-service.ts
|
|
113
|
+
import { resolve as resolve2 } from "node:path";
|
|
114
|
+
|
|
115
|
+
// src/utils/prompt.ts
|
|
116
|
+
import { createInterface } from "node:readline";
|
|
117
|
+
var confirmPrompt = async (question, defaultYes = true) => {
|
|
118
|
+
const suffix = defaultYes ? "[Y/n]" : "[y/N]";
|
|
119
|
+
const rl = createInterface({
|
|
120
|
+
input: process.stdin,
|
|
121
|
+
output: process.stdout
|
|
122
|
+
});
|
|
123
|
+
return new Promise((resolve2) => {
|
|
124
|
+
rl.question(` ${question} ${suffix} `, (answer) => {
|
|
125
|
+
rl.close();
|
|
126
|
+
const trimmed = answer.trim().toLowerCase();
|
|
127
|
+
if (trimmed === "") {
|
|
128
|
+
resolve2(defaultYes);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
resolve2(trimmed === "y" || trimmed === "yes");
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// src/commands/generate-service.ts
|
|
137
|
+
var toCamelCase2 = (str) => str.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
138
|
+
var toPascalCase2 = (str) => {
|
|
139
|
+
const camel = toCamelCase2(str);
|
|
140
|
+
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
|
141
|
+
};
|
|
142
|
+
var generateActionContent2 = (serviceName) => {
|
|
143
|
+
const camel = toCamelCase2(serviceName);
|
|
144
|
+
const pascal = toPascalCase2(serviceName);
|
|
145
|
+
return `import { type Action, createAction } from "@nilejs/nile";
|
|
146
|
+
import { Ok } from "slang-ts";
|
|
147
|
+
import z from "zod";
|
|
148
|
+
|
|
149
|
+
const sample${pascal}Schema = z.object({
|
|
150
|
+
name: z.string().min(1, "Name is required"),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const sample${pascal}Handler = async (data: Record<string, unknown>) => {
|
|
154
|
+
return Ok({ ${camel}: { name: data.name } });
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export const sample${pascal}Action: Action = createAction({
|
|
158
|
+
name: "sample",
|
|
159
|
+
description: "Sample ${serviceName} action",
|
|
160
|
+
handler: sample${pascal}Handler,
|
|
161
|
+
validation: sample${pascal}Schema,
|
|
162
|
+
});
|
|
163
|
+
`;
|
|
164
|
+
};
|
|
165
|
+
var generateBarrelContent = (serviceName) => {
|
|
166
|
+
const pascal = toPascalCase2(serviceName);
|
|
167
|
+
return `export { sample${pascal}Action } from "./sample";
|
|
168
|
+
`;
|
|
169
|
+
};
|
|
170
|
+
var generateConfigSnippet = (serviceName) => {
|
|
171
|
+
const pascal = toPascalCase2(serviceName);
|
|
172
|
+
const importLine = `import { sample${pascal}Action } from "./${serviceName}/sample";`;
|
|
173
|
+
const serviceEntry = ` {
|
|
174
|
+
name: "${serviceName}",
|
|
175
|
+
description: "${pascal} service",
|
|
176
|
+
actions: createActions([sample${pascal}Action]),
|
|
177
|
+
},`;
|
|
178
|
+
return `${importLine}
|
|
179
|
+
|
|
180
|
+
// Add to services array:
|
|
181
|
+
${serviceEntry}`;
|
|
182
|
+
};
|
|
183
|
+
var autoRegisterService = async (configPath, serviceName) => {
|
|
184
|
+
try {
|
|
185
|
+
let content = await readFileContent(configPath);
|
|
186
|
+
const pascal = toPascalCase2(serviceName);
|
|
187
|
+
const importLine = `import { sample${pascal}Action } from "./${serviceName}/sample";`;
|
|
188
|
+
const importLines = content.split(`
|
|
189
|
+
`).filter((line) => line.startsWith("import "));
|
|
190
|
+
if (importLines.length === 0) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
const lastImport = importLines.at(-1);
|
|
194
|
+
if (!lastImport) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
content = content.replace(lastImport, `${lastImport}
|
|
198
|
+
${importLine}`);
|
|
199
|
+
const serviceEntry = ` {
|
|
200
|
+
name: "${serviceName}",
|
|
201
|
+
description: "${pascal} service",
|
|
202
|
+
actions: createActions([sample${pascal}Action]),
|
|
203
|
+
},`;
|
|
204
|
+
const closingIndex = content.lastIndexOf("];");
|
|
205
|
+
if (closingIndex === -1) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
content = content.slice(0, closingIndex) + serviceEntry + `
|
|
209
|
+
` + content.slice(closingIndex);
|
|
210
|
+
await writeFileSafe(configPath, content);
|
|
211
|
+
return true;
|
|
212
|
+
} catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
var generateServiceCommand = async (serviceName) => {
|
|
217
|
+
const servicesDir = resolve2(process.cwd(), "src/services");
|
|
218
|
+
if (!pathExists(servicesDir)) {
|
|
219
|
+
error("Could not find src/services/ directory.");
|
|
220
|
+
hint("Make sure you're in a Nile project root.");
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
const serviceDir = resolve2(servicesDir, serviceName);
|
|
224
|
+
if (pathExists(serviceDir)) {
|
|
225
|
+
error(`Service "${serviceName}" already exists at src/services/${serviceName}/`);
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
header(`Generating service: ${serviceName}`);
|
|
229
|
+
await ensureDir(serviceDir);
|
|
230
|
+
info("Creating demo action...");
|
|
231
|
+
await writeFileSafe(resolve2(serviceDir, "sample.ts"), generateActionContent2(serviceName));
|
|
232
|
+
info("Creating barrel export...");
|
|
233
|
+
await writeFileSafe(resolve2(serviceDir, "index.ts"), generateBarrelContent(serviceName));
|
|
234
|
+
success(`Service "${serviceName}" created at src/services/${serviceName}/`);
|
|
235
|
+
const configPath = resolve2(servicesDir, "services.config.ts");
|
|
236
|
+
if (!pathExists(configPath)) {
|
|
237
|
+
warn("Could not find services.config.ts");
|
|
238
|
+
header("Add this to your services config:");
|
|
239
|
+
console.log(generateConfigSnippet(serviceName));
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const shouldRegister = await confirmPrompt("Register this service in services.config.ts?");
|
|
243
|
+
if (shouldRegister) {
|
|
244
|
+
const registered = await autoRegisterService(configPath, serviceName);
|
|
245
|
+
if (registered) {
|
|
246
|
+
success("Service registered in services.config.ts");
|
|
247
|
+
} else {
|
|
248
|
+
warn("Could not auto-register. Add manually:");
|
|
249
|
+
console.log(`
|
|
250
|
+
${generateConfigSnippet(serviceName)}
|
|
251
|
+
`);
|
|
252
|
+
}
|
|
253
|
+
} else {
|
|
254
|
+
header("Add this to your services config:");
|
|
255
|
+
console.log(generateConfigSnippet(serviceName));
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// src/commands/new.ts
|
|
260
|
+
import { resolve as resolve3 } from "node:path";
|
|
261
|
+
import { fileURLToPath } from "node:url";
|
|
262
|
+
var __dirname2 = fileURLToPath(new URL(".", import.meta.url));
|
|
263
|
+
var resolveTemplateDir = () => {
|
|
264
|
+
const devPath = resolve3(__dirname2, "../../template");
|
|
265
|
+
if (pathExists(devPath)) {
|
|
266
|
+
return devPath;
|
|
267
|
+
}
|
|
268
|
+
const distPath = resolve3(__dirname2, "../template");
|
|
269
|
+
if (pathExists(distPath)) {
|
|
270
|
+
return distPath;
|
|
271
|
+
}
|
|
272
|
+
throw new Error("Template directory not found. The CLI package may be corrupted.");
|
|
273
|
+
};
|
|
274
|
+
var newCommand = async (projectName) => {
|
|
275
|
+
const targetDir = resolve3(process.cwd(), projectName);
|
|
276
|
+
if (pathExists(targetDir)) {
|
|
277
|
+
error(`Directory "${projectName}" already exists.`);
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
header(`Creating project: ${projectName}`);
|
|
281
|
+
const templateDir = resolveTemplateDir();
|
|
282
|
+
info("Copying project files...");
|
|
283
|
+
await copyDir(templateDir, targetDir);
|
|
284
|
+
info("Configuring project...");
|
|
285
|
+
const allFiles = await getFilesRecursive(targetDir);
|
|
286
|
+
const replacements = { "{{projectName}}": projectName };
|
|
287
|
+
for (const filePath of allFiles) {
|
|
288
|
+
if (filePath.endsWith(".ts") || filePath.endsWith(".json") || filePath.endsWith(".md")) {
|
|
289
|
+
await replaceInFile(filePath, replacements);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
success(`Project "${projectName}" created.`);
|
|
293
|
+
header("Next steps:");
|
|
294
|
+
hint(`cd ${projectName}`);
|
|
295
|
+
hint("bun install");
|
|
296
|
+
hint("cp .env.example .env");
|
|
297
|
+
hint("bun run dev");
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// src/index.ts
|
|
301
|
+
var program = new Command;
|
|
302
|
+
program.name("nile").description("CLI for the Nile backend framework").version("0.0.1");
|
|
303
|
+
program.command("new").argument("<project-name>", "Name of the project to create").description("Scaffold a new Nile project").action(newCommand);
|
|
304
|
+
var generate = program.command("generate").alias("g").description("Generate services and actions");
|
|
305
|
+
generate.command("service").argument("<name>", "Service name (kebab-case recommended)").description("Generate a new service with a demo action").action(generateServiceCommand);
|
|
306
|
+
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);
|
|
307
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nilejs/cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "CLI for scaffolding and generating Nile backend projects",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"nile": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"template"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "bun run scripts/build.ts",
|
|
15
|
+
"dev": "bun run src/index.ts"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"commander": "^13.1.0",
|
|
19
|
+
"picocolors": "^1.1.1"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^22.8.1",
|
|
23
|
+
"typescript": "^5"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
MODE=dev
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# {{projectName}}
|
|
2
|
+
|
|
3
|
+
Built with [Nile](https://www.npmjs.com/package/@nilejs/nile).
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun install
|
|
9
|
+
cp .env.example .env
|
|
10
|
+
bun run dev
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The server starts at `http://localhost:3000`. PGLite creates an embedded Postgres database automatically, no external database required.
|
|
14
|
+
|
|
15
|
+
## Scripts
|
|
16
|
+
|
|
17
|
+
| Script | Description |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `bun run dev` | Start the development server |
|
|
20
|
+
| `bun run db:generate` | Generate Drizzle migrations from schema changes |
|
|
21
|
+
| `bun run db:push` | Push schema changes directly to the database |
|
|
22
|
+
| `bun run db:studio` | Open Drizzle Studio to browse your data |
|
|
23
|
+
|
|
24
|
+
## Project Structure
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
src/
|
|
28
|
+
index.ts # Server entry point
|
|
29
|
+
db/
|
|
30
|
+
client.ts # PGLite + Drizzle client
|
|
31
|
+
schema.ts # Drizzle table definitions
|
|
32
|
+
types.ts # Inferred types from schema
|
|
33
|
+
index.ts # Barrel export
|
|
34
|
+
models/
|
|
35
|
+
tasks.ts # Task model (createModel)
|
|
36
|
+
index.ts # Barrel export
|
|
37
|
+
services/
|
|
38
|
+
services.config.ts # Service registry
|
|
39
|
+
tasks/
|
|
40
|
+
create.ts # Create task action
|
|
41
|
+
list.ts # List tasks action
|
|
42
|
+
get.ts # Get task by ID action
|
|
43
|
+
update.ts # Update task action
|
|
44
|
+
delete.ts # Delete task action
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
All requests go through a single POST endpoint. The `intent` field determines the operation.
|
|
50
|
+
|
|
51
|
+
### Explore available services
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
curl -X POST http://localhost:3000/api/services \
|
|
55
|
+
-H "Content-Type: application/json" \
|
|
56
|
+
-d '{"intent":"explore","service":"*","action":"*","payload":{}}'
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Execute an action
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
curl -X POST http://localhost:3000/api/services \
|
|
63
|
+
-H "Content-Type: application/json" \
|
|
64
|
+
-d '{"intent":"execute","service":"tasks","action":"create","payload":{"title":"My first task"}}'
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Get action schemas
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
curl -X POST http://localhost:3000/api/services \
|
|
71
|
+
-H "Content-Type: application/json" \
|
|
72
|
+
-d '{"intent":"schema","service":"tasks","action":"*","payload":{}}'
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Adding Services
|
|
76
|
+
|
|
77
|
+
Generate a new service with the CLI:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
nile generate service users
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Or manually create a directory under `src/services/` with action files and register it in `src/services/services.config.ts`.
|
|
84
|
+
|
|
85
|
+
## Adding Actions
|
|
86
|
+
|
|
87
|
+
Generate a new action in an existing service:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
nile generate action users get-user
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
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
|
+
|
|
95
|
+
## Database
|
|
96
|
+
|
|
97
|
+
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.
|
|
98
|
+
|
|
99
|
+
See the [Drizzle getting started guide](https://orm.drizzle.team/docs/get-started) for setup instructions with each supported database.
|
|
100
|
+
|
|
101
|
+
## Tech Stack
|
|
102
|
+
|
|
103
|
+
- **Runtime:** [Bun](https://bun.sh)
|
|
104
|
+
- **Framework:** [@nilejs/nile](https://www.npmjs.com/package/@nilejs/nile)
|
|
105
|
+
- **Database:** [PGLite](https://electric-sql.com/product/pglite) (embedded Postgres)
|
|
106
|
+
- **ORM:** [Drizzle](https://orm.drizzle.team)
|
|
107
|
+
- **Validation:** [Zod](https://zod.dev)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"private": true,
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "bun run src/index.ts",
|
|
8
|
+
"db:generate": "bunx drizzle-kit generate",
|
|
9
|
+
"db:push": "bunx drizzle-kit push",
|
|
10
|
+
"db:studio": "bunx drizzle-kit studio"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@nilejs/nile": "^0.0.1",
|
|
14
|
+
"@electric-sql/pglite": "^0.2.5",
|
|
15
|
+
"drizzle-orm": "^0.39.2",
|
|
16
|
+
"pino": "^10.3.1",
|
|
17
|
+
"slang-ts": "^0.0.7",
|
|
18
|
+
"zod": "^4.3.6"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/bun": "latest",
|
|
22
|
+
"drizzle-kit": "^0.31.4",
|
|
23
|
+
"typescript": "^5"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { PGlite } from "@electric-sql/pglite";
|
|
2
|
+
import { drizzle } from "drizzle-orm/pglite";
|
|
3
|
+
// biome-ignore lint/performance/noNamespaceImport: Drizzle requires namespace import for schema passthrough
|
|
4
|
+
import * as schema from "./schema";
|
|
5
|
+
|
|
6
|
+
/** Resolve data directory relative to project root (where bun run is invoked) */
|
|
7
|
+
const DATA_DIR = `${process.cwd()}/data`;
|
|
8
|
+
|
|
9
|
+
/** PGLite instance with file-based persistence */
|
|
10
|
+
export const pglite = new PGlite(DATA_DIR);
|
|
11
|
+
|
|
12
|
+
/** Drizzle ORM instance wrapping PGLite, with schema for relational queries */
|
|
13
|
+
export const db = drizzle(pglite, { schema });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { taskModel } from "./tasks";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { createModel } from "@nilejs/nile";
|
|
2
|
+
import { db } from "@/db/client";
|
|
3
|
+
import { tasks } from "@/db/schema";
|
|
4
|
+
|
|
5
|
+
/** CRUD model for tasks — auto-validates, handles errors, and supports transactions */
|
|
6
|
+
export const taskModel = createModel(tasks, { db, name: "task" });
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
|
|
2
|
+
|
|
3
|
+
export const tasks = pgTable("tasks", {
|
|
4
|
+
id: uuid("id").defaultRandom().primaryKey(),
|
|
5
|
+
title: text("title").notNull(),
|
|
6
|
+
description: text("description"),
|
|
7
|
+
status: text("status", { enum: ["pending", "in-progress", "done"] })
|
|
8
|
+
.notNull()
|
|
9
|
+
.default("pending"),
|
|
10
|
+
created_at: timestamp("created_at", { withTimezone: true })
|
|
11
|
+
.notNull()
|
|
12
|
+
.defaultNow(),
|
|
13
|
+
updated_at: timestamp("updated_at", { withTimezone: true })
|
|
14
|
+
.notNull()
|
|
15
|
+
.defaultNow()
|
|
16
|
+
.$onUpdate(() => new Date()),
|
|
17
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createLogger, createNileServer } from "@nilejs/nile";
|
|
2
|
+
import { sql } from "drizzle-orm";
|
|
3
|
+
import { safeTry } from "slang-ts";
|
|
4
|
+
import { db } from "@/db/client";
|
|
5
|
+
import { services } from "@/services/services.config";
|
|
6
|
+
|
|
7
|
+
const logger = createLogger("{{projectName}}", { chunking: "monthly" });
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Push schema to PGLite on boot.
|
|
11
|
+
* Creates the tasks table if it doesn't exist.
|
|
12
|
+
*/
|
|
13
|
+
const pushSchema = async () => {
|
|
14
|
+
const result = await safeTry(() =>
|
|
15
|
+
db.execute(sql`
|
|
16
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
17
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
18
|
+
title TEXT NOT NULL,
|
|
19
|
+
description TEXT,
|
|
20
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
21
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
22
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
23
|
+
)
|
|
24
|
+
`)
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
if (result.isErr) {
|
|
28
|
+
console.error("[pushSchema] Failed:", result.error);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const server = createNileServer({
|
|
33
|
+
serverName: "{{projectName}}",
|
|
34
|
+
services,
|
|
35
|
+
resources: { logger, database: db },
|
|
36
|
+
rest: {
|
|
37
|
+
baseUrl: "/api",
|
|
38
|
+
host: "localhost",
|
|
39
|
+
port: 3000,
|
|
40
|
+
allowedOrigins: ["http://localhost:3000"],
|
|
41
|
+
enableStatus: true,
|
|
42
|
+
},
|
|
43
|
+
onBoot: {
|
|
44
|
+
fn: async () => {
|
|
45
|
+
await pushSchema();
|
|
46
|
+
logger.info({
|
|
47
|
+
atFunction: "onBoot",
|
|
48
|
+
message: "{{projectName}} booted — PGLite schema ready",
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (server.rest) {
|
|
55
|
+
const port = server.config.rest?.port ?? 3000;
|
|
56
|
+
const { fetch } = server.rest.app;
|
|
57
|
+
|
|
58
|
+
Bun.serve({ port, fetch });
|
|
59
|
+
|
|
60
|
+
console.log(`\n{{projectName}} listening on http://localhost:${port}`);
|
|
61
|
+
console.log("\nTry it:");
|
|
62
|
+
console.log(` curl -X POST http://localhost:${port}/api/services \\`);
|
|
63
|
+
console.log(` -H "Content-Type: application/json" \\`);
|
|
64
|
+
console.log(
|
|
65
|
+
` -d '{"intent":"explore","service":"*","action":"*","payload":{}}'`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createActions, type Services } from "@nilejs/nile";
|
|
2
|
+
import { createTaskAction } from "./tasks/create";
|
|
3
|
+
import { deleteTaskAction } from "./tasks/delete";
|
|
4
|
+
import { getTaskAction } from "./tasks/get";
|
|
5
|
+
import { listTaskAction } from "./tasks/list";
|
|
6
|
+
import { updateTaskAction } from "./tasks/update";
|
|
7
|
+
|
|
8
|
+
export const services: Services = [
|
|
9
|
+
{
|
|
10
|
+
name: "tasks",
|
|
11
|
+
description: "Task management with CRUD operations",
|
|
12
|
+
meta: { version: "1.0.0" },
|
|
13
|
+
actions: createActions([
|
|
14
|
+
createTaskAction,
|
|
15
|
+
listTaskAction,
|
|
16
|
+
getTaskAction,
|
|
17
|
+
updateTaskAction,
|
|
18
|
+
deleteTaskAction,
|
|
19
|
+
]),
|
|
20
|
+
},
|
|
21
|
+
];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type Action, createAction } from "@nilejs/nile";
|
|
2
|
+
import { Err, Ok } from "slang-ts";
|
|
3
|
+
import z from "zod";
|
|
4
|
+
import { taskModel } from "@/db/models";
|
|
5
|
+
|
|
6
|
+
const createTaskSchema = z.object({
|
|
7
|
+
title: z.string().min(1, "Title is required"),
|
|
8
|
+
description: z.string().optional().default(""),
|
|
9
|
+
status: z
|
|
10
|
+
.enum(["pending", "in-progress", "done"])
|
|
11
|
+
.optional()
|
|
12
|
+
.default("pending"),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const createTaskHandler = async (data: Record<string, unknown>) => {
|
|
16
|
+
const result = await taskModel.create({
|
|
17
|
+
data: {
|
|
18
|
+
title: data.title as string,
|
|
19
|
+
description: (data.description as string) ?? "",
|
|
20
|
+
status: (data.status as "pending" | "in-progress" | "done") ?? "pending",
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
if (result.isErr) {
|
|
24
|
+
return Err(result.error);
|
|
25
|
+
}
|
|
26
|
+
return Ok({ task: result.value });
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const createTaskAction: Action = createAction({
|
|
30
|
+
name: "create",
|
|
31
|
+
description: "Create a new task",
|
|
32
|
+
handler: createTaskHandler,
|
|
33
|
+
validation: createTaskSchema,
|
|
34
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type Action, createAction } from "@nilejs/nile";
|
|
2
|
+
import { Err, Ok } from "slang-ts";
|
|
3
|
+
import z from "zod";
|
|
4
|
+
import { taskModel } from "@/db/models";
|
|
5
|
+
|
|
6
|
+
const deleteTaskSchema = z.object({
|
|
7
|
+
id: z.string().min(1, "Task ID is required"),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const deleteTaskHandler = async (data: Record<string, unknown>) => {
|
|
11
|
+
const result = await taskModel.delete(data.id as string);
|
|
12
|
+
if (result.isErr) {
|
|
13
|
+
return Err(result.error);
|
|
14
|
+
}
|
|
15
|
+
return Ok({ deleted: true, id: data.id });
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const deleteTaskAction: Action = createAction({
|
|
19
|
+
name: "delete",
|
|
20
|
+
description: "Delete a task by ID",
|
|
21
|
+
handler: deleteTaskHandler,
|
|
22
|
+
validation: deleteTaskSchema,
|
|
23
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type Action, createAction } from "@nilejs/nile";
|
|
2
|
+
import { Err, Ok } from "slang-ts";
|
|
3
|
+
import z from "zod";
|
|
4
|
+
import { taskModel } from "@/db/models";
|
|
5
|
+
|
|
6
|
+
const getTaskSchema = z.object({
|
|
7
|
+
id: z.string().min(1, "Task ID is required"),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const getTaskHandler = async (data: Record<string, unknown>) => {
|
|
11
|
+
const result = await taskModel.findById(data.id as string);
|
|
12
|
+
if (result.isErr) {
|
|
13
|
+
return Err(result.error);
|
|
14
|
+
}
|
|
15
|
+
return Ok({ task: result.value });
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const getTaskAction: Action = createAction({
|
|
19
|
+
name: "get",
|
|
20
|
+
description: "Get a task by ID",
|
|
21
|
+
handler: getTaskHandler,
|
|
22
|
+
validation: getTaskSchema,
|
|
23
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Action, createAction } from "@nilejs/nile";
|
|
2
|
+
import { Err, Ok } from "slang-ts";
|
|
3
|
+
import { taskModel } from "@/db/models";
|
|
4
|
+
|
|
5
|
+
const listTasksHandler = async () => {
|
|
6
|
+
const result = await taskModel.findAll();
|
|
7
|
+
if (result.isErr) {
|
|
8
|
+
return Err(result.error);
|
|
9
|
+
}
|
|
10
|
+
return Ok({ tasks: result.value });
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const listTaskAction: Action = createAction({
|
|
14
|
+
name: "list",
|
|
15
|
+
description: "List all tasks",
|
|
16
|
+
handler: listTasksHandler,
|
|
17
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type Action, createAction } from "@nilejs/nile";
|
|
2
|
+
import { Err, Ok } from "slang-ts";
|
|
3
|
+
import z from "zod";
|
|
4
|
+
import { taskModel } from "@/db/models";
|
|
5
|
+
|
|
6
|
+
const updateTaskSchema = z.object({
|
|
7
|
+
id: z.string().min(1, "Task ID is required"),
|
|
8
|
+
title: z.string().min(1).optional(),
|
|
9
|
+
description: z.string().optional(),
|
|
10
|
+
status: z.enum(["pending", "in-progress", "done"]).optional(),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const updateTaskHandler = async (data: Record<string, unknown>) => {
|
|
14
|
+
const { id, ...updates } = data;
|
|
15
|
+
|
|
16
|
+
const result = await taskModel.update({
|
|
17
|
+
id: id as string,
|
|
18
|
+
data: updates,
|
|
19
|
+
});
|
|
20
|
+
if (result.isErr) {
|
|
21
|
+
return Err(result.error);
|
|
22
|
+
}
|
|
23
|
+
return Ok({ task: result.value });
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const updateTaskAction: Action = createAction({
|
|
27
|
+
name: "update",
|
|
28
|
+
description: "Update an existing task",
|
|
29
|
+
handler: updateTaskHandler,
|
|
30
|
+
validation: updateTaskSchema,
|
|
31
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"outDir": "./dist",
|
|
10
|
+
"rootDir": ".",
|
|
11
|
+
"baseUrl": ".",
|
|
12
|
+
"paths": {
|
|
13
|
+
"@/*": ["./src/*"]
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"include": ["./src/**/*.ts", "./drizzle.config.ts"],
|
|
17
|
+
"exclude": ["node_modules", "dist", "data"]
|
|
18
|
+
}
|