uilint 0.2.13 → 0.2.15

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.
@@ -7,13 +7,14 @@ import {
7
7
  multiselect,
8
8
  note,
9
9
  pc,
10
- printBox,
11
10
  select
12
- } from "./chunk-EYCSOCU4.js";
11
+ } from "./chunk-FRNXXIEM.js";
13
12
  import {
14
13
  GENSTYLEGUIDE_COMMAND_MD,
14
+ detectPackageManager,
15
+ installDependencies,
15
16
  loadSkill
16
- } from "./chunk-2RNDQVEK.js";
17
+ } from "./chunk-PBEKMDUH.js";
17
18
 
18
19
  // src/commands/install-ui.tsx
19
20
  import { render } from "ink";
@@ -141,6 +142,9 @@ function StatusIndicator({ status, isSelected }) {
141
142
  if (status === "installed") {
142
143
  return /* @__PURE__ */ jsx3(Text3, { color: "green", children: "\u2713" });
143
144
  }
145
+ if (status === "upgradeable") {
146
+ return isSelected ? /* @__PURE__ */ jsx3(Text3, { color: "blue", children: "\u2B06" }) : /* @__PURE__ */ jsx3(Text3, { color: "green", children: "\u2713" });
147
+ }
144
148
  if (isSelected || status === "selected") {
145
149
  return /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "\u25C9" });
146
150
  }
