apexcss-cli 0.1.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.
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Doctor command - Check system setup and diagnose issues
3
+ */
4
+
5
+ import { existsSync, readFileSync } from 'node:fs';
6
+ import { resolve } from 'node:path';
7
+ import { execSync } from 'node:child_process';
8
+ import { logger } from '../utils/logger.js';
9
+ import { detectFramework } from '../utils/framework-detector.js';
10
+
11
+ /**
12
+ * Run diagnostic checks
13
+ */
14
+ export async function doctorCommand() {
15
+ logger.header('ApexCSS Doctor');
16
+ logger.newline();
17
+
18
+ const cwd = process.cwd();
19
+
20
+ // Run all checks (synchronous)
21
+ const results = [
22
+ checkNodeVersion(),
23
+ checkPackageManager(),
24
+ checkApexcssInstallation(cwd),
25
+ checkConfigFile(cwd),
26
+ checkFramework(cwd),
27
+ checkVite(cwd)
28
+ ];
29
+
30
+ // Display results
31
+ logger.header('Diagnostic Results');
32
+ logger.newline();
33
+
34
+ let hasErrors = false;
35
+ let hasWarnings = false;
36
+
37
+ for (const result of results) {
38
+ if (result.status === 'ok') {
39
+ logger.success(`${result.name}: ${result.message}`);
40
+ } else if (result.status === 'warn') {
41
+ hasWarnings = true;
42
+ logger.warn(`${result.name}: ${result.message}`);
43
+ if (result.fix) {
44
+ logger.info(` Fix: ${result.fix}`);
45
+ }
46
+ } else if (result.status === 'error') {
47
+ hasErrors = true;
48
+ logger.error(`${result.name}: ${result.message}`);
49
+ if (result.fix) {
50
+ logger.info(` Fix: ${result.fix}`);
51
+ }
52
+ }
53
+ }
54
+
55
+ logger.newline();
56
+
57
+ if (hasErrors) {
58
+ logger.error('Some checks failed. Please fix the issues above.');
59
+ process.exit(1);
60
+ } else if (hasWarnings) {
61
+ logger.warn('Some checks have warnings. Review the messages above.');
62
+ } else {
63
+ logger.success('All checks passed! Your system is ready.');
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Check Node.js version
69
+ * @returns {object}
70
+ */
71
+ function checkNodeVersion() {
72
+ const nodeVersion = process.version;
73
+ const majorVersion = Number.parseInt(nodeVersion.slice(1).split('.')[0], 10);
74
+
75
+ if (majorVersion >= 18) {
76
+ return {
77
+ name: 'Node.js',
78
+ status: 'ok',
79
+ message: `v${nodeVersion} (supported)`
80
+ };
81
+ } else if (majorVersion >= 16) {
82
+ return {
83
+ name: 'Node.js',
84
+ status: 'warn',
85
+ message: `v${nodeVersion} (minimum recommended is v18+)`,
86
+ fix: 'Upgrade to Node.js v18 or later'
87
+ };
88
+ } else {
89
+ return {
90
+ name: 'Node.js',
91
+ status: 'error',
92
+ message: `v${nodeVersion} (not supported)`,
93
+ fix: 'Upgrade to Node.js v18 or later'
94
+ };
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Check package manager availability
100
+ * @returns {object}
101
+ */
102
+ function checkPackageManager() {
103
+ const managers = [
104
+ { name: 'npm', cmd: 'npm --version' },
105
+ { name: 'pnpm', cmd: 'pnpm --version' },
106
+ { name: 'yarn', cmd: 'yarn --version' }
107
+ ];
108
+
109
+ const available = [];
110
+
111
+ for (const manager of managers) {
112
+ try {
113
+ const version = execSync(manager.cmd, { encoding: 'utf-8', stdio: 'pipe' }).trim();
114
+ available.push(`${manager.name} v${version}`);
115
+ } catch {
116
+ // Package manager not available - expected if not installed
117
+ }
118
+ }
119
+
120
+ if (available.length > 0) {
121
+ return {
122
+ name: 'Package Manager',
123
+ status: 'ok',
124
+ message: available.join(', ')
125
+ };
126
+ }
127
+
128
+ return {
129
+ name: 'Package Manager',
130
+ status: 'error',
131
+ message: 'None detected',
132
+ fix: 'Install npm (comes with Node.js) or pnpm/yarn'
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Check if ApexCSS is installed
138
+ * @param {string} cwd
139
+ * @returns {object}
140
+ */
141
+ function checkApexcssInstallation(cwd) {
142
+ const packageJsonPath = resolve(cwd, 'package.json');
143
+
144
+ if (!existsSync(packageJsonPath)) {
145
+ return {
146
+ name: 'ApexCSS Installation',
147
+ status: 'warn',
148
+ message: 'No package.json found',
149
+ fix: 'Run "npm init" first, then "npm install apexcss"'
150
+ };
151
+ }
152
+
153
+ try {
154
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
155
+ const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
156
+
157
+ if (deps.apexcss) {
158
+ return {
159
+ name: 'ApexCSS Installation',
160
+ status: 'ok',
161
+ message: `v${deps.apexcss} installed`
162
+ };
163
+ }
164
+
165
+ return {
166
+ name: 'ApexCSS Installation',
167
+ status: 'warn',
168
+ message: 'Not installed in this project',
169
+ fix: 'Run "npm install apexcss"'
170
+ };
171
+ } catch {
172
+ return {
173
+ name: 'ApexCSS Installation',
174
+ status: 'error',
175
+ message: 'Could not parse package.json'
176
+ };
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Check for config file
182
+ * @param {string} cwd
183
+ * @returns {object}
184
+ */
185
+ function checkConfigFile(cwd) {
186
+ const configPath = resolve(cwd, 'apex.config.js');
187
+
188
+ if (existsSync(configPath)) {
189
+ return {
190
+ name: 'Configuration File',
191
+ status: 'ok',
192
+ message: 'apex.config.js exists'
193
+ };
194
+ }
195
+
196
+ return {
197
+ name: 'Configuration File',
198
+ status: 'warn',
199
+ message: 'apex.config.js not found',
200
+ fix: 'Run "npx apexcss init" to create a config file'
201
+ };
202
+ }
203
+
204
+ /**
205
+ * Check framework detection
206
+ * @param {string} cwd
207
+ * @returns {object}
208
+ */
209
+ function checkFramework(cwd) {
210
+ const framework = detectFramework(cwd);
211
+
212
+ if (framework.detected) {
213
+ return {
214
+ name: 'Framework',
215
+ status: 'ok',
216
+ message: `${framework.name} detected`
217
+ };
218
+ }
219
+
220
+ if (!framework.hasPackageJson) {
221
+ return {
222
+ name: 'Framework',
223
+ status: 'warn',
224
+ message: 'No package.json found',
225
+ fix: 'Run "npm init" to create a project'
226
+ };
227
+ }
228
+
229
+ return {
230
+ name: 'Framework',
231
+ status: 'warn',
232
+ message: 'Could not detect framework (assuming vanilla)',
233
+ fix: 'Specify framework with "npx apexcss init --framework=<name>"'
234
+ };
235
+ }
236
+
237
+ /**
238
+ * Check for Vite
239
+ * @param {string} cwd
240
+ * @returns {object}
241
+ */
242
+ function checkVite(cwd) {
243
+ const packageJsonPath = resolve(cwd, 'package.json');
244
+
245
+ if (!existsSync(packageJsonPath)) {
246
+ return {
247
+ name: 'Build Tool',
248
+ status: 'warn',
249
+ message: 'No package.json found'
250
+ };
251
+ }
252
+
253
+ try {
254
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
255
+ const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
256
+
257
+ if (deps.vite) {
258
+ return {
259
+ name: 'Build Tool',
260
+ status: 'ok',
261
+ message: `Vite v${deps.vite} detected (recommended)`
262
+ };
263
+ }
264
+
265
+ if (deps.webpack || deps['@angular/cli'] || deps.next || deps.nuxt) {
266
+ return {
267
+ name: 'Build Tool',
268
+ status: 'ok',
269
+ message: 'Build tool detected'
270
+ };
271
+ }
272
+
273
+ return {
274
+ name: 'Build Tool',
275
+ status: 'warn',
276
+ message: 'No build tool detected',
277
+ fix: 'Install Vite for best experience: "npm install -D vite"'
278
+ };
279
+ } catch {
280
+ return {
281
+ name: 'Build Tool',
282
+ status: 'warn',
283
+ message: 'Could not check build tools'
284
+ };
285
+ }
286
+ }
@@ -0,0 +1,339 @@
1
+ /**
2
+ * Init command - Initialize ApexCSS configuration in user's project
3
+ */
4
+
5
+ import { writeFileSync, existsSync, readFileSync } from 'node:fs';
6
+ import { resolve } from 'node:path';
7
+ import { mkdir } from 'node:fs/promises';
8
+ import { logger } from '../utils/logger.js';
9
+ import { generateSampleConfig } from '../utils/config-loader.js';
10
+ import { detectFramework, getRecommendedOutputDir, getAvailableFrameworks } from '../utils/framework-detector.js';
11
+
12
+ /**
13
+ * Prompt user for initialization options
14
+ * @param {object} framework - Detected framework
15
+ * @param {object} options - Current options
16
+ * @returns {Promise<{selectedFramework: object, outputDir: string, addImport: boolean}>}
17
+ */
18
+ async function promptForOptions(framework, options) {
19
+ const { default: inquirer } = await import('inquirer');
20
+
21
+ const answers = await inquirer.prompt([
22
+ {
23
+ type: 'list',
24
+ name: 'framework',
25
+ message: 'Select your framework:',
26
+ default: framework.id,
27
+ choices: getAvailableFrameworks().map(f => ({
28
+ name: f.name,
29
+ value: f.id
30
+ }))
31
+ },
32
+ {
33
+ type: 'input',
34
+ name: 'outputDir',
35
+ message: 'CSS output directory:',
36
+ default: options.outputDir || getRecommendedOutputDir(framework.id)
37
+ },
38
+ {
39
+ type: 'confirm',
40
+ name: 'addImport',
41
+ message: framework.entryFile
42
+ ? `Add import to ${framework.entryFile}?`
43
+ : 'Add import to your main entry file?',
44
+ default: true,
45
+ when: () => framework.entryFile || framework.id !== 'vanilla'
46
+ }
47
+ ]);
48
+
49
+ return {
50
+ selectedFramework: { ...framework, id: answers.framework },
51
+ outputDir: answers.outputDir,
52
+ addImport: answers.addImport
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Get user options (interactive or CLI)
58
+ * @param {object} framework - Detected framework
59
+ * @param {object} options - Command options
60
+ * @param {string} cwd - Current working directory
61
+ * @returns {Promise<{selectedFramework: object, outputDir: string, addImport: boolean}>}
62
+ */
63
+ async function getUserOptions(framework, options, cwd = process.cwd()) {
64
+ let result = {
65
+ selectedFramework: framework,
66
+ outputDir: options.outputDir,
67
+ addImport: options.addImport
68
+ };
69
+
70
+ if (options.interactive) {
71
+ try {
72
+ result = await promptForOptions(framework, options);
73
+ } catch {
74
+ // Interactive mode failed - fallback to defaults
75
+ logger.warn('Interactive mode not available, using defaults');
76
+ }
77
+ }
78
+
79
+ // Override with CLI framework option if provided
80
+ if (options.framework) {
81
+ // Update entryFile based on the new framework
82
+ const { FRAMEWORKS, getAvailableFrameworks } = await import('../utils/framework-detector.js');
83
+ const availableFrameworks = getAvailableFrameworks();
84
+ const frameworkInfo = availableFrameworks.find(f => f.id === options.framework);
85
+ const frameworkDef = FRAMEWORKS[options.framework];
86
+
87
+ if (frameworkDef) {
88
+ const entryFiles = frameworkDef.entryFiles || [];
89
+ const existingEntry = entryFiles.find(file =>
90
+ existsSync(resolve(cwd, file))
91
+ );
92
+
93
+ result.selectedFramework = {
94
+ ...framework,
95
+ ...frameworkDef,
96
+ id: options.framework,
97
+ name: frameworkInfo?.name || options.framework,
98
+ entryFile: existingEntry || frameworkDef.fallbackFile
99
+ };
100
+ }
101
+ }
102
+
103
+ return result;
104
+ }
105
+
106
+ /**
107
+ * Handle existing config file
108
+ * @param {string} configPath - Path to config file
109
+ * @param {boolean} useInteractive - Whether interactive mode is enabled
110
+ * @returns {Promise<boolean>} - True if should continue
111
+ */
112
+ async function handleExistingConfig(configPath, useInteractive) {
113
+ if (!existsSync(configPath)) {
114
+ return true;
115
+ }
116
+
117
+ logger.warn(`Config file already exists at ${configPath}`);
118
+ const { overwrite } = useInteractive ? await promptOverwrite() : { overwrite: false };
119
+
120
+ if (!overwrite) {
121
+ logger.info('Skipping config creation');
122
+ return false;
123
+ }
124
+
125
+ return true;
126
+ }
127
+
128
+ /**
129
+ * Setup gitignore for output directory
130
+ * @param {string} cwd - Current working directory
131
+ * @param {string} outputDir - Output directory
132
+ */
133
+ function setupGitignore(cwd, outputDir) {
134
+ const gitignorePath = resolve(cwd, '.gitignore');
135
+ const gitignoreEntry = `# ApexCSS generated files\n${outputDir.replace(/^\.\//, '')}\n`;
136
+
137
+ if (existsSync(gitignorePath)) {
138
+ const gitignoreContent = readFileSync(gitignorePath, 'utf-8');
139
+ if (!gitignoreContent.includes(outputDir)) {
140
+ logger.info(`Add to ${logger.path('.gitignore')}: ${outputDir}`);
141
+ }
142
+ } else {
143
+ writeFileSync(gitignorePath, gitignoreEntry);
144
+ logger.success(`Created ${logger.path('.gitignore')}`);
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Add import to framework entry file
150
+ * @param {string} cwd - Current working directory
151
+ * @param {object} framework - Selected framework
152
+ * @param {string} outputDir - Output directory
153
+ */
154
+ function addFrameworkImport(cwd, framework, outputDir) {
155
+ if (!framework.entryFile) {
156
+ return;
157
+ }
158
+
159
+ const entryFilePath = resolve(cwd, framework.entryFile);
160
+
161
+ if (!existsSync(entryFilePath)) {
162
+ logger.warn(`Entry file not found: ${framework.entryFile}`);
163
+ logger.info('Manually add import to your main entry file');
164
+ return;
165
+ }
166
+
167
+ const importStatement = getImportStatement(framework.id, outputDir);
168
+
169
+ try {
170
+ addImportToFile(entryFilePath, importStatement, framework.id);
171
+ logger.success(`Added import to ${logger.path(framework.entryFile)}`);
172
+ } catch (error) {
173
+ logger.warn(`Could not add import automatically: ${error.message}`);
174
+ logger.info(`Manually add: ${logger.cmd(importStatement.trim())}`);
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Initialize ApexCSS in the user's project
180
+ * @param {object} options - Command options
181
+ */
182
+ export async function initCommand(options) {
183
+ const cwd = process.cwd();
184
+
185
+ logger.header('ApexCSS Initialization');
186
+ logger.newline();
187
+
188
+ const framework = detectFramework(cwd);
189
+ logger.info(`Detected framework: ${framework.name}`);
190
+ logger.newline();
191
+
192
+ const { selectedFramework, outputDir, addImport } = await getUserOptions(framework, options, cwd);
193
+
194
+ const outputPath = resolve(cwd, outputDir);
195
+ await mkdir(outputPath, { recursive: true });
196
+
197
+ const configPath = resolve(cwd, options.configPath);
198
+ const shouldContinue = await handleExistingConfig(configPath, options.interactive);
199
+
200
+ if (!shouldContinue) {
201
+ return;
202
+ }
203
+
204
+ const configContent = generateSampleConfig();
205
+ writeFileSync(configPath, configContent);
206
+ logger.success(`Created config file: ${logger.path(options.configPath)}`);
207
+
208
+ setupGitignore(cwd, outputDir);
209
+
210
+ if (addImport) {
211
+ addFrameworkImport(cwd, selectedFramework, outputDir);
212
+ }
213
+
214
+ logger.newline();
215
+ logger.success('ApexCSS initialized successfully!');
216
+ logger.newline();
217
+ logger.info('Next steps:');
218
+ logger.list([
219
+ `Edit ${logger.path(options.configPath)} to customize your configuration`,
220
+ `Run ${logger.cmd('npx apexcss build')} to generate your CSS`,
221
+ `Run ${logger.cmd('npx apexcss watch')} during development`
222
+ ]);
223
+ logger.newline();
224
+ }
225
+
226
+ /**
227
+ * Prompt for overwrite confirmation
228
+ * @returns {Promise<{overwrite: boolean}>}
229
+ */
230
+ /**
231
+ * Prompt for overwrite confirmation
232
+ * @returns {Promise<{overwrite: boolean}>}
233
+ */
234
+ export async function promptOverwrite() {
235
+ try {
236
+ const { default: inquirer } = await import('inquirer');
237
+ return await inquirer.prompt([{
238
+ type: 'confirm',
239
+ name: 'overwrite',
240
+ message: 'Overwrite existing config file?',
241
+ default: false
242
+ }]);
243
+ } catch {
244
+ // Inquirer not available - default to not overwriting
245
+ return { overwrite: false };
246
+ }
247
+ }
248
+
249
+ /**
250
+ * CSS cascade layers import statement for all frameworks
251
+ * This uses the standard apexcss package imports with cascade layers
252
+ */
253
+ const CASCADE_LAYER_IMPORTS = `@layer base, utilities, themes;
254
+
255
+ @import 'apexcss/base' layer(base);
256
+ @import 'apexcss/utilities' layer(utilities);
257
+ @import 'apexcss/themes' layer(themes);
258
+ `;
259
+
260
+ /**
261
+ * Get the appropriate import statement for the framework
262
+ * @param {string} frameworkId - Framework identifier
263
+ * @param {string} outputDir - Output directory
264
+ * @returns {string} - Import statement
265
+ */
266
+ export function getImportStatement(frameworkId, outputDir) {
267
+ // Normalize path: remove leading ./ and trailing slashes
268
+ let cleanPath = outputDir.replace(/^\.\//, '').replace(/\/+$/, '');
269
+
270
+ // If path is empty after cleanup, use 'dist'
271
+ if (!cleanPath) {
272
+ cleanPath = 'dist';
273
+ }
274
+
275
+ // All CSS-based frameworks now use cascade layers with node_modules imports
276
+ switch (frameworkId) {
277
+ case 'angular':
278
+ case 'react':
279
+ case 'vue':
280
+ case 'svelte':
281
+ case 'vanilla':
282
+ case 'astro':
283
+ return CASCADE_LAYER_IMPORTS;
284
+ case 'next':
285
+ // Next.js needs JS imports in layout.tsx, not CSS @imports (CSS @import doesn't resolve node_modules)
286
+ return 'import \'apexcss/base\';\nimport \'apexcss/utilities\';\nimport \'apexcss/themes\';\n';
287
+ case 'nuxt':
288
+ return '// Add to nuxt.config.ts:\n// css: [\'apexcss/base\', \'apexcss/utilities\', \'apexcss/themes\']\n';
289
+ default:
290
+ return CASCADE_LAYER_IMPORTS;
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Add import statement to a file
296
+ * @param {string} filePath - Path to the file
297
+ * @param {string} importStatement - Import statement to add
298
+ * @param {string} frameworkId - Framework identifier
299
+ */
300
+ export function addImportToFile(filePath, importStatement, frameworkId) {
301
+ const content = readFileSync(filePath, 'utf-8');
302
+
303
+ // Check if already imported
304
+ if (content.includes('apexcss') || content.includes('apex.css')) {
305
+ return; // Already has import
306
+ }
307
+
308
+ let newContent;
309
+
310
+ // Check if this is a CSS file (for CSS cascade layer imports)
311
+ const isCSSFile = filePath.endsWith('.css') || filePath.endsWith('.scss');
312
+
313
+ if (isCSSFile) {
314
+ // For CSS files, add cascade layer imports at the top
315
+ newContent = importStatement + content;
316
+ } else if (frameworkId === 'react' || frameworkId === 'vue' || frameworkId === 'svelte' || frameworkId === 'astro' || frameworkId === 'vanilla') {
317
+ // Add after other JS imports, before code
318
+ const lines = content.split('\n');
319
+ let lastImportIndex = -1;
320
+
321
+ for (let i = 0; i < lines.length; i++) {
322
+ if (lines[i].trim().startsWith('import ') || lines[i].trim().startsWith('require(')) {
323
+ lastImportIndex = i;
324
+ }
325
+ }
326
+
327
+ if (lastImportIndex >= 0) {
328
+ lines.splice(lastImportIndex + 1, 0, importStatement.trim());
329
+ newContent = lines.join('\n');
330
+ } else {
331
+ newContent = importStatement + content;
332
+ }
333
+ } else {
334
+ // Default: add at the top
335
+ newContent = importStatement + content;
336
+ }
337
+
338
+ writeFileSync(filePath, newContent);
339
+ }