youiyoux 0.1.10 → 0.1.13
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-HDJ2TOWZ.js → chunk-5I2VHSEN.js} +1 -1
- package/dist/chunks/{chunk-T3KNPVQG.js → chunk-DX2NKY2M.js} +4 -4
- package/dist/chunks/{chunk-75DBKEAH.js → chunk-EV62TUDV.js} +1119 -619
- package/dist/chunks/{chunk-3RP664IP.js → chunk-N5Z5A637.js} +2 -2
- package/dist/chunks/{chunk-DWYDHVLZ.js → chunk-NF4OJYKC.js} +106 -66
- package/dist/chunks/{chunk-X2YYMHR4.js → chunk-PCLXMWZ2.js} +3 -3
- package/dist/chunks/{chunk-OCZJ3JGH.js → chunk-PWGZRP3C.js} +3 -3
- package/dist/chunks/{chunk-FLBRX6Y3.js → chunk-XOXF2ILO.js} +1 -1
- package/dist/chunks/{chunk-6GITMASZ.js → chunk-ZOQW6LNH.js} +56 -900
- package/dist/chunks/chunk-ZWVWENSB.js +899 -0
- package/dist/chunks/{src-XTNHB65M.js → src-4QOHR5GC.js} +3 -3
- package/dist/chunks/src-624TVB6T.js +55 -0
- package/dist/chunks/{src-DPFJ4S2N.js → src-E6ZPFKPR.js} +3 -2
- package/dist/chunks/{src-75ODGHE3.js → src-HVYMF4S2.js} +9 -7
- package/dist/chunks/{src-BYNWPNRT.js → src-IKDQ2BZO.js} +7 -1
- package/dist/chunks/{src-ZG7JU6YJ.js → src-PBM76LFQ.js} +2 -2
- package/dist/chunks/{src-6SNQVIZS.js → src-R7JGKOYI.js} +4 -4
- package/dist/chunks/{src-PURAHRCR.js → src-YC64NNQ3.js} +2 -2
- package/dist/hub/assets/{CandidatePanel-ZzbsyZo3.js → CandidatePanel-DfntneYT.js} +1 -1
- package/dist/hub/assets/{CatalogScreen-DDYlvOHy.js → CatalogScreen-kpFYZqX3.js} +1 -1
- package/dist/hub/assets/{ChangesScreen-C8_EyHgJ.js → ChangesScreen-Chu-Ui1B.js} +1 -1
- package/dist/hub/assets/{CheatSheet-DfcPiXdS.js → CheatSheet-B1dXTYTW.js} +1 -1
- package/dist/hub/assets/{CommandPalette-DfFkDTVg.js → CommandPalette-LBZHJ659.js} +1 -1
- package/dist/hub/assets/{DiscoverScreen-D6NTgEgk.js → DiscoverScreen-Yd3gVUG4.js} +2 -2
- package/dist/hub/assets/DraftEditor-QH9Sa_Uc.js +5 -0
- package/dist/hub/assets/{GapsScreen-CqedCj4z.js → GapsScreen-DPYGlNnA.js} +1 -1
- package/dist/hub/assets/{HomeScreen-CIj1QTkw.js → HomeScreen-BCPPk7mz.js} +1 -1
- package/dist/hub/assets/{InventoryScreen-CyvFXa2_.js → InventoryScreen-SleB733C.js} +1 -1
- package/dist/hub/assets/{LibraryScreen-C4WU0f42.js → LibraryScreen-BVXhOVPU.js} +1 -1
- package/dist/hub/assets/{Opened-CeRWkA7c.js → Opened-CJyMbxqV.js} +1 -1
- package/dist/hub/assets/OrgApp-Dxs_7CxZ.js +1 -0
- package/dist/hub/assets/{Preview-CgW-FMB7.js → Preview-DWFP0BIe.js} +1 -1
- package/dist/hub/assets/{RuleDetail-BDhZ1gCd.js → RuleDetail-C2fTlLJL.js} +3 -3
- package/dist/hub/assets/{RulesScreen-D3rSo_z2.js → RulesScreen-BABeAGJg.js} +3 -3
- package/dist/hub/assets/{ScreenImage-DLeA3nRh.js → ScreenImage-CyhKm_nj.js} +1 -1
- package/dist/hub/assets/ScreensScreen-8Ih8byGy.js +1 -0
- package/dist/hub/assets/{SearchField-DtEgq9-Q.js → SearchField-hF-6-YDM.js} +1 -1
- package/dist/hub/assets/{ServiceDown-Bn6AcA3J.js → ServiceDown-S4ya0Cob.js} +1 -1
- package/dist/hub/assets/{SpecimenCapture-p6YErF5y.js → SpecimenCapture-e_pgT4Sp.js} +1 -1
- package/dist/hub/assets/{Specimens-DuaE0WeO.js → Specimens-D_9hjlJH.js} +2 -2
- package/dist/hub/assets/{StackScreen-DHDulDng.js → StackScreen-J2FQilLW.js} +1 -1
- package/dist/hub/assets/{StubScreens-CmhVNX_Z.js → StubScreens-DAkMph3i.js} +1 -1
- package/dist/hub/assets/StyleScreen-C5WytSZU.js +1 -0
- package/dist/hub/assets/{Tabs-Cbh9BCKW.js → Tabs-DU-fFRDV.js} +1 -1
- package/dist/hub/assets/{TokenDetail-BICobLK1.js → TokenDetail-CDY1xxb9.js} +1 -1
- package/dist/hub/assets/{TokensScreen-CyTHn55q.js → TokensScreen-Sx4aICWk.js} +1 -1
- package/dist/hub/assets/{WorkspaceScreen-uPWqfklh.js → WorkspaceScreen-CzZ16fPZ.js} +1 -1
- package/dist/hub/assets/{client-BXHiuV_w.js → client-Bdgr12LP.js} +1 -1
- package/dist/hub/assets/{discover-CFhyKler.js → discover-va2KFuK_.js} +1 -1
- package/dist/hub/assets/{dist-DYNYuTjI.js → dist-C8tf4g8c.js} +1 -1
- package/dist/hub/assets/{dist-VmEpVQjP.js → dist-CC_p8DIj.js} +1 -1
- package/dist/hub/assets/{dist-C6iTWL0b.js → dist-YTWqg5sS.js} +1 -1
- package/dist/hub/assets/{family-tiles-B8_onXUi.js → family-tiles-qaK1-_c8.js} +1 -1
- package/dist/hub/assets/main-cvOFSjox.js +2 -0
- package/dist/hub/assets/main-pNY4iNRo.css +2 -0
- package/dist/hub/assets/{try-live-CX04d2L2.js → try-live-Iys3nCBG.js} +1 -1
- package/dist/hub/assets/when-COrv5lkZ.js +1 -0
- package/dist/hub/assets/{workflow-DbBdiOmA.js → workflow-C53UBmRZ.js} +1 -1
- package/dist/hub/index.html +2 -2
- package/dist/hub-previews.js +6 -5
- package/dist/hub-server.js +3 -3
- package/dist/hub-snapshot.js +1 -1
- package/dist/schema/youi.org.schema.json +438 -0
- package/dist/schema/youi.record.schema.json +338 -0
- package/dist/youi.js +7 -6
- package/dist/youiyoux.js +7 -6
- package/package.json +4 -4
- package/dist/hub/assets/DraftEditor-CUNGYbg0.js +0 -5
- package/dist/hub/assets/ScreensScreen-bGuFIyWj.js +0 -1
- package/dist/hub/assets/StyleScreen-LwTqfGID.js +0 -1
- package/dist/hub/assets/main-7snyIdoi.css +0 -2
- package/dist/hub/assets/main-CfbpuT4Y.js +0 -2
|
@@ -53,7 +53,7 @@ import {
|
|
|
53
53
|
vendoredPath,
|
|
54
54
|
whereUsed,
|
|
55
55
|
withFile
|
|
56
|
-
} from "./chunk-
|
|
56
|
+
} from "./chunk-PCLXMWZ2.js";
|
|
57
57
|
import {
|
|
58
58
|
escapingLinks,
|
|
59
59
|
within
|
|
@@ -67,31 +67,33 @@ import {
|
|
|
67
67
|
libraryOfPackage,
|
|
68
68
|
packageOf,
|
|
69
69
|
playwrightCli
|
|
70
|
-
} from "./chunk-
|
|
70
|
+
} from "./chunk-5I2VHSEN.js";
|
|
71
71
|
import {
|
|
72
72
|
MissingIndexError,
|
|
73
73
|
buildGaps,
|
|
74
74
|
candidateSource,
|
|
75
|
-
checkpointSqlite,
|
|
76
75
|
cleanData,
|
|
77
76
|
declaredPackages,
|
|
78
77
|
discoverCacheDir,
|
|
79
78
|
discoverIndexPath,
|
|
80
79
|
expandAll,
|
|
81
|
-
indexHome,
|
|
82
80
|
ingestCatalog,
|
|
83
|
-
inventoryRun,
|
|
84
81
|
linkCandidates,
|
|
85
|
-
openSqlite,
|
|
86
|
-
projectGraph,
|
|
87
82
|
readCatalog,
|
|
88
83
|
searchCatalog,
|
|
89
84
|
selectSources,
|
|
90
85
|
sharedStore,
|
|
91
86
|
showFromCatalog,
|
|
92
|
-
storeLocation,
|
|
93
87
|
syncIndex
|
|
94
|
-
} from "./chunk-
|
|
88
|
+
} from "./chunk-ZOQW6LNH.js";
|
|
89
|
+
import {
|
|
90
|
+
checkpointSqlite,
|
|
91
|
+
indexHome,
|
|
92
|
+
inventoryRun,
|
|
93
|
+
openSqlite,
|
|
94
|
+
projectGraph,
|
|
95
|
+
storeLocation
|
|
96
|
+
} from "./chunk-ZWVWENSB.js";
|
|
95
97
|
import {
|
|
96
98
|
DL_EXT,
|
|
97
99
|
buildTokens,
|
|
@@ -101,9 +103,10 @@ import {
|
|
|
101
103
|
parseCssColor,
|
|
102
104
|
parseDimension,
|
|
103
105
|
readRules
|
|
104
|
-
} from "./chunk-
|
|
106
|
+
} from "./chunk-XOXF2ILO.js";
|
|
105
107
|
import {
|
|
106
108
|
Baseline,
|
|
109
|
+
CiRecord,
|
|
107
110
|
DESIGN_CONFIG_FILE,
|
|
108
111
|
DefinitionError,
|
|
109
112
|
INVENTORY_ROLES,
|
|
@@ -114,6 +117,7 @@ import {
|
|
|
114
117
|
OutputConfig,
|
|
115
118
|
PACKAGE_NAME,
|
|
116
119
|
ProjectConfig,
|
|
120
|
+
RepoName,
|
|
117
121
|
ScanReport,
|
|
118
122
|
WORKSPACE_FILE,
|
|
119
123
|
WorkspaceConfig,
|
|
@@ -139,18 +143,19 @@ import {
|
|
|
139
143
|
toPosix,
|
|
140
144
|
unifiedDiff,
|
|
141
145
|
writeFileAtomic
|
|
142
|
-
} from "./chunk-
|
|
146
|
+
} from "./chunk-NF4OJYKC.js";
|
|
143
147
|
import {
|
|
144
148
|
__toESM
|
|
145
149
|
} from "./chunk-WNTS7ZO4.js";
|
|
146
150
|
|
|
147
151
|
// ../cli/src/main.ts
|
|
148
152
|
var import_yaml4 = __toESM(require_dist(), 1);
|
|
149
|
-
import { execFile as
|
|
153
|
+
import { execFile as execFile6 } from "node:child_process";
|
|
150
154
|
import { existsSync as existsSync5 } from "node:fs";
|
|
151
|
-
import {
|
|
152
|
-
import {
|
|
153
|
-
import {
|
|
155
|
+
import { homedir as homedir4 } from "node:os";
|
|
156
|
+
import { appendFile as appendFile2, readFile as readFile11, rm as rm7, rmdir as rmdir3 } from "node:fs/promises";
|
|
157
|
+
import { basename as basename5, dirname as dirname13, join as join28, relative as relative11, resolve as resolve15 } from "node:path";
|
|
158
|
+
import { parseArgs, promisify as promisify6 } from "node:util";
|
|
154
159
|
|
|
155
160
|
// ../cli/src/check.ts
|
|
156
161
|
import { dirname as dirname3, join as join7, resolve as resolve2 } from "node:path";
|
|
@@ -167,7 +172,7 @@ import { join as join2, relative, resolve } from "node:path";
|
|
|
167
172
|
|
|
168
173
|
// ../cli/src/command.ts
|
|
169
174
|
var COMMAND = "youiyoux";
|
|
170
|
-
var SUBCOMMANDS = ["show", "where", "bom", "check", "baseline", "rules", "plan", "migrate", "upgrade", "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"];
|
|
175
|
+
var SUBCOMMANDS = ["show", "where", "bom", "check", "baseline", "rules", "plan", "migrate", "upgrade", "record", "hub", "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"];
|
|
171
176
|
|
|
172
177
|
// ../cli/src/agents.ts
|
|
173
178
|
var import_yaml = __toESM(require_dist(), 1);
|
|
@@ -197,8 +202,8 @@ function viaNpx(text, root) {
|
|
|
197
202
|
return text.replace(BARE, `${portableYoui(root).run} `);
|
|
198
203
|
}
|
|
199
204
|
function diffInstruction(root) {
|
|
200
|
-
const { run:
|
|
201
|
-
return `- ${setup} After UI edits, run \`${
|
|
205
|
+
const { run: run8, setup } = portableYoui(root);
|
|
206
|
+
return `- ${setup} After UI edits, run \`${run8} check --diff\` (\`--json\` for data): it lists what your change adds that near-duplicates an existing component, hardcodes a value a token holds, or imports an element from a library other than the project's choice, each with its fix. Apply each fix.`;
|
|
202
207
|
}
|
|
203
208
|
function repoAgentsBlock(root) {
|
|
204
209
|
return viaNpx([BLOCK_BEGIN, REPO_BLOCK_HEADING, "", diffInstruction(root), "- Run `youiyoux init --design <dir>` to connect design-system rules; repository inference works without it.", BLOCK_END].join("\n"), root);
|
|
@@ -1063,7 +1068,7 @@ async function runCheck(project2, options = {}) {
|
|
|
1063
1068
|
const { report } = scan;
|
|
1064
1069
|
const checked = new Set(scan.files);
|
|
1065
1070
|
const unmatched = scope ? [...scope].filter((f) => !checked.has(f)) : [];
|
|
1066
|
-
const
|
|
1071
|
+
const failed3 = report.coverage.failed.filter((f) => !scope || scope.has(f.file) || !/\.(tsx?|css)$/.test(f.file));
|
|
1067
1072
|
const wantedRule = (id) => !options.rule || id.endsWith(`/rule/${options.rule}`);
|
|
1068
1073
|
const violations = report.violations.filter((v) => wantedRule(v.rule) && (!options.quiet || v.severity === "error"));
|
|
1069
1074
|
const notRun = failedChecks(report.checks).filter((c) => wantedRule(c.rule));
|
|
@@ -1074,7 +1079,7 @@ async function runCheck(project2, options = {}) {
|
|
|
1074
1079
|
const failing = baseline ? [...baseline.comparison.new, ...baseline.comparison.regressed] : violations;
|
|
1075
1080
|
const errors = failing.filter((v) => v.severity === "error").length + (lock ?? []).filter((i) => i.severity === "error").length;
|
|
1076
1081
|
const drift = diff ? await analyseChange(project2, scan, diff, { violations }) : null;
|
|
1077
|
-
return { identity: scan.identity, scan, scope, unmatched, failed:
|
|
1082
|
+
return { identity: scan.identity, scan, scope, unmatched, failed: failed3, notRun, violations, lock, baseline, errors, incomplete: unmatched.length > 0 || failed3.length > 0 || notRun.length > 0 || (drift?.incomplete.length ?? 0) > 0, diff: drift };
|
|
1078
1083
|
}
|
|
1079
1084
|
async function lockIssues(project2) {
|
|
1080
1085
|
const lock = await verifyLock(project2);
|
|
@@ -1288,6 +1293,63 @@ async function installWorkflow(project2, toolRoot, target, force, options = {})
|
|
|
1288
1293
|
}
|
|
1289
1294
|
return write(file, text, before, `authoritative: ${published ? "runs the published youiyoux package; pinned to" : `pinned to ${tool.slug}@${tool.commit.slice(0, 7)} and`} rules ${design.slug}@${design.commit.slice(0, 7)}${diff ? "; pull requests also fail on drift they add (youiyoux check --diff --strict)" : ""}`);
|
|
1290
1295
|
}
|
|
1296
|
+
var RECORD_WORKFLOW = ".github/workflows/youiyoux-record.yml";
|
|
1297
|
+
var RECORD_HEADER = "# Generated by `youiyoux hooks install --record`.";
|
|
1298
|
+
async function installStep(top) {
|
|
1299
|
+
const has = async (file) => await readTextOrNull(join8(top, file)) !== null;
|
|
1300
|
+
if (await has("pnpm-lock.yaml")) return " - run: corepack enable && pnpm install --frozen-lockfile --ignore-scripts\n";
|
|
1301
|
+
if (await has("yarn.lock")) return " - run: corepack enable && yarn install --immutable --mode=skip-build\n";
|
|
1302
|
+
if (await has("package-lock.json")) return " - run: npm ci --ignore-scripts\n";
|
|
1303
|
+
return null;
|
|
1304
|
+
}
|
|
1305
|
+
function recordWorkflowText(input) {
|
|
1306
|
+
const tool = `youiyoux@${input.version}`;
|
|
1307
|
+
return `${RECORD_HEADER}
|
|
1308
|
+
# Sends where each product here stands with its System to your team's hosted hub
|
|
1309
|
+
# (youiyoux hub serve): youiyoux record, redacted as the shareable report is, with no code.
|
|
1310
|
+
# Set the repository secret YOUIYOUX_HUB_URL (the hub's https address). The record proves this
|
|
1311
|
+
# repository with GitHub's OIDC token (id-token: write), so a hub that takes GitHub's needs no
|
|
1312
|
+
# other secret; a hub on a shared ingest token also needs YOUIYOUX_HUB_TOKEN.
|
|
1313
|
+
# Pinned to youiyoux ${input.version}; change it here. Only GitHub's own actions run here.
|
|
1314
|
+
name: youiyoux record
|
|
1315
|
+
on:
|
|
1316
|
+
push:
|
|
1317
|
+
branches: [${JSON.stringify(input.branch)}]
|
|
1318
|
+
workflow_dispatch:
|
|
1319
|
+
permissions:
|
|
1320
|
+
contents: read
|
|
1321
|
+
id-token: write
|
|
1322
|
+
concurrency:
|
|
1323
|
+
group: youiyoux-record
|
|
1324
|
+
cancel-in-progress: true
|
|
1325
|
+
jobs:
|
|
1326
|
+
record:
|
|
1327
|
+
runs-on: ubuntu-latest
|
|
1328
|
+
timeout-minutes: 10
|
|
1329
|
+
steps:
|
|
1330
|
+
- uses: actions/checkout@v4
|
|
1331
|
+
- uses: actions/setup-node@v4
|
|
1332
|
+
with:
|
|
1333
|
+
node-version: 24
|
|
1334
|
+
${input.install ?? ""} - name: youiyoux record
|
|
1335
|
+
env:
|
|
1336
|
+
YOUIYOUX_HUB_URL: \${{ secrets.YOUIYOUX_HUB_URL }}
|
|
1337
|
+
YOUIYOUX_HUB_TOKEN: \${{ secrets.YOUIYOUX_HUB_TOKEN }}
|
|
1338
|
+
run: npx -y ${tool} record --send
|
|
1339
|
+
`;
|
|
1340
|
+
}
|
|
1341
|
+
async function installRecordWorkflow(dir, version, force) {
|
|
1342
|
+
if (!version) throw new Error("cannot tell which youiyoux version this is, so the workflow could not be pinned: run npx youiyoux@<version> hooks install --record");
|
|
1343
|
+
const top = await gitOut(dir, ["rev-parse", "--show-toplevel"]).catch(() => {
|
|
1344
|
+
throw new Error(`${dir} is not in a git repository; the record workflow runs in one`);
|
|
1345
|
+
});
|
|
1346
|
+
const remoteHead = await gitOut(top, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"]).catch(() => "");
|
|
1347
|
+
const branch = remoteHead.replace(/^origin\//, "") || await gitOut(top, ["symbolic-ref", "--short", "HEAD"]).catch(() => "") || "main";
|
|
1348
|
+
const file = join8(top, RECORD_WORKFLOW);
|
|
1349
|
+
const before = await readTextOrNull(file);
|
|
1350
|
+
if (before !== null && !before.startsWith(RECORD_HEADER) && !force) throw new Error(`${file} exists and was not written by youiyoux; pass --force to replace it`);
|
|
1351
|
+
return write(file, recordWorkflowText({ branch, version, install: await installStep(top) }), before, `sends this repository's record (redacted, no code) to the hub in YOUIYOUX_HUB_URL on each push to ${branch}, proving the repository with GitHub's OIDC token; set YOUIYOUX_HUB_URL as a repository secret (and YOUIYOUX_HUB_TOKEN for a hub on a shared token)`);
|
|
1352
|
+
}
|
|
1291
1353
|
|
|
1292
1354
|
// ../cli/src/check-repo.ts
|
|
1293
1355
|
async function loadCheckProject(root, options = {}, inferTop = false) {
|
|
@@ -1659,7 +1721,7 @@ function githubAnnotations(result, prefix) {
|
|
|
1659
1721
|
for (const issue of result.lock ?? []) lines.push(command(issue.severity === "error" ? "error" : "warning", issue.message, { file: `${prefix}${issue.file}`, title: "youi setup" }));
|
|
1660
1722
|
for (const file of result.unmatched) lines.push(command("error", `${file} was not checked: not in this project's include globs`, { title: "youi incomplete" }));
|
|
1661
1723
|
for (const failure of result.failed) lines.push(command("error", `could not be fully analysed (${failure.reason}); a clean result here is not a pass`, { file: `${prefix}${failure.file}`, title: "youi incomplete" }));
|
|
1662
|
-
for (const
|
|
1724
|
+
for (const run8 of result.notRun) lines.push(command("error", `rule ${ruleSlug(run8.rule)} did not run (${run8.reason ?? run8.state}); its findings are unknown`, { title: "youi incomplete" }));
|
|
1663
1725
|
return lines;
|
|
1664
1726
|
}
|
|
1665
1727
|
var revision = (state) => state.commit ? `\`${state.commit}\`${state.dirty ? " (dirty)" : ""}` : "not a git checkout";
|
|
@@ -1754,8 +1816,8 @@ function findingLines(result, root) {
|
|
|
1754
1816
|
const findings2 = result.diff?.findings ?? [];
|
|
1755
1817
|
const lines = findings2.slice(0, MAX_LISTED).map((f) => `- ${diffFindingLine(f)}`);
|
|
1756
1818
|
if (findings2.length > MAX_LISTED) {
|
|
1757
|
-
const { run:
|
|
1758
|
-
lines.push(`- \u2026and ${findings2.length - MAX_LISTED} more. ${setup} Run \`${
|
|
1819
|
+
const { run: run8, setup } = portableYoui(root);
|
|
1820
|
+
lines.push(`- \u2026and ${findings2.length - MAX_LISTED} more. ${setup} Run \`${run8} check --diff\`.`);
|
|
1759
1821
|
}
|
|
1760
1822
|
for (const gap of diffGaps(result)) lines.push(`- incomplete: ${gap}; 0 findings there is not a pass`);
|
|
1761
1823
|
return lines;
|
|
@@ -2090,7 +2152,7 @@ import { homedir, userInfo } from "node:os";
|
|
|
2090
2152
|
import { join as join12, resolve as resolve6 } from "node:path";
|
|
2091
2153
|
async function swapImpact(candidate, family) {
|
|
2092
2154
|
try {
|
|
2093
|
-
const { discoverIndexPath: discoverIndexPath2, sharedStore: sharedStore2 } = await import("./src-
|
|
2155
|
+
const { discoverIndexPath: discoverIndexPath2, sharedStore: sharedStore2 } = await import("./src-E6ZPFKPR.js");
|
|
2094
2156
|
const path = discoverIndexPath2();
|
|
2095
2157
|
if (!process.env.DATABASE_URL && !existsSync(path)) return null;
|
|
2096
2158
|
const impact = await (await sharedStore2(path, process.env, { readOnly: true })).swapImpact(candidate, family);
|
|
@@ -2101,7 +2163,7 @@ async function swapImpact(candidate, family) {
|
|
|
2101
2163
|
}
|
|
2102
2164
|
async function libraryLicence(id) {
|
|
2103
2165
|
try {
|
|
2104
|
-
const { discoverCacheDir: discoverCacheDir2, findListing } = await import("./src-
|
|
2166
|
+
const { discoverCacheDir: discoverCacheDir2, findListing } = await import("./src-E6ZPFKPR.js");
|
|
2105
2167
|
const { entry } = await findListing(discoverCacheDir2(void 0, process.env), id);
|
|
2106
2168
|
return entry.id === id ? entry.license : null;
|
|
2107
2169
|
} catch {
|
|
@@ -2109,7 +2171,7 @@ async function libraryLicence(id) {
|
|
|
2109
2171
|
}
|
|
2110
2172
|
}
|
|
2111
2173
|
async function startRuleEditor(options) {
|
|
2112
|
-
const { FileStore, RuleWorkflow, serveWorkflow } = await import("./src-
|
|
2174
|
+
const { FileStore, RuleWorkflow, serveWorkflow } = await import("./src-R7JGKOYI.js");
|
|
2113
2175
|
const projectRoot = resolve6(options.project);
|
|
2114
2176
|
const load2 = { ...options.config ? { configPath: resolve6(options.config) } : {}, ...options.design ? { designRoot: resolve6(options.design) } : {}, ...options.root ? { productRoot: resolve6(options.root) } : {} };
|
|
2115
2177
|
const project2 = await loadProject(projectRoot, load2);
|
|
@@ -2786,10 +2848,10 @@ async function upgradeProject(project2, options = {}) {
|
|
|
2786
2848
|
if (late.length > 0) return await fail(`the sync conflicted in ${late.map((a) => a.file).join(", ")}`, {});
|
|
2787
2849
|
const applied = [];
|
|
2788
2850
|
for (const { spec } of planned) {
|
|
2789
|
-
const
|
|
2790
|
-
applied.push(
|
|
2791
|
-
for (const change of
|
|
2792
|
-
if (
|
|
2851
|
+
const run8 = await runMigration(project2, spec, { write: true });
|
|
2852
|
+
applied.push(run8);
|
|
2853
|
+
for (const change of run8.changes) snapshot.remember(change.file, change.before);
|
|
2854
|
+
if (run8.status === "rejected") return await fail(`${spec.id} did not hold on the written files: ${gateFailures(run8)}`, { migrations: applied });
|
|
2793
2855
|
}
|
|
2794
2856
|
const residuals = applied.reduce((n, m) => n + m.residuals.length, 0);
|
|
2795
2857
|
return result("upgraded", { sync: synced.actions.filter((a) => WRITES.has(a.action) || a.action === "kept-local"), migrations: applied }, residuals > 0 ? `fix the ${residuals === 1 ? "site" : `${residuals} sites`} listed for a person, then youiyoux check` : "youiyoux check");
|
|
@@ -2825,225 +2887,841 @@ function renderUpgrade(r, showDiff) {
|
|
|
2825
2887
|
return lines.join("\n");
|
|
2826
2888
|
}
|
|
2827
2889
|
|
|
2828
|
-
// ../cli/src/
|
|
2829
|
-
import {
|
|
2830
|
-
import {
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
if (
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
const {
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
const
|
|
2864
|
-
const
|
|
2865
|
-
const
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2890
|
+
// ../cli/src/record.ts
|
|
2891
|
+
import { execFile as execFile5 } from "node:child_process";
|
|
2892
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
2893
|
+
import { join as join20 } from "node:path";
|
|
2894
|
+
import { promisify as promisify5 } from "node:util";
|
|
2895
|
+
|
|
2896
|
+
// ../cli/src/org-hub.ts
|
|
2897
|
+
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
|
|
2898
|
+
import { readFile as readFile6 } from "node:fs/promises";
|
|
2899
|
+
import { createServer } from "node:http";
|
|
2900
|
+
import { extname, join as join17, normalize, sep as sep3 } from "node:path";
|
|
2901
|
+
|
|
2902
|
+
// ../cli/src/github-oidc.ts
|
|
2903
|
+
import { createPublicKey, verify } from "node:crypto";
|
|
2904
|
+
var GITHUB_ISSUER = "https://token.actions.githubusercontent.com";
|
|
2905
|
+
var JWKS_URL = `${GITHUB_ISSUER}/.well-known/jwks`;
|
|
2906
|
+
var LEEWAY_SECONDS = 60;
|
|
2907
|
+
var JWKS_TTL_MS = 60 * 60 * 1e3;
|
|
2908
|
+
var JWKS_RETRY_MS = 60 * 1e3;
|
|
2909
|
+
var audienceOf = (hub) => hub.trim().replace(/\/+$/, "");
|
|
2910
|
+
async function requestGithubToken(audience, env = process.env, fetcher = fetch) {
|
|
2911
|
+
const url = env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
2912
|
+
const bearer2 = env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
2913
|
+
if (!url || !bearer2) return null;
|
|
2914
|
+
const request = new URL(url);
|
|
2915
|
+
request.searchParams.set("audience", audience);
|
|
2916
|
+
const response = await fetcher(request, { headers: { authorization: `bearer ${bearer2}` }, signal: AbortSignal.timeout(15e3) });
|
|
2917
|
+
if (!response.ok) throw new Error(`GitHub Actions did not give an OIDC token (${response.status}): does the workflow have permissions: id-token: write?`);
|
|
2918
|
+
const { value } = await response.json();
|
|
2919
|
+
if (typeof value !== "string") throw new Error("GitHub Actions answered without an OIDC token");
|
|
2920
|
+
return value;
|
|
2921
|
+
}
|
|
2922
|
+
var decode = (part) => JSON.parse(Buffer.from(part, "base64url").toString("utf8"));
|
|
2923
|
+
var DEFAULT_BRANCHES = ["main", "master"];
|
|
2924
|
+
function githubVerifier(options) {
|
|
2925
|
+
const listed = options.owners.map((o) => o.trim().toLowerCase()).filter(Boolean);
|
|
2926
|
+
const ownerIds = new Set(listed.filter((o) => /^\d+$/.test(o)));
|
|
2927
|
+
const ownerNames = new Set(listed.filter((o) => !/^\d+$/.test(o)));
|
|
2928
|
+
const branches = new Set(options.branches && options.branches.length > 0 ? options.branches : DEFAULT_BRANCHES);
|
|
2929
|
+
const issuer = options.issuer ?? GITHUB_ISSUER;
|
|
2930
|
+
const audience = audienceOf(options.audience);
|
|
2931
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
2932
|
+
let cached = null;
|
|
2933
|
+
let lastFetch = 0;
|
|
2934
|
+
const fetchKeys = options.jwks ?? (async () => await (await fetch(JWKS_URL, { signal: AbortSignal.timeout(15e3) })).json());
|
|
2935
|
+
const keyFor = async (kid) => {
|
|
2936
|
+
const find = (jwks) => jwks.keys.find((k) => k.kid === kid && k.kty === "RSA") ?? null;
|
|
2937
|
+
const fresh = cached && Date.now() - cached.at < JWKS_TTL_MS ? find(cached.keys) : null;
|
|
2938
|
+
if (fresh) return fresh;
|
|
2939
|
+
if (cached && Date.now() - lastFetch < JWKS_RETRY_MS) return find(cached.keys);
|
|
2940
|
+
lastFetch = Date.now();
|
|
2941
|
+
cached = { keys: await fetchKeys(), at: Date.now() };
|
|
2942
|
+
return find(cached.keys);
|
|
2880
2943
|
};
|
|
2881
|
-
return
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
const at = findUp(cwd, WORKSPACE_FILE);
|
|
2888
|
-
if (at) {
|
|
2889
|
-
const file = join17(at, WORKSPACE_FILE);
|
|
2890
|
-
const workspace = await readDefinition(file, WorkspaceConfig);
|
|
2891
|
-
return { roots: workspace.projects.map((p) => resolve10(at, p)), workspace: file };
|
|
2892
|
-
}
|
|
2893
|
-
const top = findUp(cwd, ".git") ?? null;
|
|
2894
|
-
if (top === null) return { roots: [], workspace: null };
|
|
2895
|
-
const configs = await findFiles(cwd, [`**/${PROJECT_FILE}`]);
|
|
2896
|
-
const roots = [];
|
|
2897
|
-
for (const config of configs) {
|
|
2898
|
-
const dir = resolve10(cwd, dirname9(config));
|
|
2899
|
-
if (await realpath5(findUp(dir, ".git") ?? dir) === await realpath5(top)) roots.push(dir);
|
|
2900
|
-
}
|
|
2901
|
-
return { roots, workspace: null };
|
|
2902
|
-
}
|
|
2903
|
-
async function workspaceStatus(cwd, named2) {
|
|
2904
|
-
const { roots, workspace } = await statusTargets(cwd, named2);
|
|
2905
|
-
const projects = [];
|
|
2906
|
-
for (const root of roots) {
|
|
2944
|
+
return async (token) => {
|
|
2945
|
+
const parts = token.split(".");
|
|
2946
|
+
if (parts.length !== 3) throw new Error("not a JSON Web Token");
|
|
2947
|
+
const [head, body, signature] = parts;
|
|
2948
|
+
let header;
|
|
2949
|
+
let claims;
|
|
2907
2950
|
try {
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2951
|
+
header = decode(head);
|
|
2952
|
+
claims = decode(body);
|
|
2953
|
+
} catch {
|
|
2954
|
+
throw new Error("not a JSON Web Token");
|
|
2912
2955
|
}
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
row("sync", sync),
|
|
2930
|
-
row("drift", drift),
|
|
2931
|
-
row("check", `${counts}${baseline}${check.incomplete ? style.warn(" \xB7 incomplete: not a pass") : ""}`),
|
|
2932
|
-
row("components", `${s.components.used} of ${s.components.system} System components used, at ${plural(s.components.sites, "site")}`),
|
|
2933
|
-
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 ")),
|
|
2934
|
-
`${style.bold("Next:")} ${s.next}`
|
|
2935
|
-
];
|
|
2956
|
+
if (header.alg !== "RS256") throw new Error(`signed with ${String(header.alg)}, not GitHub's RS256`);
|
|
2957
|
+
const jwk = await keyFor(typeof header.kid === "string" ? header.kid : void 0);
|
|
2958
|
+
if (!jwk) throw new Error("not signed by a key GitHub publishes");
|
|
2959
|
+
if (!verify("RSA-SHA256", Buffer.from(`${head}.${body}`), createPublicKey({ key: { kty: "RSA", n: String(jwk.n), e: String(jwk.e) }, format: "jwk" }), Buffer.from(signature, "base64url"))) throw new Error("its signature is not GitHub's");
|
|
2960
|
+
if (claims.iss !== issuer) throw new Error(`issued by ${String(claims.iss)}, not GitHub Actions`);
|
|
2961
|
+
const audiences = Array.isArray(claims.aud) ? claims.aud : [claims.aud];
|
|
2962
|
+
if (!audiences.some((a) => typeof a === "string" && audienceOf(a) === audience)) throw new Error(`issued for ${audiences.map(String).join(", ")}, not this hub (${audience})`);
|
|
2963
|
+
const seconds = now().getTime() / 1e3;
|
|
2964
|
+
if (typeof claims.exp !== "number" || claims.exp < seconds - LEEWAY_SECONDS) throw new Error("expired");
|
|
2965
|
+
if (typeof claims.nbf === "number" && claims.nbf > seconds + LEEWAY_SECONDS) throw new Error("not valid yet");
|
|
2966
|
+
const { repository, repository_owner: owner, repository_owner_id: ownerId, sha, ref, event_name: event } = claims;
|
|
2967
|
+
if (typeof repository !== "string" || typeof owner !== "string" || typeof sha !== "string" || typeof ref !== "string") throw new Error("missing its repository, owner, commit or ref");
|
|
2968
|
+
if (!(typeof ownerId === "string" && ownerIds.has(ownerId)) && !ownerNames.has(owner.toLowerCase())) throw new Error(`from ${owner}${typeof ownerId === "string" ? ` (id ${ownerId})` : ""}, which this hub does not take records from`);
|
|
2969
|
+
if (!ref.startsWith("refs/heads/") || !branches.has(ref.slice("refs/heads/".length))) throw new Error(`from ${ref}: this hub takes records from ${[...branches].join(" and ")} only (YOUIYOUX_HUB_BRANCHES)`);
|
|
2970
|
+
return { repository, repository_owner: owner, sha, ref, ...typeof event === "string" ? { event_name: event } : {} };
|
|
2971
|
+
};
|
|
2936
2972
|
}
|
|
2937
|
-
function
|
|
2938
|
-
if (
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2973
|
+
function recordMismatch(claims, record) {
|
|
2974
|
+
if (claims.repository.toLowerCase() !== record.repo.toLowerCase()) return `the record names ${record.repo}, but it was sent from ${claims.repository}`;
|
|
2975
|
+
if (record.commit !== claims.sha) return `the record names commit ${record.commit ?? "none"}, but it was sent from ${claims.sha}`;
|
|
2976
|
+
if (!claims.ref.startsWith("refs/heads/")) return `it was sent from ${claims.ref}, not a branch`;
|
|
2977
|
+
if (claims.event_name !== void 0 && claims.event_name.startsWith("pull_request")) return `it was sent from a ${claims.event_name} run, not a push to the branch`;
|
|
2978
|
+
const branch = claims.ref.slice("refs/heads/".length);
|
|
2979
|
+
if (record.ref !== branch) return `the record names ${record.ref ?? "no branch"}, but it was sent from ${branch}`;
|
|
2980
|
+
return null;
|
|
2943
2981
|
}
|
|
2944
|
-
var statusExitCode = (report) => report.projects.some((p) => failed(p) || p.check.incomplete || p.check.errors > 0 || p.sync.state === "conflict") ? 1 : 0;
|
|
2945
2982
|
|
|
2946
|
-
// ../cli/src/
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
var
|
|
2950
|
-
|
|
2951
|
-
|
|
2983
|
+
// ../cli/src/org-hub.ts
|
|
2984
|
+
var VIEW_KEY_ENV = "YOUIYOUX_HUB_VIEW_KEY";
|
|
2985
|
+
var GITHUB_OWNERS_ENV = "YOUIYOUX_HUB_GITHUB_OWNERS";
|
|
2986
|
+
var AUDIENCE_ENV = "YOUIYOUX_HUB_AUDIENCE";
|
|
2987
|
+
var GITHUB_TOKEN_HEADER = "x-youi-github-oidc";
|
|
2988
|
+
var FUTURE_MS = 5 * 60 * 1e3;
|
|
2989
|
+
var ORG_META = "youi-org";
|
|
2990
|
+
var COOKIE = "youi_hub_session";
|
|
2991
|
+
var MAX_RECORD = 2 * 1024 * 1024;
|
|
2992
|
+
var MIN_SECRET = 24;
|
|
2993
|
+
var SESSION_DAYS = 30;
|
|
2994
|
+
function secretsProblem(ingest, view, github = false) {
|
|
2995
|
+
const missing = [!github && !ingest && "YOUIYOUX_HUB_TOKEN", !view && VIEW_KEY_ENV].filter(Boolean);
|
|
2996
|
+
if (missing.length > 0) return `set ${missing.join(" and ")} in the environment: a random string of ${MIN_SECRET} characters or more${missing.length > 1 ? " each" : ""} (openssl rand -base64 32)${github ? "" : `, the token for CI and the view key for people; or ${GITHUB_OWNERS_ENV} and ${AUDIENCE_ENV}, so GitHub Actions proves each record's repository and no CI token is shared`}`;
|
|
2997
|
+
if (ingest !== void 0 && ingest.length < MIN_SECRET || view.length < MIN_SECRET) return `${ingest !== void 0 ? "YOUIYOUX_HUB_TOKEN and " : ""}${VIEW_KEY_ENV} must be ${MIN_SECRET} characters or more (openssl rand -base64 32)`;
|
|
2998
|
+
if (ingest === view) return `YOUIYOUX_HUB_TOKEN and ${VIEW_KEY_ENV} must differ: whoever can read the hub must not be able to write to it`;
|
|
2999
|
+
return null;
|
|
3000
|
+
}
|
|
3001
|
+
var digest = (text) => createHash("sha256").update(text).digest();
|
|
3002
|
+
var same = (given, expected) => timingSafeEqual(digest(given), digest(expected));
|
|
3003
|
+
var bearer = (req) => /^Bearer (.+)$/.exec(req.headers.authorization ?? "")?.[1] ?? null;
|
|
3004
|
+
var cookie = (req, name) => {
|
|
3005
|
+
for (const part of (req.headers.cookie ?? "").split(";")) {
|
|
3006
|
+
const [key2, ...value] = part.trim().split("=");
|
|
3007
|
+
if (key2 === name) return value.join("=");
|
|
3008
|
+
}
|
|
3009
|
+
return null;
|
|
3010
|
+
};
|
|
3011
|
+
var HEADERS = {
|
|
3012
|
+
"X-Content-Type-Options": "nosniff",
|
|
3013
|
+
"Referrer-Policy": "no-referrer",
|
|
3014
|
+
"X-Frame-Options": "DENY",
|
|
3015
|
+
"X-Robots-Tag": "noindex, nofollow",
|
|
3016
|
+
"Content-Security-Policy": csp([])
|
|
3017
|
+
};
|
|
3018
|
+
function csp(inline) {
|
|
3019
|
+
const hashes = inline.map((script) => ` 'sha256-${createHash("sha256").update(script).digest("base64")}'`).join("");
|
|
3020
|
+
return `default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; font-src 'self' data:; script-src 'self'${hashes}; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'`;
|
|
3021
|
+
}
|
|
3022
|
+
function send(res, status, body, extra = {}) {
|
|
3023
|
+
res.writeHead(status, { ...HEADERS, "Content-Type": "application/json", "Cache-Control": "no-store", ...extra });
|
|
2952
3024
|
res.end(JSON.stringify(body));
|
|
2953
3025
|
}
|
|
2954
|
-
async function readBody(req) {
|
|
2955
|
-
|
|
3026
|
+
async function readBody(req, limit) {
|
|
3027
|
+
const chunks = [];
|
|
3028
|
+
let size = 0;
|
|
2956
3029
|
for await (const chunk2 of req) {
|
|
2957
|
-
|
|
2958
|
-
if (
|
|
3030
|
+
size += chunk2.length;
|
|
3031
|
+
if (size > limit) throw Object.assign(new Error(`a record is at most ${limit / 1024 / 1024} MB`), { status: 413 });
|
|
3032
|
+
chunks.push(chunk2);
|
|
2959
3033
|
}
|
|
2960
|
-
return
|
|
3034
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
2961
3035
|
}
|
|
2962
|
-
|
|
2963
|
-
const
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
3036
|
+
function unredacted(record) {
|
|
3037
|
+
const found = /* @__PURE__ */ new Set();
|
|
3038
|
+
const walk = (node) => {
|
|
3039
|
+
if (typeof node === "string") {
|
|
3040
|
+
if (/(?:^|[\s"'`(=,:;])(?:~\/|\/(?:Users|home|root)\/)|[A-Za-z]:\\Users\\/.test(node)) found.add("a path outside the repository");
|
|
3041
|
+
if (/\b[a-z][a-z0-9+.-]*:\/\/[^\s/@:"'<>]+(?::[^\s/@"'<>]*)?@/i.test(node) && !node.includes("[redacted]@")) found.add("a URL with credentials");
|
|
3042
|
+
} else if (Array.isArray(node)) node.forEach(walk);
|
|
3043
|
+
else if (node && typeof node === "object") Object.values(node).forEach(walk);
|
|
3044
|
+
};
|
|
3045
|
+
walk(record.products);
|
|
3046
|
+
return [...found];
|
|
3047
|
+
}
|
|
3048
|
+
var failed = (p) => "error" in p;
|
|
3049
|
+
function orgView(latest, now) {
|
|
3050
|
+
const systems = /* @__PURE__ */ new Map();
|
|
3051
|
+
let products = 0;
|
|
3052
|
+
let unreadable = 0;
|
|
3053
|
+
for (const { record } of latest) {
|
|
3054
|
+
for (const product2 of record.products) {
|
|
3055
|
+
products++;
|
|
3056
|
+
if (failed(product2)) {
|
|
3057
|
+
unreadable++;
|
|
3058
|
+
continue;
|
|
3059
|
+
}
|
|
3060
|
+
const system = systems.get(product2.system.id) ?? { name: product2.system.name, latest: product2.system.version, versions: /* @__PURE__ */ new Map() };
|
|
3061
|
+
if (compareVersions(product2.system.version, system.latest) > 0) system.latest = product2.system.version;
|
|
3062
|
+
const version = product2.lock?.systemVersion ?? null;
|
|
3063
|
+
system.versions.set(version, [...system.versions.get(version) ?? [], { repo: record.repo, id: product2.id }]);
|
|
3064
|
+
systems.set(product2.system.id, system);
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
const statuses = latest.flatMap(({ record }) => record.products.filter((p) => !failed(p)));
|
|
3068
|
+
const behind = statuses.filter((p) => p.lock === null || p.lock.systemVersion !== systems.get(p.system.id).latest).length;
|
|
3069
|
+
return {
|
|
3070
|
+
schema: "youi.org/v1",
|
|
3071
|
+
generatedAt: now.toISOString(),
|
|
3072
|
+
repos: latest.map(({ record, receivedAt }) => ({ repo: record.repo, commit: record.commit, ref: record.ref, generatedAt: record.generatedAt, receivedAt, products: record.products })),
|
|
3073
|
+
systems: [...systems].sort(([a], [b]) => a.localeCompare(b)).map(([id, s]) => ({
|
|
3074
|
+
id,
|
|
3075
|
+
name: s.name,
|
|
3076
|
+
latest: s.latest,
|
|
3077
|
+
versions: [...s.versions].sort(([a], [b]) => a === null ? 1 : b === null ? -1 : compareVersions(b, a)).map(([version, list]) => ({ version, products: list }))
|
|
3078
|
+
})),
|
|
3079
|
+
totals: { repos: latest.length, products, unreadable, behind, failing: statuses.filter((p) => p.check.errors > 0 || p.check.incomplete).length }
|
|
3080
|
+
};
|
|
3081
|
+
}
|
|
3082
|
+
var TYPES = { ".js": "text/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", ".svg": "image/svg+xml", ".woff2": "font/woff2", ".png": "image/png", ".webp": "image/webp", ".json": "application/json" };
|
|
3083
|
+
async function startOrgHub(options) {
|
|
3084
|
+
const problem = secretsProblem(options.github ? void 0 : options.ingestToken, options.viewKey, Boolean(options.github));
|
|
3085
|
+
if (problem) throw new Error(problem);
|
|
3086
|
+
const host = options.host ?? "127.0.0.1";
|
|
3087
|
+
const local = ["127.0.0.1", "localhost", "::1"].includes(host);
|
|
3088
|
+
const secure = options.secureCookies ?? !local;
|
|
3089
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
3090
|
+
const session = createHmac("sha256", options.viewKey).update("youiyoux hub viewer").digest("base64url");
|
|
3091
|
+
const page = options.dist ? (await readFile6(join17(options.dist, "index.html"), "utf8")).replace("</head>", ` <meta name="${ORG_META}" content="/api/org" />
|
|
3092
|
+
</head>`) : null;
|
|
3093
|
+
const pagePolicy = csp(page ? [...page.matchAll(/<script>([\s\S]*?)<\/script>/g)].map((m) => m[1]) : []);
|
|
3094
|
+
const viewer = (req) => {
|
|
3095
|
+
const given = cookie(req, COOKIE) ?? null;
|
|
3096
|
+
const key2 = bearer(req);
|
|
3097
|
+
return given !== null && same(given, session) || key2 !== null && same(key2, options.viewKey);
|
|
3098
|
+
};
|
|
3099
|
+
const cookieHeader = (value, maxAge) => `${COOKIE}=${value}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${maxAge}${secure ? "; Secure" : ""}`;
|
|
3100
|
+
const serveFile = async (res, path) => {
|
|
3101
|
+
const root = join17(options.dist, "assets");
|
|
3102
|
+
const file = normalize(join17(options.dist, path));
|
|
3103
|
+
if (!(file.startsWith(`${root}${sep3}`) || file === join17(options.dist, "favicon.svg"))) return send(res, 404, { error: "not found" });
|
|
3104
|
+
const body = await readFile6(file).catch(() => null);
|
|
3105
|
+
if (!body) return send(res, 404, { error: "not found" });
|
|
3106
|
+
res.writeHead(200, { ...HEADERS, "Content-Type": TYPES[extname(file)] ?? "application/octet-stream", "Cache-Control": path.startsWith("/assets/") ? "public, max-age=31536000, immutable" : "no-cache" });
|
|
3107
|
+
res.end(body);
|
|
3108
|
+
};
|
|
2967
3109
|
const server = createServer((req, res) => {
|
|
2968
3110
|
void (async () => {
|
|
2969
|
-
const path = (req.url ?? "").split("?")[0];
|
|
2970
|
-
|
|
2971
|
-
if (req.method !== "POST" || path !== "/api/workspace/open" && path !== "/api/workspace/refresh") {
|
|
2972
|
-
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" });
|
|
2973
|
-
}
|
|
2974
|
-
if (req.headers[KEY_HEADER] !== key2) return send(res, 401, { error: "this tab does not carry this run's key" });
|
|
2975
|
-
if (busy) return send(res, 409, { error: "another product is opening; try again in a moment" });
|
|
2976
|
-
busy = true;
|
|
3111
|
+
const path = (req.url ?? "/").split("?")[0];
|
|
3112
|
+
const method = req.method ?? "GET";
|
|
2977
3113
|
try {
|
|
2978
|
-
if (path === "/api/
|
|
2979
|
-
|
|
2980
|
-
|
|
3114
|
+
if (path === "/api/health" && method === "GET") return send(res, 200, { ok: true });
|
|
3115
|
+
if (path === "/api/records" && method === "POST") {
|
|
3116
|
+
const oidc = req.headers[GITHUB_TOKEN_HEADER];
|
|
3117
|
+
let sender = null;
|
|
3118
|
+
if (options.github) {
|
|
3119
|
+
if (typeof oidc !== "string") return send(res, 401, { error: "this hub takes records from GitHub Actions with its OIDC token: give the workflow permissions: id-token: write (youiyoux hooks install --record does)" });
|
|
3120
|
+
try {
|
|
3121
|
+
sender = await options.github(oidc);
|
|
3122
|
+
} catch (error) {
|
|
3123
|
+
return send(res, 401, { error: `the GitHub Actions token was refused: ${error instanceof Error ? error.message : String(error)}` });
|
|
3124
|
+
}
|
|
3125
|
+
} else {
|
|
3126
|
+
const token = bearer(req);
|
|
3127
|
+
if (token === null || !same(token, options.ingestToken)) return send(res, 401, { error: "this hub takes records only with its ingest token (YOUIYOUX_HUB_TOKEN)" });
|
|
3128
|
+
}
|
|
3129
|
+
if (!(req.headers["content-type"] ?? "").startsWith("application/json")) return send(res, 415, { error: "a record is JSON (content-type: application/json)" });
|
|
3130
|
+
let parsed;
|
|
3131
|
+
try {
|
|
3132
|
+
parsed = JSON.parse(await readBody(req, MAX_RECORD));
|
|
3133
|
+
} catch (error) {
|
|
3134
|
+
const status = error.status;
|
|
3135
|
+
return send(res, status ?? 400, { error: status ? error.message : "a record is JSON" });
|
|
3136
|
+
}
|
|
3137
|
+
const record = CiRecord.safeParse(parsed);
|
|
3138
|
+
if (!record.success) return send(res, 422, { error: `not a youi.record/v1: ${record.error.issues.slice(0, 3).map((i) => `${i.path.join(".") || "record"}: ${i.message}`).join("; ")}` });
|
|
3139
|
+
if (sender) {
|
|
3140
|
+
const mismatch = recordMismatch(sender, record.data);
|
|
3141
|
+
if (mismatch) return send(res, 403, { error: `${mismatch}: a repository's CI sends only its own record` });
|
|
3142
|
+
}
|
|
3143
|
+
const received = now();
|
|
3144
|
+
if (Date.parse(record.data.generatedAt) > received.getTime() + FUTURE_MS) return send(res, 422, { error: `the record is dated ${record.data.generatedAt}, after the hub's clock (${received.toISOString()}) by more than five minutes: it would stay its repository's latest, so it was not kept` });
|
|
3145
|
+
const leaks = unredacted(record.data);
|
|
3146
|
+
if (leaks.length > 0) return send(res, 422, { error: `the record holds ${leaks.join(" and ")}, so it was not kept: youiyoux record redacts these` });
|
|
3147
|
+
await options.store.putRecord(record.data, received.toISOString());
|
|
3148
|
+
return send(res, 201, { kept: record.data.repo, products: record.data.products.length });
|
|
2981
3149
|
}
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
3150
|
+
if (path === "/api/session" && method === "POST") {
|
|
3151
|
+
let key2;
|
|
3152
|
+
try {
|
|
3153
|
+
key2 = JSON.parse(await readBody(req, 4096)).key;
|
|
3154
|
+
} catch {
|
|
3155
|
+
return send(res, 400, { error: 'send { "key": "\u2026" }' });
|
|
3156
|
+
}
|
|
3157
|
+
if (typeof key2 !== "string" || !same(key2, options.viewKey)) return send(res, 401, { error: "that is not this hub's view key" });
|
|
3158
|
+
return send(res, 200, { ok: true }, { "Set-Cookie": cookieHeader(session, SESSION_DAYS * 86400) });
|
|
3159
|
+
}
|
|
3160
|
+
if (path === "/api/session" && method === "DELETE") return send(res, 200, { ok: true }, { "Set-Cookie": cookieHeader("", 0) });
|
|
3161
|
+
if (path === "/api/org" && method === "GET") {
|
|
3162
|
+
if (!viewer(req)) return send(res, 401, { error: "sign in with the hub's view key" });
|
|
3163
|
+
return send(res, 200, orgView(await options.store.latestRecords(), now()));
|
|
3164
|
+
}
|
|
3165
|
+
if (path.startsWith("/api/")) return send(res, 404, { error: "this hub is read-only: it shows the organisation, and takes records from CI" });
|
|
3166
|
+
if (method !== "GET" && method !== "HEAD") return send(res, 405, { error: "read-only" });
|
|
3167
|
+
if (!page) return send(res, 404, { error: "the hub's page is not built here; the API answers at /api/org" });
|
|
3168
|
+
if (path.startsWith("/assets/") || path === "/favicon.svg") return await serveFile(res, path);
|
|
3169
|
+
res.writeHead(200, { ...HEADERS, "Content-Security-Policy": pagePolicy, "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-cache" });
|
|
3170
|
+
res.end(page);
|
|
2990
3171
|
} catch (error) {
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
3172
|
+
process.stderr.write(`youiyoux hub: ${method} ${path} failed: ${error instanceof Error ? error.message : String(error)}
|
|
3173
|
+
`);
|
|
3174
|
+
send(res, 500, { error: "the hub could not read its store; its log says why" });
|
|
2994
3175
|
}
|
|
2995
3176
|
})();
|
|
2996
3177
|
});
|
|
2997
|
-
await new Promise((done) =>
|
|
3178
|
+
await new Promise((done, fail) => {
|
|
3179
|
+
server.once("error", fail);
|
|
3180
|
+
server.listen(options.port ?? 0, host, done);
|
|
3181
|
+
});
|
|
2998
3182
|
const address = server.address();
|
|
2999
3183
|
const port = typeof address === "object" && address ? address.port : 0;
|
|
3000
3184
|
return {
|
|
3001
|
-
url: `http
|
|
3002
|
-
key: key2,
|
|
3185
|
+
url: `http://${host.includes(":") ? `[${host}]` : host}:${port}`,
|
|
3003
3186
|
close: () => new Promise((done) => server.close(() => done()))
|
|
3004
3187
|
};
|
|
3005
3188
|
}
|
|
3006
3189
|
|
|
3007
|
-
// ../cli/src/
|
|
3008
|
-
import {
|
|
3009
|
-
import {
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3190
|
+
// ../cli/src/report/safe.ts
|
|
3191
|
+
import { realpathSync } from "node:fs";
|
|
3192
|
+
import { readFile as readFile7 } from "node:fs/promises";
|
|
3193
|
+
import { homedir as homedir2, userInfo as userInfo2 } from "node:os";
|
|
3194
|
+
import { join as join18 } from "node:path";
|
|
3195
|
+
|
|
3196
|
+
// ../cli/src/report/filters.ts
|
|
3197
|
+
var SHOWCASE_GLOBS = ["**/{__stories__,.storybook,storybook,storybook-static,example,examples,demo,demos,playground,showcase,showcases}/**", "**/*.stories.*", "**/*.story.*"];
|
|
3198
|
+
var isShowcase = (file) => /(?:^|\/)(?:__stories__|\.?storybook|storybook-static|examples?|demos?|playground|showcases?)(?:\/|$)|\.(?:stories|story)\./i.test(file.replaceAll("\\", "/"));
|
|
3199
|
+
var ROUTE_EXPORT = /^(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|generateMetadata|generateStaticParams|generateViewport|metadata|viewport)$/;
|
|
3200
|
+
var ROUTE_FILE = /(?:^|\/)(?:page|layout|route|loading|error|global-error|not-found|template|default)\.[jt]sx?$/;
|
|
3201
|
+
function uiDefinition(definition, modules) {
|
|
3202
|
+
if (isShowcase(definition.file)) return false;
|
|
3203
|
+
if (definition.kind !== "react") return true;
|
|
3204
|
+
if (ROUTE_EXPORT.test(definition.name)) return false;
|
|
3205
|
+
const module = modules.get(definition.file);
|
|
3206
|
+
if (!module || module.jsx.length === 0) return false;
|
|
3207
|
+
if (ROUTE_FILE.test(definition.file) && module.exports.some((e) => e.kind === "local" && e.name === "default" && (e.local === definition.name || definition.name === "default"))) return false;
|
|
3208
|
+
return module.jsx.some((jsx) => {
|
|
3209
|
+
let node = jsx.node.parent();
|
|
3210
|
+
while (node) {
|
|
3211
|
+
if (["function_declaration", "variable_declarator", "class_declaration"].includes(String(node.kind())) && node.field("name")?.text() === definition.name) return true;
|
|
3212
|
+
node = node.parent();
|
|
3213
|
+
}
|
|
3214
|
+
return false;
|
|
3215
|
+
});
|
|
3216
|
+
}
|
|
3217
|
+
function maskFunctions(source, pattern) {
|
|
3218
|
+
const chars = source.split("");
|
|
3219
|
+
for (const match of source.matchAll(pattern)) {
|
|
3220
|
+
let depth = 1;
|
|
3221
|
+
let end = match.index + match[0].length;
|
|
3222
|
+
for (; end < source.length && depth > 0; end++) {
|
|
3223
|
+
if (source[end] === "(") depth++;
|
|
3224
|
+
if (source[end] === ")") depth--;
|
|
3225
|
+
}
|
|
3226
|
+
for (let i = match.index; i < end; i++) if (chars[i] !== "\n" && chars[i] !== "\r") chars[i] = " ";
|
|
3019
3227
|
}
|
|
3228
|
+
return chars.join("");
|
|
3020
3229
|
}
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3230
|
+
var withoutVariables = (source) => maskFunctions(source, /\bvar\s*\(/g);
|
|
3231
|
+
var COMPONENT_WORDS = /^(?:button|btn|input|field|card|badge|chip|pill|tag|dialog|modal|popover|tooltip|toast|tabs?|nav|navbar|sidebar|header|footer|menu|dropdown|select|checkbox|radio|switch|toggle|slider|avatar|table|panel|sheet|drawer|banner|alert|hero|form|icon|kbd|callout|accordion|toolbar)$/;
|
|
3232
|
+
var CONTEXT_WORDS = /^(?:phone|mobile|tablet|desktop|wide|narrow|compact|print|touch|landscape|portrait)$/;
|
|
3233
|
+
var SCALE_STEP = /^(?:\d+(?:\.\d+)?|\d*x*[sl]|\dx[sl]|sm|md|lg|base|full|none|default|px|half)$/i;
|
|
3234
|
+
var GROUP_WORDS = /* @__PURE__ */ new Set(["space", "spacing", "radius", "radii", "rounded", "color", "colour", "colors", "size", "gap"]);
|
|
3235
|
+
var componentOf = (token) => token.cssName.split("-").find((part) => COMPONENT_WORDS.test(part)) ?? null;
|
|
3236
|
+
function tokenUsage(modules) {
|
|
3237
|
+
const count = /* @__PURE__ */ new Map();
|
|
3238
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
3239
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
3240
|
+
for (const module of modules) {
|
|
3241
|
+
if (!module.source.includes("--")) continue;
|
|
3242
|
+
const names = /* @__PURE__ */ new Set();
|
|
3243
|
+
for (const declaration of module.source.split(/[;{}]/)) {
|
|
3244
|
+
const defined = /^\s*(--[\w-]+)\s*:\s*(var\(\s*--[\w-]+\s*\)\s*$)?/.exec(declaration);
|
|
3245
|
+
if (defined) {
|
|
3246
|
+
names.add(defined[1]);
|
|
3247
|
+
if (defined[2]) aliases.add(defined[1]);
|
|
3248
|
+
} else for (const [, name] of declaration.matchAll(/var\(\s*(--[\w-]+)/g)) {
|
|
3249
|
+
names.add(name);
|
|
3250
|
+
count.set(name, (count.get(name) ?? 0) + 1);
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
if (names.size > 0) byFile.set(module.file, names);
|
|
3254
|
+
}
|
|
3255
|
+
return { count, byFile, aliases };
|
|
3256
|
+
}
|
|
3257
|
+
function genericity(token, prefix) {
|
|
3258
|
+
const parts = token.cssName.split("-").filter(Boolean);
|
|
3259
|
+
const rest = parts.filter((part, i) => !(i === 0 && part === prefix) && !GROUP_WORDS.has(part));
|
|
3260
|
+
if (rest.some((part) => COMPONENT_WORDS.test(part) || CONTEXT_WORDS.test(part))) return 0;
|
|
3261
|
+
return rest.every((part) => SCALE_STEP.test(part)) ? 2 : 1;
|
|
3262
|
+
}
|
|
3263
|
+
function prefixOf(tokens) {
|
|
3264
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3265
|
+
for (const token of tokens) {
|
|
3266
|
+
const first = token.cssName.split("-")[0];
|
|
3267
|
+
if (first.length <= 3 && token.cssName.includes("-")) counts.set(first, (counts.get(first) ?? 0) + 1);
|
|
3268
|
+
}
|
|
3269
|
+
const [best, n] = [...counts].sort((a, b) => b[1] - a[1])[0] ?? [null, 0];
|
|
3270
|
+
return best && n >= tokens.length / 4 ? best : null;
|
|
3271
|
+
}
|
|
3272
|
+
function scaleTokens(index, file, usage) {
|
|
3273
|
+
const value = (t) => `${t.id.split(".")[0]}\0${JSON.stringify(t.light)}`;
|
|
3274
|
+
const prefix = prefixOf(index.tokens);
|
|
3275
|
+
const rank = new Map(index.tokens.map((t) => [t, genericity(t, prefix)]));
|
|
3276
|
+
const general = new Set(index.tokens.filter((t) => rank.get(t) > 0).map(value));
|
|
3277
|
+
const path = file.toLowerCase();
|
|
3278
|
+
const local = usage.byFile.get(file) ?? /* @__PURE__ */ new Set();
|
|
3279
|
+
const varOf = new Map([...index.byVar].map(([name, t]) => [t, name]));
|
|
3280
|
+
const cssName = (t) => varOf.get(t) ?? `--${t.cssName}`;
|
|
3281
|
+
const tokens = index.tokens.filter((t) => {
|
|
3282
|
+
if (rank.get(t) > 0) return true;
|
|
3283
|
+
const component = componentOf(t);
|
|
3284
|
+
return component !== null && !general.has(value(t)) && new RegExp(`(?:^|[/._-])${component}s?(?:[/._-]|$)`).test(path);
|
|
3285
|
+
});
|
|
3286
|
+
const score = (t) => [rank.get(t), Number(local.has(cssName(t))), Number(!usage.aliases.has(cssName(t))), usage.count.get(cssName(t)) ?? 0, -t.cssName.length];
|
|
3287
|
+
const higher = (a, b) => {
|
|
3288
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return b[i] - a[i];
|
|
3289
|
+
return 0;
|
|
3290
|
+
};
|
|
3291
|
+
const sorted = tokens.map((t) => ({ t, s: score(t) })).sort((a, b) => higher(a.s, b.s) || a.t.cssName.localeCompare(b.t.cssName)).map(({ t }) => t);
|
|
3292
|
+
return { ...index, tokens: sorted };
|
|
3293
|
+
}
|
|
3294
|
+
var escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3295
|
+
async function reportTokens(module, tokens, usage) {
|
|
3296
|
+
if (isShowcase(module.file) || module.lang !== "css" && module.jsx.length === 0) return [];
|
|
3297
|
+
let source = withoutVariables(module.source);
|
|
3298
|
+
if (module.lang !== "css") {
|
|
3299
|
+
source = maskFunctions(source, /(?:repeating-)?(?:linear|radial|conic)-gradient\s*\(/gi);
|
|
3300
|
+
source = source.replace(/(?<=[[,]\s*)(["'`])(?:#[0-9a-fA-F]{3,8}|(?:rgba?|hsla?)\([^)\n]*\))\1(?=\s*[,\]])/g, (literal) => " ".repeat(literal.length));
|
|
3301
|
+
}
|
|
3302
|
+
const clean = source === module.source ? module : await parseModule(module.file, source);
|
|
3303
|
+
if (!clean) return [];
|
|
3304
|
+
const index = scaleTokens(tokens, module.file, usage);
|
|
3305
|
+
return hardcodedValues(clean, index, { isTokenSource: module.lang === "css", added: () => true }).filter((f) => {
|
|
3306
|
+
if (/\bvar\(/.test(f.text)) return false;
|
|
3307
|
+
const kind = /^Hardcoded (colour|radius|spacing|type)\b/.exec(f.message)?.[1];
|
|
3308
|
+
const groups = kind === "colour" ? /^(color|colour)\./ : kind === "radius" ? /^(radius|radii|rounded)\./ : kind === "spacing" ? /^(space|spacing)\./ : /^(type|typography|fontSize)\./;
|
|
3309
|
+
const named2 = index.tokens.filter((t) => new RegExp(`[\\s(]${escape(t.id)}(?=[\\s,)]|$)`).test(f.fix));
|
|
3310
|
+
return named2.length > 0 && named2.every((t) => groups.test(t.id));
|
|
3311
|
+
});
|
|
3312
|
+
}
|
|
3313
|
+
function publicLook(caption) {
|
|
3314
|
+
const name = /^(?:<)?([\w.$:-]+)/.exec(caption)?.[1] ?? "Component";
|
|
3315
|
+
const classes = [...caption.matchAll(/\bclass(?:Name)?=("(?:[^"\\]|\\.)*"|'[^']*')/g)].map((m) => `class=${m[1]}`);
|
|
3316
|
+
return [name, ...classes].join(" ");
|
|
3317
|
+
}
|
|
3318
|
+
|
|
3319
|
+
// ../cli/src/report/safe.ts
|
|
3320
|
+
var REDACTED = "[redacted]";
|
|
3321
|
+
var OUTSIDE = "[path outside the repo]";
|
|
3322
|
+
var MIN_VALUE = 6;
|
|
3323
|
+
var BENIGN_ENV = /^(?:npm_|NPM_|PNPM_|NODE_|NVM_|TERM|COLORTERM|LANG$|LANGUAGE$|LC_|SHELL$|SHLVL$|PWD$|OLDPWD$|_$|PATH$|MANPATH$|INFOPATH$|TMPDIR$|TMP$|TEMP$|EDITOR$|VISUAL$|PAGER$|LESS|LOGNAME$|USER$|USERNAME$|HOME$|XPC_|__CF|COMMAND_MODE$|DISPLAY$|HOSTTYPE$|OSTYPE$|MACHTYPE$|INIT_CWD$|FORCE_COLOR$|NO_COLOR$|CI$|COREPACK_|VSCODE_|ITERM_|TERM_|SECURITYSESSIONID$|LaunchInstanceID$|XDG_|HOMEBREW_|SSH_AUTH_SOCK$|STARSHIP_|ZSH|OMZ|P9K|POWERLEVEL|VITEST|TEST$|MODE$|DEV$|PROD$|SSR$|BASE_URL$)/;
|
|
3324
|
+
var PLAIN_WORD = /^[A-Za-z]+$/;
|
|
3325
|
+
var PLAIN = /^(?:true|false|yes|no|on|off|null|undefined|\d+(?:\.\d+)?)$/i;
|
|
3326
|
+
var escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3327
|
+
async function dotenvValues(root) {
|
|
3328
|
+
const files = await findFiles(root, [".env", ".env.*", "**/.env", "**/.env.*"]).catch(() => []);
|
|
3329
|
+
const values = [];
|
|
3330
|
+
for (const file of files) {
|
|
3331
|
+
const text = await readFile7(join18(root, file), "utf8").catch(() => "");
|
|
3332
|
+
for (const line of text.split(/\r?\n/)) {
|
|
3333
|
+
const match = /^\s*(?:export\s+)?[A-Za-z_][A-Za-z0-9_.-]*\s*=\s*(.*)$/.exec(line);
|
|
3334
|
+
if (!match) continue;
|
|
3335
|
+
const value = match[1].trim().replace(/^(['"`])(.*)\1$/, "$2").replace(/\s+#.*$/, "");
|
|
3336
|
+
if (value.length >= MIN_VALUE) values.push(value);
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
return values;
|
|
3340
|
+
}
|
|
3341
|
+
function looksSecret(text) {
|
|
3342
|
+
if (/^(?:sk|pk|rk)_(?:live|test)_|^sk-[A-Za-z0-9]|^gh[pousr]_|^github_pat_|^xox[abposr]-|^AKIA[0-9A-Z]{12}|^AIza[0-9A-Za-z_-]{20}|^glpat-|^eyJ[A-Za-z0-9_-]{10,}\./.test(text)) return true;
|
|
3343
|
+
if (text.length < 20 || /[\s:[\]]/.test(text)) return false;
|
|
3344
|
+
const classes = [/[a-z]/, /[A-Z]/, /\d/].filter((c) => c.test(text)).length;
|
|
3345
|
+
if (classes < 2 || !/\d/.test(text)) return false;
|
|
3346
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3347
|
+
for (const c of text) counts.set(c, (counts.get(c) ?? 0) + 1);
|
|
3348
|
+
const entropy = [...counts.values()].reduce((sum, n) => sum - n / text.length * Math.log2(n / text.length), 0);
|
|
3349
|
+
return entropy >= 3.5;
|
|
3350
|
+
}
|
|
3351
|
+
function withoutRoot(text, root) {
|
|
3352
|
+
const at = escapeRegExp(root);
|
|
3353
|
+
return text.replace(new RegExp(`${at}/`, "g"), "").replace(new RegExp(`${at}(?![\\w.-])`, "g"), ".");
|
|
3354
|
+
}
|
|
3355
|
+
var Redactor = class _Redactor {
|
|
3356
|
+
roots;
|
|
3357
|
+
home;
|
|
3358
|
+
username;
|
|
3359
|
+
values;
|
|
3360
|
+
includeCode;
|
|
3361
|
+
constructor(options) {
|
|
3362
|
+
const real = (path) => {
|
|
3363
|
+
try {
|
|
3364
|
+
return realpathSync(path);
|
|
3365
|
+
} catch {
|
|
3366
|
+
return path;
|
|
3367
|
+
}
|
|
3368
|
+
};
|
|
3369
|
+
this.roots = [.../* @__PURE__ */ new Set([options.root, real(options.root)])].sort((a, b) => b.length - a.length);
|
|
3370
|
+
this.home = options.home ?? homedir2();
|
|
3371
|
+
this.username = options.username ?? (() => {
|
|
3372
|
+
try {
|
|
3373
|
+
return userInfo2().username;
|
|
3374
|
+
} catch {
|
|
3375
|
+
return "";
|
|
3376
|
+
}
|
|
3377
|
+
})();
|
|
3378
|
+
const env = Object.entries(options.env ?? process.env).flatMap(([name, value]) => value && !BENIGN_ENV.test(name) && !PLAIN.test(value.trim()) && !(PLAIN_WORD.test(value) && value.length < 12) ? [value] : []);
|
|
3379
|
+
this.values = [...new Set([...env, ...options.dotenv ?? []].filter((v) => v.length >= MIN_VALUE && !this.isRoot(v)))].sort((a, b) => b.length - a.length);
|
|
3380
|
+
this.includeCode = options.includeCode === true;
|
|
3381
|
+
}
|
|
3382
|
+
static async forRepo(root, options = {}) {
|
|
3383
|
+
return new _Redactor({ ...options, root, dotenv: await dotenvValues(root) });
|
|
3384
|
+
}
|
|
3385
|
+
isRoot(value) {
|
|
3386
|
+
return this.roots.some((root) => value === root || root.startsWith(`${value}/`));
|
|
3387
|
+
}
|
|
3388
|
+
/** One string, cleaned. */
|
|
3389
|
+
text(input) {
|
|
3390
|
+
let text = input;
|
|
3391
|
+
for (const root of this.roots) text = withoutRoot(text, root);
|
|
3392
|
+
text = text.replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s/@:"'<>]+(?::[^\s/@"'<>]*)?@/gi, `$1${REDACTED}@`);
|
|
3393
|
+
for (const value of this.values) text = text.split(value).join(REDACTED);
|
|
3394
|
+
text = text.replace(/(^|[\s"'`(=,:;])(?:~\/|\/(?:Users|home|root|var|private|tmp|opt|mnt|media|Volumes|srv|etc|usr|workspace|workspaces|builds|runner|github|nix)\b)[^\s"'`<>)]*/g, `$1${OUTSIDE}`);
|
|
3395
|
+
text = text.replace(/(^|[\s"'`(=,:;])[A-Za-z]:\\[^\s"'`<>)]*/g, `$1${OUTSIDE}`);
|
|
3396
|
+
if (this.home.length > 1) text = text.split(this.home).join("~");
|
|
3397
|
+
if (this.username.length >= 3) text = text.replace(new RegExp(`(?<![A-Za-z0-9])${escapeRegExp(this.username)}(?![A-Za-z0-9])`, "g"), REDACTED);
|
|
3398
|
+
return text;
|
|
3399
|
+
}
|
|
3400
|
+
/** A line of code (--include-code): cleaned, and strings that look like keys or tokens blanked. */
|
|
3401
|
+
code(input) {
|
|
3402
|
+
const assigned = /((?:key|token|secret|passw(?:or)?d|pwd|auth|credential|session|cookie|private|dsn|api[_-]?key)[A-Za-z0-9_]*["']?\s*[:=]\s*)(["'`])([^"'`]{4,})\2/gi;
|
|
3403
|
+
const text = input.replace(assigned, `$1$2${REDACTED}$2`).replace(/(["'`])([^"'`\s]{12,})\1/g, (whole, quote, inner) => looksSecret(inner) ? `${quote}${REDACTED}${quote}` : whole).replace(/[A-Za-z0-9_\-+/=.]{20,}/g, (token) => looksSecret(token) ? REDACTED : token);
|
|
3404
|
+
return this.text(text);
|
|
3405
|
+
}
|
|
3406
|
+
/** The whole model, every string cleaned; code only when it was asked for. */
|
|
3407
|
+
model(value) {
|
|
3408
|
+
const walk = (node, key2) => {
|
|
3409
|
+
if (typeof node === "string") return key2 === "code" ? this.code(node) : this.text(node);
|
|
3410
|
+
if (Array.isArray(node)) return node.map((item) => walk(key2 === "looks" && !this.includeCode && typeof item === "string" ? publicLook(item) : item, null));
|
|
3411
|
+
if (node && typeof node === "object") {
|
|
3412
|
+
const out = {};
|
|
3413
|
+
for (const [k, v] of Object.entries(node)) {
|
|
3414
|
+
if (k === "code" && !this.includeCode) continue;
|
|
3415
|
+
out[k] = k === "light" || k === "dark" ? v : walk(v, k);
|
|
3416
|
+
}
|
|
3417
|
+
return out;
|
|
3418
|
+
}
|
|
3419
|
+
return node;
|
|
3420
|
+
};
|
|
3421
|
+
return walk(value, null);
|
|
3422
|
+
}
|
|
3423
|
+
/** What sensitive text is still in `output`: the names of the kinds found, never the values. */
|
|
3424
|
+
leaks(output) {
|
|
3425
|
+
const found = [];
|
|
3426
|
+
if (this.roots.some((root) => output.includes(root))) found.push("an absolute path to the repo");
|
|
3427
|
+
if (this.home.length > 1 && output.includes(this.home)) found.push("the home directory");
|
|
3428
|
+
if (this.username.length >= 3 && new RegExp(`[/\\\\~]${escapeRegExp(this.username)}(?![A-Za-z0-9])`).test(output)) found.push("the username in a path");
|
|
3429
|
+
if (this.values.some((v) => output.includes(v))) found.push("an environment or .env value");
|
|
3430
|
+
if (/\b[a-z][a-z0-9+.-]*:\/\/[^\s/@:"'<>]+:[^\s/@"'<>]+@/i.test(output)) found.push("a URL with credentials");
|
|
3431
|
+
return found;
|
|
3432
|
+
}
|
|
3433
|
+
/** Last pass over a written output: clean it, then refuse it if anything sensitive survived. */
|
|
3434
|
+
scrub(output) {
|
|
3435
|
+
let text = output;
|
|
3436
|
+
if (this.leaks(text).length === 0) return text;
|
|
3437
|
+
for (const root of this.roots) text = withoutRoot(text, root);
|
|
3438
|
+
if (this.home.length > 1) text = text.split(this.home).join("~");
|
|
3439
|
+
for (const value of this.values) text = text.split(value).join(REDACTED);
|
|
3440
|
+
text = text.replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s/@:"'<>]+:[^\s/@"'<>]+@/gi, `$1${REDACTED}@`);
|
|
3441
|
+
if (this.username.length >= 3) text = text.replace(new RegExp(`([/\\\\~])${escapeRegExp(this.username)}(?![A-Za-z0-9])`, "g"), `$1${REDACTED}`);
|
|
3442
|
+
const left = this.leaks(text);
|
|
3443
|
+
if (left.length > 0) throw new Error(`the report still held ${left.join(", ")} after redaction, so nothing was written`);
|
|
3444
|
+
return text;
|
|
3445
|
+
}
|
|
3446
|
+
};
|
|
3447
|
+
|
|
3448
|
+
// ../cli/src/status.ts
|
|
3449
|
+
import { realpath as realpath5 } from "node:fs/promises";
|
|
3450
|
+
import { dirname as dirname9, join as join19, relative as relative8, resolve as resolve10 } from "node:path";
|
|
3451
|
+
var placeOf = (cwd, root) => {
|
|
3452
|
+
const rel = relative8(cwd, root) || ".";
|
|
3453
|
+
return rel.split(/[\\/]/).filter((part) => part === "..").length > 1 ? resolve10(root) : rel;
|
|
3454
|
+
};
|
|
3455
|
+
var WRITES2 = /* @__PURE__ */ new Set(["created", "updated", "merged", "adopted", "conflict", "removed"]);
|
|
3456
|
+
function sourceOf(project2) {
|
|
3457
|
+
if (project2.linked) return `your checkout ${project2.linked}`;
|
|
3458
|
+
const source = project2.config.source;
|
|
3459
|
+
if ("package" in source) return `package ${source.package}`;
|
|
3460
|
+
if ("git" in source) return `git ${source.git} at ${source.ref}`;
|
|
3461
|
+
return resolve10(project2.root) === resolve10(project2.repo.root) ? "this repo" : `path ${source.path}`;
|
|
3462
|
+
}
|
|
3463
|
+
function nextStep(s, migrations) {
|
|
3464
|
+
if (s.sync.state === "never") return "youiyoux sync: this product has never been synced with its System";
|
|
3465
|
+
if (s.sync.state === "conflict") return "youiyoux sync: resolve the conflicts it reports, then youiyoux sync --adopt records them";
|
|
3466
|
+
if (migrations > 0) return `youiyoux upgrade: ${s.system.name} v${s.system.version} comes with ${plural(migrations, "migration")} for this product's code, which upgrade runs with the sync`;
|
|
3467
|
+
if (s.sync.state === "behind") return `youiyoux sync: ${plural(s.sync.changes.length, "file")} would change`;
|
|
3468
|
+
if (s.check.incomplete) return "youiyoux check: some files or rules could not be checked, and it says which";
|
|
3469
|
+
if (s.check.errors > 0) return `youiyoux check: fix ${plural(s.check.errors, "error")}`;
|
|
3470
|
+
if (s.requests.open > 0) return `youiyoux requests: answer ${plural(s.requests.open, "open request")}`;
|
|
3471
|
+
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`;
|
|
3472
|
+
return "nothing to do";
|
|
3473
|
+
}
|
|
3474
|
+
async function projectStatus(project2, cwd = process.cwd()) {
|
|
3475
|
+
const lock = await readLock(project2.root);
|
|
3476
|
+
const { actions } = await syncProject(project2, { dryRun: true });
|
|
3477
|
+
const changes = actions.filter((a) => WRITES2.has(a.action)).map((a) => ({ file: a.file, action: a.action }));
|
|
3478
|
+
const baselineFile2 = baselinePath(project2);
|
|
3479
|
+
const check = await runCheck(project2, { baseline: await readTextOrNull(baselineFile2) === null ? null : baselineFile2 });
|
|
3480
|
+
const comparison = check.baseline?.comparison;
|
|
3481
|
+
const counted = comparison ? [...comparison.new, ...comparison.regressed] : check.violations;
|
|
3482
|
+
const usages = check.scan.report.usages;
|
|
3483
|
+
const { RequestStore } = await import("./src-R7JGKOYI.js");
|
|
3484
|
+
const requests = await new RequestStore(project2.root, project2.config.id).list();
|
|
3485
|
+
const status = {
|
|
3486
|
+
id: project2.config.id,
|
|
3487
|
+
root: placeOf(cwd, project2.root),
|
|
3488
|
+
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 },
|
|
3489
|
+
lock: lock ? { systemVersion: lock.systemVersion, syncedAt: lock.syncedAt } : null,
|
|
3490
|
+
sync: { state: lock === null ? "never" : actions.some((a) => a.action === "conflict") ? "conflict" : changes.length > 0 ? "behind" : "current", changes },
|
|
3491
|
+
drift: (await verifyLock(project2) ?? []).map((i) => ({ file: i.file, severity: i.severity, message: i.message })),
|
|
3492
|
+
check: {
|
|
3493
|
+
errors: check.errors,
|
|
3494
|
+
warnings: counted.filter((v) => v.severity === "warn").length,
|
|
3495
|
+
baseline: comparison ? { known: comparison.known.length, new: comparison.new.length + comparison.regressed.length, fixed: comparison.fixed.reduce((n, f) => n + f.gone, 0) } : null,
|
|
3496
|
+
incomplete: check.incomplete
|
|
3497
|
+
},
|
|
3498
|
+
components: { system: project2.system.components.length, used: new Set(usages.map((u) => u.element)).size, sites: usages.length },
|
|
3499
|
+
requests: { open: requests.requests.filter((r) => r.status === "open").length, addressed: requests.requests.filter((r) => r.status === "addressed").length, unreadable: requests.unreadable.length }
|
|
3500
|
+
};
|
|
3501
|
+
return { ...status, next: nextStep(status, lock ? pendingMigrations(project2, lock).length : 0) };
|
|
3502
|
+
}
|
|
3503
|
+
async function statusTargets(cwd, named2) {
|
|
3504
|
+
if (named2) return { roots: [resolve10(named2)], workspace: null };
|
|
3505
|
+
const product2 = findUp(cwd, PROJECT_FILE);
|
|
3506
|
+
if (product2) return { roots: [product2], workspace: null };
|
|
3507
|
+
const at = findUp(cwd, WORKSPACE_FILE);
|
|
3508
|
+
if (at) {
|
|
3509
|
+
const file = join19(at, WORKSPACE_FILE);
|
|
3510
|
+
const workspace = await readDefinition(file, WorkspaceConfig);
|
|
3511
|
+
return { roots: workspace.projects.map((p) => resolve10(at, p)), workspace: file };
|
|
3512
|
+
}
|
|
3513
|
+
const top = findUp(cwd, ".git") ?? null;
|
|
3514
|
+
if (top === null) return { roots: [], workspace: null };
|
|
3515
|
+
const configs = await findFiles(cwd, [`**/${PROJECT_FILE}`]);
|
|
3516
|
+
const roots = [];
|
|
3517
|
+
for (const config of configs) {
|
|
3518
|
+
const dir = resolve10(cwd, dirname9(config));
|
|
3519
|
+
if (await realpath5(findUp(dir, ".git") ?? dir) === await realpath5(top)) roots.push(dir);
|
|
3520
|
+
}
|
|
3521
|
+
return { roots, workspace: null };
|
|
3522
|
+
}
|
|
3523
|
+
async function workspaceStatus(cwd, named2) {
|
|
3524
|
+
const { roots, workspace } = await statusTargets(cwd, named2);
|
|
3525
|
+
const projects = [];
|
|
3526
|
+
for (const root of roots) {
|
|
3527
|
+
try {
|
|
3528
|
+
projects.push(await projectStatus(await loadProject(root), cwd));
|
|
3529
|
+
} catch (error) {
|
|
3530
|
+
const label = placeOf(cwd, root);
|
|
3531
|
+
projects.push({ root: label, error: error instanceof Error ? error.message : String(error), next: await readTextOrNull(join19(root, PROJECT_FILE)) === null ? `youiyoux adopt or youiyoux init --design <dir> in ${label}` : `fix ${join19(label, PROJECT_FILE)}` });
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
return { schema: "youi.status/v1", generatedAt: (/* @__PURE__ */ new Date()).toISOString(), workspace: workspace ? relative8(cwd, workspace) || WORKSPACE_FILE : null, projects };
|
|
3535
|
+
}
|
|
3536
|
+
var failed2 = (p) => "error" in p;
|
|
3537
|
+
function projectLines(s) {
|
|
3538
|
+
const row = (label, text) => ` ${padEnd(label, 11)} ${text}`;
|
|
3539
|
+
const synced = s.lock ? `synced ${s.lock.syncedAt.slice(0, 10)}${s.lock.systemVersion !== s.system.version ? ` at v${s.lock.systemVersion}` : ""}` : "never synced";
|
|
3540
|
+
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];
|
|
3541
|
+
const edited = s.drift.filter((d) => d.severity === "warn");
|
|
3542
|
+
const broken = s.drift.filter((d) => d.severity === "error");
|
|
3543
|
+
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("; ");
|
|
3544
|
+
const { check } = s;
|
|
3545
|
+
const counts = `${check.errors > 0 ? style.error(plural(check.errors, "error")) : style.ok("0 errors")} \xB7 ${plural(check.warnings, "warning")}`;
|
|
3546
|
+
const baseline = check.baseline ? style.dim(` beyond the baseline (${check.baseline.known} accepted${check.baseline.fixed > 0 ? `, ${check.baseline.fixed} fixed` : ""})`) : "";
|
|
3547
|
+
return [
|
|
3548
|
+
`${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}`)}`,
|
|
3549
|
+
row("sync", sync),
|
|
3550
|
+
row("drift", drift),
|
|
3551
|
+
row("check", `${counts}${baseline}${check.incomplete ? style.warn(" \xB7 incomplete: not a pass") : ""}`),
|
|
3552
|
+
row("components", `${s.components.used} of ${s.components.system} System components used, at ${plural(s.components.sites, "site")}`),
|
|
3553
|
+
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 ")),
|
|
3554
|
+
`${style.bold("Next:")} ${s.next}`
|
|
3555
|
+
];
|
|
3556
|
+
}
|
|
3557
|
+
function renderStatus(report) {
|
|
3558
|
+
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
|
|
3559
|
+
Next: youiyoux adopt makes this product's own UI its design system, or youiyoux init --design <dir> follows one`;
|
|
3560
|
+
const blocks = report.projects.map((p) => failed2(p) ? [`${style.bold(p.root)} ${style.error("\u2716")} ${p.error}`, `${style.bold("Next:")} ${p.next}`] : projectLines(p));
|
|
3561
|
+
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"}`)}`, ""] : [];
|
|
3562
|
+
return [...head, ...blocks.flatMap((b, i) => i === 0 ? b : ["", ...b])].join("\n");
|
|
3563
|
+
}
|
|
3564
|
+
var statusExitCode = (report) => report.projects.some((p) => failed2(p) || p.check.incomplete || p.check.errors > 0 || p.sync.state === "conflict") ? 1 : 0;
|
|
3565
|
+
|
|
3566
|
+
// ../cli/src/record.ts
|
|
3567
|
+
var run6 = promisify5(execFile5);
|
|
3568
|
+
var git3 = async (cwd, args) => run6("git", args, { cwd }).then((r) => r.stdout.trim() || null, () => null);
|
|
3569
|
+
var HUB_URL_ENV = "YOUIYOUX_HUB_URL";
|
|
3570
|
+
var HUB_TOKEN_ENV = "YOUIYOUX_HUB_TOKEN";
|
|
3571
|
+
function toolVersion(root = TOOL_ROOT) {
|
|
3572
|
+
for (const file of [join20(root, "packages/youiyoux/package.json"), join20(root, "../package.json")]) {
|
|
3573
|
+
try {
|
|
3574
|
+
const pkg = JSON.parse(readFileSync2(file, "utf8"));
|
|
3575
|
+
if (pkg.name === "youiyoux" && pkg.version) return pkg.version;
|
|
3576
|
+
} catch {
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3579
|
+
return null;
|
|
3580
|
+
}
|
|
3581
|
+
async function buildRecord(cwd, options = {}) {
|
|
3582
|
+
const env = options.env ?? process.env;
|
|
3583
|
+
const top = await git3(cwd, ["rev-parse", "--show-toplevel"]);
|
|
3584
|
+
if (!top) throw new Error("youiyoux record runs in a git checkout: the record names its repository and commit");
|
|
3585
|
+
const repo = options.repo ?? env.GITHUB_REPOSITORY ?? githubSlug(await git3(top, ["remote", "get-url", "origin"]) ?? "");
|
|
3586
|
+
if (!repo || !RepoName.safeParse(repo).success) throw new Error(`${repo ? `"${repo}" is not` : "no repository name found: pass"} --repo owner/name${repo ? "" : " (or run where GITHUB_REPOSITORY is set, or with a GitHub origin remote)"}`);
|
|
3587
|
+
const commit = env.GITHUB_SHA ?? await git3(top, ["rev-parse", "HEAD"]);
|
|
3588
|
+
const branch = env.GITHUB_REF_NAME ?? await git3(top, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
3589
|
+
const report = await workspaceStatus(top);
|
|
3590
|
+
if (report.projects.length === 0) throw new Error("no product here: no youi.project.json at the top of this repository, no youi.workspace.json, and none below it");
|
|
3591
|
+
const redactor = await Redactor.forRepo(top, { env });
|
|
3592
|
+
const products = JSON.parse(redactor.scrub(JSON.stringify(redactor.model(report.projects))));
|
|
3593
|
+
return CiRecord.parse({ schema: "youi.record/v1", repo, commit: commit && /^[0-9a-f]{40}$/.test(commit) ? commit : null, ref: branch && branch !== "HEAD" ? branch : null, generatedAt: (/* @__PURE__ */ new Date()).toISOString(), tool: toolVersion() ?? "unknown", products });
|
|
3594
|
+
}
|
|
3595
|
+
function recordsUrl(hub) {
|
|
3596
|
+
let url;
|
|
3597
|
+
try {
|
|
3598
|
+
url = new URL(hub);
|
|
3599
|
+
} catch {
|
|
3600
|
+
throw new Error(`${HUB_URL_ENV} "${hub}" is not a URL: expected the hub's address, such as https://design-hub.acme.dev`);
|
|
3601
|
+
}
|
|
3602
|
+
const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
3603
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && local)) throw new Error(`the hub must be https (${url.protocol}//${url.host} would send the token in the clear); http is only for this machine`);
|
|
3604
|
+
if (url.username || url.password) throw new Error(`${HUB_URL_ENV} carries a user name or password: the token goes in ${HUB_TOKEN_ENV}`);
|
|
3605
|
+
return new URL(`${url.pathname.replace(/\/+$/, "")}/api/records`, url.origin);
|
|
3606
|
+
}
|
|
3607
|
+
async function sendRecord(record, hub, credentials, fetcher = fetch) {
|
|
3608
|
+
const url = recordsUrl(hub);
|
|
3609
|
+
const headers = { "content-type": "application/json", ...credentials.token ? { authorization: `Bearer ${credentials.token}` } : {}, ...credentials.github ? { [GITHUB_TOKEN_HEADER]: credentials.github } : {} };
|
|
3610
|
+
const response = await fetcher(url, { method: "POST", headers, body: JSON.stringify(record), redirect: "error", signal: AbortSignal.timeout(3e4) }).catch((error) => {
|
|
3611
|
+
throw new Error(`could not reach the hub at ${url.origin}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3612
|
+
});
|
|
3613
|
+
if (response.ok) return url.origin;
|
|
3614
|
+
const body = await response.json().catch(() => ({}));
|
|
3615
|
+
const why = typeof body.error === "string" ? body.error : response.statusText;
|
|
3616
|
+
throw new Error(`the hub at ${url.origin} refused the record (${response.status}): ${why}`);
|
|
3617
|
+
}
|
|
3618
|
+
function renderRecord(record, sentTo) {
|
|
3619
|
+
const failed3 = record.products.filter((p) => "error" in p).length;
|
|
3620
|
+
const head = `${style.ok(sentTo ? "sent" : "recorded")} ${record.repo}${record.commit ? style.dim(` at ${record.commit.slice(0, 7)}`) : ""}${record.ref ? style.dim(` on ${record.ref}`) : ""}: ${plural(record.products.length, "product")}${failed3 > 0 ? style.warn(` (${failed3} could not be read)`) : ""}${sentTo ? ` to ${sentTo}` : ""}`;
|
|
3621
|
+
return [head, style.dim(" redacted as the shareable report is: repo-relative paths, no home, username, env values or credentials; no code")].join("\n");
|
|
3622
|
+
}
|
|
3623
|
+
|
|
3624
|
+
// ../cli/src/workspace-hub.ts
|
|
3625
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
3626
|
+
import { createServer as createServer2 } from "node:http";
|
|
3627
|
+
var KEY_HEADER = "x-youi-session";
|
|
3628
|
+
function send2(res, status, body) {
|
|
3629
|
+
res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
3630
|
+
res.end(JSON.stringify(body));
|
|
3631
|
+
}
|
|
3632
|
+
async function readBody2(req) {
|
|
3633
|
+
let text = "";
|
|
3634
|
+
for await (const chunk2 of req) {
|
|
3635
|
+
text += String(chunk2);
|
|
3636
|
+
if (text.length > 4096) throw new Error("request too large");
|
|
3637
|
+
}
|
|
3638
|
+
return text ? JSON.parse(text) : {};
|
|
3639
|
+
}
|
|
3640
|
+
async function startWorkspaceHub(options) {
|
|
3641
|
+
const key2 = randomBytes2(18).toString("base64url");
|
|
3642
|
+
let report = await options.status();
|
|
3643
|
+
let current = options.current;
|
|
3644
|
+
let busy = false;
|
|
3645
|
+
const server = createServer2((req, res) => {
|
|
3646
|
+
void (async () => {
|
|
3647
|
+
const path = (req.url ?? "").split("?")[0];
|
|
3648
|
+
if (req.method === "GET" && path === "/api/workspace") return send2(res, 200, { report, current });
|
|
3649
|
+
if (req.method !== "POST" || path !== "/api/workspace/open" && path !== "/api/workspace/refresh") {
|
|
3650
|
+
return send2(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" });
|
|
3651
|
+
}
|
|
3652
|
+
if (req.headers[KEY_HEADER] !== key2) return send2(res, 401, { error: "this tab does not carry this run's key" });
|
|
3653
|
+
if (busy) return send2(res, 409, { error: "another product is opening; try again in a moment" });
|
|
3654
|
+
busy = true;
|
|
3655
|
+
try {
|
|
3656
|
+
if (path === "/api/workspace/refresh") {
|
|
3657
|
+
report = await options.status();
|
|
3658
|
+
return send2(res, 200, { report, current });
|
|
3659
|
+
}
|
|
3660
|
+
const { index } = await readBody2(req);
|
|
3661
|
+
const entry = typeof index === "number" && Number.isInteger(index) ? report.projects[index] : void 0;
|
|
3662
|
+
if (entry === void 0 || options.roots[index] === void 0) return send2(res, 400, { error: `no product ${JSON.stringify(index)} in this workspace` });
|
|
3663
|
+
if ("error" in entry) return send2(res, 400, { error: `${entry.root} cannot be opened: ${entry.error}`, problems: [entry.next] });
|
|
3664
|
+
const code = await options.open(options.roots[index]);
|
|
3665
|
+
if (code !== 0) return send2(res, 500, { error: `the snapshot of ${entry.root} failed (exit ${code}); the hub still shows the product it showed` });
|
|
3666
|
+
current = index;
|
|
3667
|
+
return send2(res, 200, { report, current });
|
|
3668
|
+
} catch (error) {
|
|
3669
|
+
return send2(res, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
3670
|
+
} finally {
|
|
3671
|
+
busy = false;
|
|
3672
|
+
}
|
|
3673
|
+
})();
|
|
3674
|
+
});
|
|
3675
|
+
await new Promise((done) => server.listen(0, "127.0.0.1", done));
|
|
3676
|
+
const address = server.address();
|
|
3677
|
+
const port = typeof address === "object" && address ? address.port : 0;
|
|
3678
|
+
return {
|
|
3679
|
+
url: `http://127.0.0.1:${port}`,
|
|
3680
|
+
key: key2,
|
|
3681
|
+
close: () => new Promise((done) => server.close(() => done()))
|
|
3682
|
+
};
|
|
3683
|
+
}
|
|
3684
|
+
|
|
3685
|
+
// ../cli/src/index-store.ts
|
|
3686
|
+
import { appendFile, mkdir as mkdir4, readFile as readFile8, stat as stat2, rename as rename2, mkdtemp as mkdtemp4, rm as rm6 } from "node:fs/promises";
|
|
3687
|
+
import { dirname as dirname10, join as join21, resolve as resolve11 } from "node:path";
|
|
3688
|
+
var SOURCES_FILE = "index-sources.json";
|
|
3689
|
+
var SCANS_LOG = "scans.jsonl";
|
|
3690
|
+
var EMPTY = { discover: null, inventories: [], projects: [] };
|
|
3691
|
+
async function readIndexSources(home) {
|
|
3692
|
+
try {
|
|
3693
|
+
const raw = JSON.parse(await readFile8(join21(home, SOURCES_FILE), "utf8"));
|
|
3694
|
+
return { discover: raw.discover ?? null, inventories: raw.inventories ?? [], projects: raw.projects ?? [] };
|
|
3695
|
+
} catch {
|
|
3696
|
+
return EMPTY;
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
async function noteSource(home, change) {
|
|
3700
|
+
const sources = await readIndexSources(home);
|
|
3701
|
+
const next2 = {
|
|
3702
|
+
discover: change.discover ?? sources.discover,
|
|
3703
|
+
inventories: change.inventory && !sources.inventories.includes(change.inventory) ? [...sources.inventories, change.inventory] : sources.inventories,
|
|
3704
|
+
projects: change.project ? [...sources.projects.filter((p) => p.root !== change.project.root), change.project] : sources.projects
|
|
3027
3705
|
};
|
|
3028
|
-
await writeFileAtomic(
|
|
3706
|
+
await writeFileAtomic(join21(home, SOURCES_FILE), `${JSON.stringify(next2, null, 2)}
|
|
3029
3707
|
`);
|
|
3030
3708
|
}
|
|
3031
3709
|
async function recordInventory(store, home, report, file) {
|
|
3032
|
-
const
|
|
3033
|
-
const logged = { run:
|
|
3710
|
+
const run8 = inventoryRun(report, resolve11(file));
|
|
3711
|
+
const logged = { run: run8.run, source: run8.source, scannedAt: run8.scannedAt, projects: run8.projects, families: run8.families };
|
|
3034
3712
|
await mkdir4(home, { recursive: true });
|
|
3035
|
-
await appendFile(
|
|
3713
|
+
await appendFile(join21(home, SCANS_LOG), `${JSON.stringify({ schema: "youi.scan-log/v1", ...logged })}
|
|
3036
3714
|
`);
|
|
3037
|
-
await store.recordInventory(
|
|
3038
|
-
await noteSource(home, { inventory:
|
|
3715
|
+
await store.recordInventory(run8);
|
|
3716
|
+
await noteSource(home, { inventory: run8.source });
|
|
3039
3717
|
await linkCandidates(store);
|
|
3040
|
-
return
|
|
3718
|
+
return run8;
|
|
3041
3719
|
}
|
|
3042
3720
|
async function recordProject(store, project2, usage) {
|
|
3043
3721
|
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 }));
|
|
3044
3722
|
}
|
|
3045
3723
|
async function readLog(home) {
|
|
3046
|
-
const text = await
|
|
3724
|
+
const text = await readFile8(join21(home, SCANS_LOG), "utf8").catch(() => "");
|
|
3047
3725
|
return text.split("\n").flatMap((line) => {
|
|
3048
3726
|
if (!line.trim()) return [];
|
|
3049
3727
|
try {
|
|
@@ -3060,7 +3738,7 @@ async function rebuildIndex(path, options = {}) {
|
|
|
3060
3738
|
const home = indexHome(path, env);
|
|
3061
3739
|
await mkdir4(dirname10(path), { recursive: true });
|
|
3062
3740
|
const scratch = !env.DATABASE_URL ? await mkdtemp4(`${path}.rebuild-`) : null;
|
|
3063
|
-
const target = scratch ?
|
|
3741
|
+
const target = scratch ? join21(scratch, "index.db") : path;
|
|
3064
3742
|
const store = scratch ? await openSqlite(target) : await sharedStore(path, env);
|
|
3065
3743
|
try {
|
|
3066
3744
|
if (env.DATABASE_URL) await store.reset();
|
|
@@ -3068,7 +3746,7 @@ async function rebuildIndex(path, options = {}) {
|
|
|
3068
3746
|
const sources = await readIndexSources(home);
|
|
3069
3747
|
const cache = options.discoverCache ?? sources.discover;
|
|
3070
3748
|
let discover = { cache, chosen: 0, all: 0 };
|
|
3071
|
-
if (cache && await stat2(
|
|
3749
|
+
if (cache && await stat2(join21(cache, "index.json")).then(() => true, () => false)) {
|
|
3072
3750
|
const loaded = await ingestCatalog(store, cache);
|
|
3073
3751
|
discover = { cache, chosen: loaded.chosen, all: loaded.all };
|
|
3074
3752
|
}
|
|
@@ -3085,25 +3763,25 @@ async function rebuildIndex(path, options = {}) {
|
|
|
3085
3763
|
await store.recordInventory(inventoryRun(parsed.data, file));
|
|
3086
3764
|
replayed++;
|
|
3087
3765
|
}
|
|
3088
|
-
const
|
|
3766
|
+
const failed3 = [];
|
|
3089
3767
|
let projects = 0;
|
|
3090
3768
|
const gone = [];
|
|
3091
3769
|
for (const source of sources.projects) {
|
|
3092
3770
|
if (!await stat2(source.root).then((s) => s.isDirectory(), () => false)) {
|
|
3093
3771
|
gone.push(source.root);
|
|
3094
|
-
|
|
3772
|
+
failed3.push({ root: source.root, reason: "folder not found (moved or deleted), skipped" });
|
|
3095
3773
|
continue;
|
|
3096
3774
|
}
|
|
3097
3775
|
try {
|
|
3098
3776
|
const project2 = await loadProject(source.root, source.options);
|
|
3099
|
-
const usage = ScanReport.safeParse(await readJson(source.usage ??
|
|
3777
|
+
const usage = ScanReport.safeParse(await readJson(source.usage ?? join21(project2.root, ".youi/usage.json")));
|
|
3100
3778
|
await recordProject(store, project2, usage.success ? usage.data : null);
|
|
3101
3779
|
projects++;
|
|
3102
3780
|
} catch (error) {
|
|
3103
|
-
|
|
3781
|
+
failed3.push({ root: source.root, reason: error instanceof Error ? error.message.split("\n")[0] : String(error) });
|
|
3104
3782
|
}
|
|
3105
3783
|
}
|
|
3106
|
-
const broken =
|
|
3784
|
+
const broken = failed3.filter((f) => !gone.includes(f.root));
|
|
3107
3785
|
if (broken.length) throw new Error(`Index rebuild failed: ${broken.map((f) => `${f.root}: ${f.reason}`).join("; ")}`);
|
|
3108
3786
|
await linkCandidates(store);
|
|
3109
3787
|
const tables = (await store.stats()).tables;
|
|
@@ -3127,7 +3805,7 @@ async function rebuildIndex(path, options = {}) {
|
|
|
3127
3805
|
throw error;
|
|
3128
3806
|
}
|
|
3129
3807
|
}
|
|
3130
|
-
return { location: scratch ? path : store.location, discover, scans: log.length, inventories: { replayed, missing }, projects: { replayed: projects, failed:
|
|
3808
|
+
return { location: scratch ? path : store.location, discover, scans: log.length, inventories: { replayed, missing }, projects: { replayed: projects, failed: failed3 }, tables, durationMs: Math.round(performance.now() - started) };
|
|
3131
3809
|
} finally {
|
|
3132
3810
|
if (scratch) {
|
|
3133
3811
|
await store.close();
|
|
@@ -3315,7 +3993,7 @@ function renderAdoptionRequest(request) {
|
|
|
3315
3993
|
// ../cli/src/report/run.ts
|
|
3316
3994
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
3317
3995
|
import { mkdir as mkdir5, rename as rename3, stat as stat3, writeFile as writeFile2 } from "node:fs/promises";
|
|
3318
|
-
import { join as
|
|
3996
|
+
import { join as join27, relative as relative10, resolve as resolve14 } from "node:path";
|
|
3319
3997
|
|
|
3320
3998
|
// ../cli/src/report/card.ts
|
|
3321
3999
|
import { deflateSync } from "node:zlib";
|
|
@@ -3480,209 +4158,86 @@ function fit(text, width, max, min) {
|
|
|
3480
4158
|
var CRC_TABLE = Array.from({ length: 256 }, (_, n) => {
|
|
3481
4159
|
let c = n;
|
|
3482
4160
|
for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
3483
|
-
return c >>> 0;
|
|
3484
|
-
});
|
|
3485
|
-
function crc32(bytes) {
|
|
3486
|
-
let c = 4294967295;
|
|
3487
|
-
for (const byte of bytes) c = CRC_TABLE[(c ^ byte) & 255] ^ c >>> 8;
|
|
3488
|
-
return (c ^ 4294967295) >>> 0;
|
|
3489
|
-
}
|
|
3490
|
-
function chunk(type, data) {
|
|
3491
|
-
const head = Buffer.alloc(8);
|
|
3492
|
-
head.writeUInt32BE(data.length, 0);
|
|
3493
|
-
head.write(type, 4, "ascii");
|
|
3494
|
-
const crc = Buffer.alloc(4);
|
|
3495
|
-
crc.writeUInt32BE(crc32(Buffer.concat([head.subarray(4), data])), 0);
|
|
3496
|
-
return Buffer.concat([head, data, crc]);
|
|
3497
|
-
}
|
|
3498
|
-
function encodePng(width, height, rgb) {
|
|
3499
|
-
const header = Buffer.alloc(13);
|
|
3500
|
-
header.writeUInt32BE(width, 0);
|
|
3501
|
-
header.writeUInt32BE(height, 4);
|
|
3502
|
-
header.set([8, 2, 0, 0, 0], 8);
|
|
3503
|
-
const raw = Buffer.alloc((width * 3 + 1) * height);
|
|
3504
|
-
for (let row = 0; row < height; row++) {
|
|
3505
|
-
raw[row * (width * 3 + 1)] = 0;
|
|
3506
|
-
raw.set(rgb.subarray(row * width * 3, (row + 1) * width * 3), row * (width * 3 + 1) + 1);
|
|
3507
|
-
}
|
|
3508
|
-
return Buffer.concat([Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), chunk("IHDR", header), chunk("IDAT", deflateSync(raw, { level: 9 })), chunk("IEND", new Uint8Array())]);
|
|
3509
|
-
}
|
|
3510
|
-
function drawCard(input) {
|
|
3511
|
-
const canvas = new Canvas(CARD_WIDTH, CARD_HEIGHT, INK.background);
|
|
3512
|
-
const margin = 72;
|
|
3513
|
-
const inner = CARD_WIDTH - margin * 2;
|
|
3514
|
-
canvas.text(margin, 64, "youiyoux report", 3, INK.muted);
|
|
3515
|
-
const name = fit(input.repo, inner, 9, 5);
|
|
3516
|
-
canvas.text(margin, 104, name.text, name.scale, INK.text);
|
|
3517
|
-
const total = input.parts.reduce((n, part) => n + part.places, 0);
|
|
3518
|
-
const value = fit(String(total), inner / 2, 14, 8);
|
|
3519
|
-
const valueWidth = canvas.text(margin, 236, value.text, value.scale, total > 0 ? INK.text : INK.ok);
|
|
3520
|
-
const words = total === 0 ? "places to fix: clean" : total === 1 ? "place to fix" : "places to fix";
|
|
3521
|
-
canvas.text(margin + valueWidth + 28, 236 + 7 * value.scale - 7 * 4, words, 4, INK.muted);
|
|
3522
|
-
const top = 380;
|
|
3523
|
-
const steps = [INK.accent, INK.accent2, INK.accent3];
|
|
3524
|
-
canvas.rect(margin, top, inner, 20, INK.track, 10);
|
|
3525
|
-
if (total > 0) {
|
|
3526
|
-
let x2 = margin;
|
|
3527
|
-
const shown2 = input.parts.filter((part) => part.places > 0);
|
|
3528
|
-
const room = inner - 6 * (shown2.length - 1);
|
|
3529
|
-
input.parts.forEach((part, i) => {
|
|
3530
|
-
if (part.places === 0) return;
|
|
3531
|
-
const last = part === shown2.at(-1);
|
|
3532
|
-
const width = last ? margin + inner - x2 : Math.max(10, Math.round(part.places / total * room));
|
|
3533
|
-
canvas.rect(x2, top, width, 20, steps[i] ?? INK.accent3, 10);
|
|
3534
|
-
x2 += width + 6;
|
|
3535
|
-
});
|
|
3536
|
-
}
|
|
3537
|
-
let x = margin;
|
|
3538
|
-
input.parts.forEach((part, i) => {
|
|
3539
|
-
canvas.rect(x, top + 52, 18, 18, part.places > 0 ? steps[i] ?? INK.accent3 : INK.track, 4);
|
|
3540
|
-
x += 32;
|
|
3541
|
-
x += canvas.text(x, top + 52, part.label.split(" ")[0], 3, INK.muted) + 16;
|
|
3542
|
-
x += canvas.text(x, top + 52, String(part.places), 3, part.places > 0 ? INK.text : INK.ok) + 40;
|
|
3543
|
-
});
|
|
3544
|
-
const credit = "npx youiyoux";
|
|
3545
|
-
const creditWidth = textWidth(credit, 3);
|
|
3546
|
-
canvas.text(CARD_WIDTH - margin - creditWidth, 560, credit, 3, INK.accent);
|
|
3547
|
-
const footer = fit(input.footer, inner - creditWidth - 40, 3, 2);
|
|
3548
|
-
canvas.text(margin, 560 + (footer.scale === 3 ? 0 : 4), footer.text, footer.scale, INK.muted);
|
|
3549
|
-
return encodePng(CARD_WIDTH, CARD_HEIGHT, canvas.pixels);
|
|
3550
|
-
}
|
|
3551
|
-
|
|
3552
|
-
// ../cli/src/report/model.ts
|
|
3553
|
-
import { readFile as readFile8 } from "node:fs/promises";
|
|
3554
|
-
import { join as join22 } from "node:path";
|
|
3555
|
-
|
|
3556
|
-
// ../cli/src/report/filters.ts
|
|
3557
|
-
var SHOWCASE_GLOBS = ["**/{__stories__,.storybook,storybook,storybook-static,example,examples,demo,demos,playground,showcase,showcases}/**", "**/*.stories.*", "**/*.story.*"];
|
|
3558
|
-
var isShowcase = (file) => /(?:^|\/)(?:__stories__|\.?storybook|storybook-static|examples?|demos?|playground|showcases?)(?:\/|$)|\.(?:stories|story)\./i.test(file.replaceAll("\\", "/"));
|
|
3559
|
-
var ROUTE_EXPORT = /^(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|generateMetadata|generateStaticParams|generateViewport|metadata|viewport)$/;
|
|
3560
|
-
var ROUTE_FILE = /(?:^|\/)(?:page|layout|route|loading|error|global-error|not-found|template|default)\.[jt]sx?$/;
|
|
3561
|
-
function uiDefinition(definition, modules) {
|
|
3562
|
-
if (isShowcase(definition.file)) return false;
|
|
3563
|
-
if (definition.kind !== "react") return true;
|
|
3564
|
-
if (ROUTE_EXPORT.test(definition.name)) return false;
|
|
3565
|
-
const module = modules.get(definition.file);
|
|
3566
|
-
if (!module || module.jsx.length === 0) return false;
|
|
3567
|
-
if (ROUTE_FILE.test(definition.file) && module.exports.some((e) => e.kind === "local" && e.name === "default" && (e.local === definition.name || definition.name === "default"))) return false;
|
|
3568
|
-
return module.jsx.some((jsx) => {
|
|
3569
|
-
let node = jsx.node.parent();
|
|
3570
|
-
while (node) {
|
|
3571
|
-
if (["function_declaration", "variable_declarator", "class_declaration"].includes(String(node.kind())) && node.field("name")?.text() === definition.name) return true;
|
|
3572
|
-
node = node.parent();
|
|
3573
|
-
}
|
|
3574
|
-
return false;
|
|
3575
|
-
});
|
|
3576
|
-
}
|
|
3577
|
-
function maskFunctions(source, pattern) {
|
|
3578
|
-
const chars = source.split("");
|
|
3579
|
-
for (const match of source.matchAll(pattern)) {
|
|
3580
|
-
let depth = 1;
|
|
3581
|
-
let end = match.index + match[0].length;
|
|
3582
|
-
for (; end < source.length && depth > 0; end++) {
|
|
3583
|
-
if (source[end] === "(") depth++;
|
|
3584
|
-
if (source[end] === ")") depth--;
|
|
3585
|
-
}
|
|
3586
|
-
for (let i = match.index; i < end; i++) if (chars[i] !== "\n" && chars[i] !== "\r") chars[i] = " ";
|
|
3587
|
-
}
|
|
3588
|
-
return chars.join("");
|
|
3589
|
-
}
|
|
3590
|
-
var withoutVariables = (source) => maskFunctions(source, /\bvar\s*\(/g);
|
|
3591
|
-
var COMPONENT_WORDS = /^(?:button|btn|input|field|card|badge|chip|pill|tag|dialog|modal|popover|tooltip|toast|tabs?|nav|navbar|sidebar|header|footer|menu|dropdown|select|checkbox|radio|switch|toggle|slider|avatar|table|panel|sheet|drawer|banner|alert|hero|form|icon|kbd|callout|accordion|toolbar)$/;
|
|
3592
|
-
var CONTEXT_WORDS = /^(?:phone|mobile|tablet|desktop|wide|narrow|compact|print|touch|landscape|portrait)$/;
|
|
3593
|
-
var SCALE_STEP = /^(?:\d+(?:\.\d+)?|\d*x*[sl]|\dx[sl]|sm|md|lg|base|full|none|default|px|half)$/i;
|
|
3594
|
-
var GROUP_WORDS = /* @__PURE__ */ new Set(["space", "spacing", "radius", "radii", "rounded", "color", "colour", "colors", "size", "gap"]);
|
|
3595
|
-
var componentOf = (token) => token.cssName.split("-").find((part) => COMPONENT_WORDS.test(part)) ?? null;
|
|
3596
|
-
function tokenUsage(modules) {
|
|
3597
|
-
const count = /* @__PURE__ */ new Map();
|
|
3598
|
-
const byFile = /* @__PURE__ */ new Map();
|
|
3599
|
-
const aliases = /* @__PURE__ */ new Set();
|
|
3600
|
-
for (const module of modules) {
|
|
3601
|
-
if (!module.source.includes("--")) continue;
|
|
3602
|
-
const names = /* @__PURE__ */ new Set();
|
|
3603
|
-
for (const declaration of module.source.split(/[;{}]/)) {
|
|
3604
|
-
const defined = /^\s*(--[\w-]+)\s*:\s*(var\(\s*--[\w-]+\s*\)\s*$)?/.exec(declaration);
|
|
3605
|
-
if (defined) {
|
|
3606
|
-
names.add(defined[1]);
|
|
3607
|
-
if (defined[2]) aliases.add(defined[1]);
|
|
3608
|
-
} else for (const [, name] of declaration.matchAll(/var\(\s*(--[\w-]+)/g)) {
|
|
3609
|
-
names.add(name);
|
|
3610
|
-
count.set(name, (count.get(name) ?? 0) + 1);
|
|
3611
|
-
}
|
|
3612
|
-
}
|
|
3613
|
-
if (names.size > 0) byFile.set(module.file, names);
|
|
3614
|
-
}
|
|
3615
|
-
return { count, byFile, aliases };
|
|
4161
|
+
return c >>> 0;
|
|
4162
|
+
});
|
|
4163
|
+
function crc32(bytes) {
|
|
4164
|
+
let c = 4294967295;
|
|
4165
|
+
for (const byte of bytes) c = CRC_TABLE[(c ^ byte) & 255] ^ c >>> 8;
|
|
4166
|
+
return (c ^ 4294967295) >>> 0;
|
|
3616
4167
|
}
|
|
3617
|
-
function
|
|
3618
|
-
const
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
4168
|
+
function chunk(type, data) {
|
|
4169
|
+
const head = Buffer.alloc(8);
|
|
4170
|
+
head.writeUInt32BE(data.length, 0);
|
|
4171
|
+
head.write(type, 4, "ascii");
|
|
4172
|
+
const crc = Buffer.alloc(4);
|
|
4173
|
+
crc.writeUInt32BE(crc32(Buffer.concat([head.subarray(4), data])), 0);
|
|
4174
|
+
return Buffer.concat([head, data, crc]);
|
|
3622
4175
|
}
|
|
3623
|
-
function
|
|
3624
|
-
const
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
4176
|
+
function encodePng(width, height, rgb) {
|
|
4177
|
+
const header = Buffer.alloc(13);
|
|
4178
|
+
header.writeUInt32BE(width, 0);
|
|
4179
|
+
header.writeUInt32BE(height, 4);
|
|
4180
|
+
header.set([8, 2, 0, 0, 0], 8);
|
|
4181
|
+
const raw = Buffer.alloc((width * 3 + 1) * height);
|
|
4182
|
+
for (let row = 0; row < height; row++) {
|
|
4183
|
+
raw[row * (width * 3 + 1)] = 0;
|
|
4184
|
+
raw.set(rgb.subarray(row * width * 3, (row + 1) * width * 3), row * (width * 3 + 1) + 1);
|
|
3628
4185
|
}
|
|
3629
|
-
|
|
3630
|
-
return best && n >= tokens.length / 4 ? best : null;
|
|
3631
|
-
}
|
|
3632
|
-
function scaleTokens(index, file, usage) {
|
|
3633
|
-
const value = (t) => `${t.id.split(".")[0]}\0${JSON.stringify(t.light)}`;
|
|
3634
|
-
const prefix = prefixOf(index.tokens);
|
|
3635
|
-
const rank = new Map(index.tokens.map((t) => [t, genericity(t, prefix)]));
|
|
3636
|
-
const general = new Set(index.tokens.filter((t) => rank.get(t) > 0).map(value));
|
|
3637
|
-
const path = file.toLowerCase();
|
|
3638
|
-
const local = usage.byFile.get(file) ?? /* @__PURE__ */ new Set();
|
|
3639
|
-
const varOf = new Map([...index.byVar].map(([name, t]) => [t, name]));
|
|
3640
|
-
const cssName = (t) => varOf.get(t) ?? `--${t.cssName}`;
|
|
3641
|
-
const tokens = index.tokens.filter((t) => {
|
|
3642
|
-
if (rank.get(t) > 0) return true;
|
|
3643
|
-
const component = componentOf(t);
|
|
3644
|
-
return component !== null && !general.has(value(t)) && new RegExp(`(?:^|[/._-])${component}s?(?:[/._-]|$)`).test(path);
|
|
3645
|
-
});
|
|
3646
|
-
const score = (t) => [rank.get(t), Number(local.has(cssName(t))), Number(!usage.aliases.has(cssName(t))), usage.count.get(cssName(t)) ?? 0, -t.cssName.length];
|
|
3647
|
-
const higher = (a, b) => {
|
|
3648
|
-
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return b[i] - a[i];
|
|
3649
|
-
return 0;
|
|
3650
|
-
};
|
|
3651
|
-
const sorted = tokens.map((t) => ({ t, s: score(t) })).sort((a, b) => higher(a.s, b.s) || a.t.cssName.localeCompare(b.t.cssName)).map(({ t }) => t);
|
|
3652
|
-
return { ...index, tokens: sorted };
|
|
4186
|
+
return Buffer.concat([Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), chunk("IHDR", header), chunk("IDAT", deflateSync(raw, { level: 9 })), chunk("IEND", new Uint8Array())]);
|
|
3653
4187
|
}
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
4188
|
+
function drawCard(input) {
|
|
4189
|
+
const canvas = new Canvas(CARD_WIDTH, CARD_HEIGHT, INK.background);
|
|
4190
|
+
const margin = 72;
|
|
4191
|
+
const inner = CARD_WIDTH - margin * 2;
|
|
4192
|
+
canvas.text(margin, 64, "youiyoux report", 3, INK.muted);
|
|
4193
|
+
const name = fit(input.repo, inner, 9, 5);
|
|
4194
|
+
canvas.text(margin, 104, name.text, name.scale, INK.text);
|
|
4195
|
+
const total = input.parts.reduce((n, part) => n + part.places, 0);
|
|
4196
|
+
const value = fit(String(total), inner / 2, 14, 8);
|
|
4197
|
+
const valueWidth = canvas.text(margin, 236, value.text, value.scale, total > 0 ? INK.text : INK.ok);
|
|
4198
|
+
const words = total === 0 ? "places to fix: clean" : total === 1 ? "place to fix" : "places to fix";
|
|
4199
|
+
canvas.text(margin + valueWidth + 28, 236 + 7 * value.scale - 7 * 4, words, 4, INK.muted);
|
|
4200
|
+
const top = 380;
|
|
4201
|
+
const steps = [INK.accent, INK.accent2, INK.accent3];
|
|
4202
|
+
canvas.rect(margin, top, inner, 20, INK.track, 10);
|
|
4203
|
+
if (total > 0) {
|
|
4204
|
+
let x2 = margin;
|
|
4205
|
+
const shown2 = input.parts.filter((part) => part.places > 0);
|
|
4206
|
+
const room = inner - 6 * (shown2.length - 1);
|
|
4207
|
+
input.parts.forEach((part, i) => {
|
|
4208
|
+
if (part.places === 0) return;
|
|
4209
|
+
const last = part === shown2.at(-1);
|
|
4210
|
+
const width = last ? margin + inner - x2 : Math.max(10, Math.round(part.places / total * room));
|
|
4211
|
+
canvas.rect(x2, top, width, 20, steps[i] ?? INK.accent3, 10);
|
|
4212
|
+
x2 += width + 6;
|
|
4213
|
+
});
|
|
3661
4214
|
}
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
const groups = kind === "colour" ? /^(color|colour)\./ : kind === "radius" ? /^(radius|radii|rounded)\./ : kind === "spacing" ? /^(space|spacing)\./ : /^(type|typography|fontSize)\./;
|
|
3669
|
-
const named2 = index.tokens.filter((t) => new RegExp(`[\\s(]${escape(t.id)}(?=[\\s,)]|$)`).test(f.fix));
|
|
3670
|
-
return named2.length > 0 && named2.every((t) => groups.test(t.id));
|
|
4215
|
+
let x = margin;
|
|
4216
|
+
input.parts.forEach((part, i) => {
|
|
4217
|
+
canvas.rect(x, top + 52, 18, 18, part.places > 0 ? steps[i] ?? INK.accent3 : INK.track, 4);
|
|
4218
|
+
x += 32;
|
|
4219
|
+
x += canvas.text(x, top + 52, part.label.split(" ")[0], 3, INK.muted) + 16;
|
|
4220
|
+
x += canvas.text(x, top + 52, String(part.places), 3, part.places > 0 ? INK.text : INK.ok) + 40;
|
|
3671
4221
|
});
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
const
|
|
3676
|
-
|
|
4222
|
+
const credit = "npx youiyoux";
|
|
4223
|
+
const creditWidth = textWidth(credit, 3);
|
|
4224
|
+
canvas.text(CARD_WIDTH - margin - creditWidth, 560, credit, 3, INK.accent);
|
|
4225
|
+
const footer = fit(input.footer, inner - creditWidth - 40, 3, 2);
|
|
4226
|
+
canvas.text(margin, 560 + (footer.scale === 3 ? 0 : 4), footer.text, footer.scale, INK.muted);
|
|
4227
|
+
return encodePng(CARD_WIDTH, CARD_HEIGHT, canvas.pixels);
|
|
3677
4228
|
}
|
|
3678
4229
|
|
|
4230
|
+
// ../cli/src/report/model.ts
|
|
4231
|
+
import { readFile as readFile10 } from "node:fs/promises";
|
|
4232
|
+
import { join as join25 } from "node:path";
|
|
4233
|
+
|
|
3679
4234
|
// ../cli/src/report/groups.ts
|
|
3680
4235
|
import { existsSync as existsSync3 } from "node:fs";
|
|
3681
|
-
import { join as
|
|
4236
|
+
import { join as join23 } from "node:path";
|
|
3682
4237
|
|
|
3683
4238
|
// ../cli/src/report/imports.ts
|
|
3684
|
-
import { realpathSync } from "node:fs";
|
|
3685
|
-
import { dirname as dirname11, join as
|
|
4239
|
+
import { realpathSync as realpathSync2 } from "node:fs";
|
|
4240
|
+
import { dirname as dirname11, join as join22, relative as relative9, resolve as resolve12 } from "node:path";
|
|
3686
4241
|
import ts from "typescript";
|
|
3687
4242
|
var resolvers = /* @__PURE__ */ new Map();
|
|
3688
4243
|
function resolverOf(root) {
|
|
@@ -3701,13 +4256,13 @@ function clearImportCache() {
|
|
|
3701
4256
|
}
|
|
3702
4257
|
async function verifiedImport(root, keeper, from, suggested) {
|
|
3703
4258
|
const { options, cache } = resolverOf(root);
|
|
3704
|
-
let local = relative9(dirname11(
|
|
4259
|
+
let local = relative9(dirname11(join22(root, from)), join22(root, keeper)).replaceAll("\\", "/").replace(/\.(tsx?|jsx?|mjs)$/, "");
|
|
3705
4260
|
if (!local.startsWith(".")) local = `./${local}`;
|
|
3706
4261
|
for (const specifier of /* @__PURE__ */ new Set([suggested, local])) {
|
|
3707
4262
|
const found = ts.resolveModuleName(specifier, resolve12(root, from), options, ts.sys, cache).resolvedModule?.resolvedFileName;
|
|
3708
4263
|
if (found) {
|
|
3709
4264
|
try {
|
|
3710
|
-
if (
|
|
4265
|
+
if (realpathSync2(found) === realpathSync2(join22(root, keeper))) return specifier;
|
|
3711
4266
|
} catch {
|
|
3712
4267
|
}
|
|
3713
4268
|
}
|
|
@@ -4025,7 +4580,7 @@ function commonDir(files) {
|
|
|
4025
4580
|
async function sameNamedGroups(facts, defs, usage, importText) {
|
|
4026
4581
|
const found = [];
|
|
4027
4582
|
for (const f of facts) {
|
|
4028
|
-
const twins = [...f.twins].flatMap(([name, list]) => [...Map.groupBy(list, (t) => f.packageOf(t.file)).values()].map((
|
|
4583
|
+
const twins = [...f.twins].flatMap(([name, list]) => [...Map.groupBy(list, (t) => f.packageOf(t.file)).values()].map((same2) => [name, same2]));
|
|
4029
4584
|
for (const [name, list] of twins) {
|
|
4030
4585
|
if (list.length < 2) continue;
|
|
4031
4586
|
const threshold = GENERIC_NAMES.has(name) ? GENERIC_IDENTICAL : NEAR_IDENTICAL;
|
|
@@ -4049,8 +4604,8 @@ async function sameNamedGroups(facts, defs, usage, importText) {
|
|
|
4049
4604
|
continue;
|
|
4050
4605
|
}
|
|
4051
4606
|
const [keep, ...copies] = members.map((i) => ordered[i]);
|
|
4052
|
-
const
|
|
4053
|
-
found.push(await sameNamed(f, name, keep, copies,
|
|
4607
|
+
const same2 = members.every((i, n) => members.slice(n + 1).every((j) => (score.get(`${i}:${j}`) ?? 0) >= 0.97));
|
|
4608
|
+
found.push(await sameNamed(f, name, keep, copies, same2 ? "same" : "near"));
|
|
4054
4609
|
}
|
|
4055
4610
|
const exported = singles.map((i) => ordered[i]).filter((d) => d.exported);
|
|
4056
4611
|
if (!GENERIC_NAMES.has(name) && exported.length >= 2) found.push(await sameNamed(f, name, exported[0], exported.slice(1), "different"));
|
|
@@ -4115,19 +4670,19 @@ function factsOf(project2, id, modules, eligible, workspaceNames, dependencies,
|
|
|
4115
4670
|
if (dirs.has(file.slice(0, at))) break;
|
|
4116
4671
|
dirs.add(file.slice(0, at));
|
|
4117
4672
|
}
|
|
4118
|
-
const roots = [...dirs].filter((dir) => existsSync3(
|
|
4673
|
+
const roots = [...dirs].filter((dir) => existsSync3(join23(project2.root, dir, "package.json"))).sort((a, b) => b.length - a.length);
|
|
4119
4674
|
const nested = (file) => roots.find((dir) => file.startsWith(`${dir}/`)) ?? "";
|
|
4120
4675
|
return { project: project2, id, dependencies, packageImports, jsxCounts, eligible, twins, dirs, importer, packageOf: nested };
|
|
4121
4676
|
}
|
|
4122
4677
|
|
|
4123
4678
|
// ../cli/src/report/projects.ts
|
|
4124
4679
|
var import_yaml3 = __toESM(require_dist(), 1);
|
|
4125
|
-
import { readFile as
|
|
4126
|
-
import { basename as basename4, join as
|
|
4680
|
+
import { readFile as readFile9 } from "node:fs/promises";
|
|
4681
|
+
import { basename as basename4, join as join24, resolve as resolve13 } from "node:path";
|
|
4127
4682
|
var UI_FILES = ["**/*.{tsx,jsx,astro,vue,svelte}"];
|
|
4128
4683
|
var strings = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
|
|
4129
4684
|
async function workspaceGlobs(root, pkg) {
|
|
4130
|
-
const pnpm = await
|
|
4685
|
+
const pnpm = await readFile9(join24(root, "pnpm-workspace.yaml"), "utf8").catch(() => null);
|
|
4131
4686
|
if (pnpm !== null) {
|
|
4132
4687
|
try {
|
|
4133
4688
|
const globs2 = strings((0, import_yaml3.parse)(pnpm)?.packages);
|
|
@@ -4142,7 +4697,7 @@ async function workspaceGlobs(root, pkg) {
|
|
|
4142
4697
|
var hasUi = async (dir) => (await findFiles(dir, UI_FILES, DEFAULT_EXCLUDE)).length > 0;
|
|
4143
4698
|
async function findProjects(dir) {
|
|
4144
4699
|
const root = resolve13(dir);
|
|
4145
|
-
const pkg = await readJson(
|
|
4700
|
+
const pkg = await readJson(join24(root, "package.json"));
|
|
4146
4701
|
const name = typeof pkg?.name === "string" && pkg.name.trim() ? pkg.name.trim() : basename4(root);
|
|
4147
4702
|
const single = { dir: ".", root, name: typeof pkg?.name === "string" ? pkg.name : null };
|
|
4148
4703
|
const workspace = await workspaceGlobs(root, pkg);
|
|
@@ -4153,12 +4708,12 @@ async function findProjects(dir) {
|
|
|
4153
4708
|
const projects = [];
|
|
4154
4709
|
const skipped = [];
|
|
4155
4710
|
for (const member of members) {
|
|
4156
|
-
const memberRoot =
|
|
4711
|
+
const memberRoot = join24(root, member);
|
|
4157
4712
|
if (!await hasUi(memberRoot)) {
|
|
4158
4713
|
skipped.push(member);
|
|
4159
4714
|
continue;
|
|
4160
4715
|
}
|
|
4161
|
-
const memberPkg = await readJson(
|
|
4716
|
+
const memberPkg = await readJson(join24(memberRoot, "package.json"));
|
|
4162
4717
|
projects.push({ dir: member, root: memberRoot, name: typeof memberPkg?.name === "string" ? memberPkg.name : null });
|
|
4163
4718
|
}
|
|
4164
4719
|
if (projects.length === 0) return { root, name, layout: `one project (${workspace.source} names no package with UI files)`, projects: [single], skipped };
|
|
@@ -4185,7 +4740,7 @@ var Lines = class {
|
|
|
4185
4740
|
if (!this.enabled) return { at };
|
|
4186
4741
|
let lines = this.files.get(file);
|
|
4187
4742
|
if (!lines) {
|
|
4188
|
-
lines = (await
|
|
4743
|
+
lines = (await readFile10(join25(this.root, file), "utf8").catch(() => "")).split("\n");
|
|
4189
4744
|
this.files.set(file, lines);
|
|
4190
4745
|
}
|
|
4191
4746
|
const code = lines[line - 1]?.trim();
|
|
@@ -4200,15 +4755,15 @@ function hardcodedValue(finding3) {
|
|
|
4200
4755
|
const value = bracket ?? (declaration ? `${declaration[2]} (${declaration[1]})` : finding3.text);
|
|
4201
4756
|
return { word: word === "type" ? "text size" : word, value, tokens };
|
|
4202
4757
|
}
|
|
4203
|
-
async function modulesOf(project2, target,
|
|
4758
|
+
async function modulesOf(project2, target, failed3) {
|
|
4204
4759
|
const modules = [];
|
|
4205
4760
|
for (const file of target.files) {
|
|
4206
4761
|
if (isShowcase(file)) continue;
|
|
4207
4762
|
try {
|
|
4208
|
-
const module = await parseModule(file, await
|
|
4763
|
+
const module = await parseModule(file, await readFile10(join25(project2.root, file), "utf8"));
|
|
4209
4764
|
if (module) modules.push(module);
|
|
4210
4765
|
} catch (error) {
|
|
4211
|
-
|
|
4766
|
+
failed3.push(`${repoPath(project2.dir, file)}: ${errorText(error)}`);
|
|
4212
4767
|
}
|
|
4213
4768
|
}
|
|
4214
4769
|
return modules;
|
|
@@ -4229,7 +4784,7 @@ async function grouped(project2, found, lines, describe) {
|
|
|
4229
4784
|
return findings2.sort((a, b) => b.count - a.count || a.title.localeCompare(b.title));
|
|
4230
4785
|
}
|
|
4231
4786
|
async function analyseProject(project2, index, inventory, inventoryError, workspaceNames, lines) {
|
|
4232
|
-
const
|
|
4787
|
+
const failed3 = [];
|
|
4233
4788
|
const incomplete = [];
|
|
4234
4789
|
const notes = [];
|
|
4235
4790
|
const projectId = inventory?.projects[index]?.id;
|
|
@@ -4237,7 +4792,7 @@ async function analyseProject(project2, index, inventory, inventoryError, worksp
|
|
|
4237
4792
|
let target = null;
|
|
4238
4793
|
try {
|
|
4239
4794
|
target = await loadInventoryTarget(project2.root);
|
|
4240
|
-
modules = await modulesOf(project2, target,
|
|
4795
|
+
modules = await modulesOf(project2, target, failed3);
|
|
4241
4796
|
} catch (error) {
|
|
4242
4797
|
incomplete.push(`The code could not be read (${errorText(error)})`);
|
|
4243
4798
|
}
|
|
@@ -4280,7 +4835,7 @@ async function analyseProject(project2, index, inventory, inventoryError, worksp
|
|
|
4280
4835
|
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);
|
|
4281
4836
|
const inventoryProject = inventory?.projects[index];
|
|
4282
4837
|
if (inventoryError) incomplete.push(`The component inventory could not be built (${inventoryError}); run ${NPX} inventory --project ${project2.dir} to see why`);
|
|
4283
|
-
for (const f of inventoryProject?.failed ?? [])
|
|
4838
|
+
for (const f of inventoryProject?.failed ?? []) failed3.push(`${repoPath(project2.dir, f.file)}: ${f.reason}`);
|
|
4284
4839
|
let hardcoded = [];
|
|
4285
4840
|
let offLibrary = [];
|
|
4286
4841
|
const everyLine = () => true;
|
|
@@ -4309,7 +4864,7 @@ async function analyseProject(project2, index, inventory, inventoryError, worksp
|
|
|
4309
4864
|
incomplete.push(`Library choices were not compared (${errorText(error)}); run ${NPX} detect ${project2.dir} to see why`);
|
|
4310
4865
|
}
|
|
4311
4866
|
}
|
|
4312
|
-
const pkg = await
|
|
4867
|
+
const pkg = await readFile10(join25(project2.root, "package.json"), "utf8").then((text) => JSON.parse(text), () => null);
|
|
4313
4868
|
const dependencies = new Set(["dependencies", "devDependencies", "peerDependencies"].flatMap((key2) => Object.keys(pkg?.[key2] ?? {})));
|
|
4314
4869
|
const importer = changeProject ? await importOf(changeProject) : () => "";
|
|
4315
4870
|
const facts = factsOf(project2, projectId ?? null, modules, eligible, workspaceNames, dependencies, target?.files ?? [], importer);
|
|
@@ -4317,7 +4872,7 @@ async function analyseProject(project2, index, inventory, inventoryError, worksp
|
|
|
4317
4872
|
dir: project2.dir,
|
|
4318
4873
|
name: project2.name,
|
|
4319
4874
|
files: inventoryProject?.files ?? modules.length,
|
|
4320
|
-
failed:
|
|
4875
|
+
failed: failed3,
|
|
4321
4876
|
libraries,
|
|
4322
4877
|
unused,
|
|
4323
4878
|
definitions: inventory ? inventory.definitions.filter((d) => d.project === projectId && !isShowcase(d.file)).length : 0,
|
|
@@ -4751,9 +5306,9 @@ ${model.projects.map((p) => projectSection(p, many, looks)).join("\n")}
|
|
|
4751
5306
|
|
|
4752
5307
|
// ../cli/src/report/looks.ts
|
|
4753
5308
|
import { spawn as spawn2 } from "node:child_process";
|
|
4754
|
-
import { existsSync as existsSync4, readFileSync as
|
|
4755
|
-
import { homedir as
|
|
4756
|
-
import { dirname as dirname12, join as
|
|
5309
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
|
|
5310
|
+
import { homedir as homedir3 } from "node:os";
|
|
5311
|
+
import { dirname as dirname12, join as join26 } from "node:path";
|
|
4757
5312
|
var LOOKS_PER_PROJECT = 4;
|
|
4758
5313
|
var DOWNLOAD_SIZE = "~100 MB";
|
|
4759
5314
|
var DEPENDENCIES = "Install the repo's dependencies to see rendered looks";
|
|
@@ -4761,31 +5316,31 @@ var UNDRAWABLE = /not installed|loads no stylesheet|drawn from (?:its|their) (?:
|
|
|
4761
5316
|
var playwrightDir = () => dirname12(playwrightCli());
|
|
4762
5317
|
function browsersPath(env) {
|
|
4763
5318
|
const set = env.PLAYWRIGHT_BROWSERS_PATH;
|
|
4764
|
-
if (set === "0") return
|
|
5319
|
+
if (set === "0") return join26(playwrightDir(), ".local-browsers");
|
|
4765
5320
|
if (set) return set;
|
|
4766
|
-
if (process.platform === "darwin") return
|
|
4767
|
-
if (process.platform === "win32") return
|
|
4768
|
-
return
|
|
5321
|
+
if (process.platform === "darwin") return join26(homedir3(), "Library/Caches/ms-playwright");
|
|
5322
|
+
if (process.platform === "win32") return join26(env.LOCALAPPDATA ?? join26(homedir3(), "AppData/Local"), "ms-playwright");
|
|
5323
|
+
return join26(env.XDG_CACHE_HOME ?? join26(homedir3(), ".cache"), "ms-playwright");
|
|
4769
5324
|
}
|
|
4770
5325
|
function chromiumStatus(env = process.env) {
|
|
4771
5326
|
if (env.YOUI_CHROMIUM) return { available: existsSync4(env.YOUI_CHROMIUM), where: env.YOUI_CHROMIUM };
|
|
4772
5327
|
try {
|
|
4773
|
-
const browsers = JSON.parse(
|
|
5328
|
+
const browsers = JSON.parse(readFileSync3(join26(playwrightDir(), "browsers.json"), "utf8"));
|
|
4774
5329
|
const revision2 = browsers.browsers.find((b) => b.name === "chromium-headless-shell")?.revision;
|
|
4775
|
-
const dir =
|
|
4776
|
-
return { available: revision2 !== void 0 && existsSync4(
|
|
5330
|
+
const dir = join26(browsersPath(env), `chromium_headless_shell-${revision2}`);
|
|
5331
|
+
return { available: revision2 !== void 0 && existsSync4(join26(dir, "INSTALLATION_COMPLETE")), where: dir };
|
|
4777
5332
|
} catch {
|
|
4778
5333
|
return { available: false, where: "playwright-core is not installed" };
|
|
4779
5334
|
}
|
|
4780
5335
|
}
|
|
4781
5336
|
function downloadChromium() {
|
|
4782
5337
|
return new Promise((resolve16) => {
|
|
4783
|
-
const child = spawn2(process.execPath, [
|
|
5338
|
+
const child = spawn2(process.execPath, [join26(playwrightDir(), "cli.js"), "install", "chromium-headless-shell"], { stdio: ["ignore", 2, 2] });
|
|
4784
5339
|
child.on("error", () => resolve16(false));
|
|
4785
5340
|
child.on("close", (code) => resolve16(code === 0));
|
|
4786
5341
|
});
|
|
4787
5342
|
}
|
|
4788
|
-
var dataUri = (file) => file && existsSync4(file) ? `data:image/png;base64,${
|
|
5343
|
+
var dataUri = (file) => file && existsSync4(file) ? `data:image/png;base64,${readFileSync3(file).toString("base64")}` : null;
|
|
4789
5344
|
async function addLooks(model, repoRoot, options = {}) {
|
|
4790
5345
|
const drawn = [];
|
|
4791
5346
|
const problems = [];
|
|
@@ -4796,9 +5351,9 @@ async function addLooks(model, repoRoot, options = {}) {
|
|
|
4796
5351
|
const where2 = section.dir === "." ? "this repo" : section.dir;
|
|
4797
5352
|
try {
|
|
4798
5353
|
const installed = (name) => {
|
|
4799
|
-
let dir =
|
|
5354
|
+
let dir = join26(repoRoot, section.dir);
|
|
4800
5355
|
while (true) {
|
|
4801
|
-
if (existsSync4(
|
|
5356
|
+
if (existsSync4(join26(dir, "node_modules", name, "package.json"))) return true;
|
|
4802
5357
|
if (dir === repoRoot || dirname12(dir) === dir) return false;
|
|
4803
5358
|
dir = dirname12(dir);
|
|
4804
5359
|
}
|
|
@@ -4807,13 +5362,13 @@ async function addLooks(model, repoRoot, options = {}) {
|
|
|
4807
5362
|
missing.push(where2);
|
|
4808
5363
|
continue;
|
|
4809
5364
|
}
|
|
4810
|
-
const projectRoot =
|
|
5365
|
+
const projectRoot = join26(repoRoot, section.dir);
|
|
4811
5366
|
const target = await loadInventoryTarget(projectRoot);
|
|
4812
5367
|
if (findStylesheets(projectRoot, target.files, { tsconfig: target.tsconfig, override: target.specimenCss, templates: target.templates }).found.length === 0) {
|
|
4813
5368
|
missing.push(where2);
|
|
4814
5369
|
continue;
|
|
4815
5370
|
}
|
|
4816
|
-
const report = await renderShots(
|
|
5371
|
+
const report = await renderShots(join26(repoRoot, section.dir), names, { download: false, base: "HEAD", ...options.executablePath ? { executablePath: options.executablePath } : {}, ...options.cache ? { cache: options.cache } : {}, ...options.log ? { log: options.log } : {} });
|
|
4817
5372
|
const unavailable = report.notes.some((note) => UNDRAWABLE.test(note));
|
|
4818
5373
|
if (unavailable) {
|
|
4819
5374
|
missing.push(where2);
|
|
@@ -4881,135 +5436,6 @@ function renderMarkdown(model, reportFile) {
|
|
|
4881
5436
|
return lines.join("\n");
|
|
4882
5437
|
}
|
|
4883
5438
|
|
|
4884
|
-
// ../cli/src/report/safe.ts
|
|
4885
|
-
import { realpathSync as realpathSync2 } from "node:fs";
|
|
4886
|
-
import { readFile as readFile9 } from "node:fs/promises";
|
|
4887
|
-
import { homedir as homedir3, userInfo as userInfo2 } from "node:os";
|
|
4888
|
-
import { join as join24 } from "node:path";
|
|
4889
|
-
var REDACTED = "[redacted]";
|
|
4890
|
-
var OUTSIDE = "[path outside the repo]";
|
|
4891
|
-
var MIN_VALUE = 6;
|
|
4892
|
-
var BENIGN_ENV = /^(?:npm_|NPM_|PNPM_|NODE_|NVM_|TERM|COLORTERM|LANG$|LANGUAGE$|LC_|SHELL$|SHLVL$|PWD$|OLDPWD$|_$|PATH$|MANPATH$|INFOPATH$|TMPDIR$|TMP$|TEMP$|EDITOR$|VISUAL$|PAGER$|LESS|LOGNAME$|USER$|USERNAME$|HOME$|XPC_|__CF|COMMAND_MODE$|DISPLAY$|HOSTTYPE$|OSTYPE$|MACHTYPE$|INIT_CWD$|FORCE_COLOR$|NO_COLOR$|CI$|COREPACK_|VSCODE_|ITERM_|TERM_|SECURITYSESSIONID$|LaunchInstanceID$|XDG_|HOMEBREW_|SSH_AUTH_SOCK$|STARSHIP_|ZSH|OMZ|P9K|POWERLEVEL|VITEST|TEST$|MODE$|DEV$|PROD$|SSR$|BASE_URL$)/;
|
|
4893
|
-
var PLAIN_WORD = /^[A-Za-z]+$/;
|
|
4894
|
-
var PLAIN = /^(?:true|false|yes|no|on|off|null|undefined|\d+(?:\.\d+)?)$/i;
|
|
4895
|
-
var escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4896
|
-
async function dotenvValues(root) {
|
|
4897
|
-
const files = await findFiles(root, [".env", ".env.*", "**/.env", "**/.env.*"]).catch(() => []);
|
|
4898
|
-
const values = [];
|
|
4899
|
-
for (const file of files) {
|
|
4900
|
-
const text = await readFile9(join24(root, file), "utf8").catch(() => "");
|
|
4901
|
-
for (const line of text.split(/\r?\n/)) {
|
|
4902
|
-
const match = /^\s*(?:export\s+)?[A-Za-z_][A-Za-z0-9_.-]*\s*=\s*(.*)$/.exec(line);
|
|
4903
|
-
if (!match) continue;
|
|
4904
|
-
const value = match[1].trim().replace(/^(['"`])(.*)\1$/, "$2").replace(/\s+#.*$/, "");
|
|
4905
|
-
if (value.length >= MIN_VALUE) values.push(value);
|
|
4906
|
-
}
|
|
4907
|
-
}
|
|
4908
|
-
return values;
|
|
4909
|
-
}
|
|
4910
|
-
function looksSecret(text) {
|
|
4911
|
-
if (/^(?:sk|pk|rk)_(?:live|test)_|^sk-[A-Za-z0-9]|^gh[pousr]_|^github_pat_|^xox[abposr]-|^AKIA[0-9A-Z]{12}|^AIza[0-9A-Za-z_-]{20}|^glpat-|^eyJ[A-Za-z0-9_-]{10,}\./.test(text)) return true;
|
|
4912
|
-
if (text.length < 20 || /[\s:[\]]/.test(text)) return false;
|
|
4913
|
-
const classes = [/[a-z]/, /[A-Z]/, /\d/].filter((c) => c.test(text)).length;
|
|
4914
|
-
if (classes < 2 || !/\d/.test(text)) return false;
|
|
4915
|
-
const counts = /* @__PURE__ */ new Map();
|
|
4916
|
-
for (const c of text) counts.set(c, (counts.get(c) ?? 0) + 1);
|
|
4917
|
-
const entropy = [...counts.values()].reduce((sum, n) => sum - n / text.length * Math.log2(n / text.length), 0);
|
|
4918
|
-
return entropy >= 3.5;
|
|
4919
|
-
}
|
|
4920
|
-
var Redactor = class _Redactor {
|
|
4921
|
-
roots;
|
|
4922
|
-
home;
|
|
4923
|
-
username;
|
|
4924
|
-
values;
|
|
4925
|
-
includeCode;
|
|
4926
|
-
constructor(options) {
|
|
4927
|
-
const real = (path) => {
|
|
4928
|
-
try {
|
|
4929
|
-
return realpathSync2(path);
|
|
4930
|
-
} catch {
|
|
4931
|
-
return path;
|
|
4932
|
-
}
|
|
4933
|
-
};
|
|
4934
|
-
this.roots = [.../* @__PURE__ */ new Set([options.root, real(options.root)])].sort((a, b) => b.length - a.length);
|
|
4935
|
-
this.home = options.home ?? homedir3();
|
|
4936
|
-
this.username = options.username ?? (() => {
|
|
4937
|
-
try {
|
|
4938
|
-
return userInfo2().username;
|
|
4939
|
-
} catch {
|
|
4940
|
-
return "";
|
|
4941
|
-
}
|
|
4942
|
-
})();
|
|
4943
|
-
const env = Object.entries(options.env ?? process.env).flatMap(([name, value]) => value && !BENIGN_ENV.test(name) && !PLAIN.test(value.trim()) && !(PLAIN_WORD.test(value) && value.length < 12) ? [value] : []);
|
|
4944
|
-
this.values = [...new Set([...env, ...options.dotenv ?? []].filter((v) => v.length >= MIN_VALUE && !this.isRoot(v)))].sort((a, b) => b.length - a.length);
|
|
4945
|
-
this.includeCode = options.includeCode === true;
|
|
4946
|
-
}
|
|
4947
|
-
static async forRepo(root, options = {}) {
|
|
4948
|
-
return new _Redactor({ ...options, root, dotenv: await dotenvValues(root) });
|
|
4949
|
-
}
|
|
4950
|
-
isRoot(value) {
|
|
4951
|
-
return this.roots.some((root) => value === root || root.startsWith(`${value}/`));
|
|
4952
|
-
}
|
|
4953
|
-
/** One string, cleaned. */
|
|
4954
|
-
text(input) {
|
|
4955
|
-
let text = input;
|
|
4956
|
-
for (const root of this.roots) text = text.split(`${root}/`).join("").split(root).join(".");
|
|
4957
|
-
text = text.replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s/@:"'<>]+(?::[^\s/@"'<>]*)?@/gi, `$1${REDACTED}@`);
|
|
4958
|
-
for (const value of this.values) text = text.split(value).join(REDACTED);
|
|
4959
|
-
text = text.replace(/(^|[\s"'`(=,:;])(?:~\/|\/(?:Users|home|root|var|private|tmp|opt|mnt|media|Volumes|srv|etc|usr|workspace|workspaces|builds|runner|github|nix)\b)[^\s"'`<>)]*/g, `$1${OUTSIDE}`);
|
|
4960
|
-
text = text.replace(/(^|[\s"'`(=,:;])[A-Za-z]:\\[^\s"'`<>)]*/g, `$1${OUTSIDE}`);
|
|
4961
|
-
if (this.home.length > 1) text = text.split(this.home).join("~");
|
|
4962
|
-
if (this.username.length >= 3) text = text.replace(new RegExp(`(?<![A-Za-z0-9])${escapeRegExp(this.username)}(?![A-Za-z0-9])`, "g"), REDACTED);
|
|
4963
|
-
return text;
|
|
4964
|
-
}
|
|
4965
|
-
/** A line of code (--include-code): cleaned, and strings that look like keys or tokens blanked. */
|
|
4966
|
-
code(input) {
|
|
4967
|
-
const assigned = /((?:key|token|secret|passw(?:or)?d|pwd|auth|credential|session|cookie|private|dsn|api[_-]?key)[A-Za-z0-9_]*["']?\s*[:=]\s*)(["'`])([^"'`]{4,})\2/gi;
|
|
4968
|
-
const text = input.replace(assigned, `$1$2${REDACTED}$2`).replace(/(["'`])([^"'`\s]{12,})\1/g, (whole, quote, inner) => looksSecret(inner) ? `${quote}${REDACTED}${quote}` : whole).replace(/[A-Za-z0-9_\-+/=.]{20,}/g, (token) => looksSecret(token) ? REDACTED : token);
|
|
4969
|
-
return this.text(text);
|
|
4970
|
-
}
|
|
4971
|
-
/** The whole model, every string cleaned; code only when it was asked for. */
|
|
4972
|
-
model(value) {
|
|
4973
|
-
const walk = (node, key2) => {
|
|
4974
|
-
if (typeof node === "string") return key2 === "code" ? this.code(node) : this.text(node);
|
|
4975
|
-
if (Array.isArray(node)) return node.map((item) => walk(key2 === "looks" && !this.includeCode && typeof item === "string" ? publicLook(item) : item, null));
|
|
4976
|
-
if (node && typeof node === "object") {
|
|
4977
|
-
const out = {};
|
|
4978
|
-
for (const [k, v] of Object.entries(node)) {
|
|
4979
|
-
if (k === "code" && !this.includeCode) continue;
|
|
4980
|
-
out[k] = k === "light" || k === "dark" ? v : walk(v, k);
|
|
4981
|
-
}
|
|
4982
|
-
return out;
|
|
4983
|
-
}
|
|
4984
|
-
return node;
|
|
4985
|
-
};
|
|
4986
|
-
return walk(value, null);
|
|
4987
|
-
}
|
|
4988
|
-
/** What sensitive text is still in `output`: the names of the kinds found, never the values. */
|
|
4989
|
-
leaks(output) {
|
|
4990
|
-
const found = [];
|
|
4991
|
-
if (this.roots.some((root) => output.includes(root))) found.push("an absolute path to the repo");
|
|
4992
|
-
if (this.home.length > 1 && output.includes(this.home)) found.push("the home directory");
|
|
4993
|
-
if (this.username.length >= 3 && new RegExp(`[/\\\\~]${escapeRegExp(this.username)}(?![A-Za-z0-9])`).test(output)) found.push("the username in a path");
|
|
4994
|
-
if (this.values.some((v) => output.includes(v))) found.push("an environment or .env value");
|
|
4995
|
-
if (/\b[a-z][a-z0-9+.-]*:\/\/[^\s/@:"'<>]+:[^\s/@"'<>]+@/i.test(output)) found.push("a URL with credentials");
|
|
4996
|
-
return found;
|
|
4997
|
-
}
|
|
4998
|
-
/** Last pass over a written output: clean it, then refuse it if anything sensitive survived. */
|
|
4999
|
-
scrub(output) {
|
|
5000
|
-
let text = output;
|
|
5001
|
-
if (this.leaks(text).length === 0) return text;
|
|
5002
|
-
for (const root of this.roots) text = text.split(`${root}/`).join("").split(root).join(".");
|
|
5003
|
-
if (this.home.length > 1) text = text.split(this.home).join("~");
|
|
5004
|
-
for (const value of this.values) text = text.split(value).join(REDACTED);
|
|
5005
|
-
text = text.replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s/@:"'<>]+:[^\s/@"'<>]+@/gi, `$1${REDACTED}@`);
|
|
5006
|
-
if (this.username.length >= 3) text = text.replace(new RegExp(`([/\\\\~])${escapeRegExp(this.username)}(?![A-Za-z0-9])`, "g"), `$1${REDACTED}`);
|
|
5007
|
-
const left = this.leaks(text);
|
|
5008
|
-
if (left.length > 0) throw new Error(`the report still held ${left.join(", ")} after redaction, so nothing was written`);
|
|
5009
|
-
return text;
|
|
5010
|
-
}
|
|
5011
|
-
};
|
|
5012
|
-
|
|
5013
5439
|
// ../cli/src/report/run.ts
|
|
5014
5440
|
var REPORT_FILE = "youiyoux-report.html";
|
|
5015
5441
|
var CARD_FILE = "youiyoux-card.png";
|
|
@@ -5070,7 +5496,7 @@ async function runReport(options = {}, io = {}) {
|
|
|
5070
5496
|
const clean = redactor.model(model);
|
|
5071
5497
|
const outDir = resolve14(options.out ?? root);
|
|
5072
5498
|
await mkdir5(outDir, { recursive: true });
|
|
5073
|
-
const files = { report:
|
|
5499
|
+
const files = { report: join27(outDir, REPORT_FILE), card: join27(outDir, CARD_FILE), summary: join27(outDir, SUMMARY_FILE) };
|
|
5074
5500
|
const t = clean.totals;
|
|
5075
5501
|
const card = {
|
|
5076
5502
|
repo: redactor.scrub(clean.repo),
|
|
@@ -5190,14 +5616,27 @@ ${style.bold("In a product repo")}
|
|
|
5190
5616
|
a marked rule in AGENTS.md and CLAUDE.md, and the youiyoux MCP server for
|
|
5191
5617
|
Claude Code (.mcp.json) and Codex (.codex/config.toml); default both.
|
|
5192
5618
|
Idempotent; remove takes out only youi's parts. No youi.project.json needed
|
|
5193
|
-
hooks install [--claude] [--git] [--ci [--diff]] [--strict]
|
|
5619
|
+
hooks install [--claude] [--git] [--ci [--diff]] [--record] [--strict]
|
|
5194
5620
|
live checks, existing config kept (default: --claude --git):
|
|
5195
5621
|
Claude Code PostToolUse (advisory context after each edit: violations
|
|
5196
5622
|
plus the drift the edit adds, each with its fix),
|
|
5197
5623
|
git pre-commit (advisory on staged files and their drift; --strict blocks
|
|
5198
5624
|
new errors and drift; Codex has no post-edit hook, so this and AGENTS.md
|
|
5199
5625
|
cover it), CI workflow (authoritative, pinned rules version, PR
|
|
5200
|
-
annotations; --diff also fails pull requests on drift they add)
|
|
5626
|
+
annotations; --diff also fails pull requests on drift they add);
|
|
5627
|
+
--record: a workflow that sends youiyoux record to the team's hub on each
|
|
5628
|
+
push to the default branch
|
|
5629
|
+
record [--send [--hub url]] [--repo owner/name] [--out file] [--json]
|
|
5630
|
+
this repository's record for the team's hosted hub (youi.record/v1): each
|
|
5631
|
+
product's status, redacted as the shareable report is, no code; --send
|
|
5632
|
+
posts it to YOUIYOUX_HUB_URL, proved by GitHub Actions' OIDC token
|
|
5633
|
+
(id-token: write), else with YOUIYOUX_HUB_TOKEN (environment only)
|
|
5634
|
+
hub serve [--port n] [--host h] [--db file]
|
|
5635
|
+
the team's hosted hub, read-only: CI sends records, people sign in with
|
|
5636
|
+
YOUIYOUX_HUB_VIEW_KEY and see every repository and each System's adoption
|
|
5637
|
+
by version. YOUIYOUX_HUB_GITHUB_OWNERS and YOUIYOUX_HUB_AUDIENCE: GitHub
|
|
5638
|
+
Actions proves each record's repository (else YOUIYOUX_HUB_TOKEN, shared).
|
|
5639
|
+
Records are kept in Postgres (DATABASE_URL) or a SQLite file
|
|
5201
5640
|
|
|
5202
5641
|
${style.bold("System")} ${style.dim("approved elements: build only with these")}
|
|
5203
5642
|
show <component> the contract: import, legal prop values, guidance, examples
|
|
@@ -5342,6 +5781,12 @@ var OPTIONS = {
|
|
|
5342
5781
|
package: { type: "string" },
|
|
5343
5782
|
fork: { type: "boolean", default: false },
|
|
5344
5783
|
to: { type: "string" },
|
|
5784
|
+
repo: { type: "string" },
|
|
5785
|
+
send: { type: "boolean", default: false },
|
|
5786
|
+
hub: { type: "string" },
|
|
5787
|
+
host: { type: "string" },
|
|
5788
|
+
db: { type: "string" },
|
|
5789
|
+
record: { type: "boolean", default: false },
|
|
5345
5790
|
cache: { type: "string" },
|
|
5346
5791
|
role: { type: "string" },
|
|
5347
5792
|
license: { type: "string" },
|
|
@@ -5362,7 +5807,7 @@ var OPTIONS = {
|
|
|
5362
5807
|
var INVENTORY_OPTIONS = { ...OPTIONS, project: { type: "string", multiple: true }, include: { type: "string", multiple: true }, exclude: { type: "string", multiple: true } };
|
|
5363
5808
|
var UsageError = class extends Error {
|
|
5364
5809
|
};
|
|
5365
|
-
var
|
|
5810
|
+
var run7 = promisify6(execFile6);
|
|
5366
5811
|
var print = (text) => void process.stdout.write(`${text}
|
|
5367
5812
|
`);
|
|
5368
5813
|
var json3 = (value) => print(JSON.stringify(value, null, 2));
|
|
@@ -5373,13 +5818,13 @@ async function project(values) {
|
|
|
5373
5818
|
return loadProject(values.project ?? process.cwd(), loadOptions(values));
|
|
5374
5819
|
}
|
|
5375
5820
|
async function readJsonOrFile(value) {
|
|
5376
|
-
const text = value.trim().startsWith("{") ? value : await
|
|
5821
|
+
const text = value.trim().startsWith("{") ? value : await readFile11(value, "utf8");
|
|
5377
5822
|
return JSON.parse(text);
|
|
5378
5823
|
}
|
|
5379
5824
|
async function cmdScan(values) {
|
|
5380
5825
|
const p = await project(values);
|
|
5381
5826
|
const { report } = await scanProject(p);
|
|
5382
|
-
const out = values.out ??
|
|
5827
|
+
const out = values.out ?? join28(p.root, ".youi/usage.json");
|
|
5383
5828
|
await writeFileAtomic(resolve15(out), `${JSON.stringify(report)}
|
|
5384
5829
|
`);
|
|
5385
5830
|
const recorded = await recordUsage(values, p, report, resolve15(out));
|
|
@@ -5406,7 +5851,7 @@ function indexOf(values) {
|
|
|
5406
5851
|
}
|
|
5407
5852
|
async function cmdGaps(values) {
|
|
5408
5853
|
const dirs = inventoryDirs(values);
|
|
5409
|
-
const inventory = values.inventory ? InventoryReport.parse(JSON.parse(await
|
|
5854
|
+
const inventory = values.inventory ? InventoryReport.parse(JSON.parse(await readFile11(resolve15(values.inventory), "utf8"))) : await scanInventory(values, dirs);
|
|
5410
5855
|
const cwd = values.inventory ? dirname13(resolve15(values.inventory)) : process.cwd();
|
|
5411
5856
|
const project2 = await loadProject(resolve15(dirs[0]), loadOptions(values)).catch(() => null);
|
|
5412
5857
|
const system = project2 ? { name: project2.system.config.name, components: project2.system.components.map((c) => c.contract.name) } : null;
|
|
@@ -5426,7 +5871,7 @@ async function cmdInventory(values) {
|
|
|
5426
5871
|
if (values.history) return cmdHistory(values, []);
|
|
5427
5872
|
const dirs = inventoryDirs(values);
|
|
5428
5873
|
const report = await scanInventory(values, dirs);
|
|
5429
|
-
const out = resolve15(values.out ??
|
|
5874
|
+
const out = resolve15(values.out ?? join28(dirs[0], ".youi/inventory.json"));
|
|
5430
5875
|
await writeFileAtomic(out, `${JSON.stringify(report, null, 2)}
|
|
5431
5876
|
`);
|
|
5432
5877
|
const recorded = await recordScan(values, report, out);
|
|
@@ -5456,7 +5901,7 @@ async function cmdIndex(values, positionals) {
|
|
|
5456
5901
|
const [action, ...rest] = positionals;
|
|
5457
5902
|
const { path, home } = indexOf(values);
|
|
5458
5903
|
if (action === "path") {
|
|
5459
|
-
if (values.json) json3({ store: storeLocation({ path }), sources:
|
|
5904
|
+
if (values.json) json3({ store: storeLocation({ path }), sources: join28(home, SOURCES_FILE), scans: join28(home, SCANS_LOG) });
|
|
5460
5905
|
else print(storeLocation({ path }));
|
|
5461
5906
|
return 0;
|
|
5462
5907
|
}
|
|
@@ -5513,7 +5958,7 @@ function baselineFile(values, p) {
|
|
|
5513
5958
|
var checkScope = (values, positionals) => ({ ...positionals.length > 0 ? { files: positionals } : {}, ...values.rule ? { rule: values.rule } : {}, quiet: values.quiet });
|
|
5514
5959
|
async function repoPrefix(root) {
|
|
5515
5960
|
try {
|
|
5516
|
-
return (await
|
|
5961
|
+
return (await run7("git", ["rev-parse", "--show-prefix"], { cwd: root })).stdout.trim();
|
|
5517
5962
|
} catch {
|
|
5518
5963
|
return "";
|
|
5519
5964
|
}
|
|
@@ -5585,7 +6030,7 @@ async function cmdPlan(values, positionals) {
|
|
|
5585
6030
|
async function cmdRules(values) {
|
|
5586
6031
|
const p = await project(values);
|
|
5587
6032
|
if (values.preview) {
|
|
5588
|
-
const draft = (0, import_yaml4.parse)(await
|
|
6033
|
+
const draft = (0, import_yaml4.parse)(await readFile11(values.preview, "utf8"));
|
|
5589
6034
|
const evaluation = evaluateRule(p, await scanProject(p), draft);
|
|
5590
6035
|
if (values.json) json3(evaluation);
|
|
5591
6036
|
else print(renderRulePreview(evaluation));
|
|
@@ -5599,7 +6044,7 @@ async function cmdRules(values) {
|
|
|
5599
6044
|
async function cmdMigrate(values, positionals) {
|
|
5600
6045
|
const file = positionals[0];
|
|
5601
6046
|
if (!file) throw new UsageError("youiyoux migrate <migration.yaml> [--write] [--diff]");
|
|
5602
|
-
const spec = MigrationSpec.parse((0, import_yaml4.parse)(await
|
|
6047
|
+
const spec = MigrationSpec.parse((0, import_yaml4.parse)(await readFile11(file, "utf8")));
|
|
5603
6048
|
const result = await runMigration(await project(values), spec, { write: values.write });
|
|
5604
6049
|
if (values.json) json3(result);
|
|
5605
6050
|
else print(renderMigration(result, values.diff || !values.write));
|
|
@@ -5639,11 +6084,56 @@ async function cmdUpgrade(values) {
|
|
|
5639
6084
|
else print([renderUpgrade(result, values.diff || result.status === "preview"), ...p.linked ? [style.warn(`! linked: the design system is your checkout ${p.linked}, not the config's source (youiyoux unlink to undo)`)] : []].join("\n"));
|
|
5640
6085
|
return result.status === "blocked" ? 1 : 0;
|
|
5641
6086
|
}
|
|
6087
|
+
async function cmdRecord(values) {
|
|
6088
|
+
const hub = values.hub ?? process.env[HUB_URL_ENV];
|
|
6089
|
+
const token = process.env[HUB_TOKEN_ENV] || null;
|
|
6090
|
+
if (values.send && !hub) throw new UsageError(`youiyoux record --send needs the hub's address: set ${HUB_URL_ENV} (or pass --hub <url>)`);
|
|
6091
|
+
const github = values.send ? await requestGithubToken(audienceOf(hub)) : null;
|
|
6092
|
+
if (values.send && !token && !github) throw new UsageError(`youiyoux record --send needs a way to prove where the record comes from: in GitHub Actions, permissions: id-token: write; elsewhere, the hub's ingest token in ${HUB_TOKEN_ENV} (from the environment only, never a flag)`);
|
|
6093
|
+
const record = await buildRecord(values.project ?? process.cwd(), values.repo ? { repo: values.repo } : {});
|
|
6094
|
+
if (values.out) await writeFileAtomic(resolve15(values.out), `${JSON.stringify(record, null, 2)}
|
|
6095
|
+
`);
|
|
6096
|
+
const sentTo = values.send ? await sendRecord(record, hub, { token, github }) : null;
|
|
6097
|
+
if (values.json) json3(record);
|
|
6098
|
+
else print([renderRecord(record, sentTo), ...values.out ? [style.dim(` written to ${values.out}`)] : [], ...!values.send && !values.out ? [style.dim(` youiyoux record --send posts it to the hub in ${HUB_URL_ENV}; --out <file> writes it; --json prints it`)] : []].join("\n"));
|
|
6099
|
+
return 0;
|
|
6100
|
+
}
|
|
6101
|
+
function builtHub() {
|
|
6102
|
+
for (const dir of [join28(TOOL_ROOT, "hub"), join28(TOOL_ROOT, "apps/web/dist")]) if (existsSync5(join28(dir, "index.html"))) return dir;
|
|
6103
|
+
return null;
|
|
6104
|
+
}
|
|
6105
|
+
async function cmdHubServe(values, positionals) {
|
|
6106
|
+
if (positionals[0] !== "serve" || positionals.length > 1) throw new UsageError("youiyoux hub serve [--port n] [--host h] [--db file] (the team's hosted hub: read-only, fed by youiyoux record in CI)");
|
|
6107
|
+
const owners = (process.env[GITHUB_OWNERS_ENV] ?? "").split(",").map((o) => o.trim()).filter(Boolean);
|
|
6108
|
+
const audience = process.env[AUDIENCE_ENV];
|
|
6109
|
+
if (owners.length > 0 && !audience) throw new UsageError(`${GITHUB_OWNERS_ENV} needs ${AUDIENCE_ENV}: the hub's address, exactly as CI's ${HUB_URL_ENV} names it (GitHub's token is issued for it)`);
|
|
6110
|
+
const branches = (process.env.YOUIYOUX_HUB_BRANCHES ?? "").split(",").map((b) => b.trim()).filter(Boolean);
|
|
6111
|
+
const github = owners.length > 0 ? githubVerifier({ owners, audience, branches }) : void 0;
|
|
6112
|
+
const problem = secretsProblem(github ? void 0 : process.env[HUB_TOKEN_ENV], process.env[VIEW_KEY_ENV], Boolean(github));
|
|
6113
|
+
if (problem) throw new UsageError(problem);
|
|
6114
|
+
const port = Number(values.port ?? process.env.PORT ?? 8080);
|
|
6115
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new UsageError(`--port ${JSON.stringify(values.port ?? process.env.PORT)}: expected a port number`);
|
|
6116
|
+
const { openStore, storeLocation: where2 } = await import("./src-624TVB6T.js");
|
|
6117
|
+
const path = resolve15(values.db ?? process.env.YOUI_HUB_DB ?? join28(process.env.XDG_DATA_HOME || join28(homedir4(), ".local/share"), "youiyoux/hub.db"));
|
|
6118
|
+
const store = await openStore({ path });
|
|
6119
|
+
const dist = builtHub();
|
|
6120
|
+
const hub = await startOrgHub({ store, ...github ? { github } : { ingestToken: process.env[HUB_TOKEN_ENV] }, viewKey: process.env[VIEW_KEY_ENV], dist, host: values.host ?? "127.0.0.1", port });
|
|
6121
|
+
const senders = github ? `CI sends records from GitHub Actions in ${owners.join(", ")}, on ${(branches.length > 0 ? branches : DEFAULT_BRANCHES).join(" or ")}, each proving its repository (OIDC, for ${audienceOf(audience)})${process.env[HUB_TOKEN_ENV] ? `; ${HUB_TOKEN_ENV} is not used` : ""}` : `CI sends records with ${HUB_TOKEN_ENV}, which can name any repository (${GITHUB_OWNERS_ENV} makes GitHub Actions prove each one's)`;
|
|
6122
|
+
print([`${style.ok("youiyoux hub")} ${hub.url} ${style.dim(`read-only \xB7 records kept in ${where2({ path })}`)}`, ...dist ? [] : [style.warn("! the hub's page is not built here (pnpm --filter web build): the API answers at /api/org")], style.dim(` ${senders}; people sign in with ${VIEW_KEY_ENV}. Put it behind https; --host 0.0.0.0 to listen beyond this machine`)].join("\n"));
|
|
6123
|
+
await new Promise((done) => {
|
|
6124
|
+
const stop = () => done();
|
|
6125
|
+
process.once("SIGINT", stop);
|
|
6126
|
+
process.once("SIGTERM", stop);
|
|
6127
|
+
});
|
|
6128
|
+
await hub.close();
|
|
6129
|
+
await store.close();
|
|
6130
|
+
return 0;
|
|
6131
|
+
}
|
|
5642
6132
|
async function cmdInit(values) {
|
|
5643
6133
|
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)");
|
|
5644
6134
|
const root = values.project ?? process.cwd();
|
|
5645
6135
|
const design = await designArg(values.design, root);
|
|
5646
|
-
const result = await initProject({ root, ...design, schemaDir:
|
|
6136
|
+
const result = await initProject({ root, ...design, schemaDir: join28(TOOL_ROOT, "schema"), ...values.system ? { system: values.system } : {}, ...values.components ? { componentsDir: values.components } : {}, force: values.force });
|
|
5647
6137
|
if (!values.json) {
|
|
5648
6138
|
print(`${style.ok("created")} ${result.file}`);
|
|
5649
6139
|
for (const note of result.notes) print(style.dim(` \xB7 ${note}`));
|
|
@@ -5654,27 +6144,27 @@ async function cmdInit(values) {
|
|
|
5654
6144
|
}
|
|
5655
6145
|
async function cmdAdopt(values) {
|
|
5656
6146
|
const root = resolve15(values.project ?? process.cwd());
|
|
5657
|
-
if (await exists(
|
|
6147
|
+
if (await exists(join28(root, PROJECT_FILE))) throw new Error(`${root} already has ${PROJECT_FILE}: it follows a design system already (youiyoux sync updates it)`);
|
|
5658
6148
|
const plan = await planAdoption({ root, ...values.components ? { componentsDir: values.components } : {}, ...values.css ? { css: values.css } : {}, ...values.system ? { system: values.system } : {}, fork: values.fork });
|
|
5659
|
-
const taken = (await Promise.all(plan.files.map(async (f) => await exists(
|
|
6149
|
+
const taken = (await Promise.all(plan.files.map(async (f) => await exists(join28(root, f.path)) ? f.path : null))).filter((f) => f !== null);
|
|
5660
6150
|
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" : ""}`);
|
|
5661
6151
|
if (values["dry-run"]) {
|
|
5662
6152
|
if (values.json) json3(plan);
|
|
5663
6153
|
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"));
|
|
5664
6154
|
return 0;
|
|
5665
6155
|
}
|
|
5666
|
-
const stylesheet = plan.stylesheet ? await
|
|
6156
|
+
const stylesheet = plan.stylesheet ? await readFile11(join28(root, plan.stylesheet.path), "utf8") : null;
|
|
5667
6157
|
try {
|
|
5668
|
-
for (const file of plan.files) await writeFileAtomic(
|
|
5669
|
-
if (plan.stylesheet) await writeFileAtomic(
|
|
5670
|
-
await initProject({ root, design: root, schemaDir:
|
|
6158
|
+
for (const file of plan.files) await writeFileAtomic(join28(root, file.path), file.content);
|
|
6159
|
+
if (plan.stylesheet) await writeFileAtomic(join28(root, plan.stylesheet.path), plan.stylesheet.content);
|
|
6160
|
+
await initProject({ root, design: root, schemaDir: join28(TOOL_ROOT, "schema"), system: plan.system.id, componentsDir: plan.componentsDir, outputs: { ...plan.outputs, targets: [...plan.outputs.targets], dark: [...plan.outputs.dark] }, tokenSources: plan.tokenSources });
|
|
5671
6161
|
await syncProject(await loadProject(root), { dryRun: true });
|
|
5672
6162
|
} catch (error) {
|
|
5673
|
-
for (const file of [...plan.files.map((f) => f.path), PROJECT_FILE]) await rm7(
|
|
5674
|
-
for (const folder of [...new Set(plan.files.map((f) => dirname13(f.path)))].sort((a, b) => b.length - a.length)) await rmdir3(
|
|
5675
|
-
await rmdir3(
|
|
5676
|
-
await rmdir3(
|
|
5677
|
-
if (plan.stylesheet && stylesheet !== null) await writeFileAtomic(
|
|
6163
|
+
for (const file of [...plan.files.map((f) => f.path), PROJECT_FILE]) await rm7(join28(root, file), { force: true });
|
|
6164
|
+
for (const folder of [...new Set(plan.files.map((f) => dirname13(f.path)))].sort((a, b) => b.length - a.length)) await rmdir3(join28(root, folder)).catch(() => void 0);
|
|
6165
|
+
await rmdir3(join28(root, "design/components")).catch(() => void 0);
|
|
6166
|
+
await rmdir3(join28(root, "design")).catch(() => void 0);
|
|
6167
|
+
if (plan.stylesheet && stylesheet !== null) await writeFileAtomic(join28(root, plan.stylesheet.path), stylesheet);
|
|
5678
6168
|
throw new Error(`adopt was undone, nothing kept: ${error instanceof Error ? error.message : String(error)}`);
|
|
5679
6169
|
}
|
|
5680
6170
|
if (!values.json) print([...adoptLines(plan), ""].join("\n"));
|
|
@@ -5724,8 +6214,8 @@ async function cmdSystem(values, positionals) {
|
|
|
5724
6214
|
}
|
|
5725
6215
|
print([...lines, ""].join("\n"));
|
|
5726
6216
|
const synced = await cmdSync({ ...values, project: p.root, package: void 0 });
|
|
5727
|
-
const
|
|
5728
|
-
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 ${
|
|
6217
|
+
const git4 = `push it and set source to { "git": "<its url>", "ref": "v${result.system.version}" }`;
|
|
6218
|
+
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 ${git4}` : `${git4}, or publish it as an npm package (--package names it)`}. Change components there from now on: youiyoux sync merges them into each product`);
|
|
5729
6219
|
return synced;
|
|
5730
6220
|
}
|
|
5731
6221
|
async function cmdLink(values, positionals) {
|
|
@@ -5748,7 +6238,7 @@ async function cmdUnlink(values) {
|
|
|
5748
6238
|
}
|
|
5749
6239
|
async function designArg(value, root) {
|
|
5750
6240
|
if (existsSync5(resolve15(value)) || !PACKAGE_NAME.test(value)) return { design: value };
|
|
5751
|
-
return { design: await designRootOf({ package: value },
|
|
6241
|
+
return { design: await designRootOf({ package: value }, join28(resolve15(root), PROJECT_FILE), root), package: value };
|
|
5752
6242
|
}
|
|
5753
6243
|
async function cmdBuild(values) {
|
|
5754
6244
|
const root = resolve15(values.design ?? process.cwd());
|
|
@@ -5757,10 +6247,10 @@ async function cmdBuild(values) {
|
|
|
5757
6247
|
const output = OutputConfig.parse({ cssVarPrefix: values.prefix ?? "youi", targets: ["css-vars", "tailwind-v4", "ts", "react-native"] });
|
|
5758
6248
|
const started = performance.now();
|
|
5759
6249
|
const built = await buildTokens(root, system.config, output, { contracts: system.components.map((c) => c.contract) });
|
|
5760
|
-
const out = resolve15(values.out ??
|
|
5761
|
-
const boundary = values.out ? out :
|
|
5762
|
-
if (!within(boundary, out) || built.files.some((file) => !within(out,
|
|
5763
|
-
for (const file of built.files) await writeFileAtomic(
|
|
6250
|
+
const out = resolve15(values.out ?? join28(root, "build", system.config.id));
|
|
6251
|
+
const boundary = values.out ? out : join28(root, "build");
|
|
6252
|
+
if (!within(boundary, out) || built.files.some((file) => !within(out, join28(out, file.path)))) throw new Error(`Cannot build system ${JSON.stringify(system.config.id)}: an output path leaves the output directory.`);
|
|
6253
|
+
for (const file of built.files) await writeFileAtomic(join28(out, file.path), file.content);
|
|
5764
6254
|
if (values.json) json3({ out, files: built.files.map((f) => f.path), tokens: built.tokens.length });
|
|
5765
6255
|
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)})`)}`);
|
|
5766
6256
|
return 0;
|
|
@@ -5772,7 +6262,7 @@ async function cmdParity(values) {
|
|
|
5772
6262
|
const prefix = values.prefix ?? "youi";
|
|
5773
6263
|
const dark = { selector: values.dark ?? ':root[data-theme="dark"]' };
|
|
5774
6264
|
const built = await buildTokens(root, system.config, OutputConfig.parse({ cssVarPrefix: prefix, dark: [dark], targets: ["css-vars"] }));
|
|
5775
|
-
const report = compareParity(await
|
|
6265
|
+
const report = compareParity(await readFile11(values.css, "utf8"), { light: { selector: values.light ?? ":root" }, dark }, built.files[0].content, { light: { selector: ":root" }, dark }, prefix);
|
|
5776
6266
|
if (values.json) json3(report);
|
|
5777
6267
|
else {
|
|
5778
6268
|
const ok = report.different.length === 0 && report.missing.length === 0;
|
|
@@ -5785,16 +6275,16 @@ async function cmdParity(values) {
|
|
|
5785
6275
|
async function cmdSchema(values) {
|
|
5786
6276
|
const schemas = renderJsonSchemas();
|
|
5787
6277
|
if (values.check) {
|
|
5788
|
-
const dir =
|
|
6278
|
+
const dir = join28(values.design ? resolve15(values.design) : TOOL_ROOT, "schema");
|
|
5789
6279
|
const stale = [];
|
|
5790
|
-
for (const { file, content } of schemas) if (await
|
|
6280
|
+
for (const { file, content } of schemas) if (await readFile11(join28(dir, file), "utf8").catch(() => null) !== content) stale.push(file);
|
|
5791
6281
|
if (values.json) json3({ dir, stale });
|
|
5792
6282
|
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>" : ""}`);
|
|
5793
6283
|
return stale.length === 0 ? 0 : 1;
|
|
5794
6284
|
}
|
|
5795
6285
|
if (values.write) {
|
|
5796
6286
|
const root = values.design ? resolve15(values.design) : TOOL_ROOT;
|
|
5797
|
-
for (const { file, content } of schemas) await writeFileAtomic(
|
|
6287
|
+
for (const { file, content } of schemas) await writeFileAtomic(join28(root, "schema", file), content);
|
|
5798
6288
|
print(`${style.ok("wrote")} ${schemas.map((s) => s.file).join(", ")}`);
|
|
5799
6289
|
} else json3(Object.fromEntries(schemas.map((s) => [s.file, JSON.parse(s.content)])));
|
|
5800
6290
|
return 0;
|
|
@@ -5814,16 +6304,24 @@ async function cmdHooks(values, positionals) {
|
|
|
5814
6304
|
process.stdout.write(outcome.stdout);
|
|
5815
6305
|
return outcome.exitCode;
|
|
5816
6306
|
}
|
|
5817
|
-
if (action !== "install") throw new UsageError("youiyoux hooks install [--claude] [--git] [--ci [--diff]] [--strict] or youiyoux hooks run claude|pre-commit");
|
|
6307
|
+
if (action !== "install") throw new UsageError("youiyoux hooks install [--claude] [--git] [--ci [--diff]] [--record] [--strict] or youiyoux hooks run claude|pre-commit");
|
|
6308
|
+
if (values.record && !values.claude && !values.git && !values.ci) {
|
|
6309
|
+
const result = { kind: "record", ...await installRecordWorkflow(values.project ?? process.cwd(), toolVersion(), values.force) };
|
|
6310
|
+
if (values.json) json3([result]);
|
|
6311
|
+
else print(`${result.action === "unchanged" ? style.dim(result.action) : style.ok(result.action)} ${result.kind}: ${result.file}
|
|
6312
|
+
${style.dim(result.guarantee)}`);
|
|
6313
|
+
return 0;
|
|
6314
|
+
}
|
|
5818
6315
|
const p = await loadCheckProject(values.project ?? process.cwd(), loadOptions(values), !values.project);
|
|
5819
6316
|
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");
|
|
5820
|
-
const target = { ...hookTarget(values), project: p.root, ...configured(p) && p.configFile && p.configFile !==
|
|
6317
|
+
const target = { ...hookTarget(values), project: p.root, ...configured(p) && p.configFile && p.configFile !== join28(p.root, PROJECT_FILE) ? { config: p.configFile } : {}, ...values.root !== void 0 ? { root: p.root } : {} };
|
|
5821
6318
|
const all = !values.claude && !values.git && !values.ci;
|
|
5822
6319
|
const results = [];
|
|
5823
6320
|
if (values.claude || all) results.push({ kind: "Claude Code", ...await installClaudeHook(await repoRootOf(p.root), hookCommand(TOOL_ROOT, "claude", target)) });
|
|
5824
6321
|
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) });
|
|
5825
6322
|
if (values.git || all) results.push({ kind: "Codex", ...configured(p) ? codexFallback(p.root, p.config.agents.agentsMd) : await installRepoAgentInstructions(p.root) });
|
|
5826
6323
|
if (values.ci && configured(p)) results.push({ kind: "CI", ...await installWorkflow(p, TOOL_ROOT, target, values.force, { diff: values.diff }) });
|
|
6324
|
+
if (values.record) results.push({ kind: "record", ...await installRecordWorkflow(p.root, toolVersion(), values.force) });
|
|
5827
6325
|
if (values.json) json3(results);
|
|
5828
6326
|
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}
|
|
5829
6327
|
${style.dim(r.guarantee)}`);
|
|
@@ -5836,7 +6334,7 @@ async function cmdAgents(values, positionals) {
|
|
|
5836
6334
|
const repoRoot = await repoRootOf(projectRoot);
|
|
5837
6335
|
const target = { project: projectRoot, ...values.config ? { config: resolve15(values.config) } : {}, ...values.design ? { design: resolve15(values.design) } : {}, ...values.root !== void 0 ? { root: resolve15(values.root || ".") } : {} };
|
|
5838
6336
|
const server = mcpCommand(TOOL_ROOT, target);
|
|
5839
|
-
const system = existsSync5(resolve15(values.config ??
|
|
6337
|
+
const system = existsSync5(resolve15(values.config ?? join28(projectRoot, PROJECT_FILE))) ? await project(values) : null;
|
|
5840
6338
|
const claudeServer = system && system.root === repoRoot ? { command: "node", args: mcpArgs(system) } : relativeTo(repoRoot, server);
|
|
5841
6339
|
const both = !values.claude && !values.codex;
|
|
5842
6340
|
const results = [];
|
|
@@ -5944,7 +6442,7 @@ async function cmdUi(values) {
|
|
|
5944
6442
|
async function cmdDetect(values, positionals) {
|
|
5945
6443
|
const target = detectTarget(positionals);
|
|
5946
6444
|
if (!target) throw new UsageError("youiyoux detect <repo-path> or youiyoux detect <url\u2026> [--expose] (one repo, or one or more URLs; not both)");
|
|
5947
|
-
const { repoReport, siteReport, writeReport } = await import("./src-
|
|
6445
|
+
const { repoReport, siteReport, writeReport } = await import("./src-PBM76LFQ.js");
|
|
5948
6446
|
const out = resolve15(values.out ?? ".youi/detect");
|
|
5949
6447
|
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 } : {} });
|
|
5950
6448
|
const file = await writeReport(report, out);
|
|
@@ -5978,7 +6476,7 @@ async function cmdRequests(values, positionals) {
|
|
|
5978
6476
|
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>";
|
|
5979
6477
|
const [action, id, ...rest] = positionals;
|
|
5980
6478
|
if (rest.length > 0) throw new UsageError(usage);
|
|
5981
|
-
const { RequestStore, requestBrief } = await import("./src-
|
|
6479
|
+
const { RequestStore, requestBrief } = await import("./src-R7JGKOYI.js");
|
|
5982
6480
|
const { project: project2, root } = await product(values);
|
|
5983
6481
|
const store = new RequestStore(root, project2?.config.id ?? basename5(root));
|
|
5984
6482
|
const agent = { kind: "agent", name: "youiyoux CLI" };
|
|
@@ -6011,8 +6509,8 @@ async function cmdScreens(values, positionals, io) {
|
|
|
6011
6509
|
if (!["capture", "login"].includes(positionals[0] ?? "") || positionals.length > 1 || !values.base) throw new UsageError(usage);
|
|
6012
6510
|
const { root } = await product(values);
|
|
6013
6511
|
if (positionals[0] === "login") return screensLogin(values.base, values["storage-state"] ? resolve15(values["storage-state"]) : sessionFile(root), io);
|
|
6014
|
-
const dir = resolve15(values.out ??
|
|
6015
|
-
if (!existsSync5(
|
|
6512
|
+
const dir = resolve15(values.out ?? join28(root, ".youi/screens"));
|
|
6513
|
+
if (!existsSync5(join28(dir, "screens.json"))) {
|
|
6016
6514
|
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":"/"}]}
|
|
6017
6515
|
`);
|
|
6018
6516
|
return 1;
|
|
@@ -6020,7 +6518,7 @@ async function cmdScreens(values, positionals, io) {
|
|
|
6020
6518
|
const session = chosenSession(root, values["storage-state"] ? resolve15(values["storage-state"]) : void 0);
|
|
6021
6519
|
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`)}
|
|
6022
6520
|
`);
|
|
6023
|
-
const { launchBrowser } = await import("./src-
|
|
6521
|
+
const { launchBrowser } = await import("./src-PBM76LFQ.js");
|
|
6024
6522
|
const browser = await launchBrowser(process.env.YOUI_CHROMIUM, { purpose: { what: "youiyoux screens capture", command: "youiyoux screens capture" } });
|
|
6025
6523
|
try {
|
|
6026
6524
|
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)}
|
|
@@ -6077,7 +6575,7 @@ async function cmdReport(values, positionals, io) {
|
|
|
6077
6575
|
return result.code;
|
|
6078
6576
|
}
|
|
6079
6577
|
async function cmdMcp(values) {
|
|
6080
|
-
const { startStdioServer } = await import("./src-
|
|
6578
|
+
const { startStdioServer } = await import("./src-HVYMF4S2.js");
|
|
6081
6579
|
await startStdioServer({ projectRoot: values.project ?? process.cwd(), ...loadOptions(values), ...values.cache ? { discoverCache: values.cache } : {} });
|
|
6082
6580
|
return -1;
|
|
6083
6581
|
}
|
|
@@ -6153,6 +6651,8 @@ var COMMANDS = {
|
|
|
6153
6651
|
plan: cmdPlan,
|
|
6154
6652
|
migrate: cmdMigrate,
|
|
6155
6653
|
upgrade: cmdUpgrade,
|
|
6654
|
+
record: cmdRecord,
|
|
6655
|
+
hub: cmdHubServe,
|
|
6156
6656
|
init: cmdInit,
|
|
6157
6657
|
sync: cmdSync,
|
|
6158
6658
|
scan: cmdScan,
|