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.
Files changed (68) hide show
  1. package/README.md +80 -0
  2. package/dist/commands/add.d.ts +42 -12
  3. package/dist/commands/add.d.ts.map +1 -1
  4. package/dist/commands/add.js +413 -93
  5. package/dist/commands/diff.d.ts +6 -0
  6. package/dist/commands/diff.d.ts.map +1 -1
  7. package/dist/commands/diff.js +90 -20
  8. package/dist/commands/init.d.ts +17 -5
  9. package/dist/commands/init.d.ts.map +1 -1
  10. package/dist/commands/init.js +87 -64
  11. package/dist/commands/mcp.d.ts +123 -0
  12. package/dist/commands/mcp.d.ts.map +1 -0
  13. package/dist/commands/mcp.js +289 -0
  14. package/dist/commands/migrate.d.ts +41 -0
  15. package/dist/commands/migrate.d.ts.map +1 -0
  16. package/dist/commands/migrate.js +139 -0
  17. package/dist/commands/registries.d.ts +46 -0
  18. package/dist/commands/registries.d.ts.map +1 -0
  19. package/dist/commands/registries.js +152 -0
  20. package/dist/commands/remove.d.ts +11 -0
  21. package/dist/commands/remove.d.ts.map +1 -0
  22. package/dist/commands/remove.js +171 -0
  23. package/dist/commands/schema.d.ts +16 -0
  24. package/dist/commands/schema.d.ts.map +1 -0
  25. package/dist/commands/schema.js +32 -0
  26. package/dist/commands/status.d.ts +40 -4
  27. package/dist/commands/status.d.ts.map +1 -1
  28. package/dist/commands/status.js +149 -25
  29. package/dist/commands/theme.d.ts.map +1 -1
  30. package/dist/commands/theme.js +3 -37
  31. package/dist/commands/update.d.ts +18 -1
  32. package/dist/commands/update.d.ts.map +1 -1
  33. package/dist/commands/update.js +197 -6
  34. package/dist/index.js +76 -5
  35. package/dist/schemas/registry-config.schema.json +62 -0
  36. package/dist/schemas/registry-item.schema.json +63 -0
  37. package/dist/schemas/registry.schema.json +15 -0
  38. package/dist/types/registry.d.ts +50 -0
  39. package/dist/types/registry.d.ts.map +1 -0
  40. package/dist/types/registry.js +10 -0
  41. package/dist/utils/auto-install.d.ts +11 -0
  42. package/dist/utils/auto-install.d.ts.map +1 -0
  43. package/dist/utils/auto-install.js +29 -0
  44. package/dist/utils/framework-detect.d.ts +20 -0
  45. package/dist/utils/framework-detect.d.ts.map +1 -0
  46. package/dist/utils/framework-detect.js +97 -0
  47. package/dist/utils/fuzzy-search.d.ts +16 -0
  48. package/dist/utils/fuzzy-search.d.ts.map +1 -0
  49. package/dist/utils/fuzzy-search.js +67 -0
  50. package/dist/utils/paths.d.ts +26 -0
  51. package/dist/utils/paths.d.ts.map +1 -0
  52. package/dist/utils/paths.js +35 -0
  53. package/dist/utils/registry-config.d.ts +27 -0
  54. package/dist/utils/registry-config.d.ts.map +1 -0
  55. package/dist/utils/registry-config.js +94 -0
  56. package/dist/utils/registry-resolver.d.ts +26 -0
  57. package/dist/utils/registry-resolver.d.ts.map +1 -1
  58. package/dist/utils/registry-resolver.js +134 -10
  59. package/dist/utils/snapshot.d.ts +20 -0
  60. package/dist/utils/snapshot.d.ts.map +1 -0
  61. package/dist/utils/snapshot.js +32 -0
  62. package/dist/utils/themes.d.ts +17 -0
  63. package/dist/utils/themes.d.ts.map +1 -0
  64. package/dist/utils/themes.js +49 -0
  65. package/dist/utils/tsconfig-detect.d.ts +18 -0
  66. package/dist/utils/tsconfig-detect.d.ts.map +1 -0
  67. package/dist/utils/tsconfig-detect.js +121 -0
  68. 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":"AASA,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAWrD;AAED,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAS7D"}
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
- const CACHE_DIR = path.join(CACHE_BASE, "registry");
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 CACHE_DIR;
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(CACHE_DIR, ".git"))) {
13
- execSync("git pull --ff-only", { cwd: CACHE_DIR, stdio: "pipe" });
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(CACHE_DIR)) {
18
- fs.rmSync(CACHE_DIR, { recursive: true });
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
- execSync(`git clone "${repoUrl}" "${CACHE_DIR}"`, { stdio: "pipe" });
139
+ return cacheDir;
21
140
  }
22
- return CACHE_DIR;
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(CACHE_DIR, ".git"))) {
151
+ if (!fs.existsSync(path.join(LEGACY_CACHE_DIR, ".git"))) {
28
152
  cloneRegistry(componentsJson.registrySource);
29
153
  }
30
- return CACHE_DIR;
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"}