qa-flowkit 0.4.0-alpha.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.
Files changed (149) hide show
  1. package/.qa-ai/adapters/aider/.aider/README.md +25 -0
  2. package/.qa-ai/adapters/aider/.aider.conf.yml +6 -0
  3. package/.qa-ai/adapters/claude/agents/qa-workflow-orchestrator.md +18 -0
  4. package/.qa-ai/adapters/claude/commands/qa-add-tests.md +42 -0
  5. package/.qa-ai/adapters/claude/commands/qa-automation-plan.md +43 -0
  6. package/.qa-ai/adapters/claude/commands/qa-clean.md +42 -0
  7. package/.qa-ai/adapters/claude/commands/qa-config.md +51 -0
  8. package/.qa-ai/adapters/claude/commands/qa-coverage.md +46 -0
  9. package/.qa-ai/adapters/claude/commands/qa-doctor.md +11 -0
  10. package/.qa-ai/adapters/claude/commands/qa-full-flow.md +59 -0
  11. package/.qa-ai/adapters/claude/commands/qa-gate.md +36 -0
  12. package/.qa-ai/adapters/claude/commands/qa-help.md +30 -0
  13. package/.qa-ai/adapters/claude/commands/qa-init.md +70 -0
  14. package/.qa-ai/adapters/claude/commands/qa-status.md +56 -0
  15. package/.qa-ai/adapters/claude/commands/qa-update-tests.md +47 -0
  16. package/.qa-ai/adapters/claude/commands/qa-validate-features.md +36 -0
  17. package/.qa-ai/adapters/cline/.cline/README.md +25 -0
  18. package/.qa-ai/adapters/cline/.clinerules +9 -0
  19. package/.qa-ai/adapters/codex/README.md +44 -0
  20. package/.qa-ai/adapters/codex/prompts/implement-project.md +15 -0
  21. package/.qa-ai/adapters/continue/README.md +26 -0
  22. package/.qa-ai/adapters/continue/checks/qa-feature-conventions.md +15 -0
  23. package/.qa-ai/adapters/gemini/GEMINI.md +40 -0
  24. package/.qa-ai/adapters/generic/AGENTS.md +100 -0
  25. package/.qa-ai/adapters/goose/recipes/qa-flowkit.yaml +20 -0
  26. package/.qa-ai/adapters/opencode/README.md +57 -0
  27. package/.qa-ai/adapters/opencode/agents/qa-workflow.md +18 -0
  28. package/.qa-ai/adapters/opencode/commands/qa-add-tests.md +42 -0
  29. package/.qa-ai/adapters/opencode/commands/qa-automation-plan.md +43 -0
  30. package/.qa-ai/adapters/opencode/commands/qa-clean.md +42 -0
  31. package/.qa-ai/adapters/opencode/commands/qa-config.md +51 -0
  32. package/.qa-ai/adapters/opencode/commands/qa-coverage.md +46 -0
  33. package/.qa-ai/adapters/opencode/commands/qa-doctor.md +13 -0
  34. package/.qa-ai/adapters/opencode/commands/qa-full-flow.md +59 -0
  35. package/.qa-ai/adapters/opencode/commands/qa-gate.md +36 -0
  36. package/.qa-ai/adapters/opencode/commands/qa-help.md +30 -0
  37. package/.qa-ai/adapters/opencode/commands/qa-init.md +70 -0
  38. package/.qa-ai/adapters/opencode/commands/qa-status.md +56 -0
  39. package/.qa-ai/adapters/opencode/commands/qa-update-tests.md +47 -0
  40. package/.qa-ai/adapters/opencode/commands/qa-validate-features.md +36 -0
  41. package/.qa-ai/agents/README.md +39 -0
  42. package/.qa-ai/agents/api-testing-agent.md +73 -0
  43. package/.qa-ai/agents/automation-feasibility-agent.md +128 -0
  44. package/.qa-ai/agents/gherkin-test-design-agent.md +110 -0
  45. package/.qa-ai/agents/jira-task-agent.md +92 -0
  46. package/.qa-ai/agents/pr-agent.md +101 -0
  47. package/.qa-ai/agents/qa-context-intake-agent.md +75 -0
  48. package/.qa-ai/agents/qa-workflow-orchestrator.md +113 -0
  49. package/.qa-ai/agents/release-gate-agent.md +50 -0
  50. package/.qa-ai/agents/requirements-intake-agent.md +79 -0
  51. package/.qa-ai/agents/requirements-normalization-agent.md +80 -0
  52. package/.qa-ai/agents/specialists/available/appium.md +59 -0
  53. package/.qa-ai/agents/specialists/available/cypress.md +68 -0
  54. package/.qa-ai/agents/specialists/available/generic-test-design.md +117 -0
  55. package/.qa-ai/agents/specialists/available/jira.md +108 -0
  56. package/.qa-ai/agents/specialists/available/karate.md +97 -0
  57. package/.qa-ai/agents/specialists/available/playwright-api.md +87 -0
  58. package/.qa-ai/agents/specialists/available/playwright-ui.md +87 -0
  59. package/.qa-ai/agents/specialists/available/postman.md +108 -0
  60. package/.qa-ai/agents/specialists/available/rest-assured.md +103 -0
  61. package/.qa-ai/agents/specialists/available/selenium.md +91 -0
  62. package/.qa-ai/agents/specialists/available/testrail.md +85 -0
  63. package/.qa-ai/agents/specialists/available/webdriverio.md +81 -0
  64. package/.qa-ai/agents/test-design-system-agent.md +33 -0
  65. package/.qa-ai/agents/testrail-coverage-agent.md +84 -0
  66. package/.qa-ai/agents/testrail-sync-agent.md +96 -0
  67. package/.qa-ai/agents/webdriverio-implementation-agent.md +84 -0
  68. package/.qa-ai/presets/manual-only.yaml +65 -0
  69. package/.qa-ai/presets/selenium-jest-browserstack.yaml +72 -0
  70. package/.qa-ai/presets/webdriverio-playwright-api.yaml +85 -0
  71. package/.qa-ai/rules/api-testing.rules.md +7 -0
  72. package/.qa-ai/rules/approval.rules.md +8 -0
  73. package/.qa-ai/rules/automation.rules.md +7 -0
  74. package/.qa-ai/rules/gherkin.rules.md +12 -0
  75. package/.qa-ai/rules/testrail.rules.md +10 -0
  76. package/.qa-ai/rules/webdriverio.rules.md +9 -0
  77. package/.qa-ai/scripts/bootstrap-agent-adapters.mjs +127 -0
  78. package/.qa-ai/scripts/clean.mjs +243 -0
  79. package/.qa-ai/scripts/config.mjs +202 -0
  80. package/.qa-ai/scripts/doctor.mjs +383 -0
  81. package/.qa-ai/scripts/init.mjs +447 -0
  82. package/.qa-ai/scripts/lib/markdown-table.mjs +76 -0
  83. package/.qa-ai/scripts/lib/project-config.mjs +184 -0
  84. package/.qa-ai/scripts/lib/qa-next-steps.mjs +578 -0
  85. package/.qa-ai/scripts/lib/release-gate.mjs +66 -0
  86. package/.qa-ai/scripts/lib/test-design.mjs +92 -0
  87. package/.qa-ai/scripts/lib/test-management-mapping.mjs +73 -0
  88. package/.qa-ai/scripts/lib/utils.mjs +331 -0
  89. package/.qa-ai/scripts/qa-help.mjs +44 -0
  90. package/.qa-ai/scripts/smoke-npm-pack.mjs +187 -0
  91. package/.qa-ai/scripts/smoke-test.mjs +465 -0
  92. package/.qa-ai/scripts/sync-agent-adapters.mjs +121 -0
  93. package/.qa-ai/scripts/test-validators.mjs +334 -0
  94. package/.qa-ai/scripts/validate-active-specialists.mjs +106 -0
  95. package/.qa-ai/scripts/validate-features.mjs +277 -0
  96. package/.qa-ai/scripts/validate-release-gate.mjs +105 -0
  97. package/.qa-ai/scripts/validate-sync-plan.mjs +186 -0
  98. package/.qa-ai/scripts/validate-target.mjs +104 -0
  99. package/.qa-ai/scripts/validate-test-design.mjs +117 -0
  100. package/.qa-ai/scripts/validate-traceability.mjs +183 -0
  101. package/.qa-ai/templates/automation-feasibility-report.template.md +21 -0
  102. package/.qa-ai/templates/automation-implementation-plan.template.md +23 -0
  103. package/.qa-ai/templates/feature.template +13 -0
  104. package/.qa-ai/templates/jira-automation-task.template.md +25 -0
  105. package/.qa-ai/templates/pr-template.md +60 -0
  106. package/.qa-ai/templates/release-gate.template.yaml +16 -0
  107. package/.qa-ai/templates/requirement-analysis.template.md +17 -0
  108. package/.qa-ai/templates/test-design-proposal.template.md +26 -0
  109. package/.qa-ai/templates/test-design-system.template.md +15 -0
  110. package/.qa-ai/templates/test-management-mapping.template.json +18 -0
  111. package/.qa-ai/templates/testrail-coverage-analysis.template.md +17 -0
  112. package/.qa-ai/templates/testrail-sync-plan.template.md +22 -0
  113. package/.qa-ai/templates/traceability-matrix.template.md +4 -0
  114. package/.qa-ai/workflows/automation-analysis.md +23 -0
  115. package/.qa-ai/workflows/cleanup.md +52 -0
  116. package/.qa-ai/workflows/context-intake.md +66 -0
  117. package/.qa-ai/workflows/full-flow.md +55 -0
  118. package/.qa-ai/workflows/implementation.md +24 -0
  119. package/.qa-ai/workflows/intake.md +3 -0
  120. package/.qa-ai/workflows/pr.md +3 -0
  121. package/.qa-ai/workflows/release-gate.md +22 -0
  122. package/.qa-ai/workflows/test-design-system.md +33 -0
  123. package/.qa-ai/workflows/test-design.md +23 -0
  124. package/.qa-ai/workflows/testrail-sync.md +23 -0
  125. package/CHANGELOG.md +108 -0
  126. package/CODE_OF_CONDUCT.md +11 -0
  127. package/CONTRIBUTING.md +39 -0
  128. package/LICENSE +21 -0
  129. package/README.es.md +602 -0
  130. package/README.md +633 -0
  131. package/ROADMAP.md +107 -0
  132. package/SECURITY.md +18 -0
  133. package/bin/qa-flowkit.mjs +214 -0
  134. package/docs/qa-ai/agent-compatibility.md +100 -0
  135. package/docs/qa-ai/architecture.md +130 -0
  136. package/docs/qa-ai/backlog.md +393 -0
  137. package/docs/qa-ai/cleanup.md +104 -0
  138. package/docs/qa-ai/customizing-agents.md +148 -0
  139. package/docs/qa-ai/getting-started.md +385 -0
  140. package/docs/qa-ai/implementation-guide-for-codex.md +210 -0
  141. package/docs/qa-ai/npm-migration-plan.md +50 -0
  142. package/docs/qa-ai/open-source-release-checklist.md +17 -0
  143. package/docs/qa-ai/qa-help.md +76 -0
  144. package/docs/qa-ai/release-gate.md +60 -0
  145. package/docs/qa-ai/terminal-transcripts.md +316 -0
  146. package/docs/qa-ai/test-design-dual-mode.md +75 -0
  147. package/docs/qa-ai/troubleshooting.md +740 -0
  148. package/docs/qa-ai/workflow.md +147 -0
  149. package/package.json +72 -0
