@poetora/cli 0.1.9 → 0.1.11

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.
Files changed (51) hide show
  1. package/bin/cli-builder.js +32 -18
  2. package/bin/services/link.service.js +0 -11
  3. package/bin/services/version.service.d.ts +1 -1
  4. package/bin/services/version.service.js +12 -10
  5. package/bin/utils/index.d.ts +1 -0
  6. package/bin/utils/index.js +1 -0
  7. package/bin/utils/terminate.d.ts +1 -0
  8. package/bin/utils/terminate.js +4 -0
  9. package/package.json +5 -1
  10. package/.turbo/turbo-build.log +0 -4
  11. package/src/accessibility.ts +0 -180
  12. package/src/cli-builder.ts +0 -274
  13. package/src/cli.ts +0 -22
  14. package/src/commands/__tests__/base.command.test.ts +0 -139
  15. package/src/commands/__tests__/dev.command.test.ts +0 -241
  16. package/src/commands/__tests__/init.command.test.ts +0 -281
  17. package/src/commands/__tests__/utils.ts +0 -20
  18. package/src/commands/base.command.ts +0 -97
  19. package/src/commands/check.command.ts +0 -40
  20. package/src/commands/dev.command.ts +0 -63
  21. package/src/commands/index.ts +0 -6
  22. package/src/commands/init.command.ts +0 -125
  23. package/src/commands/link.command.ts +0 -39
  24. package/src/commands/update.command.ts +0 -23
  25. package/src/constants.ts +0 -4
  26. package/src/errors/cli-error.ts +0 -83
  27. package/src/errors/index.ts +0 -1
  28. package/src/index.ts +0 -110
  29. package/src/mdxAccessibility.ts +0 -132
  30. package/src/middlewares.ts +0 -73
  31. package/src/services/__tests__/port.service.test.ts +0 -83
  32. package/src/services/__tests__/template.service.test.ts +0 -234
  33. package/src/services/__tests__/version.service.test.ts +0 -165
  34. package/src/services/accessibility-check.service.ts +0 -226
  35. package/src/services/index.ts +0 -7
  36. package/src/services/link.service.ts +0 -65
  37. package/src/services/openapi-check.service.ts +0 -68
  38. package/src/services/port.service.ts +0 -47
  39. package/src/services/template.service.ts +0 -203
  40. package/src/services/update.service.ts +0 -76
  41. package/src/services/version.service.ts +0 -161
  42. package/src/start.ts +0 -6
  43. package/src/types/common.ts +0 -53
  44. package/src/types/index.ts +0 -2
  45. package/src/types/options.ts +0 -42
  46. package/src/utils/console-logger.ts +0 -123
  47. package/src/utils/index.ts +0 -2
  48. package/src/utils/logger.interface.ts +0 -70
  49. package/tsconfig.build.json +0 -17
  50. package/tsconfig.json +0 -21
  51. package/vitest.config.ts +0 -8
