chore: initial monorepo scaffold + WDS Phase 1+2 artifacts
- Nx 22.7 monorepo (pnpm 11.1, TypeScript 5.9, Node 24) - apps/api: NestJS 11 (CJS conforme CODING-RULES.md PGD-DB-004) - apps/web: React 19 + Vite 8 (ESM) - libs/shared/api-interface: Zod contract base - Docker Compose dev: Postgres 18, Valkey 8, MinIO, Mailpit - WDS artifacts: - design-artifacts/A-Product-Brief/ (5 docs canônicos + 16 dialogs) - design-artifacts/B-Trigger-Map/ (hub + 4 personas + feature impact) - Stack canon: STACK.md v2.2 + CODING-RULES.md v2.0 + brand.md - AGENTS.md + README.md como entrada para devs/agentes Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
201
_bmad/wds/scripts/wds-nav.js
Normal file
201
_bmad/wds/scripts/wds-nav.js
Normal file
@@ -0,0 +1,201 @@
|
||||
// wds-nav.js — WDS scaffold: generate/update navigation links across all pages in a scenario
|
||||
// Usage: node src/scripts/wds-nav.js --scenario "01 Onboarding"
|
||||
// node src/scripts/wds-nav.js --all
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {};
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
if (argv[i].startsWith('--')) {
|
||||
const key = argv[i].slice(2);
|
||||
const value = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[i + 1] : true;
|
||||
args[key] = value;
|
||||
if (value !== true) i++;
|
||||
}
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
function toSlug(str) {
|
||||
return str.toLowerCase().replaceAll(/\s+/g, '-');
|
||||
}
|
||||
|
||||
function printUsage() {
|
||||
process.stdout.write(
|
||||
[
|
||||
'Usage: node src/scripts/wds-nav.js --scenario "01 Onboarding"',
|
||||
' node src/scripts/wds-nav.js --all',
|
||||
'',
|
||||
'Options:',
|
||||
' --scenario Scenario name or slug to update',
|
||||
' --all Update all scenarios',
|
||||
' --output Base path (default: current directory)',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
}
|
||||
|
||||
// Build a human-readable page name from the slug for nav labels
|
||||
// e.g. "01-start" -> "01 Start"
|
||||
function slugToLabel(slug) {
|
||||
return slug
|
||||
.split('-')
|
||||
.map((part, i) => (i === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
function buildNavRow(prev, next) {
|
||||
const leftPart = prev ? `← [${slugToLabel(prev.slug)}](../${prev.slug}/${prev.slug}.md)` : '←';
|
||||
const rightPart = next ? `[${slugToLabel(next.slug)} →](../${next.slug}/${next.slug}.md)` : '→';
|
||||
return `${leftPart} | ${rightPart}`;
|
||||
}
|
||||
|
||||
// Replace all 3 occurrences of navigation rows in a page file.
|
||||
// Nav rows are lines matching the pattern: starts with '←' or contains '| [' and ends with '→'
|
||||
// We identify them by a simple pattern: line that starts with "←" (after trim).
|
||||
function updateNavInContent(content, navRow) {
|
||||
const lines = content.split('\n');
|
||||
const result = [];
|
||||
let navCount = 0;
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
// Match navigation rows: lines that start with "←" (our nav format)
|
||||
if (trimmed.startsWith('←')) {
|
||||
result.push(navRow);
|
||||
navCount++;
|
||||
} else {
|
||||
result.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
return { content: result.join('\n'), navCount };
|
||||
}
|
||||
|
||||
function getPageFolders(scenarioDir) {
|
||||
let entries;
|
||||
try {
|
||||
entries = fs.readdirSync(scenarioDir, { withFileTypes: true });
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
return entries
|
||||
.filter((e) => e.isDirectory())
|
||||
.map((e) => e.name)
|
||||
.filter((name) => {
|
||||
// Must have a matching .md file inside
|
||||
const mdFile = path.join(scenarioDir, name, `${name}.md`);
|
||||
return fs.existsSync(mdFile);
|
||||
})
|
||||
.sort(); // Sort alphabetically — page numbers ensure correct order
|
||||
}
|
||||
|
||||
function processScenario(scenariosBase, scenarioSlug) {
|
||||
const scenarioDir = path.join(scenariosBase, scenarioSlug);
|
||||
|
||||
if (!fs.existsSync(scenarioDir)) {
|
||||
process.stderr.write(`Error: Scenario not found: ${scenarioDir}\n`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const pageSlugs = getPageFolders(scenarioDir);
|
||||
|
||||
if (pageSlugs.length === 0) {
|
||||
process.stdout.write(` ${scenarioSlug}: no pages found, skipping.\n`);
|
||||
return true;
|
||||
}
|
||||
|
||||
let updated = 0;
|
||||
|
||||
for (let i = 0; i < pageSlugs.length; i++) {
|
||||
const slug = pageSlugs[i];
|
||||
const prev = i > 0 ? { slug: pageSlugs[i - 1] } : null;
|
||||
const next = i < pageSlugs.length - 1 ? { slug: pageSlugs[i + 1] } : null;
|
||||
const navRow = buildNavRow(prev, next);
|
||||
|
||||
const mdFile = path.join(scenarioDir, slug, `${slug}.md`);
|
||||
let content;
|
||||
try {
|
||||
content = fs.readFileSync(mdFile, 'utf8');
|
||||
} catch (error) {
|
||||
process.stderr.write(` Error reading ${mdFile}: ${error.message}\n`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const { content: newContent, navCount } = updateNavInContent(content, navRow);
|
||||
|
||||
if (navCount === 0) {
|
||||
process.stderr.write(` Warning: No navigation rows found in ${slug}.md\n`);
|
||||
}
|
||||
|
||||
try {
|
||||
fs.writeFileSync(mdFile, newContent, 'utf8');
|
||||
updated++;
|
||||
} catch (error) {
|
||||
process.stderr.write(` Error writing ${mdFile}: ${error.message}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
process.stdout.write(`✓ Updated navigation for ${updated} pages in ${scenarioSlug}\n`);
|
||||
return true;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
|
||||
if (args.help) {
|
||||
printUsage();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!args.scenario && !args.all) {
|
||||
process.stderr.write('Error: --scenario or --all is required.\n\n');
|
||||
printUsage();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const outputBase = args.output || process.cwd();
|
||||
const scenariosBase = path.join(outputBase, 'C-UX-Scenarios');
|
||||
|
||||
if (!fs.existsSync(scenariosBase)) {
|
||||
process.stderr.write(`Error: C-UX-Scenarios directory not found at: ${scenariosBase}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (args.all) {
|
||||
let entries;
|
||||
try {
|
||||
entries = fs.readdirSync(scenariosBase, { withFileTypes: true });
|
||||
} catch (error) {
|
||||
process.stderr.write(`Error reading C-UX-Scenarios: ${error.message}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const scenarios = entries
|
||||
.filter((e) => e.isDirectory())
|
||||
.map((e) => e.name)
|
||||
.sort();
|
||||
|
||||
if (scenarios.length === 0) {
|
||||
process.stdout.write('No scenarios found.\n');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
let allOk = true;
|
||||
for (const slug of scenarios) {
|
||||
if (!processScenario(scenariosBase, slug)) allOk = false;
|
||||
}
|
||||
process.exit(allOk ? 0 : 1);
|
||||
} else {
|
||||
const scenarioSlug = toSlug(args.scenario);
|
||||
const ok = processScenario(scenariosBase, scenarioSlug);
|
||||
process.exit(ok ? 0 : 1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user