@poetora/cli 0.0.1 → 0.1.3

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 (114) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/LICENSE +93 -0
  3. package/bin/accessibility.js +2 -2
  4. package/bin/cli-builder.d.ts +8 -0
  5. package/bin/cli-builder.js +178 -0
  6. package/bin/cli.d.ts +5 -11
  7. package/bin/cli.js +8 -200
  8. package/bin/commands/base.command.d.ts +13 -0
  9. package/bin/commands/base.command.js +40 -0
  10. package/bin/commands/check.command.d.ts +14 -0
  11. package/bin/commands/check.command.js +21 -0
  12. package/bin/commands/dev.command.d.ts +13 -0
  13. package/bin/commands/dev.command.js +40 -0
  14. package/bin/commands/index.d.ts +6 -0
  15. package/bin/commands/index.js +6 -0
  16. package/bin/commands/init.command.d.ts +16 -0
  17. package/bin/commands/init.command.js +88 -0
  18. package/bin/commands/link.command.d.ts +13 -0
  19. package/bin/commands/link.command.js +19 -0
  20. package/bin/commands/update.command.d.ts +10 -0
  21. package/bin/commands/update.command.js +13 -0
  22. package/bin/errors/cli-error.d.ts +26 -0
  23. package/bin/errors/cli-error.js +53 -0
  24. package/bin/errors/index.d.ts +1 -0
  25. package/bin/errors/index.js +1 -0
  26. package/bin/index.js +3 -3
  27. package/bin/mdxAccessibility.js +2 -2
  28. package/bin/services/accessibility-check.service.d.ts +10 -0
  29. package/bin/services/accessibility-check.service.js +144 -0
  30. package/bin/services/index.d.ts +7 -0
  31. package/bin/services/index.js +7 -0
  32. package/bin/services/link.service.d.ts +7 -0
  33. package/bin/services/link.service.js +40 -0
  34. package/bin/services/openapi-check.service.d.ts +7 -0
  35. package/bin/services/openapi-check.service.js +43 -0
  36. package/bin/services/port.service.d.ts +7 -0
  37. package/bin/services/port.service.js +26 -0
  38. package/bin/services/template.service.d.ts +22 -0
  39. package/bin/services/template.service.js +127 -0
  40. package/bin/services/update.service.d.ts +10 -0
  41. package/bin/services/update.service.js +57 -0
  42. package/bin/services/version.service.d.ts +16 -0
  43. package/bin/services/version.service.js +102 -0
  44. package/bin/types/common.d.ts +38 -0
  45. package/bin/types/common.js +21 -0
  46. package/bin/types/index.d.ts +2 -0
  47. package/bin/types/index.js +2 -0
  48. package/bin/types/options.d.ts +23 -0
  49. package/bin/types/options.js +1 -0
  50. package/bin/utils/console-logger.d.ts +16 -0
  51. package/bin/utils/console-logger.js +65 -0
  52. package/bin/utils/index.d.ts +2 -0
  53. package/bin/utils/index.js +2 -0
  54. package/bin/utils/logger.interface.d.ts +15 -0
  55. package/bin/utils/logger.interface.js +1 -0
  56. package/package.json +30 -31
  57. package/src/accessibility.ts +2 -2
  58. package/src/cli-builder.ts +267 -0
  59. package/src/cli.ts +15 -0
  60. package/src/commands/__tests__/base.command.test.ts +145 -0
  61. package/src/commands/__tests__/dev.command.test.ts +241 -0
  62. package/src/commands/__tests__/init.command.test.ts +281 -0
  63. package/{__test__ → src/commands/__tests__}/utils.ts +1 -1
  64. package/src/commands/base.command.ts +97 -0
  65. package/src/commands/check.command.ts +40 -0
  66. package/src/commands/dev.command.ts +63 -0
  67. package/src/commands/index.ts +6 -0
  68. package/src/commands/init.command.ts +125 -0
  69. package/src/commands/link.command.ts +39 -0
  70. package/src/commands/update.command.ts +23 -0
  71. package/src/errors/cli-error.ts +83 -0
  72. package/src/errors/index.ts +1 -0
  73. package/src/index.ts +4 -4
  74. package/src/mdxAccessibility.ts +3 -4
  75. package/src/services/__tests__/port.service.test.ts +83 -0
  76. package/src/services/__tests__/template.service.test.ts +234 -0
  77. package/src/services/__tests__/version.service.test.ts +165 -0
  78. package/src/services/accessibility-check.service.ts +226 -0
  79. package/src/services/index.ts +7 -0
  80. package/src/services/link.service.ts +65 -0
  81. package/src/services/openapi-check.service.ts +68 -0
  82. package/src/services/port.service.ts +47 -0
  83. package/src/services/template.service.ts +203 -0
  84. package/src/services/update.service.ts +76 -0
  85. package/src/services/version.service.ts +161 -0
  86. package/src/types/common.ts +53 -0
  87. package/src/types/index.ts +2 -0
  88. package/src/types/options.ts +42 -0
  89. package/src/utils/console-logger.ts +114 -0
  90. package/src/utils/index.ts +2 -0
  91. package/src/utils/logger.interface.ts +70 -0
  92. package/tsconfig.build.json +2 -1
  93. package/tsconfig.json +1 -1
  94. package/.prettierignore +0 -2
  95. package/__test__/brokenLinks.test.ts +0 -93
  96. package/__test__/checkPort.test.ts +0 -92
  97. package/__test__/openApiCheck.test.ts +0 -127
  98. package/__test__/update.test.ts +0 -108
  99. package/bin/accessibilityCheck.d.ts +0 -2
  100. package/bin/accessibilityCheck.js +0 -70
  101. package/bin/helpers.d.ts +0 -17
  102. package/bin/helpers.js +0 -104
  103. package/bin/init.d.ts +0 -1
  104. package/bin/init.js +0 -73
  105. package/bin/mdxLinter.d.ts +0 -2
  106. package/bin/mdxLinter.js +0 -45
  107. package/bin/update.d.ts +0 -3
  108. package/bin/update.js +0 -32
  109. package/src/accessibilityCheck.tsx +0 -145
  110. package/src/cli.tsx +0 -302
  111. package/src/helpers.tsx +0 -131
  112. package/src/init.tsx +0 -93
  113. package/src/mdxLinter.tsx +0 -88
  114. package/src/update.tsx +0 -37
