@urbicon-ui/design 6.3.7 → 6.3.9
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 +340 -40
- package/package.json +5 -4
- package/templates/ci-github.yml +6 -0
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
2
4
|
|
|
3
5
|
// src/cli/index.ts
|
|
4
|
-
import { readFile as
|
|
5
|
-
import { resolve as
|
|
6
|
+
import { readFile as readFile11 } from "node:fs/promises";
|
|
7
|
+
import { resolve as resolve11 } from "node:path";
|
|
6
8
|
|
|
7
9
|
// src/cli/args.ts
|
|
8
10
|
var BOOLEAN_FLAGS = new Set([
|
|
@@ -762,7 +764,7 @@ function extractSection2(content, section) {
|
|
|
762
764
|
}
|
|
763
765
|
// src/cli/content.ts
|
|
764
766
|
import { readFile as readFile4 } from "node:fs/promises";
|
|
765
|
-
import { createRequire } from "node:module";
|
|
767
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
766
768
|
import { dirname as dirname2, resolve as resolve3 } from "node:path";
|
|
767
769
|
|
|
768
770
|
// ../design-content/src/content-loader.ts
|
|
@@ -803,7 +805,7 @@ function ensureContentDir() {
|
|
|
803
805
|
if (process.env.URBICON_CONTENT_DIR)
|
|
804
806
|
return;
|
|
805
807
|
try {
|
|
806
|
-
const require2 =
|
|
808
|
+
const require2 = createRequire2(import.meta.url);
|
|
807
809
|
const pkgJson = require2.resolve("@urbicon-ui/design-content/package.json");
|
|
808
810
|
process.env.URBICON_CONTENT_DIR = resolve3(dirname2(pkgJson), "content");
|
|
809
811
|
} catch {}
|
|
@@ -2401,19 +2403,303 @@ Fix the issues above and re-save. — urbicon design gate`);
|
|
|
2401
2403
|
return HOOK_BLOCK;
|
|
2402
2404
|
}
|
|
2403
2405
|
|
|
2406
|
+
// src/cli/commands/i18n.ts
|
|
2407
|
+
import { readdir as readdir2, readFile as readFile6, stat as stat2 } from "node:fs/promises";
|
|
2408
|
+
import { basename, join as join3, relative as relative2, resolve as resolve6, sep as sep2 } from "node:path";
|
|
2409
|
+
import { pathToFileURL } from "node:url";
|
|
2410
|
+
var CHECKS = ["parity", "unused", "hardcoded", "audit"];
|
|
2411
|
+
var isCheck = (value) => CHECKS.includes(value);
|
|
2412
|
+
var SKIP_DIRS2 = new Set([
|
|
2413
|
+
"node_modules",
|
|
2414
|
+
".svelte-kit",
|
|
2415
|
+
".git",
|
|
2416
|
+
"dist",
|
|
2417
|
+
"build",
|
|
2418
|
+
".next",
|
|
2419
|
+
".turbo",
|
|
2420
|
+
"coverage"
|
|
2421
|
+
]);
|
|
2422
|
+
var MAX_DEPTH2 = 24;
|
|
2423
|
+
var DEFAULT_CONFIG = {
|
|
2424
|
+
sources: ["src"],
|
|
2425
|
+
translations: ["src/lib/translations"]
|
|
2426
|
+
};
|
|
2427
|
+
function label(abs) {
|
|
2428
|
+
return relative2(process.cwd(), abs).split(sep2).join("/") || abs;
|
|
2429
|
+
}
|
|
2430
|
+
function listFlag(flags, key) {
|
|
2431
|
+
const raw = stringFlag(flags, key);
|
|
2432
|
+
if (raw === undefined)
|
|
2433
|
+
return;
|
|
2434
|
+
const items = raw.split(",").map((item) => item.trim()).filter(Boolean);
|
|
2435
|
+
return items.length ? items : undefined;
|
|
2436
|
+
}
|
|
2437
|
+
async function loadConfig(flags, sourceDirs) {
|
|
2438
|
+
let fileConfig = {};
|
|
2439
|
+
const explicit = stringFlag(flags, "config");
|
|
2440
|
+
const candidate = explicit ?? "i18n.audit.json";
|
|
2441
|
+
try {
|
|
2442
|
+
fileConfig = JSON.parse(await readFile6(resolve6(candidate), "utf-8"));
|
|
2443
|
+
} catch (error) {
|
|
2444
|
+
if (explicit)
|
|
2445
|
+
throw new Error(`cannot read config "${explicit}": ${error.message}`);
|
|
2446
|
+
}
|
|
2447
|
+
const merged = { ...DEFAULT_CONFIG, ...fileConfig };
|
|
2448
|
+
if (sourceDirs.length)
|
|
2449
|
+
merged.sources = sourceDirs;
|
|
2450
|
+
merged.functionNames = listFlag(flags, "function-names") ?? merged.functionNames;
|
|
2451
|
+
merged.dynamicKeys = listFlag(flags, "dynamic-keys") ?? merged.dynamicKeys;
|
|
2452
|
+
merged.ignoreKeys = listFlag(flags, "ignore-keys") ?? merged.ignoreKeys;
|
|
2453
|
+
merged.ignoreStrings = listFlag(flags, "ignore-strings") ?? merged.ignoreStrings;
|
|
2454
|
+
merged.baseLocale = stringFlag(flags, "base-locale") ?? merged.baseLocale;
|
|
2455
|
+
merged.translations = listFlag(flags, "translations") ?? merged.translations;
|
|
2456
|
+
merged.runtimeUsage = stringFlag(flags, "runtime-usage") ?? merged.runtimeUsage;
|
|
2457
|
+
return merged;
|
|
2458
|
+
}
|
|
2459
|
+
async function collectFiles(dir, extensions, depth = 0) {
|
|
2460
|
+
if (depth > MAX_DEPTH2)
|
|
2461
|
+
return [];
|
|
2462
|
+
let entries;
|
|
2463
|
+
try {
|
|
2464
|
+
entries = await readdir2(dir, { withFileTypes: true });
|
|
2465
|
+
} catch {
|
|
2466
|
+
return [];
|
|
2467
|
+
}
|
|
2468
|
+
const files = [];
|
|
2469
|
+
for (const entry of entries) {
|
|
2470
|
+
const full = join3(dir, entry.name);
|
|
2471
|
+
if (entry.isDirectory()) {
|
|
2472
|
+
if (SKIP_DIRS2.has(entry.name) || entry.name.startsWith("."))
|
|
2473
|
+
continue;
|
|
2474
|
+
files.push(...await collectFiles(full, extensions, depth + 1));
|
|
2475
|
+
} else if (entry.isFile() && extensions.some((ext) => entry.name.endsWith(ext)) && !/\.(test|spec)\.|\.d\.ts$/.test(entry.name)) {
|
|
2476
|
+
files.push(full);
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
return files;
|
|
2480
|
+
}
|
|
2481
|
+
var BUNDLE_EXT = /\.(ts|js|mjs)$/;
|
|
2482
|
+
async function loadBundleGroups(dirs, isSupportedLocale) {
|
|
2483
|
+
const groups = [];
|
|
2484
|
+
const errors = [];
|
|
2485
|
+
for (const dir of dirs) {
|
|
2486
|
+
const abs = resolve6(dir);
|
|
2487
|
+
let candidates;
|
|
2488
|
+
try {
|
|
2489
|
+
const info = await stat2(abs);
|
|
2490
|
+
candidates = info.isDirectory() ? (await readdir2(abs)).filter((f) => BUNDLE_EXT.test(f) && !f.endsWith(".d.ts")).map((f) => join3(abs, f)) : [abs];
|
|
2491
|
+
} catch {
|
|
2492
|
+
errors.push(`translations path not found: ${label(abs)}`);
|
|
2493
|
+
continue;
|
|
2494
|
+
}
|
|
2495
|
+
const bundles = {};
|
|
2496
|
+
let localeFiles = 0;
|
|
2497
|
+
for (const file of candidates) {
|
|
2498
|
+
const locale = basename(file).replace(BUNDLE_EXT, "");
|
|
2499
|
+
if (!isSupportedLocale(locale))
|
|
2500
|
+
continue;
|
|
2501
|
+
localeFiles++;
|
|
2502
|
+
if (bundles[locale]) {
|
|
2503
|
+
errors.push(`duplicate locale "${locale}" in ${label(abs)} — ${label(file)} ignored`);
|
|
2504
|
+
continue;
|
|
2505
|
+
}
|
|
2506
|
+
try {
|
|
2507
|
+
const mod = await import(pathToFileURL(file).href);
|
|
2508
|
+
if (mod.default && typeof mod.default === "object") {
|
|
2509
|
+
bundles[locale] = mod.default;
|
|
2510
|
+
} else {
|
|
2511
|
+
errors.push(`bundle ${label(file)} has no object default export`);
|
|
2512
|
+
}
|
|
2513
|
+
} catch (error) {
|
|
2514
|
+
errors.push(`cannot load bundle ${label(file)}: ${error.message}`);
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
if (Object.keys(bundles).length)
|
|
2518
|
+
groups.push({ name: label(abs), bundles });
|
|
2519
|
+
else if (localeFiles === 0)
|
|
2520
|
+
errors.push(`no locale bundles (en/de/…) in: ${label(abs)}`);
|
|
2521
|
+
}
|
|
2522
|
+
return { groups, errors };
|
|
2523
|
+
}
|
|
2524
|
+
function runParity(audit, config, groups) {
|
|
2525
|
+
const lines = [];
|
|
2526
|
+
let errors = 0;
|
|
2527
|
+
let warnings = 0;
|
|
2528
|
+
const allFindings = [];
|
|
2529
|
+
for (const group of groups) {
|
|
2530
|
+
const report = audit.auditTranslations(group.name, group.bundles, {
|
|
2531
|
+
baseLocale: config.baseLocale,
|
|
2532
|
+
ignoreKeys: config.ignoreKeys
|
|
2533
|
+
});
|
|
2534
|
+
errors += report.errors.length;
|
|
2535
|
+
warnings += report.warnings.length;
|
|
2536
|
+
allFindings.push(...report.findings);
|
|
2537
|
+
for (const finding of report.findings) {
|
|
2538
|
+
const icon = finding.severity === "error" ? "✗" : "!";
|
|
2539
|
+
lines.push(` ${icon} [${finding.code}] ${finding.locale} — ${finding.detail}`);
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
return { lines, errors, warnings, json: { findings: allFindings } };
|
|
2543
|
+
}
|
|
2544
|
+
async function runUnused(audit, config, groups, sources) {
|
|
2545
|
+
const defined = new Set;
|
|
2546
|
+
for (const group of groups) {
|
|
2547
|
+
const base = group.bundles.en ?? Object.values(group.bundles)[0];
|
|
2548
|
+
if (base)
|
|
2549
|
+
for (const key of audit.collectDeepKeys(base))
|
|
2550
|
+
defined.add(key);
|
|
2551
|
+
}
|
|
2552
|
+
let runtimeUsedKeys;
|
|
2553
|
+
if (config.runtimeUsage) {
|
|
2554
|
+
try {
|
|
2555
|
+
const parsed = JSON.parse(await readFile6(resolve6(config.runtimeUsage), "utf-8"));
|
|
2556
|
+
if (Array.isArray(parsed))
|
|
2557
|
+
runtimeUsedKeys = parsed.filter((k) => typeof k === "string");
|
|
2558
|
+
} catch (error) {
|
|
2559
|
+
printError(`ignoring unreadable runtime-usage file: ${error.message}`);
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
const { scan, errors: scanErrors } = await audit.scanSources(sources, {
|
|
2563
|
+
functionNames: config.functionNames
|
|
2564
|
+
});
|
|
2565
|
+
const report = audit.findUnusedKeys(defined, scan, {
|
|
2566
|
+
dynamicKeys: config.dynamicKeys,
|
|
2567
|
+
ignoreKeys: config.ignoreKeys,
|
|
2568
|
+
runtimeUsedKeys
|
|
2569
|
+
});
|
|
2570
|
+
const lines = [];
|
|
2571
|
+
for (const error of scanErrors)
|
|
2572
|
+
lines.push(` ! could not parse ${error.file}: ${error.message}`);
|
|
2573
|
+
for (const finding of report.usedButUndefined) {
|
|
2574
|
+
lines.push(` ✗ used but undefined: ${finding.key} (${finding.sites[0]?.file}:${finding.sites[0]?.line})`);
|
|
2575
|
+
}
|
|
2576
|
+
for (const finding of report.unused) {
|
|
2577
|
+
lines.push(` ! unused (${finding.tier}): ${finding.key}`);
|
|
2578
|
+
}
|
|
2579
|
+
return {
|
|
2580
|
+
lines,
|
|
2581
|
+
errors: report.usedButUndefined.length,
|
|
2582
|
+
warnings: report.unused.length,
|
|
2583
|
+
json: {
|
|
2584
|
+
unused: report.unused,
|
|
2585
|
+
usedButUndefined: report.usedButUndefined,
|
|
2586
|
+
dynamicPrefixCoverage: report.dynamicPrefixCoverage,
|
|
2587
|
+
opaqueSiteCount: report.opaqueSiteCount,
|
|
2588
|
+
scanErrors
|
|
2589
|
+
}
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2592
|
+
async function runHardcoded(audit, config, sources) {
|
|
2593
|
+
const lines = [];
|
|
2594
|
+
const all = [];
|
|
2595
|
+
let warnings = 0;
|
|
2596
|
+
for (const source of sources) {
|
|
2597
|
+
if (!source.file.endsWith(".svelte"))
|
|
2598
|
+
continue;
|
|
2599
|
+
try {
|
|
2600
|
+
const findings = await audit.findHardcodedStrings(source.code, source.file, {
|
|
2601
|
+
ignoreStrings: config.ignoreStrings
|
|
2602
|
+
});
|
|
2603
|
+
warnings += findings.length;
|
|
2604
|
+
all.push(...findings);
|
|
2605
|
+
for (const f of findings) {
|
|
2606
|
+
const where = f.kind === "attribute" ? `@${f.attribute}` : "text";
|
|
2607
|
+
lines.push(` ! hardcoded ${where}: "${f.text}" (${f.file}:${f.line})`);
|
|
2608
|
+
}
|
|
2609
|
+
} catch (error) {
|
|
2610
|
+
lines.push(` ! could not parse ${source.file}: ${error.message}`);
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
return { lines, errors: 0, warnings, json: { findings: all } };
|
|
2614
|
+
}
|
|
2615
|
+
async function runI18n(positionals, flags) {
|
|
2616
|
+
const first = positionals[0];
|
|
2617
|
+
const check = first && isCheck(first) ? first : "audit";
|
|
2618
|
+
const dirArgs = (first && isCheck(first) ? positionals.slice(1) : positionals).filter(Boolean);
|
|
2619
|
+
const asJson = boolFlag(flags, "json");
|
|
2620
|
+
const strict = boolFlag(flags, "strict");
|
|
2621
|
+
let audit;
|
|
2622
|
+
try {
|
|
2623
|
+
audit = await import("@urbicon-ui/i18n/audit");
|
|
2624
|
+
} catch (error) {
|
|
2625
|
+
printError(`cannot load @urbicon-ui/i18n/audit — is @urbicon-ui/i18n installed? (${error.message})`);
|
|
2626
|
+
return EXIT.FAIL;
|
|
2627
|
+
}
|
|
2628
|
+
let config;
|
|
2629
|
+
try {
|
|
2630
|
+
config = await loadConfig(flags, dirArgs);
|
|
2631
|
+
} catch (error) {
|
|
2632
|
+
printError(error.message);
|
|
2633
|
+
return EXIT.USAGE;
|
|
2634
|
+
}
|
|
2635
|
+
const wantsParity = check === "parity" || check === "audit";
|
|
2636
|
+
const wantsUnused = check === "unused" || check === "audit";
|
|
2637
|
+
const wantsHardcoded = check === "hardcoded" || check === "audit";
|
|
2638
|
+
let sources = [];
|
|
2639
|
+
if (wantsUnused || wantsHardcoded) {
|
|
2640
|
+
const files = (await Promise.all(config.sources.map((dir) => collectFiles(resolve6(dir), [".ts", ".js", ".svelte"])))).flat();
|
|
2641
|
+
sources = await Promise.all(files.map(async (file) => ({ file: label(file), code: await readFile6(file, "utf-8") })));
|
|
2642
|
+
}
|
|
2643
|
+
let groups = [];
|
|
2644
|
+
let bundleErrors = [];
|
|
2645
|
+
if (wantsParity || wantsUnused) {
|
|
2646
|
+
const loaded = await loadBundleGroups(config.translations, audit.isLocaleSupported);
|
|
2647
|
+
groups = loaded.groups;
|
|
2648
|
+
bundleErrors = loaded.errors;
|
|
2649
|
+
}
|
|
2650
|
+
const sections = {};
|
|
2651
|
+
if (wantsParity)
|
|
2652
|
+
sections.parity = runParity(audit, config, groups);
|
|
2653
|
+
if (wantsUnused)
|
|
2654
|
+
sections.unused = await runUnused(audit, config, groups, sources);
|
|
2655
|
+
if (wantsHardcoded)
|
|
2656
|
+
sections.hardcoded = await runHardcoded(audit, config, sources);
|
|
2657
|
+
const totalErrors = Object.values(sections).reduce((sum, s) => sum + s.errors, 0);
|
|
2658
|
+
const totalWarnings = Object.values(sections).reduce((sum, s) => sum + s.warnings, 0);
|
|
2659
|
+
const bundleFailed = (wantsParity || wantsUnused) && bundleErrors.length > 0;
|
|
2660
|
+
const failed = totalErrors > 0 || bundleFailed || strict && totalWarnings > 0;
|
|
2661
|
+
if (asJson) {
|
|
2662
|
+
console.log(JSON.stringify({
|
|
2663
|
+
ok: !failed,
|
|
2664
|
+
check,
|
|
2665
|
+
strict,
|
|
2666
|
+
bundleErrors,
|
|
2667
|
+
...Object.fromEntries(Object.entries(sections).map(([k, v]) => [k, v.json]))
|
|
2668
|
+
}, null, 2));
|
|
2669
|
+
return failed ? EXIT.FAIL : EXIT.OK;
|
|
2670
|
+
}
|
|
2671
|
+
for (const error of bundleErrors)
|
|
2672
|
+
printError(error);
|
|
2673
|
+
for (const [name, section] of Object.entries(sections)) {
|
|
2674
|
+
console.log(`
|
|
2675
|
+
${name}:`);
|
|
2676
|
+
if (section.lines.length === 0)
|
|
2677
|
+
console.log(" ✓ no findings");
|
|
2678
|
+
else
|
|
2679
|
+
for (const line of section.lines)
|
|
2680
|
+
console.log(line);
|
|
2681
|
+
}
|
|
2682
|
+
const bundleNote = bundleErrors.length ? `, ${bundleErrors.length} bundle error(s)` : "";
|
|
2683
|
+
console.log(`
|
|
2684
|
+
${totalErrors} error(s), ${totalWarnings} advisory finding(s)${bundleNote}${strict ? " (--strict: advisory gates)" : ""}.`);
|
|
2685
|
+
if (failed)
|
|
2686
|
+
console.log("FAIL.");
|
|
2687
|
+
return failed ? EXIT.FAIL : EXIT.OK;
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2404
2690
|
// src/cli/commands/init.ts
|
|
2405
|
-
import { mkdir, readFile as
|
|
2406
|
-
import { dirname as dirname5, join as
|
|
2691
|
+
import { mkdir, readFile as readFile8, writeFile as writeFile2 } from "node:fs/promises";
|
|
2692
|
+
import { dirname as dirname5, join as join4, relative as relative3, resolve as resolve8 } from "node:path";
|
|
2407
2693
|
|
|
2408
2694
|
// src/cli/package-root.ts
|
|
2409
|
-
import { readFile as
|
|
2410
|
-
import { dirname as dirname4, resolve as
|
|
2695
|
+
import { readFile as readFile7 } from "node:fs/promises";
|
|
2696
|
+
import { dirname as dirname4, resolve as resolve7 } from "node:path";
|
|
2411
2697
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2412
2698
|
async function findPackageRoot() {
|
|
2413
2699
|
let dir = dirname4(fileURLToPath2(import.meta.url));
|
|
2414
2700
|
for (let i = 0;i < 6; i++) {
|
|
2415
2701
|
try {
|
|
2416
|
-
const pkg = JSON.parse(await
|
|
2702
|
+
const pkg = JSON.parse(await readFile7(resolve7(dir, "package.json"), "utf-8"));
|
|
2417
2703
|
if (pkg.name === "@urbicon-ui/design")
|
|
2418
2704
|
return dir;
|
|
2419
2705
|
} catch {}
|
|
@@ -2452,11 +2738,11 @@ async function readTemplate(name) {
|
|
|
2452
2738
|
const root = await findPackageRoot();
|
|
2453
2739
|
if (!root)
|
|
2454
2740
|
throw new Error("could not locate the @urbicon-ui/design package root");
|
|
2455
|
-
return
|
|
2741
|
+
return readFile8(join4(root, "templates", name), "utf-8");
|
|
2456
2742
|
}
|
|
2457
2743
|
async function readOrNull(path) {
|
|
2458
2744
|
try {
|
|
2459
|
-
return await
|
|
2745
|
+
return await readFile8(path, "utf-8");
|
|
2460
2746
|
} catch {
|
|
2461
2747
|
return null;
|
|
2462
2748
|
}
|
|
@@ -2471,14 +2757,14 @@ function upsertBlock(existing, block) {
|
|
|
2471
2757
|
const content = existing.slice(0, startIdx) + block.trim() + existing.slice(endIdx + BLOCK_END.length);
|
|
2472
2758
|
return { content, replaced: true };
|
|
2473
2759
|
}
|
|
2474
|
-
const
|
|
2760
|
+
const sep3 = existing.length === 0 ? "" : existing.endsWith(`
|
|
2475
2761
|
|
|
2476
2762
|
`) ? "" : existing.endsWith(`
|
|
2477
2763
|
`) ? `
|
|
2478
2764
|
` : `
|
|
2479
2765
|
|
|
2480
2766
|
`;
|
|
2481
|
-
return { content: `${existing}${
|
|
2767
|
+
return { content: `${existing}${sep3}${block.trim()}
|
|
2482
2768
|
`, replaced: false };
|
|
2483
2769
|
}
|
|
2484
2770
|
async function mergeHook(settingsPath) {
|
|
@@ -2507,7 +2793,7 @@ async function mergeHook(settingsPath) {
|
|
|
2507
2793
|
}
|
|
2508
2794
|
async function runInit(_positionals, flags) {
|
|
2509
2795
|
const cwd = process.cwd();
|
|
2510
|
-
const rel = (p) =>
|
|
2796
|
+
const rel = (p) => relative3(cwd, p) || p;
|
|
2511
2797
|
const done = [];
|
|
2512
2798
|
const skipped = [];
|
|
2513
2799
|
let block;
|
|
@@ -2517,7 +2803,7 @@ async function runInit(_positionals, flags) {
|
|
|
2517
2803
|
printError(err.message);
|
|
2518
2804
|
return EXIT.FAIL;
|
|
2519
2805
|
}
|
|
2520
|
-
const agentsPath =
|
|
2806
|
+
const agentsPath = resolve8(stringFlag(flags, "agents-file") ?? "AGENTS.md");
|
|
2521
2807
|
const existingAgents = await readOrNull(agentsPath) ?? "";
|
|
2522
2808
|
let upserted;
|
|
2523
2809
|
try {
|
|
@@ -2537,7 +2823,7 @@ async function runInit(_positionals, flags) {
|
|
|
2537
2823
|
done.push(`${rel(manifestPath)} — scaffolded`);
|
|
2538
2824
|
}
|
|
2539
2825
|
if (boolFlag(flags, "hook")) {
|
|
2540
|
-
const settingsPath =
|
|
2826
|
+
const settingsPath = resolve8(".claude", "settings.json");
|
|
2541
2827
|
try {
|
|
2542
2828
|
const result = await mergeHook(settingsPath);
|
|
2543
2829
|
(result === "added" ? done : skipped).push(`${rel(settingsPath)} — ${result === "added" ? "wired" : "already has"} the PostToolUse \`urbicon hook\``);
|
|
@@ -2546,7 +2832,7 @@ async function runInit(_positionals, flags) {
|
|
|
2546
2832
|
}
|
|
2547
2833
|
}
|
|
2548
2834
|
if (boolFlag(flags, "ci")) {
|
|
2549
|
-
const ciPath =
|
|
2835
|
+
const ciPath = resolve8(".github", "workflows", "design-gate.yml");
|
|
2550
2836
|
if (await readOrNull(ciPath)) {
|
|
2551
2837
|
skipped.push(`${rel(ciPath)} — already present`);
|
|
2552
2838
|
} else {
|
|
@@ -2658,9 +2944,9 @@ async function runSyncManifest(_positionals, flags) {
|
|
|
2658
2944
|
}
|
|
2659
2945
|
|
|
2660
2946
|
// src/cli/commands/validate.ts
|
|
2661
|
-
import { readdir as
|
|
2662
|
-
import { join as
|
|
2663
|
-
var
|
|
2947
|
+
import { readdir as readdir3, readFile as readFile9, stat as stat3 } from "node:fs/promises";
|
|
2948
|
+
import { join as join5, relative as relative4, resolve as resolve9, sep as sep3 } from "node:path";
|
|
2949
|
+
var SKIP_DIRS3 = new Set([
|
|
2664
2950
|
"node_modules",
|
|
2665
2951
|
".svelte-kit",
|
|
2666
2952
|
".git",
|
|
@@ -2670,27 +2956,27 @@ var SKIP_DIRS2 = new Set([
|
|
|
2670
2956
|
".turbo",
|
|
2671
2957
|
"coverage"
|
|
2672
2958
|
]);
|
|
2673
|
-
var
|
|
2674
|
-
function
|
|
2675
|
-
return
|
|
2959
|
+
var MAX_DEPTH3 = 24;
|
|
2960
|
+
function label2(abs) {
|
|
2961
|
+
return relative4(process.cwd(), abs).split(sep3).join("/") || abs;
|
|
2676
2962
|
}
|
|
2677
2963
|
async function collectSvelte(dir, depth = 0) {
|
|
2678
|
-
if (depth >
|
|
2964
|
+
if (depth > MAX_DEPTH3)
|
|
2679
2965
|
return [];
|
|
2680
2966
|
let entries;
|
|
2681
2967
|
try {
|
|
2682
|
-
entries = await
|
|
2968
|
+
entries = await readdir3(dir, { withFileTypes: true });
|
|
2683
2969
|
} catch {
|
|
2684
2970
|
return [];
|
|
2685
2971
|
}
|
|
2686
2972
|
const files = [];
|
|
2687
2973
|
for (const entry of entries) {
|
|
2688
2974
|
if (entry.isDirectory()) {
|
|
2689
|
-
if (
|
|
2975
|
+
if (SKIP_DIRS3.has(entry.name) || entry.name.startsWith("."))
|
|
2690
2976
|
continue;
|
|
2691
|
-
files.push(...await collectSvelte(
|
|
2977
|
+
files.push(...await collectSvelte(join5(dir, entry.name), depth + 1));
|
|
2692
2978
|
} else if (entry.isFile() && entry.name.endsWith(".svelte")) {
|
|
2693
|
-
files.push(
|
|
2979
|
+
files.push(join5(dir, entry.name));
|
|
2694
2980
|
}
|
|
2695
2981
|
}
|
|
2696
2982
|
return files;
|
|
@@ -2711,20 +2997,20 @@ async function gather(positionals) {
|
|
|
2711
2997
|
units.push({ label: "<stdin>", code: await readStdin2() });
|
|
2712
2998
|
continue;
|
|
2713
2999
|
}
|
|
2714
|
-
const abs =
|
|
3000
|
+
const abs = resolve9(p);
|
|
2715
3001
|
let info;
|
|
2716
3002
|
try {
|
|
2717
|
-
info = await
|
|
3003
|
+
info = await stat3(abs);
|
|
2718
3004
|
} catch {
|
|
2719
3005
|
printError(`cannot read "${p}"`);
|
|
2720
3006
|
return null;
|
|
2721
3007
|
}
|
|
2722
3008
|
if (info.isDirectory()) {
|
|
2723
3009
|
for (const file of await collectSvelte(abs)) {
|
|
2724
|
-
units.push({ label:
|
|
3010
|
+
units.push({ label: label2(file), code: await readFile9(file, "utf-8") });
|
|
2725
3011
|
}
|
|
2726
3012
|
} else {
|
|
2727
|
-
units.push({ label:
|
|
3013
|
+
units.push({ label: label2(abs), code: await readFile9(abs, "utf-8") });
|
|
2728
3014
|
}
|
|
2729
3015
|
}
|
|
2730
3016
|
return units;
|
|
@@ -2785,7 +3071,7 @@ ${reports.length} file(s) · ${totals.error} error(s), ` + `${totals.warning} wa
|
|
|
2785
3071
|
}
|
|
2786
3072
|
if (extraTokens.length > 0) {
|
|
2787
3073
|
console.log(`
|
|
2788
|
-
${extraTokens.length} project token override(s) applied from ${
|
|
3074
|
+
${extraTokens.length} project token override(s) applied from ${relative4(process.cwd(), manifestPath).split(sep3).join("/")}.`);
|
|
2789
3075
|
}
|
|
2790
3076
|
if (gate.slopBreaches.length > 0) {
|
|
2791
3077
|
console.log(`
|
|
@@ -2802,12 +3088,12 @@ FAIL — ${reason}.`);
|
|
|
2802
3088
|
}
|
|
2803
3089
|
|
|
2804
3090
|
// src/cli/commands/verb.ts
|
|
2805
|
-
import { readdir as
|
|
2806
|
-
import { resolve as
|
|
3091
|
+
import { readdir as readdir4, readFile as readFile10 } from "node:fs/promises";
|
|
3092
|
+
import { resolve as resolve10 } from "node:path";
|
|
2807
3093
|
var SAFE_VERB = /^[a-z][a-z0-9-]*$/;
|
|
2808
3094
|
async function resolveVerbsDir() {
|
|
2809
3095
|
const root = await findPackageRoot();
|
|
2810
|
-
return root ?
|
|
3096
|
+
return root ? resolve10(root, "skill", "verbs") : null;
|
|
2811
3097
|
}
|
|
2812
3098
|
function purposeOf(body) {
|
|
2813
3099
|
const heading = body.split(`
|
|
@@ -2823,7 +3109,7 @@ async function runVerbList(_positionals, _flags) {
|
|
|
2823
3109
|
}
|
|
2824
3110
|
let files;
|
|
2825
3111
|
try {
|
|
2826
|
-
files = (await
|
|
3112
|
+
files = (await readdir4(dir)).filter((f) => f.endsWith(".md")).sort();
|
|
2827
3113
|
} catch {
|
|
2828
3114
|
printError(`no verb recipes found at ${dir}`);
|
|
2829
3115
|
return EXIT.FAIL;
|
|
@@ -2833,7 +3119,7 @@ async function runVerbList(_positionals, _flags) {
|
|
|
2833
3119
|
const name = file.replace(/\.md$/, "");
|
|
2834
3120
|
let purpose = "";
|
|
2835
3121
|
try {
|
|
2836
|
-
purpose = purposeOf(await
|
|
3122
|
+
purpose = purposeOf(await readFile10(resolve10(dir, file), "utf-8"));
|
|
2837
3123
|
} catch {}
|
|
2838
3124
|
console.log(` ${name.padEnd(10)} ${purpose}`);
|
|
2839
3125
|
}
|
|
@@ -2855,7 +3141,7 @@ async function runVerb(positionals, _flags) {
|
|
|
2855
3141
|
return EXIT.FAIL;
|
|
2856
3142
|
}
|
|
2857
3143
|
try {
|
|
2858
|
-
console.log(await
|
|
3144
|
+
console.log(await readFile10(resolve10(dir, `${name}.md`), "utf-8"));
|
|
2859
3145
|
return EXIT.OK;
|
|
2860
3146
|
} catch {
|
|
2861
3147
|
printError(`unknown verb "${name}" — list the available verbs with \`urbicon verbs\``);
|
|
@@ -2917,6 +3203,18 @@ Commands:
|
|
|
2917
3203
|
--src <dir> Source tree to scan (default ./src).
|
|
2918
3204
|
--manifest <path> Manifest file (default ./design.manifest.md).
|
|
2919
3205
|
--json Emit the scan result as JSON.
|
|
3206
|
+
i18n [check] [dirs…] Audit @urbicon-ui/i18n usage. check = parity | unused |
|
|
3207
|
+
hardcoded | audit (all, default). Run under Bun.
|
|
3208
|
+
--translations <d> Locale-bundle dir(s), comma-separated
|
|
3209
|
+
(default src/lib/translations).
|
|
3210
|
+
--config <path> i18n.audit.json (default ./i18n.audit.json).
|
|
3211
|
+
--dynamic-keys <g> Key globs built dynamically (errors.*).
|
|
3212
|
+
--ignore-keys <g> Key globs to skip entirely.
|
|
3213
|
+
--ignore-strings Hardcoded-string globs to skip.
|
|
3214
|
+
--base-locale <l> Parity base (default en).
|
|
3215
|
+
--json / --strict Machine-readable / gate advisory too.
|
|
3216
|
+
Gates on parity errors + used-but-undefined; unused,
|
|
3217
|
+
hardcoded and parity warnings are advisory.
|
|
2920
3218
|
verbs List the design verbs (recipes over the design loop).
|
|
2921
3219
|
verb <name> Print one verb recipe, e.g. "urbicon verb compose".
|
|
2922
3220
|
help Show this help.
|
|
@@ -2938,7 +3236,7 @@ async function readVersion() {
|
|
|
2938
3236
|
if (!root)
|
|
2939
3237
|
return "unknown";
|
|
2940
3238
|
try {
|
|
2941
|
-
const pkg = JSON.parse(await
|
|
3239
|
+
const pkg = JSON.parse(await readFile11(resolve11(root, "package.json"), "utf-8"));
|
|
2942
3240
|
return pkg.version ?? "unknown";
|
|
2943
3241
|
} catch {
|
|
2944
3242
|
return "unknown";
|
|
@@ -2971,6 +3269,8 @@ async function main(argv) {
|
|
|
2971
3269
|
return runRecordDecision(positionals, flags);
|
|
2972
3270
|
case "sync-manifest":
|
|
2973
3271
|
return runSyncManifest(positionals, flags);
|
|
3272
|
+
case "i18n":
|
|
3273
|
+
return runI18n(positionals, flags);
|
|
2974
3274
|
case "verbs":
|
|
2975
3275
|
return runVerbList(positionals, flags);
|
|
2976
3276
|
case "verb":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@urbicon-ui/design",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.9",
|
|
4
4
|
"description": "The urbicon CLI — version-pinned design validation and design-manifest tooling for projects built with Urbicon UI. Wraps @urbicon-ui/design-engine for editor hooks and CI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
],
|
|
31
31
|
"sideEffects": false,
|
|
32
32
|
"scripts": {
|
|
33
|
-
"build": "bun build ./src/cli/index.ts --target node --outfile dist/cli.js",
|
|
33
|
+
"build": "bun build ./src/cli/index.ts --target node --outfile dist/cli.js --external @urbicon-ui/i18n",
|
|
34
34
|
"dev": "bun run --watch src/cli/index.ts",
|
|
35
35
|
"start": "bun run src/cli/index.ts",
|
|
36
36
|
"check": "tsc --noEmit",
|
|
@@ -38,8 +38,9 @@
|
|
|
38
38
|
"test:run": "vitest run"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@urbicon-ui/design-content": "6.3.
|
|
42
|
-
"@urbicon-ui/design-engine": "6.3.
|
|
41
|
+
"@urbicon-ui/design-content": "6.3.9",
|
|
42
|
+
"@urbicon-ui/design-engine": "6.3.9",
|
|
43
|
+
"@urbicon-ui/i18n": "6.3.9"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
46
|
"typescript": "^6.0.3",
|
package/templates/ci-github.yml
CHANGED
|
@@ -24,3 +24,9 @@ jobs:
|
|
|
24
24
|
# fails below the floor); add --record to append a drift point to the
|
|
25
25
|
# design.manifest.history.ndjson sidecar.
|
|
26
26
|
- run: bunx urbicon validate src/ --json
|
|
27
|
+
# i18n audit — gates on translation-parity errors + keys used in code but
|
|
28
|
+
# missing from the bundles. Unused keys and hardcoded UI strings are
|
|
29
|
+
# advisory (add --strict to gate them too). Loads .ts locale bundles, so it
|
|
30
|
+
# needs Bun; point --translations at your bundle dir(s). Drop this step if
|
|
31
|
+
# the project does not use @urbicon-ui/i18n.
|
|
32
|
+
- run: bunx urbicon i18n audit src/ --translations src/lib/translations
|