@karmaniverous/smoz 0.1.2 → 0.1.4

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