csspilot 0.3.1 → 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/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 {
@@ -30183,7 +30183,17 @@ var configSchema = external_exports.strictObject({
30183
30183
  cliPath: external_exports.string().min(1).optional()
30184
30184
  }).optional()
30185
30185
  });
30186
+ function interpolateEnvRefs(value) {
30187
+ return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (match, name) => process.env[name] ?? match);
30188
+ }
30189
+ function loadProjectEnvFile(root = process.cwd()) {
30190
+ try {
30191
+ process.loadEnvFile(join3(root, ".env"));
30192
+ } catch {
30193
+ }
30194
+ }
30186
30195
  async function loadTestpilotConfig(cwd = process.cwd()) {
30196
+ loadProjectEnvFile(cwd);
30187
30197
  const file2 = join3(cwd, "testpilot.yaml");
30188
30198
  let raw;
30189
30199
  try {
@@ -30199,19 +30209,44 @@ async function loadTestpilotConfig(cwd = process.cwd()) {
30199
30209
  const detail = result.error.issues.map((issue2) => `${issue2.path.map(String).join(".")}: ${issue2.message}`).join("; ");
30200
30210
  throw new Error(`testpilot.yaml \u914D\u7F6E\u4E0D\u5408\u6CD5:${detail}`);
30201
30211
  }
30212
+ const parsed = result.data;
30213
+ const environment = parsed.environment ? Object.fromEntries(
30214
+ Object.entries(parsed.environment).map(([name, entry]) => {
30215
+ if (name === "default" || typeof entry === "string") {
30216
+ return [name, entry];
30217
+ }
30218
+ if (entry === void 0) {
30219
+ return [name, void 0];
30220
+ }
30221
+ return [
30222
+ name,
30223
+ { baseUrl: entry.baseUrl === void 0 ? void 0 : interpolateEnvRefs(entry.baseUrl) }
30224
+ ];
30225
+ })
30226
+ ) : void 0;
30202
30227
  return {
30203
- version: result.data.version,
30228
+ version: parsed.version,
30204
30229
  // 用例目录:旧字段 casesDir 优先,其次 test.caseDirectory,最后默认值
30205
- casesDir: result.data.casesDir ?? result.data.test?.caseDirectory ?? DEFAULT_CASES_DIR,
30206
- testDirectory: result.data.test?.directory,
30207
- projectName: result.data.project?.name,
30208
- environment: result.data.environment,
30209
- adapters: result.data.adapters,
30210
- workspace: result.data.workspace,
30211
- server: result.data.server,
30212
- web: result.data.web,
30213
- api: result.data.api,
30214
- miniapp: result.data.miniapp
30230
+ casesDir: parsed.casesDir ?? parsed.test?.caseDirectory ?? DEFAULT_CASES_DIR,
30231
+ testDirectory: parsed.test?.directory,
30232
+ projectName: parsed.project?.name,
30233
+ environment,
30234
+ adapters: parsed.adapters,
30235
+ workspace: parsed.workspace,
30236
+ server: parsed.server ? {
30237
+ baseUrl: parsed.server.baseUrl === void 0 ? void 0 : interpolateEnvRefs(parsed.server.baseUrl)
30238
+ } : void 0,
30239
+ web: parsed.web ? {
30240
+ baseUrl: parsed.web.baseUrl === void 0 ? void 0 : interpolateEnvRefs(parsed.web.baseUrl)
30241
+ } : void 0,
30242
+ api: parsed.api ? {
30243
+ baseUrl: parsed.api.baseUrl === void 0 ? void 0 : interpolateEnvRefs(parsed.api.baseUrl),
30244
+ timeoutMs: parsed.api.timeoutMs
30245
+ } : void 0,
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
30215
30250
  };
30216
30251
  }
30217
30252
 
@@ -32134,8 +32169,12 @@ async function collectEnvVarRefs(root) {
32134
32169
  return [];
32135
32170
  }
32136
32171
  const names = /* @__PURE__ */ new Set();
32137
- for (const match of text.matchAll(ENV_REF_PATTERN)) {
32138
- 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
+ }
32139
32178
  }
32140
32179
  return [...names].sort();
32141
32180
  }
@@ -32227,6 +32266,144 @@ function adaptersFor(type) {
32227
32266
  return ["playwright"];
32228
32267
  }
32229
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
+
32230
32407
  // src/commands/init/doctor.ts
