@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,135 @@
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 paths = require('../core/paths.js');
7
+ const loader = require('./loader.js');
8
+ const { AuthExpiredError } = require('./base.js');
9
+
10
+ /**
11
+ * Provider lifecycle helpers used by callers (devflow ingest / deliver / sync / ...).
12
+ *
13
+ * Provides:
14
+ * loadOrAuth({ name, type, interactive }) → provider, auto-running login if needed
15
+ * withAutoRetry(provider, fn) → run fn(); on AuthExpiredError, login + retry once
16
+ * securityCheck(root) → audit credential files & config
17
+ *
18
+ * Notes:
19
+ * - We never log token contents.
20
+ * - We chmod 600 every providers.json after writing.
21
+ * - We prefer ${env:NAME} substitution to plain-text values; securityCheck warns
22
+ * when a string field contains anything that looks like a token literal.
23
+ */
24
+
25
+ async function loadOrAuth(root, opts = {}) {
26
+ const { name, type, interactive = isTty() } = opts;
27
+ let provider;
28
+ try {
29
+ provider = await loader.load(root, { name, type });
30
+ } catch (e) {
31
+ if (e.code === 'PROVIDER_NOT_INSTALLED' && interactive) {
32
+ log.warn(e.message);
33
+ throw e; // caller decides whether to invoke `devflow provider add`
34
+ }
35
+ throw e;
36
+ }
37
+ const v = await provider.validate();
38
+ if (v.ok) return provider;
39
+
40
+ if (v.needsLogin && typeof provider.login === 'function') {
41
+ log.warn(`provider ${provider.name}: ${v.reason || 'auth required'} — running login`);
42
+ await provider.login({ force: false, interactive });
43
+ const v2 = await provider.validate();
44
+ if (!v2.ok) throw new Error(`${provider.name}: still not ok after login: ${v2.reason}`);
45
+ return provider;
46
+ }
47
+ throw new Error(`${provider.name}: not usable: ${v.reason || 'unknown'}`);
48
+ }
49
+
50
+ async function withAutoRetry(provider, fn, { interactive = isTty() } = {}) {
51
+ try {
52
+ return await fn(provider);
53
+ } catch (e) {
54
+ if (e instanceof AuthExpiredError && typeof provider.login === 'function') {
55
+ log.warn(`${provider.name}: ${e.message} — re-login + retry`);
56
+ await provider.login({ force: true, interactive });
57
+ return await fn(provider);
58
+ }
59
+ throw e;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Audit credential files + provider configs.
65
+ *
66
+ * Returns: [{ severity: 'info|warn|error', file, message }]
67
+ */
68
+ async function securityCheck(root) {
69
+ const findings = [];
70
+ const files = [paths.projectProvidersFile(root), paths.userProvidersFile()];
71
+ for (const file of files) {
72
+ if (!fsSync.existsSync(file)) continue;
73
+ try {
74
+ const stat = await fs.stat(file);
75
+ const mode = stat.mode & 0o777;
76
+ if (mode & 0o077) {
77
+ findings.push({
78
+ severity: 'warn',
79
+ file,
80
+ message: `permissive mode 0${mode.toString(8)} — should be 0600`,
81
+ fix: 'chmod600',
82
+ });
83
+ }
84
+ } catch { /* ignore */ }
85
+ try {
86
+ const text = await fs.readFile(file, 'utf8');
87
+ const json = JSON.parse(text);
88
+ for (const name of Object.keys(json)) {
89
+ const cfg = json[name].config || {};
90
+ const issues = scanConfigForSecrets(cfg, []);
91
+ for (const issue of issues) {
92
+ findings.push({
93
+ severity: 'warn',
94
+ file,
95
+ message: `${name}.config.${issue.path} looks like a plaintext secret — use \${env:VAR}`,
96
+ });
97
+ }
98
+ }
99
+ } catch { /* invalid JSON handled elsewhere */ }
100
+ }
101
+ return findings;
102
+ }
103
+
104
+ const SECRET_KEYS = /(token|secret|password|pwd|key|auth|cookie|credential|passcode)/i;
105
+
106
+ function scanConfigForSecrets(obj, parentPath) {
107
+ const out = [];
108
+ if (!obj || typeof obj !== 'object') return out;
109
+ for (const k of Object.keys(obj)) {
110
+ const v = obj[k];
111
+ const here = [...parentPath, k];
112
+ if (v && typeof v === 'object') {
113
+ out.push(...scanConfigForSecrets(v, here));
114
+ continue;
115
+ }
116
+ if (typeof v !== 'string' || v === '') continue;
117
+ if (!SECRET_KEYS.test(k)) continue;
118
+ if (v.startsWith('${env:')) continue; // OK — env-substituted
119
+ if (v.length < 8) continue; // not really a secret
120
+ out.push({ path: here.join('.') });
121
+ }
122
+ return out;
123
+ }
124
+
125
+ async function applySecureMode(file) {
126
+ try { await fs.chmod(file, 0o600); } catch { /* ignore on win */ }
127
+ }
128
+
129
+ function isTty() {
130
+ return !!(process.stdout && process.stdout.isTTY);
131
+ }
132
+
133
+ module.exports = {
134
+ loadOrAuth, withAutoRetry, securityCheck, applySecureMode, scanConfigForSecrets,
135
+ };
@@ -0,0 +1,132 @@
1
+ 'use strict';
2
+ const fs = require('fs/promises');
3
+ const fsSync = require('fs');
4
+ const path = require('path');
5
+ const paths = require('../core/paths.js');
6
+ const local = require('./local.js');
7
+
8
+ /**
9
+ * Provider registry & loader.
10
+ *
11
+ * Resolution order for a given type:
12
+ * 1. project-level providers.json
13
+ * 2. user-level providers.json (~/.devflow/providers.json)
14
+ * 3. built-in local fallback
15
+ *
16
+ * Each entry shape:
17
+ * {
18
+ * "intake.confluence": {
19
+ * "type": "intake",
20
+ * "driver": "confluence",
21
+ * "config": { "baseUrl": "...", "token": "...", "user": "..." }
22
+ * }
23
+ * }
24
+ */
25
+
26
+ const BUILTIN_LOCALS = {
27
+ intake: () => new local.LocalIntakeProvider(),
28
+ issue: () => new local.LocalIssueProvider(),
29
+ kb: () => new local.LocalKbProvider(),
30
+ notify: () => new local.LocalNotifyProvider(),
31
+ ci: () => new local.LocalCiProvider(),
32
+ vcs: () => new local.LocalVcsProvider(),
33
+ };
34
+
35
+ async function readJson(file) {
36
+ if (!fsSync.existsSync(file)) return {};
37
+ try {
38
+ return JSON.parse(await fs.readFile(file, 'utf8'));
39
+ } catch (e) {
40
+ return {};
41
+ }
42
+ }
43
+
44
+ async function readAllConfigs(root) {
45
+ const project = await readJson(paths.projectProvidersFile(root));
46
+ const legacyUser = paths.legacyUserProvidersFile ? await readJson(paths.legacyUserProvidersFile()) : {};
47
+ const user = { ...legacyUser, ...(await readJson(paths.userProvidersFile())) };
48
+ return { project, user };
49
+ }
50
+
51
+ function resolveValue(v) {
52
+ if (typeof v !== 'string') return v;
53
+ return v.replace(/\$\{env:([A-Z0-9_]+)\}/g, (_, k) => process.env[k] || '');
54
+ }
55
+
56
+ function resolveConfigEnvs(cfg) {
57
+ if (!cfg || typeof cfg !== 'object') return cfg;
58
+ const out = Array.isArray(cfg) ? [] : {};
59
+ for (const k of Object.keys(cfg)) {
60
+ const v = cfg[k];
61
+ if (v && typeof v === 'object') out[k] = resolveConfigEnvs(v);
62
+ else out[k] = resolveValue(v);
63
+ }
64
+ return out;
65
+ }
66
+
67
+ async function listConfigured(root) {
68
+ const { project, user } = await readAllConfigs(root);
69
+ const merged = { ...user, ...project };
70
+ return Object.keys(merged).map((name) => ({
71
+ name,
72
+ type: merged[name].type,
73
+ driver: merged[name].driver,
74
+ source: project[name] ? 'project' : 'user',
75
+ }));
76
+ }
77
+
78
+ /**
79
+ * Load a provider instance.
80
+ * - if name given, load that specific provider config and instantiate via driver
81
+ * - if only type given, return first configured matching one, else local fallback
82
+ */
83
+ async function load(root, { name, type } = {}) {
84
+ const { project, user } = await readAllConfigs(root);
85
+ const merged = { ...user, ...project };
86
+
87
+ let entry;
88
+ let resolvedName;
89
+ if (name) {
90
+ entry = merged[name];
91
+ if (!entry) throw new Error(`provider not configured: ${name}`);
92
+ resolvedName = name;
93
+ } else if (type) {
94
+ const found = Object.keys(merged).find((k) => merged[k].type === type);
95
+ if (found) {
96
+ entry = merged[found];
97
+ resolvedName = found;
98
+ }
99
+ }
100
+
101
+ if (!entry) {
102
+ if (!type) throw new Error('load() requires { name } or { type }');
103
+ const builtin = BUILTIN_LOCALS[type];
104
+ if (!builtin) throw new Error(`no built-in local provider for type: ${type}`);
105
+ return builtin();
106
+ }
107
+
108
+ const driver = entry.driver;
109
+ const cfg = resolveConfigEnvs(entry.config || {});
110
+
111
+ if (driver === 'local') {
112
+ return BUILTIN_LOCALS[entry.type]();
113
+ }
114
+
115
+ // Try to require an external driver: ./drivers/<type>-<driver>.js
116
+ const driverPath = path.join(__dirname, 'drivers', `${entry.type}-${driver}.js`);
117
+ if (fsSync.existsSync(driverPath)) {
118
+ const mod = require(driverPath);
119
+ return new mod.default({ name: resolvedName, type: entry.type, driver, config: cfg });
120
+ }
121
+
122
+ // Stub: not yet installed. Fail soft.
123
+ const err = new Error(
124
+ `provider driver not installed: ${entry.type}/${driver}\n` +
125
+ ` expected at: ${driverPath}\n` +
126
+ ` install with: devflow provider install ${driver}`
127
+ );
128
+ err.code = 'PROVIDER_NOT_INSTALLED';
129
+ throw err;
130
+ }
131
+
132
+ module.exports = { load, listConfigured, BUILTIN_LOCALS };
@@ -0,0 +1,190 @@
1
+ 'use strict';
2
+ const fs = require('fs/promises');
3
+ const fsSync = require('fs');
4
+ const path = require('path');
5
+ const { execSync, spawnSync } = require('child_process');
6
+ const { Provider } = require('./base.js');
7
+
8
+ /**
9
+ * Local providers — zero network, zero dep. Default fallback for every type.
10
+ */
11
+
12
+ class LocalIntakeProvider extends Provider {
13
+ constructor(opts = {}) {
14
+ super({ name: 'intake.local', type: 'intake', driver: 'local', config: opts });
15
+ }
16
+ supports(input) {
17
+ return typeof input === 'string' && (
18
+ input.startsWith('file://') ||
19
+ input.startsWith('/') ||
20
+ input.startsWith('./') ||
21
+ input.startsWith('../') ||
22
+ /\.(md|markdown|txt)$/i.test(input)
23
+ );
24
+ }
25
+ async fetch(input) {
26
+ let p = input;
27
+ if (input.startsWith('file://')) p = input.slice(7);
28
+ p = path.resolve(p);
29
+ const body = await fs.readFile(p, 'utf8');
30
+ const title = (body.match(/^#\s+(.+)$/m) || [, path.basename(p, path.extname(p))])[1];
31
+ return {
32
+ title: title.trim(),
33
+ body_md: body,
34
+ attachments: [],
35
+ source: { type: 'url', ref: 'file://' + p },
36
+ };
37
+ }
38
+ async search() { return []; }
39
+ }
40
+
41
+ class LocalIssueProvider extends Provider {
42
+ constructor(opts = {}) {
43
+ super({ name: 'issue.local', type: 'issue', driver: 'local', config: opts });
44
+ }
45
+ supports(input) {
46
+ // Best-effort: if input looks like JIRA-style PROJ-NUM, return false (let real provider handle).
47
+ // Local issue store is just .devflow/issues/<id>.md
48
+ if (typeof input !== 'string') return false;
49
+ const file = path.join('.devflow', 'issues', `${input}.md`);
50
+ return fsSync.existsSync(file);
51
+ }
52
+ async fetch(id) {
53
+ const file = path.join('.devflow', 'issues', `${id}.md`);
54
+ const body = await fs.readFile(file, 'utf8');
55
+ const title = (body.match(/^#\s+(.+)$/m) || [, id])[1];
56
+ return {
57
+ title: title.trim(),
58
+ body_md: body,
59
+ status: 'open',
60
+ labels: [],
61
+ source: { type: 'issue', ref: id },
62
+ };
63
+ }
64
+ }
65
+
66
+ class LocalKbProvider extends Provider {
67
+ constructor(opts = {}) {
68
+ super({ name: 'kb.local', type: 'kb', driver: 'local', config: opts });
69
+ }
70
+ async upload(file) {
71
+ return { uuid: 'local-' + path.basename(file), url: 'file://' + path.resolve(file) };
72
+ }
73
+ async update() {}
74
+ async delete() {}
75
+ async search() { return []; }
76
+ }
77
+
78
+ class LocalNotifyProvider extends Provider {
79
+ constructor(opts = {}) {
80
+ super({ name: 'notify.local', type: 'notify', driver: 'local', config: opts });
81
+ }
82
+ async send({ subject, body_md, links = [] }) {
83
+ process.stdout.write(`\n--- devflow notify (local) ---\n`);
84
+ process.stdout.write(`subject: ${subject}\n`);
85
+ if (links.length) {
86
+ process.stdout.write(`links:\n`);
87
+ for (const l of links) process.stdout.write(` - ${l}\n`);
88
+ }
89
+ process.stdout.write(`\n${body_md}\n`);
90
+ process.stdout.write(`--- end ---\n\n`);
91
+ return { id: 'stdout' };
92
+ }
93
+ }
94
+
95
+ class LocalCiProvider extends Provider {
96
+ constructor(opts = {}) {
97
+ super({ name: 'ci.local', type: 'ci', driver: 'local', config: opts });
98
+ }
99
+ async trigger(job, params = {}) {
100
+ process.stdout.write(`[ci.local] would trigger job=${job} params=${JSON.stringify(params)}\n`);
101
+ return { runId: 'noop', url: null };
102
+ }
103
+ async status() { return { status: 'unknown', url: null }; }
104
+ }
105
+
106
+ class LocalVcsProvider extends Provider {
107
+ constructor(opts = {}) {
108
+ super({ name: 'vcs.local', type: 'vcs', driver: 'local', config: opts });
109
+ }
110
+ async pr({ title, body_md, base, head, draft = false }) {
111
+ // Resolve base: caller-provided value wins; otherwise autodetect from the
112
+ // current repo so commands that forget to pass --base still target the
113
+ // right branch (master vs main vs custom).
114
+ if (!base) {
115
+ try {
116
+ const autodetect = require('../core/autodetect.js');
117
+ base = autodetect.detectDefaultBranch(process.cwd()) || 'main';
118
+ } catch { base = 'main'; }
119
+ }
120
+
121
+ // Detect remote platform to prefer the right CLI.
122
+ let platform = 'unknown';
123
+ try {
124
+ const ad = require('../core/autodetect.js');
125
+ const remote = ad.parseRemote(ad.gitRemote(process.cwd()));
126
+ if (remote) {
127
+ const h = (remote.host || '').toLowerCase();
128
+ if (h.includes('github')) platform = 'github';
129
+ else if (h.includes('gitlab') || h.includes('code.')) platform = 'gitlab';
130
+ }
131
+ } catch { /* ignore */ }
132
+
133
+ // Try gh (GitHub CLI) — works for GitHub or GitHub Enterprise.
134
+ if (platform !== 'gitlab' && which('gh')) {
135
+ const args = ['pr', 'create', '--title', title, '--body', body_md, '--base', base];
136
+ if (head) args.push('--head', head);
137
+ if (draft) args.push('--draft');
138
+ const r = spawnSync('gh', args, { encoding: 'utf8' });
139
+ if (r.status === 0) return { url: (r.stdout || '').trim(), number: null };
140
+ // If gh is configured but failed, surface the error rather than silently
141
+ // falling through to glab (which might create a duplicate on wrong platform).
142
+ if (platform === 'github') {
143
+ throw new Error('gh pr create failed: ' + (r.stderr || r.stdout || ''));
144
+ }
145
+ }
146
+
147
+ // Try glab (GitLab CLI) — works for gitlab.com and self-hosted GitLab.
148
+ if (platform !== 'github' && which('glab')) {
149
+ const args = ['mr', 'create', '--title', title, '--description', body_md, '--target-branch', base, '--yes'];
150
+ if (head) args.push('--source-branch', head);
151
+ if (draft) args.push('--draft');
152
+ const r = spawnSync('glab', args, { encoding: 'utf8' });
153
+ if (r.status === 0) {
154
+ // glab outputs the MR URL on the last non-empty line.
155
+ const url = (r.stdout || '').trim().split('\n').filter(Boolean).pop() || null;
156
+ return { url, number: null };
157
+ }
158
+ if (platform === 'gitlab') {
159
+ throw new Error('glab mr create failed: ' + (r.stderr || r.stdout || ''));
160
+ }
161
+ }
162
+
163
+ // Final fallback: print the PR/MR draft so the user can create it manually.
164
+ process.stdout.write('\n--- devflow knowledge PR/MR draft (no CLI available) ---\n');
165
+ process.stdout.write(`title: ${title}\nbase: ${base}\nbranch: ${head || '(current)'}\n\n`);
166
+ process.stdout.write(body_md);
167
+ process.stdout.write('\n--- end ---\n');
168
+ process.stdout.write('\nTip: install `gh` (GitHub) or `glab` (GitLab) to automate PR/MR creation.\n');
169
+ process.stdout.write(' or run: devflow provider setup to configure a vcs provider.\n\n');
170
+ return { url: null, number: null };
171
+ }
172
+ }
173
+
174
+ function which(cmd) {
175
+ try {
176
+ execSync(process.platform === 'win32' ? `where ${cmd}` : `command -v ${cmd}`, { stdio: 'ignore' });
177
+ return true;
178
+ } catch {
179
+ return false;
180
+ }
181
+ }
182
+
183
+ module.exports = {
184
+ LocalIntakeProvider,
185
+ LocalIssueProvider,
186
+ LocalKbProvider,
187
+ LocalNotifyProvider,
188
+ LocalCiProvider,
189
+ LocalVcsProvider,
190
+ };