zyncat-ui 0.12.0 → 0.13.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 (3) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.js +61 -42
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -10,8 +10,8 @@ pnpm dlx zyncat-ui init
10
10
 
11
11
  Run it inside a React project. One command installs `@zyncat/ui` (and React 19 if the project
12
12
  needs it), imports the base stylesheet at your app root, installs the agent skill into
13
- `.claude/skills/`, registers the MCP server in `.mcp.json`, and scaffolds a typed
14
- `zyncat.theme.ts`. Re-run it after upgrading `@zyncat/ui` to refresh everything.
13
+ `.claude/skills/`, and registers the MCP server in `.mcp.json`. Re-run it after upgrading
14
+ `@zyncat/ui` to refresh everything.
15
15
 
16
16
  This package is where the CLI lives, and it holds the unscoped name so the command works before
17
17
  `@zyncat/ui` is installed. It ships one bundled file and declares zero runtime dependencies - the
package/dist/cli.js CHANGED
@@ -212,6 +212,12 @@ function majorOf(range) {
212
212
  const match = range?.match(/\d+/);
213
213
  return match ? Number(match[0]) : null;
214
214
  }
215
+ var release = (version2) => (version2.match(/\d+\.\d+\.\d+/)?.[0] ?? "0.0.0").split(".").map(Number);
216
+ function isOlder(version2, than) {
217
+ const [a2, b] = [release(version2), release(than)];
218
+ for (let i2 = 0; i2 < 3; i2++) if (a2[i2] !== b[i2]) return a2[i2] < b[i2];
219
+ return false;
220
+ }
215
221
  function installedVersion(cwd, name) {
216
222
  return readJson(join(cwd, "node_modules", name, "package.json"))?.version ?? null;
217
223
  }
@@ -1531,21 +1537,14 @@ var PACKAGE = "@zyncat/ui";
1531
1537
  var MCP_SERVER_ENTRY = { command: "node", args: ["./node_modules/@zyncat/ui/dist/mcp.js"] };
1532
1538
  var STYLES_IMPORT = "import '@zyncat/ui/styles.css';";
1533
1539
  var DOCS_URL = "https://ui.zyncat.app";
1534
- var THEME_STARTER = `import { defineTheme } from '@zyncat/ui/theme';
1535
-
1536
- export const base = defineTheme({
1537
- color: { accent: 'oklch(0.63 0.118 198)' },
1538
- });
1539
-
1540
- export const dark = defineTheme({
1541
- color: { bgApp: 'oklch(0.19 0.008 198)', textBody: 'oklch(0.92 0.004 198)' },
1542
- });
1543
- `;
1544
1540
  var interactive = () => Boolean(process.stdout.isTTY && process.stdin.isTTY);
1541
+ var installTouchedProject = false;
1545
1542
  function bail(message, hint) {
1546
1543
  log.error(message);
1547
1544
  if (hint) log.message(dim(hint), { spacing: 0 });
1548
- outro(red("Nothing was changed."));
1545
+ outro(
1546
+ red(installTouchedProject ? "Stopped here - the install ran, nothing else was wired." : "Nothing was changed.")
1547
+ );
1549
1548
  process.exit(1);
1550
1549
  }
1551
1550
  async function pickPm(cwd, targetPkg, flags) {
@@ -1563,13 +1562,20 @@ async function pickPm(cwd, targetPkg, flags) {
1563
1562
  }
1564
1563
  return choice;
1565
1564
  }
