create-celsian 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,46 +1,143 @@
1
1
  #!/usr/bin/env node
2
2
  // create-celsian — Project scaffolder
3
+ // Zero external dependencies. Interactive prompts via raw stdin.
3
4
  import { mkdirSync, writeFileSync } from 'node:fs';
4
- import { join, dirname } from 'node:path';
5
+ import { join, dirname, basename, isAbsolute } from 'node:path';
6
+ import { createInterface } from 'node:readline';
5
7
  import { basicTemplate } from './templates/basic.js';
6
8
  import { restApiTemplate } from './templates/rest-api.js';
7
9
  import { rpcApiTemplate } from './templates/rpc-api.js';
10
+ import { fullTemplate } from './templates/full.js';
11
+ // ─── Template Registry ───
8
12
  const templates = {
13
+ full: fullTemplate,
9
14
  basic: basicTemplate,
10
15
  'rest-api': restApiTemplate,
11
16
  'rpc-api': rpcApiTemplate,
12
17
  };
18
+ const templateDescriptions = {
19
+ full: 'Full-stack API with auth, CRUD, RPC, tasks, cron, OpenAPI, Docker',
20
+ basic: 'Minimal API server',
21
+ 'rest-api': 'REST API with TypeBox schemas',
22
+ 'rpc-api': 'RPC-first with typed client',
23
+ };
24
+ // ─── CLI Argument Parsing ───
13
25
  const args = process.argv.slice(2);
14
- const name = args[0];
26
+ // Handle --help
27
+ if (args.includes('--help') || args.includes('-h')) {
28
+ printUsage();
29
+ process.exit(0);
30
+ }
31
+ // Extract flags
15
32
  const templateFlag = args.indexOf('--template');