@@ -0,0 +1,281 @@
1
+ import { input, select } from '@inquirer/prompts';
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { ValidationError } from '../../errors/index.js';
4
+ import type { TemplateService } from '../../services/template.service.js';
5
+ import type { InitOptions } from '../../types/index.js';
6
+ import type { ILogger } from '../../utils/index.js';
7
+ import { InitCommand } from '../init.command.js';
8
+
9
+ vi.mock('@inquirer/prompts');
10
+
11
+ describe('InitCommand', () => {
12
+ let command: InitCommand;
13
+ let mockLogger: ILogger;
14
+ let mockTemplateService: TemplateService;
15
+
16
+ beforeEach(() => {
17
+ mockLogger = {
18
+ info: vi.fn(),
19
+ success: vi.fn(),
20
+ error: vi.fn(),
21
+ warn: vi.fn(),
22
+ spinner: vi.fn().mockReturnValue({
23
+ start: vi.fn(),
24
+ succeed: vi.fn(),
25
+ fail: vi.fn(),
26
+ stop: vi.fn(),
27
+ }),
28
+ log: vi.fn(),
29
+ logColor: vi.fn(),
30
+ logBold: vi.fn(),
31
+ logSeparator: vi.fn(),
32
+ logNewLine: vi.fn(),
33
+ logHeader: vi.fn(),
34
+ };
35
+
36
+ mockTemplateService = {
37
+ checkDirectory: vi.fn(),
38
+ getAvailableThemes: vi.fn(),
39
+ installTemplate: vi.fn(),
40
+ } as unknown as TemplateService;
41
+
42
+ command = new InitCommand(mockLogger, mockTemplateService);
43
+ });
44
+
45
+ afterEach(() => {
46
+ vi.clearAllMocks();
47
+ });
48
+
49
+ describe('run - empty directory', () => {
50
+ beforeEach(() => {
51
+ vi.mocked(mockTemplateService.checkDirectory).mockResolvedValue({
52
+ exists: true,
53
+ hasContents: false,
54
+ });
55
+
56
+ vi.mocked(mockTemplateService.getAvailableThemes).mockReturnValue(['ora', 'sol']);
57
+
58
+ vi.mocked(input).mockResolvedValue('My Project');
59
+ vi.mocked(select).mockResolvedValue('ora');
60
+ vi.mocked(mockTemplateService.installTemplate).mockResolvedValue(undefined);
61
+ });
62
+
63
+ it('should install template directly for empty directory', async () => {
64
+ const options: InitOptions = {
65
+ directory: '.',
66
+ };
67
+
68
+ await command.run(options);
69
+
70
+ expect(mockTemplateService.checkDirectory).toHaveBeenCalledWith('.');
71
+ expect(input).toHaveBeenCalled();
72
+ expect(select).toHaveBeenCalled();
73
+ expect(mockTemplateService.installTemplate).toHaveBeenCalledWith({
74
+ directory: '.',
75
+ projectName: 'My Project',
76
+ theme: 'ora',
77
+ });
78
+ expect(mockLogger.success).toHaveBeenCalledWith('Documentation Setup!');
79
+ });
80
+
81
+ it('should use directory name as default project name', async () => {
82
+ vi.mocked(input).mockResolvedValue('my-docs');
83
+
84
+ const options: InitOptions = {
85
+ directory: 'my-docs',
86
+ };
87
+
88
+ await command.run(options);
89
+
90
+ expect(input).toHaveBeenCalledWith({
91
+ message: 'Project Name',
92
+ default: 'my-docs',
93
+ });
94
+ });
95
+
96
+ it('should use "Poetora" as default for current directory', async () => {
97
+ vi.mocked(input).mockResolvedValue('Poetora');
98
+
99
+ const options: InitOptions = {
100
+ directory: '.',
101
+ };
102
+
103
+ await command.run(options);
104
+
105
+ expect(input).toHaveBeenCalledWith({
106
+ message: 'Project Name',
107
+ default: 'Poetora',
108
+ });
109
+ });
110
+ });
111
+
112
+ describe('run - non-empty directory', () => {
113
+ beforeEach(() => {
114
+ vi.mocked(mockTemplateService.checkDirectory).mockResolvedValue({
115
+ exists: true,
116
+ hasContents: true,
117
+ });
118
+
119
+ vi.mocked(mockTemplateService.getAvailableThemes).mockReturnValue(['quartz']);
120
+ vi.mocked(input).mockResolvedValue('My Project');
121
+ vi.mocked(mockTemplateService.installTemplate).mockResolvedValue(undefined);
122
+ });
123
+
124
+ it('should cancel installation when user chooses cancel', async () => {
125
+ vi.mocked(select).mockResolvedValueOnce('cancel');
126
+
127
+ const options: InitOptions = {
128
+ directory: 'existing-dir',
129
+ };
130
+
131
+ await command.run(options);
132
+
133
+ expect(mockLogger.info).toHaveBeenCalledWith('Installation cancelled');
134
+ expect(mockTemplateService.installTemplate).not.toHaveBeenCalled();
135
+ });
136
+
137
+ it('should overwrite when user chooses overwrite', async () => {
138
+ vi.mocked(select)
139
+ .mockResolvedValueOnce('overwrite') // directory choice
140
+ .mockResolvedValueOnce('ora'); // theme choice
141
+
142
+ const options: InitOptions = {
143
+ directory: 'existing-dir',
144
+ };
145
+
146
+ await command.run(options);
147
+
148
+ expect(mockTemplateService.installTemplate).toHaveBeenCalledWith({
149
+ directory: 'existing-dir',
150
+ projectName: 'My Project',
151
+ theme: 'ora',
152
+ });
153
+ });
154
+
155
+ it('should create subdirectory when user chooses subdir', async () => {
156
+ vi.mocked(select)
157
+ .mockResolvedValueOnce('subdir') // directory choice
158
+ .mockResolvedValueOnce('ora'); // theme choice
159
+
160
+ vi.mocked(input)
161
+ .mockResolvedValueOnce('docs') // subdirectory name
162
+ .mockResolvedValueOnce('My Project'); // project name
163
+
164
+ const options: InitOptions = {
165
+ directory: '.',
166
+ };
167
+
168
+ await command.run(options);
169
+
170
+ expect(input).toHaveBeenCalledWith({
171
+ message: 'Subdirectory name:',
172
+ default: 'docs',
173
+ });
174
+
175
+ expect(mockTemplateService.installTemplate).toHaveBeenCalledWith({
176
+ directory: 'docs',
177
+ projectName: 'My Project',
178
+ theme: 'ora',
179
+ });
180
+ });
181
+
182
+ it('should combine parent dir with subdir when not current directory', async () => {
183
+ vi.mocked(select).mockResolvedValueOnce('subdir').mockResolvedValueOnce('ora');
184
+
185
+ vi.mocked(input).mockResolvedValueOnce('docs').mockResolvedValueOnce('My Project');
186
+
187
+ const options: InitOptions = {
188
+ directory: 'parent',
189
+ };
190
+
191
+ await command.run(options);
192
+
193
+ expect(mockTemplateService.installTemplate).toHaveBeenCalledWith({
194
+ directory: 'parent/docs',
195
+ projectName: 'My Project',
196
+ theme: 'ora',
197
+ });
198
+ });
199
+
200
+ it('should throw ValidationError for empty subdirectory name', async () => {
201
+ vi.mocked(select).mockResolvedValue('subdir');
202
+ vi.mocked(input).mockResolvedValue(' '); // empty with spaces
203
+
204
+ const options: InitOptions = {
205
+ directory: '.',
206
+ };
207
+
208
+ await expect(command.run(options)).rejects.toThrow(ValidationError);
209
+ await expect(command.run(options)).rejects.toThrow('Subdirectory name cannot be empty');
210
+ });
211
+ });
212
+
213
+ describe('theme selection', () => {
214
+ beforeEach(() => {
215
+ vi.mocked(mockTemplateService.checkDirectory).mockResolvedValue({
216
+ exists: true,
217
+ hasContents: false,
218
+ });
219
+
220
+ vi.mocked(input).mockResolvedValue('My Project');
221
+ vi.mocked(mockTemplateService.installTemplate).mockResolvedValue(undefined);
222
+ });
223
+
224
+ it('should display all available themes', async () => {
225
+ vi.mocked(mockTemplateService.getAvailableThemes).mockReturnValue(['ora', 'sol', 'custom']);
226
+
227
+ vi.mocked(select).mockResolvedValue('sol');
228
+
229
+ const options: InitOptions = {
230
+ directory: '.',
231
+ };
232
+
233
+ await command.run(options);
234
+
235
+ expect(select).toHaveBeenCalledWith({
236
+ message: 'Theme',
237
+ choices: [
238
+ { name: 'ora', value: 'ora' },
239
+ { name: 'sol', value: 'sol' },
240
+ { name: 'custom', value: 'custom' },
241
+ ],
242
+ });
243
+ });
244
+ });
245
+
246
+ describe('onboarding message', () => {
247
+ beforeEach(() => {
248
+ vi.mocked(mockTemplateService.checkDirectory).mockResolvedValue({
249
+ exists: true,
250
+ hasContents: false,
251
+ });
252
+
253
+ vi.mocked(mockTemplateService.getAvailableThemes).mockReturnValue(['ora']);
254
+ vi.mocked(input).mockResolvedValue('My Project');
255
+ vi.mocked(select).mockResolvedValue('ora');
256
+ vi.mocked(mockTemplateService.installTemplate).mockResolvedValue(undefined);
257
+ });
258
+
259
+ it('should show cd command for subdirectory installation', async () => {
260
+ const options: InitOptions = {
261
+ directory: 'docs',
262
+ };
263
+
264
+ await command.run(options);
265
+
266
+ expect(mockLogger.log).toHaveBeenCalledWith(' cd docs');
267
+ expect(mockLogger.log).toHaveBeenCalledWith(' poet dev');
268
+ });
269
+
270
+ it('should not show cd command for current directory', async () => {
271
+ const options: InitOptions = {
272
+ directory: '.',
273
+ };
274
+
275
+ await command.run(options);
276
+
277
+ expect(mockLogger.log).not.toHaveBeenCalledWith(expect.stringContaining('cd'));
278
+ expect(mockLogger.log).toHaveBeenCalledWith(' poet dev');
279
+ });
280
+ });
281
+ });
@@ -1,4 +1,4 @@
1
- import { cli } from '../src/cli';
1
+ import { cli } from '../../cli';
2
2
 
