@vegastack/design 0.1.1 → 0.3.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.
package/bin/doctor.mjs ADDED
@@ -0,0 +1,332 @@
1
+ // `vegastack-design doctor` — check a consuming project's setup.
2
+ //
3
+ // Motivated by a real consumer failure (VegaStack CRM, 2026-07-27): a missing
4
+ // `@tailwindcss/postcss` plugin produces two different, equally misleading results.
5
+ // Under Turbopack the build dies with `Can't resolve 'tw-animate-css'`, naming a
6
+ // dependency that is installed and fine. Under webpack the build SUCCEEDS, the token
7
+ // theme lands (it is literal CSS inside preset.css), and zero utility classes are
8
+ // generated — so the app renders with correct colours and no spacing or layout, which
9
+ // reads as "the design system is broken".
10
+ //
11
+ // A human will not attribute either symptom correctly, which is exactly why this is a
12
+ // command and not a paragraph in a guide. Every check here maps to a documented failure
13
+ // mode in the Troubleshooting guide.
14
+ //
15
+ // Read-only: it never writes, installs, or edits. Exit 0 = all good, 1 = a real problem,
16
+ // so it composes into CI as `vegastack-design doctor`.
17
+
18
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
19
+ import { dirname, join, relative } from "node:path";
20
+
21
+ const POSTCSS_CONFIGS = [
22
+ "postcss.config.mjs",
23
+ "postcss.config.js",
24
+ "postcss.config.cjs",
25
+ "postcss.config.ts",
26
+ "postcss.config.json",
27
+ ".postcssrc",
28
+ ".postcssrc.json",
29
+ ];
30
+
31
+ const CSS_SEARCH_DIRS = [
32
+ "app",
33
+ "src/app",
34
+ "src/styles",
35
+ "styles",
36
+ "src",
37
+ "apps",
38
+ ];
39
+
40
+ const USAGE = `
41
+ Usage: vegastack-design doctor [options]
42
+
43
+ Checks a consuming project's VegaStack setup and reports what is wrong and how to fix it.
44
+
45
+ Options:
46
+ --dir <path> Project root to inspect (default: the current directory)
47
+ -h, --help Show this help
48
+
49
+ Exit codes: 0 = no problems · 1 = at least one failure · 2 = bad usage
50
+ `.trim();
51
+
52
+ /** Collect .css files under a few conventional roots, shallowly, without walking node_modules. */
53
+ function findCssFiles(root, max = 200) {
54
+ const out = [];
55
+ const seen = new Set();
56
+ const walk = (dir, depth) => {
57
+ if (out.length >= max || depth > 4) return;
58
+ let entries;
59
+ try {
60
+ entries = readdirSync(dir, { withFileTypes: true });
61
+ } catch {
62
+ return;
63
+ }
64
+ for (const e of entries) {
65
+ if (out.length >= max) return;
66
+ if (e.name === "node_modules" || e.name.startsWith(".")) continue;
67
+ const full = join(dir, e.name);
68
+ if (seen.has(full)) continue;
69
+ seen.add(full);
70
+ if (e.isDirectory()) walk(full, depth + 1);
71
+ else if (e.name.endsWith(".css")) out.push(full);
72
+ }
73
+ };
74
+ for (const d of CSS_SEARCH_DIRS) {
75
+ const full = join(root, d);
76
+ if (existsSync(full) && statSync(full).isDirectory()) walk(full, 0);
77
+ }
78
+ return out;
79
+ }
80
+
81
+ /** CSS comments frequently *mention* the thing we are looking for — strip them before matching. */
82
+ function stripCssComments(src) {
83
+ return src.replace(/\/\*[\s\S]*?\*\//g, "");
84
+ }
85
+
86
+ /** Nearest ancestor of `from` (inclusive) that has a package.json, bounded by `root`. */
87
+ function nearestPackageRoot(from, root) {
88
+ let dir = from;
89
+ for (let i = 0; i < 8; i += 1) {
90
+ if (existsSync(join(dir, "package.json"))) return dir;
91
+ if (dir === root) break;
92
+ const parent = dirname(dir);
93
+ if (parent === dir) break;
94
+ dir = parent;
95
+ }
96
+ return root;
97
+ }
98
+
99
+ function readIfExists(path) {
100
+ try {
101
+ return readFileSync(path, "utf8");
102
+ } catch {
103
+ return null;
104
+ }
105
+ }
106
+
107
+ function readJson(path) {
108
+ const raw = readIfExists(path);
109
+ if (raw == null) return null;
110
+ try {
111
+ return JSON.parse(raw);
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
116
+
117
+ export function main(argv = []) {
118
+ if (argv.includes("-h") || argv.includes("--help")) {
119
+ console.log(USAGE);
120
+ return 0;
121
+ }
122
+ const dirFlag = argv.indexOf("--dir");
123
+ if (dirFlag !== -1 && argv[dirFlag + 1] == null) {
124
+ console.error("doctor: --dir requires a path\n");
125
+ console.error(USAGE);
126
+ return 2;
127
+ }
128
+ const root = dirFlag === -1 ? process.cwd() : argv[dirFlag + 1];
129
+
130
+ if (!existsSync(root)) {
131
+ console.error(`doctor: no such directory: ${root}`);
132
+ return 2;
133
+ }
134
+
135
+ const results = [];
136
+ const ok = (name, detail) => results.push({ level: "ok", name, detail });
137
+ const warn = (name, detail, fix) =>
138
+ results.push({ level: "warn", name, detail, fix });
139
+ const fail = (name, detail, fix) =>
140
+ results.push({ level: "fail", name, detail, fix });
141
+
142
+ // ---- 1. the design system is installed -------------------------------------------------
143
+ const pkg = readJson(join(root, "package.json"));
144
+ const deps = {
145
+ ...(pkg?.dependencies ?? {}),
146
+ ...(pkg?.devDependencies ?? {}),
147
+ };
148
+ const designInstalled =
149
+ "@vegastack/design" in deps ||
150
+ existsSync(join(root, "node_modules", "@vegastack", "design"));
151
+
152
+ if (designInstalled) {
153
+ const installed = readJson(
154
+ join(root, "node_modules", "@vegastack", "design", "package.json"),
155
+ );
156
+ ok(
157
+ "@vegastack/design installed",
158
+ installed?.version
159
+ ? `v${installed.version}`
160
+ : (deps["@vegastack/design"] ?? ""),
161
+ );
162
+ } else {
163
+ fail(
164
+ "@vegastack/design installed",
165
+ "not found in package.json or node_modules",
166
+ "pnpm add @vegastack/design",
167
+ );
168
+ }
169
+
170
+ // ---- 2. the preset is imported ----------------------------------------------------------
171
+ const cssFiles = findCssFiles(root);
172
+ const presetFiles = cssFiles.filter((f) =>
173
+ (readIfExists(f) ?? "").includes("@vegastack/design/preset.css"),
174
+ );
175
+
176
+ if (presetFiles.length > 0) {
177
+ ok(
178
+ "preset.css imported",
179
+ presetFiles.map((f) => relative(root, f)).join(", "),
180
+ );
181
+ } else {
182
+ fail(
183
+ "preset.css imported",
184
+ cssFiles.length === 0
185
+ ? "no .css files found under app/, src/, or styles/"
186
+ : `none of ${cssFiles.length} .css file(s) import it`,
187
+ 'add `@import "@vegastack/design/preset.css";` to your global stylesheet',
188
+ );
189
+ }
190
+
191
+ // ---- 3. THE BIG ONE: the Tailwind PostCSS plugin ----------------------------------------
192
+ // Without it Tailwind never runs: no utilities are generated, and depending on the bundler
193
+ // you either get a misleading `Can't resolve 'tw-animate-css'` or a silently unstyled app.
194
+ // In a workspace the config correctly lives in the APP package, not the repo root, so search
195
+ // every package that owns a preset-importing stylesheet as well as the root itself.
196
+ const postcssRoots = [
197
+ root,
198
+ ...presetFiles.map((f) => nearestPackageRoot(dirname(f), root)),
199
+ ].filter((d, i, a) => a.indexOf(d) === i);
200
+ const postcssPath = postcssRoots
201
+ .flatMap((d) => POSTCSS_CONFIGS.map((n) => join(d, n)))
202
+ .find(existsSync);
203
+ const postcssOwnerPkg = postcssPath
204
+ ? readJson(join(dirname(postcssPath), "package.json"))
205
+ : null;
206
+ const postcssInline = (postcssOwnerPkg ?? pkg)?.postcss
207
+ ? JSON.stringify((postcssOwnerPkg ?? pkg).postcss)
208
+ : null;
209
+ const postcssSource = postcssPath ? readIfExists(postcssPath) : postcssInline;
210
+ const hasPlugin =
211
+ postcssSource != null && postcssSource.includes("@tailwindcss/postcss");
212
+
213
+ if (hasPlugin) {
214
+ ok(
215
+ "Tailwind PostCSS plugin",
216
+ postcssPath ? relative(root, postcssPath) : "package.json#postcss",
217
+ );
218
+ } else if (postcssSource != null) {
219
+ fail(
220
+ "Tailwind PostCSS plugin",
221
+ `${postcssPath ? relative(root, postcssPath) : "package.json#postcss"} exists but does not configure @tailwindcss/postcss`,
222
+ 'add `"@tailwindcss/postcss": {}` to its plugins',
223
+ );
224
+ } else {
225
+ fail(
226
+ "Tailwind PostCSS plugin",
227
+ "no PostCSS config found — Tailwind will not run, so NO utility classes are generated",
228
+ 'pnpm add -D @tailwindcss/postcss, then create postcss.config.mjs:\n const config = { plugins: { "@tailwindcss/postcss": {} } };\n export default config;',
229
+ );
230
+ }
231
+
232
+ // ---- 4. no duplicate Tailwind import ----------------------------------------------------
233
+ // preset.css already imports Tailwind; a second bare import is the documented cause of
234
+ // "utilities exist but everything is unstyled".
235
+ const duplicateTailwind = presetFiles.filter((f) => {
236
+ const src = stripCssComments(readIfExists(f) ?? "");
237
+ return /@import\s+["']tailwindcss["']/.test(src);
238
+ });
239
+ if (duplicateTailwind.length > 0) {
240
+ warn(
241
+ "no duplicate Tailwind import",
242
+ `${duplicateTailwind.map((f) => relative(root, f)).join(", ")} also imports "tailwindcss" directly`,
243
+ "remove it — preset.css imports Tailwind itself",
244
+ );
245
+ } else if (presetFiles.length > 0) {
246
+ ok("no duplicate Tailwind import", "preset.css is the only Tailwind entry");
247
+ }
248
+
249
+ // ---- 5. registry access is configured ---------------------------------------------------
250
+ let componentsJsonPath = join(root, "components.json");
251
+ let componentsJson = readJson(componentsJsonPath);
252
+ if (componentsJson == null) {
253
+ // Walk up: in a workspace the canonical components.json commonly sits at the repo root.
254
+ let dir = root;
255
+ for (let i = 0; i < 5 && componentsJson == null; i += 1) {
256
+ const parent = dirname(dir);
257
+ if (parent === dir) break;
258
+ dir = parent;
259
+ const candidate = join(dir, "components.json");
260
+ if (existsSync(candidate)) {
261
+ componentsJsonPath = candidate;
262
+ componentsJson = readJson(candidate);
263
+ }
264
+ }
265
+ }
266
+ if (componentsJson == null) {
267
+ warn(
268
+ "registry configured",
269
+ "no components.json here or in any parent directory",
270
+ "see the Quickstart — needed before `shadcn add @vegastack/<name>`",
271
+ );
272
+ } else if (componentsJson.registries?.["@vegastack"] == null) {
273
+ fail(
274
+ "registry configured",
275
+ 'components.json has no `registries["@vegastack"]` entry',
276
+ "add the registries block from the Quickstart",
277
+ );
278
+ } else {
279
+ ok(
280
+ "registry configured",
281
+ `${relative(root, componentsJsonPath) || "components.json"} declares @vegastack`,
282
+ );
283
+ }
284
+
285
+ // ---- 6. monorepo hint --------------------------------------------------------------------
286
+ // Source detection is relative to the CSS file, so components living outside the app's tree
287
+ // compile to nothing unless declared. Only worth saying when this actually looks like a workspace.
288
+ const isWorkspace =
289
+ existsSync(join(root, "pnpm-workspace.yaml")) ||
290
+ Array.isArray(pkg?.workspaces) ||
291
+ pkg?.workspaces != null;
292
+ if (isWorkspace && presetFiles.length > 0) {
293
+ const declaresSource = presetFiles.some((f) =>
294
+ (readIfExists(f) ?? "").includes("@source"),
295
+ );
296
+ if (declaresSource) {
297
+ ok("monorepo sources declared", "@source directives present");
298
+ } else {
299
+ warn(
300
+ "monorepo sources declared",
301
+ "workspace detected but no @source directive — components outside this app's tree will compile to nothing",
302
+ 'add e.g. `@source "../../../../packages/ui/src";` next to the preset import',
303
+ );
304
+ }
305
+ }
306
+
307
+ // ---- report -------------------------------------------------------------------------------
308
+ const glyph = { ok: "✓", warn: "!", fail: "✗" };
309
+ console.log("");
310
+ for (const r of results) {
311
+ console.log(
312
+ ` ${glyph[r.level]} ${r.name}${r.detail ? ` — ${r.detail}` : ""}`,
313
+ );
314
+ if (r.fix) console.log(` fix: ${r.fix}`);
315
+ }
316
+
317
+ const failures = results.filter((r) => r.level === "fail").length;
318
+ const warnings = results.filter((r) => r.level === "warn").length;
319
+ console.log("");
320
+ if (failures > 0) {
321
+ console.log(
322
+ ` ${failures} problem(s), ${warnings} warning(s). See https://design.vegastack.com/docs/guides/troubleshooting`,
323
+ );
324
+ return 1;
325
+ }
326
+ console.log(
327
+ warnings > 0
328
+ ? ` setup looks correct (${warnings} warning(s)).`
329
+ : " setup looks correct.",
330
+ );
331
+ return 0;
332
+ }
package/bin/skills.mjs ADDED
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env node
2
+ // vegastack-design skills — install the public VegaStack agent skills into a consuming project.
3
+ //
4
+ // The skills ship inside this package (see `files` in package.json), so this is a pure local
5
+ // copy: no network, no credentials, no registry access. Files are COPIED rather than symlinked
6
+ // because node_modules is ephemeral — a symlink into it breaks on the next clean install.
7
+ //
8
+ // Safety posture, matching the registry verifier:
9
+ // • never overwrite an existing file without --force (it reports what would change instead)
10
+ // • never write THROUGH a symlink at the destination — refuse it, do not follow it
11
+ // • --dry-run performs every check and writes nothing
12
+ import {
13
+ readFileSync,
14
+ readdirSync,
15
+ writeFileSync,
16
+ mkdirSync,
17
+ existsSync,
18
+ lstatSync,
19
+ } from "node:fs";
20
+ import { join, dirname, relative, resolve, sep } from "node:path";
21
+ import { fileURLToPath } from "node:url";
22
+
23
+ const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
24
+ const SKILLS_DIR = join(PKG_ROOT, "skills");
25
+
26
+ const SURFACES = {
27
+ claude: ".claude/skills",
28
+ codex: ".agents/skills",
29
+ };
30
+
31
+ const USAGE = `vegastack-design skills — install the VegaStack agent skills
32
+
33
+ Usage:
34
+ vegastack-design skills install [options] Copy the skills into this project
35
+ vegastack-design skills list Show the skills bundled with this package
36
+
37
+ Options:
38
+ --dir <path> Project root to install into (default: the current directory)
39
+ --claude Only install for Claude Code (.claude/skills)
40
+ --codex Only install for Codex (.agents/skills)
41
+ --force Overwrite files that already exist and differ
42
+ --dry-run Report what would change; write nothing
43
+ -h, --help Show this help
44
+
45
+ With neither --claude nor --codex, both surfaces are installed.`;
46
+
47
+ /** Skill directory names bundled in this package. */
48
+ function bundledSkills() {
49
+ if (!existsSync(SKILLS_DIR)) return [];
50
+ return readdirSync(SKILLS_DIR, { withFileTypes: true })
51
+ .filter(
52
+ (e) =>
53
+ e.isDirectory() && existsSync(join(SKILLS_DIR, e.name, "SKILL.md")),
54
+ )
55
+ .map((e) => e.name)
56
+ .sort();
57
+ }
58
+
59
+ /** Files of one skill, relative to that skill's directory. */
60
+ function skillFiles(skill) {
61
+ const base = join(SKILLS_DIR, skill);
62
+ const out = [];
63
+ for (const entry of readdirSync(base, {
64
+ withFileTypes: true,
65
+ recursive: true,
66
+ })) {
67
+ if (!entry.isFile()) continue;
68
+ const abs = join(entry.parentPath ?? entry.path, entry.name);
69
+ out.push(relative(base, abs).split(sep).join("/"));
70
+ }
71
+ return out.sort();
72
+ }
73
+
74
+ function describe(skill) {
75
+ const src = readFileSync(join(SKILLS_DIR, skill, "SKILL.md"), "utf8");
76
+ const fm = /^---\n([\s\S]*?)\n---/.exec(src);
77
+ const desc = fm && /^description:\s*(.+)$/m.exec(fm[1])?.[1]?.trim();
78
+ if (!desc) return "";
79
+ return desc.length > 100 ? `${desc.slice(0, 99)}…` : desc;
80
+ }
81
+
82
+ function parseArgs(argv) {
83
+ const opts = {
84
+ dir: process.cwd(),
85
+ surfaces: [],
86
+ force: false,
87
+ dryRun: false,
88
+ };
89
+ for (let i = 0; i < argv.length; i++) {
90
+ const arg = argv[i];
91
+ if (arg === "--dir") {
92
+ const value = argv[++i];
93
+ if (!value) throw new Error("--dir needs a path");
94
+ opts.dir = resolve(value);
95
+ } else if (arg === "--claude") opts.surfaces.push("claude");
96
+ else if (arg === "--codex") opts.surfaces.push("codex");
97
+ else if (arg === "--force") opts.force = true;
98
+ else if (arg === "--dry-run") opts.dryRun = true;
99
+ else throw new Error(`unknown option: ${arg}`);
100
+ }
101
+ if (opts.surfaces.length === 0) opts.surfaces = ["claude", "codex"];
102
+ return opts;
103
+ }
104
+
105
+ function list() {
106
+ const skills = bundledSkills();
107
+ if (skills.length === 0) {
108
+ console.error("no skills are bundled with this build of @vegastack/design");
109
+ return 1;
110
+ }
111
+ console.log(`${skills.length} skill(s) bundled with @vegastack/design:\n`);
112
+ for (const skill of skills)
113
+ console.log(` ${skill}\n ${describe(skill)}\n`);
114
+ console.log("Install with: vegastack-design skills install");
115
+ return 0;
116
+ }
117
+
118
+ function install(argv) {
119
+ let opts;
120
+ try {
121
+ opts = parseArgs(argv);
122
+ } catch (error) {
123
+ console.error(`${error.message}\n`);
124
+ console.error(USAGE);
125
+ return 2;
126
+ }
127
+
128
+ const skills = bundledSkills();
129
+ if (skills.length === 0) {
130
+ console.error("no skills are bundled with this build of @vegastack/design");
131
+ return 1;
132
+ }
133
+ if (!existsSync(opts.dir)) {
134
+ console.error(`target directory does not exist: ${opts.dir}`);
135
+ return 1;
136
+ }
137
+
138
+ const planned = []; // { to, from, action }
139
+ const blocked = [];
140
+ const conflicts = [];
141
+
142
+ for (const surface of opts.surfaces) {
143
+ for (const skill of skills) {
144
+ for (const file of skillFiles(skill)) {
145
+ const from = join(SKILLS_DIR, skill, file);
146
+ const to = join(opts.dir, SURFACES[surface], skill, file);
147
+
148
+ // Refuse to write through a symlink anywhere on the destination path we own.
149
+ let symlinked = false;
150
+ for (
151
+ let probe = to;
152
+ probe.startsWith(join(opts.dir, SURFACES[surface].split("/")[0]));
153
+ probe = dirname(probe)
154
+ ) {
155
+ try {
156
+ if (lstatSync(probe).isSymbolicLink()) {
157
+ symlinked = true;
158
+ break;
159
+ }
160
+ } catch {
161
+ /* does not exist yet — fine */
162
+ }
163
+ }
164
+ if (symlinked) {
165
+ blocked.push(relative(opts.dir, to));
166
+ continue;
167
+ }
168
+
169
+ if (existsSync(to)) {
170
+ if (readFileSync(to).equals(readFileSync(from))) continue; // already correct
171
+ if (!opts.force) {
172
+ conflicts.push(relative(opts.dir, to));
173
+ continue;
174
+ }
175
+ planned.push({ from, to, action: "overwrite" });
176
+ } else {
177
+ planned.push({ from, to, action: "write" });
178
+ }
179
+ }
180
+ }
181
+ }
182
+
183
+ if (blocked.length) {
184
+ console.error("refusing to write through a symlink:");
185
+ for (const path of blocked) console.error(` ${path}`);
186
+ console.error("\nRemove or relocate these entries, then run again.");
187
+ return 1;
188
+ }
189
+
190
+ if (conflicts.length) {
191
+ console.error("these files already exist and differ — not overwriting:");
192
+ for (const path of conflicts) console.error(` ${path}`);
193
+ console.error(
194
+ "\nRe-run with --force to overwrite, or move your versions aside first.",
195
+ );
196
+ return 1;
197
+ }
198
+
199
+ if (planned.length === 0) {
200
+ console.log(`✓ skills already up to date in ${opts.dir}`);
201
+ return 0;
202
+ }
203
+
204
+ if (opts.dryRun) {
205
+ console.log(`Would write ${planned.length} file(s) into ${opts.dir}:\n`);
206
+ for (const { to, action } of planned)
207
+ console.log(` ${action} ${relative(opts.dir, to)}`);
208
+ console.log("\n(dry run — nothing was written)");
209
+ return 0;
210
+ }
211
+
212
+ // A read-only checkout, a permissions problem, or a full disk must fail with something a human
213
+ // can act on — not an unhandled stack trace out of a CLI a consumer just installed.
214
+ const written = [];
215
+ for (const { from, to } of planned) {
216
+ try {
217
+ mkdirSync(dirname(to), { recursive: true });
218
+ writeFileSync(to, readFileSync(from));
219
+ written.push(to);
220
+ } catch (error) {
221
+ console.error(
222
+ `failed to write ${relative(opts.dir, to)}: ${error.code ?? ""} ${error.message}`,
223
+ );
224
+ if (written.length) {
225
+ console.error(
226
+ `\n${written.length} file(s) were already written before this failed:`,
227
+ );
228
+ for (const path of written)
229
+ console.error(` ${relative(opts.dir, path)}`);
230
+ console.error(
231
+ "Re-run once the cause is fixed; the command is idempotent.",
232
+ );
233
+ }
234
+ return 1;
235
+ }
236
+ }
237
+
238
+ const surfaceLabel = opts.surfaces.map((s) => SURFACES[s]).join(" and ");
239
+ console.log(`✓ installed ${skills.length} skill(s) into ${surfaceLabel}`);
240
+ for (const skill of skills) console.log(` ${skill}`);
241
+ console.log(
242
+ "\nRestart your agent if it was already running, so it picks up the new directory.",
243
+ );
244
+ return 0;
245
+ }
246
+
247
+ export function main(argv) {
248
+ const [sub, ...rest] = argv;
249
+ if (sub === "--help" || sub === "-h" || sub === "help" || sub == null) {
250
+ console.log(USAGE);
251
+ return 0;
252
+ }
253
+ if (sub === "list") return list();
254
+ if (sub === "install") return install(rest);
255
+ console.error(`unknown skills subcommand: ${sub}\n`);
256
+ console.error(USAGE);
257
+ return 2;
258
+ }
259
+
260
+ if (import.meta.url === `file://${process.argv[1]}`) {
261
+ process.exit(main(process.argv.slice(2)));
262
+ }
@@ -4,15 +4,17 @@
4
4
  // Subcommands:
5
5
  // check-updates Show which copied-in components have newer registry versions (what to re-pull).
6
6
  // verify Verify a registry item's integrity before/after `shadcn add` (Sigstore + hash).
7
+ // skills Install the bundled VegaStack agent skills into the consuming project.
8
+ // doctor Check a consuming project's setup (PostCSS plugin, preset import, registry).
7
9
  //
8
10
  // The bin is named `vegastack-design` (NOT `vegastack`) so it never collides with a platform CLI.
9
11
  // `check-updates` is imported in-process; `verify` is spawned (it's the standalone, hash-parity-tested
10
12
  // verifier — we run it untouched). Exit codes are forwarded from the subcommand.
11
- import { spawnSync } from 'node:child_process';
12
- import { readFileSync } from 'node:fs';
13
- import { fileURLToPath } from 'node:url';
13
+ import { spawnSync } from "node:child_process";
14
+ import { readFileSync } from "node:fs";
15
+ import { fileURLToPath } from "node:url";
14
16
 
15
- const HERE = new URL('.', import.meta.url);
17
+ const HERE = new URL(".", import.meta.url);
16
18
 
17
19
  const USAGE = `vegastack-design — VegaStack design-system CLI
18
20
 
@@ -21,6 +23,8 @@ Usage: vegastack-design <command> [options]
21
23
  Commands:
22
24
  check-updates Show which copied-in components have newer registry versions
23
25
  verify Verify a registry item's integrity (pre/post \`shadcn add\`)
26
+ skills Install the VegaStack agent skills (Claude Code + Codex)
27
+ doctor Check this project's setup and report what is wrong
24
28
 
25
29
  Run \`vegastack-design <command> --help\` for command options.
26
30
  -v, --version Print version
@@ -28,39 +32,61 @@ Run \`vegastack-design <command> --help\` for command options.
28
32
 
29
33
  function version() {
30
34
  try {
31
- return JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
35
+ return JSON.parse(
36
+ readFileSync(new URL("../package.json", import.meta.url), "utf8"),
37
+ ).version;
32
38
  } catch {
33
- return '0.0.0';
39
+ return "0.0.0";
34
40
  }
35
41
  }
36
42
 
37
43
  const [cmd, ...rest] = process.argv.slice(2);
38
44
 
39
- if (cmd === '--version' || cmd === '-v') {
45
+ if (cmd === "--version" || cmd === "-v") {
40
46
  console.log(version());
41
47
  process.exit(0);
42
48
  }
43
- if (cmd == null || cmd === '--help' || cmd === '-h' || cmd === 'help') {
49
+ if (cmd == null || cmd === "--help" || cmd === "-h" || cmd === "help") {
44
50
  console.log(USAGE);
45
51
  process.exit(0);
46
52
  }
47
53
 
48
- if (cmd === 'check-updates') {
54
+ if (cmd === "check-updates") {
49
55
  // imported in-process (it's our own code with an exported main())
50
- const { main } = await import(new URL('./check-updates.mjs', HERE).href);
56
+ const { main } = await import(new URL("./check-updates.mjs", HERE).href);
51
57
  process.exit(await main(rest));
52
58
  }
53
59
 
54
- if (cmd === 'verify') {
60
+ if (cmd === "skills") {
61
+ // imported in-process (our own code, no network, no credentials)
62
+ const { main } = await import(new URL("./skills.mjs", HERE).href);
63
+ process.exit(main(rest));
64
+ }
65
+
66
+ if (cmd === "doctor") {
67
+ // imported in-process (our own code, read-only, no network, no credentials)
68
+ const { main } = await import(new URL("./doctor.mjs", HERE).href);
69
+ process.exit(main(rest));
70
+ }
71
+
72
+ if (cmd === "verify") {
55
73
  // spawn the standalone verifier untouched; mark the dispatch so it skips its deprecation notice.
56
- const verifier = fileURLToPath(new URL('./verify-registry-item.mjs', HERE));
74
+ const verifier = fileURLToPath(new URL("./verify-registry-item.mjs", HERE));
57
75
  const r = spawnSync(process.execPath, [verifier, ...rest], {
58
- stdio: 'inherit',
59
- env: { ...process.env, VEGASTACK_DESIGN_DISPATCH: '1' },
76
+ stdio: "inherit",
77
+ env: { ...process.env, VEGASTACK_DESIGN_DISPATCH: "1" },
60
78
  });
61
79
  process.exit(r.status ?? 1);
62
80
  }
63
81
 
82
+ // Name the installed version: the most common cause of "unknown command" is a consumer following
83
+ // documentation for a newer release than the one they actually have installed.
64
84
  console.error(`unknown command: ${cmd}\n`);
85
+ console.error(
86
+ `(this is @vegastack/design@${version()} — if you expected "${cmd}", check whether it`,
87
+ );
88
+ console.error(
89
+ `requires a newer version: npm view @vegastack/design version)\n`,
90
+ );
65
91
  console.error(USAGE);
66
92
  process.exit(2);