@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.
Files changed (198) hide show
  1. package/CHANGELOG.md +232 -0
  2. package/LICENSE +21 -0
  3. package/README.md +539 -0
  4. package/bin/devflow.js +9 -0
  5. package/docs/RFC-001-devflow-kit.md +617 -0
  6. package/docs/RFC-002-workflow-kernel.md +134 -0
  7. package/docs/enterprise-integration-supplement.md +274 -0
  8. package/docs/internal-gitlab-setup.md +426 -0
  9. package/docs/marketplace-skills.md +231 -0
  10. package/docs/migration-from-arb.md +232 -0
  11. package/docs/tooling-overview.md +774 -0
  12. package/docs/workflow-orchestration.md +695 -0
  13. package/docs/workflow-ui-prototype.html +271 -0
  14. package/package.json +52 -0
  15. package/schemas/config.schema.json +51 -0
  16. package/schemas/delta.schema.json +22 -0
  17. package/schemas/state.schema.json +130 -0
  18. package/schemas/status-surface.schema.json +197 -0
  19. package/schemas/workflow-confirmation-surface.schema.json +70 -0
  20. package/schemas/workflow-picker.schema.json +94 -0
  21. package/scripts/postinstall.js +101 -0
  22. package/scripts/render-workflow-ui-prototype.js +271 -0
  23. package/skills/apply/SKILL.md +313 -0
  24. package/skills/apply/references/discipline-checklist.md +145 -0
  25. package/skills/apply/references/subagent-implementer-prompt.md +113 -0
  26. package/skills/apply/references/subagent-orchestration.md +150 -0
  27. package/skills/apply/references/subagent-reviewer-prompt.md +180 -0
  28. package/skills/apply/references/tdd-loop.md +287 -0
  29. package/skills/apply/references/when-plan-is-wrong.md +279 -0
  30. package/skills/apply/references/worktree-swarm.md +292 -0
  31. package/skills/archive/SKILL.md +229 -0
  32. package/skills/archive/references/conflict-resolution.md +336 -0
  33. package/skills/archive/references/knowledge-deposit.md +381 -0
  34. package/skills/archive/references/spec-merge.md +365 -0
  35. package/skills/brainstorm/SKILL.md +123 -0
  36. package/skills/brainstorm/references/proposal-template.md +244 -0
  37. package/skills/brainstorm/references/question-catalog.md +168 -0
  38. package/skills/brainstorm/references/session-template.md +184 -0
  39. package/skills/ci-fix/SKILL.md +63 -0
  40. package/skills/ci-fix/references/loop.md +25 -0
  41. package/skills/code-review/SKILL.md +279 -0
  42. package/skills/code-review/references/escalation-playbook.md +192 -0
  43. package/skills/code-review/references/language-cheatsheets/go.md +175 -0
  44. package/skills/code-review/references/language-cheatsheets/java-spring-mybatis.md +246 -0
  45. package/skills/code-review/references/language-cheatsheets/python.md +170 -0
  46. package/skills/code-review/references/language-cheatsheets/vue.md +199 -0
  47. package/skills/code-review/references/output-template.md +275 -0
  48. package/skills/code-review/references/review-checklist.md +251 -0
  49. package/skills/complexity-grading/SKILL.md +259 -0
  50. package/skills/deliver/SKILL.md +271 -0
  51. package/skills/deliver/references/delivery-modes.md +299 -0
  52. package/skills/deliver/references/notify.md +359 -0
  53. package/skills/deliver/references/pr-description.md +319 -0
  54. package/skills/dependency-upgrade/SKILL.md +57 -0
  55. package/skills/dependency-upgrade/references/risk-matrix.md +38 -0
  56. package/skills/df-orchestrator/SKILL.md +407 -0
  57. package/skills/df-orchestrator/references/complexity-grading.md +177 -0
  58. package/skills/df-orchestrator/references/escalation-matrix.md +191 -0
  59. package/skills/df-orchestrator/references/routing-rules.md +290 -0
  60. package/skills/df-orchestrator/references/workflow-state-machine.md +208 -0
  61. package/skills/frontend-quality/SKILL.md +61 -0
  62. package/skills/frontend-quality/references/checklist.md +35 -0
  63. package/skills/handoff-resume/SKILL.md +59 -0
  64. package/skills/handoff-resume/references/handoff-template.md +54 -0
  65. package/skills/plan/SKILL.md +166 -0
  66. package/skills/plan/references/task-breakdown.md +207 -0
  67. package/skills/plan/references/task-sequencing.md +143 -0
  68. package/skills/plan/references/task-template.md +248 -0
  69. package/skills/requirement-analysis/SKILL.md +499 -0
  70. package/skills/requirement-analysis/references/acceptance-criteria.md +183 -0
  71. package/skills/requirement-analysis/references/code-recon.md +151 -0
  72. package/skills/requirement-analysis/references/edge-case-catalog.md +164 -0
  73. package/skills/requirement-analysis/references/requirement-template.md +339 -0
  74. package/skills/requirement-analysis/references/scope-negotiation.md +162 -0
  75. package/skills/security-hardening/SKILL.md +60 -0
  76. package/skills/security-hardening/references/checklist.md +42 -0
  77. package/skills/tech-spec/SKILL.md +388 -0
  78. package/skills/tech-spec/references/api-contract-design.md +172 -0
  79. package/skills/tech-spec/references/decision-records.md +110 -0
  80. package/skills/tech-spec/references/design-template.md +301 -0
  81. package/skills/tech-spec/references/rollout-and-rollback.md +203 -0
  82. package/skills/tech-spec/references/spec-delta-conventions.md +250 -0
  83. package/skills/tech-spec/references/transaction-patterns.md +212 -0
  84. package/skills/test-spec/SKILL.md +219 -0
  85. package/skills/test-spec/references/coverage-strategy.md +218 -0
  86. package/skills/test-spec/references/edge-case-to-test.md +143 -0
  87. package/skills/test-spec/references/test-case-template.md +276 -0
  88. package/skills/verify/SKILL.md +232 -0
  89. package/skills/verify/references/nfr-verification.md +292 -0
  90. package/skills/verify/references/report-templates.md +510 -0
  91. package/skills/verify/references/self-test-guide.md +240 -0
  92. package/skills/verify/references/verify-rollback-map.md +247 -0
  93. package/src/cli/commands/_helpers.js +108 -0
  94. package/src/cli/commands/_submit.js +718 -0
  95. package/src/cli/commands/apply.js +198 -0
  96. package/src/cli/commands/archive.js +180 -0
  97. package/src/cli/commands/checkpoint.js +113 -0
  98. package/src/cli/commands/deliver.js +377 -0
  99. package/src/cli/commands/deploy.js +504 -0
  100. package/src/cli/commands/design.js +158 -0
  101. package/src/cli/commands/disable.js +21 -0
  102. package/src/cli/commands/doctor.js +178 -0
  103. package/src/cli/commands/enable.js +21 -0
  104. package/src/cli/commands/flow.js +645 -0
  105. package/src/cli/commands/help.js +93 -0
  106. package/src/cli/commands/ingest.js +602 -0
  107. package/src/cli/commands/init.js +341 -0
  108. package/src/cli/commands/knowledge.js +523 -0
  109. package/src/cli/commands/logs.js +43 -0
  110. package/src/cli/commands/new.js +202 -0
  111. package/src/cli/commands/plan.js +49 -0
  112. package/src/cli/commands/propose.js +27 -0
  113. package/src/cli/commands/provider.js +698 -0
  114. package/src/cli/commands/report.js +143 -0
  115. package/src/cli/commands/requirement.js +227 -0
  116. package/src/cli/commands/review.js +301 -0
  117. package/src/cli/commands/skills.js +457 -0
  118. package/src/cli/commands/status.js +925 -0
  119. package/src/cli/commands/switch.js +27 -0
  120. package/src/cli/commands/sync.js +47 -0
  121. package/src/cli/commands/test.js +366 -0
  122. package/src/cli/commands/uninstall.js +32 -0
  123. package/src/cli/commands/update.js +74 -0
  124. package/src/cli/commands/verify.js +354 -0
  125. package/src/cli/commands/worktree.js +78 -0
  126. package/src/cli/index.js +72 -0
  127. package/src/cli/parse-args.js +102 -0
  128. package/src/core/autodetect.js +271 -0
  129. package/src/core/change.js +208 -0
  130. package/src/core/checkpoint.js +217 -0
  131. package/src/core/config.js +60 -0
  132. package/src/core/delta.js +290 -0
  133. package/src/core/markers.js +59 -0
  134. package/src/core/paths.js +173 -0
  135. package/src/core/plan-tasks.js +36 -0
  136. package/src/core/project-routing.js +285 -0
  137. package/src/core/projects.js +200 -0
  138. package/src/core/state.js +200 -0
  139. package/src/core/workflow-check.js +177 -0
  140. package/src/core/workflow-init.js +34 -0
  141. package/src/core/workflow-picker.js +154 -0
  142. package/src/core/workflow-policy.js +119 -0
  143. package/src/core/workflow-suggest.js +181 -0
  144. package/src/core/workflow-verify.js +88 -0
  145. package/src/core/workflow.js +433 -0
  146. package/src/core/worktree.js +241 -0
  147. package/src/knowledge/categories.js +107 -0
  148. package/src/knowledge/classify.js +125 -0
  149. package/src/knowledge/deposit.js +414 -0
  150. package/src/knowledge/migrate.js +149 -0
  151. package/src/knowledge/mr.js +219 -0
  152. package/src/knowledge/query.js +131 -0
  153. package/src/knowledge/registry.js +151 -0
  154. package/src/knowledge/sync.js +179 -0
  155. package/src/providers/base.js +74 -0
  156. package/src/providers/drivers/api-yapi.js +78 -0
  157. package/src/providers/drivers/ci-jenkins.js +109 -0
  158. package/src/providers/drivers/intake-confluence.js +544 -0
  159. package/src/providers/drivers/kb-git.js +549 -0
  160. package/src/providers/drivers/kb-weknora.js +472 -0
  161. package/src/providers/drivers/notify-smtp.js +515 -0
  162. package/src/providers/drivers/observability-oss.js +43 -0
  163. package/src/providers/drivers/observability-sls.js +50 -0
  164. package/src/providers/lifecycle.js +135 -0
  165. package/src/providers/loader.js +132 -0
  166. package/src/providers/local.js +190 -0
  167. package/src/providers/userconfig.js +283 -0
  168. package/src/reports/aggregate.js +185 -0
  169. package/src/reports/coverage.js +163 -0
  170. package/src/reports/detect.js +143 -0
  171. package/src/reports/parse.js +236 -0
  172. package/src/templates/files/ci/github.yml +38 -0
  173. package/src/templates/files/ci/gitlab.yml +27 -0
  174. package/src/templates/files/design.md +63 -0
  175. package/src/templates/files/ide/devflow-workflow.md +58 -0
  176. package/src/templates/files/ide/project-overview-reference.md +1 -0
  177. package/src/templates/files/ide/project-overview.md +27 -0
  178. package/src/templates/files/knowledge-index.json +17 -0
  179. package/src/templates/files/knowledge.md +28 -0
  180. package/src/templates/files/meta.json +8 -0
  181. package/src/templates/files/plan.md +38 -0
  182. package/src/templates/files/proposal.md +33 -0
  183. package/src/templates/files/reports/contract-test.md +40 -0
  184. package/src/templates/files/reports/e2e-test.md +30 -0
  185. package/src/templates/files/reports/integration-test.md +36 -0
  186. package/src/templates/files/reports/joint-test.md +58 -0
  187. package/src/templates/files/reports/perf.md +24 -0
  188. package/src/templates/files/reports/regression.md +20 -0
  189. package/src/templates/files/reports/remote-test.md +55 -0
  190. package/src/templates/files/reports/self-test.md +43 -0
  191. package/src/templates/files/reports/smoke-test.md +22 -0
  192. package/src/templates/files/reports/unit-test.md +36 -0
  193. package/src/templates/files/requirement.md +51 -0
  194. package/src/templates/files/review.md +38 -0
  195. package/src/templates/files/tests.md +36 -0
  196. package/src/templates/files/verify.md +32 -0
  197. package/src/templates/index.js +21 -0
  198. package/src/utils/log.js +37 -0
