@karmaniverous/smoz 0.1.8 → 0.2.1
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 +28 -4
- package/dist/cli/index.cjs +282 -122
- package/package.json +5 -8
- package/templates/{minimal → default}/app/config/app.config.ts +50 -50
- package/templates/{minimal → default}/app/config/openapi.ts +45 -45
- package/templates/{full → default}/app/functions/rest/hello/get/handler.ts +18 -18
- package/templates/{full → default}/app/functions/rest/hello/get/lambda.ts +28 -28
- package/templates/{minimal → default}/app/functions/rest/hello/get/openapi.ts +23 -23
- package/templates/{minimal → default}/app/functions/rest/openapi/get/handler.ts +22 -22
- package/templates/{minimal → default}/app/functions/rest/openapi/get/lambda.ts +29 -29
- package/templates/{full → default}/app/functions/rest/openapi/get/openapi.ts +20 -20
- package/templates/{project → default}/eslint.config.ts +6 -15
- package/templates/default/package.json +44 -0
- package/templates/default/serverless.ts +113 -0
- package/templates/default/tsconfig.eslint.json +13 -0
- package/templates/{minimal → default}/types/registers.d.ts +11 -11
- package/templates/.check/eslint.minimal.config.ts +0 -41
- package/templates/.check/eslint.templates.config.ts +0 -60
- package/templates/.check/tsconfig.eslintconfig.json +0 -6
- package/templates/.check/tsconfig.minimal.json +0 -14
- package/templates/.manifests/package.full.json +0 -22
- package/templates/.manifests/package.minimal.json +0 -22
- package/templates/.manifests/package.project.json +0 -24
- package/templates/full/app/config/app.config.ts +0 -50
- package/templates/full/app/config/openapi.ts +0 -44
- package/templates/full/app/functions/rest/hello/get/openapi.ts +0 -19
- package/templates/full/app/functions/rest/openapi/get/handler.ts +0 -20
- package/templates/full/app/functions/rest/openapi/get/lambda.ts +0 -29
- package/templates/full/app/functions/sqs/tick/handler.ts +0 -7
- package/templates/full/app/functions/sqs/tick/lambda.ts +0 -24
- package/templates/full/app/functions/sqs/tick/serverless.ts +0 -8
- package/templates/full/serverless.ts +0 -33
- package/templates/full/tsconfig.json +0 -25
- package/templates/full/types/registers.d.ts +0 -11
- package/templates/minimal/app/functions/rest/hello/get/handler.ts +0 -18
- package/templates/minimal/app/functions/rest/hello/get/lambda.ts +0 -28
- package/templates/minimal/app/functions/rest/openapi/get/openapi.ts +0 -20
- package/templates/minimal/app/generated/openapi.json +0 -8
- package/templates/minimal/serverless.ts +0 -34
- package/templates/minimal/tsconfig.json +0 -25
- /package/templates/{project → default}/.prettierrc.json +0 -0
- /package/templates/{project → default}/.vscode/extensions.json +0 -0
- /package/templates/{project → default}/.vscode/settings.json +0 -0
- /package/templates/{project → default}/README.md +0 -0
- /package/templates/{full → default}/app/generated/openapi.json +0 -0
- /package/templates/{project → default}/gitignore +0 -0
- /package/templates/{project → default}/test/smoke.test.ts +0 -0
- /package/templates/{project → default}/tsconfig.base.json +0 -0
- /package/templates/{project → default}/tsconfig.json +0 -0
- /package/templates/{project → default}/tsdoc.json +0 -0
- /package/templates/{project → default}/typedoc.json +0 -0
- /package/templates/{project → default}/vitest.config.ts +0 -0
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
|
|
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
|
|
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)
|
package/dist/cli/index.cjs
CHANGED
|
@@ -9,6 +9,8 @@ var node_url = require('node:url');
|
|
|
9
9
|
var chokidar = require('chokidar');
|
|
10
10
|
var node_child_process = require('node:child_process');
|
|
11
11
|
var os = require('node:os');
|
|
12
|
+
var node_process = require('node:process');
|
|
13
|
+
var promises = require('node:readline/promises');
|
|
12
14
|
|
|
13
15
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
14
16
|
/**
|
|
@@ -790,17 +792,6 @@ const launchInline = async (root, opts) => {
|
|
|
790
792
|
return { close, restart };
|
|
791
793
|
};
|
|
792
794
|
|
|
793
|
-
/**
|
|
794
|
-
* smoz init
|
|
795
|
-
*
|
|
796
|
-
* Scaffolds a new project from packaged templates.
|
|
797
|
-
* - Copies ./templates/project/ into the target root (shared boilerplate)
|
|
798
|
-
* - Copies ./templates/<template>/ into the target root (default: minimal)
|
|
799
|
-
* - Seeds app/generated/register.*.ts (empty modules) if missing * - 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
795
|
const writeIfAbsent = async (outFile, content) => {
|
|
805
796
|
if (fs.existsSync(outFile))
|
|
806
797
|
return { created: false };
|
|
@@ -821,15 +812,6 @@ const walk = async (dir, out = []) => {
|
|
|
821
812
|
}
|
|
822
813
|
return out;
|
|
823
814
|
};
|
|
824
|
-
const resolveTemplatesBase = () => {
|
|
825
|
-
// Resolve the templates folder from the CLI package install root,
|
|
826
|
-
// not the caller’s project root. This makes --template <name> work
|
|
827
|
-
// consistently whether smoz is run from a consuming app or this repo.
|
|
828
|
-
// Anchor discovery to this module’s directory.
|
|
829
|
-
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))));
|
|
830
|
-
const pkgRoot = packageDirectory.packageDirectorySync({ cwd: here }) ?? process.cwd(); // conservative fallback
|
|
831
|
-
return path.resolve(pkgRoot, 'templates');
|
|
832
|
-
};
|
|
833
815
|
const readJson = async (file) => {
|
|
834
816
|
try {
|
|
835
817
|
const data = await fs.promises.readFile(file, 'utf8');
|
|
@@ -843,6 +825,79 @@ const writeJson = async (file, obj) => {
|
|
|
843
825
|
await fs.promises.mkdir(path.dirname(file), { recursive: true });
|
|
844
826
|
await fs.promises.writeFile(file, JSON.stringify(obj, null, 2), 'utf8');
|
|
845
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
|
+
const dest = path.resolve(dstRoot, rel);
|
|
854
|
+
const data = await fs.promises.readFile(abs, 'utf8');
|
|
855
|
+
if (!fs.existsSync(dest)) {
|
|
856
|
+
const { created: c } = await writeIfAbsent(dest, data);
|
|
857
|
+
if (c)
|
|
858
|
+
created.push(path.posix.normalize(dest));
|
|
859
|
+
else
|
|
860
|
+
skipped.push(path.posix.normalize(dest));
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
// Conflict flow
|
|
864
|
+
let decision = opts.conflict === 'ask' ? 'example' : opts.conflict;
|
|
865
|
+
if (opts.conflict === 'ask' && opts.rl && !sticky) {
|
|
866
|
+
const ans = await askConflict(opts.rl, path.posix.normalize(dest));
|
|
867
|
+
if (ans === 'all-overwrite') {
|
|
868
|
+
sticky = 'overwrite';
|
|
869
|
+
}
|
|
870
|
+
else if (ans === 'all-example') {
|
|
871
|
+
sticky = 'example';
|
|
872
|
+
}
|
|
873
|
+
else if (ans === 'all-skip') {
|
|
874
|
+
sticky = 'skip';
|
|
875
|
+
}
|
|
876
|
+
else {
|
|
877
|
+
decision = ans;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
if (sticky)
|
|
881
|
+
decision = sticky;
|
|
882
|
+
if (decision === 'overwrite') {
|
|
883
|
+
await fs.promises.mkdir(path.dirname(dest), { recursive: true });
|
|
884
|
+
await fs.promises.writeFile(dest, data, 'utf8');
|
|
885
|
+
created.push(path.posix.normalize(dest));
|
|
886
|
+
}
|
|
887
|
+
else if (decision === 'example') {
|
|
888
|
+
const ex = `${dest}.example`;
|
|
889
|
+
const { created: c } = await writeIfAbsent(ex, data);
|
|
890
|
+
if (c)
|
|
891
|
+
examples.push(path.posix.normalize(ex));
|
|
892
|
+
else
|
|
893
|
+
skipped.push(path.posix.normalize(ex));
|
|
894
|
+
}
|
|
895
|
+
else {
|
|
896
|
+
skipped.push(path.posix.normalize(dest));
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
|
|
846
901
|
const detectPm = (root) => {
|
|
847
902
|
if (fs.existsSync(path.join(root, 'pnpm-lock.yaml')))
|
|
848
903
|
return 'pnpm';
|
|
@@ -869,7 +924,6 @@ const runInstall = (root, pm) => {
|
|
|
869
924
|
const known = pm === 'pnpm' || pm === 'yarn' || pm === 'bun' || pm === 'npm';
|
|
870
925
|
if (!known)
|
|
871
926
|
return 'unknown-pm';
|
|
872
|
-
// Spawn with explicit args; avoid tuple inference that widens types.
|
|
873
927
|
const res = node_child_process.spawnSync(pm, ['install'], {
|
|
874
928
|
stdio: 'inherit',
|
|
875
929
|
cwd: root,
|
|
@@ -887,10 +941,10 @@ const runInstall = (root, pm) => {
|
|
|
887
941
|
}
|
|
888
942
|
return 'failed';
|
|
889
943
|
};
|
|
944
|
+
|
|
890
945
|
const mergeAdditive = (target, source) => {
|
|
891
946
|
const merged = [];
|
|
892
947
|
const mergeKey = (key) => {
|
|
893
|
-
// Allow possibly-undefined shapes to satisfy lint (no-unnecessary-condition).
|
|
894
948
|
const src = source[key] ?? {};
|
|
895
949
|
const dst = target[key] ?? {};
|
|
896
950
|
const out = { ...dst };
|
|
@@ -928,51 +982,122 @@ const mergeAdditive = (target, source) => {
|
|
|
928
982
|
target.scripts = scriptsOut;
|
|
929
983
|
return merged;
|
|
930
984
|
};
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
const { created: c } = await writeIfAbsent(dest, data);
|
|
949
|
-
if (c)
|
|
950
|
-
created.push(path.posix.normalize(dest));
|
|
951
|
-
else
|
|
952
|
-
skipped.push(path.posix.normalize(dest));
|
|
985
|
+
/**
|
|
986
|
+
* Ensure a runtime dependency on @karmaniverous/smoz is present in the
|
|
987
|
+
* target manifest, using a caret version derived from the toolkit package.
|
|
988
|
+
* Returns the merge descriptor string when added, else undefined.
|
|
989
|
+
*/
|
|
990
|
+
const ensureToolkitDependency = async (targetPkg, templatesBase) => {
|
|
991
|
+
try {
|
|
992
|
+
const toolkitRoot = path.dirname(templatesBase);
|
|
993
|
+
const toolkitPkg = await readJson(path.join(toolkitRoot, 'package.json'));
|
|
994
|
+
const verRaw = toolkitPkg?.version?.trim();
|
|
995
|
+
const depVersion = verRaw ? `^${verRaw}` : '^0.0.0';
|
|
996
|
+
const deps = targetPkg.dependencies ?? {};
|
|
997
|
+
if (!deps['@karmaniverous/smoz']) {
|
|
998
|
+
targetPkg.dependencies ??=
|
|
999
|
+
{};
|
|
1000
|
+
targetPkg.dependencies['@karmaniverous/smoz'] = depVersion;
|
|
1001
|
+
return `dependencies:@karmaniverous/smoz@${depVersion}`;
|
|
953
1002
|
}
|
|
954
1003
|
}
|
|
1004
|
+
catch {
|
|
1005
|
+
// best-effort; ignore
|
|
1006
|
+
}
|
|
1007
|
+
return undefined;
|
|
1008
|
+
};
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
+ * Resolve the packaged templates root from the CLI install location,
|
|
1012
|
+
* not the caller's project root. This makes -t <name> work both from
|
|
1013
|
+
* a consuming app and from this repository.
|
|
1014
|
+
+ */
|
|
1015
|
+
const resolveTemplatesBase = () => {
|
|
1016
|
+
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))));
|
|
1017
|
+
const pkgRoot = packageDirectory.packageDirectorySync({ cwd: here }) ?? process.cwd();
|
|
1018
|
+
return path.resolve(pkgRoot, 'templates');
|
|
1019
|
+
};
|
|
1020
|
+
const toPosix = (p) =>
|
|
1021
|
+
// local helper for path presentation (not used for FS operations)
|
|
1022
|
+
p.replace(/\\/g, '/');
|
|
1023
|
+
|
|
1024
|
+
const seedRegisterPlaceholders = async (root) => {
|
|
1025
|
+
const genDir = path.join(root, 'app', 'generated');
|
|
1026
|
+
const seeds = [
|
|
1027
|
+
{
|
|
1028
|
+
path: path.join(genDir, 'register.functions.ts'),
|
|
1029
|
+
content: '/* AUTO-GENERATED placeholder; will be rewritten by `smoz register` */\nexport {};\n',
|
|
1030
|
+
},
|
|
1031
|
+
{
|
|
1032
|
+
path: path.join(genDir, 'register.openapi.ts'),
|
|
1033
|
+
content: '/* AUTO-GENERATED placeholder; will be rewritten by `smoz register` */\nexport {};\n',
|
|
1034
|
+
},
|
|
1035
|
+
{
|
|
1036
|
+
path: path.join(genDir, 'register.serverless.ts'),
|
|
1037
|
+
content: '/* AUTO-GENERATED placeholder; will be rewritten by `smoz register` */\nexport {};\n',
|
|
1038
|
+
},
|
|
1039
|
+
];
|
|
1040
|
+
const created = [];
|
|
1041
|
+
const skipped = [];
|
|
1042
|
+
for (const s of seeds) {
|
|
1043
|
+
const { created: c } = await writeIfAbsent(s.path, s.content);
|
|
1044
|
+
(c ? created : skipped).push(path.posix.normalize(s.path));
|
|
1045
|
+
}
|
|
1046
|
+
return { created, skipped };
|
|
955
1047
|
};
|
|
956
|
-
|
|
1048
|
+
|
|
1049
|
+
const toPosixSep = (p) => p.split(path.sep).join('/');
|
|
1050
|
+
const runInit = async (root, template = 'default', opts) => {
|
|
957
1051
|
const created = [];
|
|
958
1052
|
const skipped = [];
|
|
959
1053
|
const examples = [];
|
|
960
1054
|
const merged = [];
|
|
1055
|
+
const optAll = opts ?? {};
|
|
961
1056
|
const templatesBase = resolveTemplatesBase();
|
|
962
|
-
|
|
1057
|
+
// Resolve template source: named template or filesystem path
|
|
1058
|
+
const templateIsPath = fs.existsSync(template) && (await fs.promises.stat(template)).isDirectory();
|
|
1059
|
+
const srcBase = templateIsPath
|
|
1060
|
+
? path.resolve(template)
|
|
1061
|
+
: path.resolve(templatesBase, template);
|
|
963
1062
|
const projectBase = path.resolve(templatesBase, 'project');
|
|
964
1063
|
if (!fs.existsSync(srcBase)) {
|
|
965
|
-
throw new Error(`Template "${template}" not found
|
|
1064
|
+
throw new Error(`Template "${template}" not found (path or name). Tried: ${toPosix(srcBase)}.`);
|
|
966
1065
|
}
|
|
967
|
-
// 1) Copy shared
|
|
1066
|
+
// 1) Copy shared project boilerplate
|
|
968
1067
|
if (fs.existsSync(projectBase)) {
|
|
969
|
-
|
|
1068
|
+
const rl = optAll.yes === true
|
|
1069
|
+
? undefined
|
|
1070
|
+
: promises.createInterface({ input: node_process.stdin, output: node_process.stdout, terminal: true });
|
|
1071
|
+
let policy;
|
|
1072
|
+
const c = optAll.conflict;
|
|
1073
|
+
if (c === 'overwrite' || c === 'example' || c === 'skip' || c === 'ask')
|
|
1074
|
+
policy = c;
|
|
1075
|
+
else
|
|
1076
|
+
policy = optAll.yes ? 'example' : 'ask';
|
|
1077
|
+
const copyOpts = rl
|
|
1078
|
+
? { conflict: policy, rl }
|
|
1079
|
+
: { conflict: policy };
|
|
1080
|
+
await copyDirWithConflicts(projectBase, root, created, skipped, examples, copyOpts);
|
|
1081
|
+
rl?.close();
|
|
970
1082
|
}
|
|
971
1083
|
// 2) Copy selected template
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1084
|
+
{
|
|
1085
|
+
const rl = optAll.yes === true
|
|
1086
|
+
? undefined
|
|
1087
|
+
: promises.createInterface({ input: node_process.stdin, output: node_process.stdout, terminal: true });
|
|
1088
|
+
let policy;
|
|
1089
|
+
const c = optAll.conflict;
|
|
1090
|
+
if (c === 'overwrite' || c === 'example' || c === 'skip' || c === 'ask')
|
|
1091
|
+
policy = c;
|
|
1092
|
+
else
|
|
1093
|
+
policy = optAll.yes ? 'example' : 'ask';
|
|
1094
|
+
const copyOpts = rl
|
|
1095
|
+
? { conflict: policy, rl }
|
|
1096
|
+
: { conflict: policy };
|
|
1097
|
+
await copyDirWithConflicts(srcBase, root, created, skipped, examples, copyOpts);
|
|
1098
|
+
rl?.close();
|
|
1099
|
+
}
|
|
1100
|
+
// 2.5) Convert template 'gitignore' into a real '.gitignore'
|
|
976
1101
|
try {
|
|
977
1102
|
const giSrc = path.join(root, 'gitignore');
|
|
978
1103
|
const giDot = path.join(root, '.gitignore');
|
|
@@ -982,8 +1107,6 @@ const runInit = async (root, template = 'minimal', opts) => {
|
|
|
982
1107
|
created.push(path.posix.normalize(giDot));
|
|
983
1108
|
}
|
|
984
1109
|
else {
|
|
985
|
-
// Both exist: preserve the template as an example (if not already present),
|
|
986
|
-
// then remove the extra 'gitignore' to avoid clutter.
|
|
987
1110
|
const example = path.join(root, 'gitignore.example');
|
|
988
1111
|
if (!fs.existsSync(example)) {
|
|
989
1112
|
const data = await fs.promises.readFile(giSrc, 'utf8');
|
|
@@ -995,76 +1118,63 @@ const runInit = async (root, template = 'minimal', opts) => {
|
|
|
995
1118
|
}
|
|
996
1119
|
}
|
|
997
1120
|
catch {
|
|
998
|
-
// best-effort
|
|
1121
|
+
// best-effort
|
|
999
1122
|
}
|
|
1000
|
-
// Seed app/generated/register
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
content: `/* AUTO-GENERATED placeholder; will be rewritten by \`smoz register\` */\nexport {};\n`,
|
|
1006
|
-
},
|
|
1007
|
-
{
|
|
1008
|
-
path: path.join(genDir, 'register.openapi.ts'),
|
|
1009
|
-
content: `/* AUTO-GENERATED placeholder; will be rewritten by \`smoz register\` */\nexport {};\n`,
|
|
1010
|
-
},
|
|
1011
|
-
{
|
|
1012
|
-
path: path.join(genDir, 'register.serverless.ts'),
|
|
1013
|
-
content: `/* AUTO-GENERATED placeholder; will be rewritten by \`smoz register\` */\nexport {};\n`,
|
|
1014
|
-
},
|
|
1015
|
-
];
|
|
1016
|
-
for (const s of seeds) {
|
|
1017
|
-
const { created: c } = await writeIfAbsent(s.path, s.content);
|
|
1018
|
-
if (c)
|
|
1019
|
-
created.push(path.posix.normalize(s.path));
|
|
1020
|
-
else
|
|
1021
|
-
skipped.push(path.posix.normalize(s.path));
|
|
1123
|
+
// Seed app/generated/register.* placeholders
|
|
1124
|
+
{
|
|
1125
|
+
const res = await seedRegisterPlaceholders(root);
|
|
1126
|
+
created.push(...res.created);
|
|
1127
|
+
skipped.push(...res.skipped);
|
|
1022
1128
|
}
|
|
1023
|
-
// 3) package.json presence (
|
|
1129
|
+
// 3) package.json presence (create when missing)
|
|
1024
1130
|
const pkgPath = path.join(root, 'package.json');
|
|
1025
1131
|
let pkg = await readJson(pkgPath);
|
|
1026
1132
|
if (!pkg) {
|
|
1027
|
-
const
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
const dryRunCreate = !!opts.dryRun;
|
|
1039
|
-
if (!dryRunCreate)
|
|
1040
|
-
await writeJson(pkgPath, pkg);
|
|
1041
|
-
created.push(path.posix.normalize(pkgPath));
|
|
1042
|
-
}
|
|
1043
|
-
else {
|
|
1044
|
-
throw new Error('No package.json found. Run "npm init -y" (or re-run with --init) and then "smoz init" again.');
|
|
1045
|
-
}
|
|
1133
|
+
const name = toPosixSep(root).split('/').pop() ?? 'smoz-app';
|
|
1134
|
+
pkg = {
|
|
1135
|
+
name,
|
|
1136
|
+
private: true,
|
|
1137
|
+
type: 'module',
|
|
1138
|
+
version: '0.0.0',
|
|
1139
|
+
scripts: {},
|
|
1140
|
+
};
|
|
1141
|
+
if (!optAll.dryRun)
|
|
1142
|
+
await writeJson(pkgPath, pkg);
|
|
1143
|
+
created.push(path.posix.normalize(pkgPath));
|
|
1046
1144
|
}
|
|
1047
|
-
// 4) Merge manifest (
|
|
1048
|
-
const
|
|
1049
|
-
const manifest =
|
|
1145
|
+
// 4) Merge manifest additively (prefer template's embedded package.json)
|
|
1146
|
+
const templatePkgPath = path.resolve(srcBase, 'package.json');
|
|
1147
|
+
const manifest = fs.existsSync(templatePkgPath)
|
|
1148
|
+
? await readJson(templatePkgPath)
|
|
1149
|
+
: await readJson(path.resolve(templatesBase, '.manifests', `package.${template}.json`));
|
|
1150
|
+
let pkgChanged = false;
|
|
1050
1151
|
if (manifest) {
|
|
1051
|
-
const before = JSON.stringify(pkg);
|
|
1052
1152
|
const added = mergeAdditive(pkg, manifest);
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1153
|
+
if (added.length > 0) {
|
|
1154
|
+
pkgChanged = true;
|
|
1155
|
+
merged.push(...added);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
// 4.5) Ensure runtime dependency on @karmaniverous/smoz is present
|
|
1159
|
+
{
|
|
1160
|
+
const injected = await ensureToolkitDependency(pkg, templatesBase);
|
|
1161
|
+
if (injected) {
|
|
1162
|
+
merged.push(injected);
|
|
1163
|
+
pkgChanged = true;
|
|
1057
1164
|
}
|
|
1058
1165
|
}
|
|
1166
|
+
if (!optAll.dryRun && pkgChanged)
|
|
1167
|
+
await writeJson(pkgPath, pkg);
|
|
1059
1168
|
// 5) Optional install
|
|
1060
1169
|
let installed = 'skipped';
|
|
1061
|
-
|
|
1062
|
-
//
|
|
1063
|
-
|
|
1170
|
+
// Policy:
|
|
1171
|
+
// -y implies auto install unless --no-install
|
|
1172
|
+
const installOpt = optAll.install ?? false;
|
|
1173
|
+
const impliedAuto = optAll.yes === true && optAll.noInstall !== true && installOpt !== false;
|
|
1064
1174
|
const hasInstallString = typeof installOpt === 'string' && installOpt.trim() !== '';
|
|
1065
1175
|
const pm = hasInstallString
|
|
1066
1176
|
? installOpt
|
|
1067
|
-
: installOpt === true
|
|
1177
|
+
: installOpt === true || impliedAuto
|
|
1068
1178
|
? detectPm(root)
|
|
1069
1179
|
: undefined;
|
|
1070
1180
|
installed = pm ? runInstall(root, pm) : installed;
|
|
@@ -1089,6 +1199,22 @@ const readPkg = (root) => {
|
|
|
1089
1199
|
return {};
|
|
1090
1200
|
}
|
|
1091
1201
|
};
|
|
1202
|
+
const readSmozConfig = (root) => {
|
|
1203
|
+
try {
|
|
1204
|
+
const p = path.join(root, 'smoz.config.json');
|
|
1205
|
+
if (!fs.existsSync(p))
|
|
1206
|
+
return {};
|
|
1207
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
1208
|
+
const parsed = JSON.parse(raw);
|
|
1209
|
+
if (parsed && typeof parsed === 'object') {
|
|
1210
|
+
return parsed;
|
|
1211
|
+
}
|
|
1212
|
+
return {};
|
|
1213
|
+
}
|
|
1214
|
+
catch {
|
|
1215
|
+
return {};
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
1092
1218
|
const detectPackageManager = () => {
|
|
1093
1219
|
const ua = process.env.npm_config_user_agent ?? '';
|
|
1094
1220
|
if (ua.includes('pnpm'))
|
|
@@ -1134,7 +1260,8 @@ const main = () => {
|
|
|
1134
1260
|
program
|
|
1135
1261
|
.name('smoz')
|
|
1136
1262
|
.description('SMOZ CLI')
|
|
1137
|
-
|
|
1263
|
+
// Add -v alias for version in addition to default --version behavior
|
|
1264
|
+
.version(pkg.version ?? '0.0.0', '-v, --version', 'output the version');
|
|
1138
1265
|
program
|
|
1139
1266
|
.command('add')
|
|
1140
1267
|
.argument('<spec>', 'Add function: HTTP <eventType>/<segments...>/<method> or non-HTTP <eventType>/<segments...>')
|
|
@@ -1155,16 +1282,46 @@ const main = () => {
|
|
|
1155
1282
|
});
|
|
1156
1283
|
program
|
|
1157
1284
|
.command('init')
|
|
1158
|
-
.description('Scaffold a new SMOZ app from packaged templates (default:
|
|
1159
|
-
.option('--template <
|
|
1160
|
-
.option('--init', 'Create a minimal package.json if missing')
|
|
1285
|
+
.description('Scaffold a new SMOZ app from packaged templates (default: default)')
|
|
1286
|
+
.option('-t, --template <nameOrPath>', 'Template name or directory path', 'default')
|
|
1161
1287
|
.option('-i, --install [pm]', 'Install dependencies (optionally specify pm: npm|pnpm|yarn|bun)')
|
|
1162
|
-
.option('--
|
|
1288
|
+
.option('--no-install', 'Skip dependency installation (overrides -y)', false)
|
|
1289
|
+
.option('-y, --yes', 'Skip prompts (non-interactive)', false)
|
|
1163
1290
|
.option('--dry-run', 'Show planned actions without writing', false)
|
|
1164
1291
|
.action(async (opts) => {
|
|
1165
1292
|
try {
|
|
1166
|
-
const
|
|
1167
|
-
|
|
1293
|
+
const cfg = readSmozConfig(root).cliDefaults?.init ?? {};
|
|
1294
|
+
// Resolve template: CLI > config > default
|
|
1295
|
+
const tpl = typeof opts.template === 'string'
|
|
1296
|
+
? opts.template
|
|
1297
|
+
: typeof cfg.template === 'string'
|
|
1298
|
+
? cfg.template
|
|
1299
|
+
: 'default';
|
|
1300
|
+
// Resolve install behavior: CLI > --no-install > config
|
|
1301
|
+
let install = opts.install;
|
|
1302
|
+
if (opts.noInstall === true) {
|
|
1303
|
+
install = false;
|
|
1304
|
+
}
|
|
1305
|
+
else if (install === undefined) {
|
|
1306
|
+
const d = cfg.install;
|
|
1307
|
+
install =
|
|
1308
|
+
d === 'auto'
|
|
1309
|
+
? true
|
|
1310
|
+
: d === 'none'
|
|
1311
|
+
? false
|
|
1312
|
+
: typeof d === 'string'
|
|
1313
|
+
? d
|
|
1314
|
+
: undefined;
|
|
1315
|
+
}
|
|
1316
|
+
const conflict = typeof opts.conflict === 'string' ? opts.conflict : cfg.onConflict;
|
|
1317
|
+
const { created, skipped, examples, merged, installed } = await runInit(root, tpl, {
|
|
1318
|
+
// Include only when defined to satisfy exactOptionalPropertyTypes
|
|
1319
|
+
...(install !== undefined ? { install } : {}),
|
|
1320
|
+
...(typeof conflict === 'string' ? { conflict } : {}),
|
|
1321
|
+
yes: opts.yes === true,
|
|
1322
|
+
noInstall: opts.noInstall === true,
|
|
1323
|
+
dryRun: opts.dryRun === true,
|
|
1324
|
+
});
|
|
1168
1325
|
console.log([
|
|
1169
1326
|
created.length
|
|
1170
1327
|
? `Created:\n - ${created.join('\n - ')}`
|
|
@@ -1220,14 +1377,17 @@ const main = () => {
|
|
|
1220
1377
|
.option('-v, --verbose', 'Verbose logging', false)
|
|
1221
1378
|
.action(async (opts) => {
|
|
1222
1379
|
try {
|
|
1380
|
+
const cfg = readSmozConfig(root).cliDefaults?.dev ?? {};
|
|
1381
|
+
// Resolve local mode default from config if not provided
|
|
1382
|
+
const localResolved = typeof opts.local === 'string'
|
|
1383
|
+
? opts.local
|
|
1384
|
+
: opts.local === false
|
|
1385
|
+
? false
|
|
1386
|
+
: (cfg.local ?? 'inline');
|
|
1223
1387
|
await runDev(root, {
|
|
1224
1388
|
register: opts.register !== false,
|
|
1225
1389
|
openapi: opts.openapi !== false,
|
|
1226
|
-
local:
|
|
1227
|
-
? opts.local
|
|
1228
|
-
: opts.local === false
|
|
1229
|
-
? false
|
|
1230
|
-
: 'inline',
|
|
1390
|
+
local: localResolved,
|
|
1231
1391
|
...(typeof opts.stage === 'string' ? { stage: opts.stage } : {}),
|
|
1232
1392
|
port: opts.port ?? 0,
|
|
1233
1393
|
verbose: !!opts.verbose,
|
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.
|
|
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.
|
|
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.
|
|
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 .",
|
|
@@ -182,8 +179,8 @@
|
|
|
182
179
|
"test": "vitest run",
|
|
183
180
|
"typecheck": "tsx src/cli/index.ts register && tsc -p tsconfig.json --noEmit",
|
|
184
181
|
"templates:typecheck": "tsx scripts/templates-typecheck.ts",
|
|
185
|
-
"templates:lint": "eslint --fix -c templates
|
|
182
|
+
"templates:lint": "eslint --fix -c templates/default/eslint.config.ts \"templates/default/**/*.{ts,tsx,js,jsx}\" && eslint --fix templates/default/eslint.config.ts"
|
|
186
183
|
},
|
|
187
184
|
"type": "module",
|
|
188
|
-
"version": "0.1
|
|
185
|
+
"version": "0.2.1"
|
|
189
186
|
}
|