@iamu-dev/iamu 0.2.0-beta.7 → 0.2.0-beta.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +1562 -236
- package/dist/index.js +1558 -232
- package/dist/templates/skills/iamu-new.md +3 -0
- package/dist/templates/skills/iamu-propose.md +6 -1
- package/dist/templates/skills/iamu-run.md +5 -1
- package/dist/templates/skills/iamu-setup.md +35 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10487,9 +10487,275 @@ var init_prune = __esm({
|
|
|
10487
10487
|
}
|
|
10488
10488
|
});
|
|
10489
10489
|
|
|
10490
|
-
// ../../packages/local-store/dist/
|
|
10490
|
+
// ../../packages/local-store/dist/explore-map.js
|
|
10491
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
10491
10492
|
import { promises as fs19 } from "fs";
|
|
10492
10493
|
import path15 from "path";
|
|
10494
|
+
function templateToken(token) {
|
|
10495
|
+
if (token === "")
|
|
10496
|
+
return token;
|
|
10497
|
+
if (/^\d+$/.test(token))
|
|
10498
|
+
return ":id";
|
|
10499
|
+
if (UUID.test(token))
|
|
10500
|
+
return ":id";
|
|
10501
|
+
if (DATE.test(token))
|
|
10502
|
+
return ":date";
|
|
10503
|
+
if (/^[0-9a-f]{8,}$/i.test(token) && /\d/.test(token))
|
|
10504
|
+
return ":id";
|
|
10505
|
+
if (token.length >= 16 && /\d/.test(token) && /[a-z]/i.test(token)) {
|
|
10506
|
+
return ":id";
|
|
10507
|
+
}
|
|
10508
|
+
return token;
|
|
10509
|
+
}
|
|
10510
|
+
function templatePath(pathname) {
|
|
10511
|
+
const trimmed = pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname || "/";
|
|
10512
|
+
return trimmed.split("/").map((seg) => templateToken(decodeSegment(seg))).join("/");
|
|
10513
|
+
}
|
|
10514
|
+
function encodeQueryValue(v) {
|
|
10515
|
+
return v.startsWith(":") ? v : encodeURIComponent(v);
|
|
10516
|
+
}
|
|
10517
|
+
function decodeSegment(seg) {
|
|
10518
|
+
try {
|
|
10519
|
+
return decodeURIComponent(seg);
|
|
10520
|
+
} catch {
|
|
10521
|
+
return seg;
|
|
10522
|
+
}
|
|
10523
|
+
}
|
|
10524
|
+
function explorePlaceKey(raw) {
|
|
10525
|
+
if (!raw)
|
|
10526
|
+
return null;
|
|
10527
|
+
let u;
|
|
10528
|
+
try {
|
|
10529
|
+
u = new URL(raw);
|
|
10530
|
+
} catch {
|
|
10531
|
+
return null;
|
|
10532
|
+
}
|
|
10533
|
+
if (u.protocol !== "http:" && u.protocol !== "https:")
|
|
10534
|
+
return null;
|
|
10535
|
+
const params = [...u.searchParams.entries()].filter(([k]) => !k.toLowerCase().startsWith("utm_")).map(([k, v]) => [k, templateToken(v)]).sort(([a, av], [b, bv]) => a.localeCompare(b) || av.localeCompare(bv));
|
|
10536
|
+
const query = params.map(([k, v]) => `${encodeURIComponent(k)}=${encodeQueryValue(v)}`).join("&");
|
|
10537
|
+
let key = templatePath(u.pathname);
|
|
10538
|
+
if (query)
|
|
10539
|
+
key += `?${query}`;
|
|
10540
|
+
const route = /^#!?(\/.*)$/.exec(u.hash);
|
|
10541
|
+
if (route) {
|
|
10542
|
+
const [hashPath] = route[1].split("?");
|
|
10543
|
+
key += `#${templatePath(hashPath)}`;
|
|
10544
|
+
}
|
|
10545
|
+
return key;
|
|
10546
|
+
}
|
|
10547
|
+
function exploreViewSignature(headings) {
|
|
10548
|
+
return headings.map((h) => h.trim().replace(/\s+/g, " ").replace(/\d+/g, "#").toLowerCase()).filter(Boolean).join(" | ");
|
|
10549
|
+
}
|
|
10550
|
+
function exploreSignature(headings, title) {
|
|
10551
|
+
return exploreViewSignature(headings) || title.trim().toLowerCase();
|
|
10552
|
+
}
|
|
10553
|
+
function matchExploreSurface(map2, url, signature) {
|
|
10554
|
+
const place = explorePlaceKey(url);
|
|
10555
|
+
if (!place)
|
|
10556
|
+
return null;
|
|
10557
|
+
const atPlace = map2.surfaces.filter((s) => s.place === place);
|
|
10558
|
+
return atPlace.find((s) => s.signature === signature) ?? (atPlace.length === 1 ? atPlace[0] : null);
|
|
10559
|
+
}
|
|
10560
|
+
function exploreSurfaceKey(place, view) {
|
|
10561
|
+
return view === null ? place : `${place}~${fnv1a(view)}`;
|
|
10562
|
+
}
|
|
10563
|
+
function fnv1a(s) {
|
|
10564
|
+
let h = 2166136261;
|
|
10565
|
+
for (let i = 0; i < s.length; i++) {
|
|
10566
|
+
h ^= s.charCodeAt(i);
|
|
10567
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
10568
|
+
}
|
|
10569
|
+
return h.toString(16).padStart(8, "0");
|
|
10570
|
+
}
|
|
10571
|
+
function exploreDir(projectRoot) {
|
|
10572
|
+
return path15.join(cacheDir(projectRoot), "explore");
|
|
10573
|
+
}
|
|
10574
|
+
function composeExploreId(now, randomHex) {
|
|
10575
|
+
const ts = now.toISOString().replace(/[:.]/g, "-").replace(/Z$/, "");
|
|
10576
|
+
return `${ts}-${randomHex ?? randomBytes2(3).toString("hex")}`;
|
|
10577
|
+
}
|
|
10578
|
+
function isValidExploreId(id) {
|
|
10579
|
+
return MAP_ID.test(id);
|
|
10580
|
+
}
|
|
10581
|
+
function mapPath(projectRoot, id) {
|
|
10582
|
+
if (!isValidExploreId(id))
|
|
10583
|
+
throw new Error(`Not an explore map id: ${id}`);
|
|
10584
|
+
return path15.join(exploreDir(projectRoot), id, "map.json");
|
|
10585
|
+
}
|
|
10586
|
+
async function writeExploreMap(projectRoot, map2) {
|
|
10587
|
+
const file = mapPath(projectRoot, map2.id);
|
|
10588
|
+
await fs19.mkdir(path15.dirname(file), { recursive: true });
|
|
10589
|
+
await atomicWriteFile(file, `${JSON.stringify(map2, null, 2)}
|
|
10590
|
+
`);
|
|
10591
|
+
return file;
|
|
10592
|
+
}
|
|
10593
|
+
async function readExploreMap(projectRoot, id) {
|
|
10594
|
+
const raw = await fs19.readFile(mapPath(projectRoot, id), "utf8");
|
|
10595
|
+
const map2 = JSON.parse(raw);
|
|
10596
|
+
if (map2?.schema !== EXPLORE_MAP_SCHEMA || !Array.isArray(map2.surfaces)) {
|
|
10597
|
+
throw new Error(`Unreadable explore map: ${id}`);
|
|
10598
|
+
}
|
|
10599
|
+
return map2;
|
|
10600
|
+
}
|
|
10601
|
+
async function listExploreMaps(projectRoot) {
|
|
10602
|
+
try {
|
|
10603
|
+
const entries = await fs19.readdir(exploreDir(projectRoot), {
|
|
10604
|
+
withFileTypes: true
|
|
10605
|
+
});
|
|
10606
|
+
return entries.filter((e) => e.isDirectory() && isValidExploreId(e.name)).map((e) => e.name).sort();
|
|
10607
|
+
} catch (err) {
|
|
10608
|
+
if (err.code === "ENOENT")
|
|
10609
|
+
return [];
|
|
10610
|
+
throw err;
|
|
10611
|
+
}
|
|
10612
|
+
}
|
|
10613
|
+
function exploreCoverage(map2, steps) {
|
|
10614
|
+
const byPlace = /* @__PURE__ */ new Map();
|
|
10615
|
+
for (const s of map2.surfaces) {
|
|
10616
|
+
byPlace.set(s.place, [...byPlace.get(s.place) ?? [], s]);
|
|
10617
|
+
}
|
|
10618
|
+
const reachedBy = /* @__PURE__ */ new Map();
|
|
10619
|
+
const unresolvedBy = /* @__PURE__ */ new Map();
|
|
10620
|
+
const runOnlyBy = /* @__PURE__ */ new Map();
|
|
10621
|
+
const add = (m, k, s) => m.set(k, [...m.get(k) ?? [], s]);
|
|
10622
|
+
for (const step of steps) {
|
|
10623
|
+
const place = explorePlaceKey(step.url);
|
|
10624
|
+
if (!place)
|
|
10625
|
+
continue;
|
|
10626
|
+
const atPlace = byPlace.get(place);
|
|
10627
|
+
if (!atPlace) {
|
|
10628
|
+
add(runOnlyBy, place, step);
|
|
10629
|
+
continue;
|
|
10630
|
+
}
|
|
10631
|
+
const hit = tieStep(map2, atPlace, step);
|
|
10632
|
+
if (hit.length === 0)
|
|
10633
|
+
add(unresolvedBy, place, step);
|
|
10634
|
+
for (const s of hit)
|
|
10635
|
+
add(reachedBy, s.key, step);
|
|
10636
|
+
}
|
|
10637
|
+
const surfaces = map2.surfaces.map((s) => {
|
|
10638
|
+
const base = {
|
|
10639
|
+
key: s.key,
|
|
10640
|
+
label: s.label,
|
|
10641
|
+
place: s.place,
|
|
10642
|
+
url: s.url,
|
|
10643
|
+
depth: s.depth
|
|
10644
|
+
};
|
|
10645
|
+
const reached = reachedBy.get(s.key);
|
|
10646
|
+
if (reached)
|
|
10647
|
+
return { ...base, status: "reached", runs: distinctRuns(reached) };
|
|
10648
|
+
const unresolved = unresolvedBy.get(s.place);
|
|
10649
|
+
if (unresolved) {
|
|
10650
|
+
return { ...base, status: "place_only", runs: distinctRuns(unresolved) };
|
|
10651
|
+
}
|
|
10652
|
+
return { ...base, status: "unreached", runs: [] };
|
|
10653
|
+
});
|
|
10654
|
+
const runOnly = [...runOnlyBy].map(([place, list2]) => ({
|
|
10655
|
+
place,
|
|
10656
|
+
steps: list2.length,
|
|
10657
|
+
runs: distinctRuns(list2)
|
|
10658
|
+
}));
|
|
10659
|
+
runOnly.sort((a, b) => b.steps - a.steps || a.place.localeCompare(b.place));
|
|
10660
|
+
return {
|
|
10661
|
+
map_id: map2.id,
|
|
10662
|
+
runs: new Set(steps.map((s) => s.run_id)).size,
|
|
10663
|
+
steps: steps.length,
|
|
10664
|
+
reached: surfaces.filter((s) => s.status === "reached").length,
|
|
10665
|
+
place_only: surfaces.filter((s) => s.status === "place_only").length,
|
|
10666
|
+
unreached: surfaces.filter((s) => s.status === "unreached").length,
|
|
10667
|
+
surfaces,
|
|
10668
|
+
run_only_places: runOnly
|
|
10669
|
+
};
|
|
10670
|
+
}
|
|
10671
|
+
function tieStep(map2, atPlace, step) {
|
|
10672
|
+
if (typeof step.signature === "string") {
|
|
10673
|
+
const m = matchExploreSurface(map2, step.url, step.signature);
|
|
10674
|
+
return m ? [m] : [];
|
|
10675
|
+
}
|
|
10676
|
+
if (atPlace.length === 1)
|
|
10677
|
+
return [atPlace[0]];
|
|
10678
|
+
const name = step.surface?.trim();
|
|
10679
|
+
if (!name)
|
|
10680
|
+
return [];
|
|
10681
|
+
return atPlace.filter((s) => [s.label, ...s.headings].filter(Boolean).some((n) => surfaceMatches(n, name) || surfaceMatches(name, n)));
|
|
10682
|
+
}
|
|
10683
|
+
function distinctRuns(steps) {
|
|
10684
|
+
return [...new Set(steps.map((s) => s.run_id))].sort();
|
|
10685
|
+
}
|
|
10686
|
+
async function readAllRunSteps(projectRoot) {
|
|
10687
|
+
const out = [];
|
|
10688
|
+
for (const runId of await listRuns(projectRoot)) {
|
|
10689
|
+
let steps;
|
|
10690
|
+
try {
|
|
10691
|
+
steps = await readStepsForRun(projectRoot, runId);
|
|
10692
|
+
} catch {
|
|
10693
|
+
continue;
|
|
10694
|
+
}
|
|
10695
|
+
for (const s of steps) {
|
|
10696
|
+
out.push({ run_id: runId, url: s.url, surface: s.surface });
|
|
10697
|
+
}
|
|
10698
|
+
}
|
|
10699
|
+
return out;
|
|
10700
|
+
}
|
|
10701
|
+
function actionSignature(a) {
|
|
10702
|
+
const where = a.outcome.kind === "surface" ? a.outcome.surface : a.outcome.kind === "skipped" ? `skipped:${a.outcome.reason}` : a.outcome.kind === "external" ? `external:${a.outcome.url}` : a.outcome.kind;
|
|
10703
|
+
return `${a.role} "${a.name}" \u2192 ${where}`;
|
|
10704
|
+
}
|
|
10705
|
+
function diffExploreMaps(a, b) {
|
|
10706
|
+
const aByKey = new Map(a.surfaces.map((s) => [s.key, s]));
|
|
10707
|
+
const bByKey = new Map(b.surfaces.map((s) => [s.key, s]));
|
|
10708
|
+
const ref = (s) => ({
|
|
10709
|
+
key: s.key,
|
|
10710
|
+
label: s.label
|
|
10711
|
+
});
|
|
10712
|
+
const onlyA = a.surfaces.filter((s) => !bByKey.has(s.key)).map(ref);
|
|
10713
|
+
const onlyB = b.surfaces.filter((s) => !aByKey.has(s.key)).map(ref);
|
|
10714
|
+
const changed = [];
|
|
10715
|
+
let common2 = 0;
|
|
10716
|
+
for (const sa of a.surfaces) {
|
|
10717
|
+
const sb = bByKey.get(sa.key);
|
|
10718
|
+
if (!sb)
|
|
10719
|
+
continue;
|
|
10720
|
+
common2++;
|
|
10721
|
+
const setA = new Set(sa.actions.map(actionSignature));
|
|
10722
|
+
const setB = new Set(sb.actions.map(actionSignature));
|
|
10723
|
+
const inA = [...setA].filter((x) => !setB.has(x)).sort();
|
|
10724
|
+
const inB = [...setB].filter((x) => !setA.has(x)).sort();
|
|
10725
|
+
if (inA.length > 0 || inB.length > 0) {
|
|
10726
|
+
changed.push({ ...ref(sa), only_a: inA, only_b: inB });
|
|
10727
|
+
}
|
|
10728
|
+
}
|
|
10729
|
+
const union = common2 + onlyA.length + onlyB.length;
|
|
10730
|
+
return {
|
|
10731
|
+
a: a.id,
|
|
10732
|
+
b: b.id,
|
|
10733
|
+
common: common2,
|
|
10734
|
+
only_a: onlyA,
|
|
10735
|
+
only_b: onlyB,
|
|
10736
|
+
surface_overlap: union === 0 ? 1 : common2 / union,
|
|
10737
|
+
changed_actions: changed
|
|
10738
|
+
};
|
|
10739
|
+
}
|
|
10740
|
+
var EXPLORE_MAP_SCHEMA, UUID, DATE, MAP_ID;
|
|
10741
|
+
var init_explore_map = __esm({
|
|
10742
|
+
"../../packages/local-store/dist/explore-map.js"() {
|
|
10743
|
+
"use strict";
|
|
10744
|
+
init_atomic();
|
|
10745
|
+
init_paths();
|
|
10746
|
+
init_revisit();
|
|
10747
|
+
init_runs();
|
|
10748
|
+
init_walkthrough_canonical2();
|
|
10749
|
+
EXPLORE_MAP_SCHEMA = 1;
|
|
10750
|
+
UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
10751
|
+
DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
10752
|
+
MAP_ID = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}-[0-9a-f]{6}$/;
|
|
10753
|
+
}
|
|
10754
|
+
});
|
|
10755
|
+
|
|
10756
|
+
// ../../packages/local-store/dist/human-test-link.js
|
|
10757
|
+
import { promises as fs20 } from "fs";
|
|
10758
|
+
import path16 from "path";
|
|
10493
10759
|
function humanTestLinks(verdicts) {
|
|
10494
10760
|
const best = /* @__PURE__ */ new Map();
|
|
10495
10761
|
for (const v of verdicts) {
|
|
@@ -10512,7 +10778,7 @@ async function linkFindingsToHumanTests(projectRoot, runId, verdicts) {
|
|
|
10512
10778
|
const dir = entityDirPath(projectRoot, "finding");
|
|
10513
10779
|
let names;
|
|
10514
10780
|
try {
|
|
10515
|
-
names = await
|
|
10781
|
+
names = await fs20.readdir(dir);
|
|
10516
10782
|
} catch {
|
|
10517
10783
|
return [];
|
|
10518
10784
|
}
|
|
@@ -10520,11 +10786,11 @@ async function linkFindingsToHumanTests(projectRoot, runId, verdicts) {
|
|
|
10520
10786
|
for (const name of names) {
|
|
10521
10787
|
if (!name.endsWith(".md") || name.startsWith("."))
|
|
10522
10788
|
continue;
|
|
10523
|
-
const file =
|
|
10789
|
+
const file = path16.join(dir, name);
|
|
10524
10790
|
let raw;
|
|
10525
10791
|
let fm;
|
|
10526
10792
|
try {
|
|
10527
|
-
raw = await
|
|
10793
|
+
raw = await fs20.readFile(file, "utf8");
|
|
10528
10794
|
fm = parseFrontmatter(raw).frontmatter;
|
|
10529
10795
|
} catch {
|
|
10530
10796
|
continue;
|
|
@@ -10755,14 +11021,14 @@ var init_cloud_client = __esm({
|
|
|
10755
11021
|
});
|
|
10756
11022
|
|
|
10757
11023
|
// ../../packages/local-store/dist/last-pull.js
|
|
10758
|
-
import { promises as
|
|
10759
|
-
import
|
|
11024
|
+
import { promises as fs21 } from "fs";
|
|
11025
|
+
import path17 from "path";
|
|
10760
11026
|
function lastPullPath(projectRoot) {
|
|
10761
|
-
return
|
|
11027
|
+
return path17.join(cacheDir(projectRoot), "last-pull.json");
|
|
10762
11028
|
}
|
|
10763
11029
|
async function readLastPull(projectRoot) {
|
|
10764
11030
|
try {
|
|
10765
|
-
const raw = await
|
|
11031
|
+
const raw = await fs21.readFile(lastPullPath(projectRoot), "utf8");
|
|
10766
11032
|
const parsed = JSON.parse(raw);
|
|
10767
11033
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
10768
11034
|
return parsed;
|
|
@@ -11048,7 +11314,7 @@ var init_frontmatter2 = __esm({
|
|
|
11048
11314
|
});
|
|
11049
11315
|
|
|
11050
11316
|
// ../../packages/local-store/dist/pull/write.js
|
|
11051
|
-
import { promises as
|
|
11317
|
+
import { promises as fs22 } from "fs";
|
|
11052
11318
|
async function loadLocalIndex(projectRoot, type2) {
|
|
11053
11319
|
const byId = /* @__PURE__ */ new Map();
|
|
11054
11320
|
const bySlug = /* @__PURE__ */ new Map();
|
|
@@ -11077,7 +11343,7 @@ async function classifyExisting(projectRoot, type2, cloudId, localSlug, targetSl
|
|
|
11077
11343
|
if (localSlug !== targetSlug)
|
|
11078
11344
|
return "untracked";
|
|
11079
11345
|
const lastSynced = localIndex.lastPushById[cloudId];
|
|
11080
|
-
const bytes = await
|
|
11346
|
+
const bytes = await fs22.readFile(entityFilePath(projectRoot, type2, localSlug), "utf8");
|
|
11081
11347
|
return lastSynced && hashNormalizedContent(bytes) === lastSynced ? "fast_forward" : "diverged";
|
|
11082
11348
|
}
|
|
11083
11349
|
async function writeOrSkip(projectRoot, type2, cloudId, slug, frontmatter, body, localIndex, mode, cloudVersion) {
|
|
@@ -11138,13 +11404,13 @@ async function writeOrSkip(projectRoot, type2, cloudId, slug, frontmatter, body,
|
|
|
11138
11404
|
}
|
|
11139
11405
|
if (existingSlugForId && existingSlugForId !== slug) {
|
|
11140
11406
|
try {
|
|
11141
|
-
await
|
|
11407
|
+
await fs22.unlink(entityFilePath(projectRoot, type2, existingSlugForId));
|
|
11142
11408
|
localIndex.bySlug.delete(existingSlugForId);
|
|
11143
11409
|
} catch {
|
|
11144
11410
|
}
|
|
11145
11411
|
}
|
|
11146
11412
|
await writeEntity(projectRoot, type2, frontmatter, text);
|
|
11147
|
-
const bytes = await
|
|
11413
|
+
const bytes = await fs22.readFile(entityFilePath(projectRoot, type2, slug), "utf8");
|
|
11148
11414
|
const hash = hashNormalizedContent(bytes);
|
|
11149
11415
|
localIndex.byId.set(cloudId, slug);
|
|
11150
11416
|
localIndex.bySlug.set(slug, cloudId);
|
|
@@ -11198,7 +11464,7 @@ var init_write = __esm({
|
|
|
11198
11464
|
});
|
|
11199
11465
|
|
|
11200
11466
|
// ../../packages/local-store/dist/pull/entities.js
|
|
11201
|
-
import { promises as
|
|
11467
|
+
import { promises as fs23 } from "fs";
|
|
11202
11468
|
function buildContextFrontmatter(payload) {
|
|
11203
11469
|
const cloudCtx = payload.context ?? {};
|
|
11204
11470
|
const glossary = (payload.glossary ?? []).map((g) => ({
|
|
@@ -11253,7 +11519,7 @@ async function pullContext(projectRoot, cloud, projectId, mode, result) {
|
|
|
11253
11519
|
const frontmatter = buildContextFrontmatter(payload);
|
|
11254
11520
|
let hasLocal = false;
|
|
11255
11521
|
try {
|
|
11256
|
-
await
|
|
11522
|
+
await fs23.access(contextPath(projectRoot));
|
|
11257
11523
|
hasLocal = true;
|
|
11258
11524
|
} catch {
|
|
11259
11525
|
hasLocal = false;
|
|
@@ -11283,7 +11549,7 @@ async function pullContext(projectRoot, cloud, projectId, mode, result) {
|
|
|
11283
11549
|
frontmatter,
|
|
11284
11550
|
notes_body: existingNotes
|
|
11285
11551
|
});
|
|
11286
|
-
const bytes = await
|
|
11552
|
+
const bytes = await fs23.readFile(contextPath(projectRoot), "utf8");
|
|
11287
11553
|
const hash = hashNormalizedContent(bytes);
|
|
11288
11554
|
await recordLastPull(projectRoot, "context:root", hash);
|
|
11289
11555
|
await recordLastPush(projectRoot, "context:root", hash);
|
|
@@ -11813,6 +12079,7 @@ var init_dist2 = __esm({
|
|
|
11813
12079
|
init_prune();
|
|
11814
12080
|
init_fix_state();
|
|
11815
12081
|
init_revisit();
|
|
12082
|
+
init_explore_map();
|
|
11816
12083
|
init_walkthrough_canonical2();
|
|
11817
12084
|
init_journey_diff();
|
|
11818
12085
|
init_finding_match();
|
|
@@ -11917,15 +12184,15 @@ function resolveApiUrl(configured) {
|
|
|
11917
12184
|
assertSecureApiUrl(url, configured ? "API URL" : "IAMU_API_URL");
|
|
11918
12185
|
return url;
|
|
11919
12186
|
}
|
|
11920
|
-
function parseStore(raw,
|
|
12187
|
+
function parseStore(raw, path30) {
|
|
11921
12188
|
let parsed;
|
|
11922
12189
|
try {
|
|
11923
12190
|
parsed = JSON.parse(raw);
|
|
11924
12191
|
} catch (err) {
|
|
11925
|
-
throw new CredentialsFileError(
|
|
12192
|
+
throw new CredentialsFileError(path30, "not valid JSON", err);
|
|
11926
12193
|
}
|
|
11927
12194
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
11928
|
-
throw new CredentialsFileError(
|
|
12195
|
+
throw new CredentialsFileError(path30, "expected a JSON object");
|
|
11929
12196
|
}
|
|
11930
12197
|
return parsed;
|
|
11931
12198
|
}
|
|
@@ -11933,26 +12200,26 @@ function isNotFound(err) {
|
|
|
11933
12200
|
return typeof err === "object" && err !== null && err.code === "ENOENT";
|
|
11934
12201
|
}
|
|
11935
12202
|
async function readCredentialsStore() {
|
|
11936
|
-
const
|
|
12203
|
+
const path30 = getCredentialsPath();
|
|
11937
12204
|
let raw;
|
|
11938
12205
|
try {
|
|
11939
|
-
raw = await readFile(
|
|
12206
|
+
raw = await readFile(path30, "utf-8");
|
|
11940
12207
|
} catch (err) {
|
|
11941
12208
|
if (isNotFound(err)) return {};
|
|
11942
12209
|
warnUnreadableStoreOnce(err);
|
|
11943
12210
|
throw err;
|
|
11944
12211
|
}
|
|
11945
12212
|
try {
|
|
11946
|
-
return parseStore(raw,
|
|
12213
|
+
return parseStore(raw, path30);
|
|
11947
12214
|
} catch (err) {
|
|
11948
12215
|
warnUnreadableStoreOnce(err);
|
|
11949
12216
|
throw err;
|
|
11950
12217
|
}
|
|
11951
12218
|
}
|
|
11952
12219
|
async function writeCredentialsStore(store) {
|
|
11953
|
-
const
|
|
11954
|
-
await mkdir(dirname(
|
|
11955
|
-
await atomicWriteFile(
|
|
12220
|
+
const path30 = getCredentialsPath();
|
|
12221
|
+
await mkdir(dirname(path30), { recursive: true, mode: 448 });
|
|
12222
|
+
await atomicWriteFile(path30, `${JSON.stringify(store, null, 2)}
|
|
11956
12223
|
`, {
|
|
11957
12224
|
mode: 384
|
|
11958
12225
|
});
|
|
@@ -11972,16 +12239,16 @@ function resolveApiKey(apiUrl) {
|
|
|
11972
12239
|
} catch {
|
|
11973
12240
|
return null;
|
|
11974
12241
|
}
|
|
11975
|
-
const
|
|
12242
|
+
const path30 = getCredentialsPath();
|
|
11976
12243
|
let raw;
|
|
11977
12244
|
try {
|
|
11978
|
-
raw = readFileSync(
|
|
12245
|
+
raw = readFileSync(path30, "utf-8");
|
|
11979
12246
|
} catch (err) {
|
|
11980
12247
|
if (!isNotFound(err)) warnUnreadableStoreOnce(err);
|
|
11981
12248
|
return null;
|
|
11982
12249
|
}
|
|
11983
12250
|
try {
|
|
11984
|
-
const entry = parseStore(raw,
|
|
12251
|
+
const entry = parseStore(raw, path30)[url];
|
|
11985
12252
|
if (typeof entry !== "object" || entry === null) return null;
|
|
11986
12253
|
const token = entry.token;
|
|
11987
12254
|
return typeof token === "string" && token ? token : null;
|
|
@@ -12020,12 +12287,12 @@ var init_credentials = __esm({
|
|
|
12020
12287
|
* @param reason - What is wrong with it.
|
|
12021
12288
|
* @param cause - Underlying parse error, if any.
|
|
12022
12289
|
*/
|
|
12023
|
-
constructor(
|
|
12290
|
+
constructor(path30, reason, cause) {
|
|
12024
12291
|
super(
|
|
12025
|
-
`Credentials file ${
|
|
12292
|
+
`Credentials file ${path30} is corrupt (${reason}). Fix it by hand, or delete it and run \`iamu auth login\` again.`,
|
|
12026
12293
|
{
|
|
12027
12294
|
cause,
|
|
12028
|
-
suggestedAction: `Delete ${
|
|
12295
|
+
suggestedAction: `Delete ${path30} and run \`iamu auth login\`, or set IAMU_TOKEN.`
|
|
12029
12296
|
}
|
|
12030
12297
|
);
|
|
12031
12298
|
}
|
|
@@ -12544,16 +12811,16 @@ async function needsWrite(filePath, content) {
|
|
|
12544
12811
|
return true;
|
|
12545
12812
|
}
|
|
12546
12813
|
}
|
|
12547
|
-
function isCliWrittenSkill(
|
|
12814
|
+
function isCliWrittenSkill(path30) {
|
|
12548
12815
|
try {
|
|
12549
|
-
return /^generatedBy:/m.test(readFileSync3(
|
|
12816
|
+
return /^generatedBy:/m.test(readFileSync3(path30, "utf-8"));
|
|
12550
12817
|
} catch {
|
|
12551
12818
|
return false;
|
|
12552
12819
|
}
|
|
12553
12820
|
}
|
|
12554
|
-
function isCliWrittenPrompt(
|
|
12821
|
+
function isCliWrittenPrompt(path30) {
|
|
12555
12822
|
try {
|
|
12556
|
-
const content = readFileSync3(
|
|
12823
|
+
const content = readFileSync3(path30, "utf-8");
|
|
12557
12824
|
return content.includes("iamu") || /^generatedBy:/m.test(content);
|
|
12558
12825
|
} catch {
|
|
12559
12826
|
return false;
|
|
@@ -12565,8 +12832,8 @@ function planSkillFiles(hosts, projectRoot, version) {
|
|
|
12565
12832
|
for (const host of hosts) {
|
|
12566
12833
|
if (pluginProvidesSkills(host, projectRoot)) {
|
|
12567
12834
|
for (const name of skillTemplates.keys()) {
|
|
12568
|
-
const
|
|
12569
|
-
if (isCliWrittenSkill(
|
|
12835
|
+
const path30 = join4(host.getSkillsDir(projectRoot), name, "SKILL.md");
|
|
12836
|
+
if (isCliWrittenSkill(path30)) files.push({ host, path: path30, content: null });
|
|
12570
12837
|
}
|
|
12571
12838
|
} else if (host.supportsSkills) {
|
|
12572
12839
|
const skillsDir = host.getSkillsDir(projectRoot);
|
|
@@ -14086,7 +14353,7 @@ import {
|
|
|
14086
14353
|
readlinkSync,
|
|
14087
14354
|
realpathSync as realpathSync2
|
|
14088
14355
|
} from "fs";
|
|
14089
|
-
import
|
|
14356
|
+
import path18 from "path";
|
|
14090
14357
|
function git(args, cwd) {
|
|
14091
14358
|
return execFileSync("git", args, {
|
|
14092
14359
|
cwd,
|
|
@@ -14139,8 +14406,8 @@ function detectWorktreeState(projectRoot, iamuDirName = IAMU_DIR) {
|
|
|
14139
14406
|
const top = git(["rev-parse", "--show-toplevel"], projectRoot).toString("utf8").trim();
|
|
14140
14407
|
if (!top) return void 0;
|
|
14141
14408
|
const realTop = realpathSync2(top);
|
|
14142
|
-
const realIamu =
|
|
14143
|
-
const iamuRel =
|
|
14409
|
+
const realIamu = path18.join(realpathSync2(projectRoot), iamuDirName);
|
|
14410
|
+
const iamuRel = path18.relative(realTop, realIamu).split(path18.sep).join("/");
|
|
14144
14411
|
const pathspec = iamuRel.startsWith("..") ? [":(top)"] : [":(top)", `:(top,exclude,literal)${iamuRel}`];
|
|
14145
14412
|
const status2 = git(
|
|
14146
14413
|
[
|
|
@@ -14170,7 +14437,7 @@ function detectWorktreeState(projectRoot, iamuDirName = IAMU_DIR) {
|
|
|
14170
14437
|
);
|
|
14171
14438
|
for (const file of sorted.slice(0, MAX_UNTRACKED_HASHED)) {
|
|
14172
14439
|
const rel = file.replace(/\/$/, "");
|
|
14173
|
-
hash.update(`${file}\0${hashFile(
|
|
14440
|
+
hash.update(`${file}\0${hashFile(path18.join(realTop, rel))}
|
|
14174
14441
|
`);
|
|
14175
14442
|
}
|
|
14176
14443
|
if (sorted.length > MAX_UNTRACKED_HASHED) {
|
|
@@ -14343,7 +14610,7 @@ __export(start_run_exports, {
|
|
|
14343
14610
|
resolveStartUrl: () => resolveStartUrl,
|
|
14344
14611
|
startRun: () => startRun
|
|
14345
14612
|
});
|
|
14346
|
-
import
|
|
14613
|
+
import path19 from "path";
|
|
14347
14614
|
import chalk11 from "chalk";
|
|
14348
14615
|
function resolveStartUrl(target, scenario) {
|
|
14349
14616
|
const base = target ?? scenario.target_url ?? null;
|
|
@@ -14519,7 +14786,7 @@ async function startRun(scenarioSlug, opts) {
|
|
|
14519
14786
|
...viewport ? { viewport } : {}
|
|
14520
14787
|
};
|
|
14521
14788
|
await atomicWriteFile(
|
|
14522
|
-
|
|
14789
|
+
path19.join(run.absPath, "walkthrough.json"),
|
|
14523
14790
|
JSON.stringify(manifest, null, 2) + "\n"
|
|
14524
14791
|
);
|
|
14525
14792
|
if (opts.json) {
|
|
@@ -14527,7 +14794,7 @@ async function startRun(scenarioSlug, opts) {
|
|
|
14527
14794
|
JSON.stringify(
|
|
14528
14795
|
{
|
|
14529
14796
|
run_id: run.runId,
|
|
14530
|
-
path:
|
|
14797
|
+
path: path19.relative(projectRoot, run.absPath),
|
|
14531
14798
|
scenario_slug: scenarioSlug,
|
|
14532
14799
|
target_url: start2.url,
|
|
14533
14800
|
target_reachable: targetReachable,
|
|
@@ -14575,7 +14842,7 @@ async function startRun(scenarioSlug, opts) {
|
|
|
14575
14842
|
);
|
|
14576
14843
|
}
|
|
14577
14844
|
console.log(
|
|
14578
|
-
` ${chalk11.dim("path:")} ${
|
|
14845
|
+
` ${chalk11.dim("path:")} ${path19.relative(projectRoot, run.absPath)}`
|
|
14579
14846
|
);
|
|
14580
14847
|
console.log();
|
|
14581
14848
|
console.log(` ${chalk11.bold("Next steps for blind execution:")}`);
|
|
@@ -14616,8 +14883,8 @@ var report_exports = {};
|
|
|
14616
14883
|
__export(report_exports, {
|
|
14617
14884
|
report: () => report
|
|
14618
14885
|
});
|
|
14619
|
-
import { promises as
|
|
14620
|
-
import
|
|
14886
|
+
import { promises as fs24 } from "fs";
|
|
14887
|
+
import path20 from "path";
|
|
14621
14888
|
import chalk12 from "chalk";
|
|
14622
14889
|
async function report(scenario, opts) {
|
|
14623
14890
|
const ctx = await resolveProject(opts);
|
|
@@ -14636,8 +14903,8 @@ async function report(scenario, opts) {
|
|
|
14636
14903
|
for (const rid of runs.reverse()) {
|
|
14637
14904
|
try {
|
|
14638
14905
|
const dir = runDir(projectRoot, rid);
|
|
14639
|
-
const mRaw = await
|
|
14640
|
-
|
|
14906
|
+
const mRaw = await fs24.readFile(
|
|
14907
|
+
path20.join(dir, "walkthrough.json"),
|
|
14641
14908
|
"utf8"
|
|
14642
14909
|
);
|
|
14643
14910
|
const manifest = JSON.parse(mRaw);
|
|
@@ -14645,8 +14912,8 @@ async function report(scenario, opts) {
|
|
|
14645
14912
|
if (scenario && manifest.scenario_slug !== scenario) continue;
|
|
14646
14913
|
let canonical = null;
|
|
14647
14914
|
try {
|
|
14648
|
-
const cRaw = await
|
|
14649
|
-
|
|
14915
|
+
const cRaw = await fs24.readFile(
|
|
14916
|
+
path20.join(dir, "canonical.json"),
|
|
14650
14917
|
"utf8"
|
|
14651
14918
|
);
|
|
14652
14919
|
canonical = JSON.parse(cRaw);
|
|
@@ -15923,6 +16190,35 @@ async function init(opts) {
|
|
|
15923
16190
|
""
|
|
15924
16191
|
)
|
|
15925
16192
|
);
|
|
16193
|
+
const testabilityPath = join5(iamuDir2, "testability.md");
|
|
16194
|
+
if (!existsSync7(testabilityPath)) {
|
|
16195
|
+
await writeFile3(
|
|
16196
|
+
testabilityPath,
|
|
16197
|
+
serializeFrontmatter(
|
|
16198
|
+
{
|
|
16199
|
+
status: "draft",
|
|
16200
|
+
target_url: "http://localhost:3000"
|
|
16201
|
+
},
|
|
16202
|
+
`# Project Testability & Environment
|
|
16203
|
+
|
|
16204
|
+
Target URL and execution harness for walkthroughs.
|
|
16205
|
+
Audited and populated during /iamu:init.
|
|
16206
|
+
|
|
16207
|
+
## Target & Run Instructions
|
|
16208
|
+
- Default URL: http://localhost:3000
|
|
16209
|
+
|
|
16210
|
+
## Reachable Surfaces
|
|
16211
|
+
(Run /iamu:init to discover testable routes)
|
|
16212
|
+
|
|
16213
|
+
## Mocks & Data Fixtures
|
|
16214
|
+
(Run /iamu:init to inspect existing harnesses)
|
|
16215
|
+
|
|
16216
|
+
## Untestable Boundaries
|
|
16217
|
+
(None documented yet)
|
|
16218
|
+
`
|
|
16219
|
+
)
|
|
16220
|
+
);
|
|
16221
|
+
}
|
|
15926
16222
|
}
|
|
15927
16223
|
const promptTemplates = loadPromptTemplates();
|
|
15928
16224
|
const promptsDir = join5(iamuDir2, ".prompts");
|
|
@@ -16532,7 +16828,7 @@ var init_browse_driver = __esm({
|
|
|
16532
16828
|
|
|
16533
16829
|
// src/commands/step-numbering.ts
|
|
16534
16830
|
import { open, readdir, unlink } from "fs/promises";
|
|
16535
|
-
import
|
|
16831
|
+
import path21 from "path";
|
|
16536
16832
|
function stepFileName(stepNumber2) {
|
|
16537
16833
|
return `${String(stepNumber2).padStart(3, "0")}.json`;
|
|
16538
16834
|
}
|
|
@@ -16554,7 +16850,7 @@ async function reserveStepFile(stepsDir, limit) {
|
|
|
16554
16850
|
if (candidate > limit.maxSteps) {
|
|
16555
16851
|
throw new Error(stepLimitMessage(limit.maxSteps, limit.runId));
|
|
16556
16852
|
}
|
|
16557
|
-
const stepPath =
|
|
16853
|
+
const stepPath = path21.join(stepsDir, stepFileName(candidate));
|
|
16558
16854
|
try {
|
|
16559
16855
|
const handle = await open(stepPath, "wx");
|
|
16560
16856
|
await handle.close();
|
|
@@ -16586,7 +16882,7 @@ __export(step_exports, {
|
|
|
16586
16882
|
stepCommand: () => stepCommand
|
|
16587
16883
|
});
|
|
16588
16884
|
import { copyFile, mkdir as mkdir5, stat } from "fs/promises";
|
|
16589
|
-
import
|
|
16885
|
+
import path22 from "path";
|
|
16590
16886
|
import chalk19 from "chalk";
|
|
16591
16887
|
function parseMaxSteps(val) {
|
|
16592
16888
|
if (typeof val === "number" && Number.isFinite(val) && val > 0) return val;
|
|
@@ -16605,8 +16901,8 @@ async function recordStep(input) {
|
|
|
16605
16901
|
if (!runDirPath) {
|
|
16606
16902
|
throw new Error(`Run directory not found for run "${input.runId}"`);
|
|
16607
16903
|
}
|
|
16608
|
-
const stepsDir =
|
|
16609
|
-
const screenshotsDir =
|
|
16904
|
+
const stepsDir = path22.join(runDirPath, "steps");
|
|
16905
|
+
const screenshotsDir = path22.join(runDirPath, "screenshots");
|
|
16610
16906
|
await mkdir5(stepsDir, { recursive: true });
|
|
16611
16907
|
await mkdir5(screenshotsDir, { recursive: true });
|
|
16612
16908
|
const shotSource = input.shot ? await existingShot(input.shot) : null;
|
|
@@ -16620,11 +16916,11 @@ async function recordStep(input) {
|
|
|
16620
16916
|
let finalScreenshotPath = null;
|
|
16621
16917
|
if (shotSource) {
|
|
16622
16918
|
const destName = `step-${stepNumber2}.png`;
|
|
16623
|
-
finalScreenshotPath =
|
|
16624
|
-
if (
|
|
16919
|
+
finalScreenshotPath = path22.join(screenshotsDir, destName);
|
|
16920
|
+
if (path22.resolve(finalScreenshotPath) !== shotSource) {
|
|
16625
16921
|
await copyFile(shotSource, finalScreenshotPath);
|
|
16626
16922
|
}
|
|
16627
|
-
screenshotRef =
|
|
16923
|
+
screenshotRef = path22.join("screenshots", destName);
|
|
16628
16924
|
}
|
|
16629
16925
|
const parsedStep = runStepSchema.parse({
|
|
16630
16926
|
step_number: stepNumber2,
|
|
@@ -16649,7 +16945,7 @@ async function recordStep(input) {
|
|
|
16649
16945
|
}
|
|
16650
16946
|
}
|
|
16651
16947
|
async function existingShot(shot) {
|
|
16652
|
-
const resolved =
|
|
16948
|
+
const resolved = path22.resolve(shot);
|
|
16653
16949
|
const isFile = await stat(resolved).then((s) => s.isFile()).catch(() => false);
|
|
16654
16950
|
if (!isFile) {
|
|
16655
16951
|
throw new Error(
|
|
@@ -16691,7 +16987,7 @@ async function stepCommand(runArg, opts) {
|
|
|
16691
16987
|
JSON.stringify(
|
|
16692
16988
|
{
|
|
16693
16989
|
step_number: res.step.step_number,
|
|
16694
|
-
path:
|
|
16990
|
+
path: path22.relative(projectRoot, res.stepPath),
|
|
16695
16991
|
action: res.step.action,
|
|
16696
16992
|
note: res.step.note,
|
|
16697
16993
|
url: res.step.url,
|
|
@@ -16718,7 +17014,7 @@ async function stepCommand(runArg, opts) {
|
|
|
16718
17014
|
console.log(` ${chalk19.dim("shot:")} ${res.step.screenshot}`);
|
|
16719
17015
|
}
|
|
16720
17016
|
console.log(
|
|
16721
|
-
` ${chalk19.dim("file:")} ${
|
|
17017
|
+
` ${chalk19.dim("file:")} ${path22.relative(projectRoot, res.stepPath)}`
|
|
16722
17018
|
);
|
|
16723
17019
|
console.log();
|
|
16724
17020
|
} catch (err) {
|
|
@@ -16761,21 +17057,21 @@ import {
|
|
|
16761
17057
|
} from "fs";
|
|
16762
17058
|
import { rm as rm2 } from "fs/promises";
|
|
16763
17059
|
import { homedir as homedir3 } from "os";
|
|
16764
|
-
import
|
|
17060
|
+
import path23 from "path";
|
|
16765
17061
|
import chalk20 from "chalk";
|
|
16766
17062
|
function stateDir() {
|
|
16767
|
-
return
|
|
16768
|
-
process.env.IAMU_HOME ??
|
|
17063
|
+
return path23.join(
|
|
17064
|
+
process.env.IAMU_HOME ?? path23.join(homedir3(), ".iamu"),
|
|
16769
17065
|
"browse"
|
|
16770
17066
|
);
|
|
16771
17067
|
}
|
|
16772
17068
|
function profileDir() {
|
|
16773
|
-
return
|
|
17069
|
+
return path23.join(stateDir(), "profile");
|
|
16774
17070
|
}
|
|
16775
17071
|
function readBrowseState() {
|
|
16776
17072
|
try {
|
|
16777
17073
|
return JSON.parse(
|
|
16778
|
-
readFileSync6(
|
|
17074
|
+
readFileSync6(path23.join(stateDir(), "state.json"), "utf8")
|
|
16779
17075
|
);
|
|
16780
17076
|
} catch {
|
|
16781
17077
|
return null;
|
|
@@ -16783,7 +17079,7 @@ function readBrowseState() {
|
|
|
16783
17079
|
}
|
|
16784
17080
|
function writeState(state) {
|
|
16785
17081
|
mkdirSync(stateDir(), { recursive: true });
|
|
16786
|
-
writeFileSync(
|
|
17082
|
+
writeFileSync(path23.join(stateDir(), "state.json"), JSON.stringify(state));
|
|
16787
17083
|
}
|
|
16788
17084
|
function fail(message, opts) {
|
|
16789
17085
|
if (opts?.json) {
|
|
@@ -16846,7 +17142,7 @@ async function requestedViewport(opts) {
|
|
|
16846
17142
|
let manifest;
|
|
16847
17143
|
try {
|
|
16848
17144
|
manifest = JSON.parse(
|
|
16849
|
-
readFileSync6(
|
|
17145
|
+
readFileSync6(path23.join(dir, "walkthrough.json"), "utf8")
|
|
16850
17146
|
);
|
|
16851
17147
|
} catch {
|
|
16852
17148
|
fail(
|
|
@@ -16952,7 +17248,7 @@ async function stop(opts, driver) {
|
|
|
16952
17248
|
await driver.closeBrowser(state.port);
|
|
16953
17249
|
if (state.pid) await reapBrowser(state.pid, profileDir(), driver);
|
|
16954
17250
|
try {
|
|
16955
|
-
unlinkSync(
|
|
17251
|
+
unlinkSync(path23.join(stateDir(), "state.json"));
|
|
16956
17252
|
} catch {
|
|
16957
17253
|
}
|
|
16958
17254
|
report2(opts, { stopped: true }, [` ${chalk20.green("\u2713")} Browser stopped`]);
|
|
@@ -17018,8 +17314,8 @@ async function withPage(opts, driver, fn, target) {
|
|
|
17018
17314
|
let screenshot = null;
|
|
17019
17315
|
if (opts.shot) {
|
|
17020
17316
|
await waitForVisualStability(page);
|
|
17021
|
-
screenshot =
|
|
17022
|
-
mkdirSync(
|
|
17317
|
+
screenshot = path23.resolve(opts.shot);
|
|
17318
|
+
mkdirSync(path23.dirname(screenshot), { recursive: true });
|
|
17023
17319
|
await page.screenshot({ path: screenshot, fullPage: Boolean(opts.full) });
|
|
17024
17320
|
}
|
|
17025
17321
|
return { url: page.url(), title: await page.title(), screenshot };
|
|
@@ -17098,7 +17394,7 @@ function printStep(opts, record, projectRoot) {
|
|
|
17098
17394
|
if (opts.json) {
|
|
17099
17395
|
const doc = {
|
|
17100
17396
|
step_number: step.step_number,
|
|
17101
|
-
path:
|
|
17397
|
+
path: path23.relative(projectRoot, record.stepPath),
|
|
17102
17398
|
action: step.action,
|
|
17103
17399
|
note: step.note,
|
|
17104
17400
|
url: step.url,
|
|
@@ -17172,10 +17468,10 @@ async function browseStep(args, opts, driver) {
|
|
|
17172
17468
|
const runDirPath = resolveRunDir(ctx.projectRoot, opts.run);
|
|
17173
17469
|
if (!runDirPath) return;
|
|
17174
17470
|
const maxSteps = parseMaxSteps(opts.maxSteps);
|
|
17175
|
-
if (await peekNextStepNumber(
|
|
17471
|
+
if (await peekNextStepNumber(path23.join(runDirPath, "steps")) > maxSteps) {
|
|
17176
17472
|
return fail(stepLimitMessage(maxSteps, opts.run), opts);
|
|
17177
17473
|
}
|
|
17178
|
-
const pendingShot =
|
|
17474
|
+
const pendingShot = path23.join(
|
|
17179
17475
|
runDirPath,
|
|
17180
17476
|
"screenshots",
|
|
17181
17477
|
`.pending-${randomUUID7()}.png`
|
|
@@ -17246,11 +17542,14 @@ var doctor_exports = {};
|
|
|
17246
17542
|
__export(doctor_exports, {
|
|
17247
17543
|
checkCdpResponsive: () => checkCdpResponsive,
|
|
17248
17544
|
checkPortAvailable: () => checkPortAvailable,
|
|
17545
|
+
checkProjectHarnesses: () => checkProjectHarnesses,
|
|
17546
|
+
checkProjectTestabilityContract: () => checkProjectTestabilityContract,
|
|
17547
|
+
checkTargetHealth: () => checkTargetHealth,
|
|
17249
17548
|
doctor: () => doctor,
|
|
17250
17549
|
runDoctorChecks: () => runDoctorChecks
|
|
17251
17550
|
});
|
|
17252
17551
|
import { createServer } from "net";
|
|
17253
|
-
import { existsSync as existsSync10 } from "fs";
|
|
17552
|
+
import { existsSync as existsSync10, promises as fs25 } from "fs";
|
|
17254
17553
|
import { join as join7 } from "path";
|
|
17255
17554
|
import chalk21 from "chalk";
|
|
17256
17555
|
function checkPortAvailable(port) {
|
|
@@ -17528,11 +17827,186 @@ async function checkAgentIntegrations(projectRoot) {
|
|
|
17528
17827
|
}
|
|
17529
17828
|
return checks;
|
|
17530
17829
|
}
|
|
17531
|
-
async function
|
|
17830
|
+
async function checkTargetHealth(targetUrl) {
|
|
17831
|
+
const checks = [];
|
|
17832
|
+
let parsedUrl;
|
|
17833
|
+
try {
|
|
17834
|
+
parsedUrl = new URL(targetUrl);
|
|
17835
|
+
} catch {
|
|
17836
|
+
checks.push({
|
|
17837
|
+
category: "target",
|
|
17838
|
+
name: "Target URL Format",
|
|
17839
|
+
status: "error",
|
|
17840
|
+
message: `Invalid target URL: "${targetUrl}"`,
|
|
17841
|
+
remedy: "Provide a valid URL with protocol (e.g. http://localhost:3000)."
|
|
17842
|
+
});
|
|
17843
|
+
return checks;
|
|
17844
|
+
}
|
|
17845
|
+
try {
|
|
17846
|
+
const res = await fetch(targetUrl, {
|
|
17847
|
+
method: "GET",
|
|
17848
|
+
redirect: "manual",
|
|
17849
|
+
signal: AbortSignal.timeout(3e3)
|
|
17850
|
+
});
|
|
17851
|
+
if ([301, 302, 303, 307, 308].includes(res.status)) {
|
|
17852
|
+
const location = res.headers.get("location") || "";
|
|
17853
|
+
const isExternalAuth = /auth0\.com|okta\.com|clerk\.com|accounts\.google\.com|cognito|login\.microsoftonline\.com/i.test(
|
|
17854
|
+
location
|
|
17855
|
+
) || location.includes("/oauth");
|
|
17856
|
+
if (isExternalAuth) {
|
|
17857
|
+
let redirectHost = location;
|
|
17858
|
+
try {
|
|
17859
|
+
redirectHost = new URL(location, targetUrl).host;
|
|
17860
|
+
} catch {
|
|
17861
|
+
}
|
|
17862
|
+
checks.push({
|
|
17863
|
+
category: "target",
|
|
17864
|
+
name: "Authentication Boundary",
|
|
17865
|
+
status: "warn",
|
|
17866
|
+
message: `Target immediately redirects to external auth (${redirectHost})`,
|
|
17867
|
+
remedy: "Configure a dev auth bypass or prepare for interactive login handshake before walking."
|
|
17868
|
+
});
|
|
17869
|
+
return checks;
|
|
17870
|
+
}
|
|
17871
|
+
checks.push({
|
|
17872
|
+
category: "target",
|
|
17873
|
+
name: "Target Reachability",
|
|
17874
|
+
status: "ok",
|
|
17875
|
+
message: `HTTP ${res.status} redirect -> ${location}`
|
|
17876
|
+
});
|
|
17877
|
+
return checks;
|
|
17878
|
+
}
|
|
17879
|
+
if (res.status >= 500) {
|
|
17880
|
+
checks.push({
|
|
17881
|
+
category: "target",
|
|
17882
|
+
name: "Target Health",
|
|
17883
|
+
status: "error",
|
|
17884
|
+
message: `Target returned HTTP ${res.status} ${res.statusText || "Server Error"}`,
|
|
17885
|
+
remedy: "Check dev server logs for unhandled exceptions or missing environment variables."
|
|
17886
|
+
});
|
|
17887
|
+
return checks;
|
|
17888
|
+
}
|
|
17889
|
+
checks.push({
|
|
17890
|
+
category: "target",
|
|
17891
|
+
name: "Target Reachability",
|
|
17892
|
+
status: "ok",
|
|
17893
|
+
message: `HTTP ${res.status} OK (${parsedUrl.origin})`
|
|
17894
|
+
});
|
|
17895
|
+
} catch (err) {
|
|
17896
|
+
const message = err.message;
|
|
17897
|
+
checks.push({
|
|
17898
|
+
category: "target",
|
|
17899
|
+
name: "Target Reachability",
|
|
17900
|
+
status: "warn",
|
|
17901
|
+
message: `Cannot connect to ${targetUrl} (${message})`,
|
|
17902
|
+
remedy: "Ensure your dev server is running and listening on the specified port."
|
|
17903
|
+
});
|
|
17904
|
+
}
|
|
17905
|
+
return checks;
|
|
17906
|
+
}
|
|
17907
|
+
async function checkProjectHarnesses(projectRoot) {
|
|
17908
|
+
const checks = [];
|
|
17909
|
+
const pkgPath = join7(projectRoot, "package.json");
|
|
17910
|
+
if (!existsSync10(pkgPath)) return checks;
|
|
17911
|
+
try {
|
|
17912
|
+
const raw = await fs25.readFile(pkgPath, "utf8");
|
|
17913
|
+
const pkg = JSON.parse(raw);
|
|
17914
|
+
const scripts = pkg.scripts || {};
|
|
17915
|
+
const allDeps = {
|
|
17916
|
+
...pkg.dependencies || {},
|
|
17917
|
+
...pkg.devDependencies || {}
|
|
17918
|
+
};
|
|
17919
|
+
const devScripts = ["dev", "start", "dev:mock", "serve"].filter(
|
|
17920
|
+
(s) => s in scripts
|
|
17921
|
+
);
|
|
17922
|
+
if (devScripts.length > 0) {
|
|
17923
|
+
checks.push({
|
|
17924
|
+
category: "target",
|
|
17925
|
+
name: "Dev Server Script",
|
|
17926
|
+
status: "ok",
|
|
17927
|
+
message: `Available scripts: ${devScripts.map((s) => `pnpm ${s}`).join(", ")}`
|
|
17928
|
+
});
|
|
17929
|
+
} else {
|
|
17930
|
+
checks.push({
|
|
17931
|
+
category: "target",
|
|
17932
|
+
name: "Dev Server Script",
|
|
17933
|
+
status: "warn",
|
|
17934
|
+
message: "No standard dev script (dev, start, dev:mock) found in package.json",
|
|
17935
|
+
remedy: "Add a `dev` or `start` script to package.json to boot your app locally."
|
|
17936
|
+
});
|
|
17937
|
+
}
|
|
17938
|
+
if (allDeps.msw || existsSync10(join7(projectRoot, "src/mocks")) || existsSync10(join7(projectRoot, "mocks"))) {
|
|
17939
|
+
checks.push({
|
|
17940
|
+
category: "target",
|
|
17941
|
+
name: "Mock Harness (MSW)",
|
|
17942
|
+
status: "ok",
|
|
17943
|
+
message: "Mock Service Worker detected. API mocks can decouple walks from live backends."
|
|
17944
|
+
});
|
|
17945
|
+
}
|
|
17946
|
+
const seedScript = Object.keys(scripts).find(
|
|
17947
|
+
(s) => /seed|db:seed|fixtures/i.test(s)
|
|
17948
|
+
);
|
|
17949
|
+
if (seedScript) {
|
|
17950
|
+
checks.push({
|
|
17951
|
+
category: "target",
|
|
17952
|
+
name: "Database Seed",
|
|
17953
|
+
status: "ok",
|
|
17954
|
+
message: `Seed script detected (${seedScript}). Preconditions can be reset between runs.`
|
|
17955
|
+
});
|
|
17956
|
+
}
|
|
17957
|
+
} catch {
|
|
17958
|
+
}
|
|
17959
|
+
return checks;
|
|
17960
|
+
}
|
|
17961
|
+
async function checkProjectTestabilityContract(projectRoot) {
|
|
17962
|
+
const checks = [];
|
|
17963
|
+
const testabilityPath = join7(projectRoot, "iamu", "testability.md");
|
|
17964
|
+
if (!existsSync10(testabilityPath)) {
|
|
17965
|
+
checks.push({
|
|
17966
|
+
category: "target",
|
|
17967
|
+
name: "Testability Contract (testability.md)",
|
|
17968
|
+
status: "warn",
|
|
17969
|
+
message: "iamu/testability.md not found",
|
|
17970
|
+
remedy: "Run `/iamu:init` to audit your codebase and record testable routes, auth strategies, and mock harnesses."
|
|
17971
|
+
});
|
|
17972
|
+
return { checks };
|
|
17973
|
+
}
|
|
17974
|
+
try {
|
|
17975
|
+
const raw = await fs25.readFile(testabilityPath, "utf8");
|
|
17976
|
+
const { frontmatter } = parseFrontmatter(raw);
|
|
17977
|
+
const targetUrl = typeof frontmatter.target_url === "string" ? frontmatter.target_url : void 0;
|
|
17978
|
+
const details = [];
|
|
17979
|
+
if (targetUrl) details.push(`Target: ${targetUrl}`);
|
|
17980
|
+
if (frontmatter.dev_command) {
|
|
17981
|
+
details.push(`Command: ${frontmatter.dev_command}`);
|
|
17982
|
+
}
|
|
17983
|
+
if (frontmatter.auth_strategy) {
|
|
17984
|
+
details.push(`Auth: ${frontmatter.auth_strategy}`);
|
|
17985
|
+
}
|
|
17986
|
+
checks.push({
|
|
17987
|
+
category: "target",
|
|
17988
|
+
name: "Testability Contract (testability.md)",
|
|
17989
|
+
status: "ok",
|
|
17990
|
+
message: `Configured (${details.join(" \xB7 ") || "present"})`
|
|
17991
|
+
});
|
|
17992
|
+
return { checks, targetUrl };
|
|
17993
|
+
} catch (err) {
|
|
17994
|
+
checks.push({
|
|
17995
|
+
category: "target",
|
|
17996
|
+
name: "Testability Contract (testability.md)",
|
|
17997
|
+
status: "warn",
|
|
17998
|
+
message: `Unreadable testability contract: ${err.message}`,
|
|
17999
|
+
remedy: "Repair iamu/testability.md frontmatter."
|
|
18000
|
+
});
|
|
18001
|
+
return { checks };
|
|
18002
|
+
}
|
|
18003
|
+
}
|
|
18004
|
+
async function runDoctorChecks(projectRoot, opts) {
|
|
17532
18005
|
const checks = [
|
|
17533
18006
|
...checkRuntimeChecks(),
|
|
17534
18007
|
...await checkBrowserChecks()
|
|
17535
18008
|
];
|
|
18009
|
+
let effectiveTarget = opts?.target;
|
|
17536
18010
|
if (!projectRoot || !existsSync10(join7(projectRoot, "iamu"))) {
|
|
17537
18011
|
checks.push({
|
|
17538
18012
|
category: "project",
|
|
@@ -17552,6 +18026,15 @@ async function runDoctorChecks(projectRoot) {
|
|
|
17552
18026
|
checks.push(...await checkProjectConfiguration(projectRoot));
|
|
17553
18027
|
checks.push(...await checkProjectContextAndEntities(projectRoot));
|
|
17554
18028
|
checks.push(...await checkAgentIntegrations(projectRoot));
|
|
18029
|
+
checks.push(...await checkProjectHarnesses(projectRoot));
|
|
18030
|
+
const testability = await checkProjectTestabilityContract(projectRoot);
|
|
18031
|
+
checks.push(...testability.checks);
|
|
18032
|
+
if (!effectiveTarget && testability.targetUrl) {
|
|
18033
|
+
effectiveTarget = testability.targetUrl;
|
|
18034
|
+
}
|
|
18035
|
+
}
|
|
18036
|
+
if (effectiveTarget) {
|
|
18037
|
+
checks.push(...await checkTargetHealth(effectiveTarget));
|
|
17555
18038
|
}
|
|
17556
18039
|
const okCount = checks.filter((c) => c.status === "ok").length;
|
|
17557
18040
|
const warnCount = checks.filter((c) => c.status === "warn").length;
|
|
@@ -17568,7 +18051,7 @@ async function runDoctorChecks(projectRoot) {
|
|
|
17568
18051
|
}
|
|
17569
18052
|
async function doctor(opts) {
|
|
17570
18053
|
const projectRoot = findProjectRoot(process.cwd()) ?? void 0;
|
|
17571
|
-
const result = await runDoctorChecks(projectRoot);
|
|
18054
|
+
const result = await runDoctorChecks(projectRoot, { target: opts?.target });
|
|
17572
18055
|
if (opts?.json) {
|
|
17573
18056
|
console.log(JSON.stringify(result, null, 2));
|
|
17574
18057
|
if (!result.ok) process.exitCode = 1;
|
|
@@ -17582,7 +18065,8 @@ async function doctor(opts) {
|
|
|
17582
18065
|
{ key: "browser", title: "Browser Automation" },
|
|
17583
18066
|
{ key: "project", title: "Project Configuration" },
|
|
17584
18067
|
{ key: "agent", title: "Agent Integrations" },
|
|
17585
|
-
{ key: "cloud", title: "Cloud & Authentication" }
|
|
18068
|
+
{ key: "cloud", title: "Cloud & Authentication" },
|
|
18069
|
+
{ key: "target", title: "Target & Testability" }
|
|
17586
18070
|
];
|
|
17587
18071
|
for (const cat of categories) {
|
|
17588
18072
|
const catChecks = result.checks.filter((c) => c.category === cat.key);
|
|
@@ -18060,13 +18544,13 @@ __export(validate_exports, {
|
|
|
18060
18544
|
validate: () => validate
|
|
18061
18545
|
});
|
|
18062
18546
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
18063
|
-
import { promises as
|
|
18064
|
-
import
|
|
18547
|
+
import { promises as fs26 } from "fs";
|
|
18548
|
+
import path24 from "path";
|
|
18065
18549
|
import chalk23 from "chalk";
|
|
18066
18550
|
function inferTypeFromPath(filePath, projectRoot) {
|
|
18067
|
-
const rel =
|
|
18068
|
-
if (rel.startsWith("..") ||
|
|
18069
|
-
const segments = rel.split(
|
|
18551
|
+
const rel = path24.relative(path24.join(projectRoot, "iamu"), filePath);
|
|
18552
|
+
if (rel.startsWith("..") || path24.isAbsolute(rel)) return null;
|
|
18553
|
+
const segments = rel.split(path24.sep);
|
|
18070
18554
|
if (segments.length < 2) return null;
|
|
18071
18555
|
return TYPE_BY_ALIAS.get(segments[0]) ?? null;
|
|
18072
18556
|
}
|
|
@@ -18096,7 +18580,7 @@ function validateFile(filePath, raw, projectRoot) {
|
|
|
18096
18580
|
);
|
|
18097
18581
|
return result;
|
|
18098
18582
|
}
|
|
18099
|
-
if (
|
|
18583
|
+
if (path24.basename(filePath) === "context.yml") {
|
|
18100
18584
|
applyZodValidation(contextFrontmatterSchema, frontmatter, result);
|
|
18101
18585
|
const warnings = [];
|
|
18102
18586
|
const hasNotes = Boolean(
|
|
@@ -18156,17 +18640,17 @@ async function buildRepairLookups(projectRoot) {
|
|
|
18156
18640
|
}
|
|
18157
18641
|
}
|
|
18158
18642
|
const runScenario = /* @__PURE__ */ new Map();
|
|
18159
|
-
const runsDir2 =
|
|
18643
|
+
const runsDir2 = path24.join(projectRoot, "iamu", ".runs");
|
|
18160
18644
|
let runIds = [];
|
|
18161
18645
|
try {
|
|
18162
|
-
runIds = await
|
|
18646
|
+
runIds = await fs26.readdir(runsDir2);
|
|
18163
18647
|
} catch {
|
|
18164
18648
|
}
|
|
18165
18649
|
for (const runId of runIds) {
|
|
18166
18650
|
try {
|
|
18167
18651
|
const manifest = JSON.parse(
|
|
18168
|
-
await
|
|
18169
|
-
|
|
18652
|
+
await fs26.readFile(
|
|
18653
|
+
path24.join(runsDir2, runId, "walkthrough.json"),
|
|
18170
18654
|
"utf8"
|
|
18171
18655
|
)
|
|
18172
18656
|
);
|
|
@@ -18188,7 +18672,7 @@ async function fixFindings(projectRoot, results, dryRun) {
|
|
|
18188
18672
|
let raw;
|
|
18189
18673
|
let parsed;
|
|
18190
18674
|
try {
|
|
18191
|
-
raw = await
|
|
18675
|
+
raw = await fs26.readFile(result.path, "utf8");
|
|
18192
18676
|
parsed = parseFrontmatter(raw);
|
|
18193
18677
|
} catch {
|
|
18194
18678
|
continue;
|
|
@@ -18197,7 +18681,7 @@ async function fixFindings(projectRoot, results, dryRun) {
|
|
|
18197
18681
|
parsed.frontmatter,
|
|
18198
18682
|
{
|
|
18199
18683
|
...lookups,
|
|
18200
|
-
fileSlug:
|
|
18684
|
+
fileSlug: path24.basename(result.path, ".md"),
|
|
18201
18685
|
newId: randomUUID8,
|
|
18202
18686
|
body: parsed.body
|
|
18203
18687
|
}
|
|
@@ -18232,9 +18716,9 @@ async function validate(filePath, opts) {
|
|
|
18232
18716
|
if (process.exitCode === 1) return;
|
|
18233
18717
|
const results = [];
|
|
18234
18718
|
if (filePath) {
|
|
18235
|
-
const abs =
|
|
18719
|
+
const abs = path24.isAbsolute(filePath) ? filePath : path24.resolve(process.cwd(), filePath);
|
|
18236
18720
|
try {
|
|
18237
|
-
const raw = await
|
|
18721
|
+
const raw = await fs26.readFile(abs, "utf8");
|
|
18238
18722
|
results.push(validateFile(abs, raw, projectRoot));
|
|
18239
18723
|
} catch (err) {
|
|
18240
18724
|
commandError(
|
|
@@ -18244,24 +18728,24 @@ async function validate(filePath, opts) {
|
|
|
18244
18728
|
return;
|
|
18245
18729
|
}
|
|
18246
18730
|
} else if (opts.run) {
|
|
18247
|
-
const findingsDir =
|
|
18731
|
+
const findingsDir = path24.join(projectRoot, "iamu", ENTITY_DIRS.finding);
|
|
18248
18732
|
let names = [];
|
|
18249
18733
|
try {
|
|
18250
|
-
names = await
|
|
18734
|
+
names = await fs26.readdir(findingsDir);
|
|
18251
18735
|
} catch {
|
|
18252
18736
|
}
|
|
18253
18737
|
for (const name of names) {
|
|
18254
18738
|
if (!name.endsWith(".md")) continue;
|
|
18255
|
-
const abs =
|
|
18739
|
+
const abs = path24.join(findingsDir, name);
|
|
18256
18740
|
try {
|
|
18257
|
-
const raw = await
|
|
18741
|
+
const raw = await fs26.readFile(abs, "utf8");
|
|
18258
18742
|
const { frontmatter } = parseFrontmatter(raw);
|
|
18259
18743
|
if (frontmatter.run === opts.run) {
|
|
18260
18744
|
results.push(validateFile(abs, raw, projectRoot));
|
|
18261
18745
|
}
|
|
18262
18746
|
} catch {
|
|
18263
18747
|
results.push({
|
|
18264
|
-
path:
|
|
18748
|
+
path: path24.relative(projectRoot, abs),
|
|
18265
18749
|
valid: false,
|
|
18266
18750
|
errors: ["Cannot read file or parse frontmatter"]
|
|
18267
18751
|
});
|
|
@@ -18294,31 +18778,31 @@ async function validate(filePath, opts) {
|
|
|
18294
18778
|
} else {
|
|
18295
18779
|
const typesToCheck = typeFilter ? ENTITY_TYPES.filter((t) => typeFilter.has(t)) : ENTITY_TYPES;
|
|
18296
18780
|
for (const type2 of typesToCheck) {
|
|
18297
|
-
const iamuTypeDir =
|
|
18781
|
+
const iamuTypeDir = path24.join(projectRoot, "iamu", ENTITY_DIRS[type2]);
|
|
18298
18782
|
let names;
|
|
18299
18783
|
try {
|
|
18300
|
-
names = await
|
|
18784
|
+
names = await fs26.readdir(iamuTypeDir);
|
|
18301
18785
|
} catch {
|
|
18302
18786
|
continue;
|
|
18303
18787
|
}
|
|
18304
18788
|
for (const name of names) {
|
|
18305
18789
|
if (!name.endsWith(".md")) continue;
|
|
18306
|
-
const abs =
|
|
18790
|
+
const abs = path24.join(iamuTypeDir, name);
|
|
18307
18791
|
try {
|
|
18308
|
-
const raw = await
|
|
18792
|
+
const raw = await fs26.readFile(abs, "utf8");
|
|
18309
18793
|
results.push(validateFile(abs, raw, projectRoot));
|
|
18310
18794
|
} catch {
|
|
18311
18795
|
results.push({
|
|
18312
|
-
path:
|
|
18796
|
+
path: path24.relative(projectRoot, abs),
|
|
18313
18797
|
valid: false,
|
|
18314
18798
|
errors: ["Cannot read file"]
|
|
18315
18799
|
});
|
|
18316
18800
|
}
|
|
18317
18801
|
}
|
|
18318
18802
|
}
|
|
18319
|
-
const ctxPath =
|
|
18803
|
+
const ctxPath = path24.join(projectRoot, "iamu", "context.yml");
|
|
18320
18804
|
try {
|
|
18321
|
-
const raw = await
|
|
18805
|
+
const raw = await fs26.readFile(ctxPath, "utf8");
|
|
18322
18806
|
results.push(validateFile(ctxPath, raw, projectRoot));
|
|
18323
18807
|
} catch {
|
|
18324
18808
|
}
|
|
@@ -18337,19 +18821,19 @@ async function validate(filePath, opts) {
|
|
|
18337
18821
|
valid,
|
|
18338
18822
|
errors: results.filter((r) => !r.valid).flatMap(
|
|
18339
18823
|
(r) => r.errors.map((e) => ({
|
|
18340
|
-
path:
|
|
18824
|
+
path: path24.relative(projectRoot, r.path),
|
|
18341
18825
|
message: e
|
|
18342
18826
|
}))
|
|
18343
18827
|
),
|
|
18344
18828
|
warnings: results.filter((r) => r.warnings && r.warnings.length > 0).flatMap(
|
|
18345
18829
|
(r) => (r.warnings ?? []).map((w) => ({
|
|
18346
|
-
path:
|
|
18830
|
+
path: path24.relative(projectRoot, r.path),
|
|
18347
18831
|
message: w
|
|
18348
18832
|
}))
|
|
18349
18833
|
),
|
|
18350
18834
|
...opts.fix ? {
|
|
18351
18835
|
repaired: repaired.map((r) => ({
|
|
18352
|
-
path:
|
|
18836
|
+
path: path24.relative(projectRoot, r.path),
|
|
18353
18837
|
changes: r.repairs,
|
|
18354
18838
|
valid: r.valid
|
|
18355
18839
|
}))
|
|
@@ -18371,7 +18855,7 @@ async function validate(filePath, opts) {
|
|
|
18371
18855
|
}
|
|
18372
18856
|
let hasErrors = false;
|
|
18373
18857
|
for (const r of results) {
|
|
18374
|
-
const relPath =
|
|
18858
|
+
const relPath = path24.relative(projectRoot, r.path);
|
|
18375
18859
|
if (r.valid) {
|
|
18376
18860
|
if (r.warnings && r.warnings.length > 0) {
|
|
18377
18861
|
console.log(` ${symbols.warning} ` + chalk23.yellow(relPath));
|
|
@@ -18685,6 +19169,797 @@ var init_new_entity = __esm({
|
|
|
18685
19169
|
}
|
|
18686
19170
|
});
|
|
18687
19171
|
|
|
19172
|
+
// src/commands/explore-crawl.ts
|
|
19173
|
+
function candidateSig(c) {
|
|
19174
|
+
return `${c.role}
|
|
19175
|
+
${c.name}
|
|
19176
|
+
${c.occurrence}`;
|
|
19177
|
+
}
|
|
19178
|
+
function originOf(url) {
|
|
19179
|
+
try {
|
|
19180
|
+
return new URL(url).origin;
|
|
19181
|
+
} catch {
|
|
19182
|
+
return null;
|
|
19183
|
+
}
|
|
19184
|
+
}
|
|
19185
|
+
function skipReason(c, origin, deny) {
|
|
19186
|
+
if (deny.some((re) => re.test(c.name) || c.href && re.test(c.href))) {
|
|
19187
|
+
return "denied";
|
|
19188
|
+
}
|
|
19189
|
+
if (c.submit) return "submit";
|
|
19190
|
+
if (UNSAFE_NAME.test(c.name)) return "unsafe_name";
|
|
19191
|
+
if (c.href) {
|
|
19192
|
+
let u = null;
|
|
19193
|
+
try {
|
|
19194
|
+
u = new URL(c.href);
|
|
19195
|
+
} catch {
|
|
19196
|
+
}
|
|
19197
|
+
if (u && u.protocol !== "javascript:") {
|
|
19198
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") return "external";
|
|
19199
|
+
if (u.origin !== origin) return "external";
|
|
19200
|
+
if (UNSAFE_PATH.test(u.pathname)) return "unsafe_name";
|
|
19201
|
+
}
|
|
19202
|
+
if (c.download) return "download";
|
|
19203
|
+
}
|
|
19204
|
+
return null;
|
|
19205
|
+
}
|
|
19206
|
+
async function settle(page, settleMs) {
|
|
19207
|
+
await page.waitForLoadState("domcontentloaded", { timeout: NAV_TIMEOUT_MS }).catch(() => {
|
|
19208
|
+
});
|
|
19209
|
+
await page.waitForLoadState("networkidle", { timeout: settleMs }).catch(() => {
|
|
19210
|
+
});
|
|
19211
|
+
}
|
|
19212
|
+
async function readPage(page) {
|
|
19213
|
+
return page.evaluate(READ_PAGE);
|
|
19214
|
+
}
|
|
19215
|
+
async function markDocument(page) {
|
|
19216
|
+
await page.evaluate("window.__iamuDoc = true").catch(() => {
|
|
19217
|
+
});
|
|
19218
|
+
}
|
|
19219
|
+
async function isSameDocument(page) {
|
|
19220
|
+
return page.evaluate("window.__iamuDoc === true").catch(() => false);
|
|
19221
|
+
}
|
|
19222
|
+
async function clickCandidate(page, want) {
|
|
19223
|
+
const state = await readPage(page);
|
|
19224
|
+
const c = state.candidates.find(
|
|
19225
|
+
(x) => candidateSig(x) === candidateSig(want)
|
|
19226
|
+
);
|
|
19227
|
+
if (!c) return false;
|
|
19228
|
+
await page.locator(`[data-iamu-x="${c.index}"]`).click({ timeout: CLICK_TIMEOUT_MS });
|
|
19229
|
+
return true;
|
|
19230
|
+
}
|
|
19231
|
+
async function crawl(page, opts) {
|
|
19232
|
+
const origin = originOf(opts.startUrl);
|
|
19233
|
+
if (!origin) throw new Error(`Not a URL: ${opts.startUrl}`);
|
|
19234
|
+
let blocked = [];
|
|
19235
|
+
await page.route("**/*", (route) => {
|
|
19236
|
+
const req = route.request();
|
|
19237
|
+
if (SAFE_METHODS.has(req.method())) return route.continue();
|
|
19238
|
+
let where = req.url();
|
|
19239
|
+
try {
|
|
19240
|
+
const u = new URL(where);
|
|
19241
|
+
where = u.origin === origin ? u.pathname : `${u.origin}${u.pathname}`;
|
|
19242
|
+
} catch {
|
|
19243
|
+
}
|
|
19244
|
+
blocked.push(`${req.method()} ${where}`);
|
|
19245
|
+
return route.abort("blockedbyclient");
|
|
19246
|
+
});
|
|
19247
|
+
const onDialog = (d) => {
|
|
19248
|
+
d.dismiss().catch(() => {
|
|
19249
|
+
});
|
|
19250
|
+
};
|
|
19251
|
+
const onPopup = (p) => {
|
|
19252
|
+
if (p !== page) p.close().catch(() => {
|
|
19253
|
+
});
|
|
19254
|
+
};
|
|
19255
|
+
page.on("dialog", onDialog);
|
|
19256
|
+
page.context().on("page", onPopup);
|
|
19257
|
+
try {
|
|
19258
|
+
return await crawlFrom(page, opts, origin, () => {
|
|
19259
|
+
const out = blocked;
|
|
19260
|
+
blocked = [];
|
|
19261
|
+
return out;
|
|
19262
|
+
});
|
|
19263
|
+
} finally {
|
|
19264
|
+
page.off("dialog", onDialog);
|
|
19265
|
+
page.context().off("page", onPopup);
|
|
19266
|
+
await page.unroute("**/*").catch(() => {
|
|
19267
|
+
});
|
|
19268
|
+
}
|
|
19269
|
+
}
|
|
19270
|
+
async function crawlFrom(page, opts, origin, takeBlocked) {
|
|
19271
|
+
const deny = opts.deny ?? [];
|
|
19272
|
+
const settleMs = opts.settleMs ?? 1500;
|
|
19273
|
+
const { budget } = opts;
|
|
19274
|
+
const stats = {
|
|
19275
|
+
surfaces: 0,
|
|
19276
|
+
actions_tried: 0,
|
|
19277
|
+
skipped: 0,
|
|
19278
|
+
blocked_requests: 0,
|
|
19279
|
+
errors: 0,
|
|
19280
|
+
truncated: false
|
|
19281
|
+
};
|
|
19282
|
+
const surfaces = [];
|
|
19283
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
19284
|
+
const byPlaceView = /* @__PURE__ */ new Map();
|
|
19285
|
+
const signatureOf = (s) => exploreSignature(s.headings, s.title);
|
|
19286
|
+
function addSurface(state, place, view, path30, depth) {
|
|
19287
|
+
const key = exploreSurfaceKey(place, view);
|
|
19288
|
+
const surface = {
|
|
19289
|
+
key,
|
|
19290
|
+
url: state.url,
|
|
19291
|
+
place,
|
|
19292
|
+
view,
|
|
19293
|
+
signature: signatureOf(state),
|
|
19294
|
+
label: state.headings[0] ?? (state.title || place),
|
|
19295
|
+
headings: state.headings,
|
|
19296
|
+
fields: state.fields,
|
|
19297
|
+
depth,
|
|
19298
|
+
path: path30,
|
|
19299
|
+
actions: []
|
|
19300
|
+
};
|
|
19301
|
+
surfaces.push(surface);
|
|
19302
|
+
byKey.set(key, surface);
|
|
19303
|
+
byPlaceView.set(`${place}
|
|
19304
|
+
${surface.signature}`, key);
|
|
19305
|
+
stats.surfaces++;
|
|
19306
|
+
opts.log?.(` ${" ".repeat(depth)}${key} ${surface.label}`);
|
|
19307
|
+
return surface;
|
|
19308
|
+
}
|
|
19309
|
+
async function goto(url) {
|
|
19310
|
+
await page.goto(url, {
|
|
19311
|
+
waitUntil: "domcontentloaded",
|
|
19312
|
+
timeout: NAV_TIMEOUT_MS
|
|
19313
|
+
});
|
|
19314
|
+
await settle(page, settleMs);
|
|
19315
|
+
}
|
|
19316
|
+
async function replay(path30) {
|
|
19317
|
+
if (!("goto" in (path30[0] ?? {}))) await goto(opts.startUrl);
|
|
19318
|
+
for (const step of path30) {
|
|
19319
|
+
if ("goto" in step) {
|
|
19320
|
+
await goto(step.goto);
|
|
19321
|
+
} else {
|
|
19322
|
+
if (!await clickCandidate(page, step.click)) return false;
|
|
19323
|
+
await settle(page, settleMs);
|
|
19324
|
+
}
|
|
19325
|
+
}
|
|
19326
|
+
return true;
|
|
19327
|
+
}
|
|
19328
|
+
await goto(opts.startUrl);
|
|
19329
|
+
if (await page.evaluate("Boolean(navigator.serviceWorker?.controller)").catch(() => false)) {
|
|
19330
|
+
throw new Error(
|
|
19331
|
+
"A service worker controls this page, and the write guard cannot see its requests. Explore without --attach (service workers are blocked there)."
|
|
19332
|
+
);
|
|
19333
|
+
}
|
|
19334
|
+
const first = await readPage(page);
|
|
19335
|
+
const firstPlace = explorePlaceKey(first.url);
|
|
19336
|
+
if (!firstPlace || originOf(first.url) !== origin) {
|
|
19337
|
+
throw new Error(`The start URL led off the product, to ${first.url}`);
|
|
19338
|
+
}
|
|
19339
|
+
addSurface(first, firstPlace, null, [], 0);
|
|
19340
|
+
for (let i = 0; i < surfaces.length; i++) {
|
|
19341
|
+
const surface = surfaces[i];
|
|
19342
|
+
if (!await replay(surface.path)) {
|
|
19343
|
+
stats.errors++;
|
|
19344
|
+
surface.actions.push({
|
|
19345
|
+
role: "",
|
|
19346
|
+
name: "",
|
|
19347
|
+
occurrence: 0,
|
|
19348
|
+
outcome: { kind: "error", message: LOST }
|
|
19349
|
+
});
|
|
19350
|
+
continue;
|
|
19351
|
+
}
|
|
19352
|
+
const home = await readPage(page);
|
|
19353
|
+
const seen = new Set(home.candidates.map(candidateSig));
|
|
19354
|
+
const work = home.candidates.map((c) => ({ c, via: [] }));
|
|
19355
|
+
const homeList = home.candidates.map(candidateSig).join("\n");
|
|
19356
|
+
let pristine = true;
|
|
19357
|
+
for (let w = 0; w < work.length; w++) {
|
|
19358
|
+
const { c, via } = work[w];
|
|
19359
|
+
const action = {
|
|
19360
|
+
role: c.role,
|
|
19361
|
+
name: c.name,
|
|
19362
|
+
occurrence: c.occurrence,
|
|
19363
|
+
...c.href ? { href: c.href } : {},
|
|
19364
|
+
...via.length > 0 ? { via } : {},
|
|
19365
|
+
outcome: { kind: "same" }
|
|
19366
|
+
};
|
|
19367
|
+
surface.actions.push(action);
|
|
19368
|
+
const reason = skipReason(c, origin, deny);
|
|
19369
|
+
if (reason) {
|
|
19370
|
+
action.outcome = { kind: "skipped", reason };
|
|
19371
|
+
stats.skipped++;
|
|
19372
|
+
continue;
|
|
19373
|
+
}
|
|
19374
|
+
if (surface.depth >= budget.max_depth || stats.actions_tried >= budget.max_actions) {
|
|
19375
|
+
action.outcome = { kind: "skipped", reason: "budget" };
|
|
19376
|
+
stats.skipped++;
|
|
19377
|
+
stats.truncated = true;
|
|
19378
|
+
continue;
|
|
19379
|
+
}
|
|
19380
|
+
stats.actions_tried++;
|
|
19381
|
+
takeBlocked();
|
|
19382
|
+
const click = {
|
|
19383
|
+
click: { role: c.role, name: c.name, occurrence: c.occurrence }
|
|
19384
|
+
};
|
|
19385
|
+
let outcome;
|
|
19386
|
+
let revealed = 0;
|
|
19387
|
+
let left = false;
|
|
19388
|
+
let lost = false;
|
|
19389
|
+
try {
|
|
19390
|
+
if (!pristine || via.length > 0) {
|
|
19391
|
+
if (!await replay([...surface.path, ...via])) {
|
|
19392
|
+
throw new Error(LOST);
|
|
19393
|
+
}
|
|
19394
|
+
}
|
|
19395
|
+
pristine = false;
|
|
19396
|
+
const before = await readPage(page);
|
|
19397
|
+
await markDocument(page);
|
|
19398
|
+
if (c.newTab && c.href) {
|
|
19399
|
+
await goto(c.href);
|
|
19400
|
+
} else {
|
|
19401
|
+
if (!await clickCandidate(page, c)) {
|
|
19402
|
+
throw new Error("control is gone");
|
|
19403
|
+
}
|
|
19404
|
+
await settle(page, settleMs);
|
|
19405
|
+
}
|
|
19406
|
+
const after = await readPage(page);
|
|
19407
|
+
if (originOf(after.url) !== origin) {
|
|
19408
|
+
outcome = { kind: "external", url: after.url };
|
|
19409
|
+
left = true;
|
|
19410
|
+
} else {
|
|
19411
|
+
const place = explorePlaceKey(after.url) ?? before.url;
|
|
19412
|
+
const sig = signatureOf(after);
|
|
19413
|
+
const sameDoc = !(c.newTab && c.href) && await isSameDocument(page);
|
|
19414
|
+
const beforePlace = explorePlaceKey(before.url);
|
|
19415
|
+
let key = byPlaceView.get(`${place}
|
|
19416
|
+
${sig}`);
|
|
19417
|
+
let view = null;
|
|
19418
|
+
if (!key) {
|
|
19419
|
+
const inPage = sameDoc && place === beforePlace;
|
|
19420
|
+
view = inPage ? sig : null;
|
|
19421
|
+
key = exploreSurfaceKey(place, view);
|
|
19422
|
+
if (byKey.has(key)) byPlaceView.set(`${place}
|
|
19423
|
+
${sig}`, key);
|
|
19424
|
+
}
|
|
19425
|
+
if (key === surface.key) {
|
|
19426
|
+
outcome = { kind: "same" };
|
|
19427
|
+
const list2 = after.candidates.map(candidateSig).join("\n");
|
|
19428
|
+
for (const n of after.candidates) {
|
|
19429
|
+
const s = candidateSig(n);
|
|
19430
|
+
if (seen.has(s)) continue;
|
|
19431
|
+
seen.add(s);
|
|
19432
|
+
work.push({ c: n, via: [...via, click] });
|
|
19433
|
+
revealed++;
|
|
19434
|
+
}
|
|
19435
|
+
if (list2 === homeList) pristine = true;
|
|
19436
|
+
} else {
|
|
19437
|
+
outcome = { kind: "surface", surface: key };
|
|
19438
|
+
if (!byKey.has(key)) {
|
|
19439
|
+
if (stats.surfaces >= budget.max_surfaces) {
|
|
19440
|
+
stats.truncated = true;
|
|
19441
|
+
} else {
|
|
19442
|
+
const path30 = !sameDoc && view === null ? [{ goto: after.url }] : [...surface.path, ...via, click];
|
|
19443
|
+
addSurface(after, place, view, path30, surface.depth + 1);
|
|
19444
|
+
}
|
|
19445
|
+
}
|
|
19446
|
+
}
|
|
19447
|
+
}
|
|
19448
|
+
} catch (err) {
|
|
19449
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
19450
|
+
outcome = { kind: "error", message: msg.split("\n")[0].slice(0, 200) };
|
|
19451
|
+
stats.errors++;
|
|
19452
|
+
lost = msg === LOST;
|
|
19453
|
+
}
|
|
19454
|
+
action.outcome = outcome;
|
|
19455
|
+
if (revealed > 0) action.revealed = revealed;
|
|
19456
|
+
const writes = takeBlocked();
|
|
19457
|
+
if (writes.length > 0) {
|
|
19458
|
+
action.blocked_requests = [...new Set(writes)];
|
|
19459
|
+
stats.blocked_requests += writes.length;
|
|
19460
|
+
}
|
|
19461
|
+
if (left) pristine = false;
|
|
19462
|
+
if (lost) break;
|
|
19463
|
+
}
|
|
19464
|
+
}
|
|
19465
|
+
return { surfaces, stats };
|
|
19466
|
+
}
|
|
19467
|
+
var UNSAFE_NAME, UNSAFE_PATH, SAFE_METHODS, CLICK_TIMEOUT_MS, LOST, NAV_TIMEOUT_MS, READ_PAGE;
|
|
19468
|
+
var init_explore_crawl = __esm({
|
|
19469
|
+
"src/commands/explore-crawl.ts"() {
|
|
19470
|
+
"use strict";
|
|
19471
|
+
init_dist2();
|
|
19472
|
+
UNSAFE_NAME = new RegExp(
|
|
19473
|
+
`(?<![\\p{L}\\p{N}])(${[
|
|
19474
|
+
// English
|
|
19475
|
+
"delete",
|
|
19476
|
+
"remove",
|
|
19477
|
+
"destroy",
|
|
19478
|
+
"erase",
|
|
19479
|
+
"pay",
|
|
19480
|
+
"pay now",
|
|
19481
|
+
"purchase",
|
|
19482
|
+
"buy",
|
|
19483
|
+
"checkout",
|
|
19484
|
+
"place order",
|
|
19485
|
+
"order now",
|
|
19486
|
+
"send",
|
|
19487
|
+
"submit",
|
|
19488
|
+
"save",
|
|
19489
|
+
"confirm",
|
|
19490
|
+
"approve",
|
|
19491
|
+
"reject",
|
|
19492
|
+
"decline",
|
|
19493
|
+
"publish",
|
|
19494
|
+
"archive",
|
|
19495
|
+
"unsubscribe",
|
|
19496
|
+
"log ?out",
|
|
19497
|
+
"sign ?out",
|
|
19498
|
+
"reset",
|
|
19499
|
+
"transfer",
|
|
19500
|
+
"invite",
|
|
19501
|
+
"upload",
|
|
19502
|
+
"import",
|
|
19503
|
+
"deactivate",
|
|
19504
|
+
"close account",
|
|
19505
|
+
// Swedish
|
|
19506
|
+
"ta bort",
|
|
19507
|
+
"radera",
|
|
19508
|
+
"betala",
|
|
19509
|
+
"k\xF6p",
|
|
19510
|
+
"best\xE4ll",
|
|
19511
|
+
"skicka",
|
|
19512
|
+
"skicka in",
|
|
19513
|
+
"spara",
|
|
19514
|
+
"bekr\xE4fta",
|
|
19515
|
+
"godk\xE4nn",
|
|
19516
|
+
"avsl\xE5",
|
|
19517
|
+
"publicera",
|
|
19518
|
+
"arkivera",
|
|
19519
|
+
"logga ut",
|
|
19520
|
+
"\xE5terst\xE4ll",
|
|
19521
|
+
"\xF6verf\xF6r",
|
|
19522
|
+
"bjud in",
|
|
19523
|
+
"ladda upp"
|
|
19524
|
+
].join("|")})(?![\\p{L}\\p{N}])`,
|
|
19525
|
+
"iu"
|
|
19526
|
+
);
|
|
19527
|
+
UNSAFE_PATH = /(^|\/)(log-?out|sign-?out|logga-?ut|delete|remove|destroy|unsubscribe)(\/|$)/i;
|
|
19528
|
+
SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
19529
|
+
CLICK_TIMEOUT_MS = 3e3;
|
|
19530
|
+
LOST = "could not return to surface";
|
|
19531
|
+
NAV_TIMEOUT_MS = 15e3;
|
|
19532
|
+
READ_PAGE = String.raw`(() => {
|
|
19533
|
+
const SELECTOR = [
|
|
19534
|
+
"a[href]", "button", "summary",
|
|
19535
|
+
'input[type="button"]', 'input[type="submit"]',
|
|
19536
|
+
'input[type="reset"]', 'input[type="image"]',
|
|
19537
|
+
'[role="button"]', '[role="link"]', '[role="tab"]',
|
|
19538
|
+
'[role="menuitem"]', '[role="menuitemradio"]', '[role="menuitemcheckbox"]',
|
|
19539
|
+
].join(",");
|
|
19540
|
+
const clean = (s) => (s ?? "").replace(/\s+/g, " ").trim();
|
|
19541
|
+
const visible = (el) => {
|
|
19542
|
+
if (typeof el.checkVisibility === "function" &&
|
|
19543
|
+
!el.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true })) {
|
|
19544
|
+
return false;
|
|
19545
|
+
}
|
|
19546
|
+
const r = el.getBoundingClientRect();
|
|
19547
|
+
return r.width > 0 && r.height > 0;
|
|
19548
|
+
};
|
|
19549
|
+
const textOf = (id) => clean(document.getElementById(id)?.innerText);
|
|
19550
|
+
const nameOf = (el) => {
|
|
19551
|
+
const label = clean(el.getAttribute("aria-label"));
|
|
19552
|
+
if (label) return label;
|
|
19553
|
+
const by = el.getAttribute("aria-labelledby");
|
|
19554
|
+
if (by) {
|
|
19555
|
+
const t = clean(by.split(/\s+/).map(textOf).join(" "));
|
|
19556
|
+
if (t) return t;
|
|
19557
|
+
}
|
|
19558
|
+
if (el instanceof HTMLInputElement) {
|
|
19559
|
+
return clean(el.value) || clean(el.alt) || clean(el.title);
|
|
19560
|
+
}
|
|
19561
|
+
const text = clean(el.innerText);
|
|
19562
|
+
if (text) return text.slice(0, 80);
|
|
19563
|
+
const img = el.querySelector("img[alt]");
|
|
19564
|
+
return clean(img?.getAttribute("alt")) || clean(el.title);
|
|
19565
|
+
};
|
|
19566
|
+
const roleOf = (el) =>
|
|
19567
|
+
el.getAttribute("role") || (el.tagName === "A" ? "link" : "button");
|
|
19568
|
+
|
|
19569
|
+
for (const el of document.querySelectorAll("[data-iamu-x]")) {
|
|
19570
|
+
el.removeAttribute("data-iamu-x");
|
|
19571
|
+
}
|
|
19572
|
+
const seen = new Map();
|
|
19573
|
+
const candidates = [];
|
|
19574
|
+
let index = 0;
|
|
19575
|
+
for (const el of document.querySelectorAll(SELECTOR)) {
|
|
19576
|
+
if (!visible(el)) continue;
|
|
19577
|
+
if (el.disabled || el.getAttribute("aria-disabled") === "true") continue;
|
|
19578
|
+
const role = roleOf(el);
|
|
19579
|
+
const name = nameOf(el);
|
|
19580
|
+
const sig = role + "\n" + name;
|
|
19581
|
+
const occurrence = seen.get(sig) ?? 0;
|
|
19582
|
+
seen.set(sig, occurrence + 1);
|
|
19583
|
+
let submit = false;
|
|
19584
|
+
if (el instanceof HTMLButtonElement) {
|
|
19585
|
+
// A button in a form submits unless it says type="button".
|
|
19586
|
+
submit = el.form
|
|
19587
|
+
? el.type === "submit" || el.type === "reset"
|
|
19588
|
+
: el.getAttribute("type") === "submit";
|
|
19589
|
+
} else if (el instanceof HTMLInputElement) {
|
|
19590
|
+
submit = ["submit", "reset", "image"].includes(el.type);
|
|
19591
|
+
}
|
|
19592
|
+
const anchor = el instanceof HTMLAnchorElement ? el : null;
|
|
19593
|
+
el.setAttribute("data-iamu-x", String(index));
|
|
19594
|
+
candidates.push({
|
|
19595
|
+
index, role, name, occurrence,
|
|
19596
|
+
href: anchor ? anchor.href : null,
|
|
19597
|
+
download: anchor ? anchor.hasAttribute("download") : false,
|
|
19598
|
+
newTab: anchor ? anchor.target === "_blank" : false,
|
|
19599
|
+
submit,
|
|
19600
|
+
});
|
|
19601
|
+
index++;
|
|
19602
|
+
}
|
|
19603
|
+
|
|
19604
|
+
const headings = [];
|
|
19605
|
+
for (const el of document.querySelectorAll(
|
|
19606
|
+
'h1, h2, [role="heading"][aria-level="1"], [role="heading"][aria-level="2"]',
|
|
19607
|
+
)) {
|
|
19608
|
+
if (!visible(el)) continue;
|
|
19609
|
+
const t = clean(el.innerText);
|
|
19610
|
+
if (t) headings.push(t.slice(0, 120));
|
|
19611
|
+
}
|
|
19612
|
+
|
|
19613
|
+
const fields = [];
|
|
19614
|
+
for (const f of document.querySelectorAll("input, select, textarea")) {
|
|
19615
|
+
if (["hidden", "submit", "button", "reset", "image"].includes(f.type)) continue;
|
|
19616
|
+
if (!visible(f)) continue;
|
|
19617
|
+
const forLabel = f.id
|
|
19618
|
+
? document.querySelector('label[for="' + CSS.escape(f.id) + '"]')
|
|
19619
|
+
: null;
|
|
19620
|
+
const name =
|
|
19621
|
+
clean(f.getAttribute("aria-label")) ||
|
|
19622
|
+
clean(forLabel?.innerText) ||
|
|
19623
|
+
clean(f.closest("label")?.innerText) ||
|
|
19624
|
+
clean(f.placeholder) ||
|
|
19625
|
+
clean(f.name);
|
|
19626
|
+
if (name && !fields.includes(name)) fields.push(name.slice(0, 80));
|
|
19627
|
+
if (fields.length >= 40) break;
|
|
19628
|
+
}
|
|
19629
|
+
|
|
19630
|
+
return { url: location.href, title: clean(document.title), headings, fields, candidates };
|
|
19631
|
+
})()`;
|
|
19632
|
+
}
|
|
19633
|
+
});
|
|
19634
|
+
|
|
19635
|
+
// src/commands/explore.ts
|
|
19636
|
+
var explore_exports = {};
|
|
19637
|
+
__export(explore_exports, {
|
|
19638
|
+
exploreCoverageCommand: () => exploreCoverageCommand,
|
|
19639
|
+
exploreDiffCommand: () => exploreDiffCommand,
|
|
19640
|
+
exploreList: () => exploreList,
|
|
19641
|
+
exploreRun: () => exploreRun
|
|
19642
|
+
});
|
|
19643
|
+
import { promises as fs27 } from "fs";
|
|
19644
|
+
import path25 from "path";
|
|
19645
|
+
import chalk26 from "chalk";
|
|
19646
|
+
function fail2(message, json2) {
|
|
19647
|
+
if (json2) console.error(JSON.stringify({ error: message }));
|
|
19648
|
+
else console.error(chalk26.red(` ${message}`));
|
|
19649
|
+
process.exitCode = 1;
|
|
19650
|
+
}
|
|
19651
|
+
function positiveInt(raw, fallback, flag) {
|
|
19652
|
+
if (raw === void 0) return fallback;
|
|
19653
|
+
const n = Number(raw);
|
|
19654
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
19655
|
+
throw new Error(`${flag} must be a positive whole number, got "${raw}"`);
|
|
19656
|
+
}
|
|
19657
|
+
return n;
|
|
19658
|
+
}
|
|
19659
|
+
async function testabilityTarget(projectRoot) {
|
|
19660
|
+
try {
|
|
19661
|
+
const raw = await fs27.readFile(
|
|
19662
|
+
path25.join(projectRoot, "iamu", "testability.md"),
|
|
19663
|
+
"utf8"
|
|
19664
|
+
);
|
|
19665
|
+
const { frontmatter } = parseFrontmatter(raw);
|
|
19666
|
+
return typeof frontmatter.target_url === "string" ? frontmatter.target_url : null;
|
|
19667
|
+
} catch {
|
|
19668
|
+
return null;
|
|
19669
|
+
}
|
|
19670
|
+
}
|
|
19671
|
+
async function openPage(opts) {
|
|
19672
|
+
const { chromium } = await import("playwright-core");
|
|
19673
|
+
const viewport = parseViewport(opts.viewport) ?? {
|
|
19674
|
+
width: 1280,
|
|
19675
|
+
height: 800
|
|
19676
|
+
};
|
|
19677
|
+
if (opts.attach) {
|
|
19678
|
+
const state = readBrowseState();
|
|
19679
|
+
if (!state) {
|
|
19680
|
+
throw new Error(
|
|
19681
|
+
"No browser to attach to \u2014 start one with `iamu browse start` and sign in first."
|
|
19682
|
+
);
|
|
19683
|
+
}
|
|
19684
|
+
const browser2 = await chromium.connectOverCDP(
|
|
19685
|
+
`http://127.0.0.1:${state.port}`
|
|
19686
|
+
);
|
|
19687
|
+
const context = browser2.contexts()[0] ?? await browser2.newContext();
|
|
19688
|
+
const page2 = await context.newPage();
|
|
19689
|
+
await page2.setViewportSize(viewport);
|
|
19690
|
+
return {
|
|
19691
|
+
page: page2,
|
|
19692
|
+
close: async () => {
|
|
19693
|
+
await page2.close().catch(() => {
|
|
19694
|
+
});
|
|
19695
|
+
await browser2.close().catch(() => {
|
|
19696
|
+
});
|
|
19697
|
+
}
|
|
19698
|
+
};
|
|
19699
|
+
}
|
|
19700
|
+
const chrome = findChrome();
|
|
19701
|
+
if (!chrome) {
|
|
19702
|
+
throw new Error(
|
|
19703
|
+
"No Chrome or Chromium found \u2014 install one or set IAMU_BROWSER."
|
|
19704
|
+
);
|
|
19705
|
+
}
|
|
19706
|
+
const browser = await chromium.launch({
|
|
19707
|
+
executablePath: chrome,
|
|
19708
|
+
headless: true,
|
|
19709
|
+
chromiumSandbox: !shouldDisableSandbox()
|
|
19710
|
+
});
|
|
19711
|
+
const page = await browser.newPage({ viewport, serviceWorkers: "block" });
|
|
19712
|
+
return { page, close: () => browser.close().catch(() => {
|
|
19713
|
+
}) };
|
|
19714
|
+
}
|
|
19715
|
+
async function exploreRun(url, opts) {
|
|
19716
|
+
const ctx = await resolveProject(opts);
|
|
19717
|
+
if (!ctx) return;
|
|
19718
|
+
const target = url ?? await testabilityTarget(ctx.projectRoot);
|
|
19719
|
+
if (!target) {
|
|
19720
|
+
fail2(
|
|
19721
|
+
"No URL to explore \u2014 pass one (iamu explore http://localhost:3000) or set target_url in iamu/testability.md.",
|
|
19722
|
+
opts.json
|
|
19723
|
+
);
|
|
19724
|
+
return;
|
|
19725
|
+
}
|
|
19726
|
+
let startUrl;
|
|
19727
|
+
let budget;
|
|
19728
|
+
let deny;
|
|
19729
|
+
try {
|
|
19730
|
+
startUrl = new URL(target).href;
|
|
19731
|
+
budget = {
|
|
19732
|
+
max_surfaces: positiveInt(
|
|
19733
|
+
opts.maxSurfaces,
|
|
19734
|
+
DEFAULTS.max_surfaces,
|
|
19735
|
+
"--max-surfaces"
|
|
19736
|
+
),
|
|
19737
|
+
max_actions: positiveInt(
|
|
19738
|
+
opts.maxActions,
|
|
19739
|
+
DEFAULTS.max_actions,
|
|
19740
|
+
"--max-actions"
|
|
19741
|
+
),
|
|
19742
|
+
max_depth: positiveInt(opts.maxDepth, DEFAULTS.max_depth, "--max-depth")
|
|
19743
|
+
};
|
|
19744
|
+
deny = (opts.deny ?? []).map((d) => new RegExp(d, "i"));
|
|
19745
|
+
} catch (err) {
|
|
19746
|
+
fail2(err.message, opts.json);
|
|
19747
|
+
return;
|
|
19748
|
+
}
|
|
19749
|
+
const quiet = opts.json || process.env.IAMU_QUIET === "1";
|
|
19750
|
+
if (!quiet) {
|
|
19751
|
+
console.error(chalk26.dim(` Exploring ${startUrl} (read-only)\u2026`));
|
|
19752
|
+
}
|
|
19753
|
+
const now = /* @__PURE__ */ new Date();
|
|
19754
|
+
let opened = null;
|
|
19755
|
+
let result;
|
|
19756
|
+
try {
|
|
19757
|
+
opened = await openPage(opts);
|
|
19758
|
+
result = await crawl(opened.page, {
|
|
19759
|
+
startUrl,
|
|
19760
|
+
budget,
|
|
19761
|
+
deny,
|
|
19762
|
+
log: quiet ? void 0 : (line) => console.error(chalk26.dim(line))
|
|
19763
|
+
});
|
|
19764
|
+
} catch (err) {
|
|
19765
|
+
fail2(err.message.split("\n")[0], opts.json);
|
|
19766
|
+
return;
|
|
19767
|
+
} finally {
|
|
19768
|
+
await opened?.close();
|
|
19769
|
+
}
|
|
19770
|
+
const build = detectBuild({ cwd: ctx.projectRoot });
|
|
19771
|
+
const map2 = {
|
|
19772
|
+
schema: 1,
|
|
19773
|
+
id: composeExploreId(now),
|
|
19774
|
+
created_at: now.toISOString(),
|
|
19775
|
+
target_url: startUrl,
|
|
19776
|
+
...build ? { build: { ...build } } : {},
|
|
19777
|
+
budget,
|
|
19778
|
+
stats: result.stats,
|
|
19779
|
+
surfaces: result.surfaces
|
|
19780
|
+
};
|
|
19781
|
+
const file = await writeExploreMap(ctx.projectRoot, map2);
|
|
19782
|
+
const rel = path25.relative(process.cwd(), file);
|
|
19783
|
+
if (opts.json) {
|
|
19784
|
+
console.log(JSON.stringify({ id: map2.id, path: rel, stats: map2.stats }));
|
|
19785
|
+
return;
|
|
19786
|
+
}
|
|
19787
|
+
const s = map2.stats;
|
|
19788
|
+
console.log(
|
|
19789
|
+
`
|
|
19790
|
+
${chalk26.bold(`${s.surfaces} surfaces`)} \xB7 ${s.actions_tried} controls tried \xB7 ${s.skipped} skipped \xB7 ${s.blocked_requests} writes blocked${s.errors ? ` \xB7 ${s.errors} errors` : ""}`
|
|
19791
|
+
);
|
|
19792
|
+
if (s.truncated) {
|
|
19793
|
+
console.log(
|
|
19794
|
+
chalk26.yellow(
|
|
19795
|
+
" Stopped by the budget before the product ran out \u2014 raise --max-surfaces / --max-actions / --max-depth to go further."
|
|
19796
|
+
)
|
|
19797
|
+
);
|
|
19798
|
+
}
|
|
19799
|
+
console.log(` Map: ${rel}`);
|
|
19800
|
+
console.log(chalk26.dim(" Next: iamu explore coverage"));
|
|
19801
|
+
}
|
|
19802
|
+
async function resolveMapId(projectRoot, id, offsetFromEnd = 1) {
|
|
19803
|
+
if (id && id !== "latest") return id;
|
|
19804
|
+
const ids = await listExploreMaps(projectRoot);
|
|
19805
|
+
return ids.at(-offsetFromEnd) ?? null;
|
|
19806
|
+
}
|
|
19807
|
+
async function exploreList(opts) {
|
|
19808
|
+
const ctx = await resolveProject(opts);
|
|
19809
|
+
if (!ctx) return;
|
|
19810
|
+
const rows = [];
|
|
19811
|
+
for (const id of await listExploreMaps(ctx.projectRoot)) {
|
|
19812
|
+
try {
|
|
19813
|
+
const m = await readExploreMap(ctx.projectRoot, id);
|
|
19814
|
+
rows.push({ id, target_url: m.target_url, surfaces: m.surfaces.length });
|
|
19815
|
+
} catch {
|
|
19816
|
+
}
|
|
19817
|
+
}
|
|
19818
|
+
if (opts.json) {
|
|
19819
|
+
console.log(JSON.stringify({ maps: rows }));
|
|
19820
|
+
return;
|
|
19821
|
+
}
|
|
19822
|
+
if (rows.length === 0) {
|
|
19823
|
+
console.log(" No maps yet. Run: iamu explore <url>");
|
|
19824
|
+
return;
|
|
19825
|
+
}
|
|
19826
|
+
for (const r of rows) {
|
|
19827
|
+
console.log(
|
|
19828
|
+
` ${r.id} ${r.surfaces} surfaces ${chalk26.dim(r.target_url)}`
|
|
19829
|
+
);
|
|
19830
|
+
}
|
|
19831
|
+
}
|
|
19832
|
+
async function exploreCoverageCommand(mapId, opts) {
|
|
19833
|
+
const ctx = await resolveProject(opts);
|
|
19834
|
+
if (!ctx) return;
|
|
19835
|
+
const id = await resolveMapId(ctx.projectRoot, mapId);
|
|
19836
|
+
if (!id) {
|
|
19837
|
+
fail2("No map yet. Run: iamu explore <url>", opts.json);
|
|
19838
|
+
return;
|
|
19839
|
+
}
|
|
19840
|
+
let map2;
|
|
19841
|
+
try {
|
|
19842
|
+
map2 = await readExploreMap(ctx.projectRoot, id);
|
|
19843
|
+
} catch (err) {
|
|
19844
|
+
fail2(err.message, opts.json);
|
|
19845
|
+
return;
|
|
19846
|
+
}
|
|
19847
|
+
const cov = exploreCoverage(map2, await readAllRunSteps(ctx.projectRoot));
|
|
19848
|
+
if (opts.json) {
|
|
19849
|
+
console.log(JSON.stringify(cov));
|
|
19850
|
+
return;
|
|
19851
|
+
}
|
|
19852
|
+
console.log(
|
|
19853
|
+
`
|
|
19854
|
+
Map ${id} (${map2.surfaces.length} surfaces) against ${cov.runs} runs, ${cov.steps} steps`
|
|
19855
|
+
);
|
|
19856
|
+
console.log(
|
|
19857
|
+
` ${chalk26.green(`${cov.reached} reached`)} \xB7 ${chalk26.yellow(`${cov.place_only} URL reached, view unclear`)} \xB7 ${chalk26.red(`${cov.unreached} never reached`)}`
|
|
19858
|
+
);
|
|
19859
|
+
const section = (title, rows, color) => {
|
|
19860
|
+
if (rows.length === 0) return;
|
|
19861
|
+
console.log(`
|
|
19862
|
+
${color(title)}`);
|
|
19863
|
+
for (const r of rows) {
|
|
19864
|
+
console.log(
|
|
19865
|
+
` ${r.key.padEnd(32)} ${r.label}${chalk26.dim(` (depth ${r.depth})`)}`
|
|
19866
|
+
);
|
|
19867
|
+
}
|
|
19868
|
+
};
|
|
19869
|
+
section(
|
|
19870
|
+
"Never reached by a run",
|
|
19871
|
+
cov.surfaces.filter((s) => s.status === "unreached"),
|
|
19872
|
+
chalk26.red
|
|
19873
|
+
);
|
|
19874
|
+
section(
|
|
19875
|
+
"URL reached, but no step names this view",
|
|
19876
|
+
cov.surfaces.filter((s) => s.status === "place_only"),
|
|
19877
|
+
chalk26.yellow
|
|
19878
|
+
);
|
|
19879
|
+
if (cov.run_only_places.length > 0) {
|
|
19880
|
+
console.log(`
|
|
19881
|
+
${chalk26.cyan("Walked by runs, not in the map")}`);
|
|
19882
|
+
for (const p of cov.run_only_places) {
|
|
19883
|
+
console.log(
|
|
19884
|
+
` ${p.place.padEnd(32)} ${p.steps} steps, ${p.runs.length} runs`
|
|
19885
|
+
);
|
|
19886
|
+
}
|
|
19887
|
+
}
|
|
19888
|
+
if (cov.runs === 0) {
|
|
19889
|
+
console.log(
|
|
19890
|
+
chalk26.dim(
|
|
19891
|
+
"\n No runs yet \u2014 every surface counts as never reached. Walk a scenario first (iamu-run)."
|
|
19892
|
+
)
|
|
19893
|
+
);
|
|
19894
|
+
}
|
|
19895
|
+
}
|
|
19896
|
+
async function exploreDiffCommand(a, b, opts) {
|
|
19897
|
+
const ctx = await resolveProject(opts);
|
|
19898
|
+
if (!ctx) return;
|
|
19899
|
+
const idA = a ?? await resolveMapId(ctx.projectRoot, void 0, 2);
|
|
19900
|
+
const idB = b ?? await resolveMapId(ctx.projectRoot, void 0, 1);
|
|
19901
|
+
if (!idA || !idB || idA === idB) {
|
|
19902
|
+
fail2(
|
|
19903
|
+
"Need two maps to compare \u2014 explore again (iamu explore <url>), or name both maps.",
|
|
19904
|
+
opts.json
|
|
19905
|
+
);
|
|
19906
|
+
return;
|
|
19907
|
+
}
|
|
19908
|
+
let mapA;
|
|
19909
|
+
let mapB;
|
|
19910
|
+
try {
|
|
19911
|
+
mapA = await readExploreMap(ctx.projectRoot, idA);
|
|
19912
|
+
mapB = await readExploreMap(ctx.projectRoot, idB);
|
|
19913
|
+
} catch (err) {
|
|
19914
|
+
fail2(err.message, opts.json);
|
|
19915
|
+
return;
|
|
19916
|
+
}
|
|
19917
|
+
const d = diffExploreMaps(mapA, mapB);
|
|
19918
|
+
if (opts.json) {
|
|
19919
|
+
console.log(JSON.stringify(d));
|
|
19920
|
+
return;
|
|
19921
|
+
}
|
|
19922
|
+
const pct = Math.round(d.surface_overlap * 100);
|
|
19923
|
+
console.log(`
|
|
19924
|
+
${idA} \u2192 ${idB}`);
|
|
19925
|
+
console.log(
|
|
19926
|
+
` ${chalk26.bold(`${pct}% of surfaces match`)} (${d.common} shared, ${d.only_a.length} only in the first, ${d.only_b.length} only in the second)`
|
|
19927
|
+
);
|
|
19928
|
+
if (d.only_a.length > 0) {
|
|
19929
|
+
console.log(`
|
|
19930
|
+
${chalk26.red("Only in the first")}`);
|
|
19931
|
+
for (const s of d.only_a) console.log(` ${s.key.padEnd(32)} ${s.label}`);
|
|
19932
|
+
}
|
|
19933
|
+
if (d.only_b.length > 0) {
|
|
19934
|
+
console.log(`
|
|
19935
|
+
${chalk26.green("Only in the second")}`);
|
|
19936
|
+
for (const s of d.only_b) console.log(` ${s.key.padEnd(32)} ${s.label}`);
|
|
19937
|
+
}
|
|
19938
|
+
if (d.changed_actions.length > 0) {
|
|
19939
|
+
console.log(
|
|
19940
|
+
`
|
|
19941
|
+
${chalk26.yellow(`Controls differ on ${d.changed_actions.length} shared surfaces`)}`
|
|
19942
|
+
);
|
|
19943
|
+
for (const c of d.changed_actions) {
|
|
19944
|
+
console.log(` ${c.key} ${c.label}`);
|
|
19945
|
+
for (const x of c.only_a) console.log(chalk26.red(` - ${x}`));
|
|
19946
|
+
for (const x of c.only_b) console.log(chalk26.green(` + ${x}`));
|
|
19947
|
+
}
|
|
19948
|
+
}
|
|
19949
|
+
}
|
|
19950
|
+
var DEFAULTS;
|
|
19951
|
+
var init_explore = __esm({
|
|
19952
|
+
"src/commands/explore.ts"() {
|
|
19953
|
+
"use strict";
|
|
19954
|
+
init_dist2();
|
|
19955
|
+
init_build_info();
|
|
19956
|
+
init_browse();
|
|
19957
|
+
init_context4();
|
|
19958
|
+
init_explore_crawl();
|
|
19959
|
+
DEFAULTS = { max_surfaces: 40, max_actions: 300, max_depth: 6 };
|
|
19960
|
+
}
|
|
19961
|
+
});
|
|
19962
|
+
|
|
18688
19963
|
// src/commands/completion.ts
|
|
18689
19964
|
var completion_exports = {};
|
|
18690
19965
|
__export(completion_exports, {
|
|
@@ -18881,7 +20156,7 @@ __export(instructions_walk_exports, {
|
|
|
18881
20156
|
});
|
|
18882
20157
|
import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
|
|
18883
20158
|
import { join as join9 } from "path";
|
|
18884
|
-
import
|
|
20159
|
+
import chalk27 from "chalk";
|
|
18885
20160
|
function selfCommand() {
|
|
18886
20161
|
const q = (p) => /[\s"'$]/.test(p) ? JSON.stringify(p) : p;
|
|
18887
20162
|
return `${q(process.execPath)} ${q(process.argv[1] ?? "iamu")}`;
|
|
@@ -19208,35 +20483,35 @@ async function instructionsWalk(opts) {
|
|
|
19208
20483
|
return;
|
|
19209
20484
|
}
|
|
19210
20485
|
console.log();
|
|
19211
|
-
console.log(
|
|
19212
|
-
console.log(` ${
|
|
19213
|
-
console.log(` ${
|
|
19214
|
-
console.log(` ${
|
|
20486
|
+
console.log(chalk27.bold(" Walk instructions"));
|
|
20487
|
+
console.log(` ${chalk27.dim("scenario:")} ${sfm.slug}`);
|
|
20488
|
+
console.log(` ${chalk27.dim("persona:")} ${pfm.name} (${pfm.role})`);
|
|
20489
|
+
console.log(` ${chalk27.dim("outline:")} ${ofm.title}`);
|
|
19215
20490
|
if (sfm.state?.description || sfm.state?.approximation) {
|
|
19216
20491
|
console.log(
|
|
19217
|
-
` ${
|
|
20492
|
+
` ${chalk27.dim("state:")} ${sfm.state.description ? sfm.state.description : sfm.state.approximation ?? ""}`
|
|
19218
20493
|
);
|
|
19219
20494
|
}
|
|
19220
|
-
if (targetUrl) console.log(` ${
|
|
20495
|
+
if (targetUrl) console.log(` ${chalk27.dim("target:")} ${targetUrl}`);
|
|
19221
20496
|
console.log(
|
|
19222
|
-
` ${
|
|
20497
|
+
` ${chalk27.dim("access:")} ${projectAccess || personaAccess ? `${projectAccess ? "project" : ""}${projectAccess && personaAccess ? " + " : ""}${personaAccess ? "persona" : ""}` : "not recorded (walker would run as whoever is signed in)"}`
|
|
19223
20498
|
);
|
|
19224
20499
|
console.log(
|
|
19225
|
-
` ${
|
|
20500
|
+
` ${chalk27.dim("dismissed:")} ${dismissedFacts.length} findings to skip`
|
|
19226
20501
|
);
|
|
19227
20502
|
console.log(
|
|
19228
|
-
` ${
|
|
20503
|
+
` ${chalk27.dim("guidance:")} ${guidance.length} tester guidance rule${guidance.length === 1 ? "" : "s"}`
|
|
19229
20504
|
);
|
|
19230
20505
|
if (revisit.length > 0) {
|
|
19231
20506
|
console.log(
|
|
19232
|
-
` ${
|
|
20507
|
+
` ${chalk27.dim("re-check:")} ${revisit.length} place${revisit.length === 1 ? "" : "s"} where fixes await a re-run`
|
|
19233
20508
|
);
|
|
19234
20509
|
}
|
|
19235
20510
|
console.log(
|
|
19236
|
-
` ${
|
|
20511
|
+
` ${chalk27.dim("methodology:")} ${methodology ? "loaded" : "missing (run iamu update)"}`
|
|
19237
20512
|
);
|
|
19238
20513
|
console.log();
|
|
19239
|
-
console.log(
|
|
20514
|
+
console.log(chalk27.dim(" Use --json to get the full instruction bundle."));
|
|
19240
20515
|
console.log();
|
|
19241
20516
|
}
|
|
19242
20517
|
var SCREEN;
|
|
@@ -19262,8 +20537,8 @@ __export(new_finding_exports, {
|
|
|
19262
20537
|
});
|
|
19263
20538
|
import { randomUUID as randomUUID10 } from "crypto";
|
|
19264
20539
|
import { access, readFile as readFile6 } from "fs/promises";
|
|
19265
|
-
import
|
|
19266
|
-
import
|
|
20540
|
+
import path26 from "path";
|
|
20541
|
+
import chalk28 from "chalk";
|
|
19267
20542
|
async function newFinding(opts) {
|
|
19268
20543
|
const ctx = await resolveProject(opts);
|
|
19269
20544
|
if (!ctx) return;
|
|
@@ -19329,7 +20604,7 @@ async function newFinding(opts) {
|
|
|
19329
20604
|
}
|
|
19330
20605
|
try {
|
|
19331
20606
|
await access(
|
|
19332
|
-
|
|
20607
|
+
path26.join(runDirPath, "steps", `${String(n).padStart(3, "0")}.json`)
|
|
19333
20608
|
);
|
|
19334
20609
|
} catch {
|
|
19335
20610
|
commandError(
|
|
@@ -19344,7 +20619,7 @@ async function newFinding(opts) {
|
|
|
19344
20619
|
if (!scenarioSlug) {
|
|
19345
20620
|
try {
|
|
19346
20621
|
const manifestRaw = await readFile6(
|
|
19347
|
-
|
|
20622
|
+
path26.join(runDirPath, "walkthrough.json"),
|
|
19348
20623
|
"utf8"
|
|
19349
20624
|
);
|
|
19350
20625
|
const manifest = JSON.parse(manifestRaw);
|
|
@@ -19433,10 +20708,10 @@ async function newFinding(opts) {
|
|
|
19433
20708
|
return;
|
|
19434
20709
|
}
|
|
19435
20710
|
console.log();
|
|
19436
|
-
console.log(` ${symbols.success} Created finding: ${
|
|
19437
|
-
console.log(` ${
|
|
19438
|
-
console.log(` ${
|
|
19439
|
-
for (const w of warnings) console.log(` ${
|
|
20711
|
+
console.log(` ${symbols.success} Created finding: ${chalk28.cyan(slug)}`);
|
|
20712
|
+
console.log(` ${chalk28.dim("path:")} ${record.path}`);
|
|
20713
|
+
console.log(` ${chalk28.dim("id:")} ${id}`);
|
|
20714
|
+
for (const w of warnings) console.log(` ${chalk28.yellow("!")} ${w}`);
|
|
19440
20715
|
console.log();
|
|
19441
20716
|
} catch (err) {
|
|
19442
20717
|
commandError(
|
|
@@ -19462,7 +20737,7 @@ __export(view_results_exports, {
|
|
|
19462
20737
|
promptViewResults: () => promptViewResults
|
|
19463
20738
|
});
|
|
19464
20739
|
import { isCancel as isCancel4, select as select3 } from "@clack/prompts";
|
|
19465
|
-
import
|
|
20740
|
+
import chalk29 from "chalk";
|
|
19466
20741
|
function launchCloudDashboard(config, runId) {
|
|
19467
20742
|
const cloudRunId = runId ? toCloudRunUuid(runId) : void 0;
|
|
19468
20743
|
const url = getCloudDashboardUrl(
|
|
@@ -19471,7 +20746,7 @@ function launchCloudDashboard(config, runId) {
|
|
|
19471
20746
|
cloudRunId
|
|
19472
20747
|
);
|
|
19473
20748
|
console.log();
|
|
19474
|
-
console.log(` ${symbols.success} Opening Iamu Cloud: ${
|
|
20749
|
+
console.log(` ${symbols.success} Opening Iamu Cloud: ${chalk29.cyan(url)}
|
|
19475
20750
|
`);
|
|
19476
20751
|
openBrowser(url);
|
|
19477
20752
|
}
|
|
@@ -19539,9 +20814,9 @@ __export(evaluate_exports, {
|
|
|
19539
20814
|
evaluate: () => evaluate,
|
|
19540
20815
|
evaluateRecord: () => evaluateRecord
|
|
19541
20816
|
});
|
|
19542
|
-
import { promises as
|
|
19543
|
-
import
|
|
19544
|
-
import
|
|
20817
|
+
import { promises as fs28 } from "fs";
|
|
20818
|
+
import path27 from "path";
|
|
20819
|
+
import chalk30 from "chalk";
|
|
19545
20820
|
import { z as z25 } from "zod";
|
|
19546
20821
|
function formatIssues(error) {
|
|
19547
20822
|
return error.issues.map(
|
|
@@ -19571,10 +20846,10 @@ async function evaluate(runId, opts) {
|
|
|
19571
20846
|
const resolved = await resolveProjectAndRunDir(runId, opts);
|
|
19572
20847
|
if (!resolved) return;
|
|
19573
20848
|
const { projectRoot, runDirPath } = resolved;
|
|
19574
|
-
const canonicalPath =
|
|
20849
|
+
const canonicalPath = path27.join(runDirPath, "canonical.json");
|
|
19575
20850
|
let canonical;
|
|
19576
20851
|
try {
|
|
19577
|
-
const raw = await
|
|
20852
|
+
const raw = await fs28.readFile(canonicalPath, "utf8");
|
|
19578
20853
|
canonical = JSON.parse(raw);
|
|
19579
20854
|
} catch {
|
|
19580
20855
|
commandError(
|
|
@@ -19630,15 +20905,15 @@ async function evaluate(runId, opts) {
|
|
|
19630
20905
|
)
|
|
19631
20906
|
};
|
|
19632
20907
|
const prompt = composeJudgePrompt(input);
|
|
19633
|
-
const promptFilePath = opts?.promptFile ??
|
|
19634
|
-
await
|
|
20908
|
+
const promptFilePath = opts?.promptFile ?? path27.join(runDirPath, "judge_prompt.md");
|
|
20909
|
+
await fs28.writeFile(promptFilePath, prompt, "utf8");
|
|
19635
20910
|
if (opts?.json) {
|
|
19636
20911
|
console.log(
|
|
19637
20912
|
JSON.stringify(
|
|
19638
20913
|
{
|
|
19639
20914
|
...input,
|
|
19640
|
-
prompt_file:
|
|
19641
|
-
prompt_file_abs:
|
|
20915
|
+
prompt_file: path27.relative(projectRoot, promptFilePath),
|
|
20916
|
+
prompt_file_abs: path27.resolve(promptFilePath),
|
|
19642
20917
|
judge_prompt: prompt
|
|
19643
20918
|
},
|
|
19644
20919
|
null,
|
|
@@ -19653,10 +20928,10 @@ async function evaluateRecord(runId, opts) {
|
|
|
19653
20928
|
const resolved = await resolveProjectAndRunDir(runId, opts);
|
|
19654
20929
|
if (!resolved) return;
|
|
19655
20930
|
const { projectRoot, runDirPath } = resolved;
|
|
19656
|
-
const canonicalPath =
|
|
20931
|
+
const canonicalPath = path27.join(runDirPath, "canonical.json");
|
|
19657
20932
|
let canonical;
|
|
19658
20933
|
try {
|
|
19659
|
-
const raw = await
|
|
20934
|
+
const raw = await fs28.readFile(canonicalPath, "utf8");
|
|
19660
20935
|
canonical = JSON.parse(raw);
|
|
19661
20936
|
} catch {
|
|
19662
20937
|
commandError(`No canonical.json for run: ${runId}`, opts);
|
|
@@ -19664,7 +20939,7 @@ async function evaluateRecord(runId, opts) {
|
|
|
19664
20939
|
}
|
|
19665
20940
|
let parsedFile;
|
|
19666
20941
|
try {
|
|
19667
|
-
const raw = await
|
|
20942
|
+
const raw = await fs28.readFile(opts.file, "utf8");
|
|
19668
20943
|
parsedFile = JSON.parse(raw);
|
|
19669
20944
|
} catch {
|
|
19670
20945
|
commandError(`Cannot read verdicts file: ${opts.file}`, opts);
|
|
@@ -19721,8 +20996,8 @@ ${formatIssues(fileResult.error)}`,
|
|
|
19721
20996
|
overall: inputVerdicts.overall
|
|
19722
20997
|
});
|
|
19723
20998
|
const reportContent = renderCanonicalMarkdown(canonicalForReport);
|
|
19724
|
-
const verdictsPath =
|
|
19725
|
-
const reportPath =
|
|
20999
|
+
const verdictsPath = path27.join(runDirPath, "verdicts.json");
|
|
21000
|
+
const reportPath = path27.join(runDirPath, "report.md");
|
|
19726
21001
|
await Promise.all([
|
|
19727
21002
|
atomicWriteFile(
|
|
19728
21003
|
verdictsPath,
|
|
@@ -19742,8 +21017,8 @@ ${formatIssues(fileResult.error)}`,
|
|
|
19742
21017
|
JSON.stringify(
|
|
19743
21018
|
{
|
|
19744
21019
|
runId,
|
|
19745
|
-
verdictsPath:
|
|
19746
|
-
reportPath:
|
|
21020
|
+
verdictsPath: path27.relative(projectRoot, verdictsPath),
|
|
21021
|
+
reportPath: path27.relative(projectRoot, reportPath),
|
|
19747
21022
|
verdictsCount: validatedVerdicts.length,
|
|
19748
21023
|
linkedCount: linked.length,
|
|
19749
21024
|
...cloudInfo ? { cloud: cloudInfo } : {}
|
|
@@ -19756,18 +21031,18 @@ ${formatIssues(fileResult.error)}`,
|
|
|
19756
21031
|
}
|
|
19757
21032
|
console.log();
|
|
19758
21033
|
console.log(
|
|
19759
|
-
` ${symbols.success} Recorded ${validatedVerdicts.length} verdicts for run ${
|
|
21034
|
+
` ${symbols.success} Recorded ${validatedVerdicts.length} verdicts for run ${chalk30.cyan(runId)}`
|
|
19760
21035
|
);
|
|
19761
21036
|
if (linked.length > 0) {
|
|
19762
21037
|
console.log(
|
|
19763
|
-
` ${
|
|
21038
|
+
` ${chalk30.dim("linked:")} ${linked.length} finding${linked.length === 1 ? "" : "s"} to the human test${linked.length === 1 ? "" : "s"} citing ${linked.length === 1 ? "it" : "them"}`
|
|
19764
21039
|
);
|
|
19765
21040
|
}
|
|
19766
21041
|
console.log(
|
|
19767
|
-
` ${
|
|
21042
|
+
` ${chalk30.dim("verdicts:")} ${path27.relative(projectRoot, verdictsPath)}`
|
|
19768
21043
|
);
|
|
19769
21044
|
console.log(
|
|
19770
|
-
` ${
|
|
21045
|
+
` ${chalk30.dim("report:")} ${path27.relative(projectRoot, reportPath)}`
|
|
19771
21046
|
);
|
|
19772
21047
|
printCloudRunInfo(cloudInfo);
|
|
19773
21048
|
await promptViewResults(projectRoot, { ...opts, runId });
|
|
@@ -19831,9 +21106,9 @@ __export(complete_exports, {
|
|
|
19831
21106
|
screenshotSizeWarning: () => screenshotSizeWarning,
|
|
19832
21107
|
validateCanonicalWalkthrough: () => validateCanonicalWalkthrough
|
|
19833
21108
|
});
|
|
19834
|
-
import { promises as
|
|
19835
|
-
import
|
|
19836
|
-
import
|
|
21109
|
+
import { promises as fs29 } from "fs";
|
|
21110
|
+
import path28 from "path";
|
|
21111
|
+
import chalk31 from "chalk";
|
|
19837
21112
|
async function complete(runId, opts) {
|
|
19838
21113
|
const run = await loadRun(runId, opts);
|
|
19839
21114
|
if (!run) return;
|
|
@@ -19843,7 +21118,7 @@ async function complete(runId, opts) {
|
|
|
19843
21118
|
}
|
|
19844
21119
|
if (!opts?.json) {
|
|
19845
21120
|
console.log();
|
|
19846
|
-
console.log(
|
|
21121
|
+
console.log(chalk31.bold(" Assembling canonical JSON\u2026"));
|
|
19847
21122
|
}
|
|
19848
21123
|
const provenanceAudit = auditStepProvenance(
|
|
19849
21124
|
await readStepsForRun(run.projectRoot, runId)
|
|
@@ -19856,8 +21131,8 @@ async function complete(runId, opts) {
|
|
|
19856
21131
|
const walkerSummary = await readWalkerSummary(run.runDirPath);
|
|
19857
21132
|
let judgeReported;
|
|
19858
21133
|
try {
|
|
19859
|
-
const verdictsRaw = await
|
|
19860
|
-
|
|
21134
|
+
const verdictsRaw = await fs29.readFile(
|
|
21135
|
+
path28.join(run.runDirPath, "verdicts.json"),
|
|
19861
21136
|
"utf8"
|
|
19862
21137
|
);
|
|
19863
21138
|
const parsed = JSON.parse(verdictsRaw);
|
|
@@ -19909,16 +21184,16 @@ async function loadRun(runId, opts) {
|
|
|
19909
21184
|
if (!resolved) return null;
|
|
19910
21185
|
const { projectRoot, runDirPath } = resolved;
|
|
19911
21186
|
try {
|
|
19912
|
-
await
|
|
21187
|
+
await fs29.access(runDirPath);
|
|
19913
21188
|
} catch {
|
|
19914
21189
|
commandError(`Run not found: ${runId}`, opts);
|
|
19915
21190
|
return null;
|
|
19916
21191
|
}
|
|
19917
|
-
const manifestPath =
|
|
21192
|
+
const manifestPath = path28.join(runDirPath, "walkthrough.json");
|
|
19918
21193
|
let manifest;
|
|
19919
21194
|
try {
|
|
19920
21195
|
manifest = JSON.parse(
|
|
19921
|
-
await
|
|
21196
|
+
await fs29.readFile(manifestPath, "utf8")
|
|
19922
21197
|
);
|
|
19923
21198
|
} catch {
|
|
19924
21199
|
commandError(`No walkthrough.json found for run: ${runId}`, opts);
|
|
@@ -19930,8 +21205,8 @@ async function reportAlreadyCompleted(run, opts) {
|
|
|
19930
21205
|
const { runId, manifest } = run;
|
|
19931
21206
|
if (opts?.json) {
|
|
19932
21207
|
try {
|
|
19933
|
-
const raw = await
|
|
19934
|
-
|
|
21208
|
+
const raw = await fs29.readFile(
|
|
21209
|
+
path28.join(run.runDirPath, "canonical.json"),
|
|
19935
21210
|
"utf8"
|
|
19936
21211
|
);
|
|
19937
21212
|
console.log(raw.trim());
|
|
@@ -19951,18 +21226,18 @@ async function reportAlreadyCompleted(run, opts) {
|
|
|
19951
21226
|
return;
|
|
19952
21227
|
}
|
|
19953
21228
|
console.log();
|
|
19954
|
-
console.log(
|
|
19955
|
-
console.log(` status ${
|
|
21229
|
+
console.log(chalk31.bold(" Run: ") + chalk31.cyan(runId));
|
|
21230
|
+
console.log(` status ${chalk31.green("completed")}`);
|
|
19956
21231
|
if (manifest.completed_at) {
|
|
19957
|
-
console.log(` completed ${
|
|
21232
|
+
console.log(` completed ${chalk31.dim(manifest.completed_at)}`);
|
|
19958
21233
|
}
|
|
19959
21234
|
const viewUrl = await pushedRunUrl(run.projectRoot, runId);
|
|
19960
21235
|
if (viewUrl) {
|
|
19961
|
-
console.log(` view ${
|
|
21236
|
+
console.log(` view ${chalk31.underline(viewUrl)}`);
|
|
19962
21237
|
}
|
|
19963
21238
|
console.log();
|
|
19964
21239
|
console.log(
|
|
19965
|
-
|
|
21240
|
+
chalk31.dim(
|
|
19966
21241
|
" Already completed \u2014 nothing to do. Use --force to reassemble the report."
|
|
19967
21242
|
)
|
|
19968
21243
|
);
|
|
@@ -19978,13 +21253,13 @@ function reportStrictProvenanceFailure(audit, opts) {
|
|
|
19978
21253
|
}
|
|
19979
21254
|
console.error();
|
|
19980
21255
|
console.error(
|
|
19981
|
-
|
|
21256
|
+
chalk31.red(` ${symbols.error} Strict provenance audit failed:`)
|
|
19982
21257
|
);
|
|
19983
21258
|
for (const w of audit.warnings) {
|
|
19984
|
-
console.error(
|
|
21259
|
+
console.error(chalk31.red(` \u2022 ${w}`));
|
|
19985
21260
|
}
|
|
19986
21261
|
console.error(
|
|
19987
|
-
|
|
21262
|
+
chalk31.dim(
|
|
19988
21263
|
" Run was flagged as synthetic / scripted. Remove --strict-provenance to complete anyway."
|
|
19989
21264
|
)
|
|
19990
21265
|
);
|
|
@@ -20025,41 +21300,41 @@ function completeJsonPayload(canonical, extras) {
|
|
|
20025
21300
|
function printCompletionSummary(run, completedAt, assembled, extras, provenanceAudit) {
|
|
20026
21301
|
const { runId, projectRoot, manifest } = run;
|
|
20027
21302
|
const { canonical } = assembled;
|
|
20028
|
-
const count = (n) => n === 0 ?
|
|
20029
|
-
console.log(
|
|
20030
|
-
console.log(` scenario ${
|
|
20031
|
-
console.log(` url ${
|
|
21303
|
+
const count = (n) => n === 0 ? chalk31.dim("0") : chalk31.cyan(String(n));
|
|
21304
|
+
console.log(chalk31.bold(" Run: ") + chalk31.cyan(runId));
|
|
21305
|
+
console.log(` scenario ${chalk31.dim(manifest.scenario_slug)}`);
|
|
21306
|
+
console.log(` url ${chalk31.dim(manifest.target_url)}`);
|
|
20032
21307
|
if (manifest.environment) {
|
|
20033
|
-
console.log(` env ${
|
|
21308
|
+
console.log(` env ${chalk31.dim(manifest.environment)}`);
|
|
20034
21309
|
}
|
|
20035
21310
|
if (manifest.build) {
|
|
20036
|
-
console.log(` build ${
|
|
21311
|
+
console.log(` build ${chalk31.dim(formatBuild(manifest.build))}`);
|
|
20037
21312
|
}
|
|
20038
21313
|
if (manifest.viewport) {
|
|
20039
|
-
console.log(` device ${
|
|
21314
|
+
console.log(` device ${chalk31.dim(formatViewport(manifest.viewport))}`);
|
|
20040
21315
|
}
|
|
20041
|
-
console.log(` status ${
|
|
21316
|
+
console.log(` status ${chalk31.green("completed")}`);
|
|
20042
21317
|
console.log(` findings ${count(canonical.findings.length)}`);
|
|
20043
21318
|
console.log(` steps ${count(canonical.steps.length)}`);
|
|
20044
|
-
console.log(` started ${
|
|
20045
|
-
console.log(` completed ${
|
|
21319
|
+
console.log(` started ${chalk31.dim(manifest.created_at)}`);
|
|
21320
|
+
console.log(` completed ${chalk31.dim(completedAt)}`);
|
|
20046
21321
|
const models = manifest.models;
|
|
20047
21322
|
if (models?.velocity_sec_per_step != null) {
|
|
20048
21323
|
console.log(
|
|
20049
|
-
` velocity ${
|
|
21324
|
+
` velocity ${chalk31.cyan(`~${models.velocity_sec_per_step}s/step`)} ${chalk31.dim(`(${models.total_duration_sec}s total)`)}`
|
|
20050
21325
|
);
|
|
20051
21326
|
}
|
|
20052
21327
|
if (models && (models.walker || models.judge || models.matcher)) {
|
|
20053
21328
|
const w = models.walker_reported ? `${models.walker} (${models.walker_reported})` : models.walker ?? "fast";
|
|
20054
21329
|
const j = models.judge_reported ? `${models.judge} (${models.judge_reported})` : models.judge ?? "thorough";
|
|
20055
|
-
console.log(` models ${
|
|
21330
|
+
console.log(` models ${chalk31.dim(`walker: ${w} \xB7 judge: ${j}`)}`);
|
|
20056
21331
|
}
|
|
20057
21332
|
console.log();
|
|
20058
21333
|
console.log(
|
|
20059
|
-
` ${
|
|
21334
|
+
` ${chalk31.dim("canonical:")} ${chalk31.dim(path28.relative(projectRoot, assembled.canonicalPath))}`
|
|
20060
21335
|
);
|
|
20061
21336
|
console.log(
|
|
20062
|
-
` ${
|
|
21337
|
+
` ${chalk31.dim("report:")} ${chalk31.dim(path28.relative(projectRoot, assembled.reportPath))}`
|
|
20063
21338
|
);
|
|
20064
21339
|
if (extras.viewportWarning) {
|
|
20065
21340
|
console.log();
|
|
@@ -20067,7 +21342,7 @@ function printCompletionSummary(run, completedAt, assembled, extras, provenanceA
|
|
|
20067
21342
|
}
|
|
20068
21343
|
if (extras.codeWarning) {
|
|
20069
21344
|
console.log();
|
|
20070
|
-
console.log(` ${
|
|
21345
|
+
console.log(` ${chalk31.yellow("!")} ${extras.codeWarning}`);
|
|
20071
21346
|
}
|
|
20072
21347
|
if (extras.revisit) printRevisit(extras.revisit);
|
|
20073
21348
|
if (assembled.verification) printVerification(assembled.verification);
|
|
@@ -20079,13 +21354,13 @@ function printProvenanceWarnings(audit) {
|
|
|
20079
21354
|
if (audit.warnings.length === 0) return;
|
|
20080
21355
|
console.log();
|
|
20081
21356
|
console.log(
|
|
20082
|
-
` ${symbols.warning} ${
|
|
21357
|
+
` ${symbols.warning} ${chalk31.yellow("Step provenance audit flagged suspicious timing:")}`
|
|
20083
21358
|
);
|
|
20084
21359
|
for (const w of audit.warnings) {
|
|
20085
|
-
console.log(` ${
|
|
21360
|
+
console.log(` ${chalk31.dim("\u2022")} ${w}`);
|
|
20086
21361
|
}
|
|
20087
21362
|
console.log(
|
|
20088
|
-
|
|
21363
|
+
chalk31.dim(
|
|
20089
21364
|
" Run appears to be programmatically batch-generated rather than an interactive walkthrough."
|
|
20090
21365
|
)
|
|
20091
21366
|
);
|
|
@@ -20117,17 +21392,17 @@ function screenshotFitsViewport(width, expected) {
|
|
|
20117
21392
|
}
|
|
20118
21393
|
async function screenshotSizeWarning(runDirPath, viewport) {
|
|
20119
21394
|
if (!viewport) return null;
|
|
20120
|
-
const dir =
|
|
21395
|
+
const dir = path28.join(runDirPath, "screenshots");
|
|
20121
21396
|
let first;
|
|
20122
21397
|
try {
|
|
20123
|
-
first = (await
|
|
21398
|
+
first = (await fs29.readdir(dir)).filter((f) => f.toLowerCase().endsWith(".png")).sort((a, b) => a.localeCompare(b, void 0, { numeric: true }))[0];
|
|
20124
21399
|
} catch {
|
|
20125
21400
|
return null;
|
|
20126
21401
|
}
|
|
20127
21402
|
if (!first) return null;
|
|
20128
21403
|
let size = null;
|
|
20129
21404
|
try {
|
|
20130
|
-
const handle = await
|
|
21405
|
+
const handle = await fs29.open(path28.join(dir, first), "r");
|
|
20131
21406
|
try {
|
|
20132
21407
|
const head = Buffer.alloc(24);
|
|
20133
21408
|
const { bytesRead } = await handle.read(head, 0, 24, 0);
|
|
@@ -20144,7 +21419,7 @@ async function screenshotSizeWarning(runDirPath, viewport) {
|
|
|
20144
21419
|
async function readWalkerSummary(runDirPath) {
|
|
20145
21420
|
try {
|
|
20146
21421
|
const data = JSON.parse(
|
|
20147
|
-
await
|
|
21422
|
+
await fs29.readFile(path28.join(runDirPath, "summary.json"), "utf8")
|
|
20148
21423
|
);
|
|
20149
21424
|
const strings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.trim() !== "") : void 0;
|
|
20150
21425
|
const model = typeof data.model === "string" && data.model.trim() !== "" ? data.model.trim() : void 0;
|
|
@@ -20253,7 +21528,7 @@ function validateCanonicalWalkthrough(canonical) {
|
|
|
20253
21528
|
async function applyRecordedVerdicts(canonical, runDirPath) {
|
|
20254
21529
|
try {
|
|
20255
21530
|
const recorded = JSON.parse(
|
|
20256
|
-
await
|
|
21531
|
+
await fs29.readFile(path28.join(runDirPath, "verdicts.json"), "utf8")
|
|
20257
21532
|
);
|
|
20258
21533
|
if (Array.isArray(recorded.verdicts)) {
|
|
20259
21534
|
return applyVerdicts(canonical, recorded);
|
|
@@ -20263,7 +21538,7 @@ async function applyRecordedVerdicts(canonical, runDirPath) {
|
|
|
20263
21538
|
return canonical;
|
|
20264
21539
|
}
|
|
20265
21540
|
async function persistCanonical(runDirPath, canonical) {
|
|
20266
|
-
const canonicalPath =
|
|
21541
|
+
const canonicalPath = path28.join(runDirPath, "canonical.json");
|
|
20267
21542
|
await atomicWriteFile(
|
|
20268
21543
|
canonicalPath,
|
|
20269
21544
|
`${JSON.stringify(canonical, null, 2)}
|
|
@@ -20272,7 +21547,7 @@ async function persistCanonical(runDirPath, canonical) {
|
|
|
20272
21547
|
return canonicalPath;
|
|
20273
21548
|
}
|
|
20274
21549
|
async function persistReport(runDirPath, canonical) {
|
|
20275
|
-
const reportPath =
|
|
21550
|
+
const reportPath = path28.join(runDirPath, "report.md");
|
|
20276
21551
|
await atomicWriteFile(reportPath, renderCanonicalMarkdown(canonical));
|
|
20277
21552
|
return reportPath;
|
|
20278
21553
|
}
|
|
@@ -20372,17 +21647,17 @@ function placeLabel(p) {
|
|
|
20372
21647
|
function printRevisit(r) {
|
|
20373
21648
|
const missed = r.places.filter((p) => !p.reached);
|
|
20374
21649
|
console.log();
|
|
20375
|
-
const head = ` ${
|
|
21650
|
+
const head = ` ${chalk31.bold("Re-check places:")} reached ${r.reached} of ${r.requested}`;
|
|
20376
21651
|
if (missed.length === 0) {
|
|
20377
|
-
console.log(`${head} ${
|
|
21652
|
+
console.log(`${head} ${chalk31.green("\u2713")}`);
|
|
20378
21653
|
return;
|
|
20379
21654
|
}
|
|
20380
21655
|
console.log(`${head} \u2014 not reached:`);
|
|
20381
21656
|
for (const p of missed) {
|
|
20382
|
-
console.log(` ${
|
|
21657
|
+
console.log(` ${chalk31.yellow("\u25CB")} ${placeLabel(p)}`);
|
|
20383
21658
|
}
|
|
20384
21659
|
console.log(
|
|
20385
|
-
|
|
21660
|
+
chalk31.dim(
|
|
20386
21661
|
" Fixes there stay awaiting a re-run until a walk gets there."
|
|
20387
21662
|
)
|
|
20388
21663
|
);
|
|
@@ -20390,7 +21665,7 @@ function printRevisit(r) {
|
|
|
20390
21665
|
function printVerification(verification) {
|
|
20391
21666
|
console.log();
|
|
20392
21667
|
console.log(
|
|
20393
|
-
|
|
21668
|
+
chalk31.bold(" Since this scenario's last run") + chalk31.dim(` (${verification.baseline_run_id.slice(0, 30)}\u2026)`) + chalk31.bold(":")
|
|
20394
21669
|
);
|
|
20395
21670
|
if (verification.resolved.length > 0) {
|
|
20396
21671
|
console.log(
|
|
@@ -20399,7 +21674,7 @@ function printVerification(verification) {
|
|
|
20399
21674
|
}
|
|
20400
21675
|
if (verification.not_observed.length > 0) {
|
|
20401
21676
|
console.log(
|
|
20402
|
-
` ${symbols.pending} ${verification.not_observed.length} not seen this run ${
|
|
21677
|
+
` ${symbols.pending} ${verification.not_observed.length} not seen this run ${chalk31.dim("(not evidence of a fix)")}`
|
|
20403
21678
|
);
|
|
20404
21679
|
}
|
|
20405
21680
|
if (verification.persists.length > 0) {
|
|
@@ -20440,14 +21715,14 @@ __export(match_exports, {
|
|
|
20440
21715
|
match: () => match,
|
|
20441
21716
|
matchRecord: () => matchRecord
|
|
20442
21717
|
});
|
|
20443
|
-
import { promises as
|
|
20444
|
-
import
|
|
20445
|
-
import
|
|
21718
|
+
import { promises as fs30 } from "fs";
|
|
21719
|
+
import path29 from "path";
|
|
21720
|
+
import chalk32 from "chalk";
|
|
20446
21721
|
async function completedManifest2(runDirPath, runId, opts) {
|
|
20447
21722
|
let manifest;
|
|
20448
21723
|
try {
|
|
20449
21724
|
manifest = JSON.parse(
|
|
20450
|
-
await
|
|
21725
|
+
await fs30.readFile(path29.join(runDirPath, "walkthrough.json"), "utf8")
|
|
20451
21726
|
);
|
|
20452
21727
|
} catch {
|
|
20453
21728
|
commandError(`Run not found: ${runId}`, opts);
|
|
@@ -20476,12 +21751,12 @@ async function match(runId, opts) {
|
|
|
20476
21751
|
);
|
|
20477
21752
|
const needed = input.current.length > 0 && (input.prior.length > 0 || (input.guidance ?? []).length > 0);
|
|
20478
21753
|
const prompt = needed ? composeMatchPrompt(input) : null;
|
|
20479
|
-
const promptFilePath = opts?.promptFile ??
|
|
21754
|
+
const promptFilePath = opts?.promptFile ?? path29.join(runDirPath, "match_prompt.md");
|
|
20480
21755
|
if (prompt) {
|
|
20481
|
-
await
|
|
21756
|
+
await fs30.writeFile(promptFilePath, prompt, "utf8");
|
|
20482
21757
|
} else {
|
|
20483
21758
|
try {
|
|
20484
|
-
await
|
|
21759
|
+
await fs30.unlink(promptFilePath);
|
|
20485
21760
|
} catch {
|
|
20486
21761
|
}
|
|
20487
21762
|
}
|
|
@@ -20491,8 +21766,8 @@ async function match(runId, opts) {
|
|
|
20491
21766
|
{
|
|
20492
21767
|
...input,
|
|
20493
21768
|
needed,
|
|
20494
|
-
prompt_file: prompt ?
|
|
20495
|
-
prompt_file_abs: prompt ?
|
|
21769
|
+
prompt_file: prompt ? path29.relative(ctx.projectRoot, promptFilePath) : null,
|
|
21770
|
+
prompt_file_abs: prompt ? path29.resolve(promptFilePath) : null,
|
|
20496
21771
|
match_prompt: prompt
|
|
20497
21772
|
},
|
|
20498
21773
|
null,
|
|
@@ -20503,7 +21778,7 @@ async function match(runId, opts) {
|
|
|
20503
21778
|
}
|
|
20504
21779
|
if (!prompt) {
|
|
20505
21780
|
console.log(
|
|
20506
|
-
|
|
21781
|
+
chalk32.dim(
|
|
20507
21782
|
" No earlier findings or tester guidance for this scenario to match against \u2014 nothing to do."
|
|
20508
21783
|
)
|
|
20509
21784
|
);
|
|
@@ -20519,7 +21794,7 @@ async function matchRecord(runId, opts) {
|
|
|
20519
21794
|
if (!manifest) return;
|
|
20520
21795
|
let answer;
|
|
20521
21796
|
try {
|
|
20522
|
-
answer = JSON.parse(await
|
|
21797
|
+
answer = JSON.parse(await fs30.readFile(opts.file, "utf8"));
|
|
20523
21798
|
} catch {
|
|
20524
21799
|
commandError(`Cannot read matches file: ${opts.file}`, opts);
|
|
20525
21800
|
return;
|
|
@@ -20560,17 +21835,17 @@ async function matchRecord(runId, opts) {
|
|
|
20560
21835
|
}
|
|
20561
21836
|
console.log();
|
|
20562
21837
|
console.log(
|
|
20563
|
-
` ${
|
|
21838
|
+
` ${chalk32.green("\u2713")} Matched ${result.linked} finding${result.linked === 1 ? "" : "s"} to earlier ones, ${result.new} first seen, ${result.rechecked_fixed} earlier re-checked fixed`
|
|
20564
21839
|
);
|
|
20565
21840
|
if (result.suppressed > 0) {
|
|
20566
21841
|
console.log(
|
|
20567
|
-
` ${
|
|
21842
|
+
` ${chalk32.yellow("!")} ${result.suppressed} finding${result.suppressed === 1 ? "" : "s"} match tester guidance (flagged with suppressed_by_guidance \u2014 dismiss them in triage if the rule holds)`
|
|
20568
21843
|
);
|
|
20569
21844
|
}
|
|
20570
21845
|
if (verification) printVerification(verification);
|
|
20571
21846
|
console.log();
|
|
20572
21847
|
console.log(
|
|
20573
|
-
|
|
21848
|
+
chalk32.dim(" Run `iamu sync` to push the linked findings to the cloud.")
|
|
20574
21849
|
);
|
|
20575
21850
|
console.log();
|
|
20576
21851
|
}
|
|
@@ -20591,7 +21866,7 @@ __export(prune_exports, {
|
|
|
20591
21866
|
prune: () => prune
|
|
20592
21867
|
});
|
|
20593
21868
|
import { confirm, isCancel as isCancel5 } from "@clack/prompts";
|
|
20594
|
-
import
|
|
21869
|
+
import chalk33 from "chalk";
|
|
20595
21870
|
function formatBytes(bytes) {
|
|
20596
21871
|
if (bytes <= 0) return "0 B";
|
|
20597
21872
|
if (bytes < 1024) return `${bytes} B`;
|
|
@@ -20637,7 +21912,7 @@ async function prune(opts = {}) {
|
|
|
20637
21912
|
});
|
|
20638
21913
|
if (preview.prunedRuns.length === 0) {
|
|
20639
21914
|
console.log(
|
|
20640
|
-
|
|
21915
|
+
chalk33.dim(" No runs match the pruning criteria. Nothing to prune.")
|
|
20641
21916
|
);
|
|
20642
21917
|
return preview;
|
|
20643
21918
|
}
|
|
@@ -20646,7 +21921,7 @@ async function prune(opts = {}) {
|
|
|
20646
21921
|
message: `Prune ${mode} ${preview.prunedRuns.length} run(s) and reclaim ${formatBytes(preview.totalBytesReclaimed)}?`
|
|
20647
21922
|
});
|
|
20648
21923
|
if (isCancel5(confirmed) || !confirmed) {
|
|
20649
|
-
console.log(
|
|
21924
|
+
console.log(chalk33.yellow(" Pruning cancelled."));
|
|
20650
21925
|
return;
|
|
20651
21926
|
}
|
|
20652
21927
|
}
|
|
@@ -20657,14 +21932,14 @@ async function prune(opts = {}) {
|
|
|
20657
21932
|
}
|
|
20658
21933
|
if (result.dryRun) {
|
|
20659
21934
|
console.log(
|
|
20660
|
-
|
|
21935
|
+
chalk33.cyan(
|
|
20661
21936
|
`
|
|
20662
21937
|
[DRY RUN] Would prune ${result.prunedRuns.length} of ${result.totalRunsScanned} run(s):`
|
|
20663
21938
|
)
|
|
20664
21939
|
);
|
|
20665
21940
|
} else {
|
|
20666
21941
|
console.log(
|
|
20667
|
-
|
|
21942
|
+
chalk33.bold(
|
|
20668
21943
|
`
|
|
20669
21944
|
${symbols.success} Pruned ${result.prunedRuns.length} of ${result.totalRunsScanned} run(s):`
|
|
20670
21945
|
)
|
|
@@ -20672,18 +21947,18 @@ async function prune(opts = {}) {
|
|
|
20672
21947
|
}
|
|
20673
21948
|
if (result.prunedRuns.length === 0) {
|
|
20674
21949
|
console.log(
|
|
20675
|
-
|
|
21950
|
+
chalk33.dim(" No runs matched the pruning criteria. Nothing pruned.")
|
|
20676
21951
|
);
|
|
20677
21952
|
} else {
|
|
20678
21953
|
for (const run of result.prunedRuns) {
|
|
20679
21954
|
const mode = run.fullyDeleted ? "removed" : "screenshots purged";
|
|
20680
21955
|
const count = run.screenshotsDeleted > 0 ? `${run.screenshotsDeleted} screenshots, ` : "";
|
|
20681
21956
|
console.log(
|
|
20682
|
-
` ${
|
|
21957
|
+
` ${chalk33.dim("\u2022")} ${chalk33.bold(run.runId)} ${chalk33.dim(`(${run.scenarioSlug ?? "unknown scenario"})`)}: ${chalk33.yellow(formatBytes(run.bytesReclaimed))} (${count}${mode})`
|
|
20683
21958
|
);
|
|
20684
21959
|
}
|
|
20685
21960
|
console.log(
|
|
20686
|
-
|
|
21961
|
+
chalk33.dim(
|
|
20687
21962
|
`
|
|
20688
21963
|
Total reclaimed: ${formatBytes(result.totalBytesReclaimed)} (${result.totalScreenshotsDeleted} screenshots deleted)
|
|
20689
21964
|
`
|
|
@@ -20834,10 +22109,13 @@ function registerProjectCommands(program) {
|
|
|
20834
22109
|
await status2(opts);
|
|
20835
22110
|
});
|
|
20836
22111
|
program.command("doctor").helpGroup("Project:").description(
|
|
20837
|
-
"Diagnose environment: runtime, browser automation, skills, cloud"
|
|
22112
|
+
"Diagnose environment: runtime, browser automation, skills, cloud, and target testability"
|
|
22113
|
+
).option(
|
|
22114
|
+
"-t, --target <url>",
|
|
22115
|
+
"Probe target URL health, reachability, and auth redirects"
|
|
20838
22116
|
).option("--json", "Output diagnostics as JSON").addHelpText(
|
|
20839
22117
|
"after",
|
|
20840
|
-
"\nExamples:\n $ iamu doctor\n $ iamu doctor --json"
|
|
22118
|
+
"\nExamples:\n $ iamu doctor\n $ iamu doctor --target http://localhost:3000\n $ iamu doctor --json"
|
|
20841
22119
|
).action(async (opts) => {
|
|
20842
22120
|
const { doctor: doctor2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
|
|
20843
22121
|
await doctor2(opts);
|
|
@@ -20869,9 +22147,9 @@ function registerProjectCommands(program) {
|
|
|
20869
22147
|
"after",
|
|
20870
22148
|
"\nExamples:\n $ iamu validate --all\n $ iamu validate --all --fix --dry-run\n $ iamu validate iamu/personas/maria.md"
|
|
20871
22149
|
).action(
|
|
20872
|
-
async (
|
|
22150
|
+
async (path30, opts) => {
|
|
20873
22151
|
const { validate: validate2 } = await Promise.resolve().then(() => (init_validate(), validate_exports));
|
|
20874
|
-
await validate2(
|
|
22152
|
+
await validate2(path30, opts);
|
|
20875
22153
|
}
|
|
20876
22154
|
);
|
|
20877
22155
|
program.command("update").helpGroup("Project:").description("Update and repair agent skills and prompts to latest version").option(
|
|
@@ -20910,6 +22188,54 @@ function registerProjectCommands(program) {
|
|
|
20910
22188
|
await newEntity2(type2, opts);
|
|
20911
22189
|
}
|
|
20912
22190
|
);
|
|
22191
|
+
const explore = program.command("explore").helpGroup("Project:").description(
|
|
22192
|
+
"Map the running product with a read-only crawl; compare the map with runs (coverage) or with another map (diff)"
|
|
22193
|
+
);
|
|
22194
|
+
explore.command("run [url]", { isDefault: true }).description(
|
|
22195
|
+
"Crawl from <url> (default: target_url in iamu/testability.md) and write iamu/.cache/explore/<id>/map.json"
|
|
22196
|
+
).option("--max-surfaces <n>", "Stop after this many surfaces (default 40)").option("--max-actions <n>", "Stop after this many clicks (default 300)").option(
|
|
22197
|
+
"--max-depth <n>",
|
|
22198
|
+
"Don't go more than this many clicks from the start (default 6)"
|
|
22199
|
+
).option(
|
|
22200
|
+
"--deny <pattern...>",
|
|
22201
|
+
"Never click a control whose name or link matches (regex, case-insensitive)"
|
|
22202
|
+
).option("--viewport <WxH>", "Page size, e.g. 390x844 (default 1280x800)").option(
|
|
22203
|
+
"--attach",
|
|
22204
|
+
"Crawl in a new tab of the `iamu browse` browser, keeping its sign-ins"
|
|
22205
|
+
).option("--json", "Output as JSON").addHelpText(
|
|
22206
|
+
"after",
|
|
22207
|
+
`
|
|
22208
|
+
Read-only: controls that read like writes (delete, pay, send, save, log out \u2026),
|
|
22209
|
+
form submits, downloads and other origins are never clicked, nothing is typed,
|
|
22210
|
+
and every request but GET/HEAD/OPTIONS is blocked before it leaves the browser.
|
|
22211
|
+
|
|
22212
|
+
Examples:
|
|
22213
|
+
$ iamu explore http://localhost:3000
|
|
22214
|
+
$ iamu explore --attach --deny "^Admin"
|
|
22215
|
+
$ iamu explore coverage
|
|
22216
|
+
$ iamu explore diff`
|
|
22217
|
+
).action(
|
|
22218
|
+
async (url, opts) => {
|
|
22219
|
+
const { exploreRun: exploreRun2 } = await Promise.resolve().then(() => (init_explore(), explore_exports));
|
|
22220
|
+
await exploreRun2(url, opts);
|
|
22221
|
+
}
|
|
22222
|
+
);
|
|
22223
|
+
explore.command("coverage [map]").description(
|
|
22224
|
+
"Surfaces of a map (default: latest) that no run reached, and places runs reached that the map lacks"
|
|
22225
|
+
).option("--json", "Output as JSON").action(async (map2, opts) => {
|
|
22226
|
+
const { exploreCoverageCommand: exploreCoverageCommand2 } = await Promise.resolve().then(() => (init_explore(), explore_exports));
|
|
22227
|
+
await exploreCoverageCommand2(map2, opts);
|
|
22228
|
+
});
|
|
22229
|
+
explore.command("diff [a] [b]").description("Compare two maps (default: the two latest)").option("--json", "Output as JSON").action(
|
|
22230
|
+
async (a, b, opts) => {
|
|
22231
|
+
const { exploreDiffCommand: exploreDiffCommand2 } = await Promise.resolve().then(() => (init_explore(), explore_exports));
|
|
22232
|
+
await exploreDiffCommand2(a, b, opts);
|
|
22233
|
+
}
|
|
22234
|
+
);
|
|
22235
|
+
explore.command("list").description("List maps").option("--json", "Output as JSON").action(async (opts) => {
|
|
22236
|
+
const { exploreList: exploreList2 } = await Promise.resolve().then(() => (init_explore(), explore_exports));
|
|
22237
|
+
await exploreList2(opts);
|
|
22238
|
+
});
|
|
20913
22239
|
program.command("completion [shell]").helpGroup("Project:").description("Output shell completion script (bash, zsh, fish)").action(async (shell) => {
|
|
20914
22240
|
const { completion: completion2 } = await Promise.resolve().then(() => (init_completion(), completion_exports));
|
|
20915
22241
|
completion2(program, shell);
|
|
@@ -21117,7 +22443,7 @@ function registerAllCommands(program) {
|
|
|
21117
22443
|
|
|
21118
22444
|
// src/ui/panic.ts
|
|
21119
22445
|
init_logger();
|
|
21120
|
-
import
|
|
22446
|
+
import chalk34 from "chalk";
|
|
21121
22447
|
var handlersRegistered = false;
|
|
21122
22448
|
var customSigintHandler = null;
|
|
21123
22449
|
function registerSigintCleanup(handler) {
|
|
@@ -21129,17 +22455,17 @@ function registerSigintCleanup(handler) {
|
|
|
21129
22455
|
function logFatalAndExit(title, errOrReason) {
|
|
21130
22456
|
process.stderr.write("\x1B[?25h");
|
|
21131
22457
|
if (isVerbose()) {
|
|
21132
|
-
console.error(
|
|
22458
|
+
console.error(chalk34.red(`
|
|
21133
22459
|
${title}:`));
|
|
21134
22460
|
console.error(
|
|
21135
22461
|
errOrReason instanceof Error ? errOrReason.stack || errOrReason : errOrReason
|
|
21136
22462
|
);
|
|
21137
22463
|
} else {
|
|
21138
22464
|
const msg = errOrReason instanceof Error ? errOrReason.message : String(errOrReason);
|
|
21139
|
-
console.error(
|
|
22465
|
+
console.error(chalk34.red(`
|
|
21140
22466
|
Fatal Error: ${msg}`));
|
|
21141
22467
|
console.error(
|
|
21142
|
-
|
|
22468
|
+
chalk34.dim(
|
|
21143
22469
|
" Run with --verbose or DEBUG=1 to view the full stack trace.\n"
|
|
21144
22470
|
)
|
|
21145
22471
|
);
|
|
@@ -21158,7 +22484,7 @@ function setupProcessHandlers() {
|
|
|
21158
22484
|
}
|
|
21159
22485
|
process.exit(130);
|
|
21160
22486
|
} else {
|
|
21161
|
-
console.error(
|
|
22487
|
+
console.error(chalk34.dim("\n Operation cancelled."));
|
|
21162
22488
|
process.exit(130);
|
|
21163
22489
|
}
|
|
21164
22490
|
});
|