@shohojdhara/atomix 0.4.8 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/atomix.config.ts +58 -1
  2. package/dist/atomix.css +148 -120
  3. package/dist/atomix.css.map +1 -1
  4. package/dist/atomix.min.css +1 -1
  5. package/dist/atomix.min.css.map +1 -1
  6. package/dist/charts.d.ts +33 -0
  7. package/dist/charts.js +1227 -122
  8. package/dist/charts.js.map +1 -1
  9. package/dist/core.d.ts +33 -10
  10. package/dist/core.js +1052 -41
  11. package/dist/core.js.map +1 -1
  12. package/dist/forms.d.ts +33 -0
  13. package/dist/forms.js +2086 -1035
  14. package/dist/forms.js.map +1 -1
  15. package/dist/heavy.d.ts +42 -1
  16. package/dist/heavy.js +1620 -600
  17. package/dist/heavy.js.map +1 -1
  18. package/dist/index.d.ts +441 -270
  19. package/dist/index.esm.js +1900 -638
  20. package/dist/index.esm.js.map +1 -1
  21. package/dist/index.js +1935 -670
  22. package/dist/index.js.map +1 -1
  23. package/dist/index.min.js +1 -1
  24. package/dist/index.min.js.map +1 -1
  25. package/package.json +6 -3
  26. package/scripts/atomix-cli.js +148 -4
  27. package/scripts/cli/__tests__/basic.test.js +3 -2
  28. package/scripts/cli/__tests__/clean.test.js +278 -0
  29. package/scripts/cli/__tests__/component-validator.test.js +433 -0
  30. package/scripts/cli/__tests__/generator.test.js +613 -0
  31. package/scripts/cli/__tests__/glass-motion.test.js +256 -0
  32. package/scripts/cli/__tests__/integration.test.js +719 -108
  33. package/scripts/cli/__tests__/migrate.test.js +74 -0
  34. package/scripts/cli/__tests__/security.test.js +206 -0
  35. package/scripts/cli/__tests__/test-setup.js +3 -1
  36. package/scripts/cli/__tests__/theme-bridge.test.js +507 -0
  37. package/scripts/cli/__tests__/token-provider.test.js +361 -0
  38. package/scripts/cli/__tests__/utils.test.js +5 -5
  39. package/scripts/cli/commands/benchmark.js +105 -0
  40. package/scripts/cli/commands/build-theme.js +4 -1
  41. package/scripts/cli/commands/clean.js +109 -0
  42. package/scripts/cli/commands/doctor.js +88 -0
  43. package/scripts/cli/commands/generate.js +135 -14
  44. package/scripts/cli/commands/init.js +45 -18
  45. package/scripts/cli/commands/migrate.js +106 -0
  46. package/scripts/cli/commands/sync-tokens.js +206 -0
  47. package/scripts/cli/commands/theme-bridge.js +248 -0
  48. package/scripts/cli/commands/tokens.js +157 -0
  49. package/scripts/cli/commands/validate.js +194 -0
  50. package/scripts/cli/internal/ai-engine.js +156 -0
  51. package/scripts/cli/internal/component-validator.js +443 -0
  52. package/scripts/cli/internal/config-loader.js +162 -0
  53. package/scripts/cli/internal/filesystem.js +102 -2
  54. package/scripts/cli/internal/generator.js +359 -39
  55. package/scripts/cli/internal/glass-generator.js +398 -0
  56. package/scripts/cli/internal/hook-generator.js +369 -0
  57. package/scripts/cli/internal/hooks.js +61 -0
  58. package/scripts/cli/internal/itcss-generator.js +565 -0
  59. package/scripts/cli/internal/motion-generator.js +679 -0
  60. package/scripts/cli/internal/template-engine.js +301 -0
  61. package/scripts/cli/internal/theme-bridge.js +664 -0
  62. package/scripts/cli/internal/tokens/engine.js +122 -0
  63. package/scripts/cli/internal/tokens/provider.js +34 -0
  64. package/scripts/cli/internal/tokens/providers/figma.js +50 -0
  65. package/scripts/cli/internal/tokens/providers/style-dictionary.js +48 -0
  66. package/scripts/cli/internal/tokens/providers/w3c.js +48 -0
  67. package/scripts/cli/internal/tokens/token-provider.js +443 -0
  68. package/scripts/cli/internal/tokens/token-validator.js +513 -0
  69. package/scripts/cli/internal/validator.js +276 -0
  70. package/scripts/cli/internal/wizard.js +60 -6
  71. package/scripts/cli/mappings.js +23 -0
  72. package/scripts/cli/migration-tools.js +164 -94
  73. package/scripts/cli/plugins/style-dictionary.js +46 -0
  74. package/scripts/cli/templates/README.md +525 -95
  75. package/scripts/cli/templates/common-templates.js +40 -14
  76. package/scripts/cli/templates/components/react-component.ts +282 -0
  77. package/scripts/cli/templates/config/project-config.ts +112 -0
  78. package/scripts/cli/templates/hooks/use-component.ts +477 -0
  79. package/scripts/cli/templates/index.js +19 -4
  80. package/scripts/cli/templates/index.ts +171 -0
  81. package/scripts/cli/templates/next-templates.js +72 -0
  82. package/scripts/cli/templates/react-templates.js +70 -126
  83. package/scripts/cli/templates/scss-templates.js +35 -35
  84. package/scripts/cli/templates/stories/storybook-story.ts +241 -0
  85. package/scripts/cli/templates/styles/scss-component.ts +255 -0
  86. package/scripts/cli/templates/tests/vitest-test.ts +229 -0
  87. package/scripts/cli/templates/token-templates.js +337 -1
  88. package/scripts/cli/templates/tokens/token-generators.ts +1088 -0
  89. package/scripts/cli/templates/types/component-types.ts +145 -0
  90. package/scripts/cli/templates/utils/testing-utils.ts +144 -0
  91. package/scripts/cli/templates/vanilla-templates.js +39 -0
  92. package/scripts/cli/token-manager.js +8 -2
  93. package/scripts/cli/utils/cache-manager.js +240 -0
  94. package/scripts/cli/utils/detector.js +46 -0
  95. package/scripts/cli/utils/diagnostics.js +289 -0
  96. package/scripts/cli/utils/error.js +45 -3
  97. package/scripts/cli/utils/helpers.js +24 -0
  98. package/scripts/cli/utils/logger.js +1 -1
  99. package/scripts/cli/utils/security.js +302 -0
  100. package/scripts/cli/utils/telemetry.js +115 -0
  101. package/scripts/cli/utils/validation.js +4 -38
  102. package/scripts/cli/utils.js +46 -0
  103. package/src/components/Accordion/Accordion.stories.tsx +0 -18
  104. package/src/components/Accordion/Accordion.test.tsx +0 -17
  105. package/src/components/Accordion/Accordion.tsx +0 -4
  106. package/src/components/AtomixGlass/AtomixGlass.tsx +102 -2
  107. package/src/components/AtomixGlass/AtomixGlassContainer.tsx +125 -12
  108. package/src/components/AtomixGlass/PerformanceDashboard.tsx +219 -0
  109. package/src/components/AtomixGlass/README.md +25 -10
  110. package/src/components/AtomixGlass/animation-system.ts +578 -0
  111. package/src/components/AtomixGlass/shader-utils.ts +3 -0
  112. package/src/components/AtomixGlass/stories/AnimationFeatures.stories.tsx +653 -0
  113. package/src/components/AtomixGlass/stories/AnimationTests.stories.tsx +95 -0
  114. package/src/components/AtomixGlass/stories/CardExamples.stories.tsx +212 -0
  115. package/src/components/AtomixGlass/stories/DashboardExamples.stories.tsx +348 -0
  116. package/src/components/AtomixGlass/stories/EcommerceExamples.stories.tsx +410 -0
  117. package/src/components/AtomixGlass/stories/FormExamples.stories.tsx +436 -0
  118. package/src/components/AtomixGlass/stories/HeroExamples.stories.tsx +264 -0
  119. package/src/components/AtomixGlass/stories/InteractivePlayground.stories.tsx +247 -0
  120. package/src/components/AtomixGlass/stories/MobileUIExamples.stories.tsx +418 -0
  121. package/src/components/AtomixGlass/stories/ModalExamples.stories.tsx +402 -0
  122. package/src/components/AtomixGlass/stories/Overview.stories.tsx +157 -6
  123. package/src/components/AtomixGlass/stories/Playground.stories.tsx +658 -93
  124. package/src/components/AtomixGlass/stories/PresetGallery.stories.tsx +335 -0
  125. package/src/components/AtomixGlass/stories/WidgetExamples.stories.tsx +441 -0
  126. package/src/components/AtomixGlass/stories/argTypes.ts +384 -0
  127. package/src/components/AtomixGlass/stories/shared-components.tsx +91 -1
  128. package/src/components/AtomixGlass/stories/types.ts +127 -0
  129. package/src/components/Avatar/Avatar.tsx +1 -1
  130. package/src/components/Button/Button.stories.disabled-link.tsx +10 -0
  131. package/src/components/Button/Button.stories.tsx +10 -0
  132. package/src/components/Button/Button.test.tsx +16 -11
  133. package/src/components/Button/Button.tsx +4 -4
  134. package/src/components/Card/Card.tsx +1 -1
  135. package/src/components/Dropdown/Dropdown.tsx +12 -12
  136. package/src/components/Form/Select.tsx +62 -3
  137. package/src/components/Modal/Modal.tsx +14 -3
  138. package/src/components/Navigation/Navbar/Navbar.tsx +44 -0
  139. package/src/components/Slider/Slider.stories.tsx +3 -3
  140. package/src/components/Slider/Slider.tsx +38 -0
  141. package/src/components/Steps/Steps.tsx +3 -3
  142. package/src/components/Tabs/Tabs.tsx +77 -8
  143. package/src/components/Testimonial/Testimonial.tsx +1 -1
  144. package/src/components/TypedButton/TypedButton.stories.tsx +59 -0
  145. package/src/components/TypedButton/TypedButton.tsx +39 -0
  146. package/src/components/TypedButton/index.ts +2 -0
  147. package/src/components/VideoPlayer/VideoPlayer.tsx +11 -4
  148. package/src/lib/composables/index.ts +4 -7
  149. package/src/lib/composables/types.ts +45 -0
  150. package/src/lib/composables/useAccordion.ts +0 -7
  151. package/src/lib/composables/useAtomixGlass.ts +144 -5
  152. package/src/lib/composables/useChartExport.ts +3 -13
  153. package/src/lib/composables/useDropdown.ts +66 -0
  154. package/src/lib/composables/useFocusTrap.ts +80 -0
  155. package/src/lib/composables/usePerformanceMonitor.ts +448 -0
  156. package/src/lib/composables/useResponsiveGlass.presets.ts +192 -0
  157. package/src/lib/composables/useResponsiveGlass.ts +441 -0
  158. package/src/lib/composables/useTooltip.ts +16 -0
  159. package/src/lib/composables/useTypedButton.ts +66 -0
  160. package/src/lib/config/index.ts +62 -5
  161. package/src/lib/constants/components.ts +55 -0
  162. package/src/lib/theme/devtools/__tests__/useHistory.test.tsx +150 -0
  163. package/src/lib/theme/tokens/centralized-tokens.ts +120 -0
  164. package/src/lib/theme/utils/__tests__/domUtils.test.ts +101 -0
  165. package/src/lib/types/components.ts +37 -11
  166. package/src/lib/types/glass.ts +35 -0
  167. package/src/lib/types/index.ts +1 -0
  168. package/src/lib/utils/displacement-generator.ts +1 -1
  169. package/src/styles/01-settings/_settings.testtypecheck.scss +53 -0
  170. package/src/styles/01-settings/_settings.typedbutton.scss +53 -0
  171. package/src/styles/06-components/_components.testbutton.scss +212 -0
  172. package/src/styles/06-components/_components.testtypecheck.scss +212 -0
  173. package/src/styles/06-components/_components.typedbutton.scss +212 -0
  174. package/src/styles/99-utilities/_index.scss +1 -0
  175. package/src/styles/99-utilities/_utilities.text.scss +1 -1
  176. package/src/styles/99-utilities/_utilities.touch-target.scss +36 -0
  177. package/src/styles/06-components/old.chart.styles.scss +0 -2788
