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,191 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const inquirer = require("inquirer");
|
|
4
|
+
|
|
5
|
+
const VALID_FRAMEWORKS = ["ultimate-express", "express"];
|
|
6
|
+
const VALID_DATABASES = [
|
|
7
|
+
"mysql",
|
|
8
|
+
"mariadb",
|
|
9
|
+
"postgres",
|
|
10
|
+
"sqlite3",
|
|
11
|
+
"mongodb",
|
|
12
|
+
"mssql",
|
|
13
|
+
"cockroachdb",
|
|
14
|
+
"oracle",
|
|
15
|
+
"redis",
|
|
16
|
+
"dynamodb",
|
|
17
|
+
];
|
|
18
|
+
const VALID_SESSIONS = ["memory", "redis", "database"];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Parse CLI arguments into a partial answers object.
|
|
22
|
+
* Supports: --framework, --database, --session, --rateLimiting, --helmet, --logger
|
|
23
|
+
* Boolean flags accept: true/false, yes/no, 1/0, or just --flag (implies true)
|
|
24
|
+
* @param {string[]} argv
|
|
25
|
+
* @returns {Partial<import('./types').InitAnswers>}
|
|
26
|
+
*/
|
|
27
|
+
function parseInitArgs(argv) {
|
|
28
|
+
const args = {};
|
|
29
|
+
for (let i = 0; i < argv.length; i++) {
|
|
30
|
+
const arg = argv[i];
|
|
31
|
+
if (arg.startsWith("--")) {
|
|
32
|
+
const key = arg.slice(2);
|
|
33
|
+
const next = argv[i + 1];
|
|
34
|
+
if (next && !next.startsWith("--")) {
|
|
35
|
+
args[key] = next;
|
|
36
|
+
i++;
|
|
37
|
+
} else {
|
|
38
|
+
args[key] = "true";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const partial = {};
|
|
44
|
+
|
|
45
|
+
if (args.framework && VALID_FRAMEWORKS.includes(args.framework)) {
|
|
46
|
+
partial.framework = args.framework;
|
|
47
|
+
}
|
|
48
|
+
if (args.database && VALID_DATABASES.includes(args.database)) {
|
|
49
|
+
partial.database = args.database;
|
|
50
|
+
}
|
|
51
|
+
// Accept --db as alias for --database
|
|
52
|
+
if (!partial.database && args.db && VALID_DATABASES.includes(args.db)) {
|
|
53
|
+
partial.database = args.db;
|
|
54
|
+
}
|
|
55
|
+
if (args.session && VALID_SESSIONS.includes(args.session)) {
|
|
56
|
+
partial.session = args.session;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Boolean flags
|
|
60
|
+
for (const flag of ["rateLimiting", "helmet", "logger", "loki"]) {
|
|
61
|
+
if (args[flag] !== undefined) {
|
|
62
|
+
partial[flag] = parseBool(args[flag]);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Output directory
|
|
67
|
+
if (args.output !== undefined && args.output !== "true") {
|
|
68
|
+
partial.output = args.output;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return partial;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Parse a string as a boolean.
|
|
76
|
+
* @param {string} val
|
|
77
|
+
* @returns {boolean}
|
|
78
|
+
*/
|
|
79
|
+
function parseBool(val) {
|
|
80
|
+
return ["true", "yes", "1"].includes(String(val).toLowerCase());
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Run the interactive prompt flow to collect user preferences.
|
|
85
|
+
* Any values already present in `prefilledAnswers` are skipped (not prompted).
|
|
86
|
+
* When all 6 values are provided, no prompts are shown at all.
|
|
87
|
+
* @param {Partial<import('./types').InitAnswers>} [prefilledAnswers={}]
|
|
88
|
+
* @returns {Promise<import('./types').InitAnswers>}
|
|
89
|
+
*/
|
|
90
|
+
async function promptUser(prefilledAnswers) {
|
|
91
|
+
const prefilled = prefilledAnswers || {};
|
|
92
|
+
|
|
93
|
+
const questions = [];
|
|
94
|
+
|
|
95
|
+
if (prefilled.framework === undefined) {
|
|
96
|
+
questions.push({
|
|
97
|
+
type: "list",
|
|
98
|
+
name: "framework",
|
|
99
|
+
message: "Select your Express framework:",
|
|
100
|
+
choices: VALID_FRAMEWORKS,
|
|
101
|
+
default: "ultimate-express",
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (prefilled.database === undefined) {
|
|
106
|
+
questions.push({
|
|
107
|
+
type: "list",
|
|
108
|
+
name: "database",
|
|
109
|
+
message: "Select your database:",
|
|
110
|
+
choices: VALID_DATABASES,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (prefilled.session === undefined) {
|
|
115
|
+
questions.push({
|
|
116
|
+
type: "list",
|
|
117
|
+
name: "session",
|
|
118
|
+
message: "Select your session store:",
|
|
119
|
+
choices: VALID_SESSIONS,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (prefilled.output === undefined) {
|
|
124
|
+
questions.push({
|
|
125
|
+
type: "input",
|
|
126
|
+
name: "output",
|
|
127
|
+
message:
|
|
128
|
+
"Output directory for backend source files (leave empty for root):",
|
|
129
|
+
default: "",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (prefilled.rateLimiting === undefined) {
|
|
134
|
+
questions.push({
|
|
135
|
+
type: "confirm",
|
|
136
|
+
name: "rateLimiting",
|
|
137
|
+
message: "Enable rate limiting?",
|
|
138
|
+
default: true,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (prefilled.helmet === undefined) {
|
|
143
|
+
questions.push({
|
|
144
|
+
type: "confirm",
|
|
145
|
+
name: "helmet",
|
|
146
|
+
message: "Enable Helmet security headers?",
|
|
147
|
+
default: true,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (prefilled.logger === undefined) {
|
|
152
|
+
questions.push({
|
|
153
|
+
type: "confirm",
|
|
154
|
+
name: "logger",
|
|
155
|
+
message: "Enable request/response logger (Winston)?",
|
|
156
|
+
default: true,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// If all values are prefilled, skip prompts entirely
|
|
161
|
+
if (questions.length === 0) {
|
|
162
|
+
return /** @type {import('./types').InitAnswers} */ (prefilled);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const prompted = await inquirer.prompt(questions);
|
|
166
|
+
|
|
167
|
+
// Follow-up: if logger is enabled, ask about Loki
|
|
168
|
+
if (prompted.logger && prefilled.loki === undefined) {
|
|
169
|
+
const lokiAnswer = await inquirer.prompt([
|
|
170
|
+
{
|
|
171
|
+
type: "confirm",
|
|
172
|
+
name: "loki",
|
|
173
|
+
message: "Send logs to Grafana Loki?",
|
|
174
|
+
default: false,
|
|
175
|
+
},
|
|
176
|
+
]);
|
|
177
|
+
prompted.loki = lokiAnswer.loki;
|
|
178
|
+
} else if (!prompted.logger && prefilled.logger === undefined) {
|
|
179
|
+
prompted.loki = false;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return Object.assign({}, prefilled, prompted);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = {
|
|
186
|
+
promptUser,
|
|
187
|
+
parseInitArgs,
|
|
188
|
+
VALID_FRAMEWORKS,
|
|
189
|
+
VALID_DATABASES,
|
|
190
|
+
VALID_SESSIONS,
|
|
191
|
+
};
|
package/src/cli/init.js
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require("fs");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const { execSync } = require("child_process");
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
generateAppJs,
|
|
10
|
+
generateAppJsV2,
|
|
11
|
+
generateEnvFile,
|
|
12
|
+
generateEnvExample,
|
|
13
|
+
generateLoggerMiddleware,
|
|
14
|
+
generateInitialMigration,
|
|
15
|
+
generateSessionMigration,
|
|
16
|
+
generateGitignore,
|
|
17
|
+
generateDockerfile,
|
|
18
|
+
generateDockerignore,
|
|
19
|
+
generateGrafanaDatasources,
|
|
20
|
+
generateDockerCompose,
|
|
21
|
+
generateCloudBeaverDataSources,
|
|
22
|
+
generateSessionJs,
|
|
23
|
+
generateMigrateModule,
|
|
24
|
+
generateAddMigrationModule,
|
|
25
|
+
generateSecurityJs,
|
|
26
|
+
generateHealthRoute,
|
|
27
|
+
generateRouteIndexFile,
|
|
28
|
+
generateDbModule,
|
|
29
|
+
randomPassword,
|
|
30
|
+
} = require("./init/generators");
|
|
31
|
+
|
|
32
|
+
const { collectDependencies, getScripts } = require("./init/dependencies");
|
|
33
|
+
const { promptUser, parseInitArgs } = require("./init/prompt");
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Ensure a package.json exists in the current directory.
|
|
37
|
+
* Uses `npm init -y` for non-interactive creation. Exits with code 1 on failure.
|
|
38
|
+
*/
|
|
39
|
+
function ensurePackageJson() {
|
|
40
|
+
if (fs.existsSync("package.json")) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
execSync("npm init -y", { stdio: "inherit" });
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.error("Error: npm init failed or was aborted.");
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
if (!fs.existsSync("package.json")) {
|
|
50
|
+
console.error("Error: package.json was not created. Aborting.");
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Write a file, but warn (and skip) if it already exists.
|
|
57
|
+
* @param {string} filePath
|
|
58
|
+
* @param {string} content
|
|
59
|
+
* @returns {boolean} true if written, false if skipped
|
|
60
|
+
*/
|
|
61
|
+
function safeWriteFile(filePath, content) {
|
|
62
|
+
if (fs.existsSync(filePath)) {
|
|
63
|
+
console.log(` Skipped ${filePath} (already exists)`);
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
fs.writeFileSync(filePath, content);
|
|
67
|
+
console.log(` Created ${filePath}`);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Generate all project files based on user answers.
|
|
73
|
+
* Creates directories and writes files. Skips files that already exist.
|
|
74
|
+
* Returns the list of generated filenames for the summary.
|
|
75
|
+
* @param {import('./init/types').InitAnswers} answers
|
|
76
|
+
* @param {string} [outputDir] - relative directory for source files (e.g. "backend")
|
|
77
|
+
* @returns {{ files: string[], migrationFiles: string[] }}
|
|
78
|
+
*/
|
|
79
|
+
function generateFiles(answers, outputDir) {
|
|
80
|
+
const files = [];
|
|
81
|
+
const migrationFiles = [];
|
|
82
|
+
|
|
83
|
+
// Resolve source directory (outputDir-relative or cwd)
|
|
84
|
+
const srcBase = outputDir || ".";
|
|
85
|
+
|
|
86
|
+
// Generate random secrets (shared between .env and docker-compose)
|
|
87
|
+
const secrets = {
|
|
88
|
+
dbPass: randomPassword(),
|
|
89
|
+
redisPass: answers.session === "redis" ? randomPassword() : "",
|
|
90
|
+
sessionSecret: randomPassword(32),
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Create directories
|
|
94
|
+
const dirs = [
|
|
95
|
+
path.join(srcBase, "middleware"),
|
|
96
|
+
path.join(srcBase, "migrations"),
|
|
97
|
+
path.join(srcBase, "commons"),
|
|
98
|
+
path.join(srcBase, "route"),
|
|
99
|
+
];
|
|
100
|
+
// SQLite3 needs a data/ folder for the database file
|
|
101
|
+
if (answers.database === "sqlite3") {
|
|
102
|
+
dirs.push("data");
|
|
103
|
+
}
|
|
104
|
+
for (const dir of dirs) {
|
|
105
|
+
if (!fs.existsSync(dir)) {
|
|
106
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Root-level files (always in cwd, not in outputDir)
|
|
111
|
+
// app.js uses the v2 generator that links commons/route modules
|
|
112
|
+
if (safeWriteFile("app.js", generateAppJsV2(answers, outputDir || "")))
|
|
113
|
+
files.push("app.js");
|
|
114
|
+
if (safeWriteFile(".env", generateEnvFile(answers, secrets)))
|
|
115
|
+
files.push(".env");
|
|
116
|
+
if (safeWriteFile(".env.example", generateEnvExample(answers)))
|
|
117
|
+
files.push(".env.example");
|
|
118
|
+
if (safeWriteFile(".gitignore", generateGitignore()))
|
|
119
|
+
files.push(".gitignore");
|
|
120
|
+
|
|
121
|
+
// docker-compose.yml (if the database needs Docker)
|
|
122
|
+
const dockerCompose = generateDockerCompose(answers, secrets);
|
|
123
|
+
if (dockerCompose !== null) {
|
|
124
|
+
if (safeWriteFile("docker-compose.yml", dockerCompose))
|
|
125
|
+
files.push("docker-compose.yml");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// CloudBeaver data-sources.json (auto-connect config)
|
|
129
|
+
const cbDataSources = generateCloudBeaverDataSources(answers, secrets);
|
|
130
|
+
if (cbDataSources !== null) {
|
|
131
|
+
const cbDir = ".cloudbeaver";
|
|
132
|
+
if (!fs.existsSync(cbDir)) {
|
|
133
|
+
fs.mkdirSync(cbDir, { recursive: true });
|
|
134
|
+
}
|
|
135
|
+
const cbPath = path.join(cbDir, "data-sources.json");
|
|
136
|
+
if (safeWriteFile(cbPath, cbDataSources))
|
|
137
|
+
files.push(".cloudbeaver/data-sources.json");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Grafana datasource provisioning (when loki is enabled)
|
|
141
|
+
if (answers.loki) {
|
|
142
|
+
const grafanaDir = ".grafana";
|
|
143
|
+
if (!fs.existsSync(grafanaDir)) {
|
|
144
|
+
fs.mkdirSync(grafanaDir, { recursive: true });
|
|
145
|
+
}
|
|
146
|
+
const grafanaPath = path.join(grafanaDir, "datasources.yml");
|
|
147
|
+
if (safeWriteFile(grafanaPath, generateGrafanaDatasources()))
|
|
148
|
+
files.push(".grafana/datasources.yml");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Dockerfile and .dockerignore
|
|
152
|
+
if (safeWriteFile("Dockerfile", generateDockerfile(answers, outputDir)))
|
|
153
|
+
files.push("Dockerfile");
|
|
154
|
+
if (safeWriteFile(".dockerignore", generateDockerignore()))
|
|
155
|
+
files.push(".dockerignore");
|
|
156
|
+
|
|
157
|
+
// Source files inside outputDir (or cwd if no outputDir)
|
|
158
|
+
const loggerPath = path.join(srcBase, "middleware", "logger.js");
|
|
159
|
+
if (safeWriteFile(loggerPath, generateLoggerMiddleware(answers)))
|
|
160
|
+
files.push(path.join(srcBase, "middleware/logger.js"));
|
|
161
|
+
|
|
162
|
+
// commons/session.js
|
|
163
|
+
const sessionPath = path.join(srcBase, "commons", "session.js");
|
|
164
|
+
if (safeWriteFile(sessionPath, generateSessionJs(answers)))
|
|
165
|
+
files.push(path.join(srcBase, "commons/session.js"));
|
|
166
|
+
|
|
167
|
+
// commons/migrate.js
|
|
168
|
+
const migratePath = path.join(srcBase, "commons", "migrate.js");
|
|
169
|
+
if (safeWriteFile(migratePath, generateMigrateModule(answers, outputDir)))
|
|
170
|
+
files.push(path.join(srcBase, "commons/migrate.js"));
|
|
171
|
+
|
|
172
|
+
// commons/add_migration.js
|
|
173
|
+
const addMigrationPath = path.join(srcBase, "commons", "add_migration.js");
|
|
174
|
+
if (
|
|
175
|
+
safeWriteFile(
|
|
176
|
+
addMigrationPath,
|
|
177
|
+
generateAddMigrationModule(answers, outputDir),
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
files.push(path.join(srcBase, "commons/add_migration.js"));
|
|
181
|
+
|
|
182
|
+
// commons/security.js
|
|
183
|
+
const securityPath = path.join(srcBase, "commons", "security.js");
|
|
184
|
+
if (safeWriteFile(securityPath, generateSecurityJs(answers)))
|
|
185
|
+
files.push(path.join(srcBase, "commons/security.js"));
|
|
186
|
+
|
|
187
|
+
// commons/db.js
|
|
188
|
+
const dbPath = path.join(srcBase, "commons", "db.js");
|
|
189
|
+
if (safeWriteFile(dbPath, generateDbModule(answers)))
|
|
190
|
+
files.push(path.join(srcBase, "commons/db.js"));
|
|
191
|
+
|
|
192
|
+
// route/health.js
|
|
193
|
+
const healthPath = path.join(srcBase, "route", "health.js");
|
|
194
|
+
if (safeWriteFile(healthPath, generateHealthRoute()))
|
|
195
|
+
files.push(path.join(srcBase, "route/health.js"));
|
|
196
|
+
|
|
197
|
+
// route/index.js
|
|
198
|
+
const routeIndexPath = path.join(srcBase, "route", "index.js");
|
|
199
|
+
if (safeWriteFile(routeIndexPath, generateRouteIndexFile()))
|
|
200
|
+
files.push(path.join(srcBase, "route/index.js"));
|
|
201
|
+
|
|
202
|
+
// Initial migration (inside outputDir/migrations)
|
|
203
|
+
const initialMigration = generateInitialMigration(answers);
|
|
204
|
+
const initialPath = path.join(
|
|
205
|
+
srcBase,
|
|
206
|
+
"migrations",
|
|
207
|
+
initialMigration.filename,
|
|
208
|
+
);
|
|
209
|
+
if (safeWriteFile(initialPath, initialMigration.content)) {
|
|
210
|
+
migrationFiles.push(initialMigration.filename);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Conditional session migration
|
|
214
|
+
const sessionMigration = generateSessionMigration(answers);
|
|
215
|
+
if (sessionMigration !== null) {
|
|
216
|
+
const sessionMigPath = path.join(
|
|
217
|
+
srcBase,
|
|
218
|
+
"migrations",
|
|
219
|
+
sessionMigration.filename,
|
|
220
|
+
);
|
|
221
|
+
if (safeWriteFile(sessionMigPath, sessionMigration.content)) {
|
|
222
|
+
migrationFiles.push(sessionMigration.filename);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return { files, migrationFiles };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Update package.json with scripts and dependencies from the answers.
|
|
231
|
+
* @param {import('./init/types').InitAnswers} answers
|
|
232
|
+
* @param {string} [outputDir] - relative output directory for source files
|
|
233
|
+
*/
|
|
234
|
+
function updatePackageJson(answers, outputDir) {
|
|
235
|
+
let raw;
|
|
236
|
+
try {
|
|
237
|
+
raw = fs.readFileSync("package.json", "utf8");
|
|
238
|
+
} catch (err) {
|
|
239
|
+
console.error("Error: Could not read package.json.");
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let pkg;
|
|
244
|
+
try {
|
|
245
|
+
pkg = JSON.parse(raw);
|
|
246
|
+
} catch (err) {
|
|
247
|
+
console.error(
|
|
248
|
+
"Error: package.json contains invalid JSON. Please fix it manually and re-run.",
|
|
249
|
+
);
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const { dependencies, devDependencies } = collectDependencies(answers);
|
|
254
|
+
const scripts = getScripts(outputDir);
|
|
255
|
+
|
|
256
|
+
pkg.type = "module";
|
|
257
|
+
pkg.scripts = Object.assign({}, pkg.scripts || {}, scripts);
|
|
258
|
+
pkg.dependencies = Object.assign({}, pkg.dependencies || {}, dependencies);
|
|
259
|
+
pkg.devDependencies = Object.assign(
|
|
260
|
+
{},
|
|
261
|
+
pkg.devDependencies || {},
|
|
262
|
+
devDependencies,
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
fs.writeFileSync("package.json", JSON.stringify(pkg, null, 2) + "\n");
|
|
266
|
+
console.log(" Updated package.json");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Run npm install. On failure, print manual install instructions and exit with code 1.
|
|
271
|
+
*/
|
|
272
|
+
function runInstall() {
|
|
273
|
+
console.log("\nInstalling dependencies...\n");
|
|
274
|
+
try {
|
|
275
|
+
execSync("npm install", { stdio: "inherit" });
|
|
276
|
+
} catch (err) {
|
|
277
|
+
console.error(
|
|
278
|
+
"\nError: npm install failed. Please run the following command manually:",
|
|
279
|
+
);
|
|
280
|
+
console.error(" npm install");
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Print a summary of generated files and next-step instructions.
|
|
287
|
+
* Uses the file lists captured during generation (no re-calling generators).
|
|
288
|
+
* @param {{ files: string[], migrationFiles: string[] }} generated
|
|
289
|
+
*/
|
|
290
|
+
function printSummary(generated) {
|
|
291
|
+
console.log("\n✔ Project scaffolded successfully!\n");
|
|
292
|
+
console.log("Generated files:");
|
|
293
|
+
|
|
294
|
+
for (const f of generated.files) {
|
|
295
|
+
console.log(` ${f}`);
|
|
296
|
+
}
|
|
297
|
+
if (generated.migrationFiles.length > 0) {
|
|
298
|
+
console.log(" migrations/");
|
|
299
|
+
for (const m of generated.migrationFiles) {
|
|
300
|
+
console.log(` └── ${m}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
console.log("\nNext steps:");
|
|
305
|
+
console.log(" 1. Edit .env with your database credentials");
|
|
306
|
+
console.log(" 2. Run: npm run dev");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Main orchestrator.
|
|
311
|
+
*/
|
|
312
|
+
async function main() {
|
|
313
|
+
const cliArgs = parseInitArgs(process.argv.slice(2));
|
|
314
|
+
|
|
315
|
+
// If --help flag, print usage and exit
|
|
316
|
+
if (process.argv.includes("--help")) {
|
|
317
|
+
printUsage();
|
|
318
|
+
process.exit(0);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
ensurePackageJson();
|
|
322
|
+
|
|
323
|
+
let answers;
|
|
324
|
+
try {
|
|
325
|
+
answers = await promptUser(cliArgs);
|
|
326
|
+
} catch (err) {
|
|
327
|
+
// Handle Ctrl+C (inquirer throws on user cancel)
|
|
328
|
+
console.log("\nAborted.");
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
let generated;
|
|
333
|
+
try {
|
|
334
|
+
generated = generateFiles(answers);
|
|
335
|
+
} catch (err) {
|
|
336
|
+
if (err.code === "EACCES" || err.code === "EPERM") {
|
|
337
|
+
console.error(
|
|
338
|
+
`Error: Permission denied writing files. Check directory permissions.\n ${err.message}`,
|
|
339
|
+
);
|
|
340
|
+
} else {
|
|
341
|
+
console.error(`Error: Failed to generate files.\n ${err.message}`);
|
|
342
|
+
}
|
|
343
|
+
process.exit(1);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
updatePackageJson(answers);
|
|
347
|
+
runInstall();
|
|
348
|
+
printSummary(generated);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Print CLI usage information.
|
|
353
|
+
*/
|
|
354
|
+
function printUsage() {
|
|
355
|
+
console.log(`
|
|
356
|
+
Usage: db-model-router-init [options]
|
|
357
|
+
|
|
358
|
+
Scaffolds a complete Express-based REST API project. When all options are
|
|
359
|
+
provided, runs non-interactively (no prompts). Missing options are prompted.
|
|
360
|
+
|
|
361
|
+
Options:
|
|
362
|
+
--framework <name> Express framework: ultimate-express, express
|
|
363
|
+
--database <name> Database: mysql, postgres, sqlite3, mongodb, mssql,
|
|
364
|
+
cockroachdb, oracle, redis, dynamodb
|
|
365
|
+
--db <name> Alias for --database
|
|
366
|
+
--session <type> Session store: memory, redis, database
|
|
367
|
+
--output <dir> Directory for backend source files (e.g. --output backend).
|
|
368
|
+
package.json stays in root; index.js, commons/, route/,
|
|
369
|
+
middleware/, and migrations/ go inside the output folder.
|
|
370
|
+
--rateLimiting Enable rate limiting (express-rate-limit)
|
|
371
|
+
--helmet Enable Helmet security headers
|
|
372
|
+
--logger Enable request/response logger (express-mung)
|
|
373
|
+
--help Show this help message
|
|
374
|
+
|
|
375
|
+
Examples:
|
|
376
|
+
# Fully non-interactive (LLM-friendly)
|
|
377
|
+
db-model-router-init --framework express --database postgres --session redis --rateLimiting --helmet --logger
|
|
378
|
+
|
|
379
|
+
# With output directory
|
|
380
|
+
db-model-router-init --framework express --database postgres --output backend --yes
|
|
381
|
+
|
|
382
|
+
# Partial — only prompts for missing values
|
|
383
|
+
db-model-router-init --database mysql --session memory
|
|
384
|
+
|
|
385
|
+
# Interactive (no flags)
|
|
386
|
+
db-model-router-init
|
|
387
|
+
`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (require.main === module) {
|
|
391
|
+
main().catch((err) => {
|
|
392
|
+
console.error("Error:", err.message);
|
|
393
|
+
process.exit(1);
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
module.exports = {
|
|
398
|
+
ensurePackageJson,
|
|
399
|
+
generateFiles,
|
|
400
|
+
updatePackageJson,
|
|
401
|
+
runInstall,
|
|
402
|
+
printSummary,
|
|
403
|
+
main,
|
|
404
|
+
};
|