32231
32408
  async function runDoctor(root) {
32232
32409
  const sections = [];
@@ -32251,25 +32428,37 @@ async function runDoctor(root) {
32251
32428
  const projectItems = [
32252
32429
  ok(`Project ${info.name}(${info.type})`),
32253
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"),
32254
- existsSync6(join21(root, ".testpilot", "project.json")) ? ok(".testpilot/project.json") : error62(".testpilot/project.json", "\u8FD0\u884C npx csspilot init \u751F\u6210"),
32255
- 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")
32256
32433
  ];
32257
32434
  push("Project", projectItems);
32258
32435
  const config2 = await loadTestpilotConfig(root).catch(() => void 0);
32259
32436
  const casesDir = config2?.casesDir ?? "tests/e2e/cases";
32260
- const cases = await collectCases([join21(root, casesDir)], { root }).catch(() => []);
32437
+ const cases = await collectCases([join22(root, casesDir)], { root }).catch(() => []);
32261
32438
  const invalidCount = cases.filter((item) => !item.valid).length;
32262
- const testDir = join21(root, "tests", "e2e");
32439
+ const testDir = join22(root, "tests", "e2e");
32263
32440
  push("Test", [
32264
- existsSync6(testDir) ? ok("tests/e2e") : error62("tests/e2e", "\u8FD0\u884C npx csspilot init \u521B\u5EFA"),
32265
- 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"),
32266
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")
32267
32444
  ]);
32268
- const skillDir = join21(root, ".agents", "skills", "testpilot");
32269
- push("Agent", [
32270
- existsSync6(join21(skillDir, "manifest.yaml")) ? ok("TestPilot Skill(.agents/skills/testpilot)") : error62("TestPilot Skill", "\u8FD0\u884C npx csspilot init \u5B89\u88C5"),
32271
- 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")
32272
- ]);
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);
32273
32462
  const adapters = await detectAdapters(root, info.type);
32274
32463
  push(
32275
32464
  "Adapters",
@@ -32354,11 +32543,11 @@ function makeDoctorCommand() {
32354
32543
  import { join as join25 } from "node:path";
32355
32544
 
32356
32545
  // src/commands/init/config-initializer.ts
32357
- import { access as access2, mkdir as mkdir11, writeFile as writeFile9 } from "node:fs/promises";
32358
- import { basename as basename2, join as join22 } from "node:path";
32359
- 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) {
32360
32549
  try {
32361
- await access2(path);
32550
+ await access3(path);
32362
32551
  return true;
32363
32552
  } catch {
32364
32553
  return false;
@@ -32399,28 +32588,72 @@ ${adapters}
32399
32588
  # server:
32400
32589
  # baseUrl: http://127.0.0.1:3000
32401
32590
 
32402
- # 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)
32403
32592
  # web:
32404
- # baseUrl: http://127.0.0.1:8080
32593
+ # baseUrl: \${WEB_BASE_URL}
32405
32594
 
32406
- # \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)
32407
32596
  # miniapp:
32408
- # projectPath: path/to/miniprogram
32409
- # 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}
32410
32599
  `;
32411
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
32618
+ `;
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
+ }
32412
32638
  async function initTestPilotConfig(root, info, adapters) {
32413
- const yamlPath = join22(root, "testpilot.yaml");
32639
+ const yamlPath = join23(root, "testpilot.yaml");
32414
32640
  let yaml = "reused";
32415
- if (!await exists3(yamlPath)) {
32416
- await writeFile9(yamlPath, renderTestpilotYaml(info), "utf8");
32641
+ if (!await exists4(yamlPath)) {
32642
+ await writeFile10(yamlPath, renderTestpilotYaml(info), "utf8");
32417
32643
  yaml = "created";
32418
32644
  }
32419
- const runtimeDir = join22(root, ".testpilot", "artifacts");
32420
- await mkdir11(runtimeDir, { recursive: true });
32421
- const keep = join22(runtimeDir, ".gitkeep");
32422
- if (!await exists3(keep)) {
32423
- 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");
32424
32657
  }
32425
32658
  const existing = await readProjectLink(root);
32426
32659
  const defaultAdapter = defaultAdapterFor(info.type);
@@ -32439,7 +32672,14 @@ async function initTestPilotConfig(root, info, adapters) {
32439
32672
  }
32440
32673
  if (link.serverUrl === void 0 && serverUrl) link.serverUrl = serverUrl;
32441
32674
  const projectLinkFile = await writeProjectLink(link, root);
32442
- 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
+ };
32443
32683
  }
32444
32684
  async function createProjectOnServer(root, info, _adapters, serverUrl) {
32445
32685
  const git = info.git;
@@ -32468,106 +32708,6 @@ function randomId() {
32468
32708
  return Math.random().toString(16).slice(2, 10);
32469
32709
  }
32470
32710
 
32471
- // src/commands/init/skill-installer.ts
32472
- import { existsSync as existsSync7 } from "node:fs";
32473
- import { access as access3, cp, mkdir as mkdir12, writeFile as writeFile10 } from "node:fs/promises";
32474
- import { dirname as dirname7, join as join23 } from "node:path";
32475
- import { fileURLToPath } from "node:url";
32476
- async function exists4(path) {
32477
- try {
32478
- await access3(path);
32479
- return true;
32480
- } catch {
32481
- return false;
32482
- }
32483
- }
32484
- function resolveSkillSourceDir() {
32485
- const fromEnv = process.env.TESTPILOT_SKILL_DIR;
32486
- if (fromEnv) return fromEnv;
32487
- const bundled = fileURLToPath(new URL("./skills/testpilot", import.meta.url));
32488
- if (existsSync7(bundled)) return bundled;
32489
- const inRepo = fileURLToPath(new URL("../../../../../skills/testpilot", import.meta.url));
32490
- return existsSync7(inRepo) ? inRepo : void 0;
32491
- }
32492
- async function installSkill(root, info, adapters) {
32493
- const dest = join23(root, ".agents", "skills", "testpilot");
32494
- let core = "reused";
32495
- if (!await exists4(join23(dest, "manifest.yaml"))) {
32496
- const src = resolveSkillSourceDir();
32497
- if (!src) {
32498
- throw new Error(
32499
- "\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"
32500
- );
32501
- }
32502
- await mkdir12(dirname7(dest), { recursive: true });
32503
- await cp(src, dest, { recursive: true });
32504
- core = "created";
32505
- }
32506
- const config2 = await loadTestpilotConfig(root).catch(() => void 0);
32507
- const defaultEnvironment = typeof config2?.environment?.default === "string" ? config2.environment.default : "\u672A\u8BBE\u7F6E";
32508
- const defaultWorkspace = config2?.workspace?.default ?? "\u672A\u8BBE\u7F6E(\u53EF\u5728 yaml workspace.default \u914D\u7F6E)";
32509
- const refsDir = join23(dest, "references");
32510
- const files = [
32511
- ["project.md", () => renderProjectMd(info, adapters, defaultEnvironment, defaultWorkspace)],
32512
- ["test-conventions.md", renderTestConventionsMd],
32513
- ["adapters.md", () => renderAdaptersMd(adapters)]
32514
- ];
32515
- let references = "reused";
32516
- for (const [name, render] of files) {
32517
- const file2 = join23(refsDir, name);
32518
- if (await exists4(file2)) continue;
32519
- await mkdir12(refsDir, { recursive: true });
32520
- await writeFile10(file2, render(), "utf8");
32521
- references = "created";
32522
- }
32523
- return { core, references, dest };
32524
- }
32525
- function renderProjectMd(info, adapters, defaultEnvironment, defaultWorkspace) {
32526
- const available = adapters.filter((item) => item.available).map((item) => item.id);
32527
- return `# Project
32528
-
32529
- \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
32530
-
32531
- - Project name: ${info.name}
32532
- - Project type: ${info.type}
32533
- - Language: ${info.language}
32534
- - Package manager: ${info.packageManager}
32535
- - Test directory: tests/e2e
32536
- - Case directory: tests/e2e/cases
32537
- - Available adapters: ${available.length > 0 ? available.join(", ") : "(none detected)"}
32538
- - Default environment: ${defaultEnvironment}
32539
- - Default workspace: ${defaultWorkspace}
32540
- `;
32541
- }
32542
- function renderTestConventionsMd() {
32543
- return `# Test Conventions
32544
-
32545
- \u672C\u9879\u76EE E2E \u6D4B\u8BD5\u7EA6\u5B9A,Agent \u751F\u6210/\u4FEE\u6539 Case \u65F6\u5FC5\u987B\u9075\u5B88:
32546
-
32547
- 1. E2E Case \u4F4D\u4E8E tests/e2e/cases
32548
- 2. Case \u4F7F\u7528 TestPilot DSL
32549
- 3. \u4E0D\u5141\u8BB8\u76F4\u63A5\u8C03\u7528 Playwright
32550
- 4. \u4E0D\u5141\u8BB8\u76F4\u63A5\u8C03\u7528 WeChatIDE
32551
- 5. \u4E0D\u5141\u8BB8\u5728 Case \u4E2D\u5199\u660E\u6587\u5BC6\u7801(\u7528 accountRef \u5F15\u7528)
32552
- 6. \u4F18\u5148\u590D\u7528\u5DF2\u6709 Case
32553
- 7. \u65B0\u589E Case \u5FC5\u987B\u901A\u8FC7 csspilot validate
32554
- 8. Case ID \u5FC5\u987B\u552F\u4E00
32555
- `;
32556
- }
32557
- function renderAdaptersMd(adapters) {
32558
- const lines = adapters.map(
32559
- (item) => `- ${item.id}: ${item.available ? "\u53EF\u7528" : "\u4E0D\u53EF\u7528"}${item.required ? "(\u9879\u76EE\u9700\u8981)" : "(\u53EF\u9009)"} \u2014 ${item.detail}`
32560
- );
32561
- return `# Adapters
32562
-
32563
- \u6267\u884C\u7AEF\u68C0\u6D4B\u7ED3\u679C(\u4EE5 \`csspilot doctor\` \u6700\u65B0\u8F93\u51FA\u4E3A\u51C6):
32564
-
32565
- ${lines.join("\n")}
32566
-
32567
- Agent \u6C38\u8FDC\u4E0D\u76F4\u63A5\u64CD\u4F5C Adapter;Case \u7ECF Execution Engine \u8C03\u5EA6\u5230\u5BF9\u5E94 Adapter \u6267\u884C\u3002
32568
- `;
32569
- }
32570
-
32571
32711
  // src/commands/init/test-structure.ts
32572
32712
  import { existsSync as existsSync8, readdirSync as readdirSync2 } from "node:fs";
32573
32713
  import { mkdir as mkdir13, writeFile as writeFile11 } from "node:fs/promises";
@@ -32625,6 +32765,12 @@ async function runInit(root, options = {}) {
32625
32765
  console.log(`\u2713 Runtime \u76EE\u5F55 ${join25(".testpilot", "artifacts")}`);
32626
32766
  const serverMode = config2.projectLink.serverUrl ? `Server:${config2.projectLink.serverUrl}` : "\u672C\u5730\u6A21\u5F0F";
32627
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
+ );
32628
32774
  const testStructure = await initTestStructure(root, project);
32629
32775
  const reuseNote = project.existingE2eDirs.filter((dir) => dir !== "tests/e2e");
32630
32776
  console.log(
@@ -32634,9 +32780,15 @@ async function runInit(root, options = {}) {
32634
32780
  console.log(` \u26A0 \u68C0\u6D4B\u5230\u5176\u4ED6 E2E \u76EE\u5F55:${reuseNote.join(", ")}(\u4FDD\u7559\u4E0D\u52A8,\u672A\u5F3A\u5236\u8FC1\u79FB)`);
32635
32781
  }
32636
32782
  const skill = await installSkill(root, project, adapters);
32637
- console.log(
32638
- skill.core === "created" ? `\u2713 Skill \u5B89\u88C5 ${join25(".agents", "skills", "testpilot")}` : "\u2713 Skill \u5B89\u88C5 .agents/skills/testpilot/(\u5DF2\u5B58\u5728,\u8DF3\u8FC7)"
32639
- );
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
+ }
32640
32792
  console.log(
32641
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)"
32642
32794
  );
@@ -32866,6 +33018,37 @@ function makeSyncMetadataCommand() {
32866
33018
  });
32867
33019
  }
32868
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
+
32869
33052
  // src/commands/validate.ts
32870
33053
  function makeValidateCommand() {
32871
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) => {
@@ -32902,10 +33085,10 @@ function makeValidateCommand() {
32902
33085
  }
32903
33086
 
32904
33087
  // src/cli.ts
32905
- var CLI_VERSION = "0.3.1";
33088
+ var CLI_VERSION = "0.4.0";
32906
33089
  async function createCli() {
32907
33090
  const program2 = new Command();
32908
- 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());
32909
33092
  return program2;
32910
33093
  }
32911
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