@devfellowship/components 1.2.1 → 1.2.3

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/dist/cli.js ADDED
@@ -0,0 +1,737 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/cli/commands/init.ts
7
+ import fs from "fs-extra";
8
+ import path from "path";
9
+ import chalk2 from "chalk";
10
+ import ora from "ora";
11
+ import prompts from "prompts";
12
+
13
+ // src/cli/types/config.ts
14
+ import { z } from "zod";
15
+ var configSchema = z.object({
16
+ typescript: z.boolean().default(true),
17
+ aliases: z.object({
18
+ components: z.string().default("@/components/dfl"),
19
+ hooks: z.string().default("@/hooks"),
20
+ providers: z.string().default("@/providers"),
21
+ pages: z.string().default("@/pages")
22
+ }),
23
+ registry: z.string().default("https://raw.githubusercontent.com/taigfs/dfl-components-cli/main/registry")
24
+ });
25
+
26
+ // src/cli/utils/logger.ts
27
+ import chalk from "chalk";
28
+ var logger = {
29
+ info: (message) => console.log(chalk.blue("info"), message),
30
+ success: (message) => console.log(chalk.green("\u2713"), message),
31
+ warn: (message) => console.log(chalk.yellow("warn"), message),
32
+ error: (message) => console.log(chalk.red("error"), message),
33
+ break: () => console.log("")
34
+ };
35
+
36
+ // src/cli/commands/init.ts
37
+ async function init(options) {
38
+ const cwd = path.resolve(options.cwd);
39
+ logger.info("Initializing dfl-components configuration...");
40
+ logger.break();
41
+ const configPath = path.join(cwd, "dfl-components.json");
42
+ if (await fs.pathExists(configPath)) {
43
+ const { overwrite } = await prompts({
44
+ type: "confirm",
45
+ name: "overwrite",
46
+ message: "dfl-components.json already exists. Overwrite?",
47
+ initial: false
48
+ });
49
+ if (!overwrite) {
50
+ logger.warn("Aborted.");
51
+ return;
52
+ }
53
+ }
54
+ let config;
55
+ if (options.yes) {
56
+ config = configSchema.parse({
57
+ aliases: {}
58
+ });
59
+ } else {
60
+ const answers = await prompts([
61
+ {
62
+ type: "confirm",
63
+ name: "typescript",
64
+ message: "Would you like to use TypeScript?",
65
+ initial: true
66
+ },
67
+ {
68
+ type: "text",
69
+ name: "componentsPath",
70
+ message: "Where would you like to install components?",
71
+ initial: "@/components/dfl"
72
+ },
73
+ {
74
+ type: "text",
75
+ name: "hooksPath",
76
+ message: "Where would you like to install hooks?",
77
+ initial: "@/hooks"
78
+ },
79
+ {
80
+ type: "text",
81
+ name: "providersPath",
82
+ message: "Where would you like to install providers?",
83
+ initial: "@/providers"
84
+ },
85
+ {
86
+ type: "text",
87
+ name: "pagesPath",
88
+ message: "Where would you like to install pages?",
89
+ initial: "@/pages"
90
+ }
91
+ ]);
92
+ if (!answers.typescript) {
93
+ logger.warn("Aborted.");
94
+ return;
95
+ }
96
+ config = configSchema.parse({
97
+ typescript: answers.typescript,
98
+ aliases: {
99
+ components: answers.componentsPath,
100
+ hooks: answers.hooksPath,
101
+ providers: answers.providersPath,
102
+ pages: answers.pagesPath
103
+ }
104
+ });
105
+ }
106
+ const spinner = ora("Writing configuration...").start();
107
+ try {
108
+ await fs.writeJson(configPath, config, { spaces: 2 });
109
+ spinner.succeed("Configuration saved to dfl-components.json");
110
+ logger.break();
111
+ logger.success("Project initialized successfully!");
112
+ logger.break();
113
+ console.log("You can now add components:");
114
+ console.log(chalk2.cyan(" npx dfl-components add button"));
115
+ console.log(chalk2.cyan(" npx dfl-components add card input"));
116
+ console.log(chalk2.cyan(" npx dfl-components add auth-pages"));
117
+ logger.break();
118
+ } catch (error) {
119
+ spinner.fail("Failed to write configuration");
120
+ throw error;
121
+ }
122
+ }
123
+
124
+ // src/cli/commands/add.ts
125
+ import fs2 from "fs-extra";
126
+ import path2 from "path";
127
+ import chalk3 from "chalk";
128
+ import ora2 from "ora";
129
+ import prompts2 from "prompts";
130
+
131
+ // src/cli/utils/get-config.ts
132
+ import { cosmiconfig } from "cosmiconfig";
133
+ async function getConfig(cwd) {
134
+ const explorer = cosmiconfig("dfl-components", {
135
+ searchPlaces: ["dfl-components.json"]
136
+ });
137
+ const result = await explorer.search(cwd);
138
+ if (!result) {
139
+ return null;
140
+ }
141
+ return configSchema.parse(result.config);
142
+ }
143
+
144
+ // src/cli/utils/get-registry.ts
145
+ import fetch2 from "node-fetch";
146
+ async function getRegistry(registryUrl) {
147
+ const response = await fetch2(`${registryUrl}/registry.json`);
148
+ if (!response.ok) {
149
+ throw new Error(`Failed to fetch registry: ${response.statusText}`);
150
+ }
151
+ return response.json();
152
+ }
153
+ function getComponentFolderByCategory(category) {
154
+ switch (category) {
155
+ case "Hooks":
156
+ return "hooks";
157
+ case "Providers":
158
+ return "providers";
159
+ case "Pages":
160
+ return "pages";
161
+ default:
162
+ return "components";
163
+ }
164
+ }
165
+
166
+ // src/cli/utils/resolve-alias.ts
167
+ function resolveAlias(aliasPath) {
168
+ if (aliasPath.startsWith("@/")) {
169
+ return aliasPath.replace("@/", "src/");
170
+ }
171
+ return aliasPath;
172
+ }
173
+ function getTargetPath(category, aliases) {
174
+ switch (category) {
175
+ case "Hooks":
176
+ return resolveAlias(aliases.hooks);
177
+ case "Providers":
178
+ return resolveAlias(aliases.providers);
179
+ case "Pages":
180
+ return resolveAlias(aliases.pages);
181
+ default:
182
+ return resolveAlias(aliases.components);
183
+ }
184
+ }
185
+
186
+ // src/cli/commands/add.ts
187
+ async function add(components, options) {
188
+ const cwd = path2.resolve(options.cwd);
189
+ const config = await getConfig(cwd);
190
+ if (!config) {
191
+ logger.error("No dfl-components.json found.");
192
+ console.log("Run", chalk3.cyan("npx dfl-components init"), "first.");
193
+ return;
194
+ }
195
+ const spinner = ora2("Fetching registry...").start();
196
+ try {
197
+ let resolveDeps2 = function(name) {
198
+ if (allComponents.has(name)) return;
199
+ allComponents.add(name);
200
+ const item = registry.items.find((i) => i.name === name);
201
+ if (item?.registryDependencies) {
202
+ for (const dep of item.registryDependencies) {
203
+ resolveDeps2(dep);
204
+ }
205
+ }
206
+ };
207
+ var resolveDeps = resolveDeps2;
208
+ const registry = await getRegistry(config.registry);
209
+ let toInstall;
210
+ if (options.all) {
211
+ toInstall = registry.items.map((item) => item.name);
212
+ } else if (components.length === 0) {
213
+ spinner.stop();
214
+ const { selected } = await prompts2({
215
+ type: "multiselect",
216
+ name: "selected",
217
+ message: "Which components would you like to add?",
218
+ choices: registry.items.map((item) => ({
219
+ title: `${item.title} (${item.category})`,
220
+ value: item.name,
221
+ description: item.description
222
+ })),
223
+ min: 1
224
+ });
225
+ if (!selected || selected.length === 0) {
226
+ logger.warn("No components selected.");
227
+ return;
228
+ }
229
+ toInstall = selected;
230
+ spinner.start("Fetching components...");
231
+ } else {
232
+ toInstall = components;
233
+ }
234
+ const validComponents = [];
235
+ for (const name of toInstall) {
236
+ const item = registry.items.find((i) => i.name === name);
237
+ if (!item) {
238
+ logger.warn(`Component "${name}" not found in registry. Skipping.`);
239
+ } else {
240
+ validComponents.push(name);
241
+ }
242
+ }
243
+ if (validComponents.length === 0) {
244
+ spinner.fail("No valid components to install.");
245
+ return;
246
+ }
247
+ const allComponents = /* @__PURE__ */ new Set();
248
+ for (const component of validComponents) {
249
+ resolveDeps2(component);
250
+ }
251
+ spinner.text = `Installing ${allComponents.size} component(s)...`;
252
+ const installed = [];
253
+ const errors = [];
254
+ for (const componentName of allComponents) {
255
+ const registryItem = registry.items.find((i) => i.name === componentName);
256
+ if (!registryItem) continue;
257
+ try {
258
+ const folder = getComponentFolderByCategory(registryItem.category);
259
+ const url = `${config.registry}/${folder}/${componentName}.json`;
260
+ const response = await fetch(url);
261
+ if (!response.ok) {
262
+ throw new Error(`Failed to fetch: ${response.statusText}`);
263
+ }
264
+ const componentData = await response.json();
265
+ for (const file of componentData.files) {
266
+ const targetDir = file.target ? path2.dirname(path2.join(cwd, "src", file.target)) : path2.join(cwd, getTargetPath(registryItem.category, config.aliases));
267
+ const targetPath = file.target ? path2.join(cwd, "src", file.target) : path2.join(targetDir, file.path);
268
+ if (await fs2.pathExists(targetPath)) {
269
+ if (!options.overwrite && !options.yes) {
270
+ spinner.stop();
271
+ const { overwrite } = await prompts2({
272
+ type: "confirm",
273
+ name: "overwrite",
274
+ message: `${path2.relative(cwd, targetPath)} already exists. Overwrite?`,
275
+ initial: false
276
+ });
277
+ if (!overwrite) {
278
+ logger.info(`Skipping ${path2.basename(targetPath)}`);
279
+ continue;
280
+ }
281
+ spinner.start();
282
+ } else if (!options.overwrite) {
283
+ logger.info(`Skipping ${path2.basename(targetPath)} (use --overwrite to replace)`);
284
+ continue;
285
+ }
286
+ }
287
+ await fs2.ensureDir(path2.dirname(targetPath));
288
+ await fs2.writeFile(targetPath, file.content);
289
+ installed.push(targetPath);
290
+ }
291
+ } catch (error) {
292
+ errors.push(`${componentName}: ${error instanceof Error ? error.message : "Unknown error"}`);
293
+ }
294
+ }
295
+ spinner.succeed(`Installed ${installed.length} file(s)`);
296
+ if (installed.length > 0) {
297
+ logger.break();
298
+ console.log(chalk3.green("Files created:"));
299
+ for (const file of installed) {
300
+ console.log(chalk3.gray(" -"), path2.relative(cwd, file));
301
+ }
302
+ }
303
+ if (errors.length > 0) {
304
+ logger.break();
305
+ console.log(chalk3.red("Errors:"));
306
+ for (const error of errors) {
307
+ console.log(chalk3.gray(" -"), error);
308
+ }
309
+ }
310
+ logger.break();
311
+ } catch (error) {
312
+ spinner.fail("Failed to add components");
313
+ throw error;
314
+ }
315
+ }
316
+
317
+ // src/cli/ux-paths/commands/init.ts
318
+ import { mkdirSync, writeFileSync, existsSync } from "fs";
319
+ import { resolve, basename } from "path";
320
+ import chalk4 from "chalk";
321
+ function registerInit(program2) {
322
+ program2.command("init").description("Bootstrap a .dfl-ux-paths/ directory in the current cwd.").option("--app-id <id>", "App identifier (defaults to current dir name).").option("--force", "Overwrite an existing flows.json if present.", false).action((opts) => {
323
+ const cwd = process.cwd();
324
+ const dir = resolve(cwd, ".dfl-ux-paths");
325
+ const flowsPath = resolve(dir, "flows.json");
326
+ const appId = opts.appId || basename(cwd);
327
+ if (existsSync(flowsPath) && !opts.force) {
328
+ console.error(
329
+ chalk4.yellow(`${flowsPath} already exists. Use --force to overwrite.`)
330
+ );
331
+ process.exit(1);
332
+ }
333
+ mkdirSync(dir, { recursive: true });
334
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
335
+ const stub = {
336
+ schema_version: "1.0.0",
337
+ app_id: appId,
338
+ app_version: `${today}-0000000`,
339
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
340
+ tech_stack: [],
341
+ screens: [],
342
+ flows: []
343
+ };
344
+ writeFileSync(flowsPath, JSON.stringify(stub, null, 2) + "\n", "utf8");
345
+ console.log(chalk4.green("Created"), flowsPath);
346
+ console.log(
347
+ chalk4.gray(
348
+ "Next steps: populate screens/flows, then run `dfl-components ux-paths validate` and `dfl-components ux-paths generate-mermaid`."
349
+ )
350
+ );
351
+ });
352
+ }
353
+
354
+ // src/cli/ux-paths/commands/validate.ts
355
+ import { readFileSync, existsSync as existsSync2 } from "fs";
356
+ import { resolve as resolve2 } from "path";
357
+ import Ajv2020 from "ajv/dist/2020.js";
358
+ import addFormats from "ajv-formats";
359
+ import chalk5 from "chalk";
360
+
361
+ // src/cli/ux-paths/lib/load-schema.ts
362
+ var SCHEMA_URL = "https://raw.githubusercontent.com/devfellowship/dfl-ux-paths/main/schema/v1.json";
363
+ var cached = null;
364
+ async function loadSchemaV1() {
365
+ if (cached) return cached;
366
+ let res;
367
+ try {
368
+ res = await fetch(SCHEMA_URL);
369
+ } catch (err) {
370
+ throw new Error(
371
+ `Unable to fetch the UX Paths schema from ${SCHEMA_URL}: ${err.message}`
372
+ );
373
+ }
374
+ if (!res.ok) {
375
+ throw new Error(
376
+ `Unable to fetch the UX Paths schema from ${SCHEMA_URL}: HTTP ${res.status} ${res.statusText}`
377
+ );
378
+ }
379
+ cached = await res.json();
380
+ return cached;
381
+ }
382
+
383
+ // src/cli/ux-paths/commands/validate.ts
384
+ function registerValidate(program2) {
385
+ program2.command("validate [path]").description("Validate a flows.json against the DFL UX Paths v1 schema.").action(async (maybePath) => {
386
+ const path3 = resolve2(process.cwd(), maybePath || ".dfl-ux-paths/flows.json");
387
+ if (!existsSync2(path3)) {
388
+ console.error(chalk5.red("File not found:"), path3);
389
+ process.exit(1);
390
+ }
391
+ let doc;
392
+ try {
393
+ doc = JSON.parse(readFileSync(path3, "utf8"));
394
+ } catch (err) {
395
+ console.error(chalk5.red("Invalid JSON:"), err.message);
396
+ process.exit(1);
397
+ }
398
+ const ajv = new Ajv2020({ allErrors: true, strict: false });
399
+ addFormats(ajv);
400
+ let schema;
401
+ try {
402
+ schema = await loadSchemaV1();
403
+ } catch (err) {
404
+ console.error(chalk5.red("Schema error:"), err.message);
405
+ process.exit(1);
406
+ }
407
+ const validate = ajv.compile(schema);
408
+ const ok = validate(doc);
409
+ if (ok) {
410
+ console.log(chalk5.green("OK"), path3, "conforms to schema v1.");
411
+ process.exit(0);
412
+ }
413
+ console.error(chalk5.red("FAIL"), path3);
414
+ for (const err of validate.errors ?? []) {
415
+ console.error(
416
+ chalk5.yellow(" -"),
417
+ err.instancePath || "<root>",
418
+ err.message,
419
+ err.params ? JSON.stringify(err.params) : ""
420
+ );
421
+ }
422
+ process.exit(1);
423
+ });
424
+ }
425
+
426
+ // src/cli/ux-paths/commands/generate-mermaid.ts
427
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync3 } from "fs";
428
+ import { resolve as resolve3, dirname } from "path";
429
+ import chalk6 from "chalk";
430
+
431
+ // src/cli/ux-paths/lib/json-to-mermaid.ts
432
+ function jsonToMermaid(doc) {
433
+ const lines = [];
434
+ lines.push("graph TD");
435
+ lines.push(` %% app_id: ${doc.app_id}`);
436
+ lines.push(` %% app_version: ${doc.app_version}`);
437
+ lines.push(` %% schema_version: ${doc.schema_version}`);
438
+ if (doc.test_metadata?.base_url) {
439
+ lines.push(` %% base_url: ${doc.test_metadata.base_url}`);
440
+ }
441
+ for (const screen of doc.screens) {
442
+ const lockPrefix = screen.prerequisites?.auth_required ? "\u{1F512} " : "";
443
+ lines.push(
444
+ ` ${sanitizeId(screen.id)}["${escapeLabel(lockPrefix + screen.name)}"]`
445
+ );
446
+ }
447
+ for (const screen of doc.screens) {
448
+ if (screen.navigation_path && screen.navigation_path.length > 0) {
449
+ const summary = screen.navigation_path.map((s) => `${s.action} ${s.selector}`).join(" -> ");
450
+ lines.push(` %% nav[${sanitizeId(screen.id)}]: ${summary}`);
451
+ }
452
+ }
453
+ for (const screen of doc.screens) {
454
+ for (const action of screen.actions ?? []) {
455
+ if (action.next_screen) {
456
+ lines.push(
457
+ ` ${sanitizeId(screen.id)} -->|${escapeLabel(action.label)}| ${sanitizeId(
458
+ action.next_screen
459
+ )}`
460
+ );
461
+ }
462
+ }
463
+ }
464
+ for (const flow of doc.flows) {
465
+ const flowKey = sanitizeId(`flow_${flow.name}`);
466
+ lines.push(` subgraph ${flowKey}["Flow: ${escapeLabel(flow.name)}"]`);
467
+ const nodes = flowStepsToNodes(flow.start, flow.steps);
468
+ for (let i = 0; i < nodes.length - 1; i++) {
469
+ const from = nodes[i];
470
+ const to = nodes[i + 1];
471
+ if (to.label) {
472
+ lines.push(
473
+ ` ${sanitizeId(from.id)} -.->|${escapeLabel(to.label)}| ${sanitizeId(to.id)}`
474
+ );
475
+ } else {
476
+ lines.push(` ${sanitizeId(from.id)} -.-> ${sanitizeId(to.id)}`);
477
+ }
478
+ }
479
+ if (nodes.length === 1) {
480
+ lines.push(` ${sanitizeId(nodes[0].id)}`);
481
+ }
482
+ lines.push(" end");
483
+ }
484
+ if (doc.dead_code && doc.dead_code.length > 0) {
485
+ lines.push(" %% --- dead code ---");
486
+ for (const entry of doc.dead_code) {
487
+ lines.push(` %% dead: ${entry.component} \u2014 ${entry.reason}`);
488
+ }
489
+ }
490
+ return lines.join("\n") + "\n";
491
+ }
492
+ function flowStepsToNodes(start, steps) {
493
+ const out = [{ id: start }];
494
+ for (const step of steps) {
495
+ if (typeof step === "string") {
496
+ out.push({ id: step });
497
+ } else {
498
+ const id = step.target_screen ?? step.screen;
499
+ const labelParts = [];
500
+ if (step.action) labelParts.push(step.action);
501
+ if (step.selector) labelParts.push(step.selector);
502
+ const label = labelParts.length > 0 ? labelParts.join(": ") : void 0;
503
+ out.push({ id, label });
504
+ }
505
+ }
506
+ return out;
507
+ }
508
+ function sanitizeId(raw) {
509
+ return raw.replace(/[^A-Za-z0-9_]/g, "_");
510
+ }
511
+ function escapeLabel(raw) {
512
+ return raw.replace(/"/g, '\\"');
513
+ }
514
+
515
+ // src/cli/ux-paths/commands/generate-mermaid.ts
516
+ var HEADER = "%% AUTO-GENERATED by dfl-components ux-paths \u2014 do not edit by hand.\n";
517
+ function registerGenerateMermaid(program2) {
518
+ program2.command("generate-mermaid [path]").description("Generate a Mermaid graph from a flows.json file.").option("-o, --out <file>", "Output path (defaults to <dir>/flows.mmd).").action((maybePath, opts) => {
519
+ const path3 = resolve3(process.cwd(), maybePath || ".dfl-ux-paths/flows.json");
520
+ if (!existsSync3(path3)) {
521
+ console.error(chalk6.red("File not found:"), path3);
522
+ process.exit(1);
523
+ }
524
+ const doc = JSON.parse(readFileSync2(path3, "utf8"));
525
+ const out = opts.out ? resolve3(process.cwd(), opts.out) : resolve3(dirname(path3), "flows.mmd");
526
+ const body = HEADER + jsonToMermaid(doc);
527
+ writeFileSync2(out, body, "utf8");
528
+ console.log(chalk6.green("Wrote"), out);
529
+ });
530
+ }
531
+
532
+ // src/cli/ux-paths/commands/diff.ts
533
+ import { readFileSync as readFileSync3, existsSync as existsSync4 } from "fs";
534
+ import { resolve as resolve4 } from "path";
535
+ import chalk7 from "chalk";
536
+
537
+ // src/cli/ux-paths/lib/flows-diff.ts
538
+ function flowsDiff(a, b) {
539
+ const aScreenIds = new Set(a.screens.map((s) => s.id));
540
+ const bScreenIds = new Set(b.screens.map((s) => s.id));
541
+ const screensAdded = [...bScreenIds].filter((id) => !aScreenIds.has(id)).sort();
542
+ const screensRemoved = [...aScreenIds].filter((id) => !bScreenIds.has(id)).sort();
543
+ const screensCommon = [...aScreenIds].filter((id) => bScreenIds.has(id)).sort();
544
+ const aActionKeys = collectActionKeys(a);
545
+ const bActionKeys = collectActionKeys(b);
546
+ const actionsAdded = [...bActionKeys].filter((k) => !aActionKeys.has(k)).sort();
547
+ const actionsRemoved = [...aActionKeys].filter((k) => !bActionKeys.has(k)).sort();
548
+ const aFlowsByName = new Map(a.flows.map((f) => [f.name, f]));
549
+ const bFlowsByName = new Map(b.flows.map((f) => [f.name, f]));
550
+ const flowsAdded = [...bFlowsByName.keys()].filter((n) => !aFlowsByName.has(n)).sort();
551
+ const flowsRemoved = [...aFlowsByName.keys()].filter((n) => !bFlowsByName.has(n)).sort();
552
+ const flowsChanged = [];
553
+ for (const name of [...aFlowsByName.keys()].sort()) {
554
+ const aFlow = aFlowsByName.get(name);
555
+ const bFlow = bFlowsByName.get(name);
556
+ if (!bFlow) continue;
557
+ const reasons = [];
558
+ if (aFlow.start !== bFlow.start) {
559
+ reasons.push(`start: ${aFlow.start} \u2192 ${bFlow.start}`);
560
+ }
561
+ if (!stepArraysEqual(aFlow.steps, bFlow.steps)) {
562
+ reasons.push(`steps changed (${aFlow.steps.length} \u2192 ${bFlow.steps.length})`);
563
+ }
564
+ if (!arraysEqual(aFlow.actions ?? [], bFlow.actions ?? [])) {
565
+ reasons.push("actions changed");
566
+ }
567
+ if (reasons.length > 0) {
568
+ flowsChanged.push({ name, reason: reasons.join("; ") });
569
+ }
570
+ }
571
+ return {
572
+ screens: { added: screensAdded, removed: screensRemoved, common: screensCommon },
573
+ actions: { added: actionsAdded, removed: actionsRemoved },
574
+ flows: { added: flowsAdded, removed: flowsRemoved, changed: flowsChanged }
575
+ };
576
+ }
577
+ function collectActionKeys(doc) {
578
+ const out = /* @__PURE__ */ new Set();
579
+ for (const screen of doc.screens) {
580
+ for (const action of screen.actions ?? []) {
581
+ out.add(`${screen.id}.${action.id}`);
582
+ }
583
+ }
584
+ return out;
585
+ }
586
+ function arraysEqual(a, b) {
587
+ if (a.length !== b.length) return false;
588
+ for (let i = 0; i < a.length; i++) {
589
+ if (a[i] !== b[i]) return false;
590
+ }
591
+ return true;
592
+ }
593
+ function stepArraysEqual(a, b) {
594
+ if (a.length !== b.length) return false;
595
+ for (let i = 0; i < a.length; i++) {
596
+ if (stepKey(a[i]) !== stepKey(b[i])) return false;
597
+ }
598
+ return true;
599
+ }
600
+ function stepKey(step) {
601
+ if (typeof step === "string") return step;
602
+ return step.target_screen ?? step.screen;
603
+ }
604
+
605
+ // src/cli/ux-paths/commands/diff.ts
606
+ function registerDiff(program2) {
607
+ program2.command("diff <a> <b>").description("Diff screens/actions/flows between two flows.json files (a = baseline).").option("--json", "Emit machine-readable JSON instead of human-readable output.", false).action((aPath, bPath, opts) => {
608
+ const a = loadDoc(aPath);
609
+ const b = loadDoc(bPath);
610
+ const result = flowsDiff(a, b);
611
+ if (opts.json) {
612
+ console.log(JSON.stringify(result, null, 2));
613
+ return;
614
+ }
615
+ console.log(chalk7.bold(`Diff ${a.app_id}@${a.app_version} \u2192 ${b.app_id}@${b.app_version}`));
616
+ printSection("Screens added", result.screens.added, chalk7.green);
617
+ printSection("Screens removed", result.screens.removed, chalk7.red);
618
+ printSection("Actions added", result.actions.added, chalk7.green);
619
+ printSection("Actions removed", result.actions.removed, chalk7.red);
620
+ printSection("Flows added", result.flows.added, chalk7.green);
621
+ printSection("Flows removed", result.flows.removed, chalk7.red);
622
+ if (result.flows.changed.length > 0) {
623
+ console.log(chalk7.yellow("\nFlows changed:"));
624
+ for (const ch of result.flows.changed) {
625
+ console.log(` - ${ch.name}: ${ch.reason}`);
626
+ }
627
+ }
628
+ });
629
+ }
630
+ function loadDoc(p) {
631
+ const path3 = resolve4(process.cwd(), p);
632
+ if (!existsSync4(path3)) {
633
+ console.error(chalk7.red("File not found:"), path3);
634
+ process.exit(1);
635
+ }
636
+ return JSON.parse(readFileSync3(path3, "utf8"));
637
+ }
638
+ function printSection(label, items, color) {
639
+ if (items.length === 0) return;
640
+ console.log(color(`
641
+ ${label} (${items.length}):`));
642
+ for (const item of items) {
643
+ console.log(` - ${item}`);
644
+ }
645
+ }
646
+
647
+ // src/cli/ux-paths/commands/stamp.ts
648
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync5 } from "fs";
649
+ import { resolve as resolve5 } from "path";
650
+ import { execSync } from "child_process";
651
+ import chalk8 from "chalk";
652
+
653
+ // src/cli/ux-paths/lib/preserve-format.ts
654
+ function replaceTopLevelStringField(raw, key, newValue) {
655
+ const re = new RegExp(`("${escapeRegExp(key)}"\\s*:\\s*")((?:[^"\\\\]|\\\\.)*)(")`);
656
+ if (!re.test(raw)) return raw;
657
+ return raw.replace(re, (_m, pre, _old, post) => {
658
+ return pre + escapeJsonStringValue(newValue) + post;
659
+ });
660
+ }
661
+ function stampPreserveFormat(raw, fields) {
662
+ let out = raw;
663
+ out = replaceTopLevelStringField(out, "app_version", fields.app_version);
664
+ out = replaceTopLevelStringField(out, "generated_at", fields.generated_at);
665
+ return out;
666
+ }
667
+ function escapeRegExp(s) {
668
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
669
+ }
670
+ function escapeJsonStringValue(s) {
671
+ const json = JSON.stringify(s);
672
+ return json.slice(1, -1);
673
+ }
674
+
675
+ // src/cli/ux-paths/commands/stamp.ts
676
+ function registerStamp(program2) {
677
+ program2.command("stamp [path]").description("Refresh app_version to YYYY-MM-DD-<git-sha-short> and bump generated_at.").option(
678
+ "--no-preserve-format",
679
+ "Reformat the whole file (legacy). By default, stamp surgically updates only app_version/generated_at, preserving the existing formatting (minimal diff)."
680
+ ).action((maybePath, opts) => {
681
+ const path3 = resolve5(process.cwd(), maybePath || ".dfl-ux-paths/flows.json");
682
+ if (!existsSync5(path3)) {
683
+ console.error(chalk8.red("File not found:"), path3);
684
+ process.exit(1);
685
+ }
686
+ const raw = readFileSync4(path3, "utf8");
687
+ const doc = JSON.parse(raw);
688
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
689
+ let sha = "0000000";
690
+ try {
691
+ sha = execSync("git rev-parse --short=7 HEAD", { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
692
+ if (!sha) sha = "0000000";
693
+ } catch {
694
+ }
695
+ const newVersion = `${today}-${sha}`;
696
+ const newGeneratedAt = (/* @__PURE__ */ new Date()).toISOString();
697
+ const preserve = opts.preserveFormat !== false;
698
+ let output;
699
+ if (preserve) {
700
+ output = stampPreserveFormat(raw, {
701
+ app_version: newVersion,
702
+ generated_at: newGeneratedAt
703
+ });
704
+ } else {
705
+ const updated = {
706
+ ...doc,
707
+ app_version: newVersion,
708
+ generated_at: newGeneratedAt
709
+ };
710
+ output = JSON.stringify(updated, null, 2) + "\n";
711
+ }
712
+ writeFileSync3(path3, output, "utf8");
713
+ console.log(chalk8.green("Stamped"), path3, preserve ? chalk8.gray("(minimal diff)") : "");
714
+ console.log(chalk8.gray(` app_version: ${newVersion}`));
715
+ console.log(chalk8.gray(` generated_at: ${newGeneratedAt}`));
716
+ });
717
+ }
718
+
719
+ // src/cli/ux-paths/index.ts
720
+ function registerUxPaths(program2) {
721
+ const group = program2.command("ux-paths").description("Versioned, schema-validated per-app user-flow mapping (DFL UX Paths).");
722
+ registerInit(group);
723
+ registerValidate(group);
724
+ registerGenerateMermaid(group);
725
+ registerDiff(group);
726
+ registerStamp(group);
727
+ }
728
+
729
+ // src/cli/index.ts
730
+ var program = new Command();
731
+ program.name("dfl-components").description(
732
+ "DevFellowship components CLI \u2014 add shared components AND map app UX paths (folds the dfl-ux-paths CLI). Shipped as the `dfl-components` bin of @devfellowship/components."
733
+ ).version("1.0.0");
734
+ program.command("init").description("Initialize your project with dfl-components configuration").option("-y, --yes", "Skip prompts and use defaults").option("-c, --cwd <path>", "Working directory", process.cwd()).action(init);
735
+ program.command("add").description("Add a component to your project").argument("[components...]", "Components to add").option("-y, --yes", "Skip confirmation prompts").option("-o, --overwrite", "Overwrite existing files").option("-c, --cwd <path>", "Working directory", process.cwd()).option("-a, --all", "Add all available components").action(add);
736
+ registerUxPaths(program);
737
+ program.parse();
package/dist/index.cjs CHANGED
@@ -167,7 +167,7 @@ __export(src_exports, {
167
167
  InputOTPGroup: () => InputOTPGroup,
168
168
  InputOTPSeparator: () => InputOTPSeparator,
169
169
  InputOTPSlot: () => InputOTPSlot,
170
- Kbd: () => Kbd,
170
+ Kbd: () => Kbd2,
171
171
  Label: () => Label3,
172
172
  LoginPage: () => LoginPage,
173
173
  LoginScreen: () => LoginScreen,
@@ -492,13 +492,37 @@ var Spinner = () => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
492
492
  ]
493
493
  }
494
494
  );
495
+ var Kbd = ({ children }) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
496
+ "kbd",
497
+ {
498
+ className: cn(
499
+ "ml-1 inline-flex items-center justify-center font-mono uppercase tracking-[0.04em]",
500
+ "text-[10.5px] text-[var(--s-ink-muted)]",
501
+ "min-w-[18px] h-[18px] px-1 rounded-[var(--p-radius-sm)]",
502
+ "border border-[var(--s-border-subtle)] bg-[var(--s-surface-raised)]"
503
+ ),
504
+ children
505
+ }
506
+ );
495
507
  var Button = React3.forwardRef(
496
508
  ({ className, variant, size, asChild = false, loading = false, disabled, kbd, children, ...props }, ref) => {
497
- const Comp = asChild ? import_react_slot.Slot : "button";
509
+ const classes = cn(buttonVariants({ variant, size, className }));
510
+ if (asChild) {
511
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
512
+ import_react_slot.Slot,
513
+ {
514
+ className: classes,
515
+ ref,
516
+ "data-loading": loading ? "" : void 0,
517
+ ...props,
518
+ children
519
+ }
520
+ );
521
+ }
498
522
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
499
- Comp,
523
+ "button",
500
524
  {
501
- className: cn(buttonVariants({ variant, size, className })),
525
+ className: classes,
502
526
  ref,
503
527
  disabled: disabled || loading,
504
528
  "data-loading": loading ? "" : void 0,
@@ -506,18 +530,7 @@ var Button = React3.forwardRef(
506
530
  children: [
507
531
  loading ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Spinner, {}) : null,
508
532
  children,
509
- kbd ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
510
- "kbd",
511
- {
512
- className: cn(
513
- "ml-1 inline-flex items-center justify-center font-mono uppercase tracking-[0.04em]",
514
- "text-[10.5px] text-[var(--s-ink-muted)]",
515
- "min-w-[18px] h-[18px] px-1 rounded-[var(--p-radius-sm)]",
516
- "border border-[var(--s-border-subtle)] bg-[var(--s-surface-raised)]"
517
- ),
518
- children: kbd
519
- }
520
- ) : null
533
+ kbd ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Kbd, { children: kbd }) : null
521
534
  ]
