@geekmidas/cli 0.7.0 → 0.9.0

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.
@@ -0,0 +1,204 @@
1
+ import { mkdir, readFile, rm } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { e } from '@geekmidas/constructs/endpoints';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+ import type { BuildContext } from '../build/types';
6
+ import { EndpointGenerator } from '../generators/EndpointGenerator';
7
+ import type { GeneratedConstruct } from '../generators/Generator';
8
+
9
+ // Create a minimal mock endpoint for testing
10
+ const mockEndpoint = e.get('/test').handle(async () => ({ ok: true }));
11
+
12
+ describe('EndpointGenerator hooks generation', () => {
13
+ const testOutputDir = join(process.cwd(), '.test-output');
14
+ let generator: EndpointGenerator;
15
+
16
+ // Mock endpoint construct for testing
17
+ const mockConstruct: GeneratedConstruct<typeof mockEndpoint> = {
18
+ key: 'testEndpoint',
19
+ construct: mockEndpoint,
20
+ path: {
21
+ relative: '/project/src/endpoints/test.ts',
22
+ absolute: '/project/src/endpoints/test.ts',
23
+ },
24
+ };
25
+
26
+ beforeEach(async () => {
27
+ generator = new EndpointGenerator();
28
+ await mkdir(testOutputDir, { recursive: true });
29
+ });
30
+
31
+ afterEach(async () => {
32
+ await rm(testOutputDir, { recursive: true, force: true });
33
+ });
34
+
35
+ const baseContext: BuildContext = {
36
+ envParserPath: '/project/src/config/env.ts',
37
+ envParserImportPattern: '{ envParser }',
38
+ loggerPath: '/project/src/config/logger.ts',
39
+ loggerImportPattern: 'logger',
40
+ };
41
+
42
+ describe('generateAppFile', () => {
43
+ it('should not include hooks when hooks config is undefined', async () => {
44
+ await generator.build(baseContext, [mockConstruct], testOutputDir, {
45
+ provider: 'server',
46
+ });
47
+
48
+ const appContent = await readFile(join(testOutputDir, 'app.ts'), 'utf-8');
49
+
50
+ expect(appContent).not.toContain('serverHooks');
51
+ expect(appContent).not.toContain('beforeSetup');
52
+ expect(appContent).not.toContain('afterSetup');
53
+ });
54
+
55
+ it('should include hooks imports when hooks config is provided', async () => {
56
+ const contextWithHooks: BuildContext = {
57
+ ...baseContext,
58
+ hooks: {
59
+ serverHooksPath: '/project/src/config/hooks.ts',
60
+ },
61
+ };
62
+
63
+ await generator.build(contextWithHooks, [mockConstruct], testOutputDir, {
64
+ provider: 'server',
65
+ });
66
+
67
+ const appContent = await readFile(join(testOutputDir, 'app.ts'), 'utf-8');
68
+
69
+ expect(appContent).toContain('import * as serverHooks from');
70
+ expect(appContent).toContain('hooks.ts');
71
+ });
72
+
73
+ it('should include beforeSetup hook call', async () => {
74
+ const contextWithHooks: BuildContext = {
75
+ ...baseContext,
76
+ hooks: {
77
+ serverHooksPath: '/project/src/config/hooks.ts',
78
+ },
79
+ };
80
+
81
+ await generator.build(contextWithHooks, [mockConstruct], testOutputDir, {
82
+ provider: 'server',
83
+ });
84
+
85
+ const appContent = await readFile(join(testOutputDir, 'app.ts'), 'utf-8');
86
+
87
+ expect(appContent).toContain(
88
+ "if (typeof serverHooks.beforeSetup === 'function')",
89
+ );
90
+ expect(appContent).toContain(
91
+ 'await serverHooks.beforeSetup(honoApp, { envParser, logger })',
92
+ );
93
+ });
94
+
95
+ it('should include afterSetup hook call', async () => {
96
+ const contextWithHooks: BuildContext = {
97
+ ...baseContext,
98
+ hooks: {
99
+ serverHooksPath: '/project/src/config/hooks.ts',
100
+ },
101
+ };
102
+
103
+ await generator.build(contextWithHooks, [mockConstruct], testOutputDir, {
104
+ provider: 'server',
105
+ });
106
+
107
+ const appContent = await readFile(join(testOutputDir, 'app.ts'), 'utf-8');
108
+
109
+ expect(appContent).toContain(
110
+ "if (typeof serverHooks.afterSetup === 'function')",
111
+ );
112
+ expect(appContent).toContain(
113
+ 'await serverHooks.afterSetup(honoApp, { envParser, logger })',
114
+ );
115
+ });
116
+
117
+ it('should place telescope before beforeSetup, and beforeSetup before endpoints', async () => {
118
+ const contextWithHooks: BuildContext = {
119
+ ...baseContext,
120
+ hooks: {
121
+ serverHooksPath: '/project/src/config/hooks.ts',
122
+ },
123
+ telescope: {
124
+ enabled: true,
125
+ path: '/__telescope',
126
+ maxEntries: 100,
127
+ recordBody: true,
128
+ ignore: [],
129
+ websocket: false,
130
+ },
131
+ };
132
+
133
+ await generator.build(contextWithHooks, [mockConstruct], testOutputDir, {
134
+ provider: 'server',
135
+ });
136
+
137
+ const appContent = await readFile(join(testOutputDir, 'app.ts'), 'utf-8');
138
+
139
+ // Use specific patterns to find the actual calls (not imports/comments)
140
+ const telescopeIndex = appContent.indexOf('createMiddleware(telescope)');
141
+ const beforeSetupCallIndex = appContent.indexOf(
142
+ 'serverHooks.beforeSetup(honoApp',
143
+ );
144
+ const setupEndpointsIndex = appContent.indexOf(
145
+ 'await setupEndpoints(honoApp',
146
+ );
147
+
148
+ expect(telescopeIndex).toBeGreaterThan(0);
149
+ expect(beforeSetupCallIndex).toBeGreaterThan(0);
150
+ expect(setupEndpointsIndex).toBeGreaterThan(0);
151
+ // Telescope middleware first (to capture all requests)
152
+ expect(telescopeIndex).toBeLessThan(beforeSetupCallIndex);
153
+ // Then beforeSetup hook
154
+ expect(beforeSetupCallIndex).toBeLessThan(setupEndpointsIndex);
155
+ });
156
+
157
+ it('should place afterSetup after setupEndpoints', async () => {
158
+ const contextWithHooks: BuildContext = {
159
+ ...baseContext,
160
+ hooks: {
161
+ serverHooksPath: '/project/src/config/hooks.ts',
162
+ },
163
+ };
164
+
165
+ await generator.build(contextWithHooks, [mockConstruct], testOutputDir, {
166
+ provider: 'server',
167
+ });
168
+
169
+ const appContent = await readFile(join(testOutputDir, 'app.ts'), 'utf-8');
170
+
171
+ // Use specific patterns to find the actual calls (not imports/comments)
172
+ const setupEndpointsIndex = appContent.indexOf(
173
+ 'await setupEndpoints(honoApp',
174
+ );
175
+ const afterSetupCallIndex = appContent.indexOf(
176
+ 'serverHooks.afterSetup(honoApp',
177
+ );
178
+
179
+ expect(setupEndpointsIndex).toBeGreaterThan(0);
180
+ expect(afterSetupCallIndex).toBeGreaterThan(0);
181
+ expect(afterSetupCallIndex).toBeGreaterThan(setupEndpointsIndex);
182
+ });
183
+
184
+ it('should generate correct relative import path for hooks', async () => {
185
+ const contextWithHooks: BuildContext = {
186
+ ...baseContext,
187
+ hooks: {
188
+ serverHooksPath: join(testOutputDir, '../../src/config/hooks.ts'),
189
+ },
190
+ };
191
+
192
+ await generator.build(contextWithHooks, [mockConstruct], testOutputDir, {
193
+ provider: 'server',
194
+ });
195
+
196
+ const appContent = await readFile(join(testOutputDir, 'app.ts'), 'utf-8');
197
+
198
+ // Should have a relative import path
199
+ expect(appContent).toMatch(
200
+ /import \* as serverHooks from '\.\.\/.*hooks\.ts'/,
201
+ );
202
+ });
203
+ });
204
+ });
@@ -0,0 +1,63 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { normalizeHooksConfig } from '../dev';
3
+
4
+ describe('normalizeHooksConfig', () => {
5
+ const originalCwd = process.cwd();
6
+
7
+ beforeEach(() => {
8
+ vi.spyOn(process, 'cwd').mockReturnValue('/test/project');
9
+ });
10
+
11
+ afterEach(() => {
12
+ vi.restoreAllMocks();
13
+ });
14
+
15
+ it('should return undefined when hooks config is undefined', () => {
16
+ const result = normalizeHooksConfig(undefined);
17
+ expect(result).toBeUndefined();
18
+ });
19
+
20
+ it('should return undefined when hooks config is empty object', () => {
21
+ const result = normalizeHooksConfig({});
22
+ expect(result).toBeUndefined();
23
+ });
24
+
25
+ it('should return undefined when server hooks path is not provided', () => {
26
+ const result = normalizeHooksConfig({ server: undefined });
27
+ expect(result).toBeUndefined();
28
+ });
29
+
30
+ it('should normalize path with .ts extension', () => {
31
+ const result = normalizeHooksConfig({ server: './src/hooks.ts' });
32
+
33
+ expect(result).toBeDefined();
34
+ expect(result!.serverHooksPath).toBe('/test/project/src/hooks.ts');
35
+ });
36
+
37
+ it('should add .ts extension when missing', () => {
38
+ const result = normalizeHooksConfig({ server: './src/hooks' });
39
+
40
+ expect(result).toBeDefined();
41
+ expect(result!.serverHooksPath).toBe('/test/project/src/hooks.ts');
42
+ });
43
+
44
+ it('should resolve relative paths from cwd', () => {
45
+ const result = normalizeHooksConfig({ server: 'config/server-hooks' });
46
+
47
+ expect(result).toBeDefined();
48
+ expect(result!.serverHooksPath).toBe(
49
+ '/test/project/config/server-hooks.ts',
50
+ );
51
+ });
52
+
53
+ it('should handle nested directory paths', () => {
54
+ const result = normalizeHooksConfig({
55
+ server: './src/config/hooks/server',
56
+ });
57
+
58
+ expect(result).toBeDefined();
59
+ expect(result!.serverHooksPath).toBe(
60
+ '/test/project/src/config/hooks/server.ts',
61
+ );
62
+ });
63
+ });
@@ -5,7 +5,7 @@ import type { Endpoint } from '@geekmidas/constructs/endpoints';
5
5
  import type { Function } from '@geekmidas/constructs/functions';
