@devfellowship/components 2.0.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +470 -385
- package/package.json +4 -8
package/dist/cli.js
CHANGED
|
@@ -3,321 +3,10 @@
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
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
6
|
// src/cli/ux-paths/commands/init.ts
|
|
318
7
|
import { mkdirSync, writeFileSync, existsSync } from "fs";
|
|
319
8
|
import { resolve, basename } from "path";
|
|
320
|
-
import
|
|
9
|
+
import chalk from "chalk";
|
|
321
10
|
function registerInit(program2) {
|
|
322
11
|
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
12
|
const cwd = process.cwd();
|
|
@@ -326,7 +15,7 @@ function registerInit(program2) {
|
|
|
326
15
|
const appId = opts.appId || basename(cwd);
|
|
327
16
|
if (existsSync(flowsPath) && !opts.force) {
|
|
328
17
|
console.error(
|
|
329
|
-
|
|
18
|
+
chalk.yellow(`${flowsPath} already exists. Use --force to overwrite.`)
|
|
330
19
|
);
|
|
331
20
|
process.exit(1);
|
|
332
21
|
}
|
|
@@ -342,9 +31,9 @@ function registerInit(program2) {
|
|
|
342
31
|
flows: []
|
|
343
32
|
};
|
|
344
33
|
writeFileSync(flowsPath, JSON.stringify(stub, null, 2) + "\n", "utf8");
|
|
345
|
-
console.log(
|
|
34
|
+
console.log(chalk.green("Created"), flowsPath);
|
|
346
35
|
console.log(
|
|
347
|
-
|
|
36
|
+
chalk.gray(
|
|
348
37
|
"Next steps: populate screens/flows, then run `dfl-components ux-paths validate` and `dfl-components ux-paths generate-mermaid`."
|
|
349
38
|
)
|
|
350
39
|
);
|
|
@@ -356,43 +45,441 @@ import { readFileSync, existsSync as existsSync2 } from "fs";
|
|
|
356
45
|
import { resolve as resolve2 } from "path";
|
|
357
46
|
import Ajv2020 from "ajv/dist/2020.js";
|
|
358
47
|
import addFormats from "ajv-formats";
|
|
359
|
-
import
|
|
48
|
+
import chalk2 from "chalk";
|
|
49
|
+
|
|
50
|
+
// src/cli/ux-paths/lib/v1.schema.json
|
|
51
|
+
var v1_schema_default = {
|
|
52
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
53
|
+
$id: "https://raw.githubusercontent.com/devfellowship/dfl-ux-paths/main/schema/v1.json",
|
|
54
|
+
title: "DFL UX Paths v1",
|
|
55
|
+
description: "Versioned per-app user-flow JSON schema for migration-gap analysis and dead-code detection. Accepts schema_version 1.0.0, 1.1.0, 1.2.0 and 1.3.0 (all additive \u2014 v1.1 adds optional navigation/prereq/test_metadata fields; v1.2 adds optional source_ref (screen\u2192repo file) and screenshots[] (per platform\xD7orientation), and documents `route` as the canonical shared route nomenclature for 1:1 cross-app mapping; v1.3 adds optional source (build|spec), phases[], and phase/description on screen and flow, so the format can also describe a product that has not been built yet. Backward compatible \u2014 v1.0/v1.1/v1.2 files still validate).",
|
|
56
|
+
$comment: "schema v1.2 \u2014 additive over v1.1. New optional screen fields: source_ref { component_file, file_path_chain? } links a screen to the repo file(s) that render it (Figma-Code-Connect style); screenshots[] { platform, orientation, viewport?, url, captured_at? } captures rendered images per platform\xD7orientation for vision-first comparison. `screen.id` is the STICKY 1:1 JOIN KEY across apps \u2014 comparators pair screens by shared id. `route` is the canonical shared route nomenclature (e.g. studio/project/:id/editor) letting two apps map 1:1 by the same name even when their real URLs differ. Draft 2020-12. The file `$id` is intentionally unchanged across additive bumps so consumers pinned to the canonical URL keep working. || schema v1.3 \u2014 additive over v1.2, and the first bump aimed at files authored BEFORE the code exists. `source: \"spec\"` says so explicitly: routes are proposed nomenclature, app_version carries the zero-sha `init` emits, and the absence of source_ref/screenshots/base_url is correct rather than incomplete. `phases[]` declares the app's delivery-phase vocabulary once; `screen.phase` / `flow.phase` reference a phase id. That pair is what lets a viewer render committed scope differently from work the requirement explicitly parks (`phases[].optional: true`) instead of showing one undifferentiated blob \u2014 the distinction a client reads as 'what am I actually getting'. `screen.description` / `flow.description` carry the prose a spec needs and a generated-from-code file usually does not.",
|
|
57
|
+
type: "object",
|
|
58
|
+
required: ["schema_version", "app_id", "app_version", "screens", "flows"],
|
|
59
|
+
additionalProperties: false,
|
|
60
|
+
properties: {
|
|
61
|
+
schema_version: {
|
|
62
|
+
type: "string",
|
|
63
|
+
enum: ["1.0.0", "1.1.0", "1.2.0", "1.3.0"],
|
|
64
|
+
description: "Version of the dfl-ux-paths schema this file conforms to. v1.1.0 adds optional navigation_path, prerequisites, test_metadata, and richer flow step objects. v1.2.0 adds optional screen.source_ref (screen\u2192repo file link) and screen.screenshots[] (per platform\xD7orientation), and documents `route` as the canonical shared route nomenclature for 1:1 cross-app mapping. v1.3.0 adds optional spec-first authoring fields: top-level `source` (build|spec) and `phases[]`, plus optional `phase`/`description` on screens and flows."
|
|
65
|
+
},
|
|
66
|
+
app_id: {
|
|
67
|
+
type: "string",
|
|
68
|
+
minLength: 1,
|
|
69
|
+
description: "Stable identifier for the app (e.g. 'dfl-learn-mobile')."
|
|
70
|
+
},
|
|
71
|
+
app_version: {
|
|
72
|
+
type: "string",
|
|
73
|
+
pattern: "^\\d{4}-\\d{2}-\\d{2}-[0-9a-f]{4,40}$",
|
|
74
|
+
description: 'App build identifier in the form YYYY-MM-DD-<git-sha-short>. A file authored before any build exists uses the zero-sha form `YYYY-MM-DD-0000000` that `dfl-ux-paths init` already emits, and should also set `source: "spec"`.'
|
|
75
|
+
},
|
|
76
|
+
source: {
|
|
77
|
+
type: "string",
|
|
78
|
+
enum: ["build", "spec"],
|
|
79
|
+
description: "v1.3 \u2014 provenance of this file. 'build' (the assumed default when omitted) means it was derived from a shipped build, so `route` values are real and source_ref/screenshots are meaningful. 'spec' means it was authored from a requirement document before the code exists: routes are PROPOSED nomenclature, app_version carries a zero-sha, and the absence of source_ref/screenshots/test_metadata.base_url is expected rather than a gap. Optional, additive."
|
|
80
|
+
},
|
|
81
|
+
phases: {
|
|
82
|
+
type: "array",
|
|
83
|
+
items: { $ref: "#/$defs/phase" },
|
|
84
|
+
description: "v1.3 \u2014 the app's delivery-phase vocabulary, declared once and referenced by `screen.phase` / `flow.phase`. Phase ids are free-form on purpose (every product names its phases differently). Mark a phase `optional: true` when the requirement explicitly parks it outside committed scope. Optional, additive."
|
|
85
|
+
},
|
|
86
|
+
generated_at: {
|
|
87
|
+
type: "string",
|
|
88
|
+
format: "date-time",
|
|
89
|
+
description: "ISO-8601 timestamp when this snapshot was generated."
|
|
90
|
+
},
|
|
91
|
+
tech_stack: {
|
|
92
|
+
type: "array",
|
|
93
|
+
items: { type: "string" },
|
|
94
|
+
description: "List of tech-stack tags (e.g. 'react-native', 'expo', 'powersync')."
|
|
95
|
+
},
|
|
96
|
+
test_metadata: {
|
|
97
|
+
$ref: "#/$defs/testMetadata",
|
|
98
|
+
description: "v1.1 \u2014 app-level metadata used by e2e-user-persona / Playwright skills to bootstrap a session (auth strategy, default viewport, base URL)."
|
|
99
|
+
},
|
|
100
|
+
screens: {
|
|
101
|
+
type: "array",
|
|
102
|
+
items: { $ref: "#/$defs/screen" }
|
|
103
|
+
},
|
|
104
|
+
flows: {
|
|
105
|
+
type: "array",
|
|
106
|
+
items: { $ref: "#/$defs/flow" }
|
|
107
|
+
},
|
|
108
|
+
dead_code: {
|
|
109
|
+
type: "array",
|
|
110
|
+
items: { $ref: "#/$defs/deadCodeEntry" }
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
$defs: {
|
|
114
|
+
phase: {
|
|
115
|
+
type: "object",
|
|
116
|
+
required: ["id"],
|
|
117
|
+
additionalProperties: false,
|
|
118
|
+
description: "v1.3 \u2014 one entry in the app's delivery-phase vocabulary.",
|
|
119
|
+
properties: {
|
|
120
|
+
id: {
|
|
121
|
+
type: "string",
|
|
122
|
+
minLength: 1,
|
|
123
|
+
description: "Stable phase id referenced by `screen.phase` / `flow.phase` (e.g. 'alpha', 'beta', 'mvp', 'optional')."
|
|
124
|
+
},
|
|
125
|
+
label: {
|
|
126
|
+
type: "string",
|
|
127
|
+
description: "Human-readable phase name."
|
|
128
|
+
},
|
|
129
|
+
description: {
|
|
130
|
+
type: "string",
|
|
131
|
+
description: "What this phase covers \u2014 ideally quoting the source requirement rather than paraphrasing it."
|
|
132
|
+
},
|
|
133
|
+
optional: {
|
|
134
|
+
type: "boolean",
|
|
135
|
+
description: "True when the requirement explicitly parks this phase outside committed scope (e.g. 'eventual landing, not in MVP'). Viewers and generated Mermaid MUST render it as visually distinct from in-scope work, so a reader never mistakes parked work for a commitment."
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
screen: {
|
|
140
|
+
type: "object",
|
|
141
|
+
required: ["id", "name"],
|
|
142
|
+
additionalProperties: false,
|
|
143
|
+
properties: {
|
|
144
|
+
id: {
|
|
145
|
+
type: "string",
|
|
146
|
+
minLength: 1,
|
|
147
|
+
description: "Stable screen identifier (snake_case recommended). REQUIRED. This is the STICKY 1:1 JOIN KEY across apps: cross-app comparators (web \u2194 mobile, old build \u2194 new build) pair screens by shared `id`. Treat it as a contract \u2014 keep ids stable across versions and identical across apps you intend to map 1:1, or diffs/parity comparisons break."
|
|
148
|
+
},
|
|
149
|
+
name: {
|
|
150
|
+
type: "string",
|
|
151
|
+
minLength: 1,
|
|
152
|
+
description: "Human-readable screen name."
|
|
153
|
+
},
|
|
154
|
+
route: {
|
|
155
|
+
type: "string",
|
|
156
|
+
description: "v1.2 \u2014 canonical SHARED route nomenclature for 1:1 cross-app mapping (e.g. 'studio/project/:id/editor'). Lets two apps (web \u2194 mobile) map by the same route name even when their real URLs/deep-links differ. Also doubles as the screen's route/path/deep-link when the app uses a single URL scheme. Use ':param' for dynamic segments. Optional, additive."
|
|
157
|
+
},
|
|
158
|
+
description: {
|
|
159
|
+
type: "string",
|
|
160
|
+
description: "v1.3 \u2014 free-form prose describing this screen. For a spec-first file the description IS the requirement, so keep it faithful to the source document. Optional, additive."
|
|
161
|
+
},
|
|
162
|
+
phase: {
|
|
163
|
+
type: "string",
|
|
164
|
+
minLength: 1,
|
|
165
|
+
description: "v1.3 \u2014 id of the phase (from top-level `phases[]`) this screen belongs to. Optional, additive."
|
|
166
|
+
},
|
|
167
|
+
source_ref: {
|
|
168
|
+
$ref: "#/$defs/sourceRef",
|
|
169
|
+
description: "v1.2 \u2014 link from this screen to the repo file(s) that render it (route/page/modal component). Figma-Code-Connect style. Optional, additive \u2014 populated by the route\u2192file resolver (bin/resolve-routes.mjs) or by hand."
|
|
170
|
+
},
|
|
171
|
+
screenshots: {
|
|
172
|
+
type: "array",
|
|
173
|
+
items: { $ref: "#/$defs/screenshot" },
|
|
174
|
+
description: "v1.2 \u2014 rendered screenshots of this screen, one entry per platform\xD7orientation(\xD7viewport). URLs are committed (e.g. S3 via supabase-upload) so humans AND agents view the same image for vision-first comparison. Optional, additive."
|
|
175
|
+
},
|
|
176
|
+
components: {
|
|
177
|
+
type: "array",
|
|
178
|
+
items: { type: "string" },
|
|
179
|
+
description: "Component identifiers rendered on this screen."
|
|
180
|
+
},
|
|
181
|
+
api_calls: {
|
|
182
|
+
type: "array",
|
|
183
|
+
items: { type: "string" },
|
|
184
|
+
description: "API endpoints or RPC calls invoked from this screen."
|
|
185
|
+
},
|
|
186
|
+
actions: {
|
|
187
|
+
type: "array",
|
|
188
|
+
items: { $ref: "#/$defs/action" }
|
|
189
|
+
},
|
|
190
|
+
navigation_path: {
|
|
191
|
+
type: "array",
|
|
192
|
+
items: { $ref: "#/$defs/navigationStep" },
|
|
193
|
+
description: "v1.1 \u2014 ordered list of UI actions to navigate TO this screen from the app entrypoint. Used by e2e-user-persona to auto-generate persona scripts instead of LLM-driven discovery. When multiple navigationStep entries are present, the runner tries them in order and the first matching selector wins (useful for resilience across renames)."
|
|
194
|
+
},
|
|
195
|
+
prerequisites: {
|
|
196
|
+
$ref: "#/$defs/prerequisites",
|
|
197
|
+
description: "v1.1 \u2014 preconditions that must hold for this screen to be reachable / functional."
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
sourceRef: {
|
|
202
|
+
type: "object",
|
|
203
|
+
required: ["component_file"],
|
|
204
|
+
additionalProperties: false,
|
|
205
|
+
description: "v1.2 \u2014 points a screen at the repo file(s) that render it.",
|
|
206
|
+
properties: {
|
|
207
|
+
component_file: {
|
|
208
|
+
type: "string",
|
|
209
|
+
minLength: 1,
|
|
210
|
+
description: "Repo-relative path to the route/page/modal component that renders this screen (e.g. 'app/studio/[id]/editor.tsx' for Expo Router, 'app/studio/[id]/editor/page.tsx' for Next app-router). This is what the route\u2192file resolver emits."
|
|
211
|
+
},
|
|
212
|
+
file_path_chain: {
|
|
213
|
+
type: "array",
|
|
214
|
+
items: { type: "string" },
|
|
215
|
+
minItems: 1,
|
|
216
|
+
description: "Optional \u2014 ordered repo-relative paths from the app entry (e.g. 'App.tsx' / 'app/_layout.tsx') down to the component, giving the 'tree' from app root to this screen's renderer."
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
screenshot: {
|
|
221
|
+
type: "object",
|
|
222
|
+
required: ["platform", "orientation", "url"],
|
|
223
|
+
additionalProperties: false,
|
|
224
|
+
description: "v1.2 \u2014 one rendered screenshot of a screen, keyed by platform\xD7orientation(\xD7viewport).",
|
|
225
|
+
properties: {
|
|
226
|
+
platform: {
|
|
227
|
+
type: "string",
|
|
228
|
+
enum: ["web", "mobile"],
|
|
229
|
+
description: "Rendering platform the screenshot was captured on."
|
|
230
|
+
},
|
|
231
|
+
orientation: {
|
|
232
|
+
type: "string",
|
|
233
|
+
enum: ["portrait", "landscape"],
|
|
234
|
+
description: "Device/viewport orientation."
|
|
235
|
+
},
|
|
236
|
+
viewport: {
|
|
237
|
+
type: "string",
|
|
238
|
+
description: "Optional viewport dimensions string, e.g. '375x812' or '1440x900'."
|
|
239
|
+
},
|
|
240
|
+
url: {
|
|
241
|
+
type: "string",
|
|
242
|
+
format: "uri",
|
|
243
|
+
description: "Public URL of the committed screenshot (e.g. S3 via supabase-upload)."
|
|
244
|
+
},
|
|
245
|
+
captured_at: {
|
|
246
|
+
type: "string",
|
|
247
|
+
format: "date-time",
|
|
248
|
+
description: "Optional ISO-8601 timestamp when the screenshot was captured."
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
action: {
|
|
253
|
+
type: "object",
|
|
254
|
+
required: ["id", "label"],
|
|
255
|
+
additionalProperties: false,
|
|
256
|
+
properties: {
|
|
257
|
+
id: {
|
|
258
|
+
type: "string",
|
|
259
|
+
minLength: 1
|
|
260
|
+
},
|
|
261
|
+
label: {
|
|
262
|
+
type: "string",
|
|
263
|
+
minLength: 1
|
|
264
|
+
},
|
|
265
|
+
next_screen: {
|
|
266
|
+
type: "string",
|
|
267
|
+
description: "Screen id the user lands on after this action."
|
|
268
|
+
},
|
|
269
|
+
side_effect: {
|
|
270
|
+
type: "string",
|
|
271
|
+
description: "Free-form description of side effect (mutation, analytics, etc.)."
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
flow: {
|
|
276
|
+
type: "object",
|
|
277
|
+
required: ["name", "start", "steps"],
|
|
278
|
+
additionalProperties: false,
|
|
279
|
+
properties: {
|
|
280
|
+
name: {
|
|
281
|
+
type: "string",
|
|
282
|
+
minLength: 1
|
|
283
|
+
},
|
|
284
|
+
description: {
|
|
285
|
+
type: "string",
|
|
286
|
+
description: "v1.3 \u2014 free-form prose describing what this flow accomplishes and why it matters. Optional, additive."
|
|
287
|
+
},
|
|
288
|
+
phase: {
|
|
289
|
+
type: "string",
|
|
290
|
+
minLength: 1,
|
|
291
|
+
description: "v1.3 \u2014 id of the phase (from top-level `phases[]`) this flow belongs to. Optional, additive."
|
|
292
|
+
},
|
|
293
|
+
start: {
|
|
294
|
+
type: "string",
|
|
295
|
+
minLength: 1,
|
|
296
|
+
description: "Screen id where the flow starts."
|
|
297
|
+
},
|
|
298
|
+
steps: {
|
|
299
|
+
type: "array",
|
|
300
|
+
items: {
|
|
301
|
+
oneOf: [
|
|
302
|
+
{
|
|
303
|
+
type: "string",
|
|
304
|
+
description: "v1.0 \u2014 screen id traversed in this flow."
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
$ref: "#/$defs/flowStepObject"
|
|
308
|
+
}
|
|
309
|
+
]
|
|
310
|
+
},
|
|
311
|
+
description: "Ordered list of flow steps. v1.0 accepts plain screen ids (strings); v1.1 additionally accepts step objects with action/selector/target_screen for executable scripts. Mixed arrays are allowed during migration."
|
|
312
|
+
},
|
|
313
|
+
actions: {
|
|
314
|
+
type: "array",
|
|
315
|
+
items: { type: "string" },
|
|
316
|
+
description: "Ordered list of action ids exercised during this flow."
|
|
317
|
+
},
|
|
318
|
+
tested_by: {
|
|
319
|
+
type: "array",
|
|
320
|
+
items: { type: "string" },
|
|
321
|
+
description: "References to tests (file paths, test names) that cover this flow."
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
flowStepObject: {
|
|
326
|
+
type: "object",
|
|
327
|
+
required: ["screen"],
|
|
328
|
+
additionalProperties: false,
|
|
329
|
+
properties: {
|
|
330
|
+
screen: {
|
|
331
|
+
type: "string",
|
|
332
|
+
minLength: 1,
|
|
333
|
+
description: "Screen id where this step is executed."
|
|
334
|
+
},
|
|
335
|
+
action: {
|
|
336
|
+
type: "string",
|
|
337
|
+
description: "Action verb (tap, type, scroll, swipe, navigate, wait, assert, etc.)."
|
|
338
|
+
},
|
|
339
|
+
selector: {
|
|
340
|
+
type: "string",
|
|
341
|
+
description: "Playwright-compatible selector (e.g. 'text=Next', 'role=button[name=Save]', 'css=[data-testid=foo]')."
|
|
342
|
+
},
|
|
343
|
+
value: {
|
|
344
|
+
type: "string",
|
|
345
|
+
description: "Optional value to type / select / assert."
|
|
346
|
+
},
|
|
347
|
+
target_screen: {
|
|
348
|
+
type: "string",
|
|
349
|
+
description: "Screen id the user lands on after this step."
|
|
350
|
+
},
|
|
351
|
+
wait_for_target: {
|
|
352
|
+
type: "string",
|
|
353
|
+
description: "Selector / condition to wait for to confirm the target screen rendered."
|
|
354
|
+
},
|
|
355
|
+
note: {
|
|
356
|
+
type: "string",
|
|
357
|
+
description: "Free-form annotation for humans / debugging."
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
navigationStep: {
|
|
362
|
+
type: "object",
|
|
363
|
+
required: ["selector", "action"],
|
|
364
|
+
additionalProperties: false,
|
|
365
|
+
properties: {
|
|
366
|
+
selector: {
|
|
367
|
+
type: "string",
|
|
368
|
+
minLength: 1,
|
|
369
|
+
description: "Playwright-compatible selector (e.g. 'text=Studio', 'role=tab[name=Studio]', 'css=[data-testid=studio-tab]')."
|
|
370
|
+
},
|
|
371
|
+
action: {
|
|
372
|
+
type: "string",
|
|
373
|
+
enum: ["tap", "click", "type", "scroll", "swipe", "navigate", "wait", "assert"],
|
|
374
|
+
description: "Interaction primitive to execute."
|
|
375
|
+
},
|
|
376
|
+
value: {
|
|
377
|
+
type: "string",
|
|
378
|
+
description: "Optional value for type/select/assert."
|
|
379
|
+
},
|
|
380
|
+
wait_for: {
|
|
381
|
+
type: "string",
|
|
382
|
+
description: "Selector / condition to wait for after performing the action to confirm success."
|
|
383
|
+
},
|
|
384
|
+
note: {
|
|
385
|
+
type: "string",
|
|
386
|
+
description: "Free-form annotation."
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
prerequisites: {
|
|
391
|
+
type: "object",
|
|
392
|
+
additionalProperties: false,
|
|
393
|
+
properties: {
|
|
394
|
+
auth_required: {
|
|
395
|
+
type: "boolean",
|
|
396
|
+
description: "True if a logged-in user session is required to reach this screen."
|
|
397
|
+
},
|
|
398
|
+
auth_role: {
|
|
399
|
+
type: "string",
|
|
400
|
+
description: "Optional \u2014 specific role required (e.g. 'admin', 'instructor'). Omit for any authenticated user."
|
|
401
|
+
},
|
|
402
|
+
data_required: {
|
|
403
|
+
type: "array",
|
|
404
|
+
items: { type: "string" },
|
|
405
|
+
description: "Free-form descriptions of DB/state preconditions (e.g. 'lesson_studio.projects has >= 0 rows')."
|
|
406
|
+
},
|
|
407
|
+
preconditions_in_app: {
|
|
408
|
+
type: "array",
|
|
409
|
+
items: { type: "string" },
|
|
410
|
+
description: "Free-form descriptions of in-app preconditions (feature flag enabled, onboarding complete, etc.)."
|
|
411
|
+
},
|
|
412
|
+
feature_flags: {
|
|
413
|
+
type: "array",
|
|
414
|
+
items: { type: "string" },
|
|
415
|
+
description: "Feature flag identifiers that must be enabled."
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
},
|
|
419
|
+
testMetadata: {
|
|
420
|
+
type: "object",
|
|
421
|
+
additionalProperties: false,
|
|
422
|
+
properties: {
|
|
423
|
+
auth_strategy: {
|
|
424
|
+
type: "string",
|
|
425
|
+
description: "Brief description of how to authenticate (e.g. 'supabase-anon + CLAUDE_SMOKE creds', 'magic-link', 'oauth google')."
|
|
426
|
+
},
|
|
427
|
+
default_viewport: {
|
|
428
|
+
type: "string",
|
|
429
|
+
enum: ["mobile", "tablet", "desktop", "responsive"],
|
|
430
|
+
description: "Hint for test runners \u2014 preferred viewport for this app."
|
|
431
|
+
},
|
|
432
|
+
base_url: {
|
|
433
|
+
type: "string",
|
|
434
|
+
format: "uri",
|
|
435
|
+
description: "Production / smoke-target base URL (e.g. 'https://mobile.devfellowship.com/')."
|
|
436
|
+
},
|
|
437
|
+
test_user_secret_path: {
|
|
438
|
+
type: "string",
|
|
439
|
+
description: "Infisical path to credentials suitable for smoke tests (e.g. '/shared/CLAUDE_SMOKE_USERNAME')."
|
|
440
|
+
},
|
|
441
|
+
notes: {
|
|
442
|
+
type: "string",
|
|
443
|
+
description: "Free-form notes for test authors / persona-script generators."
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
deadCodeEntry: {
|
|
448
|
+
type: "object",
|
|
449
|
+
required: ["component", "reason"],
|
|
450
|
+
additionalProperties: false,
|
|
451
|
+
properties: {
|
|
452
|
+
component: {
|
|
453
|
+
type: "string",
|
|
454
|
+
minLength: 1
|
|
455
|
+
},
|
|
456
|
+
reason: {
|
|
457
|
+
type: "string",
|
|
458
|
+
minLength: 1
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
};
|
|
360
464
|
|
|
361
465
|
// 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
466
|
async function loadSchemaV1() {
|
|
365
|
-
|
|
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;
|
|
467
|
+
return v1_schema_default;
|
|
381
468
|
}
|
|
382
469
|
|
|
383
470
|
// src/cli/ux-paths/commands/validate.ts
|
|
384
471
|
function registerValidate(program2) {
|
|
385
472
|
program2.command("validate [path]").description("Validate a flows.json against the DFL UX Paths v1 schema.").action(async (maybePath) => {
|
|
386
|
-
const
|
|
387
|
-
if (!existsSync2(
|
|
388
|
-
console.error(
|
|
473
|
+
const path = resolve2(process.cwd(), maybePath || ".dfl-ux-paths/flows.json");
|
|
474
|
+
if (!existsSync2(path)) {
|
|
475
|
+
console.error(chalk2.red("File not found:"), path);
|
|
389
476
|
process.exit(1);
|
|
390
477
|
}
|
|
391
478
|
let doc;
|
|
392
479
|
try {
|
|
393
|
-
doc = JSON.parse(readFileSync(
|
|
480
|
+
doc = JSON.parse(readFileSync(path, "utf8"));
|
|
394
481
|
} catch (err) {
|
|
395
|
-
console.error(
|
|
482
|
+
console.error(chalk2.red("Invalid JSON:"), err.message);
|
|
396
483
|
process.exit(1);
|
|
397
484
|
}
|
|
398
485
|
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
@@ -401,19 +488,19 @@ function registerValidate(program2) {
|
|
|
401
488
|
try {
|
|
402
489
|
schema = await loadSchemaV1();
|
|
403
490
|
} catch (err) {
|
|
404
|
-
console.error(
|
|
491
|
+
console.error(chalk2.red("Schema error:"), err.message);
|
|
405
492
|
process.exit(1);
|
|
406
493
|
}
|
|
407
494
|
const validate = ajv.compile(schema);
|
|
408
495
|
const ok = validate(doc);
|
|
409
496
|
if (ok) {
|
|
410
|
-
console.log(
|
|
497
|
+
console.log(chalk2.green("OK"), path, "conforms to schema v1.");
|
|
411
498
|
process.exit(0);
|
|
412
499
|
}
|
|
413
|
-
console.error(
|
|
500
|
+
console.error(chalk2.red("FAIL"), path);
|
|
414
501
|
for (const err of validate.errors ?? []) {
|
|
415
502
|
console.error(
|
|
416
|
-
|
|
503
|
+
chalk2.yellow(" -"),
|
|
417
504
|
err.instancePath || "<root>",
|
|
418
505
|
err.message,
|
|
419
506
|
err.params ? JSON.stringify(err.params) : ""
|
|
@@ -426,7 +513,7 @@ function registerValidate(program2) {
|
|
|
426
513
|
// src/cli/ux-paths/commands/generate-mermaid.ts
|
|
427
514
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync3 } from "fs";
|
|
428
515
|
import { resolve as resolve3, dirname } from "path";
|
|
429
|
-
import
|
|
516
|
+
import chalk3 from "chalk";
|
|
430
517
|
|
|
431
518
|
// src/cli/ux-paths/lib/json-to-mermaid.ts
|
|
432
519
|
function jsonToMermaid(doc) {
|
|
@@ -516,23 +603,23 @@ function escapeLabel(raw) {
|
|
|
516
603
|
var HEADER = "%% AUTO-GENERATED by dfl-components ux-paths \u2014 do not edit by hand.\n";
|
|
517
604
|
function registerGenerateMermaid(program2) {
|
|
518
605
|
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
|
|
520
|
-
if (!existsSync3(
|
|
521
|
-
console.error(
|
|
606
|
+
const path = resolve3(process.cwd(), maybePath || ".dfl-ux-paths/flows.json");
|
|
607
|
+
if (!existsSync3(path)) {
|
|
608
|
+
console.error(chalk3.red("File not found:"), path);
|
|
522
609
|
process.exit(1);
|
|
523
610
|
}
|
|
524
|
-
const doc = JSON.parse(readFileSync2(
|
|
525
|
-
const out = opts.out ? resolve3(process.cwd(), opts.out) : resolve3(dirname(
|
|
611
|
+
const doc = JSON.parse(readFileSync2(path, "utf8"));
|
|
612
|
+
const out = opts.out ? resolve3(process.cwd(), opts.out) : resolve3(dirname(path), "flows.mmd");
|
|
526
613
|
const body = HEADER + jsonToMermaid(doc);
|
|
527
614
|
writeFileSync2(out, body, "utf8");
|
|
528
|
-
console.log(
|
|
615
|
+
console.log(chalk3.green("Wrote"), out);
|
|
529
616
|
});
|
|
530
617
|
}
|
|
531
618
|
|
|
532
619
|
// src/cli/ux-paths/commands/diff.ts
|
|
533
620
|
import { readFileSync as readFileSync3, existsSync as existsSync4 } from "fs";
|
|
534
621
|
import { resolve as resolve4 } from "path";
|
|
535
|
-
import
|
|
622
|
+
import chalk4 from "chalk";
|
|
536
623
|
|
|
537
624
|
// src/cli/ux-paths/lib/flows-diff.ts
|
|
538
625
|
function flowsDiff(a, b) {
|
|
@@ -612,15 +699,15 @@ function registerDiff(program2) {
|
|
|
612
699
|
console.log(JSON.stringify(result, null, 2));
|
|
613
700
|
return;
|
|
614
701
|
}
|
|
615
|
-
console.log(
|
|
616
|
-
printSection("Screens added", result.screens.added,
|
|
617
|
-
printSection("Screens removed", result.screens.removed,
|
|
618
|
-
printSection("Actions added", result.actions.added,
|
|
619
|
-
printSection("Actions removed", result.actions.removed,
|
|
620
|
-
printSection("Flows added", result.flows.added,
|
|
621
|
-
printSection("Flows removed", result.flows.removed,
|
|
702
|
+
console.log(chalk4.bold(`Diff ${a.app_id}@${a.app_version} \u2192 ${b.app_id}@${b.app_version}`));
|
|
703
|
+
printSection("Screens added", result.screens.added, chalk4.green);
|
|
704
|
+
printSection("Screens removed", result.screens.removed, chalk4.red);
|
|
705
|
+
printSection("Actions added", result.actions.added, chalk4.green);
|
|
706
|
+
printSection("Actions removed", result.actions.removed, chalk4.red);
|
|
707
|
+
printSection("Flows added", result.flows.added, chalk4.green);
|
|
708
|
+
printSection("Flows removed", result.flows.removed, chalk4.red);
|
|
622
709
|
if (result.flows.changed.length > 0) {
|
|
623
|
-
console.log(
|
|
710
|
+
console.log(chalk4.yellow("\nFlows changed:"));
|
|
624
711
|
for (const ch of result.flows.changed) {
|
|
625
712
|
console.log(` - ${ch.name}: ${ch.reason}`);
|
|
626
713
|
}
|
|
@@ -628,12 +715,12 @@ function registerDiff(program2) {
|
|
|
628
715
|
});
|
|
629
716
|
}
|
|
630
717
|
function loadDoc(p) {
|
|
631
|
-
const
|
|
632
|
-
if (!existsSync4(
|
|
633
|
-
console.error(
|
|
718
|
+
const path = resolve4(process.cwd(), p);
|
|
719
|
+
if (!existsSync4(path)) {
|
|
720
|
+
console.error(chalk4.red("File not found:"), path);
|
|
634
721
|
process.exit(1);
|
|
635
722
|
}
|
|
636
|
-
return JSON.parse(readFileSync3(
|
|
723
|
+
return JSON.parse(readFileSync3(path, "utf8"));
|
|
637
724
|
}
|
|
638
725
|
function printSection(label, items, color) {
|
|
639
726
|
if (items.length === 0) return;
|
|
@@ -648,7 +735,7 @@ ${label} (${items.length}):`));
|
|
|
648
735
|
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync5 } from "fs";
|
|
649
736
|
import { resolve as resolve5 } from "path";
|
|
650
737
|
import { execSync } from "child_process";
|
|
651
|
-
import
|
|
738
|
+
import chalk5 from "chalk";
|
|
652
739
|
|
|
653
740
|
// src/cli/ux-paths/lib/preserve-format.ts
|
|
654
741
|
function replaceTopLevelStringField(raw, key, newValue) {
|
|
@@ -678,12 +765,12 @@ function registerStamp(program2) {
|
|
|
678
765
|
"--no-preserve-format",
|
|
679
766
|
"Reformat the whole file (legacy). By default, stamp surgically updates only app_version/generated_at, preserving the existing formatting (minimal diff)."
|
|
680
767
|
).action((maybePath, opts) => {
|
|
681
|
-
const
|
|
682
|
-
if (!existsSync5(
|
|
683
|
-
console.error(
|
|
768
|
+
const path = resolve5(process.cwd(), maybePath || ".dfl-ux-paths/flows.json");
|
|
769
|
+
if (!existsSync5(path)) {
|
|
770
|
+
console.error(chalk5.red("File not found:"), path);
|
|
684
771
|
process.exit(1);
|
|
685
772
|
}
|
|
686
|
-
const raw = readFileSync4(
|
|
773
|
+
const raw = readFileSync4(path, "utf8");
|
|
687
774
|
const doc = JSON.parse(raw);
|
|
688
775
|
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
689
776
|
let sha = "0000000";
|
|
@@ -709,10 +796,10 @@ function registerStamp(program2) {
|
|
|
709
796
|
};
|
|
710
797
|
output = JSON.stringify(updated, null, 2) + "\n";
|
|
711
798
|
}
|
|
712
|
-
writeFileSync3(
|
|
713
|
-
console.log(
|
|
714
|
-
console.log(
|
|
715
|
-
console.log(
|
|
799
|
+
writeFileSync3(path, output, "utf8");
|
|
800
|
+
console.log(chalk5.green("Stamped"), path, preserve ? chalk5.gray("(minimal diff)") : "");
|
|
801
|
+
console.log(chalk5.gray(` app_version: ${newVersion}`));
|
|
802
|
+
console.log(chalk5.gray(` generated_at: ${newGeneratedAt}`));
|
|
716
803
|
});
|
|
717
804
|
}
|
|
718
805
|
|
|
@@ -729,7 +816,7 @@ function registerUxPaths(program2) {
|
|
|
729
816
|
// src/cli/check-style-imports/index.ts
|
|
730
817
|
import { resolve as resolve6 } from "path";
|
|
731
818
|
import { existsSync as existsSync6 } from "fs";
|
|
732
|
-
import
|
|
819
|
+
import chalk6 from "chalk";
|
|
733
820
|
|
|
734
821
|
// src/cli/check-style-imports/detect.ts
|
|
735
822
|
import { readdirSync, readFileSync as readFileSync5 } from "fs";
|
|
@@ -821,7 +908,7 @@ function registerCheckStyleImports(program2) {
|
|
|
821
908
|
).option("--json", "Emit the raw detection result as JSON instead of a human report.").action((maybeDir, opts) => {
|
|
822
909
|
const root = resolve6(process.cwd(), maybeDir || ".");
|
|
823
910
|
if (!existsSync6(root)) {
|
|
824
|
-
console.error(
|
|
911
|
+
console.error(chalk6.red("Directory not found:"), root);
|
|
825
912
|
process.exit(2);
|
|
826
913
|
}
|
|
827
914
|
const result = detectInDir(root);
|
|
@@ -832,33 +919,33 @@ function registerCheckStyleImports(program2) {
|
|
|
832
919
|
if (!result.conflict) {
|
|
833
920
|
if (result.hits.length === 0) {
|
|
834
921
|
console.log(
|
|
835
|
-
|
|
922
|
+
chalk6.green("OK"),
|
|
836
923
|
"no @devfellowship/components/{styles,shadcn} imports found."
|
|
837
924
|
);
|
|
838
925
|
} else {
|
|
839
926
|
const which = result.styles.length > 0 ? "styles" : "shadcn";
|
|
840
927
|
console.log(
|
|
841
|
-
|
|
928
|
+
chalk6.green("OK"),
|
|
842
929
|
`app imports only @devfellowship/components/${which} (${result.hits.length} reference${result.hits.length === 1 ? "" : "s"}).`
|
|
843
930
|
);
|
|
844
931
|
}
|
|
845
932
|
process.exit(0);
|
|
846
933
|
}
|
|
847
934
|
console.error(
|
|
848
|
-
|
|
935
|
+
chalk6.red("CONFLICT"),
|
|
849
936
|
"this app imports BOTH @devfellowship/components/styles AND /shadcn."
|
|
850
937
|
);
|
|
851
938
|
console.error(
|
|
852
|
-
|
|
939
|
+
chalk6.yellow(
|
|
853
940
|
"\nThese exports define the SAME CSS vars (--background, --primary, \u2026) in INCOMPATIBLE formats:\n - /styles ships them as HEX (#0A0908)\n - /shadcn ships them as HSL CHANNELS (30 11% 4%)\nImporting both clobbers --background \u2014 e.g. hsl(#0A0908) is invalid CSS \u2192 the\ndeclaration drops \u2192 surfaces render TRANSPARENT (the transparent-dialog bug).\n"
|
|
854
941
|
)
|
|
855
942
|
);
|
|
856
|
-
console.error(
|
|
943
|
+
console.error(chalk6.bold(" /styles imports:"));
|
|
857
944
|
for (const h of result.styles) console.error(` ${h.file}:${h.line} ${h.text}`);
|
|
858
|
-
console.error(
|
|
945
|
+
console.error(chalk6.bold(" /shadcn imports:"));
|
|
859
946
|
for (const h of result.shadcn) console.error(` ${h.file}:${h.line} ${h.text}`);
|
|
860
947
|
console.error(
|
|
861
|
-
|
|
948
|
+
chalk6.cyan(
|
|
862
949
|
`
|
|
863
950
|
FIX: keep exactly ONE. A DS-native (hex) app imports /styles only; a
|
|
864
951
|
shadcn-slate (HSL-channel) app imports /shadcn only. NEVER both.
|
|
@@ -872,10 +959,8 @@ See: ${DOC_URL}`
|
|
|
872
959
|
// src/cli/index.ts
|
|
873
960
|
var program = new Command();
|
|
874
961
|
program.name("dfl-components").description(
|
|
875
|
-
|
|
876
|
-
).version("
|
|
877
|
-
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);
|
|
878
|
-
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);
|
|
962
|
+
'DevFellowship components CLI \u2014 map app UX paths (folds the dfl-ux-paths CLI). Shipped as the `dfl-components` bin of @devfellowship/components. The component set is consumed as a library import (`import { Button } from "@devfellowship/components"`), not scaffolded.'
|
|
963
|
+
).version("3.0.0");
|
|
879
964
|
registerUxPaths(program);
|
|
880
965
|
registerCheckStyleImports(program);
|
|
881
966
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devfellowship/components",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "DFL Design System — UI components, hooks, utils and providers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
],
|
|
44
44
|
"scripts": {
|
|
45
45
|
"build": "tsup && tsup --config tsup.cli.config.ts",
|
|
46
|
+
"release": "npm run build && changeset publish",
|
|
46
47
|
"build:lib": "tsup",
|
|
47
48
|
"build:cli": "tsup --config tsup.cli.config.ts",
|
|
48
49
|
"build:watch": "tsup --watch",
|
|
@@ -68,6 +69,8 @@
|
|
|
68
69
|
}
|
|
69
70
|
},
|
|
70
71
|
"devDependencies": {
|
|
72
|
+
"@changesets/changelog-github": "^0.7.0",
|
|
73
|
+
"@changesets/cli": "^2.31.0",
|
|
71
74
|
"@storybook/addon-a11y": "^9.1.20",
|
|
72
75
|
"@storybook/addon-themes": "^9.1.20",
|
|
73
76
|
"@storybook/react": "^9.1.20",
|
|
@@ -76,9 +79,7 @@
|
|
|
76
79
|
"@tailwindcss/vite": "^4.2.2",
|
|
77
80
|
"@testing-library/jest-dom": "^6.9.1",
|
|
78
81
|
"@testing-library/react": "^16.3.2",
|
|
79
|
-
"@types/fs-extra": "^11.0.4",
|
|
80
82
|
"@types/node": "^20.0.0",
|
|
81
|
-
"@types/prompts": "^2.4.9",
|
|
82
83
|
"@types/react": "^18.3.28",
|
|
83
84
|
"@types/react-dom": "^18.3.7",
|
|
84
85
|
"@vitejs/plugin-react": "^4.7.0",
|
|
@@ -128,16 +129,11 @@
|
|
|
128
129
|
"clsx": "^2.1.1",
|
|
129
130
|
"cmdk": "^1.1.1",
|
|
130
131
|
"commander": "^12.0.0",
|
|
131
|
-
"cosmiconfig": "^9.0.0",
|
|
132
132
|
"date-fns": "^3.6.0",
|
|
133
133
|
"embla-carousel-react": "^8.6.0",
|
|
134
|
-
"fs-extra": "^11.2.0",
|
|
135
134
|
"input-otp": "^1.4.2",
|
|
136
135
|
"lucide-react": "^0.462.0",
|
|
137
136
|
"next-themes": "^0.4.6",
|
|
138
|
-
"node-fetch": "^3.3.2",
|
|
139
|
-
"ora": "^8.0.1",
|
|
140
|
-
"prompts": "^2.4.2",
|
|
141
137
|
"react-day-picker": "^8.10.1",
|
|
142
138
|
"react-hook-form": "^7.72.1",
|
|
143
139
|
"react-resizable-panels": "^2.1.9",
|