@kznlabs/labsui 0.1.10 → 0.1.11

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 +9 -5
  2. package/dist/index.js +55 -34
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -60,7 +60,9 @@ Run it again any time: existing files are kept.
60
60
  Resolves dependencies through the registry (`data-table` brings `table`,
61
61
  `checkbox`, `pagination`, …), writes each file under your alias folder and
62
62
  installs the npm packages it imports. Existing files are kept unless you pass
63
- `--overwrite`. If your alias isn't `@/`, imports are rewritten to match.
63
+ `--overwrite`; a kept dependency whose code differs from this version is
64
+ named, with the `labsui update` that brings it level, since the new item may
65
+ need it. If your alias isn't `@/`, imports are rewritten to match.
64
66
  Components that live once in the root layout (`toast`, `confirm`) print the
65
67
  line to add there. A mistyped name gets a suggestion: `buton` suggests `button`.
66
68
 
@@ -109,16 +111,18 @@ changes. `labsui.json` records which labsui version wrote each item, and
109
111
  The base comes from JSR, which keeps every release's files. Offline, with a
110
112
  hosted registry, or for items installed before versions were recorded,
111
113
  edited files are left as they are. `labsui diff` lists installed items that
112
- differ from this version; `labsui diff dialog` prints the patch from this
113
- version's file to yours.
114
+ differ from this version, marking those that differ only in blank lines or
115
+ indentation; `labsui diff dialog` prints the patch from this version's file
116
+ to yours, or says `dialog` isn't installed.
114
117
 
115
118
  ### `doctor`
116
119
 
117
120
  Checks the setup and prints a fix for each problem: React 19 and Tailwind v4,
118
121
  the path alias and `labsui.json`, the stylesheet import, `MotionProvider` and
119
122
  the theme script, `<Toaster />` and `<Confirmer />` mounted when installed,
120
- missing npm packages or files. It also lists installed items that differ from
121
- this version, your edits or an older install. Exits with 1 when something is
123
+ missing npm packages, and files your installed components import but the
124
+ project lacks. It also lists installed items that differ from this version,
125
+ your edits or an older install. Exits with 1 when something is
122
126
  broken, so it can run in CI.
123
127
 
124
128
  It reads your code, not a framework's conventions: it works the same in any
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { resolve as resolve2 } from "node:path";
5
5
  import { parseArgs } from "node:util";
6
6
 
7
7
  // src/commands/add.ts
8
- import { existsSync as existsSync3, mkdirSync, realpathSync, writeFileSync as writeFileSync2 } from "node:fs";
8
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, realpathSync, writeFileSync as writeFileSync2 } from "node:fs";
9
9
  import { basename, dirname, isAbsolute, join as join3, relative as relative2, resolve } from "node:path";
10
10
 
11
11
  // src/install.ts
@@ -62,6 +62,16 @@ function installDeps(cwd, deps, opts) {
62
62
  return true;
63
63
  }
64
64
 
65
+ // src/lines.ts
66
+ var lines = (text) => text.replace(/\r\n/g, `
67
+ `).replace(/\n$/, "").split(`
68
+ `);
69
+ function sameCode(a, b) {
70
+ const strip = (text) => lines(text).map((line) => line.trim()).filter(Boolean).join(`
71
+ `);
72
+ return strip(a) === strip(b);
73
+ }
74
+
65
75
  // src/project.ts
