@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/bin.js
CHANGED
|
@@ -10488,9 +10488,275 @@ var init_prune = __esm({
|
|
|
10488
10488
|
}
|
|
10489
10489
|
});
|
|
10490
10490
|
|
|
10491
|
-
// ../../packages/local-store/dist/
|
|
10491
|
+
// ../../packages/local-store/dist/explore-map.js
|
|
10492
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
10492
10493
|
import { promises as fs19 } from "fs";
|
|
10493
10494
|
import path15 from "path";
|
|
10495
|
+
function templateToken(token) {
|
|
10496
|
+
if (token === "")
|
|
10497
|
+
return token;
|
|
10498
|
+
if (/^\d+$/.test(token))
|
|
10499
|
+
return ":id";
|
|
10500
|
+
if (UUID.test(token))
|
|
10501
|
+
return ":id";
|
|
10502
|
+
if (DATE.test(token))
|
|
10503
|
+
return ":date";
|
|
10504
|
+
if (/^[0-9a-f]{8,}$/i.test(token) && /\d/.test(token))
|
|
10505
|
+
return ":id";
|
|
10506
|
+
if (token.length >= 16 && /\d/.test(token) && /[a-z]/i.test(token)) {
|
|
10507
|
+
return ":id";
|
|
10508
|
+
}
|
|
10509
|
+
return token;
|
|
10510
|
+
}
|
|
10511
|
+
function templatePath(pathname) {
|
|
10512
|
+
const trimmed = pathname.length > 1 ? pathname.replace(/\/+$/, "") : pathname || "/";
|
|
10513
|
+
return trimmed.split("/").map((seg) => templateToken(decodeSegment(seg))).join("/");
|
|
10514
|
+
}
|
|
10515
|
+
function encodeQueryValue(v) {
|
|
10516
|
+
return v.startsWith(":") ? v : encodeURIComponent(v);
|
|
10517
|
+
}
|
|
10518
|
+
function decodeSegment(seg) {
|
|
10519
|
+
try {
|
|
10520
|
+
return decodeURIComponent(seg);
|
|
10521
|
+
} catch {
|
|
10522
|
+
return seg;
|
|
10523
|
+
}
|
|
10524
|
+
}
|
|
10525
|
+
function explorePlaceKey(raw) {
|
|
10526
|
+
if (!raw)
|
|
10527
|
+
return null;
|
|
10528
|
+
let u;
|
|
10529
|
+
try {
|
|
10530
|
+
u = new URL(raw);
|
|
10531
|
+
} catch {
|
|
10532
|
+
return null;
|
|
10533
|
+
}
|
|
10534
|
+
if (u.protocol !== "http:" && u.protocol !== "https:")
|
|
10535
|
+
return null;
|
|
10536
|
+
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));
|
|
10537
|
+
const query = params.map(([k, v]) => `${encodeURIComponent(k)}=${encodeQueryValue(v)}`).join("&");
|
|
10538
|
+
let key = templatePath(u.pathname);
|
|
10539
|
+
if (query)
|
|
10540
|
+
key += `?${query}`;
|
|
10541
|
+
const route = /^#!?(\/.*)$/.exec(u.hash);
|
|
10542
|
+
if (route) {
|
|
10543
|
+
const [hashPath] = route[1].split("?");
|
|
10544
|
+
key += `#${templatePath(hashPath)}`;
|
|
10545
|
+
}
|
|
10546
|
+
return key;
|
|
10547
|
+
}
|
|
10548
|
+
function exploreViewSignature(headings) {
|
|
10549
|
+
return headings.map((h) => h.trim().replace(/\s+/g, " ").replace(/\d+/g, "#").toLowerCase()).filter(Boolean).join(" | ");
|
|
10550
|
+
}
|
|
10551
|
+
function exploreSignature(headings, title) {
|
|
10552
|
+
return exploreViewSignature(headings) || title.trim().toLowerCase();
|
|
10553
|
+
}
|
|
10554
|
+
function matchExploreSurface(map2, url, signature) {
|
|
10555
|
+
const place = explorePlaceKey(url);
|
|
10556
|
+
if (!place)
|
|
10557
|
+
return null;
|
|
10558
|
+
const atPlace = map2.surfaces.filter((s) => s.place === place);
|
|
10559
|
+
return atPlace.find((s) => s.signature === signature) ?? (atPlace.length === 1 ? atPlace[0] : null);
|
|
10560
|
+
}
|
|
10561
|
+
function exploreSurfaceKey(place, view) {
|
|
10562
|
+
return view === null ? place : `${place}~${fnv1a(view)}`;
|
|
10563
|
+
}
|
|
10564
|
+
function fnv1a(s) {
|
|
10565
|
+
let h = 2166136261;
|
|
10566
|
+
for (let i = 0; i < s.length; i++) {
|
|
10567
|
+
h ^= s.charCodeAt(i);
|
|
10568
|
+
h = Math.imul(h, 16777619) >>> 0;
|
|
10569
|
+
}
|
|
10570
|
+
return h.toString(16).padStart(8, "0");
|
|
10571
|
+
}
|
|
10572
|
+
function exploreDir(projectRoot) {
|
|
10573
|
+
return path15.join(cacheDir(projectRoot), "explore");
|
|
10574
|
+
}
|
|
10575
|
+
function composeExploreId(now, randomHex) {
|
|
10576
|
+
const ts = now.toISOString().replace(/[:.]/g, "-").replace(/Z$/, "");
|
|
10577
|
+
return `${ts}-${randomHex ?? randomBytes2(3).toString("hex")}`;
|
|
10578
|
+
}
|
|
10579
|
+
function isValidExploreId(id) {
|
|
10580
|
+
return MAP_ID.test(id);
|
|
10581
|
+
}
|
|
10582
|
+
function mapPath(projectRoot, id) {
|
|
10583
|
+
if (!isValidExploreId(id))
|
|
10584
|
+
throw new Error(`Not an explore map id: ${id}`);
|
|
10585
|
+
return path15.join(exploreDir(projectRoot), id, "map.json");
|
|
10586
|
+
}
|
|
10587
|
+
async function writeExploreMap(projectRoot, map2) {
|
|
10588
|
+
const file = mapPath(projectRoot, map2.id);
|
|
10589
|
+
await fs19.mkdir(path15.dirname(file), { recursive: true });
|
|
10590
|
+
await atomicWriteFile(file, `${JSON.stringify(map2, null, 2)}
|
|
10591
|
+
`);
|
|
10592
|
+
return file;
|
|
10593
|
+
}
|
|
10594
|
+
async function readExploreMap(projectRoot, id) {
|
|
10595
|
+
const raw = await fs19.readFile(mapPath(projectRoot, id), "utf8");
|
|
10596
|
+
const map2 = JSON.parse(raw);
|
|
10597
|
+
if (map2?.schema !== EXPLORE_MAP_SCHEMA || !Array.isArray(map2.surfaces)) {
|
|
10598
|
+
throw new Error(`Unreadable explore map: ${id}`);
|
|
10599
|
+
}
|
|
10600
|
+
return map2;
|
|
10601
|
+
}
|
|
10602
|
+
async function listExploreMaps(projectRoot) {
|
|
10603
|
+
try {
|
|
10604
|
+
const entries = await fs19.readdir(exploreDir(projectRoot), {
|
|
10605
|
+
withFileTypes: true
|
|
10606
|
+
});
|
|
10607
|
+
return entries.filter((e) => e.isDirectory() && isValidExploreId(e.name)).map((e) => e.name).sort();
|
|
10608
|
+
} catch (err) {
|
|
10609
|
+
if (err.code === "ENOENT")
|
|
10610
|
+
return [];
|
|
10611
|
+
throw err;
|
|
10612
|
+
}
|
|
10613
|
+
}
|
|
10614
|
+
function exploreCoverage(map2, steps) {
|
|
10615
|
+
const byPlace = /* @__PURE__ */ new Map();
|
|
10616
|
+
for (const s of map2.surfaces) {
|
|
10617
|
+
byPlace.set(s.place, [...byPlace.get(s.place) ?? [], s]);
|
|
10618
|
+
}
|
|
10619
|
+
const reachedBy = /* @__PURE__ */ new Map();
|
|
10620
|
+
const unresolvedBy = /* @__PURE__ */ new Map();
|
|
10621
|
+
const runOnlyBy = /* @__PURE__ */ new Map();
|
|
10622
|
+
const add = (m, k, s) => m.set(k, [...m.get(k) ?? [], s]);
|
|
10623
|
+
for (const step of steps) {
|
|
10624
|
+
const place = explorePlaceKey(step.url);
|
|
10625
|
+
if (!place)
|
|
10626
|
+
continue;
|
|
10627
|
+
const atPlace = byPlace.get(place);
|
|
10628
|
+
if (!atPlace) {
|
|
10629
|
+
add(runOnlyBy, place, step);
|
|
10630
|
+
continue;
|
|
10631
|
+
}
|
|
10632
|
+
const hit = tieStep(map2, atPlace, step);
|
|
10633
|
+
if (hit.length === 0)
|
|
10634
|
+
add(unresolvedBy, place, step);
|
|
10635
|
+
for (const s of hit)
|
|
10636
|
+
add(reachedBy, s.key, step);
|
|
10637
|
+
}
|
|
10638
|
+
const surfaces = map2.surfaces.map((s) => {
|
|
10639
|
+
const base = {
|
|
10640
|
+
key: s.key,
|
|
10641
|
+
label: s.label,
|
|
10642
|
+
place: s.place,
|
|
10643
|
+
url: s.url,
|
|
10644
|
+
depth: s.depth
|
|
10645
|
+
};
|
|
10646
|
+
const reached = reachedBy.get(s.key);
|
|
10647
|
+
if (reached)
|
|
10648
|
+
return { ...base, status: "reached", runs: distinctRuns(reached) };
|
|
10649
|
+
const unresolved = unresolvedBy.get(s.place);
|
|
10650
|
+
if (unresolved) {
|
|
10651
|
+
return { ...base, status: "place_only", runs: distinctRuns(unresolved) };
|
|
10652
|
+
}
|
|
10653
|
+
return { ...base, status: "unreached", runs: [] };
|
|
10654
|
+
});
|
|
10655
|
+
const runOnly = [...runOnlyBy].map(([place, list2]) => ({
|
|
10656
|
+
place,
|
|
10657
|
+
steps: list2.length,
|
|
10658
|
+
runs: distinctRuns(list2)
|
|
10659
|
+
}));
|
|
10660
|
+
runOnly.sort((a, b) => b.steps - a.steps || a.place.localeCompare(b.place));
|
|
10661
|
+
return {
|
|
10662
|
+
map_id: map2.id,
|
|
10663
|
+
runs: new Set(steps.map((s) => s.run_id)).size,
|
|
10664
|
+
steps: steps.length,
|
|
10665
|
+
reached: surfaces.filter((s) => s.status === "reached").length,
|
|
10666
|
+
place_only: surfaces.filter((s) => s.status === "place_only").length,
|
|
10667
|
+
unreached: surfaces.filter((s) => s.status === "unreached").length,
|
|
10668
|
+
surfaces,
|
|
10669
|
+
run_only_places: runOnly
|
|
10670
|
+
};
|
|
10671
|
+
}
|
|
10672
|
+
function tieStep(map2, atPlace, step) {
|
|
10673
|
+
if (typeof step.signature === "string") {
|
|
10674
|
+
const m = matchExploreSurface(map2, step.url, step.signature);
|
|
10675
|
+
return m ? [m] : [];
|
|
10676
|
+
}
|
|
10677
|
+
if (atPlace.length === 1)
|
|
10678
|
+
return [atPlace[0]];
|
|
10679
|
+
const name = step.surface?.trim();
|
|
10680
|
+
if (!name)
|
|
10681
|
+
return [];
|
|
10682
|
+
return atPlace.filter((s) => [s.label, ...s.headings].filter(Boolean).some((n) => surfaceMatches(n, name) || surfaceMatches(name, n)));
|
|
10683
|
+
}
|
|
10684
|
+
function distinctRuns(steps) {
|
|
10685
|
+
return [...new Set(steps.map((s) => s.run_id))].sort();
|
|
10686
|
+
}
|
|
10687
|
+
async function readAllRunSteps(projectRoot) {
|
|
10688
|
+
const out = [];
|
|
10689
|
+
for (const runId of await listRuns(projectRoot)) {
|
|
10690
|
+
let steps;
|
|
10691
|
+
try {
|
|
10692
|
+
steps = await readStepsForRun(projectRoot, runId);
|
|
10693
|
+
} catch {
|
|
10694
|
+
continue;
|
|
10695
|
+
}
|
|
10696
|
+
for (const s of steps) {
|
|
10697
|
+
out.push({ run_id: runId, url: s.url, surface: s.surface });
|
|
10698
|
+
}
|
|
10699
|
+
}
|
|
10700
|
+
return out;
|
|
10701
|
+
}
|
|
10702
|
+
function actionSignature(a) {
|
|
10703
|
+
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;
|
|
10704
|
+
return `${a.role} "${a.name}" \u2192 ${where}`;
|
|
10705
|
+
}
|
|
10706
|
+
function diffExploreMaps(a, b) {
|
|
10707
|
+
const aByKey = new Map(a.surfaces.map((s) => [s.key, s]));
|
|
10708
|
+
const bByKey = new Map(b.surfaces.map((s) => [s.key, s]));
|
|
10709
|
+
const ref = (s) => ({
|
|
10710
|
+
key: s.key,
|
|
10711
|
+
label: s.label
|
|
10712
|
+
});
|
|
10713
|
+
const onlyA = a.surfaces.filter((s) => !bByKey.has(s.key)).map(ref);
|
|
10714
|
+
const onlyB = b.surfaces.filter((s) => !aByKey.has(s.key)).map(ref);
|
|
10715
|
+
const changed = [];
|
|
10716
|
+
let common2 = 0;
|
|
10717
|
+
for (const sa of a.surfaces) {
|
|
10718
|
+
const sb = bByKey.get(sa.key);
|
|
10719
|
+
if (!sb)
|
|
10720
|
+
continue;
|
|
10721
|
+
common2++;
|
|
10722
|
+
const setA = new Set(sa.actions.map(actionSignature));
|
|
10723
|
+
const setB = new Set(sb.actions.map(actionSignature));
|
|
10724
|
+
const inA = [...setA].filter((x) => !setB.has(x)).sort();
|
|
10725
|
+
const inB = [...setB].filter((x) => !setA.has(x)).sort();
|
|
10726
|
+
if (inA.length > 0 || inB.length > 0) {
|
|
10727
|
+
changed.push({ ...ref(sa), only_a: inA, only_b: inB });
|
|
10728
|
+
}
|
|
10729
|
+
}
|
|
10730
|
+
const union = common2 + onlyA.length + onlyB.length;
|
|
10731
|
+
return {
|
|
10732
|
+
a: a.id,
|
|
10733
|
+
b: b.id,
|
|
10734
|
+
common: common2,
|
|
10735
|
+
only_a: onlyA,
|
|
10736
|
+
only_b: onlyB,
|
|
10737
|
+
surface_overlap: union === 0 ? 1 : common2 / union,
|
|
10738
|
+
changed_actions: changed
|
|
10739
|
+
};
|
|
10740
|
+
}
|
|
10741
|
+
var EXPLORE_MAP_SCHEMA, UUID, DATE, MAP_ID;
|
|
10742
|
+
var init_explore_map = __esm({
|
|
10743
|
+
"../../packages/local-store/dist/explore-map.js"() {
|
|
10744
|
+
"use strict";
|
|
10745
|
+
init_atomic();
|
|
10746
|
+
init_paths();
|
|
10747
|
+
init_revisit();
|
|
10748
|
+
init_runs();
|
|
10749
|
+
init_walkthrough_canonical2();
|
|
10750
|
+
EXPLORE_MAP_SCHEMA = 1;
|
|
10751
|
+
UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
10752
|
+
DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
10753
|
+
MAP_ID = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}-[0-9a-f]{6}$/;
|
|
10754
|
+
}
|
|
10755
|
+
});
|
|
10756
|
+
|
|
10757
|
+
// ../../packages/local-store/dist/human-test-link.js
|
|
10758
|
+
import { promises as fs20 } from "fs";
|
|
10759
|
+
import path16 from "path";
|
|
10494
10760
|
function humanTestLinks(verdicts) {
|
|
10495
10761
|
const best = /* @__PURE__ */ new Map();
|
|
10496
10762
|
for (const v of verdicts) {
|
|
@@ -10513,7 +10779,7 @@ async function linkFindingsToHumanTests(projectRoot, runId, verdicts) {
|
|
|
10513
10779
|
const dir = entityDirPath(projectRoot, "finding");
|
|
10514
10780
|
let names;
|
|
10515
10781
|
try {
|
|
10516
|
-
names = await
|
|
10782
|
+
names = await fs20.readdir(dir);
|
|
10517
10783
|
} catch {
|
|
10518
10784
|
return [];
|
|
10519
10785
|
}
|
|
@@ -10521,11 +10787,11 @@ async function linkFindingsToHumanTests(projectRoot, runId, verdicts) {
|
|
|
10521
10787
|
for (const name of names) {
|
|
10522
10788
|
if (!name.endsWith(".md") || name.startsWith("."))
|
|
10523
10789
|
continue;
|
|
10524
|
-
const file =
|
|
10790
|
+
const file = path16.join(dir, name);
|
|
10525
10791
|
let raw;
|
|
10526
10792
|
let fm;
|
|
10527
10793
|
try {
|
|
10528
|
-
raw = await
|
|
10794
|
+
raw = await fs20.readFile(file, "utf8");
|
|
10529
10795
|
fm = parseFrontmatter(raw).frontmatter;
|
|
10530
10796
|
} catch {
|
|
10531
10797
|
continue;
|
|
@@ -10756,14 +11022,14 @@ var init_cloud_client = __esm({
|
|
|
10756
11022
|
});
|
|
10757
11023
|
|
|
10758
11024
|
// ../../packages/local-store/dist/last-pull.js
|
|
10759
|
-
import { promises as
|
|
10760
|
-
import
|
|
11025
|
+
import { promises as fs21 } from "fs";
|
|
11026
|
+
import path17 from "path";
|
|
10761
11027
|
function lastPullPath(projectRoot) {
|
|
10762
|
-
return
|
|
11028
|
+
return path17.join(cacheDir(projectRoot), "last-pull.json");
|
|
10763
11029
|
}
|
|
10764
11030
|
async function readLastPull(projectRoot) {
|
|
10765
11031
|
try {
|
|
10766
|
-
const raw = await
|
|
11032
|
+
const raw = await fs21.readFile(lastPullPath(projectRoot), "utf8");
|
|
10767
11033
|
const parsed = JSON.parse(raw);
|
|
10768
11034
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
10769
11035
|
return parsed;
|
|
@@ -11049,7 +11315,7 @@ var init_frontmatter2 = __esm({
|
|
|
11049
11315
|
});
|
|
11050
11316
|
|
|
11051
11317
|
// ../../packages/local-store/dist/pull/write.js
|
|
11052
|
-
import { promises as
|
|
11318
|
+
import { promises as fs22 } from "fs";
|
|
11053
11319
|
async function loadLocalIndex(projectRoot, type2) {
|
|
11054
11320
|
const byId = /* @__PURE__ */ new Map();
|
|
11055
11321
|
const bySlug = /* @__PURE__ */ new Map();
|
|
@@ -11078,7 +11344,7 @@ async function classifyExisting(projectRoot, type2, cloudId, localSlug, targetSl
|
|
|
11078
11344
|
if (localSlug !== targetSlug)
|
|
11079
11345
|
return "untracked";
|
|
11080
11346
|
const lastSynced = localIndex.lastPushById[cloudId];
|
|
11081
|
-
const bytes = await
|
|
11347
|
+
const bytes = await fs22.readFile(entityFilePath(projectRoot, type2, localSlug), "utf8");
|
|
11082
11348
|
return lastSynced && hashNormalizedContent(bytes) === lastSynced ? "fast_forward" : "diverged";
|
|
11083
11349
|
}
|
|
11084
11350
|
async function writeOrSkip(projectRoot, type2, cloudId, slug, frontmatter, body, localIndex, mode, cloudVersion) {
|
|
@@ -11139,13 +11405,13 @@ async function writeOrSkip(projectRoot, type2, cloudId, slug, frontmatter, body,
|
|
|
11139
11405
|
}
|
|
11140
11406
|
if (existingSlugForId && existingSlugForId !== slug) {
|
|
11141
11407
|
try {
|
|
11142
|
-
await
|
|
11408
|
+
await fs22.unlink(entityFilePath(projectRoot, type2, existingSlugForId));
|
|
11143
11409
|
localIndex.bySlug.delete(existingSlugForId);
|
|
11144
11410
|
} catch {
|
|
11145
11411
|
}
|
|
11146
11412
|
}
|
|
11147
11413
|
await writeEntity(projectRoot, type2, frontmatter, text);
|
|
11148
|
-
const bytes = await
|
|
11414
|
+
const bytes = await fs22.readFile(entityFilePath(projectRoot, type2, slug), "utf8");
|
|
11149
11415
|
const hash = hashNormalizedContent(bytes);
|
|
11150
11416
|
localIndex.byId.set(cloudId, slug);
|
|
11151
11417
|
localIndex.bySlug.set(slug, cloudId);
|
|
@@ -11199,7 +11465,7 @@ var init_write = __esm({
|
|
|
11199
11465
|
});
|
|
11200
11466
|
|
|
11201
11467
|
// ../../packages/local-store/dist/pull/entities.js
|
|
11202
|
-
import { promises as
|
|
11468
|
+
import { promises as fs23 } from "fs";
|
|
11203
11469
|
function buildContextFrontmatter(payload) {
|
|
11204
11470
|
const cloudCtx = payload.context ?? {};
|
|
11205
11471
|
const glossary = (payload.glossary ?? []).map((g) => ({
|
|
@@ -11254,7 +11520,7 @@ async function pullContext(projectRoot, cloud, projectId, mode, result) {
|
|
|
11254
11520
|
const frontmatter = buildContextFrontmatter(payload);
|
|
11255
11521
|
let hasLocal = false;
|
|
11256
11522
|
try {
|
|
11257
|
-
await
|
|
11523
|
+
await fs23.access(contextPath(projectRoot));
|
|
11258
11524
|
hasLocal = true;
|
|
11259
11525
|
} catch {
|
|
11260
11526
|
hasLocal = false;
|
|
@@ -11284,7 +11550,7 @@ async function pullContext(projectRoot, cloud, projectId, mode, result) {
|
|
|
11284
11550
|
frontmatter,
|
|
11285
11551
|
notes_body: existingNotes
|
|
11286
11552
|
});
|
|
11287
|
-
const bytes = await
|
|
11553
|
+
const bytes = await fs23.readFile(contextPath(projectRoot), "utf8");
|
|
11288
11554
|
const hash = hashNormalizedContent(bytes);
|
|
11289
11555
|
await recordLastPull(projectRoot, "context:root", hash);
|
|
11290
11556
|
await recordLastPush(projectRoot, "context:root", hash);
|
|
@@ -11814,6 +12080,7 @@ var init_dist2 = __esm({
|
|
|
11814
12080
|
init_prune();
|
|
11815
12081
|
init_fix_state();
|
|
11816
12082
|
init_revisit();
|
|
12083
|
+
init_explore_map();
|
|
11817
12084
|
init_walkthrough_canonical2();
|
|
11818
12085
|
init_journey_diff();
|
|
11819
12086
|
init_finding_match();
|
|
@@ -11845,15 +12112,15 @@ function resolveApiUrl(configured) {
|
|
|
11845
12112
|
assertSecureApiUrl(url, configured ? "API URL" : "IAMU_API_URL");
|
|
11846
12113
|
return url;
|
|
11847
12114
|
}
|
|
11848
|
-
function parseStore(raw,
|
|
12115
|
+
function parseStore(raw, path30) {
|
|
11849
12116
|
let parsed;
|
|
11850
12117
|
try {
|
|
11851
12118
|
parsed = JSON.parse(raw);
|
|
11852
12119
|
} catch (err) {
|
|
11853
|
-
throw new CredentialsFileError(
|
|
12120
|
+
throw new CredentialsFileError(path30, "not valid JSON", err);
|
|
11854
12121
|
}
|
|
11855
12122
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
11856
|
-
throw new CredentialsFileError(
|
|
12123
|
+
throw new CredentialsFileError(path30, "expected a JSON object");
|
|
11857
12124
|
}
|
|
11858
12125
|
return parsed;
|
|
11859
12126
|
}
|
|
@@ -11861,26 +12128,26 @@ function isNotFound(err) {
|
|
|
11861
12128
|
return typeof err === "object" && err !== null && err.code === "ENOENT";
|
|
11862
12129
|
}
|
|
11863
12130
|
async function readCredentialsStore() {
|
|
11864
|
-
const
|
|
12131
|
+
const path30 = getCredentialsPath();
|
|
11865
12132
|
let raw;
|
|
11866
12133
|
try {
|
|
11867
|
-
raw = await readFile(
|
|
12134
|
+
raw = await readFile(path30, "utf-8");
|
|
11868
12135
|
} catch (err) {
|
|
11869
12136
|
if (isNotFound(err)) return {};
|
|
11870
12137
|
warnUnreadableStoreOnce(err);
|
|
11871
12138
|
throw err;
|
|
11872
12139
|
}
|
|
11873
12140
|
try {
|
|
11874
|
-
return parseStore(raw,
|
|
12141
|
+
return parseStore(raw, path30);
|
|
11875
12142
|
} catch (err) {
|
|
11876
12143
|
warnUnreadableStoreOnce(err);
|
|
11877
12144
|
throw err;
|
|
11878
12145
|
}
|
|
11879
12146
|
}
|
|
11880
12147
|
async function writeCredentialsStore(store) {
|
|
11881
|
-
const
|
|
11882
|
-
await mkdir(dirname(
|
|
11883
|
-
await atomicWriteFile(
|
|
12148
|
+
const path30 = getCredentialsPath();
|
|
12149
|
+
await mkdir(dirname(path30), { recursive: true, mode: 448 });
|
|
12150
|
+
await atomicWriteFile(path30, `${JSON.stringify(store, null, 2)}
|
|
11884
12151
|
`, {
|
|
11885
12152
|
mode: 384
|
|
11886
12153
|
});
|
|
@@ -11900,16 +12167,16 @@ function resolveApiKey(apiUrl) {
|
|
|
11900
12167
|
} catch {
|
|
11901
12168
|
return null;
|
|
11902
12169
|
}
|
|
11903
|
-
const
|
|
12170
|
+
const path30 = getCredentialsPath();
|
|
11904
12171
|
let raw;
|
|
11905
12172
|
try {
|
|
11906
|
-
raw = readFileSync(
|
|
12173
|
+
raw = readFileSync(path30, "utf-8");
|
|
11907
12174
|
} catch (err) {
|
|
11908
12175
|
if (!isNotFound(err)) warnUnreadableStoreOnce(err);
|
|
11909
12176
|
return null;
|
|
11910
12177
|
}
|
|
11911
12178
|
try {
|
|
11912
|
-
const entry = parseStore(raw,
|
|
12179
|
+
const entry = parseStore(raw, path30)[url];
|
|
11913
12180
|
if (typeof entry !== "object" || entry === null) return null;
|
|
11914
12181
|
const token = entry.token;
|
|
11915
12182
|
return typeof token === "string" && token ? token : null;
|
|
@@ -11948,12 +12215,12 @@ var init_credentials = __esm({
|
|
|
11948
12215
|
* @param reason - What is wrong with it.
|
|
11949
12216
|
* @param cause - Underlying parse error, if any.
|
|
11950
12217
|
*/
|
|
11951
|
-
constructor(
|
|
12218
|
+
constructor(path30, reason, cause) {
|
|
11952
12219
|
super(
|
|
11953
|
-
`Credentials file ${
|
|
12220
|
+
`Credentials file ${path30} is corrupt (${reason}). Fix it by hand, or delete it and run \`iamu auth login\` again.`,
|
|
11954
12221
|
{
|
|
11955
12222
|
cause,
|
|
11956
|
-
suggestedAction: `Delete ${
|
|
12223
|
+
suggestedAction: `Delete ${path30} and run \`iamu auth login\`, or set IAMU_TOKEN.`
|
|
11957
12224
|
}
|
|
11958
12225
|
);
|
|
11959
12226
|
}
|
|
@@ -12565,16 +12832,16 @@ async function needsWrite(filePath, content) {
|
|
|
12565
12832
|
return true;
|
|
12566
12833
|
}
|
|
12567
12834
|
}
|
|
12568
|
-
function isCliWrittenSkill(
|
|
12835
|
+
function isCliWrittenSkill(path30) {
|
|
12569
12836
|
try {
|
|
12570
|
-
return /^generatedBy:/m.test(readFileSync3(
|
|
12837
|
+
return /^generatedBy:/m.test(readFileSync3(path30, "utf-8"));
|
|
12571
12838
|
} catch {
|
|
12572
12839
|
return false;
|
|
12573
12840
|
}
|
|
12574
12841
|
}
|
|
12575
|
-
function isCliWrittenPrompt(
|
|
12842
|
+
function isCliWrittenPrompt(path30) {
|
|
12576
12843
|
try {
|
|
12577
|
-
const content = readFileSync3(
|
|
12844
|
+
const content = readFileSync3(path30, "utf-8");
|
|
12578
12845
|
return content.includes("iamu") || /^generatedBy:/m.test(content);
|
|
12579
12846
|
} catch {
|
|
12580
12847
|
return false;
|
|
@@ -12586,8 +12853,8 @@ function planSkillFiles(hosts, projectRoot, version2) {
|
|
|
12586
12853
|
for (const host of hosts) {
|
|
12587
12854
|
if (pluginProvidesSkills(host, projectRoot)) {
|
|
12588
12855
|
for (const name of skillTemplates.keys()) {
|
|
12589
|
-
const
|
|
12590
|
-
if (isCliWrittenSkill(
|
|
12856
|
+
const path30 = join4(host.getSkillsDir(projectRoot), name, "SKILL.md");
|
|
12857
|
+
if (isCliWrittenSkill(path30)) files.push({ host, path: path30, content: null });
|
|
12591
12858
|
}
|
|
12592
12859
|
} else if (host.supportsSkills) {
|
|
12593
12860
|
const skillsDir = host.getSkillsDir(projectRoot);
|
|
@@ -14107,7 +14374,7 @@ import {
|
|
|
14107
14374
|
readlinkSync,
|
|
14108
14375
|
realpathSync as realpathSync2
|
|
14109
14376
|
} from "fs";
|
|
14110
|
-
import
|
|
14377
|
+
import path18 from "path";
|
|
14111
14378
|
function git(args, cwd) {
|
|
14112
14379
|
return execFileSync("git", args, {
|
|
14113
14380
|
cwd,
|
|
@@ -14160,8 +14427,8 @@ function detectWorktreeState(projectRoot, iamuDirName = IAMU_DIR) {
|
|
|
14160
14427
|
const top = git(["rev-parse", "--show-toplevel"], projectRoot).toString("utf8").trim();
|
|
14161
14428
|
if (!top) return void 0;
|
|
14162
14429
|
const realTop = realpathSync2(top);
|
|
14163
|
-
const realIamu =
|
|
14164
|
-
const iamuRel =
|
|
14430
|
+
const realIamu = path18.join(realpathSync2(projectRoot), iamuDirName);
|
|
14431
|
+
const iamuRel = path18.relative(realTop, realIamu).split(path18.sep).join("/");
|
|
14165
14432
|
const pathspec = iamuRel.startsWith("..") ? [":(top)"] : [":(top)", `:(top,exclude,literal)${iamuRel}`];
|
|
14166
14433
|
const status2 = git(
|
|
14167
14434
|
[
|
|
@@ -14191,7 +14458,7 @@ function detectWorktreeState(projectRoot, iamuDirName = IAMU_DIR) {
|
|
|
14191
14458
|
);
|
|
14192
14459
|
for (const file of sorted.slice(0, MAX_UNTRACKED_HASHED)) {
|
|
14193
14460
|
const rel = file.replace(/\/$/, "");
|
|
14194
|
-
hash.update(`${file}\0${hashFile(
|
|
14461
|
+
hash.update(`${file}\0${hashFile(path18.join(realTop, rel))}
|
|
14195
14462
|
`);
|
|
14196
14463
|
}
|
|
14197
14464
|
if (sorted.length > MAX_UNTRACKED_HASHED) {
|
|
@@ -14364,7 +14631,7 @@ __export(start_run_exports, {
|
|
|
14364
14631
|
resolveStartUrl: () => resolveStartUrl,
|
|
14365
14632
|
startRun: () => startRun
|
|
14366
14633
|
});
|
|
14367
|
-
import
|
|
14634
|
+
import path19 from "path";
|
|
14368
14635
|
import chalk11 from "chalk";
|
|
14369
14636
|
function resolveStartUrl(target, scenario) {
|
|
14370
14637
|
const base = target ?? scenario.target_url ?? null;
|
|
@@ -14540,7 +14807,7 @@ async function startRun(scenarioSlug, opts) {
|
|
|
14540
14807
|
...viewport ? { viewport } : {}
|
|
14541
14808
|
};
|
|
14542
14809
|
await atomicWriteFile(
|
|
14543
|
-
|
|
14810
|
+
path19.join(run.absPath, "walkthrough.json"),
|
|
14544
14811
|
JSON.stringify(manifest, null, 2) + "\n"
|
|
14545
14812
|
);
|
|
14546
14813
|
if (opts.json) {
|
|
@@ -14548,7 +14815,7 @@ async function startRun(scenarioSlug, opts) {
|
|
|
14548
14815
|
JSON.stringify(
|
|
14549
14816
|
{
|
|
14550
14817
|
run_id: run.runId,
|
|
14551
|
-
path:
|
|
14818
|
+
path: path19.relative(projectRoot, run.absPath),
|
|
14552
14819
|
scenario_slug: scenarioSlug,
|
|
14553
14820
|
target_url: start2.url,
|
|
14554
14821
|
target_reachable: targetReachable,
|
|
@@ -14596,7 +14863,7 @@ async function startRun(scenarioSlug, opts) {
|
|
|
14596
14863
|
);
|
|
14597
14864
|
}
|
|
14598
14865
|
console.log(
|
|
14599
|
-
` ${chalk11.dim("path:")} ${
|
|
14866
|
+
` ${chalk11.dim("path:")} ${path19.relative(projectRoot, run.absPath)}`
|
|
14600
14867
|
);
|
|
14601
14868
|
console.log();
|
|
14602
14869
|
console.log(` ${chalk11.bold("Next steps for blind execution:")}`);
|
|
@@ -14637,8 +14904,8 @@ var report_exports = {};
|
|
|
14637
14904
|
__export(report_exports, {
|
|
14638
14905
|
report: () => report
|
|
14639
14906
|
});
|
|
14640
|
-
import { promises as
|
|
14641
|
-
import
|
|
14907
|
+
import { promises as fs24 } from "fs";
|
|
14908
|
+
import path20 from "path";
|
|
14642
14909
|
import chalk12 from "chalk";
|
|
14643
14910
|
async function report(scenario, opts) {
|
|
14644
14911
|
const ctx = await resolveProject(opts);
|
|
@@ -14657,8 +14924,8 @@ async function report(scenario, opts) {
|
|
|
14657
14924
|
for (const rid of runs.reverse()) {
|
|
14658
14925
|
try {
|
|
14659
14926
|
const dir = runDir(projectRoot, rid);
|
|
14660
|
-
const mRaw = await
|
|
14661
|
-
|
|
14927
|
+
const mRaw = await fs24.readFile(
|
|
14928
|
+
path20.join(dir, "walkthrough.json"),
|
|
14662
14929
|
"utf8"
|
|
14663
14930
|
);
|
|
14664
14931
|
const manifest = JSON.parse(mRaw);
|
|
@@ -14666,8 +14933,8 @@ async function report(scenario, opts) {
|
|
|
14666
14933
|
if (scenario && manifest.scenario_slug !== scenario) continue;
|
|
14667
14934
|
let canonical = null;
|
|
14668
14935
|
try {
|
|
14669
|
-
const cRaw = await
|
|
14670
|
-
|
|
14936
|
+
const cRaw = await fs24.readFile(
|
|
14937
|
+
path20.join(dir, "canonical.json"),
|
|
14671
14938
|
"utf8"
|
|
14672
14939
|
);
|
|
14673
14940
|
canonical = JSON.parse(cRaw);
|
|
@@ -15944,6 +16211,35 @@ async function init(opts) {
|
|
|
15944
16211
|
""
|
|
15945
16212
|
)
|
|
15946
16213
|
);
|
|
16214
|
+
const testabilityPath = join5(iamuDir2, "testability.md");
|
|
16215
|
+
if (!existsSync7(testabilityPath)) {
|
|
16216
|
+
await writeFile3(
|
|
16217
|
+
testabilityPath,
|
|
16218
|
+
serializeFrontmatter(
|
|
16219
|
+
{
|
|
16220
|
+
status: "draft",
|
|
16221
|
+
target_url: "http://localhost:3000"
|
|
16222
|
+
},
|
|
16223
|
+
`# Project Testability & Environment
|
|
16224
|
+
|
|
16225
|
+
Target URL and execution harness for walkthroughs.
|
|
16226
|
+
Audited and populated during /iamu:init.
|
|
16227
|
+
|
|
16228
|
+
## Target & Run Instructions
|
|
16229
|
+
- Default URL: http://localhost:3000
|
|
16230
|
+
|
|
16231
|
+
## Reachable Surfaces
|
|
16232
|
+
(Run /iamu:init to discover testable routes)
|
|
16233
|
+
|
|
16234
|
+
## Mocks & Data Fixtures
|
|
16235
|
+
(Run /iamu:init to inspect existing harnesses)
|
|
16236
|
+
|
|
16237
|
+
## Untestable Boundaries
|
|
16238
|
+
(None documented yet)
|
|
16239
|
+
`
|
|
16240
|
+
)
|
|
16241
|
+
);
|
|
16242
|
+
}
|
|
15947
16243
|
}
|
|
15948
16244
|
const promptTemplates = loadPromptTemplates();
|
|
15949
16245
|
const promptsDir = join5(iamuDir2, ".prompts");
|
|
@@ -16553,7 +16849,7 @@ var init_browse_driver = __esm({
|
|
|
16553
16849
|
|
|
16554
16850
|
// src/commands/step-numbering.ts
|
|
16555
16851
|
import { open, readdir, unlink } from "fs/promises";
|
|
16556
|
-
import
|
|
16852
|
+
import path21 from "path";
|
|
16557
16853
|
function stepFileName(stepNumber2) {
|
|
16558
16854
|
return `${String(stepNumber2).padStart(3, "0")}.json`;
|
|
16559
16855
|
}
|
|
@@ -16575,7 +16871,7 @@ async function reserveStepFile(stepsDir, limit) {
|
|
|
16575
16871
|
if (candidate > limit.maxSteps) {
|
|
16576
16872
|
throw new Error(stepLimitMessage(limit.maxSteps, limit.runId));
|
|
16577
16873
|
}
|
|
16578
|
-
const stepPath =
|
|
16874
|
+
const stepPath = path21.join(stepsDir, stepFileName(candidate));
|
|
16579
16875
|
try {
|
|
16580
16876
|
const handle = await open(stepPath, "wx");
|
|
16581
16877
|
await handle.close();
|
|
@@ -16607,7 +16903,7 @@ __export(step_exports, {
|
|
|
16607
16903
|
stepCommand: () => stepCommand
|
|
16608
16904
|
});
|
|
16609
16905
|
import { copyFile, mkdir as mkdir5, stat } from "fs/promises";
|
|
16610
|
-
import
|
|
16906
|
+
import path22 from "path";
|
|
16611
16907
|
import chalk19 from "chalk";
|
|
16612
16908
|
function parseMaxSteps(val) {
|
|
16613
16909
|
if (typeof val === "number" && Number.isFinite(val) && val > 0) return val;
|
|
@@ -16626,8 +16922,8 @@ async function recordStep(input) {
|
|
|
16626
16922
|
if (!runDirPath) {
|
|
16627
16923
|
throw new Error(`Run directory not found for run "${input.runId}"`);
|
|
16628
16924
|
}
|
|
16629
|
-
const stepsDir =
|
|
16630
|
-
const screenshotsDir =
|
|
16925
|
+
const stepsDir = path22.join(runDirPath, "steps");
|
|
16926
|
+
const screenshotsDir = path22.join(runDirPath, "screenshots");
|
|
16631
16927
|
await mkdir5(stepsDir, { recursive: true });
|
|
16632
16928
|
await mkdir5(screenshotsDir, { recursive: true });
|
|
16633
16929
|
const shotSource = input.shot ? await existingShot(input.shot) : null;
|
|
@@ -16641,11 +16937,11 @@ async function recordStep(input) {
|
|
|
16641
16937
|
let finalScreenshotPath = null;
|
|
16642
16938
|
if (shotSource) {
|
|
16643
16939
|
const destName = `step-${stepNumber2}.png`;
|
|
16644
|
-
finalScreenshotPath =
|
|
16645
|
-
if (
|
|
16940
|
+
finalScreenshotPath = path22.join(screenshotsDir, destName);
|
|
16941
|
+
if (path22.resolve(finalScreenshotPath) !== shotSource) {
|
|
16646
16942
|
await copyFile(shotSource, finalScreenshotPath);
|
|
16647
16943
|
}
|
|
16648
|
-
screenshotRef =
|
|
16944
|
+
screenshotRef = path22.join("screenshots", destName);
|
|
16649
16945
|
}
|
|
16650
16946
|
const parsedStep = runStepSchema.parse({
|
|
16651
16947
|
step_number: stepNumber2,
|
|
@@ -16670,7 +16966,7 @@ async function recordStep(input) {
|
|
|
16670
16966
|
}
|
|
16671
16967
|
}
|
|
16672
16968
|
async function existingShot(shot) {
|
|
16673
|
-
const resolved =
|
|
16969
|
+
const resolved = path22.resolve(shot);
|
|
16674
16970
|
const isFile = await stat(resolved).then((s) => s.isFile()).catch(() => false);
|
|
16675
16971
|
if (!isFile) {
|
|
16676
16972
|
throw new Error(
|
|
@@ -16712,7 +17008,7 @@ async function stepCommand(runArg, opts) {
|
|
|
16712
17008
|
JSON.stringify(
|
|
16713
17009
|
{
|
|
16714
17010
|
step_number: res.step.step_number,
|
|
16715
|
-
path:
|
|
17011
|
+
path: path22.relative(projectRoot, res.stepPath),
|
|
16716
17012
|
action: res.step.action,
|
|
16717
17013
|
note: res.step.note,
|
|
16718
17014
|
url: res.step.url,
|
|
@@ -16739,7 +17035,7 @@ async function stepCommand(runArg, opts) {
|
|
|
16739
17035
|
console.log(` ${chalk19.dim("shot:")} ${res.step.screenshot}`);
|
|
16740
17036
|
}
|
|
16741
17037
|
console.log(
|
|
16742
|
-
` ${chalk19.dim("file:")} ${
|
|
17038
|
+
` ${chalk19.dim("file:")} ${path22.relative(projectRoot, res.stepPath)}`
|
|
16743
17039
|
);
|
|
16744
17040
|
console.log();
|
|
16745
17041
|
} catch (err) {
|
|
@@ -16782,21 +17078,21 @@ import {
|
|
|
16782
17078
|
} from "fs";
|
|
16783
17079
|
import { rm as rm2 } from "fs/promises";
|
|
16784
17080
|
import { homedir as homedir3 } from "os";
|
|
16785
|
-
import
|
|
17081
|
+
import path23 from "path";
|
|
16786
17082
|
import chalk20 from "chalk";
|
|
16787
17083
|
function stateDir() {
|
|
16788
|
-
return
|
|
16789
|
-
process.env.IAMU_HOME ??
|
|
17084
|
+
return path23.join(
|
|
17085
|
+
process.env.IAMU_HOME ?? path23.join(homedir3(), ".iamu"),
|
|
16790
17086
|
"browse"
|
|
16791
17087
|
);
|
|
16792
17088
|
}
|
|
16793
17089
|
function profileDir() {
|
|
16794
|
-
return
|
|
17090
|
+
return path23.join(stateDir(), "profile");
|
|
16795
17091
|
}
|
|
16796
17092
|
function readBrowseState() {
|
|
16797
17093
|
try {
|
|
16798
17094
|
return JSON.parse(
|
|
16799
|
-
readFileSync6(
|
|
17095
|
+
readFileSync6(path23.join(stateDir(), "state.json"), "utf8")
|
|
16800
17096
|
);
|
|
16801
17097
|
} catch {
|
|
16802
17098
|
return null;
|
|
@@ -16804,7 +17100,7 @@ function readBrowseState() {
|
|
|
16804
17100
|
}
|
|
16805
17101
|
function writeState(state) {
|
|
16806
17102
|
mkdirSync(stateDir(), { recursive: true });
|
|
16807
|
-
writeFileSync(
|
|
17103
|
+
writeFileSync(path23.join(stateDir(), "state.json"), JSON.stringify(state));
|
|
16808
17104
|
}
|
|
16809
17105
|
function fail(message, opts) {
|
|
16810
17106
|
if (opts?.json) {
|
|
@@ -16867,7 +17163,7 @@ async function requestedViewport(opts) {
|
|
|
16867
17163
|
let manifest;
|
|
16868
17164
|
try {
|
|
16869
17165
|
manifest = JSON.parse(
|
|
16870
|
-
readFileSync6(
|
|
17166
|
+
readFileSync6(path23.join(dir, "walkthrough.json"), "utf8")
|
|
16871
17167
|
);
|
|
16872
17168
|
} catch {
|
|
16873
17169
|
fail(
|
|
@@ -16973,7 +17269,7 @@ async function stop(opts, driver) {
|
|
|
16973
17269
|
await driver.closeBrowser(state.port);
|
|
16974
17270
|
if (state.pid) await reapBrowser(state.pid, profileDir(), driver);
|
|
16975
17271
|
try {
|
|
16976
|
-
unlinkSync(
|
|
17272
|
+
unlinkSync(path23.join(stateDir(), "state.json"));
|
|
16977
17273
|
} catch {
|
|
16978
17274
|
}
|
|
16979
17275
|
report2(opts, { stopped: true }, [` ${chalk20.green("\u2713")} Browser stopped`]);
|
|
@@ -17039,8 +17335,8 @@ async function withPage(opts, driver, fn, target) {
|
|
|
17039
17335
|
let screenshot = null;
|
|
17040
17336
|
if (opts.shot) {
|
|
17041
17337
|
await waitForVisualStability(page);
|
|
17042
|
-
screenshot =
|
|
17043
|
-
mkdirSync(
|
|
17338
|
+
screenshot = path23.resolve(opts.shot);
|
|
17339
|
+
mkdirSync(path23.dirname(screenshot), { recursive: true });
|
|
17044
17340
|
await page.screenshot({ path: screenshot, fullPage: Boolean(opts.full) });
|
|
17045
17341
|
}
|
|
17046
17342
|
return { url: page.url(), title: await page.title(), screenshot };
|
|
@@ -17119,7 +17415,7 @@ function printStep(opts, record, projectRoot) {
|
|
|
17119
17415
|
if (opts.json) {
|
|
17120
17416
|
const doc = {
|
|
17121
17417
|
step_number: step.step_number,
|
|
17122
|
-
path:
|
|
17418
|
+
path: path23.relative(projectRoot, record.stepPath),
|
|
17123
17419
|
action: step.action,
|
|
17124
17420
|
note: step.note,
|
|
17125
17421
|
url: step.url,
|
|
@@ -17193,10 +17489,10 @@ async function browseStep(args, opts, driver) {
|
|
|
17193
17489
|
const runDirPath = resolveRunDir(ctx.projectRoot, opts.run);
|
|
17194
17490
|
if (!runDirPath) return;
|
|
17195
17491
|
const maxSteps = parseMaxSteps(opts.maxSteps);
|
|
17196
|
-
if (await peekNextStepNumber(
|
|
17492
|
+
if (await peekNextStepNumber(path23.join(runDirPath, "steps")) > maxSteps) {
|
|
17197
17493
|
return fail(stepLimitMessage(maxSteps, opts.run), opts);
|
|
17198
17494
|
}
|
|
17199
|
-
const pendingShot =
|
|
17495
|
+
const pendingShot = path23.join(
|
|
17200
17496
|
runDirPath,
|
|
17201
17497
|
"screenshots",
|
|
17202
17498
|
`.pending-${randomUUID7()}.png`
|
|
@@ -17267,11 +17563,14 @@ var doctor_exports = {};
|
|
|
17267
17563
|
__export(doctor_exports, {
|
|
17268
17564
|
checkCdpResponsive: () => checkCdpResponsive,
|
|
17269
17565
|
checkPortAvailable: () => checkPortAvailable,
|
|
17566
|
+
checkProjectHarnesses: () => checkProjectHarnesses,
|
|
17567
|
+
checkProjectTestabilityContract: () => checkProjectTestabilityContract,
|
|
17568
|
+
checkTargetHealth: () => checkTargetHealth,
|
|
17270
17569
|
doctor: () => doctor,
|
|
17271
17570
|
runDoctorChecks: () => runDoctorChecks
|
|
17272
17571
|
});
|
|
17273
17572
|
import { createServer } from "net";
|
|
17274
|
-
import { existsSync as existsSync10 } from "fs";
|
|
17573
|
+
import { existsSync as existsSync10, promises as fs25 } from "fs";
|
|
17275
17574
|
import { join as join7 } from "path";
|
|
17276
17575
|
import chalk21 from "chalk";
|
|
17277
17576
|
function checkPortAvailable(port) {
|
|
@@ -17549,11 +17848,186 @@ async function checkAgentIntegrations(projectRoot) {
|
|
|
17549
17848
|
}
|
|
17550
17849
|
return checks;
|
|
17551
17850
|
}
|
|
17552
|
-
async function
|
|
17851
|
+
async function checkTargetHealth(targetUrl) {
|
|
17852
|
+
const checks = [];
|
|
17853
|
+
let parsedUrl;
|
|
17854
|
+
try {
|
|
17855
|
+
parsedUrl = new URL(targetUrl);
|
|
17856
|
+
} catch {
|
|
17857
|
+
checks.push({
|
|
17858
|
+
category: "target",
|
|
17859
|
+
name: "Target URL Format",
|
|
17860
|
+
status: "error",
|
|
17861
|
+
message: `Invalid target URL: "${targetUrl}"`,
|
|
17862
|
+
remedy: "Provide a valid URL with protocol (e.g. http://localhost:3000)."
|
|
17863
|
+
});
|
|
17864
|
+
return checks;
|
|
17865
|
+
}
|
|
17866
|
+
try {
|
|
17867
|
+
const res = await fetch(targetUrl, {
|
|
17868
|
+
method: "GET",
|
|
17869
|
+
redirect: "manual",
|
|
17870
|
+
signal: AbortSignal.timeout(3e3)
|
|
17871
|
+
});
|
|
17872
|
+
if ([301, 302, 303, 307, 308].includes(res.status)) {
|
|
17873
|
+
const location = res.headers.get("location") || "";
|
|
17874
|
+
const isExternalAuth = /auth0\.com|okta\.com|clerk\.com|accounts\.google\.com|cognito|login\.microsoftonline\.com/i.test(
|
|
17875
|
+
location
|
|
17876
|
+
) || location.includes("/oauth");
|
|
17877
|
+
if (isExternalAuth) {
|
|
17878
|
+
let redirectHost = location;
|
|
17879
|
+
try {
|
|
17880
|
+
redirectHost = new URL(location, targetUrl).host;
|
|
17881
|
+
} catch {
|
|
17882
|
+
}
|
|
17883
|
+
checks.push({
|
|
17884
|
+
category: "target",
|
|
17885
|
+
name: "Authentication Boundary",
|
|
17886
|
+
status: "warn",
|
|
17887
|
+
message: `Target immediately redirects to external auth (${redirectHost})`,
|
|
17888
|
+
remedy: "Configure a dev auth bypass or prepare for interactive login handshake before walking."
|
|
17889
|
+
});
|
|
17890
|
+
return checks;
|
|
17891
|
+
}
|
|
17892
|
+
checks.push({
|
|
17893
|
+
category: "target",
|
|
17894
|
+
name: "Target Reachability",
|
|
17895
|
+
status: "ok",
|
|
17896
|
+
message: `HTTP ${res.status} redirect -> ${location}`
|
|
17897
|
+
});
|
|
17898
|
+
return checks;
|
|
17899
|
+
}
|
|
17900
|
+
if (res.status >= 500) {
|
|
17901
|
+
checks.push({
|
|
17902
|
+
category: "target",
|
|
17903
|
+
name: "Target Health",
|
|
17904
|
+
status: "error",
|
|
17905
|
+
message: `Target returned HTTP ${res.status} ${res.statusText || "Server Error"}`,
|
|
17906
|
+
remedy: "Check dev server logs for unhandled exceptions or missing environment variables."
|
|
17907
|
+
});
|
|
17908
|
+
return checks;
|
|
17909
|
+
}
|
|
17910
|
+
checks.push({
|
|
17911
|
+
category: "target",
|
|
17912
|
+
name: "Target Reachability",
|
|
17913
|
+
status: "ok",
|
|
17914
|
+
message: `HTTP ${res.status} OK (${parsedUrl.origin})`
|
|
17915
|
+
});
|
|
17916
|
+
} catch (err) {
|
|
17917
|
+
const message = err.message;
|
|
17918
|
+
checks.push({
|
|
17919
|
+
category: "target",
|
|
17920
|
+
name: "Target Reachability",
|
|
17921
|
+
status: "warn",
|
|
17922
|
+
message: `Cannot connect to ${targetUrl} (${message})`,
|
|
17923
|
+
remedy: "Ensure your dev server is running and listening on the specified port."
|
|
17924
|
+
});
|
|
17925
|
+
}
|
|
17926
|
+
return checks;
|
|
17927
|
+
}
|
|
17928
|
+
async function checkProjectHarnesses(projectRoot) {
|
|
17929
|
+
const checks = [];
|
|
17930
|
+
const pkgPath = join7(projectRoot, "package.json");
|
|
17931
|
+
if (!existsSync10(pkgPath)) return checks;
|
|
17932
|
+
try {
|
|
17933
|
+
const raw = await fs25.readFile(pkgPath, "utf8");
|
|
17934
|
+
const pkg = JSON.parse(raw);
|
|
17935
|
+
const scripts = pkg.scripts || {};
|
|
17936
|
+
const allDeps = {
|
|
17937
|
+
...pkg.dependencies || {},
|
|
17938
|
+
...pkg.devDependencies || {}
|
|
17939
|
+
};
|
|
17940
|
+
const devScripts = ["dev", "start", "dev:mock", "serve"].filter(
|
|
17941
|
+
(s) => s in scripts
|
|
17942
|
+
);
|
|
17943
|
+
if (devScripts.length > 0) {
|
|
17944
|
+
checks.push({
|
|
17945
|
+
category: "target",
|
|
17946
|
+
name: "Dev Server Script",
|
|
17947
|
+
status: "ok",
|
|
17948
|
+
message: `Available scripts: ${devScripts.map((s) => `pnpm ${s}`).join(", ")}`
|
|
17949
|
+
});
|
|
17950
|
+
} else {
|
|
17951
|
+
checks.push({
|
|
17952
|
+
category: "target",
|
|
17953
|
+
name: "Dev Server Script",
|
|
17954
|
+
status: "warn",
|
|
17955
|
+
message: "No standard dev script (dev, start, dev:mock) found in package.json",
|
|
17956
|
+
remedy: "Add a `dev` or `start` script to package.json to boot your app locally."
|
|
17957
|
+
});
|
|
17958
|
+
}
|
|
17959
|
+
if (allDeps.msw || existsSync10(join7(projectRoot, "src/mocks")) || existsSync10(join7(projectRoot, "mocks"))) {
|
|
17960
|
+
checks.push({
|
|
17961
|
+
category: "target",
|
|
17962
|
+
name: "Mock Harness (MSW)",
|
|
17963
|
+
status: "ok",
|
|
17964
|
+
message: "Mock Service Worker detected. API mocks can decouple walks from live backends."
|
|
17965
|
+
});
|
|
17966
|
+
}
|
|
17967
|
+
const seedScript = Object.keys(scripts).find(
|
|
17968
|
+
(s) => /seed|db:seed|fixtures/i.test(s)
|
|
17969
|
+
);
|
|
17970
|
+
if (seedScript) {
|
|
17971
|
+
checks.push({
|
|
17972
|
+
category: "target",
|
|
17973
|
+
name: "Database Seed",
|
|
17974
|
+
status: "ok",
|
|
17975
|
+
message: `Seed script detected (${seedScript}). Preconditions can be reset between runs.`
|
|
17976
|
+
});
|
|
17977
|
+
}
|
|
17978
|
+
} catch {
|
|
17979
|
+
}
|
|
17980
|
+
return checks;
|
|
17981
|
+
}
|
|
17982
|
+
async function checkProjectTestabilityContract(projectRoot) {
|
|
17983
|
+
const checks = [];
|
|
17984
|
+
const testabilityPath = join7(projectRoot, "iamu", "testability.md");
|
|
17985
|
+
if (!existsSync10(testabilityPath)) {
|
|
17986
|
+
checks.push({
|
|
17987
|
+
category: "target",
|
|
17988
|
+
name: "Testability Contract (testability.md)",
|
|
17989
|
+
status: "warn",
|
|
17990
|
+
message: "iamu/testability.md not found",
|
|
17991
|
+
remedy: "Run `/iamu:init` to audit your codebase and record testable routes, auth strategies, and mock harnesses."
|
|
17992
|
+
});
|
|
17993
|
+
return { checks };
|
|
17994
|
+
}
|
|
17995
|
+
try {
|
|
17996
|
+
const raw = await fs25.readFile(testabilityPath, "utf8");
|
|
17997
|
+
const { frontmatter } = parseFrontmatter(raw);
|
|
17998
|
+
const targetUrl = typeof frontmatter.target_url === "string" ? frontmatter.target_url : void 0;
|
|
17999
|
+
const details = [];
|
|
18000
|
+
if (targetUrl) details.push(`Target: ${targetUrl}`);
|
|
18001
|
+
if (frontmatter.dev_command) {
|
|
18002
|
+
details.push(`Command: ${frontmatter.dev_command}`);
|
|
18003
|
+
}
|
|
18004
|
+
if (frontmatter.auth_strategy) {
|
|
18005
|
+
details.push(`Auth: ${frontmatter.auth_strategy}`);
|
|
18006
|
+
}
|
|
18007
|
+
checks.push({
|
|
18008
|
+
category: "target",
|
|
18009
|
+
name: "Testability Contract (testability.md)",
|
|
18010
|
+
status: "ok",
|
|
18011
|
+
message: `Configured (${details.join(" \xB7 ") || "present"})`
|
|
18012
|
+
});
|
|
18013
|
+
return { checks, targetUrl };
|
|
18014
|
+
} catch (err) {
|
|
18015
|
+
checks.push({
|
|
18016
|
+
category: "target",
|
|
18017
|
+
name: "Testability Contract (testability.md)",
|
|
18018
|
+
status: "warn",
|
|
18019
|
+
message: `Unreadable testability contract: ${err.message}`,
|
|
18020
|
+
remedy: "Repair iamu/testability.md frontmatter."
|
|
18021
|
+
});
|
|
18022
|
+
return { checks };
|
|
18023
|
+
}
|
|
18024
|
+
}
|
|
18025
|
+
async function runDoctorChecks(projectRoot, opts) {
|
|
17553
18026
|
const checks = [
|
|
17554
18027
|
...checkRuntimeChecks(),
|
|
17555
18028
|
...await checkBrowserChecks()
|
|
17556
18029
|
];
|
|
18030
|
+
let effectiveTarget = opts?.target;
|
|
17557
18031
|
if (!projectRoot || !existsSync10(join7(projectRoot, "iamu"))) {
|
|
17558
18032
|
checks.push({
|
|
17559
18033
|
category: "project",
|
|
@@ -17573,6 +18047,15 @@ async function runDoctorChecks(projectRoot) {
|
|
|
17573
18047
|
checks.push(...await checkProjectConfiguration(projectRoot));
|
|
17574
18048
|
checks.push(...await checkProjectContextAndEntities(projectRoot));
|
|
17575
18049
|
checks.push(...await checkAgentIntegrations(projectRoot));
|
|
18050
|
+
checks.push(...await checkProjectHarnesses(projectRoot));
|
|
18051
|
+
const testability = await checkProjectTestabilityContract(projectRoot);
|
|
18052
|
+
checks.push(...testability.checks);
|
|
18053
|
+
if (!effectiveTarget && testability.targetUrl) {
|
|
18054
|
+
effectiveTarget = testability.targetUrl;
|
|
18055
|
+
}
|
|
18056
|
+
}
|
|
18057
|
+
if (effectiveTarget) {
|
|
18058
|
+
checks.push(...await checkTargetHealth(effectiveTarget));
|
|
17576
18059
|
}
|
|
17577
18060
|
const okCount = checks.filter((c) => c.status === "ok").length;
|
|
17578
18061
|
const warnCount = checks.filter((c) => c.status === "warn").length;
|
|
@@ -17589,7 +18072,7 @@ async function runDoctorChecks(projectRoot) {
|
|
|
17589
18072
|
}
|
|
17590
18073
|
async function doctor(opts) {
|
|
17591
18074
|
const projectRoot = findProjectRoot(process.cwd()) ?? void 0;
|
|
17592
|
-
const result = await runDoctorChecks(projectRoot);
|
|
18075
|
+
const result = await runDoctorChecks(projectRoot, { target: opts?.target });
|
|
17593
18076
|
if (opts?.json) {
|
|
17594
18077
|
console.log(JSON.stringify(result, null, 2));
|
|
17595
18078
|
if (!result.ok) process.exitCode = 1;
|
|
@@ -17603,7 +18086,8 @@ async function doctor(opts) {
|
|
|
17603
18086
|
{ key: "browser", title: "Browser Automation" },
|
|
17604
18087
|
{ key: "project", title: "Project Configuration" },
|
|
17605
18088
|
{ key: "agent", title: "Agent Integrations" },
|
|
17606
|
-
{ key: "cloud", title: "Cloud & Authentication" }
|
|
18089
|
+
{ key: "cloud", title: "Cloud & Authentication" },
|
|
18090
|
+
{ key: "target", title: "Target & Testability" }
|
|
17607
18091
|
];
|
|
17608
18092
|
for (const cat of categories) {
|
|
17609
18093
|
const catChecks = result.checks.filter((c) => c.category === cat.key);
|
|
@@ -18081,13 +18565,13 @@ __export(validate_exports, {
|
|
|
18081
18565
|
validate: () => validate
|
|
18082
18566
|
});
|
|
18083
18567
|
import { randomUUID as randomUUID8 } from "crypto";
|
|
18084
|
-
import { promises as
|
|
18085
|
-
import
|
|
18568
|
+
import { promises as fs26 } from "fs";
|
|
18569
|
+
import path24 from "path";
|
|
18086
18570
|
import chalk23 from "chalk";
|
|
18087
18571
|
function inferTypeFromPath(filePath, projectRoot) {
|
|
18088
|
-
const rel =
|
|
18089
|
-
if (rel.startsWith("..") ||
|
|
18090
|
-
const segments = rel.split(
|
|
18572
|
+
const rel = path24.relative(path24.join(projectRoot, "iamu"), filePath);
|
|
18573
|
+
if (rel.startsWith("..") || path24.isAbsolute(rel)) return null;
|
|
18574
|
+
const segments = rel.split(path24.sep);
|
|
18091
18575
|
if (segments.length < 2) return null;
|
|
18092
18576
|
return TYPE_BY_ALIAS.get(segments[0]) ?? null;
|
|
18093
18577
|
}
|
|
@@ -18117,7 +18601,7 @@ function validateFile(filePath, raw, projectRoot) {
|
|
|
18117
18601
|
);
|
|
18118
18602
|
return result;
|
|
18119
18603
|
}
|
|
18120
|
-
if (
|
|
18604
|
+
if (path24.basename(filePath) === "context.yml") {
|
|
18121
18605
|
applyZodValidation(contextFrontmatterSchema, frontmatter, result);
|
|
18122
18606
|
const warnings = [];
|
|
18123
18607
|
const hasNotes = Boolean(
|
|
@@ -18177,17 +18661,17 @@ async function buildRepairLookups(projectRoot) {
|
|
|
18177
18661
|
}
|
|
18178
18662
|
}
|
|
18179
18663
|
const runScenario = /* @__PURE__ */ new Map();
|
|
18180
|
-
const runsDir2 =
|
|
18664
|
+
const runsDir2 = path24.join(projectRoot, "iamu", ".runs");
|
|
18181
18665
|
let runIds = [];
|
|
18182
18666
|
try {
|
|
18183
|
-
runIds = await
|
|
18667
|
+
runIds = await fs26.readdir(runsDir2);
|
|
18184
18668
|
} catch {
|
|
18185
18669
|
}
|
|
18186
18670
|
for (const runId of runIds) {
|
|
18187
18671
|
try {
|
|
18188
18672
|
const manifest = JSON.parse(
|
|
18189
|
-
await
|
|
18190
|
-
|
|
18673
|
+
await fs26.readFile(
|
|
18674
|
+
path24.join(runsDir2, runId, "walkthrough.json"),
|
|
18191
18675
|
"utf8"
|
|
18192
18676
|
)
|
|
18193
18677
|
);
|
|
@@ -18209,7 +18693,7 @@ async function fixFindings(projectRoot, results, dryRun) {
|
|
|
18209
18693
|
let raw;
|
|
18210
18694
|
let parsed;
|
|
18211
18695
|
try {
|
|
18212
|
-
raw = await
|
|
18696
|
+
raw = await fs26.readFile(result.path, "utf8");
|
|
18213
18697
|
parsed = parseFrontmatter(raw);
|
|
18214
18698
|
} catch {
|
|
18215
18699
|
continue;
|
|
@@ -18218,7 +18702,7 @@ async function fixFindings(projectRoot, results, dryRun) {
|
|
|
18218
18702
|
parsed.frontmatter,
|
|
18219
18703
|
{
|
|
18220
18704
|
...lookups,
|
|
18221
|
-
fileSlug:
|
|
18705
|
+
fileSlug: path24.basename(result.path, ".md"),
|
|
18222
18706
|
newId: randomUUID8,
|
|
18223
18707
|
body: parsed.body
|
|
18224
18708
|
}
|
|
@@ -18253,9 +18737,9 @@ async function validate(filePath, opts) {
|
|
|
18253
18737
|
if (process.exitCode === 1) return;
|
|
18254
18738
|
const results = [];
|
|
18255
18739
|
if (filePath) {
|
|
18256
|
-
const abs =
|
|
18740
|
+
const abs = path24.isAbsolute(filePath) ? filePath : path24.resolve(process.cwd(), filePath);
|
|
18257
18741
|
try {
|
|
18258
|
-
const raw = await
|
|
18742
|
+
const raw = await fs26.readFile(abs, "utf8");
|
|
18259
18743
|
results.push(validateFile(abs, raw, projectRoot));
|
|
18260
18744
|
} catch (err) {
|
|
18261
18745
|
commandError(
|
|
@@ -18265,24 +18749,24 @@ async function validate(filePath, opts) {
|
|
|
18265
18749
|
return;
|
|
18266
18750
|
}
|
|
18267
18751
|
} else if (opts.run) {
|
|
18268
|
-
const findingsDir =
|
|
18752
|
+
const findingsDir = path24.join(projectRoot, "iamu", ENTITY_DIRS.finding);
|
|
18269
18753
|
let names = [];
|
|
18270
18754
|
try {
|
|
18271
|
-
names = await
|
|
18755
|
+
names = await fs26.readdir(findingsDir);
|
|
18272
18756
|
} catch {
|
|
18273
18757
|
}
|
|
18274
18758
|
for (const name of names) {
|
|
18275
18759
|
if (!name.endsWith(".md")) continue;
|
|
18276
|
-
const abs =
|
|
18760
|
+
const abs = path24.join(findingsDir, name);
|
|
18277
18761
|
try {
|
|
18278
|
-
const raw = await
|
|
18762
|
+
const raw = await fs26.readFile(abs, "utf8");
|
|
18279
18763
|
const { frontmatter } = parseFrontmatter(raw);
|
|
18280
18764
|
if (frontmatter.run === opts.run) {
|
|
18281
18765
|
results.push(validateFile(abs, raw, projectRoot));
|
|
18282
18766
|
}
|
|
18283
18767
|
} catch {
|
|
18284
18768
|
results.push({
|
|
18285
|
-
path:
|
|
18769
|
+
path: path24.relative(projectRoot, abs),
|
|
18286
18770
|
valid: false,
|
|
18287
18771
|
errors: ["Cannot read file or parse frontmatter"]
|
|
18288
18772
|
});
|
|
@@ -18315,31 +18799,31 @@ async function validate(filePath, opts) {
|
|
|
18315
18799
|
} else {
|
|
18316
18800
|
const typesToCheck = typeFilter ? ENTITY_TYPES.filter((t) => typeFilter.has(t)) : ENTITY_TYPES;
|
|
18317
18801
|
for (const type2 of typesToCheck) {
|
|
18318
|
-
const iamuTypeDir =
|
|
18802
|
+
const iamuTypeDir = path24.join(projectRoot, "iamu", ENTITY_DIRS[type2]);
|
|
18319
18803
|
let names;
|
|
18320
18804
|
try {
|
|
18321
|
-
names = await
|
|
18805
|
+
names = await fs26.readdir(iamuTypeDir);
|
|
18322
18806
|
} catch {
|
|
18323
18807
|
continue;
|
|
18324
18808
|
}
|
|
18325
18809
|
for (const name of names) {
|
|
18326
18810
|
if (!name.endsWith(".md")) continue;
|
|
18327
|
-
const abs =
|
|
18811
|
+
const abs = path24.join(iamuTypeDir, name);
|
|
18328
18812
|
try {
|
|
18329
|
-
const raw = await
|
|
18813
|
+
const raw = await fs26.readFile(abs, "utf8");
|
|
18330
18814
|
results.push(validateFile(abs, raw, projectRoot));
|
|
18331
18815
|
} catch {
|
|
18332
18816
|
results.push({
|
|
18333
|
-
path:
|
|
18817
|
+
path: path24.relative(projectRoot, abs),
|
|
18334
18818
|
valid: false,
|
|
18335
18819
|
errors: ["Cannot read file"]
|
|
18336
18820
|
});
|
|
18337
18821
|
}
|
|
18338
18822
|
}
|
|
18339
18823
|
}
|
|
18340
|
-
const ctxPath =
|
|
18824
|
+
const ctxPath = path24.join(projectRoot, "iamu", "context.yml");
|
|
18341
18825
|
try {
|
|
18342
|
-
const raw = await
|
|
18826
|
+
const raw = await fs26.readFile(ctxPath, "utf8");
|
|
18343
18827
|
results.push(validateFile(ctxPath, raw, projectRoot));
|
|
18344
18828
|
} catch {
|
|
18345
18829
|
}
|
|
@@ -18358,19 +18842,19 @@ async function validate(filePath, opts) {
|
|
|
18358
18842
|
valid,
|
|
18359
18843
|
errors: results.filter((r) => !r.valid).flatMap(
|
|
18360
18844
|
(r) => r.errors.map((e) => ({
|
|
18361
|
-
path:
|
|
18845
|
+
path: path24.relative(projectRoot, r.path),
|
|
18362
18846
|
message: e
|
|
18363
18847
|
}))
|
|
18364
18848
|
),
|
|
18365
18849
|
warnings: results.filter((r) => r.warnings && r.warnings.length > 0).flatMap(
|
|
18366
18850
|
(r) => (r.warnings ?? []).map((w) => ({
|
|
18367
|
-
path:
|
|
18851
|
+
path: path24.relative(projectRoot, r.path),
|
|
18368
18852
|
message: w
|
|
18369
18853
|
}))
|
|
18370
18854
|
),
|
|
18371
18855
|
...opts.fix ? {
|
|
18372
18856
|
repaired: repaired.map((r) => ({
|
|
18373
|
-
path:
|
|
18857
|
+
path: path24.relative(projectRoot, r.path),
|
|
18374
18858
|
changes: r.repairs,
|
|
18375
18859
|
valid: r.valid
|
|
18376
18860
|
}))
|
|
@@ -18392,7 +18876,7 @@ async function validate(filePath, opts) {
|
|
|
18392
18876
|
}
|
|
18393
18877
|
let hasErrors = false;
|
|
18394
18878
|
for (const r of results) {
|
|
18395
|
-
const relPath =
|
|
18879
|
+
const relPath = path24.relative(projectRoot, r.path);
|
|
18396
18880
|
if (r.valid) {
|
|
18397
18881
|
if (r.warnings && r.warnings.length > 0) {
|
|
18398
18882
|
console.log(` ${symbols.warning} ` + chalk23.yellow(relPath));
|
|
@@ -18706,6 +19190,797 @@ var init_new_entity = __esm({
|
|
|
18706
19190
|
}
|
|
18707
19191
|
});
|
|
18708
19192
|
|
|
19193
|
+
// src/commands/explore-crawl.ts
|
|
19194
|
+
function candidateSig(c) {
|
|
19195
|
+
return `${c.role}
|
|
19196
|
+
${c.name}
|
|
19197
|
+
${c.occurrence}`;
|
|
19198
|
+
}
|
|
19199
|
+
function originOf(url) {
|
|
19200
|
+
try {
|
|
19201
|
+
return new URL(url).origin;
|
|
19202
|
+
} catch {
|
|
19203
|
+
return null;
|
|
19204
|
+
}
|
|
19205
|
+
}
|
|
19206
|
+
function skipReason(c, origin, deny) {
|
|
19207
|
+
if (deny.some((re) => re.test(c.name) || c.href && re.test(c.href))) {
|
|
19208
|
+
return "denied";
|
|
19209
|
+
}
|
|
19210
|
+
if (c.submit) return "submit";
|
|
19211
|
+
if (UNSAFE_NAME.test(c.name)) return "unsafe_name";
|
|
19212
|
+
if (c.href) {
|
|
19213
|
+
let u = null;
|
|
19214
|
+
try {
|
|
19215
|
+
u = new URL(c.href);
|
|
19216
|
+
} catch {
|
|
19217
|
+
}
|
|
19218
|
+
if (u && u.protocol !== "javascript:") {
|
|
19219
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") return "external";
|
|
19220
|
+
if (u.origin !== origin) return "external";
|
|
19221
|
+
if (UNSAFE_PATH.test(u.pathname)) return "unsafe_name";
|
|
19222
|
+
}
|
|
19223
|
+
if (c.download) return "download";
|
|
19224
|
+
}
|
|
19225
|
+
return null;
|
|
19226
|
+
}
|
|
19227
|
+
async function settle(page, settleMs) {
|
|
19228
|
+
await page.waitForLoadState("domcontentloaded", { timeout: NAV_TIMEOUT_MS }).catch(() => {
|
|
19229
|
+
});
|
|
19230
|
+
await page.waitForLoadState("networkidle", { timeout: settleMs }).catch(() => {
|
|
19231
|
+
});
|
|
19232
|
+
}
|
|
19233
|
+
async function readPage(page) {
|
|
19234
|
+
return page.evaluate(READ_PAGE);
|
|
19235
|
+
}
|
|
19236
|
+
async function markDocument(page) {
|
|
19237
|
+
await page.evaluate("window.__iamuDoc = true").catch(() => {
|
|
19238
|
+
});
|
|
19239
|
+
}
|
|
19240
|
+
async function isSameDocument(page) {
|
|
19241
|
+
return page.evaluate("window.__iamuDoc === true").catch(() => false);
|
|
19242
|
+
}
|
|
19243
|
+
async function clickCandidate(page, want) {
|
|
19244
|
+
const state = await readPage(page);
|
|
19245
|
+
const c = state.candidates.find(
|
|
19246
|
+
(x) => candidateSig(x) === candidateSig(want)
|
|
19247
|
+
);
|
|
19248
|
+
if (!c) return false;
|
|
19249
|
+
await page.locator(`[data-iamu-x="${c.index}"]`).click({ timeout: CLICK_TIMEOUT_MS });
|
|
19250
|
+
return true;
|
|
19251
|
+
}
|
|
19252
|
+
async function crawl(page, opts) {
|
|
19253
|
+
const origin = originOf(opts.startUrl);
|
|
19254
|
+
if (!origin) throw new Error(`Not a URL: ${opts.startUrl}`);
|
|
19255
|
+
let blocked = [];
|
|
19256
|
+
await page.route("**/*", (route) => {
|
|
19257
|
+
const req = route.request();
|
|
19258
|
+
if (SAFE_METHODS.has(req.method())) return route.continue();
|
|
19259
|
+
let where = req.url();
|
|
19260
|
+
try {
|
|
19261
|
+
const u = new URL(where);
|
|
19262
|
+
where = u.origin === origin ? u.pathname : `${u.origin}${u.pathname}`;
|
|
19263
|
+
} catch {
|
|
19264
|
+
}
|
|
19265
|
+
blocked.push(`${req.method()} ${where}`);
|
|
19266
|
+
return route.abort("blockedbyclient");
|
|
19267
|
+
});
|
|
19268
|
+
const onDialog = (d) => {
|
|
19269
|
+
d.dismiss().catch(() => {
|
|
19270
|
+
});
|
|
19271
|
+
};
|
|
19272
|
+
const onPopup = (p) => {
|
|
19273
|
+
if (p !== page) p.close().catch(() => {
|
|
19274
|
+
});
|
|
19275
|
+
};
|
|
19276
|
+
page.on("dialog", onDialog);
|
|
19277
|
+
page.context().on("page", onPopup);
|
|
19278
|
+
try {
|
|
19279
|
+
return await crawlFrom(page, opts, origin, () => {
|
|
19280
|
+
const out = blocked;
|
|
19281
|
+
blocked = [];
|
|
19282
|
+
return out;
|
|
19283
|
+
});
|
|
19284
|
+
} finally {
|
|
19285
|
+
page.off("dialog", onDialog);
|
|
19286
|
+
page.context().off("page", onPopup);
|
|
19287
|
+
await page.unroute("**/*").catch(() => {
|
|
19288
|
+
});
|
|
19289
|
+
}
|
|
19290
|
+
}
|
|
19291
|
+
async function crawlFrom(page, opts, origin, takeBlocked) {
|
|
19292
|
+
const deny = opts.deny ?? [];
|
|
19293
|
+
const settleMs = opts.settleMs ?? 1500;
|
|
19294
|
+
const { budget } = opts;
|
|
19295
|
+
const stats = {
|
|
19296
|
+
surfaces: 0,
|
|
19297
|
+
actions_tried: 0,
|
|
19298
|
+
skipped: 0,
|
|
19299
|
+
blocked_requests: 0,
|
|
19300
|
+
errors: 0,
|
|
19301
|
+
truncated: false
|
|
19302
|
+
};
|
|
19303
|
+
const surfaces = [];
|
|
19304
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
19305
|
+
const byPlaceView = /* @__PURE__ */ new Map();
|
|
19306
|
+
const signatureOf = (s) => exploreSignature(s.headings, s.title);
|
|
19307
|
+
function addSurface(state, place, view, path30, depth) {
|
|
19308
|
+
const key = exploreSurfaceKey(place, view);
|
|
19309
|
+
const surface = {
|
|
19310
|
+
key,
|
|
19311
|
+
url: state.url,
|
|
19312
|
+
place,
|
|
19313
|
+
view,
|
|
19314
|
+
signature: signatureOf(state),
|
|
19315
|
+
label: state.headings[0] ?? (state.title || place),
|
|
19316
|
+
headings: state.headings,
|
|
19317
|
+
fields: state.fields,
|
|
19318
|
+
depth,
|
|
19319
|
+
path: path30,
|
|
19320
|
+
actions: []
|
|
19321
|
+
};
|
|
19322
|
+
surfaces.push(surface);
|
|
19323
|
+
byKey.set(key, surface);
|
|
19324
|
+
byPlaceView.set(`${place}
|
|
19325
|
+
${surface.signature}`, key);
|
|
19326
|
+
stats.surfaces++;
|
|
19327
|
+
opts.log?.(` ${" ".repeat(depth)}${key} ${surface.label}`);
|
|
19328
|
+
return surface;
|
|
19329
|
+
}
|
|
19330
|
+
async function goto(url) {
|
|
19331
|
+
await page.goto(url, {
|
|
19332
|
+
waitUntil: "domcontentloaded",
|
|
19333
|
+
timeout: NAV_TIMEOUT_MS
|
|
19334
|
+
});
|
|
19335
|
+
await settle(page, settleMs);
|
|
19336
|
+
}
|
|
19337
|
+
async function replay(path30) {
|
|
19338
|
+
if (!("goto" in (path30[0] ?? {}))) await goto(opts.startUrl);
|
|
19339
|
+
for (const step of path30) {
|
|
19340
|
+
if ("goto" in step) {
|
|
19341
|
+
await goto(step.goto);
|
|
19342
|
+
} else {
|
|
19343
|
+
if (!await clickCandidate(page, step.click)) return false;
|
|
19344
|
+
await settle(page, settleMs);
|
|
19345
|
+
}
|
|
19346
|
+
}
|
|
19347
|
+
return true;
|
|
19348
|
+
}
|
|
19349
|
+
await goto(opts.startUrl);
|
|
19350
|
+
if (await page.evaluate("Boolean(navigator.serviceWorker?.controller)").catch(() => false)) {
|
|
19351
|
+
throw new Error(
|
|
19352
|
+
"A service worker controls this page, and the write guard cannot see its requests. Explore without --attach (service workers are blocked there)."
|
|
19353
|
+
);
|
|
19354
|
+
}
|
|
19355
|
+
const first = await readPage(page);
|
|
19356
|
+
const firstPlace = explorePlaceKey(first.url);
|
|
19357
|
+
if (!firstPlace || originOf(first.url) !== origin) {
|
|
19358
|
+
throw new Error(`The start URL led off the product, to ${first.url}`);
|
|
19359
|
+
}
|
|
19360
|
+
addSurface(first, firstPlace, null, [], 0);
|
|
19361
|
+
for (let i = 0; i < surfaces.length; i++) {
|
|
19362
|
+
const surface = surfaces[i];
|
|
19363
|
+
if (!await replay(surface.path)) {
|
|
19364
|
+
stats.errors++;
|
|
19365
|
+
surface.actions.push({
|
|
19366
|
+
role: "",
|
|
19367
|
+
name: "",
|
|
19368
|
+
occurrence: 0,
|
|
19369
|
+
outcome: { kind: "error", message: LOST }
|
|
19370
|
+
});
|
|
19371
|
+
continue;
|
|
19372
|
+
}
|
|
19373
|
+
const home = await readPage(page);
|
|
19374
|
+
const seen = new Set(home.candidates.map(candidateSig));
|
|
19375
|
+
const work = home.candidates.map((c) => ({ c, via: [] }));
|
|
19376
|
+
const homeList = home.candidates.map(candidateSig).join("\n");
|
|
19377
|
+
let pristine = true;
|
|
19378
|
+
for (let w = 0; w < work.length; w++) {
|
|
19379
|
+
const { c, via } = work[w];
|
|
19380
|
+
const action = {
|
|
19381
|
+
role: c.role,
|
|
19382
|
+
name: c.name,
|
|
19383
|
+
occurrence: c.occurrence,
|
|
19384
|
+
...c.href ? { href: c.href } : {},
|
|
19385
|
+
...via.length > 0 ? { via } : {},
|
|
19386
|
+
outcome: { kind: "same" }
|
|
19387
|
+
};
|
|
19388
|
+
surface.actions.push(action);
|
|
19389
|
+
const reason = skipReason(c, origin, deny);
|
|
19390
|
+
if (reason) {
|
|
19391
|
+
action.outcome = { kind: "skipped", reason };
|
|
19392
|
+
stats.skipped++;
|
|
19393
|
+
continue;
|
|
19394
|
+
}
|
|
19395
|
+
if (surface.depth >= budget.max_depth || stats.actions_tried >= budget.max_actions) {
|
|
19396
|
+
action.outcome = { kind: "skipped", reason: "budget" };
|
|
19397
|
+
stats.skipped++;
|
|
19398
|
+
stats.truncated = true;
|
|
19399
|
+
continue;
|
|
19400
|
+
}
|
|
19401
|
+
stats.actions_tried++;
|
|
19402
|
+
takeBlocked();
|
|
19403
|
+
const click = {
|
|
19404
|
+
click: { role: c.role, name: c.name, occurrence: c.occurrence }
|
|
19405
|
+
};
|
|
19406
|
+
let outcome;
|
|
19407
|
+
let revealed = 0;
|
|
19408
|
+
let left = false;
|
|
19409
|
+
let lost = false;
|
|
19410
|
+
try {
|
|
19411
|
+
if (!pristine || via.length > 0) {
|
|
19412
|
+
if (!await replay([...surface.path, ...via])) {
|
|
19413
|
+
throw new Error(LOST);
|
|
19414
|
+
}
|
|
19415
|
+
}
|
|
19416
|
+
pristine = false;
|
|
19417
|
+
const before = await readPage(page);
|
|
19418
|
+
await markDocument(page);
|
|
19419
|
+
if (c.newTab && c.href) {
|
|
19420
|
+
await goto(c.href);
|
|
19421
|
+
} else {
|
|
19422
|
+
if (!await clickCandidate(page, c)) {
|
|
19423
|
+
throw new Error("control is gone");
|
|
19424
|
+
}
|
|
19425
|
+
await settle(page, settleMs);
|
|
19426
|
+
}
|
|
19427
|
+
const after = await readPage(page);
|
|
19428
|
+
if (originOf(after.url) !== origin) {
|
|
19429
|
+
outcome = { kind: "external", url: after.url };
|
|
19430
|
+
left = true;
|
|
19431
|
+
} else {
|
|
19432
|
+
const place = explorePlaceKey(after.url) ?? before.url;
|
|
19433
|
+
const sig = signatureOf(after);
|
|
19434
|
+
const sameDoc = !(c.newTab && c.href) && await isSameDocument(page);
|
|
19435
|
+
const beforePlace = explorePlaceKey(before.url);
|
|
19436
|
+
let key = byPlaceView.get(`${place}
|
|
19437
|
+
${sig}`);
|
|
19438
|
+
let view = null;
|
|
19439
|
+
if (!key) {
|
|
19440
|
+
const inPage = sameDoc && place === beforePlace;
|
|
19441
|
+
view = inPage ? sig : null;
|
|
19442
|
+
key = exploreSurfaceKey(place, view);
|
|
19443
|
+
if (byKey.has(key)) byPlaceView.set(`${place}
|
|
19444
|
+
${sig}`, key);
|
|
19445
|
+
}
|
|
19446
|
+
if (key === surface.key) {
|
|
19447
|
+
outcome = { kind: "same" };
|
|
19448
|
+
const list2 = after.candidates.map(candidateSig).join("\n");
|
|
19449
|
+
for (const n of after.candidates) {
|
|
19450
|
+
const s = candidateSig(n);
|
|
19451
|
+
if (seen.has(s)) continue;
|
|
19452
|
+
seen.add(s);
|
|
19453
|
+
work.push({ c: n, via: [...via, click] });
|
|
19454
|
+
revealed++;
|
|
19455
|
+
}
|
|
19456
|
+
if (list2 === homeList) pristine = true;
|
|
19457
|
+
} else {
|
|
19458
|
+
outcome = { kind: "surface", surface: key };
|
|
19459
|
+
if (!byKey.has(key)) {
|
|
19460
|
+
if (stats.surfaces >= budget.max_surfaces) {
|
|
19461
|
+
stats.truncated = true;
|
|
19462
|
+
} else {
|
|
19463
|
+
const path30 = !sameDoc && view === null ? [{ goto: after.url }] : [...surface.path, ...via, click];
|
|
19464
|
+
addSurface(after, place, view, path30, surface.depth + 1);
|
|
19465
|
+
}
|
|
19466
|
+
}
|
|
19467
|
+
}
|
|
19468
|
+
}
|
|
19469
|
+
} catch (err) {
|
|
19470
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
19471
|
+
outcome = { kind: "error", message: msg.split("\n")[0].slice(0, 200) };
|
|
19472
|
+
stats.errors++;
|
|
19473
|
+
lost = msg === LOST;
|
|
19474
|
+
}
|
|
19475
|
+
action.outcome = outcome;
|
|
19476
|
+
if (revealed > 0) action.revealed = revealed;
|
|
19477
|
+
const writes = takeBlocked();
|
|
19478
|
+
if (writes.length > 0) {
|
|
19479
|
+
action.blocked_requests = [...new Set(writes)];
|
|
19480
|
+
stats.blocked_requests += writes.length;
|
|
19481
|
+
}
|
|
19482
|
+
if (left) pristine = false;
|
|
19483
|
+
if (lost) break;
|
|
19484
|
+
}
|
|
19485
|
+
}
|
|
19486
|
+
return { surfaces, stats };
|
|
19487
|
+
}
|
|
19488
|
+
var UNSAFE_NAME, UNSAFE_PATH, SAFE_METHODS, CLICK_TIMEOUT_MS, LOST, NAV_TIMEOUT_MS, READ_PAGE;
|
|
19489
|
+
var init_explore_crawl = __esm({
|
|
19490
|
+
"src/commands/explore-crawl.ts"() {
|
|
19491
|
+
"use strict";
|
|
19492
|
+
init_dist2();
|
|
19493
|
+
UNSAFE_NAME = new RegExp(
|
|
19494
|
+
`(?<![\\p{L}\\p{N}])(${[
|
|
19495
|
+
// English
|
|
19496
|
+
"delete",
|
|
19497
|
+
"remove",
|
|
19498
|
+
"destroy",
|
|
19499
|
+
"erase",
|
|
19500
|
+
"pay",
|
|
19501
|
+
"pay now",
|
|
19502
|
+
"purchase",
|
|
19503
|
+
"buy",
|
|
19504
|
+
"checkout",
|
|
19505
|
+
"place order",
|
|
19506
|
+
"order now",
|
|
19507
|
+
"send",
|
|
19508
|
+
"submit",
|
|
19509
|
+
"save",
|
|
19510
|
+
"confirm",
|
|
19511
|
+
"approve",
|
|
19512
|
+
"reject",
|
|
19513
|
+
"decline",
|
|
19514
|
+
"publish",
|
|
19515
|
+
"archive",
|
|
19516
|
+
"unsubscribe",
|
|
19517
|
+
"log ?out",
|
|
19518
|
+
"sign ?out",
|
|
19519
|
+
"reset",
|
|
19520
|
+
"transfer",
|
|
19521
|
+
"invite",
|
|
19522
|
+
"upload",
|
|
19523
|
+
"import",
|
|
19524
|
+
"deactivate",
|
|
19525
|
+
"close account",
|
|
19526
|
+
// Swedish
|
|
19527
|
+
"ta bort",
|
|
19528
|
+
"radera",
|
|
19529
|
+
"betala",
|
|
19530
|
+
"k\xF6p",
|
|
19531
|
+
"best\xE4ll",
|
|
19532
|
+
"skicka",
|
|
19533
|
+
"skicka in",
|
|
19534
|
+
"spara",
|
|
19535
|
+
"bekr\xE4fta",
|
|
19536
|
+
"godk\xE4nn",
|
|
19537
|
+
"avsl\xE5",
|
|
19538
|
+
"publicera",
|
|
19539
|
+
"arkivera",
|
|
19540
|
+
"logga ut",
|
|
19541
|
+
"\xE5terst\xE4ll",
|
|
19542
|
+
"\xF6verf\xF6r",
|
|
19543
|
+
"bjud in",
|
|
19544
|
+
"ladda upp"
|
|
19545
|
+
].join("|")})(?![\\p{L}\\p{N}])`,
|
|
19546
|
+
"iu"
|
|
19547
|
+
);
|
|
19548
|
+
UNSAFE_PATH = /(^|\/)(log-?out|sign-?out|logga-?ut|delete|remove|destroy|unsubscribe)(\/|$)/i;
|
|
19549
|
+
SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
19550
|
+
CLICK_TIMEOUT_MS = 3e3;
|
|
19551
|
+
LOST = "could not return to surface";
|
|
19552
|
+
NAV_TIMEOUT_MS = 15e3;
|
|
19553
|
+
READ_PAGE = String.raw`(() => {
|
|
19554
|
+
const SELECTOR = [
|
|
19555
|
+
"a[href]", "button", "summary",
|
|
19556
|
+
'input[type="button"]', 'input[type="submit"]',
|
|
19557
|
+
'input[type="reset"]', 'input[type="image"]',
|
|
19558
|
+
'[role="button"]', '[role="link"]', '[role="tab"]',
|
|
19559
|
+
'[role="menuitem"]', '[role="menuitemradio"]', '[role="menuitemcheckbox"]',
|
|
19560
|
+
].join(",");
|
|
19561
|
+
const clean = (s) => (s ?? "").replace(/\s+/g, " ").trim();
|
|
19562
|
+
const visible = (el) => {
|
|
19563
|
+
if (typeof el.checkVisibility === "function" &&
|
|
19564
|
+
!el.checkVisibility({ checkOpacity: true, checkVisibilityCSS: true })) {
|
|
19565
|
+
return false;
|
|
19566
|
+
}
|
|
19567
|
+
const r = el.getBoundingClientRect();
|
|
19568
|
+
return r.width > 0 && r.height > 0;
|
|
19569
|
+
};
|
|
19570
|
+
const textOf = (id) => clean(document.getElementById(id)?.innerText);
|
|
19571
|
+
const nameOf = (el) => {
|
|
19572
|
+
const label = clean(el.getAttribute("aria-label"));
|
|
19573
|
+
if (label) return label;
|
|
19574
|
+
const by = el.getAttribute("aria-labelledby");
|
|
19575
|
+
if (by) {
|
|
19576
|
+
const t = clean(by.split(/\s+/).map(textOf).join(" "));
|
|
19577
|
+
if (t) return t;
|
|
19578
|
+
}
|
|
19579
|
+
if (el instanceof HTMLInputElement) {
|
|
19580
|
+
return clean(el.value) || clean(el.alt) || clean(el.title);
|
|
19581
|
+
}
|
|
19582
|
+
const text = clean(el.innerText);
|
|
19583
|
+
if (text) return text.slice(0, 80);
|
|
19584
|
+
const img = el.querySelector("img[alt]");
|
|
19585
|
+
return clean(img?.getAttribute("alt")) || clean(el.title);
|
|
19586
|
+
};
|
|
19587
|
+
const roleOf = (el) =>
|
|
19588
|
+
el.getAttribute("role") || (el.tagName === "A" ? "link" : "button");
|
|
19589
|
+
|
|
19590
|
+
for (const el of document.querySelectorAll("[data-iamu-x]")) {
|
|
19591
|
+
el.removeAttribute("data-iamu-x");
|
|
19592
|
+
}
|
|
19593
|
+
const seen = new Map();
|
|
19594
|
+
const candidates = [];
|
|
19595
|
+
let index = 0;
|
|
19596
|
+
for (const el of document.querySelectorAll(SELECTOR)) {
|
|
19597
|
+
if (!visible(el)) continue;
|
|
19598
|
+
if (el.disabled || el.getAttribute("aria-disabled") === "true") continue;
|
|
19599
|
+
const role = roleOf(el);
|
|
19600
|
+
const name = nameOf(el);
|
|
19601
|
+
const sig = role + "\n" + name;
|
|
19602
|
+
const occurrence = seen.get(sig) ?? 0;
|
|
19603
|
+
seen.set(sig, occurrence + 1);
|
|
19604
|
+
let submit = false;
|
|
19605
|
+
if (el instanceof HTMLButtonElement) {
|
|
19606
|
+
// A button in a form submits unless it says type="button".
|
|
19607
|
+
submit = el.form
|
|
19608
|
+
? el.type === "submit" || el.type === "reset"
|
|
19609
|
+
: el.getAttribute("type") === "submit";
|
|
19610
|
+
} else if (el instanceof HTMLInputElement) {
|
|
19611
|
+
submit = ["submit", "reset", "image"].includes(el.type);
|
|
19612
|
+
}
|
|
19613
|
+
const anchor = el instanceof HTMLAnchorElement ? el : null;
|
|
19614
|
+
el.setAttribute("data-iamu-x", String(index));
|
|
19615
|
+
candidates.push({
|
|
19616
|
+
index, role, name, occurrence,
|
|
19617
|
+
href: anchor ? anchor.href : null,
|
|
19618
|
+
download: anchor ? anchor.hasAttribute("download") : false,
|
|
19619
|
+
newTab: anchor ? anchor.target === "_blank" : false,
|
|
19620
|
+
submit,
|
|
19621
|
+
});
|
|
19622
|
+
index++;
|
|
19623
|
+
}
|
|
19624
|
+
|
|
19625
|
+
const headings = [];
|
|
19626
|
+
for (const el of document.querySelectorAll(
|
|
19627
|
+
'h1, h2, [role="heading"][aria-level="1"], [role="heading"][aria-level="2"]',
|
|
19628
|
+
)) {
|
|
19629
|
+
if (!visible(el)) continue;
|
|
19630
|
+
const t = clean(el.innerText);
|
|
19631
|
+
if (t) headings.push(t.slice(0, 120));
|
|
19632
|
+
}
|
|
19633
|
+
|
|
19634
|
+
const fields = [];
|
|
19635
|
+
for (const f of document.querySelectorAll("input, select, textarea")) {
|
|
19636
|
+
if (["hidden", "submit", "button", "reset", "image"].includes(f.type)) continue;
|
|
19637
|
+
if (!visible(f)) continue;
|
|
19638
|
+
const forLabel = f.id
|
|
19639
|
+
? document.querySelector('label[for="' + CSS.escape(f.id) + '"]')
|
|
19640
|
+
: null;
|
|
19641
|
+
const name =
|
|
19642
|
+
clean(f.getAttribute("aria-label")) ||
|
|
19643
|
+
clean(forLabel?.innerText) ||
|
|
19644
|
+
clean(f.closest("label")?.innerText) ||
|
|
19645
|
+
clean(f.placeholder) ||
|
|
19646
|
+
clean(f.name);
|
|
19647
|
+
if (name && !fields.includes(name)) fields.push(name.slice(0, 80));
|
|
19648
|
+
if (fields.length >= 40) break;
|
|
19649
|
+
}
|
|
19650
|
+
|
|
19651
|
+
return { url: location.href, title: clean(document.title), headings, fields, candidates };
|
|
19652
|
+
})()`;
|
|
19653
|
+
}
|
|
19654
|
+
});
|
|
19655
|
+
|
|
19656
|
+
// src/commands/explore.ts
|
|
19657
|
+
var explore_exports = {};
|
|
19658
|
+
__export(explore_exports, {
|
|
19659
|
+
exploreCoverageCommand: () => exploreCoverageCommand,
|
|
19660
|
+
exploreDiffCommand: () => exploreDiffCommand,
|
|
19661
|
+
exploreList: () => exploreList,
|
|
19662
|
+
exploreRun: () => exploreRun
|
|
19663
|
+
});
|
|
19664
|
+
import { promises as fs27 } from "fs";
|
|
19665
|
+
import path25 from "path";
|
|
19666
|
+
import chalk26 from "chalk";
|
|
19667
|
+
function fail2(message, json2) {
|
|
19668
|
+
if (json2) console.error(JSON.stringify({ error: message }));
|
|
19669
|
+
else console.error(chalk26.red(` ${message}`));
|
|
19670
|
+
process.exitCode = 1;
|
|
19671
|
+
}
|
|
19672
|
+
function positiveInt(raw, fallback, flag) {
|
|
19673
|
+
if (raw === void 0) return fallback;
|
|
19674
|
+
const n = Number(raw);
|
|
19675
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
19676
|
+
throw new Error(`${flag} must be a positive whole number, got "${raw}"`);
|
|
19677
|
+
}
|
|
19678
|
+
return n;
|
|
19679
|
+
}
|
|
19680
|
+
async function testabilityTarget(projectRoot) {
|
|
19681
|
+
try {
|
|
19682
|
+
const raw = await fs27.readFile(
|
|
19683
|
+
path25.join(projectRoot, "iamu", "testability.md"),
|
|
19684
|
+
"utf8"
|
|
19685
|
+
);
|
|
19686
|
+
const { frontmatter } = parseFrontmatter(raw);
|
|
19687
|
+
return typeof frontmatter.target_url === "string" ? frontmatter.target_url : null;
|
|
19688
|
+
} catch {
|
|
19689
|
+
return null;
|
|
19690
|
+
}
|
|
19691
|
+
}
|
|
19692
|
+
async function openPage(opts) {
|
|
19693
|
+
const { chromium } = await import("playwright-core");
|
|
19694
|
+
const viewport = parseViewport(opts.viewport) ?? {
|
|
19695
|
+
width: 1280,
|
|
19696
|
+
height: 800
|
|
19697
|
+
};
|
|
19698
|
+
if (opts.attach) {
|
|
19699
|
+
const state = readBrowseState();
|
|
19700
|
+
if (!state) {
|
|
19701
|
+
throw new Error(
|
|
19702
|
+
"No browser to attach to \u2014 start one with `iamu browse start` and sign in first."
|
|
19703
|
+
);
|
|
19704
|
+
}
|
|
19705
|
+
const browser2 = await chromium.connectOverCDP(
|
|
19706
|
+
`http://127.0.0.1:${state.port}`
|
|
19707
|
+
);
|
|
19708
|
+
const context = browser2.contexts()[0] ?? await browser2.newContext();
|
|
19709
|
+
const page2 = await context.newPage();
|
|
19710
|
+
await page2.setViewportSize(viewport);
|
|
19711
|
+
return {
|
|
19712
|
+
page: page2,
|
|
19713
|
+
close: async () => {
|
|
19714
|
+
await page2.close().catch(() => {
|
|
19715
|
+
});
|
|
19716
|
+
await browser2.close().catch(() => {
|
|
19717
|
+
});
|
|
19718
|
+
}
|
|
19719
|
+
};
|
|
19720
|
+
}
|
|
19721
|
+
const chrome = findChrome();
|
|
19722
|
+
if (!chrome) {
|
|
19723
|
+
throw new Error(
|
|
19724
|
+
"No Chrome or Chromium found \u2014 install one or set IAMU_BROWSER."
|
|
19725
|
+
);
|
|
19726
|
+
}
|
|
19727
|
+
const browser = await chromium.launch({
|
|
19728
|
+
executablePath: chrome,
|
|
19729
|
+
headless: true,
|
|
19730
|
+
chromiumSandbox: !shouldDisableSandbox()
|
|
19731
|
+
});
|
|
19732
|
+
const page = await browser.newPage({ viewport, serviceWorkers: "block" });
|
|
19733
|
+
return { page, close: () => browser.close().catch(() => {
|
|
19734
|
+
}) };
|
|
19735
|
+
}
|
|
19736
|
+
async function exploreRun(url, opts) {
|
|
19737
|
+
const ctx = await resolveProject(opts);
|
|
19738
|
+
if (!ctx) return;
|
|
19739
|
+
const target = url ?? await testabilityTarget(ctx.projectRoot);
|
|
19740
|
+
if (!target) {
|
|
19741
|
+
fail2(
|
|
19742
|
+
"No URL to explore \u2014 pass one (iamu explore http://localhost:3000) or set target_url in iamu/testability.md.",
|
|
19743
|
+
opts.json
|
|
19744
|
+
);
|
|
19745
|
+
return;
|
|
19746
|
+
}
|
|
19747
|
+
let startUrl;
|
|
19748
|
+
let budget;
|
|
19749
|
+
let deny;
|
|
19750
|
+
try {
|
|
19751
|
+
startUrl = new URL(target).href;
|
|
19752
|
+
budget = {
|
|
19753
|
+
max_surfaces: positiveInt(
|
|
19754
|
+
opts.maxSurfaces,
|
|
19755
|
+
DEFAULTS.max_surfaces,
|
|
19756
|
+
"--max-surfaces"
|
|
19757
|
+
),
|
|
19758
|
+
max_actions: positiveInt(
|
|
19759
|
+
opts.maxActions,
|
|
19760
|
+
DEFAULTS.max_actions,
|
|
19761
|
+
"--max-actions"
|
|
19762
|
+
),
|
|
19763
|
+
max_depth: positiveInt(opts.maxDepth, DEFAULTS.max_depth, "--max-depth")
|
|
19764
|
+
};
|
|
19765
|
+
deny = (opts.deny ?? []).map((d) => new RegExp(d, "i"));
|
|
19766
|
+
} catch (err) {
|
|
19767
|
+
fail2(err.message, opts.json);
|
|
19768
|
+
return;
|
|
19769
|
+
}
|
|
19770
|
+
const quiet = opts.json || process.env.IAMU_QUIET === "1";
|
|
19771
|
+
if (!quiet) {
|
|
19772
|
+
console.error(chalk26.dim(` Exploring ${startUrl} (read-only)\u2026`));
|
|
19773
|
+
}
|
|
19774
|
+
const now = /* @__PURE__ */ new Date();
|
|
19775
|
+
let opened = null;
|
|
19776
|
+
let result;
|
|
19777
|
+
try {
|
|
19778
|
+
opened = await openPage(opts);
|
|
19779
|
+
result = await crawl(opened.page, {
|
|
19780
|
+
startUrl,
|
|
19781
|
+
budget,
|
|
19782
|
+
deny,
|
|
19783
|
+
log: quiet ? void 0 : (line) => console.error(chalk26.dim(line))
|
|
19784
|
+
});
|
|
19785
|
+
} catch (err) {
|
|
19786
|
+
fail2(err.message.split("\n")[0], opts.json);
|
|
19787
|
+
return;
|
|
19788
|
+
} finally {
|
|
19789
|
+
await opened?.close();
|
|
19790
|
+
}
|
|
19791
|
+
const build = detectBuild({ cwd: ctx.projectRoot });
|
|
19792
|
+
const map2 = {
|
|
19793
|
+
schema: 1,
|
|
19794
|
+
id: composeExploreId(now),
|
|
19795
|
+
created_at: now.toISOString(),
|
|
19796
|
+
target_url: startUrl,
|
|
19797
|
+
...build ? { build: { ...build } } : {},
|
|
19798
|
+
budget,
|
|
19799
|
+
stats: result.stats,
|
|
19800
|
+
surfaces: result.surfaces
|
|
19801
|
+
};
|
|
19802
|
+
const file = await writeExploreMap(ctx.projectRoot, map2);
|
|
19803
|
+
const rel = path25.relative(process.cwd(), file);
|
|
19804
|
+
if (opts.json) {
|
|
19805
|
+
console.log(JSON.stringify({ id: map2.id, path: rel, stats: map2.stats }));
|
|
19806
|
+
return;
|
|
19807
|
+
}
|
|
19808
|
+
const s = map2.stats;
|
|
19809
|
+
console.log(
|
|
19810
|
+
`
|
|
19811
|
+
${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` : ""}`
|
|
19812
|
+
);
|
|
19813
|
+
if (s.truncated) {
|
|
19814
|
+
console.log(
|
|
19815
|
+
chalk26.yellow(
|
|
19816
|
+
" Stopped by the budget before the product ran out \u2014 raise --max-surfaces / --max-actions / --max-depth to go further."
|
|
19817
|
+
)
|
|
19818
|
+
);
|
|
19819
|
+
}
|
|
19820
|
+
console.log(` Map: ${rel}`);
|
|
19821
|
+
console.log(chalk26.dim(" Next: iamu explore coverage"));
|
|
19822
|
+
}
|
|
19823
|
+
async function resolveMapId(projectRoot, id, offsetFromEnd = 1) {
|
|
19824
|
+
if (id && id !== "latest") return id;
|
|
19825
|
+
const ids = await listExploreMaps(projectRoot);
|
|
19826
|
+
return ids.at(-offsetFromEnd) ?? null;
|
|
19827
|
+
}
|
|
19828
|
+
async function exploreList(opts) {
|
|
19829
|
+
const ctx = await resolveProject(opts);
|
|
19830
|
+
if (!ctx) return;
|
|
19831
|
+
const rows = [];
|
|
19832
|
+
for (const id of await listExploreMaps(ctx.projectRoot)) {
|
|
19833
|
+
try {
|
|
19834
|
+
const m = await readExploreMap(ctx.projectRoot, id);
|
|
19835
|
+
rows.push({ id, target_url: m.target_url, surfaces: m.surfaces.length });
|
|
19836
|
+
} catch {
|
|
19837
|
+
}
|
|
19838
|
+
}
|
|
19839
|
+
if (opts.json) {
|
|
19840
|
+
console.log(JSON.stringify({ maps: rows }));
|
|
19841
|
+
return;
|
|
19842
|
+
}
|
|
19843
|
+
if (rows.length === 0) {
|
|
19844
|
+
console.log(" No maps yet. Run: iamu explore <url>");
|
|
19845
|
+
return;
|
|
19846
|
+
}
|
|
19847
|
+
for (const r of rows) {
|
|
19848
|
+
console.log(
|
|
19849
|
+
` ${r.id} ${r.surfaces} surfaces ${chalk26.dim(r.target_url)}`
|
|
19850
|
+
);
|
|
19851
|
+
}
|
|
19852
|
+
}
|
|
19853
|
+
async function exploreCoverageCommand(mapId, opts) {
|
|
19854
|
+
const ctx = await resolveProject(opts);
|
|
19855
|
+
if (!ctx) return;
|
|
19856
|
+
const id = await resolveMapId(ctx.projectRoot, mapId);
|
|
19857
|
+
if (!id) {
|
|
19858
|
+
fail2("No map yet. Run: iamu explore <url>", opts.json);
|
|
19859
|
+
return;
|
|
19860
|
+
}
|
|
19861
|
+
let map2;
|
|
19862
|
+
try {
|
|
19863
|
+
map2 = await readExploreMap(ctx.projectRoot, id);
|
|
19864
|
+
} catch (err) {
|
|
19865
|
+
fail2(err.message, opts.json);
|
|
19866
|
+
return;
|
|
19867
|
+
}
|
|
19868
|
+
const cov = exploreCoverage(map2, await readAllRunSteps(ctx.projectRoot));
|
|
19869
|
+
if (opts.json) {
|
|
19870
|
+
console.log(JSON.stringify(cov));
|
|
19871
|
+
return;
|
|
19872
|
+
}
|
|
19873
|
+
console.log(
|
|
19874
|
+
`
|
|
19875
|
+
Map ${id} (${map2.surfaces.length} surfaces) against ${cov.runs} runs, ${cov.steps} steps`
|
|
19876
|
+
);
|
|
19877
|
+
console.log(
|
|
19878
|
+
` ${chalk26.green(`${cov.reached} reached`)} \xB7 ${chalk26.yellow(`${cov.place_only} URL reached, view unclear`)} \xB7 ${chalk26.red(`${cov.unreached} never reached`)}`
|
|
19879
|
+
);
|
|
19880
|
+
const section = (title, rows, color) => {
|
|
19881
|
+
if (rows.length === 0) return;
|
|
19882
|
+
console.log(`
|
|
19883
|
+
${color(title)}`);
|
|
19884
|
+
for (const r of rows) {
|
|
19885
|
+
console.log(
|
|
19886
|
+
` ${r.key.padEnd(32)} ${r.label}${chalk26.dim(` (depth ${r.depth})`)}`
|
|
19887
|
+
);
|
|
19888
|
+
}
|
|
19889
|
+
};
|
|
19890
|
+
section(
|
|
19891
|
+
"Never reached by a run",
|
|
19892
|
+
cov.surfaces.filter((s) => s.status === "unreached"),
|
|
19893
|
+
chalk26.red
|
|
19894
|
+
);
|
|
19895
|
+
section(
|
|
19896
|
+
"URL reached, but no step names this view",
|
|
19897
|
+
cov.surfaces.filter((s) => s.status === "place_only"),
|
|
19898
|
+
chalk26.yellow
|
|
19899
|
+
);
|
|
19900
|
+
if (cov.run_only_places.length > 0) {
|
|
19901
|
+
console.log(`
|
|
19902
|
+
${chalk26.cyan("Walked by runs, not in the map")}`);
|
|
19903
|
+
for (const p of cov.run_only_places) {
|
|
19904
|
+
console.log(
|
|
19905
|
+
` ${p.place.padEnd(32)} ${p.steps} steps, ${p.runs.length} runs`
|
|
19906
|
+
);
|
|
19907
|
+
}
|
|
19908
|
+
}
|
|
19909
|
+
if (cov.runs === 0) {
|
|
19910
|
+
console.log(
|
|
19911
|
+
chalk26.dim(
|
|
19912
|
+
"\n No runs yet \u2014 every surface counts as never reached. Walk a scenario first (iamu-run)."
|
|
19913
|
+
)
|
|
19914
|
+
);
|
|
19915
|
+
}
|
|
19916
|
+
}
|
|
19917
|
+
async function exploreDiffCommand(a, b, opts) {
|
|
19918
|
+
const ctx = await resolveProject(opts);
|
|
19919
|
+
if (!ctx) return;
|
|
19920
|
+
const idA = a ?? await resolveMapId(ctx.projectRoot, void 0, 2);
|
|
19921
|
+
const idB = b ?? await resolveMapId(ctx.projectRoot, void 0, 1);
|
|
19922
|
+
if (!idA || !idB || idA === idB) {
|
|
19923
|
+
fail2(
|
|
19924
|
+
"Need two maps to compare \u2014 explore again (iamu explore <url>), or name both maps.",
|
|
19925
|
+
opts.json
|
|
19926
|
+
);
|
|
19927
|
+
return;
|
|
19928
|
+
}
|
|
19929
|
+
let mapA;
|
|
19930
|
+
let mapB;
|
|
19931
|
+
try {
|
|
19932
|
+
mapA = await readExploreMap(ctx.projectRoot, idA);
|
|
19933
|
+
mapB = await readExploreMap(ctx.projectRoot, idB);
|
|
19934
|
+
} catch (err) {
|
|
19935
|
+
fail2(err.message, opts.json);
|
|
19936
|
+
return;
|
|
19937
|
+
}
|
|
19938
|
+
const d = diffExploreMaps(mapA, mapB);
|
|
19939
|
+
if (opts.json) {
|
|
19940
|
+
console.log(JSON.stringify(d));
|
|
19941
|
+
return;
|
|
19942
|
+
}
|
|
19943
|
+
const pct = Math.round(d.surface_overlap * 100);
|
|
19944
|
+
console.log(`
|
|
19945
|
+
${idA} \u2192 ${idB}`);
|
|
19946
|
+
console.log(
|
|
19947
|
+
` ${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)`
|
|
19948
|
+
);
|
|
19949
|
+
if (d.only_a.length > 0) {
|
|
19950
|
+
console.log(`
|
|
19951
|
+
${chalk26.red("Only in the first")}`);
|
|
19952
|
+
for (const s of d.only_a) console.log(` ${s.key.padEnd(32)} ${s.label}`);
|
|
19953
|
+
}
|
|
19954
|
+
if (d.only_b.length > 0) {
|
|
19955
|
+
console.log(`
|
|
19956
|
+
${chalk26.green("Only in the second")}`);
|
|
19957
|
+
for (const s of d.only_b) console.log(` ${s.key.padEnd(32)} ${s.label}`);
|
|
19958
|
+
}
|
|
19959
|
+
if (d.changed_actions.length > 0) {
|
|
19960
|
+
console.log(
|
|
19961
|
+
`
|
|
19962
|
+
${chalk26.yellow(`Controls differ on ${d.changed_actions.length} shared surfaces`)}`
|
|
19963
|
+
);
|
|
19964
|
+
for (const c of d.changed_actions) {
|
|
19965
|
+
console.log(` ${c.key} ${c.label}`);
|
|
19966
|
+
for (const x of c.only_a) console.log(chalk26.red(` - ${x}`));
|
|
19967
|
+
for (const x of c.only_b) console.log(chalk26.green(` + ${x}`));
|
|
19968
|
+
}
|
|
19969
|
+
}
|
|
19970
|
+
}
|
|
19971
|
+
var DEFAULTS;
|
|
19972
|
+
var init_explore = __esm({
|
|
19973
|
+
"src/commands/explore.ts"() {
|
|
19974
|
+
"use strict";
|
|
19975
|
+
init_dist2();
|
|
19976
|
+
init_build_info();
|
|
19977
|
+
init_browse();
|
|
19978
|
+
init_context4();
|
|
19979
|
+
init_explore_crawl();
|
|
19980
|
+
DEFAULTS = { max_surfaces: 40, max_actions: 300, max_depth: 6 };
|
|
19981
|
+
}
|
|
19982
|
+
});
|
|
19983
|
+
|
|
18709
19984
|
// src/commands/completion.ts
|
|
18710
19985
|
var completion_exports = {};
|
|
18711
19986
|
__export(completion_exports, {
|
|
@@ -18902,7 +20177,7 @@ __export(instructions_walk_exports, {
|
|
|
18902
20177
|
});
|
|
18903
20178
|
import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
|
|
18904
20179
|
import { join as join9 } from "path";
|
|
18905
|
-
import
|
|
20180
|
+
import chalk27 from "chalk";
|
|
18906
20181
|
function selfCommand() {
|
|
18907
20182
|
const q = (p) => /[\s"'$]/.test(p) ? JSON.stringify(p) : p;
|
|
18908
20183
|
return `${q(process.execPath)} ${q(process.argv[1] ?? "iamu")}`;
|
|
@@ -19229,35 +20504,35 @@ async function instructionsWalk(opts) {
|
|
|
19229
20504
|
return;
|
|
19230
20505
|
}
|
|
19231
20506
|
console.log();
|
|
19232
|
-
console.log(
|
|
19233
|
-
console.log(` ${
|
|
19234
|
-
console.log(` ${
|
|
19235
|
-
console.log(` ${
|
|
20507
|
+
console.log(chalk27.bold(" Walk instructions"));
|
|
20508
|
+
console.log(` ${chalk27.dim("scenario:")} ${sfm.slug}`);
|
|
20509
|
+
console.log(` ${chalk27.dim("persona:")} ${pfm.name} (${pfm.role})`);
|
|
20510
|
+
console.log(` ${chalk27.dim("outline:")} ${ofm.title}`);
|
|
19236
20511
|
if (sfm.state?.description || sfm.state?.approximation) {
|
|
19237
20512
|
console.log(
|
|
19238
|
-
` ${
|
|
20513
|
+
` ${chalk27.dim("state:")} ${sfm.state.description ? sfm.state.description : sfm.state.approximation ?? ""}`
|
|
19239
20514
|
);
|
|
19240
20515
|
}
|
|
19241
|
-
if (targetUrl) console.log(` ${
|
|
20516
|
+
if (targetUrl) console.log(` ${chalk27.dim("target:")} ${targetUrl}`);
|
|
19242
20517
|
console.log(
|
|
19243
|
-
` ${
|
|
20518
|
+
` ${chalk27.dim("access:")} ${projectAccess || personaAccess ? `${projectAccess ? "project" : ""}${projectAccess && personaAccess ? " + " : ""}${personaAccess ? "persona" : ""}` : "not recorded (walker would run as whoever is signed in)"}`
|
|
19244
20519
|
);
|
|
19245
20520
|
console.log(
|
|
19246
|
-
` ${
|
|
20521
|
+
` ${chalk27.dim("dismissed:")} ${dismissedFacts.length} findings to skip`
|
|
19247
20522
|
);
|
|
19248
20523
|
console.log(
|
|
19249
|
-
` ${
|
|
20524
|
+
` ${chalk27.dim("guidance:")} ${guidance.length} tester guidance rule${guidance.length === 1 ? "" : "s"}`
|
|
19250
20525
|
);
|
|
19251
20526
|
if (revisit.length > 0) {
|
|
19252
20527
|
console.log(
|
|
19253
|
-
` ${
|
|
20528
|
+
` ${chalk27.dim("re-check:")} ${revisit.length} place${revisit.length === 1 ? "" : "s"} where fixes await a re-run`
|
|
19254
20529
|
);
|
|
19255
20530
|
}
|
|
19256
20531
|
console.log(
|
|
19257
|
-
` ${
|
|
20532
|
+
` ${chalk27.dim("methodology:")} ${methodology ? "loaded" : "missing (run iamu update)"}`
|
|
19258
20533
|
);
|
|
19259
20534
|
console.log();
|
|
19260
|
-
console.log(
|
|
20535
|
+
console.log(chalk27.dim(" Use --json to get the full instruction bundle."));
|
|
19261
20536
|
console.log();
|
|
19262
20537
|
}
|
|
19263
20538
|
var SCREEN;
|
|
@@ -19283,8 +20558,8 @@ __export(new_finding_exports, {
|
|
|
19283
20558
|
});
|
|
19284
20559
|
import { randomUUID as randomUUID10 } from "crypto";
|
|
19285
20560
|
import { access, readFile as readFile6 } from "fs/promises";
|
|
19286
|
-
import
|
|
19287
|
-
import
|
|
20561
|
+
import path26 from "path";
|
|
20562
|
+
import chalk28 from "chalk";
|
|
19288
20563
|
async function newFinding(opts) {
|
|
19289
20564
|
const ctx = await resolveProject(opts);
|
|
19290
20565
|
if (!ctx) return;
|
|
@@ -19350,7 +20625,7 @@ async function newFinding(opts) {
|
|
|
19350
20625
|
}
|
|
19351
20626
|
try {
|
|
19352
20627
|
await access(
|
|
19353
|
-
|
|
20628
|
+
path26.join(runDirPath, "steps", `${String(n).padStart(3, "0")}.json`)
|
|
19354
20629
|
);
|
|
19355
20630
|
} catch {
|
|
19356
20631
|
commandError(
|
|
@@ -19365,7 +20640,7 @@ async function newFinding(opts) {
|
|
|
19365
20640
|
if (!scenarioSlug) {
|
|
19366
20641
|
try {
|
|
19367
20642
|
const manifestRaw = await readFile6(
|
|
19368
|
-
|
|
20643
|
+
path26.join(runDirPath, "walkthrough.json"),
|
|
19369
20644
|
"utf8"
|
|
19370
20645
|
);
|
|
19371
20646
|
const manifest = JSON.parse(manifestRaw);
|
|
@@ -19454,10 +20729,10 @@ async function newFinding(opts) {
|
|
|
19454
20729
|
return;
|
|
19455
20730
|
}
|
|
19456
20731
|
console.log();
|
|
19457
|
-
console.log(` ${symbols.success} Created finding: ${
|
|
19458
|
-
console.log(` ${
|
|
19459
|
-
console.log(` ${
|
|
19460
|
-
for (const w of warnings) console.log(` ${
|
|
20732
|
+
console.log(` ${symbols.success} Created finding: ${chalk28.cyan(slug)}`);
|
|
20733
|
+
console.log(` ${chalk28.dim("path:")} ${record.path}`);
|
|
20734
|
+
console.log(` ${chalk28.dim("id:")} ${id}`);
|
|
20735
|
+
for (const w of warnings) console.log(` ${chalk28.yellow("!")} ${w}`);
|
|
19461
20736
|
console.log();
|
|
19462
20737
|
} catch (err) {
|
|
19463
20738
|
commandError(
|
|
@@ -19483,7 +20758,7 @@ __export(view_results_exports, {
|
|
|
19483
20758
|
promptViewResults: () => promptViewResults
|
|
19484
20759
|
});
|
|
19485
20760
|
import { isCancel as isCancel4, select as select3 } from "@clack/prompts";
|
|
19486
|
-
import
|
|
20761
|
+
import chalk29 from "chalk";
|
|
19487
20762
|
function launchCloudDashboard(config, runId) {
|
|
19488
20763
|
const cloudRunId = runId ? toCloudRunUuid(runId) : void 0;
|
|
19489
20764
|
const url = getCloudDashboardUrl(
|
|
@@ -19492,7 +20767,7 @@ function launchCloudDashboard(config, runId) {
|
|
|
19492
20767
|
cloudRunId
|
|
19493
20768
|
);
|
|
19494
20769
|
console.log();
|
|
19495
|
-
console.log(` ${symbols.success} Opening Iamu Cloud: ${
|
|
20770
|
+
console.log(` ${symbols.success} Opening Iamu Cloud: ${chalk29.cyan(url)}
|
|
19496
20771
|
`);
|
|
19497
20772
|
openBrowser(url);
|
|
19498
20773
|
}
|
|
@@ -19560,9 +20835,9 @@ __export(evaluate_exports, {
|
|
|
19560
20835
|
evaluate: () => evaluate,
|
|
19561
20836
|
evaluateRecord: () => evaluateRecord
|
|
19562
20837
|
});
|
|
19563
|
-
import { promises as
|
|
19564
|
-
import
|
|
19565
|
-
import
|
|
20838
|
+
import { promises as fs28 } from "fs";
|
|
20839
|
+
import path27 from "path";
|
|
20840
|
+
import chalk30 from "chalk";
|
|
19566
20841
|
import { z as z25 } from "zod";
|
|
19567
20842
|
function formatIssues(error) {
|
|
19568
20843
|
return error.issues.map(
|
|
@@ -19592,10 +20867,10 @@ async function evaluate(runId, opts) {
|
|
|
19592
20867
|
const resolved = await resolveProjectAndRunDir(runId, opts);
|
|
19593
20868
|
if (!resolved) return;
|
|
19594
20869
|
const { projectRoot, runDirPath } = resolved;
|
|
19595
|
-
const canonicalPath =
|
|
20870
|
+
const canonicalPath = path27.join(runDirPath, "canonical.json");
|
|
19596
20871
|
let canonical;
|
|
19597
20872
|
try {
|
|
19598
|
-
const raw = await
|
|
20873
|
+
const raw = await fs28.readFile(canonicalPath, "utf8");
|
|
19599
20874
|
canonical = JSON.parse(raw);
|
|
19600
20875
|
} catch {
|
|
19601
20876
|
commandError(
|
|
@@ -19651,15 +20926,15 @@ async function evaluate(runId, opts) {
|
|
|
19651
20926
|
)
|
|
19652
20927
|
};
|
|
19653
20928
|
const prompt = composeJudgePrompt(input);
|
|
19654
|
-
const promptFilePath = opts?.promptFile ??
|
|
19655
|
-
await
|
|
20929
|
+
const promptFilePath = opts?.promptFile ?? path27.join(runDirPath, "judge_prompt.md");
|
|
20930
|
+
await fs28.writeFile(promptFilePath, prompt, "utf8");
|
|
19656
20931
|
if (opts?.json) {
|
|
19657
20932
|
console.log(
|
|
19658
20933
|
JSON.stringify(
|
|
19659
20934
|
{
|
|
19660
20935
|
...input,
|
|
19661
|
-
prompt_file:
|
|
19662
|
-
prompt_file_abs:
|
|
20936
|
+
prompt_file: path27.relative(projectRoot, promptFilePath),
|
|
20937
|
+
prompt_file_abs: path27.resolve(promptFilePath),
|
|
19663
20938
|
judge_prompt: prompt
|
|
19664
20939
|
},
|
|
19665
20940
|
null,
|
|
@@ -19674,10 +20949,10 @@ async function evaluateRecord(runId, opts) {
|
|
|
19674
20949
|
const resolved = await resolveProjectAndRunDir(runId, opts);
|
|
19675
20950
|
if (!resolved) return;
|
|
19676
20951
|
const { projectRoot, runDirPath } = resolved;
|
|
19677
|
-
const canonicalPath =
|
|
20952
|
+
const canonicalPath = path27.join(runDirPath, "canonical.json");
|
|
19678
20953
|
let canonical;
|
|
19679
20954
|
try {
|
|
19680
|
-
const raw = await
|
|
20955
|
+
const raw = await fs28.readFile(canonicalPath, "utf8");
|
|
19681
20956
|
canonical = JSON.parse(raw);
|
|
19682
20957
|
} catch {
|
|
19683
20958
|
commandError(`No canonical.json for run: ${runId}`, opts);
|
|
@@ -19685,7 +20960,7 @@ async function evaluateRecord(runId, opts) {
|
|
|
19685
20960
|
}
|
|
19686
20961
|
let parsedFile;
|
|
19687
20962
|
try {
|
|
19688
|
-
const raw = await
|
|
20963
|
+
const raw = await fs28.readFile(opts.file, "utf8");
|
|
19689
20964
|
parsedFile = JSON.parse(raw);
|
|
19690
20965
|
} catch {
|
|
19691
20966
|
commandError(`Cannot read verdicts file: ${opts.file}`, opts);
|
|
@@ -19742,8 +21017,8 @@ ${formatIssues(fileResult.error)}`,
|
|
|
19742
21017
|
overall: inputVerdicts.overall
|
|
19743
21018
|
});
|
|
19744
21019
|
const reportContent = renderCanonicalMarkdown(canonicalForReport);
|
|
19745
|
-
const verdictsPath =
|
|
19746
|
-
const reportPath =
|
|
21020
|
+
const verdictsPath = path27.join(runDirPath, "verdicts.json");
|
|
21021
|
+
const reportPath = path27.join(runDirPath, "report.md");
|
|
19747
21022
|
await Promise.all([
|
|
19748
21023
|
atomicWriteFile(
|
|
19749
21024
|
verdictsPath,
|
|
@@ -19763,8 +21038,8 @@ ${formatIssues(fileResult.error)}`,
|
|
|
19763
21038
|
JSON.stringify(
|
|
19764
21039
|
{
|
|
19765
21040
|
runId,
|
|
19766
|
-
verdictsPath:
|
|
19767
|
-
reportPath:
|
|
21041
|
+
verdictsPath: path27.relative(projectRoot, verdictsPath),
|
|
21042
|
+
reportPath: path27.relative(projectRoot, reportPath),
|
|
19768
21043
|
verdictsCount: validatedVerdicts.length,
|
|
19769
21044
|
linkedCount: linked.length,
|
|
19770
21045
|
...cloudInfo ? { cloud: cloudInfo } : {}
|
|
@@ -19777,18 +21052,18 @@ ${formatIssues(fileResult.error)}`,
|
|
|
19777
21052
|
}
|
|
19778
21053
|
console.log();
|
|
19779
21054
|
console.log(
|
|
19780
|
-
` ${symbols.success} Recorded ${validatedVerdicts.length} verdicts for run ${
|
|
21055
|
+
` ${symbols.success} Recorded ${validatedVerdicts.length} verdicts for run ${chalk30.cyan(runId)}`
|
|
19781
21056
|
);
|
|
19782
21057
|
if (linked.length > 0) {
|
|
19783
21058
|
console.log(
|
|
19784
|
-
` ${
|
|
21059
|
+
` ${chalk30.dim("linked:")} ${linked.length} finding${linked.length === 1 ? "" : "s"} to the human test${linked.length === 1 ? "" : "s"} citing ${linked.length === 1 ? "it" : "them"}`
|
|
19785
21060
|
);
|
|
19786
21061
|
}
|
|
19787
21062
|
console.log(
|
|
19788
|
-
` ${
|
|
21063
|
+
` ${chalk30.dim("verdicts:")} ${path27.relative(projectRoot, verdictsPath)}`
|
|
19789
21064
|
);
|
|
19790
21065
|
console.log(
|
|
19791
|
-
` ${
|
|
21066
|
+
` ${chalk30.dim("report:")} ${path27.relative(projectRoot, reportPath)}`
|
|
19792
21067
|
);
|
|
19793
21068
|
printCloudRunInfo(cloudInfo);
|
|
19794
21069
|
await promptViewResults(projectRoot, { ...opts, runId });
|
|
@@ -19852,9 +21127,9 @@ __export(complete_exports, {
|
|
|
19852
21127
|
screenshotSizeWarning: () => screenshotSizeWarning,
|
|
19853
21128
|
validateCanonicalWalkthrough: () => validateCanonicalWalkthrough
|
|
19854
21129
|
});
|
|
19855
|
-
import { promises as
|
|
19856
|
-
import
|
|
19857
|
-
import
|
|
21130
|
+
import { promises as fs29 } from "fs";
|
|
21131
|
+
import path28 from "path";
|
|
21132
|
+
import chalk31 from "chalk";
|
|
19858
21133
|
async function complete(runId, opts) {
|
|
19859
21134
|
const run = await loadRun(runId, opts);
|
|
19860
21135
|
if (!run) return;
|
|
@@ -19864,7 +21139,7 @@ async function complete(runId, opts) {
|
|
|
19864
21139
|
}
|
|
19865
21140
|
if (!opts?.json) {
|
|
19866
21141
|
console.log();
|
|
19867
|
-
console.log(
|
|
21142
|
+
console.log(chalk31.bold(" Assembling canonical JSON\u2026"));
|
|
19868
21143
|
}
|
|
19869
21144
|
const provenanceAudit = auditStepProvenance(
|
|
19870
21145
|
await readStepsForRun(run.projectRoot, runId)
|
|
@@ -19877,8 +21152,8 @@ async function complete(runId, opts) {
|
|
|
19877
21152
|
const walkerSummary = await readWalkerSummary(run.runDirPath);
|
|
19878
21153
|
let judgeReported;
|
|
19879
21154
|
try {
|
|
19880
|
-
const verdictsRaw = await
|
|
19881
|
-
|
|
21155
|
+
const verdictsRaw = await fs29.readFile(
|
|
21156
|
+
path28.join(run.runDirPath, "verdicts.json"),
|
|
19882
21157
|
"utf8"
|
|
19883
21158
|
);
|
|
19884
21159
|
const parsed = JSON.parse(verdictsRaw);
|
|
@@ -19930,16 +21205,16 @@ async function loadRun(runId, opts) {
|
|
|
19930
21205
|
if (!resolved) return null;
|
|
19931
21206
|
const { projectRoot, runDirPath } = resolved;
|
|
19932
21207
|
try {
|
|
19933
|
-
await
|
|
21208
|
+
await fs29.access(runDirPath);
|
|
19934
21209
|
} catch {
|
|
19935
21210
|
commandError(`Run not found: ${runId}`, opts);
|
|
19936
21211
|
return null;
|
|
19937
21212
|
}
|
|
19938
|
-
const manifestPath =
|
|
21213
|
+
const manifestPath = path28.join(runDirPath, "walkthrough.json");
|
|
19939
21214
|
let manifest;
|
|
19940
21215
|
try {
|
|
19941
21216
|
manifest = JSON.parse(
|
|
19942
|
-
await
|
|
21217
|
+
await fs29.readFile(manifestPath, "utf8")
|
|
19943
21218
|
);
|
|
19944
21219
|
} catch {
|
|
19945
21220
|
commandError(`No walkthrough.json found for run: ${runId}`, opts);
|
|
@@ -19951,8 +21226,8 @@ async function reportAlreadyCompleted(run, opts) {
|
|
|
19951
21226
|
const { runId, manifest } = run;
|
|
19952
21227
|
if (opts?.json) {
|
|
19953
21228
|
try {
|
|
19954
|
-
const raw = await
|
|
19955
|
-
|
|
21229
|
+
const raw = await fs29.readFile(
|
|
21230
|
+
path28.join(run.runDirPath, "canonical.json"),
|
|
19956
21231
|
"utf8"
|
|
19957
21232
|
);
|
|
19958
21233
|
console.log(raw.trim());
|
|
@@ -19972,18 +21247,18 @@ async function reportAlreadyCompleted(run, opts) {
|
|
|
19972
21247
|
return;
|
|
19973
21248
|
}
|
|
19974
21249
|
console.log();
|
|
19975
|
-
console.log(
|
|
19976
|
-
console.log(` status ${
|
|
21250
|
+
console.log(chalk31.bold(" Run: ") + chalk31.cyan(runId));
|
|
21251
|
+
console.log(` status ${chalk31.green("completed")}`);
|
|
19977
21252
|
if (manifest.completed_at) {
|
|
19978
|
-
console.log(` completed ${
|
|
21253
|
+
console.log(` completed ${chalk31.dim(manifest.completed_at)}`);
|
|
19979
21254
|
}
|
|
19980
21255
|
const viewUrl = await pushedRunUrl(run.projectRoot, runId);
|
|
19981
21256
|
if (viewUrl) {
|
|
19982
|
-
console.log(` view ${
|
|
21257
|
+
console.log(` view ${chalk31.underline(viewUrl)}`);
|
|
19983
21258
|
}
|
|
19984
21259
|
console.log();
|
|
19985
21260
|
console.log(
|
|
19986
|
-
|
|
21261
|
+
chalk31.dim(
|
|
19987
21262
|
" Already completed \u2014 nothing to do. Use --force to reassemble the report."
|
|
19988
21263
|
)
|
|
19989
21264
|
);
|
|
@@ -19999,13 +21274,13 @@ function reportStrictProvenanceFailure(audit, opts) {
|
|
|
19999
21274
|
}
|
|
20000
21275
|
console.error();
|
|
20001
21276
|
console.error(
|
|
20002
|
-
|
|
21277
|
+
chalk31.red(` ${symbols.error} Strict provenance audit failed:`)
|
|
20003
21278
|
);
|
|
20004
21279
|
for (const w of audit.warnings) {
|
|
20005
|
-
console.error(
|
|
21280
|
+
console.error(chalk31.red(` \u2022 ${w}`));
|
|
20006
21281
|
}
|
|
20007
21282
|
console.error(
|
|
20008
|
-
|
|
21283
|
+
chalk31.dim(
|
|
20009
21284
|
" Run was flagged as synthetic / scripted. Remove --strict-provenance to complete anyway."
|
|
20010
21285
|
)
|
|
20011
21286
|
);
|
|
@@ -20046,41 +21321,41 @@ function completeJsonPayload(canonical, extras) {
|
|
|
20046
21321
|
function printCompletionSummary(run, completedAt, assembled, extras, provenanceAudit) {
|
|
20047
21322
|
const { runId, projectRoot, manifest } = run;
|
|
20048
21323
|
const { canonical } = assembled;
|
|
20049
|
-
const count = (n) => n === 0 ?
|
|
20050
|
-
console.log(
|
|
20051
|
-
console.log(` scenario ${
|
|
20052
|
-
console.log(` url ${
|
|
21324
|
+
const count = (n) => n === 0 ? chalk31.dim("0") : chalk31.cyan(String(n));
|
|
21325
|
+
console.log(chalk31.bold(" Run: ") + chalk31.cyan(runId));
|
|
21326
|
+
console.log(` scenario ${chalk31.dim(manifest.scenario_slug)}`);
|
|
21327
|
+
console.log(` url ${chalk31.dim(manifest.target_url)}`);
|
|
20053
21328
|
if (manifest.environment) {
|
|
20054
|
-
console.log(` env ${
|
|
21329
|
+
console.log(` env ${chalk31.dim(manifest.environment)}`);
|
|
20055
21330
|
}
|
|
20056
21331
|
if (manifest.build) {
|
|
20057
|
-
console.log(` build ${
|
|
21332
|
+
console.log(` build ${chalk31.dim(formatBuild(manifest.build))}`);
|
|
20058
21333
|
}
|
|
20059
21334
|
if (manifest.viewport) {
|
|
20060
|
-
console.log(` device ${
|
|
21335
|
+
console.log(` device ${chalk31.dim(formatViewport(manifest.viewport))}`);
|
|
20061
21336
|
}
|
|
20062
|
-
console.log(` status ${
|
|
21337
|
+
console.log(` status ${chalk31.green("completed")}`);
|
|
20063
21338
|
console.log(` findings ${count(canonical.findings.length)}`);
|
|
20064
21339
|
console.log(` steps ${count(canonical.steps.length)}`);
|
|
20065
|
-
console.log(` started ${
|
|
20066
|
-
console.log(` completed ${
|
|
21340
|
+
console.log(` started ${chalk31.dim(manifest.created_at)}`);
|
|
21341
|
+
console.log(` completed ${chalk31.dim(completedAt)}`);
|
|
20067
21342
|
const models = manifest.models;
|
|
20068
21343
|
if (models?.velocity_sec_per_step != null) {
|
|
20069
21344
|
console.log(
|
|
20070
|
-
` velocity ${
|
|
21345
|
+
` velocity ${chalk31.cyan(`~${models.velocity_sec_per_step}s/step`)} ${chalk31.dim(`(${models.total_duration_sec}s total)`)}`
|
|
20071
21346
|
);
|
|
20072
21347
|
}
|
|
20073
21348
|
if (models && (models.walker || models.judge || models.matcher)) {
|
|
20074
21349
|
const w = models.walker_reported ? `${models.walker} (${models.walker_reported})` : models.walker ?? "fast";
|
|
20075
21350
|
const j = models.judge_reported ? `${models.judge} (${models.judge_reported})` : models.judge ?? "thorough";
|
|
20076
|
-
console.log(` models ${
|
|
21351
|
+
console.log(` models ${chalk31.dim(`walker: ${w} \xB7 judge: ${j}`)}`);
|
|
20077
21352
|
}
|
|
20078
21353
|
console.log();
|
|
20079
21354
|
console.log(
|
|
20080
|
-
` ${
|
|
21355
|
+
` ${chalk31.dim("canonical:")} ${chalk31.dim(path28.relative(projectRoot, assembled.canonicalPath))}`
|
|
20081
21356
|
);
|
|
20082
21357
|
console.log(
|
|
20083
|
-
` ${
|
|
21358
|
+
` ${chalk31.dim("report:")} ${chalk31.dim(path28.relative(projectRoot, assembled.reportPath))}`
|
|
20084
21359
|
);
|
|
20085
21360
|
if (extras.viewportWarning) {
|
|
20086
21361
|
console.log();
|
|
@@ -20088,7 +21363,7 @@ function printCompletionSummary(run, completedAt, assembled, extras, provenanceA
|
|
|
20088
21363
|
}
|
|
20089
21364
|
if (extras.codeWarning) {
|
|
20090
21365
|
console.log();
|
|
20091
|
-
console.log(` ${
|
|
21366
|
+
console.log(` ${chalk31.yellow("!")} ${extras.codeWarning}`);
|
|
20092
21367
|
}
|
|
20093
21368
|
if (extras.revisit) printRevisit(extras.revisit);
|
|
20094
21369
|
if (assembled.verification) printVerification(assembled.verification);
|
|
@@ -20100,13 +21375,13 @@ function printProvenanceWarnings(audit) {
|
|
|
20100
21375
|
if (audit.warnings.length === 0) return;
|
|
20101
21376
|
console.log();
|
|
20102
21377
|
console.log(
|
|
20103
|
-
` ${symbols.warning} ${
|
|
21378
|
+
` ${symbols.warning} ${chalk31.yellow("Step provenance audit flagged suspicious timing:")}`
|
|
20104
21379
|
);
|
|
20105
21380
|
for (const w of audit.warnings) {
|
|
20106
|
-
console.log(` ${
|
|
21381
|
+
console.log(` ${chalk31.dim("\u2022")} ${w}`);
|
|
20107
21382
|
}
|
|
20108
21383
|
console.log(
|
|
20109
|
-
|
|
21384
|
+
chalk31.dim(
|
|
20110
21385
|
" Run appears to be programmatically batch-generated rather than an interactive walkthrough."
|
|
20111
21386
|
)
|
|
20112
21387
|
);
|
|
@@ -20138,17 +21413,17 @@ function screenshotFitsViewport(width, expected) {
|
|
|
20138
21413
|
}
|
|
20139
21414
|
async function screenshotSizeWarning(runDirPath, viewport) {
|
|
20140
21415
|
if (!viewport) return null;
|
|
20141
|
-
const dir =
|
|
21416
|
+
const dir = path28.join(runDirPath, "screenshots");
|
|
20142
21417
|
let first;
|
|
20143
21418
|
try {
|
|
20144
|
-
first = (await
|
|
21419
|
+
first = (await fs29.readdir(dir)).filter((f) => f.toLowerCase().endsWith(".png")).sort((a, b) => a.localeCompare(b, void 0, { numeric: true }))[0];
|
|
20145
21420
|
} catch {
|
|
20146
21421
|
return null;
|
|
20147
21422
|
}
|
|
20148
21423
|
if (!first) return null;
|
|
20149
21424
|
let size = null;
|
|
20150
21425
|
try {
|
|
20151
|
-
const handle = await
|
|
21426
|
+
const handle = await fs29.open(path28.join(dir, first), "r");
|
|
20152
21427
|
try {
|
|
20153
21428
|
const head = Buffer.alloc(24);
|
|
20154
21429
|
const { bytesRead } = await handle.read(head, 0, 24, 0);
|
|
@@ -20165,7 +21440,7 @@ async function screenshotSizeWarning(runDirPath, viewport) {
|
|
|
20165
21440
|
async function readWalkerSummary(runDirPath) {
|
|
20166
21441
|
try {
|
|
20167
21442
|
const data = JSON.parse(
|
|
20168
|
-
await
|
|
21443
|
+
await fs29.readFile(path28.join(runDirPath, "summary.json"), "utf8")
|
|
20169
21444
|
);
|
|
20170
21445
|
const strings = (v) => Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.trim() !== "") : void 0;
|
|
20171
21446
|
const model = typeof data.model === "string" && data.model.trim() !== "" ? data.model.trim() : void 0;
|
|
@@ -20274,7 +21549,7 @@ function validateCanonicalWalkthrough(canonical) {
|
|
|
20274
21549
|
async function applyRecordedVerdicts(canonical, runDirPath) {
|
|
20275
21550
|
try {
|
|
20276
21551
|
const recorded = JSON.parse(
|
|
20277
|
-
await
|
|
21552
|
+
await fs29.readFile(path28.join(runDirPath, "verdicts.json"), "utf8")
|
|
20278
21553
|
);
|
|
20279
21554
|
if (Array.isArray(recorded.verdicts)) {
|
|
20280
21555
|
return applyVerdicts(canonical, recorded);
|
|
@@ -20284,7 +21559,7 @@ async function applyRecordedVerdicts(canonical, runDirPath) {
|
|
|
20284
21559
|
return canonical;
|
|
20285
21560
|
}
|
|
20286
21561
|
async function persistCanonical(runDirPath, canonical) {
|
|
20287
|
-
const canonicalPath =
|
|
21562
|
+
const canonicalPath = path28.join(runDirPath, "canonical.json");
|
|
20288
21563
|
await atomicWriteFile(
|
|
20289
21564
|
canonicalPath,
|
|
20290
21565
|
`${JSON.stringify(canonical, null, 2)}
|
|
@@ -20293,7 +21568,7 @@ async function persistCanonical(runDirPath, canonical) {
|
|
|
20293
21568
|
return canonicalPath;
|
|
20294
21569
|
}
|
|
20295
21570
|
async function persistReport(runDirPath, canonical) {
|
|
20296
|
-
const reportPath =
|
|
21571
|
+
const reportPath = path28.join(runDirPath, "report.md");
|
|
20297
21572
|
await atomicWriteFile(reportPath, renderCanonicalMarkdown(canonical));
|
|
20298
21573
|
return reportPath;
|
|
20299
21574
|
}
|
|
@@ -20393,17 +21668,17 @@ function placeLabel(p) {
|
|
|
20393
21668
|
function printRevisit(r) {
|
|
20394
21669
|
const missed = r.places.filter((p) => !p.reached);
|
|
20395
21670
|
console.log();
|
|
20396
|
-
const head = ` ${
|
|
21671
|
+
const head = ` ${chalk31.bold("Re-check places:")} reached ${r.reached} of ${r.requested}`;
|
|
20397
21672
|
if (missed.length === 0) {
|
|
20398
|
-
console.log(`${head} ${
|
|
21673
|
+
console.log(`${head} ${chalk31.green("\u2713")}`);
|
|
20399
21674
|
return;
|
|
20400
21675
|
}
|
|
20401
21676
|
console.log(`${head} \u2014 not reached:`);
|
|
20402
21677
|
for (const p of missed) {
|
|
20403
|
-
console.log(` ${
|
|
21678
|
+
console.log(` ${chalk31.yellow("\u25CB")} ${placeLabel(p)}`);
|
|
20404
21679
|
}
|
|
20405
21680
|
console.log(
|
|
20406
|
-
|
|
21681
|
+
chalk31.dim(
|
|
20407
21682
|
" Fixes there stay awaiting a re-run until a walk gets there."
|
|
20408
21683
|
)
|
|
20409
21684
|
);
|
|
@@ -20411,7 +21686,7 @@ function printRevisit(r) {
|
|
|
20411
21686
|
function printVerification(verification) {
|
|
20412
21687
|
console.log();
|
|
20413
21688
|
console.log(
|
|
20414
|
-
|
|
21689
|
+
chalk31.bold(" Since this scenario's last run") + chalk31.dim(` (${verification.baseline_run_id.slice(0, 30)}\u2026)`) + chalk31.bold(":")
|
|
20415
21690
|
);
|
|
20416
21691
|
if (verification.resolved.length > 0) {
|
|
20417
21692
|
console.log(
|
|
@@ -20420,7 +21695,7 @@ function printVerification(verification) {
|
|
|
20420
21695
|
}
|
|
20421
21696
|
if (verification.not_observed.length > 0) {
|
|
20422
21697
|
console.log(
|
|
20423
|
-
` ${symbols.pending} ${verification.not_observed.length} not seen this run ${
|
|
21698
|
+
` ${symbols.pending} ${verification.not_observed.length} not seen this run ${chalk31.dim("(not evidence of a fix)")}`
|
|
20424
21699
|
);
|
|
20425
21700
|
}
|
|
20426
21701
|
if (verification.persists.length > 0) {
|
|
@@ -20461,14 +21736,14 @@ __export(match_exports, {
|
|
|
20461
21736
|
match: () => match,
|
|
20462
21737
|
matchRecord: () => matchRecord
|
|
20463
21738
|
});
|
|
20464
|
-
import { promises as
|
|
20465
|
-
import
|
|
20466
|
-
import
|
|
21739
|
+
import { promises as fs30 } from "fs";
|
|
21740
|
+
import path29 from "path";
|
|
21741
|
+
import chalk32 from "chalk";
|
|
20467
21742
|
async function completedManifest2(runDirPath, runId, opts) {
|
|
20468
21743
|
let manifest;
|
|
20469
21744
|
try {
|
|
20470
21745
|
manifest = JSON.parse(
|
|
20471
|
-
await
|
|
21746
|
+
await fs30.readFile(path29.join(runDirPath, "walkthrough.json"), "utf8")
|
|
20472
21747
|
);
|
|
20473
21748
|
} catch {
|
|
20474
21749
|
commandError(`Run not found: ${runId}`, opts);
|
|
@@ -20497,12 +21772,12 @@ async function match(runId, opts) {
|
|
|
20497
21772
|
);
|
|
20498
21773
|
const needed = input.current.length > 0 && (input.prior.length > 0 || (input.guidance ?? []).length > 0);
|
|
20499
21774
|
const prompt = needed ? composeMatchPrompt(input) : null;
|
|
20500
|
-
const promptFilePath = opts?.promptFile ??
|
|
21775
|
+
const promptFilePath = opts?.promptFile ?? path29.join(runDirPath, "match_prompt.md");
|
|
20501
21776
|
if (prompt) {
|
|
20502
|
-
await
|
|
21777
|
+
await fs30.writeFile(promptFilePath, prompt, "utf8");
|
|
20503
21778
|
} else {
|
|
20504
21779
|
try {
|
|
20505
|
-
await
|
|
21780
|
+
await fs30.unlink(promptFilePath);
|
|
20506
21781
|
} catch {
|
|
20507
21782
|
}
|
|
20508
21783
|
}
|
|
@@ -20512,8 +21787,8 @@ async function match(runId, opts) {
|
|
|
20512
21787
|
{
|
|
20513
21788
|
...input,
|
|
20514
21789
|
needed,
|
|
20515
|
-
prompt_file: prompt ?
|
|
20516
|
-
prompt_file_abs: prompt ?
|
|
21790
|
+
prompt_file: prompt ? path29.relative(ctx.projectRoot, promptFilePath) : null,
|
|
21791
|
+
prompt_file_abs: prompt ? path29.resolve(promptFilePath) : null,
|
|
20517
21792
|
match_prompt: prompt
|
|
20518
21793
|
},
|
|
20519
21794
|
null,
|
|
@@ -20524,7 +21799,7 @@ async function match(runId, opts) {
|
|
|
20524
21799
|
}
|
|
20525
21800
|
if (!prompt) {
|
|
20526
21801
|
console.log(
|
|
20527
|
-
|
|
21802
|
+
chalk32.dim(
|
|
20528
21803
|
" No earlier findings or tester guidance for this scenario to match against \u2014 nothing to do."
|
|
20529
21804
|
)
|
|
20530
21805
|
);
|
|
@@ -20540,7 +21815,7 @@ async function matchRecord(runId, opts) {
|
|
|
20540
21815
|
if (!manifest) return;
|
|
20541
21816
|
let answer;
|
|
20542
21817
|
try {
|
|
20543
|
-
answer = JSON.parse(await
|
|
21818
|
+
answer = JSON.parse(await fs30.readFile(opts.file, "utf8"));
|
|
20544
21819
|
} catch {
|
|
20545
21820
|
commandError(`Cannot read matches file: ${opts.file}`, opts);
|
|
20546
21821
|
return;
|
|
@@ -20581,17 +21856,17 @@ async function matchRecord(runId, opts) {
|
|
|
20581
21856
|
}
|
|
20582
21857
|
console.log();
|
|
20583
21858
|
console.log(
|
|
20584
|
-
` ${
|
|
21859
|
+
` ${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`
|
|
20585
21860
|
);
|
|
20586
21861
|
if (result.suppressed > 0) {
|
|
20587
21862
|
console.log(
|
|
20588
|
-
` ${
|
|
21863
|
+
` ${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)`
|
|
20589
21864
|
);
|
|
20590
21865
|
}
|
|
20591
21866
|
if (verification) printVerification(verification);
|
|
20592
21867
|
console.log();
|
|
20593
21868
|
console.log(
|
|
20594
|
-
|
|
21869
|
+
chalk32.dim(" Run `iamu sync` to push the linked findings to the cloud.")
|
|
20595
21870
|
);
|
|
20596
21871
|
console.log();
|
|
20597
21872
|
}
|
|
@@ -20612,7 +21887,7 @@ __export(prune_exports, {
|
|
|
20612
21887
|
prune: () => prune
|
|
20613
21888
|
});
|
|
20614
21889
|
import { confirm, isCancel as isCancel5 } from "@clack/prompts";
|
|
20615
|
-
import
|
|
21890
|
+
import chalk33 from "chalk";
|
|
20616
21891
|
function formatBytes(bytes) {
|
|
20617
21892
|
if (bytes <= 0) return "0 B";
|
|
20618
21893
|
if (bytes < 1024) return `${bytes} B`;
|
|
@@ -20658,7 +21933,7 @@ async function prune(opts = {}) {
|
|
|
20658
21933
|
});
|
|
20659
21934
|
if (preview.prunedRuns.length === 0) {
|
|
20660
21935
|
console.log(
|
|
20661
|
-
|
|
21936
|
+
chalk33.dim(" No runs match the pruning criteria. Nothing to prune.")
|
|
20662
21937
|
);
|
|
20663
21938
|
return preview;
|
|
20664
21939
|
}
|
|
@@ -20667,7 +21942,7 @@ async function prune(opts = {}) {
|
|
|
20667
21942
|
message: `Prune ${mode} ${preview.prunedRuns.length} run(s) and reclaim ${formatBytes(preview.totalBytesReclaimed)}?`
|
|
20668
21943
|
});
|
|
20669
21944
|
if (isCancel5(confirmed) || !confirmed) {
|
|
20670
|
-
console.log(
|
|
21945
|
+
console.log(chalk33.yellow(" Pruning cancelled."));
|
|
20671
21946
|
return;
|
|
20672
21947
|
}
|
|
20673
21948
|
}
|
|
@@ -20678,14 +21953,14 @@ async function prune(opts = {}) {
|
|
|
20678
21953
|
}
|
|
20679
21954
|
if (result.dryRun) {
|
|
20680
21955
|
console.log(
|
|
20681
|
-
|
|
21956
|
+
chalk33.cyan(
|
|
20682
21957
|
`
|
|
20683
21958
|
[DRY RUN] Would prune ${result.prunedRuns.length} of ${result.totalRunsScanned} run(s):`
|
|
20684
21959
|
)
|
|
20685
21960
|
);
|
|
20686
21961
|
} else {
|
|
20687
21962
|
console.log(
|
|
20688
|
-
|
|
21963
|
+
chalk33.bold(
|
|
20689
21964
|
`
|
|
20690
21965
|
${symbols.success} Pruned ${result.prunedRuns.length} of ${result.totalRunsScanned} run(s):`
|
|
20691
21966
|
)
|
|
@@ -20693,18 +21968,18 @@ async function prune(opts = {}) {
|
|
|
20693
21968
|
}
|
|
20694
21969
|
if (result.prunedRuns.length === 0) {
|
|
20695
21970
|
console.log(
|
|
20696
|
-
|
|
21971
|
+
chalk33.dim(" No runs matched the pruning criteria. Nothing pruned.")
|
|
20697
21972
|
);
|
|
20698
21973
|
} else {
|
|
20699
21974
|
for (const run of result.prunedRuns) {
|
|
20700
21975
|
const mode = run.fullyDeleted ? "removed" : "screenshots purged";
|
|
20701
21976
|
const count = run.screenshotsDeleted > 0 ? `${run.screenshotsDeleted} screenshots, ` : "";
|
|
20702
21977
|
console.log(
|
|
20703
|
-
` ${
|
|
21978
|
+
` ${chalk33.dim("\u2022")} ${chalk33.bold(run.runId)} ${chalk33.dim(`(${run.scenarioSlug ?? "unknown scenario"})`)}: ${chalk33.yellow(formatBytes(run.bytesReclaimed))} (${count}${mode})`
|
|
20704
21979
|
);
|
|
20705
21980
|
}
|
|
20706
21981
|
console.log(
|
|
20707
|
-
|
|
21982
|
+
chalk33.dim(
|
|
20708
21983
|
`
|
|
20709
21984
|
Total reclaimed: ${formatBytes(result.totalBytesReclaimed)} (${result.totalScreenshotsDeleted} screenshots deleted)
|
|
20710
21985
|
`
|
|
@@ -20727,7 +22002,7 @@ init_context4();
|
|
|
20727
22002
|
import { existsSync as existsSync13, readFileSync as readFileSync9 } from "fs";
|
|
20728
22003
|
import { dirname as dirname5, resolve as resolve3 } from "path";
|
|
20729
22004
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
20730
|
-
import
|
|
22005
|
+
import chalk36 from "chalk";
|
|
20731
22006
|
import { Command } from "commander";
|
|
20732
22007
|
|
|
20733
22008
|
// src/commands/register-cloud.ts
|
|
@@ -20863,10 +22138,13 @@ function registerProjectCommands(program2) {
|
|
|
20863
22138
|
await status2(opts);
|
|
20864
22139
|
});
|
|
20865
22140
|
program2.command("doctor").helpGroup("Project:").description(
|
|
20866
|
-
"Diagnose environment: runtime, browser automation, skills, cloud"
|
|
22141
|
+
"Diagnose environment: runtime, browser automation, skills, cloud, and target testability"
|
|
22142
|
+
).option(
|
|
22143
|
+
"-t, --target <url>",
|
|
22144
|
+
"Probe target URL health, reachability, and auth redirects"
|
|
20867
22145
|
).option("--json", "Output diagnostics as JSON").addHelpText(
|
|
20868
22146
|
"after",
|
|
20869
|
-
"\nExamples:\n $ iamu doctor\n $ iamu doctor --json"
|
|
22147
|
+
"\nExamples:\n $ iamu doctor\n $ iamu doctor --target http://localhost:3000\n $ iamu doctor --json"
|
|
20870
22148
|
).action(async (opts) => {
|
|
20871
22149
|
const { doctor: doctor2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
|
|
20872
22150
|
await doctor2(opts);
|
|
@@ -20898,9 +22176,9 @@ function registerProjectCommands(program2) {
|
|
|
20898
22176
|
"after",
|
|
20899
22177
|
"\nExamples:\n $ iamu validate --all\n $ iamu validate --all --fix --dry-run\n $ iamu validate iamu/personas/maria.md"
|
|
20900
22178
|
).action(
|
|
20901
|
-
async (
|
|
22179
|
+
async (path30, opts) => {
|
|
20902
22180
|
const { validate: validate2 } = await Promise.resolve().then(() => (init_validate(), validate_exports));
|
|
20903
|
-
await validate2(
|
|
22181
|
+
await validate2(path30, opts);
|
|
20904
22182
|
}
|
|
20905
22183
|
);
|
|
20906
22184
|
program2.command("update").helpGroup("Project:").description("Update and repair agent skills and prompts to latest version").option(
|
|
@@ -20939,6 +22217,54 @@ function registerProjectCommands(program2) {
|
|
|
20939
22217
|
await newEntity2(type2, opts);
|
|
20940
22218
|
}
|
|
20941
22219
|
);
|
|
22220
|
+
const explore = program2.command("explore").helpGroup("Project:").description(
|
|
22221
|
+
"Map the running product with a read-only crawl; compare the map with runs (coverage) or with another map (diff)"
|
|
22222
|
+
);
|
|
22223
|
+
explore.command("run [url]", { isDefault: true }).description(
|
|
22224
|
+
"Crawl from <url> (default: target_url in iamu/testability.md) and write iamu/.cache/explore/<id>/map.json"
|
|
22225
|
+
).option("--max-surfaces <n>", "Stop after this many surfaces (default 40)").option("--max-actions <n>", "Stop after this many clicks (default 300)").option(
|
|
22226
|
+
"--max-depth <n>",
|
|
22227
|
+
"Don't go more than this many clicks from the start (default 6)"
|
|
22228
|
+
).option(
|
|
22229
|
+
"--deny <pattern...>",
|
|
22230
|
+
"Never click a control whose name or link matches (regex, case-insensitive)"
|
|
22231
|
+
).option("--viewport <WxH>", "Page size, e.g. 390x844 (default 1280x800)").option(
|
|
22232
|
+
"--attach",
|
|
22233
|
+
"Crawl in a new tab of the `iamu browse` browser, keeping its sign-ins"
|
|
22234
|
+
).option("--json", "Output as JSON").addHelpText(
|
|
22235
|
+
"after",
|
|
22236
|
+
`
|
|
22237
|
+
Read-only: controls that read like writes (delete, pay, send, save, log out \u2026),
|
|
22238
|
+
form submits, downloads and other origins are never clicked, nothing is typed,
|
|
22239
|
+
and every request but GET/HEAD/OPTIONS is blocked before it leaves the browser.
|
|
22240
|
+
|
|
22241
|
+
Examples:
|
|
22242
|
+
$ iamu explore http://localhost:3000
|
|
22243
|
+
$ iamu explore --attach --deny "^Admin"
|
|
22244
|
+
$ iamu explore coverage
|
|
22245
|
+
$ iamu explore diff`
|
|
22246
|
+
).action(
|
|
22247
|
+
async (url, opts) => {
|
|
22248
|
+
const { exploreRun: exploreRun2 } = await Promise.resolve().then(() => (init_explore(), explore_exports));
|
|
22249
|
+
await exploreRun2(url, opts);
|
|
22250
|
+
}
|
|
22251
|
+
);
|
|
22252
|
+
explore.command("coverage [map]").description(
|
|
22253
|
+
"Surfaces of a map (default: latest) that no run reached, and places runs reached that the map lacks"
|
|
22254
|
+
).option("--json", "Output as JSON").action(async (map2, opts) => {
|
|
22255
|
+
const { exploreCoverageCommand: exploreCoverageCommand2 } = await Promise.resolve().then(() => (init_explore(), explore_exports));
|
|
22256
|
+
await exploreCoverageCommand2(map2, opts);
|
|
22257
|
+
});
|
|
22258
|
+
explore.command("diff [a] [b]").description("Compare two maps (default: the two latest)").option("--json", "Output as JSON").action(
|
|
22259
|
+
async (a, b, opts) => {
|
|
22260
|
+
const { exploreDiffCommand: exploreDiffCommand2 } = await Promise.resolve().then(() => (init_explore(), explore_exports));
|
|
22261
|
+
await exploreDiffCommand2(a, b, opts);
|
|
22262
|
+
}
|
|
22263
|
+
);
|
|
22264
|
+
explore.command("list").description("List maps").option("--json", "Output as JSON").action(async (opts) => {
|
|
22265
|
+
const { exploreList: exploreList2 } = await Promise.resolve().then(() => (init_explore(), explore_exports));
|
|
22266
|
+
await exploreList2(opts);
|
|
22267
|
+
});
|
|
20942
22268
|
program2.command("completion [shell]").helpGroup("Project:").description("Output shell completion script (bash, zsh, fish)").action(async (shell) => {
|
|
20943
22269
|
const { completion: completion2 } = await Promise.resolve().then(() => (init_completion(), completion_exports));
|
|
20944
22270
|
completion2(program2, shell);
|
|
@@ -21146,23 +22472,23 @@ function registerAllCommands(program2) {
|
|
|
21146
22472
|
|
|
21147
22473
|
// src/ui/panic.ts
|
|
21148
22474
|
init_logger();
|
|
21149
|
-
import
|
|
22475
|
+
import chalk34 from "chalk";
|
|
21150
22476
|
var handlersRegistered = false;
|
|
21151
22477
|
var customSigintHandler = null;
|
|
21152
22478
|
function logFatalAndExit(title, errOrReason) {
|
|
21153
22479
|
process.stderr.write("\x1B[?25h");
|
|
21154
22480
|
if (isVerbose()) {
|
|
21155
|
-
console.error(
|
|
22481
|
+
console.error(chalk34.red(`
|
|
21156
22482
|
${title}:`));
|
|
21157
22483
|
console.error(
|
|
21158
22484
|
errOrReason instanceof Error ? errOrReason.stack || errOrReason : errOrReason
|
|
21159
22485
|
);
|
|
21160
22486
|
} else {
|
|
21161
22487
|
const msg = errOrReason instanceof Error ? errOrReason.message : String(errOrReason);
|
|
21162
|
-
console.error(
|
|
22488
|
+
console.error(chalk34.red(`
|
|
21163
22489
|
Fatal Error: ${msg}`));
|
|
21164
22490
|
console.error(
|
|
21165
|
-
|
|
22491
|
+
chalk34.dim(
|
|
21166
22492
|
" Run with --verbose or DEBUG=1 to view the full stack trace.\n"
|
|
21167
22493
|
)
|
|
21168
22494
|
);
|
|
@@ -21181,7 +22507,7 @@ function setupProcessHandlers() {
|
|
|
21181
22507
|
}
|
|
21182
22508
|
process.exit(130);
|
|
21183
22509
|
} else {
|
|
21184
|
-
console.error(
|
|
22510
|
+
console.error(chalk34.dim("\n Operation cancelled."));
|
|
21185
22511
|
process.exit(130);
|
|
21186
22512
|
}
|
|
21187
22513
|
});
|
|
@@ -21207,7 +22533,7 @@ function setupProcessHandlers() {
|
|
|
21207
22533
|
import { mkdir as mkdir7, readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
|
|
21208
22534
|
import { homedir as homedir4 } from "os";
|
|
21209
22535
|
import { join as join10 } from "path";
|
|
21210
|
-
import
|
|
22536
|
+
import chalk35 from "chalk";
|
|
21211
22537
|
var CACHE_FILE = join10(homedir4(), ".iamu", "update-check.json");
|
|
21212
22538
|
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
21213
22539
|
function isSemverGreater(latest, current) {
|
|
@@ -21260,7 +22586,7 @@ async function checkForUpdate(currentVersion) {
|
|
|
21260
22586
|
function printUpdateNotice(currentVersion, latestVersion) {
|
|
21261
22587
|
console.error();
|
|
21262
22588
|
console.error(
|
|
21263
|
-
` ${
|
|
22589
|
+
` ${chalk35.yellow("Update available:")} ${chalk35.dim(currentVersion)} \u2192 ${chalk35.green(latestVersion)} ${chalk35.dim("(run: npm i -g @iamu-dev/iamu)")}`
|
|
21264
22590
|
);
|
|
21265
22591
|
}
|
|
21266
22592
|
|
|
@@ -21305,7 +22631,7 @@ var program = new Command().name("iamu").description(
|
|
|
21305
22631
|
}
|
|
21306
22632
|
}
|
|
21307
22633
|
if (root.color === false || process.env.NO_COLOR) {
|
|
21308
|
-
|
|
22634
|
+
chalk36.level = 0;
|
|
21309
22635
|
}
|
|
21310
22636
|
if (root.verbose) {
|
|
21311
22637
|
process.env.IAMU_VERBOSE = "1";
|