@@ -0,0 +1,504 @@
1
+ 'use strict';
2
+ const fsSync = require('fs');
3
+ const path = require('path');
4
+ const cp = require('child_process');
5
+ const readline = require('readline');
6
+ const log = require('../../utils/log.js');
7
+ const change = require('../../core/change.js');
8
+ const state = require('../../core/state.js');
9
+ const projects = require('../../core/projects.js');
10
+ const providers = require('../../providers/loader.js');
11
+ const aggregate = require('../../reports/aggregate.js');
12
+
13
+ async function run({ sub, flags = {}, positional = [], cwd, _providers, _exec, _confirmMerge } = {}) {
14
+ const root = cwd || process.cwd();
15
+ const env = sub || positional[0] || flags.env || flags.environment || 'test';
16
+ const projectName = flags.project || positional[1];
17
+ const project = await projects.findProject(projectName);
18
+ if (!project) {
19
+ log.error(`deploy blocked: project not found: ${projectName || '(missing)'}`);
20
+ process.exitCode = 1;
21
+ return;
22
+ }
23
+ const job = project.jenkins && project.jenkins[env];
24
+ if (!job) {
25
+ log.error(`deploy blocked: no Jenkins job configured for project=${project.name || projectName} env=${env}`);
26
+ process.exitCode = 1;
27
+ return;
28
+ }
29
+
30
+ const ci = _providers && _providers.ci ? _providers.ci : await providers.load(root, { type: 'ci' });
31
+ const params = buildParams({ env, projectName: project.name || projectName, project, job, flags });
32
+ const exec = _exec || spawn;
33
+ const prep = await prepareDeployment({
34
+ root,
35
+ env,
36
+ project,
37
+ projectName: project.name || projectName,
38
+ flags,
39
+ exec,
40
+ confirmMerge: _confirmMerge || confirmMerge,
41
+ });
42
+ if (prep.status === 'fail') {
43
+ log.error(`deploy blocked: ${prep.error}`);
44
+ process.exitCode = 1;
45
+ return;
46
+ }
47
+
48
+ const result = await ci.trigger(job, params);
49
+ let status = { status: 'triggered', url: result.url };
50
+ if (flags.wait === true || flags.wait === 'true') {
51
+ status = await ci.status(result.runId || result.url);
52
+ }
53
+
54
+ const slug = flags.slug || await change.getCurrent(root);
55
+ if (slug) {
56
+ await writeDeployReport({ root, slug, env, projectName: project.name || projectName, job, params, result, status, prep });
57
+ const updateSelfTest = flags.updateSelfTest === true || flags['update-self-test'] === true;
58
+ if (updateSelfTest) {
59
+ await writeSelfTestReport({ root, slug, env, projectName: project.name || projectName, status, result });
60
+ }
61
+ const st = await state.read(root, slug).catch(() => null);
62
+ if (st) {
63
+ st.deployments = st.deployments || [];
64
+ st.deployments.push({
65
+ env,
66
+ project: project.name || projectName,
67
+ job,
68
+ runId: result.runId || null,
69
+ url: result.url || null,
70
+ status: status.status,
71
+ branchPrepared: prep.branch && prep.branch.status === 'done',
72
+ preflight: prep.preflight && prep.preflight.status,
73
+ ts: new Date().toISOString(),
74
+ });
75
+ state.logEvent(st, 'deploy.trigger', { env, project: project.name || projectName, runId: result.runId || null });
76
+ await state.write(root, slug, st);
77
+ }
78
+ }
79
+
80
+ log.ok(`deploy ${env}: ${project.name || projectName} -> ${result.url || result.runId || 'triggered'}`);
81
+ log.dim(`next: devflow test smoke --project=${project.name || projectName} --env=${env}`);
82
+ log.dim('deployment evidence: reports/test-report.md#deploy; add --update-self-test only when you want to refresh self-test section');
83
+ log.dim('next: devflow verify finalize');
84
+ if (isFailureStatus(status.status)) process.exitCode = 1;
85
+ }
86
+
87
+ function buildParams({ env, projectName, project, job, flags }) {
88
+ const configuredParams = configuredDeployParams(project, env);
89
+ const explicitParams = parseCliParams(flags.param || flags.params || flags.parameter);
90
+ const hasExplicitJenkinsParams =
91
+ isUrl(job) ||
92
+ hasParameterizedEndpoint(job) ||
93
+ Object.keys(configuredParams).length > 0 ||
94
+ Object.keys(explicitParams).length > 0;
95
+ const includeDefaults = resolveIncludeDefaultParams(project, env, flags, !hasExplicitJenkinsParams);
96
+ const params = {};
97
+ if (includeDefaults) {
98
+ params.env = env;
99
+ params.project = projectName;
100
+ }
101
+ Object.assign(params, configuredParams);
102
+ if (flags.branch) params.branch = flags.branch;
103
+ if (flags.tag) params.tag = flags.tag;
104
+ if (flags.commit) params.commit = flags.commit;
105
+ if (flags.ref) params.ref = flags.ref;
106
+ Object.assign(params, explicitParams);
107
+ return params;
108
+ }
109
+
110
+ function configuredDeployParams(project, env) {
111
+ const raw = envConfig(project, env, 'params') || envConfig(project, env, 'parameters');
112
+ return normalizeParams(raw);
113
+ }
114
+
115
+ function normalizeParams(raw) {
116
+ if (!raw) return {};
117
+ if (typeof raw === 'object' && !Array.isArray(raw)) return { ...raw };
118
+ return parseParamString(raw);
119
+ }
120
+
121
+ function parseCliParams(raw) {
122
+ if (Array.isArray(raw)) {
123
+ return raw.reduce((acc, item) => ({ ...acc, ...parseParamString(item) }), {});
124
+ }
125
+ return parseParamString(raw);
126
+ }
127
+
128
+ function parseParamString(raw) {
129
+ if (!raw || raw === true) return {};
130
+ const text = String(raw).trim();
131
+ if (!text) return {};
132
+ if (text.startsWith('{')) {
133
+ try {
134
+ const parsed = JSON.parse(text);
135
+ return normalizeParams(parsed);
136
+ } catch {
137
+ return {};
138
+ }
139
+ }
140
+ const out = {};
141
+ for (const part of text.split(/[,&]/)) {
142
+ const eq = part.indexOf('=');
143
+ if (eq === -1) continue;
144
+ const key = part.slice(0, eq).trim();
145
+ const value = part.slice(eq + 1).trim();
146
+ if (key) out[key] = value;
147
+ }
148
+ return out;
149
+ }
150
+
151
+ function resolveIncludeDefaultParams(project, env, flags, fallback) {
152
+ const raw = flags.includeDefaultParams !== undefined
153
+ ? flags.includeDefaultParams
154
+ : flags.defaultParams !== undefined
155
+ ? flags.defaultParams
156
+ : envConfig(project, env, 'includeDefaultParams') !== undefined
157
+ ? envConfig(project, env, 'includeDefaultParams')
158
+ : envConfig(project, env, 'defaultParams');
159
+ if (raw === undefined) return fallback;
160
+ return !(raw === false || raw === 'false' || raw === '0' || raw === 'no');
161
+ }
162
+
163
+ function hasParameterizedEndpoint(job) {
164
+ if (!isUrl(job)) return false;
165
+ try {
166
+ const u = new URL(String(job));
167
+ return /\/buildWithParameters\/?$/i.test(u.pathname);
168
+ } catch {
169
+ return /\/buildWithParameters(?:\?|$)/i.test(String(job || ''));
170
+ }
171
+ }
172
+
173
+ function isUrl(value) {
174
+ return /^https?:\/\//i.test(String(value || ''));
175
+ }
176
+
177
+ async function prepareDeployment({ root, env, project, projectName, flags, exec, confirmMerge }) {
178
+ const repoDir = path.resolve(flags.repo || flags.repoDir || flags['repo-dir'] || project.localPath || project.worktreePath || root);
179
+ const out = { status: 'ok', repoDir, branch: null, preflight: null };
180
+
181
+ const explicitMergeTo = flags['merge-to'] || flags.mergeTo || envConfig(project, env, 'mergeTo') || envConfig(project, env, 'branch');
182
+ const defaultTarget = env === 'test' ? 'test' : null;
183
+ const mergeTo = explicitMergeTo || defaultTarget;
184
+ if (mergeTo) {
185
+ const sourceBranch = flags.branch || flags.sourceBranch || currentBranch(repoDir, exec);
186
+ if (!sourceBranch) {
187
+ if (explicitMergeTo) return { ...out, status: 'fail', error: 'cannot determine source branch for --merge-to' };
188
+ } else if (sourceBranch === String(mergeTo)) {
189
+ out.branch = { status: 'already_on_target', source: sourceBranch, target: String(mergeTo), push: false };
190
+ } else if (!explicitMergeTo) {
191
+ const ok = await confirmMerge({
192
+ projectName,
193
+ env,
194
+ repoDir,
195
+ sourceBranch,
196
+ targetBranch: String(mergeTo),
197
+ });
198
+ if (!ok) {
199
+ return {
200
+ ...out,
201
+ status: 'fail',
202
+ error: `current branch is ${sourceBranch}; merge to ${mergeTo} was not confirmed`,
203
+ branch: { status: 'declined', source: sourceBranch, target: String(mergeTo), push: false },
204
+ };
205
+ }
206
+ out.branch = runMergeCommands({ repoDir, sourceBranch, mergeTo, flags, exec });
207
+ if (out.branch.status === 'fail') return { ...out, status: 'fail', error: out.branch.error };
208
+ } else {
209
+ out.branch = runMergeCommands({ repoDir, sourceBranch, mergeTo, flags, exec });
210
+ if (out.branch.status === 'fail') return { ...out, status: 'fail', error: out.branch.error };
211
+ }
212
+ }
213
+
214
+ const preflight = resolvePreflightCommand(project, env, flags, repoDir);
215
+ if (preflight) {
216
+ const r = exec('sh', ['-c', preflight], { cwd: repoDir, encoding: 'utf8', shell: false });
217
+ out.preflight = {
218
+ status: r.status === 0 ? 'pass' : 'fail',
219
+ command: preflight,
220
+ output: truncate(((r.stdout || '') + (r.stderr || '')).trim(), 4000),
221
+ };
222
+ if (r.status !== 0) {
223
+ return {
224
+ ...out,
225
+ status: 'fail',
226
+ error: `preflight failed: ${preflight}`,
227
+ };
228
+ }
229
+ }
230
+
231
+ return out;
232
+ }
233
+
234
+ function runMergeCommands({ repoDir, sourceBranch, mergeTo, flags, exec }) {
235
+ const pushEnabled = !(flags['no-push'] === true || flags.noPush === true || flags.push === false || flags.push === 'false');
236
+ const commands = [
237
+ ['git', ['fetch', 'origin']],
238
+ ['git', ['checkout', String(mergeTo)]],
239
+ ['git', ['pull', 'origin', String(mergeTo)]],
240
+ ['git', ['merge', String(sourceBranch), '--no-edit']],
241
+ ];
242
+ if (pushEnabled) commands.push(['git', ['push', 'origin', String(mergeTo)]]);
243
+ commands.push(['git', ['checkout', String(sourceBranch)]]);
244
+ for (const [cmd, args] of commands) {
245
+ const r = exec(cmd, args, { cwd: repoDir, encoding: 'utf8' });
246
+ if (r.status !== 0) {
247
+ return {
248
+ status: 'fail',
249
+ error: `${cmd} ${args.join(' ')} failed${r.stderr ? `: ${String(r.stderr).trim()}` : ''}`,
250
+ source: sourceBranch,
251
+ target: String(mergeTo),
252
+ push: pushEnabled,
253
+ };
254
+ }
255
+ }
256
+ return { status: 'done', source: sourceBranch, target: String(mergeTo), push: pushEnabled };
257
+ }
258
+
259
+ function spawn(cmd, args, opts = {}) {
260
+ return cp.spawnSync(cmd, args, { encoding: 'utf8', ...opts });
261
+ }
262
+
263
+ function envConfig(project, env, key) {
264
+ const deploy = project && project.deploy;
265
+ if (!deploy) return undefined;
266
+ if (deploy[env] && deploy[env][key] !== undefined) return deploy[env][key];
267
+ return deploy[key];
268
+ }
269
+
270
+ function currentBranch(repoDir, exec) {
271
+ const r = exec('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: repoDir, encoding: 'utf8' });
272
+ if (r.status !== 0) return null;
273
+ const branch = String(r.stdout || '').trim();
274
+ return branch && branch !== 'HEAD' ? branch : null;
275
+ }
276
+
277
+ async function confirmMerge({ projectName, env, repoDir, sourceBranch, targetBranch }) {
278
+ if (!process.stdin.isTTY || !process.stdout.isTTY || process.env.CI) {
279
+ log.error(`deploy ${env}: current branch is ${sourceBranch}, Jenkins target branch is ${targetBranch}.`);
280
+ log.dim(`rerun with: devflow deploy ${env} --project=${projectName} --merge-to=${targetBranch}`);
281
+ return false;
282
+ }
283
+ log.warn(`current branch is ${sourceBranch}; Jenkins deploy should use ${targetBranch}.`);
284
+ log.dim(`repo: ${repoDir}`);
285
+ const answer = await ask(`merge ${sourceBranch} -> ${targetBranch}, push, then trigger Jenkins? [Y/n] `, 'Y');
286
+ const lc = answer.trim().toLowerCase();
287
+ return lc === '' || lc === 'y' || lc === 'yes';
288
+ }
289
+
290
+ function ask(q, def = '') {
291
+ return new Promise((resolve) => {
292
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
293
+ rl.question(q, (answer) => {
294
+ rl.close();
295
+ resolve(answer || def);
296
+ });
297
+ });
298
+ }
299
+
300
+ function resolvePreflightCommand(project, env, flags, repoDir) {
301
+ if (flags['skip-preflight'] === true || flags.skipPreflight === true) return null;
302
+ if (flags.preflight && flags.preflight !== true) return String(flags.preflight);
303
+ if (flags['preflight-cmd']) return String(flags['preflight-cmd']);
304
+ const configured = envConfig(project, env, 'preflight') || envConfig(project, env, 'preflightCommand');
305
+ if (configured) return String(configured);
306
+ if (flags.preflight === true || flags.preflight === 'true') return autoPreflightCommand(repoDir);
307
+ return null;
308
+ }
309
+
310
+ function autoPreflightCommand(repoDir) {
311
+ if (fsSync.existsSync(path.join(repoDir, 'go.mod'))) return 'go build ./...';
312
+ if (fsSync.existsSync(path.join(repoDir, 'pom.xml'))) return 'mvn compile -q -DskipTests';
313
+ if (fsSync.existsSync(path.join(repoDir, 'setup.py')) || fsSync.existsSync(path.join(repoDir, 'pyproject.toml'))) return 'python -m compileall .';
314
+ const pkg = path.join(repoDir, 'package.json');
315
+ if (fsSync.existsSync(pkg)) {
316
+ try {
317
+ const json = JSON.parse(fsSync.readFileSync(pkg, 'utf8'));
318
+ if (json.scripts && json.scripts.build) return 'npm run build';
319
+ } catch { /* ignore */ }
320
+ }
321
+ return null;
322
+ }
323
+
324
+ async function writeDeployReport({ root, slug, env, projectName, job, params, result, status, prep }) {
325
+ const reportsDir = await change.ensureSubdir(root, slug, 'reports');
326
+ const body = [
327
+ '---',
328
+ `slug: ${slug}`,
329
+ 'kind: deploy',
330
+ `env: ${env}`,
331
+ `project: ${projectName}`,
332
+ `status: ${status.status || 'triggered'}`,
333
+ `ts: ${new Date().toISOString()}`,
334
+ '---',
335
+ '',
336
+ `# 部署报告(${env})`,
337
+ '',
338
+ '| 项 | 值 |',
339
+ '| --- | --- |',
340
+ `| 项目 | ${projectName} |`,
341
+ `| 环境 | ${env} |`,
342
+ `| Jenkins job | ${job} |`,
343
+ `| runId | ${result.runId || '-'} |`,
344
+ `| URL | ${result.url || '-'} |`,
345
+ `| 状态 | ${status.status || 'triggered'} |`,
346
+ `| 代码库 | ${(prep && prep.repoDir) || '-'} |`,
347
+ '',
348
+ '## 参数',
349
+ '',
350
+ '```json',
351
+ JSON.stringify(params, null, 2),
352
+ '```',
353
+ '',
354
+ '## 分支准备',
355
+ '',
356
+ renderBranchPrep(prep),
357
+ '',
358
+ '## 本地编译预检',
359
+ '',
360
+ renderPreflight(prep),
361
+ '',
362
+ ].join('\n');
363
+ await aggregate.upsertReport({
364
+ reportsDir,
365
+ slug,
366
+ title: slug,
367
+ kind: 'deploy',
368
+ body,
369
+ meta: { status: normalizeDeployStatus(status.status) },
370
+ });
371
+ }
372
+
373
+ async function writeSelfTestReport({ root, slug, env, projectName, status, result }) {
374
+ const reportsDir = await change.ensureSubdir(root, slug, 'reports');
375
+ const deployReport = 'reports/test-report.md#deploy';
376
+ const reportStatus = {
377
+ unit: readReportStatus(reportsDir, 'unit-test.md'),
378
+ integration: readReportStatus(reportsDir, 'integration-test.md'),
379
+ e2e: readReportStatus(reportsDir, 'e2e-test.md'),
380
+ joint: readReportStatus(reportsDir, 'joint-test.md'),
381
+ remote: readReportStatus(reportsDir, 'remote-test.md'),
382
+ smoke: readReportStatus(reportsDir, 'smoke-test.md'),
383
+ };
384
+ const selfStatus = deploymentSelfTestStatus(status.status, reportStatus.smoke);
385
+ const body = [
386
+ '---',
387
+ `slug: ${slug}`,
388
+ 'kind: self-test',
389
+ `status: ${selfStatus}`,
390
+ `env: ${env}`,
391
+ `project: ${projectName}`,
392
+ `ts: ${new Date().toISOString()}`,
393
+ '---',
394
+ '',
395
+ `# 提测自测报告 - ${projectName}`,
396
+ '',
397
+ '## 部署证据',
398
+ '',
399
+ '| 项 | 值 |',
400
+ '| --- | --- |',
401
+ `| 环境 | ${env} |`,
402
+ `| 部署报告 | ${deployReport} |`,
403
+ `| Jenkins | ${result.url || result.runId || '-'} |`,
404
+ `| 部署状态 | ${status.status || 'triggered'} |`,
405
+ '',
406
+ '## 测试覆盖',
407
+ '',
408
+ '| 类型 | 报告 | 状态 |',
409
+ '| --- | --- | --- |',
410
+ `| 单元测试 | reports/test-report.md#unit | ${reportStatus.unit} |`,
411
+ `| 集成测试 | reports/test-report.md#integration | ${reportStatus.integration} |`,
412
+ `| 端到端测试 | reports/test-report.md#e2e | ${reportStatus.e2e} |`,
413
+ `| 联调测试 | reports/test-report.md#joint | ${reportStatus.joint} |`,
414
+ `| 远程 API | reports/test-report.md#remote | ${reportStatus.remote} |`,
415
+ `| 冒烟测试 | reports/test-report.md#smoke | ${reportStatus.smoke} |`,
416
+ '',
417
+ '## 结论',
418
+ '',
419
+ selfStatus === 'pass'
420
+ ? '- 已完成部署并通过部署后自测证据,可提测。'
421
+ : '- 部署后自测证据未完全通过,请补齐 smoke/API/联调报告后再提测。',
422
+ '',
423
+ ].join('\n');
424
+ await aggregate.upsertReport({
425
+ reportsDir,
426
+ slug,
427
+ title: slug,
428
+ kind: 'self-test',
429
+ body,
430
+ meta: { status: selfStatus },
431
+ });
432
+ }
433
+
434
+ function renderBranchPrep(prep) {
435
+ if (!prep || !prep.branch) return '- 未启用。使用 `--merge-to=<branch>` 或项目 deploy 配置开启。';
436
+ const b = prep.branch;
437
+ return [
438
+ `- 状态: ${b.status}`,
439
+ `- 分支: ${b.source} -> ${b.target}`,
440
+ `- 是否推送: ${b.push ? '是' : '否'}`,
441
+ ].join('\n');
442
+ }
443
+
444
+ function renderPreflight(prep) {
445
+ if (!prep || !prep.preflight) return '- 未启用。使用 `--preflight`、`--preflight-cmd` 或项目 deploy 配置开启。';
446
+ return [
447
+ `- 状态: ${prep.preflight.status}`,
448
+ `- 命令: \`${prep.preflight.command}\``,
449
+ '',
450
+ '```',
451
+ prep.preflight.output || '',
452
+ '```',
453
+ ].join('\n');
454
+ }
455
+
456
+ function readReportStatus(reportsDir, name) {
457
+ const file = path.join(reportsDir, name);
458
+ if (!fsSync.existsSync(file)) {
459
+ const agg = aggregate.readAggregate(reportsDir);
460
+ const kind = name.replace(/-test\.md$/, '');
461
+ const found = agg.reports.find((r) => r.kind === kind);
462
+ return found ? found.status : 'missing';
463
+ }
464
+ const content = fsSync.readFileSync(file, 'utf8');
465
+ const m = content.match(/^status:\s*([^\s]+)/m);
466
+ return m ? m[1] : 'present';
467
+ }
468
+
469
+ function deploymentSelfTestStatus(deployStatus, smokeStatus) {
470
+ if (isFailureStatus(deployStatus) || smokeStatus === 'fail') return 'fail';
471
+ if (isSuccessStatus(deployStatus) && smokeStatus === 'pass') return 'pass';
472
+ return 'draft';
473
+ }
474
+
475
+ function normalizeDeployStatus(status) {
476
+ if (isFailureStatus(status)) return 'fail';
477
+ if (status === 'success' || status === 'pass' || status === 'completed') return 'pass';
478
+ return 'pending';
479
+ }
480
+
481
+ function isSuccessStatus(s) {
482
+ return ['success', 'succeeded', 'pass', 'passed', 'completed', 'done'].includes(String(s || '').toLowerCase());
483
+ }
484
+
485
+ function isFailureStatus(s) {
486
+ return ['fail', 'failed', 'failure', 'error', 'aborted', 'cancelled', 'canceled'].includes(String(s || '').toLowerCase());
487
+ }
488
+
489
+ function truncate(s, max) {
490
+ const text = String(s || '');
491
+ return text.length > max ? text.slice(0, max) + `\n... [${text.length - max} bytes truncated]` : text;
492
+ }
493
+
494
+ module.exports = {
495
+ run,
496
+ _internals: {
497
+ buildParams,
498
+ prepareDeployment,
499
+ writeDeployReport,
500
+ writeSelfTestReport,
501
+ deploymentSelfTestStatus,
502
+ autoPreflightCommand,
503
+ },
504
+ };
@@ -0,0 +1,158 @@
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 state = require('../../core/state.js');
7
+ const change = require('../../core/change.js');
8
+ const checkpoint = require('../../core/checkpoint.js');
9
+ const helpers = require('./_helpers.js');
10
+
11
+ async function run({ flags = {}, positional = [], cwd }) {
12
+ const root = cwd || process.cwd();
13
+ const slug = await helpers.resolveSlug(root, flags, positional);
14
+ if (!slug) { process.exitCode = 1; return; }
15
+ const st = await helpers.loadStateOrFail(root, slug);
16
+ if (!st) { process.exitCode = 1; return; }
17
+
18
+ if (st.mode === 'micro' || st.level === 'L0') {
19
+ log.error('design skipped: this change is L0 micro mode. Upgrade to L1 before running tech-spec.');
20
+ log.dim(`run: devflow status set-level L1 --slug=${slug} --reason="micro scope expanded"`);
21
+ process.exitCode = 1;
22
+ return;
23
+ }
24
+ if (helpers.blockWorkflowStep(root, st, slug, 'design', 'design')) return;
25
+
26
+ if (await blockPendingCheckpoint(root, slug, st, flags, ['problem_card', 'project_confirmation'], 'design')) return;
27
+
28
+ await helpers.writePhaseArtifact(root, slug, st, 'design.md', 'design.md', {});
29
+
30
+ const wantsTests = flags['with-tests'] || flags.withTests || st.level === 'L3';
31
+ if (wantsTests) {
32
+ await helpers.writePhaseArtifact(root, slug, st, 'tests.md', 'tests.md', {});
33
+ st.enabled = st.enabled || {};
34
+ st.enabled['tests.md'] = true;
35
+ }
36
+
37
+ // Spec delta is useful only when this change updates long-lived capabilities.
38
+ // Keep it explicit so simple changes do not get an empty delta folder.
39
+ const wantsSpec = !!(flags['with-spec'] || flags.withSpec);
40
+ const skipDelta = !!(flags['no-delta'] || flags.noDelta);
41
+ if (wantsSpec && !skipDelta) {
42
+ const deltaDir = await change.ensureSubdir(root, slug, 'delta');
43
+ const sample = path.join(deltaDir, 'sample-capability.md');
44
+ if (!(await fileExists(sample))) {
45
+ await fs.writeFile(sample, sampleDelta(slug), 'utf8');
46
+ log.ok(`delta/sample-capability.md — rename & fill for each changed capability`);
47
+ }
48
+ st.enabled = st.enabled || {};
49
+ st.enabled['delta'] = true;
50
+ }
51
+
52
+ state.setPhase(st, 'requirement', 'completed');
53
+ state.setPhase(st, 'design', 'in_progress');
54
+ helpers.completeWorkflowStep(st, 'design');
55
+ state.logEvent(st, 'design scaffold');
56
+ await state.write(root, slug, st);
57
+ log.dim('next: devflow plan');
58
+ }
59
+
60
+ async function blockPendingCheckpoint(root, slug, st, flags, types, commandName) {
61
+ const pending = checkpoint.latestPendingByTypes(st, types);
62
+ if (!pending) return false;
63
+ if (!flags.force) {
64
+ log.error(`${commandName} blocked: pending ${pending.type} checkpoint.`);
65
+ log.raw('');
66
+ log.raw(checkpoint.renderNextStepCard(pending));
67
+ log.dim(`override: devflow ${commandName} --force --reason="..."`);
68
+ process.exitCode = 1;
69
+ return true;
70
+ }
71
+ if (!flags.reason) {
72
+ log.error(`${commandName} --force requires --reason`);
73
+ process.exitCode = 2;
74
+ return true;
75
+ }
76
+ state.logEvent(st, `${commandName}.force_bypass_checkpoint`, {
77
+ checkpoint: pending.id,
78
+ type: pending.type,
79
+ reason: flags.reason,
80
+ });
81
+ await state.write(root, slug, st);
82
+ log.warn(`${commandName}: force-bypassed pending ${pending.type} checkpoint`);
83
+ return false;
84
+ }
85
+
86
+ async function fileExists(f) { try { await fs.access(f); return true; } catch { return false; } }
87
+
88
+ function sampleDelta(slug) {
89
+ return `---
90
+ slug: ${slug}
91
+ capability: <domain>/<capability-name>
92
+ ---
93
+
94
+ # delta: ADDED specs/<domain>/spec.md
95
+
96
+ > **OpenSpec 风格 delta** — \`devflow archive\` 时按 \`capability\` 的业务域落盘到对应 spec 文件。
97
+ >
98
+ > 使用步骤:
99
+ > 1. 修改 frontmatter \`capability\` 为 \`<业务域>/<能力名>\`
100
+ > - 例:\`coupon/grant\` → 落盘到 \`specs/coupon/spec.md\`
101
+ > - 例:\`order/create\` → 落盘到 \`specs/order/spec.md\`
102
+ > 2. 重命名本文件为 \`<capability-name>.md\`(便于阅读,不影响落盘路径)
103
+ > 3. 填写 ADDED / MODIFIED / REMOVED Requirements(只保留本次涉及的块)
104
+ > 4. MODIFIED / REMOVED 的 \`### heading\` 必须与目标 spec **一字不差**
105
+
106
+ ## ADDED Requirements
107
+
108
+ ### Requirement: <业务能力名>
109
+
110
+ 系统 SHALL <描述该业务能力对外承诺>。
111
+
112
+ 1. 系统 MUST <约束 1>
113
+ 2. 系统 MUST <约束 2>
114
+ 3. 系统 SHOULD <约束 3>
115
+
116
+ #### Scenario: <场景名>
117
+
118
+ - **WHEN** <触发条件>
119
+ - **THEN** 系统 MUST <结果>
120
+ - **AND** <补充断言>
121
+
122
+ ---
123
+
124
+ ## MODIFIED Requirements
125
+
126
+ <!-- 若本次无修改,删除整块 -->
127
+ ### Requirement: <既有业务能力名>
128
+
129
+ <!-- heading 必须与 specs/<domain>/spec.md 中的 ### 完全一致,否则 archive 会报 delta_conflict -->
130
+ <!-- 改动示例 -->
131
+ - ~~旧描述 / 旧约束~~
132
+ - 新描述 / 新约束
133
+
134
+ ---
135
+
136
+ ## REMOVED Requirements
137
+
138
+ <!-- 若本次无删除,删除整块 -->
139
+ ### Requirement: <废弃业务能力名>
140
+
141
+ 下线原因:...
142
+ 下线时间:YYYY-QX
143
+
144
+ ---
145
+
146
+ ## REJECTED Alternatives
147
+
148
+ <!-- 记录否决的备选方案,帮助后人理解决策背景 -->
149
+ ### 备选方案名称
150
+
151
+ **否决原因**:
152
+ - 理由 1
153
+ - 理由 2
154
+
155
+ `;
156
+ }
157
+
158
+ module.exports = { run };
@@ -0,0 +1,21 @@
1
+ 'use strict';
2
+ const log = require('../../utils/log.js');
3
+ const state = require('../../core/state.js');
4
+ const helpers = require('./_helpers.js');
5
+
6
+ async function run({ flags = {}, positional = [], cwd }) {
7
+ const root = cwd || process.cwd();
8
+ const feature = positional[0];
9
+ if (!feature) { log.error('usage: devflow disable <feature>'); process.exitCode = 2; return; }
10
+ const slug = await helpers.resolveSlug(root, flags, positional.slice(1));
11
+ if (!slug) { process.exitCode = 1; return; }
12
+ const st = await helpers.loadStateOrFail(root, slug);
13
+ if (!st) { process.exitCode = 1; return; }
14
+ st.enabled = st.enabled || {};
15
+ st.enabled[feature] = false;
16
+ state.logEvent(st, `disable ${feature}`);
17
+ await state.write(root, slug, st);
18
+ log.ok(`disabled: ${feature}`);
19
+ }
20
+
21
+ module.exports = { run };