@poetora/cli 0.1.9 → 0.1.10

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 (46) hide show
  1. package/bin/cli-builder.js +1 -1
  2. package/bin/services/version.service.d.ts +1 -1
  3. package/bin/services/version.service.js +12 -10
  4. package/package.json +7 -3
  5. package/.turbo/turbo-build.log +0 -4
  6. package/src/accessibility.ts +0 -180
  7. package/src/cli-builder.ts +0 -274
  8. package/src/cli.ts +0 -22
  9. package/src/commands/__tests__/base.command.test.ts +0 -139
  10. package/src/commands/__tests__/dev.command.test.ts +0 -241
  11. package/src/commands/__tests__/init.command.test.ts +0 -281
  12. package/src/commands/__tests__/utils.ts +0 -20
  13. package/src/commands/base.command.ts +0 -97
  14. package/src/commands/check.command.ts +0 -40
  15. package/src/commands/dev.command.ts +0 -63
  16. package/src/commands/index.ts +0 -6
  17. package/src/commands/init.command.ts +0 -125
  18. package/src/commands/link.command.ts +0 -39
  19. package/src/commands/update.command.ts +0 -23
  20. package/src/constants.ts +0 -4
  21. package/src/errors/cli-error.ts +0 -83
  22. package/src/errors/index.ts +0 -1
  23. package/src/index.ts +0 -110
  24. package/src/mdxAccessibility.ts +0 -132
  25. package/src/middlewares.ts +0 -73
  26. package/src/services/__tests__/port.service.test.ts +0 -83
  27. package/src/services/__tests__/template.service.test.ts +0 -234
  28. package/src/services/__tests__/version.service.test.ts +0 -165
  29. package/src/services/accessibility-check.service.ts +0 -226
  30. package/src/services/index.ts +0 -7
  31. package/src/services/link.service.ts +0 -65
  32. package/src/services/openapi-check.service.ts +0 -68
  33. package/src/services/port.service.ts +0 -47
  34. package/src/services/template.service.ts +0 -203
  35. package/src/services/update.service.ts +0 -76
  36. package/src/services/version.service.ts +0 -161
  37. package/src/start.ts +0 -6
  38. package/src/types/common.ts +0 -53
  39. package/src/types/index.ts +0 -2
  40. package/src/types/options.ts +0 -42
  41. package/src/utils/console-logger.ts +0 -123
  42. package/src/utils/index.ts +0 -2
  43. package/src/utils/logger.interface.ts +0 -70
  44. package/tsconfig.build.json +0 -17
  45. package/tsconfig.json +0 -21
  46. package/vitest.config.ts +0 -8