66
76
  import { existsSync as existsSync2, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
67
77
  import { join as join2, relative } from "node:path";
@@ -17811,7 +17821,7 @@ async function fetchAllItems(source) {
17811
17821
  // jsr.json
17812
17822
  var jsr_default = {
17813
17823
  name: "@kznlabs/labsui",
17814
- version: "0.1.10",
17824
+ version: "0.1.11",
17815
17825
  license: "MIT",
17816
17826
  exports: "./src/index.ts",
17817
17827
  publish: {
@@ -17861,6 +17871,7 @@ async function add(cwd, config, names, opts) {
17861
17871
  fresh: [],
17862
17872
  written: [],
17863
17873
  kept: [],
17874
+ outdated: [],
17864
17875
  packages: [],
17865
17876
  mounts: [],
17866
17877
  dryRun: opts.dryRun
@@ -17897,6 +17908,8 @@ function writeItem(item, base, cwd, config, opts, result, named) {
17897
17908
  if (existsSync3(target) && !opts.overwrite) {
17898
17909
  if (named)
17899
17910
  result.kept.push(shown);
17911
+ else if (!result.outdated.includes(item.name) && !sameCode(readFileSync2(target, "utf8"), rewriteAlias(file.content, config.alias)))
17912
+ result.outdated.push(item.name);
17900
17913
  continue;
17901
17914
  }
17902
17915
  if (!opts.dryRun) {
@@ -17918,6 +17931,8 @@ ${fileList(result.written)}`);
17918
17931
  if (result.kept.length)
17919
17932
  console.log(`${yellow("•")} Kept ${result.kept.length} existing file(s) (--overwrite replaces them):
17920
17933
  ${fileList(result.kept)}`);
17934
+ if (result.outdated.length)
17935
+ console.log(`${yellow("•")} Kept dependencies that differ from this version (older or edited): ${result.outdated.join(", ")}. The new items may need this version: ${bold(`labsui update ${result.outdated.join(" ")}`)}`);
17921
17936
  if (result.installCommand)
17922
17937
  console.log(`${yellow("•")} Install these yourself: ${bold(result.installCommand)}`);
17923
17938
  else if (result.packages.length)
@@ -17932,16 +17947,8 @@ ${bold("Mount once")} in the root layout, inside <MotionProvider>:`);
17932
17947
  }
17933
17948
 
17934
17949
  // src/commands/diff.ts
17935
- import { existsSync as existsSync4, readFileSync as readFileSync2 } from "node:fs";
17950
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
17936
17951
  import { relative as relative3 } from "node:path";
17937
- var lines = (text) => text.replace(/\r\n/g, `
17938
- `).replace(/\n$/, "").split(`
17939
- `);
17940
- function sameCode(a, b) {
17941
- const strip = (text) => lines(text).map((line) => line.trim()).filter(Boolean).join(`
17942
- `);
17943
- return strip(a) === strip(b);
17944
- }
17945
17952
  function matches(a, b) {
17946
17953
  const out = new Int32Array(a.length).fill(-1);
17947
17954
  let i = 0;
@@ -18016,9 +18023,8 @@ function unifiedDiff(from, to, fromName, toName, context = 3) {
18016
18023
  return out.join(`
18017
18024
  `);
18018
18025
  }
18019
- async function installedItems(config, base) {
18020
- const all = await fetchAllItems(config.registry);
18021
- return all.flatMap((item) => {
18026
+ async function installedItems(config, base, all) {
18027
+ return (all ?? await fetchAllItems(config.registry)).flatMap((item) => {
18022
18028
  const files = item.files.map((file) => safeTarget(base, file.target ?? file.path));
18023
18029
  return files.some((file) => existsSync4(file)) ? [{ item, files }] : [];
18024
18030
  });
@@ -18026,8 +18032,12 @@ async function installedItems(config, base) {
18026
18032
  async function diff(cwd, config, names, version) {
18027
18033
  const base = projectBase(cwd, config);
18028
18034
  const wanted = new Set(names.map(itemName));
18029
- const installed = await installedItems(config, base);
18030
- const out = [];
18035
+ const all = await fetchAllItems(config.registry);
18036
+ const unknown = [...wanted].filter((name) => !all.some((item) => item.name === name));
18037
+ if (unknown.length)
18038
+ throw new CliError(`Unknown item(s): ${unknown.join(", ")}. Search finds the names.`);
18039
+ const installed = await installedItems(config, base, all);
18040
+ const out = [...wanted].filter((name) => !installed.some(({ item }) => item.name === name)).map((name) => ({ name, installed: false, files: [] }));
18031
18041
  for (const { item, files } of installed) {
18032
18042
  if (wanted.size && !wanted.has(item.name))
18033
18043
  continue;
@@ -18038,7 +18048,7 @@ async function diff(cwd, config, names, version) {
18038
18048
  return;
18039
18049
  const shown = relative3(cwd, local).replaceAll("\\", "/");
18040
18050
  const theirs = rewriteAlias(file.content, config.alias);
18041
- const yours = readFileSync2(local, "utf8");
18051
+ const yours = readFileSync3(local, "utf8");
18042
18052
  const patch = unifiedDiff(theirs, yours, `labsui@${version}/${file.path}`, shown);
18043
18053
  if (!patch)
18044
18054
  return;
@@ -18053,14 +18063,16 @@ async function diff(cwd, config, names, version) {
18053
18063
  });
18054
18064
  });
18055
18065
  if (changed.length)
18056
- out.push({ name: item.name, files: changed });
18066
+ out.push({ name: item.name, installed: true, files: changed });
18057
18067
  }
18058
18068
  return out;
18059
18069
  }
18060
18070
  function printDiff(items, full) {
18061
18071
  if (!items.length)
18062
18072
  return console.log(`${green("✓")} Installed items match this labsui version.`);
18063
- for (const item of items)
18073
+ for (const item of items) {
18074
+ if (!item.installed)
18075
+ console.log(` ${item.name.padEnd(24)} ${dim(`not installed (labsui add ${item.name})`)}`);
18064
18076
  for (const file of item.files) {
18065
18077
  if (!full) {
18066
18078
  console.log(` ${item.name.padEnd(24)} ${green(`+${file.added}`)} ${red(`-${file.removed}`)} ${dim(file.path)}${file.formatting ? dim(" (formatting only)") : ""}`);
@@ -18070,13 +18082,14 @@ function printDiff(items, full) {
18070
18082
  `))
18071
18083
  console.log(line.startsWith("+") ? green(line) : line.startsWith("-") ? red(line) : line.startsWith("@@") ? dim(line) : line);
18072
18084
  }
18085
+ }
18073
18086
  if (!full)
18074
18087
  console.log(`
18075
18088
  ${dim("labsui diff <name> shows the changes; add <name> --overwrite takes this version.")}`);
18076
18089
  }
18077
18090
 
18078
18091
  // src/commands/doctor.ts
18079
- import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
18092
+ import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2 } from "node:fs";
18080
18093
  import { join as join4, relative as relative4 } from "node:path";
18081
18094
 
18082
18095
  // src/merge.ts
@@ -18196,13 +18209,13 @@ async function doctor(cwd, registryFlag) {
18196
18209
  else {
18197
18210
  const css = findCss(cwd, config.root);
18198
18211
  const shown = css && relative4(cwd, css).replaceAll("\\", "/");
18199
- if (css && readFileSync3(css, "utf8").includes("labsui.css"))
18212
+ if (css && readFileSync4(css, "utf8").includes("labsui.css"))
18200
18213
  add("styles", "pass", `${shown} imports labsui.css`);
18201
18214
  else
18202
18215
  add("styles", "fail", css ? `${shown} does not import labsui.css.` : "No stylesheet imports Tailwind.", 'Run npx @kznlabs/labsui init, or add @import "<path>/styles/labsui.css"; after @import "tailwindcss".');
18203
18216
  }
18204
18217
  const sources = sourceFiles(cwd === base ? cwd : base).concat(cwd === base ? [] : sourceFiles(join4(cwd, "app"))).filter((file) => !owned.has(file));
18205
- const texts = sources.map((source) => readFileSync3(source, "utf8"));
18218
+ const texts = sources.map((source) => readFileSync4(source, "utf8"));
18206
18219
  const used = (needle) => texts.some((text) => text.includes(needle));
18207
18220
  for (const [needle, label, fix] of [
18208
18221
  [
@@ -18235,18 +18248,23 @@ async function doctor(cwd, registryFlag) {
18235
18248
  add("packages", "fail", `Missing npm packages: ${missing.map(packageName).join(", ")}`, installCommand(cwd, missing).join(" "));
18236
18249
  else if (items.length)
18237
18250
  add("packages", "pass", "npm packages installed");
18238
- const names = new Set(items.map(({ item }) => item.name));
18239
- const broken = items.flatMap(({ item }) => (item.registryDependencies ?? []).map((dependency) => dependency.split("/").pop()?.replace(/\.json$/, "") ?? dependency).filter((dependency) => !names.has(dependency)).map((dependency) => `${item.name} → ${dependency}`));
18251
+ const alias = config.alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
18252
+ const local = new RegExp(`from "(?:${alias}((?:components|lib)/[\\w/-]+)|\\./([\\w-]+))"`, "g");
18253
+ const resolves = (path) => [".ts", ".tsx"].some((ext) => existsSync5(path + ext));
18254
+ const broken = items.flatMap(({ item, files }) => files.filter((file) => existsSync5(file)).flatMap((file) => [...readFileSync4(file, "utf8").matchAll(local)].flatMap(([, fromAlias, sibling]) => {
18255
+ const path = fromAlias ? join4(base, fromAlias) : join4(file, "..", sibling ?? "");
18256
+ return resolves(path) ? [] : [`${item.name} → ${path.split(/[\\/]/).pop()}`];
18257
+ })));
18240
18258
  if (broken.length)
18241
18259
  add("files", "fail", `Missing files: ${broken.join(", ")}`, `npx @kznlabs/labsui add ${[...new Set(broken.map((line) => line.split(" → ")[1]))].join(" ")}`);
18242
- const unresolved = items.flatMap(({ files }) => files).filter((file) => existsSync5(file) && hasConflictMarkers(readFileSync3(file, "utf8"))).map((file) => relative4(cwd, file).replaceAll("\\", "/"));
18260
+ const unresolved = items.flatMap(({ files }) => files).filter((file) => existsSync5(file) && hasConflictMarkers(readFileSync4(file, "utf8"))).map((file) => relative4(cwd, file).replaceAll("\\", "/"));
18243
18261
  if (unresolved.length)
18244
18262
  add("merge", "fail", `Unresolved update conflicts in ${unresolved.join(", ")}`, "Pick between the <<<<<<< yours and >>>>>>> labsui sides, then delete the markers.");
18245
18263
  const drift = (code) => items.filter(({ item, files }) => item.files.some((file, i) => {
18246
18264
  const local = files[i];
18247
18265
  if (!local || !existsSync5(local))
18248
18266
  return false;
18249
- const yours = readFileSync3(local, "utf8");
18267
+ const yours = readFileSync4(local, "utf8");
18250
18268
  const theirs = rewriteAlias(file.content, config.alias);
18251
18269
  const norm = (text) => text.replace(/\r\n/g, `
18252
18270
  `).trimEnd();
@@ -18281,7 +18299,7 @@ ${result.ok ? green(bold("Setup OK")) : red(bold(`${fails} problem(s)`))}${warns
18281
18299
  }
18282
18300
 
18283
18301
  // src/commands/init.ts
18284
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
18302
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
18285
18303
  import { dirname as dirname2, join as join5, relative as relative5 } from "node:path";
18286
18304
  function missingPeers(cwd, warnings) {
18287
18305
  const pkg = readPackage(cwd);
@@ -18304,7 +18322,7 @@ function wireStylesheet(cwd, root, opts, warnings) {
18304
18322
  let spec = relative5(dirname2(css), join5(cwd, root, "styles/labsui.css")).replaceAll("\\", "/");
18305
18323
  if (!spec.startsWith("."))
18306
18324
  spec = `./${spec}`;
18307
- const text = readFileSync4(css, "utf8");
18325
+ const text = readFileSync5(css, "utf8");
18308
18326
  const clashes = [
18309
18327
  ...new Set([
18310
18328
  ...text.matchAll(/(--(?:background|foreground|primary|border|radius))\s*:|(font-family)\s*:/g)
@@ -18475,7 +18493,7 @@ ${dim(`labsui view ${hits[0]?.name} shows props and an example.`)}`);
18475
18493
  }
18476
18494
 
18477
18495
  // src/commands/update.ts
18478
- import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
18496
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
18479
18497
  import { dirname as dirname3, relative as relative6 } from "node:path";
18480
18498
  var jsrBase = async (version) => {
18481
18499
  try {
@@ -18505,7 +18523,7 @@ async function update(cwd, config, names, opts, loadBase = jsrBase) {
18505
18523
  for (const { item, files } of installed) {
18506
18524
  const shown = (file) => relative6(cwd, file).replaceAll("\\", "/");
18507
18525
  const next = item.files.map((file) => norm(rewriteAlias(file.content, config.alias)));
18508
- const local = files.map((file) => existsSync6(file) ? norm(readFileSync5(file, "utf8")) : undefined);
18526
+ const local = files.map((file) => existsSync6(file) ? norm(readFileSync6(file, "utf8")) : undefined);
18509
18527
  if (local.every((line, i) => line === next[i])) {
18510
18528
  items.push({ name: item.name, status: "current", files: [], conflicts: 0 });
18511
18529
  continue;
@@ -18783,7 +18801,7 @@ var TOOLS = [
18783
18801
  {
18784
18802
  name: "add",
18785
18803
  title: "Add labsUI items",
18786
- description: "Install labsUI items into the project: copies each item and everything it imports, then installs their npm packages. Existing files are kept, never overwritten. Returns the files written, the packages installed and anything to mount once in the root layout. Needs init first; dryRun previews. To bring installed items to a newer version, use update instead.",
18804
+ description: "Install labsUI items into the project: copies each item and everything it imports, then installs their npm packages. Existing files are kept, never overwritten. Returns the files written, the packages installed, dependencies kept that differ from this version (call update on them) and anything to mount once in the root layout. Needs init first; dryRun previews. To bring installed items to a newer version, use update instead.",
18787
18805
  inputSchema: {
18788
18806
  type: "object",
18789
18807
  properties: { names, dryRun },
@@ -18804,7 +18822,7 @@ var TOOLS = [
18804
18822
  {
18805
18823
  name: "diff",
18806
18824
  title: "Diff installed labsUI items",
18807
- description: "Compare installed labsUI items with this labsUI version. Returns a unified patch per changed file, from the registry file to the project file (+ lines exist only in the project), with formatting: true when only blank lines or indentation differ; matching items are left out, and when nothing differs it says so. Use it to see local edits before update. Needs init.",
18825
+ description: "Compare installed labsUI items with this labsUI version. Returns a unified patch per changed file, from the registry file to the project file (+ lines exist only in the project), with formatting: true when only blank lines or indentation differ; matching items are left out, a named item that isn't installed comes back with installed: false, and when nothing differs it says so. Use it to see local edits before update. Needs init.",
18808
18826
  inputSchema: {
18809
18827
  type: "object",
18810
18828
  properties: { names: installed }
@@ -18866,6 +18884,9 @@ async function callTool(ctx, name, args) {
18866
18884
  const lines = [
18867
18885
  `${added.dryRun ? "Would write" : "Wrote"} ${added.written.length} file(s)${added.written.length ? `: ${added.written.join(", ")}` : ""}.`,
18868
18886
  ...added.kept.length ? [`Kept existing: ${added.kept.join(", ")}.`] : [],
18887
+ ...added.outdated.length ? [
18888
+ `Dependencies already here differ from this version (older or edited): ${added.outdated.join(", ")}. The new items may not compile against them; call update with these names.`
18889
+ ] : [],
18869
18890
  ...added.installCommand ? [`Install: ${added.installCommand}`] : added.packages.length ? [`Installed ${added.packages.join(", ")}.`] : [],
18870
18891
  ...added.mounts.map((mount) => `Mount once in the root layout, inside <MotionProvider>: ${mount.tag} from "${mount.from}".`)
18871
18892
  ];
@@ -18890,9 +18911,9 @@ async function callTool(ctx, name, args) {
18890
18911
  if (!config)
18891
18912
  throw new CliError("labsUI is not set up here. Call the init tool first.");
18892
18913
  const items = await diff(ctx.cwd, { ...config, ...ctx.registry && { registry: ctx.registry } }, list(args.names), ctx.version);
18893
- const body = items.length ? items.flatMap((i) => i.files.map((file) => `\`\`\`diff
18914
+ const body = items.length ? items.flatMap((i) => i.installed ? i.files.map((file) => `\`\`\`diff
18894
18915
  ${file.patch}
18895
- \`\`\``)).join(`
18916
+ \`\`\``) : [`${i.name} is not installed; add installs it.`]).join(`
18896
18917
 
18897
18918
  `) : "Installed items match this labsui version.";
18898
18919
  return text(body, { items });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kznlabs/labsui",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "Add labsUI components to your React + Tailwind v4 project: clean primitives you own, with smooth spring motion.",
5
5
  "type": "module",
6
6
  "mcpName": "io.github.kznlabs/labsui",