3
3
  /**
4
4
  * Programmatically set arguments and execute the CLI script
@@ -0,0 +1,97 @@
1
+ import { CliError } from '../errors/index.js';
2
+ import type { ILogger } from '../utils/index.js';
3
+
4
+ /**
5
+ * Base abstract class for all CLI commands
6
+ * Implements Template Method pattern for consistent command execution flow
7
+ */
8
+ export abstract class BaseCommand<TOptions = unknown, TResult = void> {
9
+ /**
10
+ * Command name (e.g., 'dev', 'init', 'check')
11
+ */
12
+ abstract readonly name: string;
13
+
14
+ /**
15
+ * Command description for help text
16
+ */
17
+ abstract readonly description: string;
18
+
19
+ constructor(
20
+ protected readonly logger: ILogger,
21
+ protected readonly packageName: string = 'poet'
22
+ ) {}
23
+
24
+ /**
25
+ * Template method that orchestrates command execution
26
+ * This is the main entry point called by the CLI framework
27
+ */
28
+ /* prettier-ignore */
29
+ async run(options: TOptions): Promise<TResult> {
30
+ try {
31
+ // 1. Validate input options
32
+ await this.validate(options);
33
+
34
+ // 2. Execute command logic
35
+ const result = await this.execute(options);
36
+
37
+ // 3. Return result
38
+ return result;
39
+ } catch (error) {
40
+ // 4. Handle errors consistently
41
+ this.handleError(error);
42
+ throw error;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Validate command options before execution
48
+ * Override this method to add custom validation logic
49
+ * @param options - Command options to validate
50
+ * @throws {ValidationError} if validation fails
51
+ */
52
+ // prettier-ignore
53
+ protected async validate(_options: TOptions): Promise<void> {
54
+ // Default: no validation
55
+ // Subclasses can override to add validation
56
+ }
57
+
58
+ /**
59
+ * Execute the main command logic
60
+ * This method must be implemented by all command subclasses
61
+ * @param options - Validated command options
62
+ * @returns Command execution result
63
+ */
64
+ protected abstract execute(options: TOptions): Promise<TResult>;
65
+
66
+ /**
67
+ * Handle errors that occur during command execution
68
+ * Can be overridden to provide custom error handling
69
+ * @param error - The error to handle
70
+ */
71
+ protected handleError(error: unknown): void {
72
+ if (error instanceof CliError) {
73
+ // CLI-specific errors with user-friendly messages
74
+ this.logger.error(error.message);
75
+ } else if (error instanceof Error) {
76
+ // Generic errors
77
+ this.logger.error(error.message);
78
+
79
+ // In debug mode, show stack trace
80
+ if (process.env.DEBUG === 'true') {
81
+ console.error(error.stack);
82
+ }
83
+ } else {
84
+ // Unknown error types
85
+ this.logger.error('An unexpected error occurred');
86
+ console.error(error);
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Exit the process with given code
92
+ * Separated for easier testing (can be mocked)
93
+ */
94
+ protected exit(code: number): never {
95
+ process.exit(code);
96
+ }
97
+ }
@@ -0,0 +1,40 @@
1
+ import type { AccessibilityCheckService, OpenApiCheckService } from '../services/index.js';
2
+ import type { OpenApiCheckOptions } from '../types/index.js';
3
+ import type { ILogger } from '../utils/index.js';
4
+ import { BaseCommand } from './base.command.js';
5
+
6
+ /**
7
+ * Check command - Validate OpenAPI specs and accessibility
8
+ */
9
+ export class CheckCommand extends BaseCommand {
10
+ readonly name = 'check';
11
+ readonly description = 'check OpenAPI specs or accessibility';
12
+
13
+ constructor(
14
+ logger: ILogger,
15
+ private readonly openApiCheckService: OpenApiCheckService,
16
+ private readonly accessibilityCheckService: AccessibilityCheckService,
17
+ packageName: string = 'poet'
18
+ ) {
19
+ super(logger, packageName);
20
+ }
21
+
22
+ /**
23
+ * Execute OpenAPI check
24
+ */
25
+ async checkOpenApi(options: OpenApiCheckOptions): Promise<void> {
26
+ await this.openApiCheckService.validateSpec(options.filename, options.localSchema);
27
+ }
28
+
29
+ /**
30
+ * Execute accessibility check
31
+ */
32
+ async checkAccessibility(): Promise<number> {
33
+ return await this.accessibilityCheckService.checkAccessibility();
34
+ }
35
+
36
+ protected override async execute(options: OpenApiCheckOptions): Promise<void> {
37
+ // This is for openapi-check command
38
+ await this.openApiCheckService.validateSpec(options.filename, options.localSchema);
39
+ }
40
+ }
@@ -0,0 +1,63 @@
1
+ import { dev } from '@poetora/previewing';
2
+ import type { ArgumentsCamelCase } from 'yargs';
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 { BaseCommand } from './base.command.js';
8
+
9
+ /**
10
+ * Dev command - Start local development server
11
+ */
12
+ export class DevCommand extends BaseCommand {
13
+ readonly name = 'dev';
14
+ readonly description = 'initialize a local preview environment';
15
+
16
+ constructor(
17
+ logger: ILogger,
18
+ private readonly versionService: VersionService,
19
+ private readonly portService: PortService,
20
+ packageName: string = 'poet'
21
+ ) {
22
+ super(logger, packageName);
23
+ }
24
+
25
+ protected override async validate(_options: DevOptions): Promise<void> {
26
+ // Check Node.js version
27
+ const versionResult = this.versionService.checkNodeVersion();
28
+
29
+ if (!versionResult.isValid) {
30
+ throw new InvalidEnvironmentError(versionResult.message ?? 'Unsupported Node.js version');
31
+ }
32
+
33
+ // Show warning if below recommended version
34
+ if (versionResult.hasWarning && versionResult.message) {
35
+ this.logger.warn(versionResult.message);
36
+ }
37
+ }
38
+
39
+ protected override async execute(options: DevOptions): Promise<void> {
40
+ // Find available port
41
+ const port = await this.portService.findAvailablePort(options.port);
42
+
43
+ // Get CLI version
44
+ const cliVersion = this.versionService.getCliVersion();
45
+
46
+ // Start development server (delegate to @poetora/previewing)
47
+ // Convert DevOptions to ArgumentsCamelCase format expected by dev()
48
+ const devArgs: ArgumentsCamelCase = {
49
+ _: [],
50
+ $0: this.packageName,
51
+ port,
52
+ open: options.open ?? true,
53
+ localSchema: options.localSchema ?? false,
54
+ clientVersion: options.clientVersion,
55
+ groups: options.groups,
56
+ disableOpenapi: options.disableOpenapi ?? false,
57
+ packageName: this.packageName,
58
+ cliVersion,
59
+ };
60
+
61
+ await dev(devArgs);
62
+ }
63
+ }
@@ -0,0 +1,6 @@
1
+ export * from './base.command.js';
2
+ export * from './check.command.js';
3
+ export * from './dev.command.js';
4
+ export * from './init.command.js';
5
+ export * from './link.command.js';
6
+ export * from './update.command.js';
@@ -0,0 +1,125 @@
1
+ import { input, select } from '@inquirer/prompts';
2
+ import { ValidationError } from '../errors/index.js';
3
+ import type { TemplateService } from '../services/template.service.js';
4
+ import type { InitOptions } from '../types/index.js';
5
+ import type { ILogger } from '../utils/index.js';
6
+ import { BaseCommand } from './base.command.js';
7
+
8
+ /**
9
+ * Init command - Create a new Poetora documentation site
10
+ */
11
+ export class InitCommand extends BaseCommand {
12
+ readonly name = 'init';
13
+ readonly description = 'Create a new Poetora documentation site';
14
+
15
+ constructor(
16
+ logger: ILogger,
17
+ private readonly templateService: TemplateService,
18
+ packageName: string = 'poet'
19
+ ) {
20
+ super(logger, packageName);
21
+ }
22
+
23
+ protected override async execute(options: InitOptions): Promise<void> {
24
+ let installDir = options.directory;
25
+
26
+ // Step 1: Handle existing directory
27
+ const dirStatus = await this.templateService.checkDirectory(installDir);
28
+
29
+ if (dirStatus.exists && dirStatus.hasContents) {
30
+ const choice = await this.promptDirectoryChoice(installDir);
31
+
32
+ if (choice === 'cancel') {
33
+ this.logger.info('Installation cancelled');
34
+ return;
35
+ }
36
+
37
+ if (choice === 'subdir') {
38
+ const subdir = await this.promptSubdirectoryName();
39
+ installDir = installDir === '.' ? subdir : `${installDir}/${subdir}`;
40
+ }
41
+ }
42
+
43
+ // Step 2: Prompt for project configuration
44
+ const projectName = await this.promptProjectName(installDir);
45
+ const theme = await this.promptTheme();
46
+
47
+ // Step 3: Download and install template
48
+ this.logger.info('Setting up documentation project...');
49
+
50
+ await this.templateService.installTemplate({
51
+ directory: installDir,
52
+ projectName,
53
+ theme,
54
+ });
55
+
56
+ // Step 4: Show success message
57
+ this.showOnboardingMessage(installDir);
58
+ }
59
+
60
+ private async promptDirectoryChoice(
61
+ directory: string
62
+ ): Promise<'subdir' | 'overwrite' | 'cancel'> {
63
+ const choice = await select({
64
+ message: `Directory ${directory} is not empty. What would you like to do?`,
65
+ choices: [
66
+ { name: 'Create in a subdirectory', value: 'subdir' },
67
+ { name: 'Overwrite current directory (may lose contents)', value: 'overwrite' },
68
+ { name: 'Cancel', value: 'cancel' },
69
+ ],
70
+ });
71
+
72
+ return choice as 'subdir' | 'overwrite' | 'cancel';
73
+ }
74
+
75
+ private async promptSubdirectoryName(): Promise<string> {
76
+ const subdir = await input({
77
+ message: 'Subdirectory name:',
78
+ default: 'docs',
79
+ });
80
+
81
+ if (!subdir || subdir.trim() === '') {
82
+ throw new ValidationError('Subdirectory name cannot be empty');
83
+ }
84
+
85
+ return subdir.trim();
86
+ }
87
+
88
+ private async promptProjectName(installDir: string): Promise<string> {
89
+ const defaultProject = installDir === '.' ? 'Poetora' : installDir;
90
+
91
+ const projectName = await input({
92
+ message: 'Project Name',
93
+ default: defaultProject,
94
+ });
95
+
96
+ return projectName || defaultProject;
97
+ }
98
+
99
+ private async promptTheme(): Promise<string> {
100
+ const themes = this.templateService.getAvailableThemes();
101
+
102
+ const theme = await select({
103
+ message: 'Theme',
104
+ choices: themes.map((t) => ({
105
+ name: t,
106
+ value: t,
107
+ })),
108
+ });
109
+
110
+ return theme;
111
+ }
112
+
113
+ private showOnboardingMessage(installDir: string): void {
114
+ this.logger.log('');
115
+ this.logger.success('Documentation Setup!');
116
+ this.logger.log('');
117
+ this.logger.log('To see your docs run:');
118
+ this.logger.log('');
119
+ if (installDir !== '.') {
120
+ this.logger.log(` cd ${installDir}`);
121
+ }
122
+ this.logger.log(` ${this.packageName} dev`);
123
+ this.logger.log('');
124
+ }
125
+ }
@@ -0,0 +1,39 @@
1
+ import type { LinkService } from '../services/index.js';
2
+ import type { RenameOptions } from '../types/index.js';
3
+ import type { ILogger } from '../utils/index.js';
4
+ import { BaseCommand } from './base.command.js';
5
+
6
+ /**
7
+ * Link command - Check broken links and rename files
8
+ */
9
+ export class LinkCommand extends BaseCommand {
10
+ readonly name = 'link';
11
+ readonly description = 'manage documentation links';
12
+
13
+ constructor(
14
+ logger: ILogger,
15
+ private readonly linkService: LinkService,
16
+ packageName: string = 'poet'
17
+ ) {
18
+ super(logger, packageName);
19
+ }
20
+
21
+ /**
22
+ * Check for broken links
23
+ */
24
+ async checkBrokenLinks(): Promise<Record<string, string[]>> {
25
+ return await this.linkService.checkBrokenLinks();
26
+ }
27
+
28
+ /**
29
+ * Rename file and update references
30
+ */
31
+ async renameFile(options: RenameOptions): Promise<void> {
32
+ await this.linkService.renameFile(options.from, options.to, options.force);
33
+ }
34
+
35
+ protected override async execute(): Promise<void> {
36
+ // Default action: check broken links
37
+ await this.linkService.checkBrokenLinks();
38
+ }
39
+ }
@@ -0,0 +1,23 @@
1
+ import type { UpdateService } from '../services/index.js';
2
+ import type { ILogger } from '../utils/index.js';
3
+ import { BaseCommand } from './base.command.js';
4
+
5
+ /**
6
+ * Update command - Update CLI to latest version
7
+ */
8
+ export class UpdateCommand extends BaseCommand {
9
+ readonly name = 'update';
10
+ readonly description = 'update the CLI to the latest version';
11
+
12
+ constructor(
13
+ logger: ILogger,
14
+ private readonly updateService: UpdateService,
15
+ packageName: string = 'poet'
16
+ ) {
17
+ super(logger, packageName);
18
+ }
19
+
20
+ protected override async execute(): Promise<void> {
21
+ await this.updateService.update();
22
+ }
23
+ }