@@ -1,226 +0,0 @@
1
- import { getConfigObj, getConfigPath } from '@poetora/prebuild';
2
- import { getBackgroundColors } from '@poetora/shared';
3
- import path from 'path';
4
- import {
5
- type AccessibilityCheckResult,
6
- type ContrastResult,
7
- checkDocsColors,
8
- } from '../accessibility.js';
9
- import { CMD_EXEC_PATH } from '../constants.js';
10
- import { type AccessibilityFixAttribute, checkMdxAccessibility } from '../mdxAccessibility.js';
11
- import { ConsoleLogger } from '../utils/console-logger.js';
12
- import type { ILogger } from '../utils/logger.interface.js';
13
-
14
- /**
15
- * Service for accessibility checking
16
- */
17
- export class AccessibilityCheckService {
18
- constructor(private readonly logger: ILogger = new ConsoleLogger()) {}
19
-
20
- /**
21
- * Display contrast result with color-coded output
22
- */
23
- private displayContrastResult(
24
- result: ContrastResult | null,
25
- label: string,
26
- prefix: string = ''
27
- ): void {
28
- if (!result) return;
29
-
30
- const { recommendation } = result;
31
-
32
- // Custom messages with our own tone
33
- let statusText: string;
34
- let detailMessage: string;
35
-
36
- if (recommendation === 'pass') {
37
- if (result.meetsAAA) {
38
- statusText = '✓ Excellent';
39
- detailMessage = `ratio ${result.ratio.toFixed(2)}:1 (AAA standard)`;
40
- } else {
41
- statusText = '✓ Good';
42
- detailMessage = `ratio ${result.ratio.toFixed(2)}:1 (AA standard)`;
43
- }
44
- } else if (recommendation === 'warning') {
45
- statusText = '⚠ Acceptable';
46
- detailMessage = `ratio ${result.ratio.toFixed(2)}:1 (consider improving)`;
47
- } else {
48
- statusText = '✗ Poor';
49
- detailMessage = `ratio ${result.ratio.toFixed(2)}:1 (below AA standard)`;
50
- }
51
-
52
- // Display with label in cyan and status+message in appropriate color
53
- const labelText = `${prefix}${label}:`;
54
- const statusColor =
55
- recommendation === 'pass' ? 'green' : recommendation === 'warning' ? 'yellow' : 'red';
56
-
57
- this.logger.log(`${labelText} ${this.colorize(statusText, statusColor)} ${detailMessage}`);
58
- }
59
-
60
- private colorize(text: string, color: 'green' | 'yellow' | 'red'): string {
61
- // Use chalk directly for inline coloring
62
- const chalk = (text: string, color: 'green' | 'yellow' | 'red') => {
63
- const colors: Record<string, string> = {
64
- green: '\x1b[32m',
65
- yellow: '\x1b[33m',
66
- red: '\x1b[31m',
67
- reset: '\x1b[0m',
68
- };
69
- return `${colors[color]}${text}${colors.reset}`;
70
- };
71
- return chalk(text, color);
72
- }
73
-
74
- /**
75
- * Check color contrast accessibility
76
- */
77
- private async checkColorAccessibility(): /* prettier-ignore */ Promise<number> {
78
- try {
79
- const docsConfigPath = await getConfigPath(CMD_EXEC_PATH);
80
-
81
- if (!docsConfigPath) {
82
- this.logger.error(
83
- 'No configuration file found. Please run this command from a directory with a docs.json file.'
84
- );
85
- return 1;
86
- }
87
-
88
- const config = await getConfigObj(CMD_EXEC_PATH);
89
-
90
- if (!config?.colors) {
91
- this.logger.warn('No colors section found in configuration file');
92
- return 0;
93
- }
94
-
95
- const { colors, navigation } = config;
96
- const { lightHex, darkHex } = getBackgroundColors(config);
97
-
98
- const results: AccessibilityCheckResult = checkDocsColors(
99
- colors,
100
- { lightHex, darkHex },
101
- navigation
102
- );
103
-
104
- this.logger.log('Checking color accessibility...');
105
-
106
- this.displayContrastResult(
107
- results.primaryContrast,
108
- `Primary Color (${colors.primary}) vs Light Background`
109
- );
110
- this.displayContrastResult(
111
- results.lightContrast,
112
- `Light Color (${colors.light}) vs Dark Background`
113
- );
114
- this.displayContrastResult(
115
- results.darkContrast,
116
- `Dark Color (${colors.dark}) vs Dark Background`
117
- );
118
- this.displayContrastResult(
119
- results.darkOnLightContrast,
120
- `Dark Color (${colors.dark}) vs Light Background`
121
- );
122
-
123
- const anchorsWithResults = results.anchorResults.filter(
124
- (anchor) => anchor.lightContrast || anchor.darkContrast
125
- );
126
-
127
- if (anchorsWithResults.length > 0) {
128
- for (const anchor of anchorsWithResults) {
129
- this.displayContrastResult(anchor.lightContrast, `${anchor.name} vs Light Background`);
130
- this.displayContrastResult(anchor.darkContrast, `${anchor.name} vs Dark Background`);
131
- }
132
- }
133
-
134
- // Overall assessment with custom messages
135
- let overallDisplay: string;
136
- if (results.overallScore === 'fail') {
137
- overallDisplay =
138
- this.colorize('✗ Action needed:', 'red') +
139
- ' Update colors to meet accessibility standards';
140
- } else if (results.overallScore === 'warning') {
141
- overallDisplay = `${this.colorize('⚠ Room for improvement:', 'yellow')} Consider enhancing color contrast`;
142
- } else {
143
- overallDisplay = `${this.colorize('✓ All good:', 'green')} Colors meet accessibility guidelines`;
144
- }
145
-
146
- this.logger.logNewLine();
147
- this.logger.log(overallDisplay);
148
-
149
- return results.overallScore === 'fail' ? 1 : 0;
150
- } catch (error) {
151
- this.logger.error(`Failed to check color accessibility: ${error}`);
152
- return 1;
153
- }
154
- }
155
-
156
- /**
157
- * Check MDX files for accessibility issues
158
- */
159
- private async checkMdxAccessibility(): /* prettier-ignore */ Promise<number> {
160
- try {
161
- this.logger.log('Checking mdx files for accessibility issues...');
162
-
163
- const results = await checkMdxAccessibility();
164
-
165
- if (results.missingAltAttributes.length === 0) {
166
- this.logger.success('no accessibility issues found');
167
- this.logger.info(
168
- `Checked ${results.totalFiles} MDX files - all images and videos have alt attributes.`
169
- );
170
- return 0;
171
- }
172
-
173
- const issuesByFile: Record<string, AccessibilityFixAttribute[]> = {};
174
- results.missingAltAttributes.forEach((issue) => {
175
- if (!issuesByFile[issue.filePath]) {
176
- issuesByFile[issue.filePath] = [];
177
- }
178
- issuesByFile[issue.filePath]?.push(issue);
179
- });
180
-
181
- this.logger.log(
182
- `Found ${this.logger.highlight?.(results.missingAltAttributes.length.toString()) ?? results.missingAltAttributes.length} accessibility issue${results.missingAltAttributes.length === 1 ? '' : 's'} in ${this.logger.highlight?.(results.filesWithIssues.toString()) ?? results.filesWithIssues} file${results.filesWithIssues === 1 ? '' : 's'}:`
183
- );
184
-
185
- for (const [filePath, issues] of Object.entries(issuesByFile)) {
186
- const relativePath = path.relative(process.cwd(), filePath);
187
- this.logger.log(`${relativePath}:`);
188
-
189
- for (const issue of issues) {
190
- const location =
191
- issue.line && issue.column ? ` (line ${issue.line}, col ${issue.column})` : '';
192
- if (issue.element === 'a') {
193
- this.logger.logColor(
194
- ` ✗ Missing text attribute ${issue.tagName} element${location}`,
195
- 'red'
196
- );
197
- } else {
198
- this.logger.logColor(
199
- ` ✗ Missing alt attribute on ${issue.tagName} element${location}`,
200
- 'red'
201
- );
202
- }
203
- }
204
- }
205
-
206
- this.logger.warn(
207
- 'Recommendation: Add alt attributes to all images and videos for better accessibility.'
208
- );
209
-
210
- return 1;
211
- } catch (error) {
212
- this.logger.error(`Failed to check MDX accessibility: ${error}`);
213
- return 1;
214
- }
215
- }
216
-
217
- /**
218
- * Run accessibility checks on documentation
219
- * @returns Exit code (0 for success, 1 for failure)
220
- */
221
- async checkAccessibility(): /* prettier-ignore */ Promise<number> {
222
- const colorCheckCode = await this.checkColorAccessibility();
223
- const mdxCheckCode = await this.checkMdxAccessibility();
224
- return colorCheckCode || mdxCheckCode;
225
- }
226
- }
@@ -1,7 +0,0 @@
1
- export * from './accessibility-check.service.js';
2
- export * from './link.service.js';
3
- export * from './openapi-check.service.js';
4
- export * from './port.service.js';
5
- export * from './template.service.js';
6
- export * from './update.service.js';
7
- export * from './version.service.js';
@@ -1,65 +0,0 @@
1
- import {
2
- getBrokenInternalLinks,
3
- type MdxPath,
4
- renameFilesAndUpdateLinksInContent,
5
- } from '@poetora/link-rot';
6
- import * as path from 'path';
7
-
8
- import type { ILogger } from '../utils/index.js';
9
-
10
- /**
11
- * Service for link management
12
- */
13
- export class LinkService {
14
- constructor(private readonly logger: ILogger) {}
15
-
16
- /**
17
- * Check for broken internal links
18
- * @returns Broken links grouped by file
19
- */
20
- async checkBrokenLinks(): Promise<Record<string, string[]>> {
21
- const brokenLinks = await getBrokenInternalLinks();
22
-
23
- if (brokenLinks.length === 0) {
24
- this.logger.success('no broken links found');
25
- return {};
26
- }
27
-
28
- // Group broken links by file
29
- const brokenLinksByFile: Record<string, string[]> = {};
30
- brokenLinks.forEach((mdxPath: MdxPath) => {
31
- const filename = path.join(mdxPath.relativeDir, mdxPath.filename);
32
- const brokenLinksForFile = brokenLinksByFile[filename];
33
- if (brokenLinksForFile) {
34
- brokenLinksForFile.push(mdxPath.originalPath);
35
- } else {
36
- brokenLinksByFile[filename] = [mdxPath.originalPath];
37
- }
38
- });
39
-
40
- // Display broken links with highlighted numbers
41
- const fileCount = Object.keys(brokenLinksByFile).length;
42
- this.logger.log(
43
- `found ${this.logger.highlight?.(brokenLinks.length.toString()) ?? brokenLinks.length} broken link${brokenLinks.length === 1 ? '' : 's'} in ${this.logger.highlight?.(fileCount.toString()) ?? fileCount} file${fileCount === 1 ? '' : 's'}`
44
- );
45
- this.logger.logNewLine();
46
-
47
- for (const [filename, links] of Object.entries(brokenLinksByFile)) {
48
- // Underline filename for better visibility
49
- this.logger.logColor(filename, 'cyan');
50
- links.forEach((link) => {
51
- this.logger.logColor(` ⎿ ${link}`, 'gray');
52
- });
53
- this.logger.logNewLine();
54
- }
55
-
56
- return brokenLinksByFile;
57
- }
58
-
59
- /**
60
- * Rename a file and update all internal link references
61
- */
62
- async renameFile(from: string, to: string, force: boolean = false): Promise<void> {
63
- await renameFilesAndUpdateLinksInContent(from, to, force);
64
- }
65
- }
@@ -1,68 +0,0 @@
1
- import { getOpenApiDocumentFromUrl, isAllowedLocalSchemaUrl, validate } from '@poetora/shared';
2
- import * as fs from 'fs';
3
- import * as yaml from 'js-yaml';
4
-
5
- import { FileSystemError, ValidationError } from '../errors/index.js';
6
- import type { ILogger } from '../utils/index.js';
7
-
8
- /**
9
- * Service for OpenAPI validation
10
- */
11
- export class OpenApiCheckService {
12
- constructor(private readonly logger: ILogger) {}
13
-
14
- /**
15
- * Validate OpenAPI spec from URL or local file
16
- */
17
- async validateSpec(filename: string, localSchema: boolean = false): Promise<void> {
18
- // Check if it's a URL
19
- if (isAllowedLocalSchemaUrl(filename, localSchema)) {
20
- await getOpenApiDocumentFromUrl(filename);
21
- this.logger.success('OpenAPI definition is valid.');
22
- return;
23
- }
24
-
25
- // Warn for http:// URLs without --local-schema
26
- if (filename.startsWith('http://') && !localSchema) {
27
- this.logger.warn('include the --local-schema flag to check locally hosted OpenAPI files');
28
- this.logger.warn('only https protocol is supported in production');
29
- return;
30
- }
31
-
32
- // Read and validate local file
33
- const document = await this.readLocalOpenApiFile(filename);
34
-
35
- if (!document) {
36
- throw new ValidationError(
37
- 'failed to parse OpenAPI spec: could not parse file correctly, please check for any syntax errors.'
38
- );
39
- }
40
-
41
- await validate(document);
42
- this.logger.success('OpenAPI definition is valid.');
43
- }
44
-
45
- /**
46
- * Read OpenAPI file from local filesystem
47
- */
48
- private async readLocalOpenApiFile(filename: string): Promise<unknown> {
49
- try {
50
- const fileContents = await fs.promises.readFile(filename, 'utf-8');
51
-
52
- // Try to parse as JSON first
53
- if (filename.endsWith('.json')) {
54
- return JSON.parse(fileContents);
55
- }
56
-
57
- // Try YAML parsing
58
- return yaml.load(fileContents);
59
- } catch (error) {
60
- if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
61
- throw new FileSystemError(`file not found, please check the path provided: ${filename}`);
62
- }
63
- throw new ValidationError(
64
- `Failed to parse OpenAPI spec: ${error instanceof Error ? error.message : 'unknown error'}`
65
- );
66
- }
67
- }
68
- }
@@ -1,47 +0,0 @@
1
- import detect from 'detect-port';
2
-
3
- import { NoAvailablePortError } from '../errors/index.js';
4
- import { CLI_CONSTANTS } from '../types/index.js';
5
- import type { ILogger } from '../utils/index.js';
6
-
7
- /**
8
- * Service for handling port detection and availability checks
9
- */
10
- export class PortService {
11
- constructor(private readonly logger?: ILogger) {}
12
-
13
- /**
14
- * Find an available port starting from the preferred port
15
- * @param preferredPort - The preferred port to start from (defaults to 3000)
16
- * @returns The first available port found
17
- * @throws {NoAvailablePortError} if no port is available after MAX_ATTEMPTS tries
18
- */
19
- async findAvailablePort(preferredPort?: number): Promise<number> {
20
- const startPort = preferredPort ?? CLI_CONSTANTS.PORT.DEFAULT;
21
-
22
- for (let attempt = 0; attempt < CLI_CONSTANTS.PORT.MAX_ATTEMPTS; attempt++) {
23
- const port = startPort + attempt;
24
-
25
- if (await this.isPortAvailable(port)) {
26
- if (attempt > 0 && this.logger) {
27
- this.logger.info(`port ${startPort} is already in use. using ${port} instead`);
28
- }
29
- return port;
30
- }
31
- }
32
-
33
- throw new NoAvailablePortError(
34
- `No available port found after ${CLI_CONSTANTS.PORT.MAX_ATTEMPTS} attempts starting from ${startPort}`
35
- );
36
- }
37
-
38
- /**
39
- * Check if a specific port is available
40
- * @param port - The port number to check
41
- * @returns true if the port is available, false otherwise
42
- */
43
- private async isPortAvailable(port: number): Promise<boolean> {
44
- const detectedPort = await detect(port);
45
- return detectedPort === port;
46
- }
47
- }
@@ -1,203 +0,0 @@
1
- import { docsConfigSchema } from '@poetora/validation';
2
- import AdmZip from 'adm-zip';
3
- import * as fs from 'fs';
4
- import * as fse from 'fs-extra';
5
- import * as path from 'path';
6
-
7
- import { ExternalServiceError, FileSystemError } from '../errors/index.js';
8
-
9
- export interface DirectoryStatus {
10
- exists: boolean;
11
- hasContents: boolean;
12
- }
13
-
14
- export interface InstallTemplateParams {
15
- directory: string;
16
- projectName: string;
17
- theme: string;
18
- }
19
-
20
- interface DocsConfig {
21
- name: string;
22
- theme: string;
23
- [key: string]: unknown;
24
- }
25
-
26
- /**
27
- * Service for managing documentation templates
28
- */
29
- export class TemplateService {
30
- private readonly TEMPLATE_URL = 'https://github.com/poetora/starter/archive/refs/heads/main.zip';
31
- private readonly TEMP_ZIP_PATH = 'poetora-starter.zip';
32
- private readonly TEMP_EXTRACT_DIR = 'poetora-starter-temp';
33
-
34
- /**
35
- * Check if directory exists and has contents
36
- */
37
- async checkDirectory(directory: string): Promise<DirectoryStatus> {
38
- try {
39
- await fse.ensureDir(directory);
40
-
41
- const files = await fs.promises.readdir(directory);
42
- const hasContents = files.length > 0;
43
-
44
- return {
45
- exists: true,
46
- hasContents,
47
- };
48
- } catch (error) {
49
- if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
50
- return {
51
- exists: false,
52
- hasContents: false,
53
- };
54
- }
55
- throw new FileSystemError(`Failed to check directory: ${(error as Error).message}`);
56
- }
57
- }
58
-
59
- /**
60
- * Get available themes from validation package
61
- */
62
- getAvailableThemes(): string[] {
63
- // docsConfigSchema is a discriminated union with 'theme' as the discriminator
64
- // Extract theme values from each option's shape.theme field
65
- const themes = docsConfigSchema.options
66
- .map((option) => {
67
- // Each option has a shape with theme field (ZodLiteral)
68
- const themeField = (
69
- option as unknown as { shape?: { theme?: { _def?: { value?: string } } } }
70
- ).shape?.theme;
71
- if (themeField?._def && 'value' in themeField._def) {
72
- return themeField._def.value as string;
73
- }
74
- return null;
75
- })
76
- .filter((theme): theme is string => theme !== null);
77
-
78
- return themes.length > 0 ? themes : ['ora'];
79
- }
80
-
81
- /**
82
- * Download and install template to target directory
83
- */
84
- async installTemplate(params: InstallTemplateParams): Promise<void> {
85
- const { directory, projectName, theme } = params;
86
-
87
- try {
88
- // Download template
89
- await this.downloadTemplate();
90
-
91
- // Extract template
92
- await this.extractTemplate();
93
-
94
- // Copy files to target directory
95
- await this.copyTemplateFiles(directory);
96
-
97
- // Update configuration
98
- await this.updateDocsConfig(directory, projectName, theme);
99
-
100
- // Cleanup temporary files
101
- await this.cleanup();
102
- } catch (error) {
103
- // Ensure cleanup on error
104
- await this.cleanup();
105
- throw error;
106
- }
107
- }
108
-
109
- private async downloadTemplate(): Promise<void> {
110
- try {
111
- const response = await fetch(this.TEMPLATE_URL);
112
-
113
- if (!response.ok) {
114
- throw new ExternalServiceError(
115
- `Failed to download template: HTTP ${response.status} ${response.statusText}`
116
- );
117
- }
118
-
119
- const arrayBuffer = await response.arrayBuffer();
120
- const buffer = Buffer.from(arrayBuffer);
121
-
122
- await fs.promises.writeFile(this.TEMP_ZIP_PATH, buffer);
123
- } catch (error) {
124
- if (error instanceof ExternalServiceError) {
125
- throw error;
126
- }
127
- throw new ExternalServiceError(`Failed to download template: ${(error as Error).message}`);
128
- }
129
- }
130
-
131
- private async extractTemplate(): Promise<void> {
132
- try {
133
- const zip = new AdmZip(this.TEMP_ZIP_PATH);
134
- zip.extractAllTo(this.TEMP_EXTRACT_DIR, true);
135
- } catch (error) {
136
- throw new FileSystemError(`Failed to extract template: ${(error as Error).message}`);
137
- }
138
- }
139
-
140
- private async copyTemplateFiles(targetDir: string): Promise<void> {
141
- try {
142
- // The extracted folder is typically named "starter-main"
143
- const extractedFolder = path.join(this.TEMP_EXTRACT_DIR, 'starter-main');
144
-
145
- // Ensure target directory exists
146
- await fse.ensureDir(targetDir);
147
-
148
- // Copy all files from extracted folder to target directory
149
- await fse.copy(extractedFolder, targetDir, {
150
- overwrite: true,
151
- errorOnExist: false,
152
- });
153
- } catch (error) {
154
- throw new FileSystemError(`Failed to copy template files: ${(error as Error).message}`);
155
- }
156
- }
157
-
158
- private async updateDocsConfig(
159
- directory: string,
160
- projectName: string,
161
- theme: string
162
- ): Promise<void> {
163
- try {
164
- const configPath = path.join(directory, 'docs.json');
165
-
166
- let config: DocsConfig;
167
-
168
- // Try to read existing config
169
- try {
170
- const configContent = await fs.promises.readFile(configPath, 'utf-8');
171
- config = JSON.parse(configContent) as DocsConfig;
172
- } catch {
173
- // If file doesn't exist or is invalid, create new config
174
- config = {} as DocsConfig;
175
- }
176
-
177
- // Update config with user choices
178
- config.name = projectName;
179
- config.theme = theme;
180
-
181
- // Write updated config
182
- await fs.promises.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
183
- } catch (error) {
184
- throw new FileSystemError(`Failed to update docs.json: ${(error as Error).message}`);
185
- }
186
- }
187
-
188
- private async cleanup(): Promise<void> {
189
- try {
190
- // Remove temporary files
191
- await Promise.all([
192
- fse.remove(this.TEMP_ZIP_PATH).catch(() => {
193
- /* ignore */
194
- }),
195
- fse.remove(this.TEMP_EXTRACT_DIR).catch(() => {
196
- /* ignore */
197
- }),
198
- ]);
199
- } catch {
200
- // Ignore cleanup errors
201
- }
202
- }
203
- }
@@ -1,76 +0,0 @@
1
- import { exec } from 'child_process';
2
- import { promisify } from 'util';
3
- import { ExternalServiceError } from '../errors/index.js';
4
- import type { ILogger } from '../utils/index.js';
5
- import type { VersionService } from './version.service.js';
6
-
7
- const execAsync = promisify(exec);
8
-
9
- /**
10
- * Service for CLI updates
11
- */
12
- export class UpdateService {
13
- constructor(
14
- private readonly logger: ILogger,
15
- private readonly versionService: VersionService,
16
- private readonly packageName: string
17
- ) {}
18
-
19
- /**
20
- * Update CLI to latest version
21
- */
22
- async update(): Promise<void> {
23
- const spinner = this.logger.spinner('checking for updates...');
24
- spinner.start();
25
-
26
- const existingCliVersion = this.versionService.getCliVersion();
27
- const latestCliVersion = this.versionService.getLatestCliVersion(this.packageName);
28
-
29
- const isUpToDate =
30
- existingCliVersion &&
31
- latestCliVersion &&
32
- latestCliVersion.trim() === existingCliVersion.trim();
33
-
34
- if (isUpToDate) {
35
- spinner.succeed('already up to date');
36
- return;
37
- }
38
-
39
- if (existingCliVersion && latestCliVersion.trim() !== existingCliVersion.trim()) {
40
- try {
41
- spinner.stop();
42
- const updateSpinner = this.logger.spinner(`updating ${this.packageName} package...`);
43
- updateSpinner.start();
44
-
45
- const packageManager = await this.detectPackageManager();
46
-
47
- if (packageManager === 'pnpm') {
48
- await execAsync(`pnpm install -g ${this.packageName}@latest --silent`);
49
- } else {
50
- await execAsync(`npm install -g ${this.packageName}@latest --silent`);
51
- }
52
-
53
- updateSpinner.succeed(
54
- `updated ${this.packageName} to version ${this.logger.highlight?.(latestCliVersion) ?? latestCliVersion}`
55
- );
56
- } catch (_err) {
57
- throw new ExternalServiceError(`failed to update ${this.packageName}@latest`);
58
- }
59
- }
60
- }
61
-
62
- /**
63
- * Detect which package manager was used to install the CLI
64
- */
65
- private async detectPackageManager(): Promise<'npm' | 'pnpm'> {
66
- try {
67
- const { stdout: packagePath } = await execAsync(`which ${this.packageName}`);
68
- if (packagePath.includes('pnpm')) {
69
- return 'pnpm';
70
- }
71
- return 'npm';
72
- } catch (_error) {
73
- return 'npm';
74
- }
75
- }
76
- }