@shohojdhara/atomix 0.4.7 → 0.4.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/atomix.config.ts +58 -1
- package/dist/atomix.css +172 -157
- package/dist/atomix.css.map +1 -1
- package/dist/atomix.min.css +4 -4
- package/dist/atomix.min.css.map +1 -1
- package/dist/charts.d.ts +33 -0
- package/dist/charts.js +1274 -164
- package/dist/charts.js.map +1 -1
- package/dist/core.d.ts +33 -10
- package/dist/core.js +1099 -83
- package/dist/core.js.map +1 -1
- package/dist/forms.d.ts +33 -0
- package/dist/forms.js +2106 -1050
- package/dist/forms.js.map +1 -1
- package/dist/heavy.d.ts +42 -1
- package/dist/heavy.js +1663 -638
- package/dist/heavy.js.map +1 -1
- package/dist/index.d.ts +442 -270
- package/dist/index.esm.js +1947 -680
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1982 -712
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/package.json +6 -3
- package/scripts/atomix-cli.js +136 -1827
- package/scripts/cli/__tests__/basic.test.js +3 -2
- package/scripts/cli/__tests__/clean.test.js +278 -0
- package/scripts/cli/__tests__/component-validator.test.js +433 -0
- package/scripts/cli/__tests__/generator.test.js +613 -0
- package/scripts/cli/__tests__/glass-motion.test.js +256 -0
- package/scripts/cli/__tests__/integration.test.js +719 -108
- package/scripts/cli/__tests__/migrate.test.js +74 -0
- package/scripts/cli/__tests__/security.test.js +206 -0
- package/scripts/cli/__tests__/test-setup.js +3 -1
- package/scripts/cli/__tests__/theme-bridge.test.js +507 -0
- package/scripts/cli/__tests__/token-provider.test.js +361 -0
- package/scripts/cli/__tests__/utils.test.js +5 -5
- package/scripts/cli/commands/benchmark.js +105 -0
- package/scripts/cli/commands/build-theme.js +115 -0
- package/scripts/cli/commands/clean.js +109 -0
- package/scripts/cli/commands/doctor.js +88 -0
- package/scripts/cli/commands/generate.js +218 -0
- package/scripts/cli/commands/init.js +73 -0
- package/scripts/cli/commands/migrate.js +106 -0
- package/scripts/cli/commands/sync-tokens.js +206 -0
- package/scripts/cli/commands/theme-bridge.js +248 -0
- package/scripts/cli/commands/tokens.js +157 -0
- package/scripts/cli/commands/validate.js +194 -0
- package/scripts/cli/internal/ai-engine.js +156 -0
- package/scripts/cli/internal/compiler.js +114 -0
- package/scripts/cli/internal/component-validator.js +443 -0
- package/scripts/cli/internal/config-loader.js +162 -0
- package/scripts/cli/internal/filesystem.js +158 -0
- package/scripts/cli/internal/generator.js +430 -0
- package/scripts/cli/internal/glass-generator.js +398 -0
- package/scripts/cli/internal/hook-generator.js +369 -0
- package/scripts/cli/internal/hooks.js +61 -0
- package/scripts/cli/internal/itcss-generator.js +565 -0
- package/scripts/cli/internal/motion-generator.js +679 -0
- package/scripts/cli/internal/template-engine.js +301 -0
- package/scripts/cli/internal/theme-bridge.js +664 -0
- package/scripts/cli/internal/tokens/engine.js +122 -0
- package/scripts/cli/internal/tokens/provider.js +34 -0
- package/scripts/cli/internal/tokens/providers/figma.js +50 -0
- package/scripts/cli/internal/tokens/providers/style-dictionary.js +48 -0
- package/scripts/cli/internal/tokens/providers/w3c.js +48 -0
- package/scripts/cli/internal/tokens/token-provider.js +443 -0
- package/scripts/cli/internal/tokens/token-validator.js +513 -0
- package/scripts/cli/internal/validator.js +276 -0
- package/scripts/cli/internal/wizard.js +115 -0
- package/scripts/cli/mappings.js +23 -0
- package/scripts/cli/migration-tools.js +164 -94
- package/scripts/cli/plugins/style-dictionary.js +46 -0
- package/scripts/cli/templates/README.md +525 -95
- package/scripts/cli/templates/common-templates.js +40 -14
- package/scripts/cli/templates/components/react-component.ts +282 -0
- package/scripts/cli/templates/config/project-config.ts +112 -0
- package/scripts/cli/templates/hooks/use-component.ts +477 -0
- package/scripts/cli/templates/index.js +19 -4
- package/scripts/cli/templates/index.ts +171 -0
- package/scripts/cli/templates/next-templates.js +72 -0
- package/scripts/cli/templates/react-templates.js +70 -126
- package/scripts/cli/templates/scss-templates.js +35 -35
- package/scripts/cli/templates/stories/storybook-story.ts +241 -0
- package/scripts/cli/templates/styles/scss-component.ts +255 -0
- package/scripts/cli/templates/tests/vitest-test.ts +229 -0
- package/scripts/cli/templates/token-templates.js +337 -1
- package/scripts/cli/templates/tokens/token-generators.ts +1088 -0
- package/scripts/cli/templates/types/component-types.ts +145 -0
- package/scripts/cli/templates/utils/testing-utils.ts +144 -0
- package/scripts/cli/templates/vanilla-templates.js +39 -0
- package/scripts/cli/token-manager.js +8 -2
- package/scripts/cli/utils/cache-manager.js +240 -0
- package/scripts/cli/utils/detector.js +46 -0
- package/scripts/cli/utils/diagnostics.js +289 -0
- package/scripts/cli/utils/error.js +89 -0
- package/scripts/cli/utils/helpers.js +67 -0
- package/scripts/cli/utils/logger.js +75 -0
- package/scripts/cli/utils/security.js +302 -0
- package/scripts/cli/utils/telemetry.js +115 -0
- package/scripts/cli/utils/validation.js +37 -0
- package/scripts/cli/utils.js +28 -341
- package/src/components/Accordion/Accordion.stories.tsx +0 -18
- package/src/components/Accordion/Accordion.test.tsx +0 -17
- package/src/components/Accordion/Accordion.tsx +0 -4
- package/src/components/AtomixGlass/AtomixGlass.test.tsx +37 -3
- package/src/components/AtomixGlass/AtomixGlass.tsx +143 -31
- package/src/components/AtomixGlass/AtomixGlassContainer.tsx +129 -31
- package/src/components/AtomixGlass/PerformanceDashboard.tsx +219 -0
- package/src/components/AtomixGlass/README.md +25 -10
- package/src/components/AtomixGlass/__snapshots__/AtomixGlass.test.tsx.snap +216 -0
- package/src/components/AtomixGlass/animation-system.ts +578 -0
- package/src/components/AtomixGlass/shader-utils.ts +4 -1
- package/src/components/AtomixGlass/stories/Overview.stories.tsx +157 -6
- package/src/components/AtomixGlass/stories/Phase1-Animation.stories.tsx +653 -0
- package/src/components/AtomixGlass/stories/Phase1-Test.stories.tsx +95 -0
- package/src/components/AtomixGlass/stories/Playground.stories.tsx +51 -51
- package/src/components/AtomixGlass/stories/shared-components.tsx +6 -0
- package/src/components/Avatar/Avatar.tsx +1 -1
- package/src/components/Button/Button.stories.disabled-link.tsx +10 -0
- package/src/components/Button/Button.stories.tsx +10 -0
- package/src/components/Button/Button.test.tsx +16 -11
- package/src/components/Button/Button.tsx +4 -4
- package/src/components/Card/Card.tsx +1 -1
- package/src/components/Dropdown/Dropdown.tsx +12 -12
- package/src/components/Form/Select.tsx +62 -3
- package/src/components/Modal/Modal.tsx +14 -3
- package/src/components/Navigation/Navbar/Navbar.tsx +44 -0
- package/src/components/Slider/Slider.stories.tsx +3 -3
- package/src/components/Slider/Slider.tsx +38 -0
- package/src/components/Steps/Steps.tsx +3 -3
- package/src/components/Tabs/Tabs.tsx +77 -8
- package/src/components/Testimonial/Testimonial.tsx +1 -1
- package/src/components/TypedButton/TypedButton.stories.tsx +59 -0
- package/src/components/TypedButton/TypedButton.tsx +39 -0
- package/src/components/TypedButton/index.ts +2 -0
- package/src/components/VideoPlayer/VideoPlayer.tsx +11 -4
- package/src/lib/composables/index.ts +4 -7
- package/src/lib/composables/types.ts +45 -0
- package/src/lib/composables/useAccordion.ts +0 -7
- package/src/lib/composables/useAtomixGlass.ts +148 -6
- package/src/lib/composables/useAtomixGlassStyles.ts +9 -7
- package/src/lib/composables/useChartExport.ts +3 -13
- package/src/lib/composables/useDropdown.ts +66 -0
- package/src/lib/composables/useFocusTrap.ts +80 -0
- package/src/lib/composables/usePerformanceMonitor.ts +448 -0
- package/src/lib/composables/useResponsiveGlass.presets.ts +192 -0
- package/src/lib/composables/useResponsiveGlass.ts +441 -0
- package/src/lib/composables/useTooltip.ts +16 -0
- package/src/lib/composables/useTypedButton.ts +66 -0
- package/src/lib/config/index.ts +62 -5
- package/src/lib/constants/components.ts +62 -7
- package/src/lib/theme/devtools/__tests__/useHistory.test.tsx +150 -0
- package/src/lib/theme/tokens/centralized-tokens.ts +120 -0
- package/src/lib/theme/utils/__tests__/domUtils.test.ts +101 -0
- package/src/lib/types/components.ts +37 -11
- package/src/lib/types/glass.ts +35 -0
- package/src/lib/types/index.ts +1 -0
- package/src/lib/utils/displacement-generator.ts +1 -1
- package/src/styles/01-settings/_settings.testtypecheck.scss +53 -0
- package/src/styles/01-settings/_settings.typedbutton.scss +53 -0
- package/src/styles/06-components/_components.atomix-glass.scss +17 -21
- package/src/styles/06-components/_components.edge-panel.scss +1 -5
- package/src/styles/06-components/_components.modal.scss +1 -4
- package/src/styles/06-components/_components.navbar.scss +1 -1
- package/src/styles/06-components/_components.testbutton.scss +212 -0
- package/src/styles/06-components/_components.testtypecheck.scss +212 -0
- package/src/styles/06-components/_components.tooltip.scss +9 -5
- package/src/styles/06-components/_components.typedbutton.scss +212 -0
- package/src/styles/99-utilities/_index.scss +1 -0
- package/src/styles/99-utilities/_utilities.text.scss +1 -1
- package/src/styles/99-utilities/_utilities.touch-target.scss +36 -0
- package/scripts/cli/component-generator.js +0 -564
- package/scripts/cli/interactive-init.js +0 -357
- package/src/styles/06-components/old.chart.styles.scss +0 -2788
|
@@ -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();
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atomix CLI Validation Utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Validates component names according to PascalCase convention
|
|
7
|
+
* @param {string} name - The component name to validate
|
|
8
|
+
* @returns {Object} { isValid: boolean, error?: string }
|
|
9
|
+
*/
|
|
10
|
+
export async function validateComponentName(name) {
|
|
11
|
+
// Use the enhanced security validation
|
|
12
|
+
const { validateComponentNameSecure } = await import('./security.js');
|
|
13
|
+
return validateComponentNameSecure(name);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Validates theme names according to kebab-case convention
|
|
18
|
+
*/
|
|
19
|
+
export function validateThemeName(name) {
|
|
20
|
+
if (!name || typeof name !== 'string') return { isValid: false, error: 'Theme name must be a string' };
|
|
21
|
+
if (!/^[a-z][a-z0-9-]*$/.test(name)) return { isValid: false, error: 'Theme name must be kebab-case' };
|
|
22
|
+
if (/--/.test(name)) return { isValid: false, error: 'Cannot contain consecutive hyphens' };
|
|
23
|
+
if (name.endsWith('-')) return { isValid: false, error: 'Cannot end with a hyphen' };
|
|
24
|
+
return { isValid: true };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Validates SCSS/CSS color values
|
|
29
|
+
*/
|
|
30
|
+
export function isValidColor(color) {
|
|
31
|
+
const patterns = [
|
|
32
|
+
/^#[0-9A-F]{3,8}$/i,
|
|
33
|
+
/^(rgb|rgba|hsl|hsla)\(/i,
|
|
34
|
+
/^var\(--/
|
|
35
|
+
];
|
|
36
|
+
return patterns.some(pattern => pattern.test(color));
|
|
37
|
+
}
|