inai-react-components 0.1.7 → 1.2.1

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 (62) hide show
  1. package/README.md +80 -0
  2. package/dist/commands/add.d.ts +38 -5
  3. package/dist/commands/add.d.ts.map +1 -1
  4. package/dist/commands/add.js +391 -80
  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 +92 -20
  8. package/dist/commands/init.d.ts +6 -3
  9. package/dist/commands/init.d.ts.map +1 -1
  10. package/dist/commands/init.js +33 -36
  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 +170 -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 +32 -3
  27. package/dist/commands/status.d.ts.map +1 -1
  28. package/dist/commands/status.js +148 -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 +195 -5
  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 +15 -0
  45. package/dist/utils/framework-detect.d.ts.map +1 -0
  46. package/dist/utils/framework-detect.js +90 -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/registry-config.d.ts +27 -0
  51. package/dist/utils/registry-config.d.ts.map +1 -0
  52. package/dist/utils/registry-config.js +94 -0
  53. package/dist/utils/registry-resolver.d.ts +26 -0
  54. package/dist/utils/registry-resolver.d.ts.map +1 -1
  55. package/dist/utils/registry-resolver.js +134 -10
  56. package/dist/utils/snapshot.d.ts +20 -0
  57. package/dist/utils/snapshot.d.ts.map +1 -0
  58. package/dist/utils/snapshot.js +32 -0
  59. package/dist/utils/themes.d.ts +17 -0
  60. package/dist/utils/themes.d.ts.map +1 -0
  61. package/dist/utils/themes.js +49 -0
  62. package/package.json +9 -3
@@ -0,0 +1,90 @@
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. Used by `inai-ui init`
52
+ * to pre-populate the prompt that asks where tailwind/tokens should be
53
+ * imported.
54
+ */
55
+ export function getCssEntryPoint(framework) {
56
+ switch (framework) {
57
+ case "next-app":
58
+ return "app/globals.css";
59
+ case "next-pages":
60
+ return "styles/globals.css";
61
+ case "vite":
62
+ return "src/index.css";
63
+ case "remix":
64
+ return "app/styles/globals.css";
65
+ case "astro":
66
+ return "src/styles/globals.css";
67
+ case "tanstack-start":
68
+ return "src/styles/app.css";
69
+ default:
70
+ return "src/index.css";
71
+ }
72
+ }
73
+ export function frameworkLabel(framework) {
74
+ switch (framework) {
75
+ case "next-app":
76
+ return "Next.js (App Router)";
77
+ case "next-pages":
78
+ return "Next.js (Pages Router)";
79
+ case "vite":
80
+ return "Vite";
81
+ case "remix":
82
+ return "Remix";
83
+ case "astro":
84
+ return "Astro";
85
+ case "tanstack-start":
86
+ return "TanStack Start";
87
+ default:
88
+ return "Unknown";
89
+ }
90
+ }
@@ -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,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"}
@@ -0,0 +1,49 @@
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 const THEMES = [
8
+ // Brand
9
+ { name: "inai", label: "InAI", category: "brand", description: "Default InAI brand theme — vibrant purple primary on OKLCH." },
10
+ { name: "monday", label: "Monday", category: "brand", description: "Monday.com-inspired vibrant SaaS aesthetic." },
11
+ { name: "linear", label: "Linear", category: "brand", description: "Linear-inspired minimal monochrome theme." },
12
+ { name: "notion", label: "Notion", category: "brand", description: "Notion-inspired soft neutral theme." },
13
+ { name: "vercel", label: "Vercel", category: "brand", description: "Vercel-inspired high-contrast black & white." },
14
+ { name: "anthropic", label: "Anthropic", category: "brand", description: "Anthropic-inspired warm editorial theme." },
15
+ // shadcn palette (17)
16
+ { name: "shadcn-default", label: "shadcn Default", category: "shadcn", description: "shadcn/ui default neutral theme." },
17
+ { name: "shadcn-blue", label: "shadcn Blue", category: "shadcn", description: "shadcn/ui blue primary variant." },
18
+ { name: "shadcn-green", label: "shadcn Green", category: "shadcn", description: "shadcn/ui green primary variant." },
19
+ { name: "shadcn-orange", label: "shadcn Orange", category: "shadcn", description: "shadcn/ui orange primary variant." },
20
+ { name: "shadcn-red", label: "shadcn Red", category: "shadcn", description: "shadcn/ui red primary variant." },
21
+ { name: "shadcn-rose", label: "shadcn Rose", category: "shadcn", description: "shadcn/ui rose primary variant." },
22
+ { name: "shadcn-violet", label: "shadcn Violet", category: "shadcn", description: "shadcn/ui violet primary variant." },
23
+ { name: "shadcn-yellow", label: "shadcn Yellow", category: "shadcn", description: "shadcn/ui yellow primary variant." },
24
+ { name: "shadcn-amber", label: "shadcn Amber", category: "shadcn", description: "shadcn/ui amber primary variant." },
25
+ { name: "shadcn-lime", label: "shadcn Lime", category: "shadcn", description: "shadcn/ui lime primary variant." },
26
+ { name: "shadcn-emerald", label: "shadcn Emerald", category: "shadcn", description: "shadcn/ui emerald primary variant." },
27
+ { name: "shadcn-teal", label: "shadcn Teal", category: "shadcn", description: "shadcn/ui teal primary variant." },
28
+ { name: "shadcn-cyan", label: "shadcn Cyan", category: "shadcn", description: "shadcn/ui cyan primary variant." },
29
+ { name: "shadcn-sky", label: "shadcn Sky", category: "shadcn", description: "shadcn/ui sky primary variant." },
30
+ { name: "shadcn-indigo", label: "shadcn Indigo", category: "shadcn", description: "shadcn/ui indigo primary variant." },
31
+ { name: "shadcn-purple", label: "shadcn Purple", category: "shadcn", description: "shadcn/ui purple primary variant." },
32
+ { name: "shadcn-fuchsia", label: "shadcn Fuchsia", category: "shadcn", description: "shadcn/ui fuchsia primary variant." },
33
+ { name: "shadcn-pink", label: "shadcn Pink", category: "shadcn", description: "shadcn/ui pink primary variant." },
34
+ // Creative (10)
35
+ { name: "cyberpunk", label: "Cyberpunk", category: "creative", description: "Neon pink and electric cyan on deep violet." },
36
+ { name: "aurora", label: "Aurora", category: "creative", description: "Northern-lights greens and teals." },
37
+ { name: "sunset", label: "Sunset", category: "creative", description: "Warm orange-to-pink gradient palette." },
38
+ { name: "midnight", label: "Midnight", category: "creative", description: "Deep-blue near-black editorial theme." },
39
+ { name: "acid", label: "Acid", category: "creative", description: "High-energy acid green and yellow." },
40
+ { name: "sakura", label: "Sakura", category: "creative", description: "Soft cherry-blossom pinks and neutrals." },
41
+ { name: "ocean", label: "Ocean", category: "creative", description: "Deep ocean blues and cyans." },
42
+ { name: "cosmic", label: "Cosmic", category: "creative", description: "Space-inspired violet and nebula hues." },
43
+ { name: "matcha", label: "Matcha", category: "creative", description: "Earthy matcha greens and cream." },
44
+ { name: "volcanic", label: "Volcanic", category: "creative", description: "Charcoal and lava orange contrast." },
45
+ ];
46
+ export const THEME_NAMES = THEMES.map((t) => t.name);
47
+ export function isKnownTheme(name) {
48
+ return THEME_NAMES.includes(name);
49
+ }