youiyoux 0.1.6 → 0.1.8
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/chunks/{chunk-RHXBYN4W.js → chunk-3HON5Q5K.js} +20 -15
- package/dist/chunks/{chunk-XAO3LS7M.js → chunk-6GITMASZ.js} +1 -1
- package/dist/chunks/{chunk-W2KFNGQP.js → chunk-BARSW3JO.js} +947 -228
- package/dist/chunks/{chunk-5YVH6NHT.js → chunk-DCBF7QQY.js} +4 -4
- package/dist/chunks/{chunk-DP54SKL4.js → chunk-DWYDHVLZ.js} +87 -11
- package/dist/chunks/{chunk-5Y2MVIGH.js → chunk-F33PKGES.js} +2 -2
- package/dist/chunks/{chunk-VOSWAWKC.js → chunk-FLBRX6Y3.js} +94 -23
- package/dist/chunks/{chunk-S57F4CMN.js → chunk-HDJ2TOWZ.js} +1 -1
- package/dist/chunks/{chunk-BHJCAZXO.js → chunk-Y4IBE6WS.js} +3 -3
- package/dist/chunks/{src-URZDH3QH.js → src-BYNWPNRT.js} +13 -1
- package/dist/chunks/{src-7RRKYIAS.js → src-DPFJ4S2N.js} +2 -2
- package/dist/chunks/{src-VKQJF7KR.js → src-EUVBYTKY.js} +4 -4
- package/dist/chunks/{src-6HTTOVZ5.js → src-M3OJMCUE.js} +5 -3
- package/dist/chunks/{src-4VYLEZ3Z.js → src-PURAHRCR.js} +6 -2
- package/dist/chunks/{src-DZWWMUDR.js → src-YKPJ3PII.js} +6 -6
- package/dist/chunks/{src-YYI52B5R.js → src-ZG7JU6YJ.js} +2 -2
- package/dist/hub/assets/{CandidatePanel-CnbK4S0j.js → CandidatePanel-ZzbsyZo3.js} +1 -1
- package/dist/hub/assets/{CatalogScreen-B5OVL-AR.js → CatalogScreen-DDYlvOHy.js} +1 -1
- package/dist/hub/assets/{ChangesScreen-GwGqJwxN.js → ChangesScreen-C8_EyHgJ.js} +1 -1
- package/dist/hub/assets/{CheatSheet-BLSOq-xa.js → CheatSheet-DfcPiXdS.js} +1 -1
- package/dist/hub/assets/{CommandPalette-CAWQfdXJ.js → CommandPalette-DfFkDTVg.js} +1 -1
- package/dist/hub/assets/DiscoverScreen-D6NTgEgk.js +2 -0
- package/dist/hub/assets/{DraftEditor-yIDapXTs.js → DraftEditor-CUNGYbg0.js} +2 -2
- package/dist/hub/assets/{GapsScreen-DXZAlm6v.js → GapsScreen-CqedCj4z.js} +1 -1
- package/dist/hub/assets/{HomeScreen-CEpiVhOa.js → HomeScreen-CIj1QTkw.js} +1 -1
- package/dist/hub/assets/{InventoryScreen-CEG3Pmw_.js → InventoryScreen-CyvFXa2_.js} +1 -1
- package/dist/hub/assets/{LibraryScreen-IaVnxrX9.js → LibraryScreen-C4WU0f42.js} +1 -1
- package/dist/hub/assets/{Opened-DJyf4Hnj.js → Opened-CeRWkA7c.js} +1 -1
- package/dist/hub/assets/{Preview-C069zVMP.js → Preview-CgW-FMB7.js} +1 -1
- package/dist/hub/assets/{RuleDetail-B4g2-TRe.js → RuleDetail-BDhZ1gCd.js} +1 -1
- package/dist/hub/assets/{RulesScreen-CAaXIWjO.js → RulesScreen-D3rSo_z2.js} +1 -1
- package/dist/hub/assets/{ScreenImage-DcI7_G3O.js → ScreenImage-DLeA3nRh.js} +1 -1
- package/dist/hub/assets/{ScreensScreen-DwczCjZD.js → ScreensScreen-bGuFIyWj.js} +1 -1
- package/dist/hub/assets/{SearchField-DaCpNAj6.js → SearchField-DtEgq9-Q.js} +1 -1
- package/dist/hub/assets/{ServiceDown-Bv-BPXro.js → ServiceDown-Bn6AcA3J.js} +1 -1
- package/dist/hub/assets/{SpecimenCapture-DlsjshVE.js → SpecimenCapture-p6YErF5y.js} +1 -1
- package/dist/hub/assets/{Specimens-C-lHp7sr.js → Specimens-DuaE0WeO.js} +2 -2
- package/dist/hub/assets/{StackScreen-DkmELsqR.js → StackScreen-DHDulDng.js} +1 -1
- package/dist/hub/assets/{StubScreens-Bt7lWLVK.js → StubScreens-CmhVNX_Z.js} +1 -1
- package/dist/hub/assets/StyleScreen-LwTqfGID.js +1 -0
- package/dist/hub/assets/{Tabs-1QQDS0I6.js → Tabs-Cbh9BCKW.js} +1 -1
- package/dist/hub/assets/{TokenDetail-DvCw6GqC.js → TokenDetail-BICobLK1.js} +1 -1
- package/dist/hub/assets/{TokensScreen-DoENV8Xy.js → TokensScreen-CyTHn55q.js} +1 -1
- package/dist/hub/assets/WorkspaceScreen-uPWqfklh.js +1 -0
- package/dist/hub/assets/{client-D7whEyep.js → client-BXHiuV_w.js} +1 -1
- package/dist/hub/assets/color-C6bxX90C.js +1 -0
- package/dist/hub/assets/contrast-B-uhyF_b.js +1 -0
- package/dist/hub/assets/{discover-XjJEViwk.js → discover-CFhyKler.js} +1 -1
- package/dist/hub/assets/{dist-BYQYaPBV.js → dist-C6iTWL0b.js} +1 -1
- package/dist/hub/assets/{dist-Bk4srtDn.js → dist-DYNYuTjI.js} +1 -1
- package/dist/hub/assets/{dist-D0z7Bvsy.js → dist-VmEpVQjP.js} +1 -1
- package/dist/hub/assets/{family-tiles-CyTdy_cP.js → family-tiles-B8_onXUi.js} +1 -1
- package/dist/hub/assets/folder-tree-YghfTmlZ.js +1 -0
- package/dist/hub/assets/main-CfbpuT4Y.js +2 -0
- package/dist/hub/assets/{try-live-zX3CSd3P.js → try-live-CX04d2L2.js} +1 -1
- package/dist/hub/assets/values-cLekxBZj.js +1 -0
- package/dist/hub/assets/workflow-DbBdiOmA.js +1 -0
- package/dist/hub/index.html +2 -1
- package/dist/hub-previews.js +5 -5
- package/dist/hub-server.js +3 -3
- package/dist/hub-snapshot.js +9 -8
- package/dist/schema/youi.config.schema.json +4 -0
- package/dist/schema/youi.project.schema.json +1 -1
- package/dist/schema/youi.status.schema.json +306 -0
- package/dist/schema/youi.workspace.schema.json +31 -0
- package/dist/youi.js +6 -6
- package/dist/youiyoux.js +6 -6
- package/package.json +4 -4
- package/dist/hub/assets/DiscoverScreen-Tl_Z6mgY.js +0 -2
- package/dist/hub/assets/StyleScreen-tq1tbRBo.js +0 -1
- package/dist/hub/assets/contrast-BkNHUAPa.js +0 -1
- package/dist/hub/assets/main-B9CIkk5m.js +0 -2
- package/dist/hub/assets/values-DzxR_TlI.js +0 -1
- package/dist/hub/assets/workflow-jpV8wh-U.js +0 -1
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
captureText,
|
|
14
14
|
checkCapabilities,
|
|
15
15
|
chosenSession,
|
|
16
|
+
collectComponents,
|
|
16
17
|
compareToBaseline,
|
|
17
18
|
componentCard,
|
|
18
19
|
createBaseline,
|
|
@@ -50,9 +51,11 @@ import {
|
|
|
50
51
|
shotsText,
|
|
51
52
|
substituteVars,
|
|
52
53
|
vendoredPath,
|
|
53
|
-
whereUsed
|
|
54
|
-
|
|
54
|
+
whereUsed,
|
|
55
|
+
withFile
|
|
56
|
+
} from "./chunk-3HON5Q5K.js";
|
|
55
57
|
import {
|
|
58
|
+
escapingLinks,
|
|
56
59
|
within
|
|
57
60
|
} from "./chunk-42YJX3W5.js";
|
|
58
61
|
import {
|
|
@@ -64,7 +67,7 @@ import {
|
|
|
64
67
|
libraryOfPackage,
|
|
65
68
|
packageOf,
|
|
66
69
|
playwrightCli
|
|
67
|
-
} from "./chunk-
|
|
70
|
+
} from "./chunk-HDJ2TOWZ.js";
|
|
68
71
|
import {
|
|
69
72
|
MissingIndexError,
|
|
70
73
|
buildGaps,
|
|
@@ -88,13 +91,17 @@ import {
|
|
|
88
91
|
showFromCatalog,
|
|
89
92
|
storeLocation,
|
|
90
93
|
syncIndex
|
|
91
|
-
} from "./chunk-
|
|
94
|
+
} from "./chunk-6GITMASZ.js";
|
|
92
95
|
import {
|
|
96
|
+
DL_EXT,
|
|
93
97
|
buildTokens,
|
|
94
98
|
compareParity,
|
|
95
99
|
cssValue,
|
|
96
|
-
|
|
97
|
-
|
|
100
|
+
importTokens,
|
|
101
|
+
parseCssColor,
|
|
102
|
+
parseDimension,
|
|
103
|
+
readRules
|
|
104
|
+
} from "./chunk-FLBRX6Y3.js";
|
|
98
105
|
import {
|
|
99
106
|
Baseline,
|
|
100
107
|
DESIGN_CONFIG_FILE,
|
|
@@ -108,11 +115,14 @@ import {
|
|
|
108
115
|
PACKAGE_NAME,
|
|
109
116
|
ProjectConfig,
|
|
110
117
|
ScanReport,
|
|
118
|
+
WORKSPACE_FILE,
|
|
119
|
+
WorkspaceConfig,
|
|
111
120
|
copyleft,
|
|
112
121
|
executionLabel,
|
|
113
122
|
findFiles,
|
|
114
123
|
findUp,
|
|
115
124
|
getSystem,
|
|
125
|
+
implementationsDir,
|
|
116
126
|
loadDesignRepo,
|
|
117
127
|
loadLibrary,
|
|
118
128
|
matchesAny,
|
|
@@ -128,17 +138,17 @@ import {
|
|
|
128
138
|
shortHash,
|
|
129
139
|
toPosix,
|
|
130
140
|
writeFileAtomic
|
|
131
|
-
} from "./chunk-
|
|
141
|
+
} from "./chunk-DWYDHVLZ.js";
|
|
132
142
|
import {
|
|
133
143
|
__toESM
|
|
134
144
|
} from "./chunk-WNTS7ZO4.js";
|
|
135
145
|
|
|
136
146
|
// ../cli/src/main.ts
|
|
137
|
-
var
|
|
147
|
+
var import_yaml4 = __toESM(require_dist(), 1);
|
|
138
148
|
import { execFile as execFile5 } from "node:child_process";
|
|
139
149
|
import { existsSync as existsSync5 } from "node:fs";
|
|
140
|
-
import { appendFile as appendFile2, readFile as
|
|
141
|
-
import { basename as
|
|
150
|
+
import { appendFile as appendFile2, readFile as readFile10, rm as rm6, rmdir as rmdir3 } from "node:fs/promises";
|
|
151
|
+
import { basename as basename5, dirname as dirname13, join as join25, relative as relative11, resolve as resolve15 } from "node:path";
|
|
142
152
|
import { parseArgs, promisify as promisify5 } from "node:util";
|
|
143
153
|
|
|
144
154
|
// ../cli/src/check.ts
|
|
@@ -156,7 +166,7 @@ import { join as join2, relative, resolve } from "node:path";
|
|
|
156
166
|
|
|
157
167
|
// ../cli/src/command.ts
|
|
158
168
|
var COMMAND = "youiyoux";
|
|
159
|
-
var SUBCOMMANDS = ["show", "where", "bom", "check", "baseline", "rules", "plan", "migrate", "init", "sync", "link", "unlink", "scan", "watch", "hooks", "agents", "mcp", "ui", "library", "discover", "index", "report", "render", "screens", "requests", "detect", "build", "parity", "schema", "inventory", "gaps", "help"];
|
|
169
|
+
var SUBCOMMANDS = ["show", "where", "bom", "check", "baseline", "rules", "plan", "migrate", "init", "adopt", "status", "system", "sync", "link", "unlink", "scan", "watch", "hooks", "agents", "mcp", "ui", "library", "discover", "index", "report", "render", "screens", "requests", "detect", "build", "parity", "schema", "inventory", "gaps", "help"];
|
|
160
170
|
|
|
161
171
|
// ../cli/src/agents.ts
|
|
162
172
|
var import_yaml = __toESM(require_dist(), 1);
|
|
@@ -206,7 +216,7 @@ function enumSummary(contract) {
|
|
|
206
216
|
return Object.entries(contract.props).filter(([, spec]) => spec.type === "enum").map(([name, spec]) => `${name}: ${spec.values.join("|")}`).join("; ");
|
|
207
217
|
}
|
|
208
218
|
function importHint(config) {
|
|
209
|
-
return config.importFrom ? `\`${config.importFrom}\`` : `\`${config.componentsDir}\` (by a relative path from your file)`;
|
|
219
|
+
return config.importFrom ? `\`${withFile(config.importFrom)}\`` : `\`${config.componentsDir}\` (by a relative path from your file)`;
|
|
210
220
|
}
|
|
211
221
|
function agentsBlock(project2) {
|
|
212
222
|
const { config, system } = project2;
|
|
@@ -260,7 +270,7 @@ function frontmatter(project2, build) {
|
|
|
260
270
|
if (dark !== colors[key(t)]) colors[`${key(t)}-dark`] = dark;
|
|
261
271
|
}
|
|
262
272
|
const typography = {};
|
|
263
|
-
for (const t of byGroup("typography")) {
|
|
273
|
+
for (const t of byGroup("typography").filter((t2) => t2.type === "typography")) {
|
|
264
274
|
const v = t.light;
|
|
265
275
|
typography[key(t)] = {
|
|
266
276
|
fontFamily: cssValue("fontFamily", v.fontFamily),
|
|
@@ -466,8 +476,9 @@ async function initProject(options) {
|
|
|
466
476
|
}
|
|
467
477
|
notes.push(`owned components: ${componentsDir}`);
|
|
468
478
|
const alias = await aliasRoot(root);
|
|
469
|
-
const
|
|
470
|
-
|
|
479
|
+
const barrel = (await Promise.all(["index.ts", "index.tsx", "index.js"].map((f) => exists(join2(root, componentsDir, f))))).some(Boolean);
|
|
480
|
+
const importFrom = alias !== void 0 && componentsDir.startsWith(alias) ? `@/${componentsDir.slice(alias.length)}${barrel ? "" : "/{file}"}` : void 0;
|
|
481
|
+
if (importFrom) notes.push(`imports use the @/ alias: ${importFrom}${barrel ? "" : " (each component from its own file; no index barrel)"}`);
|
|
471
482
|
const include = (await Promise.all(
|
|
472
483
|
[
|
|
473
484
|
["app", "app/**/*.{ts,tsx,css}"],
|
|
@@ -491,7 +502,8 @@ async function initProject(options) {
|
|
|
491
502
|
componentsDir,
|
|
492
503
|
...importFrom ? { importFrom } : {},
|
|
493
504
|
routes: expo ? { strategy: "expo-router", appDir } : { strategy: "none" },
|
|
494
|
-
...prefix ? { outputs: { cssVarPrefix: prefix } } : {}
|
|
505
|
+
...options.outputs ? { outputs: options.outputs } : prefix ? { outputs: { cssVarPrefix: prefix } } : {},
|
|
506
|
+
...options.tokenSources && options.tokenSources.length > 0 ? { tokenSources: [...options.tokenSources] } : {}
|
|
495
507
|
});
|
|
496
508
|
await writeFileAtomic(file, `${JSON.stringify(config, null, 2)}
|
|
497
509
|
`);
|
|
@@ -617,7 +629,7 @@ var DRIFT_SEVERITY = { "near-duplicate": "warn", "hardcoded-value": "error", "of
|
|
|
617
629
|
async function importOf(project2) {
|
|
618
630
|
const alias = await aliasRoot(project2.root, project2.config.tsconfig);
|
|
619
631
|
return (file, fromFile) => {
|
|
620
|
-
if (project2.system && file.startsWith(`${project2.config.componentsDir}/`)) return importSpecifier(project2.config, fromFile);
|
|
632
|
+
if (project2.system && file.startsWith(`${project2.config.componentsDir}/`)) return importSpecifier(project2.config, fromFile, file);
|
|
621
633
|
if (alias !== void 0 && file.startsWith(alias)) return `@/${file.slice(alias.length).replace(/\.(tsx?|jsx?|mjs)$/, "").replace(/\/index$/, "")}`;
|
|
622
634
|
const path = posix.relative(posix.dirname(fromFile), file).replace(/\.(tsx?|jsx?|mjs|astro)$/, "").replace(/\/index$/, "");
|
|
623
635
|
return path.startsWith(".") ? path : `./${path}`;
|
|
@@ -634,7 +646,7 @@ async function chosenLibraries(project2, added, notes) {
|
|
|
634
646
|
const chosen = /* @__PURE__ */ new Map();
|
|
635
647
|
for (const { contract } of project2.system?.components ?? []) {
|
|
636
648
|
const type = EXPORT_TYPES[contract.name];
|
|
637
|
-
if (type && !chosen.has(type)) chosen.set(type, { library: "system", label: `the System's ${contract.name} (${contract.id})`, fix: (from) => `import { ${contract.name} } from "${importSpecifier(project2.config, from)}"` });
|
|
649
|
+
if (type && !chosen.has(type)) chosen.set(type, { library: "system", label: `the System's ${contract.name} (${contract.id})`, fix: (from) => `import { ${contract.name} } from "${importSpecifier(project2.config, from, contract.source.file)}"` });
|
|
638
650
|
}
|
|
639
651
|
let hits;
|
|
640
652
|
try {
|
|
@@ -917,23 +929,24 @@ var Writer = class {
|
|
|
917
929
|
}
|
|
918
930
|
async link(file, target) {
|
|
919
931
|
const path = this.path(file);
|
|
920
|
-
const
|
|
921
|
-
if (
|
|
922
|
-
if (
|
|
932
|
+
const stat4 = await lstat(path).catch(() => null);
|
|
933
|
+
if (stat4?.isSymbolicLink() && await readlink(path) === target) return this.record({ file, role: "agents", action: "unchanged" });
|
|
934
|
+
if (stat4 && !stat4.isSymbolicLink()) return this.record({ file, role: "agents", action: "skipped", detail: "a real directory is in the way" });
|
|
923
935
|
if (!this.options.dryRun) {
|
|
924
936
|
await mkdir(dirname2(path), { recursive: true });
|
|
925
|
-
if (
|
|
937
|
+
if (stat4) await rm(path);
|
|
926
938
|
await symlink(target, path);
|
|
927
939
|
}
|
|
928
|
-
this.record({ file, role: "agents", action:
|
|
940
|
+
this.record({ file, role: "agents", action: stat4 ? "updated" : "created", detail: `\u2192 ${target}` });
|
|
929
941
|
}
|
|
930
942
|
};
|
|
931
943
|
async function syncProject(project2, options = {}) {
|
|
932
944
|
const { config, system, repo } = project2;
|
|
933
945
|
const previous = await readLock(project2.root);
|
|
934
946
|
const writer = new Writer(project2, options, previous);
|
|
947
|
+
const implementations = implementationsDir(system.config);
|
|
935
948
|
for (const { contract } of system.components) {
|
|
936
|
-
if (await readTextOrNull(join6(repo.root,
|
|
949
|
+
if (await readTextOrNull(join6(repo.root, implementations, contract.source.file)) === null) throw new Error(`${implementations}/${contract.source.file} is named by ${contract.id} but does not exist; nothing was written`);
|
|
937
950
|
}
|
|
938
951
|
const build = await buildTokens(repo.root, system.config, config.outputs, { contracts: system.components.map((c) => c.contract) });
|
|
939
952
|
for (const file of build.files) await writer.generated(`${config.outputs.dir}/${file.path}`, file.content, "tokens");
|
|
@@ -952,11 +965,15 @@ async function syncProject(project2, options = {}) {
|
|
|
952
965
|
for (const support of system.config.support) if (!sources.has(support)) sources.set(support, { elements: system.components.map((c) => c.contract.id), version: system.config.version });
|
|
953
966
|
const upstreams = /* @__PURE__ */ new Map();
|
|
954
967
|
for (const source of sources.keys()) {
|
|
955
|
-
const upstream = await readTextOrNull(join6(repo.root,
|
|
956
|
-
if (upstream === null) throw new Error(`${
|
|
968
|
+
const upstream = await readTextOrNull(join6(repo.root, implementations, source));
|
|
969
|
+
if (upstream === null) throw new Error(`${implementations}/${source} is named by a contract but does not exist`);
|
|
957
970
|
upstreams.set(source, upstream);
|
|
958
971
|
}
|
|
959
|
-
for (const [source, { elements, version }] of sources)
|
|
972
|
+
for (const [source, { elements, version }] of sources) {
|
|
973
|
+
const file = vendoredPath(project2, source);
|
|
974
|
+
if (await sameFile(join6(repo.root, implementations, source), join6(project2.root, file))) writer.record({ file, role: "component", action: "unchanged", detail: "the system's own implementation, in this project" });
|
|
975
|
+
else await writer.owned(file, upstreams.get(source), elements, version);
|
|
976
|
+
}
|
|
960
977
|
await writer.retire();
|
|
961
978
|
const lock = {
|
|
962
979
|
schema: "youi.lock/v1",
|
|
@@ -970,6 +987,10 @@ async function syncProject(project2, options = {}) {
|
|
|
970
987
|
`);
|
|
971
988
|
return { actions: writer.actions, lock, unknownTools: unknownToolMentions(project2) };
|
|
972
989
|
}
|
|
990
|
+
async function sameFile(a, b) {
|
|
991
|
+
const [x, y] = await Promise.all([realpath(a).catch(() => null), realpath(b).catch(() => null)]);
|
|
992
|
+
return x !== null && x === y;
|
|
993
|
+
}
|
|
973
994
|
function mcpArgs(project2) {
|
|
974
995
|
const rel = (path) => relative3(project2.root, path) || ".";
|
|
975
996
|
return [
|
|
@@ -1039,7 +1060,7 @@ async function runCheck(project2, options = {}) {
|
|
|
1039
1060
|
const { report } = scan;
|
|
1040
1061
|
const checked = new Set(scan.files);
|
|
1041
1062
|
const unmatched = scope ? [...scope].filter((f) => !checked.has(f)) : [];
|
|
1042
|
-
const
|
|
1063
|
+
const failed2 = report.coverage.failed.filter((f) => !scope || scope.has(f.file) || !/\.(tsx?|css)$/.test(f.file));
|
|
1043
1064
|
const wantedRule = (id) => !options.rule || id.endsWith(`/rule/${options.rule}`);
|
|
1044
1065
|
const violations = report.violations.filter((v) => wantedRule(v.rule) && (!options.quiet || v.severity === "error"));
|
|
1045
1066
|
const notRun = failedChecks(report.checks).filter((c) => wantedRule(c.rule));
|
|
@@ -1050,7 +1071,7 @@ async function runCheck(project2, options = {}) {
|
|
|
1050
1071
|
const failing = baseline ? [...baseline.comparison.new, ...baseline.comparison.regressed] : violations;
|
|
1051
1072
|
const errors = failing.filter((v) => v.severity === "error").length + (lock ?? []).filter((i) => i.severity === "error").length;
|
|
1052
1073
|
const drift = diff ? await analyseChange(project2, scan, diff, { violations }) : null;
|
|
1053
|
-
return { identity: scan.identity, scan, scope, unmatched, failed, notRun, violations, lock, baseline, errors, incomplete: unmatched.length > 0 ||
|
|
1074
|
+
return { identity: scan.identity, scan, scope, unmatched, failed: failed2, notRun, violations, lock, baseline, errors, incomplete: unmatched.length > 0 || failed2.length > 0 || notRun.length > 0 || (drift?.incomplete.length ?? 0) > 0, diff: drift };
|
|
1054
1075
|
}
|
|
1055
1076
|
async function lockIssues(project2) {
|
|
1056
1077
|
const lock = await verifyLock(project2);
|
|
@@ -1760,18 +1781,18 @@ async function runClaudeHook(input, target = {}) {
|
|
|
1760
1781
|
try {
|
|
1761
1782
|
const project2 = await load(target, dirname5(file));
|
|
1762
1783
|
const real = (path) => realpath3(path).catch(() => path);
|
|
1763
|
-
const
|
|
1764
|
-
if (!isChecked(project2,
|
|
1765
|
-
const found = await changeFor(() => workingTreeChange(project2, void 0, [
|
|
1784
|
+
const relative12 = relPath(await real(project2.root), await real(file));
|
|
1785
|
+
if (!isChecked(project2, relative12)) return SILENT;
|
|
1786
|
+
const found = await changeFor(() => workingTreeChange(project2, void 0, [relative12]));
|
|
1766
1787
|
if (found.problem !== void 0) return claudeContext(found.problem, project2.root);
|
|
1767
1788
|
const { change } = found;
|
|
1768
1789
|
const result = configured(project2) ? await runCheck(project2, { files: [file], lock: false, baseline: await baselineFor(project2, target), diff: change }) : await runRepoCheck(project2, change);
|
|
1769
1790
|
const lines = findingLines(result, project2.root);
|
|
1770
|
-
const see = COMPONENT_FILE.test(
|
|
1771
|
-
if (lines.length === 0) return see ? claudeContext(`youiyoux: ${
|
|
1791
|
+
const see = COMPONENT_FILE.test(relative12) ? `See it: youiyoux render ${quoted(relative12)} (or the MCP tool ui_render), then open the images: light and dark, desktop and phone, accessibility issues and the diff against the base.` : null;
|
|
1792
|
+
if (lines.length === 0) return see ? claudeContext(`youiyoux: ${relative12} passes the design-system check. ${see}`, project2.root) : SILENT;
|
|
1772
1793
|
const older = "violations" in result ? result.violations.filter((v) => !isAdded(change.added, v.file, v.line)).length : 0;
|
|
1773
1794
|
const known = older > 0 ? ` (${older} older findings on unchanged lines omitted)` : "";
|
|
1774
|
-
return claudeContext(`youiyoux design-system check (advisory) on ${
|
|
1795
|
+
return claudeContext(`youiyoux design-system check (advisory) on ${relative12}${known}:
|
|
1775
1796
|
${lines.join("\n")}${!configured(project2) ? `
|
|
1776
1797
|
${result.diff?.notes.join("\n")}` : ""}
|
|
1777
1798
|
Fix these before finishing (each "Fix:" says what to write); \`youiyoux show <component>\` and the youiyoux MCP tools give the legal values. CI runs the authoritative check.${see ? `
|
|
@@ -1919,30 +1940,30 @@ var serialize = ({ data, indent, newline }) => `${JSON.stringify(data, null, ind
|
|
|
1919
1940
|
async function installClaudeMcp(repoRoot, server) {
|
|
1920
1941
|
const file = join11(repoRoot, CLAUDE_MCP);
|
|
1921
1942
|
const before = await readTextOrNull(file);
|
|
1922
|
-
const
|
|
1923
|
-
const servers =
|
|
1924
|
-
const next2 = { ...
|
|
1943
|
+
const json4 = readJson2(file, before);
|
|
1944
|
+
const servers = json4.data.mcpServers ?? {};
|
|
1945
|
+
const next2 = { ...json4, data: { ...json4.data, mcpServers: { ...servers, [MCP_NAME]: { command: server.command, args: server.args } } } };
|
|
1925
1946
|
return write(file, serialize(next2), before, `Claude Code starts the ${MCP_NAME} MCP server in this repo (it asks once before trusting a project .mcp.json); the command uses the installed tool or portable npx invocation; youiyoux sync writes the same entry`);
|
|
1926
1947
|
}
|
|
1927
1948
|
async function removeClaudeMcp(repoRoot) {
|
|
1928
1949
|
const file = join11(repoRoot, CLAUDE_MCP);
|
|
1929
1950
|
const before = await readTextOrNull(file);
|
|
1930
1951
|
if (before === null) return { file, action: "unchanged", guarantee: `no ${MCP_NAME} server here` };
|
|
1931
|
-
const
|
|
1932
|
-
const servers = { ...
|
|
1952
|
+
const json4 = readJson2(file, before);
|
|
1953
|
+
const servers = { ...json4.data.mcpServers ?? {} };
|
|
1933
1954
|
if (!(MCP_NAME in servers)) return { file, action: "unchanged", guarantee: `no ${MCP_NAME} server here` };
|
|
1934
1955
|
delete servers[MCP_NAME];
|
|
1935
|
-
const { mcpServers: _, ...rest } =
|
|
1936
|
-
const data = Object.keys(servers).length > 0 ? { ...
|
|
1937
|
-
return removeOrWrite(file, Object.keys(data).length > 0 ? serialize({ ...
|
|
1956
|
+
const { mcpServers: _, ...rest } = json4.data;
|
|
1957
|
+
const data = Object.keys(servers).length > 0 ? { ...json4.data, mcpServers: servers } : rest;
|
|
1958
|
+
return removeOrWrite(file, Object.keys(data).length > 0 ? serialize({ ...json4, data }) : "", `${MCP_NAME} removed; other servers kept`);
|
|
1938
1959
|
}
|
|
1939
1960
|
var tomlString = (value) => JSON.stringify(value);
|
|
1940
1961
|
function codexBlock(server) {
|
|
1941
1962
|
return [TOML_BEGIN, `[mcp_servers.${MCP_NAME}]`, `command = ${tomlString(server.command)}`, `args = [${server.args.map(tomlString).join(", ")}]`, TOML_END].join("\n");
|
|
1942
1963
|
}
|
|
1943
1964
|
function foreignTable(text) {
|
|
1944
|
-
const
|
|
1945
|
-
return new RegExp(`^\\s*\\[\\s*mcp_servers\\s*\\.\\s*"?${MCP_NAME}"?\\s*\\]`, "m").test(
|
|
1965
|
+
const inside3 = withoutBlock(text, TOML_BEGIN, TOML_END) ?? text;
|
|
1966
|
+
return new RegExp(`^\\s*\\[\\s*mcp_servers\\s*\\.\\s*"?${MCP_NAME}"?\\s*\\]`, "m").test(inside3);
|
|
1946
1967
|
}
|
|
1947
1968
|
async function installCodexMcp(repoRoot, server) {
|
|
1948
1969
|
const file = join11(repoRoot, CODEX_CONFIG);
|
|
@@ -2066,7 +2087,7 @@ import { homedir, userInfo } from "node:os";
|
|
|
2066
2087
|
import { join as join12, resolve as resolve6 } from "node:path";
|
|
2067
2088
|
async function swapImpact(candidate, family) {
|
|
2068
2089
|
try {
|
|
2069
|
-
const { discoverIndexPath: discoverIndexPath2, sharedStore: sharedStore2 } = await import("./src-
|
|
2090
|
+
const { discoverIndexPath: discoverIndexPath2, sharedStore: sharedStore2 } = await import("./src-DPFJ4S2N.js");
|
|
2070
2091
|
const path = discoverIndexPath2();
|
|
2071
2092
|
if (!process.env.DATABASE_URL && !existsSync(path)) return null;
|
|
2072
2093
|
const impact = await (await sharedStore2(path, process.env, { readOnly: true })).swapImpact(candidate, family);
|
|
@@ -2077,7 +2098,7 @@ async function swapImpact(candidate, family) {
|
|
|
2077
2098
|
}
|
|
2078
2099
|
async function libraryLicence(id) {
|
|
2079
2100
|
try {
|
|
2080
|
-
const { discoverCacheDir: discoverCacheDir2, findListing } = await import("./src-
|
|
2101
|
+
const { discoverCacheDir: discoverCacheDir2, findListing } = await import("./src-DPFJ4S2N.js");
|
|
2081
2102
|
const { entry } = await findListing(discoverCacheDir2(void 0, process.env), id);
|
|
2082
2103
|
return entry.id === id ? entry.license : null;
|
|
2083
2104
|
} catch {
|
|
@@ -2085,7 +2106,7 @@ async function libraryLicence(id) {
|
|
|
2085
2106
|
}
|
|
2086
2107
|
}
|
|
2087
2108
|
async function startRuleEditor(options) {
|
|
2088
|
-
const { FileStore, RuleWorkflow, serveWorkflow } = await import("./src-
|
|
2109
|
+
const { FileStore, RuleWorkflow, serveWorkflow } = await import("./src-EUVBYTKY.js");
|
|
2089
2110
|
const projectRoot = resolve6(options.project);
|
|
2090
2111
|
const load2 = { ...options.config ? { configPath: resolve6(options.config) } : {}, ...options.design ? { designRoot: resolve6(options.design) } : {}, ...options.root ? { productRoot: resolve6(options.root) } : {} };
|
|
2091
2112
|
const project2 = await loadProject(projectRoot, load2);
|
|
@@ -2126,7 +2147,7 @@ function builtHubPlan(toolRoot, options, out) {
|
|
|
2126
2147
|
if (options.watch && !namesSystem(options)) throw new Error("--watch keeps a system's snapshot live: name it with --design or --project (the demo system is not watched)");
|
|
2127
2148
|
const hub = join13(toolRoot, BUILT_HUB);
|
|
2128
2149
|
const snapshot = flags.length > 0 ? [join13(toolRoot, "hub-snapshot.js"), "--out", out, ...flags] : null;
|
|
2129
|
-
const server = [join13(toolRoot, "hub-server.js"), "--dist", hub, "--kit", join13(hub, "kit"), "--snapshot", snapshot ? out : join13(toolRoot, BUILT_DEMO), ...snapshot ? ["--build-frame"] : [], ...options.port ? ["--port", options.port] : [], ...options.api ? ["--api", options.api] : [], ...options.watch ? ["--live"] : []];
|
|
2150
|
+
const server = [join13(toolRoot, "hub-server.js"), "--dist", hub, "--kit", join13(hub, "kit"), "--snapshot", snapshot ? out : join13(toolRoot, BUILT_DEMO), ...snapshot ? ["--build-frame"] : [], ...options.port ? ["--port", options.port] : [], ...options.api ? ["--api", options.api] : [], ...options.watch || options.workspace ? ["--live"] : []];
|
|
2130
2151
|
return { snapshot, server, watch: options.watch && snapshot ? [...snapshot, "--watch"] : null };
|
|
2131
2152
|
}
|
|
2132
2153
|
function run5(args, cwd, env, started) {
|
|
@@ -2151,6 +2172,7 @@ async function runBuiltHub(toolRoot, options) {
|
|
|
2151
2172
|
const code = await run5(plan.snapshot, process.cwd(), {}, (child) => children.add(child));
|
|
2152
2173
|
if (code !== 0) return code;
|
|
2153
2174
|
}
|
|
2175
|
+
options.workspace?.((project2) => run5(builtHubPlan(toolRoot, { ...options, project: project2 }, out).snapshot, process.cwd(), {}, (child) => children.add(child)));
|
|
2154
2176
|
if (plan.watch) children.add(spawn(process.execPath, plan.watch, { cwd: process.cwd(), stdio: "inherit" }));
|
|
2155
2177
|
return await run5(plan.server, process.cwd(), {}, (child) => children.add(child));
|
|
2156
2178
|
} finally {
|
|
@@ -2170,6 +2192,7 @@ async function runHub(toolRoot, options) {
|
|
|
2170
2192
|
const code = await run5(plan.snapshot, process.cwd(), {});
|
|
2171
2193
|
if (code !== 0) return code;
|
|
2172
2194
|
}
|
|
2195
|
+
options.workspace?.((project2) => run5(hubPlan(toolRoot, { ...options, project: project2 }).snapshot, process.cwd(), {}));
|
|
2173
2196
|
const watcher = plan.watch ? spawn(process.execPath, plan.watch, { cwd: process.cwd(), stdio: "inherit" }) : null;
|
|
2174
2197
|
try {
|
|
2175
2198
|
return await run5(plan.vite, plan.web, plan.env);
|
|
@@ -2217,15 +2240,587 @@ function renderRequests(briefs, status, unreadable) {
|
|
|
2217
2240
|
return lines.join("\n\n");
|
|
2218
2241
|
}
|
|
2219
2242
|
|
|
2243
|
+
// ../cli/src/adopt.ts
|
|
2244
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
2245
|
+
import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute5, join as join14, posix as posix3, relative as relative6, resolve as resolve8 } from "node:path";
|
|
2246
|
+
var import_yaml2 = __toESM(require_dist(), 1);
|
|
2247
|
+
var OUT_DIR = "design/build";
|
|
2248
|
+
var DARK_BLOCKS = [
|
|
2249
|
+
{ selector: ".dark" },
|
|
2250
|
+
{ selector: ":root.dark" },
|
|
2251
|
+
{ selector: '[data-theme="dark"]' },
|
|
2252
|
+
{ selector: ':root[data-theme="dark"]' },
|
|
2253
|
+
{ selector: ":root", outer: "@media (prefers-color-scheme: dark)" },
|
|
2254
|
+
{ selector: ':root:not([data-theme="light"])', outer: "@media (prefers-color-scheme: dark)" }
|
|
2255
|
+
];
|
|
2256
|
+
var kebab = (name) => name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
2257
|
+
var pascal = (file) => basename2(file).replace(/\.[^.]+$/, "").split(/[-_.]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
2258
|
+
var json = (value) => `${JSON.stringify(value, null, 2)}
|
|
2259
|
+
`;
|
|
2260
|
+
function slugOf(name) {
|
|
2261
|
+
return name.replace(/^@[^/]+\//, "").toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^[^a-z]+/, "").replace(/-+$/, "") || "app";
|
|
2262
|
+
}
|
|
2263
|
+
function inside2(root, path, what) {
|
|
2264
|
+
const normal = posix3.normalize(path.split("\\").join("/")).replace(/^\.\//, "").replace(/\/+$/, "");
|
|
2265
|
+
if (isAbsolute5(normal) || normal === ".." || normal.startsWith("../") || !within(root, resolve8(root, normal))) throw new Error(`${what} ${JSON.stringify(path)} leads outside ${root}: adopt reads and writes only inside the product`);
|
|
2266
|
+
return normal;
|
|
2267
|
+
}
|
|
2268
|
+
function exportedNames(source) {
|
|
2269
|
+
const names = /* @__PURE__ */ new Set();
|
|
2270
|
+
for (const match of source.matchAll(/^export\s+(?:declare\s+)?(?:const|let|var|function\*?|class|async\s+function)\s+([A-Za-z_$][\w$]*)/gm)) names.add(match[1]);
|
|
2271
|
+
for (const match of source.matchAll(/^export\s+\{([^}]*)\}/gm)) {
|
|
2272
|
+
for (const part of match[1].split(",")) {
|
|
2273
|
+
const spec = part.trim();
|
|
2274
|
+
if (!spec || spec.startsWith("type ")) continue;
|
|
2275
|
+
names.add(spec.split(/\s+as\s+/).at(-1).trim());
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
return [...names];
|
|
2279
|
+
}
|
|
2280
|
+
function defaultVariants(source) {
|
|
2281
|
+
const body = /defaultVariants\s*:\s*\{([^}]*)\}/.exec(source)?.[1] ?? "";
|
|
2282
|
+
return Object.fromEntries([...body.matchAll(/([A-Za-z_$][\w$]*)\s*:\s*["'`]([^"'`]+)["'`]/g)].map((m) => [m[1], m[2]]));
|
|
2283
|
+
}
|
|
2284
|
+
function propsOf(facts, defaults) {
|
|
2285
|
+
const props = {};
|
|
2286
|
+
for (const prop of facts?.props ?? []) {
|
|
2287
|
+
if (prop.values && prop.values.length > 0) props[prop.name] = { type: "enum", values: [...prop.values], required: false, ...defaults[prop.name] && prop.values.includes(defaults[prop.name]) ? { default: defaults[prop.name] } : {} };
|
|
2288
|
+
else if (prop.name === "className") props[prop.name] = { type: "string", required: false, description: "Extra classes, merged last." };
|
|
2289
|
+
else if (prop.name === "children") props[prop.name] = { type: "node", required: false };
|
|
2290
|
+
else if (/^on[A-Z]/.test(prop.name)) props[prop.name] = { type: "function", required: false };
|
|
2291
|
+
}
|
|
2292
|
+
return props;
|
|
2293
|
+
}
|
|
2294
|
+
function generatedOf(file, css) {
|
|
2295
|
+
const head = css.slice(0, 400);
|
|
2296
|
+
if (!head.includes(GENERATED_MARK)) return null;
|
|
2297
|
+
const from = /@generated by youi from the (.+?) design language v(\d+\.\d+\.\d+(?:-[\w.-]+?)?)\./.exec(head);
|
|
2298
|
+
return { file, count: rootVariables(css).length, system: from?.[1] ?? null, version: from?.[2] ?? null };
|
|
2299
|
+
}
|
|
2300
|
+
async function stylesheetOf(root, asked, fork) {
|
|
2301
|
+
const generated = [];
|
|
2302
|
+
let forked = null;
|
|
2303
|
+
const usable = async (file) => {
|
|
2304
|
+
const css = await readTextOrNull(join14(root, file));
|
|
2305
|
+
if (css === null) return false;
|
|
2306
|
+
const made = generatedOf(file, css);
|
|
2307
|
+
if (made && !generated.some((g) => g.file === file)) generated.push(made);
|
|
2308
|
+
if (made && fork) forked = made;
|
|
2309
|
+
return !made || fork;
|
|
2310
|
+
};
|
|
2311
|
+
if (asked) {
|
|
2312
|
+
if (!await usable(asked) && generated.length > 0) throw new Error(`${asked} is generated by youiyoux sync${generated[0].system ? ` from ${generated[0].system}` : ""}: name the stylesheet the product writes its own variables in, or take that System's values as the product's own with youiyoux adopt --fork`);
|
|
2313
|
+
return { file: asked, generated, forked };
|
|
2314
|
+
}
|
|
2315
|
+
const shadcn = (await readJson(join14(root, "components.json")))?.tailwind?.css;
|
|
2316
|
+
if (typeof shadcn === "string" && await usable(shadcn)) return { file: posix3.normalize(shadcn), generated, forked };
|
|
2317
|
+
let best = null;
|
|
2318
|
+
for (const file of await findFiles(root, ["**/*.css"], ["**/build/**", "**/out/**", "**/.next/**", `${OUT_DIR}/**`])) {
|
|
2319
|
+
const css = await readTextOrNull(join14(root, file)) ?? "";
|
|
2320
|
+
const count = rootVariables(css).length;
|
|
2321
|
+
if (count === 0) continue;
|
|
2322
|
+
const made = generatedOf(file, css);
|
|
2323
|
+
if (made && !generated.some((g) => g.file === file)) generated.push(made);
|
|
2324
|
+
if (made && !fork) continue;
|
|
2325
|
+
if (!best || count > best.count) best = { file, count, made };
|
|
2326
|
+
}
|
|
2327
|
+
return { file: best?.file ?? null, generated, forked: best?.made ?? forked };
|
|
2328
|
+
}
|
|
2329
|
+
function rootVariables(css) {
|
|
2330
|
+
return readRules(css).filter((r) => r.preludes.length === 1 && r.preludes[0] === ":root").flatMap((r) => r.declarations).filter((d) => d.property.startsWith("--") && !d.property.startsWith("---")).map((d) => ({ name: d.property.slice(2), value: d.value }));
|
|
2331
|
+
}
|
|
2332
|
+
var NOT_A_PREFIX = /* @__PURE__ */ new Set(["color", "colors", "bg", "fg", "radius", "rounded", "font", "text", "shadow", "space", "spacing", "size", "ease", "easing", "duration", "motion", "z", "leading", "tracking", "border", "ring", "chart", "sidebar", "background", "foreground", "primary", "secondary", "muted", "accent", "destructive", "card", "popover", "input", "typography", "brand", "gray", "grey", "neutral"]);
|
|
2333
|
+
function commonPrefix(names) {
|
|
2334
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2335
|
+
for (const name of names) if (name.includes("-")) counts.set(name.split("-")[0], (counts.get(name.split("-")[0]) ?? 0) + 1);
|
|
2336
|
+
const [word, count] = [...counts].sort((a, b) => b[1] - a[1])[0] ?? ["", 0];
|
|
2337
|
+
return count >= 3 && count >= names.length * 0.8 && /^[a-z][a-z0-9]*$/.test(word) && !NOT_A_PREFIX.has(word) ? word : "";
|
|
2338
|
+
}
|
|
2339
|
+
function lengthPath(name, value) {
|
|
2340
|
+
if (parseDimension(value) === null && !/^var\(/.test(value)) return null;
|
|
2341
|
+
if (/radius/.test(name)) return `radius.${name.replace(/^radius-(?=.)/, "")}`;
|
|
2342
|
+
if (/^typography-/.test(name)) return `typography.${name.slice("typography-".length)}`;
|
|
2343
|
+
if (/^(?:text|font)-|-(?:font-size|line-height|letter-spacing|font-weight)$/.test(name)) return `typography.${name}`;
|
|
2344
|
+
return null;
|
|
2345
|
+
}
|
|
2346
|
+
function variableNames(tree, path = []) {
|
|
2347
|
+
return Object.entries(tree).flatMap(([key2, node]) => {
|
|
2348
|
+
if (key2.startsWith("$")) return [];
|
|
2349
|
+
if (node && typeof node === "object" && "$value" in node) {
|
|
2350
|
+
const css = node.$extensions?.[DL_EXT]?.css;
|
|
2351
|
+
return [css ?? [...path, key2].join("-")];
|
|
2352
|
+
}
|
|
2353
|
+
return variableNames(node, [...path, key2]);
|
|
2354
|
+
});
|
|
2355
|
+
}
|
|
2356
|
+
function moveDeclarations(css, blocks, moved, importPath) {
|
|
2357
|
+
const rules = readRules(css).filter((r) => blocks.some((b) => r.preludes.at(-1) === b.selector && (b.outer ? r.preludes.length === 2 && r.preludes[0] === b.outer : r.preludes.length === 1)));
|
|
2358
|
+
if (rules.length === 0) return null;
|
|
2359
|
+
const lines = css.split("\n");
|
|
2360
|
+
const drop = /* @__PURE__ */ new Set();
|
|
2361
|
+
for (const rule2 of rules) {
|
|
2362
|
+
for (const d of rule2.declarations) {
|
|
2363
|
+
const line = lines[d.line - 1] ?? "";
|
|
2364
|
+
if (moved.has(d.property.slice(2)) && new RegExp(`^\\s*${d.property.replace(/[-]/g, "\\-")}\\s*:[^;]*;?\\s*$`).test(line)) drop.add(d.line - 1);
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
if (drop.size === 0) return null;
|
|
2368
|
+
let content = lines.filter((_, i) => !drop.has(i)).join("\n");
|
|
2369
|
+
for (const block of blocks) content = content.replace(new RegExp(`(^|\\n)[ \\t]*${block.selector.replace(/[.[\]()"=:]/g, "\\$&")}\\s*\\{\\s*\\}[ \\t]*(?=\\n|$)`, "g"), "");
|
|
2370
|
+
for (const outer of new Set(blocks.flatMap((b) => b.outer ? [b.outer] : []))) content = content.replace(new RegExp(`(^|\\n)[ \\t]*${outer.replace(/[.[\]()"=:]/g, "\\$&")}\\s*\\{\\s*\\}[ \\t]*(?=\\n|$)`, "g"), "");
|
|
2371
|
+
const statement = `@import "${importPath}";`;
|
|
2372
|
+
const imports = [...content.matchAll(/^@import[^;]*;[ \t]*$/gm)];
|
|
2373
|
+
const at = imports.length > 0 ? imports.at(-1).index + imports.at(-1)[0].length : 0;
|
|
2374
|
+
content = at > 0 ? `${content.slice(0, at)}
|
|
2375
|
+
${statement}${content.slice(at)}` : `${statement}
|
|
2376
|
+
${content}`;
|
|
2377
|
+
return { content: content.replace(/\n{3,}/g, "\n\n"), moved: drop.size };
|
|
2378
|
+
}
|
|
2379
|
+
async function planAdoption(options) {
|
|
2380
|
+
const root = options.root;
|
|
2381
|
+
const pkg = await readJson(join14(root, "package.json"));
|
|
2382
|
+
const name = typeof pkg?.name === "string" ? pkg.name : basename2(root);
|
|
2383
|
+
const id = options.system ?? slugOf(name);
|
|
2384
|
+
if (!/^[a-z][a-z0-9-]*$/.test(id)) throw new Error(`"${id}" cannot be a system id: use lowercase letters, digits and hyphens, starting with a letter (--system <id>)`);
|
|
2385
|
+
const notes = [];
|
|
2386
|
+
const facts = await collectComponents(root);
|
|
2387
|
+
const chosen = options.componentsDir ?? facts.shadcnUi ?? await uiFolder(root) ?? facts.componentsDir;
|
|
2388
|
+
const componentsDir = chosen ? inside2(root, chosen, "the components folder") : null;
|
|
2389
|
+
if (!componentsDir || !await exists(join14(root, componentsDir))) throw new Error("no folder of components found: pass --components <dir> (the folder your app imports its UI components from)");
|
|
2390
|
+
const inDir = (file) => posix3.dirname(file) === componentsDir;
|
|
2391
|
+
const files = (await findFiles(root, [`${componentsDir}/*.{ts,tsx,js,jsx}`])).filter((f) => !/^index\./.test(posix3.basename(f)) && !/\.(test|spec|stories|d)\.[^.]+$/.test(f));
|
|
2392
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
2393
|
+
for (const c of facts.components) if (c.definition.kind === "react" && inDir(c.definition.file)) byFile.set(c.definition.file, [...byFile.get(c.definition.file) ?? [], c]);
|
|
2394
|
+
const out = [];
|
|
2395
|
+
const components = [];
|
|
2396
|
+
const support = [];
|
|
2397
|
+
const RULES = ["color.no-raw-color", "tokens.no-arbitrary-values", "components.canonical-import"].map((r) => `youi:${id}/rule/${r}`);
|
|
2398
|
+
for (const file of files) {
|
|
2399
|
+
const defined = (byFile.get(file) ?? []).filter((c) => /^[A-Z]/.test(c.export));
|
|
2400
|
+
const source = await readFile4(join14(root, file), "utf8");
|
|
2401
|
+
if (defined.length === 0) {
|
|
2402
|
+
support.push(posix3.basename(file));
|
|
2403
|
+
continue;
|
|
2404
|
+
}
|
|
2405
|
+
const main2 = defined.find((c) => c.export === pascal(file)) ?? defined[0];
|
|
2406
|
+
const folder = kebab(main2.export);
|
|
2407
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(folder)) {
|
|
2408
|
+
notes.push(`${file}: ${main2.export} has no contract (its name is not plain letters and digits); write one by hand`);
|
|
2409
|
+
continue;
|
|
2410
|
+
}
|
|
2411
|
+
const exportsOf = exportedNames(source);
|
|
2412
|
+
const sites = main2.sites.length;
|
|
2413
|
+
const contract = {
|
|
2414
|
+
schema: "youi.component/v1",
|
|
2415
|
+
id: `youi:${id}/component/${folder}`,
|
|
2416
|
+
name: main2.export,
|
|
2417
|
+
version: "0.1.0",
|
|
2418
|
+
status: "draft",
|
|
2419
|
+
summary: `The product's own ${main2.export} (${file}), used at ${sites} ${sites === 1 ? "site" : "sites"}.`.slice(0, 200),
|
|
2420
|
+
source: { file: posix3.basename(file), export: main2.export, also: defined.filter((c) => c !== main2).map((c) => c.export) },
|
|
2421
|
+
platforms: facts.dependencies.includes("react-native") && !facts.dependencies.includes("react-dom") ? ["ios", "android"] : ["web"],
|
|
2422
|
+
props: propsOf(main2, defaultVariants(source)),
|
|
2423
|
+
tokens: {},
|
|
2424
|
+
a11y: { notes: [] },
|
|
2425
|
+
rules: RULES,
|
|
2426
|
+
guidance: { do: [], dont: [] },
|
|
2427
|
+
examples: [],
|
|
2428
|
+
aliases: exportsOf.filter((n) => !/^[A-Z]/.test(n)),
|
|
2429
|
+
owners: []
|
|
2430
|
+
};
|
|
2431
|
+
out.push({ path: `design/components/${folder}/${folder}.contract.json`, content: json(contract) });
|
|
2432
|
+
components.push(main2.export);
|
|
2433
|
+
}
|
|
2434
|
+
if (components.length === 0) throw new Error(`${componentsDir} exports no React components: pass --components <dir> (the folder your app imports its UI components from)`);
|
|
2435
|
+
const found = await stylesheetOf(root, options.css, options.fork === true);
|
|
2436
|
+
const stylesheet = found.file ? inside2(root, found.file, "the stylesheet") : null;
|
|
2437
|
+
const css = stylesheet ? await readTextOrNull(join14(root, stylesheet)) : null;
|
|
2438
|
+
if (stylesheet && css === null) throw new Error(`${stylesheet} does not exist (--css <file>: the stylesheet that defines your :root variables)`);
|
|
2439
|
+
const darkBlocks = css ? DARK_BLOCKS.filter((b) => readRules(css).some((r) => r.preludes.at(-1) === b.selector && (b.outer ? r.preludes.length === 2 && r.preludes[0] === b.outer : r.preludes.length === 1) && r.declarations.some((d) => d.property.startsWith("--")))) : [];
|
|
2440
|
+
const darkBlock = darkBlocks[0];
|
|
2441
|
+
const variables = css ? rootVariables(css) : [];
|
|
2442
|
+
const prefix = commonPrefix(variables.map((v) => v.name));
|
|
2443
|
+
const full = (name2) => prefix ? `${prefix}-${name2}` : name2;
|
|
2444
|
+
const own = variables.filter((v) => !prefix || v.name.startsWith(`${prefix}-`)).map((v) => ({ name: prefix ? v.name.slice(prefix.length + 1) : v.name, value: v.value }));
|
|
2445
|
+
const unprefixed = variables.filter((v) => prefix && !v.name.startsWith(`${prefix}-`)).map((v) => v.name);
|
|
2446
|
+
if (unprefixed.length > 0) notes.push(`${plural(unprefixed.length, "variable")} without the --${prefix}- prefix stay in ${stylesheet}: ${unprefixed.slice(0, 4).map((n) => `--${n}`).join(", ")}${unprefixed.length > 4 ? ", \u2026" : ""}`);
|
|
2447
|
+
const paths = Object.fromEntries(own.flatMap((v) => lengthPath(v.name, v.value) ? [[v.name, lengthPath(v.name, v.value)]] : []));
|
|
2448
|
+
const imported = css ? importTokens({ css: { css, prefix, light: { selector: ":root" }, ...darkBlock ? { dark: darkBlock } : {} }, paths }) : null;
|
|
2449
|
+
const tokenCount = imported?.report.fromCss.length ?? 0;
|
|
2450
|
+
const hasDark = Boolean(imported && Object.keys(imported.dark).length > 0);
|
|
2451
|
+
out.push({ path: `design/tokens/${id}.tokens.json`, content: json(imported?.base ?? {}) });
|
|
2452
|
+
if (hasDark) out.push({ path: `design/tokens/${id}.dark.tokens.json`, content: json(imported.dark) });
|
|
2453
|
+
out.push({
|
|
2454
|
+
path: `design/tokens/${id}.resolver.json`,
|
|
2455
|
+
content: json({
|
|
2456
|
+
$schema: "https://www.designtokens.org/schemas/2025.10/resolver.json",
|
|
2457
|
+
name,
|
|
2458
|
+
version: "2025.10",
|
|
2459
|
+
description: `${name}'s tokens, adopted from ${stylesheet ?? "no stylesheet"}.`,
|
|
2460
|
+
sets: { base: { sources: [{ $ref: `${id}.tokens.json` }] } },
|
|
2461
|
+
...hasDark ? { modifiers: { theme: { contexts: { light: [], dark: [{ $ref: `${id}.dark.tokens.json` }] }, default: "light" } } } : {},
|
|
2462
|
+
resolutionOrder: [{ $ref: "#/sets/base" }, ...hasDark ? [{ $ref: "#/modifiers/theme" }] : []]
|
|
2463
|
+
})
|
|
2464
|
+
});
|
|
2465
|
+
if (!stylesheet) notes.push("no stylesheet defines :root variables, so the System has no tokens yet: add them to design/tokens");
|
|
2466
|
+
const left = (imported?.report.unparsed ?? []).map((l) => ({ ...l, name: full(l.name) }));
|
|
2467
|
+
const moved = new Set(imported ? variableNames(imported.base).map(full) : []);
|
|
2468
|
+
const importPath = stylesheet ? relative6(dirname7(join14(root, stylesheet)), join14(root, OUT_DIR, "tokens.css")).split("\\").join("/") : "";
|
|
2469
|
+
const moving = css && moved.size > 0 ? moveDeclarations(css, [{ selector: ":root" }, ...darkBlocks], moved, importPath.startsWith(".") ? importPath : `./${importPath}`) : null;
|
|
2470
|
+
const rewritten = moving && found.forked ? { ...moving, content: moving.content.split("\n").filter((line) => !line.includes(GENERATED_MARK) && !/^\/\* Built with Terrazzo/.test(line)).join("\n") } : moving;
|
|
2471
|
+
if (found.forked) notes.push(`forked from ${found.forked.system ?? "the System"}${found.forked.version ? ` v${found.forked.version}` : ""}: this product's System starts from the ${found.forked.count} variables it generated into ${found.forked.file}, and the product no longer follows ${found.forked.system ?? "it"}`);
|
|
2472
|
+
if (css && moved.size > 0 && !rewritten) notes.push(`${stylesheet}'s variables were copied, not moved (their blocks are nested or share lines): import ${OUT_DIR}/tokens.css from it and remove them by hand`);
|
|
2473
|
+
for (const [rule2, content] of starterRules(id, componentsDir)) out.push({ path: `design/rules/${rule2}.rule.yaml`, content });
|
|
2474
|
+
out.push({
|
|
2475
|
+
path: "design/design.md",
|
|
2476
|
+
content: `# ${name}
|
|
2477
|
+
|
|
2478
|
+
The design system of ${name}, adopted from its own code: the components in \`${componentsDir}\` and the variables ${stylesheet ? `\`${stylesheet}\`` : "its stylesheet"} defined. Write here what each colour and component is for and the rules people follow; agents read it in DESIGN.md.
|
|
2479
|
+
`
|
|
2480
|
+
});
|
|
2481
|
+
const system = {
|
|
2482
|
+
id,
|
|
2483
|
+
name,
|
|
2484
|
+
version: "0.1.0",
|
|
2485
|
+
description: `${name}'s own components and tokens, adopted with youiyoux adopt.`,
|
|
2486
|
+
tokens: `design/tokens/${id}.resolver.json`,
|
|
2487
|
+
...hasDark ? { themeModifier: { name: "theme", light: "light", dark: "dark" } } : {},
|
|
2488
|
+
components: "design/components",
|
|
2489
|
+
implementations: componentsDir,
|
|
2490
|
+
support,
|
|
2491
|
+
rules: "design/rules",
|
|
2492
|
+
design: "design/design.md"
|
|
2493
|
+
};
|
|
2494
|
+
out.unshift({ path: "youi.config.json", content: json({ schema: "youi.config/v1", name, systems: [system] }) });
|
|
2495
|
+
return {
|
|
2496
|
+
system,
|
|
2497
|
+
componentsDir,
|
|
2498
|
+
files: out,
|
|
2499
|
+
components,
|
|
2500
|
+
support,
|
|
2501
|
+
tokens: { stylesheet, count: tokenCount, dark: hasDark, left },
|
|
2502
|
+
stylesheet: rewritten && stylesheet ? { path: stylesheet, ...rewritten } : null,
|
|
2503
|
+
outputs: { dir: OUT_DIR, cssVarPrefix: prefix, dark: darkBlocks.map((b) => ({ ...b.outer ? { media: b.outer.replace(/^@media\s+/, "") } : {}, selector: b.selector })), targets: ["css-vars"] },
|
|
2504
|
+
tokenSources: stylesheet ? [stylesheet] : [],
|
|
2505
|
+
notes,
|
|
2506
|
+
follows: found.generated.filter((g) => g.file !== found.forked?.file),
|
|
2507
|
+
forked: found.forked
|
|
2508
|
+
};
|
|
2509
|
+
}
|
|
2510
|
+
async function uiFolder(root) {
|
|
2511
|
+
const counts = /* @__PURE__ */ new Map();
|
|
2512
|
+
for (const file of await findFiles(root, ["**/components/ui/*.{tsx,jsx}"], ["**/build/**", "**/out/**", "**/.next/**"])) counts.set(posix3.dirname(file), (counts.get(posix3.dirname(file)) ?? 0) + 1);
|
|
2513
|
+
return [...counts].sort((a, b) => b[1] - a[1] || a[0].length - b[0].length)[0]?.[0] ?? null;
|
|
2514
|
+
}
|
|
2515
|
+
function starterRules(id, componentsDir) {
|
|
2516
|
+
const rule2 = (spec) => (0, import_yaml2.stringify)({ schema: "youi.rule/v1", ...spec });
|
|
2517
|
+
return [
|
|
2518
|
+
["color.no-raw-color", rule2({
|
|
2519
|
+
id: `youi:${id}/rule/color.no-raw-color`,
|
|
2520
|
+
title: "No raw colours",
|
|
2521
|
+
severity: "warn",
|
|
2522
|
+
statement: "Colour comes from the tokens. Never write a hex, rgb(), hsl() or oklch() literal in a component, class name, inline style or stylesheet; only the token source may define colour values.",
|
|
2523
|
+
rationale: "A literal colour cannot follow the dark scheme or a token change, and it hides from every audit.",
|
|
2524
|
+
agent: "Never hard-code a colour: use the token utilities (bg-primary, text-muted-foreground) or the variables (var(--primary)); look up the role with system_get_tokens.",
|
|
2525
|
+
check: { engine: "youi", id: "raw-color", options: {} },
|
|
2526
|
+
files: ["**/*.{ts,tsx,css}"]
|
|
2527
|
+
})],
|
|
2528
|
+
["tokens.no-arbitrary-values", rule2({
|
|
2529
|
+
id: `youi:${id}/rule/tokens.no-arbitrary-values`,
|
|
2530
|
+
title: "Use token utilities, not arbitrary values",
|
|
2531
|
+
severity: "warn",
|
|
2532
|
+
statement: "When a token holds a value, use its utility instead of a Tailwind arbitrary value.",
|
|
2533
|
+
rationale: "An arbitrary value copies a token's value once; it does not follow when the token changes.",
|
|
2534
|
+
agent: "Prefer token utilities over [arbitrary] values; youiyoux check names the utility.",
|
|
2535
|
+
check: { engine: "youi", id: "arbitrary-value", options: {} },
|
|
2536
|
+
files: ["**/*.tsx"]
|
|
2537
|
+
})],
|
|
2538
|
+
["components.canonical-import", rule2({
|
|
2539
|
+
id: `youi:${id}/rule/components.canonical-import`,
|
|
2540
|
+
title: "Render the System's component, not a lookalike",
|
|
2541
|
+
severity: "warn",
|
|
2542
|
+
statement: `A component named like one of the System's (Button, Input, Card) must be the one in ${componentsDir}. A second one with the same name splits the System in two.`,
|
|
2543
|
+
rationale: "Two Buttons drift apart: a fix to one never reaches the other.",
|
|
2544
|
+
agent: `Import components only from ${componentsDir}; never define or import another component with one of their names.`,
|
|
2545
|
+
check: { engine: "youi", id: "lookalike-component", options: {} },
|
|
2546
|
+
files: ["**/*.tsx"]
|
|
2547
|
+
})]
|
|
2548
|
+
];
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
// ../cli/src/extract.ts
|
|
2552
|
+
import { cp, mkdir as mkdir3, mkdtemp as mkdtemp3, readdir as readdir3, readFile as readFile5, realpath as realpath4, rename, rm as rm4, rmdir as rmdir2, stat } from "node:fs/promises";
|
|
2553
|
+
import { basename as basename3, dirname as dirname8, join as join15, posix as posix4, relative as relative7, resolve as resolve9, sep as sep2 } from "node:path";
|
|
2554
|
+
var json2 = (value) => `${JSON.stringify(value, null, 2)}
|
|
2555
|
+
`;
|
|
2556
|
+
var toPosix2 = (path) => path.split(sep2).join("/");
|
|
2557
|
+
async function isDirectory(path) {
|
|
2558
|
+
return (await stat(path).catch(() => null))?.isDirectory() ?? false;
|
|
2559
|
+
}
|
|
2560
|
+
async function heldInPlace(project2) {
|
|
2561
|
+
const [repo, root] = await Promise.all([realpath4(project2.repo.root), realpath4(project2.root)]);
|
|
2562
|
+
return repo === root && project2.system.config.implementations !== void 0;
|
|
2563
|
+
}
|
|
2564
|
+
function ownedPaths(project2, paths, kept) {
|
|
2565
|
+
const root = project2.repo.root;
|
|
2566
|
+
for (const path of paths) {
|
|
2567
|
+
const own = normalizeRelPath(path);
|
|
2568
|
+
if (own === "." || own.startsWith("../") || !within(root, join15(root, own)) || resolve9(root, own) === resolve9(root)) throw new Error(`the System's ${path} is not a folder of its own inside ${root}: move its files into one (design/\u2026) before extracting; nothing was changed`);
|
|
2569
|
+
for (const keep of kept.map((k) => normalizeRelPath(toPosix2(k)))) {
|
|
2570
|
+
if (keep === own || keep.startsWith(`${own}/`)) throw new Error(`the System's ${own} holds ${keep}, which stays in the product: move the System's files into a folder of their own before extracting; nothing was changed`);
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
async function extractSystem(project2, dir, options = {}) {
|
|
2575
|
+
if (!await heldInPlace(project2)) throw new Error(`${project2.root}'s System is already a design repo of its own (${project2.repo.root}): extract moves a System youiyoux adopt keeps inside the product`);
|
|
2576
|
+
const target = resolve9(dir);
|
|
2577
|
+
if (await isDirectory(target)) {
|
|
2578
|
+
if ((await readdir3(target)).length > 0) throw new Error(`${target} already exists and is not empty: extract into a new folder`);
|
|
2579
|
+
} else if (await stat(target).catch(() => null) !== null) throw new Error(`${target} already exists: extract into a new folder`);
|
|
2580
|
+
const { repo, system } = project2;
|
|
2581
|
+
const config = system.config;
|
|
2582
|
+
const from = (path) => join15(repo.root, path);
|
|
2583
|
+
const components = from(config.components);
|
|
2584
|
+
const implementations = from(implementationsDir(config));
|
|
2585
|
+
const tokensDir = posix4.dirname(config.tokens);
|
|
2586
|
+
const removed = [DESIGN_CONFIG_FILE, config.components, tokensDir, config.rules, ...config.design ? [config.design] : []];
|
|
2587
|
+
ownedPaths(project2, removed, [implementationsDir(config), relative7(project2.root, project2.configFile ?? join15(project2.root, PROJECT_FILE)), project2.config.outputs.dir, relative7(project2.root, target)]);
|
|
2588
|
+
const { implementations: _inPlace, ...rest } = config;
|
|
2589
|
+
const shared = config.support.map((file) => `_shared/${posix4.basename(file)}`);
|
|
2590
|
+
const next2 = { ...rest, tokens: `tokens/${posix4.relative(tokensDir, config.tokens)}`, components: "components", support: shared, rules: "rules", ...config.design ? { design: "design.md" } : {} };
|
|
2591
|
+
const written = [];
|
|
2592
|
+
const temp = await mkdtemp3(join15(dirname8(target), `.${basename3(target)}.`));
|
|
2593
|
+
try {
|
|
2594
|
+
const copy = async (source2, to) => {
|
|
2595
|
+
await mkdir3(dirname8(join15(temp, to)), { recursive: true });
|
|
2596
|
+
await cp(source2, join15(temp, to), { recursive: true });
|
|
2597
|
+
};
|
|
2598
|
+
await copy(components, "components");
|
|
2599
|
+
const placed = /* @__PURE__ */ new Map();
|
|
2600
|
+
for (const { contract, dir: folder } of system.components) {
|
|
2601
|
+
const contractFile = (await readdir3(folder)).find((name) => name.endsWith(".contract.json"));
|
|
2602
|
+
const at = toPosix2(relative7(components, folder));
|
|
2603
|
+
const file = placed.get(contract.source.file) ?? `${at}/${posix4.basename(contract.source.file)}`;
|
|
2604
|
+
if (!placed.has(contract.source.file)) {
|
|
2605
|
+
await copy(join15(implementations, contract.source.file), `components/${file}`);
|
|
2606
|
+
placed.set(contract.source.file, file);
|
|
2607
|
+
}
|
|
2608
|
+
const raw = JSON.parse(await readFile5(join15(folder, contractFile), "utf8"));
|
|
2609
|
+
await writeFileAtomic(join15(temp, "components", at, contractFile), json2({ ...raw, source: { ...raw.source, file } }));
|
|
2610
|
+
}
|
|
2611
|
+
for (const [i, file] of config.support.entries()) await copy(join15(implementations, file), `components/${shared[i]}`);
|
|
2612
|
+
await copy(from(tokensDir), "tokens");
|
|
2613
|
+
if (await isDirectory(from(config.rules))) await copy(from(config.rules), "rules");
|
|
2614
|
+
if (config.design) await copy(from(config.design), "design.md");
|
|
2615
|
+
await writeFileAtomic(join15(temp, DESIGN_CONFIG_FILE), json2({ schema: "youi.config/v1", name: repo.config.name, systems: [next2] }));
|
|
2616
|
+
if (options.package) await writeFileAtomic(join15(temp, "package.json"), json2({ name: options.package, version: config.version, description: config.description, files: [DESIGN_CONFIG_FILE, "components", "tokens", "rules", ...config.design ? ["design.md"] : []] }));
|
|
2617
|
+
const leaving = escapingLinks(temp);
|
|
2618
|
+
if (leaving.length > 0) throw new Error(`the System has links that lead outside it (${leaving.slice(0, 3).join(", ")}); nothing was changed`);
|
|
2619
|
+
await loadDesignRepo(temp);
|
|
2620
|
+
for (const entry of await readdir3(temp, { recursive: true })) if (!await isDirectory(join15(temp, entry))) written.push(toPosix2(entry));
|
|
2621
|
+
written.sort();
|
|
2622
|
+
const source = { path: toPosix2(relative7(project2.root, target)) || "." };
|
|
2623
|
+
if (!options.dryRun) {
|
|
2624
|
+
if (await isDirectory(target)) await rmdir2(target);
|
|
2625
|
+
await rename(temp, target);
|
|
2626
|
+
const configFile = project2.configFile ?? join15(project2.root, PROJECT_FILE);
|
|
2627
|
+
const raw = JSON.parse(await readTextOrNull(configFile) ?? "{}");
|
|
2628
|
+
await writeFileAtomic(configFile, json2({ ...raw, source }));
|
|
2629
|
+
for (const path of removed) await rm4(join15(repo.root, path), { recursive: true, force: true });
|
|
2630
|
+
}
|
|
2631
|
+
return { target, system: next2, written, removed, source };
|
|
2632
|
+
} finally {
|
|
2633
|
+
await rm4(temp, { recursive: true, force: true });
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
// ../cli/src/status.ts
|
|
2638
|
+
import { realpath as realpath5 } from "node:fs/promises";
|
|
2639
|
+
import { dirname as dirname9, join as join16, relative as relative8, resolve as resolve10 } from "node:path";
|
|
2640
|
+
var placeOf = (cwd, root) => {
|
|
2641
|
+
const rel = relative8(cwd, root) || ".";
|
|
2642
|
+
return rel.split(/[\\/]/).filter((part) => part === "..").length > 1 ? resolve10(root) : rel;
|
|
2643
|
+
};
|
|
2644
|
+
var WRITES = /* @__PURE__ */ new Set(["created", "updated", "merged", "adopted", "conflict", "removed"]);
|
|
2645
|
+
function sourceOf(project2) {
|
|
2646
|
+
if (project2.linked) return `your checkout ${project2.linked}`;
|
|
2647
|
+
const source = project2.config.source;
|
|
2648
|
+
if ("package" in source) return `package ${source.package}`;
|
|
2649
|
+
if ("git" in source) return `git ${source.git} at ${source.ref}`;
|
|
2650
|
+
return resolve10(project2.root) === resolve10(project2.repo.root) ? "this repo" : `path ${source.path}`;
|
|
2651
|
+
}
|
|
2652
|
+
function nextStep(s) {
|
|
2653
|
+
if (s.sync.state === "never") return "youiyoux sync: this product has never been synced with its System";
|
|
2654
|
+
if (s.sync.state === "conflict") return "youiyoux sync: resolve the conflicts it reports, then youiyoux sync --adopt records them";
|
|
2655
|
+
if (s.sync.state === "behind") return `youiyoux sync: ${plural(s.sync.changes.length, "file")} would change`;
|
|
2656
|
+
if (s.check.incomplete) return "youiyoux check: some files or rules could not be checked, and it says which";
|
|
2657
|
+
if (s.check.errors > 0) return `youiyoux check: fix ${plural(s.check.errors, "error")}`;
|
|
2658
|
+
if (s.requests.open > 0) return `youiyoux requests: answer ${plural(s.requests.open, "open request")}`;
|
|
2659
|
+
if (s.check.warnings > 0 && !s.check.baseline) return `youiyoux check: ${plural(s.check.warnings, "warning")} to fix, or youiyoux baseline records them as accepted debt`;
|
|
2660
|
+
return "nothing to do";
|
|
2661
|
+
}
|
|
2662
|
+
async function projectStatus(project2, cwd = process.cwd()) {
|
|
2663
|
+
const lock = await readLock(project2.root);
|
|
2664
|
+
const { actions } = await syncProject(project2, { dryRun: true });
|
|
2665
|
+
const changes = actions.filter((a) => WRITES.has(a.action)).map((a) => ({ file: a.file, action: a.action }));
|
|
2666
|
+
const baselineFile2 = baselinePath(project2);
|
|
2667
|
+
const check = await runCheck(project2, { baseline: await readTextOrNull(baselineFile2) === null ? null : baselineFile2 });
|
|
2668
|
+
const comparison = check.baseline?.comparison;
|
|
2669
|
+
const counted = comparison ? [...comparison.new, ...comparison.regressed] : check.violations;
|
|
2670
|
+
const usages = check.scan.report.usages;
|
|
2671
|
+
const { RequestStore } = await import("./src-EUVBYTKY.js");
|
|
2672
|
+
const requests = await new RequestStore(project2.root, project2.config.id).list();
|
|
2673
|
+
const status = {
|
|
2674
|
+
id: project2.config.id,
|
|
2675
|
+
root: placeOf(cwd, project2.root),
|
|
2676
|
+
system: { id: project2.system.config.id, name: project2.system.config.name, version: project2.system.config.version, source: sourceOf(project2), linked: Boolean(project2.linked), commit: project2.designCommit ?? null },
|
|
2677
|
+
lock: lock ? { systemVersion: lock.systemVersion, syncedAt: lock.syncedAt } : null,
|
|
2678
|
+
sync: { state: lock === null ? "never" : actions.some((a) => a.action === "conflict") ? "conflict" : changes.length > 0 ? "behind" : "current", changes },
|
|
2679
|
+
drift: (await verifyLock(project2) ?? []).map((i) => ({ file: i.file, severity: i.severity, message: i.message })),
|
|
2680
|
+
check: {
|
|
2681
|
+
errors: check.errors,
|
|
2682
|
+
warnings: counted.filter((v) => v.severity === "warn").length,
|
|
2683
|
+
baseline: comparison ? { known: comparison.known.length, new: comparison.new.length + comparison.regressed.length, fixed: comparison.fixed.reduce((n, f) => n + f.gone, 0) } : null,
|
|
2684
|
+
incomplete: check.incomplete
|
|
2685
|
+
},
|
|
2686
|
+
components: { system: project2.system.components.length, used: new Set(usages.map((u) => u.element)).size, sites: usages.length },
|
|
2687
|
+
requests: { open: requests.requests.filter((r) => r.status === "open").length, addressed: requests.requests.filter((r) => r.status === "addressed").length, unreadable: requests.unreadable.length }
|
|
2688
|
+
};
|
|
2689
|
+
return { ...status, next: nextStep(status) };
|
|
2690
|
+
}
|
|
2691
|
+
async function statusTargets(cwd, named2) {
|
|
2692
|
+
if (named2) return { roots: [resolve10(named2)], workspace: null };
|
|
2693
|
+
const product2 = findUp(cwd, PROJECT_FILE);
|
|
2694
|
+
if (product2) return { roots: [product2], workspace: null };
|
|
2695
|
+
const at = findUp(cwd, WORKSPACE_FILE);
|
|
2696
|
+
if (at) {
|
|
2697
|
+
const file = join16(at, WORKSPACE_FILE);
|
|
2698
|
+
const workspace = await readDefinition(file, WorkspaceConfig);
|
|
2699
|
+
return { roots: workspace.projects.map((p) => resolve10(at, p)), workspace: file };
|
|
2700
|
+
}
|
|
2701
|
+
const top = findUp(cwd, ".git") ?? null;
|
|
2702
|
+
if (top === null) return { roots: [], workspace: null };
|
|
2703
|
+
const configs = await findFiles(cwd, [`**/${PROJECT_FILE}`]);
|
|
2704
|
+
const roots = [];
|
|
2705
|
+
for (const config of configs) {
|
|
2706
|
+
const dir = resolve10(cwd, dirname9(config));
|
|
2707
|
+
if (await realpath5(findUp(dir, ".git") ?? dir) === await realpath5(top)) roots.push(dir);
|
|
2708
|
+
}
|
|
2709
|
+
return { roots, workspace: null };
|
|
2710
|
+
}
|
|
2711
|
+
async function workspaceStatus(cwd, named2) {
|
|
2712
|
+
const { roots, workspace } = await statusTargets(cwd, named2);
|
|
2713
|
+
const projects = [];
|
|
2714
|
+
for (const root of roots) {
|
|
2715
|
+
try {
|
|
2716
|
+
projects.push(await projectStatus(await loadProject(root), cwd));
|
|
2717
|
+
} catch (error) {
|
|
2718
|
+
const label = placeOf(cwd, root);
|
|
2719
|
+
projects.push({ root: label, error: error instanceof Error ? error.message : String(error), next: await readTextOrNull(join16(root, PROJECT_FILE)) === null ? `youiyoux adopt or youiyoux init --design <dir> in ${label}` : `fix ${join16(label, PROJECT_FILE)}` });
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
return { schema: "youi.status/v1", generatedAt: (/* @__PURE__ */ new Date()).toISOString(), workspace: workspace ? relative8(cwd, workspace) || WORKSPACE_FILE : null, projects };
|
|
2723
|
+
}
|
|
2724
|
+
var failed = (p) => "error" in p;
|
|
2725
|
+
function projectLines(s) {
|
|
2726
|
+
const row = (label, text) => ` ${padEnd(label, 11)} ${text}`;
|
|
2727
|
+
const synced = s.lock ? `synced ${s.lock.syncedAt.slice(0, 10)}${s.lock.systemVersion !== s.system.version ? ` at v${s.lock.systemVersion}` : ""}` : "never synced";
|
|
2728
|
+
const sync = { current: style.ok("up to date"), behind: style.warn(`behind: ${plural(s.sync.changes.length, "file")} would change`), conflict: style.error(`conflict: ${plural(s.sync.changes.filter((c) => c.action === "conflict").length, "file")}`), never: style.warn("never synced") }[s.sync.state];
|
|
2729
|
+
const edited = s.drift.filter((d) => d.severity === "warn");
|
|
2730
|
+
const broken = s.drift.filter((d) => d.severity === "error");
|
|
2731
|
+
const drift = s.drift.length === 0 ? style.ok("none") : [broken.length > 0 ? style.error(`${plural(broken.length, "file")} wrong (${broken.slice(0, 3).map((d) => d.file.split("/").at(-1)).join(", ")})`) : "", edited.length > 0 ? style.warn(`${plural(edited.length, "owned file")} edited locally, merged on sync (${edited.slice(0, 3).map((d) => d.file.split("/").at(-1)).join(", ")})`) : ""].filter(Boolean).join("; ");
|
|
2732
|
+
const { check } = s;
|
|
2733
|
+
const counts = `${check.errors > 0 ? style.error(plural(check.errors, "error")) : style.ok("0 errors")} \xB7 ${plural(check.warnings, "warning")}`;
|
|
2734
|
+
const baseline = check.baseline ? style.dim(` beyond the baseline (${check.baseline.known} accepted${check.baseline.fixed > 0 ? `, ${check.baseline.fixed} fixed` : ""})`) : "";
|
|
2735
|
+
return [
|
|
2736
|
+
`${style.bold(s.id)}${s.root === s.id ? "" : ` ${style.dim(`\xB7 ${s.root}`)}`} \xB7 ${s.system.name} v${s.system.version} ${style.dim(`(${s.system.source}${s.system.linked ? ", linked" : ""}) \xB7 ${synced}`)}`,
|
|
2737
|
+
row("sync", sync),
|
|
2738
|
+
row("drift", drift),
|
|
2739
|
+
row("check", `${counts}${baseline}${check.incomplete ? style.warn(" \xB7 incomplete: not a pass") : ""}`),
|
|
2740
|
+
row("components", `${s.components.used} of ${s.components.system} System components used, at ${plural(s.components.sites, "site")}`),
|
|
2741
|
+
row("requests", s.requests.open + s.requests.addressed + s.requests.unreadable === 0 ? style.dim("none") : [s.requests.open > 0 ? style.warn(`${s.requests.open} open`) : "0 open", `${s.requests.addressed} addressed`, ...s.requests.unreadable > 0 ? [style.error(`${s.requests.unreadable} unreadable`)] : []].join(" \xB7 ")),
|
|
2742
|
+
`${style.bold("Next:")} ${s.next}`
|
|
2743
|
+
];
|
|
2744
|
+
}
|
|
2745
|
+
function renderStatus(report) {
|
|
2746
|
+
if (report.projects.length === 0) return `${style.warn("!")} no product here: no ${PROJECT_FILE} upward, no ${WORKSPACE_FILE}, and none in this repository below here
|
|
2747
|
+
Next: youiyoux adopt makes this product's own UI its design system, or youiyoux init --design <dir> follows one`;
|
|
2748
|
+
const blocks = report.projects.map((p) => failed(p) ? [`${style.bold(p.root)} ${style.error("\u2716")} ${p.error}`, `${style.bold("Next:")} ${p.next}`] : projectLines(p));
|
|
2749
|
+
const head = report.projects.length > 1 ? [`${style.bold("workspace")} ${style.dim(`\xB7 ${plural(report.projects.length, "product")}${report.workspace ? ` from ${report.workspace}` : " in this repository"}`)}`, ""] : [];
|
|
2750
|
+
return [...head, ...blocks.flatMap((b, i) => i === 0 ? b : ["", ...b])].join("\n");
|
|
2751
|
+
}
|
|
2752
|
+
var statusExitCode = (report) => report.projects.some((p) => failed(p) || p.check.incomplete || p.check.errors > 0 || p.sync.state === "conflict") ? 1 : 0;
|
|
2753
|
+
|
|
2754
|
+
// ../cli/src/workspace-hub.ts
|
|
2755
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
2756
|
+
import { createServer } from "node:http";
|
|
2757
|
+
var KEY_HEADER = "x-youi-session";
|
|
2758
|
+
function send(res, status, body) {
|
|
2759
|
+
res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
2760
|
+
res.end(JSON.stringify(body));
|
|
2761
|
+
}
|
|
2762
|
+
async function readBody(req) {
|
|
2763
|
+
let text = "";
|
|
2764
|
+
for await (const chunk2 of req) {
|
|
2765
|
+
text += String(chunk2);
|
|
2766
|
+
if (text.length > 4096) throw new Error("request too large");
|
|
2767
|
+
}
|
|
2768
|
+
return text ? JSON.parse(text) : {};
|
|
2769
|
+
}
|
|
2770
|
+
async function startWorkspaceHub(options) {
|
|
2771
|
+
const key2 = randomBytes2(18).toString("base64url");
|
|
2772
|
+
let report = await options.status();
|
|
2773
|
+
let current = options.current;
|
|
2774
|
+
let busy = false;
|
|
2775
|
+
const server = createServer((req, res) => {
|
|
2776
|
+
void (async () => {
|
|
2777
|
+
const path = (req.url ?? "").split("?")[0];
|
|
2778
|
+
if (req.method === "GET" && path === "/api/workspace") return send(res, 200, { report, current });
|
|
2779
|
+
if (req.method !== "POST" || path !== "/api/workspace/open" && path !== "/api/workspace/refresh") {
|
|
2780
|
+
return send(res, 404, { error: "Not in a workspace hub: Rules and Changes are read-only here; youiyoux ui --project <dir> opens the rule editor on one product" });
|
|
2781
|
+
}
|
|
2782
|
+
if (req.headers[KEY_HEADER] !== key2) return send(res, 401, { error: "this tab does not carry this run's key" });
|
|
2783
|
+
if (busy) return send(res, 409, { error: "another product is opening; try again in a moment" });
|
|
2784
|
+
busy = true;
|
|
2785
|
+
try {
|
|
2786
|
+
if (path === "/api/workspace/refresh") {
|
|
2787
|
+
report = await options.status();
|
|
2788
|
+
return send(res, 200, { report, current });
|
|
2789
|
+
}
|
|
2790
|
+
const { index } = await readBody(req);
|
|
2791
|
+
const entry = typeof index === "number" && Number.isInteger(index) ? report.projects[index] : void 0;
|
|
2792
|
+
if (entry === void 0 || options.roots[index] === void 0) return send(res, 400, { error: `no product ${JSON.stringify(index)} in this workspace` });
|
|
2793
|
+
if ("error" in entry) return send(res, 400, { error: `${entry.root} cannot be opened: ${entry.error}`, problems: [entry.next] });
|
|
2794
|
+
const code = await options.open(options.roots[index]);
|
|
2795
|
+
if (code !== 0) return send(res, 500, { error: `the snapshot of ${entry.root} failed (exit ${code}); the hub still shows the product it showed` });
|
|
2796
|
+
current = index;
|
|
2797
|
+
return send(res, 200, { report, current });
|
|
2798
|
+
} catch (error) {
|
|
2799
|
+
return send(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
2800
|
+
} finally {
|
|
2801
|
+
busy = false;
|
|
2802
|
+
}
|
|
2803
|
+
})();
|
|
2804
|
+
});
|
|
2805
|
+
await new Promise((done) => server.listen(0, "127.0.0.1", done));
|
|
2806
|
+
const address = server.address();
|
|
2807
|
+
const port = typeof address === "object" && address ? address.port : 0;
|
|
2808
|
+
return {
|
|
2809
|
+
url: `http://127.0.0.1:${port}`,
|
|
2810
|
+
key: key2,
|
|
2811
|
+
close: () => new Promise((done) => server.close(() => done()))
|
|
2812
|
+
};
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2220
2815
|
// ../cli/src/index-store.ts
|
|
2221
|
-
import { appendFile, mkdir as
|
|
2222
|
-
import { dirname as
|
|
2816
|
+
import { appendFile, mkdir as mkdir4, readFile as readFile6, stat as stat2, rename as rename2, mkdtemp as mkdtemp4, rm as rm5 } from "node:fs/promises";
|
|
2817
|
+
import { dirname as dirname10, join as join17, resolve as resolve11 } from "node:path";
|
|
2223
2818
|
var SOURCES_FILE = "index-sources.json";
|
|
2224
2819
|
var SCANS_LOG = "scans.jsonl";
|
|
2225
2820
|
var EMPTY = { discover: null, inventories: [], projects: [] };
|
|
2226
2821
|
async function readIndexSources(home) {
|
|
2227
2822
|
try {
|
|
2228
|
-
const raw = JSON.parse(await
|
|
2823
|
+
const raw = JSON.parse(await readFile6(join17(home, SOURCES_FILE), "utf8"));
|
|
2229
2824
|
return { discover: raw.discover ?? null, inventories: raw.inventories ?? [], projects: raw.projects ?? [] };
|
|
2230
2825
|
} catch {
|
|
2231
2826
|
return EMPTY;
|
|
@@ -2238,14 +2833,14 @@ async function noteSource(home, change) {
|
|
|
2238
2833
|
inventories: change.inventory && !sources.inventories.includes(change.inventory) ? [...sources.inventories, change.inventory] : sources.inventories,
|
|
2239
2834
|
projects: change.project ? [...sources.projects.filter((p) => p.root !== change.project.root), change.project] : sources.projects
|
|
2240
2835
|
};
|
|
2241
|
-
await writeFileAtomic(
|
|
2836
|
+
await writeFileAtomic(join17(home, SOURCES_FILE), `${JSON.stringify(next2, null, 2)}
|
|
2242
2837
|
`);
|
|
2243
2838
|
}
|
|
2244
2839
|
async function recordInventory(store, home, report, file) {
|
|
2245
|
-
const run7 = inventoryRun(report,
|
|
2840
|
+
const run7 = inventoryRun(report, resolve11(file));
|
|
2246
2841
|
const logged = { run: run7.run, source: run7.source, scannedAt: run7.scannedAt, projects: run7.projects, families: run7.families };
|
|
2247
|
-
await
|
|
2248
|
-
await appendFile(
|
|
2842
|
+
await mkdir4(home, { recursive: true });
|
|
2843
|
+
await appendFile(join17(home, SCANS_LOG), `${JSON.stringify({ schema: "youi.scan-log/v1", ...logged })}
|
|
2249
2844
|
`);
|
|
2250
2845
|
await store.recordInventory(run7);
|
|
2251
2846
|
await noteSource(home, { inventory: run7.source });
|
|
@@ -2256,7 +2851,7 @@ async function recordProject(store, project2, usage) {
|
|
|
2256
2851
|
await store.replaceGraph(`project:${project2.root}`, projectGraph({ project: project2.config.id, rules: project2.system.rules, components: project2.system.components.map((c) => ({ id: c.contract.id, name: c.contract.name })), usage }));
|
|
2257
2852
|
}
|
|
2258
2853
|
async function readLog(home) {
|
|
2259
|
-
const text = await
|
|
2854
|
+
const text = await readFile6(join17(home, SCANS_LOG), "utf8").catch(() => "");
|
|
2260
2855
|
return text.split("\n").flatMap((line) => {
|
|
2261
2856
|
if (!line.trim()) return [];
|
|
2262
2857
|
try {
|
|
@@ -2271,9 +2866,9 @@ async function rebuildIndex(path, options = {}) {
|
|
|
2271
2866
|
const started = performance.now();
|
|
2272
2867
|
const env = options.env ?? process.env;
|
|
2273
2868
|
const home = indexHome(path, env);
|
|
2274
|
-
await
|
|
2275
|
-
const scratch = !env.DATABASE_URL ? await
|
|
2276
|
-
const target = scratch ?
|
|
2869
|
+
await mkdir4(dirname10(path), { recursive: true });
|
|
2870
|
+
const scratch = !env.DATABASE_URL ? await mkdtemp4(`${path}.rebuild-`) : null;
|
|
2871
|
+
const target = scratch ? join17(scratch, "index.db") : path;
|
|
2277
2872
|
const store = scratch ? await openSqlite(target) : await sharedStore(path, env);
|
|
2278
2873
|
try {
|
|
2279
2874
|
if (env.DATABASE_URL) await store.reset();
|
|
@@ -2281,7 +2876,7 @@ async function rebuildIndex(path, options = {}) {
|
|
|
2281
2876
|
const sources = await readIndexSources(home);
|
|
2282
2877
|
const cache = options.discoverCache ?? sources.discover;
|
|
2283
2878
|
let discover = { cache, chosen: 0, all: 0 };
|
|
2284
|
-
if (cache && await
|
|
2879
|
+
if (cache && await stat2(join17(cache, "index.json")).then(() => true, () => false)) {
|
|
2285
2880
|
const loaded = await ingestCatalog(store, cache);
|
|
2286
2881
|
discover = { cache, chosen: loaded.chosen, all: loaded.all };
|
|
2287
2882
|
}
|
|
@@ -2298,53 +2893,53 @@ async function rebuildIndex(path, options = {}) {
|
|
|
2298
2893
|
await store.recordInventory(inventoryRun(parsed.data, file));
|
|
2299
2894
|
replayed++;
|
|
2300
2895
|
}
|
|
2301
|
-
const
|
|
2896
|
+
const failed2 = [];
|
|
2302
2897
|
let projects = 0;
|
|
2303
2898
|
const gone = [];
|
|
2304
2899
|
for (const source of sources.projects) {
|
|
2305
|
-
if (!await
|
|
2900
|
+
if (!await stat2(source.root).then((s) => s.isDirectory(), () => false)) {
|
|
2306
2901
|
gone.push(source.root);
|
|
2307
|
-
|
|
2902
|
+
failed2.push({ root: source.root, reason: "folder not found (moved or deleted), skipped" });
|
|
2308
2903
|
continue;
|
|
2309
2904
|
}
|
|
2310
2905
|
try {
|
|
2311
2906
|
const project2 = await loadProject(source.root, source.options);
|
|
2312
|
-
const usage = ScanReport.safeParse(await readJson(source.usage ??
|
|
2907
|
+
const usage = ScanReport.safeParse(await readJson(source.usage ?? join17(project2.root, ".youi/usage.json")));
|
|
2313
2908
|
await recordProject(store, project2, usage.success ? usage.data : null);
|
|
2314
2909
|
projects++;
|
|
2315
2910
|
} catch (error) {
|
|
2316
|
-
|
|
2911
|
+
failed2.push({ root: source.root, reason: error instanceof Error ? error.message.split("\n")[0] : String(error) });
|
|
2317
2912
|
}
|
|
2318
2913
|
}
|
|
2319
|
-
const broken =
|
|
2914
|
+
const broken = failed2.filter((f) => !gone.includes(f.root));
|
|
2320
2915
|
if (broken.length) throw new Error(`Index rebuild failed: ${broken.map((f) => `${f.root}: ${f.reason}`).join("; ")}`);
|
|
2321
2916
|
await linkCandidates(store);
|
|
2322
2917
|
const tables = (await store.stats()).tables;
|
|
2323
2918
|
if (scratch) {
|
|
2324
2919
|
await store.close();
|
|
2325
2920
|
checkpointSqlite(target);
|
|
2326
|
-
if (await
|
|
2921
|
+
if (await stat2(path).then(() => true, () => false)) checkpointSqlite(path);
|
|
2327
2922
|
const moved = [];
|
|
2328
2923
|
try {
|
|
2329
2924
|
for (const suffix of ["-wal", "-shm"]) {
|
|
2330
2925
|
try {
|
|
2331
|
-
await
|
|
2926
|
+
await rename2(`${path}${suffix}`, `${target}.old${suffix}`);
|
|
2332
2927
|
moved.push(suffix);
|
|
2333
2928
|
} catch (error) {
|
|
2334
2929
|
if (error.code !== "ENOENT") throw error;
|
|
2335
2930
|
}
|
|
2336
2931
|
}
|
|
2337
|
-
await
|
|
2932
|
+
await rename2(target, path);
|
|
2338
2933
|
} catch (error) {
|
|
2339
|
-
for (const suffix of moved) await
|
|
2934
|
+
for (const suffix of moved) await rename2(`${target}.old${suffix}`, `${path}${suffix}`);
|
|
2340
2935
|
throw error;
|
|
2341
2936
|
}
|
|
2342
2937
|
}
|
|
2343
|
-
return { location: scratch ? path : store.location, discover, scans: log.length, inventories: { replayed, missing }, projects: { replayed: projects, failed }, tables, durationMs: Math.round(performance.now() - started) };
|
|
2938
|
+
return { location: scratch ? path : store.location, discover, scans: log.length, inventories: { replayed, missing }, projects: { replayed: projects, failed: failed2 }, tables, durationMs: Math.round(performance.now() - started) };
|
|
2344
2939
|
} finally {
|
|
2345
2940
|
if (scratch) {
|
|
2346
2941
|
await store.close();
|
|
2347
|
-
await
|
|
2942
|
+
await rm5(scratch, { recursive: true, force: true });
|
|
2348
2943
|
}
|
|
2349
2944
|
}
|
|
2350
2945
|
}
|
|
@@ -2526,9 +3121,9 @@ function renderAdoptionRequest(request) {
|
|
|
2526
3121
|
}
|
|
2527
3122
|
|
|
2528
3123
|
// ../cli/src/report/run.ts
|
|
2529
|
-
import { randomBytes as
|
|
2530
|
-
import { mkdir as
|
|
2531
|
-
import { join as
|
|
3124
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
3125
|
+
import { mkdir as mkdir5, rename as rename3, stat as stat3, writeFile as writeFile2 } from "node:fs/promises";
|
|
3126
|
+
import { join as join24, relative as relative10, resolve as resolve14 } from "node:path";
|
|
2532
3127
|
|
|
2533
3128
|
// ../cli/src/report/card.ts
|
|
2534
3129
|
import { deflateSync } from "node:zlib";
|
|
@@ -2763,8 +3358,8 @@ function drawCard(input) {
|
|
|
2763
3358
|
}
|
|
2764
3359
|
|
|
2765
3360
|
// ../cli/src/report/model.ts
|
|
2766
|
-
import { readFile as
|
|
2767
|
-
import { join as
|
|
3361
|
+
import { readFile as readFile8 } from "node:fs/promises";
|
|
3362
|
+
import { join as join21 } from "node:path";
|
|
2768
3363
|
|
|
2769
3364
|
// ../cli/src/report/filters.ts
|
|
2770
3365
|
var SHOWCASE_GLOBS = ["**/{__stories__,.storybook,storybook,storybook-static,example,examples,demo,demos,playground,showcase,showcases}/**", "**/*.stories.*", "**/*.story.*"];
|
|
@@ -2891,11 +3486,11 @@ function publicLook(caption) {
|
|
|
2891
3486
|
|
|
2892
3487
|
// ../cli/src/report/groups.ts
|
|
2893
3488
|
import { existsSync as existsSync3 } from "node:fs";
|
|
2894
|
-
import { join as
|
|
3489
|
+
import { join as join19 } from "node:path";
|
|
2895
3490
|
|
|
2896
3491
|
// ../cli/src/report/imports.ts
|
|
2897
3492
|
import { realpathSync } from "node:fs";
|
|
2898
|
-
import { dirname as
|
|
3493
|
+
import { dirname as dirname11, join as join18, relative as relative9, resolve as resolve12 } from "node:path";
|
|
2899
3494
|
import ts from "typescript";
|
|
2900
3495
|
var resolvers = /* @__PURE__ */ new Map();
|
|
2901
3496
|
function resolverOf(root) {
|
|
@@ -2903,7 +3498,7 @@ function resolverOf(root) {
|
|
|
2903
3498
|
if (!found) {
|
|
2904
3499
|
const config = ts.findConfigFile(root, ts.sys.fileExists, "tsconfig.json") ?? ts.findConfigFile(root, ts.sys.fileExists, "jsconfig.json");
|
|
2905
3500
|
const read = config ? ts.readConfigFile(config, ts.sys.readFile) : null;
|
|
2906
|
-
const options = config && read && !read.error ? ts.parseJsonConfigFileContent(read.config, ts.sys,
|
|
3501
|
+
const options = config && read && !read.error ? ts.parseJsonConfigFileContent(read.config, ts.sys, dirname11(config)).options : {};
|
|
2907
3502
|
found = { options, cache: ts.createModuleResolutionCache(root, (name) => name, options) };
|
|
2908
3503
|
resolvers.set(root, found);
|
|
2909
3504
|
}
|
|
@@ -2914,13 +3509,13 @@ function clearImportCache() {
|
|
|
2914
3509
|
}
|
|
2915
3510
|
async function verifiedImport(root, keeper, from, suggested) {
|
|
2916
3511
|
const { options, cache } = resolverOf(root);
|
|
2917
|
-
let local =
|
|
3512
|
+
let local = relative9(dirname11(join18(root, from)), join18(root, keeper)).replaceAll("\\", "/").replace(/\.(tsx?|jsx?|mjs)$/, "");
|
|
2918
3513
|
if (!local.startsWith(".")) local = `./${local}`;
|
|
2919
3514
|
for (const specifier of /* @__PURE__ */ new Set([suggested, local])) {
|
|
2920
|
-
const found = ts.resolveModuleName(specifier,
|
|
3515
|
+
const found = ts.resolveModuleName(specifier, resolve12(root, from), options, ts.sys, cache).resolvedModule?.resolvedFileName;
|
|
2921
3516
|
if (found) {
|
|
2922
3517
|
try {
|
|
2923
|
-
if (realpathSync(found) === realpathSync(
|
|
3518
|
+
if (realpathSync(found) === realpathSync(join18(root, keeper))) return specifier;
|
|
2924
3519
|
} catch {
|
|
2925
3520
|
}
|
|
2926
3521
|
}
|
|
@@ -3033,7 +3628,7 @@ var NEAR_IDENTICAL = 0.5;
|
|
|
3033
3628
|
var MIN_STRUCTURE = 6;
|
|
3034
3629
|
var GENERIC_IDENTICAL = 0.85;
|
|
3035
3630
|
var repoPath = (dir, file) => dir === "." ? file : `${dir}/${file}`;
|
|
3036
|
-
var
|
|
3631
|
+
var kebab2 = (name) => name.replace(/[a-z0-9](?=[A-Z])/g, "$&-").toLowerCase();
|
|
3037
3632
|
function shapeWords(shape) {
|
|
3038
3633
|
const { height, paddingX, radius, fontSize } = shape;
|
|
3039
3634
|
return [height !== null ? `${height}px high` : null, paddingX !== null ? `${paddingX}px side padding` : null, radius !== null ? radius === 9999 ? "pill-shaped" : `${radius}px corners` : null, fontSize !== null ? `${fontSize}px text` : null].filter(Boolean).join(", ");
|
|
@@ -3134,7 +3729,7 @@ async function nearDuplicateGroups(input) {
|
|
|
3134
3729
|
const name = EXTRACT_NAMES[kind];
|
|
3135
3730
|
const fileIn = (f, dir2) => {
|
|
3136
3731
|
for (let n = 1; ; n++) {
|
|
3137
|
-
const file = `${dir2 ? `${dir2}/` : ""}${
|
|
3732
|
+
const file = `${dir2 ? `${dir2}/` : ""}${kebab2(name)}${n === 1 ? "" : `-${n}`}.tsx`;
|
|
3138
3733
|
if (![...defs.values()].some((d) => d.facts === f && d.def.file === file)) return file;
|
|
3139
3734
|
}
|
|
3140
3735
|
};
|
|
@@ -3328,22 +3923,22 @@ function factsOf(project2, id, modules, eligible, workspaceNames, dependencies,
|
|
|
3328
3923
|
if (dirs.has(file.slice(0, at))) break;
|
|
3329
3924
|
dirs.add(file.slice(0, at));
|
|
3330
3925
|
}
|
|
3331
|
-
const roots = [...dirs].filter((dir) => existsSync3(
|
|
3926
|
+
const roots = [...dirs].filter((dir) => existsSync3(join19(project2.root, dir, "package.json"))).sort((a, b) => b.length - a.length);
|
|
3332
3927
|
const nested = (file) => roots.find((dir) => file.startsWith(`${dir}/`)) ?? "";
|
|
3333
3928
|
return { project: project2, id, dependencies, packageImports, jsxCounts, eligible, twins, dirs, importer, packageOf: nested };
|
|
3334
3929
|
}
|
|
3335
3930
|
|
|
3336
3931
|
// ../cli/src/report/projects.ts
|
|
3337
|
-
var
|
|
3338
|
-
import { readFile as
|
|
3339
|
-
import { basename as
|
|
3932
|
+
var import_yaml3 = __toESM(require_dist(), 1);
|
|
3933
|
+
import { readFile as readFile7 } from "node:fs/promises";
|
|
3934
|
+
import { basename as basename4, join as join20, resolve as resolve13 } from "node:path";
|
|
3340
3935
|
var UI_FILES = ["**/*.{tsx,jsx,astro,vue,svelte}"];
|
|
3341
3936
|
var strings = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
|
|
3342
3937
|
async function workspaceGlobs(root, pkg) {
|
|
3343
|
-
const pnpm = await
|
|
3938
|
+
const pnpm = await readFile7(join20(root, "pnpm-workspace.yaml"), "utf8").catch(() => null);
|
|
3344
3939
|
if (pnpm !== null) {
|
|
3345
3940
|
try {
|
|
3346
|
-
const globs2 = strings((0,
|
|
3941
|
+
const globs2 = strings((0, import_yaml3.parse)(pnpm)?.packages);
|
|
3347
3942
|
if (globs2.length > 0) return { globs: globs2, source: "pnpm-workspace.yaml" };
|
|
3348
3943
|
} catch {
|
|
3349
3944
|
}
|
|
@@ -3354,9 +3949,9 @@ async function workspaceGlobs(root, pkg) {
|
|
|
3354
3949
|
}
|
|
3355
3950
|
var hasUi = async (dir) => (await findFiles(dir, UI_FILES, DEFAULT_EXCLUDE)).length > 0;
|
|
3356
3951
|
async function findProjects(dir) {
|
|
3357
|
-
const root =
|
|
3358
|
-
const pkg = await readJson(
|
|
3359
|
-
const name = typeof pkg?.name === "string" && pkg.name.trim() ? pkg.name.trim() :
|
|
3952
|
+
const root = resolve13(dir);
|
|
3953
|
+
const pkg = await readJson(join20(root, "package.json"));
|
|
3954
|
+
const name = typeof pkg?.name === "string" && pkg.name.trim() ? pkg.name.trim() : basename4(root);
|
|
3360
3955
|
const single = { dir: ".", root, name: typeof pkg?.name === "string" ? pkg.name : null };
|
|
3361
3956
|
const workspace = await workspaceGlobs(root, pkg);
|
|
3362
3957
|
if (!workspace) return { root, name, layout: "one project", projects: [single], skipped: [] };
|
|
@@ -3366,12 +3961,12 @@ async function findProjects(dir) {
|
|
|
3366
3961
|
const projects = [];
|
|
3367
3962
|
const skipped = [];
|
|
3368
3963
|
for (const member of members) {
|
|
3369
|
-
const memberRoot =
|
|
3964
|
+
const memberRoot = join20(root, member);
|
|
3370
3965
|
if (!await hasUi(memberRoot)) {
|
|
3371
3966
|
skipped.push(member);
|
|
3372
3967
|
continue;
|
|
3373
3968
|
}
|
|
3374
|
-
const memberPkg = await readJson(
|
|
3969
|
+
const memberPkg = await readJson(join20(memberRoot, "package.json"));
|
|
3375
3970
|
projects.push({ dir: member, root: memberRoot, name: typeof memberPkg?.name === "string" ? memberPkg.name : null });
|
|
3376
3971
|
}
|
|
3377
3972
|
if (projects.length === 0) return { root, name, layout: `one project (${workspace.source} names no package with UI files)`, projects: [single], skipped };
|
|
@@ -3398,7 +3993,7 @@ var Lines = class {
|
|
|
3398
3993
|
if (!this.enabled) return { at };
|
|
3399
3994
|
let lines = this.files.get(file);
|
|
3400
3995
|
if (!lines) {
|
|
3401
|
-
lines = (await
|
|
3996
|
+
lines = (await readFile8(join21(this.root, file), "utf8").catch(() => "")).split("\n");
|
|
3402
3997
|
this.files.set(file, lines);
|
|
3403
3998
|
}
|
|
3404
3999
|
const code = lines[line - 1]?.trim();
|
|
@@ -3413,15 +4008,15 @@ function hardcodedValue(finding3) {
|
|
|
3413
4008
|
const value = bracket ?? (declaration ? `${declaration[2]} (${declaration[1]})` : finding3.text);
|
|
3414
4009
|
return { word: word === "type" ? "text size" : word, value, tokens };
|
|
3415
4010
|
}
|
|
3416
|
-
async function modulesOf(project2, target,
|
|
4011
|
+
async function modulesOf(project2, target, failed2) {
|
|
3417
4012
|
const modules = [];
|
|
3418
4013
|
for (const file of target.files) {
|
|
3419
4014
|
if (isShowcase(file)) continue;
|
|
3420
4015
|
try {
|
|
3421
|
-
const module = await parseModule(file, await
|
|
4016
|
+
const module = await parseModule(file, await readFile8(join21(project2.root, file), "utf8"));
|
|
3422
4017
|
if (module) modules.push(module);
|
|
3423
4018
|
} catch (error) {
|
|
3424
|
-
|
|
4019
|
+
failed2.push(`${repoPath(project2.dir, file)}: ${errorText(error)}`);
|
|
3425
4020
|
}
|
|
3426
4021
|
}
|
|
3427
4022
|
return modules;
|
|
@@ -3442,7 +4037,7 @@ async function grouped(project2, found, lines, describe) {
|
|
|
3442
4037
|
return findings2.sort((a, b) => b.count - a.count || a.title.localeCompare(b.title));
|
|
3443
4038
|
}
|
|
3444
4039
|
async function analyseProject(project2, index, inventory, inventoryError, workspaceNames, lines) {
|
|
3445
|
-
const
|
|
4040
|
+
const failed2 = [];
|
|
3446
4041
|
const incomplete = [];
|
|
3447
4042
|
const notes = [];
|
|
3448
4043
|
const projectId = inventory?.projects[index]?.id;
|
|
@@ -3450,7 +4045,7 @@ async function analyseProject(project2, index, inventory, inventoryError, worksp
|
|
|
3450
4045
|
let target = null;
|
|
3451
4046
|
try {
|
|
3452
4047
|
target = await loadInventoryTarget(project2.root);
|
|
3453
|
-
modules = await modulesOf(project2, target,
|
|
4048
|
+
modules = await modulesOf(project2, target, failed2);
|
|
3454
4049
|
} catch (error) {
|
|
3455
4050
|
incomplete.push(`The code could not be read (${errorText(error)})`);
|
|
3456
4051
|
}
|
|
@@ -3493,7 +4088,7 @@ async function analyseProject(project2, index, inventory, inventoryError, worksp
|
|
|
3493
4088
|
const components = [...byComponent.values()].map((c) => ({ name: c.name, file: c.file, uses: c.roles.length, role: [...new Set(c.roles)].sort((a, b) => c.roles.filter((r) => r === b).length - c.roles.filter((r) => r === a).length)[0] })).sort((a, b) => b.uses - a.uses || a.name.localeCompare(b.name)).slice(0, TOP_COMPONENTS);
|
|
3494
4089
|
const inventoryProject = inventory?.projects[index];
|
|
3495
4090
|
if (inventoryError) incomplete.push(`The component inventory could not be built (${inventoryError}); run ${NPX} inventory --project ${project2.dir} to see why`);
|
|
3496
|
-
for (const f of inventoryProject?.failed ?? [])
|
|
4091
|
+
for (const f of inventoryProject?.failed ?? []) failed2.push(`${repoPath(project2.dir, f.file)}: ${f.reason}`);
|
|
3497
4092
|
let hardcoded = [];
|
|
3498
4093
|
let offLibrary = [];
|
|
3499
4094
|
const everyLine = () => true;
|
|
@@ -3522,7 +4117,7 @@ async function analyseProject(project2, index, inventory, inventoryError, worksp
|
|
|
3522
4117
|
incomplete.push(`Library choices were not compared (${errorText(error)}); run ${NPX} detect ${project2.dir} to see why`);
|
|
3523
4118
|
}
|
|
3524
4119
|
}
|
|
3525
|
-
const pkg = await
|
|
4120
|
+
const pkg = await readFile8(join21(project2.root, "package.json"), "utf8").then((text) => JSON.parse(text), () => null);
|
|
3526
4121
|
const dependencies = new Set(["dependencies", "devDependencies", "peerDependencies"].flatMap((key2) => Object.keys(pkg?.[key2] ?? {})));
|
|
3527
4122
|
const importer = changeProject ? await importOf(changeProject) : () => "";
|
|
3528
4123
|
const facts = factsOf(project2, projectId ?? null, modules, eligible, workspaceNames, dependencies, target?.files ?? [], importer);
|
|
@@ -3530,7 +4125,7 @@ async function analyseProject(project2, index, inventory, inventoryError, worksp
|
|
|
3530
4125
|
dir: project2.dir,
|
|
3531
4126
|
name: project2.name,
|
|
3532
4127
|
files: inventoryProject?.files ?? modules.length,
|
|
3533
|
-
failed,
|
|
4128
|
+
failed: failed2,
|
|
3534
4129
|
libraries,
|
|
3535
4130
|
unused,
|
|
3536
4131
|
definitions: inventory ? inventory.definitions.filter((d) => d.project === projectId && !isShowcase(d.file)).length : 0,
|
|
@@ -3966,36 +4561,36 @@ ${model.projects.map((p) => projectSection(p, many, looks)).join("\n")}
|
|
|
3966
4561
|
import { spawn as spawn2 } from "node:child_process";
|
|
3967
4562
|
import { existsSync as existsSync4, readFileSync as readFileSync2 } from "node:fs";
|
|
3968
4563
|
import { homedir as homedir2 } from "node:os";
|
|
3969
|
-
import { dirname as
|
|
4564
|
+
import { dirname as dirname12, join as join22 } from "node:path";
|
|
3970
4565
|
var LOOKS_PER_PROJECT = 4;
|
|
3971
4566
|
var DOWNLOAD_SIZE = "~100 MB";
|
|
3972
4567
|
var DEPENDENCIES = "Install the repo's dependencies to see rendered looks";
|
|
3973
4568
|
var UNDRAWABLE = /not installed|loads no stylesheet|drawn from (?:its|their) (?:tag and )?classes|did not render|bundle failed|unstyled|stub fallback/i;
|
|
3974
|
-
var playwrightDir = () =>
|
|
4569
|
+
var playwrightDir = () => dirname12(playwrightCli());
|
|
3975
4570
|
function browsersPath(env) {
|
|
3976
4571
|
const set = env.PLAYWRIGHT_BROWSERS_PATH;
|
|
3977
|
-
if (set === "0") return
|
|
4572
|
+
if (set === "0") return join22(playwrightDir(), ".local-browsers");
|
|
3978
4573
|
if (set) return set;
|
|
3979
|
-
if (process.platform === "darwin") return
|
|
3980
|
-
if (process.platform === "win32") return
|
|
3981
|
-
return
|
|
4574
|
+
if (process.platform === "darwin") return join22(homedir2(), "Library/Caches/ms-playwright");
|
|
4575
|
+
if (process.platform === "win32") return join22(env.LOCALAPPDATA ?? join22(homedir2(), "AppData/Local"), "ms-playwright");
|
|
4576
|
+
return join22(env.XDG_CACHE_HOME ?? join22(homedir2(), ".cache"), "ms-playwright");
|
|
3982
4577
|
}
|
|
3983
4578
|
function chromiumStatus(env = process.env) {
|
|
3984
4579
|
if (env.YOUI_CHROMIUM) return { available: existsSync4(env.YOUI_CHROMIUM), where: env.YOUI_CHROMIUM };
|
|
3985
4580
|
try {
|
|
3986
|
-
const browsers = JSON.parse(readFileSync2(
|
|
4581
|
+
const browsers = JSON.parse(readFileSync2(join22(playwrightDir(), "browsers.json"), "utf8"));
|
|
3987
4582
|
const revision2 = browsers.browsers.find((b) => b.name === "chromium-headless-shell")?.revision;
|
|
3988
|
-
const dir =
|
|
3989
|
-
return { available: revision2 !== void 0 && existsSync4(
|
|
4583
|
+
const dir = join22(browsersPath(env), `chromium_headless_shell-${revision2}`);
|
|
4584
|
+
return { available: revision2 !== void 0 && existsSync4(join22(dir, "INSTALLATION_COMPLETE")), where: dir };
|
|
3990
4585
|
} catch {
|
|
3991
4586
|
return { available: false, where: "playwright-core is not installed" };
|
|
3992
4587
|
}
|
|
3993
4588
|
}
|
|
3994
4589
|
function downloadChromium() {
|
|
3995
|
-
return new Promise((
|
|
3996
|
-
const child = spawn2(process.execPath, [
|
|
3997
|
-
child.on("error", () =>
|
|
3998
|
-
child.on("close", (code) =>
|
|
4590
|
+
return new Promise((resolve16) => {
|
|
4591
|
+
const child = spawn2(process.execPath, [join22(playwrightDir(), "cli.js"), "install", "chromium-headless-shell"], { stdio: ["ignore", 2, 2] });
|
|
4592
|
+
child.on("error", () => resolve16(false));
|
|
4593
|
+
child.on("close", (code) => resolve16(code === 0));
|
|
3999
4594
|
});
|
|
4000
4595
|
}
|
|
4001
4596
|
var dataUri = (file) => file && existsSync4(file) ? `data:image/png;base64,${readFileSync2(file).toString("base64")}` : null;
|
|
@@ -4009,24 +4604,24 @@ async function addLooks(model, repoRoot, options = {}) {
|
|
|
4009
4604
|
const where2 = section.dir === "." ? "this repo" : section.dir;
|
|
4010
4605
|
try {
|
|
4011
4606
|
const installed = (name) => {
|
|
4012
|
-
let dir =
|
|
4607
|
+
let dir = join22(repoRoot, section.dir);
|
|
4013
4608
|
while (true) {
|
|
4014
|
-
if (existsSync4(
|
|
4015
|
-
if (dir === repoRoot ||
|
|
4016
|
-
dir =
|
|
4609
|
+
if (existsSync4(join22(dir, "node_modules", name, "package.json"))) return true;
|
|
4610
|
+
if (dir === repoRoot || dirname12(dir) === dir) return false;
|
|
4611
|
+
dir = dirname12(dir);
|
|
4017
4612
|
}
|
|
4018
4613
|
};
|
|
4019
4614
|
if (!installed("react") || !installed("react-dom")) {
|
|
4020
4615
|
missing.push(where2);
|
|
4021
4616
|
continue;
|
|
4022
4617
|
}
|
|
4023
|
-
const projectRoot =
|
|
4618
|
+
const projectRoot = join22(repoRoot, section.dir);
|
|
4024
4619
|
const target = await loadInventoryTarget(projectRoot);
|
|
4025
4620
|
if (findStylesheets(projectRoot, target.files, { tsconfig: target.tsconfig, override: target.specimenCss, templates: target.templates }).found.length === 0) {
|
|
4026
4621
|
missing.push(where2);
|
|
4027
4622
|
continue;
|
|
4028
4623
|
}
|
|
4029
|
-
const report = await renderShots(
|
|
4624
|
+
const report = await renderShots(join22(repoRoot, section.dir), names, { download: false, base: "HEAD", ...options.executablePath ? { executablePath: options.executablePath } : {}, ...options.cache ? { cache: options.cache } : {}, ...options.log ? { log: options.log } : {} });
|
|
4030
4625
|
const unavailable = report.notes.some((note) => UNDRAWABLE.test(note));
|
|
4031
4626
|
if (unavailable) {
|
|
4032
4627
|
missing.push(where2);
|
|
@@ -4096,9 +4691,9 @@ function renderMarkdown(model, reportFile) {
|
|
|
4096
4691
|
|
|
4097
4692
|
// ../cli/src/report/safe.ts
|
|
4098
4693
|
import { realpathSync as realpathSync2 } from "node:fs";
|
|
4099
|
-
import { readFile as
|
|
4694
|
+
import { readFile as readFile9 } from "node:fs/promises";
|
|
4100
4695
|
import { homedir as homedir3, userInfo as userInfo2 } from "node:os";
|
|
4101
|
-
import { join as
|
|
4696
|
+
import { join as join23 } from "node:path";
|
|
4102
4697
|
var REDACTED = "[redacted]";
|
|
4103
4698
|
var OUTSIDE = "[path outside the repo]";
|
|
4104
4699
|
var MIN_VALUE = 6;
|
|
@@ -4110,7 +4705,7 @@ async function dotenvValues(root) {
|
|
|
4110
4705
|
const files = await findFiles(root, [".env", ".env.*", "**/.env", "**/.env.*"]).catch(() => []);
|
|
4111
4706
|
const values = [];
|
|
4112
4707
|
for (const file of files) {
|
|
4113
|
-
const text = await
|
|
4708
|
+
const text = await readFile9(join23(root, file), "utf8").catch(() => "");
|
|
4114
4709
|
for (const line of text.split(/\r?\n/)) {
|
|
4115
4710
|
const match = /^\s*(?:export\s+)?[A-Za-z_][A-Za-z0-9_.-]*\s*=\s*(.*)$/.exec(line);
|
|
4116
4711
|
if (!match) continue;
|
|
@@ -4237,12 +4832,12 @@ async function askTerminal(question) {
|
|
|
4237
4832
|
}
|
|
4238
4833
|
}
|
|
4239
4834
|
async function writeBinaryAtomic(file, bytes) {
|
|
4240
|
-
const temp = `${file}.${process.pid}.${
|
|
4835
|
+
const temp = `${file}.${process.pid}.${randomBytes3(6).toString("hex")}.tmp`;
|
|
4241
4836
|
await writeFile2(temp, bytes);
|
|
4242
|
-
await
|
|
4837
|
+
await rename3(temp, file);
|
|
4243
4838
|
}
|
|
4244
4839
|
var shown = (file) => {
|
|
4245
|
-
const rel =
|
|
4840
|
+
const rel = relative10(process.cwd(), file);
|
|
4246
4841
|
return rel && !rel.startsWith("..") ? rel : file;
|
|
4247
4842
|
};
|
|
4248
4843
|
async function runReport(options = {}, io = {}) {
|
|
@@ -4251,8 +4846,8 @@ async function runReport(options = {}, io = {}) {
|
|
|
4251
4846
|
const error = io.error ?? ((text) => void process.stderr.write(`${text}
|
|
4252
4847
|
`));
|
|
4253
4848
|
const env = io.env ?? process.env;
|
|
4254
|
-
const root =
|
|
4255
|
-
if (!(await
|
|
4849
|
+
const root = resolve14(options.path ?? process.cwd());
|
|
4850
|
+
if (!(await stat3(root).catch(() => null))?.isDirectory()) {
|
|
4256
4851
|
error(`\u2716 ${options.path ?? root} is not a directory: run ${NPX} in a repo, or pass its path (${NPX} path/to/repo)`);
|
|
4257
4852
|
return { code: 2, files: null, model: null };
|
|
4258
4853
|
}
|
|
@@ -4281,9 +4876,9 @@ async function runReport(options = {}, io = {}) {
|
|
|
4281
4876
|
model.durationMs += Math.round(performance.now() - started);
|
|
4282
4877
|
const redactor = await Redactor.forRepo(root, { includeCode: options.includeCode === true, env });
|
|
4283
4878
|
const clean = redactor.model(model);
|
|
4284
|
-
const outDir =
|
|
4285
|
-
await
|
|
4286
|
-
const files = { report:
|
|
4879
|
+
const outDir = resolve14(options.out ?? root);
|
|
4880
|
+
await mkdir5(outDir, { recursive: true });
|
|
4881
|
+
const files = { report: join24(outDir, REPORT_FILE), card: join24(outDir, CARD_FILE), summary: join24(outDir, SUMMARY_FILE) };
|
|
4287
4882
|
const t = clean.totals;
|
|
4288
4883
|
const card = {
|
|
4289
4884
|
repo: redactor.scrub(clean.repo),
|
|
@@ -4367,6 +4962,23 @@ ${style.bold("Anywhere")} ${style.dim("no design system needed")}
|
|
|
4367
4962
|
an unchanged component comes from the cache (YOUI_SPECIMEN_CACHE)
|
|
4368
4963
|
|
|
4369
4964
|
${style.bold("In a product repo")}
|
|
4965
|
+
status [--json] where the product stands with its design system, in a few lines: the System
|
|
4966
|
+
and its source, what a sync would change (a dry run, offline), local drift,
|
|
4967
|
+
errors and warnings (beyond the baseline), components used, open requests,
|
|
4968
|
+
and the one thing to do next. In a workspace (youi.workspace.json, or a repo
|
|
4969
|
+
holding several products), one block per product
|
|
4970
|
+
adopt [--components dir] [--css file] [--system id] [--fork] [--dry-run]
|
|
4971
|
+
make this product's own UI its design system, kept in this repo: a draft
|
|
4972
|
+
contract per component in the components folder (the files stay where they
|
|
4973
|
+
are), the stylesheet's :root and dark variables as tokens (moved: the
|
|
4974
|
+
stylesheet imports the generated tokens.css, same names and values), three
|
|
4975
|
+
starter rules as warnings, then init and sync; --dry-run writes nothing.
|
|
4976
|
+
Variables youiyoux generated from another System are left to it (adopt says
|
|
4977
|
+
which); --fork takes them as the start of this product's own System
|
|
4978
|
+
system extract <dir> [--package name] [--dry-run]
|
|
4979
|
+
move the System adopt keeps here into a design repo of its own at <dir>
|
|
4980
|
+
(contracts with their implementations, tokens, rules), for a second app to
|
|
4981
|
+
follow; this product then follows it by path, and sync vendors its components
|
|
4370
4982
|
init --design <dir|package> write youi.project.json pointing at a design repo checkout, or at
|
|
4371
4983
|
the npm package that holds it (named, so no machine's folder), then sync;
|
|
4372
4984
|
a git repository goes in its source: { "git": "github:acme/design", "ref": "v3" }
|
|
@@ -4458,7 +5070,8 @@ ${style.bold("Detect")} ${style.dim("which UI libraries a repo or website uses
|
|
|
4458
5070
|
|
|
4459
5071
|
${style.bold("The hub")}
|
|
4460
5072
|
ui [--design dir] [--project dir] [--config file] [--root dir] [--port n] [--watch] [--state dir] [--inventory file] [--detect file]
|
|
4461
|
-
open the web hub on a design system (default: the demo system
|
|
5073
|
+
open the web hub on a design system (default: the demo system; beside a
|
|
5074
|
+
youi.workspace.json, its products, with Work > Workspace to switch);
|
|
4462
5075
|
--project adds that product's usage map (read-only scan) and the
|
|
4463
5076
|
rule editor (System > Rules, Work > Changes): drafts, impact preview,
|
|
4464
5077
|
approval, a review branch in the design repo (drafts kept in --state);
|
|
@@ -4528,6 +5141,8 @@ var OPTIONS = {
|
|
|
4528
5141
|
dark: { type: "string" },
|
|
4529
5142
|
state: { type: "string" },
|
|
4530
5143
|
inventory: { type: "string" },
|
|
5144
|
+
package: { type: "string" },
|
|
5145
|
+
fork: { type: "boolean", default: false },
|
|
4531
5146
|
cache: { type: "string" },
|
|
4532
5147
|
role: { type: "string" },
|
|
4533
5148
|
license: { type: "string" },
|
|
@@ -4551,7 +5166,7 @@ var UsageError = class extends Error {
|
|
|
4551
5166
|
var run6 = promisify5(execFile5);
|
|
4552
5167
|
var print = (text) => void process.stdout.write(`${text}
|
|
4553
5168
|
`);
|
|
4554
|
-
var
|
|
5169
|
+
var json3 = (value) => print(JSON.stringify(value, null, 2));
|
|
4555
5170
|
function loadOptions(values) {
|
|
4556
5171
|
return { ...values.config ? { configPath: values.config } : {}, ...values.design ? { designRoot: values.design } : {}, ...values.root !== void 0 ? { productRoot: values.root || "." } : {} };
|
|
4557
5172
|
}
|
|
@@ -4559,17 +5174,17 @@ async function project(values) {
|
|
|
4559
5174
|
return loadProject(values.project ?? process.cwd(), loadOptions(values));
|
|
4560
5175
|
}
|
|
4561
5176
|
async function readJsonOrFile(value) {
|
|
4562
|
-
const text = value.trim().startsWith("{") ? value : await
|
|
5177
|
+
const text = value.trim().startsWith("{") ? value : await readFile10(value, "utf8");
|
|
4563
5178
|
return JSON.parse(text);
|
|
4564
5179
|
}
|
|
4565
5180
|
async function cmdScan(values) {
|
|
4566
5181
|
const p = await project(values);
|
|
4567
5182
|
const { report } = await scanProject(p);
|
|
4568
|
-
const out = values.out ??
|
|
4569
|
-
await writeFileAtomic(
|
|
5183
|
+
const out = values.out ?? join25(p.root, ".youi/usage.json");
|
|
5184
|
+
await writeFileAtomic(resolve15(out), `${JSON.stringify(report)}
|
|
4570
5185
|
`);
|
|
4571
|
-
const recorded = await recordUsage(values, p, report,
|
|
4572
|
-
if (values.json)
|
|
5186
|
+
const recorded = await recordUsage(values, p, report, resolve15(out));
|
|
5187
|
+
if (values.json) json3(report);
|
|
4573
5188
|
else print([renderScan(report, out), ...recorded ? [recorded] : []].join("\n"));
|
|
4574
5189
|
return 0;
|
|
4575
5190
|
}
|
|
@@ -4577,7 +5192,7 @@ async function recordUsage(values, p, report, usage) {
|
|
|
4577
5192
|
const { path, home } = indexOf(values);
|
|
4578
5193
|
try {
|
|
4579
5194
|
await recordProject(await sharedStore(path), p, report);
|
|
4580
|
-
await noteSource(home, { project: { root:
|
|
5195
|
+
await noteSource(home, { project: { root: resolve15(values.project ?? process.cwd()), options: Object.fromEntries(Object.entries(loadOptions(values)).map(([key2, value]) => [key2, resolve15(value)])), usage } });
|
|
4581
5196
|
return null;
|
|
4582
5197
|
} catch (error) {
|
|
4583
5198
|
return notRecorded(error);
|
|
@@ -4592,19 +5207,19 @@ function indexOf(values) {
|
|
|
4592
5207
|
}
|
|
4593
5208
|
async function cmdGaps(values) {
|
|
4594
5209
|
const dirs = inventoryDirs(values);
|
|
4595
|
-
const inventory = values.inventory ? InventoryReport.parse(JSON.parse(await
|
|
4596
|
-
const cwd = values.inventory ?
|
|
4597
|
-
const project2 = await loadProject(
|
|
5210
|
+
const inventory = values.inventory ? InventoryReport.parse(JSON.parse(await readFile10(resolve15(values.inventory), "utf8"))) : await scanInventory(values, dirs);
|
|
5211
|
+
const cwd = values.inventory ? dirname13(resolve15(values.inventory)) : process.cwd();
|
|
5212
|
+
const project2 = await loadProject(resolve15(dirs[0]), loadOptions(values)).catch(() => null);
|
|
4598
5213
|
const system = project2 ? { name: project2.system.config.name, components: project2.system.components.map((c) => c.contract.name) } : null;
|
|
4599
|
-
const store = await readCatalog(
|
|
5214
|
+
const store = await readCatalog(resolve15(discoverCacheDir(values.cache)), indexOf(values).path).catch((error) => {
|
|
4600
5215
|
if (error instanceof MissingIndexError) return null;
|
|
4601
5216
|
throw error;
|
|
4602
5217
|
});
|
|
4603
|
-
const roots = new Map(inventory.projects.map((p) => [p.id,
|
|
5218
|
+
const roots = new Map(inventory.projects.map((p) => [p.id, resolve15(cwd, p.root)]));
|
|
4604
5219
|
const report = await buildGaps({ inventory, system, dependencies: declaredPackages(roots) }, store);
|
|
4605
|
-
if (values.out) await writeFileAtomic(
|
|
5220
|
+
if (values.out) await writeFileAtomic(resolve15(values.out), `${JSON.stringify(report, null, 2)}
|
|
4606
5221
|
`);
|
|
4607
|
-
if (values.json)
|
|
5222
|
+
if (values.json) json3(report);
|
|
4608
5223
|
else print(renderGaps(report));
|
|
4609
5224
|
return 0;
|
|
4610
5225
|
}
|
|
@@ -4612,12 +5227,12 @@ async function cmdInventory(values) {
|
|
|
4612
5227
|
if (values.history) return cmdHistory(values, []);
|
|
4613
5228
|
const dirs = inventoryDirs(values);
|
|
4614
5229
|
const report = await scanInventory(values, dirs);
|
|
4615
|
-
const out =
|
|
5230
|
+
const out = resolve15(values.out ?? join25(dirs[0], ".youi/inventory.json"));
|
|
4616
5231
|
await writeFileAtomic(out, `${JSON.stringify(report, null, 2)}
|
|
4617
5232
|
`);
|
|
4618
5233
|
const recorded = await recordScan(values, report, out);
|
|
4619
|
-
if (values.json)
|
|
4620
|
-
else print([renderInventory(report,
|
|
5234
|
+
if (values.json) json3(report);
|
|
5235
|
+
else print([renderInventory(report, relative11(process.cwd(), out).startsWith("..") ? out : relative11(process.cwd(), out)), recorded].join("\n"));
|
|
4621
5236
|
return 0;
|
|
4622
5237
|
}
|
|
4623
5238
|
async function recordScan(values, report, out) {
|
|
@@ -4634,7 +5249,7 @@ async function recordScan(values, report, out) {
|
|
|
4634
5249
|
async function cmdHistory(values, positionals) {
|
|
4635
5250
|
const { path } = indexOf(values);
|
|
4636
5251
|
const scans = await (await sharedStore(path, process.env, { readOnly: true })).history({ ...positionals[0] ? { project: positionals[0] } : {}, limit: positiveInt(values.limit, "--limit") ?? 50 });
|
|
4637
|
-
if (values.json)
|
|
5252
|
+
if (values.json) json3(scans);
|
|
4638
5253
|
else print(renderHistory(scans));
|
|
4639
5254
|
return 0;
|
|
4640
5255
|
}
|
|
@@ -4642,19 +5257,19 @@ async function cmdIndex(values, positionals) {
|
|
|
4642
5257
|
const [action, ...rest] = positionals;
|
|
4643
5258
|
const { path, home } = indexOf(values);
|
|
4644
5259
|
if (action === "path") {
|
|
4645
|
-
if (values.json)
|
|
5260
|
+
if (values.json) json3({ store: storeLocation({ path }), sources: join25(home, SOURCES_FILE), scans: join25(home, SCANS_LOG) });
|
|
4646
5261
|
else print(storeLocation({ path }));
|
|
4647
5262
|
return 0;
|
|
4648
5263
|
}
|
|
4649
5264
|
if (action === "stats") {
|
|
4650
5265
|
const stats = await (await sharedStore(path, process.env, { readOnly: true })).stats();
|
|
4651
|
-
if (values.json)
|
|
5266
|
+
if (values.json) json3(stats);
|
|
4652
5267
|
else print([`${style.bold("index")} ${stats.location} ${style.dim(`\xB7 ${stats.engine} \xB7 schema ${stats.schema} \xB7 generation ${stats.generation ?? "not rebuilt"}`)}`, ...Object.entries(stats.tables).map(([table, n]) => ` ${table.padEnd(16)} ${n.toLocaleString("en-US")}`)].join("\n"));
|
|
4653
5268
|
return 0;
|
|
4654
5269
|
}
|
|
4655
5270
|
if (action === "rebuild") {
|
|
4656
|
-
const summary = await rebuildIndex(path, values.cache ? { discoverCache:
|
|
4657
|
-
if (values.json)
|
|
5271
|
+
const summary = await rebuildIndex(path, values.cache ? { discoverCache: resolve15(values.cache) } : {});
|
|
5272
|
+
if (values.json) json3(summary);
|
|
4658
5273
|
else print(renderRebuild(summary));
|
|
4659
5274
|
return 0;
|
|
4660
5275
|
}
|
|
@@ -4668,7 +5283,7 @@ async function cmdWhere(values, positionals) {
|
|
|
4668
5283
|
const { report } = await scanProject(p);
|
|
4669
5284
|
const limit = values.all ? Number.MAX_SAFE_INTEGER : Number(values.limit ?? (values.json ? 20 : 24));
|
|
4670
5285
|
const where2 = whereUsed(p, report, query, { ...values.route ? { route: values.route } : {}, limit });
|
|
4671
|
-
if (values.json)
|
|
5286
|
+
if (values.json) json3(where2);
|
|
4672
5287
|
else print(renderWhere(where2, report));
|
|
4673
5288
|
return 0;
|
|
4674
5289
|
}
|
|
@@ -4679,7 +5294,7 @@ async function cmdShow(values, positionals) {
|
|
|
4679
5294
|
const contract = findContract(p, query);
|
|
4680
5295
|
if (!contract) throw new Error(`no component "${query}"; known: ${p.system.components.map((c) => c.contract.name).join(", ")}`);
|
|
4681
5296
|
const card = componentCard(p, contract, (await scanProject(p)).report);
|
|
4682
|
-
if (values.json)
|
|
5297
|
+
if (values.json) json3(card);
|
|
4683
5298
|
else print(renderShow(card));
|
|
4684
5299
|
return 0;
|
|
4685
5300
|
}
|
|
@@ -4688,12 +5303,12 @@ async function cmdBom(values, positionals) {
|
|
|
4688
5303
|
if (!route) throw new UsageError("youiyoux bom <route>");
|
|
4689
5304
|
const p = await project(values);
|
|
4690
5305
|
const bom = screenBom(p, (await scanProject(p)).report, route);
|
|
4691
|
-
if (values.json)
|
|
5306
|
+
if (values.json) json3(bom);
|
|
4692
5307
|
else print(renderBom(bom));
|
|
4693
5308
|
return 0;
|
|
4694
5309
|
}
|
|
4695
5310
|
function baselineFile(values, p) {
|
|
4696
|
-
if (values["baseline-file"]) return
|
|
5311
|
+
if (values["baseline-file"]) return resolve15(values["baseline-file"]);
|
|
4697
5312
|
return values.baseline ? baselinePath(p, values.config) : null;
|
|
4698
5313
|
}
|
|
4699
5314
|
var checkScope = (values, positionals) => ({ ...positionals.length > 0 ? { files: positionals } : {}, ...values.rule ? { rule: values.rule } : {}, quiet: values.quiet });
|
|
@@ -4712,7 +5327,7 @@ async function cmdCheck(values, positionals) {
|
|
|
4712
5327
|
const result = await runCheck(p, { ...checkScope(values, positionals), baseline: baselineFile(values, p) });
|
|
4713
5328
|
if (format === "json") {
|
|
4714
5329
|
const { report } = result.scan;
|
|
4715
|
-
|
|
5330
|
+
json3({ errors: result.errors, incomplete: result.incomplete, identity: result.identity, unmatched: result.unmatched, notRun: result.notRun, violations: result.violations, baseline: result.baseline, lock: result.lock, suppressed: report.suppressed, coverage: report.coverage, status: report.status, checks: report.checks });
|
|
4716
5331
|
} else if (format === "github") {
|
|
4717
5332
|
for (const line of githubAnnotations(result, await repoPrefix(p.root))) print(line);
|
|
4718
5333
|
print(renderCheckText(result));
|
|
@@ -4724,7 +5339,7 @@ async function cmdCheckDiff(values, positionals, p, format) {
|
|
|
4724
5339
|
if (values.baseline) throw new UsageError("youiyoux check --diff looks only at added lines, so a baseline does not apply; drop --baseline");
|
|
4725
5340
|
const change = await workingTreeChange(p, values.base);
|
|
4726
5341
|
const result = configured(p) ? await runCheck(p, { ...checkScope(values, positionals), diff: change }) : await runRepoCheck(p, change);
|
|
4727
|
-
if (format === "json")
|
|
5342
|
+
if (format === "json") json3(diffJson(result));
|
|
4728
5343
|
else if (format === "github") {
|
|
4729
5344
|
for (const line of githubDiffAnnotations(result, await repoPrefix(p.root))) print(line);
|
|
4730
5345
|
print(renderDiffText(result));
|
|
@@ -4748,9 +5363,9 @@ async function cmdBaseline(values) {
|
|
|
4748
5363
|
const file = baselineFile(values, p) ?? baselinePath(p, values.config);
|
|
4749
5364
|
const written = await writeBaseline(p, file, { force: values.force });
|
|
4750
5365
|
const { totals } = written.baseline;
|
|
4751
|
-
if (values.json)
|
|
5366
|
+
if (values.json) json3({ file: written.file, action: written.action, totals, notAdded: written.notAdded, identity: written.baseline.identity });
|
|
4752
5367
|
else {
|
|
4753
|
-
print(`${style.ok(written.action)} ${
|
|
5368
|
+
print(`${style.ok(written.action)} ${relative11(process.cwd(), file) || file}: ${plural(totals.errors, "error")} \xB7 ${plural(totals.warnings, "warning")} accepted`);
|
|
4754
5369
|
print(style.dim(` rules ${describeIdentity(written.baseline.identity)}`));
|
|
4755
5370
|
if (written.notAdded > 0) print(style.warn(` ${plural(written.notAdded, "current violation")} not added: fix them, or accept them deliberately with --force`));
|
|
4756
5371
|
}
|
|
@@ -4764,30 +5379,30 @@ async function cmdPlan(values, positionals) {
|
|
|
4764
5379
|
else if (positionals[0] && values.patch) input = { element: positionals[0], patch: await readJsonOrFile(values.patch) };
|
|
4765
5380
|
else throw new UsageError("youiyoux plan <component> --patch <json|file> or youiyoux plan --token <id> [--value v]");
|
|
4766
5381
|
const plan = await planChange(p, scan, input);
|
|
4767
|
-
if (values.json)
|
|
5382
|
+
if (values.json) json3(plan);
|
|
4768
5383
|
else print(renderPlan(plan));
|
|
4769
5384
|
return 0;
|
|
4770
5385
|
}
|
|
4771
5386
|
async function cmdRules(values) {
|
|
4772
5387
|
const p = await project(values);
|
|
4773
5388
|
if (values.preview) {
|
|
4774
|
-
const draft = (0,
|
|
5389
|
+
const draft = (0, import_yaml4.parse)(await readFile10(values.preview, "utf8"));
|
|
4775
5390
|
const evaluation = evaluateRule(p, await scanProject(p), draft);
|
|
4776
|
-
if (values.json)
|
|
5391
|
+
if (values.json) json3(evaluation);
|
|
4777
5392
|
else print(renderRulePreview(evaluation));
|
|
4778
5393
|
return evaluation.ok && evaluation.findings !== null ? 0 : 1;
|
|
4779
5394
|
}
|
|
4780
5395
|
const rules = p.system.rules.map((rule2) => ({ rule: rule2, execution: ruleCapability(rule2) }));
|
|
4781
|
-
if (values.json)
|
|
5396
|
+
if (values.json) json3({ capabilities: checkCapabilities(), rules: rules.map(({ rule: rule2, execution }) => ({ id: rule2.id, severity: rule2.severity, ...execution })) });
|
|
4782
5397
|
else print(renderRules(rules));
|
|
4783
5398
|
return rules.some(({ execution }) => execution.state === "unsupported") ? 1 : 0;
|
|
4784
5399
|
}
|
|
4785
5400
|
async function cmdMigrate(values, positionals) {
|
|
4786
5401
|
const file = positionals[0];
|
|
4787
5402
|
if (!file) throw new UsageError("youiyoux migrate <migration.yaml> [--write] [--diff]");
|
|
4788
|
-
const spec = MigrationSpec.parse((0,
|
|
5403
|
+
const spec = MigrationSpec.parse((0, import_yaml4.parse)(await readFile10(file, "utf8")));
|
|
4789
5404
|
const result = await runMigration(await project(values), spec, { write: values.write });
|
|
4790
|
-
if (values.json)
|
|
5405
|
+
if (values.json) json3(result);
|
|
4791
5406
|
else print(renderMigration(result, values.diff || !values.write));
|
|
4792
5407
|
return result.status === "rejected" ? 1 : 0;
|
|
4793
5408
|
}
|
|
@@ -4797,14 +5412,14 @@ async function cmdLibrary(values, positionals) {
|
|
|
4797
5412
|
const p = await project(values);
|
|
4798
5413
|
const words = positionals.slice(1).join(" ");
|
|
4799
5414
|
const result = searchLibrary(await loadLibrary(p.repo.root), words, Number(values.limit ?? 20));
|
|
4800
|
-
if (values.json)
|
|
5415
|
+
if (values.json) json3(result);
|
|
4801
5416
|
else print(renderLibrarySearch(result, words));
|
|
4802
5417
|
return 0;
|
|
4803
5418
|
}
|
|
4804
5419
|
if (action === "request" && query && values.reason) {
|
|
4805
5420
|
const p = await project(values);
|
|
4806
5421
|
const request = adoptionRequest(p, await loadLibrary(p.repo.root), query, values.reason, await scanProject(p));
|
|
4807
|
-
if (values.json)
|
|
5422
|
+
if (values.json) json3(request);
|
|
4808
5423
|
else print(renderAdoptionRequest(request));
|
|
4809
5424
|
return 0;
|
|
4810
5425
|
}
|
|
@@ -4813,7 +5428,7 @@ async function cmdLibrary(values, positionals) {
|
|
|
4813
5428
|
async function cmdSync(values) {
|
|
4814
5429
|
const p = await loadProject(values.project ?? process.cwd(), { ...loadOptions(values), refreshDesign: true });
|
|
4815
5430
|
const result = await syncProject(p, { dryRun: values["dry-run"], adopt: values.adopt });
|
|
4816
|
-
if (values.json)
|
|
5431
|
+
if (values.json) json3(result);
|
|
4817
5432
|
else print([renderSync(result, values["dry-run"]), ...p.linked ? [style.warn(`! linked: the design system is your checkout ${p.linked}, not the config's source (youiyoux unlink to undo)`)] : []].join("\n"));
|
|
4818
5433
|
return result.actions.some((a) => a.action === "conflict") ? 1 : 0;
|
|
4819
5434
|
}
|
|
@@ -4821,7 +5436,7 @@ async function cmdInit(values) {
|
|
|
4821
5436
|
if (!values.design) throw new UsageError("youiyoux init --design <dir|package> (a design repo checkout, the directory holding youi.config.json, or the npm package that holds it)");
|
|
4822
5437
|
const root = values.project ?? process.cwd();
|
|
4823
5438
|
const design = await designArg(values.design, root);
|
|
4824
|
-
const result = await initProject({ root, ...design, schemaDir:
|
|
5439
|
+
const result = await initProject({ root, ...design, schemaDir: join25(TOOL_ROOT, "schema"), ...values.system ? { system: values.system } : {}, ...values.components ? { componentsDir: values.components } : {}, force: values.force });
|
|
4825
5440
|
if (!values.json) {
|
|
4826
5441
|
print(`${style.ok("created")} ${result.file}`);
|
|
4827
5442
|
for (const note of result.notes) print(style.dim(` \xB7 ${note}`));
|
|
@@ -4830,6 +5445,82 @@ async function cmdInit(values) {
|
|
|
4830
5445
|
const { design: _named, ...rest } = values;
|
|
4831
5446
|
return cmdSync({ ...design.package ? rest : values, project: root });
|
|
4832
5447
|
}
|
|
5448
|
+
async function cmdAdopt(values) {
|
|
5449
|
+
const root = resolve15(values.project ?? process.cwd());
|
|
5450
|
+
if (await exists(join25(root, PROJECT_FILE))) throw new Error(`${root} already has ${PROJECT_FILE}: it follows a design system already (youiyoux sync updates it)`);
|
|
5451
|
+
const plan = await planAdoption({ root, ...values.components ? { componentsDir: values.components } : {}, ...values.css ? { css: values.css } : {}, ...values.system ? { system: values.system } : {}, fork: values.fork });
|
|
5452
|
+
const taken = (await Promise.all(plan.files.map(async (f) => await exists(join25(root, f.path)) ? f.path : null))).filter((f) => f !== null);
|
|
5453
|
+
if (taken.length > 0) throw new Error(`nothing was written: ${taken.slice(0, 5).join(", ")}${taken.length > 5 ? ` and ${taken.length - 5} more` : ""} already exist${taken.length === 1 ? "s" : ""}`);
|
|
5454
|
+
if (values["dry-run"]) {
|
|
5455
|
+
if (values.json) json3(plan);
|
|
5456
|
+
else print([...adoptLines(plan, true), "", style.dim(`dry run: nothing written; would write ${plan.files.map((f) => f.path).join(", ")}${plan.stylesheet ? ` and change ${plan.stylesheet.path}` : ""}`)].join("\n"));
|
|
5457
|
+
return 0;
|
|
5458
|
+
}
|
|
5459
|
+
const stylesheet = plan.stylesheet ? await readFile10(join25(root, plan.stylesheet.path), "utf8") : null;
|
|
5460
|
+
try {
|
|
5461
|
+
for (const file of plan.files) await writeFileAtomic(join25(root, file.path), file.content);
|
|
5462
|
+
if (plan.stylesheet) await writeFileAtomic(join25(root, plan.stylesheet.path), plan.stylesheet.content);
|
|
5463
|
+
await initProject({ root, design: root, schemaDir: join25(TOOL_ROOT, "schema"), system: plan.system.id, componentsDir: plan.componentsDir, outputs: { ...plan.outputs, targets: [...plan.outputs.targets], dark: [...plan.outputs.dark] }, tokenSources: plan.tokenSources });
|
|
5464
|
+
await syncProject(await loadProject(root), { dryRun: true });
|
|
5465
|
+
} catch (error) {
|
|
5466
|
+
for (const file of [...plan.files.map((f) => f.path), PROJECT_FILE]) await rm6(join25(root, file), { force: true });
|
|
5467
|
+
for (const folder of [...new Set(plan.files.map((f) => dirname13(f.path)))].sort((a, b) => b.length - a.length)) await rmdir3(join25(root, folder)).catch(() => void 0);
|
|
5468
|
+
await rmdir3(join25(root, "design/components")).catch(() => void 0);
|
|
5469
|
+
await rmdir3(join25(root, "design")).catch(() => void 0);
|
|
5470
|
+
if (plan.stylesheet && stylesheet !== null) await writeFileAtomic(join25(root, plan.stylesheet.path), stylesheet);
|
|
5471
|
+
throw new Error(`adopt was undone, nothing kept: ${error instanceof Error ? error.message : String(error)}`);
|
|
5472
|
+
}
|
|
5473
|
+
if (!values.json) print([...adoptLines(plan), ""].join("\n"));
|
|
5474
|
+
const synced = await cmdSync({ ...values, project: root, components: void 0, css: void 0, system: void 0 });
|
|
5475
|
+
if (!values.json) print(`Next: describe each component in design/components/<name>/<name>.contract.json (its summary, guidance and examples; agents read them), see what is left with youiyoux check, and when a second app needs this System, youiyoux system extract <dir> moves it into its own repo`);
|
|
5476
|
+
return synced;
|
|
5477
|
+
}
|
|
5478
|
+
function adoptLines(plan, dryRun = false) {
|
|
5479
|
+
const { tokens } = plan;
|
|
5480
|
+
const shown2 = plan.components.slice(0, 6).join(", ") + (plan.components.length > 6 ? `, \u2026 ${plan.components.length - 6} more` : "");
|
|
5481
|
+
return [
|
|
5482
|
+
`${style.ok(dryRun ? "would adopt" : "adopted")} ${plan.system.name} ${style.dim(`(system ${plan.system.id}) from this product's own UI`)}`,
|
|
5483
|
+
` components ${plural(plan.components.length, "component")} in ${plan.componentsDir} (${shown2}): a draft contract each in design/components; the files stay where they are${plan.support.length > 0 ? ` (${plan.support.join(", ")} as support)` : ""}`,
|
|
5484
|
+
tokens.stylesheet ? ` tokens ${plural(tokens.count, "variable")}${tokens.dark ? ", light and dark," : ""} from ${tokens.stylesheet} \u2192 design/tokens${tokens.left.length > 0 ? `; ${tokens.left.length} left in the stylesheet, not typed: ${tokens.left.slice(0, 4).map((l) => `--${l.name} (${l.value})`).join(", ")}${tokens.left.length > 4 ? ", \u2026" : ""}` : ""}` : ` tokens none: no stylesheet defines :root variables (--css <file> names one)`,
|
|
5485
|
+
...plan.stylesheet ? [` stylesheet ${plan.stylesheet.path}: their ${plural(plan.stylesheet.moved, "declaration")}${tokens.dark ? " (light and dark)" : ""} moved into the System; it imports ${plan.outputs.dir}/tokens.css (same names, same values)`] : [],
|
|
5486
|
+
` rules 3 starter rules, as warnings: design/rules`,
|
|
5487
|
+
` config youi.config.json (the System) and youi.project.json (this product follows it)`,
|
|
5488
|
+
...plan.notes.map((n) => style.warn(` ! ${n}`)),
|
|
5489
|
+
// The product follows a System already: its values were left to it, and here is how to keep one System, or fork it.
|
|
5490
|
+
...plan.follows.map((g) => {
|
|
5491
|
+
const system = g.system ?? "a System";
|
|
5492
|
+
return style.warn(` ! ${g.file} holds ${plural(g.count, "variable")} youiyoux generated from ${system}${g.version ? ` v${g.version}` : ""}: this product already uses ${g.system ? "that System" : "one"}, so they were left to it. To manage the product with ${system}, run youiyoux init --design <${system}'s repo or package>; to make a System of the product's own from them, run youiyoux adopt --fork`);
|
|
5493
|
+
})
|
|
5494
|
+
];
|
|
5495
|
+
}
|
|
5496
|
+
async function cmdStatus(values) {
|
|
5497
|
+
const report = await workspaceStatus(process.cwd(), values.project);
|
|
5498
|
+
if (values.json) json3(report);
|
|
5499
|
+
else print(renderStatus(report));
|
|
5500
|
+
return report.projects.length === 0 ? 1 : statusExitCode(report);
|
|
5501
|
+
}
|
|
5502
|
+
async function cmdSystem(values, positionals) {
|
|
5503
|
+
const [action, dir, ...extra] = positionals;
|
|
5504
|
+
if (action !== "extract" || dir === void 0 || extra.length > 0) throw new UsageError("youiyoux system extract <dir> [--package <name>] [--dry-run] (move the System youiyoux adopt keeps in this product into a design repo of its own)");
|
|
5505
|
+
if (values.package !== void 0 && !PACKAGE_NAME.test(values.package)) throw new UsageError(`--package ${JSON.stringify(values.package)}: expected an npm package name such as @acme/design`);
|
|
5506
|
+
const p = await project(values);
|
|
5507
|
+
const result = await extractSystem(p, dir, { ...values.package ? { package: values.package } : {}, dryRun: values["dry-run"] });
|
|
5508
|
+
const contracts = result.written.filter((f) => f.endsWith(".contract.json")).length;
|
|
5509
|
+
const lines = [
|
|
5510
|
+
`${style.ok(values["dry-run"] ? "would extract" : "extracted")} ${p.system.config.name} ${style.dim(`(system ${result.system.id})`)} \u2192 ${result.target}`,
|
|
5511
|
+
` repo ${plural(result.written.length, "file")}: youi.config.json, ${plural(contracts, "contract")} with their implementations beside them, the tokens, rules${result.system.design ? " and design.md" : ""}${values.package ? `, and package.json (${values.package})` : ""}`,
|
|
5512
|
+
` product youi.project.json follows it (source: ${JSON.stringify(result.source)}); ${values["dry-run"] ? "would remove" : "removed"} its System files: ${result.removed.join(", ")}`
|
|
5513
|
+
];
|
|
5514
|
+
if (values["dry-run"]) {
|
|
5515
|
+
print([...lines, "", style.dim("dry run: nothing written or removed")].join("\n"));
|
|
5516
|
+
return 0;
|
|
5517
|
+
}
|
|
5518
|
+
print([...lines, ""].join("\n"));
|
|
5519
|
+
const synced = await cmdSync({ ...values, project: p.root, package: void 0 });
|
|
5520
|
+
const git3 = `push it and set source to { "git": "<its url>", "ref": "v${result.system.version}" }`;
|
|
5521
|
+
print(`Next: make ${relative11(process.cwd(), result.target) || "."} a repository of its own and commit it. For each app to follow it, ${values.package ? `publish ${values.package}, add it as a dependency and set source to { "package": "${values.package}" }, or ${git3}` : `${git3}, or publish it as an npm package (--package names it)`}. Change components there from now on: youiyoux sync merges them into each product`);
|
|
5522
|
+
return synced;
|
|
5523
|
+
}
|
|
4833
5524
|
async function cmdLink(values, positionals) {
|
|
4834
5525
|
const [dir] = positionals;
|
|
4835
5526
|
const p = await loadProject(values.project ?? process.cwd(), { ...loadOptions(values), ...dir !== void 0 ? { designRoot: dir } : {} });
|
|
@@ -4845,37 +5536,37 @@ async function cmdUnlink(values) {
|
|
|
4845
5536
|
const root = values.project ?? process.cwd();
|
|
4846
5537
|
const linked = await linkedDesign(root);
|
|
4847
5538
|
if (linked !== null) await setLink(root, null);
|
|
4848
|
-
print(linked ? `${style.ok("unlinked")} ${
|
|
5539
|
+
print(linked ? `${style.ok("unlinked")} ${resolve15(root)} uses its config's source again` : `${resolve15(root)} was not linked`);
|
|
4849
5540
|
return 0;
|
|
4850
5541
|
}
|
|
4851
5542
|
async function designArg(value, root) {
|
|
4852
|
-
if (existsSync5(
|
|
4853
|
-
return { design: await designRootOf({ package: value },
|
|
5543
|
+
if (existsSync5(resolve15(value)) || !PACKAGE_NAME.test(value)) return { design: value };
|
|
5544
|
+
return { design: await designRootOf({ package: value }, join25(resolve15(root), PROJECT_FILE), root), package: value };
|
|
4854
5545
|
}
|
|
4855
5546
|
async function cmdBuild(values) {
|
|
4856
|
-
const root =
|
|
5547
|
+
const root = resolve15(values.design ?? process.cwd());
|
|
4857
5548
|
const system = getSystem(await loadDesignRepo(root), values.system);
|
|
4858
5549
|
if (safeId(system.config.id) === null) throw new Error(`Cannot build system ${JSON.stringify(system.config.id)}: the id must be a plain name without ".." or path separators.`);
|
|
4859
5550
|
const output = OutputConfig.parse({ cssVarPrefix: values.prefix ?? "youi", targets: ["css-vars", "tailwind-v4", "ts", "react-native"] });
|
|
4860
5551
|
const started = performance.now();
|
|
4861
5552
|
const built = await buildTokens(root, system.config, output, { contracts: system.components.map((c) => c.contract) });
|
|
4862
|
-
const out =
|
|
4863
|
-
const boundary = values.out ? out :
|
|
4864
|
-
if (!within(boundary, out) || built.files.some((file) => !within(out,
|
|
4865
|
-
for (const file of built.files) await writeFileAtomic(
|
|
4866
|
-
if (values.json)
|
|
5553
|
+
const out = resolve15(values.out ?? join25(root, "build", system.config.id));
|
|
5554
|
+
const boundary = values.out ? out : join25(root, "build");
|
|
5555
|
+
if (!within(boundary, out) || built.files.some((file) => !within(out, join25(out, file.path)))) throw new Error(`Cannot build system ${JSON.stringify(system.config.id)}: an output path leaves the output directory.`);
|
|
5556
|
+
for (const file of built.files) await writeFileAtomic(join25(out, file.path), file.content);
|
|
5557
|
+
if (values.json) json3({ out, files: built.files.map((f) => f.path), tokens: built.tokens.length });
|
|
4867
5558
|
else print(`${style.ok("built")} ${plural(built.tokens.length, "token")} \u2192 ${built.files.map((f) => f.path).join(", ")} ${style.dim(`in ${out} (${ms(performance.now() - started)})`)}`);
|
|
4868
5559
|
return 0;
|
|
4869
5560
|
}
|
|
4870
5561
|
async function cmdParity(values) {
|
|
4871
5562
|
if (!values.css) throw new UsageError("youiyoux parity --css <file> [--prefix p]");
|
|
4872
|
-
const root =
|
|
5563
|
+
const root = resolve15(values.design ?? process.cwd());
|
|
4873
5564
|
const system = getSystem(await loadDesignRepo(root), values.system);
|
|
4874
5565
|
const prefix = values.prefix ?? "youi";
|
|
4875
5566
|
const dark = { selector: values.dark ?? ':root[data-theme="dark"]' };
|
|
4876
5567
|
const built = await buildTokens(root, system.config, OutputConfig.parse({ cssVarPrefix: prefix, dark: [dark], targets: ["css-vars"] }));
|
|
4877
|
-
const report = compareParity(await
|
|
4878
|
-
if (values.json)
|
|
5568
|
+
const report = compareParity(await readFile10(values.css, "utf8"), { light: { selector: values.light ?? ":root" }, dark }, built.files[0].content, { light: { selector: ":root" }, dark }, prefix);
|
|
5569
|
+
if (values.json) json3(report);
|
|
4879
5570
|
else {
|
|
4880
5571
|
const ok = report.different.length === 0 && report.missing.length === 0;
|
|
4881
5572
|
print(`${ok ? style.ok("\u2713 parity") : style.error("\u2716 parity")} ${report.matched} matched \xB7 ${report.different.length} different \xB7 ${report.missing.length} missing \xB7 ${style.dim(`${report.added.length} new in the build`)}`);
|
|
@@ -4887,18 +5578,18 @@ async function cmdParity(values) {
|
|
|
4887
5578
|
async function cmdSchema(values) {
|
|
4888
5579
|
const schemas = renderJsonSchemas();
|
|
4889
5580
|
if (values.check) {
|
|
4890
|
-
const dir =
|
|
5581
|
+
const dir = join25(values.design ? resolve15(values.design) : TOOL_ROOT, "schema");
|
|
4891
5582
|
const stale = [];
|
|
4892
|
-
for (const { file, content } of schemas) if (await
|
|
4893
|
-
if (values.json)
|
|
5583
|
+
for (const { file, content } of schemas) if (await readFile10(join25(dir, file), "utf8").catch(() => null) !== content) stale.push(file);
|
|
5584
|
+
if (values.json) json3({ dir, stale });
|
|
4894
5585
|
else print(stale.length === 0 ? `${style.ok("\u2713")} ${dir} matches this youi's schemas` : `${style.error("\u2716")} out of date in ${dir}: ${stale.join(", ")}; run youiyoux schema --write${values.design ? " --design <dir>" : ""}`);
|
|
4895
5586
|
return stale.length === 0 ? 0 : 1;
|
|
4896
5587
|
}
|
|
4897
5588
|
if (values.write) {
|
|
4898
|
-
const root = values.design ?
|
|
4899
|
-
for (const { file, content } of schemas) await writeFileAtomic(
|
|
5589
|
+
const root = values.design ? resolve15(values.design) : TOOL_ROOT;
|
|
5590
|
+
for (const { file, content } of schemas) await writeFileAtomic(join25(root, "schema", file), content);
|
|
4900
5591
|
print(`${style.ok("wrote")} ${schemas.map((s) => s.file).join(", ")}`);
|
|
4901
|
-
} else
|
|
5592
|
+
} else json3(Object.fromEntries(schemas.map((s) => [s.file, JSON.parse(s.content)])));
|
|
4902
5593
|
return 0;
|
|
4903
5594
|
}
|
|
4904
5595
|
function hookTarget(values) {
|
|
@@ -4919,14 +5610,14 @@ async function cmdHooks(values, positionals) {
|
|
|
4919
5610
|
if (action !== "install") throw new UsageError("youiyoux hooks install [--claude] [--git] [--ci [--diff]] [--strict] or youiyoux hooks run claude|pre-commit");
|
|
4920
5611
|
const p = await loadCheckProject(values.project ?? process.cwd(), loadOptions(values), !values.project);
|
|
4921
5612
|
if (values.ci && !configured(p)) throw new UsageError("Run youiyoux init --design <dir> before installing the pinned CI workflow; repo-mode CI can run youiyoux check --diff <base> --strict directly");
|
|
4922
|
-
const target = { ...hookTarget(values), project: p.root, ...configured(p) && p.configFile && p.configFile !==
|
|
5613
|
+
const target = { ...hookTarget(values), project: p.root, ...configured(p) && p.configFile && p.configFile !== join25(p.root, PROJECT_FILE) ? { config: p.configFile } : {}, ...values.root !== void 0 ? { root: p.root } : {} };
|
|
4923
5614
|
const all = !values.claude && !values.git && !values.ci;
|
|
4924
5615
|
const results = [];
|
|
4925
5616
|
if (values.claude || all) results.push({ kind: "Claude Code", ...await installClaudeHook(await repoRootOf(p.root), hookCommand(TOOL_ROOT, "claude", target)) });
|
|
4926
5617
|
if (values.git || all) results.push({ kind: "git pre-commit", ...await installPreCommit(p.root, hookCommand(TOOL_ROOT, "pre-commit", target, values.strict ? ["--strict"] : []), values.strict) });
|
|
4927
5618
|
if (values.git || all) results.push({ kind: "Codex", ...configured(p) ? codexFallback(p.root, p.config.agents.agentsMd) : await installRepoAgentInstructions(p.root) });
|
|
4928
5619
|
if (values.ci && configured(p)) results.push({ kind: "CI", ...await installWorkflow(p, TOOL_ROOT, target, values.force, { diff: values.diff }) });
|
|
4929
|
-
if (values.json)
|
|
5620
|
+
if (values.json) json3(results);
|
|
4930
5621
|
else for (const r of results) print(`${r.action === "unchanged" || r.action === "skipped" ? style.dim(r.action) : style.ok(r.action)} ${r.kind}: ${r.file}
|
|
4931
5622
|
${style.dim(r.guarantee)}`);
|
|
4932
5623
|
return 0;
|
|
@@ -4934,11 +5625,11 @@ async function cmdHooks(values, positionals) {
|
|
|
4934
5625
|
async function cmdAgents(values, positionals) {
|
|
4935
5626
|
const [action] = positionals;
|
|
4936
5627
|
if (action !== "install" && action !== "remove") throw new UsageError("youiyoux agents install|remove [--claude] [--codex] [--json]");
|
|
4937
|
-
const projectRoot =
|
|
5628
|
+
const projectRoot = resolve15(values.project ?? process.cwd());
|
|
4938
5629
|
const repoRoot = await repoRootOf(projectRoot);
|
|
4939
|
-
const target = { project: projectRoot, ...values.config ? { config:
|
|
5630
|
+
const target = { project: projectRoot, ...values.config ? { config: resolve15(values.config) } : {}, ...values.design ? { design: resolve15(values.design) } : {}, ...values.root !== void 0 ? { root: resolve15(values.root || ".") } : {} };
|
|
4940
5631
|
const server = mcpCommand(TOOL_ROOT, target);
|
|
4941
|
-
const system = existsSync5(
|
|
5632
|
+
const system = existsSync5(resolve15(values.config ?? join25(projectRoot, PROJECT_FILE))) ? await project(values) : null;
|
|
4942
5633
|
const claudeServer = system && system.root === repoRoot ? { command: "node", args: mcpArgs(system) } : relativeTo(repoRoot, server);
|
|
4943
5634
|
const both = !values.claude && !values.codex;
|
|
4944
5635
|
const results = [];
|
|
@@ -4946,7 +5637,7 @@ async function cmdAgents(values, positionals) {
|
|
|
4946
5637
|
results.push(...rule2.map((r) => ({ kind: "agent rule", ...r })));
|
|
4947
5638
|
if (values.claude || both) results.push({ kind: "Claude Code MCP", ...action === "install" ? await installClaudeMcp(repoRoot, claudeServer) : await removeClaudeMcp(repoRoot) });
|
|
4948
5639
|
if (values.codex || both) results.push({ kind: "Codex MCP", ...action === "install" ? await installCodexMcp(repoRoot, server) : await removeCodexMcp(repoRoot) });
|
|
4949
|
-
if (values.json)
|
|
5640
|
+
if (values.json) json3(results);
|
|
4950
5641
|
else {
|
|
4951
5642
|
for (const r of results) print(`${r.action === "unchanged" ? style.dim("unchanged") : style.ok(r.action)} ${r.kind}: ${r.file}
|
|
4952
5643
|
${style.dim(r.guarantee)}`);
|
|
@@ -5009,7 +5700,32 @@ async function startEditor(values, port) {
|
|
|
5009
5700
|
return null;
|
|
5010
5701
|
}
|
|
5011
5702
|
}
|
|
5703
|
+
async function workspaceUi(values) {
|
|
5704
|
+
if (values.design || values.project || values.config || values.root) return null;
|
|
5705
|
+
const { roots, workspace } = await statusTargets(process.cwd());
|
|
5706
|
+
if (workspace === null || roots.length === 0) return null;
|
|
5707
|
+
print(style.dim(`reading the status of ${plural(roots.length, "product")}\u2026`));
|
|
5708
|
+
const report = await workspaceStatus(process.cwd());
|
|
5709
|
+
const first = report.projects.findIndex((p) => !("error" in p));
|
|
5710
|
+
if (first < 0) {
|
|
5711
|
+
print(renderStatus(report));
|
|
5712
|
+
throw new Error("no product in this workspace could be read: see each one's Next above");
|
|
5713
|
+
}
|
|
5714
|
+
const port = values.port ?? DEFAULT_HUB_PORT;
|
|
5715
|
+
let open = null;
|
|
5716
|
+
const hub = await startWorkspaceHub({ roots, current: first, status: () => workspaceStatus(process.cwd()), open: (root) => open ? open(root) : Promise.resolve(1) });
|
|
5717
|
+
print(`${style.ok("workspace hub")} http://localhost:${port}/workspace#key=${hub.key}`);
|
|
5718
|
+
print(style.dim(` ${plural(roots.length, "product")}; showing ${report.projects[first].root} first. Open the link: it carries this run's key, which switching needs`));
|
|
5719
|
+
print(style.dim(" Rules and Changes are read-only here; youiyoux ui --project <dir> opens the rule editor on one product"));
|
|
5720
|
+
try {
|
|
5721
|
+
return await runHub(TOOL_ROOT, { project: roots[first], port, api: hub.url, workspace: (snapshot) => open = snapshot, ...values.inventory ? { inventory: values.inventory } : {}, ...values.detect ? { detect: values.detect } : {} });
|
|
5722
|
+
} finally {
|
|
5723
|
+
await hub.close();
|
|
5724
|
+
}
|
|
5725
|
+
}
|
|
5012
5726
|
async function cmdUi(values) {
|
|
5727
|
+
const workspace = await workspaceUi(values);
|
|
5728
|
+
if (workspace !== null) return workspace;
|
|
5013
5729
|
const port = values.port ?? DEFAULT_HUB_PORT;
|
|
5014
5730
|
const editor = await startEditor(values, port);
|
|
5015
5731
|
try {
|
|
@@ -5021,23 +5737,23 @@ async function cmdUi(values) {
|
|
|
5021
5737
|
async function cmdDetect(values, positionals) {
|
|
5022
5738
|
const target = detectTarget(positionals);
|
|
5023
5739
|
if (!target) throw new UsageError("youiyoux detect <repo-path> or youiyoux detect <url\u2026> [--expose] (one repo, or one or more URLs; not both)");
|
|
5024
|
-
const { repoReport, siteReport, writeReport } = await import("./src-
|
|
5025
|
-
const out =
|
|
5740
|
+
const { repoReport, siteReport, writeReport } = await import("./src-ZG7JU6YJ.js");
|
|
5741
|
+
const out = resolve15(values.out ?? ".youi/detect");
|
|
5026
5742
|
const report = target.kind === "repo" ? await repoReport(target.path) : await siteReport(target.urls, { out, expose: values.expose, ...process.env.YOUI_CHROMIUM ? { executablePath: process.env.YOUI_CHROMIUM } : {} });
|
|
5027
5743
|
const file = await writeReport(report, out);
|
|
5028
|
-
if (values.json)
|
|
5744
|
+
if (values.json) json3(report);
|
|
5029
5745
|
else {
|
|
5030
|
-
const shown2 =
|
|
5746
|
+
const shown2 = relative11(process.cwd(), file);
|
|
5031
5747
|
print(renderDetect(report, shown2 && !shown2.startsWith("..") ? shown2 : file));
|
|
5032
5748
|
}
|
|
5033
5749
|
return 0;
|
|
5034
5750
|
}
|
|
5035
5751
|
async function cmdRender(values, positionals, io) {
|
|
5036
|
-
const root =
|
|
5037
|
-
const targets = positionals.map((t) => existsSync5(
|
|
5752
|
+
const root = resolve15(values.root ?? values.project ?? process.cwd());
|
|
5753
|
+
const targets = positionals.map((t) => existsSync5(resolve15(t)) ? resolve15(t) : t);
|
|
5038
5754
|
try {
|
|
5039
5755
|
const report = await renderShots(root, targets, { ...values.base ? { base: values.base } : {}, ...process.env.YOUI_CHROMIUM ? { executablePath: process.env.YOUI_CHROMIUM } : {}, ...io.render });
|
|
5040
|
-
if (values.json)
|
|
5756
|
+
if (values.json) json3({ ...report, next: shotsNext(report, (t) => `run youiyoux render${t ? ` ${t}` : ""} again`) });
|
|
5041
5757
|
else print(renderShots2(report));
|
|
5042
5758
|
return targets.length > 0 && report.components.every((c) => c.shots.length === 0) ? 1 : 0;
|
|
5043
5759
|
} catch (error) {
|
|
@@ -5048,20 +5764,20 @@ async function cmdRender(values, positionals, io) {
|
|
|
5048
5764
|
}
|
|
5049
5765
|
}
|
|
5050
5766
|
async function product(values) {
|
|
5051
|
-
const project2 = await loadProject(
|
|
5052
|
-
return { project: project2, root: project2?.root ??
|
|
5767
|
+
const project2 = await loadProject(resolve15(values.project ?? process.cwd()), loadOptions(values)).catch(() => null);
|
|
5768
|
+
return { project: project2, root: project2?.root ?? resolve15(values.root ?? values.project ?? process.cwd()) };
|
|
5053
5769
|
}
|
|
5054
5770
|
async function cmdRequests(values, positionals) {
|
|
5055
5771
|
const usage = "youiyoux requests [--status open|addressed|closed|all] [--route r] or youiyoux requests show <id> | reply <id> --message <text> | addressed <id> --message <what you changed>";
|
|
5056
5772
|
const [action, id, ...rest] = positionals;
|
|
5057
5773
|
if (rest.length > 0) throw new UsageError(usage);
|
|
5058
|
-
const { RequestStore, requestBrief } = await import("./src-
|
|
5774
|
+
const { RequestStore, requestBrief } = await import("./src-EUVBYTKY.js");
|
|
5059
5775
|
const { project: project2, root } = await product(values);
|
|
5060
|
-
const store = new RequestStore(root, project2?.config.id ??
|
|
5776
|
+
const store = new RequestStore(root, project2?.config.id ?? basename5(root));
|
|
5061
5777
|
const agent = { kind: "agent", name: "youiyoux CLI" };
|
|
5062
5778
|
const one = (request) => {
|
|
5063
5779
|
const brief = requestBrief(request, root);
|
|
5064
|
-
if (values.json)
|
|
5780
|
+
if (values.json) json3(brief);
|
|
5065
5781
|
else print(renderBrief(brief, true));
|
|
5066
5782
|
return 0;
|
|
5067
5783
|
};
|
|
@@ -5071,7 +5787,7 @@ async function cmdRequests(values, positionals) {
|
|
|
5071
5787
|
if (!["open", "addressed", "closed", "all"].includes(status)) throw new UsageError("--status takes open, addressed, closed or all");
|
|
5072
5788
|
const { requests, unreadable } = await store.list();
|
|
5073
5789
|
const briefs = requests.filter((r) => (status === "all" || r.status === status) && (!values.route || r.target.route === values.route)).map((r) => requestBrief(r, root));
|
|
5074
|
-
if (values.json)
|
|
5790
|
+
if (values.json) json3({ requests: briefs, unreadable });
|
|
5075
5791
|
else print(renderRequests(briefs, status, unreadable));
|
|
5076
5792
|
return unreadable.length ? 1 : 0;
|
|
5077
5793
|
}
|
|
@@ -5087,22 +5803,22 @@ async function cmdScreens(values, positionals, io) {
|
|
|
5087
5803
|
const usage = "youiyoux screens capture --base <url of the app in development> [--route <one route>] [--project dir] [--out <screens dir>] [--storage-state <signed-in session>] [--json] or youiyoux screens login --base <url> [--project dir] [--storage-state <file>]";
|
|
5088
5804
|
if (!["capture", "login"].includes(positionals[0] ?? "") || positionals.length > 1 || !values.base) throw new UsageError(usage);
|
|
5089
5805
|
const { root } = await product(values);
|
|
5090
|
-
if (positionals[0] === "login") return screensLogin(values.base, values["storage-state"] ?
|
|
5091
|
-
const dir =
|
|
5092
|
-
if (!existsSync5(
|
|
5806
|
+
if (positionals[0] === "login") return screensLogin(values.base, values["storage-state"] ? resolve15(values["storage-state"]) : sessionFile(root), io);
|
|
5807
|
+
const dir = resolve15(values.out ?? join25(root, ".youi/screens"));
|
|
5808
|
+
if (!existsSync5(join25(dir, "screens.json"))) {
|
|
5093
5809
|
process.stderr.write(`${style.error("\u2716")} no screens.json in ${dir}: list the routes to capture there, as {"schema":"youiyoux.screens/v1","project":"<name>","source":"<what the app is>","capturedAt":"","viewport":{"width":1440,"height":900},"screens":[{"route":"/","path":"/"}]}
|
|
5094
5810
|
`);
|
|
5095
5811
|
return 1;
|
|
5096
5812
|
}
|
|
5097
|
-
const session = chosenSession(root, values["storage-state"] ?
|
|
5813
|
+
const session = chosenSession(root, values["storage-state"] ? resolve15(values["storage-state"]) : void 0);
|
|
5098
5814
|
if (session?.saved) process.stderr.write(`${style.dim(`signed in with the session youiyoux screens login saved (${session.file}); delete it to capture signed out`)}
|
|
5099
5815
|
`);
|
|
5100
|
-
const { launchBrowser } = await import("./src-
|
|
5816
|
+
const { launchBrowser } = await import("./src-ZG7JU6YJ.js");
|
|
5101
5817
|
const browser = await launchBrowser(process.env.YOUI_CHROMIUM, { purpose: { what: "youiyoux screens capture", command: "youiyoux screens capture" } });
|
|
5102
5818
|
try {
|
|
5103
5819
|
const report = await captureScreens(dir, { base: values.base, root, browser, ...values.route ? { routes: [values.route] } : {}, ...session ? { storageState: session.file } : {}, log: (line) => process.stderr.write(`${style.dim(line)}
|
|
5104
5820
|
`) });
|
|
5105
|
-
if (values.json)
|
|
5821
|
+
if (values.json) json3(report);
|
|
5106
5822
|
else print(captureText(report));
|
|
5107
5823
|
return report.screens.every((s) => s.error) ? 1 : 0;
|
|
5108
5824
|
} finally {
|
|
@@ -5154,7 +5870,7 @@ async function cmdReport(values, positionals, io) {
|
|
|
5154
5870
|
return result.code;
|
|
5155
5871
|
}
|
|
5156
5872
|
async function cmdMcp(values) {
|
|
5157
|
-
const { startStdioServer } = await import("./src-
|
|
5873
|
+
const { startStdioServer } = await import("./src-YKPJ3PII.js");
|
|
5158
5874
|
await startStdioServer({ projectRoot: values.project ?? process.cwd(), ...loadOptions(values), ...values.cache ? { discoverCache: values.cache } : {} });
|
|
5159
5875
|
return -1;
|
|
5160
5876
|
}
|
|
@@ -5165,13 +5881,13 @@ function positiveInt(value, flag) {
|
|
|
5165
5881
|
return n;
|
|
5166
5882
|
}
|
|
5167
5883
|
async function cmdDiscover(values, positionals, io) {
|
|
5168
|
-
const cacheDir =
|
|
5884
|
+
const cacheDir = resolve15(discoverCacheDir(values.cache));
|
|
5169
5885
|
const { path, home } = indexOf(values);
|
|
5170
5886
|
const catalog = () => readCatalog(cacheDir, path);
|
|
5171
5887
|
const [action, ...rest] = positionals;
|
|
5172
5888
|
if (action === "sources") {
|
|
5173
5889
|
const names = await selectSources(cacheDir, rest[0], rest[1]);
|
|
5174
|
-
if (values.json)
|
|
5890
|
+
if (values.json) json3(names);
|
|
5175
5891
|
else print(names.join("\n"));
|
|
5176
5892
|
return 0;
|
|
5177
5893
|
}
|
|
@@ -5181,7 +5897,7 @@ async function cmdDiscover(values, positionals, io) {
|
|
|
5181
5897
|
const summary = await syncIndex({ ...io.sleep ? { sleep: io.sleep } : {}, cacheDir, icons: values.icons ?? false, ...io.fetcher ? { fetcher: io.fetcher } : {}, ...io.timeoutMs ? { timeoutMs: io.timeoutMs } : {}, ...token ? { token } : {}, ...maxRegistries !== void 0 ? { maxRegistries } : {} });
|
|
5182
5898
|
const loaded = await ingestCatalog(await sharedStore(path), cacheDir);
|
|
5183
5899
|
await noteSource(home, { discover: cacheDir });
|
|
5184
|
-
if (values.json)
|
|
5900
|
+
if (values.json) json3(summary);
|
|
5185
5901
|
else print(`${renderDiscoverSync(summary)}
|
|
5186
5902
|
${style.dim(`store \u2192 ${storeLocation({ path })} (${plural(loaded.chosen, "listing")})`)}`);
|
|
5187
5903
|
return 0;
|
|
@@ -5194,11 +5910,11 @@ ${style.dim(`store \u2192 ${storeLocation({ path })} (${plural(loaded.chosen, "l
|
|
|
5194
5910
|
if (action === "show") {
|
|
5195
5911
|
if (!rest[0]) throw new UsageError("youiyoux discover show <id|ref>");
|
|
5196
5912
|
if (values.source) {
|
|
5197
|
-
|
|
5913
|
+
json3(await candidateSource(cacheDir, rest.join(" "), io));
|
|
5198
5914
|
return 0;
|
|
5199
5915
|
}
|
|
5200
5916
|
const shown2 = await showFromCatalog(await catalog(), rest.join(" "));
|
|
5201
|
-
if (values.json)
|
|
5917
|
+
if (values.json) json3(shown2);
|
|
5202
5918
|
else print(renderDiscoverShow(shown2));
|
|
5203
5919
|
return 0;
|
|
5204
5920
|
}
|
|
@@ -5211,11 +5927,14 @@ ${style.dim(`store \u2192 ${storeLocation({ path })} (${plural(loaded.chosen, "l
|
|
|
5211
5927
|
await noteSource(home, { discover: cacheDir });
|
|
5212
5928
|
}
|
|
5213
5929
|
const result = await searchCatalog(await catalog(), need, { ...values.role ? { role: values.role } : {}, ...licenses?.length ? { constraints: { licenses } } : {}, limit: positiveInt(values.limit, "--limit") ?? 10, offset: positiveInt(values.offset, "--offset") ?? 0, all: values.all, facets: true });
|
|
5214
|
-
if (values.json)
|
|
5930
|
+
if (values.json) json3(result);
|
|
5215
5931
|
else print(renderDiscoverSearch(result));
|
|
5216
5932
|
return 0;
|
|
5217
5933
|
}
|
|
5218
5934
|
var COMMANDS = {
|
|
5935
|
+
adopt: cmdAdopt,
|
|
5936
|
+
status: cmdStatus,
|
|
5937
|
+
system: cmdSystem,
|
|
5219
5938
|
link: cmdLink,
|
|
5220
5939
|
unlink: cmdUnlink,
|
|
5221
5940
|
show: cmdShow,
|
|
@@ -5277,7 +5996,7 @@ ${HELP}
|
|
|
5277
5996
|
}
|
|
5278
5997
|
return guarded(() => handler(located(command2, parsed.values), rest, io));
|
|
5279
5998
|
}
|
|
5280
|
-
var IN_A_PRODUCT = /* @__PURE__ */ new Set(["link", "unlink", "show", "where", "bom", "check", "baseline", "rules", "plan", "migrate", "sync", "scan", "watch", "hooks", "agents", "mcp", "ui", "library", "render", "screens", "requests"]);
|
|
5999
|
+
var IN_A_PRODUCT = /* @__PURE__ */ new Set(["system", "link", "unlink", "show", "where", "bom", "check", "baseline", "rules", "plan", "migrate", "sync", "scan", "watch", "hooks", "agents", "mcp", "ui", "library", "render", "screens", "requests"]);
|
|
5281
6000
|
var IN_A_DESIGN_REPO = /* @__PURE__ */ new Set(["build", "parity"]);
|
|
5282
6001
|
function located(command2, values, cwd = process.cwd()) {
|
|
5283
6002
|
if (IN_A_PRODUCT.has(command2) && values.project === void 0 && values.config === void 0 && values.root === void 0) {
|