@mindfiredigital/ignix-lite-cli 1.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,233 @@
1
+ import { readFileSync, writeFileSync, mkdirSync } from 'fs'
2
+ import path from 'path'
3
+ import { fileURLToPath } from 'url'
4
+ import { spawn } from 'child_process'
5
+ import pc from 'picocolors'
6
+ import ora from 'ora'
7
+
8
+ function resolveMcpServerPath(): string {
9
+ try {
10
+ const resolvedUrl = import.meta.resolve('@mindfiredigital/ignix-lite-mcp')
11
+ return fileURLToPath(resolvedUrl)
12
+ } catch {
13
+ const thisDir = path.dirname(fileURLToPath(import.meta.url))
14
+ return path.resolve(thisDir, '../../mcp/dist/server.js')
15
+ }
16
+ }
17
+
18
+ /** Read a JSON file safely; returns {} if not found or invalid */
19
+ function readJson(filePath: string): Record<string, unknown> {
20
+ try {
21
+ return JSON.parse(readFileSync(filePath, 'utf-8'))
22
+ } catch {
23
+ return {}
24
+ }
25
+ }
26
+
27
+ /** Write a JSON file, creating parent directories as needed */
28
+ function writeJson(filePath: string, data: unknown): void {
29
+ mkdirSync(path.dirname(filePath), { recursive: true })
30
+ writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8')
31
+ }
32
+
33
+ /** The shared MCP server entry */
34
+ function mcpEntry(serverPath: string) {
35
+ return {
36
+ command: 'node',
37
+ args: [serverPath]
38
+ }
39
+ }
40
+
41
+ function setupClaudeDesktop(serverPath: string): void {
42
+ const spinner = ora('Configuring Claude Desktop...').start()
43
+ try {
44
+ const configDir =
45
+ process.platform === 'win32'
46
+ ? path.join(process.env.APPDATA || '', 'Claude')
47
+ : path.join(
48
+ process.env.HOME || '',
49
+ 'Library',
50
+ 'Application Support',
51
+ 'Claude'
52
+ )
53
+ const configPath = path.join(configDir, 'claude_desktop_config.json')
54
+ const config = readJson(configPath) as {
55
+ mcpServers?: Record<string, unknown>
56
+ }
57
+ config.mcpServers = config.mcpServers || {}
58
+ config.mcpServers['ignix-lite'] = mcpEntry(serverPath)
59
+ writeJson(configPath, config)
60
+ spinner.succeed(pc.green('Claude Desktop configured successfully!'))
61
+ console.log(pc.gray(` Config: ${configPath}`))
62
+ console.log(pc.yellow('\n ⚡ Restart Claude Desktop to apply changes.\n'))
63
+ } catch (err) {
64
+ const msg = err instanceof Error ? err.message : String(err)
65
+ spinner.fail(pc.red(`Failed to configure Claude Desktop: ${msg}`))
66
+ }
67
+ }
68
+
69
+ function setupClaudeCode(serverPath: string): void {
70
+ const spinner = ora('Configuring Claude Code (claude CLI)...').start()
71
+ try {
72
+ // User-level config: ~/.claude.json
73
+ const configPath =
74
+ process.platform === 'win32'
75
+ ? path.join(process.env.USERPROFILE || '', '.claude.json')
76
+ : path.join(process.env.HOME || '', '.claude.json')
77
+
78
+ const config = readJson(configPath) as {
79
+ mcpServers?: Record<string, unknown>
80
+ }
81
+ config.mcpServers = config.mcpServers || {}
82
+ config.mcpServers['ignix-lite'] = mcpEntry(serverPath)
83
+ writeJson(configPath, config)
84
+ spinner.succeed(pc.green('Claude Code configured successfully!'))
85
+ console.log(pc.gray(` Config: ${configPath}`))
86
+ console.log(
87
+ pc.yellow(
88
+ '\n ⚡ Run "claude" in any project to start using Ignix-Lite tools.\n'
89
+ )
90
+ )
91
+ } catch (err) {
92
+ const msg = err instanceof Error ? err.message : String(err)
93
+ spinner.fail(pc.red(`Failed to configure Claude Code: ${msg}`))
94
+ }
95
+ }
96
+
97
+ function setupGemini(serverPath: string): void {
98
+ const spinner = ora('Configuring Gemini CLI...').start()
99
+ try {
100
+ // Gemini CLI reads ~/.gemini/settings.json
101
+ const configDir =
102
+ process.platform === 'win32'
103
+ ? path.join(process.env.USERPROFILE || '', '.gemini')
104
+ : path.join(process.env.HOME || '', '.gemini')
105
+ const configPath = path.join(configDir, 'settings.json')
106
+
107
+ const config = readJson(configPath) as {
108
+ mcpServers?: Record<string, unknown>
109
+ }
110
+ config.mcpServers = config.mcpServers || {}
111
+ config.mcpServers['ignix-lite'] = mcpEntry(serverPath)
112
+ writeJson(configPath, config)
113
+ spinner.succeed(pc.green('Gemini CLI configured successfully!'))
114
+ console.log(pc.gray(` Config: ${configPath}`))
115
+ console.log(
116
+ pc.yellow(
117
+ '\n ⚡ Start a new Gemini CLI session to activate Ignix-Lite tools.\n'
118
+ )
119
+ )
120
+ } catch (err) {
121
+ const msg = err instanceof Error ? err.message : String(err)
122
+ spinner.fail(pc.red(`Failed to configure Gemini CLI: ${msg}`))
123
+ }
124
+ }
125
+
126
+ function setupCursor(serverPath: string): void {
127
+ const spinner = ora('Configuring Cursor...').start()
128
+ try {
129
+ // Cursor reads ~/.cursor/mcp.json (global scope)
130
+ const configDir =
131
+ process.platform === 'win32'
132
+ ? path.join(process.env.USERPROFILE || '', '.cursor')
133
+ : path.join(process.env.HOME || '', '.cursor')
134
+ const configPath = path.join(configDir, 'mcp.json')
135
+
136
+ const config = readJson(configPath) as {
137
+ mcpServers?: Record<string, unknown>
138
+ }
139
+ config.mcpServers = config.mcpServers || {}
140
+ config.mcpServers['ignix-lite'] = mcpEntry(serverPath)
141
+ writeJson(configPath, config)
142
+ spinner.succeed(pc.green('Cursor configured successfully!'))
143
+ console.log(pc.gray(` Config: ${configPath}`))
144
+ console.log(
145
+ pc.yellow(
146
+ '\n ⚡ Reload Cursor (Ctrl+Shift+P → "Reload Window") to activate Ignix-Lite tools.\n'
147
+ )
148
+ )
149
+ } catch (err) {
150
+ const msg = err instanceof Error ? err.message : String(err)
151
+ spinner.fail(pc.red(`Failed to configure Cursor: ${msg}`))
152
+ }
153
+ }
154
+
155
+ const SUPPORTED_CLIENTS = [
156
+ 'claude',
157
+ 'claude-desktop',
158
+ 'claude-code',
159
+ 'gemini',
160
+ 'cursor'
161
+ ]
162
+
163
+ export async function mcpSetupCommand(client?: string): Promise<void> {
164
+ const serverPath = resolveMcpServerPath()
165
+ const target = (client || '').toLowerCase().trim()
166
+
167
+ if (!target) {
168
+ console.log(pc.cyan('\nWhich client do you want to configure?\n'))
169
+ console.log(
170
+ ` ${pc.bold('claude')} Claude Desktop (auto-configure)`
171
+ )
172
+ console.log(
173
+ ` ${pc.bold('claude-code')} Claude Code CLI (auto-configure)`
174
+ )
175
+ console.log(
176
+ ` ${pc.bold('gemini')} Gemini CLI (auto-configure)`
177
+ )
178
+ console.log(
179
+ ` ${pc.bold('cursor')} Cursor editor (auto-configure)`
180
+ )
181
+ console.log()
182
+ console.log(
183
+ pc.gray(
184
+ 'Usage: ignix-lite mcp setup <client> (e.g. ignix-lite mcp setup cursor)'
185
+ )
186
+ )
187
+ console.log()
188
+ return
189
+ }
190
+
191
+ switch (target) {
192
+ case 'claude':
193
+ case 'claude-desktop':
194
+ setupClaudeDesktop(serverPath)
195
+ break
196
+
197
+ case 'claude-code':
198
+ setupClaudeCode(serverPath)
199
+ break
200
+
201
+ case 'gemini':
202
+ setupGemini(serverPath)
203
+ break
204
+
205
+ case 'cursor':
206
+ setupCursor(serverPath)
207
+ break
208
+
209
+ default:
210
+ console.log(pc.red(`\nUnknown client: "${client}"\n`))
211
+ console.log(`Supported clients: ${pc.bold(SUPPORTED_CLIENTS.join(', '))}`)
212
+ console.log()
213
+ }
214
+ }
215
+
216
+ export function mcpStartCommand(): void {
217
+ const serverPath = resolveMcpServerPath()
218
+
219
+ console.log(pc.cyan(`\nStarting Ignix-Lite MCP Server...`))
220
+ console.log(`Running: ${pc.green('node ' + serverPath)}\n`)
221
+
222
+ const child = spawn('node', [serverPath], { stdio: 'inherit' })
223
+
224
+ child.on('error', (err) => {
225
+ console.log(pc.red(`\nFailed to start MCP Server: ${err.message}`))
226
+ })
227
+
228
+ child.on('close', (code) => {
229
+ if (code !== 0) {
230
+ console.log(pc.red(`\nMCP Server exited with code ${code}`))
231
+ }
232
+ })
233
+ }
@@ -0,0 +1,79 @@
1
+ import { readFileSync, existsSync, writeFileSync } from 'fs'
2
+ import path from 'path'
3
+ import pc from 'picocolors'
4
+ import ora from 'ora'
5
+ import { preview } from '@mindfiredigital/ignix-lite-engine'
6
+
7
+ export async function previewCommand(
8
+ filePath: string,
9
+ options: { output: string; width: string; theme?: string }
10
+ ) {
11
+ const absolutePath = path.resolve(process.cwd(), filePath)
12
+
13
+ if (!existsSync(absolutePath)) {
14
+ console.log(pc.red(`Error: File not found at ${filePath}`))
15
+ process.exit(1)
16
+ }
17
+
18
+ const spinner = ora(
19
+ 'Launching headless browser and rendering preview...'
20
+ ).start()
21
+
22
+ try {
23
+ const inputContent = readFileSync(absolutePath, 'utf8')
24
+ const widthNum = parseInt(options.width, 10) || 400
25
+
26
+ const response = await preview({
27
+ input: inputContent,
28
+ options: {
29
+ width: widthNum,
30
+ theme: options.theme
31
+ }
32
+ })
33
+
34
+ if (!response.content || response.content.length === 0) {
35
+ spinner.fail(
36
+ pc.red('Render failed: Received empty response from preview engine.')
37
+ )
38
+ return
39
+ }
40
+
41
+ let result: {
42
+ error?: string
43
+ png?: string
44
+ }
45
+ try {
46
+ result = JSON.parse(response.content[0].text)
47
+ } catch {
48
+ spinner.fail(pc.red('Render failed: Invalid JSON response from preview engine.'))
49
+ return
50
+ }
51
+
52
+ if (result.error) {
53
+ spinner.fail(pc.red(`Render failed: ${result.error}`))
54
+ return
55
+ }
56
+
57
+ if (!result.png || typeof result.png !== 'string') {
58
+ spinner.fail(
59
+ pc.red('Render failed: Preview engine did not return base64 PNG data.')
60
+ )
61
+ return
62
+ }
63
+
64
+ const base64Data = result.png.replace(/^data:image\/png;base64,/, '')
65
+ const buffer = Buffer.from(base64Data, 'base64')
66
+
67
+ const outputPath = path.resolve(process.cwd(), options.output)
68
+ writeFileSync(outputPath, buffer)
69
+
70
+ spinner.succeed(
71
+ pc.green(
72
+ `Visual preview generated successfully! Saved to ${pc.blue(options.output)}`
73
+ )
74
+ )
75
+ } catch (error) {
76
+ const msg = error instanceof Error ? error.message : String(error)
77
+ spinner.fail(pc.red(`Preview failed: ${msg}`))
78
+ }
79
+ }
@@ -0,0 +1,96 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import pc from 'picocolors'
4
+ import ora from 'ora'
5
+ import { resolveTokens, buildCss } from '@mindfiredigital/ignix-lite-engine'
6
+
7
+ export async function themeCommand(
8
+ prompt?: string,
9
+ options: { primary?: string; styleFile?: string } = {}
10
+ ) {
11
+ const spinner = ora('Generating theme tokens...').start()
12
+
13
+ let stylePath = options.styleFile
14
+
15
+ if (!stylePath) {
16
+ const configPath = 'ignix.config.json'
17
+ if (!fs.existsSync(configPath)) {
18
+ spinner.fail(
19
+ pc.red(
20
+ 'ignix.config.json not found. Run "ignix-lite init" first or specify --style-file.'
21
+ )
22
+ )
23
+ return
24
+ }
25
+
26
+ try {
27
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
28
+ stylePath = config.style
29
+ } catch {
30
+ spinner.fail(pc.red('Failed to read ignix.config.json.'))
31
+ return
32
+ }
33
+ }
34
+
35
+ if (!stylePath) {
36
+ spinner.fail(
37
+ pc.red('Style path not configured. Specify -s or --style-file option.')
38
+ )
39
+ return
40
+ }
41
+
42
+ const queryParts = [prompt || '', options.primary || ''].filter(Boolean)
43
+ const query = queryParts.join(' ').trim()
44
+
45
+ if (!query) {
46
+ spinner.fail(
47
+ pc.yellow(
48
+ 'Please provide a prompt or primary color, e.g.: ignix-lite theme "dark round blue"'
49
+ )
50
+ )
51
+ return
52
+ }
53
+
54
+ try {
55
+ const tokens = resolveTokens(query)
56
+ const cssBlock = buildCss(tokens)
57
+
58
+ const absoluteStylePath = path.resolve(process.cwd(), stylePath)
59
+ if (!absoluteStylePath.startsWith(process.cwd())) {
60
+ spinner.fail(
61
+ pc.red('Error: Style file path must be within the project workspace.')
62
+ )
63
+ return
64
+ }
65
+
66
+ const cssDir = path.dirname(absoluteStylePath)
67
+
68
+ if (!fs.existsSync(cssDir)) {
69
+ fs.mkdirSync(cssDir, { recursive: true })
70
+ }
71
+
72
+ const themeRegex =
73
+ /\/\* Ignix-Lite Custom Theme Variables \*\/[\s\S]*?\}\n?/g
74
+
75
+ if (fs.existsSync(absoluteStylePath)) {
76
+ let content = fs.readFileSync(absoluteStylePath, 'utf-8')
77
+ if (themeRegex.test(content)) {
78
+ content = content.replace(themeRegex, cssBlock + '\n')
79
+ } else {
80
+ content = cssBlock + '\n\n' + content
81
+ }
82
+ fs.writeFileSync(absoluteStylePath, content)
83
+ } else {
84
+ fs.writeFileSync(absoluteStylePath, cssBlock)
85
+ }
86
+
87
+ spinner.succeed(
88
+ pc.green(`Theme successfully updated in ${pc.blue(stylePath)}`)
89
+ )
90
+ console.log(pc.gray(`Resolved primary color: ${tokens.resolvedPrimary}`))
91
+ console.log(pc.gray(`Mode: ${tokens.isDark ? 'Dark' : 'Light'}\n`))
92
+ } catch (error) {
93
+ const msg = error instanceof Error ? error.message : String(error)
94
+ spinner.fail(pc.red(`Failed to generate theme: ${msg}`))
95
+ }
96
+ }
@@ -0,0 +1,49 @@
1
+ import { readFileSync, existsSync } from 'fs'
2
+ import path from 'path'
3
+ import pc from 'picocolors'
4
+ import { validateHtml } from '@mindfiredigital/ignix-lite-engine'
5
+
6
+ export async function validateCommand(filePath: string) {
7
+ const absolutePath = path.resolve(process.cwd(), filePath)
8
+
9
+ if (!existsSync(absolutePath)) {
10
+ console.log(pc.red(`Error: File not found at ${filePath}`))
11
+ process.exit(1)
12
+ }
13
+
14
+ const html = readFileSync(absolutePath, 'utf8')
15
+ const result = validateHtml(html)
16
+
17
+ console.log(pc.bold(pc.cyan(`\n🔍 Validation Report`)))
18
+ console.log(pc.gray('═'.repeat(60)))
19
+ console.log(`${pc.bold('File:')} ${pc.blue(filePath)}`)
20
+
21
+ if (result.valid) {
22
+ console.log(
23
+ pc.bold(
24
+ pc.green(`\n✔ PASS: All checks passed! Score: ${result.score ?? 100}/100`)
25
+ )
26
+ )
27
+ } else {
28
+ console.log(
29
+ pc.bold(
30
+ pc.red(
31
+ `\n✘ FAIL: Validation failed with ${result.errors.length} violation(s)`
32
+ )
33
+ )
34
+ )
35
+ console.log(`${pc.bold('Score:')} ${pc.yellow(`${result.score}/100\n`)}`)
36
+
37
+ result.errors.forEach((err, idx) => {
38
+ console.log(pc.bold(pc.red(`[Violation ${idx + 1}]`)))
39
+ console.log(` ${pc.bold('Line:')} ${err.line}`)
40
+ console.log(` ${pc.bold('Element:')} <${err.element}>`)
41
+ console.log(` ${pc.bold('Problem:')} ${pc.yellow(err.message)}`)
42
+ if (err.fix) {
43
+ console.log(` ${pc.bold('Fix:')} ${pc.green(err.fix)}`)
44
+ }
45
+ console.log()
46
+ })
47
+ }
48
+ console.log()
49
+ }
package/src/index.ts ADDED
@@ -0,0 +1,91 @@
1
+ import { Command } from 'commander'
2
+ import { initCommand } from './commands/init.js'
3
+ import { themeCommand } from './commands/theme.js'
4
+ import { addCommand } from './commands/add.js'
5
+ import { validateCommand } from './commands/validate.js'
6
+ import { checkA11yCommand } from './commands/check-a11y.js'
7
+ import { listCommand } from './commands/list.js'
8
+ import { infoCommand } from './commands/info.js'
9
+ import { buildCommand } from './commands/build.js'
10
+ import { previewCommand } from './commands/preview.js'
11
+ import { mcpSetupCommand, mcpStartCommand } from './commands/mcp.js'
12
+
13
+ const program = new Command()
14
+
15
+ program
16
+ .name('ignix-lite')
17
+ .description(
18
+ 'CLI tool for project initialization, component scaffolding, and local validation in Ignix-Lite'
19
+ )
20
+ .version('1.0.0')
21
+
22
+ program
23
+ .command('init')
24
+ .description('Initialize Ignix-Lite in your project')
25
+ .action(initCommand)
26
+
27
+ program
28
+ .command('theme [prompt]')
29
+ .description(
30
+ 'Generate theme variables based on design prompts or primary color'
31
+ )
32
+ .option('-p, --primary <color>', 'Primary color (hex/hsl) explicitly')
33
+ .option('-s, --style-file <path>', 'Stylesheet target path')
34
+ .action(themeCommand)
35
+
36
+ program
37
+ .command('add <component>')
38
+ .description('Add or print an Ignix-Lite component template')
39
+ .action(addCommand)
40
+
41
+ program
42
+ .command('validate <file>')
43
+ .description('Validate a markup file against Ignix-Lite design rules')
44
+ .action(validateCommand)
45
+
46
+ program
47
+ .command('check-a11y <file>')
48
+ .description('Audit a local markup file for WCAG accessibility issues')
49
+ .action(checkA11yCommand)
50
+
51
+ program
52
+ .command('list')
53
+ .description('List all available Ignix-Lite components')
54
+ .action(listCommand)
55
+ program
56
+ .command('info <component>')
57
+ .description('Show detailed manifest and guidelines for a component')
58
+ .action(infoCommand)
59
+
60
+ program
61
+ .command('build <prompt>')
62
+ .description('Generate Ignix-Lite HTML/Emmet from a natural language prompt')
63
+ .option('-o, --output <file>', 'Path to write the synthesized HTML output')
64
+ .option('-e, --emmet-only', 'Output the compiled Emmet shorthand only')
65
+ .action(buildCommand)
66
+
67
+ program
68
+ .command('preview <file>')
69
+ .description('Generate a visual PNG preview of an HTML or Emmet file')
70
+ .option('-o, --output <file>', 'Output image destination', 'preview.png')
71
+ .option('-w, --width <pixels>', 'Viewport width', '400')
72
+ .option('-t, --theme <light|dark>', 'Emulated color scheme theme')
73
+ .action(previewCommand)
74
+
75
+ const mcp = program
76
+ .command('mcp')
77
+ .description('Manage the Ignix-Lite MCP server')
78
+
79
+ mcp
80
+ .command('setup [client]')
81
+ .description(
82
+ 'Configure the MCP server for an editor/client (claude, cursor, gemini)'
83
+ )
84
+ .action(mcpSetupCommand)
85
+
86
+ mcp
87
+ .command('start')
88
+ .description('Start the Ignix-Lite MCP server')
89
+ .action(mcpStartCommand)
90
+
91
+ program.parse(process.argv)
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true
13
+ },
14
+ "include": ["src/**/*"]
15
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from 'tsup'
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ format: ['esm'],
6
+ clean: true,
7
+ dts: false,
8
+ banner: {
9
+ js: '#!/usr/bin/env node',
10
+ },
11
+ })