522
535
  }
523
536
  );
@@ -2270,8 +2283,8 @@ var kbdVariants = (0, import_class_variance_authority6.cva)(
2270
2283
  }
2271
2284
  }
2272
2285
  );
2273
- var Kbd = React18.forwardRef(({ className, size, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("kbd", { ref, className: cn(kbdVariants({ size }), className), ...props }));
2274
- Kbd.displayName = "Kbd";
2286
+ var Kbd2 = React18.forwardRef(({ className, size, ...props }, ref) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("kbd", { ref, className: cn(kbdVariants({ size }), className), ...props }));
2287
+ Kbd2.displayName = "Kbd";
2275
2288
 
2276
2289
  // src/components/menubar.tsx
2277
2290
  var MenubarPrimitive = __toESM(require("@radix-ui/react-menubar"), 1);
package/dist/index.js CHANGED
@@ -166,13 +166,37 @@ var Spinner = () => /* @__PURE__ */ jsxs2(
166
166
  ]
167
167
  }
168
168
  );
169
+ var Kbd = ({ children }) => /* @__PURE__ */ jsx3(
170
+ "kbd",
171
+ {
172
+ className: cn(
173
+ "ml-1 inline-flex items-center justify-center font-mono uppercase tracking-[0.04em]",
174
+ "text-[10.5px] text-[var(--s-ink-muted)]",
175
+ "min-w-[18px] h-[18px] px-1 rounded-[var(--p-radius-sm)]",
176
+ "border border-[var(--s-border-subtle)] bg-[var(--s-surface-raised)]"
177
+ ),
178
+ children
179
+ }
180
+ );
169
181
  var Button = React3.forwardRef(
170
182
  ({ className, variant, size, asChild = false, loading = false, disabled, kbd, children, ...props }, ref) => {
171
- const Comp = asChild ? Slot : "button";
183
+ const classes = cn(buttonVariants({ variant, size, className }));
184
+ if (asChild) {
185
+ return /* @__PURE__ */ jsx3(
186
+ Slot,
187
+ {
188
+ className: classes,
189
+ ref,
190
+ "data-loading": loading ? "" : void 0,
191
+ ...props,
192
+ children
193
+ }
194
+ );
195
+ }
172
196
  return /* @__PURE__ */ jsxs2(
173
- Comp,
197
+ "button",
174
198
  {
175
- className: cn(buttonVariants({ variant, size, className })),
199
+ className: classes,
176
200
  ref,
177
201
  disabled: disabled || loading,
178
202
  "data-loading": loading ? "" : void 0,
@@ -180,18 +204,7 @@ var Button = React3.forwardRef(
180
204
  children: [
181
205
  loading ? /* @__PURE__ */ jsx3(Spinner, {}) : null,
182
206
  children,
183
- kbd ? /* @__PURE__ */ jsx3(
184
- "kbd",
185
- {
186
- className: cn(
187
- "ml-1 inline-flex items-center justify-center font-mono uppercase tracking-[0.04em]",
188
- "text-[10.5px] text-[var(--s-ink-muted)]",
189
- "min-w-[18px] h-[18px] px-1 rounded-[var(--p-radius-sm)]",
190
- "border border-[var(--s-border-subtle)] bg-[var(--s-surface-raised)]"
191
- ),
192
- children: kbd
193
- }
194
- ) : null
207
+ kbd ? /* @__PURE__ */ jsx3(Kbd, { children: kbd }) : null
195
208
  ]
196
209
  }
197
210
  );
@@ -1944,8 +1957,8 @@ var kbdVariants = cva6(
1944
1957
  }
1945
1958
  }
1946
1959
  );
1947
- var Kbd = React18.forwardRef(({ className, size, ...props }, ref) => /* @__PURE__ */ jsx26("kbd", { ref, className: cn(kbdVariants({ size }), className), ...props }));
1948
- Kbd.displayName = "Kbd";
1960
+ var Kbd2 = React18.forwardRef(({ className, size, ...props }, ref) => /* @__PURE__ */ jsx26("kbd", { ref, className: cn(kbdVariants({ size }), className), ...props }));
1961
+ Kbd2.displayName = "Kbd";
1949
1962
 
1950
1963
  // src/components/menubar.tsx
1951
1964
  import * as MenubarPrimitive from "@radix-ui/react-menubar";
@@ -5242,7 +5255,7 @@ export {
5242
5255
  InputOTPGroup,
5243
5256
  InputOTPSeparator,
5244
5257
  InputOTPSlot,
5245
- Kbd,
5258
+ Kbd2 as Kbd,
5246
5259
  Label3 as Label,
5247
5260
  LoginPage,
5248
5261
  LoginScreen,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devfellowship/components",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "DFL Design System — UI components, hooks, utils and providers",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -35,11 +35,16 @@
35
35
  "main": "./dist/index.cjs",
36
36
  "module": "./dist/index.js",
37
37
  "types": "./dist/index.d.ts",
38
+ "bin": {
39
+ "dfl-components": "./dist/cli.js"
40
+ },
38
41
  "files": [
39
42
  "dist"
40
43
  ],
41
44
  "scripts": {
42
- "build": "tsup",
45
+ "build": "tsup && tsup --config tsup.cli.config.ts",
46
+ "build:lib": "tsup",
47
+ "build:cli": "tsup --config tsup.cli.config.ts",
43
48
  "build:watch": "tsup --watch",
44
49
  "typecheck": "tsc --noEmit",
45
50
  "test": "vitest run",
@@ -72,6 +77,9 @@
72
77
  "@tailwindcss/vite": "^4.2.2",
73
78
  "@testing-library/jest-dom": "^6.9.1",
74
79
  "@testing-library/react": "^16.3.2",
80
+ "@types/fs-extra": "^11.0.4",
81
+ "@types/node": "^20.0.0",
82
+ "@types/prompts": "^2.4.9",
75
83
  "@types/react": "^18.3.28",
76
84
  "@types/react-dom": "^18.3.7",
77
85
  "@vitejs/plugin-react": "^4.7.0",
@@ -114,20 +122,30 @@
114
122
  "@radix-ui/react-toggle-group": "^1.1.11",
115
123
  "@radix-ui/react-tooltip": "^1.1.4",
116
124
  "@testing-library/dom": "^10.4.1",
125
+ "ajv": "^8.17.1",
126
+ "ajv-formats": "^3.0.1",
127
+ "chalk": "^5.3.0",
117
128
  "class-variance-authority": "^0.7.1",
118
129
  "clsx": "^2.1.1",
119
130
  "cmdk": "^1.1.1",
131
+ "commander": "^12.0.0",
132
+ "cosmiconfig": "^9.0.0",
120
133
  "date-fns": "^3.6.0",
121
134
  "embla-carousel-react": "^8.6.0",
135
+ "fs-extra": "^11.2.0",
122
136
  "input-otp": "^1.4.2",
123
137
  "lucide-react": "^0.462.0",
124
138
  "next-themes": "^0.4.6",
139
+ "node-fetch": "^3.3.2",
140
+ "ora": "^8.0.1",
141
+ "prompts": "^2.4.2",
125
142
  "react-day-picker": "^8.10.1",
126
143
  "react-hook-form": "^7.72.1",
127
144
  "react-resizable-panels": "^2.1.9",
128
145
  "recharts": "^2.15.0",
129
146
  "sonner": "^2.0.7",
130
147
  "tailwind-merge": "^2.5.2",
131
- "vaul": "^1.1.2"
148
+ "vaul": "^1.1.2",
149
+ "zod": "^3.23.8"
132
150
  }
133
151
  }