inai-react-components 1.6.2 → 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 +17 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +100 -2
- 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/add.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type RegistryComponent, type ComponentsJsonFile } from "./status.js";
|
|
1
|
+
import { type RegistryComponent, type RegistryJson, type RegistryTheme, type ComponentsJsonFile } from "./status.js";
|
|
2
2
|
export interface AddOptions {
|
|
3
3
|
/** Overwrite existing files without prompting. */
|
|
4
4
|
force?: boolean;
|
|
@@ -53,6 +53,12 @@ export declare function findInRegistry(registry: {
|
|
|
53
53
|
blocks: RegistryComponent[];
|
|
54
54
|
templates: RegistryComponent[];
|
|
55
55
|
}, name: string, prefix: string | null): RegistryComponent | null;
|
|
56
|
+
/**
|
|
57
|
+
* Find a theme in the registry by name. Themes live in a separate list
|
|
58
|
+
* from components because they are plain CSS files — no hook deps, no
|
|
59
|
+
* snapshot pipeline, no import rewriting.
|
|
60
|
+
*/
|
|
61
|
+
export declare function findThemeInRegistry(registry: RegistryJson, name: string): RegistryTheme | null;
|
|
56
62
|
/**
|
|
57
63
|
* Determine the target directory for a component based on its type and the
|
|
58
64
|
* project's aliases + layout from `components.json`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"add.d.ts","sourceRoot":"","sources":["../../src/commands/add.ts"],"names":[],"mappings":"AAKA,OAAO,EAGL,KAAK,iBAAiB,
|
|
1
|
+
{"version":3,"file":"add.d.ts","sourceRoot":"","sources":["../../src/commands/add.ts"],"names":[],"mappings":"AAKA,OAAO,EAGL,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EAExB,MAAM,aAAa,CAAC;AA0BrB,MAAM,WAAW,UAAU;IACzB,kDAAkD;IAClD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,2CAA2C;IAC3C,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,kDAAkD;IAClD,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,oDAAoD;IACpD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kDAAkD;IAClD,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,2DAA2D;IAC3D,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,4DAA4D;IAC5D,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,kEAAkE;IAClE,WAAW,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CACjD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG;IAChD,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;CACd,CAyCA;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE;IAAE,UAAU,EAAE,iBAAiB,EAAE,CAAC;IAAC,MAAM,EAAE,iBAAiB,EAAE,CAAC;IAAC,SAAS,EAAE,iBAAiB,EAAE,CAAA;CAAE,EAC1G,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GAAG,IAAI,GACpB,iBAAiB,GAAG,IAAI,CAc1B;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,YAAY,EACtB,IAAI,EAAE,MAAM,GACX,aAAa,GAAG,IAAI,CAGtB;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,SAAS,EAAE,iBAAiB,EAC5B,cAAc,EAAE,kBAAkB,GACjC,MAAM,CAiBR;AA8XD,wBAAsB,MAAM,CAC1B,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,UAAe,GACvB,OAAO,CAAC,SAAS,CAAC,CA0NpB;AAqCD,MAAM,WAAW,iBAAkB,SAAQ,UAAU;IACnD,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,wBAAsB,UAAU,CAC9B,aAAa,CAAC,EAAE,MAAM,EACtB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,IAAI,CAAC,CAqIf"}
|
package/dist/commands/add.js
CHANGED
|
@@ -46,6 +46,10 @@ export function parseComponentSpec(spec) {
|
|
|
46
46
|
prefix = "template";
|
|
47
47
|
name = rest.slice(9).trim();
|
|
48
48
|
}
|
|
49
|
+
else if (rest.startsWith("theme/")) {
|
|
50
|
+
prefix = "theme";
|
|
51
|
+
name = rest.slice(6).trim();
|
|
52
|
+
}
|
|
49
53
|
else {
|
|
50
54
|
name = rest;
|
|
51
55
|
}
|
|
@@ -75,6 +79,16 @@ export function findInRegistry(registry, name, prefix) {
|
|
|
75
79
|
];
|
|
76
80
|
return allItems.find((c) => c.name === name) ?? null;
|
|
77
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Find a theme in the registry by name. Themes live in a separate list
|
|
84
|
+
* from components because they are plain CSS files — no hook deps, no
|
|
85
|
+
* snapshot pipeline, no import rewriting.
|
|
86
|
+
*/
|
|
87
|
+
export function findThemeInRegistry(registry, name) {
|
|
88
|
+
if (!registry.themes)
|
|
89
|
+
return null;
|
|
90
|
+
return registry.themes.find((t) => t.name === name) ?? null;
|
|
91
|
+
}
|
|
78
92
|
/**
|
|
79
93
|
* Determine the target directory for a component based on its type and the
|
|
80
94
|
* project's aliases + layout from `components.json`.
|
|
@@ -339,6 +353,96 @@ async function installOne(component, registry, componentsJson, registryDir, root
|
|
|
339
353
|
skipped: skippedFiles,
|
|
340
354
|
};
|
|
341
355
|
}
|
|
356
|
+
/**
|
|
357
|
+
* Install a single theme preset (plain CSS file). Themes bypass the
|
|
358
|
+
* component pipeline entirely — no hook deps, no snapshot, no import
|
|
359
|
+
* rewriting. The destination directory is derived from the user's
|
|
360
|
+
* components.json aliases, defaulting to `<sourceRoot>/styles/themes/`.
|
|
361
|
+
*/
|
|
362
|
+
async function installTheme(theme, componentsJson, registryDir, rootDir, options) {
|
|
363
|
+
const name = theme.name;
|
|
364
|
+
// Destination: `<sourceRoot>/styles/themes/` by convention. The v2.0
|
|
365
|
+
// `components.json` schema doesn't yet expose `aliases.themes` — when
|
|
366
|
+
// it eventually does, read it here. For now use the source root.
|
|
367
|
+
const sourceRoot = getSourceRoot(componentsJson);
|
|
368
|
+
const themesDir = `${sourceRoot}/styles/themes`;
|
|
369
|
+
const copiedFiles = [];
|
|
370
|
+
const skippedFiles = [];
|
|
371
|
+
for (const filePath of theme.files) {
|
|
372
|
+
const registryFilePath = path.join(registryDir, filePath);
|
|
373
|
+
if (!fs.existsSync(registryFilePath)) {
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
const destRel = path.basename(filePath); // e.g. "ultimate.css"
|
|
377
|
+
const localFilePath = path.join(rootDir, themesDir, destRel);
|
|
378
|
+
const displayName = `${name}/${destRel}`;
|
|
379
|
+
const exists = fs.existsSync(localFilePath);
|
|
380
|
+
if (exists && !options.force) {
|
|
381
|
+
if (options.skipExisting) {
|
|
382
|
+
skippedFiles.push(displayName);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
if (options.dryRun) {
|
|
386
|
+
// would-overwrite
|
|
387
|
+
}
|
|
388
|
+
else if (process.stdin.isTTY) {
|
|
389
|
+
const res = await prompts({
|
|
390
|
+
type: "confirm",
|
|
391
|
+
name: "overwrite",
|
|
392
|
+
message: `Theme file "${path.relative(rootDir, localFilePath)}" already exists. Overwrite?`,
|
|
393
|
+
initial: false,
|
|
394
|
+
});
|
|
395
|
+
if (!res.overwrite) {
|
|
396
|
+
skippedFiles.push(displayName);
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
throw new Error(`Theme file "${path.relative(rootDir, localFilePath)}" already exists. Use --force or --skip-existing.`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const content = fs.readFileSync(registryFilePath, "utf-8");
|
|
405
|
+
if (options.dryRun) {
|
|
406
|
+
console.log(chalk.dim(` [dry-run] would write ${path.relative(rootDir, localFilePath)}`));
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
fs.mkdirSync(path.dirname(localFilePath), { recursive: true });
|
|
410
|
+
fs.writeFileSync(localFilePath, content);
|
|
411
|
+
}
|
|
412
|
+
copiedFiles.push(displayName);
|
|
413
|
+
}
|
|
414
|
+
// Track as "installedComponents" so `status` / `list` / `update` can
|
|
415
|
+
// still find it — the shape is compatible and avoids a parallel store.
|
|
416
|
+
if (!options.dryRun) {
|
|
417
|
+
const installed = componentsJson.installedComponents ?? [];
|
|
418
|
+
const existingIdx = installed.findIndex((c) => c.name === `theme/${name}`);
|
|
419
|
+
const entry = {
|
|
420
|
+
name: `theme/${name}`,
|
|
421
|
+
version: "2.0.0",
|
|
422
|
+
installedAt: new Date().toISOString().split("T")[0],
|
|
423
|
+
};
|
|
424
|
+
if (existingIdx >= 0) {
|
|
425
|
+
installed[existingIdx] = entry;
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
installed.push(entry);
|
|
429
|
+
}
|
|
430
|
+
componentsJson.installedComponents = installed;
|
|
431
|
+
const componentsJsonPath = path.join(rootDir, "components.json");
|
|
432
|
+
fs.writeFileSync(componentsJsonPath, JSON.stringify(componentsJson, null, 2) + "\n");
|
|
433
|
+
}
|
|
434
|
+
const relThemesDir = themesDir;
|
|
435
|
+
const importHint = `@import "./${relThemesDir}/${name}.css";`;
|
|
436
|
+
return {
|
|
437
|
+
added: true,
|
|
438
|
+
message: `Theme "${name}" installed to ${relThemesDir}/. ` +
|
|
439
|
+
`Add to your main stylesheet: ${importHint}`,
|
|
440
|
+
files: copiedFiles,
|
|
441
|
+
npmDeps: [],
|
|
442
|
+
internalDeps: [],
|
|
443
|
+
skipped: skippedFiles,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
342
446
|
export async function runAdd(componentSpec, rootDir, options = {}) {
|
|
343
447
|
const componentsJson = readComponentsJson(rootDir);
|
|
344
448
|
if (!componentsJson) {
|
|
@@ -424,6 +528,27 @@ export async function runAdd(componentSpec, rootDir, options = {}) {
|
|
|
424
528
|
internalDeps: [],
|
|
425
529
|
};
|
|
426
530
|
}
|
|
531
|
+
// ─── Theme install fast path ─────────────────────────────────────
|
|
532
|
+
// Themes bypass the component dependency graph entirely — they are
|
|
533
|
+
// plain CSS files with no hooks, no registryDependencies and no
|
|
534
|
+
// transformation pipeline. Handle them first when the spec uses the
|
|
535
|
+
// `theme/` prefix or when a theme with this name exists in the registry.
|
|
536
|
+
if (prefix === "theme") {
|
|
537
|
+
const theme = findThemeInRegistry(registry, name);
|
|
538
|
+
if (!theme) {
|
|
539
|
+
const themeNames = registry.themes?.map((t) => t.name) ?? [];
|
|
540
|
+
const suggestions = fuzzySearchNames(name, themeNames, 0.4).slice(0, 5);
|
|
541
|
+
return {
|
|
542
|
+
added: false,
|
|
543
|
+
message: `Theme "${name}" not found in registry.`,
|
|
544
|
+
files: [],
|
|
545
|
+
npmDeps: [],
|
|
546
|
+
internalDeps: [],
|
|
547
|
+
suggestions,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
return installTheme(theme, componentsJson, registryDir, rootDir, options);
|
|
551
|
+
}
|
|
427
552
|
const component = findInRegistry(registry, name, prefix);
|
|
428
553
|
if (!component) {
|
|
429
554
|
const label = prefix ? `${prefix}/${name}` : name;
|
|
@@ -432,6 +557,7 @@ export async function runAdd(componentSpec, rootDir, options = {}) {
|
|
|
432
557
|
...registry.components.map((c) => c.name),
|
|
433
558
|
...registry.blocks.map((c) => c.name),
|
|
434
559
|
...registry.templates.map((c) => c.name),
|
|
560
|
+
...(registry.themes?.map((t) => `theme/${t.name}`) ?? []),
|
|
435
561
|
];
|
|
436
562
|
const suggestions = fuzzySearchNames(name, allNames, 0.4).slice(0, 5);
|
|
437
563
|
return {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `inai-ui demo start` — launches the docs dev server and prints the
|
|
3
|
+
* keyboard shortcut that flips it into Pitch mode.
|
|
4
|
+
*
|
|
5
|
+
* Thin wrapper around `pnpm --filter docs dev`: the heavy lifting lives
|
|
6
|
+
* in the docs app's `PitchModeProvider` + `LiveThemeSwitcher`, so this
|
|
7
|
+
* command is really a convenience so the team can run a single command
|
|
8
|
+
* before a client call.
|
|
9
|
+
*
|
|
10
|
+
* Flags:
|
|
11
|
+
* --theme <name> preselect a theme by writing `localStorage` seed
|
|
12
|
+
* (delegated — currently prints the instructions).
|
|
13
|
+
* --port <number> vite port (defaults to 3000).
|
|
14
|
+
*/
|
|
15
|
+
export interface DemoStartOptions {
|
|
16
|
+
theme?: string;
|
|
17
|
+
port?: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function demoStartCommand(options?: DemoStartOptions): Promise<void>;
|
|
20
|
+
//# sourceMappingURL=demo.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"demo.d.ts","sourceRoot":"","sources":["../../src/commands/demo.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,wBAAsB,gBAAgB,CACpC,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,IAAI,CAAC,CAiCf"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
export async function demoStartCommand(options = {}) {
|
|
4
|
+
const port = options.port ?? 3000;
|
|
5
|
+
console.log(chalk.bold.cyan("▶ Starting InAI UI demo shell"), chalk.dim(`(port ${port})`));
|
|
6
|
+
console.log(chalk.dim(" Press ⌘⇧P (macOS) or Ctrl+Shift+P once the app is up to enter pitch mode."));
|
|
7
|
+
console.log(chalk.dim(" Use ← / → or PageUp / PageDown to walk the demo stops, Esc to exit."));
|
|
8
|
+
if (options.theme) {
|
|
9
|
+
console.log(chalk.yellow(` Theme preselect requested: "${options.theme}" — once the dock opens, the preset will be available in the 'Presets' tab.`));
|
|
10
|
+
}
|
|
11
|
+
const proc = spawn("pnpm", ["--filter", "docs", "dev", "--port", String(port)], { stdio: "inherit", env: { ...process.env } });
|
|
12
|
+
proc.on("exit", (code) => {
|
|
13
|
+
process.exit(code ?? 0);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `inai-ui generate block <description>` — scaffold a brand-new block
|
|
3
|
+
* from a natural-language description using the Anthropic API.
|
|
4
|
+
*
|
|
5
|
+
* Design goals:
|
|
6
|
+
* - **Zero new dependencies**: we talk to the Anthropic REST API via
|
|
7
|
+
* `fetch` directly. No `@anthropic-ai/sdk`.
|
|
8
|
+
* - **Safe by default**: we never overwrite existing files without an
|
|
9
|
+
* explicit `--force` flag and dry-run is always available.
|
|
10
|
+
* - **Grounded output**: the system prompt carries the canonical InAI UI
|
|
11
|
+
* conventions + a short catalog of available building blocks so the
|
|
12
|
+
* model reuses primitives instead of hallucinating new ones.
|
|
13
|
+
* - **Extractable**: responses are expected to carry a fenced ```tsx
|
|
14
|
+
* block followed by an optional ```ts data block. A thin parser pulls
|
|
15
|
+
* both sections out of the response.
|
|
16
|
+
*/
|
|
17
|
+
export interface GenerateOptions {
|
|
18
|
+
description?: string;
|
|
19
|
+
slug?: string;
|
|
20
|
+
output?: string;
|
|
21
|
+
model?: string;
|
|
22
|
+
dryRun?: boolean;
|
|
23
|
+
force?: boolean;
|
|
24
|
+
apiKey?: string;
|
|
25
|
+
maxTokens?: number;
|
|
26
|
+
}
|
|
27
|
+
export declare function toPascalCase(slug: string): string;
|
|
28
|
+
export declare function sanitizeSlug(raw: string): string;
|
|
29
|
+
export interface ParsedResponse {
|
|
30
|
+
pageTsx: string;
|
|
31
|
+
dataTs: string | null;
|
|
32
|
+
raw: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Pulls the first ```tsx / ```ts / ```typescript fenced block out of the
|
|
36
|
+
* assistant's response. We expect exactly two at most — one for the page
|
|
37
|
+
* and one for the optional data.ts.
|
|
38
|
+
*/
|
|
39
|
+
export declare function extractCodeBlocks(text: string): ParsedResponse;
|
|
40
|
+
export declare function callClaude(args: {
|
|
41
|
+
apiKey: string;
|
|
42
|
+
model: string;
|
|
43
|
+
description: string;
|
|
44
|
+
maxTokens: number;
|
|
45
|
+
}): Promise<string>;
|
|
46
|
+
export declare function generateBlockCommand(description: string | undefined, options: GenerateOptions): Promise<void>;
|
|
47
|
+
//# sourceMappingURL=generate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../src/commands/generate.ts"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,WAAW,eAAe;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA+CD,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMjD;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAOhD;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CAmB9D;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,MAAM,CAAC,CAmClB;AAED,wBAAsB,oBAAoB,CACxC,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,IAAI,CAAC,CA2Jf"}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import ora from "ora";
|
|
5
|
+
import prompts from "prompts";
|
|
6
|
+
const DEFAULT_MODEL = "claude-sonnet-4-6";
|
|
7
|
+
const DEFAULT_OUTPUT = "apps/docs/src/registry/blocks";
|
|
8
|
+
const SYSTEM_PROMPT = `You are a senior React engineer generating a production-ready "block" for the InAI UI component library. A block is a full-bleed section component that ships under \`apps/docs/src/registry/blocks/<slug>/page.tsx\` and, when helpful, a sibling \`data.ts\` for mock content.
|
|
9
|
+
|
|
10
|
+
Hard requirements, in order of importance:
|
|
11
|
+
|
|
12
|
+
1. Stack: React 19, TypeScript strict, Tailwind CSS v4 with \`@theme\`.
|
|
13
|
+
2. Always import UI primitives from \`@company/ui/<name>\` (e.g. \`import { Button } from "@company/ui/button"\`). Never barrel-import. Never hardcode colours — always use CSS custom properties from the active theme:
|
|
14
|
+
- \`--color-primary\`, \`--color-accent\`, \`--color-info\`, \`--color-success\`, \`--color-warning\`, \`--color-destructive\`
|
|
15
|
+
- Backgrounds: \`--color-background\`, \`--color-surface\`, \`--color-surface-muted\`
|
|
16
|
+
- Text: \`--color-text\`, \`--color-text-secondary\`, \`--color-text-tertiary\`
|
|
17
|
+
- Borders: \`--color-border\`
|
|
18
|
+
- Radii: \`--radius-sm\`, \`--radius-md\`, \`--radius-lg\`
|
|
19
|
+
- Shadows: \`--shadow-sm\`, \`--shadow-md\`, \`--shadow-lg\`
|
|
20
|
+
- Fonts: \`--font-display\`, \`--font-body\`, \`--font-mono\`
|
|
21
|
+
3. Use Motion (imported from \`motion/react\`) for animations. Spring physics, not duration-based easing. Always respect \`useReducedMotion\`.
|
|
22
|
+
4. Prefer existing primitives from the library when possible. Catalogue (non-exhaustive, most useful for blocks): Button, Card, Badge, Input, Textarea, Select, Checkbox, Switch, Tabs, Dialog, Sheet, Tooltip, Avatar, Progress, Separator, MarqueeInfinite, GradientMesh, TextMask, KineticHeading, DecryptText, ParticleText, MagneticCursor, NoiseTexture, HexbinHeatmap, SparklineLive, RadialProgress, FunnelAnimated, RadarMotion, HeatmapCalendar, CardStackDeal, MorphSVG, ForceGraphMini, WaveformCanvas, ChordDiagram, SceneTransitions, PremiumLineChart, PremiumBarChart, PremiumAreaChart, PremiumPieChart.
|
|
23
|
+
5. The block MUST export a default React component named from the slug in PascalCase + "Page" (e.g. slug \`hero-magnetic\` → \`HeroMagneticPage\`).
|
|
24
|
+
6. The block MUST be accessible: semantic HTML, aria-labels where appropriate, focus-visible states, and respect \`prefers-reduced-motion\`.
|
|
25
|
+
7. If the block needs content data (stats, testimonials, products, nav items, etc.), emit a sibling \`data.ts\` with a typed export and import it from the page.
|
|
26
|
+
8. No "lorem ipsum" — use believable, brand-neutral copy.
|
|
27
|
+
9. No external image URLs. For visuals use CSS gradients, SVG, \`GradientMesh\`, \`TextMask\`, or noise textures.
|
|
28
|
+
10. Keep the block self-contained. Do not modify \`block-registry.tsx\`, \`block-categories.ts\`, or any other file — the CLI will handle registration.
|
|
29
|
+
|
|
30
|
+
Response format: respond ONLY with one or two fenced code blocks, nothing else.
|
|
31
|
+
|
|
32
|
+
- First block, required: \`\`\`tsx ...full page.tsx content... \`\`\`
|
|
33
|
+
- Second block, optional: \`\`\`ts ...full data.ts content... \`\`\`
|
|
34
|
+
|
|
35
|
+
No prose between, before, or after the code fences.`;
|
|
36
|
+
export function toPascalCase(slug) {
|
|
37
|
+
return slug
|
|
38
|
+
.split(/[-_\s]+/)
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.map((w) => w[0].toUpperCase() + w.slice(1).toLowerCase())
|
|
41
|
+
.join("");
|
|
42
|
+
}
|
|
43
|
+
export function sanitizeSlug(raw) {
|
|
44
|
+
return raw
|
|
45
|
+
.toLowerCase()
|
|
46
|
+
.trim()
|
|
47
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
48
|
+
.replace(/^-+|-+$/g, "")
|
|
49
|
+
.slice(0, 64);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Pulls the first ```tsx / ```ts / ```typescript fenced block out of the
|
|
53
|
+
* assistant's response. We expect exactly two at most — one for the page
|
|
54
|
+
* and one for the optional data.ts.
|
|
55
|
+
*/
|
|
56
|
+
export function extractCodeBlocks(text) {
|
|
57
|
+
const pageMatch = text.match(/```(?:tsx|jsx|typescript|ts)\s*\n([\s\S]*?)\n```/);
|
|
58
|
+
if (!pageMatch) {
|
|
59
|
+
throw new Error("Could not find a fenced tsx code block in the model response. Re-run with a more specific description.");
|
|
60
|
+
}
|
|
61
|
+
const pageTsx = pageMatch[1].trim();
|
|
62
|
+
// Strip everything through the first block and look for a second.
|
|
63
|
+
const afterFirst = text.slice((pageMatch.index ?? 0) + pageMatch[0].length);
|
|
64
|
+
const dataMatch = afterFirst.match(/```(?:ts|typescript)\s*\n([\s\S]*?)\n```/);
|
|
65
|
+
const dataTs = dataMatch ? dataMatch[1].trim() : null;
|
|
66
|
+
return { pageTsx, dataTs, raw: text };
|
|
67
|
+
}
|
|
68
|
+
export async function callClaude(args) {
|
|
69
|
+
const { apiKey, model, description, maxTokens } = args;
|
|
70
|
+
const userPrompt = `Generate a block for this description:\n\n${description.trim()}\n\nRemember: respond only with the fenced code blocks — no prose.`;
|
|
71
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: {
|
|
74
|
+
"content-type": "application/json",
|
|
75
|
+
"x-api-key": apiKey,
|
|
76
|
+
"anthropic-version": "2023-06-01",
|
|
77
|
+
},
|
|
78
|
+
body: JSON.stringify({
|
|
79
|
+
model,
|
|
80
|
+
max_tokens: maxTokens,
|
|
81
|
+
system: SYSTEM_PROMPT,
|
|
82
|
+
messages: [{ role: "user", content: userPrompt }],
|
|
83
|
+
}),
|
|
84
|
+
});
|
|
85
|
+
if (!response.ok) {
|
|
86
|
+
const body = await response.text();
|
|
87
|
+
throw new Error(`Anthropic API error ${response.status}: ${body.slice(0, 400)}`);
|
|
88
|
+
}
|
|
89
|
+
const payload = (await response.json());
|
|
90
|
+
const textParts = payload.content
|
|
91
|
+
.filter((c) => c.type === "text")
|
|
92
|
+
.map((c) => c.text);
|
|
93
|
+
if (textParts.length === 0) {
|
|
94
|
+
throw new Error("Model returned no text content. Try again.");
|
|
95
|
+
}
|
|
96
|
+
return textParts.join("\n");
|
|
97
|
+
}
|
|
98
|
+
export async function generateBlockCommand(description, options) {
|
|
99
|
+
let desc = description ?? options.description;
|
|
100
|
+
if (!desc) {
|
|
101
|
+
const res = await prompts({
|
|
102
|
+
type: "text",
|
|
103
|
+
name: "description",
|
|
104
|
+
message: "Describe the block you want to generate (e.g. 'hero with gradient mesh and kinetic headline')",
|
|
105
|
+
validate: (v) => (typeof v === "string" && v.trim().length > 10) ||
|
|
106
|
+
"Please provide more detail",
|
|
107
|
+
});
|
|
108
|
+
if (!res.description) {
|
|
109
|
+
console.log(chalk.yellow("✖ Aborted."));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
desc = res.description;
|
|
113
|
+
}
|
|
114
|
+
const apiKey = options.apiKey ?? process.env.ANTHROPIC_API_KEY;
|
|
115
|
+
if (!apiKey) {
|
|
116
|
+
console.error(chalk.red("✖ Missing ANTHROPIC_API_KEY. Set it in your environment or pass --api-key."));
|
|
117
|
+
console.error(chalk.dim(" Get a key at https://console.anthropic.com/ — free tier includes generous credits."));
|
|
118
|
+
process.exitCode = 1;
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
// Pick a slug.
|
|
122
|
+
let slug = options.slug ? sanitizeSlug(options.slug) : "";
|
|
123
|
+
if (!slug) {
|
|
124
|
+
const shortWords = desc
|
|
125
|
+
.toLowerCase()
|
|
126
|
+
.split(/\s+/)
|
|
127
|
+
.filter((w) => w.length > 2)
|
|
128
|
+
.slice(0, 4)
|
|
129
|
+
.join("-");
|
|
130
|
+
const suggestion = sanitizeSlug(shortWords || "generated-block");
|
|
131
|
+
const res = await prompts({
|
|
132
|
+
type: "text",
|
|
133
|
+
name: "slug",
|
|
134
|
+
message: "Slug for the new block (kebab-case):",
|
|
135
|
+
initial: suggestion,
|
|
136
|
+
validate: (v) => (typeof v === "string" && /^[a-z0-9-]+$/.test(v)) ||
|
|
137
|
+
"Use lowercase letters, numbers, and hyphens only",
|
|
138
|
+
});
|
|
139
|
+
if (!res.slug) {
|
|
140
|
+
console.log(chalk.yellow("✖ Aborted."));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
slug = sanitizeSlug(res.slug);
|
|
144
|
+
}
|
|
145
|
+
const outputRoot = options.output ?? DEFAULT_OUTPUT;
|
|
146
|
+
const blockDir = path.resolve(process.cwd(), outputRoot, slug);
|
|
147
|
+
const pagePath = path.join(blockDir, "page.tsx");
|
|
148
|
+
const dataPath = path.join(blockDir, "data.ts");
|
|
149
|
+
if (fs.existsSync(blockDir) && !options.force && !options.dryRun) {
|
|
150
|
+
console.error(chalk.red(`✖ ${path.relative(process.cwd(), blockDir)} already exists. Use --force to overwrite.`));
|
|
151
|
+
process.exitCode = 1;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const model = options.model ?? DEFAULT_MODEL;
|
|
155
|
+
const maxTokens = options.maxTokens ?? 4096;
|
|
156
|
+
const spinner = ora(`Calling ${chalk.cyan(model)} to generate ${chalk.bold(slug)}…`).start();
|
|
157
|
+
let parsed;
|
|
158
|
+
try {
|
|
159
|
+
const text = await callClaude({ apiKey, model, description: desc, maxTokens });
|
|
160
|
+
parsed = extractCodeBlocks(text);
|
|
161
|
+
spinner.succeed(`Model returned ${text.length} chars.`);
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
spinner.fail(err.message);
|
|
165
|
+
process.exitCode = 1;
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
// Light post-processing: ensure the exported component name ends with "Page".
|
|
169
|
+
const expectedName = `${toPascalCase(slug)}Page`;
|
|
170
|
+
if (!parsed.pageTsx.includes(expectedName)) {
|
|
171
|
+
console.warn(chalk.yellow(`! Model did not export \`${expectedName}\`. You may need to rename the default export.`));
|
|
172
|
+
}
|
|
173
|
+
console.log();
|
|
174
|
+
console.log(chalk.bold("Generated files:"));
|
|
175
|
+
console.log(` ${chalk.cyan(path.relative(process.cwd(), pagePath))} ${chalk.dim(`(${parsed.pageTsx.split("\n").length} lines)`)}`);
|
|
176
|
+
if (parsed.dataTs) {
|
|
177
|
+
console.log(` ${chalk.cyan(path.relative(process.cwd(), dataPath))} ${chalk.dim(`(${parsed.dataTs.split("\n").length} lines)`)}`);
|
|
178
|
+
}
|
|
179
|
+
if (options.dryRun) {
|
|
180
|
+
console.log();
|
|
181
|
+
console.log(chalk.bold("── page.tsx ──"));
|
|
182
|
+
console.log(parsed.pageTsx);
|
|
183
|
+
if (parsed.dataTs) {
|
|
184
|
+
console.log();
|
|
185
|
+
console.log(chalk.bold("── data.ts ──"));
|
|
186
|
+
console.log(parsed.dataTs);
|
|
187
|
+
}
|
|
188
|
+
console.log();
|
|
189
|
+
console.log(chalk.dim("Dry run — no files written."));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
fs.mkdirSync(blockDir, { recursive: true });
|
|
193
|
+
fs.writeFileSync(pagePath, `${parsed.pageTsx}\n`, "utf8");
|
|
194
|
+
if (parsed.dataTs) {
|
|
195
|
+
fs.writeFileSync(dataPath, `${parsed.dataTs}\n`, "utf8");
|
|
196
|
+
}
|
|
197
|
+
console.log();
|
|
198
|
+
console.log(chalk.green(`✔ Block scaffolded at ${path.relative(process.cwd(), blockDir)}`));
|
|
199
|
+
console.log();
|
|
200
|
+
console.log(chalk.bold("Next steps:"));
|
|
201
|
+
console.log(` 1. Register in ${chalk.cyan("apps/docs/src/lib/block-registry.tsx")} so the route is discoverable.`);
|
|
202
|
+
console.log(` 2. Add ${chalk.bold(slug)} to a category in ${chalk.cyan("apps/docs/src/lib/block-categories.ts")} so it appears on ${chalk.cyan("/blocks")}.`);
|
|
203
|
+
console.log(` 3. Run ${chalk.cyan("pnpm --filter docs check-types")} and review the generated code before shipping.`);
|
|
204
|
+
}
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -260,6 +260,15 @@ export interface ComponentsJson {
|
|
|
260
260
|
table: boolean;
|
|
261
261
|
};
|
|
262
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* Load an InitConfig from a JSON file. Used by the `--config <path>` flag
|
|
265
|
+
* to skip interactive prompts in CI / automated bootstrap flows. Missing
|
|
266
|
+
* optional fields are filled with sensible defaults; invalid/missing
|
|
267
|
+
* required fields throw with a clear message.
|
|
268
|
+
*
|
|
269
|
+
* The accepted JSON schema lives at `packages/cli/src/schemas/init-config.schema.json`.
|
|
270
|
+
*/
|
|
271
|
+
export declare function loadInitConfigFromFile(configPath: string, framework?: Framework, layout?: ProjectLayout): InitConfig;
|
|
263
272
|
export declare function promptInitConfig(registryDir?: string, framework?: Framework, layout?: ProjectLayout): Promise<InitConfig | null>;
|
|
264
273
|
export declare function buildComponentsJson(config: InitConfig, repoUrl?: string): ComponentsJson;
|
|
265
274
|
export declare function getCnTemplate(): string;
|
|
@@ -296,6 +305,13 @@ export declare function runInit(config: InitConfig, targetDir: string, repoUrl?:
|
|
|
296
305
|
success: boolean;
|
|
297
306
|
filesCreated: string[];
|
|
298
307
|
}>;
|
|
299
|
-
export
|
|
308
|
+
export interface InitCommandOptions {
|
|
309
|
+
/** Path to a JSON config file. When provided, interactive prompts are
|
|
310
|
+
* skipped and the config is loaded from disk. Enables CI/automated
|
|
311
|
+
* bootstrap. See `packages/cli/src/schemas/init-config.schema.json`
|
|
312
|
+
* for the accepted shape. */
|
|
313
|
+
config?: string;
|
|
314
|
+
}
|
|
315
|
+
export declare function initCommand(repoUrl?: string, options?: InitCommandOptions): Promise<void>;
|
|
300
316
|
export {};
|
|
301
317
|
//# sourceMappingURL=init.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAOA,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAEL,KAAK,aAAa,EACnB,MAAM,6BAA6B,CAAC;AAIrC,KAAK,KAAK,GAAG,MAAM,CAAC;AAEpB,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4DR,CAAC;AACX,KAAK,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC;AAUxD,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,UAAU,CAAC;IACrB,WAAW,EAAE,UAAU,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,OAAO,CAAC;IACxB,aAAa,EAAE,OAAO,CAAC;IACvB,YAAY,EAAE,OAAO,CAAC;IACtB,aAAa,EAAE,OAAO,CAAC;IACvB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,uEAAuE;IACvE,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,QAAQ,EAAE;QACR,MAAM,EAAE,MAAM,CAAC;QACf,GAAG,EAAE,MAAM,CAAC;KACb,CAAC;IACF,OAAO,EAAE;QACP,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,MAAM,CAAC;QACd,GAAG,CAAC,EAAE,MAAM,CAAC;KACd,CAAC;IACF,KAAK,EAAE,KAAK,CAAC;IACb,IAAI,EAAE;QACJ,IAAI,EAAE,UAAU,CAAC;QACjB,OAAO,EAAE,UAAU,CAAC;KACrB,CAAC;IACF,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE;QACR,MAAM,EAAE,OAAO,CAAC;QAChB,KAAK,EAAE,OAAO,CAAC;QACf,IAAI,EAAE,OAAO,CAAC;QACd,KAAK,EAAE,OAAO,CAAC;KAChB,CAAC;CACH;
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAOA,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAEL,KAAK,aAAa,EACnB,MAAM,6BAA6B,CAAC;AAIrC,KAAK,KAAK,GAAG,MAAM,CAAC;AAEpB,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4DR,CAAC;AACX,KAAK,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC;AAUxD,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,UAAU,CAAC;IACrB,WAAW,EAAE,UAAU,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,OAAO,CAAC;IACxB,aAAa,EAAE,OAAO,CAAC;IACvB,YAAY,EAAE,OAAO,CAAC;IACtB,aAAa,EAAE,OAAO,CAAC;IACvB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,uEAAuE;IACvE,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,QAAQ,EAAE;QACR,MAAM,EAAE,MAAM,CAAC;QACf,GAAG,EAAE,MAAM,CAAC;KACb,CAAC;IACF,OAAO,EAAE;QACP,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,MAAM,CAAC;QACd,GAAG,CAAC,EAAE,MAAM,CAAC;KACd,CAAC;IACF,KAAK,EAAE,KAAK,CAAC;IACb,IAAI,EAAE;QACJ,IAAI,EAAE,UAAU,CAAC;QACjB,OAAO,EAAE,UAAU,CAAC;KACrB,CAAC;IACF,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE;QACR,MAAM,EAAE,OAAO,CAAC;QAChB,KAAK,EAAE,OAAO,CAAC;QACf,IAAI,EAAE,OAAO,CAAC;QACd,KAAK,EAAE,OAAO,CAAC;KAChB,CAAC;CACH;AAiCD;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,MAAM,EAClB,SAAS,CAAC,EAAE,SAAS,EACrB,MAAM,CAAC,EAAE,aAAa,GACrB,UAAU,CAyEZ;AAED,wBAAsB,gBAAgB,CACpC,WAAW,CAAC,EAAE,MAAM,EACpB,SAAS,CAAC,EAAE,SAAS,EACrB,MAAM,CAAC,EAAE,aAAa,GACrB,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CA0F5B;AAED,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,UAAU,EAClB,OAAO,CAAC,EAAE,MAAM,GACf,cAAc,CA8ChB;AAED,wBAAgB,aAAa,IAAI,MAAM,CAQtC;AAOD,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAc/H;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAahI;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAoBpE;AAsHD;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,GACrB,MAAM,EAAE,CAeV;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CACxB,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,GACrB,OAAO,CAKT;AAED,wBAAsB,OAAO,CAC3B,MAAM,EAAE,UAAU,EAClB,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,YAAY,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CA0FvD;AAED,MAAM,WAAW,kBAAkB;IACjC;;;kCAG8B;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAsB,WAAW,CAC/B,OAAO,CAAC,EAAE,MAAM,EAChB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,IAAI,CAAC,CA6Ff"}
|
package/dist/commands/init.js
CHANGED
|
@@ -90,6 +90,90 @@ function discoverThemes(registryDir) {
|
|
|
90
90
|
}
|
|
91
91
|
return [...AVAILABLE_THEMES];
|
|
92
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
|
+
}
|
|
93
177
|
export async function promptInitConfig(registryDir, framework, layout) {
|
|
94
178
|
const themes = discoverThemes(registryDir);
|
|
95
179
|
const sourceRoot = layout?.sourceRoot ?? "src";
|
|
@@ -480,7 +564,7 @@ export async function runInit(config, targetDir, repoUrl) {
|
|
|
480
564
|
filesCreated.push(cssRelPath);
|
|
481
565
|
return { success: true, filesCreated };
|
|
482
566
|
}
|
|
483
|
-
export async function initCommand(repoUrl) {
|
|
567
|
+
export async function initCommand(repoUrl, options = {}) {
|
|
484
568
|
console.log(chalk.bold("\nInAI UI - Project Initialization\n"));
|
|
485
569
|
const cwd = process.cwd();
|
|
486
570
|
const detectedFramework = detectFramework(cwd);
|
|
@@ -502,7 +586,21 @@ export async function initCommand(repoUrl) {
|
|
|
502
586
|
}
|
|
503
587
|
}
|
|
504
588
|
const registryDir = repoUrl ? getCacheDir() : undefined;
|
|
505
|
-
|
|
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
|
+
}
|
|
506
604
|
if (!config) {
|
|
507
605
|
console.log(chalk.yellow("\nInitialization cancelled."));
|
|
508
606
|
process.exit(0);
|