@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 ADDED
@@ -0,0 +1,152 @@
1
+ # @nilejs/cli
2
+
3
+ [![NPM Version](https://img.shields.io/npm/v/@nilejs/cli.svg)](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,7 @@
1
+ import { defineConfig } from "drizzle-kit";
2
+
3
+ export default defineConfig({
4
+ dialect: "postgresql",
5
+ schema: "./src/db/schema.ts",
6
+ out: "./src/db/migrations",
7
+ });
@@ -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,3 @@
1
+ export { db } from "./client";
2
+ export * from "./schema";
3
+ export type { NewTask, Task } from "./types";
@@ -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,4 @@
1
+ import type { tasks } from "./schema";
2
+
3
+ export type Task = typeof tasks.$inferSelect;
4
+ export type NewTask = typeof tasks.$inferInsert;
@@ -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
+ }