inai-react-components 0.1.7 → 1.4.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/README.md +80 -0
- package/dist/commands/add.d.ts +42 -12
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +413 -93
- package/dist/commands/diff.d.ts +6 -0
- package/dist/commands/diff.d.ts.map +1 -1
- package/dist/commands/diff.js +90 -20
- package/dist/commands/init.d.ts +17 -5
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +87 -64
- package/dist/commands/mcp.d.ts +123 -0
- package/dist/commands/mcp.d.ts.map +1 -0
- package/dist/commands/mcp.js +289 -0
- package/dist/commands/migrate.d.ts +41 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +139 -0
- package/dist/commands/registries.d.ts +46 -0
- package/dist/commands/registries.d.ts.map +1 -0
- package/dist/commands/registries.js +152 -0
- package/dist/commands/remove.d.ts +11 -0
- package/dist/commands/remove.d.ts.map +1 -0
- package/dist/commands/remove.js +171 -0
- package/dist/commands/schema.d.ts +16 -0
- package/dist/commands/schema.d.ts.map +1 -0
- package/dist/commands/schema.js +32 -0
- package/dist/commands/status.d.ts +40 -4
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +149 -25
- package/dist/commands/theme.d.ts.map +1 -1
- package/dist/commands/theme.js +3 -37
- package/dist/commands/update.d.ts +18 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +197 -6
- package/dist/index.js +76 -5
- package/dist/schemas/registry-config.schema.json +62 -0
- package/dist/schemas/registry-item.schema.json +63 -0
- package/dist/schemas/registry.schema.json +15 -0
- package/dist/types/registry.d.ts +50 -0
- package/dist/types/registry.d.ts.map +1 -0
- package/dist/types/registry.js +10 -0
- package/dist/utils/auto-install.d.ts +11 -0
- package/dist/utils/auto-install.d.ts.map +1 -0
- package/dist/utils/auto-install.js +29 -0
- package/dist/utils/framework-detect.d.ts +20 -0
- package/dist/utils/framework-detect.d.ts.map +1 -0
- package/dist/utils/framework-detect.js +97 -0
- package/dist/utils/fuzzy-search.d.ts +16 -0
- package/dist/utils/fuzzy-search.d.ts.map +1 -0
- package/dist/utils/fuzzy-search.js +67 -0
- package/dist/utils/paths.d.ts +26 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +35 -0
- package/dist/utils/registry-config.d.ts +27 -0
- package/dist/utils/registry-config.d.ts.map +1 -0
- package/dist/utils/registry-config.js +94 -0
- package/dist/utils/registry-resolver.d.ts +26 -0
- package/dist/utils/registry-resolver.d.ts.map +1 -1
- package/dist/utils/registry-resolver.js +134 -10
- package/dist/utils/snapshot.d.ts +20 -0
- package/dist/utils/snapshot.d.ts.map +1 -0
- package/dist/utils/snapshot.js +32 -0
- package/dist/utils/themes.d.ts +17 -0
- package/dist/utils/themes.d.ts.map +1 -0
- package/dist/utils/themes.js +49 -0
- package/dist/utils/tsconfig-detect.d.ts +18 -0
- package/dist/utils/tsconfig-detect.d.ts.map +1 -0
- package/dist/utils/tsconfig-detect.js +121 -0
- package/package.json +9 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"framework-detect.d.ts","sourceRoot":"","sources":["../../src/utils/framework-detect.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,SAAS,GACjB,UAAU,GACV,YAAY,GACZ,MAAM,GACN,OAAO,GACP,OAAO,GACP,gBAAgB,GAChB,SAAS,CAAC;AAEd;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAkDtD;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAmBjF;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAiB3D"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Detect the React meta-framework in use for a given project directory.
|
|
5
|
+
* Order matters: Next / Remix / Astro win over a bare vite.config because
|
|
6
|
+
* those frameworks often *also* ship a vite config under the hood.
|
|
7
|
+
*/
|
|
8
|
+
export function detectFramework(cwd) {
|
|
9
|
+
const has = (p) => fs.existsSync(path.join(cwd, p));
|
|
10
|
+
if (has("next.config.js") ||
|
|
11
|
+
has("next.config.ts") ||
|
|
12
|
+
has("next.config.mjs") ||
|
|
13
|
+
has("next.config.cjs")) {
|
|
14
|
+
if (has("app") || has("src/app"))
|
|
15
|
+
return "next-app";
|
|
16
|
+
if (has("pages") || has("src/pages"))
|
|
17
|
+
return "next-pages";
|
|
18
|
+
return "next-app";
|
|
19
|
+
}
|
|
20
|
+
if (has("remix.config.js") || has("remix.config.ts"))
|
|
21
|
+
return "remix";
|
|
22
|
+
if (has("astro.config.mjs") ||
|
|
23
|
+
has("astro.config.ts") ||
|
|
24
|
+
has("astro.config.js")) {
|
|
25
|
+
return "astro";
|
|
26
|
+
}
|
|
27
|
+
// TanStack Start is identified via package.json, not a config file.
|
|
28
|
+
try {
|
|
29
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
30
|
+
if (fs.existsSync(pkgPath)) {
|
|
31
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
32
|
+
const allDeps = {
|
|
33
|
+
...(pkg.dependencies ?? {}),
|
|
34
|
+
...(pkg.devDependencies ?? {}),
|
|
35
|
+
};
|
|
36
|
+
if ("@tanstack/react-start" in allDeps)
|
|
37
|
+
return "tanstack-start";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// fall through
|
|
42
|
+
}
|
|
43
|
+
if (has("vite.config.js") ||
|
|
44
|
+
has("vite.config.ts") ||
|
|
45
|
+
has("vite.config.mjs")) {
|
|
46
|
+
return "vite";
|
|
47
|
+
}
|
|
48
|
+
return "unknown";
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Default CSS entry point for a given framework, relative to the project
|
|
52
|
+
* root. `sourceRoot` is the project's source directory (usually "src" or
|
|
53
|
+
* "app") as detected from `tsconfig.json`. Paths are derived from it so
|
|
54
|
+
* projects that use `app/` instead of `src/` get correct defaults without
|
|
55
|
+
* manual overrides.
|
|
56
|
+
*
|
|
57
|
+
* Next.js Pages Router and Next.js App Router are special: their CSS
|
|
58
|
+
* entry point is dictated by the framework, not the user's source root.
|
|
59
|
+
*/
|
|
60
|
+
export function getCssEntryPoint(framework, sourceRoot) {
|
|
61
|
+
switch (framework) {
|
|
62
|
+
case "next-app":
|
|
63
|
+
// Next App Router always uses `app/globals.css` regardless of sourceRoot.
|
|
64
|
+
return "app/globals.css";
|
|
65
|
+
case "next-pages":
|
|
66
|
+
// Next Pages Router uses top-level `styles/globals.css`.
|
|
67
|
+
return "styles/globals.css";
|
|
68
|
+
case "vite":
|
|
69
|
+
return `${sourceRoot}/index.css`;
|
|
70
|
+
case "remix":
|
|
71
|
+
return `${sourceRoot}/styles/globals.css`;
|
|
72
|
+
case "astro":
|
|
73
|
+
return `${sourceRoot}/styles/globals.css`;
|
|
74
|
+
case "tanstack-start":
|
|
75
|
+
return `${sourceRoot}/styles/app.css`;
|
|
76
|
+
default:
|
|
77
|
+
return `${sourceRoot}/index.css`;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export function frameworkLabel(framework) {
|
|
81
|
+
switch (framework) {
|
|
82
|
+
case "next-app":
|
|
83
|
+
return "Next.js (App Router)";
|
|
84
|
+
case "next-pages":
|
|
85
|
+
return "Next.js (Pages Router)";
|
|
86
|
+
case "vite":
|
|
87
|
+
return "Vite";
|
|
88
|
+
case "remix":
|
|
89
|
+
return "Remix";
|
|
90
|
+
case "astro":
|
|
91
|
+
return "Astro";
|
|
92
|
+
case "tanstack-start":
|
|
93
|
+
return "TanStack Start";
|
|
94
|
+
default:
|
|
95
|
+
return "Unknown";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight fuzzy search for CLI component name suggestions.
|
|
3
|
+
*
|
|
4
|
+
* Standalone implementation — cannot import from @company/ui since
|
|
5
|
+
* the CLI is a separately published npm package.
|
|
6
|
+
*/
|
|
7
|
+
export interface FuzzyMatch {
|
|
8
|
+
name: string;
|
|
9
|
+
score: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Search a list of names for fuzzy matches against the given query.
|
|
13
|
+
* Returns matches sorted by score (descending), filtered to score > minScore.
|
|
14
|
+
*/
|
|
15
|
+
export declare function fuzzySearchNames(query: string, names: string[], minScore?: number): FuzzyMatch[];
|
|
16
|
+
//# sourceMappingURL=fuzzy-search.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fuzzy-search.d.ts","sourceRoot":"","sources":["../../src/utils/fuzzy-search.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA6BH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAiCD;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EAAE,EACf,QAAQ,SAAM,GACb,UAAU,EAAE,CAWd"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight fuzzy search for CLI component name suggestions.
|
|
3
|
+
*
|
|
4
|
+
* Standalone implementation — cannot import from @company/ui since
|
|
5
|
+
* the CLI is a separately published npm package.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Compute Levenshtein distance between two strings.
|
|
9
|
+
*/
|
|
10
|
+
function levenshtein(a, b) {
|
|
11
|
+
const m = a.length;
|
|
12
|
+
const n = b.length;
|
|
13
|
+
const dp = Array.from({ length: m + 1 }, () => Array.from({ length: n + 1 }, () => 0));
|
|
14
|
+
for (let i = 0; i <= m; i++)
|
|
15
|
+
dp[i][0] = i;
|
|
16
|
+
for (let j = 0; j <= n; j++)
|
|
17
|
+
dp[0][j] = j;
|
|
18
|
+
for (let i = 1; i <= m; i++) {
|
|
19
|
+
for (let j = 1; j <= n; j++) {
|
|
20
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
21
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return dp[m][n];
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Score a query against a target name. Returns a value between 0 and 1,
|
|
28
|
+
* where 1 is a perfect match.
|
|
29
|
+
*
|
|
30
|
+
* Combines Levenshtein distance with substring bonus so that both typos
|
|
31
|
+
* ("buton" -> "button") and partial names ("btn" substring of "button")
|
|
32
|
+
* are surfaced.
|
|
33
|
+
*/
|
|
34
|
+
function scoreMatch(query, target) {
|
|
35
|
+
const q = query.toLowerCase();
|
|
36
|
+
const t = target.toLowerCase();
|
|
37
|
+
// Exact match
|
|
38
|
+
if (q === t)
|
|
39
|
+
return 1;
|
|
40
|
+
// Levenshtein-based score (normalized by the longer string)
|
|
41
|
+
const maxLen = Math.max(q.length, t.length);
|
|
42
|
+
const dist = levenshtein(q, t);
|
|
43
|
+
const levScore = 1 - dist / maxLen;
|
|
44
|
+
// Substring bonus: if the query is contained in the target (or vice versa)
|
|
45
|
+
let substringScore = 0;
|
|
46
|
+
if (t.includes(q)) {
|
|
47
|
+
substringScore = q.length / t.length;
|
|
48
|
+
}
|
|
49
|
+
else if (q.includes(t)) {
|
|
50
|
+
substringScore = t.length / q.length * 0.8;
|
|
51
|
+
}
|
|
52
|
+
return Math.max(levScore, substringScore);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Search a list of names for fuzzy matches against the given query.
|
|
56
|
+
* Returns matches sorted by score (descending), filtered to score > minScore.
|
|
57
|
+
*/
|
|
58
|
+
export function fuzzySearchNames(query, names, minScore = 0.4) {
|
|
59
|
+
const results = [];
|
|
60
|
+
for (const name of names) {
|
|
61
|
+
const score = scoreMatch(query, name);
|
|
62
|
+
if (score > minScore) {
|
|
63
|
+
results.push({ name, score: Math.round(score * 100) / 100 });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return results.sort((a, b) => b.score - a.score);
|
|
67
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ComponentsJsonFile } from "../commands/status.js";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve an alias stored in `components.json` (e.g. "@/components/ui" or
|
|
4
|
+
* "app/components/ui") to an actual directory relative to the project root.
|
|
5
|
+
*
|
|
6
|
+
* Rules:
|
|
7
|
+
* 1. If the value starts with a known alias prefix (`@/`, `~/`, `$/`),
|
|
8
|
+
* substitute it with `layout.sourceRoot` from `components.json`.
|
|
9
|
+
* This preserves backwards compatibility with legacy configs that
|
|
10
|
+
* stored `@/...` aliases and assumed `src/` as the source root.
|
|
11
|
+
* 2. If the value does not start with a known prefix, it's already a
|
|
12
|
+
* filesystem-relative path and is returned unchanged. This is the
|
|
13
|
+
* preferred format going forward — `init` now writes raw paths.
|
|
14
|
+
*
|
|
15
|
+
* When `layout` is missing (pre-1.3.0 `components.json` files), the
|
|
16
|
+
* fallback is `sourceRoot = "src"`, which matches the hardcoded behaviour
|
|
17
|
+
* the CLI shipped with before this refactor.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveLocalDir(alias: string, componentsJson: Pick<ComponentsJsonFile, "layout">): string;
|
|
20
|
+
/**
|
|
21
|
+
* Get the source root directory to use for project-level files like
|
|
22
|
+
* `lib/cn.ts` and `styles/tokens/`. Falls back to `"src"` for legacy
|
|
23
|
+
* `components.json` files without a `layout` field.
|
|
24
|
+
*/
|
|
25
|
+
export declare function getSourceRoot(componentsJson: Pick<ComponentsJsonFile, "layout">): string;
|
|
26
|
+
//# sourceMappingURL=paths.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/utils/paths.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAIhE;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,MAAM,EACb,cAAc,EAAE,IAAI,CAAC,kBAAkB,EAAE,QAAQ,CAAC,GACjD,MAAM,CAQR;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAC3B,cAAc,EAAE,IAAI,CAAC,kBAAkB,EAAE,QAAQ,CAAC,GACjD,MAAM,CAER"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const KNOWN_PREFIXES = ["@/", "~/", "$/"];
|
|
2
|
+
/**
|
|
3
|
+
* Resolve an alias stored in `components.json` (e.g. "@/components/ui" or
|
|
4
|
+
* "app/components/ui") to an actual directory relative to the project root.
|
|
5
|
+
*
|
|
6
|
+
* Rules:
|
|
7
|
+
* 1. If the value starts with a known alias prefix (`@/`, `~/`, `$/`),
|
|
8
|
+
* substitute it with `layout.sourceRoot` from `components.json`.
|
|
9
|
+
* This preserves backwards compatibility with legacy configs that
|
|
10
|
+
* stored `@/...` aliases and assumed `src/` as the source root.
|
|
11
|
+
* 2. If the value does not start with a known prefix, it's already a
|
|
12
|
+
* filesystem-relative path and is returned unchanged. This is the
|
|
13
|
+
* preferred format going forward — `init` now writes raw paths.
|
|
14
|
+
*
|
|
15
|
+
* When `layout` is missing (pre-1.3.0 `components.json` files), the
|
|
16
|
+
* fallback is `sourceRoot = "src"`, which matches the hardcoded behaviour
|
|
17
|
+
* the CLI shipped with before this refactor.
|
|
18
|
+
*/
|
|
19
|
+
export function resolveLocalDir(alias, componentsJson) {
|
|
20
|
+
for (const prefix of KNOWN_PREFIXES) {
|
|
21
|
+
if (alias.startsWith(prefix)) {
|
|
22
|
+
const sourceRoot = componentsJson.layout?.sourceRoot ?? "src";
|
|
23
|
+
return `${sourceRoot}/${alias.slice(prefix.length)}`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return alias;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Get the source root directory to use for project-level files like
|
|
30
|
+
* `lib/cn.ts` and `styles/tokens/`. Falls back to `"src"` for legacy
|
|
31
|
+
* `components.json` files without a `layout` field.
|
|
32
|
+
*/
|
|
33
|
+
export function getSourceRoot(componentsJson) {
|
|
34
|
+
return componentsJson.layout?.sourceRoot ?? "src";
|
|
35
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Registry, RegistryAuth } from "../types/registry.js";
|
|
2
|
+
/**
|
|
3
|
+
* Load the list of configured registries from `components.json`.
|
|
4
|
+
*
|
|
5
|
+
* Backward compatible: if only the legacy `registrySource` field is
|
|
6
|
+
* present, it is transparently upgraded in-memory to a single-entry
|
|
7
|
+
* `registries` array (without rewriting the file).
|
|
8
|
+
*/
|
|
9
|
+
export declare function loadRegistries(cwd: string): Promise<Registry[]>;
|
|
10
|
+
/** Pick the registry to use when a component name has no namespace. */
|
|
11
|
+
export declare function getDefaultRegistry(registries: Registry[]): Registry | undefined;
|
|
12
|
+
/** Look up a registry by its alias. */
|
|
13
|
+
export declare function getRegistryByName(registries: Registry[], name: string): Registry | undefined;
|
|
14
|
+
/**
|
|
15
|
+
* Resolve a bearer token from auth config. Checks env var first, then
|
|
16
|
+
* token file. Returns null (never the literal string "null") when no
|
|
17
|
+
* token is configured or reachable — callers decide whether that is
|
|
18
|
+
* fatal for their source type.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveToken(auth: RegistryAuth | undefined): string | null;
|
|
21
|
+
/**
|
|
22
|
+
* Persist a new `registries` array to `components.json`. If the file
|
|
23
|
+
* previously only had a legacy `registrySource`, that field is removed
|
|
24
|
+
* (it has been migrated into the array).
|
|
25
|
+
*/
|
|
26
|
+
export declare function saveRegistries(cwd: string, registries: Registry[]): void;
|
|
27
|
+
//# sourceMappingURL=registry-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry-config.d.ts","sourceRoot":"","sources":["../../src/utils/registry-config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAmBnE;;;;;;GAMG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAwBrE;AAED,uEAAuE;AACvE,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,QAAQ,EAAE,GACrB,QAAQ,GAAG,SAAS,CAGtB;AAED,uCAAuC;AACvC,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,QAAQ,EAAE,EACtB,IAAI,EAAE,MAAM,GACX,QAAQ,GAAG,SAAS,CAEtB;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,YAAY,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAc1E;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,IAAI,CAkBxE"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
function readRaw(cwd) {
|
|
4
|
+
const path = join(cwd, "components.json");
|
|
5
|
+
if (!existsSync(path))
|
|
6
|
+
return null;
|
|
7
|
+
const raw = readFileSync(path, "utf8");
|
|
8
|
+
return { path, data: JSON.parse(raw) };
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Load the list of configured registries from `components.json`.
|
|
12
|
+
*
|
|
13
|
+
* Backward compatible: if only the legacy `registrySource` field is
|
|
14
|
+
* present, it is transparently upgraded in-memory to a single-entry
|
|
15
|
+
* `registries` array (without rewriting the file).
|
|
16
|
+
*/
|
|
17
|
+
export async function loadRegistries(cwd) {
|
|
18
|
+
const raw = readRaw(cwd);
|
|
19
|
+
if (!raw)
|
|
20
|
+
return [];
|
|
21
|
+
const { data } = raw;
|
|
22
|
+
if (Array.isArray(data.registries) && data.registries.length > 0) {
|
|
23
|
+
return data.registries;
|
|
24
|
+
}
|
|
25
|
+
if (typeof data.registrySource === "string" && data.registrySource.length > 0) {
|
|
26
|
+
const legacy = data.registrySource === "local"
|
|
27
|
+
? { name: "default", source: "local", default: true, path: "packages/ui" }
|
|
28
|
+
: {
|
|
29
|
+
name: "default",
|
|
30
|
+
source: "git",
|
|
31
|
+
default: true,
|
|
32
|
+
url: data.registrySource,
|
|
33
|
+
branch: "main",
|
|
34
|
+
};
|
|
35
|
+
return [legacy];
|
|
36
|
+
}
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
/** Pick the registry to use when a component name has no namespace. */
|
|
40
|
+
export function getDefaultRegistry(registries) {
|
|
41
|
+
if (registries.length === 0)
|
|
42
|
+
return undefined;
|
|
43
|
+
return registries.find((r) => r.default) ?? registries[0];
|
|
44
|
+
}
|
|
45
|
+
/** Look up a registry by its alias. */
|
|
46
|
+
export function getRegistryByName(registries, name) {
|
|
47
|
+
return registries.find((r) => r.name === name);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Resolve a bearer token from auth config. Checks env var first, then
|
|
51
|
+
* token file. Returns null (never the literal string "null") when no
|
|
52
|
+
* token is configured or reachable — callers decide whether that is
|
|
53
|
+
* fatal for their source type.
|
|
54
|
+
*/
|
|
55
|
+
export function resolveToken(auth) {
|
|
56
|
+
if (!auth || auth.type !== "bearer")
|
|
57
|
+
return null;
|
|
58
|
+
if (auth.tokenEnv) {
|
|
59
|
+
const v = process.env[auth.tokenEnv];
|
|
60
|
+
if (v && v.length > 0)
|
|
61
|
+
return v;
|
|
62
|
+
}
|
|
63
|
+
if (auth.tokenFile) {
|
|
64
|
+
try {
|
|
65
|
+
return readFileSync(auth.tokenFile, "utf8").trim();
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Persist a new `registries` array to `components.json`. If the file
|
|
75
|
+
* previously only had a legacy `registrySource`, that field is removed
|
|
76
|
+
* (it has been migrated into the array).
|
|
77
|
+
*/
|
|
78
|
+
export function saveRegistries(cwd, registries) {
|
|
79
|
+
const raw = readRaw(cwd);
|
|
80
|
+
if (!raw) {
|
|
81
|
+
throw new Error("No components.json found. Run `inai-ui init` first before managing registries.");
|
|
82
|
+
}
|
|
83
|
+
const { path, data } = raw;
|
|
84
|
+
data.registries = registries;
|
|
85
|
+
// Keep legacy field only if it still matches the first registry's url;
|
|
86
|
+
// otherwise drop it so it doesn't confuse future migrations.
|
|
87
|
+
if (data.registrySource && registries.length > 0) {
|
|
88
|
+
const first = registries[0];
|
|
89
|
+
if (first.url !== data.registrySource && first.path !== data.registrySource) {
|
|
90
|
+
delete data.registrySource;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
94
|
+
}
|
|
@@ -1,4 +1,30 @@
|
|
|
1
|
+
import type { Registry, ResolveRegistryOptions } from "../types/registry.js";
|
|
1
2
|
export declare function getCacheDir(): string;
|
|
3
|
+
export declare function getRegistryCacheDir(name: string): string;
|
|
4
|
+
/**
|
|
5
|
+
* Clone (or fast-forward) a git registry into the legacy single-cache
|
|
6
|
+
* directory. Preserved for backward compatibility with the v0.5.0
|
|
7
|
+
* code path that called this directly.
|
|
8
|
+
*/
|
|
2
9
|
export declare function cloneRegistry(repoUrl: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Resolve a single named registry to a local directory. This is the
|
|
12
|
+
* M3 multi-registry entry point — callers pass a `Registry` record
|
|
13
|
+
* from `components.json` and get back a filesystem path where the
|
|
14
|
+
* `registry.json` and any component source files live.
|
|
15
|
+
*
|
|
16
|
+
* - `local`: returns the absolute path to the project-local registry.
|
|
17
|
+
* - `git`: clones/pulls into `~/.inai-ui/registries/<name>/`.
|
|
18
|
+
* - `https`: downloads `registry.json` into the same cache directory.
|
|
19
|
+
*
|
|
20
|
+
* When `options.offline` is true, never touches the network; throws a
|
|
21
|
+
* clear error if the cache does not exist.
|
|
22
|
+
*/
|
|
23
|
+
export declare function resolveRegistry(registry: Registry, projectDir: string, options?: ResolveRegistryOptions): Promise<string>;
|
|
24
|
+
/**
|
|
25
|
+
* Legacy single-registry resolver kept for backward compatibility with
|
|
26
|
+
* v0.5.0 callers (status, list, diff, theme). New code paths that
|
|
27
|
+
* need multi-registry support should call `resolveRegistry` directly.
|
|
28
|
+
*/
|
|
3
29
|
export declare function resolveRegistryDir(projectDir: string): string;
|
|
4
30
|
//# sourceMappingURL=registry-resolver.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry-resolver.d.ts","sourceRoot":"","sources":["../../src/utils/registry-resolver.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"registry-resolver.d.ts","sourceRoot":"","sources":["../../src/utils/registry-resolver.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAS7E,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAExD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAWrD;AAmBD;;;;;;;;;;;;GAYG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,QAAQ,EAClB,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,MAAM,CAAC,CAkGjB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAS7D"}
|
|
@@ -3,31 +3,155 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import { readComponentsJson } from "../commands/status.js";
|
|
6
|
+
import { resolveToken } from "./registry-config.js";
|
|
6
7
|
const CACHE_BASE = path.join(os.homedir(), ".inai-ui");
|
|
7
|
-
|
|
8
|
+
/** Legacy single-registry cache (v0.5.0 and earlier). */
|
|
9
|
+
const LEGACY_CACHE_DIR = path.join(CACHE_BASE, "registry");
|
|
10
|
+
/** New multi-registry cache root — one subdir per named registry. */
|
|
11
|
+
const REGISTRIES_CACHE_DIR = path.join(CACHE_BASE, "registries");
|
|
8
12
|
export function getCacheDir() {
|
|
9
|
-
return
|
|
13
|
+
return LEGACY_CACHE_DIR;
|
|
10
14
|
}
|
|
15
|
+
export function getRegistryCacheDir(name) {
|
|
16
|
+
return path.join(REGISTRIES_CACHE_DIR, name);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Clone (or fast-forward) a git registry into the legacy single-cache
|
|
20
|
+
* directory. Preserved for backward compatibility with the v0.5.0
|
|
21
|
+
* code path that called this directly.
|
|
22
|
+
*/
|
|
11
23
|
export function cloneRegistry(repoUrl) {
|
|
12
|
-
if (fs.existsSync(path.join(
|
|
13
|
-
execSync("git pull --ff-only", { cwd:
|
|
24
|
+
if (fs.existsSync(path.join(LEGACY_CACHE_DIR, ".git"))) {
|
|
25
|
+
execSync("git pull --ff-only", { cwd: LEGACY_CACHE_DIR, stdio: "pipe" });
|
|
14
26
|
}
|
|
15
27
|
else {
|
|
16
28
|
fs.mkdirSync(CACHE_BASE, { recursive: true });
|
|
17
|
-
if (fs.existsSync(
|
|
18
|
-
fs.rmSync(
|
|
29
|
+
if (fs.existsSync(LEGACY_CACHE_DIR)) {
|
|
30
|
+
fs.rmSync(LEGACY_CACHE_DIR, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
execSync(`git clone "${repoUrl}" "${LEGACY_CACHE_DIR}"`, { stdio: "pipe" });
|
|
33
|
+
}
|
|
34
|
+
return LEGACY_CACHE_DIR;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Embed a bearer token into an HTTPS git URL so `git clone` picks it
|
|
38
|
+
* up via the in-URL credential form. We never log the resulting URL
|
|
39
|
+
* (it contains the token) and only use this in-process.
|
|
40
|
+
*/
|
|
41
|
+
function embedTokenInGitUrl(url, token) {
|
|
42
|
+
try {
|
|
43
|
+
const u = new URL(url);
|
|
44
|
+
if (u.protocol !== "https:")
|
|
45
|
+
return url;
|
|
46
|
+
u.username = "oauth2";
|
|
47
|
+
u.password = token;
|
|
48
|
+
return u.toString();
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return url;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Resolve a single named registry to a local directory. This is the
|
|
56
|
+
* M3 multi-registry entry point — callers pass a `Registry` record
|
|
57
|
+
* from `components.json` and get back a filesystem path where the
|
|
58
|
+
* `registry.json` and any component source files live.
|
|
59
|
+
*
|
|
60
|
+
* - `local`: returns the absolute path to the project-local registry.
|
|
61
|
+
* - `git`: clones/pulls into `~/.inai-ui/registries/<name>/`.
|
|
62
|
+
* - `https`: downloads `registry.json` into the same cache directory.
|
|
63
|
+
*
|
|
64
|
+
* When `options.offline` is true, never touches the network; throws a
|
|
65
|
+
* clear error if the cache does not exist.
|
|
66
|
+
*/
|
|
67
|
+
export async function resolveRegistry(registry, projectDir, options = {}) {
|
|
68
|
+
const { offline = false } = options;
|
|
69
|
+
if (registry.source === "local") {
|
|
70
|
+
const localPath = registry.path
|
|
71
|
+
? path.isAbsolute(registry.path)
|
|
72
|
+
? registry.path
|
|
73
|
+
: path.join(projectDir, registry.path)
|
|
74
|
+
: projectDir;
|
|
75
|
+
return localPath;
|
|
76
|
+
}
|
|
77
|
+
const cacheDir = getRegistryCacheDir(registry.name);
|
|
78
|
+
if (registry.source === "git") {
|
|
79
|
+
if (!registry.url) {
|
|
80
|
+
throw new Error(`Registry "${registry.name}" has source "git" but no url.`);
|
|
81
|
+
}
|
|
82
|
+
if (offline) {
|
|
83
|
+
if (!fs.existsSync(path.join(cacheDir, ".git"))) {
|
|
84
|
+
throw new Error(`Offline mode: no cached copy of registry "${registry.name}" at ${cacheDir}. Run without --offline first to populate the cache.`);
|
|
85
|
+
}
|
|
86
|
+
return cacheDir;
|
|
87
|
+
}
|
|
88
|
+
const token = resolveToken(registry.auth);
|
|
89
|
+
const cloneUrl = token ? embedTokenInGitUrl(registry.url, token) : registry.url;
|
|
90
|
+
const branch = registry.branch ?? "main";
|
|
91
|
+
if (fs.existsSync(path.join(cacheDir, ".git"))) {
|
|
92
|
+
try {
|
|
93
|
+
execSync("git pull --ff-only", { cwd: cacheDir, stdio: "pipe" });
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// If pull fails (network hiccup, diverged), fall back to cached copy.
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
fs.mkdirSync(REGISTRIES_CACHE_DIR, { recursive: true });
|
|
101
|
+
if (fs.existsSync(cacheDir)) {
|
|
102
|
+
fs.rmSync(cacheDir, { recursive: true });
|
|
103
|
+
}
|
|
104
|
+
execSync(`git clone --branch "${branch}" "${cloneUrl}" "${cacheDir}"`, { stdio: "pipe" });
|
|
105
|
+
}
|
|
106
|
+
return cacheDir;
|
|
107
|
+
}
|
|
108
|
+
if (registry.source === "https") {
|
|
109
|
+
if (!registry.url) {
|
|
110
|
+
throw new Error(`Registry "${registry.name}" has source "https" but no url.`);
|
|
111
|
+
}
|
|
112
|
+
const cachedJson = path.join(cacheDir, "registry.json");
|
|
113
|
+
if (offline) {
|
|
114
|
+
if (!fs.existsSync(cachedJson)) {
|
|
115
|
+
throw new Error(`Offline mode: no cached copy of registry "${registry.name}" at ${cachedJson}.`);
|
|
116
|
+
}
|
|
117
|
+
return cacheDir;
|
|
118
|
+
}
|
|
119
|
+
const token = resolveToken(registry.auth);
|
|
120
|
+
const headers = { Accept: "application/json" };
|
|
121
|
+
if (token)
|
|
122
|
+
headers.Authorization = `Bearer ${token}`;
|
|
123
|
+
try {
|
|
124
|
+
const res = await fetch(registry.url, { headers });
|
|
125
|
+
if (!res.ok) {
|
|
126
|
+
throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
127
|
+
}
|
|
128
|
+
const body = await res.text();
|
|
129
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
130
|
+
fs.writeFileSync(cachedJson, body, "utf8");
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
if (!fs.existsSync(cachedJson)) {
|
|
134
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
135
|
+
throw new Error(`Failed to fetch registry "${registry.name}" from ${registry.url}: ${reason}`);
|
|
136
|
+
}
|
|
137
|
+
// Fall through to cached copy if fetch failed but we have one.
|
|
19
138
|
}
|
|
20
|
-
|
|
139
|
+
return cacheDir;
|
|
21
140
|
}
|
|
22
|
-
|
|
141
|
+
throw new Error(`Unknown registry source "${registry.source}" for "${registry.name}".`);
|
|
23
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Legacy single-registry resolver kept for backward compatibility with
|
|
145
|
+
* v0.5.0 callers (status, list, diff, theme). New code paths that
|
|
146
|
+
* need multi-registry support should call `resolveRegistry` directly.
|
|
147
|
+
*/
|
|
24
148
|
export function resolveRegistryDir(projectDir) {
|
|
25
149
|
const componentsJson = readComponentsJson(projectDir);
|
|
26
150
|
if (componentsJson?.registrySource) {
|
|
27
|
-
if (!fs.existsSync(path.join(
|
|
151
|
+
if (!fs.existsSync(path.join(LEGACY_CACHE_DIR, ".git"))) {
|
|
28
152
|
cloneRegistry(componentsJson.registrySource);
|
|
29
153
|
}
|
|
30
|
-
return
|
|
154
|
+
return LEGACY_CACHE_DIR;
|
|
31
155
|
}
|
|
32
156
|
return projectDir;
|
|
33
157
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const SNAPSHOT_DIR = ".inai-ui/snapshots";
|
|
2
|
+
export interface SnapshotFile {
|
|
3
|
+
/** Path relative to the project root of the local copy at capture time. */
|
|
4
|
+
localPath: string;
|
|
5
|
+
/** SHA256 hash of the captured content. */
|
|
6
|
+
hash: string;
|
|
7
|
+
/** Full original content captured at install time. */
|
|
8
|
+
content: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ComponentSnapshot {
|
|
11
|
+
name: string;
|
|
12
|
+
version: string;
|
|
13
|
+
files: Record<string, SnapshotFile>;
|
|
14
|
+
capturedAt: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function sha256(content: string): string;
|
|
17
|
+
export declare function saveSnapshot(cwd: string, snapshot: ComponentSnapshot): void;
|
|
18
|
+
export declare function loadSnapshot(cwd: string, name: string): ComponentSnapshot | null;
|
|
19
|
+
export declare function deleteSnapshot(cwd: string, name: string): void;
|
|
20
|
+
//# sourceMappingURL=snapshot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot.d.ts","sourceRoot":"","sources":["../../src/utils/snapshot.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,YAAY,uBAAuB,CAAC;AAEjD,MAAM,WAAW,YAAY;IAC3B,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAC;IAClB,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACpC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE9C;AAMD,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAQ3E;AAED,wBAAgB,YAAY,CAC1B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,GACX,iBAAiB,GAAG,IAAI,CAQ1B;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAK9D"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
export const SNAPSHOT_DIR = ".inai-ui/snapshots";
|
|
5
|
+
export function sha256(content) {
|
|
6
|
+
return createHash("sha256").update(content).digest("hex");
|
|
7
|
+
}
|
|
8
|
+
function snapshotPath(cwd, name) {
|
|
9
|
+
return path.join(cwd, SNAPSHOT_DIR, `${name}.json`);
|
|
10
|
+
}
|
|
11
|
+
export function saveSnapshot(cwd, snapshot) {
|
|
12
|
+
const dir = path.join(cwd, SNAPSHOT_DIR);
|
|
13
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
14
|
+
fs.writeFileSync(snapshotPath(cwd, snapshot.name), JSON.stringify(snapshot, null, 2) + "\n", "utf8");
|
|
15
|
+
}
|
|
16
|
+
export function loadSnapshot(cwd, name) {
|
|
17
|
+
const p = snapshotPath(cwd, name);
|
|
18
|
+
if (!fs.existsSync(p))
|
|
19
|
+
return null;
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(fs.readFileSync(p, "utf8"));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function deleteSnapshot(cwd, name) {
|
|
28
|
+
const p = snapshotPath(cwd, name);
|
|
29
|
+
if (fs.existsSync(p)) {
|
|
30
|
+
fs.unlinkSync(p);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the built-in theme list used by the CLI
|
|
3
|
+
* (init, theme create, add). The registry on disk may expose more themes
|
|
4
|
+
* (discovered from packages/tokens/src/themes/*.css), but this list is the
|
|
5
|
+
* canonical fallback used when no registry is available and for validation.
|
|
6
|
+
*/
|
|
7
|
+
export type ThemeCategory = "brand" | "shadcn" | "creative";
|
|
8
|
+
export interface ThemeInfo {
|
|
9
|
+
name: string;
|
|
10
|
+
label: string;
|
|
11
|
+
category: ThemeCategory;
|
|
12
|
+
description: string;
|
|
13
|
+
}
|
|
14
|
+
export declare const THEMES: ThemeInfo[];
|
|
15
|
+
export declare const THEME_NAMES: string[];
|
|
16
|
+
export declare function isKnownTheme(name: string): boolean;
|
|
17
|
+
//# sourceMappingURL=themes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"themes.d.ts","sourceRoot":"","sources":["../../src/utils/themes.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,QAAQ,GAAG,UAAU,CAAC;AAE5D,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,aAAa,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,MAAM,EAAE,SAAS,EAwC7B,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,MAAM,EAA8B,CAAC;AAE/D,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAElD"}
|