qa-engineer 0.9.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/COMPATIBILITY.md +71 -0
- package/LICENSE +21 -0
- package/README.md +508 -0
- package/package.json +90 -0
- package/packages/installer/README.md +46 -0
- package/packages/installer/bin/qa.mjs +136 -0
- package/packages/installer/lib/agents/registry.mjs +209 -0
- package/packages/installer/lib/cli/commands.mjs +29 -0
- package/packages/installer/lib/cli/flags.mjs +64 -0
- package/packages/installer/lib/commands/doctor.mjs +190 -0
- package/packages/installer/lib/commands/install.mjs +291 -0
- package/packages/installer/lib/commands/onboard.mjs +163 -0
- package/packages/installer/lib/commands/repair.mjs +68 -0
- package/packages/installer/lib/commands/self-test.mjs +45 -0
- package/packages/installer/lib/commands/uninstall.mjs +167 -0
- package/packages/installer/lib/commands/update.mjs +69 -0
- package/packages/installer/lib/commands/verify.mjs +62 -0
- package/packages/installer/lib/constants.mjs +34 -0
- package/packages/installer/lib/core/bundle.mjs +124 -0
- package/packages/installer/lib/core/config.mjs +73 -0
- package/packages/installer/lib/core/conflict.mjs +29 -0
- package/packages/installer/lib/core/errors.mjs +29 -0
- package/packages/installer/lib/core/fs-safe.mjs +236 -0
- package/packages/installer/lib/core/hash.mjs +19 -0
- package/packages/installer/lib/core/lockfile.mjs +75 -0
- package/packages/installer/lib/core/logger.mjs +46 -0
- package/packages/installer/lib/core/manifest.mjs +89 -0
- package/packages/installer/lib/core/paths.mjs +74 -0
- package/packages/installer/lib/core/schema-validate.mjs +142 -0
- package/packages/installer/lib/core/skill-meta.mjs +75 -0
- package/packages/installer/lib/core/validate-install.mjs +152 -0
- package/packages/installer/lib/core/wrappers.mjs +91 -0
- package/packages/installer/lib/detect/environment.mjs +54 -0
- package/packages/installer/lib/detect/frameworks.mjs +94 -0
- package/packages/installer/lib/detect/project.mjs +126 -0
- package/packages/installer/lib/detect/recommend.mjs +85 -0
- package/packages/installer/lib/detect/scan.mjs +48 -0
- package/packages/installer/lib/ui/progress.mjs +32 -0
- package/packages/installer/lib/ui/theme.mjs +81 -0
- package/packages/installer/lib/version.mjs +47 -0
- package/packages/installer/package.json +28 -0
- package/packages/installer/schemas/qa-lock.schema.json +89 -0
- package/packages/installer/schemas/qa.config.schema.json +91 -0
- package/shared/analysis/lib/qa_analysis/__init__.py +11 -0
- package/shared/analysis/lib/qa_analysis/branding.json +9 -0
- package/shared/analysis/lib/qa_analysis/branding.py +175 -0
- package/shared/analysis/lib/qa_analysis/cli.py +129 -0
- package/shared/analysis/lib/qa_analysis/context.py +233 -0
- package/shared/analysis/lib/qa_analysis/contracts.py +158 -0
- package/shared/analysis/lib/qa_analysis/diff_guard.py +327 -0
- package/shared/analysis/lib/qa_analysis/discovery.py +113 -0
- package/shared/analysis/lib/qa_analysis/evidence.py +128 -0
- package/shared/analysis/lib/qa_analysis/har.py +58 -0
- package/shared/analysis/lib/qa_analysis/junit.py +80 -0
- package/shared/analysis/lib/qa_analysis/redaction.py +99 -0
- package/shared/analysis/lib/qa_analysis/taxonomy.py +98 -0
- package/shared/analysis/schemas/context.schema.json +82 -0
- package/shared/diagnostics/lib/qa_diagnostics/__init__.py +13 -0
- package/shared/diagnostics/lib/qa_diagnostics/cli.py +124 -0
- package/shared/diagnostics/lib/qa_diagnostics/engine.py +143 -0
- package/shared/diagnostics/lib/qa_diagnostics/internal_contracts.py +75 -0
- package/shared/diagnostics/lib/qa_diagnostics/prioritization.py +101 -0
- package/shared/diagnostics/lib/qa_diagnostics/repair.py +72 -0
- package/shared/diagnostics/lib/qa_diagnostics/root_cause.py +89 -0
- package/shared/diagnostics/lib/qa_diagnostics/timeline.py +71 -0
- package/shared/diagnostics/schemas/internal/analysis-result.schema.json +28 -0
- package/shared/diagnostics/schemas/internal/diagnosis.schema.json +55 -0
- package/shared/diagnostics/schemas/internal/execution-result-min.schema.json +34 -0
- package/shared/frameworks/cypress/lib/cypress_analysis.py +27 -0
- package/shared/frameworks/playwright/lib/playwright_analysis.py +167 -0
- package/shared/frameworks/registry.json +150 -0
- package/shared/frameworks/registry.mjs +37 -0
- package/shared/frameworks/registry.schema.json +74 -0
- package/shared/frameworks/selenium/lib/selenium_analysis.py +28 -0
- package/shared/frameworks/webdriverio/lib/webdriverio_analysis.py +24 -0
- package/shared/tooling/qa_tool.py +127 -0
- package/skills/README.md +31 -0
- package/skills/qa/README.md +19 -0
- package/skills/qa/SKILL.md +52 -0
- package/skills/qa/examples/routing.md +86 -0
- package/skills/qa/references/routing-map.md +33 -0
- package/skills/qa-api/README.md +19 -0
- package/skills/qa-api/SKILL.md +68 -0
- package/skills/qa-api/contracts/api-result.schema.json +51 -0
- package/skills/qa-api/examples/graphql-review.md +43 -0
- package/skills/qa-api/references/authentication.md +45 -0
- package/skills/qa-api/references/deterministic-tooling.md +128 -0
- package/skills/qa-api/references/evidence-and-reporting.md +66 -0
- package/skills/qa-api/references/graphql.md +44 -0
- package/skills/qa-api/references/rest.md +45 -0
- package/skills/qa-api/references/websocket.md +44 -0
- package/skills/qa-audit/README.md +19 -0
- package/skills/qa-audit/SKILL.md +70 -0
- package/skills/qa-audit/contracts/audit-result.schema.json +63 -0
- package/skills/qa-audit/examples/accessibility-audit.md +46 -0
- package/skills/qa-audit/references/accessibility.md +44 -0
- package/skills/qa-audit/references/deterministic-tooling.md +128 -0
- package/skills/qa-audit/references/evidence-and-reporting.md +66 -0
- package/skills/qa-audit/references/performance.md +44 -0
- package/skills/qa-audit/references/security.md +43 -0
- package/skills/qa-audit/references/visual-testing.md +44 -0
- package/skills/qa-debug/README.md +19 -0
- package/skills/qa-debug/SKILL.md +76 -0
- package/skills/qa-debug/contracts/debug-result.schema.json +121 -0
- package/skills/qa-debug/examples/failed-login.md +61 -0
- package/skills/qa-debug/examples/locator-break.md +59 -0
- package/skills/qa-debug/examples/network-timeout.md +61 -0
- package/skills/qa-debug/examples/successful-debug.md +62 -0
- package/skills/qa-debug/references/confidence-model.md +26 -0
- package/skills/qa-debug/references/deterministic-tooling.md +128 -0
- package/skills/qa-debug/references/diagnostic-engine.md +36 -0
- package/skills/qa-debug/references/evidence-and-reporting.md +66 -0
- package/skills/qa-debug/references/evidence-model.md +41 -0
- package/skills/qa-debug/references/failure-taxonomy.md +44 -0
- package/skills/qa-debug/references/finding-prioritization.md +43 -0
- package/skills/qa-debug/references/investigation-workflow.md +48 -0
- package/skills/qa-debug/references/recommendation-ranking.md +34 -0
- package/skills/qa-debug/references/root-cause-analysis.md +49 -0
- package/skills/qa-debug/references/timeline-builder.md +37 -0
- package/skills/qa-example/README.md +19 -0
- package/skills/qa-example/SKILL.md +60 -0
- package/skills/qa-example/contracts/self-check-report.schema.json +65 -0
- package/skills/qa-example/examples/self-check.md +53 -0
- package/skills/qa-example/references/example-domain.md +28 -0
- package/skills/qa-example/references/skill-format-notes.md +26 -0
- package/skills/qa-explore/README.md +22 -0
- package/skills/qa-explore/SKILL.md +87 -0
- package/skills/qa-explore/contracts/explore-result.schema.json +249 -0
- package/skills/qa-explore/examples/attached-test-cases.md +70 -0
- package/skills/qa-explore/examples/screenshot-proof-finding.md +50 -0
- package/skills/qa-explore/examples/url-smoke-explore.md +80 -0
- package/skills/qa-explore/references/accessibility.md +44 -0
- package/skills/qa-explore/references/api-replay.md +48 -0
- package/skills/qa-explore/references/browser-adapters.md +51 -0
- package/skills/qa-explore/references/evidence-and-reporting.md +66 -0
- package/skills/qa-explore/references/evidence-capture.md +54 -0
- package/skills/qa-explore/references/exploratory-qa.md +52 -0
- package/skills/qa-explore/references/finding-taxonomy.md +47 -0
- package/skills/qa-explore/references/performance.md +44 -0
- package/skills/qa-explore/references/pipeline.md +74 -0
- package/skills/qa-explore/references/report-pipeline.md +69 -0
- package/skills/qa-explore/references/security.md +43 -0
- package/skills/qa-explore/references/test-case-intake.md +44 -0
- package/skills/qa-fix/README.md +19 -0
- package/skills/qa-fix/SKILL.md +70 -0
- package/skills/qa-fix/contracts/fix-result.schema.json +132 -0
- package/skills/qa-fix/examples/repair-plan.md +56 -0
- package/skills/qa-fix/references/deterministic-tooling.md +128 -0
- package/skills/qa-fix/references/diagnostic-engine.md +36 -0
- package/skills/qa-fix/references/evidence-and-reporting.md +66 -0
- package/skills/qa-fix/references/repair-strategy.md +48 -0
- package/skills/qa-fix/references/root-cause-analysis.md +49 -0
- package/skills/qa-fix/references/suite-extension.md +73 -0
- package/skills/qa-flaky/README.md +19 -0
- package/skills/qa-flaky/SKILL.md +67 -0
- package/skills/qa-flaky/contracts/flaky-result.schema.json +62 -0
- package/skills/qa-flaky/examples/flaky-locator.md +46 -0
- package/skills/qa-flaky/references/deterministic-tooling.md +128 -0
- package/skills/qa-flaky/references/evidence-and-reporting.md +66 -0
- package/skills/qa-flaky/references/flakiness.md +48 -0
- package/skills/qa-flaky/references/retry.md +44 -0
- package/skills/qa-flaky/references/waiting-strategies.md +44 -0
- package/skills/qa-generate/README.md +42 -0
- package/skills/qa-generate/SKILL.md +83 -0
- package/skills/qa-generate/contracts/generation-result.schema.json +124 -0
- package/skills/qa-generate/examples/bootstrap-new-framework.md +24 -0
- package/skills/qa-generate/examples/extend-existing-suite.md +26 -0
- package/skills/qa-generate/references/code-style.md +29 -0
- package/skills/qa-generate/references/evidence-and-reporting.md +66 -0
- package/skills/qa-generate/references/framework-selection.md +88 -0
- package/skills/qa-generate/references/generation-strategy.md +48 -0
- package/skills/qa-generate/references/naming-conventions.md +27 -0
- package/skills/qa-generate/references/playwright-conventions.md +23 -0
- package/skills/qa-generate/references/playwright-generation.md +41 -0
- package/skills/qa-generate/references/playwright-project-discovery.md +42 -0
- package/skills/qa-generate/references/project-bootstrap.md +54 -0
- package/skills/qa-generate/references/repository-analysis.md +43 -0
- package/skills/qa-generate/references/suite-extension.md +73 -0
- package/skills/qa-generate/references/template-selection.md +48 -0
- package/skills/qa-generate/templates/playwright/api.ts +36 -0
- package/skills/qa-generate/templates/playwright/base-page.ts +14 -0
- package/skills/qa-generate/templates/playwright/data.ts +25 -0
- package/skills/qa-generate/templates/playwright/env.example +16 -0
- package/skills/qa-generate/templates/playwright/example.spec.ts +14 -0
- package/skills/qa-generate/templates/playwright/fixtures.ts +28 -0
- package/skills/qa-generate/templates/playwright/framework-readme.md +39 -0
- package/skills/qa-generate/templates/playwright/login.page.ts +26 -0
- package/skills/qa-generate/templates/playwright/playwright.config.ts +28 -0
- package/skills/qa-generate/templates/playwright/utils.ts +18 -0
- package/skills/qa-init/README.md +20 -0
- package/skills/qa-init/SKILL.md +72 -0
- package/skills/qa-init/examples/initialize-a-repo.md +91 -0
- package/skills/qa-init/references/detection-guide.md +76 -0
- package/skills/qa-init/references/deterministic-tooling.md +128 -0
- package/skills/qa-init/references/evidence-and-reporting.md +66 -0
- package/skills/qa-init/templates/context.md +68 -0
- package/skills/qa-report/README.md +19 -0
- package/skills/qa-report/SKILL.md +72 -0
- package/skills/qa-report/contracts/report-result.schema.json +186 -0
- package/skills/qa-report/examples/release-report.md +68 -0
- package/skills/qa-report/references/deterministic-tooling.md +128 -0
- package/skills/qa-report/references/diagnostic-engine.md +36 -0
- package/skills/qa-report/references/evidence-and-reporting.md +66 -0
- package/skills/qa-report/references/finding-prioritization.md +43 -0
- package/skills/qa-report/references/recommendation-ranking.md +34 -0
- package/skills/qa-report/references/report-aggregation.md +44 -0
- package/skills/qa-review/README.md +19 -0
- package/skills/qa-review/SKILL.md +58 -0
- package/skills/qa-review/contracts/review-result.schema.json +59 -0
- package/skills/qa-review/examples/suite-review.md +51 -0
- package/skills/qa-review/references/anti-patterns.md +49 -0
- package/skills/qa-review/references/assertion-patterns.md +45 -0
- package/skills/qa-review/references/evidence-and-reporting.md +66 -0
- package/skills/qa-review/references/fixtures.md +45 -0
- package/skills/qa-review/references/page-objects.md +45 -0
- package/skills/qa-run/README.md +22 -0
- package/skills/qa-run/SKILL.md +92 -0
- package/skills/qa-run/contracts/execution-plan.schema.json +132 -0
- package/skills/qa-run/contracts/execution-result.schema.json +204 -0
- package/skills/qa-run/examples/execute-playwright.md +89 -0
- package/skills/qa-run/examples/plan-a-run.md +83 -0
- package/skills/qa-run/references/artifact-collector.md +40 -0
- package/skills/qa-run/references/browser-launch.md +39 -0
- package/skills/qa-run/references/command-builder.md +39 -0
- package/skills/qa-run/references/deterministic-tooling.md +128 -0
- package/skills/qa-run/references/environment-detection.md +37 -0
- package/skills/qa-run/references/evidence-and-reporting.md +66 -0
- package/skills/qa-run/references/execution-strategy.md +54 -0
- package/skills/qa-run/references/playwright-artifacts.md +29 -0
- package/skills/qa-run/references/playwright-execution.md +48 -0
- package/skills/qa-run/references/playwright-project-discovery.md +42 -0
- package/skills/qa-run/references/report-normalization.md +49 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// `qa doctor` — environment and pack diagnostics with repair hints.
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { EXIT, SHARED_SKILLS_DIR } from '../constants.mjs';
|
|
6
|
+
import { resolveSourceRoot, resolveProjectRoot } from '../core/paths.mjs';
|
|
7
|
+
import { VERSION, SPEC_REVISION } from '../version.mjs';
|
|
8
|
+
import { AGENTS, resolveInstallTargets, listAgentIds } from '../agents/registry.mjs';
|
|
9
|
+
import { packHasBundles, findPython, verifyImports } from '../core/bundle.mjs';
|
|
10
|
+
import { BUNDLE_DEST, BUNDLE_MANIFEST, bundlePackagesForSkill } from '../core/manifest.mjs';
|
|
11
|
+
import { readLock } from '../core/lockfile.mjs';
|
|
12
|
+
import { createLogger } from '../core/logger.mjs';
|
|
13
|
+
import { parseCommonFlags } from '../cli/flags.mjs';
|
|
14
|
+
import { validateInstall } from '../core/validate-install.mjs';
|
|
15
|
+
import { scanProject } from '../detect/scan.mjs';
|
|
16
|
+
import { spawnSync } from 'node:child_process';
|
|
17
|
+
|
|
18
|
+
function hasGit(root) {
|
|
19
|
+
if (fs.existsSync(path.join(root, '.git'))) return true;
|
|
20
|
+
const probe = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
21
|
+
cwd: root,
|
|
22
|
+
encoding: 'utf8',
|
|
23
|
+
});
|
|
24
|
+
return probe.status === 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function runDoctor(argv, { log } = {}) {
|
|
28
|
+
const opts = parseCommonFlags(argv);
|
|
29
|
+
const logger = log ?? createLogger();
|
|
30
|
+
if (opts.help) {
|
|
31
|
+
logger.result(`Usage: qa doctor [--project <dir>] [--json]
|
|
32
|
+
|
|
33
|
+
Diagnose environment and QA Automation Pack installation.
|
|
34
|
+
Each failure includes an exact repair command.`);
|
|
35
|
+
return EXIT.OK;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const root = resolveProjectRoot(opts.project ?? process.cwd());
|
|
39
|
+
let source = null;
|
|
40
|
+
try {
|
|
41
|
+
source = resolveSourceRoot();
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (!opts.json) logger.warn(error.message);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const detected = AGENTS.filter((a) => a.detect(root)).map((a) => a.id);
|
|
47
|
+
const targets = resolveInstallTargets(root, opts.agents).map((a) => a.id);
|
|
48
|
+
const lock = readLock(root);
|
|
49
|
+
const python = findPython();
|
|
50
|
+
const gitOk = hasGit(root);
|
|
51
|
+
const scan = scanProject(root, opts.agents);
|
|
52
|
+
const validation = lock ? validateInstall(root) : null;
|
|
53
|
+
|
|
54
|
+
/** @type {Array<{ section: string, id: string, ok: boolean, message: string, hint?: string }>} */
|
|
55
|
+
const checklist = [];
|
|
56
|
+
|
|
57
|
+
const nodeMajor = Number(process.versions.node.split('.')[0]);
|
|
58
|
+
checklist.push({
|
|
59
|
+
section: 'Environment',
|
|
60
|
+
id: 'node',
|
|
61
|
+
ok: nodeMajor >= 18,
|
|
62
|
+
message: `Node ${process.version}`,
|
|
63
|
+
hint: nodeMajor >= 18 ? undefined : 'upgrade to Node.js 18.18+',
|
|
64
|
+
});
|
|
65
|
+
checklist.push({
|
|
66
|
+
section: 'Environment',
|
|
67
|
+
id: 'python',
|
|
68
|
+
ok: Boolean(python),
|
|
69
|
+
message: python ? `${python.bin} (${python.version})` : 'Python not found',
|
|
70
|
+
hint: python ? undefined : 'install Python 3.8+ for analysis/diagnostics engines',
|
|
71
|
+
});
|
|
72
|
+
checklist.push({
|
|
73
|
+
section: 'Environment',
|
|
74
|
+
id: 'git',
|
|
75
|
+
ok: gitOk,
|
|
76
|
+
message: gitOk ? 'git repository detected' : 'not a git repository',
|
|
77
|
+
hint: gitOk ? undefined : 'optional — git init if you want version control',
|
|
78
|
+
});
|
|
79
|
+
checklist.push({
|
|
80
|
+
section: 'Environment',
|
|
81
|
+
id: 'assistant',
|
|
82
|
+
ok: detected.length > 0,
|
|
83
|
+
message:
|
|
84
|
+
detected.length > 0
|
|
85
|
+
? `AI assistants: ${detected.join(', ')}`
|
|
86
|
+
: 'no assistant markers (will use .agents/skills)',
|
|
87
|
+
hint: detected.length > 0 ? undefined : 'optional — open the project in Cursor/Claude/OpenCode',
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (validation) {
|
|
91
|
+
for (const c of validation.checks) {
|
|
92
|
+
checklist.push({
|
|
93
|
+
section: 'QA Automation Pack',
|
|
94
|
+
id: c.id,
|
|
95
|
+
ok: c.ok,
|
|
96
|
+
message: c.message,
|
|
97
|
+
hint: c.hint,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
checklist.push({
|
|
102
|
+
section: 'QA Automation Pack',
|
|
103
|
+
id: 'lockfile',
|
|
104
|
+
ok: false,
|
|
105
|
+
message: 'no qa-lock.json',
|
|
106
|
+
hint: 'run: qa install',
|
|
107
|
+
});
|
|
108
|
+
checklist.push({
|
|
109
|
+
section: 'QA Automation Pack',
|
|
110
|
+
id: 'skills',
|
|
111
|
+
ok: false,
|
|
112
|
+
message: 'skills not installed',
|
|
113
|
+
hint: 'run: qa install',
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Deep Python import check when bundles are on disk
|
|
118
|
+
if (lock && packHasBundles() && python) {
|
|
119
|
+
const bundledSkill = Object.keys(BUNDLE_MANIFEST)[0];
|
|
120
|
+
const libDir = path.join(root, SHARED_SKILLS_DIR, bundledSkill, BUNDLE_DEST);
|
|
121
|
+
const claudeLib = path.join(root, '.claude', 'skills', bundledSkill, BUNDLE_DEST);
|
|
122
|
+
const resolvedLib = fs.existsSync(libDir) ? libDir : fs.existsSync(claudeLib) ? claudeLib : null;
|
|
123
|
+
if (resolvedLib) {
|
|
124
|
+
const packages = bundlePackagesForSkill(bundledSkill);
|
|
125
|
+
const result = verifyImports({ pythonBin: python.bin, libDir: resolvedLib, packages });
|
|
126
|
+
checklist.push({
|
|
127
|
+
section: 'QA Automation Pack',
|
|
128
|
+
id: 'engine-imports',
|
|
129
|
+
ok: result.ok,
|
|
130
|
+
message: result.ok ? 'bundled engine runs cleanly' : `engine check failed: ${result.stderr}`,
|
|
131
|
+
hint: result.ok ? undefined : 'run: qa repair',
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const blocking = checklist.filter(
|
|
137
|
+
(c) =>
|
|
138
|
+
!c.ok &&
|
|
139
|
+
['node', 'lockfile', 'skills', 'integrity', 'engine'].includes(c.id),
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const report = {
|
|
143
|
+
packVersion: VERSION,
|
|
144
|
+
specRevision: SPEC_REVISION,
|
|
145
|
+
projectRoot: root,
|
|
146
|
+
sourceRoot: source,
|
|
147
|
+
knownAgents: listAgentIds(),
|
|
148
|
+
detectedAgents: detected,
|
|
149
|
+
installTargets: targets,
|
|
150
|
+
lockfilePresent: Boolean(lock),
|
|
151
|
+
lockfilePack: lock?.pack ?? null,
|
|
152
|
+
python,
|
|
153
|
+
bundles: packHasBundles(),
|
|
154
|
+
node: process.version,
|
|
155
|
+
git: gitOk,
|
|
156
|
+
frameworks: scan.frameworks,
|
|
157
|
+
languages: scan.languages,
|
|
158
|
+
checklist,
|
|
159
|
+
ok: blocking.length === 0,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
if (!opts.json) {
|
|
163
|
+
logger.step(`QA Automation Pack doctor — ${VERSION}`);
|
|
164
|
+
logger.step(`project ${root}`);
|
|
165
|
+
let section = '';
|
|
166
|
+
for (const item of checklist) {
|
|
167
|
+
if (item.section !== section) {
|
|
168
|
+
section = item.section;
|
|
169
|
+
logger.info(`\n${section}`);
|
|
170
|
+
}
|
|
171
|
+
// A failed check that is not blocking is a warning, not an error. Rendering
|
|
172
|
+
// "no git repository" and "no assistant markers" in red — directly above a
|
|
173
|
+
// hint calling them optional — is the first thing a new user sees, and it
|
|
174
|
+
// reads as a broken install when nothing is broken.
|
|
175
|
+
const isBlocking = blocking.some((b) => b.id === item.id);
|
|
176
|
+
if (item.ok) logger.ok(item.message);
|
|
177
|
+
else if (isBlocking) logger.error(item.message);
|
|
178
|
+
else logger.warn(item.message);
|
|
179
|
+
if (!item.ok && item.hint) logger.info(` → ${item.hint}`);
|
|
180
|
+
}
|
|
181
|
+
logger.info('');
|
|
182
|
+
if (blocking.length === 0) logger.ok('doctor: no blocking issues');
|
|
183
|
+
else logger.error('doctor: blocking issues found — see hints above');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Doctor always exits 0 when it successfully produced a report; `ok` in the
|
|
187
|
+
// payload (and self-test / verify) are the gates for automation.
|
|
188
|
+
logger.result(report);
|
|
189
|
+
return EXIT.OK;
|
|
190
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// `qa install` — copy skills into agent discovery paths, optional wrappers,
|
|
2
|
+
// lockfile with per-file hashes. Never executes skill code at install time.
|
|
3
|
+
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { BACKUP_DIR, LOCKFILE, EXIT } from '../constants.mjs';
|
|
7
|
+
import { resolveSourceRoot, resolveProjectRoot, toPosix } from '../core/paths.mjs';
|
|
8
|
+
import { listSkills, skillFiles } from '../core/manifest.mjs';
|
|
9
|
+
import { bundleFilesForSkill } from '../core/bundle.mjs';
|
|
10
|
+
import { hashBytes } from '../core/hash.mjs';
|
|
11
|
+
import { detectConflicts } from '../core/conflict.mjs';
|
|
12
|
+
import { Transaction } from '../core/fs-safe.mjs';
|
|
13
|
+
import { buildLock, readLock, serializeLock, lockPath } from '../core/lockfile.mjs';
|
|
14
|
+
import { loadConfig } from '../core/config.mjs';
|
|
15
|
+
import { readSkillMeta } from '../core/skill-meta.mjs';
|
|
16
|
+
import { renderWrapper } from '../core/wrappers.mjs';
|
|
17
|
+
import { conflictError, verifyError } from '../core/errors.mjs';
|
|
18
|
+
import { resolveInstallTargets } from '../agents/registry.mjs';
|
|
19
|
+
import { createLogger } from '../core/logger.mjs';
|
|
20
|
+
import { parseCommonFlags } from '../cli/flags.mjs';
|
|
21
|
+
import { validateInstall } from '../core/validate-install.mjs';
|
|
22
|
+
import { INSTALL_STEPS, progressBar } from '../ui/progress.mjs';
|
|
23
|
+
import { detectFrameworks } from '../detect/frameworks.mjs';
|
|
24
|
+
import { getFramework } from '../../../../shared/frameworks/registry.mjs';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Say plainly which commands this project can actually use.
|
|
28
|
+
*
|
|
29
|
+
* A project with no supported end-to-end framework still gets all thirteen
|
|
30
|
+
* skills, and `/qa-run` will then stop and recommend `/qa-init` — which the user
|
|
31
|
+
* has already run. That loop reads as a broken install. Unit-test-only projects
|
|
32
|
+
* (Jest, Vitest, pytest) are common, so the honest thing is to say up front which
|
|
33
|
+
* commands work here and which do not, rather than let the user discover it by
|
|
34
|
+
* hitting a dead end.
|
|
35
|
+
*/
|
|
36
|
+
function reportFrameworkFit(root, logger) {
|
|
37
|
+
let detected = [];
|
|
38
|
+
try {
|
|
39
|
+
// detectFrameworks returns ids; the registry is the source of truth for
|
|
40
|
+
// whether an id can actually execute live.
|
|
41
|
+
detected = (detectFrameworks(root).frameworks ?? []).map(
|
|
42
|
+
(id) => getFramework(id) ?? { id, name: id, liveExecution: false },
|
|
43
|
+
);
|
|
44
|
+
} catch {
|
|
45
|
+
return; // detection is best-effort; never fail an install over it
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (detected.length > 0) {
|
|
49
|
+
const live = detected.filter((f) => f.liveExecution).map((f) => f.name ?? f.id);
|
|
50
|
+
const gated = detected.filter((f) => !f.liveExecution).map((f) => f.name ?? f.id);
|
|
51
|
+
if (live.length > 0) {
|
|
52
|
+
logger.step(`detected ${live.join(', ')} — /qa-run and /qa-generate work here`);
|
|
53
|
+
}
|
|
54
|
+
if (gated.length > 0) {
|
|
55
|
+
logger.step(
|
|
56
|
+
`detected ${gated.join(', ')} — results are understood, but running and ` +
|
|
57
|
+
'generating tests live is Playwright-only today',
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
logger.warn('no supported end-to-end framework detected (Playwright, Selenium, Cypress, WebdriverIO)');
|
|
64
|
+
logger.info(' → /qa-run and /qa-generate need one; they will tell you so rather than guess');
|
|
65
|
+
logger.info(' → these work without one: /qa-review, /qa-api, /qa-report, /qa-audit, /qa-explore');
|
|
66
|
+
logger.info(' → unit tests only (Jest, Vitest, pytest)? That is expected — see the README');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Core install implementation shared by install / onboard / repair / update.
|
|
71
|
+
*
|
|
72
|
+
* @param {{
|
|
73
|
+
* projectRoot: string,
|
|
74
|
+
* agentIds?: string[],
|
|
75
|
+
* force?: boolean,
|
|
76
|
+
* dryRun?: boolean,
|
|
77
|
+
* skipValidate?: boolean,
|
|
78
|
+
* json?: boolean,
|
|
79
|
+
* onProgress?: (label: string, index: number, total: number) => void,
|
|
80
|
+
* log?: ReturnType<typeof createLogger>,
|
|
81
|
+
* }} options
|
|
82
|
+
*/
|
|
83
|
+
export async function executeInstall({
|
|
84
|
+
projectRoot,
|
|
85
|
+
agentIds = [],
|
|
86
|
+
force = false,
|
|
87
|
+
dryRun = false,
|
|
88
|
+
skipValidate = false,
|
|
89
|
+
json = false,
|
|
90
|
+
onProgress,
|
|
91
|
+
log,
|
|
92
|
+
} = {}) {
|
|
93
|
+
const logger = log ?? createLogger();
|
|
94
|
+
const sourceRoot = resolveSourceRoot();
|
|
95
|
+
const root = resolveProjectRoot(projectRoot);
|
|
96
|
+
const { config } = loadConfig(root);
|
|
97
|
+
const explicit = agentIds.length > 0 ? agentIds : config.agents ?? [];
|
|
98
|
+
const agents = resolveInstallTargets(root, explicit);
|
|
99
|
+
const skills = listSkills(sourceRoot);
|
|
100
|
+
|
|
101
|
+
if (!json) {
|
|
102
|
+
logger.step(`source: ${sourceRoot}`);
|
|
103
|
+
logger.step(`project: ${root}`);
|
|
104
|
+
logger.step(`agents: ${agents.map((a) => a.id).join(', ')}`);
|
|
105
|
+
logger.step(`skills: ${skills.length}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** @type {Array<{path:string, sha256:string, bytes:number, owner:string, skill?:string, agent?:string, content:Buffer}>} */
|
|
109
|
+
const planned = [];
|
|
110
|
+
|
|
111
|
+
for (const agent of agents) {
|
|
112
|
+
for (const skill of skills) {
|
|
113
|
+
const files = skillFiles(sourceRoot, skill);
|
|
114
|
+
for (const rel of files) {
|
|
115
|
+
if (rel.startsWith('tests/') || rel.includes('/tests/')) continue;
|
|
116
|
+
const abs = path.join(sourceRoot, 'skills', skill, rel);
|
|
117
|
+
const content = fs.readFileSync(abs);
|
|
118
|
+
const dest = toPosix(path.join(agent.skillsDir, skill, rel));
|
|
119
|
+
planned.push({
|
|
120
|
+
path: dest,
|
|
121
|
+
sha256: hashBytes(content),
|
|
122
|
+
bytes: content.length,
|
|
123
|
+
owner: 'skill',
|
|
124
|
+
skill,
|
|
125
|
+
agent: agent.id,
|
|
126
|
+
content,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
for (const bundled of bundleFilesForSkill(sourceRoot, skill)) {
|
|
130
|
+
const dest = toPosix(path.join(agent.skillsDir, skill, bundled.rel));
|
|
131
|
+
planned.push({
|
|
132
|
+
path: dest,
|
|
133
|
+
sha256: hashBytes(bundled.content),
|
|
134
|
+
bytes: bundled.content.length,
|
|
135
|
+
owner: 'skill',
|
|
136
|
+
skill,
|
|
137
|
+
agent: agent.id,
|
|
138
|
+
content: bundled.content,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (agent.wrapperFormat && agent.wrapperDir) {
|
|
144
|
+
for (const skill of skills) {
|
|
145
|
+
const meta = readSkillMeta(path.join(sourceRoot, 'skills', skill, 'SKILL.md'));
|
|
146
|
+
if (!meta.name) continue;
|
|
147
|
+
const { filename, content } = renderWrapper(agent.wrapperFormat, meta);
|
|
148
|
+
const buf = Buffer.from(content, 'utf8');
|
|
149
|
+
const dest = toPosix(path.join(agent.wrapperDir, filename));
|
|
150
|
+
planned.push({
|
|
151
|
+
path: dest,
|
|
152
|
+
sha256: hashBytes(buf),
|
|
153
|
+
bytes: buf.length,
|
|
154
|
+
owner: 'wrapper',
|
|
155
|
+
skill,
|
|
156
|
+
agent: agent.id,
|
|
157
|
+
content: buf,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const byPath = new Map();
|
|
164
|
+
for (const entry of planned) {
|
|
165
|
+
const prev = byPath.get(entry.path);
|
|
166
|
+
if (prev && prev.sha256 !== entry.sha256) {
|
|
167
|
+
throw conflictError(`conflicting content for ${entry.path}`);
|
|
168
|
+
}
|
|
169
|
+
if (!prev) byPath.set(entry.path, entry);
|
|
170
|
+
}
|
|
171
|
+
const unique = [...byPath.values()];
|
|
172
|
+
|
|
173
|
+
const priorLock = readLock(root);
|
|
174
|
+
const conflicts = detectConflicts({
|
|
175
|
+
projectRoot: root,
|
|
176
|
+
planned: unique,
|
|
177
|
+
priorLock,
|
|
178
|
+
});
|
|
179
|
+
if (conflicts.length > 0 && !force) {
|
|
180
|
+
throw conflictError(
|
|
181
|
+
`refusing to overwrite ${conflicts.length} file(s) not owned by a previous install:\n` +
|
|
182
|
+
conflicts.map((c) => ` - ${c.path}`).join('\n'),
|
|
183
|
+
're-run with --force to overwrite, or remove the conflicting files',
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const reportProgress = (label, index) => {
|
|
188
|
+
if (onProgress) onProgress(label, index, INSTALL_STEPS.length);
|
|
189
|
+
else if (!json) {
|
|
190
|
+
logger.step(`${progressBar(index, INSTALL_STEPS.length)} ${label}`);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
reportProgress(INSTALL_STEPS[0].label, 1);
|
|
195
|
+
|
|
196
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
197
|
+
const backupDir = path.join(root, BACKUP_DIR, stamp);
|
|
198
|
+
const tx = new Transaction(root, backupDir, { dryRun });
|
|
199
|
+
for (const entry of unique) {
|
|
200
|
+
tx.write(entry.path, entry.content);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
reportProgress(INSTALL_STEPS[1].label, 2);
|
|
204
|
+
reportProgress(INSTALL_STEPS[2].label, 3);
|
|
205
|
+
|
|
206
|
+
const lock = buildLock({
|
|
207
|
+
agents: agents.map((a) => ({
|
|
208
|
+
id: a.id,
|
|
209
|
+
name: a.name,
|
|
210
|
+
tier: a.tier,
|
|
211
|
+
skillsDir: a.skillsDir,
|
|
212
|
+
detected: a.detected,
|
|
213
|
+
})),
|
|
214
|
+
files: unique.map(({ path: p, sha256, bytes, owner, skill, agent }) => ({
|
|
215
|
+
path: p,
|
|
216
|
+
sha256,
|
|
217
|
+
bytes,
|
|
218
|
+
owner,
|
|
219
|
+
skill,
|
|
220
|
+
agent,
|
|
221
|
+
})),
|
|
222
|
+
now: new Date().toISOString(),
|
|
223
|
+
});
|
|
224
|
+
if (!dryRun) {
|
|
225
|
+
tx.write(LOCKFILE, Buffer.from(serializeLock(lock), 'utf8'));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const summary = tx.commit();
|
|
229
|
+
|
|
230
|
+
reportProgress(INSTALL_STEPS[3].label, 4);
|
|
231
|
+
reportProgress(INSTALL_STEPS[4].label, 5);
|
|
232
|
+
|
|
233
|
+
let validation = null;
|
|
234
|
+
if (!dryRun && !skipValidate) {
|
|
235
|
+
validation = validateInstall(root);
|
|
236
|
+
if (!validation.ok) {
|
|
237
|
+
const failed = validation.checks.filter((c) => c.hard && !c.ok);
|
|
238
|
+
throw verifyError(
|
|
239
|
+
`installation completed but validation failed:\n` +
|
|
240
|
+
failed.map((c) => ` - ${c.message}`).join('\n'),
|
|
241
|
+
failed.find((c) => c.hint)?.hint ?? 'run: qa doctor',
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
reportProgress(INSTALL_STEPS[5].label, 6);
|
|
247
|
+
|
|
248
|
+
if (dryRun) {
|
|
249
|
+
if (!json) logger.ok(`dry run: would write ${summary.written} file(s)`);
|
|
250
|
+
} else if (!json) {
|
|
251
|
+
logger.ok(`installed ${unique.length} file(s); lockfile ${lockPath(root)}`);
|
|
252
|
+
for (const step of INSTALL_STEPS) logger.ok(step.label);
|
|
253
|
+
reportFrameworkFit(root, logger);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
ok: true,
|
|
258
|
+
dryRun,
|
|
259
|
+
projectRoot: root,
|
|
260
|
+
agents: agents.map((a) => a.id),
|
|
261
|
+
skills: skills.length,
|
|
262
|
+
files: unique.length,
|
|
263
|
+
lockfile: dryRun ? null : LOCKFILE,
|
|
264
|
+
validation,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export async function runInstall(argv, { log } = {}) {
|
|
269
|
+
const opts = parseCommonFlags(argv);
|
|
270
|
+
const logger = log ?? createLogger();
|
|
271
|
+
if (opts.help) {
|
|
272
|
+
logger.result(`Usage: qa install [--agent <id>]... [--force] [--dry-run] [--yes] [--json] [--project <dir>]
|
|
273
|
+
|
|
274
|
+
Copy QA Automation Pack skills into Agent Skills discovery paths
|
|
275
|
+
(.agents/skills/ and .claude/skills/ when applicable), write qa-lock.json,
|
|
276
|
+
and generate thin slash wrappers for agents that need them.`);
|
|
277
|
+
return EXIT.OK;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const result = await executeInstall({
|
|
281
|
+
projectRoot: opts.project ?? process.cwd(),
|
|
282
|
+
agentIds: opts.agents,
|
|
283
|
+
force: opts.force,
|
|
284
|
+
dryRun: opts.dryRun,
|
|
285
|
+
json: opts.json,
|
|
286
|
+
log: logger,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
logger.result(result);
|
|
290
|
+
return EXIT.OK;
|
|
291
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Interactive onboarding wizard — default bare `qa` command.
|
|
2
|
+
|
|
3
|
+
import { EXIT } from '../constants.mjs';
|
|
4
|
+
import { resolveProjectRoot } from '../core/paths.mjs';
|
|
5
|
+
import { parseCommonFlags, wantsNonInteractive } from '../cli/flags.mjs';
|
|
6
|
+
import { scanProject } from '../detect/scan.mjs';
|
|
7
|
+
import { executeInstall } from './install.mjs';
|
|
8
|
+
import { createUi, isCancel } from '../ui/theme.mjs';
|
|
9
|
+
import { createLogger } from '../core/logger.mjs';
|
|
10
|
+
import { listAgentIds } from '../agents/registry.mjs';
|
|
11
|
+
|
|
12
|
+
function formatScanNote(scan) {
|
|
13
|
+
const lines = [];
|
|
14
|
+
if (scan.detectedAgents.length) {
|
|
15
|
+
for (const a of scan.detectedAgents) lines.push(`✓ ${a.name} detected`);
|
|
16
|
+
} else {
|
|
17
|
+
lines.push('· No AI coding assistant markers found (will install shared .agents/skills)');
|
|
18
|
+
}
|
|
19
|
+
for (const fw of scan.frameworks) {
|
|
20
|
+
lines.push(`✓ ${fw[0].toUpperCase()}${fw.slice(1)} project detected`);
|
|
21
|
+
}
|
|
22
|
+
for (const lang of scan.languages) {
|
|
23
|
+
lines.push(`✓ ${lang[0].toUpperCase()}${lang.slice(1)} detected`);
|
|
24
|
+
}
|
|
25
|
+
if (scan.environment.git) lines.push('✓ Git repository detected');
|
|
26
|
+
else lines.push('· Not a git repository');
|
|
27
|
+
if (scan.environment.packageManager) {
|
|
28
|
+
lines.push(`✓ Package manager: ${scan.environment.packageManager}`);
|
|
29
|
+
}
|
|
30
|
+
return lines.join('\n');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function formatRecommendations(scan) {
|
|
34
|
+
const recommended = scan.recommendations.filter((r) => r.recommended);
|
|
35
|
+
return recommended.map((r) => `✓ ${r.label}\n ${r.reason}`).join('\n\n');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function guidedFirstRun(ui, { skip }) {
|
|
39
|
+
if (skip) {
|
|
40
|
+
ui.note(
|
|
41
|
+
[
|
|
42
|
+
'Open your AI coding assistant and try:',
|
|
43
|
+
' /qa-init',
|
|
44
|
+
' Analyze this repository.',
|
|
45
|
+
' Find bugs on the Login page. (or /qa-explore <url>)',
|
|
46
|
+
].join('\n'),
|
|
47
|
+
'Next steps',
|
|
48
|
+
);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
ui.note('Open your AI coding assistant (Cursor, Claude Code, OpenCode, …).', 'Step 1');
|
|
53
|
+
const ready = await ui.confirm('Press Enter / confirm when ready', { initialValue: true });
|
|
54
|
+
if (isCancel(ready) || !ready) {
|
|
55
|
+
ui.warn('Skipped guided first-run. Run qa self-test anytime.');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
ui.note('Ask your assistant:\n\n Analyze this repository.\n\n (or type /qa-init)', 'Step 2');
|
|
60
|
+
await ui.confirm('Continue when done', { initialValue: true });
|
|
61
|
+
|
|
62
|
+
ui.note(
|
|
63
|
+
'Ask your assistant:\n\n Find bugs in the Login page.\n\n (or /qa-explore <url>)',
|
|
64
|
+
'Step 3',
|
|
65
|
+
);
|
|
66
|
+
await ui.confirm('Continue when done', { initialValue: true });
|
|
67
|
+
|
|
68
|
+
ui.success('Your QA Automation Pack is fully operational.');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function runOnboard(argv, { log } = {}) {
|
|
72
|
+
const opts = parseCommonFlags(argv);
|
|
73
|
+
const logger = log ?? createLogger();
|
|
74
|
+
if (opts.help) {
|
|
75
|
+
logger.result(`Usage: qa [--yes] [--project <dir>] [--agent <id>]...
|
|
76
|
+
|
|
77
|
+
Interactive installer and guided first-run. Same as: qa onboard
|
|
78
|
+
|
|
79
|
+
Options:
|
|
80
|
+
--yes / --ci Skip prompts; install with detected defaults
|
|
81
|
+
--json Machine-readable result on stdout
|
|
82
|
+
--force Overwrite conflicting files
|
|
83
|
+
--project <d> Project root (default: cwd)
|
|
84
|
+
--agent <id> Target agent (repeatable). Known: ${listAgentIds().join(', ')}`);
|
|
85
|
+
return EXIT.OK;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const projectRoot = resolveProjectRoot(opts.project ?? process.cwd());
|
|
89
|
+
const nonInteractive = wantsNonInteractive(opts);
|
|
90
|
+
const ui = createUi({ quiet: nonInteractive && !opts.json, json: opts.json });
|
|
91
|
+
|
|
92
|
+
if (!nonInteractive) {
|
|
93
|
+
ui.intro('Welcome to QA Automation Pack');
|
|
94
|
+
ui.logLine('Scanning your environment...');
|
|
95
|
+
} else if (!opts.json) {
|
|
96
|
+
logger.step('Scanning environment (non-interactive)...');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const scan = scanProject(projectRoot, opts.agents);
|
|
100
|
+
|
|
101
|
+
if (!nonInteractive) {
|
|
102
|
+
ui.note(formatScanNote(scan), 'Environment');
|
|
103
|
+
ui.note(`Project: ${scan.projectName}`, 'Project');
|
|
104
|
+
ui.note(formatRecommendations(scan), 'Recommended components');
|
|
105
|
+
|
|
106
|
+
const proceed = await ui.confirm('Continue with installation?', { initialValue: true });
|
|
107
|
+
if (isCancel(proceed) || !proceed) {
|
|
108
|
+
ui.cancel('Installation cancelled.');
|
|
109
|
+
return EXIT.OK;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (scan.detectedAgents.length > 1 && opts.agents.length === 0) {
|
|
113
|
+
const picked = await ui.multiselect(
|
|
114
|
+
'Which assistants should we install for?',
|
|
115
|
+
scan.detectedAgents.map((a) => ({ value: a.id, label: a.name })),
|
|
116
|
+
{ required: true },
|
|
117
|
+
);
|
|
118
|
+
if (isCancel(picked)) {
|
|
119
|
+
ui.cancel('Installation cancelled.');
|
|
120
|
+
return EXIT.OK;
|
|
121
|
+
}
|
|
122
|
+
opts.agents = picked;
|
|
123
|
+
}
|
|
124
|
+
} else if (!opts.json) {
|
|
125
|
+
for (const line of formatScanNote(scan).split('\n')) logger.ok(line.replace(/^[✓·]\s*/, ''));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const spinner = ui.spinner();
|
|
129
|
+
if (!nonInteractive) spinner.start('Installing...');
|
|
130
|
+
|
|
131
|
+
const result = await executeInstall({
|
|
132
|
+
projectRoot,
|
|
133
|
+
agentIds: opts.agents,
|
|
134
|
+
force: opts.force,
|
|
135
|
+
dryRun: opts.dryRun,
|
|
136
|
+
json: opts.json,
|
|
137
|
+
log: createLogger({ level: opts.json || !nonInteractive ? 'error' : 'info' }),
|
|
138
|
+
onProgress: (label) => {
|
|
139
|
+
if (!nonInteractive) spinner.message(label);
|
|
140
|
+
else if (!opts.json) logger.step(label);
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (!nonInteractive) spinner.stop('Installation complete');
|
|
145
|
+
|
|
146
|
+
if (!opts.dryRun && !opts.json) {
|
|
147
|
+
await guidedFirstRun(ui, { skip: nonInteractive });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!nonInteractive) {
|
|
151
|
+
ui.outro('Installation complete — you are ready to QA.');
|
|
152
|
+
} else if (!opts.json) {
|
|
153
|
+
logger.ok('Installation complete');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
logger.result({
|
|
157
|
+
...result,
|
|
158
|
+
projectName: scan.projectName,
|
|
159
|
+
detectedAgents: scan.detectedAgents.map((a) => a.id),
|
|
160
|
+
recommendations: scan.recommendations.filter((r) => r.recommended).map((r) => r.id),
|
|
161
|
+
});
|
|
162
|
+
return EXIT.OK;
|
|
163
|
+
}
|