@objectql/cli 1.6.1 → 1.7.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.
@@ -42,7 +42,7 @@ function openBrowser(url: string) {
42
42
  }
43
43
 
44
44
  export async function startStudio(options: { port: number; dir: string, open?: boolean }) {
45
- const port = options.port || 3000;
45
+ const startPort = options.port || 5555;
46
46
  const rootDir = path.resolve(process.cwd(), options.dir || '.');
47
47
 
48
48
  console.log(chalk.blue('Starting ObjectQL Studio...'));
@@ -78,6 +78,72 @@ export async function startStudio(options: { port: number; dir: string, open?: b
78
78
  process.exit(1);
79
79
  }
80
80
 
81
+ // Initialize App if it's a configuration object
82
+ if (typeof (app as any).init !== 'function') {
83
+ const config = app as any;
84
+ console.log(chalk.gray('Configuration object detected. Initializing ObjectQL instance...'));
85
+
86
+ const datasources: any = {};
87
+
88
+ if (config.datasource && config.datasource.default) {
89
+ const dbConfig = config.datasource.default;
90
+ if (dbConfig.type === 'sqlite') {
91
+ try {
92
+ const { SqlDriver } = require('@objectql/driver-sql');
93
+ datasources.default = new SqlDriver({
94
+ client: 'sqlite3',
95
+ connection: {
96
+ filename: dbConfig.filename ? path.resolve(rootDir, dbConfig.filename) : ':memory:'
97
+ },
98
+ useNullAsDefault: true
99
+ });
100
+ } catch (e) {
101
+ console.warn(chalk.yellow('Failed to load @objectql/driver-sql. Ensure it is installed.'));
102
+ }
103
+ }
104
+ }
105
+
106
+ // Fallback to memory if no datasource
107
+ if (!datasources.default) {
108
+ console.warn(chalk.yellow('No valid datasource found. Using in-memory SQLite.'));
109
+ const { SqlDriver } = require('@objectql/driver-sql');
110
+ datasources.default = new SqlDriver({
111
+ client: 'sqlite3',
112
+ connection: { filename: ':memory:' },
113
+ useNullAsDefault: true
114
+ });
115
+ }
116
+
117
+ app = new ObjectQL({ datasources });
118
+
119
+ // Load Schema
120
+ const loader = new ObjectLoader(app.metadata);
121
+
122
+ // Load Presets
123
+ if (Array.isArray(config.presets)) {
124
+ console.log(chalk.gray(`Loading ${config.presets.length} presets...`));
125
+ for (const preset of config.presets) {
126
+ try {
127
+ loader.loadPackage(preset);
128
+ } catch (e: any) {
129
+ console.warn(chalk.yellow(`Failed to load preset ${preset}:`), e.message);
130
+ }
131
+ }
132
+ }
133
+
134
+ // Load Schema from Directory
135
+ // In a monorepo root with presets, scanning everything is dangerous.
136
+ // We check if we are in a monorepo-like environment.
137
+ const isMonorepoRoot = fs.existsSync(path.join(rootDir, 'pnpm-workspace.yaml')) || fs.existsSync(path.join(rootDir, 'pnpm-lock.yaml'));
138
+
139
+ // If we are in a likely monorepo root AND have presets, skip recursive scan
140
+ if (isMonorepoRoot && config.presets && config.presets.length > 0) {
141
+ console.log(chalk.yellow('Monorepo root detected with presets. Skipping recursive file scan of root directory to avoid conflicts.'));
142
+ } else {
143
+ loader.load(rootDir);
144
+ }
145
+ }
146
+
81
147
  // 2. Load Schema & Init
82
148
  try {
83
149
  await app.init();
@@ -254,13 +320,35 @@ export async function startStudio(options: { port: number; dir: string, open?: b
254
320
  res.end('Not Found');
255
321
  });
256
322
 
257
- server.listen(port, () => {
258
- const url = `http://localhost:${port}/studio`;
259
- console.log(chalk.green(`\nšŸš€ Studio running at: ${chalk.bold(url)}`));
260
- console.log(chalk.gray(` API endpoint: http://localhost:${port}/api`));
261
-
262
- if (options.open) {
263
- openBrowser(url);
264
- }
265
- });
323
+ const tryListen = (port: number) => {
324
+ server.removeAllListeners('error');
325
+ server.removeAllListeners('listening'); // Prevent stacking callbacks
326
+
327
+ server.on('error', (e: any) => {
328
+ if (e.code === 'EADDRINUSE') {
329
+ if (port - startPort < 10) {
330
+ console.log(chalk.yellow(`Port ${port} is in use, trying ${port + 1}...`));
331
+ server.close();
332
+ tryListen(port + 1);
333
+ } else {
334
+ console.error(chalk.red(`āŒ Unable to find a free port.`));
335
+ process.exit(1);
336
+ }
337
+ } else {
338
+ console.error(chalk.red('āŒ Server error:'), e);
339
+ }
340
+ });
341
+
342
+ server.listen(port, () => {
343
+ const url = `http://localhost:${port}/studio`;
344
+ console.log(chalk.green(`\nšŸš€ Studio running at: ${chalk.bold(url)}`));
345
+ console.log(chalk.gray(` API endpoint: http://localhost:${port}/api`));
346
+
347
+ if (options.open) {
348
+ openBrowser(url);
349
+ }
350
+ });
351
+ };
352
+
353
+ tryListen(startPort);
266
354
  }
package/src/index.ts CHANGED
@@ -3,14 +3,51 @@ import { generateTypes } from './commands/generate';
3
3
  import { startRepl } from './commands/repl';
4
4
  import { serve } from './commands/serve';
5
5
  import { startStudio } from './commands/studio';
6
+ import { initProject } from './commands/init';
7
+ import { newMetadata } from './commands/new';
8
+ import { i18nExtract, i18nInit, i18nValidate } from './commands/i18n';
9
+ import { migrate, migrateCreate, migrateStatus } from './commands/migrate';
6
10
 
7
11
  const program = new Command();
8
12
 
9
13
  program
10
14
  .name('objectql')
11
15
  .description('ObjectQL CLI tool')
12
- .version('0.1.0');
16
+ .version('1.5.0');
13
17
 
18
+ // Init command - Create new project
19
+ program
20
+ .command('init')
21
+ .description('Create a new ObjectQL project from template')
22
+ .option('-t, --template <template>', 'Template to use (basic, express-api, enterprise)', 'basic')
23
+ .option('-n, --name <name>', 'Project name')
24
+ .option('-d, --dir <path>', 'Target directory')
25
+ .option('--skip-install', 'Skip dependency installation')
26
+ .option('--skip-git', 'Skip git initialization')
27
+ .action(async (options) => {
28
+ try {
29
+ await initProject(options);
30
+ } catch (error) {
31
+ console.error(error);
32
+ process.exit(1);
33
+ }
34
+ });
35
+
36
+ // New command - Generate metadata files
37
+ program
38
+ .command('new <type> <name>')
39
+ .description('Generate a new metadata file (object, view, form, etc.)')
40
+ .option('-d, --dir <path>', 'Output directory', '.')
41
+ .action(async (type, name, options) => {
42
+ try {
43
+ await newMetadata({ type, name, dir: options.dir });
44
+ } catch (error) {
45
+ console.error(error);
46
+ process.exit(1);
47
+ }
48
+ });
49
+
50
+ // Generate command - Generate TypeScript types
14
51
  program
15
52
  .command('generate')
16
53
  .alias('g')
@@ -26,6 +63,96 @@ program
26
63
  }
27
64
  });