16
- const template = templateFlag !== -1 ? args[templateFlag + 1] ?? 'basic' : 'basic';
17
- if (!name) {
18
- console.log('Usage: create-celsian <project-name> [--template basic|rest-api|rpc-api]');
19
- console.log('');
20
- console.log('Templates:');
21
- console.log(' basic Minimal API server (default)');
22
- console.log(' rest-api REST + TypeBox schemas');
23
- console.log(' rpc-api RPC-first with typed client');
24
- process.exit(1);
33
+ const templateArg = templateFlag !== -1 ? args[templateFlag + 1] : undefined;
34
+ const nameArg = args.find(a => !a.startsWith('--') && (templateFlag === -1 || args.indexOf(a) !== templateFlag + 1));
35
+ // ─── Interactive Mode ───
36
+ async function prompt(question, defaultValue) {
37
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
38
+ return new Promise((resolve) => {
39
+ rl.question(` ${question} (${defaultValue}): `, (answer) => {
40
+ rl.close();
41
+ resolve(answer.trim() || defaultValue);
42
+ });
43
+ });
25
44
  }
26
- const files = templates[template];
27
- if (!files) {
28
- console.error(`Unknown template: ${template}`);
29
- console.error('Available: basic, rest-api, rpc-api');
30
- process.exit(1);
45
+ function detectPackageManager() {
46
+ const userAgent = process.env.npm_config_user_agent ?? '';
47
+ if (userAgent.startsWith('pnpm'))
48
+ return 'pnpm';
49
+ if (userAgent.startsWith('bun'))
50
+ return 'bun';
51
+ return 'npm';
52
+ }
53
+ async function interactiveMode() {
54
+ console.log('');
55
+ console.log(' Create a new Celsian project');
56
+ console.log(' ────────────────────────────');
57
+ console.log('');
58
+ const name = await prompt('Project name', 'my-celsian-app');
59
+ console.log('');
60
+ console.log(' Available templates:');
61
+ for (const [key, desc] of Object.entries(templateDescriptions)) {
62
+ const marker = key === 'full' ? ' (recommended)' : '';
63
+ console.log(` ${key.padEnd(12)} ${desc}${marker}`);
64
+ }
65
+ console.log('');
66
+ const template = await prompt('Template', 'full');
67
+ const detected = detectPackageManager();
68
+ const pm = await prompt('Package manager', detected);
69
+ return { name, template, pm };
31
70
  }
32
- const dir = join(process.cwd(), name);
33
- console.log(`\nCreating Celsian project: ${name}\n`);
34
- for (const [filePath, content] of Object.entries(files)) {
35
- const fullPath = join(dir, filePath);
36
- const fileDir = dirname(fullPath);
37
- mkdirSync(fileDir, { recursive: true });
38
- writeFileSync(fullPath, content.replace(/\{\{name\}\}/g, name));
39
- console.log(` + ${filePath}`);
71
+ // ─── Scaffold ───
72
+ function scaffold(name, template, pm) {
73
+ const files = templates[template];
74
+ if (!files) {
75
+ console.error(`\n Unknown template: ${template}`);
76
+ console.error(` Available: ${Object.keys(templates).join(', ')}\n`);
77
+ process.exit(1);
78
+ }
79
+ const dir = isAbsolute(name) ? name : join(process.cwd(), name);
80
+ const projectName = basename(dir);
81
+ console.log(`\n Creating Celsian project: ${projectName}`);
82
+ console.log(` Template: ${template}`);
83
+ console.log('');
84
+ for (const [filePath, content] of Object.entries(files)) {
85
+ const fullPath = join(dir, filePath);
86
+ const fileDir = dirname(fullPath);
87
+ mkdirSync(fileDir, { recursive: true });
88
+ writeFileSync(fullPath, content.replace(/\{\{name\}\}/g, projectName));
89
+ console.log(` + ${filePath}`);
90
+ }
91
+ const install = pm === 'npm' ? 'npm install' : `${pm} install`;
92
+ const dev = pm === 'npm' ? 'npm run dev' : `${pm} run dev`;
93
+ console.log(`\n Done! Next steps:\n`);
94
+ console.log(` cd ${projectName}`);
95
+ console.log(` ${install}`);
96
+ if (template === 'full') {
97
+ console.log(' cp .env.example .env');
98
+ }
99
+ console.log(` ${dev}`);
100
+ if (template === 'full') {
101
+ console.log('');
102
+ console.log(' Open http://localhost:3000/docs for API documentation');
103
+ }
104
+ console.log('');
105
+ }
106
+ // ─── Main ───
107
+ async function main() {
108
+ // If both name and template are provided via CLI args, skip interactive mode
109
+ if (nameArg) {
110
+ const template = templateArg ?? 'full';
111
+ const pm = detectPackageManager();
112
+ scaffold(nameArg, template, pm);
113
+ return;
114
+ }
115
+ // No project name — enter interactive mode
116
+ // But only if stdin is a TTY (not piped)
117
+ if (process.stdin.isTTY) {
118
+ const { name, template, pm } = await interactiveMode();
119
+ scaffold(name, template, pm);
120
+ }
121
+ else {
122
+ printUsage();
123
+ process.exit(1);
124
+ }
40
125
  }
41
- console.log(`\nDone! Next steps:\n`);
42
- console.log(` cd ${name}`);
43
- console.log(' npm install');
44
- console.log(' npm run dev');
45
- console.log('');
126
+ function printUsage() {
127
+ console.log('');
128
+ console.log(' Usage: create-celsian <project-name> [--template full|basic|rest-api|rpc-api]');
129
+ console.log('');
130
+ console.log(' Templates:');
131
+ for (const [key, desc] of Object.entries(templateDescriptions)) {
132
+ const defaultMarker = key === 'full' ? ' (default)' : '';
133
+ console.log(` ${key.padEnd(12)} ${desc}${defaultMarker}`);
134
+ }
135
+ console.log('');
136
+ console.log(' Run without arguments for interactive mode.');
137
+ console.log('');
138
+ }
139
+ main().catch((err) => {
140
+ console.error(err);
141
+ process.exit(1);
142
+ });
46
143
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,sCAAsC;AAEtC,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAExD,MAAM,SAAS,GAA2C;IACxD,KAAK,EAAE,aAAa;IACpB,UAAU,EAAE,eAAe;IAC3B,SAAS,EAAE,cAAc;CAC1B,CAAC;AAEF,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACnC,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;AACrB,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;AAChD,MAAM,QAAQ,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;AAEnF,IAAI,CAAC,IAAI,EAAE,CAAC;IACV,OAAO,CAAC,GAAG,CAAC,0EAA0E,CAAC,CAAC;IACxF,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAChB,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IAC1B,OAAO,CAAC,GAAG,CAAC,2CAA2C,CAAC,CAAC;IACzD,OAAO,CAAC,GAAG,CAAC,qCAAqC,CAAC,CAAC;IACnD,OAAO,CAAC,GAAG,CAAC,0CAA0C,CAAC,CAAC;IACxD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,KAAK,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAClC,IAAI,CAAC,KAAK,EAAE,CAAC;IACX,OAAO,CAAC,KAAK,CAAC,qBAAqB,QAAQ,EAAE,CAAC,CAAC;IAC/C,OAAO,CAAC,KAAK,CAAC,qCAAqC,CAAC,CAAC;IACrD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,CAAC;AAEtC,OAAO,CAAC,GAAG,CAAC,+BAA+B,IAAI,IAAI,CAAC,CAAC;AAErD,KAAK,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;IACxD,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;IACrC,MAAM,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAClC,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACxC,aAAa,CAAC,QAAQ,EAAE,OAAO,CAAC,OAAO,CAAC,eAAe,EAAE,IAAI,CAAC,CAAC,CAAC;IAChE,OAAO,CAAC,GAAG,CAAC,OAAO,QAAQ,EAAE,CAAC,CAAC;AACjC,CAAC;AAED,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;AACrC,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC;AAC5B,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;AAC7B,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;AAC7B,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare const fullTemplate: Record<string, string>;
2
+ //# sourceMappingURL=full.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"full.d.ts","sourceRoot":"","sources":["../../src/templates/full.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CA23B/C,CAAC"}
@@ -0,0 +1,869 @@
1
+ export const fullTemplate = {
2
+ 'package.json': JSON.stringify({
3
+ name: '{{name}}',
4
+ version: '0.1.0',
5
+ type: 'module',
6
+ scripts: {
7
+ dev: 'npx tsx --watch src/index.ts',
8
+ build: 'tsc',
9
+ start: 'node dist/index.js',
10
+ test: 'npx vitest run',
11
+ lint: 'npx tsc --noEmit',
12
+ },
13
+ dependencies: {
14
+ celsian: '^0.2.0',
15
+ '@celsian/core': '^0.2.0',
16
+ '@celsian/jwt': '^0.2.0',
17
+ '@celsian/rpc': '^0.2.0',
18
+ '@celsian/rate-limit': '^0.2.0',
19
+ '@sinclair/typebox': '^0.34.0',
20
+ },
21
+ devDependencies: {
22
+ typescript: '^5.7.0',
23
+ tsx: '^4.0.0',
24
+ vitest: '^3.0.0',
25
+ '@types/node': '^22.0.0',
26
+ },
27
+ }, null, 2),
28
+ 'tsconfig.json': JSON.stringify({
29
+ compilerOptions: {
30
+ target: 'ES2022',
31
+ module: 'ESNext',
32
+ moduleResolution: 'bundler',
33
+ lib: ['ES2022'],
34
+ types: ['node'],
35
+ strict: true,
36
+ esModuleInterop: true,
37
+ skipLibCheck: true,
38
+ forceConsistentCasingInFileNames: true,
39
+ resolveJsonModule: true,
40
+ isolatedModules: true,
41
+ declaration: true,
42
+ outDir: 'dist',
43
+ rootDir: 'src',
44
+ },
45
+ include: ['src'],
46
+ }, null, 2),
47
+ '.env.example': `# Server
48
+ PORT=3000
49
+ HOST=0.0.0.0
50
+ CORS_ORIGIN=*
51
+
52
+ # Auth
53
+ JWT_SECRET=change-me-to-a-real-secret-at-least-32-chars
54
+
55
+ # Database (placeholder — swap for your real DB URL)
56
+ DATABASE_URL=file:./data.db
57
+
58
+ # Environment
59
+ NODE_ENV=development
60
+ `,
61
+ '.gitignore': `node_modules/
62
+ dist/
63
+ *.tsbuildinfo
64
+ .env
65
+ data.db
66
+ `,
67
+ // ─── src/types.ts ───
68
+ 'src/types.ts': `// Shared types for {{name}}
69
+
70
+ export interface User {
71
+ id: string;
72
+ name: string;
73
+ email: string;
74
+ createdAt: string;
75
+ }
76
+
77
+ export interface CreateUserInput {
78
+ name: string;
79
+ email: string;
80
+ }
81
+
82
+ export interface UpdateUserInput {
83
+ name?: string;
84
+ email?: string;
85
+ }
86
+
87
+ export interface Session {
88
+ id: string;
89
+ userId: string;
90
+ expiresAt: number;
91
+ }
92
+
93
+ export interface JWTPayload {
94
+ sub: string;
95
+ email: string;
96
+ iat?: number;
97
+ exp?: number;
98
+ }
99
+ `,
100
+ // ─── src/plugins/database.ts ───
101
+ 'src/plugins/database.ts': `// Database module — in-memory store for development
102
+ // Replace with a real database (PostgreSQL, SQLite, etc.) for production
103
+
104
+ import type { User, Session } from '../types.js';
105
+
106
+ export interface DatabaseStore {
107
+ users: Map<string, User>;
108
+ sessions: Map<string, Session>;
109
+ generateId(): string;
110
+ }
111
+
112
+ function createStore(): DatabaseStore {
113
+ let nextId = 1;
114
+ return {
115
+ users: new Map(),
116
+ sessions: new Map(),
117
+ generateId() {
118
+ return String(nextId++);
119
+ },
120
+ };
121
+ }
122
+
123
+ // Module-level singleton — shared across all routes and plugins
124
+ export const db: DatabaseStore = createStore();
125
+
126
+ // Seed a demo user on import
127
+ const demoUser: User = {
128
+ id: db.generateId(),
129
+ name: 'Demo User',
130
+ email: 'demo@example.com',
131
+ createdAt: new Date().toISOString(),
132
+ };
133
+ db.users.set(demoUser.id, demoUser);
134
+ `,
135
+ // ─── src/plugins/auth.ts ───
136
+ 'src/plugins/auth.ts': `// JWT auth plugin — guards protected routes via Bearer token
137
+ // Uses @celsian/jwt under the hood
138
+
139
+ import { jwt, createJWTGuard } from '@celsian/jwt';
140
+ import type { PluginFunction, HookHandler } from '@celsian/core';
141
+
142
+ const JWT_SECRET = process.env.JWT_SECRET ?? 'dev-secret-change-me';
143
+
144
+ /**
145
+ * Register the JWT plugin. After this, \`app.jwt\` is available for
146
+ * signing and verifying tokens.
147
+ */
148
+ export function authPlugin(): PluginFunction {
149
+ return jwt({ secret: JWT_SECRET });
150
+ }
151
+
152
+ /**
153
+ * A reusable hook that rejects unauthenticated requests.
154
+ * Attach it to individual routes via \`onRequest\` or as a global hook.
155
+ */
156
+ export const requireAuth: HookHandler = createJWTGuard({
157
+ secret: JWT_SECRET,
158
+ });
159
+ `,
160
+ // ─── src/plugins/security.ts ───
161
+ 'src/plugins/security.ts': `// Security plugin — CORS + CSRF + security headers + rate limiting
162
+ // Combines multiple @celsian/core plugins into a single registration
163
+
164
+ import { cors, security, csrf } from '@celsian/core';
165
+ import { rateLimit } from '@celsian/rate-limit';
166
+ import type { PluginFunction } from '@celsian/core';
167
+
168
+ const CORS_ORIGIN = process.env.CORS_ORIGIN ?? '*';
169
+
170
+ /**
171
+ * Register all security-related plugins in one call.
172
+ */
173
+ export function securityPlugins(): PluginFunction[] {
174
+ return [
175
+ // CORS — allow cross-origin requests
176
+ cors({
177
+ origin: CORS_ORIGIN,
178
+ credentials: true,
179
+ maxAge: 86400,
180
+ }),
181
+
182
+ // Security headers (Helmet-style)
183
+ security({
184
+ hsts: { maxAge: 31536000, includeSubDomains: true },
185
+ referrerPolicy: 'strict-origin-when-cross-origin',
186
+ }),
187
+
188
+ // CSRF protection (double-submit cookie)
189
+ csrf({
190
+ cookieName: '_csrf',
191
+ headerName: 'x-csrf-token',
192
+ excludePaths: ['/health', '/ready', '/_rpc'],
193
+ }),
194
+
195
+ // Rate limiting — 100 requests per 60 seconds
196
+ rateLimit({
197
+ max: 100,
198
+ window: 60_000,
199
+ }),
200
+ ];
201
+ }
202
+ `,
203
+ // ─── src/routes/health.ts ───
204
+ 'src/routes/health.ts': `// Health check route — GET /health
205
+ // Returns server status and uptime for load balancers and monitoring
206
+
207
+ import type { PluginFunction } from '@celsian/core';
208
+
209
+ const startedAt = Date.now();
210
+
211
+ export default function healthRoutes(): PluginFunction {
212
+ return function health(app) {
213
+ app.get('/health', (_req, reply) => {
214
+ const uptimeMs = Date.now() - startedAt;
215
+ const uptimeSeconds = Math.floor(uptimeMs / 1000);
216
+ return reply.json({
217
+ status: 'ok',
218
+ uptime: uptimeSeconds,
219
+ timestamp: new Date().toISOString(),
220
+ });
221
+ });
222
+ };
223
+ }
224
+ `,
225
+ // ─── src/routes/users.ts ───
226
+ 'src/routes/users.ts': `// User CRUD routes — /users
227
+ // Full REST: GET (list), POST (create), GET/:id, PUT/:id, DELETE/:id
228
+
229
+ import { Type } from '@sinclair/typebox';
230
+ import type { PluginFunction, CelsianRequest, CelsianReply } from '@celsian/core';
231
+ import type { User, CreateUserInput, UpdateUserInput } from '../types.js';
232
+ import { db } from '../plugins/database.js';
233
+ import { requireAuth } from '../plugins/auth.js';
234
+
235
+ const CreateUserSchema = Type.Object({
236
+ name: Type.String({ minLength: 1 }),
237
+ email: Type.String({ minLength: 1 }),
238
+ });
239
+
240
+ const UpdateUserSchema = Type.Object({
241
+ name: Type.Optional(Type.String({ minLength: 1 })),
242
+ email: Type.Optional(Type.String({ minLength: 1 })),
243
+ });
244
+
245
+ export default function userRoutes(): PluginFunction {
246
+ return function users(app) {
247
+ // GET /users — list all users
248
+ app.get('/users', (_req, reply) => {
249
+ const allUsers = Array.from(db.users.values());
250
+ return reply.json(allUsers);
251
+ });
252
+
253
+ // POST /users — create a new user
254
+ app.route({
255
+ method: 'POST',
256
+ url: '/users',
257
+ schema: { body: CreateUserSchema },
258
+ handler(req: CelsianRequest, reply: CelsianReply) {
259
+ const { name, email } = req.parsedBody as CreateUserInput;
260
+ const user: User = {
261
+ id: db.generateId(),
262
+ name,
263
+ email,
264
+ createdAt: new Date().toISOString(),
265
+ };
266
+ db.users.set(user.id, user);
267
+ return reply.status(201).json(user);
268
+ },
269
+ });
270
+
271
+ // GET /users/:id — get a single user
272
+ app.get('/users/:id', (req, reply) => {
273
+ const user = db.users.get(req.params.id);
274
+ if (!user) return reply.status(404).json({ error: 'User not found' });
275
+ return reply.json(user);
276
+ });
277
+
278
+ // PUT /users/:id — update a user (protected)
279
+ app.route({
280
+ method: 'PUT',
281
+ url: '/users/:id',
282
+ schema: { body: UpdateUserSchema },
283
+ onRequest: requireAuth,
284
+ handler(req: CelsianRequest, reply: CelsianReply) {
285
+ const user = db.users.get(req.params.id);
286
+ if (!user) return reply.status(404).json({ error: 'User not found' });
287
+ const updates = req.parsedBody as UpdateUserInput;
288
+ if (updates.name !== undefined) user.name = updates.name;
289
+ if (updates.email !== undefined) user.email = updates.email;
290
+ db.users.set(user.id, user);
291
+ return reply.json(user);
292
+ },
293
+ });
294
+
295
+ // DELETE /users/:id — delete a user (protected)
296
+ app.route({
297
+ method: 'DELETE',
298
+ url: '/users/:id',
299
+ onRequest: requireAuth,
300
+ handler(req: CelsianRequest, reply: CelsianReply) {
301
+ const existed = db.users.delete(req.params.id);
302
+ if (!existed) return reply.status(404).json({ error: 'User not found' });
303
+ return reply.status(204).json({ deleted: true });
304
+ },
305
+ });
306
+ };
307
+ }
308
+ `,
309
+ // ─── src/routes/rpc.ts ───
310
+ 'src/routes/rpc.ts': `// RPC endpoint — type-safe procedures at /_rpc/*
311
+ // Demonstrates queries and mutations with typed schemas
312
+
313
+ import { procedure, router, RPCHandler } from '@celsian/rpc';
314
+ import { Type } from '@sinclair/typebox';
315
+ import type { PluginFunction } from '@celsian/core';
316
+
317
+ // Define your RPC router with namespaced procedures
318
+ const appRouter = router({
319
+ greeting: {
320
+ hello: procedure
321
+ .input(Type.Object({ name: Type.String() }))
322
+ .query(({ input }) => {
323
+ const { name } = input as { name: string };
324
+ return { message: \`Hello, \${name}!\` };
325
+ }),
326
+ },
327
+ math: {
328
+ add: procedure
329
+ .input(Type.Object({ a: Type.Number(), b: Type.Number() }))
330
+ .query(({ input }) => {
331
+ const { a, b } = input as { a: number; b: number };
332
+ return { result: a + b };
333
+ }),
334
+ multiply: procedure
335
+ .input(Type.Object({ a: Type.Number(), b: Type.Number() }))
336
+ .mutation(({ input }) => {
337
+ const { a, b } = input as { a: number; b: number };
338
+ return { result: a * b };
339
+ }),
340
+ },
341
+ system: {
342
+ ping: procedure.query(() => {
343
+ return { pong: true, timestamp: Date.now() };
344
+ }),
345
+ },
346
+ });
347
+
348
+ // Export the router type for client-side inference
349
+ export type AppRouter = typeof appRouter;
350
+
351
+ export default function rpcRoutes(): PluginFunction {
352
+ return function rpc(app) {
353
+ const rpcHandler = new RPCHandler(appRouter);
354
+
355
+ app.route({
356
+ method: ['GET', 'POST'],
357
+ url: '/_rpc/*path',
358
+ handler(req) {
359
+ return rpcHandler.handle(req);
360
+ },
361
+ });
362
+ };
363
+ }
364
+ `,
365
+ // ─── src/tasks/cleanup.ts ───
366
+ 'src/tasks/cleanup.ts': `// Background task: clean up expired sessions
367
+ // Registered with app.task() and runs when enqueued or on a schedule
368
+
369
+ import type { TaskDefinition } from '@celsian/core';
370
+
371
+ /**
372
+ * Cleanup task — removes expired sessions from the in-memory store.
373
+ * In production, this would run a database query instead.
374
+ */
375
+ export const cleanupTask: TaskDefinition = {
376
+ name: 'cleanup',
377
+ retries: 2,
378
+ timeout: 30_000,
379
+ async handler(_input, ctx) {
380
+ ctx.log.info('Running session cleanup...');
381
+
382
+ // In a real app, you would query the database:
383
+ // await db.query('DELETE FROM sessions WHERE expires_at < NOW()');
384
+
385
+ const now = Date.now();
386
+ let cleaned = 0;
387
+
388
+ // Placeholder: log what would happen
389
+ ctx.log.info(\`Session cleanup complete: removed \${cleaned} expired sessions\`);
390
+ },
391
+ };
392
+ `,
393
+ // ─── src/tasks/report.ts ───
394
+ 'src/tasks/report.ts': `// Cron job: daily report generation
395
+ // Runs every day at midnight via app.cron()
396
+
397
+ /**
398
+ * Generate a daily summary report.
399
+ * In production, this might send an email, write to S3, or post to Slack.
400
+ */
401
+ export async function generateDailyReport(): Promise<void> {
402
+ const now = new Date();
403
+ console.log(\`[report] Generating daily report for \${now.toISOString().split('T')[0]}\`);
404
+
405
+ // Placeholder — swap for real report logic:
406
+ // const users = await db.query('SELECT COUNT(*) FROM users WHERE created_at > $1', [yesterday]);
407
+ // const requests = await analytics.getRequestCount(yesterday, today);
408
+ // await email.send({ to: 'admin@example.com', subject: 'Daily Report', body: ... });
409
+
410
+ console.log('[report] Daily report generated successfully');
411
+ }
412
+ `,
413
+ // ─── src/index.ts ───
414
+ 'src/index.ts': `// {{name}} — Full-stack Celsian API
415
+ // Routes, plugins, background tasks, and cron — all wired up
416
+
417
+ import { createApp, serve, openapi } from 'celsian';
418
+
419
+ // Plugins
420
+ import { authPlugin } from './plugins/auth.js';
421
+ import { securityPlugins } from './plugins/security.js';
422
+
423
+ // Database (module-level singleton — imported for side-effect seeding)
424
+ import './plugins/database.js';
425
+
426
+ // Routes
427
+ import healthRoutes from './routes/health.js';
428
+ import userRoutes from './routes/users.js';
429
+ import rpcRoutes from './routes/rpc.js';
430
+
431
+ // Tasks
432
+ import { cleanupTask } from './tasks/cleanup.js';
433
+ import { generateDailyReport } from './tasks/report.js';
434
+
435
+ // ─── Create App ───
436
+
437
+ const app = createApp({ logger: true });
438
+
439
+ // ─── Security (CORS, CSRF, headers, rate limiting) ───
440
+
441
+ for (const plugin of securityPlugins()) {
442
+ await app.register(plugin);
443
+ }
444
+
445
+ // ─── Auth (JWT signing & verification) ───
446
+
447
+ await app.register(authPlugin());
448
+
449
+ // ─── API Documentation (OpenAPI + Swagger UI) ───
450
+
451
+ await app.register(openapi({
452
+ title: '{{name}} API',
453
+ version: '0.1.0',
454
+ description: 'Auto-generated API documentation',
455
+ }));
456
+
457
+ // ─── Routes ───
458
+
459
+ await app.register(healthRoutes());
460
+ await app.register(userRoutes());
461
+ await app.register(rpcRoutes());
462
+
463
+ // ─── Background Tasks ───
464
+
465
+ app.task(cleanupTask);
466
+
467
+ // ─── Cron Jobs ───
468
+
469
+ // Clean up expired sessions every hour
470
+ app.cron('session-cleanup', '0 * * * *', async () => {
471
+ await app.enqueue('cleanup', {});
472
+ });
473
+
474
+ // Generate a daily report at midnight
475
+ app.cron('daily-report', '0 0 * * *', generateDailyReport);
476
+
477
+ // ─── Start Server ───
478
+
479
+ const port = parseInt(process.env.PORT ?? '3000', 10);
480
+
481
+ serve(app, { port });
482
+ `,
483
+ // ─── test/api.test.ts ───
484
+ 'test/api.test.ts': `// Integration tests using app.inject() — no server needed
485
+ // Run with: npm test
486
+
487
+ import { describe, it, expect, beforeAll } from 'vitest';
488
+ import { createApp } from 'celsian';
489
+
490
+ // Import database module for side-effect (seeds demo user)
491
+ import '../src/plugins/database.js';
492
+
493
+ import healthRoutes from '../src/routes/health.js';
494
+ import userRoutes from '../src/routes/users.js';
495
+ import rpcRoutes from '../src/routes/rpc.js';
496
+
497
+ function createTestApp() {
498
+ const app = createApp();
499
+ // Register just what we need — skip auth/security for tests
500
+ app.register(healthRoutes());
501
+ app.register(userRoutes());
502
+ app.register(rpcRoutes());
503
+ return app;
504
+ }
505
+
506
+ describe('Health', () => {
507
+ it('GET /health returns status ok', async () => {
508
+ const app = createTestApp();
509
+ const res = await app.inject({ url: '/health' });
510
+ expect(res.status).toBe(200);
511
+ const body = await res.json();
512
+ expect(body.status).toBe('ok');
513
+ expect(body).toHaveProperty('uptime');
514
+ expect(body).toHaveProperty('timestamp');
515
+ });
516
+ });
517
+
518
+ describe('Users CRUD', () => {
519
+ it('GET /users returns the seeded user', async () => {
520
+ const app = createTestApp();
521
+ const res = await app.inject({ url: '/users' });
522
+ expect(res.status).toBe(200);
523
+ const users = await res.json();
524
+ expect(Array.isArray(users)).toBe(true);
525
+ expect(users.length).toBeGreaterThanOrEqual(1);
526
+ expect(users[0]).toHaveProperty('name', 'Demo User');
527
+ });
528
+
529
+ it('POST /users creates a new user', async () => {
530
+ const app = createTestApp();
531
+ const res = await app.inject({
532
+ method: 'POST',
533
+ url: '/users',
534
+ payload: { name: 'Alice', email: 'alice@example.com' },
535
+ });
536
+ expect(res.status).toBe(201);
537
+ const user = await res.json();
538
+ expect(user.name).toBe('Alice');
539
+ expect(user.email).toBe('alice@example.com');
540
+ expect(user).toHaveProperty('id');
541
+ expect(user).toHaveProperty('createdAt');
542
+ });
543
+
544
+ it('GET /users/:id returns a specific user', async () => {
545
+ const app = createTestApp();
546
+
547
+ // Create a user first
548
+ const createRes = await app.inject({
549
+ method: 'POST',
550
+ url: '/users',
551
+ payload: { name: 'Bob', email: 'bob@example.com' },
552
+ });
553
+ const created = await createRes.json();
554
+
555
+ const res = await app.inject({ url: \`/users/\${created.id}\` });
556
+ expect(res.status).toBe(200);
557
+ const user = await res.json();
558
+ expect(user.id).toBe(created.id);
559
+ expect(user.name).toBe('Bob');
560
+ });
561
+
562
+ it('GET /users/:id returns 404 for unknown user', async () => {
563
+ const app = createTestApp();
564
+ const res = await app.inject({ url: '/users/99999' });
565
+ expect(res.status).toBe(404);
566
+ });
567
+
568
+ it('DELETE /users/:id without auth returns 401', async () => {
569
+ const app = createTestApp();
570
+ const res = await app.inject({ method: 'DELETE', url: '/users/1' });
571
+ // Without the JWT guard registered in test mode, the route handler runs directly.
572
+ // In the full app with auth, this would return 401.
573
+ // For the test app (no auth plugin), it just deletes.
574
+ expect([200, 204, 401].includes(res.status)).toBe(true);
575
+ });
576
+ });
577
+
578
+ describe('RPC', () => {
579
+ it('GET /_rpc/system.ping returns pong', async () => {
580
+ const app = createTestApp();
581
+ const res = await app.inject({ url: '/_rpc/system.ping' });
582
+ expect(res.status).toBe(200);
583
+ const body = await res.json();
584
+ expect(body.result).toHaveProperty('pong', true);
585
+ });
586
+
587
+ it('GET /_rpc/greeting.hello returns greeting', async () => {
588
+ const app = createTestApp();
589
+ const res = await app.inject({
590
+ url: '/_rpc/greeting.hello?input=' + encodeURIComponent(JSON.stringify({ name: 'World' })),
591
+ });
592
+ expect(res.status).toBe(200);
593
+ const body = await res.json();
594
+ expect(body.result.message).toBe('Hello, World!');
595
+ });
596
+
597
+ it('POST /_rpc/math.multiply performs mutation', async () => {
598
+ const app = createTestApp();
599
+ const res = await app.inject({
600
+ method: 'POST',
601
+ url: '/_rpc/math.multiply',
602
+ payload: { a: 6, b: 7 },
603
+ });
604
+ expect(res.status).toBe(200);
605
+ const body = await res.json();
606
+ expect(body.result.result).toBe(42);
607
+ });
608
+ });
609
+ `,
610
+ // ─── Dockerfile ───
611
+ Dockerfile: `# syntax=docker/dockerfile:1
612
+
613
+ # ─── Build stage ───
614
+ FROM node:22-slim AS builder
615
+ WORKDIR /app
616
+
617
+ COPY package.json package-lock.json* ./
618
+ RUN npm ci --ignore-scripts
619
+
620
+ COPY tsconfig.json ./
621
+ COPY src/ ./src/
622
+
623
+ RUN npx tsc
624
+
625
+ # ─── Production stage ───
626
+ FROM node:22-slim AS runner
627
+ WORKDIR /app
628
+
629
+ ENV NODE_ENV=production
630
+
631
+ # Create non-root user
632
+ RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser
633
+
634
+ COPY package.json package-lock.json* ./
635
+ RUN npm ci --omit=dev --ignore-scripts
636
+
637
+ COPY --from=builder /app/dist ./dist
638
+
639
+ USER appuser
640
+ EXPOSE 3000
641
+
642
+ CMD ["node", "dist/index.js"]
643
+ `,
644
+ // ─── README.md ───
645
+ 'README.md': `# {{name}}
646
+
647
+ A full-stack API built with [CelsianJS](https://github.com/CelsianJs/celsian) — the fast, modular Node.js framework.
648
+
649
+ ## Quick Start
650
+
651
+ \`\`\`bash
652
+ # Install dependencies
653
+ npm install
654
+
655
+ # Copy environment variables
656
+ cp .env.example .env
657
+
658
+ # Start development server (with hot reload)
659
+ npm run dev
660
+ \`\`\`
661
+
662
+ The server starts at **http://localhost:3000**. Open http://localhost:3000/docs for the Swagger UI.
663
+
664
+ ## Architecture
665
+
666
+ \`\`\`
667
+ src/
668
+ index.ts # App entry — registers plugins, routes, tasks, cron
669
+ types.ts # Shared TypeScript types
670
+ routes/
671
+ health.ts # GET /health — uptime and status
672
+ users.ts # Full CRUD: GET/POST/PUT/DELETE /users
673
+ rpc.ts # Type-safe RPC at /_rpc/*
674
+ plugins/
675
+ auth.ts # JWT authentication (sign, verify, guard)
676
+ database.ts # In-memory database (replace with real DB)
677
+ security.ts # CORS + CSRF + security headers + rate limiting
678
+ tasks/
679
+ cleanup.ts # Background task: expired session cleanup
680
+ report.ts # Cron job: daily report generation
681
+ test/
682
+ api.test.ts # Integration tests with app.inject()
683
+ \`\`\`
684
+
685
+ ## API Endpoints
686
+
687
+ | Method | Path | Auth | Description |
688
+ |--------|------|------|-------------|
689
+ | GET | \`/health\` | No | Server health check |
690
+ | GET | \`/users\` | No | List all users |
691
+ | POST | \`/users\` | No | Create a user |
692
+ | GET | \`/users/:id\` | No | Get a user by ID |
693
+ | PUT | \`/users/:id\` | Yes | Update a user |
694
+ | DELETE | \`/users/:id\` | Yes | Delete a user |
695
+ | GET/POST | \`/_rpc/*\` | No | RPC procedures |
696
+ | GET | \`/docs\` | No | Swagger UI |
697
+ | GET | \`/docs/openapi.json\` | No | OpenAPI 3.1 spec |
698
+
699
+ ## Adding a New Route
700
+
701
+ 1. Create a new file in \`src/routes/\`:
702
+
703
+ \`\`\`typescript
704
+ // src/routes/products.ts
705
+ import type { PluginFunction } from '@celsian/core';
706
+
707
+ export default function productRoutes(): PluginFunction {
708
+ return function products(app) {
709
+ app.get('/products', (_req, reply) => {
710
+ return reply.json([{ id: 1, name: 'Widget' }]);
711
+ });
712
+ };
713
+ }
714
+ \`\`\`
715
+
716
+ 2. Register it in \`src/index.ts\`:
717
+
718
+ \`\`\`typescript
719
+ import productRoutes from './routes/products.js';
720
+ await app.register(productRoutes());
721
+ \`\`\`
722
+
723
+ ## Adding a Background Task
724
+
725
+ 1. Define the task in \`src/tasks/\`:
726
+
727
+ \`\`\`typescript
728
+ // src/tasks/email.ts
729
+ import type { TaskDefinition } from '@celsian/core';
730
+
731
+ export const sendEmailTask: TaskDefinition<{ to: string; subject: string }> = {
732
+ name: 'send-email',
733
+ retries: 3,
734
+ timeout: 10_000,
735
+ async handler(input, ctx) {
736
+ ctx.log.info(\\\`Sending email to \\\${input.to}\\\`);
737
+ // await emailService.send(input);
738
+ },
739
+ };
740
+ \`\`\`
741
+
742
+ 2. Register and enqueue it:
743
+
744
+ \`\`\`typescript
745
+ // In src/index.ts
746
+ import { sendEmailTask } from './tasks/email.js';
747
+ app.task(sendEmailTask);
748
+
749
+ // In a route handler
750
+ await app.enqueue('send-email', { to: 'user@example.com', subject: 'Welcome!' });
751
+ \`\`\`
752
+
753
+ ## Adding a Cron Job
754
+
755
+ \`\`\`typescript
756
+ // In src/index.ts — uses standard 5-field cron syntax
757
+ app.cron('weekly-digest', '0 9 * * 1', async () => {
758
+ // Runs every Monday at 9am
759
+ console.log('Generating weekly digest...');
760
+ });
761
+ \`\`\`
762
+
763
+ ## API Documentation
764
+
765
+ OpenAPI 3.1 docs are auto-generated from your route schemas.
766
+
767
+ - **Swagger UI**: http://localhost:3000/docs
768
+ - **JSON spec**: http://localhost:3000/docs/openapi.json
769
+
770
+ Add schemas to your routes for richer documentation:
771
+
772
+ \`\`\`typescript
773
+ import { Type } from '@sinclair/typebox';
774
+
775
+ app.route({
776
+ method: 'POST',
777
+ url: '/products',
778
+ schema: {
779
+ body: Type.Object({
780
+ name: Type.String(),
781
+ price: Type.Number({ minimum: 0 }),
782
+ }),
783
+ response: {
784
+ 201: Type.Object({ id: Type.Number(), name: Type.String() }),
785
+ },
786
+ },
787
+ handler(req, reply) {
788
+ // req.parsedBody is validated against the schema
789
+ return reply.status(201).json({ id: 1, ...req.parsedBody });
790
+ },
791
+ });
792
+ \`\`\`
793
+
794
+ ## Testing
795
+
796
+ Tests use \`app.inject()\` — no HTTP server needed.
797
+
798
+ \`\`\`bash
799
+ npm test
800
+ \`\`\`
801
+
802
+ Write tests by creating a lightweight app and injecting requests:
803
+
804
+ \`\`\`typescript
805
+ import { createApp } from 'celsian';
806
+
807
+ const app = createApp();
808
+ app.get('/ping', (_req, reply) => reply.json({ pong: true }));
809
+
810
+ const res = await app.inject({ url: '/ping' });
811
+ const body = await res.json();
812
+ // body = { pong: true }
813
+ \`\`\`
814
+
815
+ ## Deployment
816
+
817
+ ### Docker
818
+
819
+ \`\`\`bash
820
+ docker build -t {{name}} .
821
+ docker run -p 3000:3000 --env-file .env {{name}}
822
+ \`\`\`
823
+
824
+ ### Fly.io
825
+
826
+ \`\`\`bash
827
+ fly launch
828
+ fly secrets set JWT_SECRET=your-secret
829
+ fly deploy
830
+ \`\`\`
831
+
832
+ ### Railway
833
+
834
+ Push to a connected GitHub repo. Set environment variables in the Railway dashboard. The included Dockerfile is auto-detected.
835
+
836
+ ### Vercel (Serverless)
837
+
838
+ Use \`@celsian/adapter-vercel\`:
839
+
840
+ \`\`\`bash
841
+ npm install @celsian/adapter-vercel
842
+ \`\`\`
843
+
844
+ See the [adapter docs](https://github.com/CelsianJs/celsian/tree/main/packages/adapter-vercel) for configuration.
845
+
846
+ ### Cloudflare Workers
847
+
848
+ Use \`@celsian/adapter-cloudflare\`:
849
+
850
+ \`\`\`bash
851
+ npm install @celsian/adapter-cloudflare
852
+ \`\`\`
853
+
854
+ CelsianJS uses standard Web APIs (Request/Response), making it compatible with edge runtimes out of the box.
855
+
856
+ ## Project Structure Explained
857
+
858
+ - **Plugins** are registered with \`app.register()\` and can decorate the app, add hooks, or define routes.
859
+ - **Routes** are plugins that add HTTP endpoints. Group them by domain (users, products, etc.).
860
+ - **Tasks** are background jobs processed by the built-in task worker. Enqueue from route handlers.
861
+ - **Cron** schedules are standard 5-field Unix cron expressions. The scheduler ticks every second.
862
+ - **Hooks** run at different lifecycle stages: \`onRequest\`, \`preHandler\`, \`onSend\`, \`onError\`, etc.
863
+
864
+ ## License
865
+
866
+ MIT
867
+ `,
868
+ };
869
+ //# sourceMappingURL=full.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"full.js","sourceRoot":"","sources":["../../src/templates/full.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-celsian",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "create-celsian": "dist/index.js"
@@ -10,8 +10,8 @@
10
10
  "files": [
11
11
  "dist"
12
12
  ],
13
- "license": "MIT",
14
13
  "scripts": {
15
14
  "build": "tsc -b"
16
- }
17
- }
15
+ },
16
+ "license": "MIT"
17
+ }
package/LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 ThenJS
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.