@karmaniverous/smoz 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli/index.cjs +1218 -0
  2. package/package.json +1 -2
@@ -0,0 +1,1218 @@
1
+ #!/usr/bin/env node
2
+ #!/usr/bin/env node
3
+ 'use strict';
4
+
5
+ var fs = require('node:fs');
6
+ var path = require('node:path');
7
+ var commander = require('commander');
8
+ var packageDirectory = require('package-directory');
9
+ var node_url = require('node:url');
10
+ var chokidar = require('chokidar');
11
+ var node_child_process = require('node:child_process');
12
+ var os = require('node:os');
13
+
14
+ /**
15
+ * SMOZ CLI: add
16
+ *
17
+ * Scaffold a function under app/functions.
18
+ * Usage spec:
19
+ * - HTTP: <eventType>/<segments...>/<method>
20
+ * - non-HTTP:<eventType>/<segments...>
21
+ *
22
+ * Creates:
23
+ * - HTTP: lambda.ts, handler.ts, openapi.ts
24
+ * - non-HTTP: lambda.ts, handler.ts
25
+ *
26
+ * Idempotent and formatted (uses Prettier if available).
27
+ */
28
+ const HTTP_METHODS = new Set([
29
+ 'get',
30
+ 'post',
31
+ 'put',
32
+ 'delete',
33
+ 'patch',
34
+ 'head',
35
+ 'options',
36
+ 'trace',
37
+ ]);
38
+ const toPosix$2 = (p) => p.split(path.sep).join('/');
39
+ const formatMaybe$1 = async (root, filePath, source) => {
40
+ try {
41
+ const prettier = (await import('prettier'));
42
+ const cfg = (await prettier.resolveConfig(root)) ?? {};
43
+ return prettier.format(source, { ...cfg, filepath: filePath });
44
+ }
45
+ catch {
46
+ return source;
47
+ }
48
+ };
49
+ const writeIfAbsent$1 = async (outFile, content) => {
50
+ if (fs.existsSync(outFile))
51
+ return { created: false };
52
+ await fs.promises.mkdir(path.dirname(outFile), { recursive: true });
53
+ await fs.promises.writeFile(outFile, content, 'utf8');
54
+ return { created: true };
55
+ };
56
+ /** True if a segment encodes a path param (:id | {id} | [id]). */
57
+ const isParamSeg = (s) => s.startsWith(':') ||
58
+ (s.startsWith('{') && s.endsWith('}')) ||
59
+ (s.startsWith('[') && s.endsWith(']'));
60
+ /** Extract the bare param name from a param segment. */
61
+ const getParamName = (s) => {
62
+ if (s.startsWith(':'))
63
+ return s.slice(1);
64
+ if (s.startsWith('{') && s.endsWith('}'))
65
+ return s.slice(1, -1);
66
+ if (s.startsWith('[') && s.endsWith(']'))
67
+ return s.slice(1, -1);
68
+ return s;
69
+ };
70
+ /** Normalize a segment for the filesystem (Windows‑safe). */
71
+ const toDirSeg = (s) => isParamSeg(s) ? `[${getParamName(s)}]` : s;
72
+ /** Normalize a segment for API paths (Serverless/OpenAPI‑native). */
73
+ const toPathSeg = (s) => isParamSeg(s) ? `{${getParamName(s)}}` : s;
74
+ const lambdaHttpTemplate = ({ token, basePath, method, }) => `/**
75
+ * Registration: ${method.toUpperCase()} /${basePath} (public)
76
+ */
77
+ import { join } from 'node:path';
78
+
79
+ import { z } from 'zod';
80
+
81
+ import { app, APP_ROOT_ABS } from '@/app/config/app.config';
82
+
83
+ export const eventSchema = z.any();
84
+ export const responseSchema = z.any();
85
+
86
+ export const fn = app.defineFunction({
87
+ eventType: '${token}',
88
+ httpContexts: ['public'],
89
+ method: '${method}',
90
+ basePath: '${basePath}',
91
+ contentType: 'application/json',
92
+ eventSchema,
93
+ responseSchema,
94
+ callerModuleUrl: import.meta.url,
95
+ endpointsRootAbs: join(APP_ROOT_ABS, 'functions', '${token}').replace(/\\\\\\\\/g, '/'),
96
+ });
97
+ `;
98
+ const handlerHttpTemplate = () => `/**
99
+ * Handler: replace with your business logic.
100
+ */
101
+ import { z } from 'zod';
102
+
103
+ import type { responseSchema } from './lambda';
104
+ import { fn } from './lambda';
105
+
106
+ type Response = z.infer<typeof responseSchema>;
107
+
108
+ export const handler = fn.handler(async (): Promise<Response> => {
109
+ return {} as Response;
110
+ });
111
+ `;
112
+ const openapiTemplate = ({ params, pathTemplate, }) => {
113
+ const paramsBlock = params.length > 0
114
+ ? ` parameters: [
115
+ ${params
116
+ .map((p) => ` { name: '${p}', in: 'path', required: true, schema: { type: 'string' }, description: 'Path parameter: ${p}' },`)
117
+ .join('\n')}
118
+ ],
119
+ `
120
+ : '';
121
+ return `/* REQUIREMENTS
122
+ - Define OpenAPI Path Item for the new endpoint.
123
+ */
124
+ import { eventSchema, fn, responseSchema } from './lambda';
125
+
126
+ fn.openapi({
127
+ summary: 'Describe your endpoint',
128
+ description: 'Describe your endpoint. Path template: ${pathTemplate}.',
129
+ ${paramsBlock} requestBody: {
130
+ description: 'Request payload.',
131
+ content: { 'application/json': { schema: eventSchema } },
132
+ },
133
+ responses: {
134
+ 200: {
135
+ description: 'Ok',
136
+ content: { 'application/json': { schema: responseSchema } },
137
+ },
138
+ },
139
+ tags: [],
140
+ });
141
+
142
+ export {};
143
+ `;
144
+ };
145
+ const lambdaInternalTemplate = (token) => `/**
146
+ * Registration: internal ${token} function.
147
+ */
148
+ import { join } from 'node:path';
149
+
150
+ import { z } from 'zod';
151
+
152
+ import { app, APP_ROOT_ABS } from '@/app/config/app.config';
153
+
154
+ export const eventSchema = z.any();
155
+ export const responseSchema = z.any();
156
+
157
+ export const fn = app.defineFunction({
158
+ eventType: '${token}',
159
+ eventSchema,
160
+ responseSchema,
161
+ callerModuleUrl: import.meta.url,
162
+ endpointsRootAbs: join(APP_ROOT_ABS, 'functions', '${token}').replace(/\\\\\\\\/g, '/'),
163
+ });
164
+ `;
165
+ const handlerInternalTemplate = () => `/**
166
+ * Handler: replace with your business logic.
167
+ */
168
+ import { z } from 'zod';
169
+
170
+ import type { responseSchema } from './lambda';
171
+ import { fn } from './lambda';
172
+
173
+ type Response = z.infer<typeof responseSchema>;
174
+
175
+ export const handler = fn.handler(async (_event): Promise<Response> => {
176
+ void _event;
177
+ return {} as Response;
178
+ });
179
+ `;
180
+ const runAdd = async (root, spec) => {
181
+ const parts = spec.split('/').filter(Boolean);
182
+ if (parts.length < 2) {
183
+ throw new Error('Invalid spec. Use <eventType>/<segments...>/<method> (HTTP) or <eventType>/<segments...> (non-HTTP).');
184
+ }
185
+ const token = parts[0].toLowerCase();
186
+ const tail = parts[parts.length - 1].toLowerCase();
187
+ const isHttp = HTTP_METHODS.has(tail);
188
+ const baseParts = isHttp ? parts.slice(1, -1) : parts.slice(1);
189
+ if (baseParts.length === 0) {
190
+ throw new Error('Provide at least one path segment after the eventType.');
191
+ }
192
+ const method = isHttp ? tail : undefined;
193
+ // Normalize param segments for path and filesystem
194
+ const dirSegs = baseParts.map(toDirSeg);
195
+ const pathSegs = baseParts.map(toPathSeg);
196
+ // Base path (Serverless/OpenAPI native, uses {param})
197
+ const basePathPosix = toPosix$2(pathSegs.join('/'));
198
+ // Derive path template and param names for OpenAPI hints
199
+ const paramNames = baseParts.filter(isParamSeg).map(getParamName);
200
+ const pathTemplate = '/' + pathSegs.join('/');
201
+ const dir = path.join(root, 'app', 'functions', token, ...dirSegs, ...(method ? [method] : []));
202
+ const lambdaPath = path.join(dir, 'lambda.ts');
203
+ const handlerPath = path.join(dir, 'handler.ts');
204
+ const openapiPath = path.join(dir, 'openapi.ts');
205
+ // Compute contents
206
+ const files = [];
207
+ if (isHttp) {
208
+ files.push({
209
+ path: lambdaPath,
210
+ content: lambdaHttpTemplate({
211
+ token,
212
+ basePath: basePathPosix,
213
+ method: method,
214
+ }),
215
+ enabled: true,
216
+ });
217
+ files.push({
218
+ path: handlerPath,
219
+ content: handlerHttpTemplate(),
220
+ enabled: true,
221
+ });
222
+ files.push({
223
+ path: openapiPath,
224
+ content: openapiTemplate({ params: paramNames, pathTemplate }),
225
+ enabled: true,
226
+ });
227
+ }
228
+ else {
229
+ files.push({
230
+ path: lambdaPath,
231
+ content: lambdaInternalTemplate(token),
232
+ enabled: true,
233
+ });
234
+ files.push({
235
+ path: handlerPath,
236
+ content: handlerInternalTemplate(),
237
+ enabled: true,
238
+ });
239
+ files.push({ path: openapiPath, content: '', enabled: false });
240
+ }
241
+ const created = [];
242
+ const skipped = [];
243
+ for (const f of files) {
244
+ if (!f.enabled)
245
+ continue;
246
+ const formatted = await formatMaybe$1(root, f.path, f.content);
247
+ const { created: c } = await writeIfAbsent$1(f.path, formatted);
248
+ if (c)
249
+ created.push(path.posix.normalize(f.path));
250
+ else
251
+ skipped.push(path.posix.normalize(f.path));
252
+ }
253
+ return { created, skipped };
254
+ };
255
+
256
+ /**
257
+ * serverless-offline runner (dev-only).
258
+ *
259
+ * - Provides sane TMP/TEMP/TMPDIR fallbacks to avoid "undefined\\temp\\..." paths.
260
+ * - Spawns the local Serverless CLI to run "offline start".
261
+ * - Provides restart/close helpers for the dev orchestrator.
262
+ * - Avoids unnecessary nullish-coalescing so ESLint doesn't flag conditions.
263
+ */
264
+ const launchOffline = async (root, opts) => {
265
+ const slsJs = path.resolve(root, 'node_modules', 'serverless', 'bin', 'serverless.js');
266
+ const makeCmd = () => {
267
+ const args = [];
268
+ let cmd;
269
+ let shell = false;
270
+ if (fs.existsSync(slsJs)) {
271
+ cmd = process.execPath;
272
+ args.push(slsJs, 'offline', 'start', '--stage', opts.stage, '--httpPort', String(typeof opts.port === 'number' ? opts.port : 0));
273
+ }
274
+ else {
275
+ // Fallback to PATH
276
+ cmd = process.platform === 'win32' ? 'serverless.cmd' : 'serverless';
277
+ args.push('offline', 'start', '--stage', opts.stage, '--httpPort', String(typeof opts.port === 'number' ? opts.port : 0));
278
+ shell = true;
279
+ }
280
+ return { cmd, args, shell };
281
+ };
282
+ let child = spawnOffline(root, makeCmd(), opts.verbose);
283
+ const close = async () => new Promise((resolve) => {
284
+ if (child.killed) {
285
+ resolve();
286
+ return;
287
+ }
288
+ child.once('exit', () => {
289
+ resolve();
290
+ });
291
+ child.kill('SIGTERM');
292
+ // Safety timeout
293
+ setTimeout(() => {
294
+ resolve();
295
+ }, 1500);
296
+ });
297
+ const restart = async () => {
298
+ await close();
299
+ child = spawnOffline(root, makeCmd(), opts.verbose);
300
+ };
301
+ return { restart, close };
302
+ };
303
+ const spawnOffline = (root, cmd, verbose) => {
304
+ // Inherit the parent env, but ensure temp variables are present.
305
+ // Some toolchains (e.g., tsx/esbuild invoked under offline) interpolate TEMP/TMP,
306
+ // and if they are undefined, they may create literal "undefined\\temp\\..." paths
307
+ // relative to CWD. Use os.tmpdir() as a safe fallback.
308
+ const baseEnv = { ...process.env };
309
+ const tmp = os.tmpdir();
310
+ // Cross-platform default
311
+ if (!baseEnv.TMPDIR)
312
+ baseEnv.TMPDIR = tmp;
313
+ // Windows defaults
314
+ if (process.platform === 'win32') {
315
+ if (!baseEnv.TEMP)
316
+ baseEnv.TEMP = tmp;
317
+ if (!baseEnv.TMP)
318
+ baseEnv.TMP = tmp;
319
+ // Some nested toolchains derive cache roots from these when TMP/TEMP are absent
320
+ if (!baseEnv.LOCALAPPDATA)
321
+ baseEnv.LOCALAPPDATA = tmp;
322
+ if (!baseEnv.USERPROFILE)
323
+ baseEnv.USERPROFILE = tmp;
324
+ }
325
+ else {
326
+ // POSIX: some tools fall back to HOME for caches when TMPDIR isn't used
327
+ if (!baseEnv.HOME)
328
+ baseEnv.HOME = tmp;
329
+ }
330
+ // Optional diagnostics to verify the child sees sane temp-related envs
331
+ if (verbose) {
332
+ const snap = {
333
+ TMPDIR: baseEnv.TMPDIR,
334
+ TEMP: baseEnv.TEMP,
335
+ TMP: baseEnv.TMP,
336
+ HOME: baseEnv.HOME,
337
+ USERPROFILE: baseEnv.USERPROFILE,
338
+ LOCALAPPDATA: baseEnv.LOCALAPPDATA,
339
+ };
340
+ process.stdout.write(`[offline] env snapshot: ${JSON.stringify(snap)}\n`);
341
+ }
342
+ const child = node_child_process.spawn(cmd.cmd, cmd.args, {
343
+ cwd: root,
344
+ shell: cmd.shell,
345
+ stdio: ['ignore', 'pipe', 'pipe'],
346
+ env: baseEnv,
347
+ });
348
+ const prefix = '[offline] ';
349
+ const emit = (buf) => {
350
+ const text = buf.toString('utf8');
351
+ if (verbose)
352
+ process.stdout.write(prefix + text);
353
+ };
354
+ const emitErr = (buf) => {
355
+ const text = buf.toString('utf8');
356
+ process.stderr.write(prefix + text);
357
+ };
358
+ // With stdio: 'pipe', these streams are present
359
+ child.stdout.on('data', emit);
360
+ child.stderr.on('data', emitErr);
361
+ child.on('error', (err) => {
362
+ process.stderr.write(`${prefix}${err.message}\n`);
363
+ });
364
+ return child;
365
+ };
366
+
367
+ /* OpenAPI one-shot runner: spawn the project-local OpenAPI script via tsx.
368
+ * - Mirrors the npm script: tsx app/config/openapi && prettier (project-local script already formats).
369
+ * - Keeps CLI responsibilities minimal; errors bubble via non-zero exit.
370
+ */
371
+ const findTsxCli = (root) => {
372
+ // Prefer invoking the JS entry to avoid shell .cmd quirks on Windows.
373
+ const js = path.resolve(root, 'node_modules', 'tsx', 'dist', 'cli.js');
374
+ if (fs.existsSync(js)) {
375
+ return {
376
+ cmd: process.execPath,
377
+ args: [js, 'app/config/openapi.ts'],
378
+ shell: false,
379
+ };
380
+ }
381
+ // Fallback to "tsx" on PATH (may rely on shell resolution).
382
+ const cmd = process.platform === 'win32' ? 'tsx.cmd' : 'tsx';
383
+ return { cmd, args: ['app/config/openapi.ts'], shell: true };
384
+ };
385
+ const runOpenapi = async (root, opts) => {
386
+ // Detect changes by comparing the pre/post content of app/generated/openapi.json.
387
+ const outFile = path.resolve(root, 'app', 'generated', 'openapi.json');
388
+ let before;
389
+ try {
390
+ if (fs.existsSync(outFile))
391
+ before = fs.readFileSync(outFile, 'utf8');
392
+ }
393
+ catch {
394
+ // ignore read errors; treat as absent
395
+ before = undefined;
396
+ }
397
+ const { cmd, args, shell } = findTsxCli(root);
398
+ if (opts?.verbose) {
399
+ console.log(`[openapi] ${[cmd, ...args].join(' ')}`);
400
+ }
401
+ const res = node_child_process.spawnSync(cmd, args, {
402
+ cwd: root,
403
+ stdio: 'inherit',
404
+ shell,
405
+ });
406
+ if (typeof res.status !== 'number' || res.status !== 0) {
407
+ const code = typeof res.status === 'number' ? String(res.status) : 'unknown';
408
+ throw new Error(`openapi failed (exit ${code})`);
409
+ }
410
+ // Determine whether the file content changed
411
+ try {
412
+ if (!fs.existsSync(outFile))
413
+ return before !== undefined; // deleted vs existed
414
+ const after = fs.readFileSync(outFile, 'utf8');
415
+ return before === undefined ? true : after !== before;
416
+ }
417
+ catch {
418
+ // If we cannot read, conservatively report "changed" so callers can refresh.
419
+ return true;
420
+ }
421
+ };
422
+
423
+ /**
424
+ * SMOZ CLI: register
425
+ *
426
+ * Scan app/functions/** for {lambda,openapi,serverless}.ts and generate
427
+ * side-effect import modules under app/generated/:
428
+ * - register.functions.ts
429
+ * - register.openapi.ts
430
+ * - register.serverless.ts
431
+ *
432
+ * Idempotent and formatted (uses Prettier if available).
433
+ */
434
+ const toPosix$1 = (p) => p.split(path.sep).join('/');
435
+ const withoutTsExt = (p) => p.endsWith('.ts') ? p.slice(0, -3) : p;
436
+ const HEADER = `/* AUTO-GENERATED by smoz register — DO NOT EDIT */
437
+ `;
438
+ const formatMaybe = async (root, filePath, source) => {
439
+ try {
440
+ // Resolve Prettier if available in the project
441
+ const prettier = (await import('prettier'));
442
+ const cfg = (await prettier.resolveConfig(root)) ?? {};
443
+ return prettier.format(source, { ...cfg, filepath: filePath });
444
+ }
445
+ catch {
446
+ return source;
447
+ }
448
+ };
449
+ const writeIfChanged = async (outFile, content) => {
450
+ try {
451
+ const prev = await fs.promises.readFile(outFile, 'utf8');
452
+ if (prev === content)
453
+ return false;
454
+ }
455
+ catch {
456
+ // file missing — proceed
457
+ }
458
+ await fs.promises.mkdir(path.dirname(outFile), { recursive: true });
459
+ await fs.promises.writeFile(outFile, content, 'utf8');
460
+ return true;
461
+ };
462
+ const walk$1 = async (dir, out = []) => {
463
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
464
+ for (const ent of entries) {
465
+ const p = path.join(dir, ent.name);
466
+ if (ent.isDirectory()) {
467
+ await walk$1(p, out);
468
+ }
469
+ else if (ent.isFile()) {
470
+ out.push(p);
471
+ }
472
+ }
473
+ return out;
474
+ };
475
+ const collect = async (root) => {
476
+ const base = path.join(root, 'app', 'functions');
477
+ const buckets = { lambda: [], openapi: [], serverless: [] };
478
+ if (!fs.existsSync(base))
479
+ return buckets;
480
+ const files = await walk$1(base);
481
+ for (const abs of files) {
482
+ const name = abs.split(path.sep).pop() ?? '';
483
+ if (name === 'lambda.ts')
484
+ buckets.lambda.push(abs);
485
+ if (name === 'openapi.ts')
486
+ buckets.openapi.push(abs);
487
+ if (name === 'serverless.ts')
488
+ buckets.serverless.push(abs);
489
+ }
490
+ // stable order
491
+ buckets.lambda.sort();
492
+ buckets.openapi.sort();
493
+ buckets.serverless.sort();
494
+ return buckets;
495
+ };
496
+ const makeImports = (root, files) => {
497
+ return files.map((abs) => {
498
+ const rel = toPosix$1(path.relative(root, abs));
499
+ const noExt = withoutTsExt(rel);
500
+ return `import '@/${noExt}';`;
501
+ });
502
+ };
503
+ const buildFile = (imports) => {
504
+ if (imports.length === 0)
505
+ return `${HEADER}\n`;
506
+ return `${HEADER}\n${imports.join('\n')}\n`;
507
+ };
508
+ const runRegister = async (root) => {
509
+ const buckets = await collect(root);
510
+ const lambdaImports = makeImports(root, buckets.lambda);
511
+ const openapiImports = makeImports(root, buckets.openapi);
512
+ const serverlessImports = makeImports(root, buckets.serverless);
513
+ const outDir = path.join(root, 'app', 'generated');
514
+ const fnsPath = path.join(outDir, 'register.functions.ts');
515
+ const oaiPath = path.join(outDir, 'register.openapi.ts');
516
+ const srvPath = path.join(outDir, 'register.serverless.ts');
517
+ const fns = await formatMaybe(root, fnsPath, buildFile(lambdaImports));
518
+ const oai = await formatMaybe(root, oaiPath, buildFile(openapiImports));
519
+ const srv = await formatMaybe(root, srvPath, buildFile(serverlessImports));
520
+ const wrote = [];
521
+ if (await writeIfChanged(fnsPath, fns))
522
+ wrote.push(path.posix.normalize(fnsPath));
523
+ if (await writeIfChanged(oaiPath, oai))
524
+ wrote.push(path.posix.normalize(oaiPath));
525
+ if (await writeIfChanged(srvPath, srv))
526
+ wrote.push(path.posix.normalize(srvPath));
527
+ return { wrote };
528
+ };
529
+
530
+ /* Dev loop orchestrator
531
+ * - Watches author sources; debounces bursts; runs tasks in order: register → openapi.
532
+ * - Optional local serving (--local inline|offline).
533
+ * - Stage/env: seeds process.env with concrete values for the selected stage.
534
+ */
535
+ const runDev = async (root, opts) => {
536
+ const verbose = !!opts.verbose;
537
+ const stage = typeof opts.stage === 'string'
538
+ ? opts.stage
539
+ : inferDefaultStage(root, verbose);
540
+ // Seed env with concrete values for the selected stage.
541
+ try {
542
+ await seedEnvForStage(root, stage, verbose);
543
+ }
544
+ catch (e) {
545
+ if (verbose)
546
+ console.warn('[dev] env seeding warning:', e.message);
547
+ }
548
+ const mode = opts.local;
549
+ const port = typeof opts.port === 'number' ? opts.port : 0;
550
+ if (verbose) {
551
+ console.log(`[dev] options: register=${String(opts.register)} ` +
552
+ `openapi=${String(opts.openapi)} ` +
553
+ `local=${String(mode)} ` +
554
+ `stage=${stage} ` +
555
+ `port=${String(port)}`);
556
+ }
557
+ // Single debounced queue
558
+ let timer;
559
+ let running = false;
560
+ let pending = false;
561
+ // Small executor we can use for both pre-flight and queued runs
562
+ const executeOnce = async () => {
563
+ if (running)
564
+ return { wrote: false, openapiChanged: false };
565
+ running = true;
566
+ pending = false;
567
+ try {
568
+ let wrote = false;
569
+ if (opts.register) {
570
+ const res = await runRegister(root);
571
+ wrote = res.wrote.length > 0;
572
+ console.log(res.wrote.length
573
+ ? `Updated:\n - ${res.wrote.join('\n - ')}`
574
+ : 'No changes.');
575
+ }
576
+ let openapiChanged = false;
577
+ if (opts.openapi) {
578
+ openapiChanged = await runOpenapi(root, { verbose });
579
+ }
580
+ return { wrote, openapiChanged };
581
+ }
582
+ catch (e) {
583
+ console.error('[dev] task error:', e.message);
584
+ return { wrote: false, openapiChanged: false };
585
+ }
586
+ finally {
587
+ running = false;
588
+ // If a burst arrived while we were running, schedule again
589
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
590
+ if (pending)
591
+ schedule();
592
+ }
593
+ };
594
+ // Local child (if any)
595
+ let offline;
596
+ let inlineChild;
597
+ const schedule = () => {
598
+ if (timer)
599
+ clearTimeout(timer);
600
+ timer = setTimeout(() => {
601
+ void (async () => {
602
+ pending = false;
603
+ const { wrote, openapiChanged } = await executeOnce();
604
+ // Local backend refresh
605
+ if (mode === 'offline') {
606
+ // Restart only when route-surface can change (register wrote)
607
+ if (wrote && offline) {
608
+ if (verbose)
609
+ console.log('[dev] restarting serverless-offline (register changed)...');
610
+ await offline.restart();
611
+ }
612
+ }
613
+ else if (mode === 'inline') {
614
+ // Restart inline only if something material changed
615
+ if (wrote || openapiChanged) {
616
+ if (verbose)
617
+ console.log('[dev] restarting inline server...');
618
+ // inlineChild is created when mode === 'inline'; guard for safety
619
+ if (inlineChild) {
620
+ await inlineChild.restart();
621
+ }
622
+ }
623
+ }
624
+ })();
625
+ }, 250);
626
+ };
627
+ // Pre-flight: run tasks before launching the local backend to avoid an immediate restart
628
+ await executeOnce();
629
+ // Local serving
630
+ if (mode === 'offline') {
631
+ offline = await launchOffline(root, { stage, port, verbose });
632
+ }
633
+ else if (mode === 'inline') {
634
+ inlineChild = await launchInline(root, { stage, port, verbose });
635
+ } // Watch sources
636
+ const globs = [
637
+ path.join(root, 'app', 'functions', '**', 'lambda.ts'),
638
+ path.join(root, 'app', 'functions', '**', 'openapi.ts'),
639
+ path.join(root, 'app', 'functions', '**', 'serverless.ts'),
640
+ ];
641
+ if (verbose)
642
+ console.log('[dev] watching:', globs.map((g) => path.posix.normalize(g)).join(', '));
643
+ const watcher = chokidar.watch(globs, {
644
+ ignoreInitial: true,
645
+ awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 },
646
+ });
647
+ watcher.on('add', schedule).on('change', schedule).on('unlink', schedule);
648
+ // Keep process alive until SIGINT
649
+ await new Promise((resolve) => {
650
+ const stop = async () => {
651
+ try {
652
+ await watcher.close();
653
+ await offline?.close();
654
+ await inlineChild?.close();
655
+ }
656
+ finally {
657
+ resolve();
658
+ }
659
+ };
660
+ process.on('SIGINT', () => {
661
+ void stop();
662
+ });
663
+ process.on('SIGTERM', () => {
664
+ void stop();
665
+ });
666
+ });
667
+ };
668
+ const inferDefaultStage = (root, verbose) => {
669
+ // Prefer “dev”; if app.config.ts is available and we can inspect it cheaply later, expand behavior.
670
+ // For now, return 'dev' (explicit selection via --stage remains available).
671
+ if (verbose)
672
+ console.log('[dev] inferring stage: dev (explicit --stage overrides)');
673
+ return 'dev';
674
+ };
675
+ const seedEnvForStage = async (root, stage, verbose) => {
676
+ // Best effort: import the app config to read declared env keys and concrete values.
677
+ // Preserve existing process.env values; only seed when unset.
678
+ try {
679
+ const appConfigUrl = node_url.pathToFileURL(path.resolve(root, 'app', 'config', 'app.config.ts')).href;
680
+ // Dynamically import the TS module under tsx
681
+ const mod = (await import(appConfigUrl));
682
+ const app = mod.app;
683
+ const stages = mod.stages;
684
+ const globalKeys = Array.isArray(app?.global?.envKeys)
685
+ ? app.global.envKeys
686
+ : [];
687
+ const stageKeys = Array.isArray(app?.stage?.envKeys)
688
+ ? app.stage.envKeys
689
+ : [];
690
+ const globalParams = (stages?.default).params ?? {};
691
+ const stageParams = (stages?.[stage]).params ?? {};
692
+ const seedPair = (key, from) => {
693
+ if (key in process.env)
694
+ return;
695
+ const val = from[key];
696
+ if (val === undefined)
697
+ return;
698
+ if (typeof val === 'string') {
699
+ process.env[key] = val;
700
+ if (verbose)
701
+ console.log(`[dev] env: ${key}=${val}`);
702
+ return;
703
+ }
704
+ if (typeof val === 'number' || typeof val === 'boolean') {
705
+ const v = String(val);
706
+ process.env[key] = v;
707
+ if (verbose)
708
+ console.log(`[dev] env: ${key}=${v}`);
709
+ return;
710
+ }
711
+ // Non-primitive; skip to avoid [object Object] surprise.
712
+ if (verbose)
713
+ console.log(`[dev] env: skip ${key} (non-primitive)`);
714
+ };
715
+ for (const k of globalKeys) {
716
+ if (typeof k === 'string') {
717
+ seedPair(k, globalParams);
718
+ }
719
+ }
720
+ for (const k of stageKeys) {
721
+ if (typeof k === 'string') {
722
+ seedPair(k, stageParams);
723
+ }
724
+ }
725
+ // Ensure STAGE itself is present as a last resort
726
+ if (!process.env.STAGE) {
727
+ process.env.STAGE = stage;
728
+ if (verbose)
729
+ console.log(`[dev] env: STAGE=${stage}`);
730
+ }
731
+ }
732
+ catch {
733
+ // Fallback: seed STAGE only
734
+ if (!process.env.STAGE) {
735
+ process.env.STAGE = stage;
736
+ if (verbose)
737
+ console.log(`[dev] env: STAGE=${stage}`);
738
+ }
739
+ }
740
+ };
741
+ // Inline local runner: spawn tsx to run a TS server script.
742
+ const launchInline = async (root, opts) => {
743
+ const { spawn } = await import('node:child_process');
744
+ const path = await import('node:path');
745
+ const fs = await import('node:fs');
746
+ const tsxCli = path.resolve(root, 'node_modules', 'tsx', 'dist', 'cli.js');
747
+ const entry = path.resolve(root, 'src', 'cli', 'local', 'inline.server.ts');
748
+ if (!fs.existsSync(entry)) {
749
+ throw new Error('inline server entry missing: src/cli/local/inline.server.ts');
750
+ }
751
+ const args = [tsxCli, entry];
752
+ if (!fs.existsSync(tsxCli)) {
753
+ // Fallback to PATH resolution
754
+ args.shift();
755
+ args.unshift(process.platform === 'win32' ? 'tsx.cmd' : 'tsx');
756
+ }
757
+ else {
758
+ args.unshift(process.execPath);
759
+ }
760
+ const spawnChild = () => spawn(args[0], args.slice(1), {
761
+ cwd: root,
762
+ stdio: 'inherit',
763
+ shell: !args[0].endsWith('.js'),
764
+ env: {
765
+ ...process.env,
766
+ SMOZ_STAGE: opts.stage,
767
+ SMOZ_PORT: String(opts.port),
768
+ SMOZ_VERBOSE: opts.verbose ? '1' : '',
769
+ },
770
+ });
771
+ let child = spawnChild();
772
+ const close = async () => new Promise((resolve) => {
773
+ // If the process has already exited (exitCode set), resolve immediately.
774
+ if (child.exitCode !== null) {
775
+ resolve();
776
+ return;
777
+ }
778
+ child.once('exit', () => {
779
+ resolve();
780
+ });
781
+ child.kill('SIGTERM');
782
+ setTimeout(() => {
783
+ resolve();
784
+ }, 1500);
785
+ });
786
+ const restart = async () => {
787
+ await close();
788
+ child = spawnChild();
789
+ };
790
+ return { close, restart };
791
+ };
792
+
793
+ /**
794
+ * smoz init
795
+ *
796
+ * Scaffolds a new project from packaged templates.
797
+ * - Copies ./templates/project/ into the target root (shared boilerplate)
798
+ * - Copies ./templates/<template>/ into the target root (default: minimal)
799
+ * - Seeds app/generated/register.*.ts (empty modules) if missing
800
+ * - Idempotent: copy-if-absent; if a file exists, writes <name>.example alongside
801
+ * - Additive merge of template manifest (deps/devDeps/scripts) into package.json
802
+ * - Optional dependency installation via --install[=<pm>]
803
+ */
804
+ const toPosix = (p) => p.split(path.sep).join('/');
805
+ const writeIfAbsent = async (outFile, content) => {
806
+ if (fs.existsSync(outFile))
807
+ return { created: false };
808
+ await fs.promises.mkdir(path.dirname(outFile), { recursive: true });
809
+ await fs.promises.writeFile(outFile, content, 'utf8');
810
+ return { created: true };
811
+ };
812
+ const walk = async (dir, out = []) => {
813
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
814
+ for (const ent of entries) {
815
+ const p = path.join(dir, ent.name);
816
+ if (ent.isDirectory()) {
817
+ await walk(p, out);
818
+ }
819
+ else if (ent.isFile()) {
820
+ out.push(p);
821
+ }
822
+ }
823
+ return out;
824
+ };
825
+ const resolveTemplatesBase = () => {
826
+ // Resolve the package root (works both in dev and when installed)
827
+ const pkgRoot = packageDirectory.packageDirectorySync() ?? process.cwd();
828
+ return path.resolve(pkgRoot, 'templates');
829
+ };
830
+ const readJson = async (file) => {
831
+ try {
832
+ const data = await fs.promises.readFile(file, 'utf8');
833
+ return JSON.parse(data);
834
+ }
835
+ catch {
836
+ return undefined;
837
+ }
838
+ };
839
+ const writeJson = async (file, obj) => {
840
+ await fs.promises.mkdir(path.dirname(file), { recursive: true });
841
+ await fs.promises.writeFile(file, JSON.stringify(obj, null, 2), 'utf8');
842
+ };
843
+ const detectPm = (root) => {
844
+ if (fs.existsSync(path.join(root, 'pnpm-lock.yaml')))
845
+ return 'pnpm';
846
+ if (fs.existsSync(path.join(root, 'yarn.lock')))
847
+ return 'yarn';
848
+ if (fs.existsSync(path.join(root, 'package-lock.json')))
849
+ return 'npm';
850
+ if (fs.existsSync(path.join(root, 'bun.lockb')))
851
+ return 'bun';
852
+ const ua = process.env.npm_config_user_agent ?? '';
853
+ if (ua.includes('pnpm'))
854
+ return 'pnpm';
855
+ if (ua.includes('yarn'))
856
+ return 'yarn';
857
+ if (ua.includes('bun'))
858
+ return 'bun';
859
+ if (ua.includes('npm'))
860
+ return 'npm';
861
+ return undefined;
862
+ };
863
+ const runInstall = (root, pm) => {
864
+ if (!pm)
865
+ return 'skipped';
866
+ const known = pm === 'pnpm' || pm === 'yarn' || pm === 'bun' || pm === 'npm';
867
+ if (!known)
868
+ return 'unknown-pm';
869
+ // Spawn with explicit args; avoid tuple inference that widens types.
870
+ const res = node_child_process.spawnSync(pm, ['install'], {
871
+ stdio: 'inherit',
872
+ cwd: root,
873
+ shell: true,
874
+ });
875
+ if (res.status === 0) {
876
+ const tag = pm === 'pnpm'
877
+ ? 'ran (pnpm)'
878
+ : pm === 'yarn'
879
+ ? 'ran (yarn)'
880
+ : pm === 'bun'
881
+ ? 'ran (bun)'
882
+ : 'ran (npm)';
883
+ return tag;
884
+ }
885
+ return 'failed';
886
+ };
887
+ const mergeAdditive = (target, source) => {
888
+ const merged = [];
889
+ const mergeKey = (key) => {
890
+ // Allow possibly-undefined shapes to satisfy lint (no-unnecessary-condition).
891
+ const src = source[key] ?? {};
892
+ const dst = target[key] ?? {};
893
+ const out = { ...dst };
894
+ let changed = false;
895
+ for (const [k, v] of Object.entries(src)) {
896
+ if (!(k in dst)) {
897
+ out[k] = v;
898
+ merged.push(`${key}:${k}@${v}`);
899
+ changed = true;
900
+ }
901
+ }
902
+ if (changed) {
903
+ target[key] = out;
904
+ }
905
+ };
906
+ mergeKey('dependencies');
907
+ mergeKey('devDependencies');
908
+ mergeKey('peerDependencies');
909
+ const srcScripts = source.scripts ?? {};
910
+ const dstScripts = target.scripts ?? {};
911
+ const scriptsOut = { ...dstScripts };
912
+ for (const [name, script] of Object.entries(srcScripts)) {
913
+ if (!(name in dstScripts)) {
914
+ scriptsOut[name] = script;
915
+ merged.push(`scripts:${name}`);
916
+ }
917
+ else if (dstScripts[name] !== script) {
918
+ const alias = `${name}:smoz`;
919
+ if (!(alias in dstScripts)) {
920
+ scriptsOut[alias] = script;
921
+ merged.push(`scripts:${alias}`);
922
+ }
923
+ }
924
+ }
925
+ target.scripts = scriptsOut;
926
+ return merged;
927
+ };
928
+ const copyDirIdempotent = async (srcDir, dstRoot, created, skipped, examples) => {
929
+ const files = await walk(srcDir);
930
+ for (const abs of files) {
931
+ const rel = path.relative(srcDir, abs);
932
+ const dest = path.resolve(dstRoot, rel);
933
+ const data = await fs.promises.readFile(abs, 'utf8');
934
+ if (fs.existsSync(dest)) {
935
+ const ex = `${dest}.example`;
936
+ if (!fs.existsSync(ex)) {
937
+ await writeIfAbsent(ex, data);
938
+ examples.push(path.posix.normalize(ex));
939
+ }
940
+ else {
941
+ skipped.push(path.posix.normalize(dest));
942
+ }
943
+ }
944
+ else {
945
+ const { created: c } = await writeIfAbsent(dest, data);
946
+ if (c)
947
+ created.push(path.posix.normalize(dest));
948
+ else
949
+ skipped.push(path.posix.normalize(dest));
950
+ }
951
+ }
952
+ };
953
+ const runInit = async (root, template = 'minimal', opts) => {
954
+ const created = [];
955
+ const skipped = [];
956
+ const examples = [];
957
+ const merged = [];
958
+ const templatesBase = resolveTemplatesBase();
959
+ const srcBase = path.resolve(templatesBase, template);
960
+ const projectBase = path.resolve(templatesBase, 'project');
961
+ if (!fs.existsSync(srcBase)) {
962
+ throw new Error(`Template "${template}" not found under ${toPosix(templatesBase)}.`);
963
+ }
964
+ // 1) Copy shared boilerplate (project) first (idempotent)
965
+ if (fs.existsSync(projectBase)) {
966
+ await copyDirIdempotent(projectBase, root, created, skipped, examples);
967
+ }
968
+ // 2) Copy selected template
969
+ await copyDirIdempotent(srcBase, root, created, skipped, examples);
970
+ // Seed app/generated/register.*.ts (empty modules) if missing
971
+ const genDir = path.resolve(root, 'app', 'generated');
972
+ const seeds = [
973
+ {
974
+ path: path.join(genDir, 'register.functions.ts'),
975
+ content: `/* AUTO-GENERATED placeholder; will be rewritten by \`smoz register\` */\nexport {};\n`,
976
+ },
977
+ {
978
+ path: path.join(genDir, 'register.openapi.ts'),
979
+ content: `/* AUTO-GENERATED placeholder; will be rewritten by \`smoz register\` */\nexport {};\n`,
980
+ },
981
+ {
982
+ path: path.join(genDir, 'register.serverless.ts'),
983
+ content: `/* AUTO-GENERATED placeholder; will be rewritten by \`smoz register\` */\nexport {};\n`,
984
+ },
985
+ ];
986
+ for (const s of seeds) {
987
+ const { created: c } = await writeIfAbsent(s.path, s.content);
988
+ if (c)
989
+ created.push(path.posix.normalize(s.path));
990
+ else
991
+ skipped.push(path.posix.normalize(s.path));
992
+ }
993
+ // 3) package.json presence (guard or create with --init)
994
+ const pkgPath = path.join(root, 'package.json');
995
+ let pkg = await readJson(pkgPath);
996
+ if (!pkg) {
997
+ const shouldInit = !!(opts && opts.init);
998
+ if (shouldInit) {
999
+ const name = toPosix(root).split('/').pop() ?? 'smoz-app';
1000
+ pkg = {
1001
+ name,
1002
+ private: true,
1003
+ type: 'module',
1004
+ version: '0.0.0',
1005
+ scripts: {},
1006
+ };
1007
+ // Avoid optional chain to satisfy no-unnecessary-condition; normalize to boolean.
1008
+ const dryRunCreate = !!opts.dryRun;
1009
+ if (!dryRunCreate)
1010
+ await writeJson(pkgPath, pkg);
1011
+ created.push(path.posix.normalize(pkgPath));
1012
+ }
1013
+ else {
1014
+ throw new Error('No package.json found. Run "npm init -y" (or re-run with --init) and then "smoz init" again.');
1015
+ }
1016
+ }
1017
+ // 4) Merge manifest (deps/devDeps/scripts) additively
1018
+ const manifestPath = path.resolve(templatesBase, '.manifests', `package.${template}.json`);
1019
+ const manifest = await readJson(manifestPath);
1020
+ if (manifest) {
1021
+ const before = JSON.stringify(pkg);
1022
+ const added = mergeAdditive(pkg, manifest);
1023
+ merged.push(...added);
1024
+ const dryRun = !!(opts && opts.dryRun);
1025
+ if (!dryRun && before !== JSON.stringify(pkg)) {
1026
+ await writeJson(pkgPath, pkg);
1027
+ }
1028
+ }
1029
+ // 5) Optional install
1030
+ let installed = 'skipped';
1031
+ const installOpt = opts ? opts.install : false;
1032
+ // Derive package manager to use (explicit string or detected when true),
1033
+ // then set installed based on presence of a PM.
1034
+ const hasInstallString = typeof installOpt === 'string' && installOpt.trim() !== '';
1035
+ const pm = hasInstallString
1036
+ ? installOpt
1037
+ : installOpt === true
1038
+ ? detectPm(root)
1039
+ : undefined;
1040
+ installed = pm ? runInstall(root, pm) : installed;
1041
+ return { created, skipped, examples, merged, installed };
1042
+ };
1043
+
1044
+ /**
1045
+ * SMOZ CLI — version/signature + register/add
1046
+ *
1047
+ * - Default: print project signature (version, Node, repo root, stanPath, config presence)
1048
+ * - register: one-shot — generate app/generated/register.*.ts from app/functions/**
1049
+ * - openapi: one-shot — run the project’s OpenAPI builder
1050
+ * - dev: watch loop orchestrator for register/openapi and optional local serving
1051
+ * - add: scaffold a new function skeleton under app/functions
1052
+ */
1053
+ const getRepoRoot = () => packageDirectory.packageDirectorySync() ?? process.cwd();
1054
+ const readPkg = (root) => {
1055
+ try {
1056
+ const raw = fs.readFileSync(path.join(root, 'package.json'), 'utf8');
1057
+ return JSON.parse(raw);
1058
+ }
1059
+ catch {
1060
+ return {};
1061
+ }
1062
+ };
1063
+ const detectPackageManager = () => {
1064
+ const ua = process.env.npm_config_user_agent ?? '';
1065
+ if (ua.includes('pnpm'))
1066
+ return 'pnpm';
1067
+ if (ua.includes('yarn'))
1068
+ return 'yarn';
1069
+ if (ua.includes('npm'))
1070
+ return 'npm';
1071
+ return ua || undefined;
1072
+ };
1073
+ const detectStanPath = (root) => {
1074
+ // Keep this conservative; future: read stan.config.* if introduced.
1075
+ const candidate = '.stan';
1076
+ if (fs.existsSync(path.join(root, candidate, 'system', 'stan.system.md'))) {
1077
+ return candidate;
1078
+ }
1079
+ return candidate; // default
1080
+ };
1081
+ const printSignature = () => {
1082
+ const root = getRepoRoot();
1083
+ const pkg = readPkg(root);
1084
+ const name = pkg.name ?? 'smoz';
1085
+ const version = pkg.version ?? '0.0.0';
1086
+ const pm = detectPackageManager();
1087
+ const stanPath = detectStanPath(root);
1088
+ const hasAppConfig = fs.existsSync(path.join(root, 'app', 'config', 'app.config.ts'));
1089
+ const hasSmozJson = fs.existsSync(path.join(root, 'smoz.config.json'));
1090
+ const hasSmozYaml = fs.existsSync(path.join(root, 'smoz.config.yml')) ||
1091
+ fs.existsSync(path.join(root, 'smoz.config.yaml'));
1092
+ console.log(`${name} v${version}`);
1093
+ console.log(`Node ${process.version}`);
1094
+ console.log(`Repo: ${root}`);
1095
+ console.log(`stanPath: ${stanPath}`);
1096
+ console.log(`app/config/app.config.ts: ${hasAppConfig ? 'found' : 'missing'}`);
1097
+ console.log(`smoz.config.*: ${hasSmozJson || hasSmozYaml ? 'found' : 'absent'}`);
1098
+ if (pm)
1099
+ console.log(`PM: ${pm}`);
1100
+ };
1101
+ const main = () => {
1102
+ const root = getRepoRoot();
1103
+ const pkg = readPkg(root);
1104
+ const program = new commander.Command();
1105
+ program
1106
+ .name('smoz')
1107
+ .description('SMOZ CLI')
1108
+ .version(pkg.version ?? '0.0.0');
1109
+ program
1110
+ .command('add')
1111
+ .argument('<spec>', 'Add function: HTTP <eventType>/<segments...>/<method> or non-HTTP <eventType>/<segments...>')
1112
+ .description('Scaffold a new function under app/functions')
1113
+ .action(async (spec) => {
1114
+ try {
1115
+ const { created, skipped } = await runAdd(root, spec);
1116
+ console.log(created.length
1117
+ ? `Created:\n - ${created.join('\n - ')}${skipped.length
1118
+ ? `\nSkipped (exists):\n - ${skipped.join('\n - ')}`
1119
+ : ''}`
1120
+ : 'Nothing created (files already exist).');
1121
+ }
1122
+ catch (e) {
1123
+ console.error(e.message);
1124
+ process.exitCode = 1;
1125
+ }
1126
+ });
1127
+ program
1128
+ .command('init')
1129
+ .description('Scaffold a new SMOZ app from packaged templates (default: minimal)')
1130
+ .option('--template <name>', 'Template name (minimal|full)', 'minimal')
1131
+ .option('--init', 'Create a minimal package.json if missing')
1132
+ .option('-i, --install [pm]', 'Install dependencies (optionally specify pm: npm|pnpm|yarn|bun)')
1133
+ .option('--yes', 'Skip prompts (non-interactive)', false)
1134
+ .option('--dry-run', 'Show planned actions without writing', false)
1135
+ .action(async (opts) => {
1136
+ try {
1137
+ const tpl = typeof opts.template === 'string' ? opts.template : 'minimal';
1138
+ const { created, skipped, examples, merged, installed } = await runInit(root, tpl, opts);
1139
+ console.log([
1140
+ created.length
1141
+ ? `Created:\n - ${created.join('\n - ')}`
1142
+ : 'Created: (none)',
1143
+ examples.length
1144
+ ? `Examples (existing preserved):\n - ${examples.join('\n - ')}`
1145
+ : undefined,
1146
+ skipped.length
1147
+ ? `Skipped (exists):\n - ${skipped.join('\n - ')}`
1148
+ : undefined,
1149
+ merged.length
1150
+ ? `package.json (additive):\n - ${merged.join('\n - ')}`
1151
+ : undefined,
1152
+ `Install: ${installed}`,
1153
+ ]
1154
+ .filter(Boolean)
1155
+ .join('\n'));
1156
+ }
1157
+ catch (e) {
1158
+ console.error(e.message);
1159
+ process.exitCode = 1;
1160
+ }
1161
+ });
1162
+ program
1163
+ .command('register')
1164
+ .description('Scan app/functions/** and generate app/generated/register.*.ts (one-shot)')
1165
+ .action(async () => {
1166
+ const { wrote } = await runRegister(root);
1167
+ console.log(wrote.length ? `Updated:\n - ${wrote.join('\n - ')}` : 'No changes.');
1168
+ });
1169
+ program
1170
+ .command('openapi')
1171
+ .description('Generate app/generated/openapi.json (one-shot)')
1172
+ .action(async () => {
1173
+ try {
1174
+ await runOpenapi(root, { verbose: true });
1175
+ }
1176
+ catch (e) {
1177
+ console.error(e.message);
1178
+ process.exitCode = 1;
1179
+ }
1180
+ });
1181
+ program
1182
+ .command('dev')
1183
+ .description('Watch loop: keep registers/openapi fresh; optionally serve HTTP locally')
1184
+ .option('-r, --register', 'Enable register step on change', true)
1185
+ .option('-R, --no-register', 'Disable register step on change')
1186
+ .option('-o, --openapi', 'Enable openapi step on change', true)
1187
+ .option('-O, --no-openapi', 'Disable openapi step on change')
1188
+ .option('-l, --local [mode]', 'Local server mode: inline|offline', 'inline')
1189
+ .option('-s, --stage <name>', 'Stage name (default inferred)')
1190
+ .option('-p, --port <n>', 'Port (0=random)', (v) => Number(v), 0)
1191
+ .option('-v, --verbose', 'Verbose logging', false)
1192
+ .action(async (opts) => {
1193
+ try {
1194
+ await runDev(root, {
1195
+ register: opts.register !== false,
1196
+ openapi: opts.openapi !== false,
1197
+ local: typeof opts.local === 'string'
1198
+ ? opts.local
1199
+ : opts.local === false
1200
+ ? false
1201
+ : 'inline',
1202
+ ...(typeof opts.stage === 'string' ? { stage: opts.stage } : {}),
1203
+ port: opts.port ?? 0,
1204
+ verbose: !!opts.verbose,
1205
+ });
1206
+ }
1207
+ catch (e) {
1208
+ console.error(e.message);
1209
+ process.exitCode = 1;
1210
+ }
1211
+ });
1212
+ // Default action (no subcommand): print version + signature block
1213
+ program.action(() => {
1214
+ printSignature();
1215
+ });
1216
+ program.parse(process.argv);
1217
+ };
1218
+ main();
package/package.json CHANGED
@@ -161,7 +161,6 @@
161
161
  },
162
162
  "scripts": {
163
163
  "build": "rimraf dist && rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript",
164
- "build:cli": "rollup --config cli.rollup.config.ts --configPlugin @rollup/plugin-typescript",
165
164
  "changelog": "auto-changelog",
166
165
  "deploy": "tsx src/cli/index.ts register && serverless deploy",
167
166
  "diagrams": "cd diagrams/src && plantuml -tpng -o ../out -r .",
@@ -186,5 +185,5 @@
186
185
  "templates:lint": "eslint --fix -c templates/.check/eslint.templates.config.ts \"templates/**/*.{ts,tsx,js,jsx}\" && eslint --fix --no-ignore templates/.check/eslint.templates.config.ts"
187
186
  },
188
187
  "type": "module",
189
- "version": "0.1.2"
188
+ "version": "0.1.3"
190
189
  }