@devfellowship/components 1.2.2 → 1.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/dist/cli.js +737 -0
- package/dist/index.cjs +446 -55
- package/dist/index.d.cts +73 -1
- package/dist/index.d.ts +73 -1
- package/dist/index.js +442 -57
- package/package.json +21 -3
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();
|