@@ -0,0 +1,65 @@
1
+ version: 1
2
+ project:
3
+ name: CHANGE_ME
4
+ repoMode: qa-design-only
5
+ qaTrack: quick
6
+ defaultLanguage: en
7
+ interfaceLanguage: en
8
+ tools:
9
+ testManagement: testrail
10
+ issueTracker: none
11
+ documentation: markdown
12
+ agents:
13
+ specialistMode: auto
14
+ sources:
15
+ main: markdown
16
+ knowledge:
17
+ enabled: false
18
+ sourcePath: ''
19
+ summaryPath: qa-ai-output/qa-knowledge-summary.md
20
+ decisionsPath: qa-ai-output/qa-init-decisions.md
21
+ requirements:
22
+ requireOfficialRfId: true
23
+ allowInferredAcceptanceCriteria: true
24
+ requireApprovalForInferredCriteria: true
25
+ gherkin:
26
+ language: en
27
+ oneScenarioPerFile: true
28
+ requireAcceptanceCriteria: true
29
+ manualTestsNeedFeatureFile: true
30
+ featurePath: features
31
+ tags:
32
+ required:
33
+ - priority
34
+ - type
35
+ - manual
36
+ testrail:
37
+ enabled: true
38
+ projectName: CHANGE_ME
39
+ allowCreateSections: true
40
+ allowCreateCases: true
41
+ allowUpdateCases: approval-only
42
+ allowDeleteCases: never
43
+ mappingFile: qa-ai-output/test-management-mapping.json
44
+ automation:
45
+ ui:
46
+ framework: none
47
+ api:
48
+ framework: none
49
+ strategy:
50
+ automateAllTechnicallyPossible: false
51
+ requireProposalBeforeImplementation: true
52
+ allowModifyExistingTests: approval-only
53
+ testDesign:
54
+ systemPath: qa-ai-output/test-design-system.md
55
+ proposalPath: qa-ai-output/test-design-proposal.md
56
+ traceability:
57
+ matrixPath: qa-ai-output/traceability-matrix.md
58
+ approval:
59
+ beforeExternalWrite: true
60
+ beforeExistingFileModification: true
61
+ beforeTestRailSync: true
62
+ beforeJiraTaskCreation: true
63
+ beforePullRequest: true
64
+ commands:
65
+ testQA: node .qa-ai/scripts/validate-features.mjs
@@ -0,0 +1,72 @@
1
+ version: 1
2
+ project:
3
+ name: CHANGE_ME
4
+ repoMode: qa-and-automation
5
+ qaTrack: standard
6
+ defaultLanguage: en
7
+ interfaceLanguage: en
8
+ tools:
9
+ testManagement: testrail
10
+ issueTracker: jira
11
+ documentation: markdown
12
+ agents:
13
+ specialistMode: auto
14
+ sources:
15
+ main: jira
16
+ knowledge:
17
+ enabled: false
18
+ sourcePath: ''
19
+ summaryPath: qa-ai-output/qa-knowledge-summary.md
20
+ decisionsPath: qa-ai-output/qa-init-decisions.md
21
+ requirements:
22
+ requireOfficialRfId: true
23
+ allowInferredAcceptanceCriteria: true
24
+ requireApprovalForInferredCriteria: true
25
+ gherkin:
26
+ language: en
27
+ oneScenarioPerFile: true
28
+ requireAcceptanceCriteria: true
29
+ manualTestsNeedFeatureFile: true
30
+ featurePath: features
31
+ tags:
32
+ required:
33
+ - priority
34
+ - type
35
+ - manual
36
+ testrail:
37
+ enabled: true
38
+ projectName: CHANGE_ME
39
+ allowCreateSections: true
40
+ allowCreateCases: true
41
+ allowUpdateCases: approval-only
42
+ allowDeleteCases: never
43
+ mappingFile: qa-ai-output/test-management-mapping.json
44
+ automation:
45
+ ui:
46
+ framework: selenium-jest-browserstack
47
+ specsPath: tests/selenium/specs
48
+ pageObjectsPath: tests/selenium/pageobjects
49
+ api:
50
+ framework: undecided
51
+ specsPath: tests/api/specs
52
+ strategy:
53
+ automateAllTechnicallyPossible: true
54
+ requireProposalBeforeImplementation: true
55
+ allowModifyExistingTests: approval-only
56
+ testDesign:
57
+ systemPath: qa-ai-output/test-design-system.md
58
+ proposalPath: qa-ai-output/test-design-proposal.md
59
+ traceability:
60
+ matrixPath: qa-ai-output/traceability-matrix.md
61
+ release:
62
+ gatePath: qa-ai-output/release-gate.yaml
63
+ approval:
64
+ beforeExternalWrite: true
65
+ beforeExistingFileModification: true
66
+ beforeTestRailSync: true
67
+ beforeJiraTaskCreation: true
68
+ beforePullRequest: true
69
+ commands:
70
+ lint: npm run lint
71
+ testE2E: npm run test:e2e
72
+ testQA: npm run test:qa
@@ -0,0 +1,85 @@
1
+ version: 1
2
+ project:
3
+ name: CHANGE_ME
4
+ repoMode: qa-and-automation
5
+ qaTrack: standard
6
+ defaultLanguage: en
7
+ interfaceLanguage: en
8
+ tools:
9
+ testManagement: testrail
10
+ issueTracker: jira
11
+ documentation: confluence
12
+ agents:
13
+ specialistMode: auto
14
+ sources:
15
+ main: jira
16
+ jira:
17
+ enabled: true
18
+ projectKey: CHANGE_ME
19
+ storyIsSourceOfTruth: true
20
+ readAttachments: true
21
+ confluence:
22
+ enabled: true
23
+ markdown:
24
+ enabled: true
25
+ paths:
26
+ - docs/PRD
27
+ - docs/RF
28
+ knowledge:
29
+ enabled: false
30
+ sourcePath: ''
31
+ summaryPath: qa-ai-output/qa-knowledge-summary.md
32
+ decisionsPath: qa-ai-output/qa-init-decisions.md
33
+ requirements:
34
+ requireOfficialRfId: true
35
+ allowInferredAcceptanceCriteria: true
36
+ requireApprovalForInferredCriteria: true
37
+ gherkin:
38
+ language: en
39
+ oneScenarioPerFile: true
40
+ requireAcceptanceCriteria: true
41
+ manualTestsNeedFeatureFile: true
42
+ featurePath: features
43
+ tags:
44
+ required:
45
+ - priority
46
+ - type
47
+ - manual
48
+ testrail:
49
+ enabled: true
50
+ projectName: CHANGE_ME
51
+ allowCreateSections: true
52
+ allowCreateCases: true
53
+ allowUpdateCases: approval-only
54
+ allowDeleteCases: never
55
+ mappingFile: qa-ai-output/test-management-mapping.json
56
+ automation:
57
+ ui:
58
+ framework: webdriverio
59
+ specsPath: tests/wdio/specs
60
+ pageObjectsPath: tests/wdio/pageobjects
61
+ api:
62
+ framework: playwright-api
63
+ specsPath: tests/api/specs
64
+ strategy:
65
+ automateAllTechnicallyPossible: true
66
+ requireProposalBeforeImplementation: true
67
+ allowModifyExistingTests: approval-only
68
+ traceability:
69
+ matrixPath: qa-ai-output/traceability-matrix.md
70
+ testDesign:
71
+ systemPath: qa-ai-output/test-design-system.md
72
+ proposalPath: qa-ai-output/test-design-proposal.md
73
+ release:
74
+ gatePath: qa-ai-output/release-gate.yaml
75
+ approval:
76
+ beforeExternalWrite: true
77
+ beforeExistingFileModification: true
78
+ beforeTestRailSync: true
79
+ beforeJiraTaskCreation: true
80
+ beforePullRequest: true
81
+ commands:
82
+ lint: npm run lint
83
+ testApi: npm run test:api
84
+ testE2E: npm run test:e2e
85
+ testQA: npm run test:qa
@@ -0,0 +1,7 @@
1
+ # API Testing Rules
2
+
3
+ - Use the API/integration framework configured in `qa-ai.config.yaml` (`automation.api.framework`).
4
+ - Check existing API testing patterns before creating new ones.
5
+ - Use clients, fixtures and schemas for maintainability.
6
+ - Do not add dependencies without approval.
7
+ - If API framework is `none`, `undecided` or missing, create a proposal and keep tests as manual/blocked until decided.
@@ -0,0 +1,8 @@
1
+ # Approval Rules
2
+
3
+ - Always present a detailed plan before modifying files.
4
+ - Ask approval before external writes to configured tools.
5
+ - Ask approval before modifying existing tests.
6
+ - Never delete external test cases by default.
7
+ - Never overwrite existing files unless explicitly approved.
8
+ - Ask the user when ambiguity exists and offer options.
@@ -0,0 +1,7 @@
1
+ # Automation Rules
2
+
3
+ - Automate every test that is technically possible and valuable under current repo constraints.
4
+ - Generate a technical proposal before writing code.
5
+ - Do not modify existing tests without approval.
6
+ - If tests cannot be executed, mark first execution as manual.
7
+ - Create configured issue tracker task drafts for automatable tests that cannot be implemented now.
@@ -0,0 +1,12 @@
1
+ # Gherkin Rules
2
+
3
+ - Use the configured Gherkin language from `qa-ai.config.yaml` (`gherkin.language`): English (`en`) or Spanish (`es`).
4
+ - Use English Gherkin keywords and `Acceptance Criteria:` for `en`.
5
+ - Use Spanish Gherkin keywords and `Criterios de aceptación:` for `es`.
6
+ - Spanish `.feature` files must include `# language: es`.
7
+ - One `.feature` file per test case.
8
+ - One configured scenario keyword per `.feature` file: `Scenario:` / `Scenario Outline:` for English or `Escenario:` / `Esquema del escenario:` for Spanish.
9
+ - Include the configured acceptance criteria section after the Feature narrative.
10
+ - Required tags: `@priority:`, `@type:`, `@manual:`.
11
+ - Manual tests also require `.feature` files.
12
+ - Unit tests are excluded.
@@ -0,0 +1,10 @@
1
+ # Test Management Rules
2
+
3
+ - Use the configured test management tool from `qa-ai.config.yaml` (`tools.testManagement`).
4
+ - Ask the user for the target project/suite when needed.
5
+ - Search existing cases before proposing new cases.
6
+ - Detect duplicates and potential overlaps.
7
+ - Create new sections only after informing the user.
8
+ - Create new cases only after approval.
9
+ - Update existing cases only after explicit approval.
10
+ - Delete or archive cases only after explicit approval and only if allowed by configuration.
@@ -0,0 +1,9 @@
1
+ # UI Automation Rules
2
+
3
+ - Follow existing repository patterns.
4
+ - Reuse page objects, helpers and fixtures when possible.
5
+ - Create new page objects only when needed.
6
+ - Use the UI/E2E framework configured in `qa-ai.config.yaml` (`automation.ui.framework`).
7
+ - Do not change global framework config without approval.
8
+ - Prefer stable selectors and accessibility attributes.
9
+ - Keep tests isolated and deterministic.
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env node
2
+ import path from 'node:path';
3
+ import {
4
+ commaList,
5
+ copyFileSafe,
6
+ ensureDir,
7
+ manifestEntry,
8
+ manifestPath,
9
+ parseArgs,
10
+ pathExists,
11
+ recordManifestEntries,
12
+ relativeTo,
13
+ logHeader
14
+ } from './lib/utils.mjs';
15
+
16
+ const cwd = process.cwd();
17
+ const args = parseArgs(process.argv);
18
+ const force = Boolean(args.force);
19
+ const qaAiDir = path.join(cwd, '.qa-ai');
20
+
21
+ const bootstrapMap = {
22
+ claude: {
23
+ source: '.qa-ai/adapters/claude/commands/qa-init.md',
24
+ target: '.claude/commands/qa-init.md',
25
+ command: '/qa-init'
26
+ },
27
+ opencode: {
28
+ source: '.qa-ai/adapters/opencode/commands/qa-init.md',
29
+ target: '.opencode/commands/qa-init.md',
30
+ command: '/qa-init'
31
+ }
32
+ };
33
+
34
+ function printHelp() {
35
+ console.log(`Usage: node .qa-ai/scripts/bootstrap-agent-adapters.mjs [options]
36
+
37
+ Copies minimal root slash commands so Claude Code and OpenCode can run /qa-init
38
+ after only the .qa-ai folder has been copied.
39
+
40
+ Options:
41
+ --agents <list> Comma-separated agents to bootstrap (default: claude,opencode)
42
+ --agent <name> Repeatable single agent name
43
+ --force Overwrite existing bootstrap command files
44
+ --help Show this help
45
+
46
+ Supported agents: ${Object.keys(bootstrapMap).join(', ')}
47
+ `);
48
+ }
49
+
50
+ function selectedAgents() {
51
+ const requested = [...commaList(args.agents), ...commaList(args.agent)].map((name) => name.toLowerCase());
52
+ if (requested.length === 0 || requested.includes('all')) return Object.keys(bootstrapMap);
53
+ if (requested.includes('none')) return [];
54
+ return [...new Set(requested)];
55
+ }
56
+
57
+ async function main() {
58
+ if (args.help) {
59
+ printHelp();
60
+ return;
61
+ }
62
+
63
+ logHeader('QA FlowKit agent bootstrap');
64
+
65
+ if (!await pathExists(qaAiDir)) {
66
+ console.error('Missing .qa-ai folder. Copy it into the repository root first.');
67
+ process.exit(1);
68
+ }
69
+
70
+ const agents = selectedAgents();
71
+ const unknown = agents.filter((name) => !(name in bootstrapMap));
72
+ if (unknown.length > 0) {
73
+ console.error(`Unknown agent(s): ${unknown.join(', ')}`);
74
+ console.error(`Supported agents: ${Object.keys(bootstrapMap).join(', ')}`);
75
+ process.exit(1);
76
+ }
77
+
78
+ if (agents.length === 0) {
79
+ console.log('No agents selected.');
80
+ return;
81
+ }
82
+
83
+ const manifestEntries = [];
84
+
85
+ for (const name of agents) {
86
+ const bootstrap = bootstrapMap[name];
87
+ const source = path.join(cwd, bootstrap.source);
88
+ const target = path.join(cwd, bootstrap.target);
89
+
90
+ if (!await pathExists(source)) {
91
+ console.log(`[FAIL] ${name}: missing ${bootstrap.source}`);
92
+ continue;
93
+ }
94
+
95
+ const dirResult = await ensureDir(path.dirname(target));
96
+ if (dirResult.created) {
97
+ manifestEntries.push(await manifestEntry(cwd, dirResult.path, {
98
+ type: 'dir',
99
+ category: 'bootstrap',
100
+ source: `bootstrap:${name}`
101
+ }));
102
+ }
103
+
104
+ const result = await copyFileSafe(source, target, { force });
105
+ console.log(`${name}: ${result.copied ? 'copied ' : 'skipped'} ${relativeTo(cwd, result.path)}`);
106
+ if (result.copied) {
107
+ manifestEntries.push(await manifestEntry(cwd, result.path, {
108
+ type: 'file',
109
+ category: 'bootstrap',
110
+ source: `bootstrap:${name}`
111
+ }));
112
+ }
113
+ }
114
+
115
+ const manifest = await recordManifestEntries(cwd, manifestEntries);
116
+ if (manifest) console.log(`updated ${relativeTo(cwd, manifestPath(cwd))}`);
117
+
118
+ console.log('\nNext: open Claude Code or OpenCode in this repository and run:');
119
+ console.log('/qa-init');
120
+ console.log('\nAdvanced example for a manual-only QA setup:');
121
+ console.log('/qa-init --preset manual-only --adapters claude,opencode');
122
+ }
123
+
124
+ main().catch((error) => {
125
+ console.error(error);
126
+ process.exit(1);
127
+ });
@@ -0,0 +1,243 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import {
5
+ hashFile,
6
+ loadInitManifest,
7
+ manifestRelativePath,
8
+ parseArgs,
9
+ pathExists,
10
+ resolveInsideCwd,
11
+ saveInitManifest,
12
+ logHeader
13
+ } from './lib/utils.mjs';
14
+
15
+ const cwd = process.cwd();
16
+ const args = parseArgs(process.argv);
17
+ const force = Boolean(args.force);
18
+ const includeModified = Boolean(args['include-modified']);
19
+ const pruneState = Boolean(args['prune-state']);
20
+
21
+ function printHelp() {
22
+ console.log(`Usage: node .qa-ai/scripts/clean.mjs [options]
23
+
24
+ Default behavior is a dry-run. Nothing is deleted unless --force is passed.
25
+
26
+ Options:
27
+ --generated Include generated non-adapter files and directories from init
28
+ --adapters Include generated adapter files and adapter directories
29
+ --empty-dirs Include tracked empty directories
30
+ --all Include all tracked entries (default when no category is passed)
31
+ --force Execute the cleanup plan
32
+ --include-modified Also delete tracked files whose content changed since init
33
+ --prune-state Remove .qa-ai/state/init-manifest.json when no entries remain
34
+ --help Show this help
35
+
36
+ Examples:
37
+ node .qa-ai/scripts/clean.mjs
38
+ node .qa-ai/scripts/clean.mjs --generated --force
39
+ node .qa-ai/scripts/clean.mjs --adapters --empty-dirs --force
40
+ `);
41
+ }
42
+
43
+ function entryKey(entry) {
44
+ return `${entry.type}:${entry.path}`;
45
+ }
46
+
47
+ function selectedCategories() {
48
+ const explicit = Boolean(args.generated || args.adapters || args['empty-dirs'] || args.all);
49
+ return {
50
+ all: Boolean(args.all || !explicit),
51
+ generated: Boolean(args.generated),
52
+ adapters: Boolean(args.adapters),
53
+ emptyDirs: Boolean(args['empty-dirs'])
54
+ };
55
+ }
56
+
57
+ function shouldSelect(entry, categories) {
58
+ if (categories.all) return true;
59
+ if (entry.type === 'dir') {
60
+ if (categories.emptyDirs) return true;
61
+ if (categories.generated && entry.category === 'generated') return true;
62
+ if (categories.adapters && entry.category === 'adapter') return true;
63
+ return false;
64
+ }
65
+ if (categories.generated && entry.category === 'generated') return true;
66
+ if (categories.adapters && entry.category === 'adapter') return true;
67
+ return false;
68
+ }
69
+
70
+ function safeTarget(entry) {
71
+ if (!entry.path || path.isAbsolute(entry.path) || entry.path === '.') {
72
+ return { ok: false, reason: 'unsafe manifest path' };
73
+ }
74
+ if (entry.path === manifestRelativePath) {
75
+ return { ok: false, reason: 'manifest state is handled separately' };
76
+ }
77
+
78
+ const target = resolveInsideCwd(cwd, entry.path);
79
+ if (!target.inside || target.resolved === path.resolve(cwd)) {
80
+ return { ok: false, reason: 'path resolves outside repository root' };
81
+ }
82
+ return { ok: true, path: target.resolved };
83
+ }
84
+
85
+ async function planFile(entry) {
86
+ const target = safeTarget(entry);
87
+ if (!target.ok) return { entry, action: 'skip', reason: target.reason };
88
+ if (!await pathExists(target.path)) return { entry, action: 'missing', removeFromManifest: true };
89
+
90
+ const stat = await fs.lstat(target.path);
91
+ if (!stat.isFile()) return { entry, action: 'skip', reason: 'not a regular file' };
92
+
93
+ if (entry.sha256) {
94
+ const currentHash = await hashFile(target.path);
95
+ if (currentHash !== entry.sha256 && !includeModified) {
96
+ return { entry, action: 'skip', reason: 'modified since init' };
97
+ }
98
+ }
99
+
100
+ return {
101
+ entry,
102
+ action: force ? 'delete-file' : 'would-delete-file',
103
+ target: target.path,
104
+ removeFromManifest: force
105
+ };
106
+ }
107
+
108
+ async function planDirectory(entry, removalPaths) {
109
+ const target = safeTarget(entry);
110
+ if (!target.ok) return { entry, action: 'skip', reason: target.reason };
111
+ if (!await pathExists(target.path)) return { entry, action: 'missing', removeFromManifest: true };
112
+
113
+ const stat = await fs.lstat(target.path);
114
+ if (!stat.isDirectory()) return { entry, action: 'skip', reason: 'not a directory' };
115
+
116
+ const children = await fs.readdir(target.path);
117
+ const removableAfterTrackedChildren = children.every((child) => {
118
+ const childPath = `${entry.path.replace(/\/$/, '')}/${child}`;
119
+ return removalPaths.has(childPath);
120
+ });
121
+ if (children.length > 0 && !removableAfterTrackedChildren) {
122
+ return { entry, action: 'skip', reason: 'directory is not empty' };
123
+ }
124
+
125
+ return {
126
+ entry,
127
+ action: force ? 'remove-dir' : 'would-remove-dir',
128
+ target: target.path,
129
+ removeFromManifest: force
130
+ };
131
+ }
132
+
133
+ async function planEntries(entries) {
134
+ const files = entries.filter((entry) => entry.type === 'file');
135
+ const dirs = entries
136
+ .filter((entry) => entry.type === 'dir')
137
+ .sort((a, b) => b.path.length - a.path.length);
138
+ const planned = [];
139
+ const removalPaths = new Set();
140
+
141
+ for (const entry of files) {
142
+ const item = await planFile(entry);
143
+ planned.push(item);
144
+ if (item.removeFromManifest || item.action === 'would-delete-file') removalPaths.add(entry.path);
145
+ }
146
+ for (const entry of dirs) {
147
+ const item = await planDirectory(entry, removalPaths);
148
+ planned.push(item);
149
+ if (item.removeFromManifest || item.action === 'would-remove-dir') removalPaths.add(entry.path);
150
+ }
151
+
152
+ return planned;
153
+ }
154
+
155
+ async function executePlan(plan) {
156
+ for (const item of plan) {
157
+ if (item.action === 'delete-file') await fs.rm(item.target, { force: false });
158
+ if (item.action === 'remove-dir') await fs.rmdir(item.target);
159
+ }
160
+ }
161
+
162
+ function printPlan(plan) {
163
+ for (const item of plan) {
164
+ const label = item.action.toUpperCase().replaceAll('-', ' ');
165
+ const suffix = item.reason ? ` (${item.reason})` : '';
166
+ console.log(`[${label}] ${item.entry.path}${suffix}`);
167
+ }
168
+ }
169
+
170
+ function summarize(plan) {
171
+ const counts = new Map();
172
+ for (const item of plan) counts.set(item.action, (counts.get(item.action) || 0) + 1);
173
+ return [...counts.entries()]
174
+ .sort(([a], [b]) => a.localeCompare(b))
175
+ .map(([action, count]) => `${action}: ${count}`)
176
+ .join(', ');
177
+ }
178
+
179
+ async function pruneManifestState(manifestPath) {
180
+ if (!pruneState) return;
181
+ await fs.rm(manifestPath, { force: true });
182
+ try {
183
+ await fs.rmdir(path.dirname(manifestPath));
184
+ } catch {
185
+ // Keep the state directory when other files are present.
186
+ }
187
+ }
188
+
189
+ async function main() {
190
+ if (args.help) {
191
+ printHelp();
192
+ return;
193
+ }
194
+
195
+ logHeader('QA FlowKit clean');
196
+ const { exists, path: manifestPath, data: manifest } = await loadInitManifest(cwd);
197
+ if (!exists) {
198
+ console.log(`No init manifest found at ${manifestRelativePath}. Run init before clean can safely remove generated files.`);
199
+ return;
200
+ }
201
+
202
+ const categories = selectedCategories();
203
+ const selected = manifest.entries.filter((entry) => shouldSelect(entry, categories));
204
+ if (selected.length === 0) {
205
+ console.log('No matching manifest entries for the selected cleanup options.');
206
+ return;
207
+ }
208
+
209
+ console.log(force ? 'Mode: execute cleanup' : 'Mode: dry-run only');
210
+ if (includeModified) console.log('Modified tracked files are included because --include-modified was passed.');
211
+ console.log('');
212
+
213
+ const plan = await planEntries(selected);
214
+ printPlan(plan);
215
+ console.log(`\nSummary: ${summarize(plan) || 'nothing to do'}`);
216
+
217
+ if (!force) {
218
+ console.log('\nNo files were deleted. Re-run with --force to execute this plan.');
219
+ return;
220
+ }
221
+
222
+ await executePlan(plan);
223
+
224
+ const removeKeys = new Set(plan.filter((item) => item.removeFromManifest).map((item) => entryKey(item.entry)));
225
+ const remainingEntries = manifest.entries.filter((entry) => !removeKeys.has(entryKey(entry)));
226
+
227
+ if (remainingEntries.length === 0 && pruneState) {
228
+ await pruneManifestState(manifestPath);
229
+ console.log(`\nRemoved ${manifestRelativePath}.`);
230
+ return;
231
+ }
232
+
233
+ await saveInitManifest(cwd, {
234
+ ...manifest,
235
+ entries: remainingEntries
236
+ });
237
+ console.log(`\nUpdated ${manifestRelativePath}.`);
238
+ }
239
+
240
+ main().catch((error) => {
241
+ console.error(error);
242
+ process.exit(1);
243
+ });