28
65
 
66
+ // I18n commands
67
+ const i18nCmd = program
68
+ .command('i18n')
69
+ .description('Internationalization commands');
70
+
71
+ i18nCmd
72
+ .command('extract')
73
+ .description('Extract translatable strings from metadata files')
74
+ .option('-s, --source <path>', 'Source directory', '.')
75
+ .option('-o, --output <path>', 'Output directory', './src/i18n')
76
+ .option('-l, --lang <lang>', 'Language code', 'en')
77
+ .action(async (options) => {
78
+ try {
79
+ await i18nExtract(options);
80
+ } catch (error) {
81
+ console.error(error);
82
+ process.exit(1);
83
+ }
84
+ });
85
+
86
+ i18nCmd
87
+ .command('init <lang>')
88
+ .description('Initialize i18n for a new language')
89
+ .option('-b, --base-dir <path>', 'Base i18n directory', './src/i18n')
90
+ .action(async (lang, options) => {
91
+ try {
92
+ await i18nInit({ lang, baseDir: options.baseDir });
93
+ } catch (error) {
94
+ console.error(error);
95
+ process.exit(1);
96
+ }
97
+ });
98
+
99
+ i18nCmd
100
+ .command('validate <lang>')
101
+ .description('Validate translation completeness')
102
+ .option('-b, --base-dir <path>', 'Base i18n directory', './src/i18n')
103
+ .option('--base-lang <lang>', 'Base language to compare against', 'en')
104
+ .action(async (lang, options) => {
105
+ try {
106
+ await i18nValidate({ lang, baseDir: options.baseDir, baseLang: options.baseLang });
107
+ } catch (error) {
108
+ console.error(error);
109
+ process.exit(1);
110
+ }
111
+ });
112
+
113
+ // Migration commands
114
+ const migrateCmd = program
115
+ .command('migrate')
116
+ .description('Run pending database migrations')
117
+ .option('-c, --config <path>', 'Path to objectql.config.ts/js')
118
+ .option('-d, --dir <path>', 'Migrations directory', './migrations')
119
+ .action(async (options) => {
120
+ try {
121
+ await migrate(options);
122
+ } catch (error) {
123
+ console.error(error);
124
+ process.exit(1);
125
+ }
126
+ });
127
+
128
+ migrateCmd
129
+ .command('create <name>')
130
+ .description('Create a new migration file')
131
+ .option('-d, --dir <path>', 'Migrations directory', './migrations')
132
+ .action(async (name, options) => {
133
+ try {
134
+ await migrateCreate({ name, dir: options.dir });
135
+ } catch (error) {
136
+ console.error(error);
137
+ process.exit(1);
138
+ }
139
+ });
140
+
141
+ migrateCmd
142
+ .command('status')
143
+ .description('Show migration status')
144
+ .option('-c, --config <path>', 'Path to objectql.config.ts/js')
145
+ .option('-d, --dir <path>', 'Migrations directory', './migrations')
146
+ .action(async (options) => {
147
+ try {
148
+ await migrateStatus(options);
149
+ } catch (error) {
150
+ console.error(error);
151
+ process.exit(1);
152
+ }
153
+ });
154
+
155
+ // REPL command
29
156
  program
30
157
  .command('repl')
31
158
  .alias('r')
@@ -35,6 +162,7 @@ program
35
162
  await startRepl(options.config);
36
163
  });
37
164
 
165
+ // Serve command
38
166
  program
39
167
  .command('serve')
40
168
  .alias('s')
@@ -45,11 +173,12 @@ program
45
173
  await serve({ port: parseInt(options.port), dir: options.dir });
46
174
  });
47
175
 
176
+ // Studio command
48
177
  program
49
178
  .command('studio')
50
179
  .alias('ui')
51
180
  .description('Start the ObjectQL Studio')
52
- .option('-p, --port <number>', 'Port to listen on', '3000')
181
+ .option('-p, --port <number>', 'Port to listen on', '5555')
53
182
  .option('-d, --dir <path>', 'Directory containing schema', '.')
54
183
  .option('--no-open', 'Do not open browser automatically')
55
184
  .action(async (options) => {