csspilot 0.3.2 → 0.4.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
@@ -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,8 +30338,8 @@ 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 join22 } from "node:path";
30340
30343
 
30341
30344
  // ../../packages/sdk/src/client.ts
30342
30345
  import { isAbsolute as isAbsolute3, join as join17 } from "node:path";
@@ -32167,8 +32170,12 @@ async function collectEnvVarRefs(root) {
32167
32170
  return [];
32168
32171
  }
32169
32172
  const names = /* @__PURE__ */ new Set();
32170
- for (const match of text.matchAll(ENV_REF_PATTERN)) {
32171
- names.add(match[1]);
32173
+ for (const line of text.split(/\r?\n/)) {
32174
+ const trimmed = line.trim();
32175
+ if (trimmed.startsWith("#")) continue;
32176
+ for (const match of trimmed.matchAll(ENV_REF_PATTERN)) {
32177
+ names.add(match[1]);
32178
+ }
32172
32179
  }
32173
32180
  return [...names].sort();
32174
32181
  }
@@ -32260,6 +32267,144 @@ function adaptersFor(type) {
32260
32267
  return ["playwright"];
32261
32268
  }
32262
32269
 
32270
+ // src/commands/init/skill-installer.ts
32271
+ import { existsSync as existsSync6, readFileSync as readFileSync3 } from "node:fs";
32272
+ import { access as access2, cp, mkdir as mkdir11, writeFile as writeFile9 } from "node:fs/promises";
32273
+ import { dirname as dirname7, join as join21 } from "node:path";
32274
+ import { fileURLToPath } from "node:url";
32275
+ var CORE_FILES = ["SKILL.md", "manifest.yaml"];
32276
+ var CORE_DIRS = ["rules", "workflows"];
32277
+ async function exists3(path) {
32278
+ try {
32279
+ await access2(path);
32280
+ return true;
32281
+ } catch {
32282
+ return false;
32283
+ }
32284
+ }
32285
+ function readManifestVersion(dir) {
32286
+ const manifestPath = join21(dir, "manifest.yaml");
32287
+ if (!existsSync6(manifestPath)) return void 0;
32288
+ return /^version:\s*([^\s#]+)/m.exec(readFileSync3(manifestPath, "utf8"))?.[1];
32289
+ }
32290
+ function resolveSkillSourceDir() {
32291
+ const fromEnv = process.env.TESTPILOT_SKILL_DIR;
32292
+ if (fromEnv) return fromEnv;
32293
+ const bundled = fileURLToPath(new URL("./skills/testpilot", import.meta.url));
32294
+ if (existsSync6(bundled)) return bundled;
32295
+ const inRepo = fileURLToPath(new URL("../../../../../skills/testpilot", import.meta.url));
32296
+ return existsSync6(inRepo) ? inRepo : void 0;
32297
+ }
32298
+ async function copyCoreFiles(src, dest) {
32299
+ for (const name of CORE_FILES) {
32300
+ await cp(join21(src, name), join21(dest, name), { recursive: true, force: true });
32301
+ }
32302
+ for (const dir of CORE_DIRS) {
32303
+ if (existsSync6(join21(src, dir))) {
32304
+ await cp(join21(src, dir), join21(dest, dir), { recursive: true, force: true });
32305
+ }
32306
+ }
32307
+ }
32308
+ async function syncSkillFiles(root, options = {}) {
32309
+ const dest = join21(root, ".agents", "skills", "testpilot");
32310
+ const bundledVersion = readManifestVersion(resolveSkillSourceDir() ?? "");
32311
+ if (!await exists3(join21(dest, "manifest.yaml"))) {
32312
+ const src = resolveSkillSourceDir();
32313
+ if (!src) {
32314
+ throw new Error(
32315
+ "\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"
32316
+ );
32317
+ }
32318
+ await mkdir11(dirname7(dest), { recursive: true });
32319
+ await cp(src, dest, { recursive: true });
32320
+ return { core: "created", bundledVersion, dest };
32321
+ }
32322
+ const installedVersion = readManifestVersion(dest);
32323
+ const bundled = resolveSkillSourceDir();
32324
+ if (!bundled) {
32325
+ return { core: "current", installedVersion, dest };
32326
+ }
32327
+ if (!options.force && installedVersion === bundledVersion) {
32328
+ return { core: "current", installedVersion, bundledVersion, dest };
32329
+ }
32330
+ await copyCoreFiles(bundled, dest);
32331
+ return {
32332
+ core: "updated",
32333
+ installedVersion,
32334
+ bundledVersion,
32335
+ dest
32336
+ };
32337
+ }
32338
+ async function installSkill(root, info, adapters) {
32339
+ const dest = join21(root, ".agents", "skills", "testpilot");
32340
+ const sync = await syncSkillFiles(root);
32341
+ const core = sync.core === "created" ? "created" : sync.core === "updated" ? "updated" : "reused";
32342
+ const coreVersion = sync.core === "updated" || sync.core === "created" ? { from: sync.installedVersion, to: sync.bundledVersion } : void 0;
32343
+ const config2 = await loadTestpilotConfig(root).catch(() => void 0);
32344
+ const defaultEnvironment = typeof config2?.environment?.default === "string" ? config2.environment.default : "\u672A\u8BBE\u7F6E";
32345
+ const defaultWorkspace = config2?.workspace?.default ?? "\u672A\u8BBE\u7F6E(\u53EF\u5728 yaml workspace.default \u914D\u7F6E)";
32346
+ const refsDir = join21(dest, "references");
32347
+ const files = [
32348
+ ["project.md", () => renderProjectMd(info, adapters, defaultEnvironment, defaultWorkspace)],
32349
+ ["test-conventions.md", renderTestConventionsMd],
32350
+ ["adapters.md", () => renderAdaptersMd(adapters)]
32351
+ ];
32352
+ let references = "reused";
32353
+ for (const [name, render] of files) {
32354
+ const file2 = join21(refsDir, name);
32355
+ if (await exists3(file2)) continue;
32356
+ await mkdir11(refsDir, { recursive: true });
32357
+ await writeFile9(file2, render(), "utf8");
32358
+ references = "created";
32359
+ }
32360
+ return coreVersion ? { core, coreVersion, references, dest } : { core, references, dest };
32361
+ }
32362
+ function renderProjectMd(info, adapters, defaultEnvironment, defaultWorkspace) {
32363
+ const available = adapters.filter((item) => item.available).map((item) => item.id);
32364
+ return `# Project
32365
+
32366
+ \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
32367
+
32368
+ - Project name: ${info.name}
32369
+ - Project type: ${info.type}
32370
+ - Language: ${info.language}
32371
+ - Package manager: ${info.packageManager}
32372
+ - Test directory: tests/e2e
32373
+ - Case directory: tests/e2e/cases
32374
+ - Available adapters: ${available.length > 0 ? available.join(", ") : "(none detected)"}
32375
+ - Default environment: ${defaultEnvironment}
32376
+ - Default workspace: ${defaultWorkspace}
32377
+ `;
32378
+ }
32379
+ function renderTestConventionsMd() {
32380
+ return `# Test Conventions
32381
+
32382
+ \u672C\u9879\u76EE E2E \u6D4B\u8BD5\u7EA6\u5B9A,Agent \u751F\u6210/\u4FEE\u6539 Case \u65F6\u5FC5\u987B\u9075\u5B88:
32383
+
32384
+ 1. E2E Case \u4F4D\u4E8E tests/e2e/cases
32385
+ 2. Case \u4F7F\u7528 TestPilot DSL
32386
+ 3. \u4E0D\u5141\u8BB8\u76F4\u63A5\u8C03\u7528 Playwright
32387
+ 4. \u4E0D\u5141\u8BB8\u76F4\u63A5\u8C03\u7528 WeChatIDE
32388
+ 5. \u4E0D\u5141\u8BB8\u5728 Case \u4E2D\u5199\u660E\u6587\u5BC6\u7801(\u7528 accountRef \u5F15\u7528)
32389
+ 6. \u4F18\u5148\u590D\u7528\u5DF2\u6709 Case
32390
+ 7. \u65B0\u589E Case \u5FC5\u987B\u901A\u8FC7 csspilot validate
32391
+ 8. Case ID \u5FC5\u987B\u552F\u4E00
32392
+ `;
32393
+ }
32394
+ function renderAdaptersMd(adapters) {
32395
+ const lines = adapters.map(
32396
+ (item) => `- ${item.id}: ${item.available ? "\u53EF\u7528" : "\u4E0D\u53EF\u7528"}${item.required ? "(\u9879\u76EE\u9700\u8981)" : "(\u53EF\u9009)"} \u2014 ${item.detail}`
32397
+ );
32398
+ return `# Adapters
32399
+
32400
+ \u6267\u884C\u7AEF\u68C0\u6D4B\u7ED3\u679C(\u4EE5 \`csspilot doctor\` \u6700\u65B0\u8F93\u51FA\u4E3A\u51C6):
32401
+
32402
+ ${lines.join("\n")}
32403
+
32404
+ Agent \u6C38\u8FDC\u4E0D\u76F4\u63A5\u64CD\u4F5C Adapter;Case \u7ECF Execution Engine \u8C03\u5EA6\u5230\u5BF9\u5E94 Adapter \u6267\u884C\u3002
32405
+ `;
32406
+ }
32407
+
32263
32408
  // src/commands/init/doctor.ts
32264
32409
  async function runDoctor(root) {
32265
32410
  const sections = [];
@@ -32284,25 +32429,37 @@ async function runDoctor(root) {
32284
32429
  const projectItems = [
32285
32430
  ok(`Project ${info.name}(${info.type})`),
32286
32431
  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")
32432
+ existsSync7(join22(root, ".testpilot", "project.json")) ? ok(".testpilot/project.json") : error62(".testpilot/project.json", "\u8FD0\u884C npx csspilot init \u751F\u6210"),
32433
+ existsSync7(join22(root, "testpilot.yaml")) ? ok("testpilot.yaml") : error62("testpilot.yaml", "\u8FD0\u884C npx csspilot init \u751F\u6210")
32289
32434
  ];
32290
32435
  push("Project", projectItems);
32291
32436
  const config2 = await loadTestpilotConfig(root).catch(() => void 0);
32292
32437
  const casesDir = config2?.casesDir ?? "tests/e2e/cases";
32293
- const cases = await collectCases([join21(root, casesDir)], { root }).catch(() => []);
32438
+ const cases = await collectCases([join22(root, casesDir)], { root }).catch(() => []);
32294
32439
  const invalidCount = cases.filter((item) => !item.valid).length;
32295
- const testDir = join21(root, "tests", "e2e");
32440
+ const testDir = join22(root, "tests", "e2e");
32296
32441
  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"),
32442
+ existsSync7(testDir) ? ok("tests/e2e") : error62("tests/e2e", "\u8FD0\u884C npx csspilot init \u521B\u5EFA"),
32443
+ existsSync7(join22(root, casesDir)) ? ok(`cases directory(${casesDir})`) : error62(`cases directory(${casesDir})`, "\u8FD0\u884C npx csspilot init \u521B\u5EFA"),
32299
32444
  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
32445
  ]);
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
- ]);
32446
+ const skillDir = join22(root, ".agents", "skills", "testpilot");
32447
+ const installedSkillVersion = readManifestVersion(skillDir);
32448
+ const skillSource = resolveSkillSourceDir();
32449
+ const bundledSkillVersion = skillSource ? readManifestVersion(skillSource) : void 0;
32450
+ const skillItems = [
32451
+ installedSkillVersion ? ok(`TestPilot Skill(.agents/skills/testpilot,v${installedSkillVersion})`) : error62("TestPilot Skill", "\u8FD0\u884C npx csspilot init \u5B89\u88C5"),
32452
+ existsSync7(join22(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")
32453
+ ];
32454
+ if (installedSkillVersion && bundledSkillVersion && installedSkillVersion !== bundledSkillVersion) {
32455
+ skillItems.push(
32456
+ warn(
32457
+ `Skill \u53EF\u66F4\u65B0(v${installedSkillVersion} -> v${bundledSkillVersion})`,
32458
+ "\u8FD0\u884C npx csspilot update \u5E76\u63D0\u4EA4 .agents/skills/ \u53D8\u66F4"
32459
+ )
32460
+ );
32461
+ }
32462
+ push("Agent", skillItems);
32306
32463
  const adapters = await detectAdapters(root, info.type);
32307
32464
  push(
32308
32465
  "Adapters",
@@ -32387,11 +32544,11 @@ function makeDoctorCommand() {
32387
32544
  import { join as join25 } from "node:path";
32388
32545
 
32389
32546
  // 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) {
32547
+ import { access as access3, mkdir as mkdir12, readFile as readFile8, writeFile as writeFile10 } from "node:fs/promises";
32548
+ import { basename as basename2, join as join23 } from "node:path";
32549
+ async function exists4(path) {
32393
32550
  try {
32394
- await access2(path);
32551
+ await access3(path);
32395
32552
  return true;
32396
32553
  } catch {
32397
32554
  return false;
@@ -32432,28 +32589,72 @@ ${adapters}
32432
32589
  # server:
32433
32590
  # baseUrl: http://127.0.0.1:3000
32434
32591
 
32435
- # Web \u7AEF:navigate \u76F8\u5BF9 url \u7684\u57FA\u51C6\u5730\u5740
32592
+ # Web \u7AEF:navigate \u76F8\u5BF9 url \u7684\u57FA\u51C6\u5730\u5740(\u6BCF\u4EBA\u7684\u5730\u5740\u4E0D\u540C,\u503C\u653E .env)
32436
32593
  # web:
32437
- # baseUrl: http://127.0.0.1:8080
32594
+ # baseUrl: \${WEB_BASE_URL}
32438
32595
 
32439
- # \u5C0F\u7A0B\u5E8F\u7AEF:\u586B\u5165\u5C0F\u7A0B\u5E8F\u9879\u76EE\u76EE\u5F55\u540E\u5373\u53EF\u6267\u884C target: miniapp \u7684\u6B65\u9AA4
32596
+ # \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
32597
  # miniapp:
32441
- # projectPath: path/to/miniprogram
32442
- # cliPath: C:/Program Files (x86)/Tencent/\u5FAE\u4FE1web\u5F00\u53D1\u8005\u5DE5\u5177/cli.bat
32598
+ # projectPath: \${MINIAPP_PROJECT_PATH}
32599
+ # cliPath: \${MINIAPP_CLI_PATH}
32600
+ `;
32601
+ }
32602
+ function renderDotEnvExample() {
32603
+ 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)
32604
+ # \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
32605
+
32606
+ # \u73AF\u5883\u914D\u7F6E(environment \u6BB5)
32607
+ TEST_BASE_URL=http://127.0.0.1:8080
32608
+ STAGING_BASE_URL=https://staging.example.com
32609
+
32610
+ # Web \u7AEF\u76F8\u5BF9 url \u7684\u57FA\u51C6(\u53D6\u6D88 testpilot.yaml \u4E2D web \u6BB5\u6CE8\u91CA\u540E\u751F\u6548)
32611
+ # WEB_BASE_URL=http://127.0.0.1:8080
32612
+
32613
+ # \u5C0F\u7A0B\u5E8F\u7AEF(\u53D6\u6D88 testpilot.yaml \u4E2D miniapp \u6BB5\u6CE8\u91CA\u540E\u751F\u6548)
32614
+ # MINIAPP_PROJECT_PATH=path/to/miniprogram
32615
+ # MINIAPP_CLI_PATH=C:/Program Files (x86)/Tencent/\u5FAE\u4FE1web\u5F00\u53D1\u8005\u5DE5\u5177/cli.bat
32616
+
32617
+ # TestPilot Server(Control Plane,\u53EF\u9009)
32618
+ # TESTPILOT_SERVER_URL=http://127.0.0.1:3000
32443
32619
  `;
32444
32620
  }
32621
+ async function ensureGitignoreEntry(root, entry, comment = "TestPilot \u672C\u5730\u73AF\u5883\u53D8\u91CF") {
32622
+ const gitignorePath = join23(root, ".gitignore");
32623
+ if (!await exists4(gitignorePath)) {
32624
+ await writeFile10(gitignorePath, `# ${comment}
32625
+ ${entry}
32626
+ `, "utf8");
32627
+ return "created";
32628
+ }
32629
+ const content = await readFile8(gitignorePath, "utf8");
32630
+ if (content.split(/\r?\n/).some((item) => item.trim() === entry)) {
32631
+ return "exists";
32632
+ }
32633
+ const separator = content.endsWith("\n") || content === "" ? "" : "\n";
32634
+ await writeFile10(gitignorePath, `${content}${separator}# ${comment}
32635
+ ${entry}
32636
+ `, "utf8");
32637
+ return "updated";
32638
+ }
32445
32639
  async function initTestPilotConfig(root, info, adapters) {
32446
- const yamlPath = join22(root, "testpilot.yaml");
32640
+ const yamlPath = join23(root, "testpilot.yaml");
32447
32641
  let yaml = "reused";
32448
- if (!await exists3(yamlPath)) {
32449
- await writeFile9(yamlPath, renderTestpilotYaml(info), "utf8");
32642
+ if (!await exists4(yamlPath)) {
32643
+ await writeFile10(yamlPath, renderTestpilotYaml(info), "utf8");
32450
32644
  yaml = "created";
32451
32645
  }
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");
32646
+ const envExamplePath = join23(root, ".env.example");
32647
+ let envExample = "reused";
32648
+ if (!await exists4(envExamplePath)) {
32649
+ await writeFile10(envExamplePath, renderDotEnvExample(), "utf8");
32650
+ envExample = "created";
32651
+ }
32652
+ const gitignore = await ensureGitignoreEntry(root, ".env");
32653
+ const runtimeDir = join23(root, ".testpilot", "artifacts");
32654
+ await mkdir12(runtimeDir, { recursive: true });
32655
+ const keep = join23(runtimeDir, ".gitkeep");
32656
+ if (!await exists4(keep)) {
32657
+ await writeFile10(keep, "", "utf8");
32457
32658
  }
32458
32659
  const existing = await readProjectLink(root);
32459
32660
  const defaultAdapter = defaultAdapterFor(info.type);
@@ -32472,7 +32673,14 @@ async function initTestPilotConfig(root, info, adapters) {
32472
32673
  }
32473
32674
  if (link.serverUrl === void 0 && serverUrl) link.serverUrl = serverUrl;
32474
32675
  const projectLinkFile = await writeProjectLink(link, root);
32475
- return { yaml, runtimeDir: join22(root, ".testpilot"), projectLink: link, projectLinkFile };
32676
+ return {
32677
+ yaml,
32678
+ runtimeDir: join23(root, ".testpilot"),
32679
+ projectLink: link,
32680
+ projectLinkFile,
32681
+ envExample,
32682
+ gitignore
32683
+ };
32476
32684
  }
32477
32685
  async function createProjectOnServer(root, info, _adapters, serverUrl) {
32478
32686
  const git = info.git;
@@ -32501,106 +32709,6 @@ function randomId() {
32501
32709
  return Math.random().toString(16).slice(2, 10);
32502
32710
  }
32503
32711
 
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
32712
  // src/commands/init/test-structure.ts
32605
32713
  import { existsSync as existsSync8, readdirSync as readdirSync2 } from "node:fs";
32606
32714
  import { mkdir as mkdir13, writeFile as writeFile11 } from "node:fs/promises";
@@ -32658,6 +32766,12 @@ async function runInit(root, options = {}) {
32658
32766
  console.log(`\u2713 Runtime \u76EE\u5F55 ${join25(".testpilot", "artifacts")}`);
32659
32767
  const serverMode = config2.projectLink.serverUrl ? `Server:${config2.projectLink.serverUrl}` : "\u672C\u5730\u6A21\u5F0F";
32660
32768
  console.log(`\u2713 \u9879\u76EE\u5173\u8054 ${config2.projectLink.projectId}(${serverMode})`);
32769
+ console.log(
32770
+ 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)"
32771
+ );
32772
+ console.log(
32773
+ config2.gitignore === "exists" ? "\u2713 Git \u5FFD\u7565 .env(\u5DF2\u5728 .gitignore)" : `\u2713 Git \u5FFD\u7565 .env(\u5DF2\u5199\u5165 .gitignore)`
32774
+ );
32661
32775
  const testStructure = await initTestStructure(root, project);
32662
32776
  const reuseNote = project.existingE2eDirs.filter((dir) => dir !== "tests/e2e");
32663
32777
  console.log(
@@ -32667,9 +32781,15 @@ async function runInit(root, options = {}) {
32667
32781
  console.log(` \u26A0 \u68C0\u6D4B\u5230\u5176\u4ED6 E2E \u76EE\u5F55:${reuseNote.join(", ")}(\u4FDD\u7559\u4E0D\u52A8,\u672A\u5F3A\u5236\u8FC1\u79FB)`);
32668
32782
  }
32669
32783
  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
- );
32784
+ if (skill.core === "updated") {
32785
+ console.log(
32786
+ `\u2713 Skill \u66F4\u65B0 .agents/skills/testpilot/(v${skill.coreVersion?.from ?? "?"} -> v${skill.coreVersion?.to ?? "?"})`
32787
+ );
32788
+ } else {
32789
+ console.log(
32790
+ skill.core === "created" ? `\u2713 Skill \u5B89\u88C5 ${join25(".agents", "skills", "testpilot")}` : "\u2713 Skill \u5B89\u88C5 .agents/skills/testpilot/(\u5DF2\u5B58\u5728,\u8DF3\u8FC7)"
32791
+ );
32792
+ }
32673
32793
  console.log(
32674
32794
  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
32795
  );
@@ -32899,6 +33019,37 @@ function makeSyncMetadataCommand() {
32899
33019
  });
32900
33020
  }
32901
33021
 
33022
+ // src/commands/update.ts
33023
+ import { writeFile as writeFile12 } from "node:fs/promises";
33024
+ import { join as join27 } from "node:path";
33025
+ function makeUpdateCommand() {
33026
+ 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) => {
33027
+ const root = process.cwd();
33028
+ const dest = join27(".agents", "skills", "testpilot");
33029
+ const skill = await syncSkillFiles(root, { force: options.force });
33030
+ switch (skill.core) {
33031
+ case "created":
33032
+ console.log(`\u2713 Skill \u5B89\u88C5 ${dest}(v${skill.bundledVersion ?? "?"})`);
33033
+ break;
33034
+ case "updated":
33035
+ console.log(
33036
+ `\u2713 Skill \u66F4\u65B0 ${dest} v${skill.installedVersion ?? "?"} -> v${skill.bundledVersion ?? "?"}`
33037
+ );
33038
+ break;
33039
+ default:
33040
+ console.log(`\u2713 Skill \u5DF2\u662F\u6700\u65B0 v${skill.bundledVersion ?? "?"}`);
33041
+ }
33042
+ await writeFile12(join27(root, ".env.example"), renderDotEnvExample(), "utf8");
33043
+ console.log("\u2713 .env.example \u5DF2\u540C\u6B65\u4E3A\u5F53\u524D\u7248\u672C\u7684\u793A\u4F8B(.env \u4E0D\u53D7\u5F71\u54CD)");
33044
+ const gitignore = await ensureGitignoreEntry(root, ".env");
33045
+ console.log(
33046
+ gitignore === "exists" ? "\u2713 .gitignore \u5DF2\u5FFD\u7565 .env" : "\u2713 .gitignore \u5DF2\u8FFD\u52A0\u5FFD\u7565 .env"
33047
+ );
33048
+ 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");
33049
+ });
33050
+ return update;
33051
+ }
33052
+
32902
33053
  // src/commands/validate.ts
32903
33054
  function makeValidateCommand() {
32904
33055
  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 +33086,10 @@ function makeValidateCommand() {
32935
33086
  }
32936
33087
 
32937
33088
  // src/cli.ts
32938
- var CLI_VERSION = "0.3.2";
33089
+ var CLI_VERSION = "0.4.0";
32939
33090
  async function createCli() {
32940
33091
  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());
33092
+ 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(makeUpdateCommand()).addCommand(makeCiCommand());
32942
33093
  return program2;
32943
33094
  }
32944
33095
 
package/dist/index.js CHANGED
@@ -10449,8 +10449,8 @@ function makeCiCommand() {
10449
10449
  }
10450
10450
 
10451
10451
  // src/commands/init/doctor.ts
10452
- import { existsSync as existsSync6 } from "node:fs";
10453
- import { join as join21 } from "node:path";
10452
+ import { existsSync as existsSync7 } from "node:fs";
10453
+ import { join as join22 } from "node:path";
10454
10454
 
10455
10455
  // ../../packages/core/src/errors/index.ts
10456
10456
  var TestPilotError = class extends Error {
@@ -30243,7 +30243,10 @@ async function loadTestpilotConfig(cwd = process.cwd()) {
30243
30243
  baseUrl: parsed.api.baseUrl === void 0 ? void 0 : interpolateEnvRefs(parsed.api.baseUrl),
30244
30244
  timeoutMs: parsed.api.timeoutMs
30245
30245
  } : void 0,
30246
- miniapp: parsed.miniapp
30246
+ miniapp: parsed.miniapp ? {
30247
+ projectPath: parsed.miniapp.projectPath === void 0 ? void 0 : interpolateEnvRefs(parsed.miniapp.projectPath),
30248
+ cliPath: parsed.miniapp.cliPath === void 0 ? void 0 : interpolateEnvRefs(parsed.miniapp.cliPath)
30249
+ } : void 0
30247
30250
  };
30248
30251
  }
30249
30252
 
@@ -32166,8 +32169,12 @@ async function collectEnvVarRefs(root) {
32166
32169
  return [];
32167
32170
  }
32168
32171
  const names = /* @__PURE__ */ new Set();
32169
- for (const match of text.matchAll(ENV_REF_PATTERN)) {
32170
- names.add(match[1]);
32172
+ for (const line of text.split(/\r?\n/)) {
32173
+ const trimmed = line.trim();
32174
+ if (trimmed.startsWith("#")) continue;
32175
+ for (const match of trimmed.matchAll(ENV_REF_PATTERN)) {
32176
+ names.add(match[1]);
32177
+ }
32171
32178
  }
32172
32179
  return [...names].sort();
32173
32180
  }
@@ -32259,6 +32266,144 @@ function adaptersFor(type) {
32259
32266
  return ["playwright"];
32260
32267
  }
32261
32268
 
32269
+ // src/commands/init/skill-installer.ts
32270
+ import { existsSync as existsSync6, readFileSync as readFileSync3 } from "node:fs";
32271
+ import { access as access2, cp, mkdir as mkdir11, writeFile as writeFile9 } from "node:fs/promises";
32272
+ import { dirname as dirname7, join as join21 } from "node:path";
32273
+ import { fileURLToPath } from "node:url";
32274
+ var CORE_FILES = ["SKILL.md", "manifest.yaml"];
32275
+ var CORE_DIRS = ["rules", "workflows"];
32276
+ async function exists3(path) {
32277
+ try {
32278
+ await access2(path);
32279
+ return true;
32280
+ } catch {
32281
+ return false;
32282
+ }
32283
+ }
32284
+ function readManifestVersion(dir) {
32285
+ const manifestPath = join21(dir, "manifest.yaml");
32286
+ if (!existsSync6(manifestPath)) return void 0;
32287
+ return /^version:\s*([^\s#]+)/m.exec(readFileSync3(manifestPath, "utf8"))?.[1];
32288
+ }
32289
+ function resolveSkillSourceDir() {
32290
+ const fromEnv = process.env.TESTPILOT_SKILL_DIR;
32291
+ if (fromEnv) return fromEnv;
32292
+ const bundled = fileURLToPath(new URL("./skills/testpilot", import.meta.url));
32293
+ if (existsSync6(bundled)) return bundled;
32294
+ const inRepo = fileURLToPath(new URL("../../../../../skills/testpilot", import.meta.url));
32295
+ return existsSync6(inRepo) ? inRepo : void 0;
32296
+ }
32297
+ async function copyCoreFiles(src, dest) {
32298
+ for (const name of CORE_FILES) {
32299
+ await cp(join21(src, name), join21(dest, name), { recursive: true, force: true });
32300
+ }
32301
+ for (const dir of CORE_DIRS) {
32302
+ if (existsSync6(join21(src, dir))) {
32303
+ await cp(join21(src, dir), join21(dest, dir), { recursive: true, force: true });
32304
+ }
32305
+ }
32306
+ }
32307
+ async function syncSkillFiles(root, options = {}) {
32308
+ const dest = join21(root, ".agents", "skills", "testpilot");
32309
+ const bundledVersion = readManifestVersion(resolveSkillSourceDir() ?? "");
32310
+ if (!await exists3(join21(dest, "manifest.yaml"))) {
32311
+ const src = resolveSkillSourceDir();
32312
+ if (!src) {
32313
+ throw new Error(
32314
+ "\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"
32315
+ );
32316
+ }
32317
+ await mkdir11(dirname7(dest), { recursive: true });
32318
+ await cp(src, dest, { recursive: true });
32319
+ return { core: "created", bundledVersion, dest };
32320
+ }
32321
+ const installedVersion = readManifestVersion(dest);
32322
+ const bundled = resolveSkillSourceDir();
32323
+ if (!bundled) {
32324
+ return { core: "current", installedVersion, dest };
32325
+ }
32326
+ if (!options.force && installedVersion === bundledVersion) {
32327
+ return { core: "current", installedVersion, bundledVersion, dest };
32328
+ }
32329
+ await copyCoreFiles(bundled, dest);
32330
+ return {
32331
+ core: "updated",
32332
+ installedVersion,
32333
+ bundledVersion,
32334
+ dest
32335
+ };
32336
+ }
32337
+ async function installSkill(root, info, adapters) {
32338
+ const dest = join21(root, ".agents", "skills", "testpilot");
32339
+ const sync = await syncSkillFiles(root);
32340
+ const core = sync.core === "created" ? "created" : sync.core === "updated" ? "updated" : "reused";
32341
+ const coreVersion = sync.core === "updated" || sync.core === "created" ? { from: sync.installedVersion, to: sync.bundledVersion } : void 0;
32342
+ const config2 = await loadTestpilotConfig(root).catch(() => void 0);
32343
+ const defaultEnvironment = typeof config2?.environment?.default === "string" ? config2.environment.default : "\u672A\u8BBE\u7F6E";
32344
+ const defaultWorkspace = config2?.workspace?.default ?? "\u672A\u8BBE\u7F6E(\u53EF\u5728 yaml workspace.default \u914D\u7F6E)";
32345
+ const refsDir = join21(dest, "references");
32346
+ const files = [
32347
+ ["project.md", () => renderProjectMd(info, adapters, defaultEnvironment, defaultWorkspace)],
32348
+ ["test-conventions.md", renderTestConventionsMd],
32349
+ ["adapters.md", () => renderAdaptersMd(adapters)]
32350
+ ];
32351
+ let references = "reused";
32352
+ for (const [name, render] of files) {
32353
+ const file2 = join21(refsDir, name);
32354
+ if (await exists3(file2)) continue;
32355
+ await mkdir11(refsDir, { recursive: true });
32356
+ await writeFile9(file2, render(), "utf8");
32357
+ references = "created";
32358
+ }
32359
+ return coreVersion ? { core, coreVersion, references, dest } : { core, references, dest };
32360
+ }
32361
+ function renderProjectMd(info, adapters, defaultEnvironment, defaultWorkspace) {
32362
+ const available = adapters.filter((item) => item.available).map((item) => item.id);
32363
+ return `# Project
32364
+
32365
+ \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
32366
+
32367
+ - Project name: ${info.name}
32368
+ - Project type: ${info.type}
32369
+ - Language: ${info.language}
32370
+ - Package manager: ${info.packageManager}
32371
+ - Test directory: tests/e2e
32372
+ - Case directory: tests/e2e/cases
32373
+ - Available adapters: ${available.length > 0 ? available.join(", ") : "(none detected)"}
32374
+ - Default environment: ${defaultEnvironment}
32375
+ - Default workspace: ${defaultWorkspace}
32376
+ `;
32377
+ }
32378
+ function renderTestConventionsMd() {
32379
+ return `# Test Conventions
32380
+
32381
+ \u672C\u9879\u76EE E2E \u6D4B\u8BD5\u7EA6\u5B9A,Agent \u751F\u6210/\u4FEE\u6539 Case \u65F6\u5FC5\u987B\u9075\u5B88:
32382
+
32383
+ 1. E2E Case \u4F4D\u4E8E tests/e2e/cases
32384
+ 2. Case \u4F7F\u7528 TestPilot DSL
32385
+ 3. \u4E0D\u5141\u8BB8\u76F4\u63A5\u8C03\u7528 Playwright
32386
+ 4. \u4E0D\u5141\u8BB8\u76F4\u63A5\u8C03\u7528 WeChatIDE
32387
+ 5. \u4E0D\u5141\u8BB8\u5728 Case \u4E2D\u5199\u660E\u6587\u5BC6\u7801(\u7528 accountRef \u5F15\u7528)
32388
+ 6. \u4F18\u5148\u590D\u7528\u5DF2\u6709 Case
32389
+ 7. \u65B0\u589E Case \u5FC5\u987B\u901A\u8FC7 csspilot validate
32390
+ 8. Case ID \u5FC5\u987B\u552F\u4E00
32391
+ `;
32392
+ }
32393
+ function renderAdaptersMd(adapters) {
32394
+ const lines = adapters.map(
32395
+ (item) => `- ${item.id}: ${item.available ? "\u53EF\u7528" : "\u4E0D\u53EF\u7528"}${item.required ? "(\u9879\u76EE\u9700\u8981)" : "(\u53EF\u9009)"} \u2014 ${item.detail}`
32396
+ );
32397
+ return `# Adapters
32398
+
32399
+ \u6267\u884C\u7AEF\u68C0\u6D4B\u7ED3\u679C(\u4EE5 \`csspilot doctor\` \u6700\u65B0\u8F93\u51FA\u4E3A\u51C6):
32400
+
32401
+ ${lines.join("\n")}
32402
+
32403
+ Agent \u6C38\u8FDC\u4E0D\u76F4\u63A5\u64CD\u4F5C Adapter;Case \u7ECF Execution Engine \u8C03\u5EA6\u5230\u5BF9\u5E94 Adapter \u6267\u884C\u3002
32404
+ `;
32405
+ }
32406
+
32262
32407
  // src/commands/init/doctor.ts
32263
32408
  async function runDoctor(root) {
32264
32409
  const sections = [];
@@ -32283,25 +32428,37 @@ async function runDoctor(root) {
32283
32428
  const projectItems = [
32284
32429
  ok(`Project ${info.name}(${info.type})`),
32285
32430
  info.git.commit ? ok(`Git repository(${info.git.branch ?? "detached"} \xB7 ${info.git.commit.slice(0, 7)})`) : warn("Git repository \u672A\u68C0\u6D4B\u5230", "Case Metadata \u65E0\u6CD5\u540C\u6B65\u5230 Server"),
32286
- existsSync6(join21(root, ".testpilot", "project.json")) ? ok(".testpilot/project.json") : error62(".testpilot/project.json", "\u8FD0\u884C npx csspilot init \u751F\u6210"),
32287
- existsSync6(join21(root, "testpilot.yaml")) ? ok("testpilot.yaml") : error62("testpilot.yaml", "\u8FD0\u884C npx csspilot init \u751F\u6210")
32431
+ existsSync7(join22(root, ".testpilot", "project.json")) ? ok(".testpilot/project.json") : error62(".testpilot/project.json", "\u8FD0\u884C npx csspilot init \u751F\u6210"),
32432
+ existsSync7(join22(root, "testpilot.yaml")) ? ok("testpilot.yaml") : error62("testpilot.yaml", "\u8FD0\u884C npx csspilot init \u751F\u6210")
32288
32433
  ];
32289
32434
  push("Project", projectItems);
32290
32435
  const config2 = await loadTestpilotConfig(root).catch(() => void 0);
32291
32436
  const casesDir = config2?.casesDir ?? "tests/e2e/cases";
32292
- const cases = await collectCases([join21(root, casesDir)], { root }).catch(() => []);
32437
+ const cases = await collectCases([join22(root, casesDir)], { root }).catch(() => []);
32293
32438
  const invalidCount = cases.filter((item) => !item.valid).length;
32294
- const testDir = join21(root, "tests", "e2e");
32439
+ const testDir = join22(root, "tests", "e2e");
32295
32440
  push("Test", [
32296
- existsSync6(testDir) ? ok("tests/e2e") : error62("tests/e2e", "\u8FD0\u884C npx csspilot init \u521B\u5EFA"),
32297
- existsSync6(join21(root, casesDir)) ? ok(`cases directory(${casesDir})`) : error62(`cases directory(${casesDir})`, "\u8FD0\u884C npx csspilot init \u521B\u5EFA"),
32441
+ existsSync7(testDir) ? ok("tests/e2e") : error62("tests/e2e", "\u8FD0\u884C npx csspilot init \u521B\u5EFA"),
32442
+ existsSync7(join22(root, casesDir)) ? ok(`cases directory(${casesDir})`) : error62(`cases directory(${casesDir})`, "\u8FD0\u884C npx csspilot init \u521B\u5EFA"),
32298
32443
  cases.length === 0 ? warn("Case validation \u8DF3\u8FC7(\u672A\u627E\u5230\u7528\u4F8B)", "\u5728 tests/e2e/cases \u521B\u5EFA Case") : invalidCount === 0 ? ok(`Case validation(${cases.length} \u4E2A\u7528\u4F8B\u5168\u90E8\u6709\u6548)`) : error62(`Case validation(${invalidCount}/${cases.length} \u4E2A\u7528\u4F8B\u6821\u9A8C\u5931\u8D25)`, "\u8FD0\u884C npx csspilot validate \u67E5\u770B\u8BE6\u60C5")
32299
32444
  ]);
32300
- const skillDir = join21(root, ".agents", "skills", "testpilot");
32301
- push("Agent", [
32302
- existsSync6(join21(skillDir, "manifest.yaml")) ? ok("TestPilot Skill(.agents/skills/testpilot)") : error62("TestPilot Skill", "\u8FD0\u884C npx csspilot init \u5B89\u88C5"),
32303
- existsSync6(join21(skillDir, "references", "project.md")) ? ok("Project testing guide(references/)") : warn("Project testing guide \u7F3A\u5931", "references/ \u7531 init \u751F\u6210;\u53EF\u91CD\u65B0\u8FD0\u884C npx csspilot init")
32304
- ]);
32445
+ const skillDir = join22(root, ".agents", "skills", "testpilot");
32446
+ const installedSkillVersion = readManifestVersion(skillDir);
32447
+ const skillSource = resolveSkillSourceDir();
32448
+ const bundledSkillVersion = skillSource ? readManifestVersion(skillSource) : void 0;
32449
+ const skillItems = [
32450
+ installedSkillVersion ? ok(`TestPilot Skill(.agents/skills/testpilot,v${installedSkillVersion})`) : error62("TestPilot Skill", "\u8FD0\u884C npx csspilot init \u5B89\u88C5"),
32451
+ existsSync7(join22(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")
32452
+ ];
32453
+ if (installedSkillVersion && bundledSkillVersion && installedSkillVersion !== bundledSkillVersion) {
32454
+ skillItems.push(
32455
+ warn(
32456
+ `Skill \u53EF\u66F4\u65B0(v${installedSkillVersion} -> v${bundledSkillVersion})`,
32457
+ "\u8FD0\u884C npx csspilot update \u5E76\u63D0\u4EA4 .agents/skills/ \u53D8\u66F4"
32458
+ )
32459
+ );
32460
+ }
32461
+ push("Agent", skillItems);
32305
32462
  const adapters = await detectAdapters(root, info.type);
32306
32463
  push(
32307
32464
  "Adapters",
@@ -32386,11 +32543,11 @@ function makeDoctorCommand() {
32386
32543
  import { join as join25 } from "node:path";
32387
32544
 
32388
32545
  // src/commands/init/config-initializer.ts
32389
- import { access as access2, mkdir as mkdir11, writeFile as writeFile9 } from "node:fs/promises";
32390
- import { basename as basename2, join as join22 } from "node:path";
32391
- async function exists3(path) {
32546
+ import { access as access3, mkdir as mkdir12, readFile as readFile8, writeFile as writeFile10 } from "node:fs/promises";
32547
+ import { basename as basename2, join as join23 } from "node:path";
32548
+ async function exists4(path) {
32392
32549
  try {
32393
- await access2(path);
32550
+ await access3(path);
32394
32551
  return true;
32395
32552
  } catch {
32396
32553
  return false;
@@ -32431,28 +32588,72 @@ ${adapters}
32431
32588
  # server:
32432
32589
  # baseUrl: http://127.0.0.1:3000
32433
32590
 
32434
- # Web \u7AEF:navigate \u76F8\u5BF9 url \u7684\u57FA\u51C6\u5730\u5740
32591
+ # Web \u7AEF:navigate \u76F8\u5BF9 url \u7684\u57FA\u51C6\u5730\u5740(\u6BCF\u4EBA\u7684\u5730\u5740\u4E0D\u540C,\u503C\u653E .env)
32435
32592
  # web:
32436
- # baseUrl: http://127.0.0.1:8080
32593
+ # baseUrl: \${WEB_BASE_URL}
32437
32594
 
32438
- # \u5C0F\u7A0B\u5E8F\u7AEF:\u586B\u5165\u5C0F\u7A0B\u5E8F\u9879\u76EE\u76EE\u5F55\u540E\u5373\u53EF\u6267\u884C target: miniapp \u7684\u6B65\u9AA4
32595
+ # \u5C0F\u7A0B\u5E8F\u7AEF:\u586B\u5165\u5C0F\u7A0B\u5E8F\u9879\u76EE\u76EE\u5F55\u540E\u5373\u53EF\u6267\u884C target: miniapp \u7684\u6B65\u9AA4(\u8DEF\u5F84\u6BCF\u4EBA\u4E0D\u540C,\u8D70 .env)
32439
32596
  # miniapp:
32440
- # projectPath: path/to/miniprogram
32441
- # cliPath: C:/Program Files (x86)/Tencent/\u5FAE\u4FE1web\u5F00\u53D1\u8005\u5DE5\u5177/cli.bat
32597
+ # projectPath: \${MINIAPP_PROJECT_PATH}
32598
+ # cliPath: \${MINIAPP_CLI_PATH}
32599
+ `;
32600
+ }
32601
+ function renderDotEnvExample() {
32602
+ 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)
32603
+ # \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
32604
+
32605
+ # \u73AF\u5883\u914D\u7F6E(environment \u6BB5)
32606
+ TEST_BASE_URL=http://127.0.0.1:8080
32607
+ STAGING_BASE_URL=https://staging.example.com
32608
+
32609
+ # Web \u7AEF\u76F8\u5BF9 url \u7684\u57FA\u51C6(\u53D6\u6D88 testpilot.yaml \u4E2D web \u6BB5\u6CE8\u91CA\u540E\u751F\u6548)
32610
+ # WEB_BASE_URL=http://127.0.0.1:8080
32611
+
32612
+ # \u5C0F\u7A0B\u5E8F\u7AEF(\u53D6\u6D88 testpilot.yaml \u4E2D miniapp \u6BB5\u6CE8\u91CA\u540E\u751F\u6548)
32613
+ # MINIAPP_PROJECT_PATH=path/to/miniprogram
32614
+ # MINIAPP_CLI_PATH=C:/Program Files (x86)/Tencent/\u5FAE\u4FE1web\u5F00\u53D1\u8005\u5DE5\u5177/cli.bat
32615
+
32616
+ # TestPilot Server(Control Plane,\u53EF\u9009)
32617
+ # TESTPILOT_SERVER_URL=http://127.0.0.1:3000
32442
32618
  `;
32443
32619
  }
32620
+ async function ensureGitignoreEntry(root, entry, comment = "TestPilot \u672C\u5730\u73AF\u5883\u53D8\u91CF") {
32621
+ const gitignorePath = join23(root, ".gitignore");
32622
+ if (!await exists4(gitignorePath)) {
32623
+ await writeFile10(gitignorePath, `# ${comment}
32624
+ ${entry}
32625
+ `, "utf8");
32626
+ return "created";
32627
+ }
32628
+ const content = await readFile8(gitignorePath, "utf8");
32629
+ if (content.split(/\r?\n/).some((item) => item.trim() === entry)) {
32630
+ return "exists";
32631
+ }
32632
+ const separator = content.endsWith("\n") || content === "" ? "" : "\n";
32633
+ await writeFile10(gitignorePath, `${content}${separator}# ${comment}
32634
+ ${entry}
32635
+ `, "utf8");
32636
+ return "updated";
32637
+ }
32444
32638
  async function initTestPilotConfig(root, info, adapters) {
32445
- const yamlPath = join22(root, "testpilot.yaml");
32639
+ const yamlPath = join23(root, "testpilot.yaml");
32446
32640
  let yaml = "reused";
32447
- if (!await exists3(yamlPath)) {
32448
- await writeFile9(yamlPath, renderTestpilotYaml(info), "utf8");
32641
+ if (!await exists4(yamlPath)) {
32642
+ await writeFile10(yamlPath, renderTestpilotYaml(info), "utf8");
32449
32643
  yaml = "created";
32450
32644
  }
32451
- const runtimeDir = join22(root, ".testpilot", "artifacts");
32452
- await mkdir11(runtimeDir, { recursive: true });
32453
- const keep = join22(runtimeDir, ".gitkeep");
32454
- if (!await exists3(keep)) {
32455
- await writeFile9(keep, "", "utf8");
32645
+ const envExamplePath = join23(root, ".env.example");
32646
+ let envExample = "reused";
32647
+ if (!await exists4(envExamplePath)) {
32648
+ await writeFile10(envExamplePath, renderDotEnvExample(), "utf8");
32649
+ envExample = "created";
32650
+ }
32651
+ const gitignore = await ensureGitignoreEntry(root, ".env");
32652
+ const runtimeDir = join23(root, ".testpilot", "artifacts");
32653
+ await mkdir12(runtimeDir, { recursive: true });
32654
+ const keep = join23(runtimeDir, ".gitkeep");
32655
+ if (!await exists4(keep)) {
32656
+ await writeFile10(keep, "", "utf8");
32456
32657
  }
32457
32658
  const existing = await readProjectLink(root);
32458
32659
  const defaultAdapter = defaultAdapterFor(info.type);
@@ -32471,7 +32672,14 @@ async function initTestPilotConfig(root, info, adapters) {
32471
32672
  }
32472
32673
  if (link.serverUrl === void 0 && serverUrl) link.serverUrl = serverUrl;
32473
32674
  const projectLinkFile = await writeProjectLink(link, root);
32474
- return { yaml, runtimeDir: join22(root, ".testpilot"), projectLink: link, projectLinkFile };
32675
+ return {
32676
+ yaml,
32677
+ runtimeDir: join23(root, ".testpilot"),
32678
+ projectLink: link,
32679
+ projectLinkFile,
32680
+ envExample,
32681
+ gitignore
32682
+ };
32475
32683
  }
32476
32684
  async function createProjectOnServer(root, info, _adapters, serverUrl) {
32477
32685
  const git = info.git;
@@ -32500,106 +32708,6 @@ function randomId() {
32500
32708
  return Math.random().toString(16).slice(2, 10);
32501
32709
  }
32502
32710
 
32503
- // src/commands/init/skill-installer.ts
32504
- import { existsSync as existsSync7 } from "node:fs";
32505
- import { access as access3, cp, mkdir as mkdir12, writeFile as writeFile10 } from "node:fs/promises";
32506
- import { dirname as dirname7, join as join23 } from "node:path";
32507
- import { fileURLToPath } from "node:url";
32508
- async function exists4(path) {
32509
- try {
32510
- await access3(path);
32511
- return true;
32512
- } catch {
32513
- return false;
32514
- }
32515
- }
32516
- function resolveSkillSourceDir() {
32517
- const fromEnv = process.env.TESTPILOT_SKILL_DIR;
32518
- if (fromEnv) return fromEnv;
32519
- const bundled = fileURLToPath(new URL("./skills/testpilot", import.meta.url));
32520
- if (existsSync7(bundled)) return bundled;
32521
- const inRepo = fileURLToPath(new URL("../../../../../skills/testpilot", import.meta.url));
32522
- return existsSync7(inRepo) ? inRepo : void 0;
32523
- }
32524
- async function installSkill(root, info, adapters) {
32525
- const dest = join23(root, ".agents", "skills", "testpilot");
32526
- let core = "reused";
32527
- if (!await exists4(join23(dest, "manifest.yaml"))) {
32528
- const src = resolveSkillSourceDir();
32529
- if (!src) {
32530
- throw new Error(
32531
- "\u65E0\u6CD5\u5B9A\u4F4D TestPilot Skill \u6E90\u76EE\u5F55;\u53EF\u901A\u8FC7\u73AF\u5883\u53D8\u91CF TESTPILOT_SKILL_DIR \u6307\u5B9A skills/testpilot \u8DEF\u5F84"
32532
- );
32533
- }
32534
- await mkdir12(dirname7(dest), { recursive: true });
32535
- await cp(src, dest, { recursive: true });
32536
- core = "created";
32537
- }
32538
- const config2 = await loadTestpilotConfig(root).catch(() => void 0);
32539
- const defaultEnvironment = typeof config2?.environment?.default === "string" ? config2.environment.default : "\u672A\u8BBE\u7F6E";
32540
- const defaultWorkspace = config2?.workspace?.default ?? "\u672A\u8BBE\u7F6E(\u53EF\u5728 yaml workspace.default \u914D\u7F6E)";
32541
- const refsDir = join23(dest, "references");
32542
- const files = [
32543
- ["project.md", () => renderProjectMd(info, adapters, defaultEnvironment, defaultWorkspace)],
32544
- ["test-conventions.md", renderTestConventionsMd],
32545
- ["adapters.md", () => renderAdaptersMd(adapters)]
32546
- ];
32547
- let references = "reused";
32548
- for (const [name, render] of files) {
32549
- const file2 = join23(refsDir, name);
32550
- if (await exists4(file2)) continue;
32551
- await mkdir12(refsDir, { recursive: true });
32552
- await writeFile10(file2, render(), "utf8");
32553
- references = "created";
32554
- }
32555
- return { core, references, dest };
32556
- }
32557
- function renderProjectMd(info, adapters, defaultEnvironment, defaultWorkspace) {
32558
- const available = adapters.filter((item) => item.available).map((item) => item.id);
32559
- return `# Project
32560
-
32561
- \u81EA\u52A8\u751F\u6210\u4E8E \`csspilot init\`,\u63CF\u8FF0\u672C\u9879\u76EE\u7684 TestPilot \u63A5\u5165\u4FE1\u606F;Agent \u4F18\u5148\u4ECE\u8FD9\u91CC\u4E86\u89E3\u9879\u76EE\u3002
32562
-
32563
- - Project name: ${info.name}
32564
- - Project type: ${info.type}
32565
- - Language: ${info.language}
32566
- - Package manager: ${info.packageManager}
32567
- - Test directory: tests/e2e
32568
- - Case directory: tests/e2e/cases
32569
- - Available adapters: ${available.length > 0 ? available.join(", ") : "(none detected)"}
32570
- - Default environment: ${defaultEnvironment}
32571
- - Default workspace: ${defaultWorkspace}
32572
- `;
32573
- }
32574
- function renderTestConventionsMd() {
32575
- return `# Test Conventions
32576
-
32577
- \u672C\u9879\u76EE E2E \u6D4B\u8BD5\u7EA6\u5B9A,Agent \u751F\u6210/\u4FEE\u6539 Case \u65F6\u5FC5\u987B\u9075\u5B88:
32578
-
32579
- 1. E2E Case \u4F4D\u4E8E tests/e2e/cases
32580
- 2. Case \u4F7F\u7528 TestPilot DSL
32581
- 3. \u4E0D\u5141\u8BB8\u76F4\u63A5\u8C03\u7528 Playwright
32582
- 4. \u4E0D\u5141\u8BB8\u76F4\u63A5\u8C03\u7528 WeChatIDE
32583
- 5. \u4E0D\u5141\u8BB8\u5728 Case \u4E2D\u5199\u660E\u6587\u5BC6\u7801(\u7528 accountRef \u5F15\u7528)
32584
- 6. \u4F18\u5148\u590D\u7528\u5DF2\u6709 Case
32585
- 7. \u65B0\u589E Case \u5FC5\u987B\u901A\u8FC7 csspilot validate
32586
- 8. Case ID \u5FC5\u987B\u552F\u4E00
32587
- `;
32588
- }
32589
- function renderAdaptersMd(adapters) {
32590
- const lines = adapters.map(
32591
- (item) => `- ${item.id}: ${item.available ? "\u53EF\u7528" : "\u4E0D\u53EF\u7528"}${item.required ? "(\u9879\u76EE\u9700\u8981)" : "(\u53EF\u9009)"} \u2014 ${item.detail}`
32592
- );
32593
- return `# Adapters
32594
-
32595
- \u6267\u884C\u7AEF\u68C0\u6D4B\u7ED3\u679C(\u4EE5 \`csspilot doctor\` \u6700\u65B0\u8F93\u51FA\u4E3A\u51C6):
32596
-
32597
- ${lines.join("\n")}
32598
-
32599
- Agent \u6C38\u8FDC\u4E0D\u76F4\u63A5\u64CD\u4F5C Adapter;Case \u7ECF Execution Engine \u8C03\u5EA6\u5230\u5BF9\u5E94 Adapter \u6267\u884C\u3002
32600
- `;
32601
- }
32602
-
32603
32711
  // src/commands/init/test-structure.ts
32604
32712
  import { existsSync as existsSync8, readdirSync as readdirSync2 } from "node:fs";
32605
32713
  import { mkdir as mkdir13, writeFile as writeFile11 } from "node:fs/promises";
@@ -32657,6 +32765,12 @@ async function runInit(root, options = {}) {
32657
32765
  console.log(`\u2713 Runtime \u76EE\u5F55 ${join25(".testpilot", "artifacts")}`);
32658
32766
  const serverMode = config2.projectLink.serverUrl ? `Server:${config2.projectLink.serverUrl}` : "\u672C\u5730\u6A21\u5F0F";
32659
32767
  console.log(`\u2713 \u9879\u76EE\u5173\u8054 ${config2.projectLink.projectId}(${serverMode})`);
32768
+ console.log(
32769
+ 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)"
32770
+ );
32771
+ console.log(
32772
+ config2.gitignore === "exists" ? "\u2713 Git \u5FFD\u7565 .env(\u5DF2\u5728 .gitignore)" : `\u2713 Git \u5FFD\u7565 .env(\u5DF2\u5199\u5165 .gitignore)`
32773
+ );
32660
32774
  const testStructure = await initTestStructure(root, project);
32661
32775
  const reuseNote = project.existingE2eDirs.filter((dir) => dir !== "tests/e2e");
32662
32776
  console.log(
@@ -32666,9 +32780,15 @@ async function runInit(root, options = {}) {
32666
32780
  console.log(` \u26A0 \u68C0\u6D4B\u5230\u5176\u4ED6 E2E \u76EE\u5F55:${reuseNote.join(", ")}(\u4FDD\u7559\u4E0D\u52A8,\u672A\u5F3A\u5236\u8FC1\u79FB)`);
32667
32781
  }
32668
32782
  const skill = await installSkill(root, project, adapters);
32669
- console.log(
32670
- skill.core === "created" ? `\u2713 Skill \u5B89\u88C5 ${join25(".agents", "skills", "testpilot")}` : "\u2713 Skill \u5B89\u88C5 .agents/skills/testpilot/(\u5DF2\u5B58\u5728,\u8DF3\u8FC7)"
32671
- );
32783
+ if (skill.core === "updated") {
32784
+ console.log(
32785
+ `\u2713 Skill \u66F4\u65B0 .agents/skills/testpilot/(v${skill.coreVersion?.from ?? "?"} -> v${skill.coreVersion?.to ?? "?"})`
32786
+ );
32787
+ } else {
32788
+ console.log(
32789
+ skill.core === "created" ? `\u2713 Skill \u5B89\u88C5 ${join25(".agents", "skills", "testpilot")}` : "\u2713 Skill \u5B89\u88C5 .agents/skills/testpilot/(\u5DF2\u5B58\u5728,\u8DF3\u8FC7)"
32790
+ );
32791
+ }
32672
32792
  console.log(
32673
32793
  skill.references === "created" ? "\u2713 Agent \u4E0A\u4E0B\u6587 references/{project,test-conventions,adapters}.md" : "\u2713 Agent \u4E0A\u4E0B\u6587 references/(\u5DF2\u5B58\u5728,\u4FDD\u7559)"
32674
32794
  );
@@ -32898,6 +33018,37 @@ function makeSyncMetadataCommand() {
32898
33018
  });
32899
33019
  }
32900
33020
 
33021
+ // src/commands/update.ts
33022
+ import { writeFile as writeFile12 } from "node:fs/promises";
33023
+ import { join as join27 } from "node:path";
33024
+ function makeUpdateCommand() {
33025
+ 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) => {
33026
+ const root = process.cwd();
33027
+ const dest = join27(".agents", "skills", "testpilot");
33028
+ const skill = await syncSkillFiles(root, { force: options.force });
33029
+ switch (skill.core) {
33030
+ case "created":
33031
+ console.log(`\u2713 Skill \u5B89\u88C5 ${dest}(v${skill.bundledVersion ?? "?"})`);
33032
+ break;
33033
+ case "updated":
33034
+ console.log(
33035
+ `\u2713 Skill \u66F4\u65B0 ${dest} v${skill.installedVersion ?? "?"} -> v${skill.bundledVersion ?? "?"}`
33036
+ );
33037
+ break;
33038
+ default:
33039
+ console.log(`\u2713 Skill \u5DF2\u662F\u6700\u65B0 v${skill.bundledVersion ?? "?"}`);
33040
+ }
33041
+ await writeFile12(join27(root, ".env.example"), renderDotEnvExample(), "utf8");
33042
+ console.log("\u2713 .env.example \u5DF2\u540C\u6B65\u4E3A\u5F53\u524D\u7248\u672C\u7684\u793A\u4F8B(.env \u4E0D\u53D7\u5F71\u54CD)");
33043
+ const gitignore = await ensureGitignoreEntry(root, ".env");
33044
+ console.log(
33045
+ gitignore === "exists" ? "\u2713 .gitignore \u5DF2\u5FFD\u7565 .env" : "\u2713 .gitignore \u5DF2\u8FFD\u52A0\u5FFD\u7565 .env"
33046
+ );
33047
+ 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");
33048
+ });
33049
+ return update;
33050
+ }
33051
+
32901
33052
  // src/commands/validate.ts
32902
33053
  function makeValidateCommand() {
32903
33054
  return new Command("validate").description("\u6821\u9A8C\u7528\u4F8B DSL").argument("[paths...]", "\u7528\u4F8B\u6587\u4EF6\u6216\u76EE\u5F55,\u9ED8\u8BA4\u6309\u9879\u76EE\u914D\u7F6E").action(async (paths) => {
@@ -32934,10 +33085,10 @@ function makeValidateCommand() {
32934
33085
  }
32935
33086
 
32936
33087
  // src/cli.ts
32937
- var CLI_VERSION = "0.3.2";
33088
+ var CLI_VERSION = "0.4.0";
32938
33089
  async function createCli() {
32939
33090
  const program2 = new Command();
32940
- program2.name("csspilot").description("TestPilot \u2014 AI Native \u8DE8\u7AEF\u4E1A\u52A1\u6D4B\u8BD5\u57FA\u7840\u8BBE\u65BD CLI").version(CLI_VERSION).addCommand(makeInitCommand()).addCommand(makeValidateCommand()).addCommand(makeListCommand()).addCommand(makeRunCommand()).addCommand(makeReportCommand()).addCommand(makeSyncMetadataCommand()).addCommand(makeDoctorCommand()).addCommand(makeCiCommand());
33091
+ 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(makeUpdateCommand()).addCommand(makeCiCommand());
32941
33092
  return program2;
32942
33093
  }
32943
33094
  export {
@@ -12,6 +12,7 @@ TestPilot is a **tool, not a business test project**. Business cases always belo
12
12
 
13
13
  Use TestPilot when the user wants to:
14
14
 
15
+ - review test requirements for testability
15
16
  - create business test cases
16
17
  - validate test cases
17
18
  - run tests
@@ -20,12 +21,13 @@ Use TestPilot when the user wants to:
20
21
  ## Core workflow
21
22
 
22
23
  1. Understand user requirement
23
- 2. Inspect project
24
- 3. Identify test target
25
- 4. Create TestPilot Case (`tests/e2e/cases/*.yaml`)
26
- 5. Validate Case (`npx csspilot validate`)
27
- 6. Run Case (`npx csspilot run`)
28
- 7. Analyze result (`npx csspilot report`)
24
+ 2. Review requirements & produce test points (`workflows/test-requirements-review.md`)
25
+ 3. Inspect project
26
+ 4. Identify test target
27
+ 5. Create TestPilot Case (`tests/e2e/cases/*.yaml`, linked to test point IDs)
28
+ 6. Validate Case (`npx csspilot validate`)
29
+ 7. Run Case (`npx csspilot run`)
30
+ 8. Analyze result (`npx csspilot report`)
29
31
 
30
32
  ## Important rules
31
33
 
@@ -72,4 +74,5 @@ Read `references/` first (generated by `csspilot init` for this project):
72
74
  - `rules/dsl.md` — case structure and the V0.1 action set
73
75
  - `rules/locator.md` — locator forms and the no-invention rule
74
76
  - `rules/assertion.md` — assertions and variable references
75
- - `workflows/` — step-by-step workflows (create / validate / run / analyze)
77
+ - `workflows/test-requirements-review.md` — requirements review, test point checklist
78
+ - `workflows/` — step-by-step workflows (review / create / validate / run / analyze)
@@ -2,12 +2,13 @@
2
2
  # `npx csspilot init` 会把整个 skills/testpilot/ 安装到业务项目 .agents/skills/testpilot/
3
3
  id: testpilot
4
4
  name: TestPilot
5
- version: 0.2.0
5
+ version: 0.4.0
6
6
 
7
7
  description: >
8
8
  AI Native cross-platform testing skill.
9
9
 
10
10
  capabilities:
11
+ - test-requirements-review
11
12
  - create-case
12
13
  - validate
13
14
  - run
@@ -37,4 +38,4 @@ permissions:
37
38
  - npx
38
39
  - pnpm
39
40
  - npm
40
- - testpilot
41
+ - csspilot
@@ -1,13 +1,16 @@
1
1
  # Workflow:创建测试 Case
2
2
 
3
- 输入:用户的一句业务需求(如"帮我测试用户下单")。
3
+ 输入:用户需求,或 `test-requirements-review` 产出的**测试点清单**(推荐先做需求评审)。
4
4
 
5
- 1. 阅读本 Skill:`rules/dsl.md`、`rules/case-design.md`、`rules/locator.md`。
6
- 2. 分析项目结构:确定被测端(miniapp / web)与业务入口页面。
7
- 3. 找到业务路径上的真实页面与组件,收集证据(读代码 / DOM / 现有测试)。
8
- 4. 确定测试路径:从入口到业务结果页的完整步骤。
9
- 5. 按 DSL 规范生成 Case,写入 `tests/e2e/cases/<case-id>.yaml`。
5
+ 1. 尚无测试点清单时,先执行 `workflows/test-requirements-review.md`;已有清单则直接使用。
6
+ 2. 阅读本 Skill:`rules/dsl.md`、`rules/case-design.md`、`rules/locator.md`。
7
+ 3. 分析项目结构:确定被测端(miniapp / web)与业务入口页面。
8
+ 4. 针对测试点收集证据(读代码 / DOM / 现有测试),确定业务路径:从入口到业务结果页的完整步骤。
9
+ 5. 按 DSL 规范生成 Case,写入 `tests/e2e/cases/<case-id>.yaml`:
10
+ - 一条 Case 覆盖一个测试点(或一组强关联测试点)
11
+ - Case 的 `description` 注明关联的测试点编号(如 `测试点: TP-001`),保持需求可溯源
12
+ - 按评审产出的平台执行信息声明 `accountRef` / `workspace`;发现 `.env` 缺少某平台的地址或凭据变量时,向用户索取后写入 `.env`,**不写进 Case**
10
13
  6. 执行 `npx csspilot validate tests/e2e/cases/<case-id>.yaml`。
11
14
  7. 校验失败则修复 Case 后重新 validate,直到通过。
12
15
 
13
- 禁止:跳过第 2-3 步直接生成 Case(AI 不能凭空生成测试)。
16
+ 禁止:跳过第 3-4 步直接生成 Case(AI 不能凭空生成测试)。
@@ -0,0 +1,61 @@
1
+ # Workflow:测试需求评审(可测性评审 + 测试点分析)
2
+
3
+ 输入:需求文档 / PRD / 用户的一句话(如"帮我测试用户下单")。
4
+ 产出:**测试点清单** + **平台执行信息表** + 澄清问题 + 评审结论。测试点清单是 `create-case` 的正式输入。
5
+
6
+ ## 步骤
7
+
8
+ 1. **复述理解**:用自己的话列出需求覆盖的功能点与业务流程,向用户确认理解偏差。
9
+ 2. **平台盘点**:识别需求涉及的执行端(miniapp / web / api)。跨端需求(如"小程序下单、Web 后台验证")必须逐平台澄清执行信息,见下方"多平台"一节。
10
+ 3. **可测性检查清单**(逐项过):
11
+ - 验收标准是否明确:输入 / 操作步骤 / 预期结果是否可判定
12
+ - 边界与异常:空值、极值、重复提交、并发、权限、网络异常
13
+ - 数据准备可行性:测试账号、mock 数据、上下游依赖是否可用
14
+ - 平台与入口:miniapp / web 的页面路径与入口是否真实存在(不确定时先看项目代码)
15
+ - 回归范围:改动是否影响现有功能,需要哪些已有 Case 回归
16
+ 4. **输出测试点清单**,每条包含:
17
+
18
+ | 字段 | 说明 |
19
+ | --- | --- |
20
+ | 编号 | `TP-001` 格式,Case 需回链 |
21
+ | 模块 | 所属功能模块 |
22
+ | 场景 | 一句话描述验证什么 |
23
+ | 类型 | 正向 / 边界 / 异常 |
24
+ | 优先级 | P0 / P1 / P2 |
25
+ | 平台 | miniapp / web / 双端 |
26
+
27
+ 5. **澄清阻塞项**:需求缺失、矛盾、不可判定时,列出问题向用户提问。**不猜测**。
28
+ 6. **给出评审结论**:
29
+ - 可测:测试点清单完整,直接进入 `create-case`
30
+ - 有条件可测:部分测试点依赖澄清,先对可执行部分建 Case
31
+ - 不可测:说明缺口,等待需求补充
32
+
33
+ ## 多平台:平台执行信息澄清
34
+
35
+ 需求涉及多个平台时,**每个平台都必须澄清以下三项**;缺任何一项,该平台相关测试点只能标记"有条件可测",不得开始写 Case:
36
+
37
+ | 平台 | 必须澄清 | 落地位置 |
38
+ | --- | --- | --- |
39
+ | web | 测试地址(baseUrl)+ 页面入口路由 + 登录角色 | `.env` 的 `WEB_BASE_URL` / `TEST_BASE_URL`;`accountRef` |
40
+ | miniapp | 小程序项目路径 + 起始页面路由 + 登录角色(连哪个环境后端) | `.env` 的 `MINIAPP_PROJECT_PATH`;`accountRef` |
41
+ | api | 服务 baseUrl + 鉴权方式(token / session)+ 账号角色 | `.env` 的 `API_BASE_URL`;`accountRef` |
42
+
43
+ 规则:
44
+
45
+ - **登录角色**:每个平台确认用什么角色(普通用户 / 管理员 / 游客),映射为 `accountRef`(如 `admin` / `test-user`);真实凭据放 `TESTPILOT_ACCOUNT_<REF大写>` 环境变量或 Server Secrets,**禁止写进 Case**
46
+ - **测试地址**:一律指向测试环境,写入 `.env`(如 `WEB_BASE_URL`);**禁止把内网 / 生产地址写进 Case 或 testpilot.yaml**
47
+ - **使用 Workspace 的项目**:以上信息应登记为 Workspace 的绑定(项目 → 环境),Case 用 `workspace: <名称>` 声明,不逐条硬编码
48
+
49
+ ### 澄清问题示例(多平台需求)
50
+
51
+ - Web 后台的测试地址是什么?订单查询页面的入口路由是?
52
+ - 小程序连接哪个环境的后端?小程序项目目录在本机哪里?
53
+ - 各平台分别用什么角色登录(普通用户 / 管理员)?该角色是否有完成流程所需的权限?
54
+ - 这些地址和账号是否已写入 `.env`?缺哪个我来补?
55
+
56
+ ## 铁律
57
+
58
+ - 没有测试点清单,不要进入 `create-case`。
59
+ - 涉及多平台但执行信息(地址 / 入口 / 登录角色)不全时,结论只能是"有条件可测",先澄清再建 Case。
60
+ - 评审基于需求 + 项目代码/页面证据;两项都没有时先收集证据,不空谈。
61
+ - 每个测试点必须可验证:都能回答"怎么判定通过"。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "csspilot",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "TestPilot — AI Native 跨端业务测试基础设施 CLI",
5
5
  "license": "MIT",
6
6
  "repository": {