@@ -0,0 +1,302 @@
1
+ /**
2
+ * Atomix CLI Security Utilities
3
+ * Input sanitization and security-focused validation functions
4
+ */
5
+
6
+ import { normalize, resolve, relative, isAbsolute } from 'path';
7
+ import { logger } from './logger.js';
8
+ import chalk from 'chalk';
9
+
10
+ export const SecurityError = {
11
+ PATH_TRAVERSAL: 'PATH_TRAVERSAL',
12
+ INVALID_INPUT: 'INVALID_INPUT',
13
+ MALICIOUS_CONTENT: 'MALICIOUS_CONTENT',
14
+ RATE_LIMIT_EXCEEDED: 'RATE_LIMIT_EXCEEDED'
15
+ };
16
+
17
+ /**
18
+ * Sanitizes user input to prevent injection attacks
19
+ * @param {string} input - The user input to sanitize
20
+ * @param {string} type - The type of input (filename, componentName, path, etc.)
21
+ * @returns {string} - Sanitized input
22
+ */
23
+ export function sanitizeInput(input, type = 'generic') {
24
+ if (typeof input !== 'string') {
25
+ throw new Error(`Input must be a string, received: ${typeof input}`);
26
+ }
27
+
28
+ // Remove null bytes and control characters
29
+ let sanitized = input.replace(/[\x00-\x1F\x7F]/g, '');
30
+
31
+ switch (type) {
32
+ case 'filename':
33
+ // Remove path traversal attempts and special characters
34
+ sanitized = sanitized.replace(/\.\.\//g, '')
35
+ .replace(/[<>:"|?*]/g, '')
36
+ .replace(/\/+/g, '')
37
+ .trim();
38
+ break;
39
+
40
+ case 'componentName':
41
+ // Enforce PascalCase and remove non-alphanumeric characters
42
+ sanitized = sanitized.replace(/[^a-zA-Z0-9]/g, '');
43
+ if (sanitized.length > 0) {
44
+ sanitized = sanitized.charAt(0).toUpperCase() + sanitized.slice(1);
45
+ }
46
+ break;
47
+
48
+ case 'path':
49
+ // Basic path sanitization - more comprehensive validation happens in validatePath
50
+ sanitized = sanitized.replace(/[\x00-\x1F\x7F]/g, '')
51
+ .replace(/\/\//g, '/')
52
+ .trim();
53
+ break;
54
+
55
+ case 'prompt':
56
+ // For AI prompts, remove potentially malicious content but preserve most characters
57
+ sanitized = sanitized.replace(/[\x00-\x1F\x7F]/g, '')
58
+ .replace(/<script[^>]*>.*?<\/script>/gi, '')
59
+ .replace(/javascript:/gi, '')
60
+ .replace(/data:/gi, '')
61
+ .trim();
62
+ break;
63
+
64
+ default:
65
+ // Generic sanitization
66
+ sanitized = sanitized.trim();
67
+ }
68
+
69
+ if (sanitized.length === 0) {
70
+ throw new Error(`Sanitized ${type} input is empty`);
71
+ }
72
+
73
+ return sanitized;
74
+ }
75
+
76
+ /**
77
+ * Validates and secures file paths to prevent directory traversal attacks
78
+ * @param {string} inputPath - The path to validate
79
+ * @param {string} basePath - Base directory to resolve against
80
+ * @returns {Object} { isValid: boolean, safePath: string, error?: string }
81
+ */
82
+ export function validateSecurePath(inputPath, basePath = process.cwd()) {
83
+ try {
84
+ const normalizedBase = normalize(resolve(basePath));
85
+ const normalizedInput = normalize(isAbsolute(inputPath)
86
+ ? inputPath
87
+ : resolve(basePath, inputPath));
88
+
89
+ const relativePath = relative(normalizedBase, normalizedInput);
90
+
91
+ // Check for path traversal attempts
92
+ if (relativePath.startsWith('..') ||
93
+ /\/\.\.\//.test(relativePath) ||
94
+ relativePath.includes('..\\')) {
95
+ return {
96
+ isValid: false,
97
+ safePath: '',
98
+ error: 'Path traversal attempt detected'
99
+ };
100
+ }
101
+
102
+ // Check for dangerous patterns
103
+ const dangerousPatterns = [
104
+ /\/etc\//,
105
+ /\/proc\//,
106
+ /\/dev\//,
107
+ /\/sys\//,
108
+ /\/root\//,
109
+ /\/bin\//,
110
+ /\/sbin\//,
111
+ /\/usr\/bin\//,
112
+ /\/usr\/sbin\//
113
+ ];
114
+
115
+ for (const pattern of dangerousPatterns) {
116
+ if (pattern.test(normalizedInput)) {
117
+ return {
118
+ isValid: false,
119
+ safePath: '',
120
+ error: 'Access to system directories is not allowed'
121
+ };
122
+ }
123
+ }
124
+
125
+ return {
126
+ isValid: true,
127
+ safePath: normalizedInput
128
+ };
129
+
130
+ } catch (error) {
131
+ return {
132
+ isValid: false,
133
+ safePath: '',
134
+ error: `Path validation failed: ${error.message}`
135
+ };
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Validates component names with enhanced security checks
141
+ * @param {string} name - The component name to validate
142
+ * @returns {Object} { isValid: boolean, error?: string }
143
+ */
144
+ export function validateComponentNameSecure(name) {
145
+ try {
146
+ const sanitized = sanitizeInput(name, 'componentName');
147
+
148
+ if (!sanitized || sanitized.length < 2) {
149
+ return {
150
+ isValid: false,
151
+ error: 'Component name must be at least 2 characters long'
152
+ };
153
+ }
154
+
155
+ // Check PascalCase: starts with uppercase, only contains letters and numbers
156
+ if (!/^[A-Z][a-zA-Z0-9]*$/.test(sanitized)) {
157
+ return {
158
+ isValid: false,
159
+ error: 'Component name must be in PascalCase (e.g., Button, CardHeader)'
160
+ };
161
+ }
162
+
163
+ // Check for reserved words and potentially dangerous names
164
+ const reservedWords = [
165
+ 'Component', 'React', 'Fragment', 'Suspense', 'StrictMode',
166
+ 'Error', 'Loading', 'App', 'Root', 'Document', 'Html',
167
+ 'Window', 'Document', 'Global', 'Process', 'Console',
168
+ 'Eval', 'Function', 'Script', 'Import', 'Require', 'Module'
169
+ ];
170
+
171
+ if (reservedWords.includes(sanitized)) {
172
+ return {
173
+ isValid: false,
174
+ error: `"${sanitized}" is a reserved word. Please choose a different name.`
175
+ };
176
+ }
177
+
178
+ // Check for potentially malicious patterns
179
+ const maliciousPatterns = [
180
+ /eval/i,
181
+ /script/i,
182
+ /javascript/i,
183
+ /alert/i,
184
+ /prompt/i,
185
+ /confirm/i,
186
+ /onload/i,
187
+ /onerror/i,
188
+ /onclick/i
189
+ ];
190
+
191
+ for (const pattern of maliciousPatterns) {
192
+ if (pattern.test(sanitized)) {
193
+ return {
194
+ isValid: false,
195
+ error: 'Component name contains potentially malicious patterns'
196
+ };
197
+ }
198
+ }
199
+
200
+ return { isValid: true };
201
+
202
+ } catch (error) {
203
+ return {
204
+ isValid: false,
205
+ error: `Component name validation failed: ${error.message}`
206
+ };
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Rate limiter for AI-based generation features
212
+ */
213
+ export class RateLimiter {
214
+ constructor(maxRequests = 10, timeWindow = 60000) { // 10 requests per minute
215
+ this.maxRequests = maxRequests;
216
+ this.timeWindow = timeWindow;
217
+ this.requests = new Map();
218
+ }
219
+
220
+ checkLimit(identifier) {
221
+ const now = Date.now();
222
+ const userRequests = this.requests.get(identifier) || [];
223
+
224
+ // Remove old requests
225
+ const recentRequests = userRequests.filter(time => now - time < this.timeWindow);
226
+
227
+ if (recentRequests.length >= this.maxRequests) {
228
+ return false;
229
+ }
230
+
231
+ recentRequests.push(now);
232
+ this.requests.set(identifier, recentRequests);
233
+ return true;
234
+ }
235
+
236
+ getRemaining(identifier) {
237
+ const now = Date.now();
238
+ const userRequests = this.requests.get(identifier) || [];
239
+ const recentRequests = userRequests.filter(time => now - time < this.timeWindow);
240
+ return Math.max(0, this.maxRequests - recentRequests.length);
241
+ }
242
+
243
+ reset() {
244
+ this.requests.clear();
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Creates a backup of a file before overwriting
250
+ * @param {string} filePath - Path to the file to backup
251
+ * @param {string} backupDir - Directory to store backups
252
+ * @returns {Promise<string>} - Path to the backup file
253
+ */
254
+ export async function createBackup(filePath, backupDir = '.atomix/backups') {
255
+ const { readFile, writeFile, mkdir } = await import('fs/promises');
256
+ const { join } = await import('path');
257
+
258
+ try {
259
+ const content = await readFile(filePath, 'utf8');
260
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
261
+ const backupFileName = `${filePath.split('/').pop()}.backup.${timestamp}`;
262
+ const backupPath = join(backupDir, backupFileName);
263
+
264
+ await mkdir(backupDir, { recursive: true });
265
+ await writeFile(backupPath, content, 'utf8');
266
+
267
+ logger.debug(`Created backup: ${backupPath}`);
268
+ return backupPath;
269
+
270
+ } catch (error) {
271
+ logger.warn(`Failed to create backup for ${filePath}: ${error.message}`);
272
+ throw error;
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Retry mechanism with exponential backoff
278
+ * @param {Function} operation - The async operation to retry
279
+ * @param {number} maxRetries - Maximum number of retries
280
+ * @param {number} initialDelay - Initial delay in ms
281
+ * @returns {Promise<any>} - Result of the operation
282
+ */
283
+ export async function retryWithBackoff(operation, maxRetries = 3, initialDelay = 100) {
284
+ let retries = 0;
285
+ let delay = initialDelay;
286
+
287
+ while (true) {
288
+ try {
289
+ return await operation();
290
+ } catch (error) {
291
+ retries++;
292
+
293
+ if (retries > maxRetries) {
294
+ throw error;
295
+ }
296
+
297
+ logger.debug(`Retry ${retries}/${maxRetries} after ${delay}ms: ${error.message}`);
298
+ await new Promise(resolve => setTimeout(resolve, delay));
299
+ delay *= 2; // Exponential backoff
300
+ }
301
+ }
302
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Atomix CLI - Local Telemetry system
3
+ * Tracks command execution times and success/failure rates
4
+ */
5
+
6
+ import { writeFile, readFile, mkdir } from 'fs/promises';
7
+ import { join, dirname } from 'path';
8
+ import { existsSync } from 'fs';
9
+ import { configLoader } from '../internal/config-loader.js';
10
+ import { logger } from './logger.js';
11
+
12
+ class Telemetry {
13
+ constructor() {
14
+ this.logs = [];
15
+ this.startTime = null;
16
+ this.currentCommand = null;
17
+ }
18
+
19
+ /**
20
+ * Start tracking a command
21
+ * @param {string} commandName - Name of the command being run
22
+ */
23
+ start(commandName) {
24
+ const config = configLoader.get('telemetry') || {};
25
+ if (!config.enabled) return;
26
+
27
+ this.startTime = performance.now();
28
+ this.currentCommand = commandName;
29
+ }
30
+
31
+ /**
32
+ * Stop tracking and log the result
33
+ * @param {boolean} success - Whether the command succeeded
34
+ * @param {Object} extra - Extra data to log
35
+ */
36
+ async stop(success = true, extra = {}) {
37
+ const config = configLoader.get('telemetry') || {};
38
+ if (!config.enabled || !this.startTime) return;
39
+
40
+ const duration = performance.now() - this.startTime;
41
+ const logEntry = {
42
+ command: this.currentCommand,
43
+ duration: parseFloat(duration.toFixed(2)),
44
+ timestamp: new Date().toISOString(),
45
+ success,
46
+ ...extra
47
+ };
48
+
49
+ // Anonymize if needed
50
+ if (config.anonymize !== false) {
51
+ delete logEntry.path;
52
+ delete logEntry.source;
53
+ }
54
+
55
+ await this._saveLog(logEntry, config.path || '.atomix/telemetry.json');
56
+ this.startTime = null;
57
+ }
58
+
59
+ /**
60
+ * Save the log entry to the local file
61
+ * @private
62
+ */
63
+ async _saveLog(entry, logPath) {
64
+ try {
65
+ const fullPath = join(process.cwd(), logPath);
66
+ const dir = dirname(fullPath);
67
+
68
+ if (!existsSync(dir)) {
69
+ await mkdir(dir, { recursive: true });
70
+ }
71
+
72
+ let logs = [];
73
+ if (existsSync(fullPath)) {
74
+ const content = await readFile(fullPath, 'utf8');
75
+ try {
76
+ logs = JSON.parse(content);
77
+ if (!Array.isArray(logs)) logs = [];
78
+ } catch (e) {
79
+ logs = [];
80
+ }
81
+ }
82
+
83
+ logs.push(entry);
84
+
85
+ // Keep only last 100 logs to prevent file bloat
86
+ if (logs.length > 100) {
87
+ logs = logs.slice(-100);
88
+ }
89
+
90
+ await writeFile(fullPath, JSON.stringify(logs, null, 2), 'utf8');
91
+ } catch (error) {
92
+ logger.debug(`Telemetry failed to save: ${error.message}`);
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Get all local telemetry logs
98
+ */
99
+ async getLogs() {
100
+ const config = configLoader.get('telemetry') || {};
101
+ const logPath = config.path || '.atomix/telemetry.json';
102
+ const fullPath = join(process.cwd(), logPath);
103
+
104
+ if (!existsSync(fullPath)) return [];
105
+
106
+ try {
107
+ const content = await readFile(fullPath, 'utf8');
108
+ return JSON.parse(content);
109
+ } catch (e) {
110
+ return [];
111
+ }
112
+ }
113
+ }
114
+
115
+ export const telemetry = new Telemetry();
@@ -7,44 +7,10 @@
7
7
  * @param {string} name - The component name to validate
8
8
  * @returns {Object} { isValid: boolean, error?: string }
9
9
  */
10
- export function validateComponentName(name) {
11
- if (!name || typeof name !== 'string') {
12
- return {
13
- isValid: false,
14
- error: 'Component name must be a non-empty string'
15
- };
16
- }
17
-
18
- // Check PascalCase: starts with uppercase, only contains letters and numbers
19
- if (!/^[A-Z][a-zA-Z0-9]*$/.test(name)) {
20
- return {
21
- isValid: false,
22
- error: 'Component name must be in PascalCase (e.g., Button, CardHeader)'
23
- };
24
- }
25
-
26
- // Check for reserved words
27
- const reservedWords = [
28
- 'Component', 'React', 'Fragment', 'Suspense', 'StrictMode',
29
- 'Error', 'Loading', 'App', 'Root', 'Document', 'Html'
30
- ];
31
-
32
- if (reservedWords.includes(name)) {
33
- return {
34
- isValid: false,
35
- error: `"${name}" is a reserved word. Please choose a different name.`
36
- };
37
- }
38
-
39
- // Check minimum length
40
- if (name.length < 2) {
41
- return {
42
- isValid: false,
43
- error: 'Component name must be at least 2 characters long'
44
- };
45
- }
46
-
47
- return { isValid: true };
10
+ export async function validateComponentName(name) {
11
+ // Use the enhanced security validation
12
+ const { validateComponentNameSecure } = await import('./security.js');
13
+ return validateComponentNameSecure(name);
48
14
  }
49
15
 
50
16
  /**
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Atomix CLI Utils Barrel
3
+ * Re-exports for tests and backward compatibility. Prefer importing from utils/*.js in command code.
4
+ */
5
+
6
+ import { validateSecurePath, validateComponentNameSecure } from './utils/security.js';
7
+ import { validateThemeName } from './utils/validation.js';
8
+ import { sanitizeInput, fileExists } from './utils/helpers.js';
9
+ import { AtomixCLIError, ErrorCategory } from './utils/error.js';
10
+ import { resolve, normalize } from 'path';
11
+
12
+ /**
13
+ * Validates path: security check and sensitive file check.
14
+ * @param {string} inputPath - Path to validate
15
+ * @param {string} basePath - Base directory (defaults to process.cwd())
16
+ * @returns {{ isValid: boolean, error?: string, safePath?: string }}
17
+ */
18
+ function validatePath(inputPath, basePath = process.cwd()) {
19
+ const normalized = normalize(resolve(basePath, inputPath));
20
+ const sensitive = ['.env', '.npmrc', '.env.local', '.env.production', 'id_rsa', '.ssh'];
21
+ if (sensitive.some(s => normalized.includes(s))) {
22
+ return { isValid: false, error: 'Path targets a sensitive path and is not allowed.' };
23
+ }
24
+ const result = validateSecurePath(inputPath, basePath);
25
+ const error = result.error === 'Path traversal attempt detected'
26
+ ? 'Path is outside the project directory.'
27
+ : (result.error || null);
28
+ return {
29
+ isValid: result.isValid,
30
+ error,
31
+ safePath: result.safePath || null
32
+ };
33
+ }
34
+
35
+ /** Sync component name validation (PascalCase, reserved words). Use validation.validateComponentName for async. */
36
+ const validateComponentName = validateComponentNameSecure;
37
+
38
+ export {
39
+ validatePath,
40
+ validateComponentName,
41
+ validateThemeName,
42
+ sanitizeInput,
43
+ fileExists,
44
+ AtomixCLIError,
45
+ ErrorCategory
46
+ };
@@ -22,8 +22,6 @@ const IS_DISABLED_CLASS = ACCORDION.CLASSES.IS_DISABLED;
22
22
 
23
23
  const mockHandlers = {
24
24
  onOpenChange: fn(() => {}),
25
- onOpen: fn(() => {}),
26
- onClose: fn(() => {}),
27
25
  };
28
26
 
29
27
  // Sample content for stories
@@ -222,20 +220,6 @@ const meta = {
222
220
  type: { summary: '(open: boolean) => void' },
223
221
  },
224
222
  },
225
- onOpen: {
226
- action: 'onOpen',
227
- description: 'Callback when accordion opens',
228
- table: {
229
- type: { summary: '() => void' },
230
- },
231
- },
232
- onClose: {
233
- action: 'onClose',
234
- description: 'Callback when accordion closes',
235
- table: {
236
- type: { summary: '() => void' },
237
- },
238
- },
239
223
  },
240
224
  } satisfies Meta<typeof Accordion>;
241
225
 
@@ -268,8 +252,6 @@ export const WithAllProps: Story = {
268
252
  iconPosition: 'left',
269
253
  disabled: false,
270
254
  onOpenChange: mockHandlers.onOpenChange,
271
- onOpen: mockHandlers.onOpen,
272
- onClose: mockHandlers.onClose,
273
255
  },
274
256
  parameters: {
275
257
  docs: {
@@ -28,23 +28,6 @@ describe('Accordion Component', () => {
28
28
  expect(button).toHaveAttribute('aria-expanded', 'false');
29
29
  });
30
30
 
31
- it('calls legacy onOpen/onClose handlers', () => {
32
- const onOpen = vi.fn();
33
- const onClose = vi.fn();
34
- render(
35
- <Accordion title="Test" onOpen={onOpen} onClose={onClose}>
36
- Content
37
- </Accordion>
38
- );
39
- const button = screen.getByRole('button');
40
-
41
- fireEvent.click(button);
42
- expect(onOpen).toHaveBeenCalled();
43
-
44
- fireEvent.click(button);
45
- expect(onClose).toHaveBeenCalled();
46
- });
47
-
48
31
  it('handles controlled state', () => {
49
32
  const onOpenChange = vi.fn();
50
33
  const { rerender } = render(
@@ -114,8 +114,6 @@ const AccordionImpl = memo(
114
114
  defaultOpen = false,
115
115
  isOpen: controlledOpen,
116
116
  onOpenChange,
117
- onOpen,
118
- onClose,
119
117
  disabled = false,
120
118
  iconPosition = 'right',
121
119
  icon,
@@ -143,8 +141,6 @@ const AccordionImpl = memo(
143
141
  iconPosition,
144
142
  isOpen: controlledOpen,
145
143
  onOpenChange,
146
- onOpen,
147
- onClose,
148
144
  });
149
145
 
150
146
  const headerClassNames = generateHeaderClassNames();