uidex 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/cli.cjs +1024 -1041
- package/dist/cli/cli.cjs.map +1 -1
- package/dist/headless/index.cjs +4 -448
- package/dist/headless/index.cjs.map +1 -1
- package/dist/headless/index.d.cts +41 -11
- package/dist/headless/index.d.ts +41 -11
- package/dist/headless/index.js +4 -450
- package/dist/headless/index.js.map +1 -1
- package/dist/index.cjs +147 -3252
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +43 -316
- package/dist/index.d.ts +43 -316
- package/dist/index.js +133 -3254
- package/dist/index.js.map +1 -1
- package/dist/playwright/index.cjs +175 -0
- package/dist/playwright/index.cjs.map +1 -1
- package/dist/playwright/index.d.cts +2 -0
- package/dist/playwright/index.d.ts +2 -0
- package/dist/playwright/index.js +167 -0
- package/dist/playwright/index.js.map +1 -1
- package/dist/playwright/states-reporter.cjs +123 -0
- package/dist/playwright/states-reporter.cjs.map +1 -0
- package/dist/playwright/states-reporter.d.cts +46 -0
- package/dist/playwright/states-reporter.d.ts +46 -0
- package/dist/playwright/states-reporter.js +88 -0
- package/dist/playwright/states-reporter.js.map +1 -0
- package/dist/playwright/states.cjs +118 -0
- package/dist/playwright/states.cjs.map +1 -0
- package/dist/playwright/states.d.cts +120 -0
- package/dist/playwright/states.d.ts +120 -0
- package/dist/playwright/states.js +88 -0
- package/dist/playwright/states.js.map +1 -0
- package/dist/react/index.cjs +163 -3255
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +62 -275
- package/dist/react/index.d.ts +62 -275
- package/dist/react/index.js +151 -3268
- package/dist/react/index.js.map +1 -1
- package/dist/scan/index.cjs +1194 -322
- package/dist/scan/index.cjs.map +1 -1
- package/dist/scan/index.d.cts +274 -8
- package/dist/scan/index.d.ts +274 -8
- package/dist/scan/index.js +1185 -322
- package/dist/scan/index.js.map +1 -1
- package/package.json +27 -31
- package/dist/cloud/index.cjs +0 -682
- package/dist/cloud/index.cjs.map +0 -1
- package/dist/cloud/index.d.cts +0 -270
- package/dist/cloud/index.d.ts +0 -270
- package/dist/cloud/index.js +0 -645
- package/dist/cloud/index.js.map +0 -1
package/dist/cli/cli.cjs
CHANGED
|
@@ -181,14 +181,14 @@ ${helpText()}`);
|
|
|
181
181
|
const provider = await selectProvider(opts, flags.provider);
|
|
182
182
|
if (!provider) return out(0, "Cancelled.\n");
|
|
183
183
|
if (sub === "install") {
|
|
184
|
-
const
|
|
184
|
+
const result2 = await provider.install({
|
|
185
185
|
cwd,
|
|
186
186
|
force: flags.force === true
|
|
187
187
|
});
|
|
188
|
-
return out(0, formatChanges(provider, "Installed",
|
|
188
|
+
return out(0, formatChanges(provider, "Installed", result2));
|
|
189
189
|
}
|
|
190
|
-
const
|
|
191
|
-
return out(0, formatChanges(provider, "Uninstalled",
|
|
190
|
+
const result = await provider.uninstall({ cwd });
|
|
191
|
+
return out(0, formatChanges(provider, "Uninstalled", result));
|
|
192
192
|
}
|
|
193
193
|
async function selectProvider(opts, explicit) {
|
|
194
194
|
if (explicit) {
|
|
@@ -231,9 +231,9 @@ function parseFlags(args) {
|
|
|
231
231
|
}
|
|
232
232
|
return flags;
|
|
233
233
|
}
|
|
234
|
-
function formatChanges(provider, verb,
|
|
234
|
+
function formatChanges(provider, verb, result) {
|
|
235
235
|
const lines = [`${verb} ${provider.label}:`];
|
|
236
|
-
for (const c of
|
|
236
|
+
for (const c of result.changes) lines.push(` ${describe(c)}`);
|
|
237
237
|
return lines.join("\n") + "\n";
|
|
238
238
|
}
|
|
239
239
|
function describe(c) {
|
|
@@ -300,7 +300,9 @@ var ALLOWED_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
|
300
300
|
"output",
|
|
301
301
|
"flows",
|
|
302
302
|
"audit",
|
|
303
|
-
"conventions"
|
|
303
|
+
"conventions",
|
|
304
|
+
"statesManifest",
|
|
305
|
+
"coverageBaseline"
|
|
304
306
|
]);
|
|
305
307
|
var ALLOWED_SOURCE_KEYS = /* @__PURE__ */ new Set(["rootDir", "include", "exclude", "prefix"]);
|
|
306
308
|
var ALLOWED_CONVENTIONS_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -310,7 +312,14 @@ var ALLOWED_CONVENTIONS_KEYS = /* @__PURE__ */ new Set([
|
|
|
310
312
|
"flows",
|
|
311
313
|
"regions"
|
|
312
314
|
]);
|
|
313
|
-
var ALLOWED_AUDIT_KEYS = /* @__PURE__ */ new Set([
|
|
315
|
+
var ALLOWED_AUDIT_KEYS = /* @__PURE__ */ new Set([
|
|
316
|
+
"scopeLeak",
|
|
317
|
+
"coverage",
|
|
318
|
+
"acceptance",
|
|
319
|
+
"states",
|
|
320
|
+
"pageCoverage",
|
|
321
|
+
"stateMatrix"
|
|
322
|
+
]);
|
|
314
323
|
function fail(msg) {
|
|
315
324
|
throw new ConfigError(`Invalid .uidex.json: ${msg}`);
|
|
316
325
|
}
|
|
@@ -369,6 +378,12 @@ function validateConfig(raw) {
|
|
|
369
378
|
}
|
|
370
379
|
if (raw.exclude !== void 0) assertStringArray(raw.exclude, `exclude`);
|
|
371
380
|
if (raw.flows !== void 0) assertStringArray(raw.flows, `flows`);
|
|
381
|
+
if (raw.statesManifest !== void 0 && typeof raw.statesManifest !== "string") {
|
|
382
|
+
fail(`"statesManifest" must be a string`);
|
|
383
|
+
}
|
|
384
|
+
if (raw.coverageBaseline !== void 0 && typeof raw.coverageBaseline !== "string") {
|
|
385
|
+
fail(`"coverageBaseline" must be a string`);
|
|
386
|
+
}
|
|
372
387
|
if (raw.audit !== void 0) {
|
|
373
388
|
assertObject(raw.audit, "audit");
|
|
374
389
|
for (const key of Object.keys(raw.audit)) {
|
|
@@ -408,7 +423,9 @@ function validateConfig(raw) {
|
|
|
408
423
|
output: raw.output,
|
|
409
424
|
flows: raw.flows,
|
|
410
425
|
audit: raw.audit,
|
|
411
|
-
conventions: raw.conventions
|
|
426
|
+
conventions: raw.conventions,
|
|
427
|
+
statesManifest: raw.statesManifest,
|
|
428
|
+
coverageBaseline: raw.coverageBaseline
|
|
412
429
|
};
|
|
413
430
|
}
|
|
414
431
|
function parseConfig(json) {
|
|
@@ -598,6 +615,12 @@ var ENTITY_KINDS = [
|
|
|
598
615
|
"primitive",
|
|
599
616
|
"flow"
|
|
600
617
|
];
|
|
618
|
+
var CORE_STATE_KINDS = [
|
|
619
|
+
"loading",
|
|
620
|
+
"empty",
|
|
621
|
+
"populated",
|
|
622
|
+
"error"
|
|
623
|
+
];
|
|
601
624
|
function isMetaKind(kind) {
|
|
602
625
|
return kind !== "route" && kind !== "flow";
|
|
603
626
|
}
|
|
@@ -731,11 +754,11 @@ function createRegistry() {
|
|
|
731
754
|
};
|
|
732
755
|
const query = (predicate) => {
|
|
733
756
|
const flows = getFlows();
|
|
734
|
-
const
|
|
757
|
+
const result = [];
|
|
735
758
|
for (const entity of allEntities()) {
|
|
736
|
-
if (predicate(entity))
|
|
759
|
+
if (predicate(entity)) result.push(freezeEntity(entity, flows));
|
|
737
760
|
}
|
|
738
|
-
return
|
|
761
|
+
return result;
|
|
739
762
|
};
|
|
740
763
|
const byScope = (scope) => query(
|
|
741
764
|
(entity) => "scopes" in entity && Array.isArray(entity.scopes) && entity.scopes.includes(scope)
|
|
@@ -776,6 +799,295 @@ function createRegistry() {
|
|
|
776
799
|
};
|
|
777
800
|
}
|
|
778
801
|
|
|
802
|
+
// src/scanner/scan/page-coverage.ts
|
|
803
|
+
function compileRoute(pattern) {
|
|
804
|
+
const segs = pattern.split("/").filter(Boolean);
|
|
805
|
+
let specificity = 0;
|
|
806
|
+
const optionalTail = segs.length > 0 && /^\[\[\.{3}.+\]\]$/.test(segs[segs.length - 1]);
|
|
807
|
+
const effective = optionalTail ? segs.slice(0, -1) : segs;
|
|
808
|
+
const parts = effective.map((seg) => {
|
|
809
|
+
const catchAll = /^\[\.{3}.+\]$/.test(seg);
|
|
810
|
+
const dynamic = /^\[.+\]$/.test(seg);
|
|
811
|
+
if (catchAll) return "(?:[^/]+)(?:/[^/]+)*";
|
|
812
|
+
if (dynamic) return "[^/]+";
|
|
813
|
+
specificity++;
|
|
814
|
+
return seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
815
|
+
});
|
|
816
|
+
let body = parts.length === 0 ? "" : "/" + parts.join("/");
|
|
817
|
+
if (optionalTail) body += "(?:/[^/]+)*";
|
|
818
|
+
if (body === "") body = "/";
|
|
819
|
+
const re = new RegExp(`^${body}/?$`);
|
|
820
|
+
return { test: (url) => re.test(url), specificity };
|
|
821
|
+
}
|
|
822
|
+
function matchUrlToRoute(url, routePaths) {
|
|
823
|
+
const path12 = stripQuery(url);
|
|
824
|
+
let best = null;
|
|
825
|
+
for (const rp of routePaths) {
|
|
826
|
+
const { test, specificity } = compileRoute(rp);
|
|
827
|
+
if (!test(path12)) continue;
|
|
828
|
+
if (!best || specificity > best.specificity)
|
|
829
|
+
best = { path: rp, specificity };
|
|
830
|
+
}
|
|
831
|
+
return best?.path ?? null;
|
|
832
|
+
}
|
|
833
|
+
function stripQuery(url) {
|
|
834
|
+
const q = url.indexOf("?");
|
|
835
|
+
const h = url.indexOf("#");
|
|
836
|
+
let end = url.length;
|
|
837
|
+
if (q !== -1) end = Math.min(end, q);
|
|
838
|
+
if (h !== -1) end = Math.min(end, h);
|
|
839
|
+
return url.slice(0, end);
|
|
840
|
+
}
|
|
841
|
+
function routeForCapture(c, routes, routePaths) {
|
|
842
|
+
if (c.route && routePaths.includes(c.route)) return c.route;
|
|
843
|
+
if (c.url) {
|
|
844
|
+
const m = matchUrlToRoute(c.url, routePaths);
|
|
845
|
+
if (m) return m;
|
|
846
|
+
}
|
|
847
|
+
if (c.kind === "page") {
|
|
848
|
+
const r = routes.find((r2) => r2.page === c.entity);
|
|
849
|
+
if (r) return r.path;
|
|
850
|
+
}
|
|
851
|
+
return null;
|
|
852
|
+
}
|
|
853
|
+
function checkPageCoverage(registry, manifest, baseline) {
|
|
854
|
+
const routes = registry.list("route");
|
|
855
|
+
if (routes.length === 0) return [];
|
|
856
|
+
const routePaths = routes.map((r) => r.path);
|
|
857
|
+
const captured = manifest.captured ?? [];
|
|
858
|
+
const optedOut = /* @__PURE__ */ new Set();
|
|
859
|
+
for (const r of routes) {
|
|
860
|
+
const page = registry.get("page", r.page);
|
|
861
|
+
if (page?.meta?.capture === false) optedOut.add(r.path);
|
|
862
|
+
}
|
|
863
|
+
const covered = /* @__PURE__ */ new Set();
|
|
864
|
+
for (const c of captured) {
|
|
865
|
+
const route = routeForCapture(c, routes, routePaths);
|
|
866
|
+
if (route) covered.add(route);
|
|
867
|
+
}
|
|
868
|
+
const baselineSet = new Set(baseline?.uncapturedPages ?? []);
|
|
869
|
+
const diagnostics = [];
|
|
870
|
+
for (const r of routes) {
|
|
871
|
+
if (covered.has(r.path) || optedOut.has(r.path)) continue;
|
|
872
|
+
const loc = registry.get("page", r.page)?.loc;
|
|
873
|
+
if (baselineSet.has(r.path)) {
|
|
874
|
+
diagnostics.push({
|
|
875
|
+
code: "page-backlog",
|
|
876
|
+
severity: "info",
|
|
877
|
+
message: `route "${r.path}" has no visual-states capture (acknowledged backlog)`,
|
|
878
|
+
file: loc?.file,
|
|
879
|
+
line: loc?.line,
|
|
880
|
+
hint: `Capture it (drive the route in a states spec) and it drops off the baseline, or opt it out with \`export const uidex = { page: "${r.page}", capture: false }\`.`
|
|
881
|
+
});
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
diagnostics.push({
|
|
885
|
+
code: "page-no-capture",
|
|
886
|
+
severity: "error",
|
|
887
|
+
message: `route "${r.path}" has no visual-states capture and is not accounted for`,
|
|
888
|
+
file: loc?.file,
|
|
889
|
+
line: loc?.line,
|
|
890
|
+
entity: { kind: "route", id: r.path },
|
|
891
|
+
hint: `Capture the route in a states spec, opt it out with \`export const uidex = { page: "${r.page}", capture: false }\`, or grandfather it as acknowledged backlog with \`uidex scan --update-baseline\` (the baseline change lands in the diff for review).`
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
for (const b of baselineSet) {
|
|
895
|
+
if (!routePaths.includes(b)) {
|
|
896
|
+
diagnostics.push({
|
|
897
|
+
code: "stale-page-baseline",
|
|
898
|
+
severity: "warning",
|
|
899
|
+
message: `coverage baseline lists "${b}", which is not a route in the registry`,
|
|
900
|
+
hint: `Remove it from the baseline (\`uidex scan --update-baseline\`); the route no longer exists.`
|
|
901
|
+
});
|
|
902
|
+
} else if (covered.has(b) || optedOut.has(b)) {
|
|
903
|
+
diagnostics.push({
|
|
904
|
+
code: "page-captured",
|
|
905
|
+
severity: "warning",
|
|
906
|
+
message: `route "${b}" is now captured but is still in the coverage baseline`,
|
|
907
|
+
hint: `Prune it with \`uidex scan --update-baseline\` so the backlog reflects reality.`
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
return diagnostics;
|
|
912
|
+
}
|
|
913
|
+
function computePageBaseline(registry, manifest) {
|
|
914
|
+
const uncaptured = [];
|
|
915
|
+
for (const d of checkPageCoverage(registry, manifest, {
|
|
916
|
+
uncapturedPages: []
|
|
917
|
+
})) {
|
|
918
|
+
if (d.code === "page-no-capture" && d.entity) uncaptured.push(d.entity.id);
|
|
919
|
+
}
|
|
920
|
+
return { uncapturedPages: uncaptured.sort() };
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// src/scanner/scan/state-coverage.ts
|
|
924
|
+
var STATE_KINDS = ["page", "feature", "widget"];
|
|
925
|
+
function declaredStateEntities(registry) {
|
|
926
|
+
const out2 = [];
|
|
927
|
+
for (const kind of STATE_KINDS) {
|
|
928
|
+
for (const e of registry.list(kind)) {
|
|
929
|
+
const states = e.meta?.states;
|
|
930
|
+
if (!states || states.length === 0) continue;
|
|
931
|
+
out2.push({
|
|
932
|
+
kind,
|
|
933
|
+
id: e.id,
|
|
934
|
+
states: states.map((s) => s.name),
|
|
935
|
+
loc: e.loc
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
return out2;
|
|
940
|
+
}
|
|
941
|
+
function captureMatchesEntity(cap, ent) {
|
|
942
|
+
if (cap.entity !== ent.id) return false;
|
|
943
|
+
return cap.kind === void 0 || cap.kind === ent.kind;
|
|
944
|
+
}
|
|
945
|
+
function checkStateCoverage(registry, manifest) {
|
|
946
|
+
const diagnostics = [];
|
|
947
|
+
const declared = declaredStateEntities(registry);
|
|
948
|
+
const captured = manifest.captured ?? [];
|
|
949
|
+
for (const ent of declared) {
|
|
950
|
+
for (const state of ent.states) {
|
|
951
|
+
const hit = captured.some(
|
|
952
|
+
(c) => captureMatchesEntity(c, ent) && c.state === state
|
|
953
|
+
);
|
|
954
|
+
if (hit) continue;
|
|
955
|
+
diagnostics.push({
|
|
956
|
+
code: "missing-state-capture",
|
|
957
|
+
severity: "warning",
|
|
958
|
+
message: `${ent.kind} "${ent.id}" declares state "${state}" but no capture produced it`,
|
|
959
|
+
file: ent.loc?.file,
|
|
960
|
+
line: ent.loc?.line,
|
|
961
|
+
entity: { kind: ent.kind, id: ent.id },
|
|
962
|
+
hint: `Add a capture for "${ent.id}/${state}" to the states matrix, or remove the state from \`export const uidex = { states: [...] }\` if it no longer exists.`
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
for (const cap of captured) {
|
|
967
|
+
const candidates = declared.filter((ent2) => captureMatchesEntity(cap, ent2));
|
|
968
|
+
if (candidates.length === 0) continue;
|
|
969
|
+
const declaredSomewhere = candidates.some(
|
|
970
|
+
(ent2) => ent2.states.includes(cap.state)
|
|
971
|
+
);
|
|
972
|
+
if (declaredSomewhere) continue;
|
|
973
|
+
const ent = candidates[0];
|
|
974
|
+
diagnostics.push({
|
|
975
|
+
code: "orphan-state-capture",
|
|
976
|
+
severity: "warning",
|
|
977
|
+
message: `Capture produced state "${cap.state}" for ${ent.kind} "${ent.id}", which is not in its declared states [${ent.states.join(", ")}]`,
|
|
978
|
+
file: ent.loc?.file,
|
|
979
|
+
line: ent.loc?.line,
|
|
980
|
+
entity: { kind: ent.kind, id: ent.id },
|
|
981
|
+
hint: `Declare "${cap.state}" in \`export const uidex = { states: [...] }\` for "${ent.id}", or rename the capture to a declared state.`
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
return diagnostics;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// src/scanner/scan/state-matrix.ts
|
|
988
|
+
var CORE_KINDS = CORE_STATE_KINDS;
|
|
989
|
+
var STATE_ENTITY_KINDS = ["page", "feature", "widget"];
|
|
990
|
+
function resolveMatrixKind(registry, c) {
|
|
991
|
+
if (c.stateKind) return c.stateKind;
|
|
992
|
+
for (const kind of STATE_ENTITY_KINDS) {
|
|
993
|
+
const declared = registry.get(kind, c.entity)?.meta?.states?.find((s) => s.name === c.state)?.kind;
|
|
994
|
+
if (declared) return declared;
|
|
995
|
+
}
|
|
996
|
+
return CORE_KINDS.includes(c.state) ? c.state : "variant";
|
|
997
|
+
}
|
|
998
|
+
function coreKindsByRoute(registry, manifest) {
|
|
999
|
+
const routes = registry.list("route");
|
|
1000
|
+
const routePaths = routes.map((r) => r.path);
|
|
1001
|
+
const byRoute = /* @__PURE__ */ new Map();
|
|
1002
|
+
for (const c of manifest.captured ?? []) {
|
|
1003
|
+
const route = routeForCapture(c, routes, routePaths);
|
|
1004
|
+
if (!route) continue;
|
|
1005
|
+
if (!byRoute.has(route)) byRoute.set(route, /* @__PURE__ */ new Set());
|
|
1006
|
+
const kind = resolveMatrixKind(registry, c);
|
|
1007
|
+
if (CORE_KINDS.includes(kind)) byRoute.get(route).add(kind);
|
|
1008
|
+
}
|
|
1009
|
+
return byRoute;
|
|
1010
|
+
}
|
|
1011
|
+
function pageWaivers(registry, routePage) {
|
|
1012
|
+
return registry.get("page", routePage)?.meta?.waivers ?? {};
|
|
1013
|
+
}
|
|
1014
|
+
function isOptedOut(registry, page) {
|
|
1015
|
+
return page ? registry.get("page", page)?.meta?.capture === false : false;
|
|
1016
|
+
}
|
|
1017
|
+
function checkStateMatrix(registry, manifest, baseline) {
|
|
1018
|
+
const routes = registry.list("route");
|
|
1019
|
+
if (routes.length === 0) return [];
|
|
1020
|
+
const pageOf = new Map(routes.map((r) => [r.path, r.page]));
|
|
1021
|
+
const captured = coreKindsByRoute(registry, manifest);
|
|
1022
|
+
const baselined = baseline?.missingKinds ?? {};
|
|
1023
|
+
const diagnostics = [];
|
|
1024
|
+
for (const [route, kinds] of captured) {
|
|
1025
|
+
const page = pageOf.get(route);
|
|
1026
|
+
if (isOptedOut(registry, page)) continue;
|
|
1027
|
+
const waivers = page ? pageWaivers(registry, page) : {};
|
|
1028
|
+
const loc = page ? registry.get("page", page)?.loc : void 0;
|
|
1029
|
+
const baselinedKinds = new Set(baselined[route] ?? []);
|
|
1030
|
+
for (const kind of CORE_KINDS) {
|
|
1031
|
+
if (kinds.has(kind)) continue;
|
|
1032
|
+
if (waivers[kind]) continue;
|
|
1033
|
+
if (baselinedKinds.has(kind)) {
|
|
1034
|
+
diagnostics.push({
|
|
1035
|
+
code: "matrix-backlog",
|
|
1036
|
+
severity: "info",
|
|
1037
|
+
message: `route "${route}" does not render core kind "${kind}" (acknowledged MISSING_KINDS backlog)`,
|
|
1038
|
+
file: loc?.file,
|
|
1039
|
+
line: loc?.line,
|
|
1040
|
+
hint: `Capture the "${kind}" state for "${route}", waive it (\`waivers: { ${kind}: "why it can't" }\` on the page), or leave it in the baseline.`
|
|
1041
|
+
});
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
diagnostics.push({
|
|
1045
|
+
code: "core-kind",
|
|
1046
|
+
severity: "error",
|
|
1047
|
+
message: `route "${route}" is captured but never renders core kind "${kind}"`,
|
|
1048
|
+
file: loc?.file,
|
|
1049
|
+
line: loc?.line,
|
|
1050
|
+
entity: { kind: "route", id: route },
|
|
1051
|
+
hint: `Capture the "${kind}" state, or if the route cannot render it add \`export const uidex = { page: "${page}", waivers: { ${kind}: "why it can't" } }\`, or grandfather it via \`uidex scan --update-baseline\` (the baseline change lands in the diff for review).`
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
for (const [route, kinds] of captured) {
|
|
1056
|
+
const page = pageOf.get(route);
|
|
1057
|
+
if (!page || isOptedOut(registry, page)) continue;
|
|
1058
|
+
const waivers = pageWaivers(registry, page);
|
|
1059
|
+
const loc = registry.get("page", page)?.loc;
|
|
1060
|
+
for (const kind of Object.keys(waivers)) {
|
|
1061
|
+
if (kinds.has(kind)) {
|
|
1062
|
+
diagnostics.push({
|
|
1063
|
+
code: "stale-waiver",
|
|
1064
|
+
severity: "warning",
|
|
1065
|
+
message: `page "${page}" waives core kind "${kind}", but route "${route}" does render it`,
|
|
1066
|
+
file: loc?.file,
|
|
1067
|
+
line: loc?.line,
|
|
1068
|
+
entity: { kind: "page", id: page },
|
|
1069
|
+
hint: `Remove the "${kind}" waiver \u2014 the route renders that kind, so the waiver's reason is stale.`
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
return diagnostics;
|
|
1075
|
+
}
|
|
1076
|
+
function computeMissingKinds(registry, manifest) {
|
|
1077
|
+
const routes = registry.list("route");
|
|
1078
|
+
const pageOf = new Map(routes.map((r) => [r.path, r.page]));
|
|
1079
|
+
const captured = coreKindsByRoute(registry, manifest);
|
|
1080
|
+
const out2 = {};
|
|
1081
|
+
for (const [route, kinds] of captured) {
|
|
1082
|
+
const page = pageOf.get(route);
|
|
1083
|
+
if (isOptedOut(registry, page)) continue;
|
|
1084
|
+
const waivers = page ? pageWaivers(registry, page) : {};
|
|
1085
|
+
const missing = CORE_KINDS.filter((k) => !kinds.has(k) && !waivers[k]);
|
|
1086
|
+
if (missing.length > 0) out2[route] = missing;
|
|
1087
|
+
}
|
|
1088
|
+
return out2;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
779
1091
|
// src/scanner/scan/audit.ts
|
|
780
1092
|
function audit(opts) {
|
|
781
1093
|
const diagnostics = [];
|
|
@@ -785,6 +1097,9 @@ function audit(opts) {
|
|
|
785
1097
|
const acceptanceEnabled = config.audit?.acceptance ?? true;
|
|
786
1098
|
const scopeLeakEnabled = config.audit?.scopeLeak ?? true;
|
|
787
1099
|
const coverageEnabled = config.audit?.coverage ?? true;
|
|
1100
|
+
const statesEnabled = config.audit?.states ?? true;
|
|
1101
|
+
const pageCoverageEnabled = config.audit?.pageCoverage ?? true;
|
|
1102
|
+
const stateMatrixEnabled = config.audit?.stateMatrix ?? true;
|
|
788
1103
|
if (opts.resolveDiagnostics) diagnostics.push(...opts.resolveDiagnostics);
|
|
789
1104
|
for (const ef of extracted) {
|
|
790
1105
|
if (ef.diagnostics) diagnostics.push(...ef.diagnostics);
|
|
@@ -818,6 +1133,48 @@ function audit(opts) {
|
|
|
818
1133
|
}
|
|
819
1134
|
}
|
|
820
1135
|
}
|
|
1136
|
+
if (check && opts.typesGenerated !== void 0) {
|
|
1137
|
+
const outRel = opts.typesOutputPath ?? config.output;
|
|
1138
|
+
const fresh = normalizeForCheck(opts.typesGenerated);
|
|
1139
|
+
if (opts.typesOnDisk === null || opts.typesOnDisk === void 0) {
|
|
1140
|
+
diagnostics.push({
|
|
1141
|
+
code: "gen-missing",
|
|
1142
|
+
severity: "error",
|
|
1143
|
+
message: `Generated file "${outRel}" does not exist on disk; run \`uidex scan\` and commit the result`,
|
|
1144
|
+
file: outRel,
|
|
1145
|
+
hint: "Run `uidex scan` (without --check) to regenerate"
|
|
1146
|
+
});
|
|
1147
|
+
} else if (normalizeForCheck(opts.typesOnDisk) !== fresh) {
|
|
1148
|
+
diagnostics.push({
|
|
1149
|
+
code: "gen-stale",
|
|
1150
|
+
severity: "error",
|
|
1151
|
+
message: `Generated file "${outRel}" is stale`,
|
|
1152
|
+
file: outRel,
|
|
1153
|
+
hint: "Run `uidex scan` (without --check) to regenerate and commit the result"
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
if (check && opts.locsGenerated !== void 0) {
|
|
1158
|
+
const outRel = opts.locsOutputPath ?? config.output;
|
|
1159
|
+
const fresh = normalizeForCheck(opts.locsGenerated);
|
|
1160
|
+
if (opts.locsOnDisk === null || opts.locsOnDisk === void 0) {
|
|
1161
|
+
diagnostics.push({
|
|
1162
|
+
code: "gen-missing",
|
|
1163
|
+
severity: "error",
|
|
1164
|
+
message: `Generated file "${outRel}" does not exist on disk; run \`uidex scan\` and commit the result`,
|
|
1165
|
+
file: outRel,
|
|
1166
|
+
hint: "Run `uidex scan` (without --check) to regenerate"
|
|
1167
|
+
});
|
|
1168
|
+
} else if (normalizeForCheck(opts.locsOnDisk) !== fresh) {
|
|
1169
|
+
diagnostics.push({
|
|
1170
|
+
code: "gen-stale",
|
|
1171
|
+
severity: "error",
|
|
1172
|
+
message: `Generated file "${outRel}" is stale`,
|
|
1173
|
+
file: outRel,
|
|
1174
|
+
hint: "Run `uidex scan` (without --check) to regenerate and commit the result"
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
821
1178
|
if (lint && acceptanceEnabled) {
|
|
822
1179
|
for (const kind of ["widget", "feature", "page"]) {
|
|
823
1180
|
for (const e of registry.list(kind)) {
|
|
@@ -1077,6 +1434,21 @@ function audit(opts) {
|
|
|
1077
1434
|
}
|
|
1078
1435
|
}
|
|
1079
1436
|
}
|
|
1437
|
+
if (lint && statesEnabled && opts.capturedStates) {
|
|
1438
|
+
diagnostics.push(...checkStateCoverage(registry, opts.capturedStates));
|
|
1439
|
+
}
|
|
1440
|
+
if (lint && pageCoverageEnabled && opts.capturedStates) {
|
|
1441
|
+
diagnostics.push(
|
|
1442
|
+
...checkPageCoverage(registry, opts.capturedStates, opts.pageBaseline)
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1445
|
+
if (lint && stateMatrixEnabled && opts.capturedStates) {
|
|
1446
|
+
diagnostics.push(
|
|
1447
|
+
...checkStateMatrix(registry, opts.capturedStates, {
|
|
1448
|
+
missingKinds: opts.pageBaseline?.missingKinds ?? {}
|
|
1449
|
+
})
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1080
1452
|
const summary = {
|
|
1081
1453
|
errors: diagnostics.filter((d) => d.severity === "error").length,
|
|
1082
1454
|
warnings: diagnostics.filter((d) => d.severity === "warning").length
|
|
@@ -1241,6 +1613,148 @@ function stableReplacer(_key, value) {
|
|
|
1241
1613
|
}
|
|
1242
1614
|
|
|
1243
1615
|
// src/scanner/scan/emit.ts
|
|
1616
|
+
var AUTHORED_KINDS = /* @__PURE__ */ new Set([
|
|
1617
|
+
"route",
|
|
1618
|
+
"page",
|
|
1619
|
+
"feature",
|
|
1620
|
+
"widget",
|
|
1621
|
+
"primitive",
|
|
1622
|
+
"flow"
|
|
1623
|
+
]);
|
|
1624
|
+
function internedPayload(entities) {
|
|
1625
|
+
const fileIndex = /* @__PURE__ */ new Map();
|
|
1626
|
+
const files = [];
|
|
1627
|
+
const compact = entities.map((e) => {
|
|
1628
|
+
const loc = e.loc;
|
|
1629
|
+
if (!loc || typeof loc.file !== "string") return e;
|
|
1630
|
+
let idx = fileIndex.get(loc.file);
|
|
1631
|
+
if (idx === void 0) {
|
|
1632
|
+
idx = files.length;
|
|
1633
|
+
files.push(loc.file);
|
|
1634
|
+
fileIndex.set(loc.file, idx);
|
|
1635
|
+
}
|
|
1636
|
+
const rest = { ...loc };
|
|
1637
|
+
delete rest.file;
|
|
1638
|
+
return { ...e, loc: { f: idx, ...rest } };
|
|
1639
|
+
});
|
|
1640
|
+
return {
|
|
1641
|
+
filesDecl: `const files: readonly string[] = ${jsonStable(files)}`,
|
|
1642
|
+
parseExpr: `(JSON.parse(${JSON.stringify(
|
|
1643
|
+
JSON.stringify(compact, replacerSorted)
|
|
1644
|
+
)}) as any[]).map(hydrate)`
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
var HYDRATE_FN = [
|
|
1648
|
+
"function hydrate(e: any): any {",
|
|
1649
|
+
' if (e.loc && typeof e.loc.f === "number") {',
|
|
1650
|
+
" const { f, ...rest } = e.loc",
|
|
1651
|
+
" e.loc = { file: files[f], ...rest }",
|
|
1652
|
+
" }",
|
|
1653
|
+
" return e",
|
|
1654
|
+
"}"
|
|
1655
|
+
].join("\n");
|
|
1656
|
+
function compactMeta(meta) {
|
|
1657
|
+
if (!meta || typeof meta !== "object") return meta;
|
|
1658
|
+
const composes = meta.composes;
|
|
1659
|
+
if (!Array.isArray(composes)) return meta;
|
|
1660
|
+
return {
|
|
1661
|
+
...meta,
|
|
1662
|
+
composes: composes.map(
|
|
1663
|
+
(c) => c && typeof c === "object" && c.kind === "element" && Object.keys(c).length === 2 && typeof c.id === "string" ? c.id : c
|
|
1664
|
+
)
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
function emitLocsModule(thin, uidexImport) {
|
|
1668
|
+
const kinds = [...new Set(thin.map((e) => e.kind))].sort();
|
|
1669
|
+
const kindIdx = new Map(kinds.map((k, i) => [k, i]));
|
|
1670
|
+
const fileIndex = /* @__PURE__ */ new Map();
|
|
1671
|
+
const files = [];
|
|
1672
|
+
const fi = (f) => {
|
|
1673
|
+
let i = fileIndex.get(f);
|
|
1674
|
+
if (i === void 0) {
|
|
1675
|
+
i = files.length;
|
|
1676
|
+
files.push(f);
|
|
1677
|
+
fileIndex.set(f, i);
|
|
1678
|
+
}
|
|
1679
|
+
return i;
|
|
1680
|
+
};
|
|
1681
|
+
const bare = [];
|
|
1682
|
+
const rich = [];
|
|
1683
|
+
for (const e of thin) {
|
|
1684
|
+
const loc = e.loc;
|
|
1685
|
+
const topOnlyKindIdLoc = Object.keys(e).every(
|
|
1686
|
+
(k) => k === "kind" || k === "id" || k === "loc"
|
|
1687
|
+
);
|
|
1688
|
+
const locOnlyKnown = !!loc && typeof loc.file === "string" && Object.keys(loc).every(
|
|
1689
|
+
(k) => k === "file" || k === "line" || k === "column"
|
|
1690
|
+
);
|
|
1691
|
+
if (topOnlyKindIdLoc && locOnlyKnown) {
|
|
1692
|
+
const row = [
|
|
1693
|
+
kindIdx.get(e.kind),
|
|
1694
|
+
e.id,
|
|
1695
|
+
fi(loc.file)
|
|
1696
|
+
];
|
|
1697
|
+
if (loc.line != null || loc.column != null)
|
|
1698
|
+
row.push(Number(loc.line ?? 0));
|
|
1699
|
+
if (loc.column != null) row.push(Number(loc.column));
|
|
1700
|
+
bare.push(row);
|
|
1701
|
+
} else {
|
|
1702
|
+
const richEntity = { ...e };
|
|
1703
|
+
if (loc && typeof loc.file === "string") {
|
|
1704
|
+
const { file, ...rest } = loc;
|
|
1705
|
+
richEntity.loc = { f: fi(file), ...rest };
|
|
1706
|
+
}
|
|
1707
|
+
if (richEntity.meta) richEntity.meta = compactMeta(richEntity.meta);
|
|
1708
|
+
rich.push(richEntity);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
const str = (v) => JSON.stringify(JSON.stringify(v, replacerSorted));
|
|
1712
|
+
const l = [];
|
|
1713
|
+
l.push("// THIS FILE IS AUTO-GENERATED BY `uidex scan`. DO NOT EDIT.");
|
|
1714
|
+
l.push(
|
|
1715
|
+
"// Deferred element/region source locations for the inspector's copy-path."
|
|
1716
|
+
);
|
|
1717
|
+
l.push("// @ts-nocheck");
|
|
1718
|
+
l.push("/* eslint-disable */");
|
|
1719
|
+
l.push(`import type { Entity, Registry } from ${JSON.stringify(uidexImport)}`);
|
|
1720
|
+
l.push("");
|
|
1721
|
+
l.push(`const files: readonly string[] = ${jsonStable(files)}`);
|
|
1722
|
+
l.push(`const kinds: readonly string[] = ${jsonStable(kinds)}`);
|
|
1723
|
+
l.push("function loc(fi: number, line?: number, col?: number) {");
|
|
1724
|
+
l.push(" const o: any = { file: files[fi] }");
|
|
1725
|
+
l.push(" if (line) o.line = line");
|
|
1726
|
+
l.push(" if (col) o.column = col");
|
|
1727
|
+
l.push(" return o");
|
|
1728
|
+
l.push("}");
|
|
1729
|
+
l.push(`const bare: any[] = JSON.parse(${str(bare)})`);
|
|
1730
|
+
l.push(`const rich: any[] = JSON.parse(${str(rich)})`);
|
|
1731
|
+
l.push("");
|
|
1732
|
+
l.push(
|
|
1733
|
+
"/** Add the thin element/region entities (source locations) to a registry. */"
|
|
1734
|
+
);
|
|
1735
|
+
l.push("export function loadLocs(target: Registry): Registry {");
|
|
1736
|
+
l.push(" for (const r of bare)");
|
|
1737
|
+
l.push(
|
|
1738
|
+
" target.add({ kind: kinds[r[0]], id: r[1], loc: loc(r[2], r[3], r[4]) } as Entity)"
|
|
1739
|
+
);
|
|
1740
|
+
l.push(" for (const e of rich) {");
|
|
1741
|
+
l.push(" const out: any = { ...e }");
|
|
1742
|
+
l.push(' if (e.loc && typeof e.loc.f === "number") {');
|
|
1743
|
+
l.push(" const { f, ...rest } = e.loc");
|
|
1744
|
+
l.push(" out.loc = { file: files[f], ...rest }");
|
|
1745
|
+
l.push(" }");
|
|
1746
|
+
l.push(" if (e.meta && Array.isArray(e.meta.composes)) {");
|
|
1747
|
+
l.push(
|
|
1748
|
+
' out.meta = { ...e.meta, composes: e.meta.composes.map((c: any) => typeof c === "string" ? { id: c, kind: "element" } : c) }'
|
|
1749
|
+
);
|
|
1750
|
+
l.push(" }");
|
|
1751
|
+
l.push(" target.add(out)");
|
|
1752
|
+
l.push(" }");
|
|
1753
|
+
l.push(" return target");
|
|
1754
|
+
l.push("}");
|
|
1755
|
+
l.push("");
|
|
1756
|
+
return l.join("\n");
|
|
1757
|
+
}
|
|
1244
1758
|
function sortById(arr) {
|
|
1245
1759
|
return [...arr].sort((a, b) => a.id.localeCompare(b.id));
|
|
1246
1760
|
}
|
|
@@ -1266,6 +1780,20 @@ function emitIdUnion(name, ids) {
|
|
|
1266
1780
|
${body}
|
|
1267
1781
|
`;
|
|
1268
1782
|
}
|
|
1783
|
+
var OPAQUE_ID_UNIONS = /* @__PURE__ */ new Set(["ElementId"]);
|
|
1784
|
+
function emitId(name, ids) {
|
|
1785
|
+
if (OPAQUE_ID_UNIONS.has(name)) {
|
|
1786
|
+
return `export type ${name} = string & { readonly __uidex?: ${JSON.stringify(name)} }
|
|
1787
|
+
`;
|
|
1788
|
+
}
|
|
1789
|
+
return emitIdUnion(name, ids);
|
|
1790
|
+
}
|
|
1791
|
+
function dataOutputPath(output) {
|
|
1792
|
+
return output.replace(/\.(d\.ts|ts|tsx|mts|cts|js|mjs|cjs)$/, ".data.$1");
|
|
1793
|
+
}
|
|
1794
|
+
function locsOutputPath(output) {
|
|
1795
|
+
return output.replace(/\.(d\.ts|ts|tsx|mts|cts|js|mjs|cjs)$/, ".locs.$1");
|
|
1796
|
+
}
|
|
1269
1797
|
function emit(opts) {
|
|
1270
1798
|
const { registry, gitContext, uidexImport = "uidex" } = opts;
|
|
1271
1799
|
const routes = [...registry.list("route")].sort(
|
|
@@ -1274,112 +1802,131 @@ function emit(opts) {
|
|
|
1274
1802
|
const pages = sortById(registry.list("page"));
|
|
1275
1803
|
const features = sortById(registry.list("feature"));
|
|
1276
1804
|
const widgets = sortById(registry.list("widget"));
|
|
1805
|
+
const stateNames = /* @__PURE__ */ new Set();
|
|
1806
|
+
for (const e of [...pages, ...features, ...widgets]) {
|
|
1807
|
+
for (const s of e.meta?.states ?? []) stateNames.add(s.name);
|
|
1808
|
+
}
|
|
1277
1809
|
const regions = sortById(registry.list("region"));
|
|
1278
1810
|
const elements = sortById(registry.list("element"));
|
|
1279
1811
|
const primitives = sortById(registry.list("primitive"));
|
|
1280
1812
|
const flows = sortById(registry.list("flow"));
|
|
1281
|
-
const
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
lines.push(
|
|
1286
|
-
`import type { Entity, Registry } from ${JSON.stringify(uidexImport)}`
|
|
1813
|
+
const t = [];
|
|
1814
|
+
t.push("// THIS FILE IS AUTO-GENERATED BY `uidex scan`. DO NOT EDIT.");
|
|
1815
|
+
t.push(
|
|
1816
|
+
"// Types only \u2014 the runtime registry lives in the sibling `.data` module."
|
|
1287
1817
|
);
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1818
|
+
t.push("/* eslint-disable */");
|
|
1819
|
+
t.push("");
|
|
1820
|
+
t.push("// ---- id unions ----");
|
|
1821
|
+
t.push(
|
|
1822
|
+
emitId(
|
|
1292
1823
|
"PageId",
|
|
1293
1824
|
pages.map((e) => e.id)
|
|
1294
1825
|
)
|
|
1295
1826
|
);
|
|
1296
|
-
|
|
1297
|
-
|
|
1827
|
+
t.push(
|
|
1828
|
+
emitId(
|
|
1298
1829
|
"FeatureId",
|
|
1299
1830
|
features.map((e) => e.id)
|
|
1300
1831
|
)
|
|
1301
1832
|
);
|
|
1302
|
-
|
|
1303
|
-
|
|
1833
|
+
t.push(
|
|
1834
|
+
emitId(
|
|
1304
1835
|
"WidgetId",
|
|
1305
1836
|
widgets.map((e) => e.id)
|
|
1306
1837
|
)
|
|
1307
1838
|
);
|
|
1308
|
-
|
|
1309
|
-
|
|
1839
|
+
t.push(
|
|
1840
|
+
emitId(
|
|
1310
1841
|
"RegionId",
|
|
1311
1842
|
regions.map((e) => e.id)
|
|
1312
1843
|
)
|
|
1313
1844
|
);
|
|
1314
|
-
|
|
1315
|
-
|
|
1845
|
+
t.push(
|
|
1846
|
+
emitId(
|
|
1316
1847
|
"ElementId",
|
|
1317
1848
|
elements.map((e) => e.id)
|
|
1318
1849
|
)
|
|
1319
1850
|
);
|
|
1320
|
-
|
|
1321
|
-
|
|
1851
|
+
t.push(
|
|
1852
|
+
emitId(
|
|
1322
1853
|
"PrimitiveId",
|
|
1323
1854
|
primitives.map((e) => e.id)
|
|
1324
1855
|
)
|
|
1325
1856
|
);
|
|
1326
|
-
|
|
1327
|
-
|
|
1857
|
+
t.push(
|
|
1858
|
+
emitId(
|
|
1328
1859
|
"FlowId",
|
|
1329
1860
|
flows.map((e) => e.id)
|
|
1330
1861
|
)
|
|
1331
1862
|
);
|
|
1332
|
-
|
|
1333
|
-
|
|
1863
|
+
t.push(
|
|
1864
|
+
emitId(
|
|
1334
1865
|
"RouteId",
|
|
1335
1866
|
routes.map((e) => e.path)
|
|
1336
1867
|
)
|
|
1337
1868
|
);
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1869
|
+
t.push(emitId("StateId", [...stateNames]));
|
|
1870
|
+
t.push("");
|
|
1871
|
+
t.push("// ---- authoring-surface shape types ----");
|
|
1872
|
+
t.push("export namespace Uidex {");
|
|
1873
|
+
t.push(
|
|
1874
|
+
' export type CoreStateKind = "loading" | "empty" | "populated" | "error"'
|
|
1875
|
+
);
|
|
1876
|
+
t.push(' export type StateKind = CoreStateKind | "variant"');
|
|
1877
|
+
t.push(" export interface State {");
|
|
1878
|
+
t.push(" name: string");
|
|
1879
|
+
t.push(" kind?: StateKind");
|
|
1880
|
+
t.push(" acceptance?: readonly string[]");
|
|
1881
|
+
t.push(" description?: string");
|
|
1882
|
+
t.push(" }");
|
|
1883
|
+
t.push(" export type Waivers = Partial<Record<CoreStateKind, string>>");
|
|
1884
|
+
t.push(" export interface Page {");
|
|
1885
|
+
t.push(" page: PageId | false");
|
|
1886
|
+
t.push(" name?: string");
|
|
1887
|
+
t.push(" features?: readonly FeatureId[]");
|
|
1888
|
+
t.push(" widgets?: readonly WidgetId[]");
|
|
1889
|
+
t.push(" acceptance?: readonly string[]");
|
|
1890
|
+
t.push(" states?: readonly (string | State)[]");
|
|
1891
|
+
t.push(" capture?: boolean");
|
|
1892
|
+
t.push(" waivers?: Waivers");
|
|
1893
|
+
t.push(" description?: string");
|
|
1894
|
+
t.push(" }");
|
|
1895
|
+
t.push(" export interface Feature {");
|
|
1896
|
+
t.push(" feature: FeatureId | false");
|
|
1897
|
+
t.push(" name?: string");
|
|
1898
|
+
t.push(" features?: readonly FeatureId[]");
|
|
1899
|
+
t.push(" acceptance?: readonly string[]");
|
|
1900
|
+
t.push(" states?: readonly (string | State)[]");
|
|
1901
|
+
t.push(" description?: string");
|
|
1902
|
+
t.push(" }");
|
|
1903
|
+
t.push(" export interface Primitive {");
|
|
1904
|
+
t.push(" primitive: PrimitiveId");
|
|
1905
|
+
t.push(" name?: string");
|
|
1906
|
+
t.push(" description?: string");
|
|
1907
|
+
t.push(" }");
|
|
1908
|
+
t.push(" export interface Widget {");
|
|
1909
|
+
t.push(" widget: WidgetId");
|
|
1910
|
+
t.push(" name?: string");
|
|
1911
|
+
t.push(" acceptance?: readonly string[]");
|
|
1912
|
+
t.push(" states?: readonly (string | State)[]");
|
|
1913
|
+
t.push(" description?: string");
|
|
1914
|
+
t.push(" }");
|
|
1915
|
+
t.push(" export interface Region {");
|
|
1916
|
+
t.push(" region: RegionId | false");
|
|
1917
|
+
t.push(" name?: string");
|
|
1918
|
+
t.push(" description?: string");
|
|
1919
|
+
t.push(" }");
|
|
1920
|
+
t.push(" export interface Flow {");
|
|
1921
|
+
t.push(" flow: FlowId");
|
|
1922
|
+
t.push(" name?: string");
|
|
1923
|
+
t.push(" description?: string");
|
|
1924
|
+
t.push(" }");
|
|
1925
|
+
t.push(" export interface NotFlow {");
|
|
1926
|
+
t.push(" notFlow: true");
|
|
1927
|
+
t.push(" }");
|
|
1928
|
+
t.push("}");
|
|
1929
|
+
t.push("");
|
|
1383
1930
|
const allEntities = [
|
|
1384
1931
|
...routes,
|
|
1385
1932
|
...pages,
|
|
@@ -1390,25 +1937,39 @@ function emit(opts) {
|
|
|
1390
1937
|
...primitives,
|
|
1391
1938
|
...flows
|
|
1392
1939
|
];
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
);
|
|
1396
|
-
lines.push("");
|
|
1397
|
-
lines.push("// ---- git context ----");
|
|
1940
|
+
const authored = allEntities.filter((e) => AUTHORED_KINDS.has(e.kind));
|
|
1941
|
+
const thin = allEntities.filter((e) => !AUTHORED_KINDS.has(e.kind));
|
|
1398
1942
|
const gc = gitContext ?? { branch: null, commit: null, pr: null };
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1943
|
+
const authoredPayload = internedPayload(authored);
|
|
1944
|
+
const d = [];
|
|
1945
|
+
d.push("// THIS FILE IS AUTO-GENERATED BY `uidex scan`. DO NOT EDIT.");
|
|
1946
|
+
d.push(
|
|
1947
|
+
"// Authored-metadata registry. The thin element/region entities (source"
|
|
1948
|
+
);
|
|
1949
|
+
d.push(
|
|
1950
|
+
"// locations only) live in the sibling `.locs` module, loaded lazily."
|
|
1951
|
+
);
|
|
1952
|
+
d.push("// @ts-nocheck");
|
|
1953
|
+
d.push("/* eslint-disable */");
|
|
1954
|
+
d.push(`import { createUidex } from ${JSON.stringify(uidexImport)}`);
|
|
1955
|
+
d.push(`import type { Registry } from ${JSON.stringify(uidexImport)}`);
|
|
1956
|
+
d.push("");
|
|
1957
|
+
d.push(authoredPayload.filesDecl);
|
|
1958
|
+
d.push(HYDRATE_FN);
|
|
1959
|
+
d.push(`export const entities = ${authoredPayload.parseExpr}`);
|
|
1960
|
+
d.push("");
|
|
1961
|
+
d.push(`export const gitContext = ${jsonStable(gc)} as const`);
|
|
1962
|
+
d.push("");
|
|
1963
|
+
d.push("export function loadRegistry(target: Registry): Registry {");
|
|
1964
|
+
d.push(" for (const entity of entities) target.add(entity)");
|
|
1965
|
+
d.push(" return target");
|
|
1966
|
+
d.push("}");
|
|
1967
|
+
d.push("");
|
|
1968
|
+
d.push("export const uidex = createUidex()");
|
|
1969
|
+
d.push("for (const entity of entities) uidex.registry.add(entity)");
|
|
1970
|
+
d.push("");
|
|
1971
|
+
const locs = emitLocsModule(thin, uidexImport);
|
|
1972
|
+
return { types: t.join("\n"), data: d.join("\n"), locs };
|
|
1412
1973
|
}
|
|
1413
1974
|
|
|
1414
1975
|
// src/scanner/scan/ast.ts
|
|
@@ -1445,14 +2006,14 @@ function makeLineAt(content) {
|
|
|
1445
2006
|
function parseSource(file) {
|
|
1446
2007
|
const lineAt = makeLineAt(file.content);
|
|
1447
2008
|
try {
|
|
1448
|
-
const
|
|
2009
|
+
const result = (0, import_oxc_parser.parseSync)(file.sourcePath, file.content, {
|
|
1449
2010
|
lang: langFor(file.sourcePath),
|
|
1450
2011
|
sourceType: "module"
|
|
1451
2012
|
});
|
|
1452
2013
|
return {
|
|
1453
|
-
program:
|
|
1454
|
-
hasErrors:
|
|
1455
|
-
comments:
|
|
2014
|
+
program: result.program,
|
|
2015
|
+
hasErrors: result.errors.length > 0,
|
|
2016
|
+
comments: result.comments ?? [],
|
|
1456
2017
|
lineAt
|
|
1457
2018
|
};
|
|
1458
2019
|
} catch {
|
|
@@ -1512,20 +2073,35 @@ var ALLOWED_FIELDS = {
|
|
|
1512
2073
|
"features",
|
|
1513
2074
|
"widgets",
|
|
1514
2075
|
"acceptance",
|
|
2076
|
+
"states",
|
|
2077
|
+
"capture",
|
|
2078
|
+
"waivers",
|
|
1515
2079
|
"description"
|
|
1516
2080
|
]),
|
|
2081
|
+
// `capture` / `waivers` are route-scoped, and only a page maps to a route — so
|
|
2082
|
+
// they are page-only (the gates read them off the page). A feature/widget that
|
|
2083
|
+
// sets them gets an `uidex-export-unknown-field` error rather than a silent no-op.
|
|
1517
2084
|
feature: /* @__PURE__ */ new Set([
|
|
1518
2085
|
"feature",
|
|
1519
2086
|
"name",
|
|
1520
2087
|
"features",
|
|
1521
2088
|
"acceptance",
|
|
2089
|
+
"states",
|
|
1522
2090
|
"description"
|
|
1523
2091
|
]),
|
|
1524
2092
|
primitive: /* @__PURE__ */ new Set(["primitive", "name", "description"]),
|
|
1525
|
-
widget: /* @__PURE__ */ new Set(["widget", "name", "acceptance", "description"]),
|
|
2093
|
+
widget: /* @__PURE__ */ new Set(["widget", "name", "acceptance", "states", "description"]),
|
|
1526
2094
|
region: /* @__PURE__ */ new Set(["region", "name", "description"]),
|
|
1527
2095
|
flow: /* @__PURE__ */ new Set(["flow", "notFlow", "name", "description"])
|
|
1528
2096
|
};
|
|
2097
|
+
var STATE_FIELDS = /* @__PURE__ */ new Set([
|
|
2098
|
+
"name",
|
|
2099
|
+
"kind",
|
|
2100
|
+
"acceptance",
|
|
2101
|
+
"description"
|
|
2102
|
+
]);
|
|
2103
|
+
var CORE_STATE_KINDS2 = new Set(CORE_STATE_KINDS);
|
|
2104
|
+
var STATE_KINDS2 = /* @__PURE__ */ new Set([...CORE_STATE_KINDS, "variant"]);
|
|
1529
2105
|
var FALSEABLE = /* @__PURE__ */ new Set([
|
|
1530
2106
|
"page",
|
|
1531
2107
|
"feature",
|
|
@@ -1915,6 +2491,9 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
|
|
|
1915
2491
|
}
|
|
1916
2492
|
const featuresField = kind === "page" || kind === "feature" ? readStringArrayField(byKey, "features") : void 0;
|
|
1917
2493
|
const widgetsField = kind === "page" ? readStringArrayField(byKey, "widgets") : void 0;
|
|
2494
|
+
const states = kind === "page" || kind === "feature" || kind === "widget" ? readStatesField(byKey) : void 0;
|
|
2495
|
+
const capture = kind === "page" ? readBooleanField(byKey, "capture") : void 0;
|
|
2496
|
+
const waivers = kind === "page" ? readWaiversField(byKey) : void 0;
|
|
1918
2497
|
const notFlow = kind === "flow" && discriminator === "notFlow" ? true : void 0;
|
|
1919
2498
|
const metadata = {
|
|
1920
2499
|
source: "ts-export",
|
|
@@ -1937,6 +2516,9 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
|
|
|
1937
2516
|
metadata.widgets = widgetsField.values;
|
|
1938
2517
|
metadata.widgetSpans = widgetsField.spans;
|
|
1939
2518
|
}
|
|
2519
|
+
if (states?.length) metadata.states = states;
|
|
2520
|
+
if (capture !== void 0) metadata.capture = capture;
|
|
2521
|
+
if (waivers && Object.keys(waivers).length > 0) metadata.waivers = waivers;
|
|
1940
2522
|
if (notFlow) metadata.notFlow = true;
|
|
1941
2523
|
if (typeof id === "string" && idValue.kind === "string") {
|
|
1942
2524
|
metadata.idSpan = idValue.span;
|
|
@@ -1973,6 +2555,48 @@ function readIdField(value, kind, fieldName) {
|
|
|
1973
2555
|
value.pos
|
|
1974
2556
|
);
|
|
1975
2557
|
}
|
|
2558
|
+
function readWaiversField(byKey) {
|
|
2559
|
+
const entry = byKey.get("waivers");
|
|
2560
|
+
if (!entry) return void 0;
|
|
2561
|
+
if (entry.value.kind !== "object") {
|
|
2562
|
+
throw new ExtractError(
|
|
2563
|
+
"uidex-export-invalid-field",
|
|
2564
|
+
"`waivers` must be an object mapping a core kind to a reason.",
|
|
2565
|
+
entry.value.pos
|
|
2566
|
+
);
|
|
2567
|
+
}
|
|
2568
|
+
const waivers = {};
|
|
2569
|
+
for (const e of entry.value.entries) {
|
|
2570
|
+
if (!CORE_STATE_KINDS2.has(e.key)) {
|
|
2571
|
+
throw new ExtractError(
|
|
2572
|
+
"uidex-export-invalid-field",
|
|
2573
|
+
`waiver key "${e.key}" is not a core kind; only ${[...CORE_STATE_KINDS2].join(", ")} participate in the matrix.`,
|
|
2574
|
+
e.keyPos
|
|
2575
|
+
);
|
|
2576
|
+
}
|
|
2577
|
+
if (e.value.kind !== "string" || e.value.value.length === 0) {
|
|
2578
|
+
throw new ExtractError(
|
|
2579
|
+
"uidex-export-invalid-field",
|
|
2580
|
+
`waiver "${e.key}" needs a non-empty reason (why the route cannot render this kind).`,
|
|
2581
|
+
e.value.pos
|
|
2582
|
+
);
|
|
2583
|
+
}
|
|
2584
|
+
waivers[e.key] = e.value.value;
|
|
2585
|
+
}
|
|
2586
|
+
return waivers;
|
|
2587
|
+
}
|
|
2588
|
+
function readBooleanField(byKey, name) {
|
|
2589
|
+
const entry = byKey.get(name);
|
|
2590
|
+
if (!entry) return void 0;
|
|
2591
|
+
if (entry.value.kind !== "boolean") {
|
|
2592
|
+
throw new ExtractError(
|
|
2593
|
+
"uidex-export-invalid-field",
|
|
2594
|
+
`\`${name}\` must be a boolean.`,
|
|
2595
|
+
entry.value.pos
|
|
2596
|
+
);
|
|
2597
|
+
}
|
|
2598
|
+
return entry.value.value;
|
|
2599
|
+
}
|
|
1976
2600
|
function readStringField(byKey, name) {
|
|
1977
2601
|
const entry = byKey.get(name);
|
|
1978
2602
|
if (!entry) return void 0;
|
|
@@ -2010,6 +2634,86 @@ function readStringArrayField(byKey, name) {
|
|
|
2010
2634
|
}
|
|
2011
2635
|
return { values, spans };
|
|
2012
2636
|
}
|
|
2637
|
+
function readStatesField(byKey) {
|
|
2638
|
+
const entry = byKey.get("states");
|
|
2639
|
+
if (!entry) return void 0;
|
|
2640
|
+
if (entry.value.kind !== "array") {
|
|
2641
|
+
throw new ExtractError(
|
|
2642
|
+
"uidex-export-invalid-field",
|
|
2643
|
+
"`states` must be an array of state names or `{ name, acceptance?, description? }` objects.",
|
|
2644
|
+
entry.value.pos
|
|
2645
|
+
);
|
|
2646
|
+
}
|
|
2647
|
+
const states = [];
|
|
2648
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2649
|
+
for (const item of entry.value.items) {
|
|
2650
|
+
let state;
|
|
2651
|
+
if (item.kind === "string") {
|
|
2652
|
+
state = { name: item.value, nameSpan: item.span };
|
|
2653
|
+
} else if (item.kind === "object") {
|
|
2654
|
+
const fieldByKey = /* @__PURE__ */ new Map();
|
|
2655
|
+
for (const e of item.entries) fieldByKey.set(e.key, e);
|
|
2656
|
+
for (const e of item.entries) {
|
|
2657
|
+
if (!STATE_FIELDS.has(e.key)) {
|
|
2658
|
+
throw new ExtractError(
|
|
2659
|
+
"uidex-export-invalid-field",
|
|
2660
|
+
`Unknown field "${e.key}" in a \`states\` entry. Allowed: ${Array.from(
|
|
2661
|
+
STATE_FIELDS
|
|
2662
|
+
).sort().join(", ")}.`,
|
|
2663
|
+
e.value.pos
|
|
2664
|
+
);
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
const nameEntry = fieldByKey.get("name");
|
|
2668
|
+
if (!nameEntry || nameEntry.value.kind !== "string") {
|
|
2669
|
+
throw new ExtractError(
|
|
2670
|
+
"uidex-export-invalid-field",
|
|
2671
|
+
"Each object `states` entry must have a string `name`.",
|
|
2672
|
+
item.pos
|
|
2673
|
+
);
|
|
2674
|
+
}
|
|
2675
|
+
state = { name: nameEntry.value.value, nameSpan: nameEntry.value.span };
|
|
2676
|
+
const kind = readStringField(fieldByKey, "kind");
|
|
2677
|
+
if (kind !== void 0) {
|
|
2678
|
+
if (!STATE_KINDS2.has(kind)) {
|
|
2679
|
+
throw new ExtractError(
|
|
2680
|
+
"uidex-export-invalid-field",
|
|
2681
|
+
`state \`kind\` must be one of ${[...STATE_KINDS2].join(", ")} (got "${kind}").`,
|
|
2682
|
+
fieldByKey.get("kind").value.pos
|
|
2683
|
+
);
|
|
2684
|
+
}
|
|
2685
|
+
state.kind = kind;
|
|
2686
|
+
}
|
|
2687
|
+
const acceptance = readStringArrayField(fieldByKey, "acceptance")?.values;
|
|
2688
|
+
if (acceptance) state.acceptance = acceptance;
|
|
2689
|
+
const description = readStringField(fieldByKey, "description");
|
|
2690
|
+
if (description) state.description = description;
|
|
2691
|
+
} else {
|
|
2692
|
+
throw new ExtractError(
|
|
2693
|
+
"uidex-export-invalid-field",
|
|
2694
|
+
"`states` items must be strings or `{ name, ... }` objects.",
|
|
2695
|
+
item.pos
|
|
2696
|
+
);
|
|
2697
|
+
}
|
|
2698
|
+
if (state.name.length === 0) {
|
|
2699
|
+
throw new ExtractError(
|
|
2700
|
+
"uidex-export-invalid-field",
|
|
2701
|
+
"A `states` entry has an empty `name`.",
|
|
2702
|
+
item.pos
|
|
2703
|
+
);
|
|
2704
|
+
}
|
|
2705
|
+
if (seen.has(state.name)) {
|
|
2706
|
+
throw new ExtractError(
|
|
2707
|
+
"uidex-export-duplicate-state",
|
|
2708
|
+
`Duplicate state "${state.name}" in \`states\`; each state name must be unique within the entity.`,
|
|
2709
|
+
item.pos
|
|
2710
|
+
);
|
|
2711
|
+
}
|
|
2712
|
+
seen.add(state.name);
|
|
2713
|
+
states.push(state);
|
|
2714
|
+
}
|
|
2715
|
+
return states;
|
|
2716
|
+
}
|
|
2013
2717
|
function posAt(content, offset, p2) {
|
|
2014
2718
|
const lineStart = content.lastIndexOf("\n", offset - 1) + 1;
|
|
2015
2719
|
return { offset, line: p2.lineAt(offset), column: offset - lineStart + 1 };
|
|
@@ -2247,22 +2951,22 @@ function collectElementAttrs(opening, consts, dynamicAttrs, lineAt) {
|
|
|
2247
2951
|
if (!kind) continue;
|
|
2248
2952
|
const value = attr.value;
|
|
2249
2953
|
if (!value) continue;
|
|
2250
|
-
let
|
|
2954
|
+
let result = UNRESOLVED;
|
|
2251
2955
|
let valueSpan;
|
|
2252
2956
|
if (value.type === "Literal") {
|
|
2253
2957
|
const v = staticString(value);
|
|
2254
|
-
|
|
2255
|
-
if (
|
|
2958
|
+
result = v !== null && v.length > 0 ? { resolved: true, ids: [v] } : UNRESOLVED;
|
|
2959
|
+
if (result.resolved) valueSpan = { start: value.start, end: value.end };
|
|
2256
2960
|
} else if (value.type === "JSXExpressionContainer") {
|
|
2257
2961
|
const expr = value.expression;
|
|
2258
2962
|
if (expr && expr.type !== "JSXEmptyExpression") {
|
|
2259
|
-
|
|
2963
|
+
result = evalIdExpression(expr, consts);
|
|
2260
2964
|
const inner = unwrapTsExpression(expr);
|
|
2261
|
-
if (
|
|
2965
|
+
if (result.resolved && staticString(inner) !== null) {
|
|
2262
2966
|
valueSpan = { start: inner.start, end: inner.end };
|
|
2263
2967
|
}
|
|
2264
2968
|
}
|
|
2265
|
-
if (!
|
|
2969
|
+
if (!result.resolved) {
|
|
2266
2970
|
dynamicAttrs.push({
|
|
2267
2971
|
kind,
|
|
2268
2972
|
attrName: kind === "element" ? "data-uidex" : `data-uidex-${kind}`,
|
|
@@ -2271,15 +2975,15 @@ function collectElementAttrs(opening, consts, dynamicAttrs, lineAt) {
|
|
|
2271
2975
|
continue;
|
|
2272
2976
|
}
|
|
2273
2977
|
}
|
|
2274
|
-
if (!
|
|
2275
|
-
for (const id of
|
|
2978
|
+
if (!result.resolved) continue;
|
|
2979
|
+
for (const id of result.ids) {
|
|
2276
2980
|
const resolved = {
|
|
2277
2981
|
kind,
|
|
2278
2982
|
id,
|
|
2279
2983
|
start: attr.start,
|
|
2280
2984
|
isPattern: id.includes("*"),
|
|
2281
2985
|
// Only a single plain string literal is renameable in place.
|
|
2282
|
-
...
|
|
2986
|
+
...result.ids.length === 1 && valueSpan ? { span: valueSpan } : {}
|
|
2283
2987
|
};
|
|
2284
2988
|
if (resolved.isPattern) patterns.push(resolved);
|
|
2285
2989
|
else statics.push(resolved);
|
|
@@ -2633,7 +3337,12 @@ var BASE_EXCLUDES = [
|
|
|
2633
3337
|
"**/dist/**",
|
|
2634
3338
|
"**/build/**",
|
|
2635
3339
|
"**/.next/**",
|
|
2636
|
-
|
|
3340
|
+
// Exclude the scanner's own generated siblings so a scan never re-ingests its
|
|
3341
|
+
// output. Enumerated (not `*.gen.*`) so a hand-written file that merely contains
|
|
3342
|
+
// a `.gen.` segment, e.g. `foo.gen.helpers.ts`, is still scanned.
|
|
3343
|
+
"**/*.gen.ts",
|
|
3344
|
+
"**/*.gen.data.ts",
|
|
3345
|
+
"**/*.gen.locs.ts"
|
|
2637
3346
|
];
|
|
2638
3347
|
var TEST_EXCLUDES = [
|
|
2639
3348
|
"**/*.test.ts",
|
|
@@ -2782,6 +3491,18 @@ function buildMetaFromExport(exp) {
|
|
|
2782
3491
|
if (exp.acceptance?.length) meta.acceptance = exp.acceptance;
|
|
2783
3492
|
if (exp.features?.length) meta.features = exp.features;
|
|
2784
3493
|
if (exp.widgets?.length) meta.widgets = exp.widgets;
|
|
3494
|
+
if (exp.states?.length) {
|
|
3495
|
+
meta.states = exp.states.map((s) => ({
|
|
3496
|
+
name: s.name,
|
|
3497
|
+
...s.kind ? { kind: s.kind } : {},
|
|
3498
|
+
...s.acceptance?.length ? { acceptance: s.acceptance } : {},
|
|
3499
|
+
...s.description ? { description: s.description } : {}
|
|
3500
|
+
}));
|
|
3501
|
+
}
|
|
3502
|
+
if (exp.capture === false) meta.capture = false;
|
|
3503
|
+
if (exp.waivers && Object.keys(exp.waivers).length > 0) {
|
|
3504
|
+
meta.waivers = exp.waivers;
|
|
3505
|
+
}
|
|
2785
3506
|
return Object.keys(meta).length > 0 ? meta : void 0;
|
|
2786
3507
|
}
|
|
2787
3508
|
function resolve3(ctx) {
|
|
@@ -3200,6 +3921,68 @@ function dedupe(arr) {
|
|
|
3200
3921
|
}
|
|
3201
3922
|
|
|
3202
3923
|
// src/scanner/scan/pipeline.ts
|
|
3924
|
+
var DEFAULT_STATES_MANIFEST = "uidex-states.json";
|
|
3925
|
+
var DEFAULT_COVERAGE_BASELINE = "uidex-coverage-baseline.json";
|
|
3926
|
+
function loadPageBaseline(configDir, config) {
|
|
3927
|
+
const rel = config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
|
|
3928
|
+
const abs = path9.resolve(configDir, rel);
|
|
3929
|
+
let raw;
|
|
3930
|
+
try {
|
|
3931
|
+
raw = fs6.readFileSync(abs, "utf8");
|
|
3932
|
+
} catch {
|
|
3933
|
+
return void 0;
|
|
3934
|
+
}
|
|
3935
|
+
try {
|
|
3936
|
+
const parsed = JSON.parse(raw);
|
|
3937
|
+
if (!parsed || !Array.isArray(parsed.uncapturedPages)) return void 0;
|
|
3938
|
+
const uncapturedPages = parsed.uncapturedPages.filter(
|
|
3939
|
+
(p2) => typeof p2 === "string"
|
|
3940
|
+
);
|
|
3941
|
+
let missingKinds;
|
|
3942
|
+
const mkRaw = parsed.missingKinds;
|
|
3943
|
+
if (mkRaw && typeof mkRaw === "object" && !Array.isArray(mkRaw)) {
|
|
3944
|
+
const mk = {};
|
|
3945
|
+
for (const [route, kinds] of Object.entries(mkRaw)) {
|
|
3946
|
+
if (Array.isArray(kinds)) {
|
|
3947
|
+
mk[route] = kinds.filter((k) => typeof k === "string");
|
|
3948
|
+
}
|
|
3949
|
+
}
|
|
3950
|
+
missingKinds = mk;
|
|
3951
|
+
}
|
|
3952
|
+
return { uncapturedPages, ...missingKinds ? { missingKinds } : {} };
|
|
3953
|
+
} catch {
|
|
3954
|
+
return void 0;
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3957
|
+
function loadCapturedStates(configDir, config) {
|
|
3958
|
+
const rel = config.statesManifest ?? DEFAULT_STATES_MANIFEST;
|
|
3959
|
+
const abs = path9.resolve(configDir, rel);
|
|
3960
|
+
let raw;
|
|
3961
|
+
try {
|
|
3962
|
+
raw = fs6.readFileSync(abs, "utf8");
|
|
3963
|
+
} catch {
|
|
3964
|
+
return void 0;
|
|
3965
|
+
}
|
|
3966
|
+
try {
|
|
3967
|
+
const parsed = JSON.parse(raw);
|
|
3968
|
+
if (!parsed || !Array.isArray(parsed.captured)) return void 0;
|
|
3969
|
+
const captured = [];
|
|
3970
|
+
for (const entry of parsed.captured) {
|
|
3971
|
+
if (!entry || typeof entry !== "object") continue;
|
|
3972
|
+
const c = entry;
|
|
3973
|
+
if (typeof c.entity !== "string" || typeof c.state !== "string") continue;
|
|
3974
|
+
const rec = { entity: c.entity, state: c.state };
|
|
3975
|
+
if (typeof c.kind === "string") rec.kind = c.kind;
|
|
3976
|
+
if (typeof c.stateKind === "string") rec.stateKind = c.stateKind;
|
|
3977
|
+
if (typeof c.url === "string") rec.url = c.url;
|
|
3978
|
+
if (typeof c.route === "string") rec.route = c.route;
|
|
3979
|
+
captured.push(rec);
|
|
3980
|
+
}
|
|
3981
|
+
return { captured };
|
|
3982
|
+
} catch {
|
|
3983
|
+
return void 0;
|
|
3984
|
+
}
|
|
3985
|
+
}
|
|
3203
3986
|
function runScan(opts = {}) {
|
|
3204
3987
|
const cwd = opts.cwd ?? process.cwd();
|
|
3205
3988
|
const configs = opts.configs ?? discover({ cwd });
|
|
@@ -3226,23 +4009,29 @@ function runOne(dc, opts) {
|
|
|
3226
4009
|
flowFiles: extractedFlows
|
|
3227
4010
|
});
|
|
3228
4011
|
const gitContext = resolveGitContext({ cwd: configDir });
|
|
3229
|
-
const
|
|
4012
|
+
const emitted = emit({
|
|
3230
4013
|
registry: resolved.registry,
|
|
3231
4014
|
gitContext
|
|
3232
4015
|
});
|
|
3233
|
-
const
|
|
3234
|
-
const
|
|
3235
|
-
|
|
4016
|
+
const typesRel = config.output;
|
|
4017
|
+
const typesPath = path9.resolve(configDir, typesRel);
|
|
4018
|
+
const dataRel = dataOutputPath(config.output);
|
|
4019
|
+
const dataPath = path9.resolve(configDir, dataRel);
|
|
4020
|
+
const locsRel = locsOutputPath(config.output);
|
|
4021
|
+
const locsPath = path9.resolve(configDir, locsRel);
|
|
4022
|
+
let typesOnDisk = null;
|
|
4023
|
+
let dataOnDisk = null;
|
|
4024
|
+
let locsOnDisk = null;
|
|
3236
4025
|
if (opts.check) {
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
existingOnDisk = null;
|
|
3241
|
-
}
|
|
4026
|
+
typesOnDisk = readOrNull(typesPath);
|
|
4027
|
+
dataOnDisk = readOrNull(dataPath);
|
|
4028
|
+
locsOnDisk = readOrNull(locsPath);
|
|
3242
4029
|
}
|
|
3243
4030
|
const hasExtractDiagnostics = [...extracted, ...extractedFlows].some(
|
|
3244
4031
|
(ef) => (ef.diagnostics?.length ?? 0) > 0
|
|
3245
4032
|
);
|
|
4033
|
+
const capturedStates = opts.capturedStates ?? loadCapturedStates(configDir, config);
|
|
4034
|
+
const pageBaseline = opts.pageBaseline ?? loadPageBaseline(configDir, config);
|
|
3246
4035
|
let auditResult;
|
|
3247
4036
|
if (opts.check || opts.lint || resolved.diagnostics.length > 0 || hasExtractDiagnostics) {
|
|
3248
4037
|
auditResult = audit({
|
|
@@ -3254,9 +4043,20 @@ function runOne(dc, opts) {
|
|
|
3254
4043
|
check: opts.check,
|
|
3255
4044
|
lint: opts.lint,
|
|
3256
4045
|
resolveDiagnostics: resolved.diagnostics,
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
4046
|
+
// The entity array (and thus the meaningful drift) lives in the data file,
|
|
4047
|
+
// so the entity-aware gen-stale diff runs against it; the types file gets a
|
|
4048
|
+
// plain content compare (see audit()).
|
|
4049
|
+
generated: emitted.data,
|
|
4050
|
+
existingOnDisk: dataOnDisk,
|
|
4051
|
+
outputPath: dataRel,
|
|
4052
|
+
typesGenerated: emitted.types,
|
|
4053
|
+
typesOnDisk,
|
|
4054
|
+
typesOutputPath: typesRel,
|
|
4055
|
+
locsGenerated: emitted.locs,
|
|
4056
|
+
locsOnDisk,
|
|
4057
|
+
locsOutputPath: locsRel,
|
|
4058
|
+
capturedStates,
|
|
4059
|
+
pageBaseline
|
|
3260
4060
|
});
|
|
3261
4061
|
}
|
|
3262
4062
|
return {
|
|
@@ -3265,13 +4065,38 @@ function runOne(dc, opts) {
|
|
|
3265
4065
|
registry: resolved.registry,
|
|
3266
4066
|
gitContext,
|
|
3267
4067
|
audit: auditResult,
|
|
3268
|
-
|
|
3269
|
-
|
|
4068
|
+
types: {
|
|
4069
|
+
generated: emitted.types,
|
|
4070
|
+
outputPath: typesPath,
|
|
4071
|
+
outputRel: typesRel
|
|
4072
|
+
},
|
|
4073
|
+
data: { generated: emitted.data, outputPath: dataPath, outputRel: dataRel },
|
|
4074
|
+
locs: { generated: emitted.locs, outputPath: locsPath, outputRel: locsRel },
|
|
4075
|
+
capturedStates
|
|
3270
4076
|
};
|
|
3271
4077
|
}
|
|
3272
|
-
function
|
|
3273
|
-
|
|
3274
|
-
|
|
4078
|
+
function readOrNull(p2) {
|
|
4079
|
+
try {
|
|
4080
|
+
return fs6.readFileSync(p2, "utf8");
|
|
4081
|
+
} catch {
|
|
4082
|
+
return null;
|
|
4083
|
+
}
|
|
4084
|
+
}
|
|
4085
|
+
function writeArtifact(artifact) {
|
|
4086
|
+
if (readOrNull(artifact.outputPath) === artifact.generated) return false;
|
|
4087
|
+
fs6.mkdirSync(path9.dirname(artifact.outputPath), { recursive: true });
|
|
4088
|
+
fs6.writeFileSync(artifact.outputPath, artifact.generated, "utf8");
|
|
4089
|
+
return true;
|
|
4090
|
+
}
|
|
4091
|
+
function writeScanResult(result) {
|
|
4092
|
+
const wroteTypes = writeArtifact(result.types);
|
|
4093
|
+
const wroteData = writeArtifact(result.data);
|
|
4094
|
+
const wroteLocs = writeArtifact(result.locs);
|
|
4095
|
+
return wroteTypes || wroteData || wroteLocs;
|
|
4096
|
+
}
|
|
4097
|
+
function pageBaselinePath(result) {
|
|
4098
|
+
const rel = result.config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
|
|
4099
|
+
return path9.resolve(result.configDir, rel);
|
|
3275
4100
|
}
|
|
3276
4101
|
|
|
3277
4102
|
// src/scanner/scan/rename.ts
|
|
@@ -3411,7 +4236,7 @@ function renameEntity(opts) {
|
|
|
3411
4236
|
]
|
|
3412
4237
|
};
|
|
3413
4238
|
}
|
|
3414
|
-
const
|
|
4239
|
+
const result = applyFixes([
|
|
3415
4240
|
{
|
|
3416
4241
|
code: "rename",
|
|
3417
4242
|
severity: "info",
|
|
@@ -3422,8 +4247,8 @@ function renameEntity(opts) {
|
|
|
3422
4247
|
}
|
|
3423
4248
|
}
|
|
3424
4249
|
]);
|
|
3425
|
-
if (
|
|
3426
|
-
errors.push(`Some edits were skipped: ${
|
|
4250
|
+
if (result.skipped.length > 0) {
|
|
4251
|
+
errors.push(`Some edits were skipped: ${result.skipped[0].reason}`);
|
|
3427
4252
|
}
|
|
3428
4253
|
for (const r of runScan({ cwd })) writeScanResult(r);
|
|
3429
4254
|
return { edits: edits.length, manual, errors };
|
|
@@ -3522,10 +4347,10 @@ function parseArgs(args) {
|
|
|
3522
4347
|
async function run(opts) {
|
|
3523
4348
|
const cwd = opts.cwd ?? process.cwd();
|
|
3524
4349
|
const { positional, flags } = parseArgs(opts.argv);
|
|
3525
|
-
const
|
|
4350
|
+
const command = positional[0] ?? "help";
|
|
3526
4351
|
const writer = createWriter();
|
|
3527
4352
|
try {
|
|
3528
|
-
switch (
|
|
4353
|
+
switch (command) {
|
|
3529
4354
|
case "help":
|
|
3530
4355
|
case "--help":
|
|
3531
4356
|
case "-h":
|
|
@@ -3540,16 +4365,16 @@ async function run(opts) {
|
|
|
3540
4365
|
case "rename":
|
|
3541
4366
|
return runRename(cwd, positional.slice(1), flags, writer);
|
|
3542
4367
|
case "ai": {
|
|
3543
|
-
const
|
|
4368
|
+
const result = await runAiCommand({
|
|
3544
4369
|
cwd,
|
|
3545
4370
|
argv: opts.argv.slice(1)
|
|
3546
4371
|
});
|
|
3547
|
-
if (
|
|
3548
|
-
if (
|
|
3549
|
-
return writer.result(
|
|
4372
|
+
if (result.stdout) writer.out(result.stdout.replace(/\n$/, ""));
|
|
4373
|
+
if (result.stderr) writer.err(result.stderr.replace(/\n$/, ""));
|
|
4374
|
+
return writer.result(result.exitCode);
|
|
3550
4375
|
}
|
|
3551
4376
|
default:
|
|
3552
|
-
writer.err(`Unknown command: ${
|
|
4377
|
+
writer.err(`Unknown command: ${command}`);
|
|
3553
4378
|
writer.err(helpText2());
|
|
3554
4379
|
return writer.result(1);
|
|
3555
4380
|
}
|
|
@@ -3568,16 +4393,13 @@ function helpText2() {
|
|
|
3568
4393
|
" scaffold <widget|page|feature> <id> Emit a Playwright spec from declared acceptance",
|
|
3569
4394
|
" rename <element|widget|region> <old-id> <new-id> Rename an id everywhere (DOM attr, flows, exports)",
|
|
3570
4395
|
" ai <install|uninstall|providers> Manage AI assistant integrations",
|
|
3571
|
-
" api <METHOD> <PATH> Call the uidex API",
|
|
3572
|
-
" api --list Show available API routes",
|
|
3573
|
-
" api login Authenticate via browser",
|
|
3574
|
-
" api login --token <tok> Store an auth token directly",
|
|
3575
4396
|
"",
|
|
3576
4397
|
"Flags:",
|
|
3577
4398
|
" --check Verify the on-disk gen file matches a fresh scan; exit non-zero on drift (read-only)",
|
|
3578
4399
|
" --lint Run lint diagnostics (missing annotations, scope leak, duplicate ids, coverage)",
|
|
3579
4400
|
" --audit Equivalent to --check --lint (read-only)",
|
|
3580
4401
|
" --fix Apply machine-generated fixes (add data-uidex to unannotated interactive elements, drop empty names), then rescan and write",
|
|
4402
|
+
" --update-baseline Regenerate the page-coverage baseline (uidex-coverage-baseline.json) from the current uncaptured routes",
|
|
3581
4403
|
" --json Emit JSON diagnostics on stdout",
|
|
3582
4404
|
" --force (scaffold) overwrite existing spec",
|
|
3583
4405
|
""
|
|
@@ -3597,7 +4419,7 @@ function runInit(cwd, w) {
|
|
|
3597
4419
|
fs8.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
3598
4420
|
w.out(`Created ${configPath}`);
|
|
3599
4421
|
const gitignorePath = path11.join(cwd, ".gitignore");
|
|
3600
|
-
const entry = "*.gen
|
|
4422
|
+
const entry = "*.gen.*";
|
|
3601
4423
|
if (fs8.existsSync(gitignorePath)) {
|
|
3602
4424
|
const existing = fs8.readFileSync(gitignorePath, "utf8");
|
|
3603
4425
|
const hasEntry = existing.split("\n").some((line) => line.trim() === entry);
|
|
@@ -3620,8 +4442,9 @@ function runInit(cwd, w) {
|
|
|
3620
4442
|
}
|
|
3621
4443
|
function runScanCommand(cwd, flags, w) {
|
|
3622
4444
|
const fix = Boolean(flags.fix);
|
|
3623
|
-
const
|
|
3624
|
-
const
|
|
4445
|
+
const updateBaseline = Boolean(flags["update-baseline"]);
|
|
4446
|
+
const check = !fix && !updateBaseline && Boolean(flags.check || flags.audit);
|
|
4447
|
+
const lint = Boolean(flags.lint || flags.audit || fix || updateBaseline);
|
|
3625
4448
|
const asJson = Boolean(flags.json);
|
|
3626
4449
|
let configs = discover({ cwd });
|
|
3627
4450
|
if (configs.length === 0) {
|
|
@@ -3632,16 +4455,41 @@ function runScanCommand(cwd, flags, w) {
|
|
|
3632
4455
|
let fixSkipped = [];
|
|
3633
4456
|
if (fix) {
|
|
3634
4457
|
const discovery = runScan({ cwd, check: true, lint: true, configs });
|
|
3635
|
-
const
|
|
4458
|
+
const result = applyFixes(
|
|
3636
4459
|
discovery.flatMap((r) => r.audit?.diagnostics ?? [])
|
|
3637
4460
|
);
|
|
3638
|
-
fixed =
|
|
3639
|
-
fixSkipped =
|
|
4461
|
+
fixed = result.applied;
|
|
4462
|
+
fixSkipped = result.skipped;
|
|
3640
4463
|
configs = discover({ cwd });
|
|
3641
4464
|
}
|
|
3642
4465
|
const results = runScan({ cwd, check, lint, configs });
|
|
4466
|
+
const artifacts = results.flatMap((r) => [r.types, r.data, r.locs]);
|
|
4467
|
+
const wrote = /* @__PURE__ */ new Set();
|
|
3643
4468
|
if (!check) {
|
|
3644
|
-
for (const
|
|
4469
|
+
for (const a of artifacts) {
|
|
4470
|
+
if (writeArtifact(a)) wrote.add(a);
|
|
4471
|
+
}
|
|
4472
|
+
}
|
|
4473
|
+
if (updateBaseline) {
|
|
4474
|
+
for (const r of results) {
|
|
4475
|
+
const manifest = r.capturedStates ?? { captured: [] };
|
|
4476
|
+
const pageBaseline = computePageBaseline(r.registry, manifest);
|
|
4477
|
+
const missingKinds = computeMissingKinds(r.registry, manifest);
|
|
4478
|
+
const baseline = {
|
|
4479
|
+
...pageBaseline,
|
|
4480
|
+
...Object.keys(missingKinds).length > 0 ? { missingKinds } : {}
|
|
4481
|
+
};
|
|
4482
|
+
const p2 = pageBaselinePath(r);
|
|
4483
|
+
fs8.writeFileSync(p2, JSON.stringify(baseline, null, 2) + "\n");
|
|
4484
|
+
const kindGaps = Object.values(missingKinds).reduce(
|
|
4485
|
+
(n, ks) => n + ks.length,
|
|
4486
|
+
0
|
|
4487
|
+
);
|
|
4488
|
+
w.out(
|
|
4489
|
+
`Wrote ${p2} \u2014 ${baseline.uncapturedPages.length} uncaptured page(s), ${kindGaps} missing core-kind(s) baselined`
|
|
4490
|
+
);
|
|
4491
|
+
}
|
|
4492
|
+
return w.result(0);
|
|
3645
4493
|
}
|
|
3646
4494
|
const allDiagnostics = results.flatMap((r) => r.audit?.diagnostics ?? []);
|
|
3647
4495
|
const summary = results.reduce(
|
|
@@ -3669,10 +4517,14 @@ function runScanCommand(cwd, flags, w) {
|
|
|
3669
4517
|
);
|
|
3670
4518
|
}
|
|
3671
4519
|
for (const r of results) {
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
4520
|
+
for (const art of [r.types, r.data, r.locs]) {
|
|
4521
|
+
if (check) {
|
|
4522
|
+
w.out(`Checked ${art.outputPath}`);
|
|
4523
|
+
} else if (wrote.has(art)) {
|
|
4524
|
+
w.out(`Wrote ${art.outputPath}`);
|
|
4525
|
+
} else {
|
|
4526
|
+
w.out(`Unchanged ${art.outputPath}`);
|
|
4527
|
+
}
|
|
3676
4528
|
}
|
|
3677
4529
|
for (const d of r.audit?.diagnostics ?? []) {
|
|
3678
4530
|
const loc = d.file ? `${d.file}${d.line ? `:${d.line}` : ""}` : "";
|
|
@@ -3708,18 +4560,18 @@ function runScaffold(cwd, args, flags, w) {
|
|
|
3708
4560
|
const entity = r.registry.get(scaffoldKind, id);
|
|
3709
4561
|
if (!entity) continue;
|
|
3710
4562
|
const outDir = path11.resolve(r.configDir, "e2e");
|
|
3711
|
-
const
|
|
4563
|
+
const result = scaffoldSpec({
|
|
3712
4564
|
registry: r.registry,
|
|
3713
4565
|
kind: scaffoldKind,
|
|
3714
4566
|
id,
|
|
3715
4567
|
outDir,
|
|
3716
4568
|
force: Boolean(flags.force)
|
|
3717
4569
|
});
|
|
3718
|
-
if (
|
|
3719
|
-
w.err(
|
|
4570
|
+
if (result.skipped) {
|
|
4571
|
+
w.err(result.reason ?? "skipped");
|
|
3720
4572
|
return w.result(1);
|
|
3721
4573
|
}
|
|
3722
|
-
w.out(`Wrote ${
|
|
4574
|
+
w.out(`Wrote ${result.outputPath}`);
|
|
3723
4575
|
return w.result(0);
|
|
3724
4576
|
}
|
|
3725
4577
|
w.err(
|
|
@@ -3736,24 +4588,24 @@ function runRename(cwd, args, flags, w) {
|
|
|
3736
4588
|
);
|
|
3737
4589
|
return w.result(1);
|
|
3738
4590
|
}
|
|
3739
|
-
const
|
|
4591
|
+
const result = renameEntity({
|
|
3740
4592
|
cwd,
|
|
3741
4593
|
kind,
|
|
3742
4594
|
oldId,
|
|
3743
4595
|
newId,
|
|
3744
4596
|
force: Boolean(flags.force)
|
|
3745
4597
|
});
|
|
3746
|
-
for (const e of
|
|
3747
|
-
for (const m of
|
|
4598
|
+
for (const e of result.errors) w.err(e);
|
|
4599
|
+
for (const m of result.manual) {
|
|
3748
4600
|
w.err(`MANUAL ${m.file}:${m.line} \u2014 ${m.reason}`);
|
|
3749
4601
|
}
|
|
3750
|
-
if (
|
|
4602
|
+
if (result.errors.length > 0) return w.result(1);
|
|
3751
4603
|
w.out(
|
|
3752
|
-
`Renamed ${kind} "${oldId}" \u2192 "${newId}" (${
|
|
4604
|
+
`Renamed ${kind} "${oldId}" \u2192 "${newId}" (${result.edits} edit(s)); gen file regenerated`
|
|
3753
4605
|
);
|
|
3754
|
-
if (
|
|
4606
|
+
if (result.manual.length > 0) {
|
|
3755
4607
|
w.err(
|
|
3756
|
-
`${
|
|
4608
|
+
`${result.manual.length} occurrence(s) need manual follow-up (listed above)`
|
|
3757
4609
|
);
|
|
3758
4610
|
return w.result(1);
|
|
3759
4611
|
}
|
|
@@ -3775,883 +4627,14 @@ function createWriter() {
|
|
|
3775
4627
|
};
|
|
3776
4628
|
}
|
|
3777
4629
|
|
|
3778
|
-
// src/scanner/cli/auth.ts
|
|
3779
|
-
function createMemoryTokenStorage(initial) {
|
|
3780
|
-
let token = initial ?? null;
|
|
3781
|
-
return {
|
|
3782
|
-
get: () => token,
|
|
3783
|
-
set: (t) => {
|
|
3784
|
-
token = t;
|
|
3785
|
-
},
|
|
3786
|
-
clear: () => {
|
|
3787
|
-
token = null;
|
|
3788
|
-
}
|
|
3789
|
-
};
|
|
3790
|
-
}
|
|
3791
|
-
function createFileTokenStorage(options) {
|
|
3792
|
-
const nodeFs = require("fs");
|
|
3793
|
-
const nodePath = require("path");
|
|
3794
|
-
const file = options.path;
|
|
3795
|
-
function read() {
|
|
3796
|
-
if (!nodeFs.existsSync(file)) return null;
|
|
3797
|
-
try {
|
|
3798
|
-
const raw = nodeFs.readFileSync(file, "utf8");
|
|
3799
|
-
const parsed = JSON.parse(raw);
|
|
3800
|
-
if (parsed && typeof parsed === "object") {
|
|
3801
|
-
return parsed;
|
|
3802
|
-
}
|
|
3803
|
-
} catch {
|
|
3804
|
-
return null;
|
|
3805
|
-
}
|
|
3806
|
-
return null;
|
|
3807
|
-
}
|
|
3808
|
-
return {
|
|
3809
|
-
get: () => read()?.token ?? null,
|
|
3810
|
-
set: (token) => {
|
|
3811
|
-
nodeFs.mkdirSync(nodePath.dirname(file), { recursive: true });
|
|
3812
|
-
nodeFs.writeFileSync(file, JSON.stringify({ token }, null, 2));
|
|
3813
|
-
},
|
|
3814
|
-
clear: () => {
|
|
3815
|
-
if (nodeFs.existsSync(file)) nodeFs.unlinkSync(file);
|
|
3816
|
-
}
|
|
3817
|
-
};
|
|
3818
|
-
}
|
|
3819
|
-
function defaultTokenPath() {
|
|
3820
|
-
const os = require("os");
|
|
3821
|
-
const path12 = require("path");
|
|
3822
|
-
return path12.join(os.homedir(), ".uidex", "cloud-token.json");
|
|
3823
|
-
}
|
|
3824
|
-
|
|
3825
|
-
// src/scanner/cli/http.ts
|
|
3826
|
-
function createHttpClient(options) {
|
|
3827
|
-
const baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
3828
|
-
async function request(path12, opts = {}) {
|
|
3829
|
-
let url = `${baseUrl}${path12.startsWith("/") ? path12 : `/${path12}`}`;
|
|
3830
|
-
if (opts.query) {
|
|
3831
|
-
url += (url.includes("?") ? "&" : "?") + opts.query;
|
|
3832
|
-
}
|
|
3833
|
-
const headers = {
|
|
3834
|
-
Accept: "application/json",
|
|
3835
|
-
...opts.headers
|
|
3836
|
-
};
|
|
3837
|
-
const token = options.token();
|
|
3838
|
-
if (token) headers.Authorization = `Bearer ${token}`;
|
|
3839
|
-
if (opts.body !== void 0) {
|
|
3840
|
-
headers["Content-Type"] ??= "application/json";
|
|
3841
|
-
}
|
|
3842
|
-
const res = await fetch(url, {
|
|
3843
|
-
method: opts.method ?? (opts.body ? "POST" : "GET"),
|
|
3844
|
-
headers,
|
|
3845
|
-
body: opts.body
|
|
3846
|
-
});
|
|
3847
|
-
const raw = await res.text();
|
|
3848
|
-
let body = raw;
|
|
3849
|
-
try {
|
|
3850
|
-
body = JSON.parse(raw);
|
|
3851
|
-
} catch {
|
|
3852
|
-
}
|
|
3853
|
-
return { status: res.status, body, raw };
|
|
3854
|
-
}
|
|
3855
|
-
return { request };
|
|
3856
|
-
}
|
|
3857
|
-
|
|
3858
|
-
// src/scanner/cli/json-highlight.ts
|
|
3859
|
-
var RESET = "\x1B[0m";
|
|
3860
|
-
var BOLD = "\x1B[1m";
|
|
3861
|
-
var DIM = "\x1B[2m";
|
|
3862
|
-
var RED = "\x1B[31m";
|
|
3863
|
-
var CYAN = "\x1B[36m";
|
|
3864
|
-
var GREEN = "\x1B[32m";
|
|
3865
|
-
var YELLOW = "\x1B[33m";
|
|
3866
|
-
var MAGENTA = "\x1B[35m";
|
|
3867
|
-
function highlightJson(value, color) {
|
|
3868
|
-
const json = JSON.stringify(value, null, 2);
|
|
3869
|
-
if (!color) return json;
|
|
3870
|
-
return json.replace(
|
|
3871
|
-
/("(?:\\.|[^"\\])*")\s*:|("(?:\\.|[^"\\])*")|(\b(?:true|false|null)\b)|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g,
|
|
3872
|
-
(match, key, str, literal, num) => {
|
|
3873
|
-
if (key) return `${CYAN}${key}${RESET}:`;
|
|
3874
|
-
if (str) return `${GREEN}${str}${RESET}`;
|
|
3875
|
-
if (literal) return `${MAGENTA}${literal}${RESET}`;
|
|
3876
|
-
if (num) return `${YELLOW}${num}${RESET}`;
|
|
3877
|
-
return match;
|
|
3878
|
-
}
|
|
3879
|
-
);
|
|
3880
|
-
}
|
|
3881
|
-
function formatStatus(status, color) {
|
|
3882
|
-
if (!color) return `HTTP ${status}`;
|
|
3883
|
-
const code = status >= 400 ? RED : status >= 300 ? YELLOW : GREEN;
|
|
3884
|
-
return `${code}HTTP ${status}${RESET}`;
|
|
3885
|
-
}
|
|
3886
|
-
function boldText(text, color) {
|
|
3887
|
-
if (!color) return text;
|
|
3888
|
-
return `${BOLD}${text}${RESET}`;
|
|
3889
|
-
}
|
|
3890
|
-
function dimText(text, color) {
|
|
3891
|
-
if (!color) return text;
|
|
3892
|
-
return `${DIM}${text}${RESET}`;
|
|
3893
|
-
}
|
|
3894
|
-
|
|
3895
|
-
// src/scanner/cli/api-routes.gen.ts
|
|
3896
|
-
var API_ROUTES = [
|
|
3897
|
-
{
|
|
3898
|
-
"method": "POST",
|
|
3899
|
-
"path": "/api/ingest",
|
|
3900
|
-
"operationId": "submitReport",
|
|
3901
|
-
"summary": "Submit feedback from the SDK.",
|
|
3902
|
-
"tag": "Ingest",
|
|
3903
|
-
"params": []
|
|
3904
|
-
},
|
|
3905
|
-
{
|
|
3906
|
-
"method": "GET",
|
|
3907
|
-
"path": "/api/ingest/config",
|
|
3908
|
-
"operationId": "getIngestConfig",
|
|
3909
|
-
"summary": "Get integration configuration for the project.",
|
|
3910
|
-
"tag": "Ingest",
|
|
3911
|
-
"params": []
|
|
3912
|
-
},
|
|
3913
|
-
{
|
|
3914
|
-
"method": "GET",
|
|
3915
|
-
"path": "/api/ingest/reports",
|
|
3916
|
-
"operationId": "listIngestReports",
|
|
3917
|
-
"summary": "List feedback for the project (SDK view).",
|
|
3918
|
-
"tag": "Ingest",
|
|
3919
|
-
"params": []
|
|
3920
|
-
},
|
|
3921
|
-
{
|
|
3922
|
-
"method": "GET",
|
|
3923
|
-
"path": "/api/ingest/pins",
|
|
3924
|
-
"operationId": "listPins",
|
|
3925
|
-
"summary": "List pins by route and/or entity.",
|
|
3926
|
-
"tag": "Ingest",
|
|
3927
|
-
"params": []
|
|
3928
|
-
},
|
|
3929
|
-
{
|
|
3930
|
-
"method": "POST",
|
|
3931
|
-
"path": "/api/ingest/pins/archive",
|
|
3932
|
-
"operationId": "archivePin",
|
|
3933
|
-
"summary": "Archive a pin.",
|
|
3934
|
-
"tag": "Ingest",
|
|
3935
|
-
"params": []
|
|
3936
|
-
},
|
|
3937
|
-
{
|
|
3938
|
-
"method": "POST",
|
|
3939
|
-
"path": "/api/cli-auth/authorize",
|
|
3940
|
-
"operationId": "authorizeCli",
|
|
3941
|
-
"summary": "Authorize a CLI session by delivering a token.",
|
|
3942
|
-
"tag": "CLI Auth",
|
|
3943
|
-
"params": []
|
|
3944
|
-
},
|
|
3945
|
-
{
|
|
3946
|
-
"method": "GET",
|
|
3947
|
-
"path": "/api/organizations",
|
|
3948
|
-
"operationId": "listOrganizations",
|
|
3949
|
-
"summary": "List organizations for the current user.",
|
|
3950
|
-
"tag": "Organizations",
|
|
3951
|
-
"params": []
|
|
3952
|
-
},
|
|
3953
|
-
{
|
|
3954
|
-
"method": "POST",
|
|
3955
|
-
"path": "/api/organizations",
|
|
3956
|
-
"operationId": "createOrganization",
|
|
3957
|
-
"summary": "Create an organization.",
|
|
3958
|
-
"tag": "Organizations",
|
|
3959
|
-
"params": []
|
|
3960
|
-
},
|
|
3961
|
-
{
|
|
3962
|
-
"method": "POST",
|
|
3963
|
-
"path": "/api/organizations/switch",
|
|
3964
|
-
"operationId": "switchOrganization",
|
|
3965
|
-
"summary": "Switch the active organization.",
|
|
3966
|
-
"tag": "Organizations",
|
|
3967
|
-
"params": []
|
|
3968
|
-
},
|
|
3969
|
-
{
|
|
3970
|
-
"method": "GET",
|
|
3971
|
-
"path": "/api/organizations/{orgId}",
|
|
3972
|
-
"operationId": "getOrganization",
|
|
3973
|
-
"summary": "Get organization details.",
|
|
3974
|
-
"tag": "Organizations",
|
|
3975
|
-
"params": [
|
|
3976
|
-
"orgId"
|
|
3977
|
-
]
|
|
3978
|
-
},
|
|
3979
|
-
{
|
|
3980
|
-
"method": "PATCH",
|
|
3981
|
-
"path": "/api/organizations/{orgId}",
|
|
3982
|
-
"operationId": "updateOrganization",
|
|
3983
|
-
"summary": "Update an organization.",
|
|
3984
|
-
"tag": "Organizations",
|
|
3985
|
-
"params": [
|
|
3986
|
-
"orgId"
|
|
3987
|
-
]
|
|
3988
|
-
},
|
|
3989
|
-
{
|
|
3990
|
-
"method": "DELETE",
|
|
3991
|
-
"path": "/api/organizations/{orgId}",
|
|
3992
|
-
"operationId": "deleteOrganization",
|
|
3993
|
-
"summary": "Delete an organization.",
|
|
3994
|
-
"tag": "Organizations",
|
|
3995
|
-
"params": [
|
|
3996
|
-
"orgId"
|
|
3997
|
-
]
|
|
3998
|
-
},
|
|
3999
|
-
{
|
|
4000
|
-
"method": "GET",
|
|
4001
|
-
"path": "/api/organizations/{orgId}/members",
|
|
4002
|
-
"operationId": "listMembers",
|
|
4003
|
-
"summary": "List organization members.",
|
|
4004
|
-
"tag": "Members",
|
|
4005
|
-
"params": [
|
|
4006
|
-
"orgId"
|
|
4007
|
-
]
|
|
4008
|
-
},
|
|
4009
|
-
{
|
|
4010
|
-
"method": "DELETE",
|
|
4011
|
-
"path": "/api/organizations/{orgId}/members",
|
|
4012
|
-
"operationId": "removeMember",
|
|
4013
|
-
"summary": "Remove a member from the organization.",
|
|
4014
|
-
"tag": "Members",
|
|
4015
|
-
"params": [
|
|
4016
|
-
"orgId"
|
|
4017
|
-
]
|
|
4018
|
-
},
|
|
4019
|
-
{
|
|
4020
|
-
"method": "PATCH",
|
|
4021
|
-
"path": "/api/organizations/{orgId}/members/{memberId}",
|
|
4022
|
-
"operationId": "updateMember",
|
|
4023
|
-
"summary": "Update a member's role.",
|
|
4024
|
-
"tag": "Members",
|
|
4025
|
-
"params": [
|
|
4026
|
-
"orgId",
|
|
4027
|
-
"memberId"
|
|
4028
|
-
]
|
|
4029
|
-
},
|
|
4030
|
-
{
|
|
4031
|
-
"method": "GET",
|
|
4032
|
-
"path": "/api/organizations/{orgId}/invitations",
|
|
4033
|
-
"operationId": "listInvitations",
|
|
4034
|
-
"summary": "List organization invitations.",
|
|
4035
|
-
"tag": "Invitations",
|
|
4036
|
-
"params": [
|
|
4037
|
-
"orgId"
|
|
4038
|
-
]
|
|
4039
|
-
},
|
|
4040
|
-
{
|
|
4041
|
-
"method": "POST",
|
|
4042
|
-
"path": "/api/organizations/{orgId}/invitations",
|
|
4043
|
-
"operationId": "createInvitation",
|
|
4044
|
-
"summary": "Create an invitation link.",
|
|
4045
|
-
"tag": "Invitations",
|
|
4046
|
-
"params": [
|
|
4047
|
-
"orgId"
|
|
4048
|
-
]
|
|
4049
|
-
},
|
|
4050
|
-
{
|
|
4051
|
-
"method": "DELETE",
|
|
4052
|
-
"path": "/api/organizations/{orgId}/invitations/{inviteId}",
|
|
4053
|
-
"operationId": "deleteInvitation",
|
|
4054
|
-
"summary": "Revoke an invitation.",
|
|
4055
|
-
"tag": "Invitations",
|
|
4056
|
-
"params": [
|
|
4057
|
-
"orgId",
|
|
4058
|
-
"inviteId"
|
|
4059
|
-
]
|
|
4060
|
-
},
|
|
4061
|
-
{
|
|
4062
|
-
"method": "GET",
|
|
4063
|
-
"path": "/api/organizations/{orgId}/projects",
|
|
4064
|
-
"operationId": "listProjects",
|
|
4065
|
-
"summary": "List projects in an organization.",
|
|
4066
|
-
"tag": "Projects",
|
|
4067
|
-
"params": [
|
|
4068
|
-
"orgId"
|
|
4069
|
-
]
|
|
4070
|
-
},
|
|
4071
|
-
{
|
|
4072
|
-
"method": "POST",
|
|
4073
|
-
"path": "/api/organizations/{orgId}/projects",
|
|
4074
|
-
"operationId": "createProject",
|
|
4075
|
-
"summary": "Create a project.",
|
|
4076
|
-
"tag": "Projects",
|
|
4077
|
-
"params": [
|
|
4078
|
-
"orgId"
|
|
4079
|
-
]
|
|
4080
|
-
},
|
|
4081
|
-
{
|
|
4082
|
-
"method": "GET",
|
|
4083
|
-
"path": "/api/organizations/{orgId}/projects/{projectId}",
|
|
4084
|
-
"operationId": "getProject",
|
|
4085
|
-
"summary": "Get project details.",
|
|
4086
|
-
"tag": "Projects",
|
|
4087
|
-
"params": [
|
|
4088
|
-
"orgId",
|
|
4089
|
-
"projectId"
|
|
4090
|
-
]
|
|
4091
|
-
},
|
|
4092
|
-
{
|
|
4093
|
-
"method": "PATCH",
|
|
4094
|
-
"path": "/api/organizations/{orgId}/projects/{projectId}",
|
|
4095
|
-
"operationId": "updateProject",
|
|
4096
|
-
"summary": "Update a project.",
|
|
4097
|
-
"tag": "Projects",
|
|
4098
|
-
"params": [
|
|
4099
|
-
"orgId",
|
|
4100
|
-
"projectId"
|
|
4101
|
-
]
|
|
4102
|
-
},
|
|
4103
|
-
{
|
|
4104
|
-
"method": "DELETE",
|
|
4105
|
-
"path": "/api/organizations/{orgId}/projects/{projectId}",
|
|
4106
|
-
"operationId": "deleteProject",
|
|
4107
|
-
"summary": "Delete a project.",
|
|
4108
|
-
"tag": "Projects",
|
|
4109
|
-
"params": [
|
|
4110
|
-
"orgId",
|
|
4111
|
-
"projectId"
|
|
4112
|
-
]
|
|
4113
|
-
},
|
|
4114
|
-
{
|
|
4115
|
-
"method": "GET",
|
|
4116
|
-
"path": "/api/projects/resolve",
|
|
4117
|
-
"operationId": "resolveProject",
|
|
4118
|
-
"summary": "Resolve an API key to its project and organization.",
|
|
4119
|
-
"tag": "Projects",
|
|
4120
|
-
"params": []
|
|
4121
|
-
},
|
|
4122
|
-
{
|
|
4123
|
-
"method": "GET",
|
|
4124
|
-
"path": "/api/organizations/{orgId}/projects/{projectId}/api-keys",
|
|
4125
|
-
"operationId": "listApiKeys",
|
|
4126
|
-
"summary": "List API keys for a project.",
|
|
4127
|
-
"tag": "API Keys",
|
|
4128
|
-
"params": [
|
|
4129
|
-
"orgId",
|
|
4130
|
-
"projectId"
|
|
4131
|
-
]
|
|
4132
|
-
},
|
|
4133
|
-
{
|
|
4134
|
-
"method": "POST",
|
|
4135
|
-
"path": "/api/organizations/{orgId}/projects/{projectId}/api-keys",
|
|
4136
|
-
"operationId": "createApiKey",
|
|
4137
|
-
"summary": "Create an API key.",
|
|
4138
|
-
"tag": "API Keys",
|
|
4139
|
-
"params": [
|
|
4140
|
-
"orgId",
|
|
4141
|
-
"projectId"
|
|
4142
|
-
]
|
|
4143
|
-
},
|
|
4144
|
-
{
|
|
4145
|
-
"method": "PATCH",
|
|
4146
|
-
"path": "/api/organizations/{orgId}/projects/{projectId}/api-keys/{keyId}",
|
|
4147
|
-
"operationId": "revokeApiKey",
|
|
4148
|
-
"summary": "Revoke an API key.",
|
|
4149
|
-
"tag": "API Keys",
|
|
4150
|
-
"params": [
|
|
4151
|
-
"orgId",
|
|
4152
|
-
"projectId",
|
|
4153
|
-
"keyId"
|
|
4154
|
-
]
|
|
4155
|
-
},
|
|
4156
|
-
{
|
|
4157
|
-
"method": "DELETE",
|
|
4158
|
-
"path": "/api/organizations/{orgId}/projects/{projectId}/api-keys/{keyId}",
|
|
4159
|
-
"operationId": "deleteApiKey",
|
|
4160
|
-
"summary": "Permanently delete an API key.",
|
|
4161
|
-
"tag": "API Keys",
|
|
4162
|
-
"params": [
|
|
4163
|
-
"orgId",
|
|
4164
|
-
"projectId",
|
|
4165
|
-
"keyId"
|
|
4166
|
-
]
|
|
4167
|
-
},
|
|
4168
|
-
{
|
|
4169
|
-
"method": "GET",
|
|
4170
|
-
"path": "/api/organizations/{orgId}/projects/{projectId}/reports",
|
|
4171
|
-
"operationId": "listProjectReports",
|
|
4172
|
-
"summary": "List feedback for a project.",
|
|
4173
|
-
"tag": "Reports",
|
|
4174
|
-
"params": [
|
|
4175
|
-
"orgId",
|
|
4176
|
-
"projectId"
|
|
4177
|
-
]
|
|
4178
|
-
},
|
|
4179
|
-
{
|
|
4180
|
-
"method": "GET",
|
|
4181
|
-
"path": "/api/organizations/{orgId}/projects/{projectId}/feedback/{reportId}",
|
|
4182
|
-
"operationId": "getReport",
|
|
4183
|
-
"summary": "Get feedback details.",
|
|
4184
|
-
"tag": "Reports",
|
|
4185
|
-
"params": [
|
|
4186
|
-
"orgId",
|
|
4187
|
-
"projectId",
|
|
4188
|
-
"reportId"
|
|
4189
|
-
]
|
|
4190
|
-
},
|
|
4191
|
-
{
|
|
4192
|
-
"method": "PATCH",
|
|
4193
|
-
"path": "/api/organizations/{orgId}/projects/{projectId}/feedback/{reportId}",
|
|
4194
|
-
"operationId": "updateReport",
|
|
4195
|
-
"summary": "Update feedback status, priority, or assignment.",
|
|
4196
|
-
"tag": "Reports",
|
|
4197
|
-
"params": [
|
|
4198
|
-
"orgId",
|
|
4199
|
-
"projectId",
|
|
4200
|
-
"reportId"
|
|
4201
|
-
]
|
|
4202
|
-
},
|
|
4203
|
-
{
|
|
4204
|
-
"method": "DELETE",
|
|
4205
|
-
"path": "/api/organizations/{orgId}/projects/{projectId}/feedback/{reportId}",
|
|
4206
|
-
"operationId": "deleteReport",
|
|
4207
|
-
"summary": "Delete a feedback record.",
|
|
4208
|
-
"tag": "Reports",
|
|
4209
|
-
"params": [
|
|
4210
|
-
"orgId",
|
|
4211
|
-
"projectId",
|
|
4212
|
-
"reportId"
|
|
4213
|
-
]
|
|
4214
|
-
},
|
|
4215
|
-
{
|
|
4216
|
-
"method": "POST",
|
|
4217
|
-
"path": "/api/organizations/{orgId}/projects/{projectId}/reports/bulk-status",
|
|
4218
|
-
"operationId": "bulkUpdateReportStatus",
|
|
4219
|
-
"summary": "Update status on multiple feedback records.",
|
|
4220
|
-
"tag": "Reports",
|
|
4221
|
-
"params": [
|
|
4222
|
-
"orgId",
|
|
4223
|
-
"projectId"
|
|
4224
|
-
]
|
|
4225
|
-
},
|
|
4226
|
-
{
|
|
4227
|
-
"method": "GET",
|
|
4228
|
-
"path": "/api/organizations/{orgId}/integrations",
|
|
4229
|
-
"operationId": "listIntegrations",
|
|
4230
|
-
"summary": "List integrations for an organization.",
|
|
4231
|
-
"tag": "Integrations",
|
|
4232
|
-
"params": [
|
|
4233
|
-
"orgId"
|
|
4234
|
-
]
|
|
4235
|
-
},
|
|
4236
|
-
{
|
|
4237
|
-
"method": "POST",
|
|
4238
|
-
"path": "/api/organizations/{orgId}/integrations",
|
|
4239
|
-
"operationId": "createIntegration",
|
|
4240
|
-
"summary": "Create an integration.",
|
|
4241
|
-
"tag": "Integrations",
|
|
4242
|
-
"params": [
|
|
4243
|
-
"orgId"
|
|
4244
|
-
]
|
|
4245
|
-
},
|
|
4246
|
-
{
|
|
4247
|
-
"method": "GET",
|
|
4248
|
-
"path": "/api/organizations/{orgId}/integrations/{integrationId}",
|
|
4249
|
-
"operationId": "getIntegration",
|
|
4250
|
-
"summary": "Get integration details.",
|
|
4251
|
-
"tag": "Integrations",
|
|
4252
|
-
"params": [
|
|
4253
|
-
"orgId",
|
|
4254
|
-
"integrationId"
|
|
4255
|
-
]
|
|
4256
|
-
},
|
|
4257
|
-
{
|
|
4258
|
-
"method": "DELETE",
|
|
4259
|
-
"path": "/api/organizations/{orgId}/integrations/{integrationId}",
|
|
4260
|
-
"operationId": "deleteIntegration",
|
|
4261
|
-
"summary": "Delete an integration.",
|
|
4262
|
-
"tag": "Integrations",
|
|
4263
|
-
"params": [
|
|
4264
|
-
"orgId",
|
|
4265
|
-
"integrationId"
|
|
4266
|
-
]
|
|
4267
|
-
},
|
|
4268
|
-
{
|
|
4269
|
-
"method": "GET",
|
|
4270
|
-
"path": "/api/organizations/{orgId}/integrations/{integrationId}/targets",
|
|
4271
|
-
"operationId": "listIntegrationTargets",
|
|
4272
|
-
"summary": "List available targets (e.g. Jira projects/boards).",
|
|
4273
|
-
"tag": "Integrations",
|
|
4274
|
-
"params": [
|
|
4275
|
-
"orgId",
|
|
4276
|
-
"integrationId"
|
|
4277
|
-
]
|
|
4278
|
-
},
|
|
4279
|
-
{
|
|
4280
|
-
"method": "POST",
|
|
4281
|
-
"path": "/api/organizations/{orgId}/integrations/{integrationId}/test",
|
|
4282
|
-
"operationId": "testIntegration",
|
|
4283
|
-
"summary": "Test integration connectivity.",
|
|
4284
|
-
"tag": "Integrations",
|
|
4285
|
-
"params": [
|
|
4286
|
-
"orgId",
|
|
4287
|
-
"integrationId"
|
|
4288
|
-
]
|
|
4289
|
-
},
|
|
4290
|
-
{
|
|
4291
|
-
"method": "GET",
|
|
4292
|
-
"path": "/api/organizations/{orgId}/external-issues",
|
|
4293
|
-
"operationId": "listExternalIssues",
|
|
4294
|
-
"summary": "List external issues, optionally filtered by feedback.",
|
|
4295
|
-
"tag": "External Issues",
|
|
4296
|
-
"params": [
|
|
4297
|
-
"orgId"
|
|
4298
|
-
]
|
|
4299
|
-
},
|
|
4300
|
-
{
|
|
4301
|
-
"method": "GET",
|
|
4302
|
-
"path": "/api/organizations/{orgId}/issue-drafts",
|
|
4303
|
-
"operationId": "listIssueDrafts",
|
|
4304
|
-
"summary": "List issue drafts for a project.",
|
|
4305
|
-
"tag": "Issue Drafts",
|
|
4306
|
-
"params": [
|
|
4307
|
-
"orgId"
|
|
4308
|
-
]
|
|
4309
|
-
},
|
|
4310
|
-
{
|
|
4311
|
-
"method": "POST",
|
|
4312
|
-
"path": "/api/organizations/{orgId}/issue-drafts",
|
|
4313
|
-
"operationId": "createIssueDraft",
|
|
4314
|
-
"summary": "Manually create an issue draft.",
|
|
4315
|
-
"tag": "Issue Drafts",
|
|
4316
|
-
"params": [
|
|
4317
|
-
"orgId"
|
|
4318
|
-
]
|
|
4319
|
-
},
|
|
4320
|
-
{
|
|
4321
|
-
"method": "PATCH",
|
|
4322
|
-
"path": "/api/organizations/{orgId}/issue-drafts/{draftId}",
|
|
4323
|
-
"operationId": "updateIssueDraft",
|
|
4324
|
-
"summary": "Update an issue draft.",
|
|
4325
|
-
"tag": "Issue Drafts",
|
|
4326
|
-
"params": [
|
|
4327
|
-
"orgId",
|
|
4328
|
-
"draftId"
|
|
4329
|
-
]
|
|
4330
|
-
},
|
|
4331
|
-
{
|
|
4332
|
-
"method": "DELETE",
|
|
4333
|
-
"path": "/api/organizations/{orgId}/issue-drafts/{draftId}",
|
|
4334
|
-
"operationId": "deleteIssueDraft",
|
|
4335
|
-
"summary": "Delete an issue draft.",
|
|
4336
|
-
"tag": "Issue Drafts",
|
|
4337
|
-
"params": [
|
|
4338
|
-
"orgId",
|
|
4339
|
-
"draftId"
|
|
4340
|
-
]
|
|
4341
|
-
},
|
|
4342
|
-
{
|
|
4343
|
-
"method": "POST",
|
|
4344
|
-
"path": "/api/organizations/{orgId}/issue-drafts/{draftId}/submit",
|
|
4345
|
-
"operationId": "submitIssueDraft",
|
|
4346
|
-
"summary": "Submit a draft to the configured integration.",
|
|
4347
|
-
"tag": "Issue Drafts",
|
|
4348
|
-
"params": [
|
|
4349
|
-
"orgId",
|
|
4350
|
-
"draftId"
|
|
4351
|
-
]
|
|
4352
|
-
},
|
|
4353
|
-
{
|
|
4354
|
-
"method": "POST",
|
|
4355
|
-
"path": "/api/organizations/{orgId}/projects/{projectId}/triage",
|
|
4356
|
-
"operationId": "runTriage",
|
|
4357
|
-
"summary": "Run LLM-powered triage on untriaged feedback.",
|
|
4358
|
-
"tag": "Triage",
|
|
4359
|
-
"params": [
|
|
4360
|
-
"orgId",
|
|
4361
|
-
"projectId"
|
|
4362
|
-
]
|
|
4363
|
-
},
|
|
4364
|
-
{
|
|
4365
|
-
"method": "GET",
|
|
4366
|
-
"path": "/api/user/tokens",
|
|
4367
|
-
"operationId": "listUserTokens",
|
|
4368
|
-
"summary": "List personal access tokens.",
|
|
4369
|
-
"tag": "User Tokens",
|
|
4370
|
-
"params": []
|
|
4371
|
-
},
|
|
4372
|
-
{
|
|
4373
|
-
"method": "POST",
|
|
4374
|
-
"path": "/api/user/tokens",
|
|
4375
|
-
"operationId": "createUserToken",
|
|
4376
|
-
"summary": "Create a personal access token.",
|
|
4377
|
-
"tag": "User Tokens",
|
|
4378
|
-
"params": []
|
|
4379
|
-
},
|
|
4380
|
-
{
|
|
4381
|
-
"method": "DELETE",
|
|
4382
|
-
"path": "/api/user/tokens/{tokenId}",
|
|
4383
|
-
"operationId": "deleteUserToken",
|
|
4384
|
-
"summary": "Revoke and delete a personal access token.",
|
|
4385
|
-
"tag": "User Tokens",
|
|
4386
|
-
"params": [
|
|
4387
|
-
"tokenId"
|
|
4388
|
-
]
|
|
4389
|
-
}
|
|
4390
|
-
];
|
|
4391
|
-
|
|
4392
|
-
// src/scanner/cli/api.ts
|
|
4393
|
-
var METHODS = /* @__PURE__ */ new Set([
|
|
4394
|
-
"GET",
|
|
4395
|
-
"POST",
|
|
4396
|
-
"PUT",
|
|
4397
|
-
"PATCH",
|
|
4398
|
-
"DELETE",
|
|
4399
|
-
"HEAD",
|
|
4400
|
-
"OPTIONS"
|
|
4401
|
-
]);
|
|
4402
|
-
var HELP = `uidex api \u2014 call the uidex API
|
|
4403
|
-
|
|
4404
|
-
Usage:
|
|
4405
|
-
uidex api <METHOD> <PATH> [flags] Make an API request
|
|
4406
|
-
uidex api --list [--tag <tag>] List available routes
|
|
4407
|
-
uidex api login Authenticate via browser
|
|
4408
|
-
uidex api login --token <tok> Store an auth token directly
|
|
4409
|
-
uidex api status Check auth state
|
|
4410
|
-
|
|
4411
|
-
Flags:
|
|
4412
|
-
--body <json> Request body (JSON string)
|
|
4413
|
-
--query <params> Query string (key=val&key2=val2)
|
|
4414
|
-
--base-url <url> Override the API base URL
|
|
4415
|
-
--token <tok> Use a specific token (instead of stored)
|
|
4416
|
-
--raw Disable JSON pretty-printing
|
|
4417
|
-
--no-color Disable colored output
|
|
4418
|
-
--list List available API routes
|
|
4419
|
-
--tag <tag> Filter --list by tag
|
|
4420
|
-
--json Emit --list as JSON
|
|
4421
|
-
`;
|
|
4422
|
-
async function runApiCommand(opts) {
|
|
4423
|
-
const { positional, flags } = parseArgs(opts.argv);
|
|
4424
|
-
const stdout = [];
|
|
4425
|
-
const stderr = [];
|
|
4426
|
-
const color = opts.color ?? (flags["no-color"] ? false : process.stdout.isTTY ?? false);
|
|
4427
|
-
const ctx = { flags, opts, color, stdout, stderr };
|
|
4428
|
-
const sub = positional[0];
|
|
4429
|
-
if (!sub && !flags.list) {
|
|
4430
|
-
stdout.push(HELP);
|
|
4431
|
-
return result(0, stdout, stderr);
|
|
4432
|
-
}
|
|
4433
|
-
if (flags.list) return listRoutes(ctx);
|
|
4434
|
-
if (sub === "help" || sub === "--help" || sub === "-h") {
|
|
4435
|
-
stdout.push(HELP);
|
|
4436
|
-
return result(0, stdout, stderr);
|
|
4437
|
-
}
|
|
4438
|
-
if (sub === "login") return runLogin(ctx);
|
|
4439
|
-
if (sub === "status") return runStatus(ctx);
|
|
4440
|
-
const method = sub?.toUpperCase();
|
|
4441
|
-
const path12 = positional[1];
|
|
4442
|
-
if (!method || !METHODS.has(method)) {
|
|
4443
|
-
stderr.push(`Unknown command or method: ${sub}`);
|
|
4444
|
-
stderr.push(HELP);
|
|
4445
|
-
return result(1, stdout, stderr);
|
|
4446
|
-
}
|
|
4447
|
-
if (!path12) {
|
|
4448
|
-
stderr.push("Missing path. Usage: uidex api GET /api/organizations");
|
|
4449
|
-
return result(1, stdout, stderr);
|
|
4450
|
-
}
|
|
4451
|
-
return runRequest(ctx, method, path12);
|
|
4452
|
-
}
|
|
4453
|
-
function listRoutes(ctx) {
|
|
4454
|
-
const { flags, color, stdout, stderr } = ctx;
|
|
4455
|
-
let routes = API_ROUTES;
|
|
4456
|
-
const tagFilter = flags.tag;
|
|
4457
|
-
if (typeof tagFilter === "string") {
|
|
4458
|
-
routes = routes.filter(
|
|
4459
|
-
(r) => r.tag.toLowerCase() === tagFilter.toLowerCase()
|
|
4460
|
-
);
|
|
4461
|
-
}
|
|
4462
|
-
if (flags.json) {
|
|
4463
|
-
stdout.push(JSON.stringify(routes, null, 2));
|
|
4464
|
-
return result(0, stdout, stderr);
|
|
4465
|
-
}
|
|
4466
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
4467
|
-
for (const r of routes) {
|
|
4468
|
-
const tag = r.tag || "Other";
|
|
4469
|
-
let list = grouped.get(tag);
|
|
4470
|
-
if (!list) {
|
|
4471
|
-
list = [];
|
|
4472
|
-
grouped.set(tag, list);
|
|
4473
|
-
}
|
|
4474
|
-
list.push(r);
|
|
4475
|
-
}
|
|
4476
|
-
const methodPad = 7;
|
|
4477
|
-
for (const [tag, tagRoutes] of grouped) {
|
|
4478
|
-
stdout.push("");
|
|
4479
|
-
stdout.push(boldText(tag, color));
|
|
4480
|
-
for (const r of tagRoutes) {
|
|
4481
|
-
const m = r.method.padEnd(methodPad);
|
|
4482
|
-
const desc = r.summary ? dimText(` ${r.operationId} \u2014 ${r.summary}`, color) : dimText(` ${r.operationId}`, color);
|
|
4483
|
-
stdout.push(` ${m} ${r.path}${desc}`);
|
|
4484
|
-
}
|
|
4485
|
-
}
|
|
4486
|
-
stdout.push("");
|
|
4487
|
-
return result(0, stdout, stderr);
|
|
4488
|
-
}
|
|
4489
|
-
async function runLogin(ctx) {
|
|
4490
|
-
const { flags, opts, stdout, stderr } = ctx;
|
|
4491
|
-
const token = flags.token;
|
|
4492
|
-
if (typeof token === "string" && token.length > 0) {
|
|
4493
|
-
const storage = resolveTokenStorage(opts);
|
|
4494
|
-
storage.set(token);
|
|
4495
|
-
stdout.push("Token stored.");
|
|
4496
|
-
return result(0, stdout, stderr);
|
|
4497
|
-
}
|
|
4498
|
-
return runBrowserLogin(ctx);
|
|
4499
|
-
}
|
|
4500
|
-
async function runBrowserLogin(ctx) {
|
|
4501
|
-
const { flags, opts, color, stdout, stderr } = ctx;
|
|
4502
|
-
const http = await import("http");
|
|
4503
|
-
const crypto = await import("crypto");
|
|
4504
|
-
const { exec } = await import("child_process");
|
|
4505
|
-
const baseUrl = resolveBaseUrl(flags, opts);
|
|
4506
|
-
const code = crypto.randomBytes(3).toString("hex").toUpperCase();
|
|
4507
|
-
return new Promise((resolve7) => {
|
|
4508
|
-
let settled = false;
|
|
4509
|
-
const settle = (exitCode) => {
|
|
4510
|
-
if (settled) return;
|
|
4511
|
-
settled = true;
|
|
4512
|
-
server.close();
|
|
4513
|
-
clearTimeout(timeout);
|
|
4514
|
-
resolve7(result(exitCode, stdout, stderr));
|
|
4515
|
-
};
|
|
4516
|
-
const timeout = setTimeout(() => {
|
|
4517
|
-
stderr.push("Timed out waiting for browser authorization.");
|
|
4518
|
-
settle(1);
|
|
4519
|
-
}, 12e4);
|
|
4520
|
-
const server = http.createServer((req, res) => {
|
|
4521
|
-
if (!req.url?.startsWith("/callback")) {
|
|
4522
|
-
res.writeHead(404);
|
|
4523
|
-
res.end();
|
|
4524
|
-
return;
|
|
4525
|
-
}
|
|
4526
|
-
const url = new URL(req.url, `http://127.0.0.1`);
|
|
4527
|
-
const receivedToken = url.searchParams.get("token");
|
|
4528
|
-
const receivedCode = url.searchParams.get("code");
|
|
4529
|
-
if (receivedCode !== code) {
|
|
4530
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4531
|
-
res.end(JSON.stringify({ error: "Code mismatch" }));
|
|
4532
|
-
stderr.push("Received callback with mismatched code. Aborting.");
|
|
4533
|
-
settle(1);
|
|
4534
|
-
return;
|
|
4535
|
-
}
|
|
4536
|
-
if (!receivedToken) {
|
|
4537
|
-
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4538
|
-
res.end(JSON.stringify({ error: "Missing token" }));
|
|
4539
|
-
stderr.push("Received callback without token. Aborting.");
|
|
4540
|
-
settle(1);
|
|
4541
|
-
return;
|
|
4542
|
-
}
|
|
4543
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4544
|
-
res.end(JSON.stringify({ ok: true }));
|
|
4545
|
-
const storage = resolveTokenStorage(opts);
|
|
4546
|
-
storage.set(receivedToken);
|
|
4547
|
-
stdout.push(dimText("Authenticated successfully.", color));
|
|
4548
|
-
settle(0);
|
|
4549
|
-
});
|
|
4550
|
-
server.listen(0, "127.0.0.1", () => {
|
|
4551
|
-
const addr = server.address();
|
|
4552
|
-
if (!addr || typeof addr === "string") {
|
|
4553
|
-
stderr.push("Failed to start local server.");
|
|
4554
|
-
settle(1);
|
|
4555
|
-
return;
|
|
4556
|
-
}
|
|
4557
|
-
const port = addr.port;
|
|
4558
|
-
const authUrl = `${baseUrl}/cli-auth?code=${encodeURIComponent(code)}&port=${port}`;
|
|
4559
|
-
stdout.push(dimText("Opening browser to authorize...", color));
|
|
4560
|
-
stdout.push("");
|
|
4561
|
-
stdout.push(` Confirmation code: ${boldText(code, color)}`);
|
|
4562
|
-
stdout.push("");
|
|
4563
|
-
stdout.push(
|
|
4564
|
-
dimText("If the browser doesn't open, visit this URL manually:", color)
|
|
4565
|
-
);
|
|
4566
|
-
stdout.push(` ${authUrl}`);
|
|
4567
|
-
stdout.push("");
|
|
4568
|
-
process.stdout.write(result(0, stdout, stderr).stdout);
|
|
4569
|
-
stdout.length = 0;
|
|
4570
|
-
openBrowser(authUrl, exec);
|
|
4571
|
-
});
|
|
4572
|
-
server.on("error", (err2) => {
|
|
4573
|
-
stderr.push(`Failed to start local server: ${err2.message}`);
|
|
4574
|
-
settle(1);
|
|
4575
|
-
});
|
|
4576
|
-
});
|
|
4577
|
-
}
|
|
4578
|
-
function openBrowser(url, exec) {
|
|
4579
|
-
const platform = process.platform;
|
|
4580
|
-
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
4581
|
-
exec(`${cmd} ${JSON.stringify(url)}`);
|
|
4582
|
-
}
|
|
4583
|
-
function runStatus(ctx) {
|
|
4584
|
-
const { flags, opts, stdout, stderr } = ctx;
|
|
4585
|
-
const storage = resolveTokenStorage(opts);
|
|
4586
|
-
const baseUrl = resolveBaseUrl(flags, opts);
|
|
4587
|
-
const token = storage.get();
|
|
4588
|
-
stdout.push(`baseUrl: ${baseUrl}`);
|
|
4589
|
-
stdout.push(`auth: ${token ? "authenticated" : "not authenticated"}`);
|
|
4590
|
-
return result(token ? 0 : 1, stdout, stderr);
|
|
4591
|
-
}
|
|
4592
|
-
async function runRequest(ctx, method, path12) {
|
|
4593
|
-
const { flags, opts, color, stdout, stderr } = ctx;
|
|
4594
|
-
const tokenFromFlag = flags.token;
|
|
4595
|
-
const token = typeof tokenFromFlag === "string" ? tokenFromFlag : resolveTokenStorage(opts).get();
|
|
4596
|
-
if (!token) {
|
|
4597
|
-
stderr.push("Not authenticated. Run: uidex api login --token <value>");
|
|
4598
|
-
return result(1, stdout, stderr);
|
|
4599
|
-
}
|
|
4600
|
-
const baseUrl = resolveBaseUrl(flags, opts);
|
|
4601
|
-
const http = opts.http ?? createHttpClient({ baseUrl, token: () => token });
|
|
4602
|
-
const bodyStr = typeof flags.body === "string" ? flags.body : void 0;
|
|
4603
|
-
if (bodyStr !== void 0) {
|
|
4604
|
-
try {
|
|
4605
|
-
JSON.parse(bodyStr);
|
|
4606
|
-
} catch {
|
|
4607
|
-
stderr.push("Invalid JSON in --body");
|
|
4608
|
-
return result(1, stdout, stderr);
|
|
4609
|
-
}
|
|
4610
|
-
}
|
|
4611
|
-
const res = await http.request(path12, {
|
|
4612
|
-
method,
|
|
4613
|
-
body: bodyStr,
|
|
4614
|
-
query: typeof flags.query === "string" ? flags.query : void 0
|
|
4615
|
-
});
|
|
4616
|
-
const isSuccess = res.status >= 200 && res.status < 300;
|
|
4617
|
-
if (!isSuccess) {
|
|
4618
|
-
stderr.push(formatStatus(res.status, color));
|
|
4619
|
-
}
|
|
4620
|
-
if (res.body !== void 0 && res.body !== null && res.body !== "") {
|
|
4621
|
-
if (flags.raw) {
|
|
4622
|
-
stdout.push(res.raw);
|
|
4623
|
-
} else {
|
|
4624
|
-
stdout.push(highlightJson(res.body, color));
|
|
4625
|
-
}
|
|
4626
|
-
}
|
|
4627
|
-
return result(isSuccess ? 0 : 1, stdout, stderr);
|
|
4628
|
-
}
|
|
4629
|
-
function resolveTokenStorage(opts) {
|
|
4630
|
-
if (opts.tokenStorage) return opts.tokenStorage;
|
|
4631
|
-
const envToken = opts.env?.UIDEX_TOKEN;
|
|
4632
|
-
if (envToken) return createMemoryTokenStorage(envToken);
|
|
4633
|
-
return createFileTokenStorage({ path: defaultTokenPath() });
|
|
4634
|
-
}
|
|
4635
|
-
function resolveBaseUrl(flags, opts) {
|
|
4636
|
-
return (typeof flags["base-url"] === "string" ? flags["base-url"] : void 0) ?? opts.baseUrl ?? opts.env?.UIDEX_API_URL ?? "https://app.uidex.dev";
|
|
4637
|
-
}
|
|
4638
|
-
function result(exitCode, stdout, stderr) {
|
|
4639
|
-
return {
|
|
4640
|
-
exitCode,
|
|
4641
|
-
stdout: stdout.join("\n") + (stdout.length ? "\n" : ""),
|
|
4642
|
-
stderr: stderr.join("\n") + (stderr.length ? "\n" : "")
|
|
4643
|
-
};
|
|
4644
|
-
}
|
|
4645
|
-
|
|
4646
4630
|
// src/scanner/cli/cli.ts
|
|
4647
4631
|
var argv = process.argv.slice(2);
|
|
4648
|
-
var
|
|
4649
|
-
var resultP = command === "api" ? runApiCommand({ argv: argv.slice(1) }) : run({ argv });
|
|
4632
|
+
var resultP = run({ argv });
|
|
4650
4633
|
resultP.then(
|
|
4651
|
-
(
|
|
4652
|
-
if (
|
|
4653
|
-
if (
|
|
4654
|
-
process.exit(
|
|
4634
|
+
(result) => {
|
|
4635
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
4636
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
4637
|
+
process.exit(result.exitCode);
|
|
4655
4638
|
},
|
|
4656
4639
|
(error) => {
|
|
4657
4640
|
process.stderr.write(
|