@poetora/cli 0.0.1

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/start.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/bin/start.js ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { cli } from './cli.js';
3
+ const packageName = process.env.POETORA_PACKAGE_NAME ?? 'poet';
4
+ cli({ packageName });
@@ -0,0 +1,3 @@
1
+ export declare const update: ({ packageName }: {
2
+ packageName: string;
3
+ }) => Promise<void>;
package/bin/update.js ADDED
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { SpinnerLog, SuccessLog, ErrorLog, addLog, clearLogs } from '@poetora/previewing';
3
+ import { execAsync, getLatestCliVersion, getVersions, detectPackageManager } from './helpers.js';
4
+ export const update = async ({ packageName }) => {
5
+ addLog(_jsx(SpinnerLog, { message: "updating..." }));
6
+ const { cli: existingCliVersion } = getVersions();
7
+ const latestCliVersion = getLatestCliVersion(packageName);
8
+ const isUpToDate = existingCliVersion && latestCliVersion && latestCliVersion.trim() === existingCliVersion.trim();
9
+ if (isUpToDate) {
10
+ addLog(_jsx(SuccessLog, { message: "already up to date" }));
11
+ return;
12
+ }
13
+ if (existingCliVersion && latestCliVersion.trim() !== existingCliVersion.trim()) {
14
+ try {
15
+ clearLogs();
16
+ addLog(_jsx(SpinnerLog, { message: `updating ${packageName} package...` }));
17
+ const packageManager = await detectPackageManager({ packageName });
18
+ if (packageManager === 'pnpm') {
19
+ await execAsync(`pnpm install -g ${packageName}@latest --silent`);
20
+ }
21
+ else {
22
+ await execAsync(`npm install -g ${packageName}@latest --silent`);
23
+ }
24
+ }
25
+ catch (err) {
26
+ addLog(_jsx(ErrorLog, { message: `failed to update ${packageName}@latest` }));
27
+ return;
28
+ }
29
+ }
30
+ clearLogs();
31
+ addLog(_jsx(SuccessLog, { message: `updated ${packageName} to the latest version: ${latestCliVersion}` }));
32
+ };
package/package.json ADDED
@@ -0,0 +1,83 @@
1
+ {
2
+ "name": "@poetora/cli",
3
+ "version": "0.0.1",
4
+ "description": "The CLI for Poetora documentation Engine",
5
+ "engines": {
6
+ "node": ">=18.0.0"
7
+ },
8
+ "author": "Poetora",
9
+ "bugs": {
10
+ "url": "https://github.com/poetora/docs/issues"
11
+ },
12
+ "license": "Elastic-2.0",
13
+ "keywords": [
14
+ "poetora",
15
+ "poet",
16
+ "cli"
17
+ ],
18
+ "type": "module",
19
+ "publishConfig": {
20
+ "access": "public",
21
+ "registry": "https://registry.npmjs.org/"
22
+ },
23
+ "exports": "./bin/index.js",
24
+ "bin": {
25
+ "poet": "bin/index.js",
26
+ "poetora": "bin/index.js"
27
+ },
28
+ "scripts": {
29
+ "dev": "pnpm build && NODE_NO_WARNINGS=1 node bin/index.js",
30
+ "build": "tsc --project tsconfig.build.json",
31
+ "clean:build": "rimraf bin",
32
+ "clean:all": "rimraf node_modules .eslintcache && pnpm clean:build",
33
+ "watch": "tsc --watch",
34
+ "test": "vitest run",
35
+ "type": "tsc --noEmit",
36
+ "format": "prettier . --write",
37
+ "format:check": "prettier . --check"
38
+ },
39
+ "dependencies": {
40
+ "@inquirer/prompts": "7.9.0",
41
+ "@poetora/link-rot": "workspace:*",
42
+ "@poetora/prebuild": "workspace:*",
43
+ "@poetora/previewing": "workspace:*",
44
+ "@poetora/shared": "workspace:*",
45
+ "@poetora/validation": "workspace:*",
46
+ "adm-zip": "0.5.16",
47
+ "chalk": "5.2.0",
48
+ "color": "4.2.3",
49
+ "detect-port": "1.5.1",
50
+ "front-matter": "4.0.2",
51
+ "fs-extra": "11.2.0",
52
+ "ink": "6.3.0",
53
+ "inquirer": "12.3.0",
54
+ "js-yaml": "4.1.0",
55
+ "mdast-util-mdx-jsx": "3.2.0",
56
+ "react": "19.2.1",
57
+ "semver": "7.7.2",
58
+ "unist-util-visit": "5.0.0",
59
+ "yargs": "17.7.1"
60
+ },
61
+ "devDependencies": {
62
+ "@trivago/prettier-plugin-sort-imports": "4.3.0",
63
+ "@tsconfig/recommended": "1.0.2",
64
+ "@types/adm-zip": "0.5.7",
65
+ "@types/color": "^3.0.3",
66
+ "@types/detect-port": "1.3.2",
67
+ "@types/fs-extra": "^9.0.13",
68
+ "@types/js-yaml": "^4.0.9",
69
+ "@types/mdast": "4.0.4",
70
+ "@types/node": "catalog:",
71
+ "@types/yargs": "17.0.22",
72
+ "@typescript-eslint/eslint-plugin": "catalog:",
73
+ "@typescript-eslint/parser": "catalog:",
74
+ "eslint": "catalog:",
75
+ "eslint-config-prettier": "catalog:",
76
+ "openapi-types": "12.1.3",
77
+ "prettier": "3.1.1",
78
+ "rimraf": "5.0.1",
79
+ "typescript": "catalog:",
80
+ "vitest": "catalog:",
81
+ "vitest-mock-process": "1.0.4"
82
+ }
83
+ }
@@ -0,0 +1,180 @@
1
+ import Color from 'color';
2
+
3
+ export const WCAG_STANDARDS = {
4
+ AA_NORMAL: 4.5,
5
+ AA_LARGE: 3,
6
+ AAA_NORMAL: 7,
7
+ AAA_LARGE: 4.5,
8
+ } as const;
9
+
10
+ export type ContrastResult = {
11
+ ratio: number;
12
+ meetsAA: boolean;
13
+ meetsAAA: boolean;
14
+ recommendation: 'pass' | 'warning' | 'fail';
15
+ message: string;
16
+ };
17
+
18
+ export function checkColorContrast(
19
+ foreground: string,
20
+ background: string,
21
+ minThreshold: number = WCAG_STANDARDS.AA_NORMAL
22
+ ): ContrastResult | null {
23
+ try {
24
+ const fg = Color(foreground);
25
+ const bg = Color(background);
26
+
27
+ const ratio = fg.contrast(bg);
28
+ const level = fg.level(bg);
29
+
30
+ const meetsAA = level === 'AA' || level === 'AAA';
31
+ const meetsAAA = level === 'AAA';
32
+
33
+ let recommendation: 'pass' | 'warning' | 'fail';
34
+ let message: string;
35
+
36
+ if (minThreshold !== WCAG_STANDARDS.AA_NORMAL) {
37
+ if (ratio >= minThreshold) {
38
+ recommendation = 'pass';
39
+ message = `Contrast ratio: ${ratio.toFixed(
40
+ 2
41
+ )}:1 (meets minimum threshold of ${minThreshold}:1)`;
42
+ } else {
43
+ recommendation = 'fail';
44
+ message = `Poor contrast ratio: ${ratio.toFixed(
45
+ 2
46
+ )}:1 (fails minimum threshold, required: ${minThreshold}:1)`;
47
+ }
48
+ } else {
49
+ if (meetsAAA) {
50
+ recommendation = 'pass';
51
+ message = `Excellent contrast ratio: ${ratio.toFixed(2)}:1 (meets WCAG AAA)`;
52
+ } else if (meetsAA) {
53
+ recommendation = 'warning';
54
+ message = `Good contrast ratio: ${ratio.toFixed(
55
+ 2
56
+ )}:1 (meets WCAG AA, consider AAA for better accessibility)`;
57
+ } else {
58
+ recommendation = 'fail';
59
+ message = `Poor contrast ratio: ${ratio.toFixed(2)}:1 (fails WCAG AA, minimum required: ${
60
+ WCAG_STANDARDS.AA_NORMAL
61
+ }:1)`;
62
+ }
63
+ }
64
+
65
+ return {
66
+ ratio,
67
+ meetsAA,
68
+ meetsAAA,
69
+ recommendation,
70
+ message,
71
+ };
72
+ } catch {
73
+ return null;
74
+ }
75
+ }
76
+
77
+ export interface AccessibilityCheckResult {
78
+ primaryContrast: ContrastResult | null;
79
+ lightContrast: ContrastResult | null;
80
+ darkContrast: ContrastResult | null;
81
+ darkOnLightContrast: ContrastResult | null;
82
+ anchorResults: Array<{
83
+ name: string;
84
+ lightContrast: ContrastResult | null;
85
+ darkContrast: ContrastResult | null;
86
+ }>;
87
+ overallScore: 'pass' | 'warning' | 'fail';
88
+ }
89
+
90
+ export function checkDocsColors(
91
+ colors: {
92
+ primary?: string;
93
+ light?: string;
94
+ dark?: string;
95
+ },
96
+ background: {
97
+ lightHex: string;
98
+ darkHex: string;
99
+ },
100
+ navigation?: {
101
+ global?: {
102
+ anchors?: Array<{
103
+ anchor: string;
104
+ color?: {
105
+ light?: string;
106
+ dark?: string;
107
+ };
108
+ }>;
109
+ };
110
+ }
111
+ ): AccessibilityCheckResult {
112
+ const lightBackground = background.lightHex;
113
+ const darkBackground = background.darkHex;
114
+
115
+ const primaryContrast = colors.primary
116
+ ? checkColorContrast(colors.primary, lightBackground)
117
+ : null;
118
+
119
+ const lightContrast = colors.light ? checkColorContrast(colors.light, darkBackground) : null;
120
+
121
+ const darkContrast = colors.dark ? checkColorContrast(colors.dark, darkBackground, 3) : null;
122
+
123
+ const darkOnLightContrast = colors.dark
124
+ ? checkColorContrast(colors.dark, lightBackground, 3)
125
+ : null;
126
+
127
+ const anchorResults: Array<{
128
+ name: string;
129
+ lightContrast: ContrastResult | null;
130
+ darkContrast: ContrastResult | null;
131
+ }> = [];
132
+
133
+ if (navigation?.global?.anchors) {
134
+ for (const anchor of navigation.global.anchors) {
135
+ if (anchor.color) {
136
+ const lightContrast = anchor.color.light
137
+ ? checkColorContrast(anchor.color.light, lightBackground)
138
+ : null;
139
+ const darkContrast = anchor.color.dark
140
+ ? checkColorContrast(anchor.color.dark, darkBackground)
141
+ : null;
142
+
143
+ anchorResults.push({
144
+ name: anchor.anchor,
145
+ lightContrast,
146
+ darkContrast,
147
+ });
148
+ }
149
+ }
150
+ }
151
+
152
+ const results = [
153
+ primaryContrast,
154
+ lightContrast,
155
+ darkContrast,
156
+ darkOnLightContrast,
157
+ ...anchorResults.flatMap((anchor) => [anchor.lightContrast, anchor.darkContrast]),
158
+ ].filter(Boolean);
159
+
160
+ const hasFailure = results.some((result) => result!.recommendation === 'fail');
161
+ const hasWarning = results.some((result) => result!.recommendation === 'warning');
162
+
163
+ let overallScore: 'pass' | 'warning' | 'fail';
164
+ if (hasFailure) {
165
+ overallScore = 'fail';
166
+ } else if (hasWarning) {
167
+ overallScore = 'warning';
168
+ } else {
169
+ overallScore = 'pass';
170
+ }
171
+
172
+ return {
173
+ primaryContrast,
174
+ lightContrast,
175
+ darkContrast,
176
+ darkOnLightContrast,
177
+ anchorResults,
178
+ overallScore,
179
+ };
180
+ }
@@ -0,0 +1,145 @@
1
+ import { getConfigObj, getConfigPath } from '@poetora/prebuild';
2
+ import { addLog, ErrorLog, WarningLog } from '@poetora/previewing';
3
+ import { getBackgroundColors } from '@poetora/shared';
4
+ import { Text } from 'ink';
5
+
6
+ import { checkDocsColors, type AccessibilityCheckResult } from './accessibility.js';
7
+ import { ContrastResult } from './accessibility.js';
8
+ import { CMD_EXEC_PATH } from './constants.js';
9
+
10
+ export type TerminateCode = 0 | 1;
11
+
12
+ export const accessibilityCheck = async (): Promise<TerminateCode> => {
13
+ try {
14
+ const docsConfigPath = await getConfigPath(CMD_EXEC_PATH);
15
+
16
+ if (!docsConfigPath) {
17
+ addLog(
18
+ <ErrorLog message="No configuration file found. Please run this command from a directory with a docs.json file." />
19
+ );
20
+ return 1;
21
+ }
22
+
23
+ const config = await getConfigObj(CMD_EXEC_PATH);
24
+
25
+ if (!config.colors) {
26
+ addLog(<WarningLog message="No colors section found in configuration file" />);
27
+ return 0;
28
+ }
29
+
30
+ const { colors, navigation } = config;
31
+
32
+ const { lightHex, darkHex } = getBackgroundColors(config);
33
+
34
+ const results: AccessibilityCheckResult = checkDocsColors(
35
+ colors,
36
+ { lightHex, darkHex },
37
+ navigation
38
+ );
39
+
40
+ const displayContrastResult = (
41
+ result: ContrastResult | null,
42
+ label: string,
43
+ prefix: string = ''
44
+ ) => {
45
+ if (!result) return;
46
+
47
+ const { recommendation, message } = result;
48
+ const icon =
49
+ recommendation === 'pass' ? 'PASS' : recommendation === 'warning' ? 'WARN' : 'FAIL';
50
+ const color =
51
+ recommendation === 'pass' ? 'green' : recommendation === 'warning' ? 'yellow' : 'red';
52
+
53
+ addLog(
54
+ <Text>
55
+ <Text bold={prefix === ''}>
56
+ {prefix}
57
+ {label}:{' '}
58
+ </Text>
59
+ <Text color={color}>
60
+ {icon} {message}
61
+ </Text>
62
+ </Text>
63
+ );
64
+ };
65
+
66
+ addLog(
67
+ <Text bold color="cyan">
68
+ Checking color accessibility...
69
+ </Text>
70
+ );
71
+ addLog(<Text></Text>);
72
+
73
+ displayContrastResult(
74
+ results.primaryContrast,
75
+ `Primary Color (${colors.primary}) vs Light Background`
76
+ );
77
+ displayContrastResult(
78
+ results.lightContrast,
79
+ `Light Color (${colors.light}) vs Dark Background`
80
+ );
81
+ displayContrastResult(results.darkContrast, `Dark Color (${colors.dark}) vs Dark Background`);
82
+ displayContrastResult(
83
+ results.darkOnLightContrast,
84
+ `Dark Color (${colors.dark}) vs Light Background`
85
+ );
86
+
87
+ const anchorsWithResults = results.anchorResults.filter(
88
+ (anchor) => anchor.lightContrast || anchor.darkContrast
89
+ );
90
+
91
+ if (anchorsWithResults.length > 0) {
92
+ addLog(<Text></Text>);
93
+ addLog(
94
+ <Text bold color="cyan">
95
+ Navigation Anchors:
96
+ </Text>
97
+ );
98
+
99
+ for (const anchor of anchorsWithResults) {
100
+ addLog(<Text bold> {anchor.name}:</Text>);
101
+ displayContrastResult(anchor.lightContrast, 'Light variant vs Light Background', ' ');
102
+ displayContrastResult(anchor.darkContrast, 'Dark variant vs Dark Background', ' ');
103
+ }
104
+ }
105
+
106
+ addLog(<Text></Text>);
107
+ const overallIcon =
108
+ results.overallScore === 'pass'
109
+ ? 'PASS'
110
+ : results.overallScore === 'warning'
111
+ ? 'WARN'
112
+ : 'FAIL';
113
+ const overallColor =
114
+ results.overallScore === 'pass'
115
+ ? 'green'
116
+ : results.overallScore === 'warning'
117
+ ? 'yellow'
118
+ : 'red';
119
+ const overallMessage =
120
+ results.overallScore === 'pass'
121
+ ? 'All colors meet accessibility standards!'
122
+ : results.overallScore === 'warning'
123
+ ? 'Some colors could be improved for better accessibility'
124
+ : 'Some colors fail accessibility standards and should be updated';
125
+
126
+ addLog(
127
+ <Text>
128
+ <Text bold color={overallColor}>
129
+ Overall Assessment: {overallIcon} {overallMessage}
130
+ </Text>
131
+ </Text>
132
+ );
133
+
134
+ return results.overallScore === 'fail' ? 1 : 0;
135
+ } catch (error) {
136
+ addLog(
137
+ <ErrorLog
138
+ message={`Accessibility check failed: ${
139
+ error instanceof Error ? error.message : 'Unknown error'
140
+ }`}
141
+ />
142
+ );
143
+ return 1;
144
+ }
145
+ };