@chenguangyao/devflow-kit 0.1.43
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/CHANGELOG.md +232 -0
- package/LICENSE +21 -0
- package/README.md +539 -0
- package/bin/devflow.js +9 -0
- package/docs/RFC-001-devflow-kit.md +617 -0
- package/docs/RFC-002-workflow-kernel.md +134 -0
- package/docs/enterprise-integration-supplement.md +274 -0
- package/docs/internal-gitlab-setup.md +426 -0
- package/docs/marketplace-skills.md +231 -0
- package/docs/migration-from-arb.md +232 -0
- package/docs/tooling-overview.md +774 -0
- package/docs/workflow-orchestration.md +695 -0
- package/docs/workflow-ui-prototype.html +271 -0
- package/package.json +52 -0
- package/schemas/config.schema.json +51 -0
- package/schemas/delta.schema.json +22 -0
- package/schemas/state.schema.json +130 -0
- package/schemas/status-surface.schema.json +197 -0
- package/schemas/workflow-confirmation-surface.schema.json +70 -0
- package/schemas/workflow-picker.schema.json +94 -0
- package/scripts/postinstall.js +101 -0
- package/scripts/render-workflow-ui-prototype.js +271 -0
- package/skills/apply/SKILL.md +313 -0
- package/skills/apply/references/discipline-checklist.md +145 -0
- package/skills/apply/references/subagent-implementer-prompt.md +113 -0
- package/skills/apply/references/subagent-orchestration.md +150 -0
- package/skills/apply/references/subagent-reviewer-prompt.md +180 -0
- package/skills/apply/references/tdd-loop.md +287 -0
- package/skills/apply/references/when-plan-is-wrong.md +279 -0
- package/skills/apply/references/worktree-swarm.md +292 -0
- package/skills/archive/SKILL.md +229 -0
- package/skills/archive/references/conflict-resolution.md +336 -0
- package/skills/archive/references/knowledge-deposit.md +381 -0
- package/skills/archive/references/spec-merge.md +365 -0
- package/skills/brainstorm/SKILL.md +123 -0
- package/skills/brainstorm/references/proposal-template.md +244 -0
- package/skills/brainstorm/references/question-catalog.md +168 -0
- package/skills/brainstorm/references/session-template.md +184 -0
- package/skills/ci-fix/SKILL.md +63 -0
- package/skills/ci-fix/references/loop.md +25 -0
- package/skills/code-review/SKILL.md +279 -0
- package/skills/code-review/references/escalation-playbook.md +192 -0
- package/skills/code-review/references/language-cheatsheets/go.md +175 -0
- package/skills/code-review/references/language-cheatsheets/java-spring-mybatis.md +246 -0
- package/skills/code-review/references/language-cheatsheets/python.md +170 -0
- package/skills/code-review/references/language-cheatsheets/vue.md +199 -0
- package/skills/code-review/references/output-template.md +275 -0
- package/skills/code-review/references/review-checklist.md +251 -0
- package/skills/complexity-grading/SKILL.md +259 -0
- package/skills/deliver/SKILL.md +271 -0
- package/skills/deliver/references/delivery-modes.md +299 -0
- package/skills/deliver/references/notify.md +359 -0
- package/skills/deliver/references/pr-description.md +319 -0
- package/skills/dependency-upgrade/SKILL.md +57 -0
- package/skills/dependency-upgrade/references/risk-matrix.md +38 -0
- package/skills/df-orchestrator/SKILL.md +407 -0
- package/skills/df-orchestrator/references/complexity-grading.md +177 -0
- package/skills/df-orchestrator/references/escalation-matrix.md +191 -0
- package/skills/df-orchestrator/references/routing-rules.md +290 -0
- package/skills/df-orchestrator/references/workflow-state-machine.md +208 -0
- package/skills/frontend-quality/SKILL.md +61 -0
- package/skills/frontend-quality/references/checklist.md +35 -0
- package/skills/handoff-resume/SKILL.md +59 -0
- package/skills/handoff-resume/references/handoff-template.md +54 -0
- package/skills/plan/SKILL.md +166 -0
- package/skills/plan/references/task-breakdown.md +207 -0
- package/skills/plan/references/task-sequencing.md +143 -0
- package/skills/plan/references/task-template.md +248 -0
- package/skills/requirement-analysis/SKILL.md +499 -0
- package/skills/requirement-analysis/references/acceptance-criteria.md +183 -0
- package/skills/requirement-analysis/references/code-recon.md +151 -0
- package/skills/requirement-analysis/references/edge-case-catalog.md +164 -0
- package/skills/requirement-analysis/references/requirement-template.md +339 -0
- package/skills/requirement-analysis/references/scope-negotiation.md +162 -0
- package/skills/security-hardening/SKILL.md +60 -0
- package/skills/security-hardening/references/checklist.md +42 -0
- package/skills/tech-spec/SKILL.md +388 -0
- package/skills/tech-spec/references/api-contract-design.md +172 -0
- package/skills/tech-spec/references/decision-records.md +110 -0
- package/skills/tech-spec/references/design-template.md +301 -0
- package/skills/tech-spec/references/rollout-and-rollback.md +203 -0
- package/skills/tech-spec/references/spec-delta-conventions.md +250 -0
- package/skills/tech-spec/references/transaction-patterns.md +212 -0
- package/skills/test-spec/SKILL.md +219 -0
- package/skills/test-spec/references/coverage-strategy.md +218 -0
- package/skills/test-spec/references/edge-case-to-test.md +143 -0
- package/skills/test-spec/references/test-case-template.md +276 -0
- package/skills/verify/SKILL.md +232 -0
- package/skills/verify/references/nfr-verification.md +292 -0
- package/skills/verify/references/report-templates.md +510 -0
- package/skills/verify/references/self-test-guide.md +240 -0
- package/skills/verify/references/verify-rollback-map.md +247 -0
- package/src/cli/commands/_helpers.js +108 -0
- package/src/cli/commands/_submit.js +718 -0
- package/src/cli/commands/apply.js +198 -0
- package/src/cli/commands/archive.js +180 -0
- package/src/cli/commands/checkpoint.js +113 -0
- package/src/cli/commands/deliver.js +377 -0
- package/src/cli/commands/deploy.js +504 -0
- package/src/cli/commands/design.js +158 -0
- package/src/cli/commands/disable.js +21 -0
- package/src/cli/commands/doctor.js +178 -0
- package/src/cli/commands/enable.js +21 -0
- package/src/cli/commands/flow.js +645 -0
- package/src/cli/commands/help.js +93 -0
- package/src/cli/commands/ingest.js +602 -0
- package/src/cli/commands/init.js +341 -0
- package/src/cli/commands/knowledge.js +523 -0
- package/src/cli/commands/logs.js +43 -0
- package/src/cli/commands/new.js +202 -0
- package/src/cli/commands/plan.js +49 -0
- package/src/cli/commands/propose.js +27 -0
- package/src/cli/commands/provider.js +698 -0
- package/src/cli/commands/report.js +143 -0
- package/src/cli/commands/requirement.js +227 -0
- package/src/cli/commands/review.js +301 -0
- package/src/cli/commands/skills.js +457 -0
- package/src/cli/commands/status.js +925 -0
- package/src/cli/commands/switch.js +27 -0
- package/src/cli/commands/sync.js +47 -0
- package/src/cli/commands/test.js +366 -0
- package/src/cli/commands/uninstall.js +32 -0
- package/src/cli/commands/update.js +74 -0
- package/src/cli/commands/verify.js +354 -0
- package/src/cli/commands/worktree.js +78 -0
- package/src/cli/index.js +72 -0
- package/src/cli/parse-args.js +102 -0
- package/src/core/autodetect.js +271 -0
- package/src/core/change.js +208 -0
- package/src/core/checkpoint.js +217 -0
- package/src/core/config.js +60 -0
- package/src/core/delta.js +290 -0
- package/src/core/markers.js +59 -0
- package/src/core/paths.js +173 -0
- package/src/core/plan-tasks.js +36 -0
- package/src/core/project-routing.js +285 -0
- package/src/core/projects.js +200 -0
- package/src/core/state.js +200 -0
- package/src/core/workflow-check.js +177 -0
- package/src/core/workflow-init.js +34 -0
- package/src/core/workflow-picker.js +154 -0
- package/src/core/workflow-policy.js +119 -0
- package/src/core/workflow-suggest.js +181 -0
- package/src/core/workflow-verify.js +88 -0
- package/src/core/workflow.js +433 -0
- package/src/core/worktree.js +241 -0
- package/src/knowledge/categories.js +107 -0
- package/src/knowledge/classify.js +125 -0
- package/src/knowledge/deposit.js +414 -0
- package/src/knowledge/migrate.js +149 -0
- package/src/knowledge/mr.js +219 -0
- package/src/knowledge/query.js +131 -0
- package/src/knowledge/registry.js +151 -0
- package/src/knowledge/sync.js +179 -0
- package/src/providers/base.js +74 -0
- package/src/providers/drivers/api-yapi.js +78 -0
- package/src/providers/drivers/ci-jenkins.js +109 -0
- package/src/providers/drivers/intake-confluence.js +544 -0
- package/src/providers/drivers/kb-git.js +549 -0
- package/src/providers/drivers/kb-weknora.js +472 -0
- package/src/providers/drivers/notify-smtp.js +515 -0
- package/src/providers/drivers/observability-oss.js +43 -0
- package/src/providers/drivers/observability-sls.js +50 -0
- package/src/providers/lifecycle.js +135 -0
- package/src/providers/loader.js +132 -0
- package/src/providers/local.js +190 -0
- package/src/providers/userconfig.js +283 -0
- package/src/reports/aggregate.js +185 -0
- package/src/reports/coverage.js +163 -0
- package/src/reports/detect.js +143 -0
- package/src/reports/parse.js +236 -0
- package/src/templates/files/ci/github.yml +38 -0
- package/src/templates/files/ci/gitlab.yml +27 -0
- package/src/templates/files/design.md +63 -0
- package/src/templates/files/ide/devflow-workflow.md +58 -0
- package/src/templates/files/ide/project-overview-reference.md +1 -0
- package/src/templates/files/ide/project-overview.md +27 -0
- package/src/templates/files/knowledge-index.json +17 -0
- package/src/templates/files/knowledge.md +28 -0
- package/src/templates/files/meta.json +8 -0
- package/src/templates/files/plan.md +38 -0
- package/src/templates/files/proposal.md +33 -0
- package/src/templates/files/reports/contract-test.md +40 -0
- package/src/templates/files/reports/e2e-test.md +30 -0
- package/src/templates/files/reports/integration-test.md +36 -0
- package/src/templates/files/reports/joint-test.md +58 -0
- package/src/templates/files/reports/perf.md +24 -0
- package/src/templates/files/reports/regression.md +20 -0
- package/src/templates/files/reports/remote-test.md +55 -0
- package/src/templates/files/reports/self-test.md +43 -0
- package/src/templates/files/reports/smoke-test.md +22 -0
- package/src/templates/files/reports/unit-test.md +36 -0
- package/src/templates/files/requirement.md +51 -0
- package/src/templates/files/review.md +38 -0
- package/src/templates/files/tests.md +36 -0
- package/src/templates/files/verify.md +32 -0
- package/src/templates/index.js +21 -0
- package/src/utils/log.js +37 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const log = require('../../utils/log.js');
|
|
3
|
+
const config = require('../../core/config.js');
|
|
4
|
+
const initCmd = require('./init.js');
|
|
5
|
+
|
|
6
|
+
async function run({ sub, positional = [], flags = {}, cwd }) {
|
|
7
|
+
const root = cwd || process.cwd();
|
|
8
|
+
const what = sub || positional[0];
|
|
9
|
+
if (what !== 'project-overview') {
|
|
10
|
+
log.error('usage: devflow switch project-overview --mode=<reference|supplement|generate>');
|
|
11
|
+
process.exitCode = 2; return;
|
|
12
|
+
}
|
|
13
|
+
const mode = flags.mode;
|
|
14
|
+
if (!['reference', 'supplement', 'generate'].includes(mode)) {
|
|
15
|
+
log.error('--mode must be reference|supplement|generate');
|
|
16
|
+
process.exitCode = 2; return;
|
|
17
|
+
}
|
|
18
|
+
const cfg = await config.ensure(root);
|
|
19
|
+
cfg.projectOverview = cfg.projectOverview || {};
|
|
20
|
+
cfg.projectOverview.mode = mode;
|
|
21
|
+
await config.write(root, cfg);
|
|
22
|
+
log.ok(`projectOverview.mode = ${mode}`);
|
|
23
|
+
log.info('refreshing IDE marker sections...');
|
|
24
|
+
await initCmd.run({ flags: { 'overview-mode': mode }, cwd: root });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = { run };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const log = require('../../utils/log.js');
|
|
3
|
+
const initCmd = require('./init.js');
|
|
4
|
+
const fs = require('fs/promises');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const providers = require('../../providers/loader.js');
|
|
7
|
+
|
|
8
|
+
async function run({ sub, positional = [], flags = {}, cwd }) {
|
|
9
|
+
const what = sub || positional[0];
|
|
10
|
+
if (what === 'yapi') return syncYapi({ root: cwd || process.cwd(), flags, positional });
|
|
11
|
+
if (what !== 'project') {
|
|
12
|
+
log.error('usage: devflow sync <project|yapi> [--check]');
|
|
13
|
+
process.exitCode = 2; return;
|
|
14
|
+
}
|
|
15
|
+
if (flags.check) {
|
|
16
|
+
log.info('check mode: re-detect overview source and report drift only (NYI in v0.1; runs as full sync).');
|
|
17
|
+
}
|
|
18
|
+
// Re-run init to refresh marker sections (idempotent)
|
|
19
|
+
await initCmd.run({ flags, cwd });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function syncYapi({ root, flags = {}, positional = [] }) {
|
|
23
|
+
const file = flags.file || positional[1] || flags.contract || flags.openApi || flags['open-api'];
|
|
24
|
+
if (!file) {
|
|
25
|
+
log.error('usage: devflow sync yapi --file=<openapi.json|swagger.json|api.md> [--provider=api.yapi] [--dry-run]');
|
|
26
|
+
process.exitCode = 2;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const abs = path.resolve(root, file);
|
|
30
|
+
const content = await fs.readFile(abs, 'utf8');
|
|
31
|
+
const api = await providers.load(root, flags.provider ? { name: flags.provider } : { type: 'api' });
|
|
32
|
+
if (typeof api.syncContract !== 'function') {
|
|
33
|
+
log.error(`provider ${api.name || api.driver} does not support YAPI contract sync`);
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const result = await api.syncContract({
|
|
38
|
+
file: abs,
|
|
39
|
+
content,
|
|
40
|
+
dryRun: flags.dryRun === true || flags['dry-run'] === true,
|
|
41
|
+
});
|
|
42
|
+
log.ok(`yapi sync: ${result.status} interfaces=${result.interfaces}`);
|
|
43
|
+
if (result.title) log.dim(`title: ${result.title}`);
|
|
44
|
+
if (result.response) log.raw(JSON.stringify(result.response, null, 2));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = { run, _internals: { syncYapi } };
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs/promises');
|
|
3
|
+
const fsSync = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
const log = require('../../utils/log.js');
|
|
7
|
+
const paths = require('../../core/paths.js');
|
|
8
|
+
const change = require('../../core/change.js');
|
|
9
|
+
const templates = require('../../templates/index.js');
|
|
10
|
+
const helpers = require('./_helpers.js');
|
|
11
|
+
const detect = require('../../reports/detect.js');
|
|
12
|
+
const parser = require('../../reports/parse.js');
|
|
13
|
+
const cov = require('../../reports/coverage.js');
|
|
14
|
+
const projects = require('../../core/projects.js');
|
|
15
|
+
const aggregate = require('../../reports/aggregate.js');
|
|
16
|
+
|
|
17
|
+
const KINDS = new Set(['unit', 'integration', 'contract', 'e2e', 'joint', 'remote', 'smoke', 'regression', 'perf']);
|
|
18
|
+
|
|
19
|
+
async function run({ sub, flags = {}, positional = [], cwd }) {
|
|
20
|
+
const root = cwd || process.cwd();
|
|
21
|
+
const kind = sub || positional[0];
|
|
22
|
+
if (!KINDS.has(kind)) {
|
|
23
|
+
log.error(`usage: devflow test <${[...KINDS].join('|')}> [--cmd "<your test command>"] [--framework=<auto|jest|...>]`);
|
|
24
|
+
process.exitCode = 2; return;
|
|
25
|
+
}
|
|
26
|
+
const slug = await helpers.resolveSlug(root, flags, []);
|
|
27
|
+
if (!slug) { process.exitCode = 1; return; }
|
|
28
|
+
const st = await helpers.loadStateOrFail(root, slug);
|
|
29
|
+
if (!st) { process.exitCode = 1; return; }
|
|
30
|
+
|
|
31
|
+
const reportsDir = await change.ensureSubdir(root, slug, 'reports');
|
|
32
|
+
|
|
33
|
+
// Decide command + framework.
|
|
34
|
+
let cmd = flags.cmd || flags.command;
|
|
35
|
+
let framework = flags.framework || flags.fw;
|
|
36
|
+
let coverageHint = null;
|
|
37
|
+
|
|
38
|
+
if (!cmd && kind !== 'unit' && flags.project) {
|
|
39
|
+
const resolved = await projects.resolveTestCommand({
|
|
40
|
+
project: flags.project,
|
|
41
|
+
env: flags.env || flags.environment || 'test',
|
|
42
|
+
kind,
|
|
43
|
+
api: flags.api === true || flags.api === 'true',
|
|
44
|
+
});
|
|
45
|
+
if (resolved.command) {
|
|
46
|
+
cmd = resolved.command;
|
|
47
|
+
if (!framework) framework = resolved.framework;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if ((!cmd || !framework) && kind === 'unit') {
|
|
52
|
+
const detected = detect.detect(root);
|
|
53
|
+
const first = detected[0];
|
|
54
|
+
if (first) {
|
|
55
|
+
if (!cmd) cmd = first.command;
|
|
56
|
+
if (!framework) framework = first.framework;
|
|
57
|
+
coverageHint = first.coverageHint;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (!framework) framework = 'generic';
|
|
61
|
+
|
|
62
|
+
const backendUrl = flags.backendUrl || flags['backend-url'] || flags.backend || '';
|
|
63
|
+
const frontendUrl = flags.frontendUrl || flags['frontend-url'] || flags.frontend || '';
|
|
64
|
+
const apiBaseUrl = flags.apiBaseUrl || flags['api-base-url'] || flags.api || flags.url || '';
|
|
65
|
+
const database = flags.database || flags.db || '-';
|
|
66
|
+
const wantsPreflight = flags.preflight === true || flags['preflight'] === true;
|
|
67
|
+
|
|
68
|
+
let preflight = null;
|
|
69
|
+
if (kind === 'joint' && wantsPreflight) {
|
|
70
|
+
preflight = runJointPreflight({ backendUrl, frontendUrl });
|
|
71
|
+
await writePreflightLog(reportsDir, 'joint-preflight.log', preflight);
|
|
72
|
+
if (!preflight.ok) {
|
|
73
|
+
await writeKindReport({
|
|
74
|
+
kind, reportsDir, slug, status: 'blocked', failureType: 'env_error',
|
|
75
|
+
total: null, passed: null, failed: null, coverage: null,
|
|
76
|
+
command: cmd || '(未执行:联调预检未通过)',
|
|
77
|
+
output: renderPreflightOutput(preflight),
|
|
78
|
+
backendUrl, frontendUrl, apiBaseUrl, database,
|
|
79
|
+
preflight,
|
|
80
|
+
splitReport: wantsSplitReport(flags),
|
|
81
|
+
});
|
|
82
|
+
log.error(`joint preflight blocked: ${preflight.errors.join('; ')}`);
|
|
83
|
+
log.dim('fix environment/browser tooling, then rerun devflow test joint --preflight ...');
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
} else if (kind === 'remote' && wantsPreflight) {
|
|
88
|
+
preflight = runRemotePreflight({ apiBaseUrl });
|
|
89
|
+
await writePreflightLog(reportsDir, 'remote-preflight.log', preflight);
|
|
90
|
+
if (!preflight.ok) {
|
|
91
|
+
await writeKindReport({
|
|
92
|
+
kind, reportsDir, slug, status: 'blocked', failureType: 'env_error',
|
|
93
|
+
total: null, passed: null, failed: null, coverage: null,
|
|
94
|
+
command: cmd || '(未执行:远程 API 预检未通过)',
|
|
95
|
+
output: renderPreflightOutput(preflight),
|
|
96
|
+
backendUrl, frontendUrl, apiBaseUrl, database,
|
|
97
|
+
preflight,
|
|
98
|
+
splitReport: wantsSplitReport(flags),
|
|
99
|
+
});
|
|
100
|
+
log.error(`remote preflight blocked: ${preflight.errors.join('; ')}`);
|
|
101
|
+
log.dim('fix test environment URL/connectivity, then rerun devflow test remote --preflight ...');
|
|
102
|
+
process.exitCode = 1;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// For non-unit kinds, --cmd is required (no auto-detect).
|
|
108
|
+
if (!cmd && kind !== 'unit') {
|
|
109
|
+
log.error(`no command. pass --cmd "<your ${kind} command>"`);
|
|
110
|
+
process.exitCode = 2; return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let stdout = '(未运行命令;请在下方手动补充输出)';
|
|
114
|
+
let runStatus = 'pending';
|
|
115
|
+
let exitCode = null;
|
|
116
|
+
if (cmd) {
|
|
117
|
+
log.info(`framework=${framework} > ${cmd}`);
|
|
118
|
+
const r = spawnSync(cmd, { shell: true, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, cwd: root });
|
|
119
|
+
stdout = ((r.stdout || '') + (r.stderr || ''));
|
|
120
|
+
exitCode = r.status;
|
|
121
|
+
runStatus = exitCode === 0 ? 'pass' : 'fail';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Parse stdout.
|
|
125
|
+
const parsed = parser.parse(framework, stdout);
|
|
126
|
+
const truncatedOutput = truncate(stdout, 12000);
|
|
127
|
+
|
|
128
|
+
// Discover coverage (unit only by default).
|
|
129
|
+
let coverage = null;
|
|
130
|
+
if (kind === 'unit') {
|
|
131
|
+
coverage = cov.discover(root, coverageHint);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// status: prefer explicit pass/fail from command exit code; if unknown, infer from parsed numbers.
|
|
135
|
+
let status = runStatus;
|
|
136
|
+
if (status === 'pending' && parsed.failed != null) {
|
|
137
|
+
status = parsed.failed > 0 ? 'fail' : 'pass';
|
|
138
|
+
}
|
|
139
|
+
const failureType = classifyFailureType({
|
|
140
|
+
explicit: flags.failureType || flags['failure-type'],
|
|
141
|
+
status,
|
|
142
|
+
preflight,
|
|
143
|
+
exitCode,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await writeKindReport({
|
|
147
|
+
kind,
|
|
148
|
+
reportsDir,
|
|
149
|
+
slug,
|
|
150
|
+
status,
|
|
151
|
+
failureType,
|
|
152
|
+
total: parsed.total,
|
|
153
|
+
passed: parsed.passed,
|
|
154
|
+
failed: parsed.failed,
|
|
155
|
+
coverage: coverage && coverage.lines ? `${coverage.lines.pct}%` : '-',
|
|
156
|
+
command: cmd || '(未执行)',
|
|
157
|
+
output: truncatedOutput,
|
|
158
|
+
backendUrl,
|
|
159
|
+
frontendUrl,
|
|
160
|
+
apiBaseUrl,
|
|
161
|
+
database,
|
|
162
|
+
preflight,
|
|
163
|
+
framework,
|
|
164
|
+
parsed,
|
|
165
|
+
coverageDetails: coverage,
|
|
166
|
+
exitCode,
|
|
167
|
+
splitReport: wantsSplitReport(flags),
|
|
168
|
+
});
|
|
169
|
+
log.ok(`reports/test-report.md#${kind} status=${status}${failureType !== '-' ? ` failureType=${failureType}` : ''} ${parsed.total != null ? `passed=${parsed.passed}/${parsed.total}` : ''}${coverage && coverage.lines ? ` cov=${coverage.lines.pct}%` : ''}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function writeKindReport({
|
|
173
|
+
kind, reportsDir, slug, status, failureType, total, passed, failed, coverage,
|
|
174
|
+
command, output, backendUrl, frontendUrl, apiBaseUrl, database,
|
|
175
|
+
preflight, framework = 'generic', parsed = {}, coverageDetails = null, exitCode = null,
|
|
176
|
+
splitReport = false,
|
|
177
|
+
}) {
|
|
178
|
+
const tplName = `reports/${kind}-test.md`;
|
|
179
|
+
let tpl;
|
|
180
|
+
try { tpl = await templates.load(tplName); }
|
|
181
|
+
catch { tpl = `---\nslug: \${slug}\nkind: \${kind}\nstatus: \${status}\nfailureType: \${failureType}\nts: \${date}\n---\n\n# ${kind} 测试报告\n\n## 命令\n\n\`\`\`\n\${command}\n\`\`\`\n\n## 输出\n\n\`\`\`\n\${output}\n\`\`\`\n`; }
|
|
182
|
+
|
|
183
|
+
const body = templates.render(tpl, {
|
|
184
|
+
slug,
|
|
185
|
+
kind,
|
|
186
|
+
status,
|
|
187
|
+
failureType: failureType || '-',
|
|
188
|
+
total: fmt(total),
|
|
189
|
+
passed: fmt(passed),
|
|
190
|
+
failed: fmt(failed),
|
|
191
|
+
coverage: coverage || '-',
|
|
192
|
+
command,
|
|
193
|
+
output,
|
|
194
|
+
backendUrl: backendUrl || '-',
|
|
195
|
+
frontendUrl: frontendUrl || '-',
|
|
196
|
+
apiBaseUrl: apiBaseUrl || '-',
|
|
197
|
+
database: database || '-',
|
|
198
|
+
preflightTool: preflight && preflight.tool ? preflight.tool : '-',
|
|
199
|
+
preflightStatus: preflight ? (preflight.ok ? 'pass' : 'blocked') : '-',
|
|
200
|
+
preflightDetail: preflight ? renderPreflightOutput(preflight) : '-',
|
|
201
|
+
date: new Date().toISOString(),
|
|
202
|
+
});
|
|
203
|
+
let final = body;
|
|
204
|
+
|
|
205
|
+
// Append a structured "failures" section + coverage details.
|
|
206
|
+
const detail = renderDetail(framework, parsed, coverageDetails, exitCode, failureType);
|
|
207
|
+
if (detail) final = final.replace(/\n*$/, '\n') + '\n' + detail + '\n';
|
|
208
|
+
|
|
209
|
+
await aggregate.upsertReport({
|
|
210
|
+
reportsDir,
|
|
211
|
+
slug,
|
|
212
|
+
title: slug,
|
|
213
|
+
kind,
|
|
214
|
+
body: final,
|
|
215
|
+
meta: { status, total, passed, failed, coverage, failureType },
|
|
216
|
+
});
|
|
217
|
+
if (splitReport) {
|
|
218
|
+
const file = path.join(reportsDir, `${kind}-test.md`);
|
|
219
|
+
await fs.writeFile(file, final, 'utf8');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function fmt(n) { return n == null ? '-' : String(n); }
|
|
224
|
+
|
|
225
|
+
function renderDetail(framework, p, c, exitCode, failureType) {
|
|
226
|
+
const lines = [];
|
|
227
|
+
lines.push('## 解析详情');
|
|
228
|
+
lines.push('');
|
|
229
|
+
lines.push(`- 测试框架: \`${framework}\``);
|
|
230
|
+
if (failureType && failureType !== '-') lines.push(`- 失败类型: \`${failureType}\``);
|
|
231
|
+
if (exitCode != null) lines.push(`- 退出码: ${exitCode}`);
|
|
232
|
+
if (p.duration_ms != null) lines.push(`- 耗时: ${p.duration_ms} ms`);
|
|
233
|
+
if (p.skipped != null) lines.push(`- 跳过数: ${p.skipped}`);
|
|
234
|
+
if (p.failures && p.failures.length) {
|
|
235
|
+
lines.push('');
|
|
236
|
+
lines.push('### 失败用例');
|
|
237
|
+
for (const f of p.failures.slice(0, 30)) {
|
|
238
|
+
lines.push(`- ${f.name}${f.file ? ` (${f.file})` : ''}`);
|
|
239
|
+
}
|
|
240
|
+
if (p.failures.length > 30) lines.push(`- 另有 ${p.failures.length - 30} 个失败用例未展开`);
|
|
241
|
+
}
|
|
242
|
+
if (c) {
|
|
243
|
+
lines.push('');
|
|
244
|
+
lines.push('### 覆盖率');
|
|
245
|
+
lines.push(`- 来源: \`${c.format}\``);
|
|
246
|
+
if (c.lines) lines.push(`- 行覆盖率: ${c.lines.pct}% (${c.lines.covered ?? '?'}/${c.lines.total ?? '?'})`);
|
|
247
|
+
if (c.branches) lines.push(`- 分支覆盖率: ${c.branches.pct}% (${c.branches.covered ?? '?'}/${c.branches.total ?? '?'})`);
|
|
248
|
+
if (c.functions) lines.push(`- 函数覆盖率: ${c.functions.pct}% (${c.functions.covered ?? '?'}/${c.functions.total ?? '?'})`);
|
|
249
|
+
if (c.statements) lines.push(`- 语句覆盖率: ${c.statements.pct}% (${c.statements.covered ?? '?'}/${c.statements.total ?? '?'})`);
|
|
250
|
+
}
|
|
251
|
+
return lines.join('\n');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function classifyFailureType({ explicit, status, preflight, exitCode }) {
|
|
255
|
+
if (explicit && explicit !== true) return String(explicit);
|
|
256
|
+
if (status === 'blocked') return 'env_error';
|
|
257
|
+
if (preflight && !preflight.ok) return 'env_error';
|
|
258
|
+
if (status === 'fail' || (exitCode != null && exitCode !== 0)) return 'code_error';
|
|
259
|
+
return '-';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function wantsSplitReport(flags = {}) {
|
|
263
|
+
return flags.splitReport === true
|
|
264
|
+
|| flags['split-report'] === true
|
|
265
|
+
|| flags.rawReport === true
|
|
266
|
+
|| flags['raw-report'] === true;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function runJointPreflight({ backendUrl, frontendUrl }) {
|
|
270
|
+
const out = { mode: 'joint', ok: true, tool: null, checks: [], errors: [] };
|
|
271
|
+
const tool = detectBrowserTool();
|
|
272
|
+
out.tool = tool || null;
|
|
273
|
+
out.checks.push({ name: 'browser-tool', status: tool ? 'pass' : 'blocked', detail: tool || '未发现 agent-browser / chrome-devtools-mcp / playwright' });
|
|
274
|
+
if (!tool) out.errors.push('browser automation tool not found');
|
|
275
|
+
for (const [name, url] of [['backend-url', backendUrl], ['frontend-url', frontendUrl]]) {
|
|
276
|
+
const check = checkUrlReachable(url);
|
|
277
|
+
out.checks.push({ name, ...check });
|
|
278
|
+
if (check.status !== 'pass') out.errors.push(`${name} ${check.detail}`);
|
|
279
|
+
}
|
|
280
|
+
out.ok = out.errors.length === 0;
|
|
281
|
+
return out;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function runRemotePreflight({ apiBaseUrl }) {
|
|
285
|
+
const out = { mode: 'remote', ok: true, tool: 'http', checks: [], errors: [] };
|
|
286
|
+
const check = checkUrlReachable(apiBaseUrl);
|
|
287
|
+
out.checks.push({ name: 'api-base-url', ...check });
|
|
288
|
+
if (check.status !== 'pass') out.errors.push(`api-base-url ${check.detail}`);
|
|
289
|
+
out.ok = out.errors.length === 0;
|
|
290
|
+
return out;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function detectBrowserTool() {
|
|
294
|
+
if (commandExists('agent-browser')) return 'agent-browser';
|
|
295
|
+
if (commandExists('chrome-devtools-mcp')) return 'chrome-devtools-mcp';
|
|
296
|
+
if (commandExists('playwright')) return 'playwright';
|
|
297
|
+
if (commandExists('npx') && spawnSync('npx', ['--no-install', 'playwright', '--version'], { encoding: 'utf8', timeout: 3000 }).status === 0) {
|
|
298
|
+
return 'playwright';
|
|
299
|
+
}
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function commandExists(name) {
|
|
304
|
+
const r = spawnSync('sh', ['-lc', `command -v ${shellQuote(name)}`], { encoding: 'utf8' });
|
|
305
|
+
return r.status === 0;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function shellQuote(s) {
|
|
309
|
+
return `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function checkUrlReachable(url) {
|
|
313
|
+
if (!url) return { status: 'blocked', detail: 'missing url' };
|
|
314
|
+
const cmd = `curl -I -L --max-time 5 -o /dev/null -s -w "%{http_code}" ${shellQuote(url)}`;
|
|
315
|
+
const r = spawnSync('sh', ['-lc', cmd], { encoding: 'utf8', timeout: 7000 });
|
|
316
|
+
if (r.status !== 0) return { status: 'blocked', detail: (r.stderr || r.stdout || 'curl failed').trim() };
|
|
317
|
+
const code = Number((r.stdout || '').trim());
|
|
318
|
+
if (code >= 200 && code < 500) return { status: 'pass', detail: `HTTP ${code}` };
|
|
319
|
+
return { status: 'blocked', detail: `HTTP ${code || 'unknown'}` };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function writePreflightLog(reportsDir, fileName, preflight) {
|
|
323
|
+
const lines = [
|
|
324
|
+
`# ${preflight.mode} preflight`,
|
|
325
|
+
'',
|
|
326
|
+
`- status: ${preflight.ok ? 'pass' : 'blocked'}`,
|
|
327
|
+
`- tool: ${preflight.tool || '-'}`,
|
|
328
|
+
'',
|
|
329
|
+
'| 检查项 | 状态 | 详情 |',
|
|
330
|
+
'| --- | --- | --- |',
|
|
331
|
+
];
|
|
332
|
+
for (const c of preflight.checks || []) lines.push(`| ${c.name} | ${c.status} | ${String(c.detail || '-').replace(/\|/g, '\\|')} |`);
|
|
333
|
+
if (preflight.errors.length) {
|
|
334
|
+
lines.push('', '## 阻塞原因', '');
|
|
335
|
+
for (const e of preflight.errors) lines.push(`- ${e}`);
|
|
336
|
+
}
|
|
337
|
+
await fs.writeFile(path.join(reportsDir, fileName), lines.join('\n') + '\n', 'utf8');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function renderPreflightOutput(preflight) {
|
|
341
|
+
return [
|
|
342
|
+
`mode=${preflight.mode}`,
|
|
343
|
+
`status=${preflight.ok ? 'pass' : 'blocked'}`,
|
|
344
|
+
`tool=${preflight.tool || '-'}`,
|
|
345
|
+
...((preflight.checks || []).map((c) => `${c.name}: ${c.status} (${c.detail || '-'})`)),
|
|
346
|
+
...((preflight.errors || []).map((e) => `error: ${e}`)),
|
|
347
|
+
].join('\n');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function truncate(s, max) {
|
|
351
|
+
const text = String(s || '');
|
|
352
|
+
return text.length > max ? text.slice(0, max) + `\n... [${text.length - max} bytes truncated]` : text;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
module.exports = {
|
|
356
|
+
run,
|
|
357
|
+
_internals: {
|
|
358
|
+
classifyFailureType,
|
|
359
|
+
truncate,
|
|
360
|
+
wantsSplitReport,
|
|
361
|
+
detectBrowserTool,
|
|
362
|
+
checkUrlReachable,
|
|
363
|
+
runJointPreflight,
|
|
364
|
+
runRemotePreflight,
|
|
365
|
+
},
|
|
366
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs/promises');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const log = require('../../utils/log.js');
|
|
5
|
+
const paths = require('../../core/paths.js');
|
|
6
|
+
const markers = require('../../core/markers.js');
|
|
7
|
+
|
|
8
|
+
const TARGETS = [
|
|
9
|
+
'.claude/CLAUDE.md',
|
|
10
|
+
'.cursor/rules/devflow.mdc',
|
|
11
|
+
'AGENTS.md',
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
async function run({ flags = {}, cwd }) {
|
|
15
|
+
const root = cwd || process.cwd();
|
|
16
|
+
for (const rel of TARGETS) {
|
|
17
|
+
const file = path.join(root, rel);
|
|
18
|
+
const removedA = await markers.removeSection(file, 'project-overview');
|
|
19
|
+
const removedB = await markers.removeSection(file, 'workflow');
|
|
20
|
+
if (removedA || removedB) log.ok(`unmarked: ${rel}`);
|
|
21
|
+
}
|
|
22
|
+
if (flags.purge) {
|
|
23
|
+
const dir = paths.devflowDir(root);
|
|
24
|
+
log.warn(`--purge: removing ${dir}`);
|
|
25
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
26
|
+
log.ok(`removed devflow/`);
|
|
27
|
+
} else {
|
|
28
|
+
log.dim('devflow/ kept. add --purge to remove all change/spec/knowledge data.');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = { run };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs/promises');
|
|
3
|
+
const fsSync = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const log = require('../../utils/log.js');
|
|
6
|
+
const markers = require('../../core/markers.js');
|
|
7
|
+
const initCmd = require('./init.js');
|
|
8
|
+
|
|
9
|
+
const IDE_TARGETS = {
|
|
10
|
+
claude: '.claude/CLAUDE.md',
|
|
11
|
+
cursor: '.cursor/rules/devflow.mdc',
|
|
12
|
+
agents: 'AGENTS.md',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* `devflow update`
|
|
17
|
+
* Default mode → idempotent re-init (refreshes marker sections only).
|
|
18
|
+
*
|
|
19
|
+
* `devflow update --ide-clean`
|
|
20
|
+
* Strip every devflow-managed marker section (`workflow`, `project-overview`)
|
|
21
|
+
* from the IDE marker files in the current repo. If the file becomes empty
|
|
22
|
+
* (only whitespace / our markers were the entire file), remove it.
|
|
23
|
+
* Useful after switching from `--ide=full` to `--ide=auto/minimal` —
|
|
24
|
+
* `init` warns about stale markers; this is the cleanup tool.
|
|
25
|
+
*/
|
|
26
|
+
async function run(ctx) {
|
|
27
|
+
const flags = ctx.flags || {};
|
|
28
|
+
if (flags['ide-clean'] === true || flags.ideClean === true) {
|
|
29
|
+
return cleanIdeMarkers(ctx.cwd || process.cwd());
|
|
30
|
+
}
|
|
31
|
+
return initCmd.run(ctx);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function cleanIdeMarkers(root) {
|
|
35
|
+
let cleaned = 0;
|
|
36
|
+
let removed = 0;
|
|
37
|
+
for (const rel of Object.values(IDE_TARGETS)) {
|
|
38
|
+
const file = path.join(root, rel);
|
|
39
|
+
if (!fsSync.existsSync(file)) continue;
|
|
40
|
+
|
|
41
|
+
const beforeSize = (await fs.stat(file)).size;
|
|
42
|
+
const removedWorkflow = await markers.removeSection(file, 'workflow');
|
|
43
|
+
const removedOverview = await markers.removeSection(file, 'project-overview');
|
|
44
|
+
if (!removedWorkflow && !removedOverview) continue;
|
|
45
|
+
cleaned++;
|
|
46
|
+
|
|
47
|
+
// If the file is now effectively empty (whitespace only), drop it. This
|
|
48
|
+
// matters most for files devflow created (.cursor/rules/devflow.mdc) which
|
|
49
|
+
// have no other reason to exist.
|
|
50
|
+
let body;
|
|
51
|
+
try { body = await fs.readFile(file, 'utf8'); } catch { body = ''; }
|
|
52
|
+
if (!body.trim()) {
|
|
53
|
+
try {
|
|
54
|
+
await fs.unlink(file);
|
|
55
|
+
removed++;
|
|
56
|
+
log.ok(`removed empty file: ${rel}`);
|
|
57
|
+
} catch (e) {
|
|
58
|
+
log.warn(`could not remove ${rel}: ${e.message}`);
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
const afterSize = (await fs.stat(file)).size;
|
|
62
|
+
log.ok(`cleaned markers from: ${rel} (${beforeSize}B → ${afterSize}B)`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (cleaned === 0) {
|
|
66
|
+
log.info('no devflow markers found in IDE files; nothing to clean');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
log.raw('');
|
|
70
|
+
log.ok(`cleanup complete: ${cleaned} file(s) touched, ${removed} removed entirely`);
|
|
71
|
+
log.dim(' re-run `devflow init` to regenerate (will follow current --ide mode)');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { run, cleanIdeMarkers };
|