1566
- async function planInstall(cwd, targetPkg, flags) {
1565
+ var ADD_COMMAND = {
1566
+ pnpm: "pnpm add",
1567
+ npm: "npm install",
1568
+ yarn: "yarn add",
1569
+ bun: "bun add"
1570
+ };
1571
+ async function planInstall(cwd, targetPkg, flags, version2) {
1567
1572
  const deps = { ...targetPkg.dependencies, ...targetPkg.devDependencies };
1568
1573
  const specs = [];
1569
1574
  let reactNote = null;
1570
1575
  const hasPackage = PACKAGE in deps;
1571
- const packageOnDisk = installedVersion(cwd, PACKAGE) !== null;
1572
- if (!hasPackage) specs.push(PACKAGE);
1576
+ const onDisk = installedVersion(cwd, PACKAGE);
1577
+ const outdated = onDisk !== null && isOlder(onDisk, version2);
1578
+ if (!hasPackage || outdated) specs.push(outdated ? `${PACKAGE}@^${version2}` : PACKAGE);
1573
1579
  if (!("react" in deps)) {
1574
1580
  specs.push("react", "react-dom");
1575
1581
  } else {
@@ -1590,7 +1596,7 @@ async function planInstall(cwd, targetPkg, flags) {
1590
1596
  else reactNote = `React ${major} stays as it is - ${PACKAGE} requires React 19, so upgrade before shipping.`;
1591
1597
  }
1592
1598
  }
1593
- return { specs, restore: hasPackage && !packageOnDisk, reactNote };
1599
+ return { specs, restore: hasPackage && onDisk === null, reactNote };
1594
1600
  }
1595
1601
  function renderProgress(pm, progress, tick) {
1596
1602
  if (pm !== "pnpm") return shimmer(`Installing with ${pm}`, tick);
@@ -1668,20 +1674,36 @@ async function installPhase(pm, plan, cwd) {
1668
1674
  process.exit(1);
1669
1675
  }
1670
1676
  }
1677
+ async function alignVersion(pm, cwd, version2) {
1678
+ const onDisk = installedVersion(cwd, PACKAGE);
1679
+ if (!onDisk || !isOlder(onDisk, version2)) return;
1680
+ log.warn(
1681
+ `${PACKAGE} ${onDisk} resolved from this project's range, older than the CLI (${version2}) - upgrading so the skill and MCP server match.`
1682
+ );
1683
+ const upgraded = await installPhase(pm, { specs: [`${PACKAGE}@^${version2}`], restore: false, reactNote: null }, cwd);
1684
+ if (upgraded) log.message(upgraded.line, { symbol: check });
1685
+ }
1671
1686
  function sourceRoot(cwd, ownRoot) {
1672
1687
  const installed = join3(cwd, "node_modules", PACKAGE);
1673
1688
  if (existsSync3(join3(installed, "skills"))) return installed;
1674
1689
  return ownRoot;
1675
1690
  }
1676
- function wireSkill(cwd, packageRoot) {
1691
+ function wireSkill(cwd, packageRoot, pm) {
1677
1692
  const source = join3(packageRoot, "skills");
1678
- if (!existsSync3(source)) bail(`No skills/ directory found in the installed ${PACKAGE} - reinstall the package.`);
1693
+ if (!existsSync3(source)) {
1694
+ const onDisk = installedVersion(cwd, PACKAGE);
1695
+ return {
1696
+ line: row("Agent skill", `skipped \xB7 ${PACKAGE}${onDisk ? ` ${onDisk}` : ""} ships no skills/`),
1697
+ done: false,
1698
+ hint: `Upgrade it with ${ADD_COMMAND[pm]} ${PACKAGE}@latest, then re-run init.`
1699
+ };
1700
+ }
1679
1701
  const dest = join3(cwd, ".claude/skills");
1680
1702
  const existed = existsSync3(join3(dest, "zyncat-ui"));
1681
1703
  mkdirSync(dest, { recursive: true });
1682
1704
  for (const name of readdirSync2(source))
1683
1705
  cpSync(join3(source, name), join3(dest, name), { recursive: true, force: true });
1684
- return row("Agent skill", `.claude/skills/zyncat-ui \xB7 ${existed ? "refreshed" : "installed"}`);
1706
+ return { line: row("Agent skill", `.claude/skills/zyncat-ui \xB7 ${existed ? "refreshed" : "installed"}`), done: true };
1685
1707
  }
1686
1708
  function wireMcp(cwd) {
1687
1709
  const path = join3(cwd, ".mcp.json");
@@ -1695,29 +1717,27 @@ function wireMcp(cwd) {
1695
1717
  config.mcpServers = { ...config.mcpServers, "zyncat-ui": MCP_SERVER_ENTRY };
1696
1718
  writeFileSync(path, `${JSON.stringify(config, null, 2)}
1697
1719
  `);
1698
- return row("MCP server", existed ? ".mcp.json \xB7 kept" : `.mcp.json ${arrow} zyncat-ui`);
1699
- }
1700
- function wireTheme(cwd) {
1701
- const file = existsSync3(join3(cwd, "tsconfig.json")) ? "zyncat.theme.ts" : "zyncat.theme.js";
1702
- const path = join3(cwd, file);
1703
- if (existsSync3(path)) return row("Typed theme", `${file} \xB7 kept`);
1704
- writeFileSync(path, THEME_STARTER);
1705
- return row("Typed theme", `${file} \xB7 created`);
1720
+ return { line: row("MCP server", existed ? ".mcp.json \xB7 kept" : `.mcp.json ${arrow} zyncat-ui`), done: true };
1706
1721
  }
1707
1722
  function wireStyles(cwd) {
1708
1723
  const entry = findAppEntry(cwd);
1709
- if (!entry) return { line: `${row("Stylesheet", "no app entry found \xB7 add the import yourself")}`, manual: true };
1724
+ if (!entry)
1725
+ return {
1726
+ line: row("Stylesheet", "no app entry found \xB7 add the import yourself"),
1727
+ done: false,
1728
+ hint: `Put ${STYLES_IMPORT} at your app root, above your own stylesheets.`
1729
+ };
1710
1730
  const path = join3(cwd, entry);
1711
1731
  const text = readFileSync2(path, "utf8");
1712
1732
  if (text.includes("@zyncat/ui/styles.css"))
1713
- return { line: row("Stylesheet", `${entry} \xB7 already imported`), manual: false };
1733
+ return { line: row("Stylesheet", `${entry} \xB7 already imported`), done: true };
1714
1734
  const eol = text.includes("\r\n") ? "\r\n" : "\n";
1715
1735
  const lines = text.split(eol);
1716
1736
  let at = 0;
1717
1737
  while (at < lines.length && (lines[at].trim() === "" || /^(['"])use [\w-]+\1;?$/.test(lines[at].trim()))) at++;
1718
1738
  lines.splice(at, 0, STYLES_IMPORT);
1719
1739
  writeFileSync(path, lines.join(eol));
1720
- return { line: row("Stylesheet", `${entry} \xB7 import added`), manual: false };
1740
+ return { line: row("Stylesheet", `${entry} \xB7 import added`), done: true };
1721
1741
  }
1722
1742
  async function init(flags) {
1723
1743
  const startedAt = performance.now();
@@ -1739,23 +1759,22 @@ async function init(flags) {
1739
1759
  const pmVer = pmVersion(pm);
1740
1760
  const project = targetPkg.name ?? "unnamed project";
1741
1761
  log.message(dim(`${project} \xB7 ${pm}${pmVer ? ` ${pmVer}` : ""}`));
1742
- const plan = await planInstall(cwd, targetPkg, flags);
1762
+ const plan = await planInstall(cwd, targetPkg, flags, version2);
1743
1763
  const installed = await installPhase(pm, plan, cwd);
1764
+ installTouchedProject = true;
1744
1765
  if (installed) log.message(installed.line, { symbol: check });
1745
1766
  if (plan.reactNote) log.warn(plan.reactNote);
1767
+ await alignVersion(pm, cwd, version2);
1746
1768
  const packageRoot = sourceRoot(cwd, ownRoot);
1747
- const lines = [wireSkill(cwd, packageRoot), wireMcp(cwd), wireTheme(cwd)];
1748
- const styles = wireStyles(cwd);
1749
- lines.push(styles.line);
1750
- for (const [index, line] of lines.entries())
1751
- log.message(line, {
1752
- symbol: styles.manual && index === lines.length - 1 ? skip : check,
1753
- spacing: index === 0 ? 1 : 0
1754
- });
1755
- if (styles.manual)
1756
- log.message(dim(`Put ${STYLES_IMPORT} at your app root, above your own stylesheets.`), { spacing: 0 });
1769
+ const rows = [wireSkill(cwd, packageRoot, pm), wireMcp(cwd), wireStyles(cwd)];
1770
+ for (const [index, entry] of rows.entries()) {
1771
+ log.message(entry.line, { symbol: entry.done ? check : skip, spacing: index === 0 ? 1 : 0 });
1772
+ if (entry.hint) log.message(dim(entry.hint), { spacing: 0 });
1773
+ }
1757
1774
  log.message(`${dim("Docs")} ${arrow} ${accentDeep(DOCS_URL)}`);
1758
- outro(`${bold(`Ready in ${seconds(performance.now() - startedAt)}.`)} Restart your agent session to load the skill.`);
1775
+ const skipped = rows.filter((entry) => !entry.done).length;
1776
+ const closing = skipped ? `${bold(`Done in ${seconds(performance.now() - startedAt)}.`)} ${skipped} step${skipped === 1 ? "" : "s"} left for you, above.` : `${bold(`Ready in ${seconds(performance.now() - startedAt)}.`)} Restart your agent session to load the skill.`;
1777
+ outro(closing);
1759
1778
  }
1760
1779
 
1761
1780
  // src/cli.ts
@@ -1778,7 +1797,7 @@ function help(version2) {
1778
1797
  ` ${wordmark(version2)}`,
1779
1798
  "",
1780
1799
  ` ${dim("Sets up @zyncat/ui in a React project: installs the package,")}`,
1781
- ` ${dim("the agent skill, the MCP server, a typed theme and the stylesheet.")}`,
1800
+ ` ${dim("the agent skill, the MCP server and the stylesheet.")}`,
1782
1801
  "",
1783
1802
  ` ${bold("Usage")}`,
1784
1803
  ` zyncat-ui init ${dim("[flags]")}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zyncat-ui",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Setup CLI for @zyncat/ui - one command installs the package and wires the project.",
5
5
  "keywords": [
6
6
  "zyncat",