@shohojdhara/atomix 0.4.7 → 0.4.8
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/dist/atomix.css +24 -37
- 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.js +51 -46
- package/dist/charts.js.map +1 -1
- package/dist/core.js +51 -46
- package/dist/core.js.map +1 -1
- package/dist/forms.js +51 -46
- package/dist/forms.js.map +1 -1
- package/dist/heavy.js +51 -46
- package/dist/heavy.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.esm.js +51 -46
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +51 -46
- 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 +1 -1
- package/scripts/atomix-cli.js +40 -1875
- package/scripts/cli/commands/build-theme.js +112 -0
- package/scripts/cli/commands/generate.js +97 -0
- package/scripts/cli/commands/init.js +46 -0
- package/scripts/cli/internal/compiler.js +114 -0
- package/scripts/cli/internal/filesystem.js +58 -0
- package/scripts/cli/internal/generator.js +110 -0
- package/scripts/cli/internal/wizard.js +61 -0
- package/scripts/cli/utils/error.js +47 -0
- package/scripts/cli/utils/helpers.js +43 -0
- package/scripts/cli/utils/logger.js +75 -0
- package/scripts/cli/utils/validation.js +71 -0
- package/src/components/AtomixGlass/AtomixGlass.test.tsx +37 -3
- package/src/components/AtomixGlass/AtomixGlass.tsx +41 -29
- package/src/components/AtomixGlass/AtomixGlassContainer.tsx +4 -19
- package/src/components/AtomixGlass/__snapshots__/AtomixGlass.test.tsx.snap +216 -0
- package/src/lib/composables/useAtomixGlass.ts +4 -1
- package/src/lib/composables/useAtomixGlassStyles.ts +9 -7
- package/src/lib/constants/components.ts +7 -7
- 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.tooltip.scss +9 -5
- package/scripts/cli/component-generator.js +0 -564
- package/scripts/cli/interactive-init.js +0 -357
- package/scripts/cli/utils.js +0 -359
package/scripts/atomix-cli.js
CHANGED
|
@@ -1,71 +1,21 @@
|
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
import
|
|
17
|
-
import
|
|
18
|
-
import
|
|
19
|
-
import chokidar from 'chokidar';
|
|
20
|
-
import inquirer from 'inquirer';
|
|
21
|
-
import boxen from 'boxen';
|
|
22
|
-
import { runInitWizard } from './cli/interactive-init.js';
|
|
23
|
-
import {
|
|
24
|
-
checkDependencies,
|
|
25
|
-
validateProjectStructure,
|
|
26
|
-
validateFrameworkConfig
|
|
27
|
-
} from './cli/dependency-checker.js';
|
|
28
|
-
import {
|
|
29
|
-
migrateTailwind,
|
|
30
|
-
migrateBootstrap,
|
|
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';
|
|
12
|
+
import { logger } from './cli/utils/logger.js';
|
|
13
|
+
import { handleCLIError } from './cli/utils/error.js';
|
|
14
|
+
|
|
15
|
+
// Action Modules
|
|
16
|
+
import { initAction } from './cli/commands/init.js';
|
|
17
|
+
import { generateAction } from './cli/commands/generate.js';
|
|
18
|
+
import { buildThemeAction } from './cli/commands/build-theme.js';
|
|
69
19
|
|
|
70
20
|
const __filename = fileURLToPath(import.meta.url);
|
|
71
21
|
const __dirname = dirname(__filename);
|
|
@@ -75,1856 +25,71 @@ const packageJson = JSON.parse(
|
|
|
75
25
|
await readFile(join(__dirname, '../package.json'), 'utf8')
|
|
76
26
|
);
|
|
77
27
|
|
|
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
28
|
program
|
|
162
29
|
.name('atomix')
|
|
163
|
-
.description('Atomix Design System CLI -
|
|
30
|
+
.description('Atomix Design System CLI - Modular Edition')
|
|
164
31
|
.version(packageJson.version)
|
|
165
32
|
.option('-d, --debug', 'Enable debug mode', false)
|
|
166
33
|
.hook('preAction', (thisCommand) => {
|
|
167
34
|
if (thisCommand.opts().debug) {
|
|
168
35
|
process.env.ATOMIX_DEBUG = 'true';
|
|
36
|
+
logger.debug('Debug mode enabled');
|
|
169
37
|
}
|
|
170
38
|
});
|
|
171
39
|
|
|
172
40
|
/**
|
|
173
|
-
*
|
|
41
|
+
* Project Initialization
|
|
174
42
|
*/
|
|
175
43
|
program
|
|
176
|
-
.command('
|
|
177
|
-
.description('
|
|
178
|
-
.
|
|
179
|
-
.option('-m, --minify', 'Generate minified version', true)
|
|
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
|
-
|
|
44
|
+
.command('init')
|
|
45
|
+
.description('Initialize a new Atomix design system project')
|
|
46
|
+
.action(async (options) => {
|
|
186
47
|
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
|
-
|
|
48
|
+
await initAction(options);
|
|
365
49
|
} catch (error) {
|
|
366
|
-
|
|
50
|
+
await handleCLIError(error);
|
|
367
51
|
}
|
|
368
52
|
});
|
|
369
53
|
|
|
370
54
|
/**
|
|
371
|
-
*
|
|
55
|
+
* Resource Generation
|
|
372
56
|
*/
|
|
373
57
|
program
|
|
374
58
|
.command('generate <type> <name>')
|
|
375
59
|
.alias('g')
|
|
376
|
-
.description('Generate
|
|
377
|
-
.option('-
|
|
378
|
-
.option('-
|
|
379
|
-
.option('--
|
|
380
|
-
.option('--
|
|
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)
|
|
60
|
+
.description('Generate components, tokens, or themes')
|
|
61
|
+
.option('-i, --interactive', 'Interactive mode', false)
|
|
62
|
+
.option('-p, --path <path>', 'Output path', './src/components')
|
|
63
|
+
.option('--complexity <level>', 'Complexity (simple|medium|complex)', 'medium')
|
|
64
|
+
.option('--validate', 'Validate after generation', true)
|
|
386
65
|
.action(async (type, name, options) => {
|
|
387
|
-
const spinner = ora(`Generating ${type}: ${name}...`).start();
|
|
388
|
-
|
|
389
|
-
try {
|
|
390
|
-
debug(`Generating ${type} with name: ${name}`, options);
|
|
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
|
-
|
|
862
|
-
} catch (error) {
|
|
863
|
-
handleError(error, spinner);
|
|
864
|
-
}
|
|
865
|
-
});
|
|
866
|
-
|
|
867
|
-
/**
|
|
868
|
-
* Validate Command - NEW
|
|
869
|
-
*/
|
|
870
|
-
program
|
|
871
|
-
.command('validate [target]')
|
|
872
|
-
.description('Validate themes, design tokens, components, or accessibility')
|
|
873
|
-
.option('--tokens', 'Validate design tokens', false)
|
|
874
|
-
.option('--theme <path>', 'Validate specific theme', '')
|
|
875
|
-
.option('--component <name>', 'Validate specific component', '')
|
|
876
|
-
.option('--a11y, --accessibility', 'Check accessibility compliance', false)
|
|
877
|
-
.option('--fix', 'Attempt to fix issues automatically', false)
|
|
878
|
-
.action(async (target, options) => {
|
|
879
|
-
const spinner = ora('Running validation...').start();
|
|
880
|
-
|
|
881
66
|
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
|
-
}
|
|
67
|
+
await generateAction(type, name, options);
|
|
1073
68
|
} catch (error) {
|
|
1074
|
-
|
|
69
|
+
await handleCLIError(error);
|
|
1075
70
|
}
|
|
1076
71
|
});
|
|
1077
72
|
|
|
1078
73
|
/**
|
|
1079
|
-
*
|
|
74
|
+
* Theme Building
|
|
1080
75
|
*/
|
|
1081
76
|
program
|
|
1082
|
-
.command('
|
|
1083
|
-
.description('
|
|
77
|
+
.command('build-theme <path>')
|
|
78
|
+
.description('Build a custom theme from SCSS')
|
|
1084
79
|
.option('-o, --output <path>', 'Output directory', './dist')
|
|
1085
|
-
.option('--
|
|
1086
|
-
.
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
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)
|
|
1114
|
-
*/
|
|
1115
|
-
program
|
|
1116
|
-
.command('migrate <from>')
|
|
1117
|
-
.description('Migrate from other CSS frameworks to Atomix design system')
|
|
1118
|
-
.option('-s, --source <path>', 'Source directory to migrate', './src')
|
|
1119
|
-
.option('--dry-run', 'Preview changes without modifying files', false)
|
|
1120
|
-
.option('--create-backup', 'Create backup before migration', true)
|
|
1121
|
-
.action(async (from, options) => {
|
|
1122
|
-
const spinner = ora('Preparing migration...').start();
|
|
1123
|
-
|
|
1124
|
-
try {
|
|
1125
|
-
debug(`Migrating from ${from}`, options);
|
|
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
|
-
|
|
1244
|
-
} catch (error) {
|
|
1245
|
-
handleError(error, spinner);
|
|
1246
|
-
}
|
|
1247
|
-
});
|
|
1248
|
-
|
|
1249
|
-
/**
|
|
1250
|
-
* Init Command - NEW (Interactive Setup Wizard)
|
|
1251
|
-
*/
|
|
1252
|
-
program
|
|
1253
|
-
.command('init')
|
|
1254
|
-
.description('Interactive setup wizard for Atomix design system')
|
|
1255
|
-
.option('--skip-install', 'Skip dependency installation', false)
|
|
1256
|
-
.action(async (options) => {
|
|
1257
|
-
try {
|
|
1258
|
-
// Set environment variable for skip install if needed
|
|
1259
|
-
if (options.skipInstall) {
|
|
1260
|
-
process.env.ATOMIX_SKIP_INSTALL = 'true';
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
// Run the interactive wizard
|
|
1264
|
-
await runInitWizard();
|
|
1265
|
-
} catch (error) {
|
|
1266
|
-
handleError(error);
|
|
1267
|
-
}
|
|
1268
|
-
});
|
|
1269
|
-
|
|
1270
|
-
/**
|
|
1271
|
-
* Tokens Command - NEW (Design Token Management)
|
|
1272
|
-
*/
|
|
1273
|
-
program
|
|
1274
|
-
.command('tokens <action>')
|
|
1275
|
-
.description('Manage design tokens (list, validate, export, import)')
|
|
1276
|
-
.option('-c, --category <category>', 'Token category (colors, typography, spacing, etc.)')
|
|
1277
|
-
.option('-f, --format <format>', 'Export format (json, css, scss, js, ts)', 'json')
|
|
1278
|
-
.option('-o, --output <path>', 'Output file path')
|
|
1279
|
-
.option('--dry-run', 'Preview changes without modifying files', false)
|
|
1280
|
-
.action(async (action, options) => {
|
|
1281
|
-
try {
|
|
1282
|
-
debug(`Token action: ${action}`, options);
|
|
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
|
-
|
|
1368
|
-
} catch (error) {
|
|
1369
|
-
handleError(error);
|
|
1370
|
-
}
|
|
1371
|
-
});
|
|
1372
|
-
|
|
1373
|
-
/**
|
|
1374
|
-
* Theme Command Group - NEW (Integrated with Theme Devtools)
|
|
1375
|
-
*/
|
|
1376
|
-
const themeCommand = program
|
|
1377
|
-
.command('theme')
|
|
1378
|
-
.description('Theme management commands');
|
|
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')
|
|
1386
|
-
.action(async (options) => {
|
|
1387
|
-
try {
|
|
1388
|
-
const themeCLI = createThemeCLIBridge();
|
|
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);
|
|
1431
|
-
} catch (error) {
|
|
1432
|
-
handleError(error);
|
|
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) => {
|
|
80
|
+
.option('-m, --minify', 'Minify CSS', true)
|
|
81
|
+
.option('-s, --sourcemap', 'Generate source maps', false)
|
|
82
|
+
.option('-w, --watch', 'Watch mode', false)
|
|
83
|
+
.option('--analyze', 'Analyze bundle', false)
|
|
84
|
+
.action(async (themePath, options) => {
|
|
1442
85
|
try {
|
|
1443
|
-
|
|
1444
|
-
await themeCLI.export(name, options);
|
|
86
|
+
await buildThemeAction(themePath, options);
|
|
1445
87
|
} catch (error) {
|
|
1446
|
-
|
|
88
|
+
await handleCLIError(error);
|
|
1447
89
|
}
|
|
1448
90
|
});
|
|
1449
91
|
|
|
1450
|
-
//
|
|
1451
|
-
|
|
1452
|
-
|
|
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
|
-
},
|
|
92
|
+
// Run program
|
|
93
|
+
program.parseAsync(process.argv).catch(async (error) => {
|
|
94
|
+
await handleCLIError(error);
|
|
1624
95
|
});
|
|
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);
|
|
1699
|
-
}
|
|
1700
|
-
});
|
|
1701
|
-
|
|
1702
|
-
/**
|
|
1703
|
-
* Docs Command Group - NEW (Documentation Management)
|
|
1704
|
-
*/
|
|
1705
|
-
const docsCommand = program
|
|
1706
|
-
.command('docs')
|
|
1707
|
-
.description('Documentation management commands');
|
|
1708
|
-
|
|
1709
|
-
// Docs sync
|
|
1710
|
-
docsCommand
|
|
1711
|
-
.command('sync')
|
|
1712
|
-
.description('Sync documentation with component guidelines')
|
|
1713
|
-
.option('--validate', 'Validate documentation after sync', true)
|
|
1714
|
-
.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
|
-
try {
|
|
1735
|
-
await validateDocumentation();
|
|
1736
|
-
} catch (error) {
|
|
1737
|
-
handleError(error);
|
|
1738
|
-
}
|
|
1739
|
-
});
|
|
1740
|
-
|
|
1741
|
-
// Docs generate CLI
|
|
1742
|
-
docsCommand
|
|
1743
|
-
.command('generate-cli')
|
|
1744
|
-
.description('Generate CLI documentation from commands')
|
|
1745
|
-
.action(async () => {
|
|
1746
|
-
const spinner = ora('Generating CLI documentation...').start();
|
|
1747
|
-
|
|
1748
|
-
try {
|
|
1749
|
-
const result = await generateCLIDocumentation();
|
|
1750
|
-
spinner.succeed(chalk.green('CLI documentation generated'));
|
|
1751
|
-
console.log(chalk.gray(` → ${result.file}`));
|
|
1752
|
-
} catch (error) {
|
|
1753
|
-
handleError(error, spinner);
|
|
1754
|
-
}
|
|
1755
|
-
});
|
|
1756
|
-
|
|
1757
|
-
/**
|
|
1758
|
-
* Doctor Command - NEW
|
|
1759
|
-
*/
|
|
1760
|
-
program
|
|
1761
|
-
.command('doctor')
|
|
1762
|
-
.alias('audit')
|
|
1763
|
-
.description('Diagnose common issues with your Atomix setup')
|
|
1764
|
-
.action(async () => {
|
|
1765
|
-
const spinner = ora('Running diagnostics...').start();
|
|
1766
|
-
|
|
1767
|
-
try {
|
|
1768
|
-
const results = {
|
|
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
|
-
|
|
1899
|
-
} catch (error) {
|
|
1900
|
-
handleError(error, spinner);
|
|
1901
|
-
}
|
|
1902
|
-
});
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
// Check dependencies before execution (except for doctor command)
|
|
1907
|
-
const args = process.argv.slice(2);
|
|
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
|
-
}
|