csspilot 0.3.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -10449,8 +10449,8 @@ function makeCiCommand() {
10449
10449
  }
10450
10450
 
10451
10451
  // src/commands/init/doctor.ts
10452
- import { existsSync as existsSync6 } from "node:fs";
10453
- import { join as join21 } from "node:path";
10452
+ import { existsSync as existsSync7 } from "node:fs";
10453
+ import { join as join24 } from "node:path";
10454
10454
 
10455
10455
  // ../../packages/core/src/errors/index.ts
10456
10456
  var TestPilotError = class extends Error {
@@ -15513,7 +15513,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
15513
15513
  var NONE = 0;
15514
15514
  var ASSUMED = 1;
15515
15515
  var PROVEN = 2;
15516
- function isRecursive(inst, stack, resolve5) {
15516
+ function isRecursive(inst, stack, resolve6) {
15517
15517
  const cached2 = recursive.get(inst);
15518
15518
  if (cached2 !== void 0)
15519
15519
  return cached2 ? PROVEN : NONE;
@@ -15523,7 +15523,7 @@ function isRecursive(inst, stack, resolve5) {
15523
15523
  let result = NONE;
15524
15524
  const check2 = (child) => {
15525
15525
  if (result !== PROVEN && child?._zod) {
15526
- const answer = isRecursive(child, stack, resolve5);
15526
+ const answer = isRecursive(child, stack, resolve6);
15527
15527
  if (answer > result)
15528
15528
  result = answer;
15529
15529
  }
@@ -15534,7 +15534,7 @@ function isRecursive(inst, stack, resolve5) {
15534
15534
  const desc = Object.getOwnPropertyDescriptor(sh, key);
15535
15535
  if (spread && !desc.enumerable)
15536
15536
  continue;
15537
- const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve5) : NONE;
15537
+ const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve6) : NONE;
15538
15538
  if (child > answer)
15539
15539
  answer = child;
15540
15540
  }
@@ -15601,7 +15601,7 @@ function isRecursive(inst, stack, resolve5) {
15601
15601
  break;
15602
15602
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
15603
15603
  case "lazy": {
15604
- const inner = def._cachedInner ?? (resolve5 ? inst._zod.innerType : void 0);
15604
+ const inner = def._cachedInner ?? (resolve6 ? inst._zod.innerType : void 0);
15605
15605
  merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
15606
15606
  break;
15607
15607
  }
@@ -30243,7 +30243,10 @@ async function loadTestpilotConfig(cwd = process.cwd()) {
30243
30243
  baseUrl: parsed.api.baseUrl === void 0 ? void 0 : interpolateEnvRefs(parsed.api.baseUrl),
30244
30244
  timeoutMs: parsed.api.timeoutMs
30245
30245
  } : void 0,
30246
- miniapp: parsed.miniapp
30246
+ miniapp: parsed.miniapp ? {
30247
+ projectPath: parsed.miniapp.projectPath === void 0 ? void 0 : interpolateEnvRefs(parsed.miniapp.projectPath),
30248
+ cliPath: parsed.miniapp.cliPath === void 0 ? void 0 : interpolateEnvRefs(parsed.miniapp.cliPath)
30249
+ } : void 0
30247
30250
  };
30248
30251
  }
30249
30252
 
@@ -30338,7 +30341,7 @@ function execToString(command, args) {
30338
30341
  }
30339
30342
 
30340
30343
  // ../../packages/sdk/src/client.ts
30341
- import { isAbsolute as isAbsolute3, join as join17 } from "node:path";
30344
+ import { isAbsolute as isAbsolute3, join as join19 } from "node:path";
30342
30345
 
30343
30346
  // ../../packages/sdk/src/cases.ts
30344
30347
  import { mkdir as mkdir3, readFile as readFile3, readdir, stat as stat2, writeFile as writeFile3 } from "node:fs/promises";
@@ -30712,12 +30715,62 @@ async function writeCaseFile(file2, source, options = {}) {
30712
30715
  }
30713
30716
 
30714
30717
  // ../../packages/sdk/src/reports.ts
30715
- import { readFile as readFile6 } from "node:fs/promises";
30716
- import { join as join16 } from "node:path";
30718
+ import { readFile as readFile7 } from "node:fs/promises";
30719
+ import { join as join18 } from "node:path";
30717
30720
 
30718
30721
  // ../../packages/reporter/src/report.ts
30719
30722
  import { writeFile as writeFile4 } from "node:fs/promises";
30720
- import { join as join7, resolve as resolve3 } from "node:path";
30723
+ import { dirname as dirname2, join as join8, resolve as resolve3 } from "node:path";
30724
+
30725
+ // ../../packages/reporter/src/classify.ts
30726
+ var FAILURE_CATEGORIES = [
30727
+ { id: "assert", label: "\u65AD\u8A00\u5931\u8D25" },
30728
+ { id: "element", label: "\u5143\u7D20\u672A\u627E\u5230" },
30729
+ { id: "adapter", label: "Adapter \u95EE\u9898" },
30730
+ { id: "env", label: "\u73AF\u5883 / \u8FDE\u63A5" },
30731
+ { id: "timeout", label: "\u8D85\u65F6" },
30732
+ { id: "other", label: "\u5176\u4ED6" }
30733
+ ];
30734
+ function categoryLabel(id) {
30735
+ return FAILURE_CATEGORIES.find((item) => item.id === id)?.label ?? id;
30736
+ }
30737
+ var RULES = [
30738
+ { id: "assert", pattern: /断言失败|AssertionError|expect\(.+\)\.to/i },
30739
+ { id: "element", pattern: /未找到元素|等待元素超时|waiting for locator|waiting for selector|element not found/i },
30740
+ { id: "adapter", pattern: /未注册 target|ADAPTER_NOT_FOUND|无法定位 TestPilot Skill/i },
30741
+ { id: "env", pattern: /net::ERR|ECONNREFUSED|Connection closed|Failed to launch|服务端口|不可达|ENOTFOUND|EACCES/i },
30742
+ { id: "timeout", pattern: /timeout|超时/i }
30743
+ ];
30744
+ function classifyFailure(message) {
30745
+ if (!message) return "other";
30746
+ for (const rule of RULES) {
30747
+ if (rule.pattern.test(message)) return rule.id;
30748
+ }
30749
+ return "other";
30750
+ }
30751
+
30752
+ // ../../packages/reporter/src/history.ts
30753
+ import { readFile as readFile4, readdir as readdir2 } from "node:fs/promises";
30754
+ import { join as join7 } from "node:path";
30755
+ async function collectHistory(runsRoot2, skipRunId, limit = 12) {
30756
+ const entries = (await readdir2(runsRoot2).catch(() => [])).filter((name) => /^\d{8}-\d{3}$/.test(name) && name !== skipRunId).sort().slice(-limit);
30757
+ const points = [];
30758
+ for (const runId of entries) {
30759
+ try {
30760
+ const summary = JSON.parse(await readFile4(join7(runsRoot2, runId, "result.json"), "utf8"));
30761
+ points.push({
30762
+ runId,
30763
+ startedAt: summary.startedAt,
30764
+ status: summary.status ?? "unknown",
30765
+ passed: summary.totals?.passed ?? 0,
30766
+ failed: summary.totals?.failed ?? 0,
30767
+ durationMs: summary.durationMs
30768
+ });
30769
+ } catch {
30770
+ }
30771
+ }
30772
+ return points;
30773
+ }
30721
30774
 
30722
30775
  // ../../packages/reporter/src/summary.ts
30723
30776
  function toSummaryView(summary) {
@@ -30736,40 +30789,201 @@ function toSummaryView(summary) {
30736
30789
  }
30737
30790
 
30738
30791
  // ../../packages/reporter/src/report.ts
30739
- async function writeReports(runDir, summary) {
30792
+ var HISTORY_LIMIT = 12;
30793
+ async function writeReports(runDir, summary, options = {}) {
30740
30794
  const dir = resolve3(runDir);
30741
- const jsonPath = join7(dir, "report.json");
30742
- const htmlPath = join7(dir, "report.html");
30743
- const payload = { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), summary };
30744
- await writeFile4(jsonPath, JSON.stringify(payload, null, 2), "utf8");
30745
- await writeFile4(htmlPath, renderHtml(summary), "utf8");
30795
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
30796
+ const history = options.history ?? await collectHistory(dirname2(dir), summary.runId, options.historyLimit ?? HISTORY_LIMIT);
30797
+ const failures = [];
30798
+ const cases = summary.cases.map((item) => {
30799
+ const categories = /* @__PURE__ */ new Set();
30800
+ const steps = item.steps.map((step) => {
30801
+ const category = step.status === "failed" ? classifyFailure(step.error) : void 0;
30802
+ if (category) {
30803
+ categories.add(category);
30804
+ failures.push({
30805
+ category,
30806
+ label: categoryLabel(category),
30807
+ caseId: item.caseId,
30808
+ index: step.index,
30809
+ message: step.error ?? ""
30810
+ });
30811
+ }
30812
+ return { ...step, category };
30813
+ });
30814
+ return { ...item, steps, categories: [...categories] };
30815
+ });
30816
+ const categoryLabels = {};
30817
+ for (const item of FAILURE_CATEGORIES) categoryLabels[item.id] = item.label;
30818
+ const data = {
30819
+ generatedAt,
30820
+ summary: { ...summary, cases },
30821
+ history,
30822
+ failures,
30823
+ view: toSummaryView(summary),
30824
+ categoryLabels
30825
+ };
30826
+ const jsonPath = join8(dir, "report.json");
30827
+ const htmlPath = join8(dir, "report.html");
30828
+ await writeFile4(jsonPath, JSON.stringify(data, null, 2), "utf8");
30829
+ await writeFile4(htmlPath, renderHtml(data), "utf8");
30746
30830
  return { json: jsonPath, html: htmlPath };
30747
30831
  }
30748
- function renderHtml(summary) {
30749
- const view = toSummaryView(summary);
30832
+ function categoryChips(data) {
30833
+ const counts = /* @__PURE__ */ new Map();
30834
+ for (const failure2 of data.failures) {
30835
+ counts.set(failure2.category, (counts.get(failure2.category) ?? 0) + 1);
30836
+ }
30837
+ const chips = [
30838
+ '<span class="chip active" data-group="status" data-value="all">\u5168\u90E8\u72B6\u6001</span>',
30839
+ '<span class="chip" data-group="status" data-value="failed">\u4EC5\u5931\u8D25</span>'
30840
+ ];
30841
+ for (const item of FAILURE_CATEGORIES) {
30842
+ const count = counts.get(item.id) ?? 0;
30843
+ if (count > 0) {
30844
+ chips.push(
30845
+ `<span class="chip" data-group="cat" data-value="${item.id}">${escapeHtml(item.label)}(${count})</span>`
30846
+ );
30847
+ }
30848
+ }
30849
+ return chips.join("\n");
30850
+ }
30851
+ function renderHtml(data) {
30852
+ const view = toSummaryView(data.summary);
30853
+ const summary = data.summary;
30854
+ const trendSvg = renderTrend(data);
30750
30855
  const casesHtml = summary.cases.map((item) => caseHtml(item)).join("\n");
30856
+ const chips = categoryChips(data);
30751
30857
  return `<!doctype html>
30752
30858
  <html lang="zh-CN">
30753
30859
  <head>
30754
30860
  <meta charset="utf-8" />
30755
30861
  <title>TestPilot Report ${escapeHtml(summary.runId)}</title>
30756
30862
  <style>
30757
- body { font-family: system-ui, sans-serif; margin: 24px; color: #1f2328; }
30758
- h1 { font-size: 20px; } h2 { font-size: 16px; margin-top: 24px; }
30759
- .badge { padding: 2px 10px; border-radius: 10px; color: #fff; font-size: 13px; }
30760
- .passed { background: #1a7f37; } .failed { background: #cf222e; } .skipped { background: #6e7781; }
30761
- table { border-collapse: collapse; width: 100%; margin: 8px 0 16px; font-size: 13px; }
30762
- th, td { border: 1px solid #d0d7de; padding: 6px 10px; text-align: left; }
30763
- td.num { text-align: right; }
30764
- .error { color: #cf222e; white-space: pre-wrap; }
30765
- a { color: #0969da; }
30766
- code { background: #f6f8fa; padding: 1px 4px; border-radius: 4px; }
30863
+ :root { --ok:#1a7f37; --fail:#cf222e; --skip:#6e7781; --line:#d0d7de; --muted:#59636e; }
30864
+ * { box-sizing: border-box; }
30865
+ body { font-family: system-ui, "Segoe UI", sans-serif; margin: 0; color: #1f2328; background: #f6f8fa; }
30866
+ .wrap { max-width: 1080px; margin: 0 auto; padding: 20px 16px 48px; }
30867
+ h1 { font-size: 20px; margin: 8px 0; } h2 { font-size: 15px; margin: 24px 0 8px; }
30868
+ .badge { display: inline-block; padding: 2px 10px; border-radius: 10px; color: #fff; font-size: 12px; }
30869
+ .passed { background: var(--ok); } .failed { background: var(--fail); } .skipped { background: var(--skip); }
30870
+ code { background: #eef1f4; padding: 1px 5px; border-radius: 4px; font-size: 12px; }
30871
+ .cards { display: flex; gap: 12px; flex-wrap: wrap; margin: 14px 0; }
30872
+ .card { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 10px 16px; min-width: 130px; }
30873
+ .card b { font-size: 20px; display: block; }
30874
+ .card span { color: var(--muted); font-size: 12px; }
30875
+ .toolbar { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin: 10px 0; }
30876
+ .chip { border: 1px solid var(--line); background: #fff; border-radius: 14px; padding: 3px 12px; font-size: 12px; cursor: pointer; }
30877
+ .chip.active { background: #0969da; color: #fff; border-color: #0969da; }
30878
+ input#q { flex: 1; min-width: 180px; padding: 5px 10px; border: 1px solid var(--line); border-radius: 6px; font-size: 13px; }
30879
+ .btn { border: 1px solid var(--line); background: #fff; border-radius: 6px; padding: 4px 10px; font-size: 12px; cursor: pointer; }
30880
+ #count { color: var(--muted); font-size: 12px; margin-left: auto; }
30881
+ details.case { background: #fff; border: 1px solid var(--line); border-radius: 8px; margin: 10px 0; }
30882
+ details.case > summary { cursor: pointer; padding: 10px 14px; font-size: 14px; list-style: none; }
30883
+ details.case > summary::-webkit-details-marker { display: none; }
30884
+ details.case > summary:hover { background: #f6f8fa; }
30885
+ table { border-collapse: collapse; width: 100%; font-size: 13px; }
30886
+ th, td { border-top: 1px solid var(--line); padding: 6px 10px; text-align: left; vertical-align: top; }
30887
+ td.num { text-align: right; white-space: nowrap; }
30888
+ .error { color: var(--fail); white-space: pre-wrap; }
30889
+ a { color: #0969da; text-decoration: none; }
30890
+ .trend { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 12px; overflow-x: auto; }
30891
+ .muted { color: var(--muted); }
30892
+ .hide { display: none !important; }
30893
+ .meta { color: var(--muted); font-size: 12px; margin: 6px 0; }
30767
30894
  </style>
30768
30895
  </head>
30769
30896
  <body>
30770
- <h1>TestPilot \u62A5\u544A <span class="badge ${summary.status}">${summary.status}</span></h1>
30771
- <p>run <code>${escapeHtml(summary.runId)}</code> \xB7 \u7528\u4F8B ${view.cases.passed}\u2713 / ${view.cases.failed}\u2717 \xB7 \u6B65\u9AA4 ${view.steps.passed}\u2713 ${view.steps.failed}\u2717 ${view.steps.skipped}\u21B7 \xB7 \u8017\u65F6 ${(view.durationMs / 1e3).toFixed(1)}s</p>
30772
- ${casesHtml}
30897
+ <div class="wrap">
30898
+ <h1>TestPilot \u6D4B\u8BD5\u62A5\u544A <span class="badge ${summary.status}">${summary.status}</span></h1>
30899
+ <p class="meta">run <code>${escapeHtml(summary.runId)}</code> \xB7 \u751F\u6210\u4E8E ${escapeHtml(data.generatedAt)} \xB7 \u8017\u65F6 ${(view.durationMs / 1e3).toFixed(1)}s</p>
30900
+
30901
+ <div class="cards">
30902
+ <div class="card"><b>${view.cases.passed}<span style="color:var(--ok)"> \u2713</span></b><span>\u901A\u8FC7\u7528\u4F8B</span></div>
30903
+ <div class="card"><b>${view.cases.failed}<span style="color:var(--fail)"> \u2717</span></b><span>\u5931\u8D25\u7528\u4F8B</span></div>
30904
+ <div class="card"><b>${view.steps.passed}<span style="color:var(--ok)"> \u2713</span></b><span>\u901A\u8FC7\u6B65\u9AA4</span></div>
30905
+ <div class="card"><b>${view.steps.failed}<span style="color:var(--fail)"> \u2717</span></b><span>\u5931\u8D25\u6B65\u9AA4</span></div>
30906
+ <div class="card"><b>${view.steps.skipped} \u21B7</b><span>\u8DF3\u8FC7\u6B65\u9AA4</span></div>
30907
+ </div>
30908
+
30909
+ <h2>\u5386\u53F2\u8D8B\u52BF(\u6700\u8FD1 ${data.history.length + 1} \u6B21\u8FD0\u884C)</h2>
30910
+ <div class="trend">${trendSvg}</div>
30911
+
30912
+ <h2>\u5931\u8D25\u5206\u7C7B</h2>
30913
+ <div class="toolbar" id="cat-chips">${chips}</div>
30914
+
30915
+ <h2>\u7528\u4F8B</h2>
30916
+ <div class="toolbar">
30917
+ <input id="q" placeholder="\u641C\u7D22\u7528\u4F8B id / \u540D\u79F0 / \u6587\u4EF6\u2026" />
30918
+ <button class="btn" id="expand">\u5C55\u5F00\u5168\u90E8</button>
30919
+ <button class="btn" id="collapse">\u6536\u8D77\u5168\u90E8</button>
30920
+ <span id="count"></span>
30921
+ </div>
30922
+ <div id="cases">${casesHtml}</div>
30923
+ </div>
30924
+
30925
+ <script id="report-data" type="application/json">${embedJson(data)}</script>
30926
+ <script>
30927
+ (function () {
30928
+ var data = JSON.parse(document.getElementById('report-data').textContent);
30929
+ var state = { status: 'all', cat: 'all', q: '' };
30930
+ var labels = data.categoryLabels || {};
30931
+
30932
+ function setChip(group, value) {
30933
+ document.querySelectorAll('.chip[data-group="' + group + '"]').forEach(function (chip) {
30934
+ chip.classList.toggle('active', chip.getAttribute('data-value') === value);
30935
+ });
30936
+ }
30937
+
30938
+ function matches(card) {
30939
+ if (state.status !== 'all' && card.getAttribute('data-status') !== state.status) return false;
30940
+ if (state.cat !== 'all' && (',' + (card.getAttribute('data-cats') || '') + ',').indexOf(',' + state.cat + ',') === -1) return false;
30941
+ if (state.q) {
30942
+ var text = (card.getAttribute('data-search') || '').toLowerCase();
30943
+ if (text.indexOf(state.q.toLowerCase()) === -1) return false;
30944
+ }
30945
+ return true;
30946
+ }
30947
+
30948
+ function apply() {
30949
+ var cards = document.querySelectorAll('details.case');
30950
+ var shown = 0;
30951
+ cards.forEach(function (card) {
30952
+ var show = matches(card);
30953
+ card.classList.toggle('hide', !show);
30954
+ if (show) shown++;
30955
+ });
30956
+ var count = document.getElementById('count');
30957
+ if (count) count.textContent = shown + ' / ' + cards.length + ' \u7528\u4F8B';
30958
+ }
30959
+
30960
+ document.querySelectorAll('.chip').forEach(function (chip) {
30961
+ chip.addEventListener('click', function () {
30962
+ var group = chip.getAttribute('data-group');
30963
+ var value = chip.getAttribute('data-value');
30964
+ if (group === 'status') state.status = value;
30965
+ if (group === 'cat') state.cat = value;
30966
+ setChip(group, value);
30967
+ apply();
30968
+ });
30969
+ });
30970
+ var q = document.getElementById('q');
30971
+ if (q) q.addEventListener('input', function () { state.q = q.value.trim(); apply(); });
30972
+ var expand = document.getElementById('expand');
30973
+ if (expand) expand.addEventListener('click', function () {
30974
+ document.querySelectorAll('details.case').forEach(function (c) { c.open = true; });
30975
+ });
30976
+ var collapse = document.getElementById('collapse');
30977
+ if (collapse) collapse.addEventListener('click', function () {
30978
+ document.querySelectorAll('details.case').forEach(function (c) { c.open = false; });
30979
+ });
30980
+
30981
+ function failedSteps(card) {
30982
+ return data.summary.cases.find(function (c) { return c.caseId === card; });
30983
+ }
30984
+ apply();
30985
+ })();
30986
+ </script>
30773
30987
  </body>
30774
30988
  </html>`;
30775
30989
  }
@@ -30780,18 +30994,27 @@ function caseHtml(item) {
30780
30994
  if (item.video) links.push(`<a href="${escapeHtml(item.video)}">\u89C6\u9891</a>`);
30781
30995
  if (item.trace) links.push(`<a href="${escapeHtml(item.trace)}">Trace</a>`);
30782
30996
  const linksLine = links.length > 0 ? `<p>${links.join(" \xB7 ")}</p>` : "";
30997
+ const search = escapeHtml(`${item.caseId} ${item.caseName} ${item.file}`).replaceAll('"', "&quot;");
30998
+ const cats = escapeHtml(item.categories.join(","));
30783
30999
  return `
30784
- <h2>${escapeHtml(item.caseId)} \xB7 ${escapeHtml(item.caseName)} <span class="badge ${item.status}">${item.status}</span></h2>
30785
- ${errorLine}
30786
- ${linksLine}
30787
- <table>
30788
- <tr><th>#</th><th>target</th><th>action</th><th>\u72B6\u6001</th><th>\u8017\u65F6</th><th>\u8BE6\u60C5</th></tr>
30789
- ${rows}
30790
- </table>`;
31000
+ <details class="case" data-status="${item.status}" data-cats="${cats}" data-search="${search}">
31001
+ <summary>
31002
+ <span class="badge ${item.status}">${item.status}</span>
31003
+ <b>${escapeHtml(item.caseId)}</b> \xB7 ${escapeHtml(item.caseName)}
31004
+ <span class="muted">\xB7 ${item.steps.length} \u6B65 \xB7 ${(item.durationMs / 1e3).toFixed(1)}s</span>
31005
+ </summary>
31006
+ ${errorLine}
31007
+ ${linksLine}
31008
+ <table>
31009
+ <tr><th>#</th><th>target</th><th>action</th><th>\u72B6\u6001</th><th>\u8017\u65F6</th><th>\u8BE6\u60C5</th></tr>
31010
+ ${rows}
31011
+ </table>
31012
+ </details>`;
30791
31013
  }
30792
31014
  function stepHtml(step) {
30793
31015
  const details = [];
30794
31016
  if (step.error) details.push(`<span class="error">${escapeHtml(step.error)}</span>`);
31017
+ if (step.category) details.push(`<span class="muted">\u5206\u7C7B: ${escapeHtml(categoryLabel(step.category))}</span>`);
30795
31018
  if (step.extracted) {
30796
31019
  for (const [name, value] of Object.entries(step.extracted)) {
30797
31020
  details.push(`\u53D8\u91CF ${escapeHtml(name)} = <code>${escapeHtml(value)}</code>`);
@@ -30807,21 +31030,344 @@ function stepHtml(step) {
30807
31030
  <td>${details.join("<br />") || "-"}</td>
30808
31031
  </tr>`;
30809
31032
  }
31033
+ function renderTrend(data) {
31034
+ const points = [
31035
+ ...data.history.map((h) => ({ runId: h.runId, passed: h.passed, failed: h.failed, status: h.status })),
31036
+ {
31037
+ runId: data.summary.runId,
31038
+ passed: data.summary.totals.passed,
31039
+ failed: data.summary.totals.failed,
31040
+ status: data.summary.status
31041
+ }
31042
+ ];
31043
+ if (points.length === 0) return '<p class="muted">\u6682\u65E0\u5386\u53F2\u6570\u636E</p>';
31044
+ const max = Math.max(1, ...points.map((p) => p.passed + p.failed));
31045
+ const barW = 34;
31046
+ const gap = 14;
31047
+ const chartH = 90;
31048
+ const labelH = 34;
31049
+ const width = points.length * (barW + gap) + gap;
31050
+ const height = chartH + labelH;
31051
+ const bars = points.map((point, index) => {
31052
+ const total = point.passed + point.failed;
31053
+ const scale = (count) => total === 0 ? 0 : count / max * chartH;
31054
+ const passedH = scale(point.passed);
31055
+ const failedH = scale(point.failed);
31056
+ const x = gap + index * (barW + gap);
31057
+ const passedY = chartH - passedH;
31058
+ const failedY = passedY - failedH;
31059
+ const tip = `${point.runId}: \u2713${point.passed} \u2717${point.failed}`;
31060
+ return `<g>
31061
+ <title>${escapeHtml(tip)}</title>
31062
+ <rect x="${x}" y="${chartH + 4}" width="${barW}" height="1" fill="var(--line)"></rect>
31063
+ ${passedH > 0 ? `<rect x="${x}" y="${passedY.toFixed(1)}" width="${barW}" height="${passedH.toFixed(1)}" fill="var(--ok)" rx="2"></rect>` : ""}
31064
+ ${failedH > 0 ? `<rect x="${x}" y="${failedY.toFixed(1)}" width="${barW}" height="${failedH.toFixed(1)}" fill="var(--fail)" rx="2"></rect>` : ""}
31065
+ <text x="${x + barW / 2}" y="${chartH + 20}" font-size="9" fill="var(--muted)" text-anchor="middle">${escapeHtml(shortId(point.runId))}</text>
31066
+ </g>`;
31067
+ }).join("\n");
31068
+ return `<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg">${bars}</svg>`;
31069
+ }
31070
+ function shortId(runId) {
31071
+ return runId.replace(/^\d{8}-/, "");
31072
+ }
30810
31073
  function escapeHtml(input2) {
30811
31074
  return input2.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
30812
31075
  }
31076
+ function embedJson(data) {
31077
+ return JSON.stringify(data).replaceAll("<", "\\u003c");
31078
+ }
31079
+
31080
+ // ../../packages/reporter/src/summary-report.ts
31081
+ import { mkdir as mkdir4, writeFile as writeFile5 } from "node:fs/promises";
31082
+ import { join as join9, resolve as resolve4 } from "node:path";
31083
+ function escapeHtml2(value) {
31084
+ return String(value).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
31085
+ }
31086
+ function pct(value) {
31087
+ return (value * 100).toFixed(1) + "%";
31088
+ }
31089
+ function buildSummaryData(runs) {
31090
+ const executions = [];
31091
+ const runPoints = [];
31092
+ for (const { runId, summary } of runs) {
31093
+ for (const item of summary.cases) {
31094
+ const platforms = [...new Set(item.steps.map((step) => step.target))];
31095
+ const failedStep = item.steps.find((step) => step.status === "failed");
31096
+ const category = item.status === "failed" ? classifyFailure(failedStep?.error) : void 0;
31097
+ executions.push({
31098
+ runId,
31099
+ caseId: item.caseId,
31100
+ caseName: item.caseName,
31101
+ status: item.status,
31102
+ category,
31103
+ platforms
31104
+ });
31105
+ }
31106
+ runPoints.push({
31107
+ runId,
31108
+ startedAt: summary.startedAt,
31109
+ status: summary.status,
31110
+ passed: summary.totals.passed,
31111
+ failed: summary.totals.failed,
31112
+ passRate: summary.totals.cases > 0 ? summary.totals.passed / summary.totals.cases : 0
31113
+ });
31114
+ }
31115
+ const byCase = /* @__PURE__ */ new Map();
31116
+ for (const exec2 of executions) {
31117
+ let agg = byCase.get(exec2.caseId);
31118
+ if (!agg) {
31119
+ agg = {
31120
+ caseId: exec2.caseId,
31121
+ caseName: exec2.caseName,
31122
+ platforms: [],
31123
+ runs: 0,
31124
+ passed: 0,
31125
+ failed: 0,
31126
+ passRate: 0,
31127
+ lastStatus: "unknown",
31128
+ flaky: false
31129
+ };
31130
+ byCase.set(exec2.caseId, agg);
31131
+ }
31132
+ agg.runs++;
31133
+ if (exec2.status === "passed") agg.passed++;
31134
+ else agg.failed++;
31135
+ for (const platform of exec2.platforms) {
31136
+ if (!agg.platforms.includes(platform)) agg.platforms.push(platform);
31137
+ }
31138
+ }
31139
+ for (const agg of byCase.values()) {
31140
+ const last = [...executions].reverse().find((exec2) => exec2.caseId === agg.caseId);
31141
+ agg.lastStatus = last?.status ?? "unknown";
31142
+ agg.lastCategory = last?.category;
31143
+ agg.passRate = agg.runs > 0 ? agg.passed / agg.runs : 0;
31144
+ agg.flaky = agg.failed > 0 && agg.passed > 0;
31145
+ }
31146
+ const cases = [...byCase.values()];
31147
+ const flaky = cases.filter((item) => item.flaky);
31148
+ const categories = FAILURE_CATEGORIES.map((item) => ({
31149
+ id: item.id,
31150
+ label: item.label,
31151
+ count: executions.filter((exec2) => exec2.status === "failed" && exec2.category === item.id).length
31152
+ })).filter((item) => item.count > 0);
31153
+ const durations = runs.map((item) => item.summary.durationMs).filter((value) => typeof value === "number");
31154
+ return {
31155
+ window: {
31156
+ runs: runs.length,
31157
+ from: runPoints[0]?.startedAt,
31158
+ to: runPoints.at(-1)?.startedAt
31159
+ },
31160
+ kpis: {
31161
+ runs: runs.length,
31162
+ executions: executions.length,
31163
+ overallRate: executions.length > 0 ? executions.filter((exec2) => exec2.status === "passed").length / executions.length : 0,
31164
+ avgDurationMs: durations.length > 0 ? durations.reduce((sum, value) => sum + value, 0) / durations.length : 0,
31165
+ flakyCount: flaky.length
31166
+ },
31167
+ runs: runPoints,
31168
+ cases,
31169
+ platforms: [...new Set(cases.flatMap((item) => item.platforms))].sort(),
31170
+ categories,
31171
+ categoryLabels: Object.fromEntries(FAILURE_CATEGORIES.map((item) => [item.id, item.label]))
31172
+ };
31173
+ }
31174
+ function renderSummaryTrend(runs) {
31175
+ if (runs.length === 0) return '<p class="muted">\u7EDF\u8BA1\u7A97\u53E3\u5185\u6CA1\u6709\u8FD0\u884C\u6570\u636E</p>';
31176
+ const barW = 30;
31177
+ const gap = 8;
31178
+ const chartH = 96;
31179
+ const width = runs.length * (barW + gap) + gap;
31180
+ const height = chartH + 30;
31181
+ const max = Math.max(1, ...runs.map((run) => run.passed + run.failed));
31182
+ const bars = runs.map((run, index) => {
31183
+ const x = gap + index * (barW + gap);
31184
+ const passedH = run.passed / max * chartH;
31185
+ const failedH = run.failed / max * chartH;
31186
+ const passedY = chartH - passedH;
31187
+ const failedY = passedY - failedH;
31188
+ const total = run.passed + run.failed;
31189
+ const rate = total > 0 ? Math.round(run.passed / total * 100) : 0;
31190
+ const label = run.runId.replace(/^\d{8}-/, "");
31191
+ return "<g><title>" + escapeHtml2(run.runId) + ": \u2713" + run.passed + " \u2717" + run.failed + '</title><rect x="' + x + '" y="' + passedY.toFixed(1) + '" width="' + barW + '" height="' + passedH.toFixed(1) + '" fill="#1a7f37" rx="2"></rect><rect x="' + x + '" y="' + failedY.toFixed(1) + '" width="' + barW + '" height="' + failedH.toFixed(1) + '" fill="#cf222e" rx="2"></rect><text x="' + (x + barW / 2) + '" y="' + (chartH + 12) + '" font-size="8" fill="#59636e" text-anchor="middle">' + escapeHtml2(label) + '</text><text x="' + (x + barW / 2) + '" y="' + (chartH + 24) + '" font-size="8" fill="#59636e" text-anchor="middle">' + rate + "%</text></g>";
31192
+ }).join("\n");
31193
+ return '<svg width="' + width + '" height="' + height + '" viewBox="0 0 ' + width + " " + height + '" xmlns="http://www.w3.org/2000/svg">' + bars + "</svg>";
31194
+ }
31195
+ function renderSummaryHtml(data) {
31196
+ const kpis = data.kpis;
31197
+ const rateText = pct(kpis.overallRate);
31198
+ const trendSvg = renderSummaryTrend(data.runs);
31199
+ const catBars = data.categories.map(
31200
+ (item) => '<div class="hbar-row" data-cat="' + item.id + '" title="\u70B9\u51FB\u7B5B\u9009\u7528\u4F8B\u660E\u7EC6"><span class="hbar-label">' + escapeHtml2(item.label) + '</span><span class="hbar-track"><span class="hbar" style="width:' + (kpis.executions > 0 ? Math.round(item.count / kpis.executions * 100) : 0) + '%"></span></span><span class="hbar-num">' + item.count + "</span></div>"
31201
+ ).join("\n");
31202
+ const catHtml = catBars || '<p class="muted">\u7EDF\u8BA1\u7A97\u53E3\u5185\u65E0\u5931\u8D25</p>';
31203
+ const flaky = data.cases.filter((item) => item.flaky);
31204
+ const flakyRows = flaky.map(
31205
+ (item) => '<tr data-platform="' + escapeHtml2(item.platforms.join(" ")) + '" data-q="' + escapeHtml2(item.caseId + " " + item.caseName) + '"><td><code class="flaky-link" data-q="' + escapeHtml2(item.caseId) + '">' + escapeHtml2(item.caseId) + "</code></td><td>" + escapeHtml2(item.caseName) + "</td><td>" + escapeHtml2(item.platforms.join(" + ")) + '</td><td class="num">' + item.failed + " / " + item.runs + '</td><td class="num">' + pct(item.passRate) + '</td><td><span class="badge ' + item.lastStatus + '">' + item.lastStatus + "</span></td></tr>"
31206
+ ).join("\n");
31207
+ const platformChips = ["all", ...data.platforms].map(
31208
+ (platform) => '<span class="chip' + (platform === "all" ? " active" : "") + '" data-platform="' + platform + '">' + (platform === "all" ? "\u5168\u90E8\u5E73\u53F0" : platform) + "</span>"
31209
+ ).join("\n");
31210
+ const detailRows = data.cases.map(
31211
+ (item) => '<tr data-platform="' + escapeHtml2(item.platforms.join(" ")) + '" data-cat="' + escapeHtml2(item.lastCategory ?? "") + '" data-q="' + escapeHtml2(item.caseId + " " + item.caseName) + '" data-pass="' + item.passRate + '" data-runs="' + item.runs + '" data-failed="' + item.failed + '"><td><code>' + escapeHtml2(item.caseId) + "</code></td><td>" + escapeHtml2(item.caseName) + "</td><td>" + escapeHtml2(item.platforms.join(" + ")) + '</td><td class="num">' + item.runs + '</td><td class="num">' + item.passed + '</td><td class="num">' + item.failed + '</td><td><span class="mini-track"><span class="mini ' + (item.failed > 0 ? "warn" : "ok") + '" style="width:' + Math.round(item.passRate * 60) + 'px"></span></span> ' + pct(item.passRate) + "</td><td>" + (item.lastCategory ? escapeHtml2(categoryLabel(item.lastCategory)) : "-") + '</td><td><span class="badge ' + item.lastStatus + '">' + item.lastStatus + "</span></td></tr>"
31212
+ ).join("\n");
31213
+ const range = (data.window.from ?? "").slice(0, 10) + " \u2192 " + (data.window.to ?? "").slice(0, 10);
31214
+ return `<!doctype html>
31215
+ <html lang="zh-CN">
31216
+ <head>
31217
+ <meta charset="utf-8" />
31218
+ <title>TestPilot \u6C47\u603B\u62A5\u544A</title>
31219
+ <style>
31220
+ :root { --ok:#1a7f37; --fail:#cf222e; --line:#d0d7de; --muted:#59636e; }
31221
+ * { box-sizing: border-box; }
31222
+ body { font-family: system-ui, "Segoe UI", sans-serif; margin: 0; color: #1f2328; background: #f6f8fa; }
31223
+ .wrap { max-width: 1120px; margin: 0 auto; padding: 20px 16px 56px; }
31224
+ h1 { font-size: 21px; margin: 8px 0; } h2 { font-size: 15px; margin: 26px 0 8px; }
31225
+ .meta { color: var(--muted); font-size: 12px; margin: 4px 0 12px; }
31226
+ .cards { display: flex; gap: 12px; flex-wrap: wrap; margin: 14px 0; }
31227
+ .card { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 12px 18px; min-width: 140px; }
31228
+ .card b { font-size: 22px; display: block; } .card span { color: var(--muted); font-size: 12px; }
31229
+ .row2 { display: flex; gap: 14px; flex-wrap: wrap; align-items: stretch; }
31230
+ .panel { background: #fff; border: 1px solid var(--line); border-radius: 8px; padding: 14px; }
31231
+ .panel h3 { margin: 0 0 8px; font-size: 13px; color: var(--muted); font-weight: 500; }
31232
+ .trend { overflow-x: auto; flex: 1; min-width: 320px; }
31233
+ table { border-collapse: collapse; width: 100%; font-size: 13px; background: #fff; }
31234
+ th, td { border-bottom: 1px solid var(--line); padding: 7px 10px; text-align: left; }
31235
+ th { color: var(--muted); font-weight: 500; cursor: pointer; white-space: nowrap; }
31236
+ th.num, td.num { text-align: right; }
31237
+ code { background: #eef1f4; padding: 1px 5px; border-radius: 4px; font-size: 12px; cursor: pointer; }
31238
+ .badge { display: inline-block; padding: 1px 9px; border-radius: 10px; color: #fff; font-size: 11px; }
31239
+ .passed { background: #1a7f37; } .failed { background: #cf222e; } .unknown { background: #6e7781; }
31240
+ .toolbar { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin: 10px 0; }
31241
+ .chip { border: 1px solid var(--line); background: #fff; border-radius: 14px; padding: 3px 12px; font-size: 12px; cursor: pointer; }
31242
+ .chip.active { background: #0969da; color: #fff; border-color: #0969da; }
31243
+ input#q { flex: 1; min-width: 160px; padding: 5px 10px; border: 1px solid var(--line); border-radius: 6px; font-size: 13px; }
31244
+ #count { color: var(--muted); font-size: 12px; margin-left: auto; }
31245
+ .hbar-row { display: flex; align-items: center; gap: 10px; margin: 7px 0; cursor: pointer; }
31246
+ .hbar-label { width: 90px; font-size: 13px; } .hbar-num { width: 30px; text-align: right; font-size: 13px; }
31247
+ .hbar-track { flex: 1; background: #eef1f4; border-radius: 5px; height: 12px; overflow: hidden; }
31248
+ .hbar { display: block; height: 100%; background: var(--fail); }
31249
+ .mini-track { display: inline-block; width: 60px; height: 7px; background: #eef1f4; border-radius: 4px; overflow: hidden; vertical-align: middle; }
31250
+ .mini { display: block; height: 100%; } .mini.ok { background: var(--ok); } .mini.warn { background: #bf8700; }
31251
+ .muted { color: var(--muted); } .hide { display: none !important; }
31252
+ </style>
31253
+ </head>
31254
+ <body>
31255
+ <div class="wrap">
31256
+ <h1>TestPilot \u6C47\u603B\u62A5\u544A</h1>
31257
+ <p class="meta">\u7EDF\u8BA1\u8303\u56F4:\u6700\u8FD1 ${data.window.runs} \u6B21\u8FD0\u884C(${escapeHtml2(range)})\xB7 \u751F\u6210\u4E8E ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}</p>
31258
+
31259
+ <div class="cards">
31260
+ <div class="card"><b>${kpis.runs}</b><span>\u8FD0\u884C\u6B21\u6570</span></div>
31261
+ <div class="card"><b>${kpis.executions}</b><span>\u7528\u4F8B\u6267\u884C\u6B21\u6570</span></div>
31262
+ <div class="card"><b>${rateText}</b><span>\u6574\u4F53\u901A\u8FC7\u7387</span></div>
31263
+ <div class="card"><b>${(kpis.avgDurationMs / 1e3).toFixed(1)}s</b><span>\u5E73\u5747\u8FD0\u884C\u8017\u65F6</span></div>
31264
+ <div class="card"><b>${kpis.flakyCount}</b><span>Flaky \u7528\u4F8B</span></div>
31265
+ </div>
31266
+
31267
+ <h2>\u8D8B\u52BF(\u6BCF\u6B21\u8FD0\u884C\u7ED3\u679C\u4E0E\u901A\u8FC7\u7387,\u60AC\u6D6E\u770B\u8BE6\u60C5)</h2>
31268
+ <div class="panel trend">${trendSvg}</div>
31269
+
31270
+ <h2>\u5931\u8D25\u5206\u7C7B\u5206\u5E03(\u70B9\u51FB\u7B5B\u9009\u660E\u7EC6)</h2>
31271
+ <div class="panel">${catHtml}</div>
31272
+
31273
+ <h2>Flaky \u699C(\u53C8\u8FC7\u53C8\u6302;\u70B9\u7528\u4F8B\u540D\u5B9A\u4F4D\u660E\u7EC6)</h2>
31274
+ <div class="panel"><table>
31275
+ <tr><th>\u7528\u4F8B</th><th>\u540D\u79F0</th><th>\u5E73\u53F0</th><th class="num">\u5931\u8D25 / \u53C2\u4E0E</th><th class="num">\u901A\u8FC7\u7387</th><th>\u6700\u8FD1\u72B6\u6001</th></tr>
31276
+ ${flakyRows || '<tr><td colspan="6" class="muted">\u7EDF\u8BA1\u7A97\u53E3\u5185\u65E0 Flaky \u7528\u4F8B</td></tr>'}
31277
+ </table></div>
31278
+
31279
+ <h2>\u7528\u4F8B\u660E\u7EC6</h2>
31280
+ <div class="toolbar">
31281
+ ${platformChips}
31282
+ <input id="q" placeholder="\u641C\u7D22\u7528\u4F8B id / \u540D\u79F0\u2026" />
31283
+ <span id="count"></span>
31284
+ </div>
31285
+ <table id="detail">
31286
+ <thead><tr>
31287
+ <th>\u7528\u4F8B</th><th>\u540D\u79F0</th><th>\u5E73\u53F0</th>
31288
+ <th class="num" data-sort="runs">\u8FD0\u884C\u6B21\u6570</th><th class="num" data-sort="passed">\u901A\u8FC7</th><th class="num" data-sort="failed">\u5931\u8D25</th>
31289
+ <th data-sort="passRate">\u901A\u8FC7\u7387 \u25BE</th><th>\u6700\u8FD1\u5206\u7C7B</th><th>\u6700\u8FD1\u72B6\u6001</th>
31290
+ </tr></thead>
31291
+ <tbody>${detailRows}</tbody>
31292
+ </table>
31293
+ </div>
31294
+
31295
+ <script id="summary-data" type="application/json">${JSON.stringify(data).replaceAll("<", "\\u003c")}</script>
31296
+ <script>
31297
+ (function () {
31298
+ var rows = Array.prototype.slice.call(document.querySelectorAll('#detail tbody tr'));
31299
+ var state = { platform: 'all', cat: 'all', q: '', sortKey: 'passRate', sortDir: -1 };
31300
+
31301
+ function apply() {
31302
+ var shown = 0;
31303
+ rows.forEach(function (row) {
31304
+ var show = true;
31305
+ if (state.platform !== 'all' && row.getAttribute('data-platform').split(' ').indexOf(state.platform) === -1) show = false;
31306
+ if (state.cat !== 'all' && (row.getAttribute('data-cat') || '') !== state.cat) show = false;
31307
+ if (state.q && row.getAttribute('data-q').toLowerCase().indexOf(state.q.toLowerCase()) === -1) show = false;
31308
+ row.classList.toggle('hide', !show);
31309
+ if (show) shown++;
31310
+ });
31311
+ document.getElementById('count').textContent = shown + ' / ' + rows.length + ' \u7528\u4F8B';
31312
+ }
31313
+
31314
+ document.querySelectorAll('.chip[data-platform]').forEach(function (chip) {
31315
+ chip.addEventListener('click', function () {
31316
+ document.querySelectorAll('.chip[data-platform]').forEach(function (c) { c.classList.remove('active'); });
31317
+ chip.classList.add('active');
31318
+ state.platform = chip.getAttribute('data-platform');
31319
+ apply();
31320
+ });
31321
+ });
31322
+ document.querySelectorAll('.hbar-row').forEach(function (bar) {
31323
+ bar.addEventListener('click', function () {
31324
+ state.cat = bar.getAttribute('data-cat');
31325
+ apply();
31326
+ });
31327
+ });
31328
+ var q = document.getElementById('q');
31329
+ q.addEventListener('input', function () { state.q = q.value.trim(); apply(); });
31330
+
31331
+ document.querySelectorAll('#detail th[data-sort]').forEach(function (th) {
31332
+ th.addEventListener('click', function () {
31333
+ var key = th.getAttribute('data-sort');
31334
+ state.sortDir = state.sortKey === key ? -state.sortDir : -1;
31335
+ state.sortKey = key;
31336
+ var sorted = rows.slice().sort(function (a, b) {
31337
+ return (Number(a.getAttribute('data-' + key)) - Number(b.getAttribute('data-' + key))) * state.sortDir;
31338
+ });
31339
+ var tbody = document.querySelector('#detail tbody');
31340
+ sorted.forEach(function (row) { tbody.appendChild(row); });
31341
+ });
31342
+ });
31343
+
31344
+ apply();
31345
+ })();
31346
+ </script>
31347
+ </body>
31348
+ </html>`;
31349
+ }
31350
+ async function writeSummaryReport(artifactsDir, data) {
31351
+ const dir = resolve4(artifactsDir);
31352
+ await mkdir4(dir, { recursive: true });
31353
+ const htmlPath = join9(dir, "summary.html");
31354
+ const jsonPath = join9(dir, "summary.json");
31355
+ await writeFile5(jsonPath, JSON.stringify(data, null, 2), "utf8");
31356
+ await writeFile5(htmlPath, renderSummaryHtml(data), "utf8");
31357
+ return { html: htmlPath, json: jsonPath };
31358
+ }
30813
31359
 
30814
31360
  // ../../packages/sdk/src/runs.ts
30815
- import { readFile as readFile5, readdir as readdir3 } from "node:fs/promises";
30816
- import { isAbsolute as isAbsolute2, join as join15 } from "node:path";
31361
+ import { readFile as readFile6, readdir as readdir4 } from "node:fs/promises";
31362
+ import { isAbsolute as isAbsolute2, join as join17 } from "node:path";
30817
31363
 
30818
31364
  // ../../packages/execution-engine/src/artifacts/index.ts
30819
- import { mkdir as mkdir8 } from "node:fs/promises";
30820
- import { join as join11 } from "node:path";
31365
+ import { mkdir as mkdir9 } from "node:fs/promises";
31366
+ import { join as join13 } from "node:path";
30821
31367
 
30822
31368
  // ../../packages/evidence/src/log.ts
30823
- import { appendFile, mkdir as mkdir4 } from "node:fs/promises";
30824
- import { dirname as dirname2 } from "node:path";
31369
+ import { appendFile, mkdir as mkdir5 } from "node:fs/promises";
31370
+ import { dirname as dirname3 } from "node:path";
30825
31371
  var RunLogger = class {
30826
31372
  constructor(filePath) {
30827
31373
  this.filePath = filePath;
@@ -30836,7 +31382,7 @@ var RunLogger = class {
30836
31382
  return this.write("ERROR", message);
30837
31383
  }
30838
31384
  async write(level, message) {
30839
- await mkdir4(dirname2(this.filePath), { recursive: true });
31385
+ await mkdir5(dirname3(this.filePath), { recursive: true });
30840
31386
  const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${level} ${message}
30841
31387
  `;
30842
31388
  await appendFile(this.filePath, line, "utf8");
@@ -30844,32 +31390,32 @@ var RunLogger = class {
30844
31390
  };
30845
31391
 
30846
31392
  // ../../packages/evidence/src/screenshot.ts
30847
- import { mkdir as mkdir5, writeFile as writeFile5 } from "node:fs/promises";
30848
- import { dirname as dirname3, join as join8 } from "node:path";
31393
+ import { mkdir as mkdir6, writeFile as writeFile6 } from "node:fs/promises";
31394
+ import { dirname as dirname4, join as join10 } from "node:path";
30849
31395
  async function saveScreenshot(dir, fileName, data) {
30850
- const file2 = join8(dir, fileName);
30851
- await mkdir5(dirname3(file2), { recursive: true });
30852
- await writeFile5(file2, data);
31396
+ const file2 = join10(dir, fileName);
31397
+ await mkdir6(dirname4(file2), { recursive: true });
31398
+ await writeFile6(file2, data);
30853
31399
  return file2;
30854
31400
  }
30855
31401
 
30856
31402
  // ../../packages/evidence/src/trace.ts
30857
- import { mkdir as mkdir6, writeFile as writeFile6 } from "node:fs/promises";
30858
- import { dirname as dirname4, join as join9 } from "node:path";
31403
+ import { mkdir as mkdir7, writeFile as writeFile7 } from "node:fs/promises";
31404
+ import { dirname as dirname5, join as join11 } from "node:path";
30859
31405
  async function saveTrace(dir, fileName, data) {
30860
- const file2 = join9(dir, fileName);
30861
- await mkdir6(dirname4(file2), { recursive: true });
30862
- await writeFile6(file2, data);
31406
+ const file2 = join11(dir, fileName);
31407
+ await mkdir7(dirname5(file2), { recursive: true });
31408
+ await writeFile7(file2, data);
30863
31409
  return file2;
30864
31410
  }
30865
31411
 
30866
31412
  // ../../packages/evidence/src/video.ts
30867
- import { mkdir as mkdir7, writeFile as writeFile7 } from "node:fs/promises";
30868
- import { dirname as dirname5, join as join10 } from "node:path";
31413
+ import { mkdir as mkdir8, writeFile as writeFile8 } from "node:fs/promises";
31414
+ import { dirname as dirname6, join as join12 } from "node:path";
30869
31415
  async function saveVideo(dir, fileName, data) {
30870
- const file2 = join10(dir, fileName);
30871
- await mkdir7(dirname5(file2), { recursive: true });
30872
- await writeFile7(file2, data);
31416
+ const file2 = join12(dir, fileName);
31417
+ await mkdir8(dirname6(file2), { recursive: true });
31418
+ await writeFile8(file2, data);
30873
31419
  return file2;
30874
31420
  }
30875
31421
 
@@ -30877,11 +31423,11 @@ async function saveVideo(dir, fileName, data) {
30877
31423
  var ArtifactManager = class {
30878
31424
  constructor(runDir) {
30879
31425
  this.runDir = runDir;
30880
- this.screenshotsDir = join11(runDir, "screenshots");
30881
- this.videosDir = join11(runDir, "videos");
30882
- this.tracesDir = join11(runDir, "traces");
30883
- this.logsDir = join11(runDir, "logs");
30884
- this.logger = new RunLogger(join11(this.logsDir, "run.log"));
31426
+ this.screenshotsDir = join13(runDir, "screenshots");
31427
+ this.videosDir = join13(runDir, "videos");
31428
+ this.tracesDir = join13(runDir, "traces");
31429
+ this.logsDir = join13(runDir, "logs");
31430
+ this.logger = new RunLogger(join13(this.logsDir, "run.log"));
30885
31431
  }
30886
31432
  screenshotsDir;
30887
31433
  videosDir;
@@ -30889,8 +31435,8 @@ var ArtifactManager = class {
30889
31435
  logsDir;
30890
31436
  logger;
30891
31437
  async prepare() {
30892
- await mkdir8(this.screenshotsDir, { recursive: true });
30893
- await mkdir8(this.logsDir, { recursive: true });
31438
+ await mkdir9(this.screenshotsDir, { recursive: true });
31439
+ await mkdir9(this.logsDir, { recursive: true });
30894
31440
  }
30895
31441
  /** 保存步骤截图,返回相对 run 目录的路径(screenshots/xxx.png,统一正斜杠供报告链接使用) */
30896
31442
  async saveStepScreenshot(fileName, data) {
@@ -30954,8 +31500,8 @@ var ExecutionContext = class {
30954
31500
  };
30955
31501
 
30956
31502
  // ../../packages/execution-engine/src/lifecycle/index.ts
30957
- import { mkdir as mkdir9, readdir as readdir2 } from "node:fs/promises";
30958
- import { join as join12 } from "node:path";
31503
+ import { mkdir as mkdir10, readdir as readdir3 } from "node:fs/promises";
31504
+ import { join as join14 } from "node:path";
30959
31505
  function dateStamp(date5 = /* @__PURE__ */ new Date()) {
30960
31506
  const year = date5.getFullYear();
30961
31507
  const month = String(date5.getMonth() + 1).padStart(2, "0");
@@ -30964,7 +31510,7 @@ function dateStamp(date5 = /* @__PURE__ */ new Date()) {
30964
31510
  }
30965
31511
  async function allocateRunId(runsRoot2) {
30966
31512
  const stamp = dateStamp();
30967
- const existing = await readdir2(runsRoot2).catch(() => []);
31513
+ const existing = await readdir3(runsRoot2).catch(() => []);
30968
31514
  let max = 0;
30969
31515
  for (const name of existing) {
30970
31516
  const match = /^(\d{8})-(\d{3})$/.exec(name);
@@ -30975,14 +31521,14 @@ async function allocateRunId(runsRoot2) {
30975
31521
  return `${stamp}-${String(max + 1).padStart(3, "0")}`;
30976
31522
  }
30977
31523
  async function ensureRunDir(runsRoot2, runId) {
30978
- const runDir = join12(runsRoot2, runId);
30979
- await mkdir9(runDir, { recursive: true });
31524
+ const runDir = join14(runsRoot2, runId);
31525
+ await mkdir10(runDir, { recursive: true });
30980
31526
  return runDir;
30981
31527
  }
30982
31528
 
30983
31529
  // ../../packages/execution-engine/src/events/index.ts
30984
- import { appendFile as appendFile2, mkdir as mkdir10 } from "node:fs/promises";
30985
- import { dirname as dirname6 } from "node:path";
31530
+ import { appendFile as appendFile2, mkdir as mkdir11 } from "node:fs/promises";
31531
+ import { dirname as dirname7 } from "node:path";
30986
31532
  var EventBus = class {
30987
31533
  subscribers = [];
30988
31534
  subscribe(subscriber) {
@@ -30996,7 +31542,7 @@ var EventBus = class {
30996
31542
  };
30997
31543
  function ndjsonSubscriber(filePath) {
30998
31544
  return async (event) => {
30999
- await mkdir10(dirname6(filePath), { recursive: true });
31545
+ await mkdir11(dirname7(filePath), { recursive: true });
31000
31546
  await appendFile2(filePath, `${JSON.stringify(event)}
31001
31547
  `, "utf8");
31002
31548
  };
@@ -31352,8 +31898,8 @@ function buildRunSummary(runId, startedAt, cases, options = {}) {
31352
31898
  }
31353
31899
 
31354
31900
  // ../../packages/execution-engine/src/engine/test-runner.ts
31355
- import { writeFile as writeFile8 } from "node:fs/promises";
31356
- import { join as join13, resolve as resolve4 } from "node:path";
31901
+ import { writeFile as writeFile9 } from "node:fs/promises";
31902
+ import { join as join15, resolve as resolve5 } from "node:path";
31357
31903
  var TestRunner = class {
31358
31904
  resolver;
31359
31905
  runsRoot;
@@ -31363,7 +31909,7 @@ var TestRunner = class {
31363
31909
  accounts;
31364
31910
  constructor(options) {
31365
31911
  this.resolver = options.resolver;
31366
- this.runsRoot = resolve4(options.runsRoot ?? join13(process.cwd(), ".testpilot", "artifacts", "runs"));
31912
+ this.runsRoot = resolve5(options.runsRoot ?? join15(process.cwd(), ".testpilot", "artifacts", "runs"));
31367
31913
  this.customRunId = options.runId;
31368
31914
  this.extraSubscriber = options.onEvent;
31369
31915
  this.signal = options.signal;
@@ -31376,7 +31922,7 @@ var TestRunner = class {
31376
31922
  const artifacts = new ArtifactManager(runDir);
31377
31923
  await artifacts.prepare();
31378
31924
  const bus = new EventBus();
31379
- bus.subscribe(ndjsonSubscriber(join13(runDir, "events.ndjson")));
31925
+ bus.subscribe(ndjsonSubscriber(join15(runDir, "events.ndjson")));
31380
31926
  if (this.extraSubscriber) bus.subscribe(this.extraSubscriber);
31381
31927
  await bus.emit({ type: "run-started", runId, totalCases: cases.length, ts: startedAt.toISOString() });
31382
31928
  await artifacts.logger.info(`run ${runId} started with ${cases.length} case(s)`);
@@ -31409,7 +31955,7 @@ var TestRunner = class {
31409
31955
  ts: (/* @__PURE__ */ new Date()).toISOString()
31410
31956
  });
31411
31957
  await artifacts.logger.info(`run ${runId} finished: ${summary.status}`);
31412
- await writeFile8(join13(runDir, RESULT_FILE), JSON.stringify(summary, null, 2), "utf8");
31958
+ await writeFile9(join15(runDir, RESULT_FILE), JSON.stringify(summary, null, 2), "utf8");
31413
31959
  return summary;
31414
31960
  }
31415
31961
  };
@@ -31443,9 +31989,9 @@ async function openCaseContext(bundle, options, evidenceTmpDir) {
31443
31989
 
31444
31990
  // ../../packages/adapter-playwright/src/page.ts
31445
31991
  import { existsSync as existsSync2 } from "node:fs";
31446
- import { mkdtemp, readFile as readFile4, rm } from "node:fs/promises";
31992
+ import { mkdtemp, readFile as readFile5, rm } from "node:fs/promises";
31447
31993
  import { tmpdir } from "node:os";
31448
- import { join as join14 } from "node:path";
31994
+ import { join as join16 } from "node:path";
31449
31995
  var PlaywrightAdapter = class {
31450
31996
  constructor(bundle, options = {}) {
31451
31997
  this.bundle = bundle;
@@ -31459,7 +32005,7 @@ var PlaywrightAdapter = class {
31459
32005
  /** 开始用例级取证:为本用例创建独立 context(录屏 + trace) */
31460
32006
  async startEvidence(_caseId) {
31461
32007
  await this.closeContextQuietly();
31462
- const evidenceTmpDir = await mkdtemp(join14(tmpdir(), "testpilot-pw-"));
32008
+ const evidenceTmpDir = await mkdtemp(join16(tmpdir(), "testpilot-pw-"));
31463
32009
  this.current = {
31464
32010
  ...await openCaseContext(this.bundle, this.options, evidenceTmpDir),
31465
32011
  evidenceTmpDir
@@ -31470,12 +32016,12 @@ var PlaywrightAdapter = class {
31470
32016
  if (!this.current) return {};
31471
32017
  const { context, page, evidenceTmpDir } = this.current;
31472
32018
  this.current = void 0;
31473
- const traceZipPath = join14(evidenceTmpDir ?? tmpdir(), "trace.zip");
32019
+ const traceZipPath = join16(evidenceTmpDir ?? tmpdir(), "trace.zip");
31474
32020
  await context.tracing.stop({ path: traceZipPath }).catch(() => void 0);
31475
32021
  const recordedVideoPath = await page.video()?.path().catch(() => void 0);
31476
32022
  await context.close().catch(() => void 0);
31477
- const video = recordedVideoPath && existsSync2(recordedVideoPath) ? await readFile4(recordedVideoPath) : void 0;
31478
- const trace = evidenceTmpDir && existsSync2(traceZipPath) ? await readFile4(traceZipPath) : void 0;
32023
+ const video = recordedVideoPath && existsSync2(recordedVideoPath) ? await readFile5(recordedVideoPath) : void 0;
32024
+ const trace = evidenceTmpDir && existsSync2(traceZipPath) ? await readFile5(traceZipPath) : void 0;
31479
32025
  if (evidenceTmpDir) await rm(evidenceTmpDir, { recursive: true, force: true }).catch(() => void 0);
31480
32026
  return { video, trace };
31481
32027
  }
@@ -31579,7 +32125,7 @@ var automatorLauncher = {
31579
32125
  }
31580
32126
  };
31581
32127
  function delay(ms) {
31582
- return new Promise((resolve5) => setTimeout(resolve5, ms));
32128
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
31583
32129
  }
31584
32130
  async function connectWithRetry(wsEndpoint, timeoutMs) {
31585
32131
  const startedAt = Date.now();
@@ -31939,7 +32485,7 @@ function createDefaultAdapters(config2) {
31939
32485
  async function runCases(options = {}) {
31940
32486
  const root = resolveRoot(options.root);
31941
32487
  const config2 = await loadProjectConfig(root);
31942
- const inputs = options.paths && options.paths.length > 0 ? options.paths.map((input2) => isAbsolute2(input2) ? input2 : join15(root, input2)) : [join15(root, config2.casesDir)];
32488
+ const inputs = options.paths && options.paths.length > 0 ? options.paths.map((input2) => isAbsolute2(input2) ? input2 : join17(root, input2)) : [join17(root, config2.casesDir)];
31943
32489
  const cases = await collectCases(inputs, { root });
31944
32490
  if (cases.length === 0) {
31945
32491
  throw new RunError("ENO_CASES", `\u672A\u627E\u5230\u7528\u4F8B\u6587\u4EF6(\u67E5\u627E:${inputs.join(", ")})`);
@@ -31965,7 +32511,7 @@ async function runCases(options = {}) {
31965
32511
  ];
31966
32512
  const runner = new TestRunner({
31967
32513
  resolver,
31968
- runsRoot: join15(root, RUNS_DIR),
32514
+ runsRoot: join17(root, RUNS_DIR),
31969
32515
  runId: options.runId,
31970
32516
  onEvent: options.onEvent,
31971
32517
  signal: options.signal,
@@ -31977,14 +32523,14 @@ async function runCases(options = {}) {
31977
32523
  return { summary, invalid, tagFiltered, missingAccounts };
31978
32524
  }
31979
32525
  function runsRoot(root) {
31980
- return join15(resolveRoot(root), RUNS_DIR);
32526
+ return join17(resolveRoot(root), RUNS_DIR);
31981
32527
  }
31982
32528
  async function listRuns(options = {}) {
31983
32529
  const base = runsRoot(options.root);
31984
- const entries = await readdir3(base).catch(() => []);
32530
+ const entries = await readdir4(base).catch(() => []);
31985
32531
  const metas = [];
31986
32532
  for (const entry of entries.filter((name) => /^\d{8}-\d{3}$/.test(name)).sort().reverse()) {
31987
- metas.push({ ...await readRunMeta(base, entry), runId: entry, dir: join15(base, entry) });
32533
+ metas.push({ ...await readRunMeta(base, entry), runId: entry, dir: join17(base, entry) });
31988
32534
  }
31989
32535
  return metas;
31990
32536
  }
@@ -31994,22 +32540,22 @@ async function latestRunId(options = {}) {
31994
32540
  }
31995
32541
  async function getRun(runId, options = {}) {
31996
32542
  const base = runsRoot(options.root);
31997
- const file2 = join15(base, runId, RESULT_FILE);
32543
+ const file2 = join17(base, runId, RESULT_FILE);
31998
32544
  try {
31999
- return JSON.parse(await readFile5(file2, "utf8"));
32545
+ return JSON.parse(await readFile6(file2, "utf8"));
32000
32546
  } catch {
32001
32547
  throw new RunError("ENO_CASES", `\u8BFB\u53D6\u8FD0\u884C\u7ED3\u679C\u5931\u8D25:${file2}(\u8FD0\u884C\u672A\u5B8C\u6210\u6216 runId \u4E0D\u5B58\u5728)`);
32002
32548
  }
32003
32549
  }
32004
32550
  async function getRunEvents(runId, options = {}) {
32005
- const file2 = join15(runsRoot(options.root), runId, "events.ndjson");
32006
- const source = await readFile5(file2, "utf8").catch(() => "");
32551
+ const file2 = join17(runsRoot(options.root), runId, "events.ndjson");
32552
+ const source = await readFile6(file2, "utf8").catch(() => "");
32007
32553
  return source.split("\n").filter((line) => line.trim().length > 0).map((line) => JSON.parse(line));
32008
32554
  }
32009
32555
  async function readRunMeta(base, runId) {
32010
32556
  try {
32011
32557
  const summary = JSON.parse(
32012
- await readFile5(join15(base, runId, RESULT_FILE), "utf8")
32558
+ await readFile6(join17(base, runId, RESULT_FILE), "utf8")
32013
32559
  );
32014
32560
  return {
32015
32561
  status: summary.status,
@@ -32030,25 +32576,47 @@ async function generateReport(runId, options = {}) {
32030
32576
  if (!id) {
32031
32577
  throw new Error("\u672A\u627E\u5230\u8FD0\u884C\u8BB0\u5F55(\u5148\u8FD0\u884C\u4E00\u6B21\u7528\u4F8B)");
32032
32578
  }
32033
- const runDir = join16(resolveRoot(options.root), RUNS_DIR, id);
32579
+ const runDir = join18(resolveRoot(options.root), RUNS_DIR, id);
32034
32580
  let summary;
32035
32581
  try {
32036
- summary = JSON.parse(await readFile6(join16(runDir, "result.json"), "utf8"));
32582
+ summary = JSON.parse(await readFile7(join18(runDir, "result.json"), "utf8"));
32037
32583
  } catch {
32038
- throw new Error(`\u8BFB\u53D6\u8FD0\u884C\u7ED3\u679C\u5931\u8D25:${join16(runDir, "result.json")}(\u8FD0\u884C\u672A\u5B8C\u6210\u6216 runId \u4E0D\u5B58\u5728)`);
32584
+ throw new Error(`\u8BFB\u53D6\u8FD0\u884C\u7ED3\u679C\u5931\u8D25:${join18(runDir, "result.json")}(\u8FD0\u884C\u672A\u5B8C\u6210\u6216 runId \u4E0D\u5B58\u5728)`);
32039
32585
  }
32040
32586
  const written = await writeReports(runDir, summary);
32041
32587
  return { runId: id, json: written.json, html: written.html };
32042
32588
  }
32589
+ async function generateSummaryReport(options = {}) {
32590
+ const root = resolveRoot(options.root);
32591
+ const limit = options.last && options.last > 0 ? options.last : 30;
32592
+ const metas = await listRuns({ root });
32593
+ if (metas.length === 0) {
32594
+ throw new Error("\u7EDF\u8BA1\u7A97\u53E3\u5185\u6CA1\u6709\u4EFB\u4F55\u8FD0\u884C\u8BB0\u5F55(\u5148\u8FD0\u884C npx csspilot run)");
32595
+ }
32596
+ const runs = [];
32597
+ for (const meta3 of metas.slice(0, limit)) {
32598
+ try {
32599
+ runs.push({ runId: meta3.runId, summary: await getRun(meta3.runId, { root }) });
32600
+ } catch {
32601
+ }
32602
+ }
32603
+ if (runs.length === 0) {
32604
+ throw new Error("\u7EDF\u8BA1\u7A97\u53E3\u5185\u7684\u8FD0\u884C\u7ED3\u679C\u5747\u4E0D\u53EF\u8BFB");
32605
+ }
32606
+ const data = buildSummaryData(runs);
32607
+ const artifactsDir = join18(root, ".testpilot", "artifacts");
32608
+ const written = await writeSummaryReport(artifactsDir, data);
32609
+ return { html: written.html, json: written.json, data, window: { runs: runs.length } };
32610
+ }
32043
32611
  async function readReport(runId, options = {}) {
32044
- const file2 = join16(resolveRoot(options.root), RUNS_DIR, runId, "report.json");
32045
- const source = await readFile6(file2, "utf8").catch(() => void 0);
32612
+ const file2 = join18(resolveRoot(options.root), RUNS_DIR, runId, "report.json");
32613
+ const source = await readFile7(file2, "utf8").catch(() => void 0);
32046
32614
  return source ? JSON.parse(source) : void 0;
32047
32615
  }
32048
32616
 
32049
32617
  // ../../packages/sdk/src/client.ts
32050
32618
  function resolveInput(root, input2) {
32051
- return isAbsolute3(input2) ? input2 : join17(root, input2);
32619
+ return isAbsolute3(input2) ? input2 : join19(root, input2);
32052
32620
  }
32053
32621
  var TestPilotClient = class {
32054
32622
  /** 业务项目根目录(绝对路径) */
@@ -32113,6 +32681,10 @@ var TestPilotClient = class {
32113
32681
  generateReport(runId) {
32114
32682
  return generateReport(runId, { root: this.root });
32115
32683
  }
32684
+ /** 生成跨 run 汇总报告(summary.html + summary.json) */
32685
+ generateSummaryReport(options = {}) {
32686
+ return generateSummaryReport({ ...options, root: this.root });
32687
+ }
32116
32688
  /** 读取已生成的报告 */
32117
32689
  readReport(runId) {
32118
32690
  return readReport(runId, { root: this.root });
@@ -32121,12 +32693,12 @@ var TestPilotClient = class {
32121
32693
 
32122
32694
  // src/commands/init/adapter-detector.ts
32123
32695
  import { existsSync as existsSync4, readFileSync } from "node:fs";
32124
- import { join as join18 } from "node:path";
32696
+ import { join as join20 } from "node:path";
32125
32697
  async function detectAdapters(root, projectType) {
32126
32698
  const required2 = projectType === "hybrid" ? ["playwright", "wechatide"] : projectType === "wechat-miniapp" ? ["wechatide"] : projectType === "web" ? ["playwright"] : [];
32127
32699
  const pkg = readPackageJsonDeps(root);
32128
32700
  const hasPlaywrightDep = pkg !== void 0 && ("playwright" in pkg || "@playwright/test" in pkg);
32129
- const playwrightInstalled = hasPlaywrightDep || existsSync4(join18(root, "node_modules", "playwright"));
32701
+ const playwrightInstalled = hasPlaywrightDep || existsSync4(join20(root, "node_modules", "playwright"));
32130
32702
  const devtoolsCli = await findWechatDevToolsCli().catch(() => void 0);
32131
32703
  return [
32132
32704
  {
@@ -32144,7 +32716,7 @@ async function detectAdapters(root, projectType) {
32144
32716
  ];
32145
32717
  }
32146
32718
  function readPackageJsonDeps(root) {
32147
- const file2 = join18(root, "package.json");
32719
+ const file2 = join20(root, "package.json");
32148
32720
  if (!existsSync4(file2)) return void 0;
32149
32721
  try {
32150
32722
  const pkg = JSON.parse(readFileSync(file2, "utf8"));
@@ -32155,26 +32727,30 @@ function readPackageJsonDeps(root) {
32155
32727
  }
32156
32728
 
32157
32729
  // src/commands/init/environment-initializer.ts
32158
- import { readFile as readFile7 } from "node:fs/promises";
32159
- import { join as join19 } from "node:path";
32730
+ import { readFile as readFile8 } from "node:fs/promises";
32731
+ import { join as join21 } from "node:path";
32160
32732
  var ENV_REF_PATTERN = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
32161
32733
  async function collectEnvVarRefs(root) {
32162
32734
  let text;
32163
32735
  try {
32164
- text = await readFile7(join19(root, "testpilot.yaml"), "utf8");
32736
+ text = await readFile8(join21(root, "testpilot.yaml"), "utf8");
32165
32737
  } catch {
32166
32738
  return [];
32167
32739
  }
32168
32740
  const names = /* @__PURE__ */ new Set();
32169
- for (const match of text.matchAll(ENV_REF_PATTERN)) {
32170
- names.add(match[1]);
32741
+ for (const line of text.split(/\r?\n/)) {
32742
+ const trimmed = line.trim();
32743
+ if (trimmed.startsWith("#")) continue;
32744
+ for (const match of trimmed.matchAll(ENV_REF_PATTERN)) {
32745
+ names.add(match[1]);
32746
+ }
32171
32747
  }
32172
32748
  return [...names].sort();
32173
32749
  }
32174
32750
 
32175
32751
  // src/commands/init/project-detector.ts
32176
32752
  import { existsSync as existsSync5, readFileSync as readFileSync2 } from "node:fs";
32177
- import { basename, join as join20 } from "node:path";
32753
+ import { basename, join as join22 } from "node:path";
32178
32754
 
32179
32755
  // src/lib/git.ts
32180
32756
  import { execFile as execFile2 } from "node:child_process";
@@ -32211,7 +32787,7 @@ var WEB_DEP_MARKERS = [
32211
32787
  ];
32212
32788
  var E2E_DIR_CANDIDATES = ["tests/e2e", "test/e2e", "e2e", "cypress"];
32213
32789
  function readPackageJson(root) {
32214
- const file2 = join20(root, "package.json");
32790
+ const file2 = join22(root, "package.json");
32215
32791
  if (!existsSync5(file2)) return void 0;
32216
32792
  try {
32217
32793
  return JSON.parse(readFileSync2(file2, "utf8"));
@@ -32222,8 +32798,8 @@ function readPackageJson(root) {
32222
32798
  function detectProjectType(root) {
32223
32799
  const pkg = readPackageJson(root);
32224
32800
  const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
32225
- const isWechat = existsSync5(join20(root, "project.config.json")) || existsSync5(join20(root, "app.json")) || existsSync5(join20(root, "miniprogram", "app.json"));
32226
- const isWeb = existsSync5(join20(root, "index.html")) || WEB_DEP_MARKERS.some((marker) => deps?.[marker] !== void 0) || [".ts", ".tsx", ".js", ".jsx"].some((ext) => existsSync5(join20(root, "src", `index${ext}`)));
32801
+ const isWechat = existsSync5(join22(root, "project.config.json")) || existsSync5(join22(root, "app.json")) || existsSync5(join22(root, "miniprogram", "app.json"));
32802
+ const isWeb = existsSync5(join22(root, "index.html")) || WEB_DEP_MARKERS.some((marker) => deps?.[marker] !== void 0) || [".ts", ".tsx", ".js", ".jsx"].some((ext) => existsSync5(join22(root, "src", `index${ext}`)));
32227
32803
  if (isWechat && isWeb) return "hybrid";
32228
32804
  if (isWechat) return "wechat-miniapp";
32229
32805
  if (isWeb) return "web";
@@ -32232,9 +32808,9 @@ function detectProjectType(root) {
32232
32808
  async function detectProject(root) {
32233
32809
  const pkg = readPackageJson(root);
32234
32810
  const type = detectProjectType(root);
32235
- const language = existsSync5(join20(root, "tsconfig.json")) ? "TypeScript" : pkg || existsSync5(join20(root, "src")) ? "JavaScript" : "Unknown";
32236
- const packageManager = existsSync5(join20(root, "pnpm-lock.yaml")) ? "pnpm" : existsSync5(join20(root, "yarn.lock")) ? "yarn" : existsSync5(join20(root, "package-lock.json")) ? "npm" : "npx";
32237
- const existingE2eDirs = E2E_DIR_CANDIDATES.filter((dir) => existsSync5(join20(root, dir)));
32811
+ const language = existsSync5(join22(root, "tsconfig.json")) ? "TypeScript" : pkg || existsSync5(join22(root, "src")) ? "JavaScript" : "Unknown";
32812
+ const packageManager = existsSync5(join22(root, "pnpm-lock.yaml")) ? "pnpm" : existsSync5(join22(root, "yarn.lock")) ? "yarn" : existsSync5(join22(root, "package-lock.json")) ? "npm" : "npx";
32813
+ const existingE2eDirs = E2E_DIR_CANDIDATES.filter((dir) => existsSync5(join22(root, dir)));
32238
32814
  return {
32239
32815
  root,
32240
32816
  name: pkg?.name ?? basename(root),
@@ -32244,8 +32820,8 @@ async function detectProject(root) {
32244
32820
  git: await readGitInfo(root),
32245
32821
  hasTestsE2e: existingE2eDirs.includes("tests/e2e"),
32246
32822
  existingE2eDirs,
32247
- hasTestpilotConfig: existsSync5(join20(root, "testpilot.yaml")),
32248
- hasProjectLink: existsSync5(join20(root, ".testpilot", "project.json"))
32823
+ hasTestpilotConfig: existsSync5(join22(root, "testpilot.yaml")),
32824
+ hasProjectLink: existsSync5(join22(root, ".testpilot", "project.json"))
32249
32825
  };
32250
32826
  }
32251
32827
  function defaultAdapterFor(type) {
@@ -32259,6 +32835,144 @@ function adaptersFor(type) {
32259
32835
  return ["playwright"];
32260
32836
  }
32261
32837
 
32838
+ // src/commands/init/skill-installer.ts
32839
+ import { existsSync as existsSync6, readFileSync as readFileSync3 } from "node:fs";
32840
+ import { access as access2, cp, mkdir as mkdir12, writeFile as writeFile10 } from "node:fs/promises";
32841
+ import { dirname as dirname8, join as join23 } from "node:path";
32842
+ import { fileURLToPath } from "node:url";
32843
+ var CORE_FILES = ["SKILL.md", "manifest.yaml"];
32844
+ var CORE_DIRS = ["rules", "workflows"];
32845
+ async function exists3(path) {
32846
+ try {
32847
+ await access2(path);
32848
+ return true;
32849
+ } catch {
32850
+ return false;
32851
+ }
32852
+ }
32853
+ function readManifestVersion(dir) {
32854
+ const manifestPath = join23(dir, "manifest.yaml");
32855
+ if (!existsSync6(manifestPath)) return void 0;
32856
+ return /^version:\s*([^\s#]+)/m.exec(readFileSync3(manifestPath, "utf8"))?.[1];
32857
+ }
32858
+ function resolveSkillSourceDir() {
32859
+ const fromEnv = process.env.TESTPILOT_SKILL_DIR;
32860
+ if (fromEnv) return fromEnv;
32861
+ const bundled = fileURLToPath(new URL("./skills/testpilot", import.meta.url));
32862
+ if (existsSync6(bundled)) return bundled;
32863
+ const inRepo = fileURLToPath(new URL("../../../../../skills/testpilot", import.meta.url));
32864
+ return existsSync6(inRepo) ? inRepo : void 0;
32865
+ }
32866
+ async function copyCoreFiles(src, dest) {
32867
+ for (const name of CORE_FILES) {
32868
+ await cp(join23(src, name), join23(dest, name), { recursive: true, force: true });
32869
+ }
32870
+ for (const dir of CORE_DIRS) {
32871
+ if (existsSync6(join23(src, dir))) {
32872
+ await cp(join23(src, dir), join23(dest, dir), { recursive: true, force: true });
32873
+ }
32874
+ }
32875
+ }
32876
+ async function syncSkillFiles(root, options = {}) {
32877
+ const dest = join23(root, ".agents", "skills", "testpilot");
32878
+ const bundledVersion = readManifestVersion(resolveSkillSourceDir() ?? "");
32879
+ if (!await exists3(join23(dest, "manifest.yaml"))) {
32880
+ const src = resolveSkillSourceDir();
32881
+ if (!src) {
32882
+ throw new Error(
32883
+ "\u65E0\u6CD5\u5B9A\u4F4D TestPilot Skill \u6E90\u76EE\u5F55;\u53EF\u901A\u8FC7\u73AF\u5883\u53D8\u91CF TESTPILOT_SKILL_DIR \u6307\u5B9A skills/testpilot \u8DEF\u5F84"
32884
+ );
32885
+ }
32886
+ await mkdir12(dirname8(dest), { recursive: true });
32887
+ await cp(src, dest, { recursive: true });
32888
+ return { core: "created", bundledVersion, dest };
32889
+ }
32890
+ const installedVersion = readManifestVersion(dest);
32891
+ const bundled = resolveSkillSourceDir();
32892
+ if (!bundled) {
32893
+ return { core: "current", installedVersion, dest };
32894
+ }
32895
+ if (!options.force && installedVersion === bundledVersion) {
32896
+ return { core: "current", installedVersion, bundledVersion, dest };
32897
+ }
32898
+ await copyCoreFiles(bundled, dest);
32899
+ return {
32900
+ core: "updated",
32901
+ installedVersion,
32902
+ bundledVersion,
32903
+ dest
32904
+ };
32905
+ }
32906
+ async function installSkill(root, info, adapters) {
32907
+ const dest = join23(root, ".agents", "skills", "testpilot");
32908
+ const sync = await syncSkillFiles(root);
32909
+ const core = sync.core === "created" ? "created" : sync.core === "updated" ? "updated" : "reused";
32910
+ const coreVersion = sync.core === "updated" || sync.core === "created" ? { from: sync.installedVersion, to: sync.bundledVersion } : void 0;
32911
+ const config2 = await loadTestpilotConfig(root).catch(() => void 0);
32912
+ const defaultEnvironment = typeof config2?.environment?.default === "string" ? config2.environment.default : "\u672A\u8BBE\u7F6E";
32913
+ const defaultWorkspace = config2?.workspace?.default ?? "\u672A\u8BBE\u7F6E(\u53EF\u5728 yaml workspace.default \u914D\u7F6E)";
32914
+ const refsDir = join23(dest, "references");
32915
+ const files = [
32916
+ ["project.md", () => renderProjectMd(info, adapters, defaultEnvironment, defaultWorkspace)],
32917
+ ["test-conventions.md", renderTestConventionsMd],
32918
+ ["adapters.md", () => renderAdaptersMd(adapters)]
32919
+ ];
32920
+ let references = "reused";
32921
+ for (const [name, render] of files) {
32922
+ const file2 = join23(refsDir, name);
32923
+ if (await exists3(file2)) continue;
32924
+ await mkdir12(refsDir, { recursive: true });
32925
+ await writeFile10(file2, render(), "utf8");
32926
+ references = "created";
32927
+ }
32928
+ return coreVersion ? { core, coreVersion, references, dest } : { core, references, dest };
32929
+ }
32930
+ function renderProjectMd(info, adapters, defaultEnvironment, defaultWorkspace) {
32931
+ const available = adapters.filter((item) => item.available).map((item) => item.id);
32932
+ return `# Project
32933
+
32934
+ \u81EA\u52A8\u751F\u6210\u4E8E \`csspilot init\`,\u63CF\u8FF0\u672C\u9879\u76EE\u7684 TestPilot \u63A5\u5165\u4FE1\u606F;Agent \u4F18\u5148\u4ECE\u8FD9\u91CC\u4E86\u89E3\u9879\u76EE\u3002
32935
+
32936
+ - Project name: ${info.name}
32937
+ - Project type: ${info.type}
32938
+ - Language: ${info.language}
32939
+ - Package manager: ${info.packageManager}
32940
+ - Test directory: tests/e2e
32941
+ - Case directory: tests/e2e/cases
32942
+ - Available adapters: ${available.length > 0 ? available.join(", ") : "(none detected)"}
32943
+ - Default environment: ${defaultEnvironment}
32944
+ - Default workspace: ${defaultWorkspace}
32945
+ `;
32946
+ }
32947
+ function renderTestConventionsMd() {
32948
+ return `# Test Conventions
32949
+
32950
+ \u672C\u9879\u76EE E2E \u6D4B\u8BD5\u7EA6\u5B9A,Agent \u751F\u6210/\u4FEE\u6539 Case \u65F6\u5FC5\u987B\u9075\u5B88:
32951
+
32952
+ 1. E2E Case \u4F4D\u4E8E tests/e2e/cases
32953
+ 2. Case \u4F7F\u7528 TestPilot DSL
32954
+ 3. \u4E0D\u5141\u8BB8\u76F4\u63A5\u8C03\u7528 Playwright
32955
+ 4. \u4E0D\u5141\u8BB8\u76F4\u63A5\u8C03\u7528 WeChatIDE
32956
+ 5. \u4E0D\u5141\u8BB8\u5728 Case \u4E2D\u5199\u660E\u6587\u5BC6\u7801(\u7528 accountRef \u5F15\u7528)
32957
+ 6. \u4F18\u5148\u590D\u7528\u5DF2\u6709 Case
32958
+ 7. \u65B0\u589E Case \u5FC5\u987B\u901A\u8FC7 csspilot validate
32959
+ 8. Case ID \u5FC5\u987B\u552F\u4E00
32960
+ `;
32961
+ }
32962
+ function renderAdaptersMd(adapters) {
32963
+ const lines = adapters.map(
32964
+ (item) => `- ${item.id}: ${item.available ? "\u53EF\u7528" : "\u4E0D\u53EF\u7528"}${item.required ? "(\u9879\u76EE\u9700\u8981)" : "(\u53EF\u9009)"} \u2014 ${item.detail}`
32965
+ );
32966
+ return `# Adapters
32967
+
32968
+ \u6267\u884C\u7AEF\u68C0\u6D4B\u7ED3\u679C(\u4EE5 \`csspilot doctor\` \u6700\u65B0\u8F93\u51FA\u4E3A\u51C6):
32969
+
32970
+ ${lines.join("\n")}
32971
+
32972
+ Agent \u6C38\u8FDC\u4E0D\u76F4\u63A5\u64CD\u4F5C Adapter;Case \u7ECF Execution Engine \u8C03\u5EA6\u5230\u5BF9\u5E94 Adapter \u6267\u884C\u3002
32973
+ `;
32974
+ }
32975
+
32262
32976
  // src/commands/init/doctor.ts
32263
32977
  async function runDoctor(root) {
32264
32978
  const sections = [];
@@ -32283,25 +32997,37 @@ async function runDoctor(root) {
32283
32997
  const projectItems = [
32284
32998
  ok(`Project ${info.name}(${info.type})`),
32285
32999
  info.git.commit ? ok(`Git repository(${info.git.branch ?? "detached"} \xB7 ${info.git.commit.slice(0, 7)})`) : warn("Git repository \u672A\u68C0\u6D4B\u5230", "Case Metadata \u65E0\u6CD5\u540C\u6B65\u5230 Server"),
32286
- existsSync6(join21(root, ".testpilot", "project.json")) ? ok(".testpilot/project.json") : error62(".testpilot/project.json", "\u8FD0\u884C npx csspilot init \u751F\u6210"),
32287
- existsSync6(join21(root, "testpilot.yaml")) ? ok("testpilot.yaml") : error62("testpilot.yaml", "\u8FD0\u884C npx csspilot init \u751F\u6210")
33000
+ existsSync7(join24(root, ".testpilot", "project.json")) ? ok(".testpilot/project.json") : error62(".testpilot/project.json", "\u8FD0\u884C npx csspilot init \u751F\u6210"),
33001
+ existsSync7(join24(root, "testpilot.yaml")) ? ok("testpilot.yaml") : error62("testpilot.yaml", "\u8FD0\u884C npx csspilot init \u751F\u6210")
32288
33002
  ];
32289
33003
  push("Project", projectItems);
32290
33004
  const config2 = await loadTestpilotConfig(root).catch(() => void 0);
32291
33005
  const casesDir = config2?.casesDir ?? "tests/e2e/cases";
32292
- const cases = await collectCases([join21(root, casesDir)], { root }).catch(() => []);
33006
+ const cases = await collectCases([join24(root, casesDir)], { root }).catch(() => []);
32293
33007
  const invalidCount = cases.filter((item) => !item.valid).length;
32294
- const testDir = join21(root, "tests", "e2e");
33008
+ const testDir = join24(root, "tests", "e2e");
32295
33009
  push("Test", [
32296
- existsSync6(testDir) ? ok("tests/e2e") : error62("tests/e2e", "\u8FD0\u884C npx csspilot init \u521B\u5EFA"),
32297
- existsSync6(join21(root, casesDir)) ? ok(`cases directory(${casesDir})`) : error62(`cases directory(${casesDir})`, "\u8FD0\u884C npx csspilot init \u521B\u5EFA"),
33010
+ existsSync7(testDir) ? ok("tests/e2e") : error62("tests/e2e", "\u8FD0\u884C npx csspilot init \u521B\u5EFA"),
33011
+ existsSync7(join24(root, casesDir)) ? ok(`cases directory(${casesDir})`) : error62(`cases directory(${casesDir})`, "\u8FD0\u884C npx csspilot init \u521B\u5EFA"),
32298
33012
  cases.length === 0 ? warn("Case validation \u8DF3\u8FC7(\u672A\u627E\u5230\u7528\u4F8B)", "\u5728 tests/e2e/cases \u521B\u5EFA Case") : invalidCount === 0 ? ok(`Case validation(${cases.length} \u4E2A\u7528\u4F8B\u5168\u90E8\u6709\u6548)`) : error62(`Case validation(${invalidCount}/${cases.length} \u4E2A\u7528\u4F8B\u6821\u9A8C\u5931\u8D25)`, "\u8FD0\u884C npx csspilot validate \u67E5\u770B\u8BE6\u60C5")
32299
33013
  ]);
32300
- const skillDir = join21(root, ".agents", "skills", "testpilot");
32301
- push("Agent", [
32302
- existsSync6(join21(skillDir, "manifest.yaml")) ? ok("TestPilot Skill(.agents/skills/testpilot)") : error62("TestPilot Skill", "\u8FD0\u884C npx csspilot init \u5B89\u88C5"),
32303
- existsSync6(join21(skillDir, "references", "project.md")) ? ok("Project testing guide(references/)") : warn("Project testing guide \u7F3A\u5931", "references/ \u7531 init \u751F\u6210;\u53EF\u91CD\u65B0\u8FD0\u884C npx csspilot init")
32304
- ]);
33014
+ const skillDir = join24(root, ".agents", "skills", "testpilot");
33015
+ const installedSkillVersion = readManifestVersion(skillDir);
33016
+ const skillSource = resolveSkillSourceDir();
33017
+ const bundledSkillVersion = skillSource ? readManifestVersion(skillSource) : void 0;
33018
+ const skillItems = [
33019
+ installedSkillVersion ? ok(`TestPilot Skill(.agents/skills/testpilot,v${installedSkillVersion})`) : error62("TestPilot Skill", "\u8FD0\u884C npx csspilot init \u5B89\u88C5"),
33020
+ existsSync7(join24(skillDir, "references", "project.md")) ? ok("Project testing guide(references/)") : warn("Project testing guide \u7F3A\u5931", "references/ \u7531 init \u751F\u6210;\u53EF\u91CD\u65B0\u8FD0\u884C npx csspilot init")
33021
+ ];
33022
+ if (installedSkillVersion && bundledSkillVersion && installedSkillVersion !== bundledSkillVersion) {
33023
+ skillItems.push(
33024
+ warn(
33025
+ `Skill \u53EF\u66F4\u65B0(v${installedSkillVersion} -> v${bundledSkillVersion})`,
33026
+ "\u8FD0\u884C npx csspilot update \u5E76\u63D0\u4EA4 .agents/skills/ \u53D8\u66F4"
33027
+ )
33028
+ );
33029
+ }
33030
+ push("Agent", skillItems);
32305
33031
  const adapters = await detectAdapters(root, info.type);
32306
33032
  push(
32307
33033
  "Adapters",
@@ -32383,14 +33109,14 @@ function makeDoctorCommand() {
32383
33109
  }
32384
33110
 
32385
33111
  // src/commands/init/index.ts
32386
- import { join as join25 } from "node:path";
33112
+ import { join as join27 } from "node:path";
32387
33113
 
32388
33114
  // src/commands/init/config-initializer.ts
32389
- import { access as access2, mkdir as mkdir11, writeFile as writeFile9 } from "node:fs/promises";
32390
- import { basename as basename2, join as join22 } from "node:path";
32391
- async function exists3(path) {
33115
+ import { access as access3, mkdir as mkdir13, readFile as readFile9, writeFile as writeFile11 } from "node:fs/promises";
33116
+ import { basename as basename2, join as join25 } from "node:path";
33117
+ async function exists4(path) {
32392
33118
  try {
32393
- await access2(path);
33119
+ await access3(path);
32394
33120
  return true;
32395
33121
  } catch {
32396
33122
  return false;
@@ -32431,28 +33157,72 @@ ${adapters}
32431
33157
  # server:
32432
33158
  # baseUrl: http://127.0.0.1:3000
32433
33159
 
32434
- # Web \u7AEF:navigate \u76F8\u5BF9 url \u7684\u57FA\u51C6\u5730\u5740
33160
+ # Web \u7AEF:navigate \u76F8\u5BF9 url \u7684\u57FA\u51C6\u5730\u5740(\u6BCF\u4EBA\u7684\u5730\u5740\u4E0D\u540C,\u503C\u653E .env)
32435
33161
  # web:
32436
- # baseUrl: http://127.0.0.1:8080
33162
+ # baseUrl: \${WEB_BASE_URL}
32437
33163
 
32438
- # \u5C0F\u7A0B\u5E8F\u7AEF:\u586B\u5165\u5C0F\u7A0B\u5E8F\u9879\u76EE\u76EE\u5F55\u540E\u5373\u53EF\u6267\u884C target: miniapp \u7684\u6B65\u9AA4
33164
+ # \u5C0F\u7A0B\u5E8F\u7AEF:\u586B\u5165\u5C0F\u7A0B\u5E8F\u9879\u76EE\u76EE\u5F55\u540E\u5373\u53EF\u6267\u884C target: miniapp \u7684\u6B65\u9AA4(\u8DEF\u5F84\u6BCF\u4EBA\u4E0D\u540C,\u8D70 .env)
32439
33165
  # miniapp:
32440
- # projectPath: path/to/miniprogram
32441
- # cliPath: C:/Program Files (x86)/Tencent/\u5FAE\u4FE1web\u5F00\u53D1\u8005\u5DE5\u5177/cli.bat
33166
+ # projectPath: \${MINIAPP_PROJECT_PATH}
33167
+ # cliPath: \${MINIAPP_CLI_PATH}
33168
+ `;
33169
+ }
33170
+ function renderDotEnvExample() {
33171
+ return `# TestPilot \u73AF\u5883\u53D8\u91CF\u793A\u4F8B:\u590D\u5236\u4E3A .env \u5E76\u586B\u5165\u771F\u5B9E\u503C(.env \u542B\u654F\u611F\u4FE1\u606F,\u4E0D\u8981\u63D0\u4EA4\u5230 Git)
33172
+ # \u4F18\u5148\u7EA7:\u771F\u5B9E\u73AF\u5883\u53D8\u91CF > .env \u6587\u4EF6;testpilot.yaml \u4E2D\u7684 \${VAR} \u5F15\u7528\u8FD0\u884C\u65F6\u81EA\u52A8\u6CE8\u5165
33173
+
33174
+ # \u73AF\u5883\u914D\u7F6E(environment \u6BB5)
33175
+ TEST_BASE_URL=http://127.0.0.1:8080
33176
+ STAGING_BASE_URL=https://staging.example.com
33177
+
33178
+ # Web \u7AEF\u76F8\u5BF9 url \u7684\u57FA\u51C6(\u53D6\u6D88 testpilot.yaml \u4E2D web \u6BB5\u6CE8\u91CA\u540E\u751F\u6548)
33179
+ # WEB_BASE_URL=http://127.0.0.1:8080
33180
+
33181
+ # \u5C0F\u7A0B\u5E8F\u7AEF(\u53D6\u6D88 testpilot.yaml \u4E2D miniapp \u6BB5\u6CE8\u91CA\u540E\u751F\u6548)
33182
+ # MINIAPP_PROJECT_PATH=path/to/miniprogram
33183
+ # MINIAPP_CLI_PATH=C:/Program Files (x86)/Tencent/\u5FAE\u4FE1web\u5F00\u53D1\u8005\u5DE5\u5177/cli.bat
33184
+
33185
+ # TestPilot Server(Control Plane,\u53EF\u9009)
33186
+ # TESTPILOT_SERVER_URL=http://127.0.0.1:3000
32442
33187
  `;
32443
33188
  }
33189
+ async function ensureGitignoreEntry(root, entry, comment = "TestPilot \u672C\u5730\u73AF\u5883\u53D8\u91CF") {
33190
+ const gitignorePath = join25(root, ".gitignore");
33191
+ if (!await exists4(gitignorePath)) {
33192
+ await writeFile11(gitignorePath, `# ${comment}
33193
+ ${entry}
33194
+ `, "utf8");
33195
+ return "created";
33196
+ }
33197
+ const content = await readFile9(gitignorePath, "utf8");
33198
+ if (content.split(/\r?\n/).some((item) => item.trim() === entry)) {
33199
+ return "exists";
33200
+ }
33201
+ const separator = content.endsWith("\n") || content === "" ? "" : "\n";
33202
+ await writeFile11(gitignorePath, `${content}${separator}# ${comment}
33203
+ ${entry}
33204
+ `, "utf8");
33205
+ return "updated";
33206
+ }
32444
33207
  async function initTestPilotConfig(root, info, adapters) {
32445
- const yamlPath = join22(root, "testpilot.yaml");
33208
+ const yamlPath = join25(root, "testpilot.yaml");
32446
33209
  let yaml = "reused";
32447
- if (!await exists3(yamlPath)) {
32448
- await writeFile9(yamlPath, renderTestpilotYaml(info), "utf8");
33210
+ if (!await exists4(yamlPath)) {
33211
+ await writeFile11(yamlPath, renderTestpilotYaml(info), "utf8");
32449
33212
  yaml = "created";
32450
33213
  }
32451
- const runtimeDir = join22(root, ".testpilot", "artifacts");
32452
- await mkdir11(runtimeDir, { recursive: true });
32453
- const keep = join22(runtimeDir, ".gitkeep");
32454
- if (!await exists3(keep)) {
32455
- await writeFile9(keep, "", "utf8");
33214
+ const envExamplePath = join25(root, ".env.example");
33215
+ let envExample = "reused";
33216
+ if (!await exists4(envExamplePath)) {
33217
+ await writeFile11(envExamplePath, renderDotEnvExample(), "utf8");
33218
+ envExample = "created";
33219
+ }
33220
+ const gitignore = await ensureGitignoreEntry(root, ".env");
33221
+ const runtimeDir = join25(root, ".testpilot", "artifacts");
33222
+ await mkdir13(runtimeDir, { recursive: true });
33223
+ const keep = join25(runtimeDir, ".gitkeep");
33224
+ if (!await exists4(keep)) {
33225
+ await writeFile11(keep, "", "utf8");
32456
33226
  }
32457
33227
  const existing = await readProjectLink(root);
32458
33228
  const defaultAdapter = defaultAdapterFor(info.type);
@@ -32471,7 +33241,14 @@ async function initTestPilotConfig(root, info, adapters) {
32471
33241
  }
32472
33242
  if (link.serverUrl === void 0 && serverUrl) link.serverUrl = serverUrl;
32473
33243
  const projectLinkFile = await writeProjectLink(link, root);
32474
- return { yaml, runtimeDir: join22(root, ".testpilot"), projectLink: link, projectLinkFile };
33244
+ return {
33245
+ yaml,
33246
+ runtimeDir: join25(root, ".testpilot"),
33247
+ projectLink: link,
33248
+ projectLinkFile,
33249
+ envExample,
33250
+ gitignore
33251
+ };
32475
33252
  }
32476
33253
  async function createProjectOnServer(root, info, _adapters, serverUrl) {
32477
33254
  const git = info.git;
@@ -32500,115 +33277,15 @@ function randomId() {
32500
33277
  return Math.random().toString(16).slice(2, 10);
32501
33278
  }
32502
33279
 
32503
- // src/commands/init/skill-installer.ts
32504
- import { existsSync as existsSync7 } from "node:fs";
32505
- import { access as access3, cp, mkdir as mkdir12, writeFile as writeFile10 } from "node:fs/promises";
32506
- import { dirname as dirname7, join as join23 } from "node:path";
32507
- import { fileURLToPath } from "node:url";
32508
- async function exists4(path) {
32509
- try {
32510
- await access3(path);
32511
- return true;
32512
- } catch {
32513
- return false;
32514
- }
32515
- }
32516
- function resolveSkillSourceDir() {
32517
- const fromEnv = process.env.TESTPILOT_SKILL_DIR;
32518
- if (fromEnv) return fromEnv;
32519
- const bundled = fileURLToPath(new URL("./skills/testpilot", import.meta.url));
32520
- if (existsSync7(bundled)) return bundled;
32521
- const inRepo = fileURLToPath(new URL("../../../../../skills/testpilot", import.meta.url));
32522
- return existsSync7(inRepo) ? inRepo : void 0;
32523
- }
32524
- async function installSkill(root, info, adapters) {
32525
- const dest = join23(root, ".agents", "skills", "testpilot");
32526
- let core = "reused";
32527
- if (!await exists4(join23(dest, "manifest.yaml"))) {
32528
- const src = resolveSkillSourceDir();
32529
- if (!src) {
32530
- throw new Error(
32531
- "\u65E0\u6CD5\u5B9A\u4F4D TestPilot Skill \u6E90\u76EE\u5F55;\u53EF\u901A\u8FC7\u73AF\u5883\u53D8\u91CF TESTPILOT_SKILL_DIR \u6307\u5B9A skills/testpilot \u8DEF\u5F84"
32532
- );
32533
- }
32534
- await mkdir12(dirname7(dest), { recursive: true });
32535
- await cp(src, dest, { recursive: true });
32536
- core = "created";
32537
- }
32538
- const config2 = await loadTestpilotConfig(root).catch(() => void 0);
32539
- const defaultEnvironment = typeof config2?.environment?.default === "string" ? config2.environment.default : "\u672A\u8BBE\u7F6E";
32540
- const defaultWorkspace = config2?.workspace?.default ?? "\u672A\u8BBE\u7F6E(\u53EF\u5728 yaml workspace.default \u914D\u7F6E)";
32541
- const refsDir = join23(dest, "references");
32542
- const files = [
32543
- ["project.md", () => renderProjectMd(info, adapters, defaultEnvironment, defaultWorkspace)],
32544
- ["test-conventions.md", renderTestConventionsMd],
32545
- ["adapters.md", () => renderAdaptersMd(adapters)]
32546
- ];
32547
- let references = "reused";
32548
- for (const [name, render] of files) {
32549
- const file2 = join23(refsDir, name);
32550
- if (await exists4(file2)) continue;
32551
- await mkdir12(refsDir, { recursive: true });
32552
- await writeFile10(file2, render(), "utf8");
32553
- references = "created";
32554
- }
32555
- return { core, references, dest };
32556
- }
32557
- function renderProjectMd(info, adapters, defaultEnvironment, defaultWorkspace) {
32558
- const available = adapters.filter((item) => item.available).map((item) => item.id);
32559
- return `# Project
32560
-
32561
- \u81EA\u52A8\u751F\u6210\u4E8E \`csspilot init\`,\u63CF\u8FF0\u672C\u9879\u76EE\u7684 TestPilot \u63A5\u5165\u4FE1\u606F;Agent \u4F18\u5148\u4ECE\u8FD9\u91CC\u4E86\u89E3\u9879\u76EE\u3002
32562
-
32563
- - Project name: ${info.name}
32564
- - Project type: ${info.type}
32565
- - Language: ${info.language}
32566
- - Package manager: ${info.packageManager}
32567
- - Test directory: tests/e2e
32568
- - Case directory: tests/e2e/cases
32569
- - Available adapters: ${available.length > 0 ? available.join(", ") : "(none detected)"}
32570
- - Default environment: ${defaultEnvironment}
32571
- - Default workspace: ${defaultWorkspace}
32572
- `;
32573
- }
32574
- function renderTestConventionsMd() {
32575
- return `# Test Conventions
32576
-
32577
- \u672C\u9879\u76EE E2E \u6D4B\u8BD5\u7EA6\u5B9A,Agent \u751F\u6210/\u4FEE\u6539 Case \u65F6\u5FC5\u987B\u9075\u5B88:
32578
-
32579
- 1. E2E Case \u4F4D\u4E8E tests/e2e/cases
32580
- 2. Case \u4F7F\u7528 TestPilot DSL
32581
- 3. \u4E0D\u5141\u8BB8\u76F4\u63A5\u8C03\u7528 Playwright
32582
- 4. \u4E0D\u5141\u8BB8\u76F4\u63A5\u8C03\u7528 WeChatIDE
32583
- 5. \u4E0D\u5141\u8BB8\u5728 Case \u4E2D\u5199\u660E\u6587\u5BC6\u7801(\u7528 accountRef \u5F15\u7528)
32584
- 6. \u4F18\u5148\u590D\u7528\u5DF2\u6709 Case
32585
- 7. \u65B0\u589E Case \u5FC5\u987B\u901A\u8FC7 csspilot validate
32586
- 8. Case ID \u5FC5\u987B\u552F\u4E00
32587
- `;
32588
- }
32589
- function renderAdaptersMd(adapters) {
32590
- const lines = adapters.map(
32591
- (item) => `- ${item.id}: ${item.available ? "\u53EF\u7528" : "\u4E0D\u53EF\u7528"}${item.required ? "(\u9879\u76EE\u9700\u8981)" : "(\u53EF\u9009)"} \u2014 ${item.detail}`
32592
- );
32593
- return `# Adapters
32594
-
32595
- \u6267\u884C\u7AEF\u68C0\u6D4B\u7ED3\u679C(\u4EE5 \`csspilot doctor\` \u6700\u65B0\u8F93\u51FA\u4E3A\u51C6):
32596
-
32597
- ${lines.join("\n")}
32598
-
32599
- Agent \u6C38\u8FDC\u4E0D\u76F4\u63A5\u64CD\u4F5C Adapter;Case \u7ECF Execution Engine \u8C03\u5EA6\u5230\u5BF9\u5E94 Adapter \u6267\u884C\u3002
32600
- `;
32601
- }
32602
-
32603
33280
  // src/commands/init/test-structure.ts
32604
33281
  import { existsSync as existsSync8, readdirSync as readdirSync2 } from "node:fs";
32605
- import { mkdir as mkdir13, writeFile as writeFile11 } from "node:fs/promises";
32606
- import { join as join24 } from "node:path";
33282
+ import { mkdir as mkdir14, writeFile as writeFile12 } from "node:fs/promises";
33283
+ import { join as join26 } from "node:path";
32607
33284
  async function initTestStructure(root, _info) {
32608
33285
  const dirs = {
32609
- cases: join24(root, "tests", "e2e", "cases"),
32610
- fixtures: join24(root, "tests", "e2e", "fixtures"),
32611
- data: join24(root, "tests", "e2e", "data")
33286
+ cases: join26(root, "tests", "e2e", "cases"),
33287
+ fixtures: join26(root, "tests", "e2e", "fixtures"),
33288
+ data: join26(root, "tests", "e2e", "data")
32612
33289
  };
32613
33290
  const existed = {
32614
33291
  cases: existsSync8(dirs.cases),
@@ -32616,11 +33293,11 @@ async function initTestStructure(root, _info) {
32616
33293
  data: existsSync8(dirs.data)
32617
33294
  };
32618
33295
  for (const dir of Object.values(dirs)) {
32619
- await mkdir13(dir, { recursive: true });
33296
+ await mkdir14(dir, { recursive: true });
32620
33297
  }
32621
33298
  for (const [name, dir] of Object.entries(dirs)) {
32622
33299
  if (!existed[name] && readdirSync2(dir).length === 0) {
32623
- await writeFile11(join24(dir, ".gitkeep"), "", "utf8").catch(() => void 0);
33300
+ await writeFile12(join26(dir, ".gitkeep"), "", "utf8").catch(() => void 0);
32624
33301
  }
32625
33302
  }
32626
33303
  return {
@@ -32654,9 +33331,15 @@ async function runInit(root, options = {}) {
32654
33331
  console.log(
32655
33332
  config2.yaml === "created" ? "\u2713 \u9879\u76EE\u914D\u7F6E testpilot.yaml" : "\u2713 \u9879\u76EE\u914D\u7F6E testpilot.yaml(\u5DF2\u5B58\u5728,\u4FDD\u7559)"
32656
33333
  );
32657
- console.log(`\u2713 Runtime \u76EE\u5F55 ${join25(".testpilot", "artifacts")}`);
33334
+ console.log(`\u2713 Runtime \u76EE\u5F55 ${join27(".testpilot", "artifacts")}`);
32658
33335
  const serverMode = config2.projectLink.serverUrl ? `Server:${config2.projectLink.serverUrl}` : "\u672C\u5730\u6A21\u5F0F";
32659
33336
  console.log(`\u2713 \u9879\u76EE\u5173\u8054 ${config2.projectLink.projectId}(${serverMode})`);
33337
+ console.log(
33338
+ config2.envExample === "created" ? "\u2713 \u73AF\u5883\u53D8\u91CF\u793A\u4F8B .env.example(\u590D\u5236\u4E3A .env \u586B\u5165\u771F\u5B9E\u503C)" : "\u2713 \u73AF\u5883\u53D8\u91CF\u793A\u4F8B .env.example(\u5DF2\u5B58\u5728,\u4FDD\u7559)"
33339
+ );
33340
+ console.log(
33341
+ config2.gitignore === "exists" ? "\u2713 Git \u5FFD\u7565 .env(\u5DF2\u5728 .gitignore)" : `\u2713 Git \u5FFD\u7565 .env(\u5DF2\u5199\u5165 .gitignore)`
33342
+ );
32660
33343
  const testStructure = await initTestStructure(root, project);
32661
33344
  const reuseNote = project.existingE2eDirs.filter((dir) => dir !== "tests/e2e");
32662
33345
  console.log(
@@ -32666,9 +33349,15 @@ async function runInit(root, options = {}) {
32666
33349
  console.log(` \u26A0 \u68C0\u6D4B\u5230\u5176\u4ED6 E2E \u76EE\u5F55:${reuseNote.join(", ")}(\u4FDD\u7559\u4E0D\u52A8,\u672A\u5F3A\u5236\u8FC1\u79FB)`);
32667
33350
  }
32668
33351
  const skill = await installSkill(root, project, adapters);
32669
- console.log(
32670
- skill.core === "created" ? `\u2713 Skill \u5B89\u88C5 ${join25(".agents", "skills", "testpilot")}` : "\u2713 Skill \u5B89\u88C5 .agents/skills/testpilot/(\u5DF2\u5B58\u5728,\u8DF3\u8FC7)"
32671
- );
33352
+ if (skill.core === "updated") {
33353
+ console.log(
33354
+ `\u2713 Skill \u66F4\u65B0 .agents/skills/testpilot/(v${skill.coreVersion?.from ?? "?"} -> v${skill.coreVersion?.to ?? "?"})`
33355
+ );
33356
+ } else {
33357
+ console.log(
33358
+ skill.core === "created" ? `\u2713 Skill \u5B89\u88C5 ${join27(".agents", "skills", "testpilot")}` : "\u2713 Skill \u5B89\u88C5 .agents/skills/testpilot/(\u5DF2\u5B58\u5728,\u8DF3\u8FC7)"
33359
+ );
33360
+ }
32672
33361
  console.log(
32673
33362
  skill.references === "created" ? "\u2713 Agent \u4E0A\u4E0B\u6587 references/{project,test-conventions,adapters}.md" : "\u2713 Agent \u4E0A\u4E0B\u6587 references/(\u5DF2\u5B58\u5728,\u4FDD\u7559)"
32674
33363
  );
@@ -32743,9 +33432,20 @@ function makeListCommand() {
32743
33432
  // src/commands/report.ts
32744
33433
  import { exec } from "node:child_process";
32745
33434
  function makeReportCommand() {
32746
- return new Command("report").description("\u751F\u6210\u6D4B\u8BD5\u62A5\u544A(JSON + HTML)").option("--run <id>", "\u8FD0\u884C\u8BB0\u5F55 ID,\u9ED8\u8BA4\u6700\u8FD1\u4E00\u6B21").option("--open", "\u751F\u6210\u540E\u5728\u6D4F\u89C8\u5668\u6253\u5F00", false).action(async (options) => {
33435
+ return new Command("report").description("\u751F\u6210\u6D4B\u8BD5\u62A5\u544A(JSON + HTML)").option("--run <id>", "\u8FD0\u884C\u8BB0\u5F55 ID,\u9ED8\u8BA4\u6700\u8FD1\u4E00\u6B21").option("--open", "\u751F\u6210\u540E\u5728\u6D4F\u89C8\u5668\u6253\u5F00", false).option("--summary", "\u751F\u6210\u8DE8 run \u6C47\u603B\u62A5\u544A(summary.html + summary.json)", false).option("--last <n>", "\u6C47\u603B\u7EDF\u8BA1\u7A97\u53E3:\u6700\u8FD1 N \u6B21\u8FD0\u884C", "30").action(async (options) => {
32747
33436
  const client = new TestPilotClient();
32748
33437
  try {
33438
+ if (options.summary) {
33439
+ const result = await client.generateSummaryReport({
33440
+ last: Number(options.last) > 0 ? Number(options.last) : void 0
33441
+ });
33442
+ console.log(
33443
+ `\u2713 summary.html ${result.html}(\u7EDF\u8BA1\u6700\u8FD1 ${result.window.runs} \u6B21\u8FD0\u884C,\u6574\u4F53\u901A\u8FC7\u7387 ${(result.data.kpis.overallRate * 100).toFixed(1)}%)`
33444
+ );
33445
+ console.log(`\u2713 summary.json ${result.json}`);
33446
+ if (options.open) await openInBrowser(result.html);
33447
+ return;
33448
+ }
32749
33449
  const report = await client.generateReport(options.run);
32750
33450
  console.log(`\u2713 report.json ${report.json}`);
32751
33451
  console.log(`\u2713 report.html ${report.html}`);
@@ -32764,7 +33464,7 @@ function openInBrowser(file2) {
32764
33464
  }
32765
33465
 
32766
33466
  // src/commands/run.ts
32767
- import { join as join26 } from "node:path";
33467
+ import { join as join28 } from "node:path";
32768
33468
  function makeRunCommand() {
32769
33469
  return new Command("run").description("\u6267\u884C\u6D4B\u8BD5\u7528\u4F8B(\u9ED8\u8BA4\u8FD0\u884C testpilot.yaml \u914D\u7F6E\u7684 Case)").argument("[paths...]", "\u7528\u4F8B\u6587\u4EF6\u6216\u76EE\u5F55,\u9ED8\u8BA4\u6309\u9879\u76EE\u914D\u7F6E").option("--tag <tag>", "\u6309\u6807\u7B7E\u8FC7\u6EE4\u7528\u4F8B").action(async (paths, options) => {
32770
33470
  const client = new TestPilotClient();
@@ -32788,7 +33488,7 @@ function makeRunCommand() {
32788
33488
  console.log(
32789
33489
  `Run ${summary.runId}${summary.cancelled ? "(\u5DF2\u53D6\u6D88)" : ""}: ${summary.status.toUpperCase()} \u2014 \u7528\u4F8B \u2713${summary.totals.passed} \u2717${summary.totals.failed},\u6B65\u9AA4 \u2713${summary.totals.stepsPassed} \u2717${summary.totals.stepsFailed} \u21B7${summary.totals.stepsSkipped}${notes ? `(${notes})` : ""}`
32790
33490
  );
32791
- console.log(`\u4EA7\u7269\u76EE\u5F55:${join26(".testpilot", "artifacts", "runs", summary.runId)}`);
33491
+ console.log(`\u4EA7\u7269\u76EE\u5F55:${join28(".testpilot", "artifacts", "runs", summary.runId)}`);
32792
33492
  console.log("\u67E5\u770B\u62A5\u544A:npx csspilot report");
32793
33493
  if (summary.status === "failed") process.exitCode = 1;
32794
33494
  } catch (err) {
@@ -32898,6 +33598,37 @@ function makeSyncMetadataCommand() {
32898
33598
  });
32899
33599
  }
32900
33600
 
33601
+ // src/commands/update.ts
33602
+ import { writeFile as writeFile13 } from "node:fs/promises";
33603
+ import { join as join29 } from "node:path";
33604
+ function makeUpdateCommand() {
33605
+ const update = new Command("update").description("\u540C\u6B65 TestPilot \u63A5\u5165\u7269\u5230\u5F53\u524D CLI \u7248\u672C(Skill / .env.example;\u4E0D\u89E6\u78B0 .env \u4E0E\u7528\u4F8B)").option("--force", "Skill \u7248\u672C\u4E00\u81F4\u65F6\u4E5F\u5F3A\u5236\u8986\u76D6\u672C\u5730\u4FEE\u6539", false).action(async (options) => {
33606
+ const root = process.cwd();
33607
+ const dest = join29(".agents", "skills", "testpilot");
33608
+ const skill = await syncSkillFiles(root, { force: options.force });
33609
+ switch (skill.core) {
33610
+ case "created":
33611
+ console.log(`\u2713 Skill \u5B89\u88C5 ${dest}(v${skill.bundledVersion ?? "?"})`);
33612
+ break;
33613
+ case "updated":
33614
+ console.log(
33615
+ `\u2713 Skill \u66F4\u65B0 ${dest} v${skill.installedVersion ?? "?"} -> v${skill.bundledVersion ?? "?"}`
33616
+ );
33617
+ break;
33618
+ default:
33619
+ console.log(`\u2713 Skill \u5DF2\u662F\u6700\u65B0 v${skill.bundledVersion ?? "?"}`);
33620
+ }
33621
+ await writeFile13(join29(root, ".env.example"), renderDotEnvExample(), "utf8");
33622
+ console.log("\u2713 .env.example \u5DF2\u540C\u6B65\u4E3A\u5F53\u524D\u7248\u672C\u7684\u793A\u4F8B(.env \u4E0D\u53D7\u5F71\u54CD)");
33623
+ const gitignore = await ensureGitignoreEntry(root, ".env");
33624
+ console.log(
33625
+ gitignore === "exists" ? "\u2713 .gitignore \u5DF2\u5FFD\u7565 .env" : "\u2713 .gitignore \u5DF2\u8FFD\u52A0\u5FFD\u7565 .env"
33626
+ );
33627
+ console.log("\u63D0\u793A:\u63D0\u4EA4 .agents/skills/ \u4E0E .env.example \u7684\u53D8\u66F4,\u56E2\u961F\u4E0E AI \u5373\u53EF\u4F7F\u7528\u6700\u65B0\u80FD\u529B");
33628
+ });
33629
+ return update;
33630
+ }
33631
+
32901
33632
  // src/commands/validate.ts
32902
33633
  function makeValidateCommand() {
32903
33634
  return new Command("validate").description("\u6821\u9A8C\u7528\u4F8B DSL").argument("[paths...]", "\u7528\u4F8B\u6587\u4EF6\u6216\u76EE\u5F55,\u9ED8\u8BA4\u6309\u9879\u76EE\u914D\u7F6E").action(async (paths) => {
@@ -32934,10 +33665,13 @@ function makeValidateCommand() {
32934
33665
  }
32935
33666
 
32936
33667
  // src/cli.ts
32937
- var CLI_VERSION = "0.3.2";
33668
+ var CLI_VERSION = "0.5.0";
32938
33669
  async function createCli() {
32939
33670
  const program2 = new Command();
32940
- program2.name("csspilot").description("TestPilot \u2014 AI Native \u8DE8\u7AEF\u4E1A\u52A1\u6D4B\u8BD5\u57FA\u7840\u8BBE\u65BD CLI").version(CLI_VERSION).addCommand(makeInitCommand()).addCommand(makeValidateCommand()).addCommand(makeListCommand()).addCommand(makeRunCommand()).addCommand(makeReportCommand()).addCommand(makeSyncMetadataCommand()).addCommand(makeDoctorCommand()).addCommand(makeCiCommand());
33671
+ program2.name("csspilot").description("TestPilot \u2014 AI Native \u8DE8\u7AEF\u4E1A\u52A1\u6D4B\u8BD5\u57FA\u7840\u8BBE\u65BD CLI").version(CLI_VERSION, "-v, --version", "output the current version").option("-V", "output the current version", () => {
33672
+ console.log(CLI_VERSION);
33673
+ process.exit(0);
33674
+ }).addCommand(makeInitCommand()).addCommand(makeValidateCommand()).addCommand(makeListCommand()).addCommand(makeRunCommand()).addCommand(makeReportCommand()).addCommand(makeSyncMetadataCommand()).addCommand(makeDoctorCommand()).addCommand(makeUpdateCommand()).addCommand(makeCiCommand());
32941
33675
  return program2;
32942
33676
  }
32943
33677
  export {