langaro-api 1.0.1 → 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.
@@ -3,7 +3,6 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
 
6
- // Load config from langaro-api.config.js at cwd, or use defaults
7
6
  function loadConfig() {
8
7
  const configPath = path.resolve(process.cwd(), 'langaro-api.config.js');
9
8
  if (fs.existsSync(configPath)) {
@@ -13,59 +12,170 @@ function loadConfig() {
13
12
  return {};
14
13
  }
15
14
 
16
- // ── Commands ──
15
+ // ── Interactive menu (arrow keys) ──
17
16
 
18
- const command = process.argv[2];
17
+ function singleSelect(question, options) {
18
+ return new Promise((resolve) => {
19
+ let cursor = 0;
20
+ // Count how many terminal lines the question occupies (including trailing newlines)
21
+ const questionLines = question.split('\n').length;
22
+ const totalLines = questionLines + options.length;
19
23
 
20
- if (command === 'init') {
21
- // Project scaffolding
22
- const runInit = require('../lib/cli/init');
23
- runInit();
24
- } else if (command === 'new') {
25
- // Interactive resource generator
26
- const runNew = require('../lib/cli/new');
24
+ function render() {
25
+ process.stdout.write(`\x1b[${totalLines}A\x1b[J`);
26
+ console.log(question);
27
+ options.forEach((opt, i) => {
28
+ const pointer = i === cursor ? '\x1b[36m> \x1b[0m' : ' ';
29
+ const label = i === cursor ? `\x1b[36m${opt.label}\x1b[0m` : opt.label;
30
+ const desc = opt.desc ? `\x1b[90m — ${opt.desc}\x1b[0m` : '';
31
+ console.log(`${pointer}${label}${desc}`);
32
+ });
33
+ }
34
+
35
+ console.log(question);
36
+ options.forEach((opt, i) => {
37
+ const pointer = i === cursor ? '\x1b[36m> \x1b[0m' : ' ';
38
+ const label = i === cursor ? `\x1b[36m${opt.label}\x1b[0m` : opt.label;
39
+ const desc = opt.desc ? `\x1b[90m — ${opt.desc}\x1b[0m` : '';
40
+ console.log(`${pointer}${label}${desc}`);
41
+ });
42
+
43
+ process.stdin.setRawMode(true);
44
+ process.stdin.resume();
45
+
46
+ const onKeypress = (key) => {
47
+ if (key[0] === 27 && key[1] === 91 && key[2] === 65) {
48
+ cursor = (cursor - 1 + options.length) % options.length;
49
+ render();
50
+ } else if (key[0] === 27 && key[1] === 91 && key[2] === 66) {
51
+ cursor = (cursor + 1) % options.length;
52
+ render();
53
+ } else if (key[0] === 13 || key[0] === 10) {
54
+ process.stdin.setRawMode(false);
55
+ process.stdin.removeListener('data', onKeypress);
56
+ resolve(options[cursor].value);
57
+ } else if (key[0] === 113 || key[0] === 3) {
58
+ process.stdin.setRawMode(false);
59
+ process.stdin.removeListener('data', onKeypress);
60
+ process.exit(0);
61
+ }
62
+ };
63
+
64
+ process.stdin.on('data', onKeypress);
65
+ });
66
+ }
67
+
68
+ function confirm(question) {
69
+ return singleSelect(question, [
70
+ { label: 'Yes, proceed' , value: true },
71
+ { label: 'No, cancel', value: false },
72
+ ]);
73
+ }
74
+
75
+ // ── Command runners ──
76
+
77
+ function runGenerateTypes() {
78
+ const { generateTypes } = require('../lib/index');
27
79
  const config = loadConfig();
28
- runNew(config);
29
- } else {
30
- // Default: generate types
80
+ generateTypes(config);
81
+ console.log('[langaro-api] Types + JSDoc annotations generated.');
82
+ }
83
+
84
+ function runWatch() {
31
85
  const { generateTypes, getWatchDirs } = require('../lib/index');
32
86
 
33
- function run() {
87
+ function generate() {
34
88
  const config = loadConfig();
35
89
  generateTypes(config);
36
90
  console.log('[langaro-api] Types + JSDoc annotations generated.');
37
91
  }
38
92
 
39
- run();
93
+ generate();
40
94
 
41
- // Watch mode
42
- if (process.argv.includes('--watch')) {
43
- const config = loadConfig();
44
- const dirs = getWatchDirs(config);
45
-
46
- let debounceTimer = null;
95
+ const config = loadConfig();
96
+ const dirs = getWatchDirs(config);
97
+ let debounceTimer = null;
47
98
 
48
- function onChange() {
49
- if (debounceTimer) clearTimeout(debounceTimer);
50
- debounceTimer = setTimeout(() => {
51
- try {
52
- run();
53
- } catch (err) {
54
- console.error('[langaro-api] Error:', err.message);
99
+ dirs.forEach((dir) => {
100
+ try {
101
+ fs.watch(dir, { recursive: true }, (eventType, filename) => {
102
+ if (filename && filename.endsWith('.js')) {
103
+ if (debounceTimer) clearTimeout(debounceTimer);
104
+ debounceTimer = setTimeout(() => {
105
+ try { generate(); } catch (err) { console.error('[langaro-api] Error:', err.message); }
106
+ }, 300);
55
107
  }
56
- }, 300);
108
+ });
109
+ } catch (err) {
110
+ console.warn(`[langaro-api] Could not watch ${dir}: ${err.message}`);
57
111
  }
112
+ });
58
113
 
59
- dirs.forEach((dir) => {
60
- try {
61
- fs.watch(dir, { recursive: true }, (eventType, filename) => {
62
- if (filename && filename.endsWith('.js')) onChange();
63
- });
64
- } catch (err) {
65
- console.warn(`[langaro-api] Could not watch ${dir}: ${err.message}`);
66
- }
67
- });
114
+ console.log(`[langaro-api] Watching ${dirs.length} directories for changes...`);
115
+ }
116
+
117
+ async function runInit() {
118
+ const ok = await confirm('\n\x1b[33mThis will create project files in the current directory.\x1b[0m Are you sure?\n');
119
+ if (!ok) { console.log('\nAborted.\n'); process.exit(0); }
120
+ const init = require('../lib/cli/init');
121
+ await init();
122
+ }
123
+
124
+ async function runMigrate() {
125
+ const ok = await confirm('\n\x1b[33mThis will replace your index.js files with langaro-api loaders.\x1b[0m Are you sure?\n');
126
+ if (!ok) { console.log('\nAborted.\n'); process.exit(0); }
127
+ const migrate = require('../lib/cli/migrate');
128
+ migrate();
129
+ }
130
+
131
+ async function runNew() {
132
+ const newCmd = require('../lib/cli/new');
133
+ const config = loadConfig();
134
+ await newCmd(config);
135
+ }
136
+
137
+ // ── Main ──
68
138
 
69
- console.log(`[langaro-api] Watching ${dirs.length} directories for changes...`);
139
+ async function main() {
140
+ const command = process.argv[2];
141
+
142
+ // Direct commands (for scripts, CI, --watch)
143
+ if (command === 'init') return runInit();
144
+ if (command === 'migrate') return runMigrate();
145
+ if (command === 'new') return runNew();
146
+ if (command === '--watch') return runWatch();
147
+ if (command === 'generate' || command === 'types') {
148
+ runGenerateTypes();
149
+ return process.exit(0);
150
+ }
151
+
152
+ // No args + non-TTY (e.g. piped, CI): just generate types
153
+ if (!process.stdin.isTTY) {
154
+ runGenerateTypes();
155
+ return process.exit(0);
70
156
  }
157
+
158
+ // No args + TTY: interactive menu
159
+ console.log('\n\x1b[36m langaro-api\x1b[0m\n');
160
+
161
+ const choice = await singleSelect(' What would you like to do?\n', [
162
+ { label: 'Generate types', value: 'types', desc: 'Generate .d.ts files and inject JSDoc annotations' },
163
+ { label: 'New resource', value: 'new', desc: 'Scaffold a new controller, service, model, or route' },
164
+ { label: 'Migrate project', value: 'migrate', desc: 'Replace index.js files with langaro-api loaders' },
165
+ { label: 'Init new project', value: 'init', desc: 'Scaffold a full project from scratch' },
166
+ ]);
167
+
168
+ console.log('');
169
+
170
+ if (choice === 'types') { runGenerateTypes(); process.exit(0); }
171
+ if (choice === 'new') return runNew();
172
+ if (choice === 'migrate') return runMigrate();
173
+ if (choice === 'init') return runInit();
174
+
175
+ return undefined;
71
176
  }
177
+
178
+ main().catch((err) => {
179
+ console.error(err);
180
+ process.exit(1);
181
+ });
package/lib/cli/init.js CHANGED
@@ -1,7 +1,8 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const readline = require('readline');
4
- const { execSync } = require('child_process');
4
+
5
+ const OWN_VERSION = require('../../package.json').version;
5
6
 
6
7
  function ask(rl, question, defaultVal) {
7
8
  const suffix = defaultVal ? ` (${defaultVal})` : '';
@@ -14,6 +15,10 @@ function ask(rl, question, defaultVal) {
14
15
 
15
16
  function writeFile(root, filePath, content) {
16
17
  const full = path.join(root, filePath);
18
+ if (fs.existsSync(full)) {
19
+ console.log(` \x1b[33mskip\x1b[0m ${filePath} (already exists)`);
20
+ return;
21
+ }
17
22
  fs.mkdirSync(path.dirname(full), { recursive: true });
18
23
  fs.writeFileSync(full, content);
19
24
  console.log(` \x1b[32mcreated\x1b[0m ${filePath}`);
@@ -75,7 +80,7 @@ function packageJson(name) {
75
80
  'knex-extended-crud': '^2.0.31',
76
81
  'knex-paginate': '3.2.0',
77
82
  'knex-schema-inspector': '^3.0.0',
78
- 'langaro-api': '^2.0.0',
83
+ 'langaro-api': `^${OWN_VERSION}`,
79
84
  lodash: '^4.17.21',
80
85
  morgan: '^1.10.0',
81
86
  multer: '^1.4.5-lts.1',
@@ -0,0 +1,99 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const INDEX_REPLACEMENTS = {
5
+ 'src/database/models/index.js': `const { loadModels } = require('langaro-api');
6
+
7
+ module.exports = (knexInstance) => loadModels(knexInstance, __dirname);
8
+ `,
9
+ 'src/database/services/index.js': `const { loadServices } = require('langaro-api');
10
+
11
+ module.exports = (models, io) => loadServices(models, io, __dirname);
12
+ `,
13
+ 'src/controllers/index.js': `const { loadControllers } = require('langaro-api');
14
+
15
+ module.exports = (services, Queue, io) => loadControllers(services, Queue, io, __dirname);
16
+ `,
17
+ 'src/routes/index.js': `const { attachRouters } = require('langaro-api');
18
+
19
+ exports.attachRouters = (express, controllers, services) => {
20
+ attachRouters(express, controllers, services, __dirname);
21
+ };
22
+ `,
23
+ 'src/jobs/index.js': `const { loadJobs } = require('langaro-api');
24
+
25
+ module.exports = (services) => loadJobs(services, __dirname);
26
+ `,
27
+ 'src/tasks/index.js': `const { loadTasks } = require('langaro-api');
28
+
29
+ module.exports = (services, Queue) => loadTasks(services, Queue, __dirname);
30
+ `,
31
+ };
32
+
33
+ function run() {
34
+ const root = process.cwd();
35
+ let replaced = 0;
36
+ let skipped = 0;
37
+ let missing = 0;
38
+
39
+ console.log('\n\x1b[36mlangaro-api migrate\x1b[0m — Replace index files with langaro-api loaders\n');
40
+
41
+ Object.entries(INDEX_REPLACEMENTS).forEach(([relPath, newContent]) => {
42
+ const fullPath = path.resolve(root, relPath);
43
+
44
+ if (!fs.existsSync(fullPath)) {
45
+ console.log(` \x1b[33mmissing\x1b[0m ${relPath} (file not found, skipping)`);
46
+ missing++;
47
+ return;
48
+ }
49
+
50
+ const current = fs.readFileSync(fullPath, 'utf8');
51
+
52
+ // Already migrated — check if it already uses langaro-api
53
+ if (current.includes("require('langaro-api')")) {
54
+ console.log(` \x1b[90mskip\x1b[0m ${relPath} (already using langaro-api)`);
55
+ skipped++;
56
+ return;
57
+ }
58
+
59
+ // Back up the original
60
+ const backupPath = `${fullPath}.bak`;
61
+ fs.writeFileSync(backupPath, current);
62
+
63
+ // Replace with the loader version
64
+ fs.writeFileSync(fullPath, newContent);
65
+ console.log(` \x1b[32mreplaced\x1b[0m ${relPath} (backup at ${path.basename(backupPath)})`);
66
+ replaced++;
67
+ });
68
+
69
+ console.log(`\n Replaced: ${replaced}, Skipped: ${skipped}, Missing: ${missing}\n`);
70
+
71
+ if (replaced > 0) {
72
+ console.log('\x1b[36mManual step required — update src/config/app.js:\x1b[0m\n');
73
+ console.log(' Replace these lines:');
74
+ console.log(" \x1b[31m- const { attachRouters } = require('@/routes');\x1b[0m");
75
+ console.log(" \x1b[31m- const models = await require('@/database/models')(this.knex);\x1b[0m");
76
+ console.log(" \x1b[31m- const services = this.services || require('@/database/services')(models.models, io);\x1b[0m");
77
+ console.log(" \x1b[31m- const controllers = require('@/controllers')(services, Queue, io);\x1b[0m");
78
+ console.log(" \x1b[31m- require('@/tasks')(services, Queue);\x1b[0m");
79
+ console.log('');
80
+ console.log(' With:');
81
+ console.log(" \x1b[32m+ const { loadModels, loadServices, loadControllers, attachRouters, loadTasks } = require('langaro-api');\x1b[0m");
82
+ console.log(' \x1b[32m+ const { models } = await loadModels(this.knex);\x1b[0m');
83
+ console.log(' \x1b[32m+ const services = this.services || loadServices(models, io);\x1b[0m');
84
+ console.log(' \x1b[32m+ const controllers = loadControllers(services, Queue, io);\x1b[0m');
85
+ console.log(' \x1b[32m+ loadTasks(services, Queue);\x1b[0m');
86
+ console.log('');
87
+ console.log(' And in queues.js replace:');
88
+ console.log(" \x1b[31m- const jobs = require('@/jobs/index')(services);\x1b[0m");
89
+ console.log(' With:');
90
+ console.log(" \x1b[32m+ const { loadJobs } = require('langaro-api');\x1b[0m");
91
+ console.log(' \x1b[32m+ const jobs = loadJobs(services);\x1b[0m');
92
+ console.log('');
93
+ console.log(' Then run \x1b[36mnpx langaro-api\x1b[0m to regenerate types.\n');
94
+ }
95
+
96
+ process.exit(0);
97
+ }
98
+
99
+ module.exports = run;
package/lib/cli/new.js CHANGED
@@ -83,59 +83,184 @@ function ask(rl, question) {
83
83
  });
84
84
  }
85
85
 
86
- function multiSelect(rl, question, options) {
86
+ function singleSelect(question, allOptions) {
87
87
  return new Promise((resolve) => {
88
- const selected = new Set(options.map((_, i) => i)); // all selected by default
88
+ const searchable = allOptions.length > 10;
89
+ let filter = '';
90
+ let filtered = allOptions;
89
91
  let cursor = 0;
92
+ let lastRenderedLines = 0;
93
+
94
+ // Compute how many items we can show based on terminal height
95
+ // Reserve lines for: question + filter bar + "more above" + "more below" + 1 buffer
96
+ function getMaxVisible() {
97
+ const termRows = process.stdout.rows || 24;
98
+ const overhead = 1 + (searchable ? 1 : 0) + 2 + 1; // question + filter + above/below + buffer
99
+ return Math.max(3, Math.min(10, termRows - overhead));
100
+ }
101
+
102
+ function getVisible() {
103
+ const maxVisible = getMaxVisible();
104
+ if (filtered.length <= maxVisible) return { items: filtered, startIdx: 0 };
105
+ let start = cursor - Math.floor(maxVisible / 2);
106
+ if (start < 0) start = 0;
107
+ if (start + maxVisible > filtered.length) start = filtered.length - maxVisible;
108
+ return { items: filtered.slice(start, start + maxVisible), startIdx: start };
109
+ }
110
+
111
+ function draw() {
112
+ if (lastRenderedLines > 0) {
113
+ // Cap move-up to terminal height to avoid glitches when output scrolled off screen
114
+ const moveUp = Math.min(lastRenderedLines, (process.stdout.rows || 24) - 1);
115
+ process.stdout.write(`\x1b[${moveUp}A\x1b[J`);
116
+ }
117
+
118
+ const lines = [];
119
+ lines.push(question.trimEnd());
120
+
121
+ if (searchable) {
122
+ lines.push(` \x1b[90mType to filter:\x1b[0m ${filter}\x1b[90m|\x1b[0m`);
123
+ }
124
+
125
+ const { items, startIdx } = getVisible();
126
+
127
+ if (startIdx > 0) lines.push(` \x1b[90m ... ${startIdx} more above\x1b[0m`);
128
+
129
+ items.forEach((opt, i) => {
130
+ const realIdx = startIdx + i;
131
+ const pointer = realIdx === cursor ? '\x1b[36m> \x1b[0m' : ' ';
132
+ const label = realIdx === cursor ? `\x1b[36m${opt.label}\x1b[0m` : opt.label;
133
+ const desc = opt.desc ? `\x1b[90m — ${opt.desc}\x1b[0m` : '';
134
+ lines.push(`${pointer}${label}${desc}`);
135
+ });
136
+
137
+ const endIdx = startIdx + items.length;
138
+ if (endIdx < filtered.length) lines.push(` \x1b[90m ... ${filtered.length - endIdx} more below\x1b[0m`);
139
+
140
+ if (filtered.length === 0) lines.push(' \x1b[33mNo matches\x1b[0m');
90
141
 
91
- function render() {
92
- // Move cursor up to clear previous render
93
- if (cursor !== -1) {
94
- process.stdout.write(`\x1b[${options.length + 1}A\x1b[J`);
142
+ process.stdout.write(`${lines.join('\n')}\n`);
143
+ lastRenderedLines = lines.length;
144
+ }
145
+
146
+ draw();
147
+
148
+ process.stdin.setRawMode(true);
149
+ process.stdin.resume();
150
+
151
+ const onKeypress = (key) => {
152
+ if (key[0] === 27 && key[1] === 91 && key[2] === 65) {
153
+ if (filtered.length > 0) cursor = (cursor - 1 + filtered.length) % filtered.length;
154
+ draw();
155
+ } else if (key[0] === 27 && key[1] === 91 && key[2] === 66) {
156
+ if (filtered.length > 0) cursor = (cursor + 1) % filtered.length;
157
+ draw();
158
+ } else if (key[0] === 13 || key[0] === 10) {
159
+ process.stdin.setRawMode(false);
160
+ process.stdin.removeListener('data', onKeypress);
161
+ if (filtered.length > 0) resolve(filtered[cursor].value);
162
+ else process.exit(0);
163
+ } else if (key[0] === 3 || (!searchable && key[0] === 113)) {
164
+ process.stdin.setRawMode(false);
165
+ process.stdin.removeListener('data', onKeypress);
166
+ process.exit(0);
167
+ } else if (searchable && (key[0] === 127 || key[0] === 8)) {
168
+ filter = filter.slice(0, -1);
169
+ filtered = allOptions.filter((o) => o.label.toLowerCase().includes(filter.toLowerCase()));
170
+ cursor = 0;
171
+ draw();
172
+ } else if (searchable && key[0] >= 32 && key[0] < 127 && key.length === 1) {
173
+ filter += String.fromCharCode(key[0]);
174
+ filtered = allOptions.filter((o) => o.label.toLowerCase().includes(filter.toLowerCase()));
175
+ cursor = 0;
176
+ draw();
95
177
  }
178
+ };
96
179
 
97
- console.log(question);
180
+ process.stdin.on('data', onKeypress);
181
+ });
182
+ }
183
+
184
+ async function fetchTablesFromDatabase() {
185
+ try {
186
+ // Resolve modules from the project's node_modules, not langaro-api's
187
+ const projectRoot = process.cwd();
188
+ const resolveFrom = (mod) => require(require.resolve(mod, { paths: [projectRoot] }));
189
+
190
+ resolveFrom('dotenv').config({ path: path.join(projectRoot, '.env') });
191
+
192
+ const {
193
+ DB_HOST, DB_USER, DB_PASSWORD, DB_NAME, DB_PORT,
194
+ } = process.env;
195
+
196
+ if (!DB_NAME) {
197
+ console.log('\x1b[33m Could not connect to database. DB_NAME not found in .env\x1b[0m');
198
+ return null;
199
+ }
200
+
201
+ const knex = resolveFrom('knex')({
202
+ client: 'mysql2',
203
+ connection: {
204
+ host: DB_HOST, user: DB_USER, password: DB_PASSWORD, database: DB_NAME, port: DB_PORT,
205
+ },
206
+ });
207
+
208
+ const results = await knex.raw(
209
+ 'SELECT table_name FROM information_schema.tables WHERE table_schema = ?',
210
+ [DB_NAME],
211
+ );
212
+
213
+ const tables = results[0].map((row) => row.TABLE_NAME).sort();
214
+ await knex.destroy();
215
+ return tables;
216
+ } catch (err) {
217
+ console.log(`\x1b[33m Could not connect to database: ${err.message}\x1b[0m`);
218
+ return null;
219
+ }
220
+ }
221
+
222
+ function multiSelect(rl, question, options) {
223
+ return new Promise((resolve) => {
224
+ const selected = new Set(options.map((_, i) => i));
225
+ let cursor = 0;
226
+ let lastRenderedLines = 0;
227
+
228
+ function draw() {
229
+ if (lastRenderedLines > 0) {
230
+ const moveUp = Math.min(lastRenderedLines, (process.stdout.rows || 24) - 1);
231
+ process.stdout.write(`\x1b[${moveUp}A\x1b[J`);
232
+ }
233
+ const lines = [];
234
+ lines.push(question);
98
235
  options.forEach((opt, i) => {
99
236
  const check = selected.has(i) ? '\x1b[32m[x]\x1b[0m' : '[ ]';
100
237
  const pointer = i === cursor ? '\x1b[36m> \x1b[0m' : ' ';
101
- console.log(`${pointer}${check} ${opt}`);
238
+ lines.push(`${pointer}${check} ${opt}`);
102
239
  });
240
+ process.stdout.write(`${lines.join('\n')}\n`);
241
+ lastRenderedLines = lines.length;
103
242
  }
104
243
 
105
- // Initial render
106
- console.log(question);
107
- options.forEach((opt, i) => {
108
- const check = selected.has(i) ? '\x1b[32m[x]\x1b[0m' : '[ ]';
109
- const pointer = i === cursor ? '\x1b[36m> \x1b[0m' : ' ';
110
- console.log(`${pointer}${check} ${opt}`);
111
- });
244
+ draw();
112
245
 
113
246
  process.stdin.setRawMode(true);
114
247
  process.stdin.resume();
115
248
 
116
249
  const onKeypress = (key) => {
117
- // Up arrow
118
250
  if (key[0] === 27 && key[1] === 91 && key[2] === 65) {
119
251
  cursor = (cursor - 1 + options.length) % options.length;
120
- render();
121
- // Down arrow
252
+ draw();
122
253
  } else if (key[0] === 27 && key[1] === 91 && key[2] === 66) {
123
254
  cursor = (cursor + 1) % options.length;
124
- render();
125
- // Space — toggle selection
255
+ draw();
126
256
  } else if (key[0] === 32) {
127
- if (selected.has(cursor)) {
128
- selected.delete(cursor);
129
- } else {
130
- selected.add(cursor);
131
- }
132
- render();
133
- // Enter — confirm
257
+ if (selected.has(cursor)) selected.delete(cursor);
258
+ else selected.add(cursor);
259
+ draw();
134
260
  } else if (key[0] === 13 || key[0] === 10) {
135
261
  process.stdin.setRawMode(false);
136
262
  process.stdin.removeListener('data', onKeypress);
137
263
  resolve(options.filter((_, i) => selected.has(i)));
138
- // q or Ctrl+C — abort
139
264
  } else if (key[0] === 113 || key[0] === 3) {
140
265
  process.stdin.setRawMode(false);
141
266
  process.stdin.removeListener('data', onKeypress);
@@ -152,24 +277,46 @@ async function run(config = {}) {
152
277
  const root = cfg.root || process.cwd();
153
278
  const resolve = (dir) => path.resolve(root, dir);
154
279
 
155
- const rl = createInterface();
156
-
157
- const name = await ask(rl, '\n\x1b[36mResource name\x1b[0m (snake_case, e.g. products, user_invoices): ');
280
+ // Step 1: Choose how to pick the resource name
281
+ console.log('');
282
+ const source = await singleSelect('\x1b[36mHow to pick the resource name?\x1b[0m', [
283
+ { label: 'Enter manually', value: 'manual', desc: 'Type a snake_case name' },
284
+ { label: 'Fetch from database', value: 'db', desc: 'Select a table from your database' },
285
+ ]);
286
+
287
+ let name;
288
+
289
+ if (source === 'db') {
290
+ console.log('\n Connecting to database...');
291
+ const tables = await fetchTablesFromDatabase();
292
+
293
+ if (!tables || tables.length === 0) {
294
+ console.log('\x1b[31m No tables found.\x1b[0m Falling back to manual input.\n');
295
+ const rl = createInterface();
296
+ name = await ask(rl, '\x1b[36mResource name\x1b[0m (snake_case, e.g. products, user_invoices): ');
297
+ rl.close();
298
+ } else {
299
+ console.log('');
300
+ const tableOptions = tables.map((t) => ({ label: t, value: t }));
301
+ name = await singleSelect(` Found ${tables.length} tables. Select one:`, tableOptions);
302
+ }
303
+ } else {
304
+ const rl = createInterface();
305
+ name = await ask(rl, '\n\x1b[36mResource name\x1b[0m (snake_case, e.g. products, user_invoices): ');
306
+ rl.close();
307
+ }
158
308
 
159
309
  if (!name) {
160
310
  console.log('No name provided. Aborting.');
161
- rl.close();
162
311
  process.exit(0);
163
312
  }
164
313
 
165
314
  // Validate name
166
315
  if (!/^[a-z][a-z0-9_]*$/.test(name)) {
167
316
  console.log('\x1b[31mInvalid name.\x1b[0m Use snake_case (e.g. products, user_invoices)');
168
- rl.close();
169
317
  process.exit(1);
170
318
  }
171
319
 
172
- rl.close();
173
320
  console.log('');
174
321
 
175
322
  const choices = await multiSelect(
@@ -244,7 +391,10 @@ async function run(config = {}) {
244
391
  }
245
392
 
246
393
  if (created.length > 0) {
247
- console.log(`\n\x1b[32mDone!\x1b[0m Created ${created.length} file(s). Run \x1b[36mlangaro-api\x1b[0m to regenerate types.\n`);
394
+ // Auto-regenerate types so the new resource gets IntelliSense immediately
395
+ const { generateTypes } = require('../index');
396
+ generateTypes(cfg);
397
+ console.log(`\n\x1b[32mDone!\x1b[0m Created ${created.length} file(s). Types regenerated.\n`);
248
398
  } else {
249
399
  console.log('\nNo files created (all already exist).\n');
250
400
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "langaro-api",
3
- "version": "1.0.1",
3
+ "version": "1.0.4",
4
4
  "description": "Auto-generate TypeScript types, JSDoc annotations, and boilerplate loaders for knex-extended-crud projects",
5
5
  "main": "lib/index.js",
6
6
  "bin": {