@poetora/cli 0.1.8 → 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.
- package/bin/cli-builder.js +1 -1
- package/bin/services/version.service.d.ts +1 -1
- package/bin/services/version.service.js +12 -10
- package/package.json +8 -3
- package/.turbo/turbo-build.log +0 -4
- package/src/accessibility.ts +0 -180
- package/src/cli-builder.ts +0 -274
- package/src/cli.ts +0 -22
- package/src/commands/__tests__/base.command.test.ts +0 -139
- package/src/commands/__tests__/dev.command.test.ts +0 -241
- package/src/commands/__tests__/init.command.test.ts +0 -281
- package/src/commands/__tests__/utils.ts +0 -20
- package/src/commands/base.command.ts +0 -97
- package/src/commands/check.command.ts +0 -40
- package/src/commands/dev.command.ts +0 -63
- package/src/commands/index.ts +0 -6
- package/src/commands/init.command.ts +0 -125
- package/src/commands/link.command.ts +0 -39
- package/src/commands/update.command.ts +0 -23
- package/src/constants.ts +0 -4
- package/src/errors/cli-error.ts +0 -83
- package/src/errors/index.ts +0 -1
- package/src/index.ts +0 -110
- package/src/mdxAccessibility.ts +0 -132
- package/src/middlewares.ts +0 -73
- package/src/services/__tests__/port.service.test.ts +0 -83
- package/src/services/__tests__/template.service.test.ts +0 -234
- package/src/services/__tests__/version.service.test.ts +0 -165
- package/src/services/accessibility-check.service.ts +0 -226
- package/src/services/index.ts +0 -7
- package/src/services/link.service.ts +0 -65
- package/src/services/openapi-check.service.ts +0 -68
- package/src/services/port.service.ts +0 -47
- package/src/services/template.service.ts +0 -203
- package/src/services/update.service.ts +0 -76
- package/src/services/version.service.ts +0 -161
- package/src/start.ts +0 -6
- package/src/types/common.ts +0 -53
- package/src/types/index.ts +0 -2
- package/src/types/options.ts +0 -42
- package/src/utils/console-logger.ts +0 -123
- package/src/utils/index.ts +0 -2
- package/src/utils/logger.interface.ts +0 -70
- package/tsconfig.build.json +0 -17
- package/tsconfig.json +0 -21
- package/vitest.config.ts +0 -8
package/src/mdxAccessibility.ts
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import { categorizeFilePaths, getPoetIgnore } from '@poetora/prebuild';
|
|
2
|
-
import { coreRemark } from '@poetora/shared';
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import type { Node, Root, Text } from 'mdast';
|
|
5
|
-
import type { MdxJsxFlowElement } from 'mdast-util-mdx-jsx';
|
|
6
|
-
import path from 'path';
|
|
7
|
-
import { visit } from 'unist-util-visit';
|
|
8
|
-
|
|
9
|
-
export interface AccessibilityFixAttribute {
|
|
10
|
-
filePath: string;
|
|
11
|
-
line?: number;
|
|
12
|
-
column?: number;
|
|
13
|
-
element: 'img' | 'video' | 'a';
|
|
14
|
-
tagName: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface MdxAccessibilityResult {
|
|
18
|
-
missingAltAttributes: AccessibilityFixAttribute[];
|
|
19
|
-
totalFiles: number;
|
|
20
|
-
filesWithIssues: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const checkAltAttributes = (filePath: string, content: string): AccessibilityFixAttribute[] => {
|
|
24
|
-
const issues: AccessibilityFixAttribute[] = [];
|
|
25
|
-
|
|
26
|
-
const visitElements = () => {
|
|
27
|
-
return (tree: Root) => {
|
|
28
|
-
visit(tree, (node) => {
|
|
29
|
-
if (node.type === 'image') {
|
|
30
|
-
if (!node.alt || node.alt.trim() === '') {
|
|
31
|
-
issues.push({
|
|
32
|
-
filePath,
|
|
33
|
-
line: node.position?.start.line,
|
|
34
|
-
column: node.position?.start.column,
|
|
35
|
-
element: 'img',
|
|
36
|
-
tagName: 'image (markdown)',
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const mdxJsxElement = node as MdxJsxFlowElement;
|
|
43
|
-
if (mdxJsxElement.name === 'img' || mdxJsxElement.name === 'video') {
|
|
44
|
-
const altAttrIndex = mdxJsxElement.attributes.findIndex(
|
|
45
|
-
(attr) => attr.type === 'mdxJsxAttribute' && attr.name === 'alt'
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
const altAttribute = mdxJsxElement.attributes[altAttrIndex];
|
|
49
|
-
const hasValidAlt =
|
|
50
|
-
altAttribute &&
|
|
51
|
-
typeof altAttribute.value === 'string' &&
|
|
52
|
-
altAttribute.value.trim() !== '';
|
|
53
|
-
|
|
54
|
-
if (!hasValidAlt) {
|
|
55
|
-
issues.push({
|
|
56
|
-
filePath,
|
|
57
|
-
line: node.position?.start.line,
|
|
58
|
-
column: node.position?.start.column,
|
|
59
|
-
element: mdxJsxElement.name,
|
|
60
|
-
tagName: mdxJsxElement.name,
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
} else if (mdxJsxElement.name === 'a') {
|
|
64
|
-
const hasTextContent = (children: Node[]): boolean => {
|
|
65
|
-
return children.some((child) => {
|
|
66
|
-
if (child.type === 'text') {
|
|
67
|
-
const textNode = child as Text;
|
|
68
|
-
return textNode.value.trim() !== '';
|
|
69
|
-
}
|
|
70
|
-
if ('children' in child && Array.isArray(child.children)) {
|
|
71
|
-
return hasTextContent(child.children as Node[]);
|
|
72
|
-
}
|
|
73
|
-
return false;
|
|
74
|
-
});
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
if (!hasTextContent(mdxJsxElement.children as Node[])) {
|
|
78
|
-
issues.push({
|
|
79
|
-
filePath,
|
|
80
|
-
line: node.position?.start.line,
|
|
81
|
-
column: node.position?.start.column,
|
|
82
|
-
element: 'a',
|
|
83
|
-
tagName: '<a>',
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
return tree;
|
|
89
|
-
};
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
try {
|
|
93
|
-
coreRemark().use(visitElements).processSync(content);
|
|
94
|
-
} catch (error) {
|
|
95
|
-
console.warn(`Warning: Could not parse ${filePath}: ${error}`);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return issues;
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
export const checkMdxAccessibility = async (
|
|
102
|
-
baseDir: string = process.cwd()
|
|
103
|
-
): Promise<MdxAccessibilityResult> => {
|
|
104
|
-
const poetIgnore = await getPoetIgnore(baseDir);
|
|
105
|
-
const { contentFilenames } = await categorizeFilePaths(baseDir, poetIgnore);
|
|
106
|
-
const mdxFiles: string[] = [];
|
|
107
|
-
for (const file of contentFilenames) {
|
|
108
|
-
mdxFiles.push(path.join(baseDir, file));
|
|
109
|
-
}
|
|
110
|
-
const allIssues: AccessibilityFixAttribute[] = [];
|
|
111
|
-
const filesWithIssues = new Set<string>();
|
|
112
|
-
|
|
113
|
-
for (const filePath of mdxFiles) {
|
|
114
|
-
try {
|
|
115
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
116
|
-
const issues = checkAltAttributes(filePath, content);
|
|
117
|
-
|
|
118
|
-
if (issues.length > 0) {
|
|
119
|
-
allIssues.push(...issues);
|
|
120
|
-
filesWithIssues.add(filePath);
|
|
121
|
-
}
|
|
122
|
-
} catch (error) {
|
|
123
|
-
console.warn(`Warning: Could not read file ${filePath}: ${error}`);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return {
|
|
128
|
-
missingAltAttributes: allIssues,
|
|
129
|
-
totalFiles: mdxFiles.length,
|
|
130
|
-
filesWithIssues: filesWithIssues.size,
|
|
131
|
-
};
|
|
132
|
-
};
|
package/src/middlewares.ts
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { addLog, ErrorLog } from '@poetora/previewing';
|
|
2
|
-
import React from 'react';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Yargs middleware functions
|
|
6
|
-
* These run before command execution
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Check if Node.js version is supported
|
|
11
|
-
* Middleware that validates Node version before any command runs
|
|
12
|
-
*
|
|
13
|
-
* Minimum requirement: Node.js >= 18.0.0
|
|
14
|
-
* Note: Individual commands may have stricter requirements (e.g., dev command)
|
|
15
|
-
*/
|
|
16
|
-
export const checkNodeVersion = async (): Promise<void> => {
|
|
17
|
-
let nodeVersionString = process.version;
|
|
18
|
-
if (nodeVersionString.charAt(0) === 'v') {
|
|
19
|
-
nodeVersionString = nodeVersionString.slice(1);
|
|
20
|
-
}
|
|
21
|
-
const versionArr = nodeVersionString.split('.');
|
|
22
|
-
const versionStr = versionArr[0];
|
|
23
|
-
if (!versionStr) {
|
|
24
|
-
addLog(
|
|
25
|
-
React.createElement(ErrorLog, {
|
|
26
|
-
message: `Unable to determine Node.js version (got "${process.version}"). Please ensure you are running Node.js >= 18.0.0.`,
|
|
27
|
-
})
|
|
28
|
-
);
|
|
29
|
-
process.exit(1);
|
|
30
|
-
}
|
|
31
|
-
const majorVersion = parseInt(versionStr, 10);
|
|
32
|
-
|
|
33
|
-
if (majorVersion < 18) {
|
|
34
|
-
addLog(
|
|
35
|
-
React.createElement(ErrorLog, {
|
|
36
|
-
message: `poetora requires Node.js >= 18.0.0 (current version ${nodeVersionString}). Please upgrade Node.js and try again.`,
|
|
37
|
-
})
|
|
38
|
-
);
|
|
39
|
-
process.exit(1);
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Suppress common console warnings that don't affect functionality
|
|
45
|
-
* Filters out known noise from dependencies
|
|
46
|
-
*/
|
|
47
|
-
export const suppressConsoleWarnings = (): void => {
|
|
48
|
-
// Ignore tailwind warnings and punycode deprecation warning
|
|
49
|
-
const ignoredMessages = [
|
|
50
|
-
'No utility classes were detected',
|
|
51
|
-
'https://tailwindcss.com/docs/content-configuration',
|
|
52
|
-
'DeprecationWarning',
|
|
53
|
-
'punycode',
|
|
54
|
-
];
|
|
55
|
-
|
|
56
|
-
const originalConsoleError = console.error;
|
|
57
|
-
console.error = (...args) => {
|
|
58
|
-
const message = args.join(' ');
|
|
59
|
-
if (ignoredMessages.some((ignoredMessage) => message.includes(ignoredMessage))) {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
originalConsoleError.apply(console, args);
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
const originalConsoleWarn = console.warn;
|
|
66
|
-
console.warn = (...args) => {
|
|
67
|
-
const message = args.join(' ');
|
|
68
|
-
if (ignoredMessages.some((ignoredMessage) => message.includes(ignoredMessage))) {
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
originalConsoleWarn.apply(console, args);
|
|
72
|
-
};
|
|
73
|
-
};
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import detect from 'detect-port';
|
|
2
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
|
|
4
|
-
import { NoAvailablePortError } from '../../errors/index.js';
|
|
5
|
-
import { CLI_CONSTANTS } from '../../types/index.js';
|
|
6
|
-
import { PortService } from '../port.service.js';
|
|
7
|
-
|
|
8
|
-
vi.mock('detect-port');
|
|
9
|
-
|
|
10
|
-
describe('PortService', () => {
|
|
11
|
-
let service: PortService;
|
|
12
|
-
const mockDetect = vi.mocked(detect);
|
|
13
|
-
|
|
14
|
-
beforeEach(() => {
|
|
15
|
-
service = new PortService();
|
|
16
|
-
vi.clearAllMocks();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
describe('findAvailablePort', () => {
|
|
20
|
-
it('should return preferred port when available', async () => {
|
|
21
|
-
mockDetect.mockResolvedValue(3000);
|
|
22
|
-
|
|
23
|
-
const port = await service.findAvailablePort(3000);
|
|
24
|
-
|
|
25
|
-
expect(port).toBe(3000);
|
|
26
|
-
expect(mockDetect).toHaveBeenCalledWith(3000);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('should return default port when no preferred port specified', async () => {
|
|
30
|
-
mockDetect.mockResolvedValue(CLI_CONSTANTS.PORT.DEFAULT);
|
|
31
|
-
|
|
32
|
-
const port = await service.findAvailablePort();
|
|
33
|
-
|
|
34
|
-
expect(port).toBe(CLI_CONSTANTS.PORT.DEFAULT);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('should try next port when first is occupied', async () => {
|
|
38
|
-
mockDetect.mockResolvedValueOnce(3001); // 3000 occupied
|
|
39
|
-
mockDetect.mockResolvedValueOnce(3001); // 3001 available
|
|
40
|
-
|
|
41
|
-
const port = await service.findAvailablePort(3000);
|
|
42
|
-
|
|
43
|
-
expect(port).toBe(3001);
|
|
44
|
-
expect(mockDetect).toHaveBeenCalledTimes(2);
|
|
45
|
-
expect(mockDetect).toHaveBeenNthCalledWith(1, 3000);
|
|
46
|
-
expect(mockDetect).toHaveBeenNthCalledWith(2, 3001);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('should try multiple ports until finding available one', async () => {
|
|
50
|
-
// Ports 3000, 3001, 3002 are occupied
|
|
51
|
-
mockDetect.mockResolvedValueOnce(3001);
|
|
52
|
-
mockDetect.mockResolvedValueOnce(3002);
|
|
53
|
-
mockDetect.mockResolvedValueOnce(3003);
|
|
54
|
-
// Port 3003 is available
|
|
55
|
-
mockDetect.mockResolvedValueOnce(3003);
|
|
56
|
-
|
|
57
|
-
const port = await service.findAvailablePort(3000);
|
|
58
|
-
|
|
59
|
-
expect(port).toBe(3003);
|
|
60
|
-
expect(mockDetect).toHaveBeenCalledTimes(4);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('should throw NoAvailablePortError after MAX_ATTEMPTS', async () => {
|
|
64
|
-
// All ports are occupied
|
|
65
|
-
mockDetect.mockImplementation((port) => Promise.resolve((port as number) + 1));
|
|
66
|
-
|
|
67
|
-
await expect(service.findAvailablePort(3000)).rejects.toThrow(NoAvailablePortError);
|
|
68
|
-
expect(mockDetect).toHaveBeenCalledTimes(CLI_CONSTANTS.PORT.MAX_ATTEMPTS);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('should include start port in error message', async () => {
|
|
72
|
-
mockDetect.mockImplementation((port) => Promise.resolve((port as number) + 1));
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
await service.findAvailablePort(5000);
|
|
76
|
-
expect.fail('Should have thrown error');
|
|
77
|
-
} catch (error) {
|
|
78
|
-
expect(error).toBeInstanceOf(NoAvailablePortError);
|
|
79
|
-
expect((error as NoAvailablePortError).message).toContain('5000');
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
});
|
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
import AdmZip from 'adm-zip';
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
import * as fse from 'fs-extra';
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
-
import { ExternalServiceError, FileSystemError } from '../../errors/index.js';
|
|
6
|
-
import { TemplateService } from '../template.service.js';
|
|
7
|
-
|
|
8
|
-
vi.mock('node:fs', () => ({
|
|
9
|
-
default: {
|
|
10
|
-
promises: {
|
|
11
|
-
readdir: vi.fn(),
|
|
12
|
-
writeFile: vi.fn(),
|
|
13
|
-
readFile: vi.fn(),
|
|
14
|
-
},
|
|
15
|
-
},
|
|
16
|
-
promises: {
|
|
17
|
-
readdir: vi.fn(),
|
|
18
|
-
writeFile: vi.fn(),
|
|
19
|
-
readFile: vi.fn(),
|
|
20
|
-
},
|
|
21
|
-
}));
|
|
22
|
-
|
|
23
|
-
vi.mock('fs-extra');
|
|
24
|
-
vi.mock('adm-zip', () => ({
|
|
25
|
-
default: vi.fn(),
|
|
26
|
-
}));
|
|
27
|
-
|
|
28
|
-
describe('TemplateService', () => {
|
|
29
|
-
let service: TemplateService;
|
|
30
|
-
|
|
31
|
-
beforeEach(() => {
|
|
32
|
-
service = new TemplateService();
|
|
33
|
-
vi.clearAllMocks();
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
afterEach(() => {
|
|
37
|
-
vi.restoreAllMocks();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
describe('checkDirectory', () => {
|
|
41
|
-
it('should return exists=true and hasContents=true for non-empty directory', async () => {
|
|
42
|
-
vi.mocked(fse.ensureDir).mockResolvedValue(undefined);
|
|
43
|
-
vi.mocked(fs.promises.readdir).mockResolvedValue([
|
|
44
|
-
'file1.txt',
|
|
45
|
-
'file2.txt',
|
|
46
|
-
] as unknown as fs.Dirent[]);
|
|
47
|
-
|
|
48
|
-
const result = await service.checkDirectory('/test/dir');
|
|
49
|
-
|
|
50
|
-
expect(result).toEqual({
|
|
51
|
-
exists: true,
|
|
52
|
-
hasContents: true,
|
|
53
|
-
});
|
|
54
|
-
expect(fse.ensureDir).toHaveBeenCalledWith('/test/dir');
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('should return exists=true and hasContents=false for empty directory', async () => {
|
|
58
|
-
vi.mocked(fse.ensureDir).mockResolvedValue(undefined);
|
|
59
|
-
vi.mocked(fs.promises.readdir).mockResolvedValue([] as unknown as fs.Dirent[]);
|
|
60
|
-
|
|
61
|
-
const result = await service.checkDirectory('/test/empty');
|
|
62
|
-
|
|
63
|
-
expect(result).toEqual({
|
|
64
|
-
exists: true,
|
|
65
|
-
hasContents: false,
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('should return exists=false for non-existent directory on ENOENT', async () => {
|
|
70
|
-
const error = new Error('ENOENT') as NodeJS.ErrnoException;
|
|
71
|
-
error.code = 'ENOENT';
|
|
72
|
-
vi.mocked(fse.ensureDir).mockRejectedValue(error);
|
|
73
|
-
|
|
74
|
-
const result = await service.checkDirectory('/nonexistent');
|
|
75
|
-
|
|
76
|
-
expect(result).toEqual({
|
|
77
|
-
exists: false,
|
|
78
|
-
hasContents: false,
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('should throw FileSystemError for other errors', async () => {
|
|
83
|
-
vi.mocked(fse.ensureDir).mockRejectedValue(new Error('Permission denied'));
|
|
84
|
-
|
|
85
|
-
await expect(service.checkDirectory('/test/dir')).rejects.toThrow(FileSystemError);
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
describe('getAvailableThemes', () => {
|
|
90
|
-
it('should return available themes from docsConfigSchema', () => {
|
|
91
|
-
const themes = service.getAvailableThemes();
|
|
92
|
-
|
|
93
|
-
expect(themes).toBeInstanceOf(Array);
|
|
94
|
-
expect(themes.length).toBeGreaterThan(0);
|
|
95
|
-
expect(themes).toContain('ora');
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
describe('installTemplate', () => {
|
|
100
|
-
const mockFetch = vi.fn();
|
|
101
|
-
const originalFetch = global.fetch;
|
|
102
|
-
|
|
103
|
-
beforeEach(() => {
|
|
104
|
-
global.fetch = mockFetch;
|
|
105
|
-
|
|
106
|
-
// Mock successful download
|
|
107
|
-
mockFetch.mockResolvedValue({
|
|
108
|
-
ok: true,
|
|
109
|
-
arrayBuffer: vi.fn().mockResolvedValue(new ArrayBuffer(8)),
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// Mock file operations
|
|
113
|
-
vi.mocked(fs.promises.writeFile).mockResolvedValue(undefined);
|
|
114
|
-
vi.mocked(fs.promises.readFile).mockResolvedValue('{}');
|
|
115
|
-
vi.mocked(fse.ensureDir).mockResolvedValue(undefined);
|
|
116
|
-
vi.mocked(fse.copy).mockResolvedValue(undefined);
|
|
117
|
-
vi.mocked(fse.remove).mockResolvedValue(undefined);
|
|
118
|
-
|
|
119
|
-
// Mock AdmZip constructor - must use function for constructor
|
|
120
|
-
vi.mocked(AdmZip).mockImplementation(function (this: AdmZip) {
|
|
121
|
-
return {
|
|
122
|
-
extractAllTo: vi.fn(),
|
|
123
|
-
} as unknown as AdmZip;
|
|
124
|
-
} as unknown as typeof AdmZip);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
afterEach(() => {
|
|
128
|
-
global.fetch = originalFetch;
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('should successfully install template', async () => {
|
|
132
|
-
await service.installTemplate({
|
|
133
|
-
directory: '/test/project',
|
|
134
|
-
projectName: 'My Project',
|
|
135
|
-
theme: 'quartz',
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
expect(mockFetch).toHaveBeenCalledWith(
|
|
139
|
-
'https://github.com/poetora/starter/archive/refs/heads/main.zip'
|
|
140
|
-
);
|
|
141
|
-
expect(fse.copy).toHaveBeenCalled();
|
|
142
|
-
expect(fs.promises.writeFile).toHaveBeenCalledWith(
|
|
143
|
-
'/test/project/docs.json',
|
|
144
|
-
expect.stringContaining('My Project'),
|
|
145
|
-
'utf-8'
|
|
146
|
-
);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it('should throw ExternalServiceError on download failure', async () => {
|
|
150
|
-
mockFetch.mockResolvedValue({
|
|
151
|
-
ok: false,
|
|
152
|
-
status: 404,
|
|
153
|
-
statusText: 'Not Found',
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
await expect(
|
|
157
|
-
service.installTemplate({
|
|
158
|
-
directory: '/test/project',
|
|
159
|
-
projectName: 'My Project',
|
|
160
|
-
theme: 'quartz',
|
|
161
|
-
})
|
|
162
|
-
).rejects.toThrow(ExternalServiceError);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('should cleanup temporary files after successful installation', async () => {
|
|
166
|
-
await service.installTemplate({
|
|
167
|
-
directory: '/test/project',
|
|
168
|
-
projectName: 'My Project',
|
|
169
|
-
theme: 'quartz',
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
expect(fse.remove).toHaveBeenCalledWith('poetora-starter.zip');
|
|
173
|
-
expect(fse.remove).toHaveBeenCalledWith('poetora-starter-temp');
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it('should cleanup temporary files even on error', async () => {
|
|
177
|
-
vi.mocked(fse.copy).mockRejectedValue(new Error('Copy failed'));
|
|
178
|
-
|
|
179
|
-
await expect(
|
|
180
|
-
service.installTemplate({
|
|
181
|
-
directory: '/test/project',
|
|
182
|
-
projectName: 'My Project',
|
|
183
|
-
theme: 'quartz',
|
|
184
|
-
})
|
|
185
|
-
).rejects.toThrow();
|
|
186
|
-
|
|
187
|
-
expect(fse.remove).toHaveBeenCalled();
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it('should handle missing docs.json by creating new config', async () => {
|
|
191
|
-
vi.mocked(fs.promises.readFile).mockRejectedValue(new Error('ENOENT'));
|
|
192
|
-
|
|
193
|
-
await service.installTemplate({
|
|
194
|
-
directory: '/test/project',
|
|
195
|
-
projectName: 'My Project',
|
|
196
|
-
theme: 'quartz',
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
expect(fs.promises.writeFile).toHaveBeenCalledWith(
|
|
200
|
-
'/test/project/docs.json',
|
|
201
|
-
expect.stringContaining('"name": "My Project"'),
|
|
202
|
-
'utf-8'
|
|
203
|
-
);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it('should preserve existing config fields when updating', async () => {
|
|
207
|
-
vi.mocked(fs.promises.readFile).mockResolvedValue(
|
|
208
|
-
JSON.stringify({
|
|
209
|
-
name: 'Old Name',
|
|
210
|
-
theme: 'old-theme',
|
|
211
|
-
customField: 'preserved',
|
|
212
|
-
})
|
|
213
|
-
);
|
|
214
|
-
|
|
215
|
-
await service.installTemplate({
|
|
216
|
-
directory: '/test/project',
|
|
217
|
-
projectName: 'New Project',
|
|
218
|
-
theme: 'quartz',
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
const writeCall = vi
|
|
222
|
-
.mocked(fs.promises.writeFile)
|
|
223
|
-
.mock.calls.find((call) => call[0] === '/test/project/docs.json');
|
|
224
|
-
|
|
225
|
-
expect(writeCall).toBeDefined();
|
|
226
|
-
const writtenConfig = JSON.parse(writeCall?.[1] as string);
|
|
227
|
-
expect(writtenConfig).toMatchObject({
|
|
228
|
-
name: 'New Project',
|
|
229
|
-
theme: 'quartz',
|
|
230
|
-
customField: 'preserved',
|
|
231
|
-
});
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
});
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { VersionService } from '../version.service.js';
|
|
3
|
-
|
|
4
|
-
describe('VersionService', () => {
|
|
5
|
-
let service: VersionService;
|
|
6
|
-
let _originalVersion: string;
|
|
7
|
-
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
service = new VersionService();
|
|
10
|
-
_originalVersion = process.version;
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
afterEach(() => {
|
|
14
|
-
vi.restoreAllMocks();
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
describe('parseNodeVersion', () => {
|
|
18
|
-
it('should parse version with v prefix', () => {
|
|
19
|
-
vi.spyOn(process, 'version', 'get').mockReturnValue('v20.17.0');
|
|
20
|
-
|
|
21
|
-
const version = service.parseNodeVersion();
|
|
22
|
-
|
|
23
|
-
expect(version.major).toBe(20);
|
|
24
|
-
expect(version.minor).toBe(17);
|
|
25
|
-
expect(version.patch).toBe(0);
|
|
26
|
-
expect(version.raw).toBe('20.17.0');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('should parse version without v prefix', () => {
|
|
30
|
-
vi.spyOn(process, 'version', 'get').mockReturnValue('18.0.0');
|
|
31
|
-
|
|
32
|
-
const version = service.parseNodeVersion();
|
|
33
|
-
|
|
34
|
-
expect(version.major).toBe(18);
|
|
35
|
-
expect(version.minor).toBe(0);
|
|
36
|
-
expect(version.patch).toBe(0);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('should parse version with only major.minor', () => {
|
|
40
|
-
vi.spyOn(process, 'version', 'get').mockReturnValue('v20.17');
|
|
41
|
-
|
|
42
|
-
const version = service.parseNodeVersion();
|
|
43
|
-
|
|
44
|
-
expect(version.major).toBe(20);
|
|
45
|
-
expect(version.minor).toBe(17);
|
|
46
|
-
expect(version.patch).toBe(0);
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
describe('checkNodeVersion', () => {
|
|
51
|
-
it('should pass for Node 20.17.0', () => {
|
|
52
|
-
vi.spyOn(process, 'version', 'get').mockReturnValue('v20.17.0');
|
|
53
|
-
|
|
54
|
-
const result = service.checkNodeVersion();
|
|
55
|
-
|
|
56
|
-
expect(result.isValid).toBe(true);
|
|
57
|
-
expect(result.hasWarning).toBe(false);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('should pass for Node 18.0.0 (minimum)', () => {
|
|
61
|
-
vi.spyOn(process, 'version', 'get').mockReturnValue('v18.0.0');
|
|
62
|
-
|
|
63
|
-
const result = service.checkNodeVersion();
|
|
64
|
-
|
|
65
|
-
expect(result.isValid).toBe(true);
|
|
66
|
-
expect(result.hasWarning).toBe(true);
|
|
67
|
-
expect(result.message).toContain('20.17');
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('should pass for Node 24.x', () => {
|
|
71
|
-
vi.spyOn(process, 'version', 'get').mockReturnValue('v24.0.0');
|
|
72
|
-
|
|
73
|
-
const result = service.checkNodeVersion();
|
|
74
|
-
|
|
75
|
-
expect(result.isValid).toBe(true);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('should fail for Node 17.x', () => {
|
|
79
|
-
vi.spyOn(process, 'version', 'get').mockReturnValue('v17.9.0');
|
|
80
|
-
|
|
81
|
-
const result = service.checkNodeVersion();
|
|
82
|
-
|
|
83
|
-
expect(result.isValid).toBe(false);
|
|
84
|
-
expect(result.message).toContain('18.0');
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('should fail for Node 25.x', () => {
|
|
88
|
-
vi.spyOn(process, 'version', 'get').mockReturnValue('v25.0.0');
|
|
89
|
-
|
|
90
|
-
const result = service.checkNodeVersion();
|
|
91
|
-
|
|
92
|
-
expect(result.isValid).toBe(false);
|
|
93
|
-
expect(result.message).toContain('not supported');
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('should warn for Node 19.x (below recommended)', () => {
|
|
97
|
-
vi.spyOn(process, 'version', 'get').mockReturnValue('v19.5.0');
|
|
98
|
-
|
|
99
|
-
const result = service.checkNodeVersion();
|
|
100
|
-
|
|
101
|
-
expect(result.isValid).toBe(true);
|
|
102
|
-
expect(result.hasWarning).toBe(true);
|
|
103
|
-
expect(result.message).toContain('recommended');
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('should warn for Node 20.16 (below recommended minor)', () => {
|
|
107
|
-
vi.spyOn(process, 'version', 'get').mockReturnValue('v20.16.0');
|
|
108
|
-
|
|
109
|
-
const result = service.checkNodeVersion();
|
|
110
|
-
|
|
111
|
-
expect(result.isValid).toBe(true);
|
|
112
|
-
expect(result.hasWarning).toBe(true);
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
describe('getCliVersion', () => {
|
|
117
|
-
it('should return test-cli in test mode', () => {
|
|
118
|
-
const originalEnv = process.env.CLI_TEST_MODE;
|
|
119
|
-
process.env.CLI_TEST_MODE = 'true';
|
|
120
|
-
|
|
121
|
-
const version = service.getCliVersion();
|
|
122
|
-
|
|
123
|
-
expect(version).toBe('test-cli');
|
|
124
|
-
|
|
125
|
-
process.env.CLI_TEST_MODE = originalEnv;
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it('should return version from package.json', () => {
|
|
129
|
-
const version = service.getCliVersion();
|
|
130
|
-
|
|
131
|
-
expect(version).toBeDefined();
|
|
132
|
-
expect(typeof version).toBe('string');
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
describe('getLatestCliVersion', () => {
|
|
137
|
-
it('should return latest version from npm', () => {
|
|
138
|
-
const version = service.getLatestCliVersion('@poetora/cli');
|
|
139
|
-
|
|
140
|
-
expect(version).toBeDefined();
|
|
141
|
-
expect(typeof version).toBe('string');
|
|
142
|
-
expect(version).toMatch(/^\d+\.\d+\.\d+/);
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
describe('isVersionUpToDate', () => {
|
|
147
|
-
it('should return true when versions match', () => {
|
|
148
|
-
const result = service.isVersionUpToDate('1.0.0', '1.0.0');
|
|
149
|
-
|
|
150
|
-
expect(result).toBe(true);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('should return false when versions differ', () => {
|
|
154
|
-
const result = service.isVersionUpToDate('1.0.0', '1.0.1');
|
|
155
|
-
|
|
156
|
-
expect(result).toBe(false);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('should handle whitespace', () => {
|
|
160
|
-
const result = service.isVersionUpToDate('1.0.0 ', ' 1.0.0');
|
|
161
|
-
|
|
162
|
-
expect(result).toBe(true);
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
});
|