@karmaniverous/smoz 0.2.9 → 0.2.11

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.
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
+ var cliHost = require('@karmaniverous/get-dotenv/cliHost');
5
+ var plugins = require('@karmaniverous/get-dotenv/plugins');
6
+ var packageDirectory = require('package-directory');
4
7
  var fs = require('node:fs');
5
8
  var path = require('node:path');
6
- var commander = require('commander');
7
- var packageDirectory = require('package-directory');
8
9
  var chokidar = require('chokidar');
9
10
  var node_child_process = require('node:child_process');
10
- var os = require('node:os');
11
+ var getDotenv = require('@karmaniverous/get-dotenv');
11
12
  var node_url = require('node:url');
12
13
  var node_process = require('node:process');
13
14
  var promises = require('node:readline/promises');
@@ -82,8 +83,8 @@ import { z } from 'zod';
82
83
 
83
84
  import { app, APP_ROOT_ABS } from '@/app/config/app.config';
84
85
 
85
- export const eventSchema = z.any();
86
- export const responseSchema = z.any();
86
+ export const eventSchema = z.unknown();
87
+ export const responseSchema = z.unknown();
87
88
 
88
89
  export const fn = app.defineFunction({
89
90
  eventType: '${token}',
@@ -153,8 +154,8 @@ import { z } from 'zod';
153
154
 
154
155
  import { app, APP_ROOT_ABS } from '@/app/config/app.config';
155
156
 
156
- export const eventSchema = z.any();
157
- export const responseSchema = z.any();
157
+ export const eventSchema = z.unknown();
158
+ export const responseSchema = z.unknown();
158
159
 
159
160
  export const fn = app.defineFunction({
160
161
  eventType: '${token}',
@@ -281,7 +282,7 @@ const launchOffline = async (root, opts) => {
281
282
  }
282
283
  return { cmd, args, shell };
283
284
  };
284
- let child = spawnOffline(root, makeCmd(), opts.verbose);
285
+ let child = await spawnOffline(root, makeCmd(), opts.verbose);
285
286
  const close = async () => new Promise((resolve) => {
286
287
  if (child.killed) {
287
288
  resolve();
@@ -298,46 +299,23 @@ const launchOffline = async (root, opts) => {
298
299
  });
299
300
  const restart = async () => {
300
301
  await close();
301
- child = spawnOffline(root, makeCmd(), opts.verbose);
302
+ child = await spawnOffline(root, makeCmd(), opts.verbose);
302
303
  };
303
304
  return { restart, close };
304
305
  };
305
- const spawnOffline = (root, cmd, verbose) => {
306
- // Inherit the parent env, but ensure temp variables are present.
307
- // Some toolchains (e.g., tsx/esbuild invoked under offline) interpolate TEMP/TMP,
308
- // and if they are undefined, they may create literal "undefined\\temp\\..." paths
309
- // relative to CWD. Use os.tmpdir() as a safe fallback.
306
+ const spawnOffline = async (root, cmd, verbose) => {
307
+ // Normalize child environment via get-dotenv when available; safe fallback otherwise.
310
308
  const baseEnv = { ...process.env };
311
- const tmp = os.tmpdir();
312
- // Cross-platform default
313
- if (!baseEnv.TMPDIR)
314
- baseEnv.TMPDIR = tmp;
315
- // Windows defaults
316
- if (process.platform === 'win32') {
317
- if (!baseEnv.TEMP)
318
- baseEnv.TEMP = tmp;
319
- if (!baseEnv.TMP)
320
- baseEnv.TMP = tmp;
321
- // Some nested toolchains derive cache roots from these when TMP/TEMP are absent
322
- if (!baseEnv.LOCALAPPDATA)
323
- baseEnv.LOCALAPPDATA = tmp;
324
- if (!baseEnv.USERPROFILE)
325
- baseEnv.USERPROFILE = tmp;
326
- }
327
- else {
328
- // POSIX: some tools fall back to HOME for caches when TMPDIR isn't used
329
- if (!baseEnv.HOME)
330
- baseEnv.HOME = tmp;
331
- }
309
+ const childEnv = getDotenv.buildSpawnEnv(baseEnv);
332
310
  // Optional diagnostics to verify the child sees sane temp-related envs
333
311
  if (verbose) {
334
312
  const snap = {
335
- TMPDIR: baseEnv.TMPDIR,
336
- TEMP: baseEnv.TEMP,
337
- TMP: baseEnv.TMP,
338
- HOME: baseEnv.HOME,
339
- USERPROFILE: baseEnv.USERPROFILE,
340
- LOCALAPPDATA: baseEnv.LOCALAPPDATA,
313
+ TMPDIR: childEnv.TMPDIR,
314
+ TEMP: childEnv.TEMP,
315
+ TMP: childEnv.TMP,
316
+ HOME: childEnv.HOME,
317
+ USERPROFILE: childEnv.USERPROFILE,
318
+ LOCALAPPDATA: childEnv.LOCALAPPDATA,
341
319
  };
342
320
  process.stdout.write(`[offline] env snapshot: ${JSON.stringify(snap)}\n`);
343
321
  }
@@ -345,7 +323,7 @@ const spawnOffline = (root, cmd, verbose) => {
345
323
  cwd: root,
346
324
  shell: cmd.shell,
347
325
  stdio: ['ignore', 'pipe', 'pipe'],
348
- env: baseEnv,
326
+ env: childEnv,
349
327
  });
350
328
  const prefix = '[offline] ';
351
329
  let announced = false;
@@ -375,9 +353,12 @@ const spawnOffline = (root, cmd, verbose) => {
375
353
  return child;
376
354
  };
377
355
 
378
- /* OpenAPI one-shot runner: spawn the project-local OpenAPI script via tsx.
379
- * - Mirrors the npm script: tsx app/config/openapi && prettier (project-local script already formats).
380
- * - Keeps CLI responsibilities minimal; errors bubble via non-zero exit.
356
+ /**
357
+ * @module OpenAPI one-shot runner.
358
+ *
359
+ * Spawns the project-local OpenAPI script via tsx, then formats the output
360
+ * with prettier. Owns the full generate-and-format cycle so consumers can
361
+ * use a single `smoz openapi` invocation.
381
362
  */
382
363
  const findTsxCli = (root) => {
383
364
  // Prefer invoking the JS entry to avoid shell .cmd quirks on Windows.
@@ -393,6 +374,17 @@ const findTsxCli = (root) => {
393
374
  const cmd = process.platform === 'win32' ? 'tsx.cmd' : 'tsx';
394
375
  return { cmd, args: ['app/config/openapi.ts'], shell: true };
395
376
  };
377
+ const findPrettierCli = (root) => {
378
+ const js = path.resolve(root, 'node_modules', 'prettier', 'bin', 'prettier.cjs');
379
+ if (!fs.existsSync(js)) {
380
+ throw new Error(`prettier not found at ${js}. Install it as a devDependency.`);
381
+ }
382
+ return {
383
+ cmd: process.execPath,
384
+ args: [js, '--write', 'app/generated/openapi.json'],
385
+ shell: false,
386
+ };
387
+ };
396
388
  const runOpenapi = async (root, opts) => {
397
389
  // Detect changes by comparing the pre/post content of app/generated/openapi.json.
398
390
  const outFile = path.resolve(root, 'app', 'generated', 'openapi.json');
@@ -405,18 +397,47 @@ const runOpenapi = async (root, opts) => {
405
397
  // ignore read errors; treat as absent
406
398
  before = undefined;
407
399
  }
408
- const { cmd, args, shell } = findTsxCli(root);
400
+ // Build a normalized child env via get-dotenv (static import).
401
+ const env = getDotenv.buildSpawnEnv({});
402
+ // Step 1: generate the OpenAPI spec.
403
+ const tsx = findTsxCli(root);
409
404
  if (opts?.verbose) {
410
- console.log(`[openapi] ${[cmd, ...args].join(' ')}`);
405
+ console.log(`[openapi] ${[tsx.cmd, ...tsx.args].join(' ')}`);
411
406
  }
412
- const res = node_child_process.spawnSync(cmd, args, {
407
+ const genRes = node_child_process.spawnSync(tsx.cmd, tsx.args, {
413
408
  cwd: root,
414
409
  stdio: 'inherit',
415
- shell,
410
+ shell: tsx.shell,
411
+ env,
416
412
  });
417
- if (typeof res.status !== 'number' || res.status !== 0) {
418
- const code = typeof res.status === 'number' ? String(res.status) : 'unknown';
419
- throw new Error(`openapi failed (exit ${code})`);
413
+ if (typeof genRes.status !== 'number' || genRes.status !== 0) {
414
+ const code = typeof genRes.status === 'number' ? String(genRes.status) : 'unknown';
415
+ const detail = genRes.error
416
+ ? `: ${genRes.error.message}`
417
+ : genRes.signal
418
+ ? ` (signal ${genRes.signal})`
419
+ : '';
420
+ throw new Error(`openapi failed (exit ${code})${detail}`);
421
+ }
422
+ // Step 2: format the output with prettier.
423
+ const prettier = findPrettierCli(root);
424
+ if (opts?.verbose) {
425
+ console.log(`[openapi] ${[prettier.cmd, ...prettier.args].join(' ')}`);
426
+ }
427
+ const fmtRes = node_child_process.spawnSync(prettier.cmd, prettier.args, {
428
+ cwd: root,
429
+ stdio: 'inherit',
430
+ shell: prettier.shell,
431
+ env,
432
+ });
433
+ if (typeof fmtRes.status !== 'number' || fmtRes.status !== 0) {
434
+ const code = typeof fmtRes.status === 'number' ? String(fmtRes.status) : 'unknown';
435
+ const detail = fmtRes.error
436
+ ? `: ${fmtRes.error.message}`
437
+ : fmtRes.signal
438
+ ? ` (signal ${fmtRes.signal})`
439
+ : '';
440
+ throw new Error(`prettier format failed (exit ${code})${detail}`);
420
441
  }
421
442
  // Determine whether the file content changed
422
443
  try {
@@ -548,6 +569,50 @@ const inferDefaultStage = (root, verbose) => {
548
569
  console.log('[dev] inferring stage: dev (explicit --stage overrides)');
549
570
  return 'dev';
550
571
  };
572
+ /**
573
+ * Resolve the effective stage for dev according to precedence:
574
+ * 1) CLI --stage
575
+ * 2) getdotenv.config.json -> plugins.smoz.stage (string)
576
+ * 3) process.env.STAGE
577
+ * 4) inferDefaultStage() fallback ("dev")
578
+ *
579
+ * Notes:
580
+ * - We intentionally read only the JSON variant to avoid adding YAML/TS loaders
581
+ * to the CLI runtime. JS/TS config will be handled by the get-dotenv host
582
+ * once the full plugin-first path is wired; this probe is a safe, minimal
583
+ * bridge that preserves current behavior when the file is absent.
584
+ */
585
+ const resolveStage = async (root, cliStage, verbose) => {
586
+ if (typeof cliStage === 'string' && cliStage.trim().length > 0) {
587
+ if (verbose)
588
+ console.log(`[dev] stage (cli): ${cliStage}`);
589
+ return cliStage;
590
+ }
591
+ // getdotenv.config.json → plugins.smoz.stage
592
+ try {
593
+ const cfgPath = path.resolve(root, 'getdotenv.config.json');
594
+ if (fs.existsSync(cfgPath)) {
595
+ const raw = fs.readFileSync(cfgPath, 'utf8');
596
+ const parsed = JSON.parse(raw);
597
+ const fromCfg = parsed.plugins?.smoz?.stage;
598
+ if (typeof fromCfg === 'string' && fromCfg.trim().length > 0) {
599
+ if (verbose)
600
+ console.log(`[dev] stage (config): ${fromCfg}`);
601
+ return fromCfg.trim();
602
+ }
603
+ }
604
+ }
605
+ catch {
606
+ // best-effort; fall through
607
+ }
608
+ const envStage = typeof process.env.STAGE === 'string' ? process.env.STAGE : undefined;
609
+ if (envStage && envStage.trim().length > 0) {
610
+ if (verbose)
611
+ console.log(`[dev] stage (env): ${envStage}`);
612
+ return envStage.trim();
613
+ }
614
+ return inferDefaultStage(root, verbose);
615
+ };
551
616
  const seedEnvForStage = async (root, stage, verbose) => {
552
617
  // Best effort: import the app config to read declared env keys and concrete values.
553
618
  // Preserve existing process.env values; only seed when unset.
@@ -653,21 +718,24 @@ const launchInline = async (root, opts) => {
653
718
  const { entry } = resolveInlineEntry(pkgRoot);
654
719
  const makeTsx = () => {
655
720
  const { cmd, args, shell } = resolveTsxCommand(root, entry);
656
- return node_child_process.spawn(cmd, args, {
657
- cwd: root,
658
- stdio: 'inherit',
659
- shell,
660
- // Enable tsconfig paths for "@/..." during TS fallback.
661
- env: {
721
+ return (async () => {
722
+ const env = getDotenv.buildSpawnEnv({
662
723
  ...process.env,
724
+ // Enable tsconfig paths for "@/..." during TS fallback.
663
725
  TSX_TSCONFIG_PATHS: '1',
664
726
  SMOZ_STAGE: opts.stage,
665
727
  SMOZ_PORT: String(opts.port),
666
728
  SMOZ_VERBOSE: opts.verbose ? '1' : '',
667
- },
668
- });
729
+ });
730
+ return node_child_process.spawn(cmd, args, {
731
+ cwd: root,
732
+ stdio: 'inherit',
733
+ shell,
734
+ env,
735
+ });
736
+ })();
669
737
  };
670
- let child = makeTsx();
738
+ let child = await makeTsx();
671
739
  const close = async () => new Promise((resolve) => {
672
740
  // If the process has already exited (exitCode set), resolve immediately.
673
741
  if (child.exitCode !== null) {
@@ -684,16 +752,14 @@ const launchInline = async (root, opts) => {
684
752
  });
685
753
  const restart = async () => {
686
754
  await close();
687
- child = makeTsx();
755
+ child = await makeTsx();
688
756
  };
689
757
  return { close, restart };
690
758
  };
691
759
 
692
760
  const runDev = async (root, opts) => {
693
761
  const verbose = !!opts.verbose;
694
- const stage = typeof opts.stage === 'string'
695
- ? opts.stage
696
- : inferDefaultStage(root, verbose);
762
+ const stage = await resolveStage(root, typeof opts.stage === 'string' ? opts.stage : undefined, verbose);
697
763
  // Seed env with concrete values for the selected stage.
698
764
  try {
699
765
  await seedEnvForStage(root, stage, verbose);
@@ -1268,226 +1334,144 @@ const runInit = async (root, template = 'default', opts) => {
1268
1334
  };
1269
1335
 
1270
1336
  /**
1271
- * SMOZ CLI version/signature + register/add
1337
+ * SMOZ command plugin (get-dotenv plugin-first host).
1272
1338
  *
1273
- * - Default: print project signature (version, Node, repo root, stanPath, config presence) * - register: one-shot generate app/generated/register.*.ts from app/functions/**
1274
- * - openapi: one-shot run the project’s OpenAPI builder
1275
- * - dev: watch loop orchestrator for register/openapi and optional local serving
1276
- * - add: scaffold a new function skeleton under app/functions
1339
+ * Registers init/add/register/openapi/dev on a GetDotenvCli instance and
1340
+ * delegates to existing implementations. No business logic here.
1277
1341
  */
1278
- const getRepoRoot = () => packageDirectory.packageDirectorySync() ?? process.cwd();
1279
- const readPkg = (root) => {
1280
- try {
1281
- const raw = fs.readFileSync(path.join(root, 'package.json'), 'utf8');
1282
- return JSON.parse(raw);
1283
- }
1284
- catch {
1285
- return {};
1286
- }
1287
- };
1288
- const readSmozConfig = (root) => {
1289
- try {
1290
- const p = path.join(root, 'smoz.config.json');
1291
- if (!fs.existsSync(p))
1292
- return {};
1293
- const raw = fs.readFileSync(p, 'utf8');
1294
- const parsed = JSON.parse(raw);
1295
- if (parsed && typeof parsed === 'object') {
1296
- return parsed;
1297
- }
1298
- return {};
1299
- }
1300
- catch {
1301
- return {};
1302
- }
1303
- };
1304
- const detectPackageManager = () => {
1305
- const ua = process.env.npm_config_user_agent ?? '';
1306
- if (ua.includes('pnpm'))
1307
- return 'pnpm';
1308
- if (ua.includes('yarn'))
1309
- return 'yarn';
1310
- if (ua.includes('npm'))
1311
- return 'npm';
1312
- return ua || undefined;
1313
- };
1314
- const detectStanPath = (root) => {
1315
- // Keep this conservative; future: read stan.config.* if introduced.
1316
- const candidate = '.stan';
1317
- if (fs.existsSync(path.join(root, candidate, 'system', 'stan.system.md'))) {
1318
- return candidate;
1319
- }
1320
- return candidate; // default
1321
- };
1322
- const printSignature = () => {
1323
- const root = getRepoRoot();
1324
- const pkg = readPkg(root);
1325
- const name = pkg.name ?? 'smoz';
1326
- const version = pkg.version ?? '0.0.0';
1327
- const pm = detectPackageManager();
1328
- const stanPath = detectStanPath(root);
1329
- const hasAppConfig = fs.existsSync(path.join(root, 'app', 'config', 'app.config.ts'));
1330
- const hasSmozJson = fs.existsSync(path.join(root, 'smoz.config.json'));
1331
- const hasSmozYaml = fs.existsSync(path.join(root, 'smoz.config.yml')) ||
1332
- fs.existsSync(path.join(root, 'smoz.config.yaml'));
1333
- console.log(`${name} v${version}`);
1334
- console.log(`Node ${process.version}`);
1335
- console.log(`Repo: ${root}`);
1336
- console.log(`stanPath: ${stanPath}`);
1337
- console.log(`app/config/app.config.ts: ${hasAppConfig ? 'found' : 'missing'}`);
1338
- console.log(`smoz.config.*: ${hasSmozJson || hasSmozYaml ? 'found' : 'absent'}`);
1339
- if (pm)
1340
- console.log(`PM: ${pm}`);
1341
- };
1342
- const main = () => {
1343
- const root = getRepoRoot();
1344
- const pkg = readPkg(root);
1345
- const program = new commander.Command();
1346
- program
1347
- .name('smoz')
1348
- .description('SMOZ CLI')
1349
- // Add -v alias for version in addition to default --version behavior
1350
- .version(pkg.version ?? '0.0.0', '-v, --version', 'output the version');
1351
- program
1352
- .command('add')
1353
- .argument('<spec>', 'Add function: HTTP <eventType>/<segments...>/<method> or non-HTTP <eventType>/<segments...>')
1354
- .description('Scaffold a new function under app/functions')
1355
- .action(async (spec) => {
1356
- try {
1357
- const { created, skipped } = await runAdd(root, spec);
1358
- console.log(created.length
1359
- ? `Created:\n - ${created.join('\n - ')}${skipped.length
1360
- ? `\nSkipped (exists):\n - ${skipped.join('\n - ')}`
1361
- : ''}`
1362
- : 'Nothing created (files already exist).');
1363
- }
1364
- catch (e) {
1365
- console.error(e.message);
1366
- process.exitCode = 1;
1367
- }
1368
- });
1369
- program
1370
- .command('init')
1371
- .description('Scaffold a new SMOZ app from packaged templates (default: default)')
1372
- .option('-t, --template <nameOrPath>', 'Template name or directory path', 'default')
1373
- .option('-i, --install [pm]', 'Install dependencies (optionally specify pm: npm|pnpm|yarn|bun)')
1374
- .option('--no-install', 'Skip dependency installation (overrides -y)', false)
1375
- .option('-y, --yes', 'Skip prompts (non-interactive)', false)
1376
- .option('--dry-run', 'Show planned actions without writing', false)
1377
- .action(async (opts) => {
1378
- try {
1379
- const cfg = readSmozConfig(root).cliDefaults?.init ?? {};
1380
- // Resolve template: CLI > config > default
1381
- const tpl = typeof opts.template === 'string'
1382
- ? opts.template
1383
- : typeof cfg.template === 'string'
1384
- ? cfg.template
1385
- : 'default';
1386
- // Resolve install behavior: CLI > --no-install > config
1387
- let install = opts.install;
1388
- if (opts.noInstall === true) {
1389
- install = false;
1390
- }
1391
- else if (install === undefined) {
1392
- const d = cfg.install;
1393
- install =
1394
- d === 'auto'
1395
- ? true
1396
- : d === 'none'
1397
- ? false
1398
- : typeof d === 'string'
1399
- ? d
1400
- : undefined;
1401
- }
1402
- const conflict = typeof opts.conflict === 'string' ? opts.conflict : cfg.onConflict;
1403
- const { created, skipped, examples, merged, installed } = await runInit(root, tpl, {
1404
- // Include only when defined to satisfy exactOptionalPropertyTypes
1405
- ...(install !== undefined ? { install } : {}),
1406
- ...(typeof conflict === 'string' ? { conflict } : {}),
1407
- yes: opts.yes === true,
1408
- noInstall: opts.noInstall === true,
1409
- dryRun: opts.dryRun === true,
1342
+ const repoRoot = () => packageDirectory.packageDirectorySync() ?? process.cwd();
1343
+ const smozPlugin = () => cliHost.definePlugin({
1344
+ id: 'smoz',
1345
+ setup(cli) {
1346
+ // init
1347
+ cli
1348
+ .command('init')
1349
+ .description('Scaffold a new SMOZ app from a template')
1350
+ .option('-t, --template <name>', 'template name', 'default')
1351
+ .option('-y, --yes', 'assume “example” on conflicts')
1352
+ .option('--no-install', 'do not install dependencies')
1353
+ .option('--install <pm>', 'explicit package manager (npm|pnpm|yarn|bun)')
1354
+ .option('--conflict <policy>', 'overwrite|example|skip|ask')
1355
+ .action(async (opts) => {
1356
+ const root = repoRoot();
1357
+ const template = typeof opts.template === 'string' ? opts.template : 'default';
1358
+ const yes = Boolean(opts.yes);
1359
+ const noInstall = opts.install === undefined &&
1360
+ opts['noInstall'] !== undefined
1361
+ ? Boolean(opts['noInstall'])
1362
+ : Boolean(opts['noInstall']);
1363
+ const pmRaw = opts.install ?? undefined;
1364
+ const conflict = opts.conflict ?? undefined;
1365
+ await runInit(root, template, {
1366
+ ...(yes ? { yes } : {}),
1367
+ ...(noInstall ? { noInstall } : {}),
1368
+ ...(pmRaw ? { install: pmRaw } : {}),
1369
+ ...(conflict ? { conflict } : {}),
1410
1370
  });
1411
- console.log([
1412
- created.length
1413
- ? `Created:\n - ${created.join('\n - ')}`
1414
- : 'Created: (none)',
1415
- examples.length
1416
- ? `Examples (existing preserved):\n - ${examples.join('\n - ')}`
1417
- : undefined,
1418
- skipped.length
1419
- ? `Skipped (exists):\n - ${skipped.join('\n - ')}`
1420
- : undefined,
1421
- merged.length
1422
- ? `package.json (additive):\n - ${merged.join('\n - ')}`
1423
- : undefined,
1424
- `Install: ${installed}`,
1425
- ]
1426
- .filter(Boolean)
1427
- .join('\n'));
1428
- }
1429
- catch (e) {
1430
- console.error(e.message);
1431
- process.exitCode = 1;
1432
- }
1433
- });
1434
- program
1435
- .command('register')
1436
- .description('Scan app/functions/** and generate app/generated/register.*.ts (one-shot)')
1437
- .action(async () => {
1438
- const { wrote } = await runRegister(root);
1439
- console.log(wrote.length ? `Updated:\n - ${wrote.join('\n - ')}` : 'No changes.');
1440
- });
1441
- program
1442
- .command('openapi')
1443
- .description('Generate app/generated/openapi.json (one-shot)')
1444
- .action(async () => {
1445
- try {
1446
- await runOpenapi(root, { verbose: true });
1447
- }
1448
- catch (e) {
1449
- console.error(e.message);
1450
- process.exitCode = 1;
1451
- }
1452
- });
1453
- program
1454
- .command('dev')
1455
- .description('Watch loop: keep registers/openapi fresh; optionally serve HTTP locally')
1456
- .option('-r, --register', 'Enable register step on change', true)
1457
- .option('-R, --no-register', 'Disable register step on change')
1458
- .option('-o, --openapi', 'Enable openapi step on change', true)
1459
- .option('-O, --no-openapi', 'Disable openapi step on change')
1460
- .option('-l, --local [mode]', 'Local server mode: inline|offline', 'inline')
1461
- .option('-s, --stage <name>', 'Stage name (default inferred)')
1462
- .option('-p, --port <n>', 'Port (0=random)', (v) => Number(v), 0)
1463
- .option('-V, --verbose', 'Verbose logging', false)
1464
- .action(async (opts) => {
1465
- try {
1466
- const cfg = readSmozConfig(root).cliDefaults?.dev ?? {};
1467
- // Resolve local mode default from config if not provided
1468
- const localResolved = typeof opts.local === 'string'
1469
- ? opts.local
1470
- : opts.local === false
1371
+ });
1372
+ // add
1373
+ cli
1374
+ .command('add')
1375
+ .description('Scaffold a function under app/functions')
1376
+ .argument('<spec>', 'spec: <eventType>/<segments...>/<method>')
1377
+ .action(async (spec) => {
1378
+ const root = repoRoot();
1379
+ if (typeof spec !== 'string' || !spec.trim()) {
1380
+ throw new Error('smoz add: missing <spec> (e.g., rest/foo/get)');
1381
+ }
1382
+ await runAdd(root, spec);
1383
+ });
1384
+ // register
1385
+ cli
1386
+ .command('register')
1387
+ .description('Generate side-effect register files')
1388
+ .action(async () => {
1389
+ const root = repoRoot();
1390
+ await runRegister(root);
1391
+ });
1392
+ // openapi
1393
+ cli
1394
+ .command('openapi')
1395
+ .description('Generate OpenAPI document (app/generated/openapi.json)')
1396
+ .option('-V, --verbose', 'verbose output')
1397
+ .action(async (opts) => {
1398
+ const root = repoRoot();
1399
+ await runOpenapi(root, { verbose: !!opts.verbose });
1400
+ });
1401
+ // dev
1402
+ cli
1403
+ .command('dev')
1404
+ .description('Dev loop: register/openapi + local backend (inline|offline)')
1405
+ .option('-r, --register', 'run register step (default on)')
1406
+ .option('-R, --no-register', 'disable register step')
1407
+ .option('-o, --openapi', 'run openapi step (default on)')
1408
+ .option('-O, --no-openapi', 'disable openapi step')
1409
+ .option('-l, --local [mode]', 'local mode: inline | offline | false (default inline)')
1410
+ .option('-s, --stage <name>', 'stage name')
1411
+ .option('-p, --port <number>', 'port (0 = random free port)')
1412
+ .option('-V, --verbose', 'verbose output')
1413
+ .action(async (opts) => {
1414
+ const root = repoRoot();
1415
+ // register/openapi defaults (on); explicit negations take precedence.
1416
+ const register = opts.register === true
1417
+ ? true
1418
+ : opts.noRegister
1419
+ ? false
1420
+ : true;
1421
+ const openapi = opts.openapi === true
1422
+ ? true
1423
+ : opts.noOpenapi
1471
1424
  ? false
1472
- : (cfg.local ?? 'inline');
1425
+ : true;
1426
+ // local mode parsing
1427
+ const raw = opts.local;
1428
+ let local = 'inline';
1429
+ if (raw === 'inline' || raw === 'offline')
1430
+ local = raw;
1431
+ else if (raw === true)
1432
+ local = 'inline';
1433
+ else if (raw === 'false')
1434
+ local = false;
1435
+ const port = typeof opts.port === 'string'
1436
+ ? Number(opts.port)
1437
+ : typeof opts.port === 'number'
1438
+ ? opts.port
1439
+ : undefined;
1473
1440
  await runDev(root, {
1474
- register: opts.register !== false,
1475
- openapi: opts.openapi !== false,
1476
- local: localResolved,
1441
+ register,
1442
+ openapi,
1443
+ local,
1477
1444
  ...(typeof opts.stage === 'string' ? { stage: opts.stage } : {}),
1478
- port: opts.port ?? 0,
1445
+ ...(typeof port === 'number' ? { port } : {}),
1479
1446
  verbose: !!opts.verbose,
1480
1447
  });
1481
- }
1482
- catch (e) {
1483
- console.error(e.message);
1484
- process.exitCode = 1;
1485
- }
1486
- });
1487
- // Default action (no subcommand): print version + signature block
1488
- program.action(() => {
1489
- printSignature();
1448
+ });
1449
+ },
1450
+ });
1451
+
1452
+ /**
1453
+ * SMOZ CLI — plugin-first host built on get-dotenv.
1454
+ *
1455
+ * - Build a GetDotenvCli host, install included plugins (cmd/batch/aws),
1456
+ * and install the SMOZ command plugin (init/add/register/openapi/dev).
1457
+ * - Resolve dotenv context once, then parse argv.
1458
+ */
1459
+ const main = async () => {
1460
+ const cli = new cliHost.GetDotenvCli('smoz');
1461
+ await cli.brand({
1462
+ importMetaUrl: (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)),
1463
+ description: 'SMOZ CLI',
1490
1464
  });
1491
- program.parse(process.argv);
1465
+ cli
1466
+ .attachRootOptions()
1467
+ .use(smozPlugin())
1468
+ .use(plugins.awsPlugin())
1469
+ .use(plugins.cmdPlugin({
1470
+ asDefault: true,
1471
+ optionAlias: '-c, --cmd <command...>',
1472
+ }))
1473
+ .use(plugins.batchPlugin())
1474
+ .passOptions();
1475
+ await cli.parseAsync();
1492
1476
  };
1493
- main();
1477
+ void main();