@@ -153,6 +157,9 @@ function StatusLabel({ status }) {
153
157
  if (status === "installed") {
154
158
  return /* @__PURE__ */ jsx3(Text3, { color: "green", dimColor: true, children: "installed" });
155
159
  }
160
+ if (status === "upgradeable") {
161
+ return /* @__PURE__ */ jsx3(Text3, { color: "blue", dimColor: true, children: "upgrade available" });
162
+ }
156
163
  if (status === "partial") {
157
164
  return /* @__PURE__ */ jsx3(Text3, { color: "yellow", dimColor: true, children: "partial" });
158
165
  }
@@ -178,7 +185,8 @@ function ConfigSelector({
178
185
  const flatItems = items;
179
186
  const handleToggle = useCallback(() => {
180
187
  const item = flatItems[cursor];
181
- if (!item || item.disabled || item.status === "installed") return;
188
+ const canToggle = item && !item.disabled && item.status !== "installed";
189
+ if (!canToggle) return;
182
190
  setSelected((prev) => {
183
191
  const next = new Set(prev);
184
192
  if (next.has(item.id)) {
@@ -519,6 +527,15 @@ function getAllInstallers() {
519
527
 
520
528
  // src/commands/install/components/InstallApp.tsx
521
529
  import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
530
+ function getTargetStatus(target) {
531
+ if (!target.isInstalled) {
532
+ return "not_installed";
533
+ }
534
+ if (target.canUpgrade) {
535
+ return "upgradeable";
536
+ }
537
+ return "installed";
538
+ }
522
539
  function buildConfigItemsForProject(project, selectedProject, selections) {
523
540
  const items = [];
524
541
  for (const selection of selections) {
@@ -546,7 +563,7 @@ function buildConfigItemsForProject(project, selectedProject, selections) {
546
563
  id: `${installer.id}:${target.id}`,
547
564
  label: installer.name,
548
565
  hint: target.hint,
549
- status: target.isInstalled ? "installed" : "not_installed",
566
+ status: getTargetStatus(target),
550
567
  category: info.category,
551
568
  categoryIcon: info.icon
552
569
  });
@@ -571,7 +588,7 @@ function buildGlobalConfigItems(selections) {
571
588
  id: `${installer.id}:${target.id}`,
572
589
  label: installer.name,
573
590
  hint: target.hint,
574
- status: target.isInstalled ? "installed" : "not_installed",
591
+ status: getTargetStatus(target),
575
592
  category: info.category,
576
593
  categoryIcon: info.icon
577
594
  });
@@ -653,11 +670,13 @@ function InstallApp({
653
670
  const installers2 = getAllInstallers();
654
671
  const initialSelections = installers2.filter((installer) => installer.isApplicable(proj)).map((installer) => {
655
672
  const targets = installer.getTargets(proj);
656
- const nonInstalledTargets = targets.filter((t) => !t.isInstalled);
673
+ const actionableTargets = targets.filter(
674
+ (t) => !t.isInstalled || t.canUpgrade
675
+ );
657
676
  return {
658
677
  installer,
659
678
  targets,
660
- selected: nonInstalledTargets.length > 0
679
+ selected: actionableTargets.length > 0
661
680
  };
662
681
  });
663
682
  setSelections(initialSelections);
@@ -798,8 +817,8 @@ function InstallApp({
798
817
  }
799
818
 
800
819
  // src/commands/install/analyze.ts
801
- import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
802
- import { join as join5 } from "path";
820
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
821
+ import { join as join4 } from "path";
803
822
  import { findWorkspaceRoot as findWorkspaceRoot2 } from "uilint-core/node";
804
823
 
805
824
  // src/utils/vite-detect.ts
@@ -1043,86 +1062,16 @@ function findPackages(rootDir, options) {
1043
1062
  });
1044
1063
  }
1045
1064
 
1046
- // src/utils/package-manager.ts
1047
- import { existsSync as existsSync3 } from "fs";
1048
- import { spawn } from "child_process";
1049
- import { dirname, join as join3 } from "path";
1050
- function detectExecutionPackageManager() {
1051
- const userAgent = process.env.npm_config_user_agent;
1052
- if (userAgent) {
1053
- if (userAgent.startsWith("pnpm/")) return "pnpm";
1054
- if (userAgent.startsWith("yarn/")) return "yarn";
1055
- if (userAgent.startsWith("bun/")) return "bun";
1056
- if (userAgent.startsWith("npm/")) return "npm";
1057
- }
1058
- const execPath = process.env.npm_execpath;
1059
- if (execPath) {
1060
- if (execPath.includes("pnpm")) return "pnpm";
1061
- if (execPath.includes("yarn")) return "yarn";
1062
- if (execPath.includes("bun")) return "bun";
1063
- if (execPath.includes("npm")) return "npm";
1064
- }
1065
- return null;
1066
- }
1067
- function detectPackageManager(projectPath) {
1068
- let dir = projectPath;
1069
- for (; ; ) {
1070
- if (existsSync3(join3(dir, "pnpm-lock.yaml"))) return "pnpm";
1071
- if (existsSync3(join3(dir, "pnpm-workspace.yaml"))) return "pnpm";
1072
- if (existsSync3(join3(dir, "yarn.lock"))) return "yarn";
1073
- if (existsSync3(join3(dir, "bun.lockb"))) return "bun";
1074
- if (existsSync3(join3(dir, "bun.lock"))) return "bun";
1075
- if (existsSync3(join3(dir, "package-lock.json"))) return "npm";
1076
- const parent = dirname(dir);
1077
- if (parent === dir) break;
1078
- dir = parent;
1079
- }
1080
- return "npm";
1081
- }
1082
- function spawnAsync(command, args, cwd) {
1083
- return new Promise((resolve, reject) => {
1084
- const child = spawn(command, args, {
1085
- cwd,
1086
- stdio: "inherit",
1087
- shell: process.platform === "win32"
1088
- });
1089
- child.on("error", reject);
1090
- child.on("close", (code) => {
1091
- if (code === 0) resolve();
1092
- else
1093
- reject(new Error(`${command} ${args.join(" ")} exited with ${code}`));
1094
- });
1095
- });
1096
- }
1097
- async function installDependencies(pm, projectPath, packages) {
1098
- if (!packages.length) return;
1099
- switch (pm) {
1100
- case "pnpm":
1101
- await spawnAsync("pnpm", ["add", ...packages], projectPath);
1102
- return;
1103
- case "yarn":
1104
- await spawnAsync("yarn", ["add", ...packages], projectPath);
1105
- return;
1106
- case "bun":
1107
- await spawnAsync("bun", ["add", ...packages], projectPath);
1108
- return;
1109
- case "npm":
1110
- default:
1111
- await spawnAsync("npm", ["install", "--save", ...packages], projectPath);
1112
- return;
1113
- }
1114
- }
1115
-
1116
1065
  // src/utils/eslint-config-inject.ts
1117
- import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync } from "fs";
1118
- import { join as join4, relative as relative2, dirname as dirname2 } from "path";
1066
+ import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync } from "fs";
1067
+ import { join as join3, relative as relative2, dirname } from "path";
1119
1068
  import { parseExpression, parseModule, generateCode } from "magicast";
1120
1069
  import { findWorkspaceRoot } from "uilint-core/node";
1121
1070
  var CONFIG_EXTENSIONS = [".ts", ".mjs", ".js", ".cjs"];
1122
1071
  function findEslintConfigFile(projectPath) {
1123
1072
  for (const ext of CONFIG_EXTENSIONS) {
1124
- const configPath = join4(projectPath, `eslint.config${ext}`);
1125
- if (existsSync4(configPath)) {
1073
+ const configPath = join3(projectPath, `eslint.config${ext}`);
1074
+ if (existsSync3(configPath)) {
1126
1075
  return configPath;
1127
1076
  }
1128
1077
  }
@@ -1345,8 +1294,8 @@ function chooseUniqueIdentifier(base, used) {
1345
1294
  function addLocalRuleImportsAst(mod, selectedRules, configPath, rulesRoot, fileExtension = ".js") {
1346
1295
  const importNames = /* @__PURE__ */ new Map();
1347
1296
  let changed = false;
1348
- const configDir = dirname2(configPath);
1349
- const rulesDir = join4(rulesRoot, ".uilint", "rules");
1297
+ const configDir = dirname(configPath);
1298
+ const rulesDir = join3(rulesRoot, ".uilint", "rules");
1350
1299
  const relativeRulesPath = relative2(configDir, rulesDir).replace(/\\/g, "/");
1351
1300
  const normalizedRulesPath = relativeRulesPath.startsWith("./") || relativeRulesPath.startsWith("../") ? relativeRulesPath : `./${relativeRulesPath}`;
1352
1301
  const used = collectTopLevelBindings(mod.$ast);
@@ -1373,8 +1322,8 @@ function addLocalRuleRequiresAst(program, selectedRules, configPath, rulesRoot,
1373
1322
  if (!program || program.type !== "Program") {
1374
1323
  return { importNames, changed };
1375
1324
  }
1376
- const configDir = dirname2(configPath);
1377
- const rulesDir = join4(rulesRoot, ".uilint", "rules");
1325
+ const configDir = dirname(configPath);
1326
+ const rulesDir = join3(rulesRoot, ".uilint", "rules");
1378
1327
  const relativeRulesPath = relative2(configDir, rulesDir).replace(/\\/g, "/");
1379
1328
  const normalizedRulesPath = relativeRulesPath.startsWith("./") || relativeRulesPath.startsWith("../") ? relativeRulesPath : `./${relativeRulesPath}`;
1380
1329
  const used = collectTopLevelBindings(program);
@@ -1585,10 +1534,10 @@ async function installEslintPlugin(opts) {
1585
1534
  };
1586
1535
  }
1587
1536
  let modifiedAst = false;
1588
- const localRulesDir = join4(opts.projectPath, ".uilint", "rules");
1537
+ const localRulesDir = join3(opts.projectPath, ".uilint", "rules");
1589
1538
  const workspaceRoot = findWorkspaceRoot(opts.projectPath);
1590
- const workspaceRulesDir = join4(workspaceRoot, ".uilint", "rules");
1591
- const rulesRoot = existsSync4(localRulesDir) ? opts.projectPath : workspaceRoot;
1539
+ const workspaceRulesDir = join3(workspaceRoot, ".uilint", "rules");
1540
+ const rulesRoot = existsSync3(localRulesDir) ? opts.projectPath : workspaceRoot;
1592
1541
  const isTypeScriptConfig = configPath.endsWith(".ts");
1593
1542
  let fileExtension = isTypeScriptConfig ? "" : ".js";
1594
1543
  let ruleImportNames;
@@ -1663,12 +1612,12 @@ async function installEslintPlugin(opts) {
1663
1612
  async function analyze(projectPath = process.cwd()) {
1664
1613
  const workspaceRoot = findWorkspaceRoot2(projectPath);
1665
1614
  const packageManager = detectPackageManager(projectPath);
1666
- const cursorDir = join5(projectPath, ".cursor");
1667
- const cursorDirExists = existsSync5(cursorDir);
1668
- const styleguidePath = join5(projectPath, ".uilint", "styleguide.md");
1669
- const styleguideExists = existsSync5(styleguidePath);
1670
- const commandsDir = join5(cursorDir, "commands");
1671
- const genstyleguideExists = existsSync5(join5(commandsDir, "genstyleguide.md"));
1615
+ const cursorDir = join4(projectPath, ".cursor");
1616
+ const cursorDirExists = existsSync4(cursorDir);
1617
+ const styleguidePath = join4(projectPath, ".uilint", "styleguide.md");
1618
+ const styleguideExists = existsSync4(styleguidePath);
1619
+ const commandsDir = join4(cursorDir, "commands");
1620
+ const genstyleguideExists = existsSync4(join4(commandsDir, "genstyleguide.md"));
1672
1621
  const nextApps = [];
1673
1622
  const directDetection = detectNextAppRouter(projectPath);
1674
1623
  if (directDetection) {
@@ -1742,7 +1691,7 @@ async function analyze(projectPath = process.cwd()) {
1742
1691
 
1743
1692
  // src/commands/install/execute.ts
1744
1693
  import {
1745
- existsSync as existsSync11,
1694
+ existsSync as existsSync10,
1746
1695
  mkdirSync,
1747
1696
  writeFileSync as writeFileSync5,
1748
1697
  readFileSync as readFileSync8,
@@ -1752,39 +1701,39 @@ import {
1752
1701
  import { dirname as dirname4 } from "path";
1753
1702
 
1754
1703
  // src/utils/react-inject.ts
1755
- import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
1756
- import { join as join6 } from "path";
1704
+ import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
1705
+ import { join as join5, relative as relative3 } from "path";
1757
1706
  import { parseModule as parseModule2, generateCode as generateCode2 } from "magicast";
1758
1707
  function getDefaultCandidates(projectPath, appRoot) {
1759
1708
  const viteMainCandidates = [
1760
- join6(appRoot, "main.tsx"),
1761
- join6(appRoot, "main.jsx"),
1762
- join6(appRoot, "main.ts"),
1763
- join6(appRoot, "main.js")
1709
+ join5(appRoot, "main.tsx"),
1710
+ join5(appRoot, "main.jsx"),
1711
+ join5(appRoot, "main.ts"),
1712
+ join5(appRoot, "main.js")
1764
1713
  ];
1765
1714
  const existingViteMain = viteMainCandidates.filter(
1766
- (rel) => existsSync6(join6(projectPath, rel))
1715
+ (rel) => existsSync5(join5(projectPath, rel))
1767
1716
  );
1768
1717
  if (existingViteMain.length > 0) return existingViteMain;
1769
- const viteAppCandidates = [join6(appRoot, "App.tsx"), join6(appRoot, "App.jsx")];
1718
+ const viteAppCandidates = [join5(appRoot, "App.tsx"), join5(appRoot, "App.jsx")];
1770
1719
  const existingViteApp = viteAppCandidates.filter(
1771
- (rel) => existsSync6(join6(projectPath, rel))
1720
+ (rel) => existsSync5(join5(projectPath, rel))
1772
1721
  );
1773
1722
  if (existingViteApp.length > 0) return existingViteApp;
1774
1723
  const layoutCandidates = [
1775
- join6(appRoot, "layout.tsx"),
1776
- join6(appRoot, "layout.jsx"),
1777
- join6(appRoot, "layout.ts"),
1778
- join6(appRoot, "layout.js")
1724
+ join5(appRoot, "layout.tsx"),
1725
+ join5(appRoot, "layout.jsx"),
1726
+ join5(appRoot, "layout.ts"),
1727
+ join5(appRoot, "layout.js")
1779
1728
  ];
1780
1729
  const existingLayouts = layoutCandidates.filter(
1781
- (rel) => existsSync6(join6(projectPath, rel))
1730
+ (rel) => existsSync5(join5(projectPath, rel))
1782
1731
  );
1783
1732
  if (existingLayouts.length > 0) {
1784
1733
  return existingLayouts;
1785
1734
  }
1786
- const pageCandidates = [join6(appRoot, "page.tsx"), join6(appRoot, "page.jsx")];
1787
- return pageCandidates.filter((rel) => existsSync6(join6(projectPath, rel)));
1735
+ const pageCandidates = [join5(appRoot, "page.tsx"), join5(appRoot, "page.jsx")];
1736
+ return pageCandidates.filter((rel) => existsSync5(join5(projectPath, rel)));
1788
1737
  }
1789
1738
  function isUseClientDirective(stmt) {
1790
1739
  return stmt?.type === "ExpressionStatement" && stmt.expression?.type === "StringLiteral" && stmt.expression.value === "use client";
@@ -1810,6 +1759,32 @@ function walkAst(node, visit) {
1810
1759
  }
1811
1760
  }
1812
1761
  }
1762
+ function ensureNamedImport(program, from, name) {
1763
+ if (!program || program.type !== "Program") return { changed: false };
1764
+ const existing = findImportDeclaration(program, from);
1765
+ if (existing) {
1766
+ const has = (existing.specifiers ?? []).some(
1767
+ (s) => s?.type === "ImportSpecifier" && (s.imported?.name === name || s.imported?.value === name)
1768
+ );
1769
+ if (has) return { changed: false };
1770
+ const spec = parseModule2(`import { ${name} } from "${from}";`).$ast.body?.[0]?.specifiers?.[0];
1771
+ if (!spec) return { changed: false };
1772
+ existing.specifiers = [...existing.specifiers ?? [], spec];
1773
+ return { changed: true };
1774
+ }
1775
+ const importDecl = parseModule2(`import { ${name} } from "${from}";`).$ast.body?.[0];
1776
+ if (!importDecl) return { changed: false };
1777
+ const body = program.body ?? [];
1778
+ let insertAt = 0;
1779
+ while (insertAt < body.length && isUseClientDirective(body[insertAt])) {
1780
+ insertAt++;
1781
+ }
1782
+ while (insertAt < body.length && body[insertAt]?.type === "ImportDeclaration") {
1783
+ insertAt++;
1784
+ }
1785
+ program.body.splice(insertAt, 0, importDecl);
1786
+ return { changed: true };
1787
+ }
1813
1788
  function hasUILintDevtoolsJsx(program) {
1814
1789
  let found = false;
1815
1790
  walkAst(program, (node) => {
@@ -1903,7 +1878,204 @@ function ensureSideEffectImport(program, from) {
1903
1878
  program.body.splice(insertAt, 0, importDecl);
1904
1879
  return { changed: true };
1905
1880
  }
1881
+ function addDevtoolsToClientComponent(program) {
1882
+ if (!program || program.type !== "Program") return { changed: false };
1883
+ if (hasUILintDevtoolsJsx(program)) return { changed: false };
1884
+ const devtoolsMod = parseModule2(
1885
+ "const __uilint_devtools = (<uilint-devtools />);"
1886
+ );
1887
+ const devtoolsJsx = devtoolsMod.$ast.body?.[0]?.declarations?.[0]?.init ?? null;
1888
+ if (!devtoolsJsx || devtoolsJsx.type !== "JSXElement")
1889
+ return { changed: false };
1890
+ let added = false;
1891
+ walkAst(program, (node) => {
1892
+ if (added) return;
1893
+ if (node.type !== "JSXElement" && node.type !== "JSXFragment") return;
1894
+ const children = node.children ?? [];
1895
+ const childrenIndex = children.findIndex(
1896
+ (child) => child?.type === "JSXExpressionContainer" && child.expression?.type === "Identifier" && child.expression.name === "children"
1897
+ );
1898
+ if (childrenIndex !== -1) {
1899
+ children.splice(childrenIndex + 1, 0, devtoolsJsx);
1900
+ added = true;
1901
+ }
1902
+ });
1903
+ if (added) return { changed: true };
1904
+ walkAst(program, (node) => {
1905
+ if (added) return;
1906
+ if (node.type !== "ReturnStatement") return;
1907
+ const arg = node.argument;
1908
+ if (!arg) return;
1909
+ if (arg.type !== "JSXElement" && arg.type !== "JSXFragment") return;
1910
+ const fragmentMod = parseModule2("const __fragment = (<></>);");
1911
+ const fragmentJsx = fragmentMod.$ast.body?.[0]?.declarations?.[0]?.init ?? null;
1912
+ if (!fragmentJsx) return;
1913
+ fragmentJsx.children = [arg, devtoolsJsx];
1914
+ node.argument = fragmentJsx;
1915
+ added = true;
1916
+ });
1917
+ if (!added) {
1918
+ throw new Error(
1919
+ "Could not find a suitable location to add devtools. Expected a component with JSX return or {children}."
1920
+ );
1921
+ }
1922
+ return { changed: true };
1923
+ }
1924
+ function generateProvidersContent(isTypeScript) {
1925
+ const ext = isTypeScript ? "tsx" : "jsx";
1926
+ const typeAnnotation = isTypeScript ? ": { children: React.ReactNode }" : "";
1927
+ return `"use client";
1928
+
1929
+ import React from "react";
1930
+ import "uilint-react/devtools";
1931
+
1932
+ export function Providers({ children }${typeAnnotation}) {
1933
+ return (
1934
+ <>
1935
+ {children}
1936
+ <uilint-devtools />
1937
+ </>
1938
+ );
1939
+ }
1940
+ `;
1941
+ }
1942
+ function wrapChildrenWithProviders(program, providersImportPath) {
1943
+ if (!program || program.type !== "Program") return { changed: false };
1944
+ let hasProvidersImport = false;
1945
+ for (const stmt of program.body ?? []) {
1946
+ if (stmt?.type !== "ImportDeclaration") continue;
1947
+ if (stmt.source?.value === providersImportPath) {
1948
+ hasProvidersImport = true;
1949
+ break;
1950
+ }
1951
+ }
1952
+ if (!hasProvidersImport) {
1953
+ const importRes = ensureNamedImport(program, providersImportPath, "Providers");
1954
+ if (!importRes.changed) return { changed: false };
1955
+ }
1956
+ let wrapped = false;
1957
+ walkAst(program, (node) => {
1958
+ if (wrapped) return;
1959
+ if (node.type !== "JSXElement" && node.type !== "JSXFragment") return;
1960
+ const children = node.children ?? [];
1961
+ const childrenIndex = children.findIndex(
1962
+ (child) => child?.type === "JSXExpressionContainer" && child.expression?.type === "Identifier" && child.expression.name === "children"
1963
+ );
1964
+ if (childrenIndex === -1) return;
1965
+ const providersMod = parseModule2(
1966
+ "const __providers = (<Providers>{children}</Providers>);"
1967
+ );
1968
+ const providersJsx = providersMod.$ast.body?.[0]?.declarations?.[0]?.init ?? null;
1969
+ if (!providersJsx) return;
1970
+ children[childrenIndex] = providersJsx;
1971
+ wrapped = true;
1972
+ });
1973
+ if (!wrapped) {
1974
+ throw new Error(
1975
+ "Could not find {children} in layout to wrap with Providers."
1976
+ );
1977
+ }
1978
+ return { changed: true };
1979
+ }
1980
+ function findLayoutFile(projectPath, appRoot) {
1981
+ const extensions = [".tsx", ".jsx", ".ts", ".js"];
1982
+ for (const ext of extensions) {
1983
+ const layoutPath = join5(projectPath, appRoot, `layout${ext}`);
1984
+ if (existsSync5(layoutPath)) return layoutPath;
1985
+ }
1986
+ return null;
1987
+ }
1988
+ async function createProvidersAndModifyLayout(projectPath, appRoot) {
1989
+ const layoutPath = findLayoutFile(projectPath, appRoot);
1990
+ if (!layoutPath) {
1991
+ throw new Error(`Could not find layout file in ${appRoot}`);
1992
+ }
1993
+ const isTypeScript = layoutPath.endsWith(".tsx") || layoutPath.endsWith(".ts");
1994
+ const providersExt = isTypeScript ? ".tsx" : ".jsx";
1995
+ const providersPath = join5(projectPath, appRoot, `providers${providersExt}`);
1996
+ if (existsSync5(providersPath)) {
1997
+ throw new Error(
1998
+ `providers${providersExt} already exists. Please select it from the list instead.`
1999
+ );
2000
+ }
2001
+ const providersContent = generateProvidersContent(isTypeScript);
2002
+ writeFileSync2(providersPath, providersContent, "utf-8");
2003
+ const layoutContent = readFileSync5(layoutPath, "utf-8");
2004
+ let layoutMod;
2005
+ try {
2006
+ layoutMod = parseModule2(layoutContent);
2007
+ } catch {
2008
+ throw new Error(
2009
+ `Unable to parse ${relative3(projectPath, layoutPath)} as JavaScript/TypeScript.`
2010
+ );
2011
+ }
2012
+ const layoutProgram = layoutMod.$ast;
2013
+ const wrapRes = wrapChildrenWithProviders(layoutProgram, "./providers");
2014
+ if (wrapRes.changed) {
2015
+ const updatedLayout = generateCode2(layoutMod).code;
2016
+ writeFileSync2(layoutPath, updatedLayout, "utf-8");
2017
+ }
2018
+ return {
2019
+ providersFile: relative3(projectPath, providersPath),
2020
+ layoutFile: relative3(projectPath, layoutPath),
2021
+ modified: true
2022
+ };
2023
+ }
1906
2024
  async function installReactUILintOverlay(opts) {
2025
+ if (opts.createProviders) {
2026
+ const result = await createProvidersAndModifyLayout(
2027
+ opts.projectPath,
2028
+ opts.appRoot
2029
+ );
2030
+ return {
2031
+ targetFile: result.providersFile,
2032
+ modified: result.modified,
2033
+ createdFile: result.providersFile,
2034
+ layoutModified: result.layoutFile
2035
+ };
2036
+ }
2037
+ if (opts.targetFile) {
2038
+ const absTarget2 = opts.targetFile;
2039
+ const relTarget = relative3(opts.projectPath, absTarget2);
2040
+ if (!existsSync5(absTarget2)) {
2041
+ throw new Error(`Target file not found: ${relTarget}`);
2042
+ }
2043
+ const original2 = readFileSync5(absTarget2, "utf-8");
2044
+ let mod2;
2045
+ try {
2046
+ mod2 = parseModule2(original2);
2047
+ } catch {
2048
+ throw new Error(
2049
+ `Unable to parse ${relTarget} as JavaScript/TypeScript. Please update it manually.`
2050
+ );
2051
+ }
2052
+ const program2 = mod2.$ast;
2053
+ const hasDevtoolsImport2 = !!findImportDeclaration(program2, "uilint-react/devtools");
2054
+ const hasOldImport2 = !!findImportDeclaration(program2, "uilint-react");
2055
+ const alreadyConfigured2 = (hasDevtoolsImport2 || hasOldImport2) && hasUILintDevtoolsJsx(program2);
2056
+ if (alreadyConfigured2) {
2057
+ return {
2058
+ targetFile: relTarget,
2059
+ modified: false,
2060
+ alreadyConfigured: true
2061
+ };
2062
+ }
2063
+ let changed2 = false;
2064
+ const importRes2 = ensureSideEffectImport(program2, "uilint-react/devtools");
2065
+ if (importRes2.changed) changed2 = true;
2066
+ const addRes2 = addDevtoolsToClientComponent(program2);
2067
+ if (addRes2.changed) changed2 = true;
2068
+ const updated2 = changed2 ? generateCode2(mod2).code : original2;
2069
+ const modified2 = updated2 !== original2;
2070
+ if (modified2) {
2071
+ writeFileSync2(absTarget2, updated2, "utf-8");
2072
+ }
2073
+ return {
2074
+ targetFile: relTarget,
2075
+ modified: modified2,
2076
+ alreadyConfigured: false
2077
+ };
2078
+ }
1907
2079
  const candidates = getDefaultCandidates(opts.projectPath, opts.appRoot);
1908
2080
  if (!candidates.length) {
1909
2081
  throw new Error(
@@ -1916,7 +2088,7 @@ async function installReactUILintOverlay(opts) {
1916
2088
  } else {
1917
2089
  chosen = candidates[0];
1918
2090
  }
1919
- const absTarget = join6(opts.projectPath, chosen);
2091
+ const absTarget = join5(opts.projectPath, chosen);
1920
2092
  const original = readFileSync5(absTarget, "utf-8");
1921
2093
  let mod;
1922
2094
  try {
@@ -1949,14 +2121,14 @@ async function installReactUILintOverlay(opts) {
1949
2121
  }
1950
2122
 
1951
2123
  // src/utils/next-config-inject.ts
1952
- import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
1953
- import { join as join7 } from "path";
2124
+ import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
2125
+ import { join as join6 } from "path";
1954
2126
  import { parseModule as parseModule3, generateCode as generateCode3 } from "magicast";
1955
2127
  var CONFIG_EXTENSIONS2 = [".ts", ".mjs", ".js", ".cjs"];
1956
2128
  function findNextConfigFile(projectPath) {
1957
2129
  for (const ext of CONFIG_EXTENSIONS2) {
1958
- const configPath = join7(projectPath, `next.config${ext}`);
1959
- if (existsSync7(configPath)) {
2130
+ const configPath = join6(projectPath, `next.config${ext}`);
2131
+ if (existsSync6(configPath)) {
1960
2132
  return configPath;
1961
2133
  }
1962
2134
  }
@@ -2099,14 +2271,14 @@ async function installJsxLocPlugin(opts) {
2099
2271
  }
2100
2272
 
2101
2273
  // src/utils/vite-config-inject.ts
2102
- import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
2103
- import { join as join8 } from "path";
2274
+ import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
2275
+ import { join as join7 } from "path";
2104
2276
  import { parseModule as parseModule4, generateCode as generateCode4 } from "magicast";
2105
2277
  var CONFIG_EXTENSIONS3 = [".ts", ".mjs", ".js", ".cjs"];
2106
2278
  function findViteConfigFile2(projectPath) {
2107
2279
  for (const ext of CONFIG_EXTENSIONS3) {
2108
- const configPath = join8(projectPath, `vite.config${ext}`);
2109
- if (existsSync8(configPath)) return configPath;
2280
+ const configPath = join7(projectPath, `vite.config${ext}`);
2281
+ if (existsSync7(configPath)) return configPath;
2110
2282
  }
2111
2283
  return null;
2112
2284
  }
@@ -2310,9 +2482,9 @@ async function installViteJsxLocPlugin(opts) {
2310
2482
  }
2311
2483
 
2312
2484
  // src/utils/next-routes.ts
2313
- import { existsSync as existsSync9 } from "fs";
2485
+ import { existsSync as existsSync8 } from "fs";
2314
2486
  import { mkdir, writeFile } from "fs/promises";
2315
- import { join as join9 } from "path";
2487
+ import { join as join8 } from "path";
2316
2488
  var DEV_SOURCE_ROUTE_TS = `/**
2317
2489
  * Dev-only API route for fetching source files
2318
2490
  *
@@ -2768,39 +2940,39 @@ export async function GET(request: NextRequest) {
2768
2940
  }
2769
2941
  `;
2770
2942
  async function writeRouteFile(absPath, relPath, content, opts) {
2771
- if (existsSync9(absPath) && !opts.force) return;
2943
+ if (existsSync8(absPath) && !opts.force) return;
2772
2944
  await writeFile(absPath, content, "utf-8");
2773
2945
  }
2774
2946
  async function installNextUILintRoutes(opts) {
2775
- const baseRel = join9(opts.appRoot, "api", ".uilint");
2776
- const baseAbs = join9(opts.projectPath, baseRel);
2777
- await mkdir(join9(baseAbs, "source"), { recursive: true });
2947
+ const baseRel = join8(opts.appRoot, "api", ".uilint");
2948
+ const baseAbs = join8(opts.projectPath, baseRel);
2949
+ await mkdir(join8(baseAbs, "source"), { recursive: true });
2778
2950
  await writeRouteFile(
2779
- join9(baseAbs, "source", "route.ts"),
2780
- join9(baseRel, "source", "route.ts"),
2951
+ join8(baseAbs, "source", "route.ts"),
2952
+ join8(baseRel, "source", "route.ts"),
2781
2953
  DEV_SOURCE_ROUTE_TS,
2782
2954
  opts
2783
2955
  );
2784
- await mkdir(join9(baseAbs, "screenshots"), { recursive: true });
2956
+ await mkdir(join8(baseAbs, "screenshots"), { recursive: true });
2785
2957
  await writeRouteFile(
2786
- join9(baseAbs, "screenshots", "route.ts"),
2787
- join9(baseRel, "screenshots", "route.ts"),
2958
+ join8(baseAbs, "screenshots", "route.ts"),
2959
+ join8(baseRel, "screenshots", "route.ts"),
2788
2960
  SCREENSHOT_ROUTE_TS,
2789
2961
  opts
2790
2962
  );
2791
2963
  }
2792
2964
 
2793
2965
  // src/utils/prettier.ts
2794
- import { existsSync as existsSync10 } from "fs";
2795
- import { spawn as spawn2 } from "child_process";
2796
- import { join as join10, dirname as dirname3 } from "path";
2966
+ import { existsSync as existsSync9 } from "fs";
2967
+ import { spawn } from "child_process";
2968
+ import { join as join9, dirname as dirname3 } from "path";
2797
2969
  function getPrettierPath(projectPath) {
2798
- const localPath = join10(projectPath, "node_modules", ".bin", "prettier");
2799
- if (existsSync10(localPath)) return localPath;
2970
+ const localPath = join9(projectPath, "node_modules", ".bin", "prettier");
2971
+ if (existsSync9(localPath)) return localPath;
2800
2972
  let dir = projectPath;
2801
2973
  for (let i = 0; i < 10; i++) {
2802
- const binPath = join10(dir, "node_modules", ".bin", "prettier");
2803
- if (existsSync10(binPath)) return binPath;
2974
+ const binPath = join9(dir, "node_modules", ".bin", "prettier");
2975
+ if (existsSync9(binPath)) return binPath;
2804
2976
  const parent = dirname3(dir);
2805
2977
  if (parent === dir) break;
2806
2978
  dir = parent;
@@ -2827,7 +2999,7 @@ async function formatWithPrettier(filePath, projectPath) {
2827
2999
  const runner = getPmRunner(pm);
2828
3000
  return new Promise((resolve) => {
2829
3001
  const args = [...runner.args, "prettier", "--write", filePath];
2830
- const child = spawn2(runner.command, args, {
3002
+ const child = spawn(runner.command, args, {
2831
3003
  cwd: projectPath,
2832
3004
  stdio: "pipe",
2833
3005
  shell: process.platform === "win32"
@@ -2849,7 +3021,7 @@ async function formatWithPrettier(filePath, projectPath) {
2849
3021
  });
2850
3022
  }
2851
3023
  return new Promise((resolve) => {
2852
- const child = spawn2(prettierPath, ["--write", filePath], {
3024
+ const child = spawn(prettierPath, ["--write", filePath], {
2853
3025
  cwd: projectPath,
2854
3026
  stdio: "pipe",
2855
3027
  shell: process.platform === "win32"
@@ -2882,7 +3054,7 @@ async function formatFilesWithPrettier(filePaths, projectPath) {
2882
3054
  const runner = getPmRunner(pm);
2883
3055
  return new Promise((resolve) => {
2884
3056
  const args = [...runner.args, "prettier", "--write", ...filePaths];
2885
- const child = spawn2(runner.command, args, {
3057
+ const child = spawn(runner.command, args, {
2886
3058
  cwd: projectPath,
2887
3059
  stdio: "pipe",
2888
3060
  shell: process.platform === "win32"
@@ -2900,7 +3072,7 @@ async function formatFilesWithPrettier(filePaths, projectPath) {
2900
3072
  });
2901
3073
  }
2902
3074
  return new Promise((resolve) => {
2903
- const child = spawn2(prettierPath, ["--write", ...filePaths], {
3075
+ const child = spawn(prettierPath, ["--write", ...filePaths], {
2904
3076
  cwd: projectPath,
2905
3077
  stdio: "pipe",
2906
3078
  shell: process.platform === "win32"
@@ -2942,7 +3114,7 @@ async function executeAction(action, options) {
2942
3114
  wouldDo: `Create directory: ${action.path}`
2943
3115
  };
2944
3116
  }
2945
- if (!existsSync11(action.path)) {
3117
+ if (!existsSync10(action.path)) {
2946
3118
  mkdirSync(action.path, { recursive: true });
2947
3119
  }
2948
3120
  return { action, success: true };
@@ -2956,7 +3128,7 @@ async function executeAction(action, options) {
2956
3128
  };
2957
3129
  }
2958
3130
  const dir = dirname4(action.path);
2959
- if (!existsSync11(dir)) {
3131
+ if (!existsSync10(dir)) {
2960
3132
  mkdirSync(dir, { recursive: true });
2961
3133
  }
2962
3134
  writeFileSync5(action.path, action.content, "utf-8");
@@ -2974,7 +3146,7 @@ async function executeAction(action, options) {
2974
3146
  };
2975
3147
  }
2976
3148
  let existing = {};
2977
- if (existsSync11(action.path)) {
3149
+ if (existsSync10(action.path)) {
2978
3150
  try {
2979
3151
  existing = JSON.parse(readFileSync8(action.path, "utf-8"));
2980
3152
  } catch {
@@ -2982,7 +3154,7 @@ async function executeAction(action, options) {
2982
3154
  }
2983
3155
  const merged = deepMerge(existing, action.merge);
2984
3156
  const dir = dirname4(action.path);
2985
- if (!existsSync11(dir)) {
3157
+ if (!existsSync10(dir)) {
2986
3158
  mkdirSync(dir, { recursive: true });
2987
3159
  }
2988
3160
  writeFileSync5(action.path, JSON.stringify(merged, null, 2), "utf-8");
@@ -2996,7 +3168,7 @@ async function executeAction(action, options) {
2996
3168
  wouldDo: `Delete file: ${action.path}`
2997
3169
  };
2998
3170
  }
2999
- if (existsSync11(action.path)) {
3171
+ if (existsSync10(action.path)) {
3000
3172
  unlinkSync(action.path);
3001
3173
  }
3002
3174
  return { action, success: true };
@@ -3009,7 +3181,7 @@ async function executeAction(action, options) {
3009
3181
  wouldDo: `Append to file: ${action.path}`
3010
3182
  };
3011
3183
  }
3012
- if (existsSync11(action.path)) {
3184
+ if (existsSync10(action.path)) {
3013
3185
  const content = readFileSync8(action.path, "utf-8");
3014
3186
  if (action.ifNotContains && content.includes(action.ifNotContains)) {
3015
3187
  return { action, success: true };
@@ -3075,11 +3247,12 @@ async function executeInjectEslint(action, options) {
3075
3247
  }
3076
3248
  async function executeInjectReact(action, options) {
3077
3249
  const { dryRun = false } = options;
3250
+ const dryRunDescription = action.createProviders ? `Create providers.tsx and inject <uilint-devtools /> in: ${action.projectPath}` : action.targetFile ? `Inject <uilint-devtools /> into: ${action.targetFile}` : `Inject <uilint-devtools /> into React app: ${action.projectPath}`;
3078
3251
  if (dryRun) {
3079
3252
  return {
3080
3253
  action,
3081
3254
  success: true,
3082
- wouldDo: `Inject <uilint-devtools /> into React app: ${action.projectPath}`
3255
+ wouldDo: dryRunDescription
3083
3256
  };
3084
3257
  }
3085
3258
  const result = await installReactUILintOverlay({
@@ -3087,7 +3260,10 @@ async function executeInjectReact(action, options) {
3087
3260
  appRoot: action.appRoot,
3088
3261
  mode: action.mode,
3089
3262
  force: false,
3090
- // Auto-select first choice for execute phase
3263
+ // Pass through targetFile and createProviders from the action
3264
+ targetFile: action.targetFile,
3265
+ createProviders: action.createProviders,
3266
+ // Auto-select first choice for execute phase (fallback if no targetFile)
3091
3267
  confirmFileChoice: async (choices) => choices[0]
3092
3268
  });
3093
3269
  const success = result.modified || result.alreadyConfigured === true;
@@ -3371,11 +3547,9 @@ async function execute(plan, options = {}) {
3371
3547
 
3372
3548
  // src/commands/install-ui.tsx
3373
3549
  import { ruleRegistry as ruleRegistry3 } from "uilint-eslint";
3374
- import { existsSync as existsSync13 } from "fs";
3375
- import { join as join14 } from "path";
3376
3550
 
3377
3551
  // src/commands/install/installers/genstyleguide.ts
3378
- import { join as join11 } from "path";
3552
+ import { join as join10 } from "path";
3379
3553
  var genstyleguideInstaller = {
3380
3554
  id: "genstyleguide",
3381
3555
  name: "/genstyleguide command",
@@ -3385,7 +3559,7 @@ var genstyleguideInstaller = {
3385
3559
  return true;
3386
3560
  },
3387
3561
  getTargets(project) {
3388
- const commandPath = join11(project.cursorDir.path, "commands", "genstyleguide.md");
3562
+ const commandPath = join10(project.cursorDir.path, "commands", "genstyleguide.md");
3389
3563
  const isInstalled = project.commands.genstyleguide;
3390
3564
  return [
3391
3565
  {
@@ -3398,7 +3572,7 @@ var genstyleguideInstaller = {
3398
3572
  },
3399
3573
  plan(targets, config, project) {
3400
3574
  const actions = [];
3401
- const commandsDir = join11(project.cursorDir.path, "commands");
3575
+ const commandsDir = join10(project.cursorDir.path, "commands");
3402
3576
  if (!project.cursorDir.exists) {
3403
3577
  actions.push({
3404
3578
  type: "create_directory",
@@ -3411,7 +3585,7 @@ var genstyleguideInstaller = {
3411
3585
  });
3412
3586
  actions.push({
3413
3587
  type: "create_file",
3414
- path: join11(commandsDir, "genstyleguide.md"),
3588
+ path: join10(commandsDir, "genstyleguide.md"),
3415
3589
  content: GENSTYLEGUIDE_COMMAND_MD
3416
3590
  });
3417
3591
  return {
@@ -3441,8 +3615,8 @@ var genstyleguideInstaller = {
3441
3615
  };
3442
3616
 
3443
3617
  // src/commands/install/installers/skill.ts
3444
- import { existsSync as existsSync12 } from "fs";
3445
- import { join as join12 } from "path";
3618
+ import { existsSync as existsSync11 } from "fs";
3619
+ import { join as join11 } from "path";
3446
3620
  var skillInstaller = {
3447
3621
  id: "skill",
3448
3622
  name: "UI Consistency Agent skill",
@@ -3452,9 +3626,9 @@ var skillInstaller = {
3452
3626
  return true;
3453
3627
  },
3454
3628
  getTargets(project) {
3455
- const skillsDir = join12(project.cursorDir.path, "skills", "ui-consistency-enforcer");
3456
- const skillMdPath = join12(skillsDir, "SKILL.md");
3457
- const isInstalled = existsSync12(skillMdPath);
3629
+ const skillsDir = join11(project.cursorDir.path, "skills", "ui-consistency-enforcer");
3630
+ const skillMdPath = join11(skillsDir, "SKILL.md");
3631
+ const isInstalled = existsSync11(skillMdPath);
3458
3632
  return [
3459
3633
  {
3460
3634
  id: "ui-consistency-skill",
@@ -3473,21 +3647,21 @@ var skillInstaller = {
3473
3647
  path: project.cursorDir.path
3474
3648
  });
3475
3649
  }
3476
- const skillsDir = join12(project.cursorDir.path, "skills");
3650
+ const skillsDir = join11(project.cursorDir.path, "skills");
3477
3651
  actions.push({
3478
3652
  type: "create_directory",
3479
3653
  path: skillsDir
3480
3654
  });
3481
3655
  try {
3482
3656
  const skill = loadSkill("ui-consistency-enforcer");
3483
- const skillDir = join12(skillsDir, skill.name);
3657
+ const skillDir = join11(skillsDir, skill.name);
3484
3658
  actions.push({
3485
3659
  type: "create_directory",
3486
3660
  path: skillDir
3487
3661
  });
3488
3662
  for (const file of skill.files) {
3489
- const filePath = join12(skillDir, file.relativePath);
3490
- const fileDir = join12(
3663
+ const filePath = join11(skillDir, file.relativePath);
3664
+ const fileDir = join11(
3491
3665
  skillDir,
3492
3666
  file.relativePath.split("/").slice(0, -1).join("/")
3493
3667
  );
@@ -3544,8 +3718,21 @@ var skillInstaller = {
3544
3718
  };
3545
3719
 
3546
3720
  // src/commands/install/installers/eslint.ts
3547
- import { join as join13 } from "path";
3721
+ import { join as join12 } from "path";
3548
3722
  import { ruleRegistry as ruleRegistry2, getRulesByCategory as getRulesByCategory2 } from "uilint-eslint";
3723
+ function getUpgradeInfo(configuredRuleIds) {
3724
+ const configuredSet = new Set(configuredRuleIds);
3725
+ const allRuleIds = ruleRegistry2.map((r) => r.id);
3726
+ const missingRules = allRuleIds.filter((id) => !configuredSet.has(id));
3727
+ if (missingRules.length === 0) {
3728
+ return void 0;
3729
+ }
3730
+ const summary = missingRules.length === 1 ? "1 new rule available" : `${missingRules.length} new rules available`;
3731
+ return {
3732
+ missingRules,
3733
+ summary
3734
+ };
3735
+ }
3549
3736
  function toInstallSpecifier(pkgName) {
3550
3737
  return pkgName;
3551
3738
  }
@@ -3566,13 +3753,22 @@ var eslintInstaller = {
3566
3753
  return project.packages.some((pkg) => pkg.eslintConfigPath !== null);
3567
3754
  },
3568
3755
  getTargets(project) {
3569
- return project.packages.filter((pkg) => pkg.eslintConfigPath !== null).map((pkg) => ({
3570
- id: `eslint-${pkg.name}`,
3571
- label: pkg.name,
3572
- path: pkg.path,
3573
- hint: pkg.eslintConfigFilename || "ESLint config detected",
3574
- isInstalled: pkg.hasUilintRules
3575
- }));
3756
+ return project.packages.filter((pkg) => pkg.eslintConfigPath !== null).map((pkg) => {
3757
+ const upgradeInfo = pkg.hasUilintRules ? getUpgradeInfo(pkg.configuredRuleIds) : void 0;
3758
+ let hint = pkg.eslintConfigFilename || "ESLint config detected";
3759
+ if (upgradeInfo?.summary) {
3760
+ hint = `${hint} \xB7 ${upgradeInfo.summary}`;
3761
+ }
3762
+ return {
3763
+ id: `eslint-${pkg.name}`,
3764
+ label: pkg.name,
3765
+ path: pkg.path,
3766
+ hint,
3767
+ isInstalled: pkg.hasUilintRules,
3768
+ canUpgrade: upgradeInfo !== void 0,
3769
+ upgradeInfo
3770
+ };
3771
+ });
3576
3772
  },
3577
3773
  async configure(targets, project) {
3578
3774
  const staticRules = getRulesByCategory2("static");
@@ -3698,14 +3894,14 @@ var eslintInstaller = {
3698
3894
  for (const target of targets) {
3699
3895
  const pkgInfo = project.packages.find((p) => p.path === target.path);
3700
3896
  if (!pkgInfo || !pkgInfo.eslintConfigPath) continue;
3701
- const rulesDir = join13(target.path, ".uilint", "rules");
3897
+ const rulesDir = join12(target.path, ".uilint", "rules");
3702
3898
  actions.push({
3703
3899
  type: "create_directory",
3704
3900
  path: rulesDir
3705
3901
  });
3706
3902
  dependencies.push({
3707
3903
  packagePath: target.path,
3708
- packageManager: project.packageManager,
3904
+ packageManager: detectPackageManager(target.path),
3709
3905
  packages: [toInstallSpecifier("uilint-eslint"), "typescript-eslint"]
3710
3906
  });
3711
3907
  actions.push({
@@ -3716,7 +3912,7 @@ var eslintInstaller = {
3716
3912
  hasExistingRules: pkgInfo.hasUilintRules
3717
3913
  });
3718
3914
  }
3719
- const gitignorePath = join13(project.workspaceRoot, ".gitignore");
3915
+ const gitignorePath = join12(project.workspaceRoot, ".gitignore");
3720
3916
  actions.push({
3721
3917
  type: "append_to_file",
3722
3918
  path: gitignorePath,
@@ -3750,6 +3946,143 @@ var eslintInstaller = {
3750
3946
  }
3751
3947
  };
3752
3948
 
3949
+ // src/utils/client-boundary-tracer.ts
3950
+ import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
3951
+ import { join as join13, dirname as dirname5, relative as relative4 } from "path";
3952
+ import { parseModule as parseModule5 } from "magicast";
3953
+ function hasUseClientDirective(filePath) {
3954
+ try {
3955
+ const content = readFileSync9(filePath, "utf-8");
3956
+ const mod = parseModule5(content);
3957
+ const program = mod.$ast;
3958
+ if (!program || program.type !== "Program") return false;
3959
+ const firstStmt = program.body?.[0];
3960
+ return firstStmt?.type === "ExpressionStatement" && firstStmt.expression?.type === "StringLiteral" && (firstStmt.expression.value === "use client" || firstStmt.expression.value === "use client");
3961
+ } catch {
3962
+ return false;
3963
+ }
3964
+ }
3965
+ function extractImports(program) {
3966
+ const imports = [];
3967
+ if (!program || program.type !== "Program") return imports;
3968
+ for (const stmt of program.body ?? []) {
3969
+ if (stmt?.type !== "ImportDeclaration") continue;
3970
+ const source = stmt.source?.value;
3971
+ if (typeof source !== "string") continue;
3972
+ if (!source.startsWith(".") && !source.startsWith("@/") && !source.startsWith("~/")) {
3973
+ continue;
3974
+ }
3975
+ const specifiers = [];
3976
+ for (const spec of stmt.specifiers ?? []) {
3977
+ if (spec.type === "ImportDefaultSpecifier") {
3978
+ specifiers.push("default");
3979
+ } else if (spec.type === "ImportSpecifier") {
3980
+ const name = spec.imported?.name ?? spec.imported?.value;
3981
+ if (name) specifiers.push(name);
3982
+ } else if (spec.type === "ImportNamespaceSpecifier") {
3983
+ specifiers.push("*");
3984
+ }
3985
+ }
3986
+ imports.push({ source, specifiers });
3987
+ }
3988
+ return imports;
3989
+ }
3990
+ function resolveImportPath(importSource, fromFile, projectPath) {
3991
+ const fromDir = dirname5(fromFile);
3992
+ let basePath;
3993
+ if (importSource.startsWith("@/")) {
3994
+ const withoutAlias = importSource.slice(2);
3995
+ const srcPath = join13(projectPath, "src", withoutAlias);
3996
+ const rootPath = join13(projectPath, withoutAlias);
3997
+ basePath = existsSync12(dirname5(srcPath)) ? srcPath : rootPath;
3998
+ } else if (importSource.startsWith("~/")) {
3999
+ basePath = join13(projectPath, importSource.slice(2));
4000
+ } else if (importSource.startsWith(".")) {
4001
+ basePath = join13(fromDir, importSource);
4002
+ } else {
4003
+ return null;
4004
+ }
4005
+ const extensions = [".tsx", ".ts", ".jsx", ".js"];
4006
+ for (const ext of extensions) {
4007
+ const fullPath = basePath + ext;
4008
+ if (existsSync12(fullPath)) return fullPath;
4009
+ }
4010
+ for (const ext of extensions) {
4011
+ const indexPath = join13(basePath, `index${ext}`);
4012
+ if (existsSync12(indexPath)) return indexPath;
4013
+ }
4014
+ if (existsSync12(basePath)) return basePath;
4015
+ return null;
4016
+ }
4017
+ function findLayoutFile2(projectPath, appRoot) {
4018
+ const extensions = [".tsx", ".jsx", ".ts", ".js"];
4019
+ for (const ext of extensions) {
4020
+ const layoutPath = join13(projectPath, appRoot, `layout${ext}`);
4021
+ if (existsSync12(layoutPath)) return layoutPath;
4022
+ }
4023
+ return null;
4024
+ }
4025
+ function traceClientBoundaries(projectPath, appRoot) {
4026
+ const layoutFile = findLayoutFile2(projectPath, appRoot);
4027
+ if (!layoutFile) {
4028
+ return null;
4029
+ }
4030
+ const layoutIsClient = hasUseClientDirective(layoutFile);
4031
+ const layoutRelative = relative4(projectPath, layoutFile);
4032
+ if (layoutIsClient) {
4033
+ return {
4034
+ layoutIsClient: true,
4035
+ clientBoundaries: [],
4036
+ layoutFile,
4037
+ layoutRelative
4038
+ };
4039
+ }
4040
+ let program;
4041
+ try {
4042
+ const content = readFileSync9(layoutFile, "utf-8");
4043
+ const mod = parseModule5(content);
4044
+ program = mod.$ast;
4045
+ } catch {
4046
+ return {
4047
+ layoutIsClient: false,
4048
+ clientBoundaries: [],
4049
+ layoutFile,
4050
+ layoutRelative
4051
+ };
4052
+ }
4053
+ const imports = extractImports(program);
4054
+ const clientBoundaries = [];
4055
+ for (const imp of imports) {
4056
+ const resolvedPath = resolveImportPath(imp.source, layoutFile, projectPath);
4057
+ if (!resolvedPath) continue;
4058
+ if (hasUseClientDirective(resolvedPath)) {
4059
+ clientBoundaries.push({
4060
+ filePath: resolvedPath,
4061
+ relativePath: relative4(projectPath, resolvedPath),
4062
+ componentNames: imp.specifiers,
4063
+ importSource: imp.source
4064
+ });
4065
+ }
4066
+ }
4067
+ return {
4068
+ layoutIsClient: false,
4069
+ clientBoundaries,
4070
+ layoutFile,
4071
+ layoutRelative
4072
+ };
4073
+ }
4074
+ function providersFileExists(projectPath, appRoot) {
4075
+ const extensions = [".tsx", ".jsx", ".ts", ".js"];
4076
+ const names = ["providers", "Providers"];
4077
+ for (const name of names) {
4078
+ for (const ext of extensions) {
4079
+ const providersPath = join13(projectPath, appRoot, `${name}${ext}`);
4080
+ if (existsSync12(providersPath)) return providersPath;
4081
+ }
4082
+ }
4083
+ return null;
4084
+ }
4085
+
3753
4086
  // src/commands/install/installers/next-overlay.ts
3754
4087
  var nextOverlayInstaller = {
3755
4088
  id: "next",
@@ -3760,21 +4093,95 @@ var nextOverlayInstaller = {
3760
4093
  return project.nextApps.length > 0;
3761
4094
  },
3762
4095
  getTargets(project) {
3763
- return project.nextApps.map((app) => ({
3764
- id: `next-${app.projectPath}`,
3765
- label: app.projectPath.split("/").pop() || app.projectPath,
3766
- path: app.projectPath,
3767
- hint: "App Router",
3768
- isInstalled: false
3769
- // TODO: Detect if already installed
3770
- }));
4096
+ const targets = [];
4097
+ for (const app of project.nextApps) {
4098
+ const traceResult = traceClientBoundaries(
4099
+ app.projectPath,
4100
+ app.detection.appRoot
4101
+ );
4102
+ if (!traceResult) {
4103
+ targets.push({
4104
+ id: `next-${app.projectPath}`,
4105
+ label: app.projectPath.split("/").pop() || app.projectPath,
4106
+ path: app.projectPath,
4107
+ hint: "App Router (no layout found)",
4108
+ isInstalled: false
4109
+ });
4110
+ continue;
4111
+ }
4112
+ if (traceResult.layoutIsClient) {
4113
+ targets.push({
4114
+ id: `next-${app.projectPath}`,
4115
+ label: app.projectPath.split("/").pop() || app.projectPath,
4116
+ path: app.projectPath,
4117
+ hint: "App Router",
4118
+ isInstalled: false,
4119
+ targetFile: traceResult.layoutFile
4120
+ });
4121
+ continue;
4122
+ }
4123
+ const existingProviders = providersFileExists(
4124
+ app.projectPath,
4125
+ app.detection.appRoot
4126
+ );
4127
+ if (!existingProviders) {
4128
+ targets.push({
4129
+ id: `next-${app.projectPath}-create-providers`,
4130
+ label: `${app.projectPath.split("/").pop() || app.projectPath}`,
4131
+ path: app.projectPath,
4132
+ hint: "Create providers.tsx (Recommended)",
4133
+ isInstalled: false,
4134
+ createProviders: true
4135
+ });
4136
+ }
4137
+ for (const boundary of traceResult.clientBoundaries) {
4138
+ const componentNames = boundary.componentNames.length > 0 ? boundary.componentNames.join(", ") : "default";
4139
+ targets.push({
4140
+ id: `next-${app.projectPath}-${boundary.relativePath}`,
4141
+ label: `${app.projectPath.split("/").pop() || app.projectPath}`,
4142
+ path: app.projectPath,
4143
+ hint: `${boundary.relativePath} (${componentNames})`,
4144
+ isInstalled: false,
4145
+ targetFile: boundary.filePath
4146
+ });
4147
+ }
4148
+ if (existingProviders) {
4149
+ const relativePath = existingProviders.replace(app.projectPath + "/", "").replace(app.projectPath, "");
4150
+ const alreadyListed = traceResult.clientBoundaries.some(
4151
+ (b) => b.filePath === existingProviders
4152
+ );
4153
+ if (!alreadyListed) {
4154
+ targets.push({
4155
+ id: `next-${app.projectPath}-existing-providers`,
4156
+ label: `${app.projectPath.split("/").pop() || app.projectPath}`,
4157
+ path: app.projectPath,
4158
+ hint: `${relativePath} (existing)`,
4159
+ isInstalled: false,
4160
+ targetFile: existingProviders
4161
+ });
4162
+ }
4163
+ }
4164
+ if (targets.filter((t) => t.path === app.projectPath).length === 0) {
4165
+ targets.push({
4166
+ id: `next-${app.projectPath}-create-providers`,
4167
+ label: `${app.projectPath.split("/").pop() || app.projectPath}`,
4168
+ path: app.projectPath,
4169
+ hint: "Create providers.tsx",
4170
+ isInstalled: false,
4171
+ createProviders: true
4172
+ });
4173
+ }
4174
+ }
4175
+ return targets;
3771
4176
  },
3772
4177
  plan(targets, config, project) {
3773
4178
  const actions = [];
3774
4179
  const dependencies = [];
3775
4180
  if (targets.length === 0) return { actions, dependencies };
3776
4181
  const target = targets[0];
3777
- const appInfo = project.nextApps.find((app) => app.projectPath === target.path);
4182
+ const appInfo = project.nextApps.find(
4183
+ (app) => app.projectPath === target.path
4184
+ );
3778
4185
  if (!appInfo) return { actions, dependencies };
3779
4186
  const { projectPath, detection } = appInfo;
3780
4187
  actions.push({
@@ -3791,7 +4198,9 @@ var nextOverlayInstaller = {
3791
4198
  type: "inject_react",
3792
4199
  projectPath,
3793
4200
  appRoot: detection.appRoot,
3794
- mode: "next"
4201
+ mode: "next",
4202
+ targetFile: target.targetFile,
4203
+ createProviders: target.createProviders
3795
4204
  });
3796
4205
  actions.push({
3797
4206
  type: "inject_next_config",
@@ -3816,10 +4225,11 @@ var nextOverlayInstaller = {
3816
4225
  message: "Installing dependencies",
3817
4226
  detail: "\u2192 uilint-react, uilint-core, jsx-loc-plugin"
3818
4227
  };
4228
+ const injectDetail = target.createProviders ? "\u2192 Creating providers.tsx" : target.targetFile ? `\u2192 ${target.hint || "client component"}` : "\u2192 <uilint-devtools /> in root layout";
3819
4229
  yield {
3820
4230
  type: "progress",
3821
4231
  message: "Injecting devtools component",
3822
- detail: "\u2192 <uilint-devtools /> in root layout"
4232
+ detail: injectDetail
3823
4233
  };
3824
4234
  yield {
3825
4235
  type: "progress",
@@ -3973,56 +4383,6 @@ function selectionsToUserChoices(selections, project, eslintRules) {
3973
4383
  function isInteractiveTerminal() {
3974
4384
  return Boolean(process.stdin.isTTY && process.stdout.isTTY);
3975
4385
  }
3976
- function getRecommendedCommand(pm) {
3977
- switch (pm) {
3978
- case "pnpm":
3979
- return "pnpm dlx uilint install";
3980
- case "yarn":
3981
- return "yarn dlx uilint install";
3982
- case "bun":
3983
- return "bunx uilint install";
3984
- case "npm":
3985
- default:
3986
- return "npx uilint install";
3987
- }
3988
- }
3989
- async function warnOnPackageManagerMismatch(projectPromise) {
3990
- const executionPm = detectExecutionPackageManager();
3991
- if (!executionPm) return;
3992
- const project = await projectPromise;
3993
- const packageManagers = /* @__PURE__ */ new Set();
3994
- const hasRootPackageJson = existsSync13(join14(project.projectPath, "package.json"));
3995
- if (hasRootPackageJson) {
3996
- packageManagers.add(project.packageManager);
3997
- } else {
3998
- for (const pkg of project.packages) {
3999
- const pm = detectPackageManager(pkg.path);
4000
- packageManagers.add(pm);
4001
- }
4002
- }
4003
- const specificPackageManagers = Array.from(packageManagers).filter(
4004
- (pm) => pm !== "npm" || hasRootPackageJson
4005
- );
4006
- if (specificPackageManagers.length === 0) return;
4007
- if (specificPackageManagers.includes(executionPm)) return;
4008
- const primaryPm = specificPackageManagers[0];
4009
- const recommendedCmd = getRecommendedCommand(primaryPm);
4010
- const pmList = specificPackageManagers.length === 1 ? specificPackageManagers[0] : specificPackageManagers.join(", ");
4011
- printBox(
4012
- "Package manager mismatch detected",
4013
- [
4014
- `This project uses ${pmList}, but you ran this command with ${executionPm}.`,
4015
- "",
4016
- `Running with ${executionPm} may create unwanted lockfiles.`,
4017
- "",
4018
- "Recommended:",
4019
- ` ${recommendedCmd}`,
4020
- "",
4021
- "Continuing anyway..."
4022
- ],
4023
- "warning"
4024
- );
4025
- }
4026
4386
  async function installUI(options = {}, executeOptions = {}) {
4027
4387
  const projectPath = process.cwd();
4028
4388
  if (!isInteractiveTerminal()) {
@@ -4031,7 +4391,6 @@ async function installUI(options = {}, executeOptions = {}) {
4031
4391
  process.exit(1);
4032
4392
  }
4033
4393
  const projectPromise = analyze(projectPath);
4034
- await warnOnPackageManagerMismatch(projectPromise);
4035
4394
  const { waitUntilExit } = render(
4036
4395
  /* @__PURE__ */ jsx6(
4037
4396
  InstallApp,
@@ -4044,7 +4403,7 @@ async function installUI(options = {}, executeOptions = {}) {
4044
4403
  console.log("\nNo items selected for installation");
4045
4404
  process.exit(0);
4046
4405
  }
4047
- const { createPlan } = await import("./plan-PX7FFJ25.js");
4406
+ const { createPlan } = await import("./plan-VPDTICSY.js");
4048
4407
  const plan = createPlan(project, choices, { force: options.force });
4049
4408
  const result = await execute(plan, {
4050
4409
  ...executeOptions,
@@ -4069,4 +4428,4 @@ async function installUI(options = {}, executeOptions = {}) {
4069
4428
  export {
4070
4429
  installUI
4071
4430
  };
4072
- //# sourceMappingURL=install-ui-ITXPOUVQ.js.map
4431
+ //# sourceMappingURL=install-ui-73AINMZ5.js.map