db-model-router 1.0.2 → 1.0.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.
- package/README.md +317 -202
- package/docs/SKILL.md +250 -33
- package/docs/adapters/cockroachdb.md +1 -1
- package/docs/adapters/dynamodb.md +1 -1
- package/docs/adapters/mongodb.md +1 -1
- package/docs/adapters/mssql.md +1 -1
- package/docs/adapters/oracle.md +1 -1
- package/docs/adapters/postgres.md +1 -1
- package/docs/adapters/redis.md +1 -1
- package/docs/adapters/sqlite3.md +1 -1
- package/package.json +12 -6
- package/src/cli/commands/diff.js +114 -0
- package/src/cli/commands/doctor.js +181 -0
- package/src/cli/commands/generate-llm-docs.js +418 -0
- package/src/cli/commands/generate.js +240 -0
- package/src/cli/commands/help.js +180 -0
- package/src/cli/commands/init.js +181 -0
- package/src/cli/commands/inspect.js +222 -0
- package/src/cli/diff-engine.js +198 -0
- package/src/cli/flags.js +112 -0
- package/src/cli/generate-model.js +5 -4
- package/src/cli/generate-route.js +255 -14
- package/src/cli/init/dependencies.js +92 -0
- package/src/cli/init/generators.js +1791 -0
- package/src/cli/init/prompt.js +191 -0
- package/src/cli/init.js +404 -0
- package/src/cli/main.js +175 -0
- package/src/commons/model.js +5 -6
- package/src/commons/route.js +24 -0
- package/src/index.js +2 -0
- package/src/schema/schema-parser.js +78 -0
- package/src/schema/schema-printer.js +77 -0
- package/src/schema/schema-to-meta.js +78 -0
- package/src/schema/schema-validator.js +255 -0
- package/src/serve.js +5 -3
- package/docs/README.md +0 -208
- package/src/cli/generate-app.js +0 -359
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { parseSchema } = require("../../schema/schema-parser");
|
|
6
|
+
const { schemaToModelMeta } = require("../../schema/schema-to-meta");
|
|
7
|
+
const { generateModelFile } = require("../generate-model");
|
|
8
|
+
const {
|
|
9
|
+
generateRouteFile,
|
|
10
|
+
generateChildRouteFile,
|
|
11
|
+
generateRoutesIndexFile,
|
|
12
|
+
generateTestFile,
|
|
13
|
+
generateChildTestFile,
|
|
14
|
+
} = require("../generate-route");
|
|
15
|
+
const { generateOpenAPISpec } = require("../generate-openapi");
|
|
16
|
+
const { generateLlmsTxt, generateLlmMd } = require("./generate-llm-docs");
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generate command handler for the unified CLI.
|
|
20
|
+
*
|
|
21
|
+
* Reads a schema file, converts to ModelMeta[], and generates
|
|
22
|
+
* models, routes, tests, and OpenAPI spec files.
|
|
23
|
+
*
|
|
24
|
+
* Supported flags:
|
|
25
|
+
* --from Path to schema file (default: dbmr.schema.json)
|
|
26
|
+
* --models Generate only model files
|
|
27
|
+
* --routes Generate only route files (including child routes and index)
|
|
28
|
+
* --openapi Generate only OpenAPI spec
|
|
29
|
+
* --tests Generate only test files
|
|
30
|
+
* --dry-run Report planned files without writing
|
|
31
|
+
* --json Output JSON result via ctx
|
|
32
|
+
*
|
|
33
|
+
* When no artifact flags are provided, all artifact types are generated.
|
|
34
|
+
*
|
|
35
|
+
* @param {object} args - Parsed key-value args
|
|
36
|
+
* @param {object} flags - Universal flags: { yes, json, dryRun, noInstall, help }
|
|
37
|
+
* @param {import('../flags').OutputContext} ctx - Output context
|
|
38
|
+
*/
|
|
39
|
+
async function generate(args, flags, ctx) {
|
|
40
|
+
const schemaPath = path.resolve(args.from || "dbmr.schema.json");
|
|
41
|
+
|
|
42
|
+
if (!fs.existsSync(schemaPath)) {
|
|
43
|
+
const msg = `Schema file not found: ${args.from || "dbmr.schema.json"}`;
|
|
44
|
+
if (flags.json) {
|
|
45
|
+
ctx.result({ error: true, code: "SCHEMA_NOT_FOUND", message: msg });
|
|
46
|
+
} else {
|
|
47
|
+
ctx.log(`Error: ${msg}`);
|
|
48
|
+
}
|
|
49
|
+
process.exitCode = 1;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let schema;
|
|
54
|
+
try {
|
|
55
|
+
const raw = fs.readFileSync(schemaPath, "utf8");
|
|
56
|
+
schema = parseSchema(raw);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
const msg = `Schema parse error: ${err.message}`;
|
|
59
|
+
if (flags.json) {
|
|
60
|
+
ctx.result({
|
|
61
|
+
error: true,
|
|
62
|
+
code: "SCHEMA_VALIDATION",
|
|
63
|
+
message: msg,
|
|
64
|
+
errors: err.errors || [],
|
|
65
|
+
});
|
|
66
|
+
} else {
|
|
67
|
+
ctx.log(`Error: ${msg}`);
|
|
68
|
+
}
|
|
69
|
+
process.exitCode = 1;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const meta = schemaToModelMeta(schema);
|
|
74
|
+
const relationships = schema.relationships || [];
|
|
75
|
+
const tableNames = meta.map((m) => m.table).sort();
|
|
76
|
+
|
|
77
|
+
// Determine which artifact types to generate
|
|
78
|
+
const hasArtifactFlag =
|
|
79
|
+
args.models === true ||
|
|
80
|
+
args.routes === true ||
|
|
81
|
+
args.openapi === true ||
|
|
82
|
+
args.tests === true ||
|
|
83
|
+
args["llm-docs"] === true;
|
|
84
|
+
|
|
85
|
+
const genModels = !hasArtifactFlag || args.models === true;
|
|
86
|
+
const genRoutes = !hasArtifactFlag || args.routes === true;
|
|
87
|
+
const genOpenapi = !hasArtifactFlag || args.openapi === true;
|
|
88
|
+
const genTests = !hasArtifactFlag || args.tests === true;
|
|
89
|
+
const genLlmDocs = !hasArtifactFlag || args["llm-docs"] === true;
|
|
90
|
+
|
|
91
|
+
const modelsRelPath = "../models";
|
|
92
|
+
const baseDir = process.cwd();
|
|
93
|
+
|
|
94
|
+
// Collect all planned files: { relPath, content }
|
|
95
|
+
const planned = [];
|
|
96
|
+
|
|
97
|
+
// --- Model files ---
|
|
98
|
+
if (genModels) {
|
|
99
|
+
for (const m of meta) {
|
|
100
|
+
planned.push({
|
|
101
|
+
relPath: `models/${m.table}.js`,
|
|
102
|
+
content: generateModelFile(m),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// --- Route files ---
|
|
108
|
+
if (genRoutes) {
|
|
109
|
+
// One route per table
|
|
110
|
+
for (const m of meta) {
|
|
111
|
+
planned.push({
|
|
112
|
+
relPath: `routes/${m.table}.js`,
|
|
113
|
+
content: generateRouteFile(m.table, modelsRelPath),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Child route files (one per relationship)
|
|
118
|
+
for (const rel of relationships) {
|
|
119
|
+
planned.push({
|
|
120
|
+
relPath: `routes/${rel.child}_child_of_${rel.parent}.js`,
|
|
121
|
+
content: generateChildRouteFile(
|
|
122
|
+
rel.child,
|
|
123
|
+
rel.parent,
|
|
124
|
+
rel.foreignKey,
|
|
125
|
+
modelsRelPath,
|
|
126
|
+
),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Routes index file
|
|
131
|
+
planned.push({
|
|
132
|
+
relPath: "routes/index.js",
|
|
133
|
+
content: generateRoutesIndexFile(tableNames, relationships),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// --- OpenAPI spec ---
|
|
138
|
+
if (genOpenapi) {
|
|
139
|
+
planned.push({
|
|
140
|
+
relPath: "openapi.json",
|
|
141
|
+
content: JSON.stringify(generateOpenAPISpec(meta), null, 2) + "\n",
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// --- Test files ---
|
|
146
|
+
if (genTests) {
|
|
147
|
+
for (const m of meta) {
|
|
148
|
+
planned.push({
|
|
149
|
+
relPath: `test/${m.table}.test.js`,
|
|
150
|
+
content: generateTestFile(m.table, m.primary_key),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Child test files (one per relationship)
|
|
155
|
+
for (const rel of relationships) {
|
|
156
|
+
const childMeta = meta.find((m) => m.table === rel.child);
|
|
157
|
+
const pk = childMeta ? childMeta.primary_key : "id";
|
|
158
|
+
planned.push({
|
|
159
|
+
relPath: `test/${rel.child}_child_of_${rel.parent}.test.js`,
|
|
160
|
+
content: generateChildTestFile(
|
|
161
|
+
rel.child,
|
|
162
|
+
rel.parent,
|
|
163
|
+
rel.foreignKey,
|
|
164
|
+
pk,
|
|
165
|
+
),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// --- LLM docs ---
|
|
171
|
+
if (genLlmDocs) {
|
|
172
|
+
planned.push({
|
|
173
|
+
relPath: "llms.txt",
|
|
174
|
+
content: generateLlmsTxt(),
|
|
175
|
+
});
|
|
176
|
+
planned.push({
|
|
177
|
+
relPath: "docs/llm.md",
|
|
178
|
+
content: generateLlmMd(),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// --- Process planned files ---
|
|
183
|
+
const results = [];
|
|
184
|
+
|
|
185
|
+
for (const { relPath, content } of planned) {
|
|
186
|
+
const fullPath = path.join(baseDir, relPath);
|
|
187
|
+
|
|
188
|
+
if (flags.dryRun) {
|
|
189
|
+
results.push({ path: relPath, status: "planned" });
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Check if file exists and content matches (skip-unchanged)
|
|
194
|
+
if (fs.existsSync(fullPath)) {
|
|
195
|
+
const existing = fs.readFileSync(fullPath, "utf8");
|
|
196
|
+
if (existing === content) {
|
|
197
|
+
results.push({ path: relPath, status: "unchanged" });
|
|
198
|
+
ctx.log(` unchanged ${relPath}`);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
// File exists but content differs — overwrite
|
|
202
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
203
|
+
fs.writeFileSync(fullPath, content, "utf8");
|
|
204
|
+
results.push({ path: relPath, status: "overwritten" });
|
|
205
|
+
ctx.log(` overwritten ${relPath}`);
|
|
206
|
+
} else {
|
|
207
|
+
// File does not exist — create
|
|
208
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
209
|
+
fs.writeFileSync(fullPath, content, "utf8");
|
|
210
|
+
results.push({ path: relPath, status: "created" });
|
|
211
|
+
ctx.log(` created ${relPath}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// --- Output ---
|
|
216
|
+
if (flags.dryRun) {
|
|
217
|
+
if (flags.json) {
|
|
218
|
+
ctx.result({ files: results });
|
|
219
|
+
} else {
|
|
220
|
+
ctx.log("Dry run — the following files would be generated:");
|
|
221
|
+
for (const r of results) {
|
|
222
|
+
ctx.log(` ${r.path}`);
|
|
223
|
+
}
|
|
224
|
+
ctx.log(`\n${results.length} file(s) planned.`);
|
|
225
|
+
}
|
|
226
|
+
} else if (flags.json) {
|
|
227
|
+
ctx.result({ files: results });
|
|
228
|
+
} else {
|
|
229
|
+
const created = results.filter((r) => r.status === "created").length;
|
|
230
|
+
const overwritten = results.filter(
|
|
231
|
+
(r) => r.status === "overwritten",
|
|
232
|
+
).length;
|
|
233
|
+
const unchanged = results.filter((r) => r.status === "unchanged").length;
|
|
234
|
+
ctx.log(
|
|
235
|
+
`\nDone. ${created} created, ${overwritten} overwritten, ${unchanged} unchanged.`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
module.exports = generate;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Per-command detailed help text.
|
|
5
|
+
* Each key matches a subcommand name from main.js.
|
|
6
|
+
*/
|
|
7
|
+
const COMMAND_HELP = {
|
|
8
|
+
init: `Usage: db-model-router init [options]
|
|
9
|
+
|
|
10
|
+
Scaffold a new project from a schema file or interactively.
|
|
11
|
+
Creates app.js, .env, commons/, route/, middleware/, and migrations/.
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
--from <path> Read adapter, framework, and options from a schema file
|
|
15
|
+
--framework <name> Express framework: express, ultimate-express
|
|
16
|
+
--database <name> Database adapter: mysql, mariadb, postgres, sqlite3, mongodb,
|
|
17
|
+
mssql, cockroachdb, oracle, redis, dynamodb
|
|
18
|
+
--db <name> Alias for --database
|
|
19
|
+
--session <type> Session store: memory, redis, database
|
|
20
|
+
--output <dir> Directory for backend source files (relative to cwd).
|
|
21
|
+
package.json and app.js stay in root; commons/, route/,
|
|
22
|
+
middleware/, and migrations/ go inside this folder.
|
|
23
|
+
--rateLimiting Enable rate limiting (default: yes)
|
|
24
|
+
--helmet Enable Helmet security headers (default: yes)
|
|
25
|
+
--logger Enable Winston + Loki logger for Grafana (default: yes)
|
|
26
|
+
--yes Accept all defaults without prompting
|
|
27
|
+
--json Output machine-readable JSON
|
|
28
|
+
--dry-run Preview planned files without writing
|
|
29
|
+
--no-install Skip npm install after scaffolding
|
|
30
|
+
--help Show this help message
|
|
31
|
+
|
|
32
|
+
Generated files:
|
|
33
|
+
app.js Express app entry point
|
|
34
|
+
.env / .env.example Environment configuration
|
|
35
|
+
.gitignore Git ignore rules
|
|
36
|
+
<output>/commons/db.js Database init, connect, and global.db
|
|
37
|
+
<output>/commons/session.js Session configuration
|
|
38
|
+
<output>/commons/migrate.js Migration runner (also runs as script)
|
|
39
|
+
<output>/commons/add_migration.js Migration creation helper (also runs as script)
|
|
40
|
+
<output>/commons/security.js Helmet, rate limiting, custom headers
|
|
41
|
+
<output>/middleware/logger.js Winston + Loki request logger
|
|
42
|
+
<output>/route/index.js Central route mounting
|
|
43
|
+
<output>/route/health.js GET /health endpoint
|
|
44
|
+
<output>/migrations/ Initial migration files
|
|
45
|
+
|
|
46
|
+
Examples:
|
|
47
|
+
db-model-router init --from dbmr.schema.json --yes --no-install
|
|
48
|
+
db-model-router init --framework express --database postgres --output backend --yes
|
|
49
|
+
db-model-router init --database mysql --session redis --helmet --rateLimiting
|
|
50
|
+
db-model-router init --dry-run`,
|
|
51
|
+
|
|
52
|
+
inspect: `Usage: db-model-router inspect [options]
|
|
53
|
+
|
|
54
|
+
Introspect a live database and produce a dbmr.schema.json file.
|
|
55
|
+
Connects to the database, reads table structures, and outputs a schema.
|
|
56
|
+
|
|
57
|
+
Options:
|
|
58
|
+
--type <adapter> Database adapter (required): mysql, postgres, sqlite3,
|
|
59
|
+
mssql, oracle, cockroachdb
|
|
60
|
+
--env <path> Path to .env file for connection parameters
|
|
61
|
+
--out <path> Output file path (default: dbmr.schema.json)
|
|
62
|
+
--tables <list> Comma-separated list of tables to include (omit for all)
|
|
63
|
+
--yes Accept all defaults without prompting
|
|
64
|
+
--json Output schema as JSON to stdout (no file write)
|
|
65
|
+
--dry-run Output schema to stdout without writing file
|
|
66
|
+
--help Show this help message
|
|
67
|
+
|
|
68
|
+
Examples:
|
|
69
|
+
db-model-router inspect --type postgres --env .env
|
|
70
|
+
db-model-router inspect --type sqlite3 --out schema.json --tables users,posts
|
|
71
|
+
db-model-router inspect --type mysql --json`,
|
|
72
|
+
|
|
73
|
+
generate: `Usage: db-model-router generate [options]
|
|
74
|
+
|
|
75
|
+
Generate models, routes, tests, OpenAPI spec, and LLM docs from a schema file.
|
|
76
|
+
When no artifact flags are provided, all artifact types are generated.
|
|
77
|
+
|
|
78
|
+
Options:
|
|
79
|
+
--from <path> Path to schema file (default: dbmr.schema.json)
|
|
80
|
+
--models Generate only model files
|
|
81
|
+
--routes Generate only route files (including child routes and index)
|
|
82
|
+
--openapi Generate only OpenAPI spec
|
|
83
|
+
--tests Generate only test files
|
|
84
|
+
--llm-docs Generate only LLM documentation (llms.txt + docs/llm.md)
|
|
85
|
+
--yes Accept all defaults without prompting
|
|
86
|
+
--json Output machine-readable JSON
|
|
87
|
+
--dry-run Report planned files without writing
|
|
88
|
+
--help Show this help message
|
|
89
|
+
|
|
90
|
+
Generated files:
|
|
91
|
+
models/<table>.js Model with CRUD operations
|
|
92
|
+
routes/<table>.js Express route handlers
|
|
93
|
+
routes/<child>_child_of_<parent>.js Child route (scoped by FK)
|
|
94
|
+
routes/index.js Route mounting index
|
|
95
|
+
test/<table>.test.js CRUD endpoint tests
|
|
96
|
+
openapi.json OpenAPI 3.0 spec
|
|
97
|
+
llms.txt LLM quick reference
|
|
98
|
+
docs/llm.md Full LLM reference
|
|
99
|
+
|
|
100
|
+
Examples:
|
|
101
|
+
db-model-router generate --from dbmr.schema.json
|
|
102
|
+
db-model-router generate --models --dry-run
|
|
103
|
+
db-model-router generate --routes --tests
|
|
104
|
+
db-model-router generate --from dbmr.schema.json --json`,
|
|
105
|
+
|
|
106
|
+
doctor: `Usage: db-model-router doctor [options]
|
|
107
|
+
|
|
108
|
+
Validate schema, check adapter driver dependencies, and verify generated
|
|
109
|
+
files are in sync with the schema.
|
|
110
|
+
|
|
111
|
+
Options:
|
|
112
|
+
--from <path> Path to schema file (default: dbmr.schema.json)
|
|
113
|
+
--yes Accept all defaults without prompting
|
|
114
|
+
--json Output machine-readable JSON
|
|
115
|
+
--help Show this help message
|
|
116
|
+
|
|
117
|
+
Checks performed:
|
|
118
|
+
1. Schema validation Syntax and structure of dbmr.schema.json
|
|
119
|
+
2. Dependency check Adapter driver present in package.json
|
|
120
|
+
3. Sync check Generated files match what the schema would produce
|
|
121
|
+
|
|
122
|
+
Examples:
|
|
123
|
+
db-model-router doctor --from dbmr.schema.json
|
|
124
|
+
db-model-router doctor --json`,
|
|
125
|
+
|
|
126
|
+
diff: `Usage: db-model-router diff [options]
|
|
127
|
+
|
|
128
|
+
Preview changes between the current generated files and what the schema
|
|
129
|
+
would produce. Read-only — does not modify any files on disk.
|
|
130
|
+
|
|
131
|
+
Options:
|
|
132
|
+
--from <path> Path to schema file (default: dbmr.schema.json)
|
|
133
|
+
--yes Accept all defaults without prompting
|
|
134
|
+
--json Output machine-readable JSON
|
|
135
|
+
--help Show this help message
|
|
136
|
+
|
|
137
|
+
Output shows:
|
|
138
|
+
+ Added New files that would be created
|
|
139
|
+
~ Modified Files with changes (includes line diffs)
|
|
140
|
+
- Deleted Extra files that would be removed
|
|
141
|
+
|
|
142
|
+
Examples:
|
|
143
|
+
db-model-router diff --from dbmr.schema.json
|
|
144
|
+
db-model-router diff --json`,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Help command handler.
|
|
149
|
+
*
|
|
150
|
+
* When called with a command name in args (e.g. `help init`), prints
|
|
151
|
+
* detailed help for that command. Otherwise prints the general overview.
|
|
152
|
+
*
|
|
153
|
+
* @param {object} args - Parsed key-value args
|
|
154
|
+
* @param {object} flags - Universal flags
|
|
155
|
+
* @param {import('../flags').OutputContext} ctx - Output context
|
|
156
|
+
* @param {object} options - Injected dependencies
|
|
157
|
+
* @param {Function} options.printHelp - General help printer from main.js
|
|
158
|
+
*/
|
|
159
|
+
async function help(args, flags, ctx, options) {
|
|
160
|
+
// The command to get help for is the first positional arg captured
|
|
161
|
+
// by parseFlags as a key-value. We also check args._command which
|
|
162
|
+
// main.js will inject.
|
|
163
|
+
const topic = args._command;
|
|
164
|
+
|
|
165
|
+
if (topic && COMMAND_HELP[topic]) {
|
|
166
|
+
ctx.log(COMMAND_HELP[topic]);
|
|
167
|
+
} else if (topic) {
|
|
168
|
+
ctx.log(`Unknown command: ${topic}\n`);
|
|
169
|
+
ctx.log(`Available commands: ${Object.keys(COMMAND_HELP).join(", ")}\n`);
|
|
170
|
+
ctx.log(`Run "db-model-router help <command>" for detailed help.`);
|
|
171
|
+
} else {
|
|
172
|
+
// No topic — print general help
|
|
173
|
+
if (options && options.printHelp) {
|
|
174
|
+
options.printHelp();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = help;
|
|
180
|
+
module.exports.COMMAND_HELP = COMMAND_HELP;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { parseSchema } = require("../../schema/schema-parser");
|
|
6
|
+
const {
|
|
7
|
+
generateFiles,
|
|
8
|
+
updatePackageJson,
|
|
9
|
+
runInstall,
|
|
10
|
+
printSummary,
|
|
11
|
+
ensurePackageJson,
|
|
12
|
+
} = require("../init");
|
|
13
|
+
const { promptUser } = require("../init/prompt");
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Default answers used when --yes is provided and no schema is available.
|
|
17
|
+
*/
|
|
18
|
+
const DEFAULT_ANSWERS = {
|
|
19
|
+
framework: "express",
|
|
20
|
+
database: "postgres",
|
|
21
|
+
session: "memory",
|
|
22
|
+
rateLimiting: true,
|
|
23
|
+
helmet: true,
|
|
24
|
+
logger: true,
|
|
25
|
+
loki: false,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Init command handler for the unified CLI.
|
|
30
|
+
*
|
|
31
|
+
* Scaffolds a new project from a schema file or interactively.
|
|
32
|
+
*
|
|
33
|
+
* @param {object} args - Parsed positional/key-value args (e.g. { from, framework, database })
|
|
34
|
+
* @param {object} flags - Universal flags: { yes, json, dryRun, noInstall, help }
|
|
35
|
+
* @param {import('../flags').OutputContext} ctx - Output context for --json support
|
|
36
|
+
*/
|
|
37
|
+
async function init(args, flags, ctx) {
|
|
38
|
+
let answers;
|
|
39
|
+
|
|
40
|
+
if (args.from) {
|
|
41
|
+
// --from points to a schema file: read adapter/framework from it
|
|
42
|
+
const schemaPath = path.resolve(args.from);
|
|
43
|
+
if (!fs.existsSync(schemaPath)) {
|
|
44
|
+
throw new Error(`Schema file not found: ${args.from}`);
|
|
45
|
+
}
|
|
46
|
+
const raw = fs.readFileSync(schemaPath, "utf8");
|
|
47
|
+
const schema = parseSchema(raw);
|
|
48
|
+
|
|
49
|
+
answers = {
|
|
50
|
+
framework: schema.framework,
|
|
51
|
+
database: schema.adapter,
|
|
52
|
+
session: (schema.options && schema.options.session) || "memory",
|
|
53
|
+
rateLimiting: !!(schema.options && schema.options.rateLimiting),
|
|
54
|
+
helmet: !!(schema.options && schema.options.helmet),
|
|
55
|
+
logger: !!(schema.options && schema.options.logger),
|
|
56
|
+
loki: !!(schema.options && schema.options.loki),
|
|
57
|
+
};
|
|
58
|
+
} else if (flags.yes) {
|
|
59
|
+
// --yes with no schema: use defaults, but allow CLI overrides
|
|
60
|
+
answers = Object.assign({}, DEFAULT_ANSWERS);
|
|
61
|
+
if (args.framework) answers.framework = args.framework;
|
|
62
|
+
if (args.database) answers.database = args.database;
|
|
63
|
+
} else {
|
|
64
|
+
// Interactive: build prefilled from CLI args, prompt for the rest
|
|
65
|
+
const prefilled = {};
|
|
66
|
+
if (args.framework) prefilled.framework = args.framework;
|
|
67
|
+
if (args.database) prefilled.database = args.database;
|
|
68
|
+
answers = await promptUser(prefilled);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Resolve --output directory (relative to cwd)
|
|
72
|
+
// CLI --output flag takes precedence, then interactive prompt answer
|
|
73
|
+
const outputDir = args.output || answers.output || "";
|
|
74
|
+
|
|
75
|
+
// --dry-run: report planned files without writing
|
|
76
|
+
if (flags.dryRun) {
|
|
77
|
+
const planned = planFiles(answers, outputDir);
|
|
78
|
+
if (flags.json) {
|
|
79
|
+
ctx.result({
|
|
80
|
+
files: planned,
|
|
81
|
+
dependencies: { installed: false },
|
|
82
|
+
actions: ["dry-run"],
|
|
83
|
+
});
|
|
84
|
+
} else {
|
|
85
|
+
ctx.log("Dry run — the following files would be created:");
|
|
86
|
+
for (const f of planned) {
|
|
87
|
+
ctx.log(` ${f}`);
|
|
88
|
+
}
|
|
89
|
+
ctx.log("\nNo files were written.");
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Ensure package.json exists
|
|
95
|
+
ensurePackageJson();
|
|
96
|
+
|
|
97
|
+
// Generate project files
|
|
98
|
+
const generated = generateFiles(answers, outputDir);
|
|
99
|
+
|
|
100
|
+
// Update package.json with deps and scripts
|
|
101
|
+
updatePackageJson(answers, outputDir);
|
|
102
|
+
|
|
103
|
+
// npm install (unless --no-install)
|
|
104
|
+
const installed = !flags.noInstall;
|
|
105
|
+
if (installed) {
|
|
106
|
+
runInstall();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Output
|
|
110
|
+
const allFiles = [
|
|
111
|
+
...generated.files,
|
|
112
|
+
...generated.migrationFiles.map((m) => {
|
|
113
|
+
const base = outputDir || ".";
|
|
114
|
+
return base === "." ? `migrations/${m}` : `${base}/migrations/${m}`;
|
|
115
|
+
}),
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
if (flags.json) {
|
|
119
|
+
ctx.result({
|
|
120
|
+
files: allFiles,
|
|
121
|
+
dependencies: { installed },
|
|
122
|
+
actions: installed ? ["scaffolded", "installed"] : ["scaffolded"],
|
|
123
|
+
});
|
|
124
|
+
} else {
|
|
125
|
+
printSummary(generated);
|
|
126
|
+
if (!installed) {
|
|
127
|
+
ctx.log(
|
|
128
|
+
"\nSkipped npm install (--no-install). Run `npm install` manually.",
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Compute the list of files that would be created (for --dry-run).
|
|
136
|
+
* This mirrors the file list from generateFiles() without writing anything.
|
|
137
|
+
*
|
|
138
|
+
* @param {object} answers
|
|
139
|
+
* @param {string} [outputDir] - relative output directory for source files
|
|
140
|
+
* @returns {string[]}
|
|
141
|
+
*/
|
|
142
|
+
function planFiles(answers, outputDir) {
|
|
143
|
+
const { isSql } = require("../init/generators");
|
|
144
|
+
const srcBase = outputDir || ".";
|
|
145
|
+
const prefix = srcBase === "." ? "" : srcBase + "/";
|
|
146
|
+
|
|
147
|
+
const files = [
|
|
148
|
+
"app.js",
|
|
149
|
+
".env",
|
|
150
|
+
".env.example",
|
|
151
|
+
".gitignore",
|
|
152
|
+
"Dockerfile",
|
|
153
|
+
".dockerignore",
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
// docker-compose.yml for databases that need Docker
|
|
157
|
+
if (answers.database !== "sqlite3") {
|
|
158
|
+
files.push("docker-compose.yml");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
files.push(
|
|
162
|
+
`${prefix}middleware/logger.js`,
|
|
163
|
+
`${prefix}commons/session.js`,
|
|
164
|
+
`${prefix}commons/migrate.js`,
|
|
165
|
+
`${prefix}commons/add_migration.js`,
|
|
166
|
+
`${prefix}commons/security.js`,
|
|
167
|
+
`${prefix}commons/db.js`,
|
|
168
|
+
`${prefix}route/health.js`,
|
|
169
|
+
`${prefix}route/index.js`,
|
|
170
|
+
`${prefix}migrations/<timestamp>_create_migrations_table` +
|
|
171
|
+
(isSql(answers.database) ? ".sql" : ".js"),
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
if (answers.session === "database" && isSql(answers.database)) {
|
|
175
|
+
files.push(`${prefix}migrations/<timestamp>_create_sessions_table.sql`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return files;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = init;
|