@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.
- package/.turbo/turbo-build.log +13 -0
- package/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +368 -0
- package/dist/index.js +828 -0
- package/package.json +35 -0
- package/src/commands/add.ts +146 -0
- package/src/commands/build.ts +66 -0
- package/src/commands/check-a11y.ts +61 -0
- package/src/commands/info.ts +66 -0
- package/src/commands/init.ts +91 -0
- package/src/commands/list.ts +22 -0
- package/src/commands/mcp.ts +233 -0
- package/src/commands/preview.ts +79 -0
- package/src/commands/theme.ts +96 -0
- package/src/commands/validate.ts +49 -0
- package/src/index.ts +91 -0
- package/tsconfig.json +15 -0
- package/tsup.config.ts +11 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,828 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/init.ts
|
|
7
|
+
import fs from "fs";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import prompts from "prompts";
|
|
10
|
+
import pc from "picocolors";
|
|
11
|
+
import ora from "ora";
|
|
12
|
+
async function initCommand() {
|
|
13
|
+
console.log(
|
|
14
|
+
pc.cyan("Welcome to Ignix-Lite! Let's get your project set up.\n")
|
|
15
|
+
);
|
|
16
|
+
const response = await prompts([
|
|
17
|
+
{
|
|
18
|
+
type: "select",
|
|
19
|
+
name: "framework",
|
|
20
|
+
message: "Which framework are you using?",
|
|
21
|
+
choices: [
|
|
22
|
+
{ title: "React", value: "react" },
|
|
23
|
+
{ title: "Vue", value: "vue" },
|
|
24
|
+
{ title: "Svelte", value: "svelte" },
|
|
25
|
+
{ title: "Vanilla HTML", value: "vanilla" }
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
type: "text",
|
|
30
|
+
name: "stylePath",
|
|
31
|
+
message: "Path to your main CSS file (where Ignix-Lite variables will be added):",
|
|
32
|
+
initial: "src/index.css"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
type: "text",
|
|
36
|
+
name: "primaryColor",
|
|
37
|
+
message: "Default primary theme color (hex):",
|
|
38
|
+
initial: "#6366f1",
|
|
39
|
+
validate: (value) => {
|
|
40
|
+
const hexRegex = /^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/;
|
|
41
|
+
return hexRegex.test(value) ? true : "Please enter a valid hex color code (e.g. #6366f1 or #fff)";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
]);
|
|
45
|
+
if (!response.framework || !response.stylePath) {
|
|
46
|
+
console.log(pc.red("\nSetup cancelled."));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const spinner = ora("Initializing project...").start();
|
|
50
|
+
const config = {
|
|
51
|
+
framework: response.framework,
|
|
52
|
+
style: response.stylePath,
|
|
53
|
+
theme: {
|
|
54
|
+
primary: response.primaryColor
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
fs.writeFileSync("ignix.config.json", JSON.stringify(config, null, 2));
|
|
58
|
+
const cssDir = path.dirname(response.stylePath);
|
|
59
|
+
if (!fs.existsSync(cssDir)) {
|
|
60
|
+
fs.mkdirSync(cssDir, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
const defaultThemeVars = `/* Ignix-Lite Custom Theme Variables */
|
|
63
|
+
:root {
|
|
64
|
+
--ix-primary: ${response.primaryColor};
|
|
65
|
+
--ix-primary-hover: ${response.primaryColor}d0;
|
|
66
|
+
--ix-primary-contrast: #ffffff;
|
|
67
|
+
--ix-primary-bg: ${response.primaryColor}1a;
|
|
68
|
+
}
|
|
69
|
+
`;
|
|
70
|
+
if (fs.existsSync(response.stylePath)) {
|
|
71
|
+
let content = fs.readFileSync(response.stylePath, "utf-8");
|
|
72
|
+
if (!content.includes("--ix-primary")) {
|
|
73
|
+
content = defaultThemeVars + "\n" + content;
|
|
74
|
+
fs.writeFileSync(response.stylePath, content);
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
fs.writeFileSync(response.stylePath, defaultThemeVars);
|
|
78
|
+
}
|
|
79
|
+
spinner.succeed(pc.green("Ignix-Lite initialized successfully!"));
|
|
80
|
+
console.log(`
|
|
81
|
+
Created ${pc.blue("ignix.config.json")}`);
|
|
82
|
+
console.log(`Updated theme variables in ${pc.blue(response.stylePath)}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/commands/theme.ts
|
|
86
|
+
import fs2 from "fs";
|
|
87
|
+
import path2 from "path";
|
|
88
|
+
import pc2 from "picocolors";
|
|
89
|
+
import ora2 from "ora";
|
|
90
|
+
import { resolveTokens, buildCss } from "@mindfiredigital/ignix-lite-engine";
|
|
91
|
+
async function themeCommand(prompt, options = {}) {
|
|
92
|
+
const spinner = ora2("Generating theme tokens...").start();
|
|
93
|
+
let stylePath = options.styleFile;
|
|
94
|
+
if (!stylePath) {
|
|
95
|
+
const configPath = "ignix.config.json";
|
|
96
|
+
if (!fs2.existsSync(configPath)) {
|
|
97
|
+
spinner.fail(
|
|
98
|
+
pc2.red(
|
|
99
|
+
'ignix.config.json not found. Run "ignix-lite init" first or specify --style-file.'
|
|
100
|
+
)
|
|
101
|
+
);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const config = JSON.parse(fs2.readFileSync(configPath, "utf-8"));
|
|
106
|
+
stylePath = config.style;
|
|
107
|
+
} catch {
|
|
108
|
+
spinner.fail(pc2.red("Failed to read ignix.config.json."));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (!stylePath) {
|
|
113
|
+
spinner.fail(
|
|
114
|
+
pc2.red("Style path not configured. Specify -s or --style-file option.")
|
|
115
|
+
);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const queryParts = [prompt || "", options.primary || ""].filter(Boolean);
|
|
119
|
+
const query = queryParts.join(" ").trim();
|
|
120
|
+
if (!query) {
|
|
121
|
+
spinner.fail(
|
|
122
|
+
pc2.yellow(
|
|
123
|
+
'Please provide a prompt or primary color, e.g.: ignix-lite theme "dark round blue"'
|
|
124
|
+
)
|
|
125
|
+
);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const tokens = resolveTokens(query);
|
|
130
|
+
const cssBlock = buildCss(tokens);
|
|
131
|
+
const absoluteStylePath = path2.resolve(process.cwd(), stylePath);
|
|
132
|
+
if (!absoluteStylePath.startsWith(process.cwd())) {
|
|
133
|
+
spinner.fail(
|
|
134
|
+
pc2.red("Error: Style file path must be within the project workspace.")
|
|
135
|
+
);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const cssDir = path2.dirname(absoluteStylePath);
|
|
139
|
+
if (!fs2.existsSync(cssDir)) {
|
|
140
|
+
fs2.mkdirSync(cssDir, { recursive: true });
|
|
141
|
+
}
|
|
142
|
+
const themeRegex = /\/\* Ignix-Lite Custom Theme Variables \*\/[\s\S]*?\}\n?/g;
|
|
143
|
+
if (fs2.existsSync(absoluteStylePath)) {
|
|
144
|
+
let content = fs2.readFileSync(absoluteStylePath, "utf-8");
|
|
145
|
+
if (themeRegex.test(content)) {
|
|
146
|
+
content = content.replace(themeRegex, cssBlock + "\n");
|
|
147
|
+
} else {
|
|
148
|
+
content = cssBlock + "\n\n" + content;
|
|
149
|
+
}
|
|
150
|
+
fs2.writeFileSync(absoluteStylePath, content);
|
|
151
|
+
} else {
|
|
152
|
+
fs2.writeFileSync(absoluteStylePath, cssBlock);
|
|
153
|
+
}
|
|
154
|
+
spinner.succeed(
|
|
155
|
+
pc2.green(`Theme successfully updated in ${pc2.blue(stylePath)}`)
|
|
156
|
+
);
|
|
157
|
+
console.log(pc2.gray(`Resolved primary color: ${tokens.resolvedPrimary}`));
|
|
158
|
+
console.log(pc2.gray(`Mode: ${tokens.isDark ? "Dark" : "Light"}
|
|
159
|
+
`));
|
|
160
|
+
} catch (error) {
|
|
161
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
162
|
+
spinner.fail(pc2.red(`Failed to generate theme: ${msg}`));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/commands/add.ts
|
|
167
|
+
import pc3 from "picocolors";
|
|
168
|
+
var COMPONENT_TEMPLATES = {
|
|
169
|
+
accordion: `<details>
|
|
170
|
+
<summary>Accordion Title</summary>
|
|
171
|
+
<p>Accordion contents goes here...</p>
|
|
172
|
+
</details>`,
|
|
173
|
+
alert: `<aside data-intent="info">
|
|
174
|
+
<strong>Head's up!</strong>
|
|
175
|
+
<span>This is a semantic alert component</span>
|
|
176
|
+
</aside>`,
|
|
177
|
+
avatar: `<span data-size="md">JD</span>`,
|
|
178
|
+
badge: `<mark data-intent="success">Saved</mark>`,
|
|
179
|
+
breadcrumb: `<nav aria-label="Breadcrumb">
|
|
180
|
+
<ol>
|
|
181
|
+
<li><a href="/">Home</a></li>
|
|
182
|
+
<li><a href="/docs" aria-current="page">Docs</a></li>
|
|
183
|
+
</ol>
|
|
184
|
+
</nav>`,
|
|
185
|
+
button: `<button data-intent="primary">Save</button>`,
|
|
186
|
+
card: `<article>
|
|
187
|
+
<h2 slot="title">Card Title</h2>
|
|
188
|
+
<p slot="body">
|
|
189
|
+
This is the main card body content.
|
|
190
|
+
</p>
|
|
191
|
+
</article>`,
|
|
192
|
+
checkbox: `<label>
|
|
193
|
+
<input type="checkbox" />
|
|
194
|
+
Remember me
|
|
195
|
+
</label>`,
|
|
196
|
+
codeblock: `<pre><code data-lang="typescript">const greet = () => "Hello World";</code></pre>`,
|
|
197
|
+
combobox: `<ix-combobox>
|
|
198
|
+
<label slot="control">
|
|
199
|
+
Search
|
|
200
|
+
<input type="text" placeholder="Search options..." />
|
|
201
|
+
</label>
|
|
202
|
+
<ul hidden>
|
|
203
|
+
<li data-value="1">Option A</li>
|
|
204
|
+
<li data-value="2">Option B</li>
|
|
205
|
+
</ul>
|
|
206
|
+
</ix-combobox>`,
|
|
207
|
+
dialog: `<dialog id="confirm-modal" data-intent="danger">
|
|
208
|
+
<h2>Confirm Action</h2>
|
|
209
|
+
<p>Are you sure you want to proceed?</p>
|
|
210
|
+
<button>Cancel</button>
|
|
211
|
+
<button>Delete</button>
|
|
212
|
+
</dialog>`,
|
|
213
|
+
divider: `<hr />`,
|
|
214
|
+
dropdown: `<ix-dropdown>
|
|
215
|
+
<button slot="trigger">Options \u25BE</button>
|
|
216
|
+
<ul slot="menu" hidden>
|
|
217
|
+
<li><button>Profile</button></li>
|
|
218
|
+
<li><button>Settings</button></li>
|
|
219
|
+
</ul>
|
|
220
|
+
</ix-dropdown>`,
|
|
221
|
+
form: `<form>
|
|
222
|
+
<label>
|
|
223
|
+
Name
|
|
224
|
+
<input type="text" required />
|
|
225
|
+
</label>
|
|
226
|
+
<button type="submit" data-intent="primary">Submit</button>
|
|
227
|
+
</form>`,
|
|
228
|
+
grid: `<section data-grid="3">
|
|
229
|
+
<section>Column 1</section>
|
|
230
|
+
<section>Column 2</section>
|
|
231
|
+
<section>Column 3</section>
|
|
232
|
+
</section>`,
|
|
233
|
+
input: `<label>
|
|
234
|
+
Username
|
|
235
|
+
<input type="text" required placeholder="Enter Username..." />
|
|
236
|
+
</label>`,
|
|
237
|
+
meter: `<meter value="70" min="0" max="100" low="30" high="70" optimum="100"></meter>`,
|
|
238
|
+
navigation: `<nav aria-label="Main Navigation">
|
|
239
|
+
<ul>
|
|
240
|
+
<li><a href="/" aria-current="page">Home</a></li>
|
|
241
|
+
<li><a href="/docs">Docs</a></li>
|
|
242
|
+
<li><a href="/blog">Blog</a></li>
|
|
243
|
+
</ul>
|
|
244
|
+
</nav>`,
|
|
245
|
+
progress: `<progress value="50" max="100"></progress>`,
|
|
246
|
+
radio: `<label>
|
|
247
|
+
<input type="radio" name="options" value="a" />
|
|
248
|
+
Option A
|
|
249
|
+
</label>`,
|
|
250
|
+
select: `<label>
|
|
251
|
+
Options
|
|
252
|
+
<select data-intent="primary">
|
|
253
|
+
<option value="1">Option 1</option>
|
|
254
|
+
<option value="2">Option 2</option>
|
|
255
|
+
</select>
|
|
256
|
+
</label>`,
|
|
257
|
+
skeleton: `<span aria-busy="true" data-shape="text"></span>`,
|
|
258
|
+
tabs: `<ix-tabs>
|
|
259
|
+
<button slot="tab" aria-selected="true">Tab 1</button>
|
|
260
|
+
<button slot="tab">Tab 2</button>
|
|
261
|
+
<section slot="panel">Content for Tab 1</section>
|
|
262
|
+
<section slot="panel" hidden>Content for Tab 2</section>
|
|
263
|
+
</ix-tabs>`,
|
|
264
|
+
table: `<table>
|
|
265
|
+
<thead>
|
|
266
|
+
<tr>
|
|
267
|
+
<th>Name</th>
|
|
268
|
+
<th>Role</th>
|
|
269
|
+
</tr>
|
|
270
|
+
</thead>
|
|
271
|
+
<tbody>
|
|
272
|
+
<tr>
|
|
273
|
+
<td>Alice</td>
|
|
274
|
+
<td>Developer</td>
|
|
275
|
+
</tr>
|
|
276
|
+
</tbody>
|
|
277
|
+
</table>`,
|
|
278
|
+
textarea: `<label>
|
|
279
|
+
Message
|
|
280
|
+
<textarea data-intent="primary" placeholder="Enter your message..."></textarea>
|
|
281
|
+
</label>`,
|
|
282
|
+
toast: `<ix-toast>
|
|
283
|
+
<aside data-intent="success" data-open="true">
|
|
284
|
+
<strong>Success!</strong>
|
|
285
|
+
<span>Action completed.</span>
|
|
286
|
+
</aside>
|
|
287
|
+
</ix-toast>`,
|
|
288
|
+
tooltip: `<ix-tooltip content="Helpful information">
|
|
289
|
+
<span>Hover me</span>
|
|
290
|
+
</ix-tooltip>`
|
|
291
|
+
};
|
|
292
|
+
async function addCommand(component) {
|
|
293
|
+
const normalized = component.toLowerCase().trim();
|
|
294
|
+
const template = COMPONENT_TEMPLATES[normalized];
|
|
295
|
+
if (!template) {
|
|
296
|
+
console.log(pc3.red(`Component "${component}" not found.`));
|
|
297
|
+
console.log(
|
|
298
|
+
pc3.yellow(
|
|
299
|
+
`Available components : ${Object.keys(COMPONENT_TEMPLATES).join(", ")}`
|
|
300
|
+
)
|
|
301
|
+
);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
console.log(pc3.cyan(`
|
|
305
|
+
Template for <${component}>:
|
|
306
|
+
`));
|
|
307
|
+
console.log(template);
|
|
308
|
+
console.log("\n");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// src/commands/validate.ts
|
|
312
|
+
import { readFileSync, existsSync } from "fs";
|
|
313
|
+
import path3 from "path";
|
|
314
|
+
import pc4 from "picocolors";
|
|
315
|
+
import { validateHtml } from "@mindfiredigital/ignix-lite-engine";
|
|
316
|
+
async function validateCommand(filePath) {
|
|
317
|
+
const absolutePath = path3.resolve(process.cwd(), filePath);
|
|
318
|
+
if (!existsSync(absolutePath)) {
|
|
319
|
+
console.log(pc4.red(`Error: File not found at ${filePath}`));
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
const html = readFileSync(absolutePath, "utf8");
|
|
323
|
+
const result = validateHtml(html);
|
|
324
|
+
console.log(pc4.bold(pc4.cyan(`
|
|
325
|
+
\u{1F50D} Validation Report`)));
|
|
326
|
+
console.log(pc4.gray("\u2550".repeat(60)));
|
|
327
|
+
console.log(`${pc4.bold("File:")} ${pc4.blue(filePath)}`);
|
|
328
|
+
if (result.valid) {
|
|
329
|
+
console.log(
|
|
330
|
+
pc4.bold(
|
|
331
|
+
pc4.green(`
|
|
332
|
+
\u2714 PASS: All checks passed! Score: ${result.score ?? 100}/100`)
|
|
333
|
+
)
|
|
334
|
+
);
|
|
335
|
+
} else {
|
|
336
|
+
console.log(
|
|
337
|
+
pc4.bold(
|
|
338
|
+
pc4.red(
|
|
339
|
+
`
|
|
340
|
+
\u2718 FAIL: Validation failed with ${result.errors.length} violation(s)`
|
|
341
|
+
)
|
|
342
|
+
)
|
|
343
|
+
);
|
|
344
|
+
console.log(`${pc4.bold("Score:")} ${pc4.yellow(`${result.score}/100
|
|
345
|
+
`)}`);
|
|
346
|
+
result.errors.forEach((err, idx) => {
|
|
347
|
+
console.log(pc4.bold(pc4.red(`[Violation ${idx + 1}]`)));
|
|
348
|
+
console.log(` ${pc4.bold("Line:")} ${err.line}`);
|
|
349
|
+
console.log(` ${pc4.bold("Element:")} <${err.element}>`);
|
|
350
|
+
console.log(` ${pc4.bold("Problem:")} ${pc4.yellow(err.message)}`);
|
|
351
|
+
if (err.fix) {
|
|
352
|
+
console.log(` ${pc4.bold("Fix:")} ${pc4.green(err.fix)}`);
|
|
353
|
+
}
|
|
354
|
+
console.log();
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
console.log();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// src/commands/check-a11y.ts
|
|
361
|
+
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
362
|
+
import path4 from "path";
|
|
363
|
+
import pc5 from "picocolors";
|
|
364
|
+
import { auditA11y } from "@mindfiredigital/ignix-lite-engine";
|
|
365
|
+
async function checkA11yCommand(filePath) {
|
|
366
|
+
const absolutePath = path4.resolve(process.cwd(), filePath);
|
|
367
|
+
if (!existsSync2(absolutePath)) {
|
|
368
|
+
console.log(pc5.red(`Error: File not found at ${filePath}`));
|
|
369
|
+
process.exit(1);
|
|
370
|
+
}
|
|
371
|
+
const html = readFileSync2(absolutePath, "utf8");
|
|
372
|
+
const result = auditA11y(html);
|
|
373
|
+
console.log(pc5.bold(pc5.cyan(`
|
|
374
|
+
\u267F Accessibility Audit Report`)));
|
|
375
|
+
console.log(pc5.gray("\u2550".repeat(60)));
|
|
376
|
+
console.log(`${pc5.bold("File:")} ${pc5.blue(filePath)}`);
|
|
377
|
+
console.log(`${pc5.bold("Standards:")} WCAG 2.2 ${pc5.bold(result.wcag)}`);
|
|
378
|
+
console.log(
|
|
379
|
+
`${pc5.bold("Summary:")} Passed ${result.passes.length} rules, Found ${result.issues.length} issue(s)`
|
|
380
|
+
);
|
|
381
|
+
if (result.issues.length === 0) {
|
|
382
|
+
console.log(
|
|
383
|
+
pc5.bold(
|
|
384
|
+
pc5.green(`
|
|
385
|
+
\u2714 PASS: No accessibility issues found! Score: 100/100`)
|
|
386
|
+
)
|
|
387
|
+
);
|
|
388
|
+
} else {
|
|
389
|
+
console.log(
|
|
390
|
+
pc5.bold(
|
|
391
|
+
pc5.red(
|
|
392
|
+
`
|
|
393
|
+
\u2718 FAIL: Accessibility check failed. Score: ${result.score}/100
|
|
394
|
+
`
|
|
395
|
+
)
|
|
396
|
+
)
|
|
397
|
+
);
|
|
398
|
+
result.issues.forEach((issue, idx) => {
|
|
399
|
+
const isError = issue.type === "error";
|
|
400
|
+
const labelColor = isError ? pc5.red : pc5.yellow;
|
|
401
|
+
const borderChar = isError ? "\u2718" : "\u26A0";
|
|
402
|
+
console.log(
|
|
403
|
+
pc5.bold(
|
|
404
|
+
labelColor(
|
|
405
|
+
`[Issue ${idx + 1}] ${borderChar} ${issue.rule} (${issue.type.toUpperCase()})`
|
|
406
|
+
)
|
|
407
|
+
)
|
|
408
|
+
);
|
|
409
|
+
console.log(` ${pc5.bold("Element:")} ${issue.element}`);
|
|
410
|
+
console.log(` ${pc5.bold("Message:")} ${issue.message}`);
|
|
411
|
+
if (issue.fix) {
|
|
412
|
+
console.log(` ${pc5.bold("Fix:")} ${pc5.green(issue.fix)}`);
|
|
413
|
+
}
|
|
414
|
+
console.log();
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
console.log();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// src/commands/list.ts
|
|
421
|
+
import pc6 from "picocolors";
|
|
422
|
+
import { manifests } from "@mindfiredigital/ignix-lite-engine";
|
|
423
|
+
function listCommand() {
|
|
424
|
+
const components = Object.keys(manifests).sort();
|
|
425
|
+
console.log(pc6.bold(pc6.cyan("\n\u{1F525} Ignix-Lite Components Catalog")));
|
|
426
|
+
console.log(pc6.gray("\u2550".repeat(70)));
|
|
427
|
+
components.forEach((name) => {
|
|
428
|
+
const desc = manifests[name].description || pc6.gray("No description available");
|
|
429
|
+
console.log(
|
|
430
|
+
` ${pc6.bold(pc6.green("\u25CF"))} ${pc6.bold(name.padEnd(15))} ${pc6.dim("\u2192")} ${desc}`
|
|
431
|
+
);
|
|
432
|
+
});
|
|
433
|
+
console.log(pc6.gray("\u2550".repeat(70)));
|
|
434
|
+
console.log(
|
|
435
|
+
pc6.cyan(`Total: ${pc6.bold(components.length)} components ready to use!
|
|
436
|
+
`)
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// src/commands/info.ts
|
|
441
|
+
import pc7 from "picocolors";
|
|
442
|
+
import { manifests as manifests2 } from "@mindfiredigital/ignix-lite-engine";
|
|
443
|
+
function infoCommand(component) {
|
|
444
|
+
const name = component.toLowerCase().trim();
|
|
445
|
+
const manifest = manifests2[name];
|
|
446
|
+
if (!manifest) {
|
|
447
|
+
console.log(pc7.red(`
|
|
448
|
+
Error: Component "${component}" does not exist.`));
|
|
449
|
+
console.log(
|
|
450
|
+
`Run ${pc7.yellow("ignix-lite list")} to see all available components.
|
|
451
|
+
`
|
|
452
|
+
);
|
|
453
|
+
process.exit(1);
|
|
454
|
+
}
|
|
455
|
+
console.log(pc7.bold(pc7.cyan(`
|
|
456
|
+
\u{1F4E6} Component: <${name.toUpperCase()}>`)));
|
|
457
|
+
console.log(pc7.gray("\u2550".repeat(60)));
|
|
458
|
+
console.log(`${pc7.bold("Description:")} ${manifest.description}`);
|
|
459
|
+
console.log(
|
|
460
|
+
`${pc7.bold("HTML Wrapper:")} ${pc7.yellow(`<${manifest.element}>`)}`
|
|
461
|
+
);
|
|
462
|
+
console.log(`${pc7.bold("Default Emmet:")} ${pc7.green(manifest.emmet)}`);
|
|
463
|
+
if (manifest.props && Object.keys(manifest.props).length > 0) {
|
|
464
|
+
console.log(pc7.bold(pc7.blue("\n\u2699 Attributes & Options:")));
|
|
465
|
+
console.log(pc7.gray(" " + "\u2500".repeat(56)));
|
|
466
|
+
Object.entries(manifest.props).forEach(([propName, propDef]) => {
|
|
467
|
+
const typeStr = pc7.magenta(propDef.type);
|
|
468
|
+
const valuesStr = propDef.values ? pc7.dim(` [${propDef.values.join(", ")}]`) : "";
|
|
469
|
+
const defaultStr = propDef.default !== void 0 ? ` (default: ${pc7.bold(String(propDef.default))})` : "";
|
|
470
|
+
console.log(
|
|
471
|
+
` ${pc7.cyan("\u2022")} ${pc7.bold(propName.padEnd(15))} ${typeStr}${valuesStr}${defaultStr}`
|
|
472
|
+
);
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
if (manifest.slots && Object.keys(manifest.slots).length > 0) {
|
|
476
|
+
console.log(pc7.bold(pc7.blue("\n\u{1F4E5} Slots:")));
|
|
477
|
+
console.log(pc7.gray(" " + "\u2500".repeat(56)));
|
|
478
|
+
Object.entries(manifest.slots).forEach(([slotName, slotDef]) => {
|
|
479
|
+
const elementsStr = slotDef.element ? pc7.dim(` (tags: ${slotDef.element.join(", ")})`) : "";
|
|
480
|
+
const requiredStr = slotDef.required ? pc7.bold(pc7.red(" (required)")) : "";
|
|
481
|
+
console.log(
|
|
482
|
+
` ${pc7.cyan("\u2022")} ${pc7.bold(slotName.padEnd(15))} ${requiredStr}${elementsStr}`
|
|
483
|
+
);
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
if (manifest.do && manifest.do.length > 0) {
|
|
487
|
+
console.log(pc7.bold(pc7.green("\n\u2714 Best Practices (Do):")));
|
|
488
|
+
manifest.do.forEach((rule) => console.log(` ${pc7.green("+")} ${rule}`));
|
|
489
|
+
}
|
|
490
|
+
if (manifest.dont && manifest.dont.length > 0) {
|
|
491
|
+
console.log(pc7.bold(pc7.red("\n\u2718 Avoid (Don't):")));
|
|
492
|
+
manifest.dont.forEach((rule) => console.log(` ${pc7.red("-")} ${rule}`));
|
|
493
|
+
}
|
|
494
|
+
console.log();
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// src/commands/build.ts
|
|
498
|
+
import { writeFileSync } from "fs";
|
|
499
|
+
import path5 from "path";
|
|
500
|
+
import pc8 from "picocolors";
|
|
501
|
+
import { howToBuild } from "@mindfiredigital/ignix-lite-engine";
|
|
502
|
+
async function buildCommand(prompt, options) {
|
|
503
|
+
const response = await howToBuild(prompt);
|
|
504
|
+
if (!response.content || response.content.length === 0) {
|
|
505
|
+
console.log(
|
|
506
|
+
pc8.red("Error: Intent synthesis engine returned an empty response.")
|
|
507
|
+
);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
let data;
|
|
511
|
+
try {
|
|
512
|
+
data = JSON.parse(response.content[0].text);
|
|
513
|
+
} catch {
|
|
514
|
+
console.log(
|
|
515
|
+
pc8.red("Error: Failed to parse response from intent synthesis engine.")
|
|
516
|
+
);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (!data || data.source === "no-match") {
|
|
520
|
+
console.log(pc8.red(`
|
|
521
|
+
Could not synthesize UI for: "${prompt}"`));
|
|
522
|
+
console.log(pc8.yellow(`Suggestion: ${data?.suggestion || "Try clarifying your request."}
|
|
523
|
+
`));
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
const outputContent = options.emmetOnly ? data.emmet : data.html;
|
|
527
|
+
if (!outputContent) {
|
|
528
|
+
console.log(pc8.red("Error: Response does not contain any synthesized code."));
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (options.output) {
|
|
532
|
+
try {
|
|
533
|
+
const absolutePath = path5.resolve(process.cwd(), options.output);
|
|
534
|
+
writeFileSync(absolutePath, outputContent, "utf8");
|
|
535
|
+
console.log(
|
|
536
|
+
pc8.green(`
|
|
537
|
+
Successfully wrote output to ${pc8.blue(options.output)}
|
|
538
|
+
`)
|
|
539
|
+
);
|
|
540
|
+
} catch (err) {
|
|
541
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
542
|
+
console.log(
|
|
543
|
+
pc8.red(`Error: Failed to write output to file. ${msg}`)
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
} else {
|
|
547
|
+
console.log(pc8.cyan("\nSynthesized Output:"));
|
|
548
|
+
console.log(outputContent);
|
|
549
|
+
console.log();
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// src/commands/preview.ts
|
|
554
|
+
import { readFileSync as readFileSync3, existsSync as existsSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
555
|
+
import path6 from "path";
|
|
556
|
+
import pc9 from "picocolors";
|
|
557
|
+
import ora3 from "ora";
|
|
558
|
+
import { preview } from "@mindfiredigital/ignix-lite-engine";
|
|
559
|
+
async function previewCommand(filePath, options) {
|
|
560
|
+
const absolutePath = path6.resolve(process.cwd(), filePath);
|
|
561
|
+
if (!existsSync3(absolutePath)) {
|
|
562
|
+
console.log(pc9.red(`Error: File not found at ${filePath}`));
|
|
563
|
+
process.exit(1);
|
|
564
|
+
}
|
|
565
|
+
const spinner = ora3(
|
|
566
|
+
"Launching headless browser and rendering preview..."
|
|
567
|
+
).start();
|
|
568
|
+
try {
|
|
569
|
+
const inputContent = readFileSync3(absolutePath, "utf8");
|
|
570
|
+
const widthNum = parseInt(options.width, 10) || 400;
|
|
571
|
+
const response = await preview({
|
|
572
|
+
input: inputContent,
|
|
573
|
+
options: {
|
|
574
|
+
width: widthNum,
|
|
575
|
+
theme: options.theme
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
if (!response.content || response.content.length === 0) {
|
|
579
|
+
spinner.fail(
|
|
580
|
+
pc9.red("Render failed: Received empty response from preview engine.")
|
|
581
|
+
);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
let result;
|
|
585
|
+
try {
|
|
586
|
+
result = JSON.parse(response.content[0].text);
|
|
587
|
+
} catch {
|
|
588
|
+
spinner.fail(pc9.red("Render failed: Invalid JSON response from preview engine."));
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
if (result.error) {
|
|
592
|
+
spinner.fail(pc9.red(`Render failed: ${result.error}`));
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
if (!result.png || typeof result.png !== "string") {
|
|
596
|
+
spinner.fail(
|
|
597
|
+
pc9.red("Render failed: Preview engine did not return base64 PNG data.")
|
|
598
|
+
);
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
const base64Data = result.png.replace(/^data:image\/png;base64,/, "");
|
|
602
|
+
const buffer = Buffer.from(base64Data, "base64");
|
|
603
|
+
const outputPath = path6.resolve(process.cwd(), options.output);
|
|
604
|
+
writeFileSync2(outputPath, buffer);
|
|
605
|
+
spinner.succeed(
|
|
606
|
+
pc9.green(
|
|
607
|
+
`Visual preview generated successfully! Saved to ${pc9.blue(options.output)}`
|
|
608
|
+
)
|
|
609
|
+
);
|
|
610
|
+
} catch (error) {
|
|
611
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
612
|
+
spinner.fail(pc9.red(`Preview failed: ${msg}`));
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// src/commands/mcp.ts
|
|
617
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync } from "fs";
|
|
618
|
+
import path7 from "path";
|
|
619
|
+
import { fileURLToPath } from "url";
|
|
620
|
+
import { spawn } from "child_process";
|
|
621
|
+
import pc10 from "picocolors";
|
|
622
|
+
import ora4 from "ora";
|
|
623
|
+
function resolveMcpServerPath() {
|
|
624
|
+
try {
|
|
625
|
+
const resolvedUrl = import.meta.resolve("@mindfiredigital/ignix-lite-mcp");
|
|
626
|
+
return fileURLToPath(resolvedUrl);
|
|
627
|
+
} catch {
|
|
628
|
+
const thisDir = path7.dirname(fileURLToPath(import.meta.url));
|
|
629
|
+
return path7.resolve(thisDir, "../../mcp/dist/server.js");
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
function readJson(filePath) {
|
|
633
|
+
try {
|
|
634
|
+
return JSON.parse(readFileSync4(filePath, "utf-8"));
|
|
635
|
+
} catch {
|
|
636
|
+
return {};
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
function writeJson(filePath, data) {
|
|
640
|
+
mkdirSync(path7.dirname(filePath), { recursive: true });
|
|
641
|
+
writeFileSync3(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
642
|
+
}
|
|
643
|
+
function mcpEntry(serverPath) {
|
|
644
|
+
return {
|
|
645
|
+
command: "node",
|
|
646
|
+
args: [serverPath]
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
function setupClaudeDesktop(serverPath) {
|
|
650
|
+
const spinner = ora4("Configuring Claude Desktop...").start();
|
|
651
|
+
try {
|
|
652
|
+
const configDir = process.platform === "win32" ? path7.join(process.env.APPDATA || "", "Claude") : path7.join(
|
|
653
|
+
process.env.HOME || "",
|
|
654
|
+
"Library",
|
|
655
|
+
"Application Support",
|
|
656
|
+
"Claude"
|
|
657
|
+
);
|
|
658
|
+
const configPath = path7.join(configDir, "claude_desktop_config.json");
|
|
659
|
+
const config = readJson(configPath);
|
|
660
|
+
config.mcpServers = config.mcpServers || {};
|
|
661
|
+
config.mcpServers["ignix-lite"] = mcpEntry(serverPath);
|
|
662
|
+
writeJson(configPath, config);
|
|
663
|
+
spinner.succeed(pc10.green("Claude Desktop configured successfully!"));
|
|
664
|
+
console.log(pc10.gray(` Config: ${configPath}`));
|
|
665
|
+
console.log(pc10.yellow("\n \u26A1 Restart Claude Desktop to apply changes.\n"));
|
|
666
|
+
} catch (err) {
|
|
667
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
668
|
+
spinner.fail(pc10.red(`Failed to configure Claude Desktop: ${msg}`));
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
function setupClaudeCode(serverPath) {
|
|
672
|
+
const spinner = ora4("Configuring Claude Code (claude CLI)...").start();
|
|
673
|
+
try {
|
|
674
|
+
const configPath = process.platform === "win32" ? path7.join(process.env.USERPROFILE || "", ".claude.json") : path7.join(process.env.HOME || "", ".claude.json");
|
|
675
|
+
const config = readJson(configPath);
|
|
676
|
+
config.mcpServers = config.mcpServers || {};
|
|
677
|
+
config.mcpServers["ignix-lite"] = mcpEntry(serverPath);
|
|
678
|
+
writeJson(configPath, config);
|
|
679
|
+
spinner.succeed(pc10.green("Claude Code configured successfully!"));
|
|
680
|
+
console.log(pc10.gray(` Config: ${configPath}`));
|
|
681
|
+
console.log(
|
|
682
|
+
pc10.yellow(
|
|
683
|
+
'\n \u26A1 Run "claude" in any project to start using Ignix-Lite tools.\n'
|
|
684
|
+
)
|
|
685
|
+
);
|
|
686
|
+
} catch (err) {
|
|
687
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
688
|
+
spinner.fail(pc10.red(`Failed to configure Claude Code: ${msg}`));
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
function setupGemini(serverPath) {
|
|
692
|
+
const spinner = ora4("Configuring Gemini CLI...").start();
|
|
693
|
+
try {
|
|
694
|
+
const configDir = process.platform === "win32" ? path7.join(process.env.USERPROFILE || "", ".gemini") : path7.join(process.env.HOME || "", ".gemini");
|
|
695
|
+
const configPath = path7.join(configDir, "settings.json");
|
|
696
|
+
const config = readJson(configPath);
|
|
697
|
+
config.mcpServers = config.mcpServers || {};
|
|
698
|
+
config.mcpServers["ignix-lite"] = mcpEntry(serverPath);
|
|
699
|
+
writeJson(configPath, config);
|
|
700
|
+
spinner.succeed(pc10.green("Gemini CLI configured successfully!"));
|
|
701
|
+
console.log(pc10.gray(` Config: ${configPath}`));
|
|
702
|
+
console.log(
|
|
703
|
+
pc10.yellow(
|
|
704
|
+
"\n \u26A1 Start a new Gemini CLI session to activate Ignix-Lite tools.\n"
|
|
705
|
+
)
|
|
706
|
+
);
|
|
707
|
+
} catch (err) {
|
|
708
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
709
|
+
spinner.fail(pc10.red(`Failed to configure Gemini CLI: ${msg}`));
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
function setupCursor(serverPath) {
|
|
713
|
+
const spinner = ora4("Configuring Cursor...").start();
|
|
714
|
+
try {
|
|
715
|
+
const configDir = process.platform === "win32" ? path7.join(process.env.USERPROFILE || "", ".cursor") : path7.join(process.env.HOME || "", ".cursor");
|
|
716
|
+
const configPath = path7.join(configDir, "mcp.json");
|
|
717
|
+
const config = readJson(configPath);
|
|
718
|
+
config.mcpServers = config.mcpServers || {};
|
|
719
|
+
config.mcpServers["ignix-lite"] = mcpEntry(serverPath);
|
|
720
|
+
writeJson(configPath, config);
|
|
721
|
+
spinner.succeed(pc10.green("Cursor configured successfully!"));
|
|
722
|
+
console.log(pc10.gray(` Config: ${configPath}`));
|
|
723
|
+
console.log(
|
|
724
|
+
pc10.yellow(
|
|
725
|
+
'\n \u26A1 Reload Cursor (Ctrl+Shift+P \u2192 "Reload Window") to activate Ignix-Lite tools.\n'
|
|
726
|
+
)
|
|
727
|
+
);
|
|
728
|
+
} catch (err) {
|
|
729
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
730
|
+
spinner.fail(pc10.red(`Failed to configure Cursor: ${msg}`));
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
var SUPPORTED_CLIENTS = [
|
|
734
|
+
"claude",
|
|
735
|
+
"claude-desktop",
|
|
736
|
+
"claude-code",
|
|
737
|
+
"gemini",
|
|
738
|
+
"cursor"
|
|
739
|
+
];
|
|
740
|
+
async function mcpSetupCommand(client) {
|
|
741
|
+
const serverPath = resolveMcpServerPath();
|
|
742
|
+
const target = (client || "").toLowerCase().trim();
|
|
743
|
+
if (!target) {
|
|
744
|
+
console.log(pc10.cyan("\nWhich client do you want to configure?\n"));
|
|
745
|
+
console.log(
|
|
746
|
+
` ${pc10.bold("claude")} Claude Desktop (auto-configure)`
|
|
747
|
+
);
|
|
748
|
+
console.log(
|
|
749
|
+
` ${pc10.bold("claude-code")} Claude Code CLI (auto-configure)`
|
|
750
|
+
);
|
|
751
|
+
console.log(
|
|
752
|
+
` ${pc10.bold("gemini")} Gemini CLI (auto-configure)`
|
|
753
|
+
);
|
|
754
|
+
console.log(
|
|
755
|
+
` ${pc10.bold("cursor")} Cursor editor (auto-configure)`
|
|
756
|
+
);
|
|
757
|
+
console.log();
|
|
758
|
+
console.log(
|
|
759
|
+
pc10.gray(
|
|
760
|
+
"Usage: ignix-lite mcp setup <client> (e.g. ignix-lite mcp setup cursor)"
|
|
761
|
+
)
|
|
762
|
+
);
|
|
763
|
+
console.log();
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
switch (target) {
|
|
767
|
+
case "claude":
|
|
768
|
+
case "claude-desktop":
|
|
769
|
+
setupClaudeDesktop(serverPath);
|
|
770
|
+
break;
|
|
771
|
+
case "claude-code":
|
|
772
|
+
setupClaudeCode(serverPath);
|
|
773
|
+
break;
|
|
774
|
+
case "gemini":
|
|
775
|
+
setupGemini(serverPath);
|
|
776
|
+
break;
|
|
777
|
+
case "cursor":
|
|
778
|
+
setupCursor(serverPath);
|
|
779
|
+
break;
|
|
780
|
+
default:
|
|
781
|
+
console.log(pc10.red(`
|
|
782
|
+
Unknown client: "${client}"
|
|
783
|
+
`));
|
|
784
|
+
console.log(`Supported clients: ${pc10.bold(SUPPORTED_CLIENTS.join(", "))}`);
|
|
785
|
+
console.log();
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
function mcpStartCommand() {
|
|
789
|
+
const serverPath = resolveMcpServerPath();
|
|
790
|
+
console.log(pc10.cyan(`
|
|
791
|
+
Starting Ignix-Lite MCP Server...`));
|
|
792
|
+
console.log(`Running: ${pc10.green("node " + serverPath)}
|
|
793
|
+
`);
|
|
794
|
+
const child = spawn("node", [serverPath], { stdio: "inherit" });
|
|
795
|
+
child.on("error", (err) => {
|
|
796
|
+
console.log(pc10.red(`
|
|
797
|
+
Failed to start MCP Server: ${err.message}`));
|
|
798
|
+
});
|
|
799
|
+
child.on("close", (code) => {
|
|
800
|
+
if (code !== 0) {
|
|
801
|
+
console.log(pc10.red(`
|
|
802
|
+
MCP Server exited with code ${code}`));
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// src/index.ts
|
|
808
|
+
var program = new Command();
|
|
809
|
+
program.name("ignix-lite").description(
|
|
810
|
+
"CLI tool for project initialization, component scaffolding, and local validation in Ignix-Lite"
|
|
811
|
+
).version("1.0.0");
|
|
812
|
+
program.command("init").description("Initialize Ignix-Lite in your project").action(initCommand);
|
|
813
|
+
program.command("theme [prompt]").description(
|
|
814
|
+
"Generate theme variables based on design prompts or primary color"
|
|
815
|
+
).option("-p, --primary <color>", "Primary color (hex/hsl) explicitly").option("-s, --style-file <path>", "Stylesheet target path").action(themeCommand);
|
|
816
|
+
program.command("add <component>").description("Add or print an Ignix-Lite component template").action(addCommand);
|
|
817
|
+
program.command("validate <file>").description("Validate a markup file against Ignix-Lite design rules").action(validateCommand);
|
|
818
|
+
program.command("check-a11y <file>").description("Audit a local markup file for WCAG accessibility issues").action(checkA11yCommand);
|
|
819
|
+
program.command("list").description("List all available Ignix-Lite components").action(listCommand);
|
|
820
|
+
program.command("info <component>").description("Show detailed manifest and guidelines for a component").action(infoCommand);
|
|
821
|
+
program.command("build <prompt>").description("Generate Ignix-Lite HTML/Emmet from a natural language prompt").option("-o, --output <file>", "Path to write the synthesized HTML output").option("-e, --emmet-only", "Output the compiled Emmet shorthand only").action(buildCommand);
|
|
822
|
+
program.command("preview <file>").description("Generate a visual PNG preview of an HTML or Emmet file").option("-o, --output <file>", "Output image destination", "preview.png").option("-w, --width <pixels>", "Viewport width", "400").option("-t, --theme <light|dark>", "Emulated color scheme theme").action(previewCommand);
|
|
823
|
+
var mcp = program.command("mcp").description("Manage the Ignix-Lite MCP server");
|
|
824
|
+
mcp.command("setup [client]").description(
|
|
825
|
+
"Configure the MCP server for an editor/client (claude, cursor, gemini)"
|
|
826
|
+
).action(mcpSetupCommand);
|
|
827
|
+
mcp.command("start").description("Start the Ignix-Lite MCP server").action(mcpStartCommand);
|
|
828
|
+
program.parse(process.argv);
|