@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.
- package/atomix.config.ts +58 -1
- package/dist/atomix.css +148 -120
- package/dist/atomix.css.map +1 -1
- package/dist/atomix.min.css +1 -1
- package/dist/atomix.min.css.map +1 -1
- package/dist/charts.d.ts +33 -0
- package/dist/charts.js +1227 -122
- package/dist/charts.js.map +1 -1
- package/dist/core.d.ts +33 -10
- package/dist/core.js +1052 -41
- package/dist/core.js.map +1 -1
- package/dist/forms.d.ts +33 -0
- package/dist/forms.js +2086 -1035
- package/dist/forms.js.map +1 -1
- package/dist/heavy.d.ts +42 -1
- package/dist/heavy.js +1620 -600
- package/dist/heavy.js.map +1 -1
- package/dist/index.d.ts +441 -270
- package/dist/index.esm.js +1900 -638
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1935 -670
- 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 +148 -4
- 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 +4 -1
- package/scripts/cli/commands/clean.js +109 -0
- package/scripts/cli/commands/doctor.js +88 -0
- package/scripts/cli/commands/generate.js +135 -14
- package/scripts/cli/commands/init.js +45 -18
- 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/component-validator.js +443 -0
- package/scripts/cli/internal/config-loader.js +162 -0
- package/scripts/cli/internal/filesystem.js +102 -2
- package/scripts/cli/internal/generator.js +359 -39
- 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 +60 -6
- 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 +45 -3
- package/scripts/cli/utils/helpers.js +24 -0
- package/scripts/cli/utils/logger.js +1 -1
- package/scripts/cli/utils/security.js +302 -0
- package/scripts/cli/utils/telemetry.js +115 -0
- package/scripts/cli/utils/validation.js +4 -38
- package/scripts/cli/utils.js +46 -0
- 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.tsx +102 -2
- package/src/components/AtomixGlass/AtomixGlassContainer.tsx +125 -12
- package/src/components/AtomixGlass/PerformanceDashboard.tsx +219 -0
- package/src/components/AtomixGlass/README.md +25 -10
- package/src/components/AtomixGlass/animation-system.ts +578 -0
- package/src/components/AtomixGlass/shader-utils.ts +3 -0
- package/src/components/AtomixGlass/stories/AnimationFeatures.stories.tsx +653 -0
- package/src/components/AtomixGlass/stories/AnimationTests.stories.tsx +95 -0
- package/src/components/AtomixGlass/stories/CardExamples.stories.tsx +212 -0
- package/src/components/AtomixGlass/stories/DashboardExamples.stories.tsx +348 -0
- package/src/components/AtomixGlass/stories/EcommerceExamples.stories.tsx +410 -0
- package/src/components/AtomixGlass/stories/FormExamples.stories.tsx +436 -0
- package/src/components/AtomixGlass/stories/HeroExamples.stories.tsx +264 -0
- package/src/components/AtomixGlass/stories/InteractivePlayground.stories.tsx +247 -0
- package/src/components/AtomixGlass/stories/MobileUIExamples.stories.tsx +418 -0
- package/src/components/AtomixGlass/stories/ModalExamples.stories.tsx +402 -0
- package/src/components/AtomixGlass/stories/Overview.stories.tsx +157 -6
- package/src/components/AtomixGlass/stories/Playground.stories.tsx +658 -93
- package/src/components/AtomixGlass/stories/PresetGallery.stories.tsx +335 -0
- package/src/components/AtomixGlass/stories/WidgetExamples.stories.tsx +441 -0
- package/src/components/AtomixGlass/stories/argTypes.ts +384 -0
- package/src/components/AtomixGlass/stories/shared-components.tsx +91 -1
- package/src/components/AtomixGlass/stories/types.ts +127 -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 +144 -5
- 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 +55 -0
- 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.testbutton.scss +212 -0
- package/src/styles/06-components/_components.testtypecheck.scss +212 -0
- 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/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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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();
|