@vegastack/design 0.1.0 → 0.1.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.
@@ -163,16 +163,43 @@ function walk(dir, out = []) {
163
163
  return out;
164
164
  }
165
165
 
166
- function readHeader(file) {
167
- let firstLine = '';
166
+ // Read a copied-in file. Returns { file, name, content, header? } where header is the parsed
167
+ // provenance line when present. The header is the fast path — but the REAL `shadcn add`
168
+ // pipeline strips leading comments during its transform, so most consumer copies have NO
169
+ // header. Headerless files are identified by filename against the registry index and compared
170
+ // by alias-normalized CONTENT instead (see `normalizeForCompare`).
171
+ function readInstalled(file) {
172
+ let content;
168
173
  try {
169
- const fd = readFileSync(file, 'utf8');
170
- firstLine = fd.slice(0, fd.indexOf('\n') === -1 ? fd.length : fd.indexOf('\n'));
174
+ content = readFileSync(file, 'utf8');
171
175
  } catch {
172
176
  return null;
173
177
  }
178
+ const firstLine = content.slice(0, content.indexOf('\n') === -1 ? content.length : content.indexOf('\n'));
174
179
  const m = PROVENANCE_RE.exec(firstLine);
175
- return m ? { name: m[1], version: m[2], hash: `sha256-${m[3]}`, file } : null;
180
+ const name = basename(file).replace(/\.tsx?$/, '');
181
+ return m
182
+ ? { file, name: m[1], content, header: { version: m[2], hash: `sha256-${m[3]}` } }
183
+ : { file, name, content, header: null };
184
+ }
185
+
186
+ // Strip a provenance header (with its optional following blank line) from file content.
187
+ function stripHeader(content) {
188
+ return content.replace(/^\/\/ @vegastack \S+@\S+ sha256-\S+\r?\n(?:\r?\n)?/, '');
189
+ }
190
+
191
+ /**
192
+ * Normalize content for the headerless comparison: drop the provenance header, unify line
193
+ * endings, rewrite the registry's canonical `@/…` import prefix to the consumer's alias root
194
+ * (the same class of rewrite `shadcn add` performs), and ignore trailing whitespace.
195
+ * Consumers on the default `@/*` alias need no rewrite at all.
196
+ */
197
+ function normalizeForCompare(content, aliasRoot) {
198
+ let s = stripHeader(content).replace(/\r\n/g, '\n');
199
+ if (aliasRoot && aliasRoot !== '@') {
200
+ s = s.replace(/((?:from\s*|import\s*|import\(\s*|require\(\s*))(['"])@\//g, (_, head, q) => `${head}${q}${aliasRoot}/`);
201
+ }
202
+ return s.trimEnd() + '\n';
176
203
  }
177
204
 
178
205
  function globToRe(pattern) {
@@ -223,19 +250,8 @@ export async function main(argv) {
223
250
  const idxUrl = indexUrl(urlTemplate);
224
251
  const dir = resolveComponentsDir(cwd, opts.dir, componentsJson);
225
252
 
226
- // scan for installed VegaStack components
227
- let installed = walk(dir).map(readHeader).filter(Boolean);
228
- if (opts.filter) {
229
- const res = opts.filter.split(',').map((s) => globToRe(s.trim()));
230
- installed = installed.filter((c) => res.some((re) => re.test(c.name)));
231
- }
232
- if (installed.length === 0) {
233
- if (opts.json) console.log(JSON.stringify({ registry: idxUrl, checked: 0, updates: 0, items: [] }, null, 2));
234
- else console.log(`No VegaStack components found in ${dir}. (Add some with \`shadcn add @vegastack/<name>\`.)`);
235
- return 0;
236
- }
237
-
238
- // fetch the registry index once
253
+ // fetch the registry index once (needed up front: headerless files are identified by
254
+ // matching their filename against the index's item names)
239
255
  let index;
240
256
  try {
241
257
  const res = await fetch(idxUrl, { headers });
@@ -252,22 +268,62 @@ export async function main(argv) {
252
268
  const remote = new Map();
253
269
  for (const item of index.items ?? []) remote.set(item.name, { version: item.meta?.version, integrity: item.meta?.integrity });
254
270
 
255
- // compare by hash
256
- const rows = installed
257
- .map((c) => {
258
- const r = remote.get(c.name);
259
- let status;
260
- if (!r) status = 'missing';
261
- else if (r.integrity && c.hash === r.integrity) status = 'current';
262
- else status = 'update';
263
- return { name: c.name, current: c.version, latest: r?.version ?? null, status };
264
- })
265
- .sort((a, b) => {
266
- const rank = { update: 0, current: 1, missing: 2 };
267
- return rank[a.status] - rank[b.status] || a.name.localeCompare(b.name);
268
- });
269
-
270
- const updates = rows.filter((r) => r.status === 'update');
271
+ // scan for installed VegaStack components:
272
+ // - headered files (our own tooling / older CLIs preserve the provenance line) — always included
273
+ // - headerless files whose basename matches a registry item — the REAL `shadcn add` strips
274
+ // the header, so this is the normal consumer case
275
+ // - headerless files NOT in the index are skipped (they're the consumer's own components)
276
+ let installed = walk(dir)
277
+ .map(readInstalled)
278
+ .filter(Boolean)
279
+ .filter((c) => c.header || remote.has(c.name));
280
+ if (opts.filter) {
281
+ const res = opts.filter.split(',').map((s) => globToRe(s.trim()));
282
+ installed = installed.filter((c) => res.some((re) => re.test(c.name)));
283
+ }
284
+ if (installed.length === 0) {
285
+ if (opts.json) console.log(JSON.stringify({ registry: idxUrl, checked: 0, updates: 0, items: [] }, null, 2));
286
+ else console.log(`No VegaStack components found in ${dir}. (Add some with \`shadcn add @vegastack/<name>\`.)`);
287
+ return 0;
288
+ }
289
+
290
+ // the consumer's alias root ('@' default; '~', 'src', … supported) for content normalization
291
+ const aliasRoot = (componentsJson?.aliases?.components ?? '@/components').split('/')[0];
292
+
293
+ // Resolve each installed file to a status:
294
+ // - headered: compare the header's item hash against the index integrity (fast, no item fetch)
295
+ // - headerless: fetch the item and compare alias-normalized CONTENT; equal → current,
296
+ // different → 'drift' (an upstream update OR local edits — `add --diff` disambiguates)
297
+ const itemUrlFor = (name) =>
298
+ urlTemplate.includes('{name}') ? urlTemplate.replace('{name}', name) : `${urlTemplate.replace(/\/$/, '')}/${name}.json`;
299
+
300
+ async function resolveStatus(c) {
301
+ const r = remote.get(c.name);
302
+ if (!r) return { name: c.name, current: c.header?.version ?? null, latest: null, status: 'missing' };
303
+ if (c.header) {
304
+ const status = r.integrity && c.header.hash === r.integrity ? 'current' : 'update';
305
+ return { name: c.name, current: c.header.version, latest: r.version ?? null, status };
306
+ }
307
+ try {
308
+ const res = await fetch(itemUrlFor(c.name), { headers });
309
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
310
+ const item = await res.json();
311
+ const base = basename(c.file);
312
+ const entry = (item.files ?? []).find((f) => basename(f.target ?? f.path ?? '') === base) ?? (item.files ?? [])[0];
313
+ const remoteContent = entry?.content ?? '';
314
+ const same = normalizeForCompare(remoteContent, aliasRoot) === normalizeForCompare(c.content, aliasRoot);
315
+ return { name: c.name, current: null, latest: r.version ?? null, status: same ? 'current' : 'drift' };
316
+ } catch (err) {
317
+ return { name: c.name, current: null, latest: r.version ?? null, status: 'drift', note: `item fetch failed: ${err.message}` };
318
+ }
319
+ }
320
+
321
+ const rows = (await Promise.all(installed.map(resolveStatus))).sort((a, b) => {
322
+ const rank = { update: 0, drift: 0, current: 1, missing: 2 };
323
+ return rank[a.status] - rank[b.status] || a.name.localeCompare(b.name);
324
+ });
325
+
326
+ const updates = rows.filter((r) => r.status === 'update' || r.status === 'drift');
271
327
 
272
328
  if (opts.json) {
273
329
  console.log(JSON.stringify({ registry: idxUrl, checked: rows.length, updates: updates.length, items: rows }, null, 2));
@@ -284,14 +340,18 @@ export async function main(argv) {
284
340
  })();
285
341
  console.log(`\nChecking ${rows.length} VegaStack component(s) against ${host} …\n`);
286
342
  const nameW = Math.max(...rows.map((r) => r.name.length), 4);
287
- const GLYPH = { update: color.yellow('⬆'), current: color.green('✓'), missing: color.dim('?') };
343
+ const GLYPH = { update: color.yellow('⬆'), drift: color.yellow('≈'), current: color.green('✓'), missing: color.dim('?') };
288
344
  for (const r of rows) {
289
- const ver = r.status === 'update' ? `${r.current} → ${r.latest ?? '?'}` : r.current;
345
+ const ver =
346
+ r.status === 'update' ? `${r.current} → ${r.latest ?? '?'}` :
347
+ r.status === 'drift' ? `→ ${r.latest ?? '?'}` :
348
+ (r.current ?? r.latest ?? '—');
290
349
  const note =
291
350
  r.status === 'update' ? color.yellow('update available') :
351
+ r.status === 'drift' ? color.yellow('differs from registry (update or local edits — review with --diff)') :
292
352
  r.status === 'current' ? color.dim('up to date') :
293
353
  color.dim('not in registry (renamed/removed)');
294
- console.log(` ${GLYPH[r.status]} ${r.name.padEnd(nameW)} ${ver.padEnd(16)} ${note}`);
354
+ console.log(` ${GLYPH[r.status]} ${r.name.padEnd(nameW)} ${String(ver).padEnd(16)} ${note}`);
295
355
  }
296
356
  console.log('');
297
357
  if (updates.length) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vegastack/design",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "VegaStack design system — cn utility, icon runtime, Tailwind v4 preset, and the vegastack-design CLI (tokens ship separately as @vegastack/design-tokens)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -44,20 +44,17 @@
44
44
  "lucide-react": "^1.24.0",
45
45
  "tailwind-merge": "^3.6.0",
46
46
  "thesvg": "^3.2.6",
47
+ "tw-animate-css": "^1.4.0",
47
48
  "@vegastack/design-tokens": "0.1.0"
48
49
  },
49
50
  "peerDependencies": {
50
51
  "react": "^19.0.0",
51
52
  "react-dom": "^19.0.0",
52
- "tailwindcss": "^4.3.2",
53
- "tw-animate-css": "^1.4.0"
53
+ "tailwindcss": "^4.3.2"
54
54
  },
55
55
  "peerDependenciesMeta": {
56
56
  "tailwindcss": {
57
57
  "optional": true
58
- },
59
- "tw-animate-css": {
60
- "optional": true
61
58
  }
62
59
  },
63
60
  "devDependencies": {