@@ -1,274 +0,0 @@
1
- import chalk from 'chalk';
2
- import type { Argv } from 'yargs';
3
- import yargs from 'yargs';
4
- import { hideBin } from 'yargs/helpers';
5
- import {
6
- CheckCommand,
7
- DevCommand,
8
- InitCommand,
9
- LinkCommand,
10
- UpdateCommand,
11
- } from './commands/index.js';
12
- import { checkNodeVersion, suppressConsoleWarnings } from './middlewares.js';
13
- import {
14
- AccessibilityCheckService,
15
- LinkService,
16
- OpenApiCheckService,
17
- PortService,
18
- TemplateService,
19
- UpdateService,
20
- VersionService,
21
- } from './services/index.js';
22
- import type { DevOptions, OpenApiCheckOptions, RenameOptions } from './types/index.js';
23
- import type { ILogger } from './utils/index.js';
24
- import { ConsoleLogger } from './utils/index.js';
25
-
26
- /**
27
- * CLI Builder - Orchestrates all commands using yargs
28
- */
29
- export class CliBuilder {
30
- private logger: ILogger;
31
-
32
- constructor(private readonly packageName: string = 'poet') {
33
- this.logger = new ConsoleLogger();
34
- }
35
-
36
- /**
37
- * Build and configure the CLI
38
- */
39
- build(): Argv {
40
- // Create services
41
- const versionService = new VersionService(this.packageName);
42
- const portService = new PortService(this.logger);
43
- const templateService = new TemplateService();
44
- const openApiCheckService = new OpenApiCheckService(this.logger);
45
- const accessibilityCheckService = new AccessibilityCheckService();
46
- const linkService = new LinkService(this.logger);
47
- const updateService = new UpdateService(this.logger, versionService, this.packageName);
48
-
49
- // Create commands
50
- const devCommand = new DevCommand(this.logger, versionService, portService, this.packageName);
51
- const initCommand = new InitCommand(this.logger, templateService, this.packageName);
52
- const checkCommand = new CheckCommand(
53
- this.logger,
54
- openApiCheckService,
55
- accessibilityCheckService,
56
- this.packageName
57
- );
58
- const linkCommand = new LinkCommand(this.logger, linkService, this.packageName);
59
- const updateCommand = new UpdateCommand(this.logger, updateService, this.packageName);
60
-
61
- return (
62
- yargs(hideBin(process.argv))
63
- .scriptName(this.packageName)
64
- .version()
65
- .middleware(checkNodeVersion) // Check Node.js version before any command
66
- .middleware(suppressConsoleWarnings) // Suppress known console warnings
67
- // Dev command
68
- .command(
69
- 'dev',
70
- 'initialize a local preview environment',
71
- (yargs) =>
72
- yargs
73
- .option('port', {
74
- type: 'number',
75
- description: 'port to run the preview server on',
76
- default: 3000,
77
- })
78
- .option('open', {
79
- type: 'boolean',
80
- default: true,
81
- description: 'open a local preview in the browser',
82
- })
83
- .option('local-schema', {
84
- type: 'boolean',
85
- default: false,
86
- hidden: true,
87
- description:
88
- 'use a locally hosted schema file (note: only https protocol is supported in production)',
89
- })
90
- .option('client-version', {
91
- type: 'string',
92
- hidden: true,
93
- description: 'the version of the client to use for cli testing',
94
- })
95
- .option('groups', {
96
- type: 'array',
97
- description: 'Mock user groups for local development and testing',
98
- example: '--groups admin user',
99
- })
100
- .option('disable-openapi', {
101
- type: 'boolean',
102
- default: false,
103
- description: 'Disable OpenAPI file generation',
104
- }),
105
- async (argv) => {
106
- try {
107
- await devCommand.run(argv as DevOptions);
108
- } catch (error) {
109
- this.logger.error(error instanceof Error ? error.message : 'Unknown error');
110
- process.exit(1);
111
- }
112
- }
113
- )
114
- // Init/New command
115
- .command(
116
- ['new [directory]', 'init [directory]'],
117
- 'Create a new Poetora documentation site',
118
- (yargs) =>
119
- yargs.positional('directory', {
120
- describe: 'The directory to initialize your documentation',
121
- type: 'string',
122
- default: '.',
123
- }),
124
- async (argv) => {
125
- try {
126
- await initCommand.run({ directory: argv.directory as string });
127
- } catch (error) {
128
- this.logger.error(error instanceof Error ? error.message : 'Unknown error');
129
- process.exit(1);
130
- }
131
- }
132
- )
133
- // OpenAPI check command
134
- .command(
135
- 'openapi-check <filename>',
136
- 'check if an OpenAPI spec is valid',
137
- (yargs) =>
138
- yargs
139
- .positional('filename', {
140
- describe:
141
- 'the filename of the OpenAPI spec (e.g. ./openapi.yaml) or the URL to the OpenAPI spec',
142
- type: 'string',
143
- demandOption: true,
144
- })
145
- .option('local-schema', {
146
- type: 'boolean',
147
- default: false,
148
- description:
149
- 'use a locally hosted schema file (note: only https protocol is supported in production)',
150
- }),
151
- async (argv) => {
152
- try {
153
- await checkCommand.checkOpenApi({
154
- filename: argv.filename as string,
155
- localSchema: argv.localSchema,
156
- } as OpenApiCheckOptions);
157
- process.exit(0);
158
- } catch (error) {
159
- this.logger.error(error instanceof Error ? error.message : 'Unknown error');
160
- process.exit(1);
161
- }
162
- }
163
- )
164
- // Accessibility check command
165
- .command(
166
- ['a11y', 'accessibility-check', 'a11y-check', 'accessibility'],
167
- 'check for accessibility issues in documentation',
168
- () => undefined,
169
- async () => {
170
- try {
171
- const exitCode = await checkCommand.checkAccessibility();
172
- process.exit(exitCode);
173
- } catch (error) {
174
- this.logger.error(error instanceof Error ? error.message : 'Unknown error');
175
- process.exit(1);
176
- }
177
- }
178
- )
179
- // Broken links command
180
- .command(
181
- 'broken-links',
182
- 'check for invalid internal links',
183
- () => undefined,
184
- async () => {
185
- try {
186
- const brokenLinks = await linkCommand.checkBrokenLinks();
187
- const hasBrokenLinks = Object.keys(brokenLinks).length > 0;
188
- process.exit(hasBrokenLinks ? 1 : 0);
189
- } catch (error) {
190
- this.logger.error(error instanceof Error ? error.message : 'Unknown error');
191
- process.exit(1);
192
- }
193
- }
194
- )
195
- // Rename command
196
- .command(
197
- 'rename <from> <to>',
198
- 'rename a file and update all internal link references',
199
- (yargs) =>
200
- yargs
201
- .positional('from', {
202
- describe: 'the file to rename',
203
- type: 'string',
204
- })
205
- .positional('to', {
206
- describe: 'the new name for the file',
207
- type: 'string',
208
- })
209
- .demandOption(['from', 'to'])
210
- .option('force', {
211
- type: 'boolean',
212
- default: false,
213
- description: 'rename files and skip errors',
214
- })
215
- .epilog(`example: \`${this.packageName} rename introduction.mdx overview.mdx\``),
216
- async (argv) => {
217
- try {
218
- await linkCommand.renameFile({
219
- from: argv.from as string,
220
- to: argv.to as string,
221
- force: argv.force,
222
- } as RenameOptions);
223
- process.exit(0);
224
- } catch (error) {
225
- this.logger.error(error instanceof Error ? error.message : 'Unknown error');
226
- process.exit(1);
227
- }
228
- }
229
- )
230
- // Update command
231
- .command(
232
- 'update',
233
- 'update the CLI to the latest version',
234
- () => undefined,
235
- async () => {
236
- try {
237
- await updateCommand.run({});
238
- process.exit(0);
239
- } catch (error) {
240
- this.logger.error(error instanceof Error ? error.message : 'Unknown error');
241
- process.exit(1);
242
- }
243
- }
244
- )
245
- // Version command
246
- .command(
247
- ['version', 'v'],
248
- 'display the current version of the CLI and client',
249
- () => undefined,
250
- async () => {
251
- const versions = versionService.getVersions();
252
- // Use chalk for consistent formatting
253
- console.log(`${chalk.bold.green('cli version')} ${versions.cli}`);
254
- console.log(`${chalk.bold.green('client version')} ${versions.client}`);
255
- process.exit(0);
256
- }
257
- )
258
- // Error handling
259
- .strictCommands()
260
- .demandCommand(1, 'unknown command. see above for the list of supported commands.')
261
-
262
- // Alias option flags --help = -h, default --version = -v
263
- .alias('h', 'help')
264
- .alias('v', 'version')
265
- );
266
- }
267
-
268
- /**
269
- * Parse and execute CLI
270
- */
271
- async run(): Promise<void> {
272
- await this.build().parseAsync();
273
- }
274
- }
package/src/cli.ts DELETED
@@ -1,22 +0,0 @@
1
- #!/usr/bin/env node
2
- import { Logs } from '@poetora/previewing';
3
- import { render } from 'ink';
4
- import React from 'react';
5
-
6
- import { CliBuilder } from './cli-builder.js';
7
-
8
- export interface CliOptions {
9
- packageName: string;
10
- }
11
-
12
- export const cli = (options: CliOptions): void => {
13
- // Initialize ink rendering for logs
14
- render(React.createElement(Logs));
15
-
16
- const builder = new CliBuilder(options.packageName);
17
-
18
- builder.run().catch((error) => {
19
- console.error('Fatal error:', error);
20
- process.exit(1);
21
- });
22
- };
@@ -1,139 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from 'vitest';
2
- import { CliError, ValidationError } from '../../errors/index.js';
3
- import type { ILogger } from '../../utils/index.js';
4
- import { BaseCommand } from '../base.command.js';
5
-
6
- // Test command implementation
7
- class TestCommand extends BaseCommand {
8
- readonly name = 'test';
9
- readonly description = 'Test command';
10
-
11
- public validateCalled = false;
12
- public executeCalled = false;
13
-
14
- protected override async validate(options: { value: string }): Promise<void> {
15
- this.validateCalled = true;
16
- if (!options.value) {
17
- throw new ValidationError('value is required');
18
- }
19
- }
20
-
21
- protected override async execute(options: { value: string }): Promise<string> {
22
- this.executeCalled = true;
23
- return `executed with ${options.value}`;
24
- }
25
- }
26
-
27
- describe('BaseCommand', () => {
28
- let mockLogger: ILogger;
29
- let command: TestCommand;
30
-
31
- beforeEach(() => {
32
- mockLogger = {
33
- info: vi.fn(),
34
- success: vi.fn(),
35
- error: vi.fn(),
36
- warn: vi.fn(),
37
- log: vi.fn(),
38
- spinner: vi.fn(),
39
- logColor: vi.fn(),
40
- logBold: vi.fn(),
41
- logSeparator: vi.fn(),
42
- logNewLine: vi.fn(),
43
- logHeader: vi.fn(),
44
- };
45
- command = new TestCommand(mockLogger);
46
- });
47
-
48
- describe('run', () => {
49
- it('should execute command successfully', async () => {
50
- const result = await command.run({ value: 'test' });
51
-
52
- expect(command.validateCalled).toBe(true);
53
- expect(command.executeCalled).toBe(true);
54
- expect(result).toBe('executed with test');
55
- });
56
-
57
- it('should validate before execute', async () => {
58
- const callOrder: string[] = [];
59
-
60
- // Use type assertion to bypass protected access for testing
61
- (command as unknown as { validate: typeof command.validate }).validate = vi.fn(async () => {
62
- callOrder.push('validate');
63
- });
64
- (command as unknown as { execute: typeof command.execute }).execute = vi.fn(async () => {
65
- callOrder.push('execute');
66
- return 'result';
67
- });
68
-
69
- await command.run({ value: 'test' });
70
-
71
- expect(callOrder).toEqual(['validate', 'execute']);
72
- });
73
-
74
- it('should handle validation errors', async () => {
75
- await expect(command.run({ value: '' })).rejects.toThrow(ValidationError);
76
-
77
- expect(command.validateCalled).toBe(true);
78
- expect(command.executeCalled).toBe(false);
79
- expect(mockLogger.error).toHaveBeenCalledWith('value is required');
80
- });
81
-
82
- it('should handle execution errors', async () => {
83
- (command as unknown as { execute: typeof command.execute }).execute = vi
84
- .fn()
85
- .mockRejectedValue(new Error('execution failed'));
86
-
87
- await expect(command.run({ value: 'test' })).rejects.toThrow('execution failed');
88
-
89
- expect(mockLogger.error).toHaveBeenCalledWith('execution failed');
90
- });
91
-
92
- it('should handle CliError specially', async () => {
93
- const cliError = new CliError('CLI error message', 'TEST_ERROR', 1);
94
- (command as unknown as { execute: typeof command.execute }).execute = vi
95
- .fn()
96
- .mockRejectedValue(cliError);
97
-
98
- await expect(command.run({ value: 'test' })).rejects.toThrow(cliError);
99
-
100
- expect(mockLogger.error).toHaveBeenCalledWith('CLI error message');
101
- });
102
-
103
- it('should handle unknown errors', async () => {
104
- (command as unknown as { execute: typeof command.execute }).execute = vi
105
- .fn()
106
- .mockRejectedValue('string error');
107
-
108
- await expect(command.run({ value: 'test' })).rejects.toBe('string error');
109
-
110
- expect(mockLogger.error).toHaveBeenCalledWith('An unexpected error occurred');
111
- expect(mockLogger.logColor).toHaveBeenCalledWith('string error', 'gray');
112
- });
113
-
114
- it('should show stack trace in debug mode', async () => {
115
- const originalDebug = process.env.DEBUG;
116
- process.env.DEBUG = 'true';
117
-
118
- const error = new Error('test error');
119
- (command as unknown as { execute: typeof command.execute }).execute = vi
120
- .fn()
121
- .mockRejectedValue(error);
122
-
123
- await expect(command.run({ value: 'test' })).rejects.toThrow(error);
124
-
125
- expect(mockLogger.logColor).toHaveBeenCalledWith(error.stack ?? '', 'gray');
126
- process.env.DEBUG = originalDebug;
127
- });
128
- });
129
-
130
- describe('command metadata', () => {
131
- it('should have name property', () => {
132
- expect(command.name).toBe('test');
133
- });
134
-
135
- it('should have description property', () => {
136
- expect(command.description).toBe('Test command');
137
- });
138
- });
139
- });
@@ -1,241 +0,0 @@
1
- import * as previewing from '@poetora/previewing';
2
- import { beforeEach, describe, expect, it, vi } from 'vitest';
3
- import { InvalidEnvironmentError } from '../../errors/index.js';
4
- import type { PortService, VersionService } from '../../services/index.js';
5
- import type { DevOptions } from '../../types/index.js';
6
- import type { ILogger } from '../../utils/index.js';
7
- import { DevCommand } from '../dev.command.js';
8
-
9
- vi.mock('@poetora/previewing', async () => {
10
- const actual = await vi.importActual<typeof import('@poetora/previewing')>('@poetora/previewing');
11
- return {
12
- ...actual,
13
- dev: vi.fn(),
14
- };
15
- });
16
-
17
- describe('DevCommand', () => {
18
- let mockLogger: ILogger;
19
- let mockVersionService: VersionService;
20
- let mockPortService: PortService;
21
- let command: DevCommand;
22
- const devSpy = vi.mocked(previewing.dev);
23
-
24
- beforeEach(() => {
25
- mockLogger = {
26
- info: vi.fn(),
27
- success: vi.fn(),
28
- error: vi.fn(),
29
- warn: vi.fn(),
30
- log: vi.fn(),
31
- spinner: vi.fn(),
32
- logColor: vi.fn(),
33
- logBold: vi.fn(),
34
- logSeparator: vi.fn(),
35
- logNewLine: vi.fn(),
36
- logHeader: vi.fn(),
37
- };
38
-
39
- mockVersionService = {
40
- checkNodeVersion: vi.fn(),
41
- getCliVersion: vi.fn(),
42
- parseNodeVersion: vi.fn(),
43
- getClientVersion: vi.fn(),
44
- getVersions: vi.fn(),
45
- getLatestCliVersion: vi.fn(),
46
- isVersionUpToDate: vi.fn(),
47
- validateNodeVersion: vi.fn(),
48
- } as unknown as VersionService;
49
-
50
- mockPortService = {
51
- findAvailablePort: vi.fn(),
52
- } as unknown as PortService;
53
-
54
- command = new DevCommand(mockLogger, mockVersionService, mockPortService, 'poet');
55
-
56
- vi.clearAllMocks();
57
- });
58
-
59
- describe('validate', () => {
60
- it('should pass validation for supported Node version', async () => {
61
- vi.mocked(mockVersionService.checkNodeVersion).mockReturnValue({
62
- isValid: true,
63
- hasWarning: false,
64
- });
65
-
66
- await expect(command.validate({} as DevOptions)).resolves.not.toThrow();
67
-
68
- expect(mockVersionService.checkNodeVersion).toHaveBeenCalled();
69
- });
70
-
71
- it('should show warning for below recommended version', async () => {
72
- vi.mocked(mockVersionService.checkNodeVersion).mockReturnValue({
73
- isValid: true,
74
- hasWarning: true,
75
- message: 'Node.js 20.17.0 recommended',
76
- });
77
-
78
- await command.validate({} as DevOptions);
79
-
80
- expect(mockLogger.warn).toHaveBeenCalledWith('Node.js 20.17.0 recommended');
81
- });
82
-
83
- it('should throw error for unsupported Node version', async () => {
84
- vi.mocked(mockVersionService.checkNodeVersion).mockReturnValue({
85
- isValid: false,
86
- hasWarning: false,
87
- message: 'Node.js 18.0.0 or higher required',
88
- });
89
-
90
- await expect(command.validate({} as DevOptions)).rejects.toThrow(InvalidEnvironmentError);
91
- });
92
- });
93
-
94
- describe('execute', () => {
95
- it('should start dev server with default options', async () => {
96
- vi.mocked(mockPortService.findAvailablePort).mockResolvedValue(3000);
97
- vi.mocked(mockVersionService.getCliVersion).mockReturnValue('1.0.0');
98
- devSpy.mockResolvedValue(undefined);
99
-
100
- const options: DevOptions = {};
101
-
102
- await command.execute(options);
103
-
104
- expect(mockPortService.findAvailablePort).toHaveBeenCalledWith(undefined);
105
- expect(mockVersionService.getCliVersion).toHaveBeenCalled();
106
- expect(devSpy).toHaveBeenCalledWith({
107
- _: [],
108
- $0: 'poet',
109
- port: 3000,
110
- open: true,
111
- localSchema: false,
112
- clientVersion: undefined,
113
- groups: undefined,
114
- disableOpenapi: false,
115
- packageName: 'poet',
116
- cliVersion: '1.0.0',
117
- });
118
- });
119
-
120
- it('should use specified port', async () => {
121
- vi.mocked(mockPortService.findAvailablePort).mockResolvedValue(5000);
122
- vi.mocked(mockVersionService.getCliVersion).mockReturnValue('1.0.0');
123
- devSpy.mockResolvedValue(undefined);
124
-
125
- const options: DevOptions = { port: 5000 };
126
-
127
- await command.execute(options);
128
-
129
- expect(mockPortService.findAvailablePort).toHaveBeenCalledWith(5000);
130
- expect(devSpy).toHaveBeenCalledWith(
131
- expect.objectContaining({
132
- port: 5000,
133
- })
134
- );
135
- });
136
-
137
- it('should respect open option', async () => {
138
- vi.mocked(mockPortService.findAvailablePort).mockResolvedValue(3000);
139
- vi.mocked(mockVersionService.getCliVersion).mockReturnValue('1.0.0');
140
- devSpy.mockResolvedValue(undefined);
141
-
142
- const options: DevOptions = { open: false };
143
-
144
- await command.execute(options);
145
-
146
- expect(devSpy).toHaveBeenCalledWith(
147
- expect.objectContaining({
148
- open: false,
149
- })
150
- );
151
- });
152
-
153
- it('should pass all options correctly', async () => {
154
- vi.mocked(mockPortService.findAvailablePort).mockResolvedValue(4000);
155
- vi.mocked(mockVersionService.getCliVersion).mockReturnValue('2.0.0');
156
- devSpy.mockResolvedValue(undefined);
157
-
158
- const options: DevOptions = {
159
- port: 4000,
160
- open: false,
161
- localSchema: true,
162
- clientVersion: '1.5.0',
163
- groups: ['admin', 'user'],
164
- disableOpenapi: true,
165
- };
166
-
167
- await command.execute(options);
168
-
169
- expect(devSpy).toHaveBeenCalledWith({
170
- _: [],
171
- $0: 'poet',
172
- port: 4000,
173
- open: false,
174
- localSchema: true,
175
- clientVersion: '1.5.0',
176
- groups: ['admin', 'user'],
177
- disableOpenapi: true,
178
- packageName: 'poet',
179
- cliVersion: '2.0.0',
180
- });
181
- });
182
-
183
- it('should handle when port service finds alternative port', async () => {
184
- vi.mocked(mockPortService.findAvailablePort).mockResolvedValue(3001);
185
- vi.mocked(mockVersionService.getCliVersion).mockReturnValue('1.0.0');
186
- devSpy.mockResolvedValue(undefined);
187
-
188
- const options: DevOptions = { port: 3000 };
189
-
190
- await command.execute(options);
191
-
192
- expect(devSpy).toHaveBeenCalledWith(
193
- expect.objectContaining({
194
- port: 3001, // Alternative port found
195
- })
196
- );
197
- });
198
- });
199
-
200
- describe('run (integration)', () => {
201
- it('should execute full flow successfully', async () => {
202
- vi.mocked(mockVersionService.checkNodeVersion).mockReturnValue({
203
- isValid: true,
204
- hasWarning: false,
205
- });
206
- vi.mocked(mockPortService.findAvailablePort).mockResolvedValue(3000);
207
- vi.mocked(mockVersionService.getCliVersion).mockReturnValue('1.0.0');
208
- devSpy.mockResolvedValue(undefined);
209
-
210
- await command.run({ port: 3000 });
211
-
212
- expect(mockVersionService.checkNodeVersion).toHaveBeenCalled();
213
- expect(mockPortService.findAvailablePort).toHaveBeenCalled();
214
- expect(devSpy).toHaveBeenCalled();
215
- });
216
-
217
- it('should fail at validation stage for unsupported Node version', async () => {
218
- vi.mocked(mockVersionService.checkNodeVersion).mockReturnValue({
219
- isValid: false,
220
- hasWarning: false,
221
- message: 'Unsupported Node.js version',
222
- });
223
-
224
- await expect(command.run({})).rejects.toThrow(InvalidEnvironmentError);
225
-
226
- // Should not reach execute stage
227
- expect(mockPortService.findAvailablePort).not.toHaveBeenCalled();
228
- expect(devSpy).not.toHaveBeenCalled();
229
- });
230
- });
231
-
232
- describe('metadata', () => {
233
- it('should have correct name', () => {
234
- expect(command.name).toBe('dev');
235
- });
236
-
237
- it('should have correct description', () => {
238
- expect(command.description).toBe('initialize a local preview environment');
239
- });
240
- });
241
- });