6
6
  import type { Subscriber } from '@geekmidas/constructs/subscribers';
7
7
  import { loadConfig, parseModuleConfig } from '../config';
8
- import { normalizeTelescopeConfig } from '../dev';
8
+ import { normalizeHooksConfig, normalizeTelescopeConfig } from '../dev';
9
9
  import {
10
10
  CronGenerator,
11
11
  EndpointGenerator,
@@ -55,12 +55,19 @@ export async function buildCommand(options: BuildOptions): Promise<void> {
55
55
  logger.log(`🔭 Telescope enabled at ${telescope.path}`);
56
56
  }
57
57
 
58
+ // Normalize hooks configuration
59
+ const hooks = normalizeHooksConfig(config.hooks);
60
+ if (hooks) {
61
+ logger.log(`🪝 Server hooks enabled`);
62
+ }
63
+
58
64
  const buildContext: BuildContext = {
59
65
  envParserPath,
60
66
  envParserImportPattern,
61
67
  loggerPath,
62
68
  loggerImportPattern,
63
69
  telescope,
70
+ hooks,
64
71
  };
65
72
 
66
73
  // Initialize generators
@@ -50,6 +50,11 @@ export interface NormalizedStudioConfig {
50
50
  schema: string;
51
51
  }
52
52
 
53
+ export interface NormalizedHooksConfig {
54
+ /** Path to server hooks module */
55
+ serverHooksPath: string;
56
+ }
57
+
53
58
  export interface BuildContext {
54
59
  envParserPath: string;
55
60
  envParserImportPattern: string;
@@ -57,6 +62,7 @@ export interface BuildContext {
57
62
  loggerImportPattern: string;
58
63
  telescope?: NormalizedTelescopeConfig;
59
64
  studio?: NormalizedStudioConfig;
65
+ hooks?: NormalizedHooksConfig;
60
66
  }
61
67
 
62
68
  export interface ProviderBuildResult {
package/src/dev/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type ChildProcess, spawn } from 'node:child_process';
1
+ import { type ChildProcess, execSync, spawn } from 'node:child_process';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { mkdir } from 'node:fs/promises';
4
4
  import { createServer } from 'node:net';
@@ -9,6 +9,7 @@ import fg from 'fast-glob';
9
9
  import { resolveProviders } from '../build/providerResolver';
10
10
  import type {
11
11
  BuildContext,
12
+ NormalizedHooksConfig,
12
13
  NormalizedStudioConfig,
13
14
  NormalizedTelescopeConfig,
14
15
  } from '../build/types';
@@ -19,7 +20,11 @@ import {
19
20
  FunctionGenerator,
20
21
  SubscriberGenerator,
21
22
  } from '../generators';
22
- import { generateOpenApi, resolveOpenApiConfig } from '../openapi';
23
+ import {
24
+ OPENAPI_OUTPUT_PATH,
25
+ generateOpenApi,
26
+ resolveOpenApiConfig,
27
+ } from '../openapi';
23
28
  import type {
24
29
  GkmConfig,
25
30
  LegacyProvider,
@@ -200,8 +205,32 @@ export function normalizeStudioConfig(
200
205
  };
201
206
  }
202
207
 
208
+ /**
209
+ * Normalize hooks configuration
210
+ * @internal Exported for testing
211
+ */
212
+ export function normalizeHooksConfig(
213
+ config: GkmConfig['hooks'],
214
+ ): NormalizedHooksConfig | undefined {
215
+ if (!config?.server) {
216
+ return undefined;
217
+ }
218
+
219
+ // Resolve the path (handle .ts extension)
220
+ const serverPath = config.server.endsWith('.ts')
221
+ ? config.server
222
+ : `${config.server}.ts`;
223
+
224
+ const resolvedPath = resolve(process.cwd(), serverPath);
225
+
226
+ return {
227
+ serverHooksPath: resolvedPath,
228
+ };
229
+ }
230
+
203
231
  export interface DevOptions {
204
232
  port?: number;
233
+ portExplicit?: boolean;
205
234
  enableOpenApi?: boolean;
206
235
  }
207
236
 
@@ -260,12 +289,18 @@ export async function devCommand(options: DevOptions): Promise<void> {
260
289
  logger.log(`🗄️ Studio enabled at ${studio.path}`);
261
290
  }
262
291
 
292
+ // Normalize hooks configuration
293
+ const hooks = normalizeHooksConfig(config.hooks);
294
+ if (hooks) {
295
+ logger.log(`🪝 Server hooks enabled from ${config.hooks?.server}`);
296
+ }
297
+
263
298
  // Resolve OpenAPI configuration
264
299
  const openApiConfig = resolveOpenApiConfig(config);
265
300
  // Enable OpenAPI docs endpoint if either root config or provider config enables it
266
301
  const enableOpenApi = openApiConfig.enabled || resolved.enableOpenApi;
267
302
  if (enableOpenApi) {
268
- logger.log(`📄 OpenAPI output: ${openApiConfig.output}`);
303
+ logger.log(`📄 OpenAPI output: ${OPENAPI_OUTPUT_PATH}`);
269
304
  }
270
305
 
271
306
  const buildContext: BuildContext = {
@@ -275,6 +310,7 @@ export async function devCommand(options: DevOptions): Promise<void> {
275
310
  loggerImportPattern,
276
311
  telescope,
277
312
  studio,
313
+ hooks,
278
314
  };
279
315
 
280
316
  // Build initial version
@@ -297,6 +333,7 @@ export async function devCommand(options: DevOptions): Promise<void> {
297
333
  const devServer = new DevServer(
298
334
  resolved.providers[0] as LegacyProvider,
299
335
  options.port || 3000,
336
+ options.portExplicit ?? false,
300
337
  enableOpenApi,
301
338
  telescope,
302
339
  studio,
@@ -309,6 +346,9 @@ export async function devCommand(options: DevOptions): Promise<void> {
309
346
  const envParserFile = config.envParser.split('#')[0];
310
347
  const loggerFile = config.logger.split('#')[0];
311
348
 
349
+ // Get hooks file path for watching
350
+ const hooksFile = config.hooks?.server?.split('#')[0];
351
+
312
352
  const watchPatterns = [
313
353
  config.routes,
314
354
  ...(config.functions ? [config.functions] : []),
@@ -317,6 +357,10 @@ export async function devCommand(options: DevOptions): Promise<void> {
317
357
  // Add .ts extension if not present for config files
318
358
  envParserFile.endsWith('.ts') ? envParserFile : `${envParserFile}.ts`,
319
359
  loggerFile.endsWith('.ts') ? loggerFile : `${loggerFile}.ts`,
360
+ // Add hooks file to watch list
361
+ ...(hooksFile
362
+ ? [hooksFile.endsWith('.ts') ? hooksFile : `${hooksFile}.ts`]
363
+ : []),
320
364
  ].flat();
321
365
 
322
366
  // Normalize patterns - remove leading ./ when using cwd option
@@ -391,11 +435,21 @@ export async function devCommand(options: DevOptions): Promise<void> {
391
435
  });
392
436
 
393
437
  // Handle graceful shutdown
394
- const shutdown = async () => {
438
+ let isShuttingDown = false;
439
+ const shutdown = () => {
440
+ if (isShuttingDown) return;
441
+ isShuttingDown = true;
442
+
395
443
  logger.log('\n🛑 Shutting down...');
396
- await watcher.close();
397
- await devServer.stop();
398
- process.exit(0);
444
+
445
+ // Use sync-style shutdown to ensure it completes before exit
446
+ Promise.all([watcher.close(), devServer.stop()])
447
+ .catch((err) => {
448
+ logger.error('Error during shutdown:', err);
449
+ })
450
+ .finally(() => {
451
+ process.exit(0);
452
+ });
399
453
  };
400
454
 
401
455
  process.on('SIGINT', shutdown);
@@ -447,6 +501,7 @@ class DevServer {
447
501
  constructor(
448
502
  private provider: LegacyProvider,
449
503
  private requestedPort: number,
504
+ private portExplicit: boolean,
450
505
  private enableOpenApi: boolean,
451
506
  private telescope?: NormalizedTelescopeConfig,
452
507
  private studio?: NormalizedStudioConfig,
@@ -460,13 +515,26 @@ class DevServer {
460
515
  await this.stop();
461
516
  }
462
517
 
463
- // Find an available port
464
- this.actualPort = await findAvailablePort(this.requestedPort);
518
+ // Check port availability
519
+ if (this.portExplicit) {
520
+ // Port was explicitly specified - throw if unavailable
521
+ const available = await isPortAvailable(this.requestedPort);
522
+ if (!available) {
523
+ throw new Error(
524
+ `Port ${this.requestedPort} is already in use. ` +
525
+ `Either stop the process using that port or omit -p/--port to auto-select an available port.`,
526
+ );
527
+ }
528
+ this.actualPort = this.requestedPort;
529
+ } else {
530
+ // Find an available port starting from the default
531
+ this.actualPort = await findAvailablePort(this.requestedPort);
465
532
 
466
- if (this.actualPort !== this.requestedPort) {
467
- logger.log(
468
- `ℹ️ Port ${this.requestedPort} was in use, using port ${this.actualPort} instead`,
469
- );
533
+ if (this.actualPort !== this.requestedPort) {
534
+ logger.log(
535
+ `ℹ️ Port ${this.requestedPort} was in use, using port ${this.actualPort} instead`,
536
+ );
537
+ }
470
538
  }
471
539
 
472
540
  const serverEntryPath = join(
@@ -530,40 +598,41 @@ class DevServer {
530
598
  }
531
599
 
532
600
  async stop(): Promise<void> {
601
+ const port = this.actualPort;
602
+
533
603
  if (this.serverProcess && this.isRunning) {
534
604
  const pid = this.serverProcess.pid;
535
605
 
536
- // Kill the entire process group (negative PID kills the group)
606
+ // Use SIGKILL directly since the server ignores SIGTERM
537
607
  if (pid) {
538
608
  try {
539
- process.kill(-pid, 'SIGTERM');
609
+ process.kill(-pid, 'SIGKILL');
540
610
  } catch {
541
- // Process might already be dead
611
+ try {
612
+ process.kill(pid, 'SIGKILL');
613
+ } catch {
614
+ // Process might already be dead
615
+ }
542
616
  }
543
617
  }
544
618
 
545
- // Wait for process to exit
546
- await new Promise<void>((resolve) => {
547
- const timeout = setTimeout(() => {
548
- if (pid) {
549
- try {
550
- process.kill(-pid, 'SIGKILL');
551
- } catch {
552
- // Process might already be dead
553
- }
554
- }
555
- resolve();
556
- }, 3000);
557
-
558
- this.serverProcess?.on('exit', () => {
559
- clearTimeout(timeout);
560
- resolve();
561
- });
562
- });
563
-
564
619
  this.serverProcess = null;
565
620
  this.isRunning = false;
566
621
  }
622
+
623
+ // Also kill any processes still holding the port
624
+ this.killProcessesOnPort(port);
625
+ }
626
+
627
+ private killProcessesOnPort(port: number): void {
628
+ try {
629
+ // Use lsof to find PIDs on the port and kill them with -9
630
+ execSync(`lsof -ti tcp:${port} | xargs kill -9 2>/dev/null || true`, {
631
+ stdio: 'ignore',
632
+ });
633
+ } catch {
634
+ // Ignore errors - port may already be free
635
+ }
567
636
  }
568
637
 
569
638
  async restart(): Promise<void> {
@@ -384,6 +384,30 @@ import { createStudioApp } from '@geekmidas/studio/server/hono';`;
384
384
  }
385
385
  }
386
386
 
387
+ // Generate imports for server hooks
388
+ let hooksImports = '';
389
+ let beforeSetupCall = '';
390
+ let afterSetupCall = '';
391
+ if (context.hooks?.serverHooksPath) {
392
+ const relativeHooksPath = relative(
393
+ dirname(appPath),
394
+ context.hooks.serverHooksPath,
395
+ );
396
+ hooksImports = `import * as serverHooks from '${relativeHooksPath}';`;
397
+ beforeSetupCall = `
398
+ // Call beforeSetup hook if defined
399
+ if (typeof serverHooks.beforeSetup === 'function') {
400
+ await serverHooks.beforeSetup(honoApp, { envParser, logger });
401
+ }
402
+ `;
403
+ afterSetupCall = `
404
+ // Call afterSetup hook if defined
405
+ if (typeof serverHooks.afterSetup === 'function') {
406
+ await serverHooks.afterSetup(honoApp, { envParser, logger });
407
+ }
408
+ `;
409
+ }
410
+
387
411
  const telescopeWebSocketSetupCode = telescopeWebSocketEnabled
388
412
  ? `
389
413
  // Setup WebSocket for real-time telescope updates
@@ -480,6 +504,7 @@ import ${context.envParserImportPattern} from '${relativeEnvParserPath}';
480
504
  import ${context.loggerImportPattern} from '${relativeLoggerPath}';
481
505
  ${telescopeImports}
482
506
  ${studioImports}
507
+ ${hooksImports}
483
508
 
484
509
  export interface ServerApp {
485
510
  app: HonoType;
@@ -525,9 +550,10 @@ export interface ServerApp {
525
550
  */
526
551
  export async function createApp(app?: HonoType, enableOpenApi: boolean = true): Promise<ServerApp> {
527
552
  const honoApp = app || new Hono();
528
- ${telescopeSetup}${studioSetup}
553
+ ${telescopeSetup}${beforeSetupCall}${studioSetup}
529
554
  // Setup HTTP endpoints
530
555
  await setupEndpoints(honoApp, envParser, logger, enableOpenApi);
556
+ ${afterSetupCall}
531
557
 
532
558
  return {
533
559
  app: honoApp,
package/src/index.ts CHANGED
@@ -111,7 +111,7 @@ program
111
111
  program
112
112
  .command('dev')
113
113
  .description('Start development server with automatic reload')
114
- .option('--port <port>', 'Port to run the development server on', '3000')
114
+ .option('-p, --port <port>', 'Port to run the development server on')
115
115
  .option(
116
116
  '--enable-openapi',
117
117
  'Enable OpenAPI documentation for development server',
@@ -126,6 +126,7 @@ program
126
126
 
127
127
  await devCommand({
128
128
  port: options.port ? Number.parseInt(options.port) : 3000,
129
+ portExplicit: !!options.port,
129
130
  enableOpenApi: options.enableOpenapi ?? true,
130
131
  });
131
132
  } catch (error) {
@@ -169,22 +170,14 @@ program
169
170
 
170
171
  program
171
172
  .command('openapi')
172
- .description(
173
- 'Generate OpenAPI specification from endpoints (TypeScript by default)',
174
- )
175
- .option(
176
- '--output <path>',
177
- 'Output file path for the OpenAPI spec',
178
- 'openapi.ts',
179
- )
180
- .option('--json', 'Generate JSON instead of TypeScript (legacy)', false)
181
- .action(async (options: { output?: string; json?: boolean }) => {
173
+ .description('Generate OpenAPI specification from endpoints')
174
+ .action(async () => {
182
175
  try {
183
176
  const globalOptions = program.opts();
184
177
  if (globalOptions.cwd) {
185
178
  process.chdir(globalOptions.cwd);
186
179
  }
187
- await openapiCommand(options);
180
+ await openapiCommand({});
188
181
  } catch (error) {
189
182
  console.error('OpenAPI generation failed:', (error as Error).message);
190
183
  process.exit(1);