@shohojdhara/atomix 0.4.7 → 0.4.9
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/atomix.config.ts +58 -1
- package/dist/atomix.css +172 -157
- package/dist/atomix.css.map +1 -1
- package/dist/atomix.min.css +4 -4
- package/dist/atomix.min.css.map +1 -1
- package/dist/charts.d.ts +33 -0
- package/dist/charts.js +1274 -164
- package/dist/charts.js.map +1 -1
- package/dist/core.d.ts +33 -10
- package/dist/core.js +1099 -83
- package/dist/core.js.map +1 -1
- package/dist/forms.d.ts +33 -0
- package/dist/forms.js +2106 -1050
- package/dist/forms.js.map +1 -1
- package/dist/heavy.d.ts +42 -1
- package/dist/heavy.js +1663 -638
- package/dist/heavy.js.map +1 -1
- package/dist/index.d.ts +442 -270
- package/dist/index.esm.js +1947 -680
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1982 -712
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/package.json +6 -3
- package/scripts/atomix-cli.js +136 -1827
- package/scripts/cli/__tests__/basic.test.js +3 -2
- package/scripts/cli/__tests__/clean.test.js +278 -0
- package/scripts/cli/__tests__/component-validator.test.js +433 -0
- package/scripts/cli/__tests__/generator.test.js +613 -0
- package/scripts/cli/__tests__/glass-motion.test.js +256 -0
- package/scripts/cli/__tests__/integration.test.js +719 -108
- package/scripts/cli/__tests__/migrate.test.js +74 -0
- package/scripts/cli/__tests__/security.test.js +206 -0
- package/scripts/cli/__tests__/test-setup.js +3 -1
- package/scripts/cli/__tests__/theme-bridge.test.js +507 -0
- package/scripts/cli/__tests__/token-provider.test.js +361 -0
- package/scripts/cli/__tests__/utils.test.js +5 -5
- package/scripts/cli/commands/benchmark.js +105 -0
- package/scripts/cli/commands/build-theme.js +115 -0
- package/scripts/cli/commands/clean.js +109 -0
- package/scripts/cli/commands/doctor.js +88 -0
- package/scripts/cli/commands/generate.js +218 -0
- package/scripts/cli/commands/init.js +73 -0
- package/scripts/cli/commands/migrate.js +106 -0
- package/scripts/cli/commands/sync-tokens.js +206 -0
- package/scripts/cli/commands/theme-bridge.js +248 -0
- package/scripts/cli/commands/tokens.js +157 -0
- package/scripts/cli/commands/validate.js +194 -0
- package/scripts/cli/internal/ai-engine.js +156 -0
- package/scripts/cli/internal/compiler.js +114 -0
- package/scripts/cli/internal/component-validator.js +443 -0
- package/scripts/cli/internal/config-loader.js +162 -0
- package/scripts/cli/internal/filesystem.js +158 -0
- package/scripts/cli/internal/generator.js +430 -0
- package/scripts/cli/internal/glass-generator.js +398 -0
- package/scripts/cli/internal/hook-generator.js +369 -0
- package/scripts/cli/internal/hooks.js +61 -0
- package/scripts/cli/internal/itcss-generator.js +565 -0
- package/scripts/cli/internal/motion-generator.js +679 -0
- package/scripts/cli/internal/template-engine.js +301 -0
- package/scripts/cli/internal/theme-bridge.js +664 -0
- package/scripts/cli/internal/tokens/engine.js +122 -0
- package/scripts/cli/internal/tokens/provider.js +34 -0
- package/scripts/cli/internal/tokens/providers/figma.js +50 -0
- package/scripts/cli/internal/tokens/providers/style-dictionary.js +48 -0
- package/scripts/cli/internal/tokens/providers/w3c.js +48 -0
- package/scripts/cli/internal/tokens/token-provider.js +443 -0
- package/scripts/cli/internal/tokens/token-validator.js +513 -0
- package/scripts/cli/internal/validator.js +276 -0
- package/scripts/cli/internal/wizard.js +115 -0
- package/scripts/cli/mappings.js +23 -0
- package/scripts/cli/migration-tools.js +164 -94
- package/scripts/cli/plugins/style-dictionary.js +46 -0
- package/scripts/cli/templates/README.md +525 -95
- package/scripts/cli/templates/common-templates.js +40 -14
- package/scripts/cli/templates/components/react-component.ts +282 -0
- package/scripts/cli/templates/config/project-config.ts +112 -0
- package/scripts/cli/templates/hooks/use-component.ts +477 -0
- package/scripts/cli/templates/index.js +19 -4
- package/scripts/cli/templates/index.ts +171 -0
- package/scripts/cli/templates/next-templates.js +72 -0
- package/scripts/cli/templates/react-templates.js +70 -126
- package/scripts/cli/templates/scss-templates.js +35 -35
- package/scripts/cli/templates/stories/storybook-story.ts +241 -0
- package/scripts/cli/templates/styles/scss-component.ts +255 -0
- package/scripts/cli/templates/tests/vitest-test.ts +229 -0
- package/scripts/cli/templates/token-templates.js +337 -1
- package/scripts/cli/templates/tokens/token-generators.ts +1088 -0
- package/scripts/cli/templates/types/component-types.ts +145 -0
- package/scripts/cli/templates/utils/testing-utils.ts +144 -0
- package/scripts/cli/templates/vanilla-templates.js +39 -0
- package/scripts/cli/token-manager.js +8 -2
- package/scripts/cli/utils/cache-manager.js +240 -0
- package/scripts/cli/utils/detector.js +46 -0
- package/scripts/cli/utils/diagnostics.js +289 -0
- package/scripts/cli/utils/error.js +89 -0
- package/scripts/cli/utils/helpers.js +67 -0
- package/scripts/cli/utils/logger.js +75 -0
- package/scripts/cli/utils/security.js +302 -0
- package/scripts/cli/utils/telemetry.js +115 -0
- package/scripts/cli/utils/validation.js +37 -0
- package/scripts/cli/utils.js +28 -341
- package/src/components/Accordion/Accordion.stories.tsx +0 -18
- package/src/components/Accordion/Accordion.test.tsx +0 -17
- package/src/components/Accordion/Accordion.tsx +0 -4
- package/src/components/AtomixGlass/AtomixGlass.test.tsx +37 -3
- package/src/components/AtomixGlass/AtomixGlass.tsx +143 -31
- package/src/components/AtomixGlass/AtomixGlassContainer.tsx +129 -31
- package/src/components/AtomixGlass/PerformanceDashboard.tsx +219 -0
- package/src/components/AtomixGlass/README.md +25 -10
- package/src/components/AtomixGlass/__snapshots__/AtomixGlass.test.tsx.snap +216 -0
- package/src/components/AtomixGlass/animation-system.ts +578 -0
- package/src/components/AtomixGlass/shader-utils.ts +4 -1
- package/src/components/AtomixGlass/stories/Overview.stories.tsx +157 -6
- package/src/components/AtomixGlass/stories/Phase1-Animation.stories.tsx +653 -0
- package/src/components/AtomixGlass/stories/Phase1-Test.stories.tsx +95 -0
- package/src/components/AtomixGlass/stories/Playground.stories.tsx +51 -51
- package/src/components/AtomixGlass/stories/shared-components.tsx +6 -0
- package/src/components/Avatar/Avatar.tsx +1 -1
- package/src/components/Button/Button.stories.disabled-link.tsx +10 -0
- package/src/components/Button/Button.stories.tsx +10 -0
- package/src/components/Button/Button.test.tsx +16 -11
- package/src/components/Button/Button.tsx +4 -4
- package/src/components/Card/Card.tsx +1 -1
- package/src/components/Dropdown/Dropdown.tsx +12 -12
- package/src/components/Form/Select.tsx +62 -3
- package/src/components/Modal/Modal.tsx +14 -3
- package/src/components/Navigation/Navbar/Navbar.tsx +44 -0
- package/src/components/Slider/Slider.stories.tsx +3 -3
- package/src/components/Slider/Slider.tsx +38 -0
- package/src/components/Steps/Steps.tsx +3 -3
- package/src/components/Tabs/Tabs.tsx +77 -8
- package/src/components/Testimonial/Testimonial.tsx +1 -1
- package/src/components/TypedButton/TypedButton.stories.tsx +59 -0
- package/src/components/TypedButton/TypedButton.tsx +39 -0
- package/src/components/TypedButton/index.ts +2 -0
- package/src/components/VideoPlayer/VideoPlayer.tsx +11 -4
- package/src/lib/composables/index.ts +4 -7
- package/src/lib/composables/types.ts +45 -0
- package/src/lib/composables/useAccordion.ts +0 -7
- package/src/lib/composables/useAtomixGlass.ts +148 -6
- package/src/lib/composables/useAtomixGlassStyles.ts +9 -7
- package/src/lib/composables/useChartExport.ts +3 -13
- package/src/lib/composables/useDropdown.ts +66 -0
- package/src/lib/composables/useFocusTrap.ts +80 -0
- package/src/lib/composables/usePerformanceMonitor.ts +448 -0
- package/src/lib/composables/useResponsiveGlass.presets.ts +192 -0
- package/src/lib/composables/useResponsiveGlass.ts +441 -0
- package/src/lib/composables/useTooltip.ts +16 -0
- package/src/lib/composables/useTypedButton.ts +66 -0
- package/src/lib/config/index.ts +62 -5
- package/src/lib/constants/components.ts +62 -7
- package/src/lib/theme/devtools/__tests__/useHistory.test.tsx +150 -0
- package/src/lib/theme/tokens/centralized-tokens.ts +120 -0
- package/src/lib/theme/utils/__tests__/domUtils.test.ts +101 -0
- package/src/lib/types/components.ts +37 -11
- package/src/lib/types/glass.ts +35 -0
- package/src/lib/types/index.ts +1 -0
- package/src/lib/utils/displacement-generator.ts +1 -1
- package/src/styles/01-settings/_settings.testtypecheck.scss +53 -0
- package/src/styles/01-settings/_settings.typedbutton.scss +53 -0
- package/src/styles/06-components/_components.atomix-glass.scss +17 -21
- package/src/styles/06-components/_components.edge-panel.scss +1 -5
- package/src/styles/06-components/_components.modal.scss +1 -4
- package/src/styles/06-components/_components.navbar.scss +1 -1
- package/src/styles/06-components/_components.testbutton.scss +212 -0
- package/src/styles/06-components/_components.testtypecheck.scss +212 -0
- package/src/styles/06-components/_components.tooltip.scss +9 -5
- package/src/styles/06-components/_components.typedbutton.scss +212 -0
- package/src/styles/99-utilities/_index.scss +1 -0
- package/src/styles/99-utilities/_utilities.text.scss +1 -1
- package/src/styles/99-utilities/_utilities.touch-target.scss +36 -0
- package/scripts/cli/component-generator.js +0 -564
- package/scripts/cli/interactive-init.js +0 -357
- package/src/styles/06-components/old.chart.styles.scss +0 -2788
package/scripts/atomix-cli.js
CHANGED
|
@@ -1,71 +1,31 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Atomix CLI
|
|
5
|
-
* Design System Development Tools
|
|
4
|
+
* Atomix CLI Orchestrator
|
|
5
|
+
* Design System Development Tools - Modular Edition
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { program } from 'commander';
|
|
9
|
-
import { readFile
|
|
10
|
-
import { join, dirname
|
|
9
|
+
import { readFile } from 'fs/promises';
|
|
10
|
+
import { join, dirname } from 'path';
|
|
11
11
|
import { fileURLToPath } from 'url';
|
|
12
|
-
import {
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
import autoprefixer from 'autoprefixer';
|
|
16
|
-
import cssnano from 'cssnano';
|
|
12
|
+
import { logger } from './cli/utils/logger.js';
|
|
13
|
+
import { handleCLIError } from './cli/utils/error.js';
|
|
14
|
+
import { telemetry } from './cli/utils/telemetry.js';
|
|
17
15
|
import chalk from 'chalk';
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
import
|
|
21
|
-
import
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
} from './cli/
|
|
28
|
-
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
migrateSCSSVariables,
|
|
32
|
-
displayMigrationReport
|
|
33
|
-
} from './cli/migration-tools.js';
|
|
34
|
-
import {
|
|
35
|
-
listTokens,
|
|
36
|
-
validateTokens,
|
|
37
|
-
exportTokens,
|
|
38
|
-
importTokens,
|
|
39
|
-
fixTokens
|
|
40
|
-
} from './cli/token-manager.js';
|
|
41
|
-
import { createThemeCLIBridge } from './cli/theme-bridge.js';
|
|
42
|
-
import {
|
|
43
|
-
validatePath,
|
|
44
|
-
validateComponentName,
|
|
45
|
-
validateThemeName,
|
|
46
|
-
sanitizeInput,
|
|
47
|
-
checkNodeVersion,
|
|
48
|
-
AtomixCLIError
|
|
49
|
-
} from './cli/utils.js';
|
|
50
|
-
import {
|
|
51
|
-
componentTemplates,
|
|
52
|
-
generateColorTokens,
|
|
53
|
-
generateSpacingTokens,
|
|
54
|
-
generateTypographyTokens,
|
|
55
|
-
generateShadowTokens,
|
|
56
|
-
generateRadiusTokens,
|
|
57
|
-
generateAnimationTokens
|
|
58
|
-
} from './cli/templates.js';
|
|
59
|
-
import {
|
|
60
|
-
interactiveComponentGeneration,
|
|
61
|
-
validateGeneratedComponent,
|
|
62
|
-
displayValidationReport
|
|
63
|
-
} from './cli/component-generator.js';
|
|
64
|
-
import {
|
|
65
|
-
syncDocumentation,
|
|
66
|
-
validateDocumentation,
|
|
67
|
-
generateCLIDocumentation
|
|
68
|
-
} from './cli/documentation-sync.js';
|
|
16
|
+
|
|
17
|
+
// Action Modules
|
|
18
|
+
import { initAction } from './cli/commands/init.js';
|
|
19
|
+
import { generateAction } from './cli/commands/generate.js';
|
|
20
|
+
import { buildThemeAction } from './cli/commands/build-theme.js';
|
|
21
|
+
import { doctorAction } from './cli/commands/doctor.js';
|
|
22
|
+
import { validateAction } from './cli/commands/validate.js';
|
|
23
|
+
import { tokensAction } from './cli/commands/tokens.js';
|
|
24
|
+
import { migrateAction } from './cli/commands/migrate.js';
|
|
25
|
+
import { benchmarkAction } from './cli/commands/benchmark.js';
|
|
26
|
+
import { cleanAction } from './cli/commands/clean.js';
|
|
27
|
+
import { themeBridgeAction } from './cli/commands/theme-bridge.js';
|
|
28
|
+
import { configLoader } from './cli/internal/config-loader.js';
|
|
69
29
|
|
|
70
30
|
const __filename = fileURLToPath(import.meta.url);
|
|
71
31
|
const __dirname = dirname(__filename);
|
|
@@ -75,1856 +35,205 @@ const packageJson = JSON.parse(
|
|
|
75
35
|
await readFile(join(__dirname, '../package.json'), 'utf8')
|
|
76
36
|
);
|
|
77
37
|
|
|
78
|
-
// CLI Configuration
|
|
79
|
-
const DEBUG = process.env.ATOMIX_DEBUG === 'true' || process.argv.includes('--debug');
|
|
80
|
-
|
|
81
|
-
const SENSITIVE_KEYS = /password|secret|token|api[-_]?key|access[-_]?key|auth[-_]?token|authorization|credential/i;
|
|
82
|
-
|
|
83
|
-
function sensitiveDataReplacer(key, value) {
|
|
84
|
-
if (key && SENSITIVE_KEYS.test(key)) {
|
|
85
|
-
return '***REDACTED***';
|
|
86
|
-
}
|
|
87
|
-
return value;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Replacer function for JSON.stringify to sanitize sensitive data
|
|
92
|
-
*/
|
|
93
|
-
function sanitizeReplacer(key, value) {
|
|
94
|
-
// If no key (root object), return value
|
|
95
|
-
if (!key) return value;
|
|
96
|
-
|
|
97
|
-
const lowerKey = key.toLowerCase();
|
|
98
|
-
|
|
99
|
-
// Heuristic for sensitive keys
|
|
100
|
-
const isSensitive =
|
|
101
|
-
lowerKey.includes('password') ||
|
|
102
|
-
lowerKey.includes('secret') ||
|
|
103
|
-
lowerKey.includes('credential') ||
|
|
104
|
-
lowerKey.includes('bearer') ||
|
|
105
|
-
// Ends with 'token' (e.g. accessToken, authToken) or is 'token', avoid 'tokenizer'
|
|
106
|
-
/token$/i.test(key) ||
|
|
107
|
-
lowerKey === 'token' ||
|
|
108
|
-
// Ends with 'key' (e.g. apiKey, accessKey) or is 'key', avoid 'keyboard', 'keyword', exclude 'publickey'
|
|
109
|
-
((/key$/i.test(key) || lowerKey === 'key') && !lowerKey.includes('public')) ||
|
|
110
|
-
// Auth
|
|
111
|
-
lowerKey === 'auth' ||
|
|
112
|
-
lowerKey.startsWith('auth_') ||
|
|
113
|
-
/^auth[A-Z]/.test(key) ||
|
|
114
|
-
lowerKey.includes('authorization');
|
|
115
|
-
|
|
116
|
-
if (isSensitive) {
|
|
117
|
-
return '[REDACTED]';
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return value;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Debug logger
|
|
125
|
-
*/
|
|
126
|
-
function debug(message, data = null) {
|
|
127
|
-
if (DEBUG) {
|
|
128
|
-
console.log(chalk.gray(`[DEBUG] ${message}`));
|
|
129
|
-
if (data) {
|
|
130
|
-
console.log(chalk.gray(JSON.stringify(data, sanitizeReplacer, 2)));
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Error handler with suggestions
|
|
137
|
-
*/
|
|
138
|
-
function handleError(error, spinner = null) {
|
|
139
|
-
if (spinner) spinner.fail(chalk.red('Operation failed'));
|
|
140
|
-
|
|
141
|
-
console.error(chalk.bold.red(`\n❌ ${error.message}`));
|
|
142
|
-
|
|
143
|
-
if (error instanceof AtomixCLIError && error.suggestions.length > 0) {
|
|
144
|
-
console.log(chalk.yellow('\n💡 Suggestions:'));
|
|
145
|
-
error.suggestions.forEach((suggestion, index) => {
|
|
146
|
-
console.log(chalk.gray(` ${index + 1}. ${suggestion}`));
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (DEBUG && error.stack) {
|
|
151
|
-
console.error(chalk.gray('\nStack trace:'));
|
|
152
|
-
console.error(chalk.gray(error.stack));
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
process.exit(1);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
// Initialize program
|
|
161
38
|
program
|
|
162
39
|
.name('atomix')
|
|
163
|
-
.description('Atomix Design System CLI -
|
|
40
|
+
.description('Atomix Design System CLI - Modular Edition')
|
|
164
41
|
.version(packageJson.version)
|
|
165
42
|
.option('-d, --debug', 'Enable debug mode', false)
|
|
166
|
-
.
|
|
43
|
+
.option('--dry-run', 'Preview changes without modifying files', false)
|
|
44
|
+
.hook('preAction', async (thisCommand) => {
|
|
45
|
+
// Load config
|
|
46
|
+
await configLoader.load();
|
|
47
|
+
|
|
167
48
|
if (thisCommand.opts().debug) {
|
|
168
49
|
process.env.ATOMIX_DEBUG = 'true';
|
|
50
|
+
logger.debug('Debug mode enabled');
|
|
169
51
|
}
|
|
52
|
+
if (thisCommand.opts().dryRun) {
|
|
53
|
+
process.env.ATOMIX_DRY_RUN = 'true';
|
|
54
|
+
logger.info(chalk.yellow('⚠️ Dry-run mode enabled. No files will be modified.'));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Start telemetry
|
|
58
|
+
const fullCommand = thisCommand.name() === 'atomix'
|
|
59
|
+
? thisCommand.args[0] || 'atomix'
|
|
60
|
+
: thisCommand.name();
|
|
61
|
+
telemetry.start(fullCommand);
|
|
62
|
+
})
|
|
63
|
+
.hook('postAction', async (thisCommand) => {
|
|
64
|
+
// Stop telemetry
|
|
65
|
+
await telemetry.stop(true);
|
|
170
66
|
});
|
|
171
67
|
|
|
172
68
|
/**
|
|
173
|
-
*
|
|
69
|
+
* Environment Diagnostics
|
|
174
70
|
*/
|
|
175
71
|
program
|
|
176
|
-
.command('
|
|
177
|
-
.description('
|
|
178
|
-
.option('
|
|
179
|
-
.
|
|
180
|
-
.option('-s, --sourcemap', 'Generate source maps', false)
|
|
181
|
-
.option('-w, --watch', 'Watch for changes and rebuild', false)
|
|
182
|
-
.option('--analyze', 'Analyze bundle size', false)
|
|
183
|
-
.action(async (themePath, options) => {
|
|
184
|
-
let spinner = ora('Initializing theme build...').start();
|
|
185
|
-
|
|
72
|
+
.command('doctor')
|
|
73
|
+
.description('Verify the environment and project health. Use --explain to list what each check does.')
|
|
74
|
+
.option('--explain', 'Print short descriptions for each doctor check and exit')
|
|
75
|
+
.action(async (options) => {
|
|
186
76
|
try {
|
|
187
|
-
|
|
188
|
-
const themePathValidation = validatePath(sanitizedThemePath);
|
|
189
|
-
if (!themePathValidation.isValid) {
|
|
190
|
-
throw new AtomixCLIError(
|
|
191
|
-
themePathValidation.error,
|
|
192
|
-
'INVALID_PATH',
|
|
193
|
-
[
|
|
194
|
-
'Ensure theme path is within the project directory',
|
|
195
|
-
'Avoid sensitive or absolute system paths',
|
|
196
|
-
'Example: atomix build-theme themes/my-theme'
|
|
197
|
-
]
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
const sanitizedOutput = sanitizeInput(options.output);
|
|
201
|
-
const outputValidation = validatePath(sanitizedOutput);
|
|
202
|
-
if (!outputValidation.isValid) {
|
|
203
|
-
throw new AtomixCLIError(
|
|
204
|
-
outputValidation.error,
|
|
205
|
-
'INVALID_PATH',
|
|
206
|
-
[
|
|
207
|
-
'Use a project-relative directory for output',
|
|
208
|
-
'Example: --output ./dist'
|
|
209
|
-
]
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
// Resolve paths
|
|
213
|
-
const indexPath = sanitizedThemePath.endsWith('.scss')
|
|
214
|
-
? resolve(themePathValidation.safePath)
|
|
215
|
-
: resolve(themePathValidation.safePath, 'index.scss');
|
|
216
|
-
|
|
217
|
-
debug(`Building theme from: ${indexPath}`);
|
|
218
|
-
|
|
219
|
-
// Check if path exists
|
|
220
|
-
try {
|
|
221
|
-
await access(indexPath);
|
|
222
|
-
} catch (error) {
|
|
223
|
-
throw new AtomixCLIError(
|
|
224
|
-
`Theme file not found: ${indexPath}`,
|
|
225
|
-
'THEME_NOT_FOUND',
|
|
226
|
-
[
|
|
227
|
-
'Check if the file path is correct',
|
|
228
|
-
'Ensure the file has a .scss extension',
|
|
229
|
-
'Create a new theme with: atomix create-theme <name>'
|
|
230
|
-
]
|
|
231
|
-
);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Build function
|
|
235
|
-
const buildTheme = async () => {
|
|
236
|
-
const startTime = Date.now();
|
|
237
|
-
|
|
238
|
-
try {
|
|
239
|
-
// Compile SCSS
|
|
240
|
-
spinner.text = 'Compiling SCSS...';
|
|
241
|
-
debug('Starting SCSS compilation');
|
|
242
|
-
|
|
243
|
-
const result = sass.compile(indexPath, {
|
|
244
|
-
loadPaths: [
|
|
245
|
-
process.cwd(),
|
|
246
|
-
join(process.cwd(), 'node_modules'),
|
|
247
|
-
join(__dirname, '../src'),
|
|
248
|
-
join(__dirname, '../src/styles'),
|
|
249
|
-
dirname(indexPath)
|
|
250
|
-
],
|
|
251
|
-
sourceMap: options.sourcemap,
|
|
252
|
-
style: 'expanded',
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
// Process with PostCSS
|
|
256
|
-
spinner.text = 'Processing with PostCSS...';
|
|
257
|
-
const processed = await postcss([
|
|
258
|
-
autoprefixer({
|
|
259
|
-
overrideBrowserslist: ['> 1%', 'last 2 versions', 'not dead'],
|
|
260
|
-
}),
|
|
261
|
-
]).process(result.css, {
|
|
262
|
-
from: indexPath,
|
|
263
|
-
map: options.sourcemap,
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
// Ensure output directory exists
|
|
267
|
-
await mkdir(outputValidation.safePath, { recursive: true });
|
|
268
|
-
|
|
269
|
-
// Get theme name
|
|
270
|
-
const themeName = basename(dirname(indexPath));
|
|
271
|
-
|
|
272
|
-
// Write expanded CSS
|
|
273
|
-
const outputPath = join(outputValidation.safePath, `${themeName}.css`);
|
|
274
|
-
await writeFile(outputPath, processed.css, 'utf8');
|
|
275
|
-
|
|
276
|
-
// Get file size
|
|
277
|
-
const stats = await stat(outputPath);
|
|
278
|
-
const sizeKB = (stats.size / 1024).toFixed(2);
|
|
279
|
-
|
|
280
|
-
spinner.succeed(chalk.green(`✓ Built ${outputPath} (${sizeKB} KB)`));
|
|
281
|
-
|
|
282
|
-
// Write minified if requested
|
|
283
|
-
if (options.minify) {
|
|
284
|
-
const minifySpinner = ora('Minifying CSS...').start();
|
|
285
|
-
const minified = await postcss([
|
|
286
|
-
autoprefixer(),
|
|
287
|
-
cssnano({ preset: 'default' }),
|
|
288
|
-
]).process(result.css, {
|
|
289
|
-
from: indexPath,
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
const minPath = join(outputValidation.safePath, `${themeName}.min.css`);
|
|
293
|
-
await writeFile(minPath, minified.css, 'utf8');
|
|
294
|
-
|
|
295
|
-
const minStats = await stat(minPath);
|
|
296
|
-
const minSizeKB = (minStats.size / 1024).toFixed(2);
|
|
297
|
-
|
|
298
|
-
minifySpinner.succeed(chalk.green(`✓ Built ${minPath} (${minSizeKB} KB)`));
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Analyze if requested
|
|
302
|
-
if (options.analyze) {
|
|
303
|
-
console.log(chalk.cyan('\n📊 Theme Analysis:'));
|
|
304
|
-
console.log(chalk.gray(` Original size: ${sizeKB} KB`));
|
|
305
|
-
if (options.minify) {
|
|
306
|
-
const minPath = join(outputValidation.safePath, `${themeName}.min.css`);
|
|
307
|
-
const minStats = await stat(minPath);
|
|
308
|
-
const minSizeKB = (minStats.size / 1024).toFixed(2);
|
|
309
|
-
const reduction = ((1 - minStats.size / stats.size) * 100).toFixed(1);
|
|
310
|
-
console.log(chalk.gray(` Minified size: ${minSizeKB} KB (${reduction}% reduction)`));
|
|
311
|
-
}
|
|
312
|
-
console.log(chalk.gray(` Build time: ${Date.now() - startTime}ms`));
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (!options.watch) {
|
|
316
|
-
console.log(chalk.bold.green('\n✨ Theme build complete!'));
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
} catch (error) {
|
|
320
|
-
if (options.watch) {
|
|
321
|
-
console.error(chalk.red(`Build error: ${error.message}`));
|
|
322
|
-
console.log(chalk.yellow('Waiting for changes...'));
|
|
323
|
-
} else {
|
|
324
|
-
throw error;
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
};
|
|
328
|
-
|
|
329
|
-
// Initial build
|
|
330
|
-
await buildTheme();
|
|
331
|
-
spinner.stop();
|
|
332
|
-
|
|
333
|
-
// Watch mode
|
|
334
|
-
if (options.watch) {
|
|
335
|
-
console.log(chalk.cyan('\n👁️ Watch mode enabled. Press Ctrl+C to exit.\n'));
|
|
336
|
-
|
|
337
|
-
const watcher = chokidar.watch([themePathValidation.safePath], {
|
|
338
|
-
ignored: /node_modules/,
|
|
339
|
-
persistent: true,
|
|
340
|
-
ignoreInitial: true
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
watcher.on('change', async (path) => {
|
|
344
|
-
console.log(chalk.gray(`\n[${new Date().toLocaleTimeString()}] File changed: ${relative(process.cwd(), path)}`));
|
|
345
|
-
spinner = ora('Rebuilding theme...').start();
|
|
346
|
-
await buildTheme();
|
|
347
|
-
spinner.stop();
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
watcher.on('add', async (path) => {
|
|
351
|
-
console.log(chalk.gray(`\n[${new Date().toLocaleTimeString()}] File added: ${relative(process.cwd(), path)}`));
|
|
352
|
-
spinner = ora('Rebuilding theme...').start();
|
|
353
|
-
await buildTheme();
|
|
354
|
-
spinner.stop();
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
// Handle graceful shutdown
|
|
358
|
-
process.on('SIGINT', () => {
|
|
359
|
-
console.log(chalk.yellow('\n\nShutting down watch mode...'));
|
|
360
|
-
watcher.close();
|
|
361
|
-
process.exit(0);
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
|
|
77
|
+
await doctorAction(options);
|
|
365
78
|
} catch (error) {
|
|
366
|
-
|
|
79
|
+
await handleCLIError(error);
|
|
367
80
|
}
|
|
368
81
|
});
|
|
369
82
|
|
|
370
83
|
/**
|
|
371
|
-
*
|
|
84
|
+
* Code & Config Validation
|
|
372
85
|
*/
|
|
373
86
|
program
|
|
374
|
-
.command('
|
|
375
|
-
.
|
|
376
|
-
.
|
|
377
|
-
.option('-t, --typescript', 'Use TypeScript (default)', true)
|
|
378
|
-
.option('-s, --story', 'Include Storybook story', true)
|
|
379
|
-
.option('--test', 'Include test file', false)
|
|
380
|
-
.option('--scss-module', 'Use SCSS modules', false)
|
|
381
|
-
.option('--path <path>', 'Custom output path', './src/components')
|
|
382
|
-
.option('-f, --force', 'Overwrite existing files', false)
|
|
383
|
-
.option('-i, --interactive', 'Interactive component generation', false)
|
|
384
|
-
.option('--complexity <level>', 'Component complexity level (simple|medium|complex)', 'medium')
|
|
385
|
-
.option('--validate', 'Validate component after generation', true)
|
|
386
|
-
.action(async (type, name, options) => {
|
|
387
|
-
const spinner = ora(`Generating ${type}: ${name}...`).start();
|
|
388
|
-
|
|
87
|
+
.command('validate [subcommand] [name]')
|
|
88
|
+
.description('Audit project quality (A11y, Tokens, Performance). Use "validate component <Name>" for component-scoped audit. Run build first for performance analysis (requires dist/).')
|
|
89
|
+
.action(async (subcommand, name, options) => {
|
|
389
90
|
try {
|
|
390
|
-
|
|
391
|
-
let safeName = sanitizeInput(name);
|
|
392
|
-
|
|
393
|
-
if (type === 'component' || type === 'c') {
|
|
394
|
-
// Handle interactive generation
|
|
395
|
-
if (options.interactive) {
|
|
396
|
-
const interactiveResult = await interactiveComponentGeneration();
|
|
397
|
-
if (!interactiveResult) {
|
|
398
|
-
spinner.stop();
|
|
399
|
-
return; // User cancelled
|
|
400
|
-
}
|
|
401
|
-
// Update options with interactive results (name, complexity, features, outputPath)
|
|
402
|
-
Object.assign(options, interactiveResult);
|
|
403
|
-
safeName = sanitizeInput(options.name);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
const nameValidation = validateComponentName(safeName);
|
|
407
|
-
if (!nameValidation.isValid) {
|
|
408
|
-
throw new AtomixCLIError(
|
|
409
|
-
nameValidation.error,
|
|
410
|
-
'INVALID_NAME',
|
|
411
|
-
[
|
|
412
|
-
'Use PascalCase naming (e.g., MyComponent)',
|
|
413
|
-
'Start with an uppercase letter',
|
|
414
|
-
'Use only letters and numbers',
|
|
415
|
-
'Avoid reserved words'
|
|
416
|
-
]
|
|
417
|
-
);
|
|
418
|
-
}
|
|
419
|
-
// Validate output path
|
|
420
|
-
const sanitizedPath = sanitizeInput(options.path);
|
|
421
|
-
const pathValidation = validatePath(sanitizedPath);
|
|
422
|
-
if (!pathValidation.isValid) {
|
|
423
|
-
throw new AtomixCLIError(
|
|
424
|
-
pathValidation.error,
|
|
425
|
-
'INVALID_PATH',
|
|
426
|
-
[
|
|
427
|
-
'Ensure the path is within the project directory',
|
|
428
|
-
'Avoid using ".." to navigate outside the project',
|
|
429
|
-
'Check for typos in the path'
|
|
430
|
-
]
|
|
431
|
-
);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
const componentPath = join(pathValidation.safePath, safeName);
|
|
435
|
-
|
|
436
|
-
// Check if component already exists
|
|
437
|
-
if (existsSync(componentPath) && !options.force) {
|
|
438
|
-
throw new AtomixCLIError(
|
|
439
|
-
`Component ${safeName} already exists`,
|
|
440
|
-
'COMPONENT_EXISTS',
|
|
441
|
-
[
|
|
442
|
-
`Delete the existing component at ${componentPath}`,
|
|
443
|
-
'Use --force flag to overwrite',
|
|
444
|
-
'Choose a different component name'
|
|
445
|
-
]
|
|
446
|
-
);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// Create component directory
|
|
450
|
-
await mkdir(componentPath, { recursive: true });
|
|
451
|
-
|
|
452
|
-
// Generate composable hook
|
|
453
|
-
const hookPath = join(process.cwd(), 'src/lib/composables');
|
|
454
|
-
if (existsSync(hookPath)) {
|
|
455
|
-
const hookContent = componentTemplates.composable.useHook(safeName);
|
|
456
|
-
const hookFilePath = join(hookPath, `use${safeName}.ts`);
|
|
457
|
-
|
|
458
|
-
if (!existsSync(hookFilePath) || options.force) {
|
|
459
|
-
await writeFile(hookFilePath, hookContent, 'utf8');
|
|
460
|
-
console.log(chalk.green(` ✓ Created use${safeName}.ts in src/lib/composables`));
|
|
461
|
-
} else {
|
|
462
|
-
console.log(chalk.yellow(` ⚠️ Hook file already exists: use${safeName}.ts`));
|
|
463
|
-
}
|
|
464
|
-
} else {
|
|
465
|
-
console.log(chalk.yellow(` ⚠️ Composables directory not found: ${hookPath}`));
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// Generate types in lib/types/components.ts
|
|
469
|
-
const typesPath = join(process.cwd(), 'src/lib/types');
|
|
470
|
-
if (existsSync(typesPath)) {
|
|
471
|
-
const componentsTypesPath = join(typesPath, 'components.ts');
|
|
472
|
-
if (existsSync(componentsTypesPath)) {
|
|
473
|
-
let typesContent = await readFile(componentsTypesPath, 'utf8');
|
|
474
|
-
const newTypeContent = componentTemplates.react.types(safeName);
|
|
475
|
-
|
|
476
|
-
// Check if type already exists
|
|
477
|
-
if (!typesContent.includes(`interface ${safeName}Props`)) {
|
|
478
|
-
// Insert before the last export
|
|
479
|
-
const lastExportIndex = typesContent.lastIndexOf('export');
|
|
480
|
-
const insertionPoint = typesContent.lastIndexOf('\n\n', lastExportIndex) + 2;
|
|
481
|
-
typesContent =
|
|
482
|
-
typesContent.slice(0, insertionPoint) +
|
|
483
|
-
'\n' + newTypeContent + '\n' +
|
|
484
|
-
typesContent.slice(insertionPoint);
|
|
485
|
-
|
|
486
|
-
await writeFile(componentsTypesPath, typesContent, 'utf8');
|
|
487
|
-
console.log(chalk.green(` ✓ Added ${safeName}Props to src/lib/types/components.ts`));
|
|
488
|
-
} else {
|
|
489
|
-
console.log(chalk.yellow(` ⚠️ ${safeName}Props already exists in types`));
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
// Generate constants in lib/constants/components.ts
|
|
495
|
-
const constantsPath = join(process.cwd(), 'src/lib/constants');
|
|
496
|
-
if (existsSync(constantsPath)) {
|
|
497
|
-
const constantsFilePath = join(constantsPath, 'components.ts');
|
|
498
|
-
if (existsSync(constantsFilePath)) {
|
|
499
|
-
let constantsContent = await readFile(constantsFilePath, 'utf8');
|
|
500
|
-
const newConstantsContent = componentTemplates.react.constants(safeName);
|
|
501
|
-
|
|
502
|
-
// Check if constants already exist
|
|
503
|
-
if (!constantsContent.includes(`export const ${safeName.toUpperCase()}`)) {
|
|
504
|
-
// Insert before the last export
|
|
505
|
-
const lastExportIndex = constantsContent.lastIndexOf('export');
|
|
506
|
-
const insertionPoint = constantsContent.lastIndexOf('\n\n', lastExportIndex) + 2;
|
|
507
|
-
constantsContent =
|
|
508
|
-
constantsContent.slice(0, insertionPoint) +
|
|
509
|
-
'\n' + newConstantsContent + '\n' +
|
|
510
|
-
constantsContent.slice(insertionPoint);
|
|
511
|
-
|
|
512
|
-
await writeFile(constantsFilePath, constantsContent, 'utf8');
|
|
513
|
-
console.log(chalk.green(` ✓ Added ${safeName.toUpperCase()} constants to src/lib/constants/components.ts`));
|
|
514
|
-
} else {
|
|
515
|
-
console.log(chalk.yellow(` ⚠️ ${safeName.toUpperCase()} constants already exist`));
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// Generate component file
|
|
521
|
-
const componentContent = componentTemplates.react.component(safeName);
|
|
522
|
-
|
|
523
|
-
await writeFile(
|
|
524
|
-
join(componentPath, `${safeName}.tsx`),
|
|
525
|
-
componentContent,
|
|
526
|
-
'utf8'
|
|
527
|
-
);
|
|
528
|
-
spinner.succeed(chalk.green(`✓ Created ${safeName}.tsx`));
|
|
529
|
-
|
|
530
|
-
// Generate index file
|
|
531
|
-
const indexContent = componentTemplates.react.index(safeName);
|
|
532
|
-
await writeFile(
|
|
533
|
-
join(componentPath, 'index.ts'),
|
|
534
|
-
indexContent,
|
|
535
|
-
'utf8'
|
|
536
|
-
);
|
|
537
|
-
console.log(chalk.green(` ✓ Created index.ts`));
|
|
538
|
-
|
|
539
|
-
// Generate SCSS files
|
|
540
|
-
if (!options.scssModule) {
|
|
541
|
-
// Generate settings file first
|
|
542
|
-
const settingsPath = join(process.cwd(), 'src/styles/01-settings');
|
|
543
|
-
if (existsSync(settingsPath)) {
|
|
544
|
-
const settingsContent = componentTemplates.react.settings(safeName);
|
|
545
|
-
const settingsFilename = `_settings.${safeName.toLowerCase()}.scss`;
|
|
546
|
-
const settingsFilePath = join(settingsPath, settingsFilename);
|
|
547
|
-
|
|
548
|
-
if (!existsSync(settingsFilePath) || options.force) {
|
|
549
|
-
await writeFile(settingsFilePath, settingsContent, 'utf8');
|
|
550
|
-
console.log(chalk.green(` ✓ Created ${settingsFilename} in src/styles/01-settings`));
|
|
551
|
-
|
|
552
|
-
// Update _index.scss in settings
|
|
553
|
-
const settingsIndexPath = join(settingsPath, '_index.scss');
|
|
554
|
-
if (existsSync(settingsIndexPath)) {
|
|
555
|
-
let indexContent = await readFile(settingsIndexPath, 'utf8');
|
|
556
|
-
const forwardStatement = `@forward 'settings.${safeName.toLowerCase()}';`;
|
|
557
|
-
|
|
558
|
-
if (!indexContent.includes(forwardStatement)) {
|
|
559
|
-
if (!indexContent.endsWith('\n')) indexContent += '\n';
|
|
560
|
-
indexContent += `${forwardStatement}\n`;
|
|
561
|
-
await writeFile(settingsIndexPath, indexContent, 'utf8');
|
|
562
|
-
console.log(chalk.green(` ✓ Updated settings _index.scss`));
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
} else {
|
|
566
|
-
console.log(chalk.yellow(` ⚠️ Settings file already exists: ${settingsFilename}`));
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// Generate component SCSS file
|
|
571
|
-
const scssContent = componentTemplates.scss.component(safeName);
|
|
572
|
-
const scssPath = join(process.cwd(), 'src/styles/06-components');
|
|
573
|
-
const scssFilename = `_components.${safeName.toLowerCase()}.scss`;
|
|
574
|
-
const scssFilePath = join(scssPath, scssFilename);
|
|
575
|
-
|
|
576
|
-
// Ensure styles directory exists
|
|
577
|
-
if (existsSync(scssPath)) {
|
|
578
|
-
// Check if SCSS file already exists
|
|
579
|
-
if (!existsSync(scssFilePath) || options.force) {
|
|
580
|
-
await writeFile(scssFilePath, scssContent, 'utf8');
|
|
581
|
-
console.log(chalk.green(` ✓ Created ${scssFilename} in src/styles/06-components`));
|
|
582
|
-
|
|
583
|
-
// Update _index.scss
|
|
584
|
-
const indexPath = join(scssPath, '_index.scss');
|
|
585
|
-
if (existsSync(indexPath)) {
|
|
586
|
-
let indexContent = await readFile(indexPath, 'utf8');
|
|
587
|
-
const forwardStatement = `@forward 'components.${safeName.toLowerCase()}';`;
|
|
588
|
-
|
|
589
|
-
if (!indexContent.includes(forwardStatement)) {
|
|
590
|
-
// Append to end of file, ensuring newline
|
|
591
|
-
if (!indexContent.endsWith('\n')) indexContent += '\n';
|
|
592
|
-
indexContent += `${forwardStatement}\n`;
|
|
593
|
-
await writeFile(indexPath, indexContent, 'utf8');
|
|
594
|
-
console.log(chalk.green(` ✓ Updated _index.scss`));
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
} else {
|
|
598
|
-
console.log(chalk.yellow(` ⚠️ SCSS file already exists: ${scssFilename}`));
|
|
599
|
-
}
|
|
600
|
-
} else {
|
|
601
|
-
console.log(chalk.yellow(` ⚠️ Styles directory not found: ${scssPath}`));
|
|
602
|
-
}
|
|
603
|
-
} else {
|
|
604
|
-
// Fallback for modules if strictly requested (though we discourage it)
|
|
605
|
-
const scssContent = componentTemplates.react.scssModule(safeName);
|
|
606
|
-
await writeFile(
|
|
607
|
-
join(componentPath, `${safeName}.module.scss`),
|
|
608
|
-
scssContent,
|
|
609
|
-
'utf8'
|
|
610
|
-
);
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// Generate Storybook story
|
|
614
|
-
if (options.story) {
|
|
615
|
-
const storyContent = componentTemplates.storybook.story(safeName);
|
|
616
|
-
await writeFile(
|
|
617
|
-
join(componentPath, `${safeName}.stories.tsx`),
|
|
618
|
-
storyContent,
|
|
619
|
-
'utf8'
|
|
620
|
-
);
|
|
621
|
-
console.log(chalk.green(` ✓ Created ${safeName}.stories.tsx`));
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
// Generate test file
|
|
625
|
-
if (options.test) {
|
|
626
|
-
const testContent = componentTemplates.react.test(safeName);
|
|
627
|
-
await writeFile(
|
|
628
|
-
join(componentPath, `${safeName}.test.tsx`),
|
|
629
|
-
testContent,
|
|
630
|
-
'utf8'
|
|
631
|
-
);
|
|
632
|
-
console.log(chalk.green(` ✓ Created ${safeName}.test.tsx`));
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
// Post-generation validation if requested
|
|
636
|
-
if (options.validate) {
|
|
637
|
-
const validationResult = await validateGeneratedComponent(safeName, componentPath);
|
|
638
|
-
const isValid = displayValidationReport(validationResult);
|
|
639
|
-
|
|
640
|
-
if (!isValid) {
|
|
641
|
-
console.log(chalk.yellow('\n💡 Some issues were found. Consider addressing them for better component quality.'));
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
// Success message with next steps
|
|
646
|
-
console.log(boxen(
|
|
647
|
-
chalk.bold.green(`🎉 Component ${safeName} created successfully!\n\n`) +
|
|
648
|
-
chalk.cyan('Next steps:\n') +
|
|
649
|
-
chalk.gray(`1. Import in your app:\n`) +
|
|
650
|
-
chalk.white(` import { ${safeName} } from '${options.path}/${safeName}';\n\n`) +
|
|
651
|
-
chalk.gray(`2. Add to design system exports:\n`) +
|
|
652
|
-
chalk.white(` export { ${safeName} } from './${safeName}';\n\n`) +
|
|
653
|
-
chalk.gray(`3. Run Storybook to see your component:\n`) +
|
|
654
|
-
chalk.white(` npm run storybook\n\n`) +
|
|
655
|
-
chalk.gray(`4. Validate component quality:\n`) +
|
|
656
|
-
chalk.white(` atomix validate component ${safeName}`),
|
|
657
|
-
{
|
|
658
|
-
padding: 1,
|
|
659
|
-
margin: 1,
|
|
660
|
-
borderStyle: 'round',
|
|
661
|
-
borderColor: 'green'
|
|
662
|
-
}
|
|
663
|
-
));
|
|
664
|
-
|
|
665
|
-
} else if (type === 'hook' || type === 'h') {
|
|
666
|
-
const hookPathValidation = validatePath(sanitizeInput(options.path));
|
|
667
|
-
if (!hookPathValidation.isValid) {
|
|
668
|
-
throw new AtomixCLIError(
|
|
669
|
-
hookPathValidation.error,
|
|
670
|
-
'INVALID_PATH',
|
|
671
|
-
[
|
|
672
|
-
'Ensure the path is within the project directory',
|
|
673
|
-
'Avoid using ".." to navigate outside the project',
|
|
674
|
-
'Example: --path ./src/components'
|
|
675
|
-
]
|
|
676
|
-
);
|
|
677
|
-
}
|
|
678
|
-
const componentPath = join(hookPathValidation.safePath, `use${safeName}`);
|
|
679
|
-
await mkdir(componentPath, { recursive: true });
|
|
680
|
-
|
|
681
|
-
const hookContent = componentTemplates.hook.hook(safeName);
|
|
682
|
-
await writeFile(join(componentPath, `use${safeName}.ts`), hookContent, 'utf8');
|
|
683
|
-
|
|
684
|
-
if (options.test) {
|
|
685
|
-
const testContent = componentTemplates.hook.test(safeName);
|
|
686
|
-
await writeFile(join(componentPath, `use${safeName}.test.ts`), testContent, 'utf8');
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
spinner.succeed(chalk.green(`✓ Created hook use${safeName}`));
|
|
690
|
-
|
|
691
|
-
} else if (type === 'layout' || type === 'l') {
|
|
692
|
-
const layoutPathValidation = validatePath(sanitizeInput(options.path));
|
|
693
|
-
if (!layoutPathValidation.isValid) {
|
|
694
|
-
throw new AtomixCLIError(
|
|
695
|
-
layoutPathValidation.error,
|
|
696
|
-
'INVALID_PATH',
|
|
697
|
-
[
|
|
698
|
-
'Ensure the path is within the project directory',
|
|
699
|
-
'Avoid using ".." to navigate outside the project',
|
|
700
|
-
'Example: --path ./src/layouts'
|
|
701
|
-
]
|
|
702
|
-
);
|
|
703
|
-
}
|
|
704
|
-
const layoutPath = join(layoutPathValidation.safePath, safeName);
|
|
705
|
-
await mkdir(layoutPath, { recursive: true });
|
|
706
|
-
|
|
707
|
-
const componentContent = componentTemplates.layout.component(safeName);
|
|
708
|
-
await writeFile(join(layoutPath, `${safeName}.tsx`), componentContent, 'utf8');
|
|
709
|
-
|
|
710
|
-
const scssContent = componentTemplates.layout.scss(safeName);
|
|
711
|
-
const scssFilename = `_layouts.${safeName.toLowerCase()}.scss`;
|
|
712
|
-
const scssPath = join(process.cwd(), 'src/styles/05-layouts');
|
|
713
|
-
|
|
714
|
-
if (existsSync(scssPath)) {
|
|
715
|
-
await writeFile(join(scssPath, scssFilename), scssContent, 'utf8');
|
|
716
|
-
|
|
717
|
-
// Update layouts index
|
|
718
|
-
const indexPath = join(scssPath, '_index.scss');
|
|
719
|
-
if (existsSync(indexPath)) {
|
|
720
|
-
let indexContent = await readFile(indexPath, 'utf8');
|
|
721
|
-
const forwardStatement = `@forward 'layouts.${safeName.toLowerCase()}';`;
|
|
722
|
-
if (!indexContent.includes(forwardStatement)) {
|
|
723
|
-
indexContent += `\n${forwardStatement}`;
|
|
724
|
-
await writeFile(indexPath, indexContent, 'utf8');
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
spinner.succeed(chalk.green(`✓ Created layout ${safeName}`));
|
|
730
|
-
|
|
731
|
-
} else if (type === 'context' || type === 'ctx') {
|
|
732
|
-
const contextPathValidation = validatePath(sanitizeInput(options.path));
|
|
733
|
-
if (!contextPathValidation.isValid) {
|
|
734
|
-
throw new AtomixCLIError(
|
|
735
|
-
contextPathValidation.error,
|
|
736
|
-
'INVALID_PATH',
|
|
737
|
-
[
|
|
738
|
-
'Ensure the path is within the project directory',
|
|
739
|
-
'Avoid using ".." to navigate outside the project',
|
|
740
|
-
'Example: --path ./src/contexts'
|
|
741
|
-
]
|
|
742
|
-
);
|
|
743
|
-
}
|
|
744
|
-
const contextPath = join(contextPathValidation.safePath, `${safeName}Context`);
|
|
745
|
-
await mkdir(contextPath, { recursive: true });
|
|
746
|
-
|
|
747
|
-
const contextContent = componentTemplates.context.context(safeName);
|
|
748
|
-
await writeFile(join(contextPath, `${safeName}Context.tsx`), contextContent, 'utf8');
|
|
749
|
-
|
|
750
|
-
spinner.succeed(chalk.green(`✓ Created context ${safeName}Context`));
|
|
751
|
-
|
|
752
|
-
} else if (type === 'token' || type === 't') {
|
|
753
|
-
// Token generation
|
|
754
|
-
const validCategories = ['colors', 'spacing', 'typography', 'shadows', 'radius', 'animations'];
|
|
755
|
-
|
|
756
|
-
if (!validCategories.includes(name.toLowerCase())) {
|
|
757
|
-
throw new AtomixCLIError(
|
|
758
|
-
`Invalid token category: ${name}`,
|
|
759
|
-
'INVALID_TOKEN_CATEGORY',
|
|
760
|
-
[
|
|
761
|
-
`Valid categories: ${validCategories.join(', ')}`,
|
|
762
|
-
'Example: atomix generate token colors',
|
|
763
|
-
'Example: atomix g t spacing'
|
|
764
|
-
]
|
|
765
|
-
);
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
const tokenPath = join(process.cwd(), 'src/styles/01-settings');
|
|
769
|
-
|
|
770
|
-
// Check if settings directory exists
|
|
771
|
-
if (!existsSync(tokenPath)) {
|
|
772
|
-
throw new AtomixCLIError(
|
|
773
|
-
'Settings directory not found',
|
|
774
|
-
'MISSING_DIRECTORY',
|
|
775
|
-
[
|
|
776
|
-
'Ensure you are in an Atomix project directory',
|
|
777
|
-
'Create the directory: mkdir -p src/styles/01-settings',
|
|
778
|
-
'Or initialize a new project: atomix init'
|
|
779
|
-
]
|
|
780
|
-
);
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
// Generate token file based on category
|
|
784
|
-
let tokenContent = '';
|
|
785
|
-
let filename = '';
|
|
786
|
-
|
|
787
|
-
switch (name.toLowerCase()) {
|
|
788
|
-
case 'colors':
|
|
789
|
-
filename = '_settings.colors.custom.scss';
|
|
790
|
-
tokenContent = generateColorTokens();
|
|
791
|
-
break;
|
|
792
|
-
case 'spacing':
|
|
793
|
-
filename = '_settings.spacing.custom.scss';
|
|
794
|
-
tokenContent = generateSpacingTokens();
|
|
795
|
-
break;
|
|
796
|
-
case 'typography':
|
|
797
|
-
filename = '_settings.typography.custom.scss';
|
|
798
|
-
tokenContent = generateTypographyTokens();
|
|
799
|
-
break;
|
|
800
|
-
case 'shadows':
|
|
801
|
-
filename = '_settings.box-shadow.custom.scss';
|
|
802
|
-
tokenContent = generateShadowTokens();
|
|
803
|
-
break;
|
|
804
|
-
case 'radius':
|
|
805
|
-
filename = '_settings.border-radius.custom.scss';
|
|
806
|
-
tokenContent = generateRadiusTokens();
|
|
807
|
-
break;
|
|
808
|
-
case 'animations':
|
|
809
|
-
filename = '_settings.animations.custom.scss';
|
|
810
|
-
tokenContent = generateAnimationTokens();
|
|
811
|
-
break;
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
const filePath = join(tokenPath, filename);
|
|
815
|
-
|
|
816
|
-
// Check if file already exists
|
|
817
|
-
if (existsSync(filePath) && !options.force) {
|
|
818
|
-
throw new AtomixCLIError(
|
|
819
|
-
`Token file already exists: ${filename}`,
|
|
820
|
-
'FILE_EXISTS',
|
|
821
|
-
[
|
|
822
|
-
'Use --force flag to overwrite',
|
|
823
|
-
`Or edit the existing file: ${filePath}`,
|
|
824
|
-
'Or choose a different category'
|
|
825
|
-
]
|
|
826
|
-
);
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
// Write token file
|
|
830
|
-
await writeFile(filePath, tokenContent, 'utf8');
|
|
831
|
-
spinner.succeed(chalk.green(`✓ Created token file: ${filename}`));
|
|
832
|
-
|
|
833
|
-
// Success message
|
|
834
|
-
console.log(boxen(
|
|
835
|
-
chalk.bold.green(`🎨 ${name} tokens generated successfully!\n\n`) +
|
|
836
|
-
chalk.cyan('Next steps:\n') +
|
|
837
|
-
chalk.gray(`1. Customize your tokens:\n`) +
|
|
838
|
-
chalk.white(` Edit ${filePath}\n\n`) +
|
|
839
|
-
chalk.gray(`2. Import in your theme:\n`) +
|
|
840
|
-
chalk.white(` @use '${filename.replace('.scss', '')}' as *;\n\n`) +
|
|
841
|
-
chalk.gray(`3. Build your styles:\n`) +
|
|
842
|
-
chalk.white(` npm run build`),
|
|
843
|
-
{
|
|
844
|
-
padding: 1,
|
|
845
|
-
margin: 1,
|
|
846
|
-
borderStyle: 'round',
|
|
847
|
-
borderColor: 'green'
|
|
848
|
-
}
|
|
849
|
-
));
|
|
850
|
-
} else {
|
|
851
|
-
throw new AtomixCLIError(
|
|
852
|
-
`Unknown generation type: ${type}`,
|
|
853
|
-
'UNKNOWN_TYPE',
|
|
854
|
-
[
|
|
855
|
-
'Valid types are: component (or c), token (or t)',
|
|
856
|
-
'Example: atomix generate component Button',
|
|
857
|
-
'Example: atomix g c Button'
|
|
858
|
-
]
|
|
859
|
-
);
|
|
860
|
-
}
|
|
861
|
-
|
|
91
|
+
await validateAction(options, subcommand, name);
|
|
862
92
|
} catch (error) {
|
|
863
|
-
|
|
93
|
+
await handleCLIError(error);
|
|
864
94
|
}
|
|
865
95
|
});
|
|
866
96
|
|
|
867
97
|
/**
|
|
868
|
-
*
|
|
98
|
+
* Design Token Management
|
|
869
99
|
*/
|
|
870
100
|
program
|
|
871
|
-
.command('
|
|
872
|
-
.description('
|
|
873
|
-
.option('--
|
|
874
|
-
.option('--
|
|
875
|
-
.option('--
|
|
876
|
-
.
|
|
877
|
-
.option('--fix', 'Attempt to fix issues automatically', false)
|
|
878
|
-
.action(async (target, options) => {
|
|
879
|
-
const spinner = ora('Running validation...').start();
|
|
880
|
-
|
|
101
|
+
.command('tokens <subcommand>')
|
|
102
|
+
.description('Manage design tokens (list, export, pull, push). For pull/push, --provider is required (configure tokenEngine.providers in atomix.config; types: figma, style-dictionary, w3c).')
|
|
103
|
+
.option('-f, --format <format>', 'Export format (css|scss|json)', 'css')
|
|
104
|
+
.option('-o, --output <path>', 'Output directory', './tokens')
|
|
105
|
+
.option('-p, --provider <provider>', 'Token provider name (required for pull and push)')
|
|
106
|
+
.action(async (subcommand, options) => {
|
|
881
107
|
try {
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
const issues = [];
|
|
885
|
-
const warnings = [];
|
|
886
|
-
|
|
887
|
-
// Token validation
|
|
888
|
-
if (options.tokens || target === 'tokens') {
|
|
889
|
-
spinner.text = 'Validating design tokens...';
|
|
890
|
-
|
|
891
|
-
const tokenFiles = [
|
|
892
|
-
'src/styles/01-settings/_settings.colors.scss',
|
|
893
|
-
'src/styles/01-settings/_settings.typography.scss',
|
|
894
|
-
'src/styles/01-settings/_settings.spacing.scss',
|
|
895
|
-
'src/styles/01-settings/_settings.radius.scss'
|
|
896
|
-
];
|
|
897
|
-
|
|
898
|
-
for (const file of tokenFiles) {
|
|
899
|
-
const filePath = join(process.cwd(), file);
|
|
900
|
-
if (existsSync(filePath)) {
|
|
901
|
-
const content = await readFile(filePath, 'utf8');
|
|
902
|
-
|
|
903
|
-
// Check for hardcoded values
|
|
904
|
-
const hardcodedColors = content.match(/#[0-9a-fA-F]{3,8}(?![0-9a-fA-F])/g);
|
|
905
|
-
if (hardcodedColors && hardcodedColors.length > 0) {
|
|
906
|
-
warnings.push({
|
|
907
|
-
file: file,
|
|
908
|
-
issue: `Found ${hardcodedColors.length} hardcoded color values`,
|
|
909
|
-
suggestion: 'Use CSS custom properties or SCSS variables'
|
|
910
|
-
});
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
// Check for missing default flags
|
|
914
|
-
const variables = content.match(/\$[a-z-]+:/gi);
|
|
915
|
-
const defaultFlags = content.match(/!default/g);
|
|
916
|
-
if (variables && (!defaultFlags || defaultFlags.length < variables.length)) {
|
|
917
|
-
warnings.push({
|
|
918
|
-
file: file,
|
|
919
|
-
issue: 'Some variables missing !default flag',
|
|
920
|
-
suggestion: 'Add !default to all token variables for better theming'
|
|
921
|
-
});
|
|
922
|
-
}
|
|
923
|
-
} else {
|
|
924
|
-
issues.push({
|
|
925
|
-
file: file,
|
|
926
|
-
issue: 'Token file not found',
|
|
927
|
-
suggestion: 'Ensure all token files are present in src/styles/01-settings/'
|
|
928
|
-
});
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
// Component validation
|
|
934
|
-
if (options.component) {
|
|
935
|
-
spinner.text = `Validating component: ${options.component}...`;
|
|
936
|
-
|
|
937
|
-
const componentPath = join('./src/components', options.component);
|
|
938
|
-
const validationResult = await validateGeneratedComponent(options.component, componentPath);
|
|
939
|
-
|
|
940
|
-
// Convert validation results to issue/warning format
|
|
941
|
-
validationResult.issues.forEach(issue => {
|
|
942
|
-
issues.push({
|
|
943
|
-
file: options.component,
|
|
944
|
-
issue: issue,
|
|
945
|
-
suggestion: 'Check component structure and implementation'
|
|
946
|
-
});
|
|
947
|
-
});
|
|
948
|
-
|
|
949
|
-
validationResult.warnings.forEach(warning => {
|
|
950
|
-
warnings.push({
|
|
951
|
-
file: options.component,
|
|
952
|
-
issue: warning,
|
|
953
|
-
suggestion: 'Improve component quality by addressing warnings'
|
|
954
|
-
});
|
|
955
|
-
});
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
// Theme validation
|
|
959
|
-
if (options.theme) {
|
|
960
|
-
spinner.text = `Validating theme: ${options.theme}...`;
|
|
961
|
-
|
|
962
|
-
const themePath = resolve(options.theme);
|
|
963
|
-
if (!existsSync(themePath)) {
|
|
964
|
-
issues.push({
|
|
965
|
-
file: options.theme,
|
|
966
|
-
issue: 'Theme file not found',
|
|
967
|
-
suggestion: 'Check the theme path is correct'
|
|
968
|
-
});
|
|
969
|
-
} else {
|
|
970
|
-
const content = await readFile(themePath, 'utf8');
|
|
971
|
-
|
|
972
|
-
// Check for required imports
|
|
973
|
-
const requiredImports = [
|
|
974
|
-
'@import.*settings',
|
|
975
|
-
'@use.*settings',
|
|
976
|
-
'@import.*tools',
|
|
977
|
-
'@use.*tools'
|
|
978
|
-
];
|
|
979
|
-
|
|
980
|
-
let hasSettings = false;
|
|
981
|
-
for (const pattern of requiredImports) {
|
|
982
|
-
if (new RegExp(pattern).test(content)) {
|
|
983
|
-
hasSettings = true;
|
|
984
|
-
break;
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
if (!hasSettings) {
|
|
989
|
-
issues.push({
|
|
990
|
-
file: options.theme,
|
|
991
|
-
issue: 'Missing design system imports',
|
|
992
|
-
suggestion: 'Import settings and tools from the design system'
|
|
993
|
-
});
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
// Accessibility validation
|
|
999
|
-
if (options.a11y || options.accessibility) {
|
|
1000
|
-
spinner.text = 'Checking accessibility compliance...';
|
|
1001
|
-
|
|
1002
|
-
// Check for focus styles
|
|
1003
|
-
const componentFiles = [
|
|
1004
|
-
'src/styles/06-components'
|
|
1005
|
-
];
|
|
1006
|
-
|
|
1007
|
-
for (const dir of componentFiles) {
|
|
1008
|
-
const dirPath = join(process.cwd(), dir);
|
|
1009
|
-
if (existsSync(dirPath)) {
|
|
1010
|
-
// This is a simplified check - in reality, we'd parse the CSS
|
|
1011
|
-
warnings.push({
|
|
1012
|
-
file: dir,
|
|
1013
|
-
issue: 'Manual accessibility review recommended',
|
|
1014
|
-
suggestion: 'Ensure all interactive components have :focus-visible styles'
|
|
1015
|
-
});
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
spinner.stop();
|
|
1021
|
-
|
|
1022
|
-
// Display results
|
|
1023
|
-
if (issues.length === 0 && warnings.length === 0) {
|
|
1024
|
-
console.log(boxen(
|
|
1025
|
-
chalk.bold.green('✅ All validations passed!\n\n') +
|
|
1026
|
-
chalk.gray('Your design system is following best practices.'),
|
|
1027
|
-
{
|
|
1028
|
-
padding: 1,
|
|
1029
|
-
margin: 1,
|
|
1030
|
-
borderStyle: 'round',
|
|
1031
|
-
borderColor: 'green'
|
|
1032
|
-
}
|
|
1033
|
-
));
|
|
1034
|
-
} else {
|
|
1035
|
-
if (issues.length > 0) {
|
|
1036
|
-
console.log(chalk.bold.red(`\n❌ Found ${issues.length} issue(s):\n`));
|
|
1037
|
-
issues.forEach((issue, index) => {
|
|
1038
|
-
console.log(chalk.red(` ${index + 1}. ${issue.file}`));
|
|
1039
|
-
console.log(chalk.gray(` Issue: ${issue.issue}`));
|
|
1040
|
-
console.log(chalk.yellow(` Fix: ${issue.suggestion}\n`));
|
|
1041
|
-
});
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
if (warnings.length > 0) {
|
|
1045
|
-
console.log(chalk.bold.yellow(`\n⚠️ Found ${warnings.length} warning(s):\n`));
|
|
1046
|
-
warnings.forEach((warning, index) => {
|
|
1047
|
-
console.log(chalk.yellow(` ${index + 1}. ${warning.file}`));
|
|
1048
|
-
console.log(chalk.gray(` Warning: ${warning.issue}`));
|
|
1049
|
-
console.log(chalk.cyan(` Suggestion: ${warning.suggestion}\n`));
|
|
1050
|
-
});
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
if (options.fix && (issues.length > 0 || warnings.length > 0)) {
|
|
1054
|
-
console.log(chalk.cyan('\n🔧 Attempting to fix issues...'));
|
|
1055
|
-
|
|
1056
|
-
let fixedCount = 0;
|
|
1057
|
-
|
|
1058
|
-
// Run token fixes
|
|
1059
|
-
if (options.tokens || target === 'tokens' || !target) {
|
|
1060
|
-
const tokenFixResult = await fixTokens(options);
|
|
1061
|
-
fixedCount += tokenFixResult.totalFixed;
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
if (fixedCount > 0) {
|
|
1065
|
-
console.log(chalk.green(`\n✨ Fixed ${fixedCount} issue(s). Please run validate again to verify.`));
|
|
1066
|
-
} else {
|
|
1067
|
-
console.log(chalk.yellow('\nCould not automatically fix all reported issues. Manual intervention required.'));
|
|
1068
|
-
}
|
|
1069
|
-
} else {
|
|
1070
|
-
console.log(chalk.yellow('\nCould not automatically fix reported issues. Manual intervention required.'));
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
108
|
+
await tokensAction(subcommand, options);
|
|
1073
109
|
} catch (error) {
|
|
1074
|
-
|
|
110
|
+
await handleCLIError(error);
|
|
1075
111
|
}
|
|
1076
112
|
});
|
|
1077
113
|
|
|
1078
114
|
/**
|
|
1079
|
-
*
|
|
1080
|
-
*/
|
|
1081
|
-
program
|
|
1082
|
-
.command('dev <theme>')
|
|
1083
|
-
.description('Start development mode with hot reload')
|
|
1084
|
-
.option('-o, --output <path>', 'Output directory', './dist')
|
|
1085
|
-
.option('--open', 'Open in browser after build', false)
|
|
1086
|
-
.action(async (theme, options) => {
|
|
1087
|
-
console.log(boxen(
|
|
1088
|
-
chalk.bold.cyan('🚀 Starting Atomix Dev Mode\n\n') +
|
|
1089
|
-
chalk.gray('Watching for changes...\n') +
|
|
1090
|
-
chalk.gray('Press Ctrl+C to exit'),
|
|
1091
|
-
{
|
|
1092
|
-
padding: 1,
|
|
1093
|
-
margin: 1,
|
|
1094
|
-
borderStyle: 'round',
|
|
1095
|
-
borderColor: 'cyan'
|
|
1096
|
-
}
|
|
1097
|
-
));
|
|
1098
|
-
|
|
1099
|
-
// Reuse build command with watch flag
|
|
1100
|
-
await program.parseAsync([
|
|
1101
|
-
...process.argv.slice(0, 2),
|
|
1102
|
-
'build-theme',
|
|
1103
|
-
theme,
|
|
1104
|
-
'--watch',
|
|
1105
|
-
'--output', options.output
|
|
1106
|
-
]);
|
|
1107
|
-
});
|
|
1108
|
-
|
|
1109
|
-
// Keep existing commands (create-theme, list-themes, info)
|
|
1110
|
-
// ... [Previous create-theme, list-themes, and info commands remain the same]
|
|
1111
|
-
|
|
1112
|
-
/**
|
|
1113
|
-
* Migrate Command - NEW (Migration from other frameworks)
|
|
115
|
+
* Project Initialization
|
|
1114
116
|
*/
|
|
1115
117
|
program
|
|
1116
|
-
.command('
|
|
1117
|
-
.description('
|
|
1118
|
-
.option('-
|
|
1119
|
-
.option('--
|
|
1120
|
-
.
|
|
1121
|
-
.action(async (from, options) => {
|
|
1122
|
-
const spinner = ora('Preparing migration...').start();
|
|
1123
|
-
|
|
118
|
+
.command('init')
|
|
119
|
+
.description('Initialize a new Atomix design system project. For CI/scripts use --type <react|nextjs|vanilla> or --yes.')
|
|
120
|
+
.option('-y, --yes', 'Use default choices (non-interactive)')
|
|
121
|
+
.option('-t, --type <type>', 'Project type: react, nextjs, or vanilla (skips prompt when set)')
|
|
122
|
+
.action(async (options) => {
|
|
1124
123
|
try {
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
// Validate migration type
|
|
1128
|
-
const validMigrations = ['tailwind', 'bootstrap', 'scss-variables'];
|
|
1129
|
-
if (!validMigrations.includes(from.toLowerCase())) {
|
|
1130
|
-
throw new AtomixCLIError(
|
|
1131
|
-
`Unknown migration source: ${from}`,
|
|
1132
|
-
'INVALID_MIGRATION',
|
|
1133
|
-
[
|
|
1134
|
-
'Valid migration sources: tailwind, bootstrap, scss-variables',
|
|
1135
|
-
'Example: atomix migrate tailwind',
|
|
1136
|
-
'Example: atomix migrate bootstrap --source ./src'
|
|
1137
|
-
]
|
|
1138
|
-
);
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
const sanitizedSource = sanitizeInput(options.source);
|
|
1142
|
-
const sourceValidation = validatePath(sanitizedSource);
|
|
1143
|
-
if (!sourceValidation.isValid) {
|
|
1144
|
-
throw new AtomixCLIError(
|
|
1145
|
-
sourceValidation.error,
|
|
1146
|
-
'INVALID_PATH',
|
|
1147
|
-
[
|
|
1148
|
-
'Ensure source path is within the project directory',
|
|
1149
|
-
'Avoid sensitive or absolute system paths',
|
|
1150
|
-
'Example: --source ./src'
|
|
1151
|
-
]
|
|
1152
|
-
);
|
|
1153
|
-
}
|
|
1154
|
-
const sourcePath = resolve(sourceValidation.safePath);
|
|
1155
|
-
if (!existsSync(sourcePath)) {
|
|
1156
|
-
throw new AtomixCLIError(
|
|
1157
|
-
`Source directory not found: ${sourcePath}`,
|
|
1158
|
-
'SOURCE_NOT_FOUND',
|
|
1159
|
-
[
|
|
1160
|
-
'Check the source path is correct',
|
|
1161
|
-
'Use --source flag to specify a different directory',
|
|
1162
|
-
'Example: atomix migrate tailwind --source ./app'
|
|
1163
|
-
]
|
|
1164
|
-
);
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
spinner.stop();
|
|
1168
|
-
|
|
1169
|
-
// Show migration preview
|
|
1170
|
-
console.log(boxen(
|
|
1171
|
-
chalk.bold.cyan(`🔄 Migration Preview\n\n`) +
|
|
1172
|
-
chalk.gray(`From: ${chalk.white(from)}\n`) +
|
|
1173
|
-
chalk.gray(`Source: ${chalk.white(sourcePath)}\n`) +
|
|
1174
|
-
chalk.gray(`Mode: ${options.dryRun ? chalk.yellow('Dry Run') : chalk.green('Live')}\n`) +
|
|
1175
|
-
chalk.gray(`Backup: ${options.createBackup ? chalk.green('Yes') : chalk.red('No')}`),
|
|
1176
|
-
{
|
|
1177
|
-
padding: 1,
|
|
1178
|
-
margin: 1,
|
|
1179
|
-
borderStyle: 'round',
|
|
1180
|
-
borderColor: 'cyan'
|
|
1181
|
-
}
|
|
1182
|
-
));
|
|
1183
|
-
|
|
1184
|
-
// Confirm migration
|
|
1185
|
-
if (!options.dryRun) {
|
|
1186
|
-
const { confirmMigration } = await inquirer.prompt([
|
|
1187
|
-
{
|
|
1188
|
-
type: 'confirm',
|
|
1189
|
-
name: 'confirmMigration',
|
|
1190
|
-
message: chalk.yellow('This will modify your files. Continue?'),
|
|
1191
|
-
default: false
|
|
1192
|
-
}
|
|
1193
|
-
]);
|
|
1194
|
-
|
|
1195
|
-
if (!confirmMigration) {
|
|
1196
|
-
console.log(chalk.yellow('\n Migration cancelled.'));
|
|
1197
|
-
return;
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
// Create backup if requested (cross-platform: fs.cpSync works on Windows and Unix)
|
|
1202
|
-
if (options.createBackup && !options.dryRun) {
|
|
1203
|
-
const backupSpinner = ora('Creating backup...').start();
|
|
1204
|
-
const backupDir = `${sourcePath}.backup.${Date.now()}`;
|
|
1205
|
-
|
|
1206
|
-
try {
|
|
1207
|
-
cpSync(sourcePath, backupDir, { recursive: true });
|
|
1208
|
-
backupSpinner.succeed(chalk.green(`✓ Backup created: ${backupDir}`));
|
|
1209
|
-
} catch (error) {
|
|
1210
|
-
backupSpinner.warn(chalk.yellow('Could not create backup, continuing anyway...'));
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
// Run migration
|
|
1215
|
-
let report;
|
|
1216
|
-
|
|
1217
|
-
switch (from.toLowerCase()) {
|
|
1218
|
-
case 'tailwind':
|
|
1219
|
-
report = await migrateTailwind(sourcePath, options);
|
|
1220
|
-
break;
|
|
1221
|
-
|
|
1222
|
-
case 'bootstrap':
|
|
1223
|
-
report = await migrateBootstrap(sourcePath, options);
|
|
1224
|
-
break;
|
|
1225
|
-
|
|
1226
|
-
case 'scss-variables':
|
|
1227
|
-
report = await migrateSCSSVariables(sourcePath, options);
|
|
1228
|
-
break;
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
// Display report
|
|
1232
|
-
displayMigrationReport(report);
|
|
1233
|
-
|
|
1234
|
-
// Next steps
|
|
1235
|
-
if (!options.dryRun && report.filesProcessed > 0) {
|
|
1236
|
-
console.log(chalk.cyan('\n📝 Next Steps:'));
|
|
1237
|
-
console.log(chalk.gray(' 1. Review the changes in your code'));
|
|
1238
|
-
console.log(chalk.gray(' 2. Install Atomix: npm install @shohojdhara/atomix'));
|
|
1239
|
-
console.log(chalk.gray(' 3. Import Atomix styles: import "@shohojdhara/atomix/css"'));
|
|
1240
|
-
console.log(chalk.gray(' 4. Test your application thoroughly'));
|
|
1241
|
-
console.log(chalk.gray(' 5. Customize with your theme: atomix create-theme custom'));
|
|
1242
|
-
}
|
|
1243
|
-
|
|
124
|
+
await initAction(options);
|
|
1244
125
|
} catch (error) {
|
|
1245
|
-
|
|
126
|
+
await handleCLIError(error);
|
|
1246
127
|
}
|
|
1247
128
|
});
|
|
1248
129
|
|
|
1249
130
|
/**
|
|
1250
|
-
*
|
|
131
|
+
* Resource Generation
|
|
1251
132
|
*/
|
|
1252
133
|
program
|
|
1253
|
-
.command('
|
|
1254
|
-
.
|
|
1255
|
-
.
|
|
1256
|
-
.
|
|
134
|
+
.command('generate <type> <name>')
|
|
135
|
+
.alias('g')
|
|
136
|
+
.description('Generate components, tokens, or themes')
|
|
137
|
+
.option('-i, --interactive', 'Interactive mode', false)
|
|
138
|
+
.option('-p, --path <path>', 'Output path', './src/components')
|
|
139
|
+
.option('--prompt <prompt>', 'AI prompt for generating component')
|
|
140
|
+
.option('--complexity <level>', 'Complexity (simple|medium|complex)', 'medium')
|
|
141
|
+
.option('--validate', 'Validate after generation', true)
|
|
142
|
+
.action(async (type, name, options) => {
|
|
1257
143
|
try {
|
|
1258
|
-
|
|
1259
|
-
if (options.skipInstall) {
|
|
1260
|
-
process.env.ATOMIX_SKIP_INSTALL = 'true';
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
// Run the interactive wizard
|
|
1264
|
-
await runInitWizard();
|
|
144
|
+
await generateAction(type, name, options);
|
|
1265
145
|
} catch (error) {
|
|
1266
|
-
|
|
146
|
+
await handleCLIError(error);
|
|
1267
147
|
}
|
|
1268
148
|
});
|
|
1269
149
|
|
|
1270
150
|
/**
|
|
1271
|
-
*
|
|
151
|
+
* Migration Tools
|
|
1272
152
|
*/
|
|
1273
153
|
program
|
|
1274
|
-
.command('
|
|
1275
|
-
.description('
|
|
1276
|
-
.option('-
|
|
1277
|
-
.
|
|
1278
|
-
.option('-o, --output <path>', 'Output file path')
|
|
1279
|
-
.option('--dry-run', 'Preview changes without modifying files', false)
|
|
1280
|
-
.action(async (action, options) => {
|
|
154
|
+
.command('migrate <type> <source>')
|
|
155
|
+
.description('Migrate from other frameworks (tailwind|bootstrap). <source> is the project root directory (e.g. . or ./my-tailwind-app).')
|
|
156
|
+
.option('-p, --preview', 'Preview side-by-side diff before applying', false)
|
|
157
|
+
.action(async (type, source, options) => {
|
|
1281
158
|
try {
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
switch (action.toLowerCase()) {
|
|
1285
|
-
case 'list':
|
|
1286
|
-
case 'ls':
|
|
1287
|
-
await listTokens(options.category);
|
|
1288
|
-
break;
|
|
1289
|
-
|
|
1290
|
-
case 'validate':
|
|
1291
|
-
case 'check': {
|
|
1292
|
-
const validationResult = await validateTokens(options);
|
|
1293
|
-
if (validationResult.issues.length > 0) {
|
|
1294
|
-
process.exit(1); // Exit with error if issues found
|
|
1295
|
-
}
|
|
1296
|
-
break;
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
case 'export':
|
|
1300
|
-
if (!options.format) {
|
|
1301
|
-
throw new AtomixCLIError(
|
|
1302
|
-
'Export format is required',
|
|
1303
|
-
'MISSING_FORMAT',
|
|
1304
|
-
[
|
|
1305
|
-
'Specify format with --format flag',
|
|
1306
|
-
'Valid formats: json, css, scss, js, ts',
|
|
1307
|
-
'Example: atomix tokens export --format json'
|
|
1308
|
-
]
|
|
1309
|
-
);
|
|
1310
|
-
}
|
|
1311
|
-
if (options.output) {
|
|
1312
|
-
const outValidation = validatePath(sanitizeInput(options.output));
|
|
1313
|
-
if (!outValidation.isValid) {
|
|
1314
|
-
throw new AtomixCLIError(
|
|
1315
|
-
outValidation.error,
|
|
1316
|
-
'INVALID_PATH',
|
|
1317
|
-
[
|
|
1318
|
-
'Use a project-relative output file path',
|
|
1319
|
-
'Example: --output ./tokens.json'
|
|
1320
|
-
]
|
|
1321
|
-
);
|
|
1322
|
-
}
|
|
1323
|
-
await exportTokens(options.format, outValidation.safePath);
|
|
1324
|
-
} else {
|
|
1325
|
-
await exportTokens(options.format, options.output);
|
|
1326
|
-
}
|
|
1327
|
-
break;
|
|
1328
|
-
|
|
1329
|
-
case 'import':
|
|
1330
|
-
if (!options.output) {
|
|
1331
|
-
throw new AtomixCLIError(
|
|
1332
|
-
'Import file path is required',
|
|
1333
|
-
'MISSING_PATH',
|
|
1334
|
-
[
|
|
1335
|
-
'Specify file with --output flag',
|
|
1336
|
-
'Example: atomix tokens import --output tokens.json'
|
|
1337
|
-
]
|
|
1338
|
-
);
|
|
1339
|
-
}
|
|
1340
|
-
{
|
|
1341
|
-
const inValidation = validatePath(sanitizeInput(options.output));
|
|
1342
|
-
if (!inValidation.isValid) {
|
|
1343
|
-
throw new AtomixCLIError(
|
|
1344
|
-
inValidation.error,
|
|
1345
|
-
'INVALID_PATH',
|
|
1346
|
-
[
|
|
1347
|
-
'Use a project-relative input file path',
|
|
1348
|
-
'Example: --output ./tokens.json'
|
|
1349
|
-
]
|
|
1350
|
-
);
|
|
1351
|
-
}
|
|
1352
|
-
await importTokens(inValidation.safePath, { dryRun: options.dryRun });
|
|
1353
|
-
}
|
|
1354
|
-
break;
|
|
1355
|
-
|
|
1356
|
-
default:
|
|
1357
|
-
throw new AtomixCLIError(
|
|
1358
|
-
`Unknown token action: ${action}`,
|
|
1359
|
-
'UNKNOWN_ACTION',
|
|
1360
|
-
[
|
|
1361
|
-
'Valid actions: list, validate, export, import',
|
|
1362
|
-
'Example: atomix tokens list',
|
|
1363
|
-
'Example: atomix tokens export --format json'
|
|
1364
|
-
]
|
|
1365
|
-
);
|
|
1366
|
-
}
|
|
1367
|
-
|
|
159
|
+
await migrateAction(type, source, options);
|
|
1368
160
|
} catch (error) {
|
|
1369
|
-
|
|
161
|
+
await handleCLIError(error);
|
|
1370
162
|
}
|
|
1371
163
|
});
|
|
1372
164
|
|
|
1373
165
|
/**
|
|
1374
|
-
*
|
|
166
|
+
* Performance & Benchmarking
|
|
1375
167
|
*/
|
|
1376
|
-
|
|
1377
|
-
.command('
|
|
1378
|
-
.description('
|
|
1379
|
-
|
|
1380
|
-
// Theme validate
|
|
1381
|
-
themeCommand
|
|
1382
|
-
.command('validate')
|
|
1383
|
-
.description('Validate theme configuration')
|
|
1384
|
-
.option('--config <path>', 'Path to theme config file')
|
|
1385
|
-
.option('--strict', 'Enable strict validation')
|
|
168
|
+
program
|
|
169
|
+
.command('benchmark')
|
|
170
|
+
.description('Profile CLI performance and show metrics. Benchmark collects metrics from previous CLI runs.')
|
|
1386
171
|
.action(async (options) => {
|
|
1387
172
|
try {
|
|
1388
|
-
|
|
1389
|
-
await themeCLI.validate(options);
|
|
1390
|
-
} catch (error) {
|
|
1391
|
-
handleError(error);
|
|
1392
|
-
}
|
|
1393
|
-
});
|
|
1394
|
-
|
|
1395
|
-
// Theme list
|
|
1396
|
-
themeCommand
|
|
1397
|
-
.command('list')
|
|
1398
|
-
.alias('ls')
|
|
1399
|
-
.description('List all available themes')
|
|
1400
|
-
.action(async () => {
|
|
1401
|
-
try {
|
|
1402
|
-
const themeCLI = createThemeCLIBridge();
|
|
1403
|
-
await themeCLI.list();
|
|
1404
|
-
} catch (error) {
|
|
1405
|
-
handleError(error);
|
|
1406
|
-
}
|
|
1407
|
-
});
|
|
1408
|
-
|
|
1409
|
-
// Theme inspect
|
|
1410
|
-
themeCommand
|
|
1411
|
-
.command('inspect <name>')
|
|
1412
|
-
.description('Inspect a specific theme')
|
|
1413
|
-
.option('--json', 'Output as JSON')
|
|
1414
|
-
.action(async (name, options) => {
|
|
1415
|
-
try {
|
|
1416
|
-
const themeCLI = createThemeCLIBridge();
|
|
1417
|
-
await themeCLI.inspect(name, options);
|
|
1418
|
-
} catch (error) {
|
|
1419
|
-
handleError(error);
|
|
1420
|
-
}
|
|
1421
|
-
});
|
|
1422
|
-
|
|
1423
|
-
// Theme compare
|
|
1424
|
-
themeCommand
|
|
1425
|
-
.command('compare <theme1> <theme2>')
|
|
1426
|
-
.description('Compare two themes')
|
|
1427
|
-
.action(async (theme1, theme2) => {
|
|
1428
|
-
try {
|
|
1429
|
-
const themeCLI = createThemeCLIBridge();
|
|
1430
|
-
await themeCLI.compare(theme1, theme2);
|
|
173
|
+
await benchmarkAction(options);
|
|
1431
174
|
} catch (error) {
|
|
1432
|
-
|
|
1433
|
-
}
|
|
1434
|
-
});
|
|
1435
|
-
|
|
1436
|
-
// Theme export
|
|
1437
|
-
themeCommand
|
|
1438
|
-
.command('export <name>')
|
|
1439
|
-
.description('Export theme to JSON')
|
|
1440
|
-
.option('-o, --output <path>', 'Output file path')
|
|
1441
|
-
.action(async (name, options) => {
|
|
1442
|
-
try {
|
|
1443
|
-
const themeCLI = createThemeCLIBridge();
|
|
1444
|
-
await themeCLI.export(name, options);
|
|
1445
|
-
} catch (error) {
|
|
1446
|
-
handleError(error);
|
|
1447
|
-
}
|
|
1448
|
-
});
|
|
1449
|
-
|
|
1450
|
-
// Theme create - NEW
|
|
1451
|
-
themeCommand
|
|
1452
|
-
.command('create <name>')
|
|
1453
|
-
.description('Create a new theme')
|
|
1454
|
-
.option('-t, --type <type>', 'Theme type (css|js)', 'css')
|
|
1455
|
-
.option('--template <name>', 'Use template (dark|light|high-contrast)')
|
|
1456
|
-
.option('--interactive', 'Interactive mode', false)
|
|
1457
|
-
.option('-o, --output <path>', 'Output directory', './themes')
|
|
1458
|
-
.option('-f, --force', 'Overwrite existing theme', false)
|
|
1459
|
-
.action(async (name, options) => {
|
|
1460
|
-
const spinner = ora('Creating theme...').start();
|
|
1461
|
-
|
|
1462
|
-
try {
|
|
1463
|
-
debug(`Creating theme: ${name}`, options);
|
|
1464
|
-
|
|
1465
|
-
// Validate name
|
|
1466
|
-
const nameValidation = validateThemeName(name);
|
|
1467
|
-
if (!nameValidation.isValid) {
|
|
1468
|
-
throw new AtomixCLIError(
|
|
1469
|
-
nameValidation.error,
|
|
1470
|
-
'INVALID_NAME',
|
|
1471
|
-
[
|
|
1472
|
-
'Use lowercase letters, numbers, and hyphens',
|
|
1473
|
-
'Start with a letter',
|
|
1474
|
-
'Example: dark-theme, light-mode, custom-theme',
|
|
1475
|
-
'Avoid consecutive or trailing hyphens'
|
|
1476
|
-
]
|
|
1477
|
-
);
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
const themePath = join(options.output, name);
|
|
1481
|
-
|
|
1482
|
-
// Check if theme already exists
|
|
1483
|
-
if (existsSync(themePath)) {
|
|
1484
|
-
if (options.force) {
|
|
1485
|
-
await rm(themePath, { recursive: true, force: true });
|
|
1486
|
-
await mkdir(themePath, { recursive: true });
|
|
1487
|
-
spinner.info(chalk.yellow(`Overwriting existing theme: ${name}`));
|
|
1488
|
-
} else {
|
|
1489
|
-
throw new AtomixCLIError(
|
|
1490
|
-
`Theme ${name} already exists`,
|
|
1491
|
-
'THEME_EXISTS',
|
|
1492
|
-
[
|
|
1493
|
-
`Delete the existing theme at ${themePath}`,
|
|
1494
|
-
'Choose a different theme name',
|
|
1495
|
-
'Use --force flag to overwrite'
|
|
1496
|
-
]
|
|
1497
|
-
);
|
|
1498
|
-
}
|
|
1499
|
-
} else {
|
|
1500
|
-
// Create theme directory
|
|
1501
|
-
await mkdir(themePath, { recursive: true });
|
|
1502
|
-
}
|
|
1503
|
-
|
|
1504
|
-
// Generate theme files based on type
|
|
1505
|
-
if (options.type === 'css') {
|
|
1506
|
-
// Create SCSS theme
|
|
1507
|
-
const scssContent = `// Theme: ${name}
|
|
1508
|
-
// =============================================================================
|
|
1509
|
-
|
|
1510
|
-
@import '../../src/styles/01-settings';
|
|
1511
|
-
@import '../../src/styles/02-tools';
|
|
1512
|
-
|
|
1513
|
-
// Theme Variables
|
|
1514
|
-
// =============================================================================
|
|
1515
|
-
:root[data-theme="${name}"] {
|
|
1516
|
-
// Colors
|
|
1517
|
-
--atomix-color-primary: #7AFFD7;
|
|
1518
|
-
--atomix-color-secondary: #FF5733;
|
|
1519
|
-
--atomix-color-success: #4DFF9F;
|
|
1520
|
-
--atomix-color-error: #FF1A1A;
|
|
1521
|
-
--atomix-color-warning: #FFB84D;
|
|
1522
|
-
|
|
1523
|
-
// Background
|
|
1524
|
-
--atomix-color-background: #000000;
|
|
1525
|
-
--atomix-color-surface: #212121;
|
|
1526
|
-
|
|
1527
|
-
// Text
|
|
1528
|
-
--atomix-color-text: #FFFFFF;
|
|
1529
|
-
--atomix-color-text-secondary: rgba(255, 255, 255, 0.8);
|
|
1530
|
-
|
|
1531
|
-
// Border
|
|
1532
|
-
--atomix-color-border: rgba(255, 255, 255, 0.1);
|
|
1533
|
-
|
|
1534
|
-
// Spacing (if needed)
|
|
1535
|
-
// --atomix-space-base: 16px;
|
|
1536
|
-
|
|
1537
|
-
// Typography (if needed)
|
|
1538
|
-
// --atomix-font-family-base: 'Inter', sans-serif;
|
|
1539
|
-
}
|
|
1540
|
-
|
|
1541
|
-
// Theme-specific Component Overrides
|
|
1542
|
-
// =============================================================================
|
|
1543
|
-
[data-theme="${name}"] {
|
|
1544
|
-
// Add component-specific overrides here
|
|
1545
|
-
|
|
1546
|
-
.c-button {
|
|
1547
|
-
// Button overrides
|
|
1548
|
-
}
|
|
1549
|
-
|
|
1550
|
-
.c-card {
|
|
1551
|
-
// Card overrides
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1554
|
-
`;
|
|
1555
|
-
|
|
1556
|
-
await writeFile(join(themePath, 'index.scss'), scssContent, 'utf8');
|
|
1557
|
-
spinner.succeed(chalk.green(`✓ Created ${name}/index.scss`));
|
|
1558
|
-
|
|
1559
|
-
} else if (options.type === 'js') {
|
|
1560
|
-
// Create JavaScript theme
|
|
1561
|
-
const jsContent = `/**
|
|
1562
|
-
* Theme: ${name}
|
|
1563
|
-
*/
|
|
1564
|
-
|
|
1565
|
-
import { createTheme } from '@shohojdhara/atomix/theme';
|
|
1566
|
-
|
|
1567
|
-
export const ${name.replace(/-([a-z])/g, (_, c) => c.toUpperCase())}Theme = createTheme({
|
|
1568
|
-
name: '${name}',
|
|
1569
|
-
palette: {
|
|
1570
|
-
primary: {
|
|
1571
|
-
main: '#7AFFD7',
|
|
1572
|
-
light: '#A0FFE6',
|
|
1573
|
-
dark: '#00E6C3',
|
|
1574
|
-
contrastText: '#000000',
|
|
1575
|
-
},
|
|
1576
|
-
secondary: {
|
|
1577
|
-
main: '#FF5733',
|
|
1578
|
-
light: '#FF8A65',
|
|
1579
|
-
dark: '#E64A19',
|
|
1580
|
-
contrastText: '#FFFFFF',
|
|
1581
|
-
},
|
|
1582
|
-
success: {
|
|
1583
|
-
main: '#4DFF9F',
|
|
1584
|
-
light: '#80FFB8',
|
|
1585
|
-
dark: '#00E66B',
|
|
1586
|
-
contrastText: '#000000',
|
|
1587
|
-
},
|
|
1588
|
-
error: {
|
|
1589
|
-
main: '#FF1A1A',
|
|
1590
|
-
light: '#FF5252',
|
|
1591
|
-
dark: '#E60000',
|
|
1592
|
-
contrastText: '#FFFFFF',
|
|
1593
|
-
},
|
|
1594
|
-
warning: {
|
|
1595
|
-
main: '#FFB84D',
|
|
1596
|
-
light: '#FFCC80',
|
|
1597
|
-
dark: '#FF9800',
|
|
1598
|
-
contrastText: '#000000',
|
|
1599
|
-
},
|
|
1600
|
-
background: {
|
|
1601
|
-
default: '#000000',
|
|
1602
|
-
paper: '#212121',
|
|
1603
|
-
},
|
|
1604
|
-
text: {
|
|
1605
|
-
primary: '#FFFFFF',
|
|
1606
|
-
secondary: 'rgba(255, 255, 255, 0.8)',
|
|
1607
|
-
disabled: 'rgba(255, 255, 255, 0.5)',
|
|
1608
|
-
},
|
|
1609
|
-
},
|
|
1610
|
-
typography: {
|
|
1611
|
-
fontFamily: 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
|
1612
|
-
fontSize: 16,
|
|
1613
|
-
fontWeightLight: 300,
|
|
1614
|
-
fontWeightRegular: 400,
|
|
1615
|
-
fontWeightMedium: 500,
|
|
1616
|
-
fontWeightBold: 700,
|
|
1617
|
-
},
|
|
1618
|
-
spacing: {
|
|
1619
|
-
unit: 8,
|
|
1620
|
-
},
|
|
1621
|
-
shape: {
|
|
1622
|
-
borderRadius: 6,
|
|
1623
|
-
},
|
|
1624
|
-
});
|
|
1625
|
-
|
|
1626
|
-
export default ${name.replace(/-([a-z])/g, (_, c) => c.toUpperCase())}Theme;
|
|
1627
|
-
`;
|
|
1628
|
-
|
|
1629
|
-
await writeFile(join(themePath, 'index.ts'), jsContent, 'utf8');
|
|
1630
|
-
spinner.succeed(chalk.green(`✓ Created ${name}/index.ts`));
|
|
1631
|
-
}
|
|
1632
|
-
|
|
1633
|
-
// Create README
|
|
1634
|
-
const readmeContent = `# ${name} Theme
|
|
1635
|
-
|
|
1636
|
-
## Description
|
|
1637
|
-
|
|
1638
|
-
A custom theme for Atomix Design System.
|
|
1639
|
-
|
|
1640
|
-
## Usage
|
|
1641
|
-
|
|
1642
|
-
### CSS Theme
|
|
1643
|
-
|
|
1644
|
-
\`\`\`scss
|
|
1645
|
-
@import 'themes/${name}';
|
|
1646
|
-
\`\`\`
|
|
1647
|
-
|
|
1648
|
-
### JavaScript Theme
|
|
1649
|
-
|
|
1650
|
-
\`\`\`typescript
|
|
1651
|
-
import { ${name.replace(/-([a-z])/g, (_, c) => c.toUpperCase())}Theme } from './themes/${name}';
|
|
1652
|
-
import { ThemeProvider } from '@shohojdhara/atomix/theme';
|
|
1653
|
-
|
|
1654
|
-
function App() {
|
|
1655
|
-
return (
|
|
1656
|
-
<ThemeProvider theme={${name.replace(/-([a-z])/g, (_, c) => c.toUpperCase())}Theme}>
|
|
1657
|
-
{/* Your app */}
|
|
1658
|
-
</ThemeProvider>
|
|
1659
|
-
);
|
|
1660
|
-
}
|
|
1661
|
-
\`\`\`
|
|
1662
|
-
|
|
1663
|
-
## Customization
|
|
1664
|
-
|
|
1665
|
-
Edit the theme variables in \`index.${options.type === 'css' ? 'scss' : 'ts'}\` to customize colors, typography, spacing, and more.
|
|
1666
|
-
|
|
1667
|
-
## Build
|
|
1668
|
-
|
|
1669
|
-
\`\`\`bash
|
|
1670
|
-
atomix build-theme themes/${name}
|
|
1671
|
-
\`\`\`
|
|
1672
|
-
`;
|
|
1673
|
-
|
|
1674
|
-
await writeFile(join(themePath, 'README.md'), readmeContent, 'utf8');
|
|
1675
|
-
console.log(chalk.green(` ✓ Created ${name}/README.md`));
|
|
1676
|
-
|
|
1677
|
-
// Success message
|
|
1678
|
-
console.log(boxen(
|
|
1679
|
-
chalk.bold.green(`🎨 Theme "${name}" created successfully!\n\n`) +
|
|
1680
|
-
chalk.cyan('Next steps:\n') +
|
|
1681
|
-
chalk.gray(`1. Customize your theme:\n`) +
|
|
1682
|
-
chalk.white(` Edit ${themePath}/index.${options.type === 'css' ? 'scss' : 'ts'}\n\n`) +
|
|
1683
|
-
(options.type === 'css'
|
|
1684
|
-
? chalk.gray(`2. Build your theme:\n`) + chalk.white(` atomix build-theme ${themePath}\n\n`)
|
|
1685
|
-
: chalk.gray(`2. Use in your app:\n`) + chalk.white(` import theme from './themes/${name}';\n\n`)
|
|
1686
|
-
) +
|
|
1687
|
-
chalk.gray(`3. Apply your theme:\n`) +
|
|
1688
|
-
chalk.white(` <ThemeProvider theme="${name}">...</ThemeProvider>`),
|
|
1689
|
-
{
|
|
1690
|
-
padding: 1,
|
|
1691
|
-
margin: 1,
|
|
1692
|
-
borderStyle: 'round',
|
|
1693
|
-
borderColor: 'green'
|
|
1694
|
-
}
|
|
1695
|
-
));
|
|
1696
|
-
|
|
1697
|
-
} catch (error) {
|
|
1698
|
-
handleError(error, spinner);
|
|
175
|
+
await handleCLIError(error);
|
|
1699
176
|
}
|
|
1700
177
|
});
|
|
1701
178
|
|
|
1702
179
|
/**
|
|
1703
|
-
*
|
|
180
|
+
* Build Artifact Cleanup
|
|
1704
181
|
*/
|
|
1705
|
-
|
|
1706
|
-
.command('
|
|
1707
|
-
.description('
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
.command('sync')
|
|
1712
|
-
.description('Sync documentation with component guidelines')
|
|
1713
|
-
.option('--validate', 'Validate documentation after sync', true)
|
|
182
|
+
program
|
|
183
|
+
.command('clean')
|
|
184
|
+
.description('Clean build artifacts and cache files. Use --dry-run to preview, --all to include node_modules.')
|
|
185
|
+
.option('--all', 'Include node_modules in cleanup', false)
|
|
186
|
+
.option('--cache', 'Only clean cache directories', false)
|
|
187
|
+
.option('--verbose', 'Show detailed output', false)
|
|
1714
188
|
.action(async (options) => {
|
|
1715
|
-
const spinner = ora('Syncing documentation...').start();
|
|
1716
|
-
|
|
1717
|
-
try {
|
|
1718
|
-
await syncDocumentation();
|
|
1719
|
-
spinner.stop();
|
|
1720
|
-
|
|
1721
|
-
if (options.validate) {
|
|
1722
|
-
await validateDocumentation();
|
|
1723
|
-
}
|
|
1724
|
-
} catch (error) {
|
|
1725
|
-
handleError(error, spinner);
|
|
1726
|
-
}
|
|
1727
|
-
});
|
|
1728
|
-
|
|
1729
|
-
// Docs validate
|
|
1730
|
-
docsCommand
|
|
1731
|
-
.command('validate')
|
|
1732
|
-
.description('Validate documentation completeness')
|
|
1733
|
-
.action(async () => {
|
|
1734
189
|
try {
|
|
1735
|
-
await
|
|
190
|
+
await cleanAction(options);
|
|
1736
191
|
} catch (error) {
|
|
1737
|
-
|
|
192
|
+
await handleCLIError(error);
|
|
1738
193
|
}
|
|
1739
194
|
});
|
|
1740
195
|
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
.
|
|
1746
|
-
|
|
1747
|
-
|
|
196
|
+
/**
|
|
197
|
+
* Theme Building
|
|
198
|
+
*/
|
|
199
|
+
program
|
|
200
|
+
.command('build-theme <path>')
|
|
201
|
+
.description('Build a custom theme from SCSS. Default output is ./dist (may overwrite app build; use -o dist-theme for custom themes).')
|
|
202
|
+
.option('-o, --output <path>', 'Output directory (default: ./dist)', './dist')
|
|
203
|
+
.option('-m, --minify', 'Minify CSS', true)
|
|
204
|
+
.option('-s, --sourcemap', 'Generate source maps', false)
|
|
205
|
+
.option('-w, --watch', 'Watch mode', false)
|
|
206
|
+
.option('--analyze', 'Analyze bundle', false)
|
|
207
|
+
.action(async (themePath, options) => {
|
|
1748
208
|
try {
|
|
1749
|
-
|
|
1750
|
-
spinner.succeed(chalk.green('CLI documentation generated'));
|
|
1751
|
-
console.log(chalk.gray(` → ${result.file}`));
|
|
209
|
+
await buildThemeAction(themePath, options);
|
|
1752
210
|
} catch (error) {
|
|
1753
|
-
|
|
211
|
+
await handleCLIError(error);
|
|
1754
212
|
}
|
|
1755
213
|
});
|
|
1756
214
|
|
|
1757
215
|
/**
|
|
1758
|
-
*
|
|
216
|
+
* Theme Bridge - Sync Design Tokens with Theme Providers
|
|
1759
217
|
*/
|
|
1760
218
|
program
|
|
1761
|
-
.command('
|
|
1762
|
-
.
|
|
1763
|
-
.
|
|
1764
|
-
.
|
|
1765
|
-
|
|
1766
|
-
|
|
219
|
+
.command('theme-bridge [source]')
|
|
220
|
+
.description('Sync design tokens with theme providers (Tailwind, CSS-in-JS, CSS Variables)')
|
|
221
|
+
.option('-o, --output <dir>', 'Output directory for theme files', './src/theme')
|
|
222
|
+
.option('-f, --format <format>', 'Theme format (tailwind, emotion, styled-components, vanilla-extract, css-variables, all)', 'all')
|
|
223
|
+
.option('--prefix <prefix>', 'CSS variable prefix', 'atomix')
|
|
224
|
+
.option('--selector <selector>', 'CSS selector for variables', ':root')
|
|
225
|
+
.option('--no-typescript', 'Skip TypeScript type generation')
|
|
226
|
+
.option('--validate', 'Validate generated theme files')
|
|
227
|
+
.option('--dry-run', 'Show what would be generated without writing files')
|
|
228
|
+
.action(async (source, options) => {
|
|
1767
229
|
try {
|
|
1768
|
-
|
|
1769
|
-
system: [],
|
|
1770
|
-
dependencies: [],
|
|
1771
|
-
structure: [],
|
|
1772
|
-
framework: [],
|
|
1773
|
-
theme: []
|
|
1774
|
-
};
|
|
1775
|
-
|
|
1776
|
-
// 1. System Checks
|
|
1777
|
-
const nodeVersion = checkNodeVersion('18.0.0');
|
|
1778
|
-
results.system.push({
|
|
1779
|
-
name: 'Node.js',
|
|
1780
|
-
status: nodeVersion.compatible ? '✅' : '❌',
|
|
1781
|
-
message: nodeVersion.compatible
|
|
1782
|
-
? `v${nodeVersion.current} (supported)`
|
|
1783
|
-
: `v${nodeVersion.current} (requires Node ${nodeVersion.required}+)`,
|
|
1784
|
-
});
|
|
1785
|
-
|
|
1786
|
-
// 2. Dependency Checks
|
|
1787
|
-
const atomixPath = join(process.cwd(), 'node_modules', '@shohojdhara', 'atomix');
|
|
1788
|
-
results.dependencies.push({
|
|
1789
|
-
name: 'Atomix Package',
|
|
1790
|
-
status: existsSync(atomixPath) ? '✅' : '❌',
|
|
1791
|
-
message: existsSync(atomixPath)
|
|
1792
|
-
? 'Installed correctly'
|
|
1793
|
-
: 'Not found - run: npm install @shohojdhara/atomix',
|
|
1794
|
-
});
|
|
1795
|
-
|
|
1796
|
-
const peerDeps = [
|
|
1797
|
-
{ name: 'react', required: '>= 18.0.0' },
|
|
1798
|
-
{ name: 'react-dom', required: '>= 18.0.0' },
|
|
1799
|
-
{ name: 'sass', required: 'installed' }
|
|
1800
|
-
];
|
|
1801
|
-
|
|
1802
|
-
for (const dep of peerDeps) {
|
|
1803
|
-
const depPath = join(process.cwd(), 'node_modules', dep.name);
|
|
1804
|
-
const exists = existsSync(depPath);
|
|
1805
|
-
results.dependencies.push({
|
|
1806
|
-
name: dep.name,
|
|
1807
|
-
status: exists ? '✅' : '⚠️',
|
|
1808
|
-
message: exists ? 'Installed' : `Missing (required: ${dep.required})`,
|
|
1809
|
-
});
|
|
1810
|
-
}
|
|
1811
|
-
|
|
1812
|
-
// 3. Project Structure Checks
|
|
1813
|
-
const structureChecks = validateProjectStructure();
|
|
1814
|
-
|
|
1815
|
-
structureChecks.forEach(check => {
|
|
1816
|
-
let statusIcon = '✅';
|
|
1817
|
-
if (!check.valid && check.required) statusIcon = '❌';
|
|
1818
|
-
else if (!check.message.includes('exists') && !check.required) statusIcon = '⚠️';
|
|
1819
|
-
|
|
1820
|
-
results.structure.push({
|
|
1821
|
-
name: check.name,
|
|
1822
|
-
status: statusIcon,
|
|
1823
|
-
message: check.message.replace(/✓ |✗ |⚠ /, '')
|
|
1824
|
-
});
|
|
1825
|
-
});
|
|
1826
|
-
|
|
1827
|
-
// 4. Framework & Config Checks
|
|
1828
|
-
const frameworkChecks = validateFrameworkConfig();
|
|
1829
|
-
|
|
1830
|
-
frameworkChecks.forEach(check => {
|
|
1831
|
-
results.framework.push({
|
|
1832
|
-
name: check.name,
|
|
1833
|
-
status: check.valid ? '✅' : '💡',
|
|
1834
|
-
message: check.message.replace(/✓ |• /, '')
|
|
1835
|
-
});
|
|
1836
|
-
});
|
|
1837
|
-
|
|
1838
|
-
const configFiles = ['.atomixrc', 'atomix.config.js', 'atomix.config.json', 'theme.config.ts', 'atomix.config.ts'];
|
|
1839
|
-
let foundConfig = null;
|
|
1840
|
-
for (const file of configFiles) {
|
|
1841
|
-
if (existsSync(join(process.cwd(), file))) {
|
|
1842
|
-
foundConfig = file;
|
|
1843
|
-
break;
|
|
1844
|
-
}
|
|
1845
|
-
}
|
|
1846
|
-
results.framework.push({
|
|
1847
|
-
name: 'Atomix Config',
|
|
1848
|
-
status: foundConfig ? '✅' : '💡',
|
|
1849
|
-
message: foundConfig ? `Found (${foundConfig})` : 'No config file (using defaults)',
|
|
1850
|
-
});
|
|
1851
|
-
|
|
1852
|
-
// 5. Theme CLI Checks
|
|
1853
|
-
const themeCLIAvailable = await import('./cli/theme-bridge.js')
|
|
1854
|
-
.then(m => m.isThemeCLIAvailable())
|
|
1855
|
-
.catch(() => false);
|
|
1856
|
-
|
|
1857
|
-
results.theme.push({
|
|
1858
|
-
name: 'Theme Devtools',
|
|
1859
|
-
status: themeCLIAvailable ? '✅' : '⚠️',
|
|
1860
|
-
message: themeCLIAvailable ? 'Available' : 'Theme devtools not found',
|
|
1861
|
-
});
|
|
1862
|
-
|
|
1863
|
-
spinner.stop();
|
|
1864
|
-
|
|
1865
|
-
// Display results
|
|
1866
|
-
console.log(chalk.bold('\n🏥 Atomix Doctor / Audit Report\n'));
|
|
1867
|
-
|
|
1868
|
-
const categories = [
|
|
1869
|
-
{ key: 'system', label: '💻 System environment' },
|
|
1870
|
-
{ key: 'dependencies', label: '📦 Core dependencies' },
|
|
1871
|
-
{ key: 'structure', label: '🏗️ Project structure' },
|
|
1872
|
-
{ key: 'framework', label: '⚙️ Configuration & Framework' },
|
|
1873
|
-
{ key: 'theme', label: '🎨 Design System tools' }
|
|
1874
|
-
];
|
|
1875
|
-
|
|
1876
|
-
categories.forEach(cat => {
|
|
1877
|
-
console.log(chalk.cyan.bold(`\n${cat.label}`));
|
|
1878
|
-
console.log(chalk.gray('-'.repeat(40)));
|
|
1879
|
-
results[cat.key].forEach(check => {
|
|
1880
|
-
console.log(` ${check.status} ${chalk.bold(check.name.padEnd(20))} ${chalk.gray(check.message)}`);
|
|
1881
|
-
});
|
|
1882
|
-
});
|
|
1883
|
-
|
|
1884
|
-
const hasIssues = Object.values(results).flat().some(c => c.status === '❌');
|
|
1885
|
-
const hasWarnings = Object.values(results).flat().some(c => c.status === '⚠️');
|
|
1886
|
-
|
|
1887
|
-
console.log('\n' + chalk.gray('='.repeat(60)));
|
|
1888
|
-
if (hasIssues) {
|
|
1889
|
-
console.log(chalk.red.bold('\n❌ Some critical issues need attention.'));
|
|
1890
|
-
console.log(chalk.gray('Check the report above and follow the suggested fixes.'));
|
|
1891
|
-
} else if (hasWarnings) {
|
|
1892
|
-
console.log(chalk.yellow.bold('\n⚠️ Your setup is functional but could be improved.'));
|
|
1893
|
-
console.log(chalk.gray('Recommended directories or scripts are missing.'));
|
|
1894
|
-
} else {
|
|
1895
|
-
console.log(chalk.green.bold('\n✨ Everything looks great! Your project is Atomix-ready.'));
|
|
1896
|
-
}
|
|
1897
|
-
console.log(chalk.gray('='.repeat(60)) + '\n');
|
|
1898
|
-
|
|
230
|
+
await themeBridgeAction(source, options);
|
|
1899
231
|
} catch (error) {
|
|
1900
|
-
|
|
232
|
+
await handleCLIError(error);
|
|
1901
233
|
}
|
|
1902
234
|
});
|
|
1903
235
|
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
const command = args[0];
|
|
1909
|
-
|
|
1910
|
-
// Skip dependency check for doctor command or help
|
|
1911
|
-
if (command !== 'doctor' && command !== '--help' && command !== '-h' && !args.includes('--help') && !args.includes('-h') && !process.env.ATOMIX_SKIP_DEP_CHECK) {
|
|
1912
|
-
try {
|
|
1913
|
-
const depResult = await checkDependencies();
|
|
1914
|
-
if (!depResult.success) {
|
|
1915
|
-
console.log(chalk.yellow('\n⚠️ Some dependencies are missing. Run `atomix doctor` for detailed information.'));
|
|
1916
|
-
// Don't exit - allow users to see help or run doctor
|
|
1917
|
-
}
|
|
1918
|
-
} catch (error) {
|
|
1919
|
-
// Silently continue if dependency check fails
|
|
1920
|
-
debug('Dependency check failed:', error.message);
|
|
1921
|
-
}
|
|
1922
|
-
}
|
|
1923
|
-
|
|
1924
|
-
// Parse arguments
|
|
1925
|
-
program.parse(process.argv);
|
|
1926
|
-
|
|
1927
|
-
// Show help if no command provided
|
|
1928
|
-
if (!process.argv.slice(2).length) {
|
|
1929
|
-
program.outputHelp();
|
|
1930
|
-
}
|
|
236
|
+
// Run program
|
|
237
|
+
program.parseAsync(process.argv).catch(async (error) => {
|
|
238
|
+
await handleCLIError(error);
|
|
239
|
+
});
|