@karmaniverous/smoz 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,17 +18,41 @@ SMOZ is a small, pragmatic toolkit for authoring AWS Lambda handlers with [Middy
18
18
  - HTTP middleware with validation, shaping, errors, CORS, negotiation, and HEAD
19
19
  - Non‑HTTP flows stay lean (no middleware overhead)
20
20
 
21
- ## Quick links
21
+ ## Quick start (from zero)
22
+
23
+ From an empty directory:
24
+
25
+ ```bash
26
+ npx @karmaniverous/smoz init -i
27
+ npx smoz dev -p 3000
28
+ ```
29
+
30
+ - The first command scaffolds a new app and installs dependencies (including a local `smoz` bin).
31
+ - The second command starts the inline local backend and keeps registers + OpenAPI fresh.
32
+ - Open http://localhost:3000/openapi in your browser.
22
33
 
34
+ Prefer `serverless‑offline`?
35
+
36
+ ```bash
37
+ npx smoz dev -l offline -p 3000
38
+ ```
39
+
40
+ Add your first endpoint (avoid clashing with the template’s hello):
41
+
42
+ ```bash
43
+ npx smoz add rest/foo/get
44
+ ```
45
+
46
+ ## Quick links
47
+
23
48
  - [Overview](https://docs.karmanivero.us/smoz/documents/Overview.html)
24
49
  - [Why smoz?](https://docs.karmanivero.us/smoz/documents/Why_smoz_.html)
25
50
  - [Getting started](https://docs.karmanivero.us/smoz/documents/Getting_started.html)
26
51
  - [10-minute tour](https://docs.karmanivero.us/smoz/documents/10%E2%80%91minute_tour.html)
27
- - [HTTP MIddleware](https://docs.karmanivero.us/smoz/documents/HTTP_middleware.html)
52
+ - [HTTP Middleware](https://docs.karmanivero.us/smoz/documents/HTTP_middleware.html)
28
53
  - [Recipes](https://docs.karmanivero.us/smoz/documents/Recipes.html)
29
54
  - [SQS function](https://docs.karmanivero.us/smoz/documents/Recipes.SQS_function.html)
30
- - [Contexts + Cognito authorizer](https://docs.karmanivero.us/smoz/documents/Recipes.Contexts_+_Cognito_authorizer.html)
31
- - [Custom middleware insertion](https://docs.karmanivero.us/smoz/documents/Recipes.Custom_middleware_insertion.html)
55
+ - [Contexts + Cognito authorizer](https://docs.karmanivero.us/smoz/documents/Recipes.Contexts_+_Cognito_authorizer.html) - [Custom middleware insertion](https://docs.karmanivero.us/smoz/documents/Recipes.Custom_middleware_insertion.html)
32
56
  - [Per‑function env](<https://docs.karmanivero.us/smoz/documents/Recipes.Per%E2%80%91function_env_(fnEnvKeys).html>)
33
57
  - [Observability](<https://docs.karmanivero.us/smoz/documents/Recipes.Observability_(requestId_header).html>)
34
58
  - [Troubleshooting](https://docs.karmanivero.us/smoz/documents/Recipes.Troubleshooting.html)
@@ -792,16 +792,6 @@ const launchInline = async (root, opts) => {
792
792
  return { close, restart };
793
793
  };
794
794
 
795
- /**
796
- * smoz init
797
- *
798
- * Scaffolds a new project from packaged templates.
799
- * - Copies ./templates/<template>/ into the target root (default: default)
800
- * - Seeds app/generated/register.*.ts (empty modules) if missing * - 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
795
  const writeIfAbsent = async (outFile, content) => {
806
796
  if (fs.existsSync(outFile))
807
797
  return { created: false };
@@ -809,25 +799,6 @@ const writeIfAbsent = async (outFile, content) => {
809
799
  await fs.promises.writeFile(outFile, content, 'utf8');
810
800
  return { created: true };
811
801
  };
812
- const askConflict = async (rl, filePath) => {
813
- const q = `File exists: ${toPosix(filePath)}\n` +
814
- `Choose: [o]verwrite, [e]xample, [s]kip, [O]verwrite all, [E]xample all, [S]kip all: `;
815
- // Single key selection for simplicity
816
- const ans = (await rl.question(q)).trim();
817
- if (/^o$/.test(ans))
818
- return 'overwrite';
819
- if (/^e$/.test(ans))
820
- return 'example';
821
- if (/^s$/.test(ans))
822
- return 'skip';
823
- if (/^O$/.test(ans))
824
- return 'all-overwrite';
825
- if (/^E$/.test(ans))
826
- return 'all-example';
827
- if (/^S$/.test(ans))
828
- return 'all-skip';
829
- return 'example';
830
- };
831
802
  const walk = async (dir, out = []) => {
832
803
  const entries = await fs.promises.readdir(dir, { withFileTypes: true });
833
804
  for (const ent of entries) {
@@ -841,15 +812,6 @@ const walk = async (dir, out = []) => {
841
812
  }
842
813
  return out;
843
814
  };
844
- const resolveTemplatesBase = () => {
845
- // Resolve the templates folder from the CLI package install root,
846
- // not the caller’s project root. This makes --template <name> work
847
- // consistently whether smoz is run from a consuming app or this repo.
848
- // Anchor discovery to this module’s directory.
849
- const here = path.dirname(node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
850
- const pkgRoot = packageDirectory.packageDirectorySync({ cwd: here }) ?? process.cwd(); // conservative fallback
851
- return path.resolve(pkgRoot, 'templates');
852
- };
853
815
  const readJson = async (file) => {
854
816
  try {
855
817
  const data = await fs.promises.readFile(file, 'utf8');
@@ -863,6 +825,83 @@ const writeJson = async (file, obj) => {
863
825
  await fs.promises.mkdir(path.dirname(file), { recursive: true });
864
826
  await fs.promises.writeFile(file, JSON.stringify(obj, null, 2), 'utf8');
865
827
  };
828
+
829
+ const askConflict = async (rl, filePath) => {
830
+ const q = `File exists: ${filePath}\n` +
831
+ `Choose: [o]verwrite, [e]xample, [s]kip, ` +
832
+ `[O]verwrite all, [E]xample all, [S]kip all: `;
833
+ const ans = (await rl.question(q)).trim();
834
+ if (/^o$/.test(ans))
835
+ return 'overwrite';
836
+ if (/^e$/.test(ans))
837
+ return 'example';
838
+ if (/^s$/.test(ans))
839
+ return 'skip';
840
+ if (/^O$/.test(ans))
841
+ return 'all-overwrite';
842
+ if (/^E$/.test(ans))
843
+ return 'all-example';
844
+ if (/^S$/.test(ans))
845
+ return 'all-skip';
846
+ return 'example';
847
+ };
848
+ const copyDirWithConflicts = async (srcDir, dstRoot, created, skipped, examples, opts) => {
849
+ const files = await walk(srcDir);
850
+ let sticky;
851
+ for (const abs of files) {
852
+ const rel = path.relative(srcDir, abs);
853
+ if (opts.exclude && opts.exclude(rel)) {
854
+ // Skip dev-only or excluded files silently.
855
+ continue;
856
+ }
857
+ const dest = path.resolve(dstRoot, rel);
858
+ const data = await fs.promises.readFile(abs, 'utf8');
859
+ if (!fs.existsSync(dest)) {
860
+ const { created: c } = await writeIfAbsent(dest, data);
861
+ if (c)
862
+ created.push(path.posix.normalize(dest));
863
+ else
864
+ skipped.push(path.posix.normalize(dest));
865
+ continue;
866
+ }
867
+ // Conflict flow
868
+ let decision = opts.conflict === 'ask' ? 'example' : opts.conflict;
869
+ if (opts.conflict === 'ask' && opts.rl && !sticky) {
870
+ const ans = await askConflict(opts.rl, path.posix.normalize(dest));
871
+ if (ans === 'all-overwrite') {
872
+ sticky = 'overwrite';
873
+ }
874
+ else if (ans === 'all-example') {
875
+ sticky = 'example';
876
+ }
877
+ else if (ans === 'all-skip') {
878
+ sticky = 'skip';
879
+ }
880
+ else {
881
+ decision = ans;
882
+ }
883
+ }
884
+ if (sticky)
885
+ decision = sticky;
886
+ if (decision === 'overwrite') {
887
+ await fs.promises.mkdir(path.dirname(dest), { recursive: true });
888
+ await fs.promises.writeFile(dest, data, 'utf8');
889
+ created.push(path.posix.normalize(dest));
890
+ }
891
+ else if (decision === 'example') {
892
+ const ex = `${dest}.example`;
893
+ const { created: c } = await writeIfAbsent(ex, data);
894
+ if (c)
895
+ examples.push(path.posix.normalize(ex));
896
+ else
897
+ skipped.push(path.posix.normalize(ex));
898
+ }
899
+ else {
900
+ skipped.push(path.posix.normalize(dest));
901
+ }
902
+ }
903
+ };
904
+
866
905
  const detectPm = (root) => {
867
906
  if (fs.existsSync(path.join(root, 'pnpm-lock.yaml')))
868
907
  return 'pnpm';
@@ -889,7 +928,6 @@ const runInstall = (root, pm) => {
889
928
  const known = pm === 'pnpm' || pm === 'yarn' || pm === 'bun' || pm === 'npm';
890
929
  if (!known)
891
930
  return 'unknown-pm';
892
- // Spawn with explicit args; avoid tuple inference that widens types.
893
931
  const res = node_child_process.spawnSync(pm, ['install'], {
894
932
  stdio: 'inherit',
895
933
  cwd: root,
@@ -907,10 +945,10 @@ const runInstall = (root, pm) => {
907
945
  }
908
946
  return 'failed';
909
947
  };
948
+
910
949
  const mergeAdditive = (target, source) => {
911
950
  const merged = [];
912
951
  const mergeKey = (key) => {
913
- // Allow possibly-undefined shapes to satisfy lint (no-unnecessary-condition).
914
952
  const src = source[key] ?? {};
915
953
  const dst = target[key] ?? {};
916
954
  const out = { ...dst };
@@ -948,53 +986,81 @@ const mergeAdditive = (target, source) => {
948
986
  target.scripts = scriptsOut;
949
987
  return merged;
950
988
  };
951
- const copyDirWithConflicts = async (srcDir, dstRoot, created, skipped, examples, opts) => {
952
- const files = await walk(srcDir);
953
- let sticky;
954
- for (const abs of files) {
955
- const rel = path.relative(srcDir, abs);
956
- const dest = path.resolve(dstRoot, rel);
957
- const data = await fs.promises.readFile(abs, 'utf8');
958
- if (!fs.existsSync(dest)) {
959
- const { created: c } = await writeIfAbsent(dest, data);
960
- if (c)
961
- created.push(path.posix.normalize(dest));
962
- else
963
- skipped.push(path.posix.normalize(dest));
964
- continue;
965
- }
966
- // Conflict
967
- let decision = opts.conflict === 'ask' ? 'example' : opts.conflict;
968
- if (opts.conflict === 'ask' && opts.rl && !sticky) {
969
- const ans = await askConflict(opts.rl, dest);
970
- if (ans === 'all-overwrite') {
971
- sticky = 'overwrite';
972
- }
973
- else if (ans === 'all-example') {
974
- sticky = 'example';
975
- }
976
- else if (ans === 'all-skip') {
977
- sticky = 'skip';
978
- }
979
- else {
980
- decision = ans;
981
- }
982
- }
983
- if (sticky)
984
- decision = sticky;
985
- if (decision === 'overwrite') {
986
- await fs.promises.writeFile(dest, data, 'utf8');
987
- created.push(path.posix.normalize(dest));
988
- }
989
- else if (decision === 'example') {
990
- const ex = `${dest}.example`;
991
- await writeIfAbsent(ex, data);
992
- examples.push(path.posix.normalize(ex));
993
- }
994
- else {
995
- skipped.push(path.posix.normalize(dest));
989
+ /**
990
+ * Ensure a runtime dependency on @karmaniverous/smoz is present in the
991
+ * target manifest, using a caret version derived from the toolkit package.
992
+ * Returns the merge descriptor string when added, else undefined.
993
+ */
994
+ const ensureToolkitDependency = async (targetPkg, templatesBase) => {
995
+ try {
996
+ const toolkitRoot = path.dirname(templatesBase);
997
+ const toolkitPkg = await readJson(path.join(toolkitRoot, 'package.json'));
998
+ const verRaw = toolkitPkg?.version?.trim();
999
+ const depVersion = verRaw ? `^${verRaw}` : '^0.0.0';
1000
+ const deps = targetPkg.dependencies ?? {};
1001
+ if (!deps['@karmaniverous/smoz']) {
1002
+ targetPkg.dependencies ??=
1003
+ {};
1004
+ targetPkg.dependencies['@karmaniverous/smoz'] = depVersion;
1005
+ return `dependencies:@karmaniverous/smoz@${depVersion}`;
996
1006
  }
997
1007
  }
1008
+ catch {
1009
+ // best-effort; ignore
1010
+ }
1011
+ return undefined;
1012
+ };
1013
+
1014
+ /**
1015
+ + * Resolve the packaged templates root from the CLI install location,
1016
+ * not the caller's project root. This makes -t <name> work both from
1017
+ * a consuming app and from this repository.
1018
+ + */
1019
+ const resolveTemplatesBase = () => {
1020
+ const here = path.dirname(node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href))));
1021
+ const pkgRoot = packageDirectory.packageDirectorySync({ cwd: here }) ?? process.cwd();
1022
+ return path.resolve(pkgRoot, 'templates');
1023
+ };
1024
+ const toPosix = (p) =>
1025
+ // local helper for path presentation (not used for FS operations)
1026
+ p.replace(/\\/g, '/');
1027
+
1028
+ const seedRegisterPlaceholders = async (root) => {
1029
+ const genDir = path.join(root, 'app', 'generated');
1030
+ const seeds = [
1031
+ {
1032
+ path: path.join(genDir, 'register.functions.ts'),
1033
+ content: '/* AUTO-GENERATED placeholder; will be rewritten by `smoz register` */\nexport {};\n',
1034
+ },
1035
+ {
1036
+ path: path.join(genDir, 'register.openapi.ts'),
1037
+ content: '/* AUTO-GENERATED placeholder; will be rewritten by `smoz register` */\nexport {};\n',
1038
+ },
1039
+ {
1040
+ path: path.join(genDir, 'register.serverless.ts'),
1041
+ content: '/* AUTO-GENERATED placeholder; will be rewritten by `smoz register` */\nexport {};\n',
1042
+ },
1043
+ ];
1044
+ const created = [];
1045
+ const skipped = [];
1046
+ for (const s of seeds) {
1047
+ const { created: c } = await writeIfAbsent(s.path, s.content);
1048
+ (c ? created : skipped).push(path.posix.normalize(s.path));
1049
+ }
1050
+ return { created, skipped };
1051
+ };
1052
+
1053
+ const toPosixSep = (p) => p.split(path.sep).join('/');
1054
+ // Exclude dev-only type stubs (not meant for downstream apps).
1055
+ const excludeDev = (rel) => {
1056
+ const posixRel = rel.replace(/\\/g, '/');
1057
+ return /\/types\/[^/]*\.dev\.d\.ts$/i.test(posixRel);
1058
+ };
1059
+ const excludeTemplate = (rel) => {
1060
+ const posixRel = rel.replace(/\\/g, '/');
1061
+ if (posixRel === 'tsconfig.json')
1062
+ return true; // skip dev tsconfig from template
1063
+ return excludeDev(rel);
998
1064
  };
999
1065
  const runInit = async (root, template = 'default', opts) => {
1000
1066
  const created = [];
@@ -1003,7 +1069,7 @@ const runInit = async (root, template = 'default', opts) => {
1003
1069
  const merged = [];
1004
1070
  const optAll = opts ?? {};
1005
1071
  const templatesBase = resolveTemplatesBase();
1006
- // Resolve template source: named template (default/minimal/full) or filesystem path
1072
+ // Resolve template source: named template or filesystem path
1007
1073
  const templateIsPath = fs.existsSync(template) && (await fs.promises.stat(template)).isDirectory();
1008
1074
  const srcBase = templateIsPath
1009
1075
  ? path.resolve(template)
@@ -1012,9 +1078,9 @@ const runInit = async (root, template = 'default', opts) => {
1012
1078
  if (!fs.existsSync(srcBase)) {
1013
1079
  throw new Error(`Template "${template}" not found (path or name). Tried: ${toPosix(srcBase)}.`);
1014
1080
  }
1015
- // 1) Copy shared boilerplate (project) first (idempotent)
1081
+ // 1) Copy shared project boilerplate
1016
1082
  if (fs.existsSync(projectBase)) {
1017
- const rl = optAll.yes
1083
+ const rl = optAll.yes === true
1018
1084
  ? undefined
1019
1085
  : promises.createInterface({ input: node_process.stdin, output: node_process.stdout, terminal: true });
1020
1086
  let policy;
@@ -1026,13 +1092,15 @@ const runInit = async (root, template = 'default', opts) => {
1026
1092
  const copyOpts = rl
1027
1093
  ? { conflict: policy, rl }
1028
1094
  : { conflict: policy };
1029
- await copyDirWithConflicts(projectBase, root, created, skipped, examples, copyOpts);
1030
- if (rl)
1031
- rl.close();
1095
+ await copyDirWithConflicts(projectBase, root, created, skipped, examples, {
1096
+ ...copyOpts,
1097
+ exclude: excludeDev,
1098
+ });
1099
+ rl?.close();
1032
1100
  }
1033
1101
  // 2) Copy selected template
1034
1102
  {
1035
- const rl = optAll.yes
1103
+ const rl = optAll.yes === true
1036
1104
  ? undefined
1037
1105
  : promises.createInterface({ input: node_process.stdin, output: node_process.stdout, terminal: true });
1038
1106
  let policy;
@@ -1044,13 +1112,13 @@ const runInit = async (root, template = 'default', opts) => {
1044
1112
  const copyOpts = rl
1045
1113
  ? { conflict: policy, rl }
1046
1114
  : { conflict: policy };
1047
- await copyDirWithConflicts(srcBase, root, created, skipped, examples, copyOpts);
1048
- if (rl)
1049
- rl.close();
1115
+ await copyDirWithConflicts(srcBase, root, created, skipped, examples, {
1116
+ ...copyOpts,
1117
+ exclude: excludeTemplate,
1118
+ });
1119
+ rl?.close();
1050
1120
  }
1051
- // 2.5) Convert template 'gitignore' into real '.gitignore'
1052
- // NPM often excludes '.gitignore' from published packages; shipping 'gitignore'
1053
- // and converting here ensures downstream projects get a proper .gitignore.
1121
+ // 2.5) Convert template 'gitignore' into a real '.gitignore'
1054
1122
  try {
1055
1123
  const giSrc = path.join(root, 'gitignore');
1056
1124
  const giDot = path.join(root, '.gitignore');
@@ -1060,8 +1128,6 @@ const runInit = async (root, template = 'default', opts) => {
1060
1128
  created.push(path.posix.normalize(giDot));
1061
1129
  }
1062
1130
  else {
1063
- // Both exist: preserve the template as an example (if not already present),
1064
- // then remove the extra 'gitignore' to avoid clutter.
1065
1131
  const example = path.join(root, 'gitignore.example');
1066
1132
  if (!fs.existsSync(example)) {
1067
1133
  const data = await fs.promises.readFile(giSrc, 'utf8');
@@ -1073,36 +1139,43 @@ const runInit = async (root, template = 'default', opts) => {
1073
1139
  }
1074
1140
  }
1075
1141
  catch {
1076
- // best-effort; ignore conversion errors
1142
+ // best-effort
1077
1143
  }
1078
- // Seed app/generated/register.*.ts (empty modules) if missing
1079
- const genDir = path.resolve(root, 'app', 'generated');
1080
- const seeds = [
1081
- {
1082
- path: path.join(genDir, 'register.functions.ts'),
1083
- content: `/* AUTO-GENERATED placeholder; will be rewritten by \`smoz register\` */\nexport {};\n`,
1084
- },
1085
- {
1086
- path: path.join(genDir, 'register.openapi.ts'),
1087
- content: `/* AUTO-GENERATED placeholder; will be rewritten by \`smoz register\` */\nexport {};\n`,
1088
- },
1089
- {
1090
- path: path.join(genDir, 'register.serverless.ts'),
1091
- content: `/* AUTO-GENERATED placeholder; will be rewritten by \`smoz register\` */\nexport {};\n`,
1092
- },
1093
- ];
1094
- for (const s of seeds) {
1095
- const { created: c } = await writeIfAbsent(s.path, s.content);
1096
- if (c)
1097
- created.push(path.posix.normalize(s.path));
1098
- else
1099
- skipped.push(path.posix.normalize(s.path));
1144
+ // 2.6) Convert 'tsconfig.downstream.json' into 'tsconfig.json'
1145
+ try {
1146
+ const ds = path.join(root, 'tsconfig.downstream.json');
1147
+ const dst = path.join(root, 'tsconfig.json');
1148
+ if (fs.existsSync(ds)) {
1149
+ if (!fs.existsSync(dst)) {
1150
+ await fs.promises.rename(ds, dst);
1151
+ created.push(path.posix.normalize(dst));
1152
+ }
1153
+ else {
1154
+ // If a tsconfig already exists, write an example and remove the source
1155
+ const example = path.join(root, 'tsconfig.json.example');
1156
+ if (!fs.existsSync(example)) {
1157
+ const data = await fs.promises.readFile(ds, 'utf8');
1158
+ await fs.promises.writeFile(example, data, 'utf8');
1159
+ examples.push(path.posix.normalize(example));
1160
+ }
1161
+ await fs.promises.rm(ds, { force: true });
1162
+ }
1163
+ }
1164
+ }
1165
+ catch {
1166
+ // best-effort
1167
+ }
1168
+ // Seed app/generated/register.* placeholders
1169
+ {
1170
+ const res = await seedRegisterPlaceholders(root);
1171
+ created.push(...res.created);
1172
+ skipped.push(...res.skipped);
1100
1173
  }
1101
1174
  // 3) package.json presence (create when missing)
1102
1175
  const pkgPath = path.join(root, 'package.json');
1103
1176
  let pkg = await readJson(pkgPath);
1104
1177
  if (!pkg) {
1105
- const name = toPosix(root).split('/').pop() ?? 'smoz-app';
1178
+ const name = toPosixSep(root).split('/').pop() ?? 'smoz-app';
1106
1179
  pkg = {
1107
1180
  name,
1108
1181
  private: true,
@@ -1110,33 +1183,39 @@ const runInit = async (root, template = 'default', opts) => {
1110
1183
  version: '0.0.0',
1111
1184
  scripts: {},
1112
1185
  };
1113
- const dryRunCreate = Boolean(optAll.dryRun);
1114
- if (!dryRunCreate)
1186
+ if (!optAll.dryRun)
1115
1187
  await writeJson(pkgPath, pkg);
1116
1188
  created.push(path.posix.normalize(pkgPath));
1117
- } // 4) Merge manifest (deps/devDeps/scripts) additively
1118
- // Prefer a real package.json in the template; fallback to legacy manifests if absent.
1189
+ }
1190
+ // 4) Merge manifest additively (prefer template's embedded package.json)
1119
1191
  const templatePkgPath = path.resolve(srcBase, 'package.json');
1120
1192
  const manifest = fs.existsSync(templatePkgPath)
1121
1193
  ? await readJson(templatePkgPath)
1122
1194
  : await readJson(path.resolve(templatesBase, '.manifests', `package.${template}.json`));
1195
+ let pkgChanged = false;
1123
1196
  if (manifest) {
1124
- const before = JSON.stringify(pkg);
1125
1197
  const added = mergeAdditive(pkg, manifest);
1126
- merged.push(...added);
1127
- const dryRun = Boolean(optAll.dryRun);
1128
- if (!dryRun && before !== JSON.stringify(pkg)) {
1129
- await writeJson(pkgPath, pkg);
1198
+ if (added.length > 0) {
1199
+ pkgChanged = true;
1200
+ merged.push(...added);
1201
+ }
1202
+ }
1203
+ // 4.5) Ensure runtime dependency on @karmaniverous/smoz is present
1204
+ {
1205
+ const injected = await ensureToolkitDependency(pkg, templatesBase);
1206
+ if (injected) {
1207
+ merged.push(injected);
1208
+ pkgChanged = true;
1130
1209
  }
1131
1210
  }
1211
+ if (!optAll.dryRun && pkgChanged)
1212
+ await writeJson(pkgPath, pkg);
1132
1213
  // 5) Optional install
1133
1214
  let installed = 'skipped';
1134
- // Install policy:
1215
+ // Policy:
1135
1216
  // -y implies auto install unless --no-install
1136
1217
  const installOpt = optAll.install ?? false;
1137
1218
  const impliedAuto = optAll.yes === true && optAll.noInstall !== true && installOpt !== false;
1138
- // Derive package manager to use (explicit string or detected when true),
1139
- // then set installed based on presence of a PM.
1140
1219
  const hasInstallString = typeof installOpt === 'string' && installOpt.trim() !== '';
1141
1220
  const pm = hasInstallString
1142
1221
  ? installOpt
@@ -1144,13 +1223,7 @@ const runInit = async (root, template = 'default', opts) => {
1144
1223
  ? detectPm(root)
1145
1224
  : undefined;
1146
1225
  installed = pm ? runInstall(root, pm) : installed;
1147
- return {
1148
- created,
1149
- skipped,
1150
- examples,
1151
- merged,
1152
- installed,
1153
- };
1226
+ return { created, skipped, examples, merged, installed };
1154
1227
  };
1155
1228
 
1156
1229
  /**
package/package.json CHANGED
@@ -13,7 +13,6 @@
13
13
  "url": "https://github.com/karmaniverous/smoz/issues"
14
14
  },
15
15
  "dependencies": {
16
- "@karmaniverous/cached-axios": "^0.2.0",
17
16
  "@middy/core": "^6.4.5",
18
17
  "@middy/http-content-negotiation": "^6.4.5",
19
18
  "@middy/http-cors": "^6.4.5",
@@ -24,11 +23,11 @@
24
23
  "@middy/http-response-serializer": "^6.4.5",
25
24
  "aws-lambda": "^1.0.7",
26
25
  "chokidar": "^4.0.3",
27
- "commander": "^14.0.0",
26
+ "commander": "^14.0.1",
28
27
  "http-errors": "^2.0.0",
29
28
  "package-directory": "^8.1.0",
30
29
  "radash": "^12.1.1",
31
- "zod": "^4.1.6"
30
+ "zod": "^4.1.8"
32
31
  },
33
32
  "description": "John Galt Services back end.",
34
33
  "devDependencies": {
@@ -48,8 +47,7 @@
48
47
  "eslint-plugin-simple-import-sort": "^12.1.1",
49
48
  "fs-extra": "^11.3.1",
50
49
  "knip": "^5.63.1",
51
- "lefthook": "^1.12.4",
52
- "orval": "^7.11.2",
50
+ "lefthook": "^1.13.0",
53
51
  "pkg-dir": "^9.0.0",
54
52
  "prettier": "^3.6.2",
55
53
  "release-it": "^19.0.4",
@@ -167,7 +165,6 @@
167
165
  "docs": "typedoc",
168
166
  "domain:create": "serverless create_domain",
169
167
  "domain:delete": "serverless delete_domain",
170
- "generate": "cd services/activecampaign && orval",
171
168
  "knip": "knip",
172
169
  "lint": "eslint .",
173
170
  "lint:fix": "eslint --fix .",
@@ -177,13 +174,12 @@
177
174
  "release:pre": "dotenvx run -f .env.local -- release-it --no-git.requireBranch --github.prerelease --preRelease",
178
175
  "remove": "serverless remove",
179
176
  "smoz": "tsx src/cli/index.ts",
180
- "stan:build": "rimraf stan.dist && rollup --config stan.rollup.config.ts --configPlugin @rollup/plugin-typescript && rimraf stan.dist",
181
177
  "stan:docs": "typedoc --emit none",
182
178
  "test": "vitest run",
183
179
  "typecheck": "tsx src/cli/index.ts register && tsc -p tsconfig.json --noEmit",
184
180
  "templates:typecheck": "tsx scripts/templates-typecheck.ts",
185
- "templates:lint": "eslint --fix -c templates/default/eslint.config.ts \"templates/default/**/*.{ts,tsx,js,jsx}\" && eslint --fix templates/default/eslint.config.ts"
181
+ "templates:lint": "eslint --fix -c templates/default/eslint.config.ts \"templates/default/**/*.{ts,tsx,js,jsx}\" \"templates/default/eslint.config.ts\""
186
182
  },
187
183
  "type": "module",
188
- "version": "0.2.0"
184
+ "version": "0.2.2"
189
185
  }
@@ -1,7 +1,7 @@
1
1
  import * as path from 'node:path';
2
2
 
3
3
  import * as fs from 'fs-extra';
4
- import { packageDirectorySync } from 'pkg-dir';
4
+ import { packageDirectorySync } from 'package-directory';
5
5
  import { createDocument } from 'zod-openapi';
6
6
 
7
7
  import { app } from '@/app/config/app.config';
@@ -5,10 +5,13 @@
5
5
  "version": "0.0.0",
6
6
  "dependencies": {
7
7
  "@middy/core": "^6.4.5",
8
- "zod": "^4.1.6"
8
+ "zod": "^4.1.6",
9
+ "fs-extra": "^11.3.1",
10
+ "package-directory": "^8.1.0"
9
11
  },
10
12
  "devDependencies": {
11
13
  "@serverless/typescript": "^4.18.2",
14
+ "@types/fs-extra": "^11.0.4",
12
15
  "@types/node": "^22",
13
16
  "eslint": "^9.35.0",
14
17
  "eslint-config-prettier": "^10.1.8",
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": ".",
4
+ "outDir": ".tsbuild",
5
+ "paths": {
6
+ "@/*": ["*"]
7
+ },
8
+ "tsBuildInfoFile": ".tsbuild/tsconfig.tsbuildinfo"
9
+ },
10
+ "exclude": [
11
+ ".serverless/**",
12
+ "node_modules/**",
13
+ "app/generated/**",
14
+ "dist/**"
15
+ ],
16
+ "extends": "./tsconfig.base.json",
17
+ "include": ["**/*", "**/*.json"]
18
+ }
@@ -1,13 +1,3 @@
1
1
  {
2
- "extends": "./tsconfig.json",
3
- "compilerOptions": {
4
- "baseUrl": ".",
5
- "paths": {
6
- "@/*": ["./*"],
7
- "@karmaniverous/smoz": [
8
- "../../.stan/dist/index.d.ts",
9
- "../../dist/index.d.ts"
10
- ]
11
- }
12
- }
2
+ "extends": "./tsconfig.json"
13
3
  }
@@ -3,14 +3,24 @@
3
3
  "baseUrl": ".",
4
4
  "outDir": ".tsbuild",
5
5
  "paths": {
6
- "@/*": ["*"]
6
+ "@/*": ["*"],
7
+ "@karmaniverous/smoz": [
8
+ "../../dist/index",
9
+ "../../dist/index.d.ts"
10
+ ]
7
11
  },
12
+ "typeRoots": [
13
+ "./types",
14
+ "../../node_modules/@types"
15
+ ],
8
16
  "tsBuildInfoFile": ".tsbuild/tsconfig.tsbuildinfo"
9
17
  },
10
- "exclude": [".serverless/**", "node_modules/**", "app/generated/**", "dist/**"],
18
+ "exclude": [
19
+ ".serverless/**",
20
+ "node_modules/**",
21
+ "app/generated/**",
22
+ "dist/**"
23
+ ],
11
24
  "extends": "./tsconfig.base.json",
12
- "include": [
13
- "**/*",
14
- "**/*.json"
15
- ]
16
- }
25
+ "include": ["**/*", "**/*.json"]
26
+ }