inai-react-components 1.6.1 → 2.0.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/dist/commands/add.d.ts +7 -1
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +126 -0
- package/dist/commands/demo.d.ts +20 -0
- package/dist/commands/demo.d.ts.map +1 -0
- package/dist/commands/demo.js +15 -0
- package/dist/commands/generate.d.ts +47 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +204 -0
- package/dist/commands/init.d.ts +32 -6
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +175 -27
- package/dist/commands/list.js +1 -1
- package/dist/commands/manifest.d.ts +9 -0
- package/dist/commands/manifest.d.ts.map +1 -0
- package/dist/commands/manifest.js +89 -0
- package/dist/commands/mcp.d.ts +208 -0
- package/dist/commands/mcp.d.ts.map +1 -1
- package/dist/commands/mcp.js +511 -0
- package/dist/commands/status.d.ts +30 -0
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/theme.d.ts +32 -0
- package/dist/commands/theme.d.ts.map +1 -1
- package/dist/commands/theme.js +113 -0
- package/dist/index.js +45 -3
- package/dist/schemas/init-config.schema.json +77 -0
- package/dist/schemas/registry-item.schema.json +9 -0
- package/dist/utils/registry-resolver.d.ts +4 -0
- package/dist/utils/registry-resolver.d.ts.map +1 -1
- package/dist/utils/registry-resolver.js +35 -2
- package/dist/utils/themes.d.ts.map +1 -1
- package/dist/utils/themes.js +7 -0
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -7,6 +7,7 @@ import { cloneRegistry, getCacheDir } from "../utils/registry-resolver.js";
|
|
|
7
7
|
import { THEME_NAMES } from "../utils/themes.js";
|
|
8
8
|
import { detectFramework, frameworkLabel, getCssEntryPoint, } from "../utils/framework-detect.js";
|
|
9
9
|
import { detectProjectLayout, } from "../utils/tsconfig-detect.js";
|
|
10
|
+
import { stripRelativeJsExtensions } from "../utils/paths.js";
|
|
10
11
|
const AVAILABLE_THEMES = THEME_NAMES;
|
|
11
12
|
const FONT_PRESETS = [
|
|
12
13
|
// Sans-serif — geometric & modern
|
|
@@ -89,6 +90,90 @@ function discoverThemes(registryDir) {
|
|
|
89
90
|
}
|
|
90
91
|
return [...AVAILABLE_THEMES];
|
|
91
92
|
}
|
|
93
|
+
// User-supplied paths in `--config <init.json>` are written into the
|
|
94
|
+
// consumer project. Reject absolute paths and any `..` segment so a
|
|
95
|
+
// malicious config can't escape the project root.
|
|
96
|
+
function assertSafeRelativePath(value, field) {
|
|
97
|
+
const trimmed = value.trim();
|
|
98
|
+
if (!trimmed) {
|
|
99
|
+
throw new Error(`Invalid ${field}: must be a non-empty string`);
|
|
100
|
+
}
|
|
101
|
+
if (path.isAbsolute(trimmed)) {
|
|
102
|
+
throw new Error(`Invalid ${field}: must be a relative path, got "${trimmed}"`);
|
|
103
|
+
}
|
|
104
|
+
if (trimmed.split(/[\\/]/).includes("..")) {
|
|
105
|
+
throw new Error(`Invalid ${field}: must not contain ".." segments`);
|
|
106
|
+
}
|
|
107
|
+
return trimmed;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Load an InitConfig from a JSON file. Used by the `--config <path>` flag
|
|
111
|
+
* to skip interactive prompts in CI / automated bootstrap flows. Missing
|
|
112
|
+
* optional fields are filled with sensible defaults; invalid/missing
|
|
113
|
+
* required fields throw with a clear message.
|
|
114
|
+
*
|
|
115
|
+
* The accepted JSON schema lives at `packages/cli/src/schemas/init-config.schema.json`.
|
|
116
|
+
*/
|
|
117
|
+
export function loadInitConfigFromFile(configPath, framework, layout) {
|
|
118
|
+
const absPath = path.isAbsolute(configPath)
|
|
119
|
+
? configPath
|
|
120
|
+
: path.join(process.cwd(), configPath);
|
|
121
|
+
if (!fs.existsSync(absPath)) {
|
|
122
|
+
throw new Error(`Config file not found: ${absPath}`);
|
|
123
|
+
}
|
|
124
|
+
let raw;
|
|
125
|
+
try {
|
|
126
|
+
raw = JSON.parse(fs.readFileSync(absPath, "utf-8"));
|
|
127
|
+
}
|
|
128
|
+
catch (e) {
|
|
129
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
130
|
+
throw new Error(`Config file is not valid JSON: ${msg}`);
|
|
131
|
+
}
|
|
132
|
+
const sourceRoot = layout?.sourceRoot ?? "src";
|
|
133
|
+
const componentPath = typeof raw.componentPath === "string"
|
|
134
|
+
? assertSafeRelativePath(raw.componentPath, "componentPath")
|
|
135
|
+
: `${sourceRoot}/components/ui`;
|
|
136
|
+
const blockPath = typeof raw.blockPath === "string"
|
|
137
|
+
? assertSafeRelativePath(raw.blockPath, "blockPath")
|
|
138
|
+
: `${sourceRoot}/components/blocks`;
|
|
139
|
+
const theme = typeof raw.theme === "string" ? raw.theme : AVAILABLE_THEMES[0] ?? "inai";
|
|
140
|
+
if (!AVAILABLE_THEMES.includes(theme)) {
|
|
141
|
+
throw new Error(`Unknown theme "${theme}". Allowed: ${AVAILABLE_THEMES.join(", ")}`);
|
|
142
|
+
}
|
|
143
|
+
const fontNames = FONT_PRESETS.map((f) => f.name);
|
|
144
|
+
const fontBody = typeof raw.fontBody === "string" && fontNames.includes(raw.fontBody)
|
|
145
|
+
? raw.fontBody
|
|
146
|
+
: "outfit";
|
|
147
|
+
const fontHeading = typeof raw.fontHeading === "string" && fontNames.includes(raw.fontHeading)
|
|
148
|
+
? raw.fontHeading
|
|
149
|
+
: "outfit";
|
|
150
|
+
const radius = typeof raw.radius === "number" ? raw.radius : 0.5;
|
|
151
|
+
const tanstackRouter = raw.tanstackRouter === true;
|
|
152
|
+
const tanstackQuery = raw.tanstackQuery === true;
|
|
153
|
+
const tanstackForm = raw.tanstackForm === true;
|
|
154
|
+
const tanstackTable = raw.tanstackTable === true;
|
|
155
|
+
const resolvedFramework = raw.framework ?? framework;
|
|
156
|
+
const cssEntryPoint = typeof raw.cssEntryPoint === "string"
|
|
157
|
+
? assertSafeRelativePath(raw.cssEntryPoint, "cssEntryPoint")
|
|
158
|
+
: resolvedFramework
|
|
159
|
+
? getCssEntryPoint(resolvedFramework, sourceRoot)
|
|
160
|
+
: `${sourceRoot}/index.css`;
|
|
161
|
+
return {
|
|
162
|
+
componentPath,
|
|
163
|
+
blockPath,
|
|
164
|
+
theme,
|
|
165
|
+
fontBody,
|
|
166
|
+
fontHeading,
|
|
167
|
+
radius,
|
|
168
|
+
tanstackRouter,
|
|
169
|
+
tanstackQuery,
|
|
170
|
+
tanstackForm,
|
|
171
|
+
tanstackTable,
|
|
172
|
+
framework: resolvedFramework,
|
|
173
|
+
cssEntryPoint,
|
|
174
|
+
layout,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
92
177
|
export async function promptInitConfig(registryDir, framework, layout) {
|
|
93
178
|
const themes = discoverThemes(registryDir);
|
|
94
179
|
const sourceRoot = layout?.sourceRoot ?? "src";
|
|
@@ -351,14 +436,41 @@ function copyTokensToProject(registryDir, targetDir, theme, sourceRoot) {
|
|
|
351
436
|
}
|
|
352
437
|
return copied;
|
|
353
438
|
}
|
|
439
|
+
/**
|
|
440
|
+
* Read a source file from the registry and write it to the user's lib
|
|
441
|
+
* directory with imports normalized — `.js` suffixes stripped so the
|
|
442
|
+
* installed code reads naturally under bundler-style resolution (Vite,
|
|
443
|
+
* Next.js, TanStack Start, …). Never overwrites a pre-existing file so
|
|
444
|
+
* user customizations stay intact across re-runs.
|
|
445
|
+
*
|
|
446
|
+
* Returns the relative destination path if written, or `null` if skipped
|
|
447
|
+
* (file missing in registry, or already present in the user project).
|
|
448
|
+
*/
|
|
449
|
+
function copyLibFileNormalized(srcPath, destPath) {
|
|
450
|
+
if (!fs.existsSync(srcPath))
|
|
451
|
+
return false;
|
|
452
|
+
if (fs.existsSync(destPath))
|
|
453
|
+
return false; // don't clobber user files
|
|
454
|
+
const raw = fs.readFileSync(srcPath, "utf-8");
|
|
455
|
+
// Strip `.js` from relative sibling imports so `i18n-context.tsx`'s
|
|
456
|
+
// `./i18n-types.js` becomes `./i18n-types`. We intentionally DON'T
|
|
457
|
+
// apply `rewriteLibImports` here — these files live inside the lib
|
|
458
|
+
// directory already, so their sibling imports are correctly relative
|
|
459
|
+
// and shouldn't be rerouted through the `@/lib/*` alias.
|
|
460
|
+
const normalized = stripRelativeJsExtensions(raw);
|
|
461
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
462
|
+
fs.writeFileSync(destPath, normalized);
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
354
465
|
/**
|
|
355
466
|
* Copies the i18n base files (context, types, defaults) from the registry
|
|
356
|
-
* to
|
|
357
|
-
* i18n context; the CLI rewrites those imports at install-time
|
|
358
|
-
*
|
|
467
|
+
* to the unified lib directory. ~75 components import `useUIMessages`
|
|
468
|
+
* from the i18n context; the CLI rewrites those imports at install-time
|
|
469
|
+
* to the `aliases.lib` directory, so the files must land there.
|
|
359
470
|
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
471
|
+
* Normalizes `.js` import suffixes in the process (so the copied
|
|
472
|
+
* `i18n-context.tsx` reads without bundler-incompatible extensions) and
|
|
473
|
+
* never overwrites pre-existing files.
|
|
362
474
|
*/
|
|
363
475
|
export function copyI18nFiles(registryDir, targetDir, libDirRelative) {
|
|
364
476
|
if (!registryDir)
|
|
@@ -366,20 +478,31 @@ export function copyI18nFiles(registryDir, targetDir, libDirRelative) {
|
|
|
366
478
|
const files = ["i18n-context.tsx", "i18n-types.ts", "i18n-defaults.ts"];
|
|
367
479
|
const srcBase = path.join(registryDir, "packages", "ui", "src", "lib");
|
|
368
480
|
const destBase = path.join(targetDir, libDirRelative);
|
|
369
|
-
fs.mkdirSync(destBase, { recursive: true });
|
|
370
481
|
const copied = [];
|
|
371
482
|
for (const file of files) {
|
|
372
483
|
const src = path.join(srcBase, file);
|
|
373
|
-
if (!fs.existsSync(src))
|
|
374
|
-
continue;
|
|
375
484
|
const dest = path.join(destBase, file);
|
|
376
|
-
if (
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
copied.push(`${libDirRelative}/${file}`);
|
|
485
|
+
if (copyLibFileNormalized(src, dest)) {
|
|
486
|
+
copied.push(`${libDirRelative}/${file}`);
|
|
487
|
+
}
|
|
380
488
|
}
|
|
381
489
|
return copied;
|
|
382
490
|
}
|
|
491
|
+
/**
|
|
492
|
+
* Copies the canonical `cn.ts` from the cached registry to the user's
|
|
493
|
+
* lib directory. We copy the REAL source file (not a hardcoded template)
|
|
494
|
+
* because components import extra type exports like `ClassNames` that
|
|
495
|
+
* any minimal template would miss. Returns `true` if the file was
|
|
496
|
+
* written, `false` if the registry copy was missing or a user file was
|
|
497
|
+
* already present.
|
|
498
|
+
*/
|
|
499
|
+
export function copyCnFile(registryDir, targetDir, libDirRelative) {
|
|
500
|
+
if (!registryDir)
|
|
501
|
+
return false;
|
|
502
|
+
const src = path.join(registryDir, "packages", "ui", "src", "lib", "cn.ts");
|
|
503
|
+
const dest = path.join(targetDir, libDirRelative, "cn.ts");
|
|
504
|
+
return copyLibFileNormalized(src, dest);
|
|
505
|
+
}
|
|
383
506
|
export async function runInit(config, targetDir, repoUrl) {
|
|
384
507
|
const filesCreated = [];
|
|
385
508
|
const sourceRoot = config.layout?.sourceRoot ?? "src";
|
|
@@ -393,27 +516,30 @@ export async function runInit(config, targetDir, repoUrl) {
|
|
|
393
516
|
fs.mkdirSync(componentDir, { recursive: true });
|
|
394
517
|
const blockDir = path.join(targetDir, config.blockPath);
|
|
395
518
|
fs.mkdirSync(blockDir, { recursive: true });
|
|
396
|
-
// 3. Create the unified lib directory under `<sourceRoot>/lib
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
// `
|
|
400
|
-
//
|
|
401
|
-
//
|
|
519
|
+
// 3. Create the unified lib directory under `<sourceRoot>/lib/`. Every
|
|
520
|
+
// component/block lib file ships here; the CLI rewrites all imports
|
|
521
|
+
// at install-time to point at the `aliases.lib` alias so there's a
|
|
522
|
+
// single source of truth. `cn.ts` is seeded here so `init` users
|
|
523
|
+
// can start writing app code immediately — the real file (not a
|
|
524
|
+
// hardcoded template) is copied from the registry so secondary
|
|
525
|
+
// exports like `ClassNames` stay available to components. Never
|
|
526
|
+
// overwrite existing user files.
|
|
402
527
|
const libDirRelative = `${sourceRoot}/lib`;
|
|
403
528
|
const libDir = path.join(targetDir, libDirRelative);
|
|
404
529
|
fs.mkdirSync(libDir, { recursive: true });
|
|
405
|
-
const cnPath = path.join(libDir, "cn.ts");
|
|
406
|
-
if (!fs.existsSync(cnPath)) {
|
|
407
|
-
fs.writeFileSync(cnPath, getCnTemplate());
|
|
408
|
-
filesCreated.push(`${libDirRelative}/cn.ts`);
|
|
409
|
-
}
|
|
410
530
|
// 4. Copy tokens and create CSS
|
|
411
531
|
const cssRelPath = config.cssEntryPoint ?? `${sourceRoot}/index.css`;
|
|
412
532
|
const cssPath = path.join(targetDir, cssRelPath);
|
|
413
533
|
fs.mkdirSync(path.dirname(cssPath), { recursive: true });
|
|
414
534
|
if (repoUrl) {
|
|
415
|
-
// Remote mode: copy tokens from cached
|
|
535
|
+
// Remote mode: copy tokens and canonical lib files from the cached
|
|
536
|
+
// registry so users get the real `cn.ts` (including type exports
|
|
537
|
+
// like `ClassNames` that downstream components import) rather than
|
|
538
|
+
// a minimal hardcoded template.
|
|
416
539
|
const registryDir = getCacheDir();
|
|
540
|
+
if (copyCnFile(registryDir, targetDir, libDirRelative)) {
|
|
541
|
+
filesCreated.push(`${libDirRelative}/cn.ts`);
|
|
542
|
+
}
|
|
417
543
|
const tokenFiles = copyTokensToProject(registryDir, targetDir, config.theme, sourceRoot);
|
|
418
544
|
filesCreated.push(...tokenFiles);
|
|
419
545
|
// Copy i18n base files to the unified lib directory so that components'
|
|
@@ -424,13 +550,21 @@ export async function runInit(config, targetDir, repoUrl) {
|
|
|
424
550
|
fs.writeFileSync(cssPath, getLocalTailwindCssTemplate(config.theme, config.fontBody, config.fontHeading, config.radius));
|
|
425
551
|
}
|
|
426
552
|
else {
|
|
427
|
-
// Local mode: reference @company/tokens package
|
|
553
|
+
// Local mode: reference @company/tokens package. Without a registry
|
|
554
|
+
// to read from, we seed `cn.ts` with a minimal template — users in
|
|
555
|
+
// this mode are almost always developing the monorepo itself, where
|
|
556
|
+
// the real `@company/ui` package is resolvable via workspace links.
|
|
557
|
+
const cnPath = path.join(libDir, "cn.ts");
|
|
558
|
+
if (!fs.existsSync(cnPath)) {
|
|
559
|
+
fs.writeFileSync(cnPath, getCnTemplate());
|
|
560
|
+
filesCreated.push(`${libDirRelative}/cn.ts`);
|
|
561
|
+
}
|
|
428
562
|
fs.writeFileSync(cssPath, getLegacyTailwindCssTemplate(config.theme, config.fontBody, config.fontHeading, config.radius));
|
|
429
563
|
}
|
|
430
564
|
filesCreated.push(cssRelPath);
|
|
431
565
|
return { success: true, filesCreated };
|
|
432
566
|
}
|
|
433
|
-
export async function initCommand(repoUrl) {
|
|
567
|
+
export async function initCommand(repoUrl, options = {}) {
|
|
434
568
|
console.log(chalk.bold("\nInAI UI - Project Initialization\n"));
|
|
435
569
|
const cwd = process.cwd();
|
|
436
570
|
const detectedFramework = detectFramework(cwd);
|
|
@@ -452,7 +586,21 @@ export async function initCommand(repoUrl) {
|
|
|
452
586
|
}
|
|
453
587
|
}
|
|
454
588
|
const registryDir = repoUrl ? getCacheDir() : undefined;
|
|
455
|
-
|
|
589
|
+
let config;
|
|
590
|
+
if (options.config) {
|
|
591
|
+
try {
|
|
592
|
+
config = loadInitConfigFromFile(options.config, detectedFramework, detectedLayout);
|
|
593
|
+
console.log(chalk.dim(`Loaded config from ${options.config} (skipping prompts)`));
|
|
594
|
+
}
|
|
595
|
+
catch (err) {
|
|
596
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
597
|
+
console.error(chalk.red(`\nFailed to load config: ${message}`));
|
|
598
|
+
process.exit(1);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
config = await promptInitConfig(registryDir, detectedFramework, detectedLayout);
|
|
603
|
+
}
|
|
456
604
|
if (!config) {
|
|
457
605
|
console.log(chalk.yellow("\nInitialization cancelled."));
|
|
458
606
|
process.exit(0);
|
package/dist/commands/list.js
CHANGED
|
@@ -11,7 +11,7 @@ function padRight(str, len) {
|
|
|
11
11
|
export function formatComponentTable(components, filterCategory) {
|
|
12
12
|
let filtered = components;
|
|
13
13
|
if (filterCategory) {
|
|
14
|
-
filtered = components.filter((c) => c.
|
|
14
|
+
filtered = components.filter((c) => (c.category ?? "").toLowerCase() === filterCategory.toLowerCase());
|
|
15
15
|
}
|
|
16
16
|
if (filtered.length === 0) {
|
|
17
17
|
if (filterCategory) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface ManifestOptions {
|
|
2
|
+
/** Output as JSON instead of a human-readable table. */
|
|
3
|
+
json?: boolean;
|
|
4
|
+
/** Only show entries where the installed version differs from the
|
|
5
|
+
* current registry version. */
|
|
6
|
+
outdated?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function manifestCommand(options?: ManifestOptions): Promise<void>;
|
|
9
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/commands/manifest.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,eAAe;IAC9B,wDAAwD;IACxD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf;oCACgC;IAChC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAuGD,wBAAsB,eAAe,CACnC,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,IAAI,CAAC,CAiDf"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { readComponentsJson, readRegistryJson, } from "./status.js";
|
|
4
|
+
import { resolveRegistryDir } from "../utils/registry-resolver.js";
|
|
5
|
+
function buildEntries(installed, registryVersion) {
|
|
6
|
+
return installed
|
|
7
|
+
.map((c) => ({
|
|
8
|
+
name: c.name,
|
|
9
|
+
installedVersion: c.version,
|
|
10
|
+
installedAt: c.installedAt,
|
|
11
|
+
registryVersion,
|
|
12
|
+
outdated: registryVersion !== null && c.version !== registryVersion,
|
|
13
|
+
}))
|
|
14
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
15
|
+
}
|
|
16
|
+
function renderHumanReadable(componentsJson, entries, registryVersion) {
|
|
17
|
+
const lines = [];
|
|
18
|
+
lines.push(chalk.bold("\nInAI UI — Component Manifest\n"));
|
|
19
|
+
if (componentsJson.registrySource) {
|
|
20
|
+
lines.push(chalk.dim(`Registry: ${componentsJson.registrySource}`));
|
|
21
|
+
}
|
|
22
|
+
if (registryVersion) {
|
|
23
|
+
lines.push(chalk.dim(`Registry version: ${registryVersion}`));
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
lines.push(chalk.dim("Registry version: unknown (no cached registry.json — run `inai-ui status` to refresh)"));
|
|
27
|
+
}
|
|
28
|
+
lines.push("");
|
|
29
|
+
if (entries.length === 0) {
|
|
30
|
+
lines.push(chalk.yellow("No components installed yet. Run `inai-ui add <name>` to install one."));
|
|
31
|
+
return lines.join("\n");
|
|
32
|
+
}
|
|
33
|
+
const nameWidth = Math.max(...entries.map((e) => e.name.length), 4) + 2;
|
|
34
|
+
const versionWidth = Math.max(...entries.map((e) => e.installedVersion.length), 7);
|
|
35
|
+
const dateWidth = 10;
|
|
36
|
+
lines.push(chalk.bold(`${"Name".padEnd(nameWidth)}${"Version".padEnd(versionWidth + 2)}${"Installed".padEnd(dateWidth + 2)}Status`));
|
|
37
|
+
lines.push(chalk.dim("─".repeat(nameWidth) +
|
|
38
|
+
"─".repeat(versionWidth + 2) +
|
|
39
|
+
"─".repeat(dateWidth + 2) +
|
|
40
|
+
"──────"));
|
|
41
|
+
for (const e of entries) {
|
|
42
|
+
const status = e.outdated
|
|
43
|
+
? chalk.yellow(`outdated → ${e.registryVersion}`)
|
|
44
|
+
: chalk.green("up-to-date");
|
|
45
|
+
lines.push(`${e.name.padEnd(nameWidth)}${e.installedVersion.padEnd(versionWidth + 2)}${e.installedAt.padEnd(dateWidth + 2)}${status}`);
|
|
46
|
+
}
|
|
47
|
+
const outdatedCount = entries.filter((e) => e.outdated).length;
|
|
48
|
+
lines.push("");
|
|
49
|
+
if (outdatedCount > 0) {
|
|
50
|
+
lines.push(chalk.yellow(`${outdatedCount}/${entries.length} components are outdated. Run \`inai-ui update <name>\` or \`inai-ui update --all\` to upgrade.`));
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
lines.push(chalk.green(`All ${entries.length} components are up to date.`));
|
|
54
|
+
}
|
|
55
|
+
return lines.join("\n");
|
|
56
|
+
}
|
|
57
|
+
export async function manifestCommand(options = {}) {
|
|
58
|
+
const rootDir = process.cwd();
|
|
59
|
+
const componentsJson = readComponentsJson(rootDir);
|
|
60
|
+
if (!componentsJson) {
|
|
61
|
+
console.error(chalk.red("\nNo components.json found. Run `inai-ui init` first to initialize the project."));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
const installed = componentsJson.installedComponents ?? [];
|
|
65
|
+
// Resolve the registry to read its current version (best-effort; if it
|
|
66
|
+
// fails we still print installed versions, just without a comparison).
|
|
67
|
+
let registryVersion = null;
|
|
68
|
+
try {
|
|
69
|
+
const registryDir = resolveRegistryDir(rootDir);
|
|
70
|
+
const registryJson = readRegistryJson(path.join(registryDir, "registry.json"));
|
|
71
|
+
registryVersion = registryJson?.version ?? null;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Offline / no registry cache yet — fine, just skip the comparison.
|
|
75
|
+
}
|
|
76
|
+
let entries = buildEntries(installed, registryVersion);
|
|
77
|
+
if (options.outdated) {
|
|
78
|
+
entries = entries.filter((e) => e.outdated);
|
|
79
|
+
}
|
|
80
|
+
if (options.json) {
|
|
81
|
+
console.log(JSON.stringify({
|
|
82
|
+
registrySource: componentsJson.registrySource ?? null,
|
|
83
|
+
registryVersion,
|
|
84
|
+
installed: entries,
|
|
85
|
+
}, null, 2));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
console.log(renderHumanReadable(componentsJson, entries, registryVersion));
|
|
89
|
+
}
|
package/dist/commands/mcp.d.ts
CHANGED
|
@@ -16,6 +16,15 @@ export declare function handleListComponents(args: {
|
|
|
16
16
|
export declare function handleViewComponent(args: {
|
|
17
17
|
name?: string;
|
|
18
18
|
} | undefined, cwd: string): Promise<ToolTextResult>;
|
|
19
|
+
/**
|
|
20
|
+
* Read the prop schema for a component from the registry's generated
|
|
21
|
+
* `props-data.json` (produced by `scripts/extract-props.ts`). Returns
|
|
22
|
+
* the schema as JSON text so AI assistants can validate props before
|
|
23
|
+
* generating JSX.
|
|
24
|
+
*/
|
|
25
|
+
export declare function handleViewComponentSchema(args: {
|
|
26
|
+
name?: string;
|
|
27
|
+
} | undefined, cwd: string): Promise<ToolTextResult>;
|
|
19
28
|
export declare function handleAddComponent(args: {
|
|
20
29
|
name?: string;
|
|
21
30
|
force?: boolean;
|
|
@@ -30,6 +39,92 @@ export declare function handleListThemes(): Promise<ToolTextResult>;
|
|
|
30
39
|
export declare function handleStatus(args: {
|
|
31
40
|
json?: boolean;
|
|
32
41
|
} | undefined, cwd: string): Promise<ToolTextResult>;
|
|
42
|
+
/**
|
|
43
|
+
* Parse the moonshots registry source file as plain text. We intentionally
|
|
44
|
+
* do *not* `import` the TS file here — the CLI is compiled ahead of time
|
|
45
|
+
* and cannot transitively load UI sources. Regex is enough because the
|
|
46
|
+
* file has a stable `{ id: N, slug: "…", title: "…", description: "…",
|
|
47
|
+
* status: "…", category: "…" }` shape.
|
|
48
|
+
*/
|
|
49
|
+
export interface MoonshotSummary {
|
|
50
|
+
id: number;
|
|
51
|
+
slug: string;
|
|
52
|
+
title: string;
|
|
53
|
+
description: string;
|
|
54
|
+
status: string;
|
|
55
|
+
category?: string;
|
|
56
|
+
components: string[];
|
|
57
|
+
demoPath?: string;
|
|
58
|
+
reducedMotionStrategy?: string;
|
|
59
|
+
}
|
|
60
|
+
export declare function parseMoonshotsFile(sourceText: string): MoonshotSummary[];
|
|
61
|
+
export declare function handleListMoonshots(args: {
|
|
62
|
+
status?: string;
|
|
63
|
+
category?: string;
|
|
64
|
+
} | undefined, cwd: string): Promise<ToolTextResult>;
|
|
65
|
+
export interface ScaffoldedPage {
|
|
66
|
+
targetPath: string;
|
|
67
|
+
source: string;
|
|
68
|
+
blocks: {
|
|
69
|
+
name: string;
|
|
70
|
+
slug: string;
|
|
71
|
+
score: number;
|
|
72
|
+
}[];
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Given a brief and a target path, compose a React `.tsx` page that
|
|
76
|
+
* imports the top-matching blocks from the local registry and renders
|
|
77
|
+
* them in order. Used by the MCP `scaffold_page` tool so an agent can
|
|
78
|
+
* drop a proposal page into the current project during a live client
|
|
79
|
+
* demo without hand-typing import boilerplate.
|
|
80
|
+
*
|
|
81
|
+
* Exported for unit tests; the CLI command wraps it.
|
|
82
|
+
*/
|
|
83
|
+
export declare function scaffoldPageSource(brief: string, blocks: Array<{
|
|
84
|
+
name: string;
|
|
85
|
+
type: string;
|
|
86
|
+
description: string;
|
|
87
|
+
}>): string;
|
|
88
|
+
/**
|
|
89
|
+
* Wrap the CLI's `runThemeFromHex` so an agent can generate a layered
|
|
90
|
+
* brand override from a single hex value during a live demo. Writes
|
|
91
|
+
* the CSS layer under `packages/tokens/src/themes/<name>.css` (when
|
|
92
|
+
* invoked from the monorepo root) or a project-local themes dir.
|
|
93
|
+
*/
|
|
94
|
+
export declare function handleGenerateTheme(args: {
|
|
95
|
+
name?: string;
|
|
96
|
+
hex?: string;
|
|
97
|
+
} | undefined, cwd: string): Promise<ToolTextResult>;
|
|
98
|
+
/**
|
|
99
|
+
* Seed a state file that the docs app's `LiveThemeSwitcher` picks up on
|
|
100
|
+
* boot, so an agent can say "use the brand theme we just generated" and
|
|
101
|
+
* the next `pnpm --filter docs dev` run opens already themed.
|
|
102
|
+
*
|
|
103
|
+
* Implementation: writes `{ theme: "<slug>" }` JSON to `.inai/state.json`
|
|
104
|
+
* in the current project (matches the convention used by other CLI
|
|
105
|
+
* commands). Non-destructive: merges with existing state if present.
|
|
106
|
+
*/
|
|
107
|
+
export declare function handleApplyTheme(args: {
|
|
108
|
+
name?: string;
|
|
109
|
+
} | undefined, cwd: string): Promise<ToolTextResult>;
|
|
110
|
+
/**
|
|
111
|
+
* Spawn `scripts/capture-moonshots.mjs` for the agent. Streams stdout
|
|
112
|
+
* + stderr back through the MCP result. Intended for the case where
|
|
113
|
+
* the team wants fresh video loops without dropping to the terminal.
|
|
114
|
+
*/
|
|
115
|
+
export declare function handleCaptureMoonshots(args: {
|
|
116
|
+
slug?: string;
|
|
117
|
+
force?: boolean;
|
|
118
|
+
} | undefined, cwd: string): Promise<ToolTextResult>;
|
|
119
|
+
export declare function handleScaffoldPage(args: {
|
|
120
|
+
brief?: string;
|
|
121
|
+
target_path?: string;
|
|
122
|
+
limit?: number;
|
|
123
|
+
} | undefined, cwd: string): Promise<ToolTextResult>;
|
|
124
|
+
export declare function handleSuggestBlocks(args: {
|
|
125
|
+
brief?: string;
|
|
126
|
+
limit?: number;
|
|
127
|
+
} | undefined, cwd: string): Promise<ToolTextResult>;
|
|
33
128
|
export declare const MCP_TOOLS: readonly [{
|
|
34
129
|
readonly name: "list_components";
|
|
35
130
|
readonly description: "List all available components, blocks and templates in the InAI UI registry.";
|
|
@@ -55,6 +150,19 @@ export declare const MCP_TOOLS: readonly [{
|
|
|
55
150
|
};
|
|
56
151
|
readonly required: readonly ["name"];
|
|
57
152
|
};
|
|
153
|
+
}, {
|
|
154
|
+
readonly name: "view_component_schema";
|
|
155
|
+
readonly description: "Return the prop schema (name, type, required, description, default) for a component. Use before generating JSX so prop names, variants and types are correct without reading the source.";
|
|
156
|
+
readonly inputSchema: {
|
|
157
|
+
readonly type: "object";
|
|
158
|
+
readonly properties: {
|
|
159
|
+
readonly name: {
|
|
160
|
+
readonly type: "string";
|
|
161
|
+
readonly description: "Component name";
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
readonly required: readonly ["name"];
|
|
165
|
+
};
|
|
58
166
|
}, {
|
|
59
167
|
readonly name: "add_component";
|
|
60
168
|
readonly description: "Install a component (and its transitive registry dependencies) into the current project.";
|
|
@@ -117,6 +225,106 @@ export declare const MCP_TOOLS: readonly [{
|
|
|
117
225
|
};
|
|
118
226
|
};
|
|
119
227
|
};
|
|
228
|
+
}, {
|
|
229
|
+
readonly name: "list_moonshots";
|
|
230
|
+
readonly description: "List the creative signature moonshots registered in InAI UI (kinetic typography, gradient mesh, magnetic cursor, decrypt text, etc.). Filter by status ('stable' | 'experimental') and/or category ('interaction' | 'typography' | 'layout' | 'dataviz' | 'color' | 'ambient'). Returns JSON metadata including reduced-motion strategy so the agent can choose moonshots responsibly.";
|
|
231
|
+
readonly inputSchema: {
|
|
232
|
+
readonly type: "object";
|
|
233
|
+
readonly properties: {
|
|
234
|
+
readonly status: {
|
|
235
|
+
readonly type: "string";
|
|
236
|
+
readonly description: "Filter by maturity: 'stable' (default returns all).";
|
|
237
|
+
};
|
|
238
|
+
readonly category: {
|
|
239
|
+
readonly type: "string";
|
|
240
|
+
readonly description: "Filter by thematic category: interaction, typography, layout, dataviz, color, ambient.";
|
|
241
|
+
};
|
|
242
|
+
};
|
|
243
|
+
};
|
|
244
|
+
}, {
|
|
245
|
+
readonly name: "suggest_blocks";
|
|
246
|
+
readonly description: "Given a free-text brief (e.g. 'fintech dashboard with live KPIs and dark hero'), rank the blocks and templates in registry.json by keyword overlap and return the top matches. Use this to scaffold a client proposal page without reading the entire registry.";
|
|
247
|
+
readonly inputSchema: {
|
|
248
|
+
readonly type: "object";
|
|
249
|
+
readonly properties: {
|
|
250
|
+
readonly brief: {
|
|
251
|
+
readonly type: "string";
|
|
252
|
+
readonly description: "Short description of the desired page or section.";
|
|
253
|
+
};
|
|
254
|
+
readonly limit: {
|
|
255
|
+
readonly type: "number";
|
|
256
|
+
readonly description: "Max number of suggestions. Defaults to 6.";
|
|
257
|
+
};
|
|
258
|
+
};
|
|
259
|
+
readonly required: readonly ["brief"];
|
|
260
|
+
};
|
|
261
|
+
}, {
|
|
262
|
+
readonly name: "scaffold_page";
|
|
263
|
+
readonly description: "Compose a fresh React .tsx page that imports and renders the top blocks matching a brief. Writes the file to `target_path` (relative to cwd) and returns a Markdown summary. Pair with `suggest_blocks` if the caller wants to preview the picks before writing anything.";
|
|
264
|
+
readonly inputSchema: {
|
|
265
|
+
readonly type: "object";
|
|
266
|
+
readonly properties: {
|
|
267
|
+
readonly brief: {
|
|
268
|
+
readonly type: "string";
|
|
269
|
+
readonly description: "Short description of the desired page (e.g. 'fintech landing with pricing and stats').";
|
|
270
|
+
};
|
|
271
|
+
readonly target_path: {
|
|
272
|
+
readonly type: "string";
|
|
273
|
+
readonly description: "Relative path where the generated .tsx file will be written (e.g. 'src/pages/client-x-landing.tsx'). Must not exist yet.";
|
|
274
|
+
};
|
|
275
|
+
readonly limit: {
|
|
276
|
+
readonly type: "number";
|
|
277
|
+
readonly description: "Max number of blocks to compose. Defaults to 6, capped at 12.";
|
|
278
|
+
};
|
|
279
|
+
};
|
|
280
|
+
readonly required: readonly ["brief", "target_path"];
|
|
281
|
+
};
|
|
282
|
+
}, {
|
|
283
|
+
readonly name: "generate_theme";
|
|
284
|
+
readonly description: "Generate a brand override theme from a single hex color. Uses the same hex → OKLCH pipeline as ThemeStudio and the live theme switcher, and writes a layered `.css` file under `packages/tokens/src/themes/` that should be imported after a base theme.";
|
|
285
|
+
readonly inputSchema: {
|
|
286
|
+
readonly type: "object";
|
|
287
|
+
readonly properties: {
|
|
288
|
+
readonly name: {
|
|
289
|
+
readonly type: "string";
|
|
290
|
+
readonly description: "Theme slug, used as the CSS filename (e.g. 'client-orbis').";
|
|
291
|
+
};
|
|
292
|
+
readonly hex: {
|
|
293
|
+
readonly type: "string";
|
|
294
|
+
readonly description: "Brand hex color, e.g. '#6b46c1'.";
|
|
295
|
+
};
|
|
296
|
+
};
|
|
297
|
+
readonly required: readonly ["name", "hex"];
|
|
298
|
+
};
|
|
299
|
+
}, {
|
|
300
|
+
readonly name: "apply_theme";
|
|
301
|
+
readonly description: "Seed `.inai/state.json` with the active theme slug so the next docs app boot picks it up automatically. Non-destructive: merges with existing state. The live theme switcher also honours this state on mount.";
|
|
302
|
+
readonly inputSchema: {
|
|
303
|
+
readonly type: "object";
|
|
304
|
+
readonly properties: {
|
|
305
|
+
readonly name: {
|
|
306
|
+
readonly type: "string";
|
|
307
|
+
readonly description: "Theme slug to activate (must already exist).";
|
|
308
|
+
};
|
|
309
|
+
};
|
|
310
|
+
readonly required: readonly ["name"];
|
|
311
|
+
};
|
|
312
|
+
}, {
|
|
313
|
+
readonly name: "capture_moonshots";
|
|
314
|
+
readonly description: "Run `scripts/capture-moonshots.mjs` to regenerate the WebM loops consumed by the gallery. Accepts an optional `slug` filter and a `force` flag to re-record already-captured moonshots. Returns the script's stdout/stderr.";
|
|
315
|
+
readonly inputSchema: {
|
|
316
|
+
readonly type: "object";
|
|
317
|
+
readonly properties: {
|
|
318
|
+
readonly slug: {
|
|
319
|
+
readonly type: "string";
|
|
320
|
+
readonly description: "Optional moonshot slug to record in isolation (e.g. 'gradient-mesh-live').";
|
|
321
|
+
};
|
|
322
|
+
readonly force: {
|
|
323
|
+
readonly type: "boolean";
|
|
324
|
+
readonly description: "Re-capture slugs that already have a video. Defaults to false.";
|
|
325
|
+
};
|
|
326
|
+
};
|
|
327
|
+
};
|
|
120
328
|
}];
|
|
121
329
|
export declare function runMcp(cwd?: string): Promise<void>;
|
|
122
330
|
export declare function mcpCommand(subcommand?: "init"): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../../src/commands/mcp.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../../src/commands/mcp.ts"],"names":[],"mappings":"AAkCA;;;GAGG;AACH,wBAAgB,cAAc,IAAI,IAAI,CAErC;AAID,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/C,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AASD,wBAAsB,oBAAoB,CACxC,IAAI,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,EACvC,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,cAAc,CAAC,CAKzB;AAED,wBAAsB,mBAAmB,CACvC,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,EACnC,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,cAAc,CAAC,CA8BzB;AAED;;;;;GAKG;AACH,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,EACnC,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,cAAc,CAAC,CAmDzB;AAED,wBAAsB,kBAAkB,CACtC,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,EACpD,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,cAAc,CAAC,CAkBzB;AAED,wBAAsB,mBAAmB,CACvC,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,EACnC,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,cAAc,CAAC,CAmCzB;AAED,wBAAsB,qBAAqB,CACzC,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,EACnC,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,cAAc,CAAC,CAgBzB;AAED,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,cAAc,CAAC,CAEhE;AAED,wBAAsB,YAAY,CAChC,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,EACpC,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,cAAc,CAAC,CAGzB;AAID;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAaD,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,eAAe,EAAE,CAqCxE;AAED,wBAAsB,mBAAmB,CACvC,IAAI,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,EACxD,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,cAAc,CAAC,CAqBzB;AAsCD,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACzD;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC,GACjE,MAAM,CA2BR;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,EACjD,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,cAAc,CAAC,CA2BzB;AAED;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,EACnC,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,cAAc,CAAC,CAiCzB;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,EACpD,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,cAAc,CAAC,CA0CzB;AAED,wBAAsB,kBAAkB,CACtC,IAAI,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,EAC1E,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,cAAc,CAAC,CAqFzB;AAED,wBAAsB,mBAAmB,CACvC,IAAI,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,EACpD,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,cAAc,CAAC,CA0CzB;AAID,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyNZ,CAAC;AAEX,wBAAsB,MAAM,CAAC,GAAG,GAAE,MAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsGvE;AAED,wBAAsB,UAAU,CAC9B,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC,CAMf"}
|