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