@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,549 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* kb-git provider — knowledge base backed by a standalone GitLab/GitHub Git repo.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors arb-workflow-kit's arb_knowledge.py design:
|
|
6
|
+
* ensure() — clone / checkout main / pull
|
|
7
|
+
* listTags() — enumerate .meta.json across all category dirs
|
|
8
|
+
* resolveId() — reverse-lookup local path from knowledge_id
|
|
9
|
+
* depositAndCreateMr() — stash → checkout main → pull → branch → pop →
|
|
10
|
+
* write files → update .meta.json → add+commit+push → MR
|
|
11
|
+
*
|
|
12
|
+
* config shape (set via `devflow provider setup`, key "kb.git"):
|
|
13
|
+
* {
|
|
14
|
+
* repoUrl: string (required) git clone URL
|
|
15
|
+
* localPath: string (required) local clone path, e.g. ~/.devflow/kb/arb-knowledge
|
|
16
|
+
* targetBranch: string (optional, default "main") base branch / MR target
|
|
17
|
+
* gitlabApiUrl: string (optional) https://code.corp.com/api/v4
|
|
18
|
+
* gitlabToken: string (optional) PRIVATE-TOKEN
|
|
19
|
+
* projectId: string (optional) "group/project" or numeric ID
|
|
20
|
+
* githubToken: string (optional) GitHub PAT
|
|
21
|
+
* githubRepo: string (optional) "owner/repo" (auto-detected from repoUrl)
|
|
22
|
+
* mrReviewers: string (optional) comma-separated GitLab usernames
|
|
23
|
+
* }
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const fs = require('fs/promises');
|
|
27
|
+
const fsSync = require('fs');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
const https = require('https');
|
|
30
|
+
const http = require('http');
|
|
31
|
+
const cp = require('child_process');
|
|
32
|
+
const os = require('os');
|
|
33
|
+
const { Provider } = require('../base.js');
|
|
34
|
+
|
|
35
|
+
class KbGitProvider extends Provider {
|
|
36
|
+
constructor({ name, type, driver, config }) {
|
|
37
|
+
super({ name, type, driver, config });
|
|
38
|
+
this.repoUrl = config.repoUrl || '';
|
|
39
|
+
this.localPath = expandHome(config.localPath || '');
|
|
40
|
+
this.targetBranch = config.targetBranch || 'main';
|
|
41
|
+
this.gitlabApiUrl = (config.gitlabApiUrl || '').replace(/\/+$/, '');
|
|
42
|
+
this.gitlabToken = config.gitlabToken || '';
|
|
43
|
+
this.projectId = config.projectId || '';
|
|
44
|
+
this.githubToken = config.githubToken || '';
|
|
45
|
+
this.githubRepo = config.githubRepo || '';
|
|
46
|
+
this.mrReviewers = (config.mrReviewers || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async validate() {
|
|
50
|
+
if (!this.repoUrl) return { ok: false, reason: 'repoUrl is required' };
|
|
51
|
+
if (!this.localPath) return { ok: false, reason: 'localPath is required' };
|
|
52
|
+
try {
|
|
53
|
+
await this.ensure();
|
|
54
|
+
return { ok: true };
|
|
55
|
+
} catch (e) {
|
|
56
|
+
return { ok: false, reason: e.message };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── Knowledge sync interface (devflow knowledge sync) ───────────────────────
|
|
61
|
+
|
|
62
|
+
async upload(filePath, { category, filename } = {}) {
|
|
63
|
+
await this.ensure();
|
|
64
|
+
const dest = path.join(this.localPath, category || '', filename || path.basename(filePath));
|
|
65
|
+
await fs.mkdir(path.dirname(dest), { recursive: true });
|
|
66
|
+
await fs.copyFile(filePath, dest);
|
|
67
|
+
this._gitCommitPush(`chore(kb): sync ${filename || path.basename(filePath)}`);
|
|
68
|
+
return { uuid: dest, url: null };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async update(uuid, filePath) {
|
|
72
|
+
await fs.copyFile(filePath, uuid);
|
|
73
|
+
this._gitCommitPush(`chore(kb): update ${path.basename(uuid)}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async delete(uuid) {
|
|
77
|
+
if (fsSync.existsSync(uuid)) await fs.unlink(uuid);
|
|
78
|
+
this._gitCommitPush(`chore(kb): delete ${path.basename(uuid)}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async search(q, { k = 8 } = {}) {
|
|
82
|
+
await this.ensure();
|
|
83
|
+
const hits = [];
|
|
84
|
+
const files = await walkMd(this.localPath);
|
|
85
|
+
const kws = q.toLowerCase().split(/\s+/).filter(Boolean);
|
|
86
|
+
for (const f of files) {
|
|
87
|
+
const content = await fs.readFile(f, 'utf8');
|
|
88
|
+
const lower = content.toLowerCase();
|
|
89
|
+
let score = 0;
|
|
90
|
+
for (const kw of kws) {
|
|
91
|
+
if (path.basename(f).toLowerCase().includes(kw)) score += 5;
|
|
92
|
+
const headings = (content.match(/^#{1,2}\s+.+$/gm) || []).join('\n').toLowerCase();
|
|
93
|
+
if (headings.includes(kw)) score += 3;
|
|
94
|
+
score += Math.min((lower.match(new RegExp(kw, 'g')) || []).length, 10);
|
|
95
|
+
}
|
|
96
|
+
score = +(score / Math.sqrt(kws.length || 1)).toFixed(2);
|
|
97
|
+
if (score > 0) hits.push({ path: path.relative(this.localPath, f), score });
|
|
98
|
+
}
|
|
99
|
+
hits.sort((a, b) => b.score - a.score);
|
|
100
|
+
return hits.slice(0, k);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Meta-mapping helpers (mirrors arb_knowledge.py iter_mappings) ───────────
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* listTags() — enumerate all .meta.json across the external repo.
|
|
107
|
+
* Returns same format as arb_knowledge.py list-tags.
|
|
108
|
+
*
|
|
109
|
+
* @returns {Array<{ directory, tag_id, tag_name, document_count, documents }>}
|
|
110
|
+
*/
|
|
111
|
+
async listTags() {
|
|
112
|
+
if (!fsSync.existsSync(this.localPath)) await this.ensure();
|
|
113
|
+
const result = [];
|
|
114
|
+
const metas = await walkFile(this.localPath, '.meta.json');
|
|
115
|
+
for (const mf of metas) {
|
|
116
|
+
let data = {};
|
|
117
|
+
try { data = JSON.parse(await fs.readFile(mf, 'utf8')); } catch { continue; }
|
|
118
|
+
const docs = data.documents || {};
|
|
119
|
+
result.push({
|
|
120
|
+
directory: path.dirname(mf),
|
|
121
|
+
tag_id: data.tag_id || null,
|
|
122
|
+
tag_name: data.tag_name || path.basename(path.dirname(mf)),
|
|
123
|
+
document_count: Object.keys(docs).length,
|
|
124
|
+
documents: Object.keys(docs),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* resolveId(knowledge_id) — reverse-lookup local file path from a knowledge_id.
|
|
132
|
+
* Mirrors arb_knowledge.py resolve sub-command.
|
|
133
|
+
*
|
|
134
|
+
* @param {string} knowledge_id
|
|
135
|
+
* @returns {{ local_path, tag_id, tag_name, filename } | null}
|
|
136
|
+
*/
|
|
137
|
+
async resolveId(knowledge_id) {
|
|
138
|
+
if (!fsSync.existsSync(this.localPath)) return null;
|
|
139
|
+
const metas = await walkFile(this.localPath, '.meta.json');
|
|
140
|
+
for (const mf of metas) {
|
|
141
|
+
let data = {};
|
|
142
|
+
try { data = JSON.parse(await fs.readFile(mf, 'utf8')); } catch { continue; }
|
|
143
|
+
const docs = data.documents || {};
|
|
144
|
+
for (const [filename, kid] of Object.entries(docs)) {
|
|
145
|
+
if (kid === knowledge_id) {
|
|
146
|
+
return {
|
|
147
|
+
local_path: path.join(path.dirname(mf), filename),
|
|
148
|
+
tag_id: data.tag_id || null,
|
|
149
|
+
tag_name: data.tag_name || null,
|
|
150
|
+
filename,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── MR/PR deposit flow ──────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* High-level deposit + MR.
|
|
162
|
+
*
|
|
163
|
+
* Git flow mirrors arb_knowledge.py deposit_flow():
|
|
164
|
+
* 1. stash dirty changes (if any)
|
|
165
|
+
* 2. checkout targetBranch + pull
|
|
166
|
+
* 3. create branch deposit/<slug>-<date>
|
|
167
|
+
* 4. stash pop
|
|
168
|
+
* 5. copy files from workspace knowledge/ (or legacy devflow/knowledge/) into external repo
|
|
169
|
+
* 6. update .meta.json for new (A) files — ID left empty for CI to backfill
|
|
170
|
+
* 7. add + commit + push
|
|
171
|
+
* 8. create MR/PR via API
|
|
172
|
+
* 9. return { branch, url, status, commit_hash? }
|
|
173
|
+
*
|
|
174
|
+
* @param {string} projectRoot
|
|
175
|
+
* @param {{ items, slug, dryRun }} opts
|
|
176
|
+
*/
|
|
177
|
+
async depositAndCreateMr(projectRoot, { items, slug, dryRun = false }) {
|
|
178
|
+
await this.ensure();
|
|
179
|
+
|
|
180
|
+
const date = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
181
|
+
const safeslug = slug.replace(/[^a-zA-Z0-9-]/g, '-').replace(/-{2,}/g, '-').slice(0, 40).replace(/-$/, '');
|
|
182
|
+
const branch = `deposit/${safeslug}-${date}`;
|
|
183
|
+
const target = this.targetBranch;
|
|
184
|
+
|
|
185
|
+
if (dryRun) {
|
|
186
|
+
return {
|
|
187
|
+
branch, url: null, status: 'dry_run',
|
|
188
|
+
message: `would clone/pull ${this.repoUrl}, create branch '${branch}', copy ${(items || []).filter((p) => p.action !== 'noop').length} file(s), then open MR`,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── 1. Stash any dirty work-in-progress in the external clone ─────────────
|
|
193
|
+
const dirtyBefore = this._git(['status', '--porcelain']).stdout.trim();
|
|
194
|
+
const stashLabel = `devflow-deposit-${branch}`;
|
|
195
|
+
if (dirtyBefore) {
|
|
196
|
+
this._git(['stash', 'push', '-u', '-m', stashLabel]);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ── 2. Checkout targetBranch + pull latest ────────────────────────────────
|
|
200
|
+
this._git(['checkout', target]);
|
|
201
|
+
this._git(['pull', 'origin', target, '--ff-only']);
|
|
202
|
+
|
|
203
|
+
// ── 3. Create deposit branch (delete first if exists) ─────────────────────
|
|
204
|
+
const branchExistsR = this._git(['rev-parse', '--verify', branch]);
|
|
205
|
+
if (branchExistsR.status === 0) {
|
|
206
|
+
this._git(['branch', '-D', branch]);
|
|
207
|
+
}
|
|
208
|
+
this._git(['checkout', '-b', branch]);
|
|
209
|
+
|
|
210
|
+
// ── 4. Restore stash ──────────────────────────────────────────────────────
|
|
211
|
+
if (dirtyBefore) {
|
|
212
|
+
const popR = this._git(['stash', 'pop']);
|
|
213
|
+
if (popR.status !== 0) {
|
|
214
|
+
// Conflict during stash pop — leave on the branch for manual resolution.
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── 5. Copy knowledge files from workspace knowledge/ → external repo ─────
|
|
219
|
+
const actionable = (items || []).filter((p) => p.action !== 'noop');
|
|
220
|
+
for (const p of actionable) {
|
|
221
|
+
const srcKbDir = p.knowledgeRoot || inferKnowledgeRoot(projectRoot, p.targetAbs);
|
|
222
|
+
const rel = path.relative(srcKbDir, p.targetAbs);
|
|
223
|
+
const dest = path.join(this.localPath, rel);
|
|
224
|
+
await fs.mkdir(path.dirname(dest), { recursive: true });
|
|
225
|
+
await fs.copyFile(p.targetAbs, dest);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ── 6. Update .meta.json for new (A) files — knowledge_id left empty ──────
|
|
229
|
+
const newFiles = actionable.filter((p) => p.action === 'create');
|
|
230
|
+
if (newFiles.length) {
|
|
231
|
+
await this._updateMetaJsonForNew(projectRoot, newFiles);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── 7. Check for changes ──────────────────────────────────────────────────
|
|
235
|
+
const statusOut = this._git(['status', '--porcelain']).stdout.trim();
|
|
236
|
+
if (!statusOut) {
|
|
237
|
+
this._git(['checkout', target]);
|
|
238
|
+
return { branch: null, url: null, status: 'no_changes' };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ── 8. Add + commit + push ────────────────────────────────────────────────
|
|
242
|
+
this._git(['add', '-A']);
|
|
243
|
+
const counts = { A: 0, M: 0 };
|
|
244
|
+
for (const p of actionable) {
|
|
245
|
+
if (p.action === 'create') counts.A++;
|
|
246
|
+
else if (p.action === 'update') counts.M++;
|
|
247
|
+
}
|
|
248
|
+
const commitMsg = `chore(knowledge): [${slug}] deposit ${counts.A} added, ${counts.M} updated`;
|
|
249
|
+
const commitR = this._git(['commit', '-m', commitMsg]);
|
|
250
|
+
if (commitR.status !== 0) {
|
|
251
|
+
this._git(['checkout', target]);
|
|
252
|
+
throw new Error(`git commit failed: ${(commitR.stderr || commitR.stdout || '').trim()}`);
|
|
253
|
+
}
|
|
254
|
+
const commitHash = this._git(['rev-parse', 'HEAD']).stdout.trim();
|
|
255
|
+
|
|
256
|
+
const pushR = this._git(['push', '-u', 'origin', branch]);
|
|
257
|
+
if (pushR.status !== 0) {
|
|
258
|
+
this._git(['checkout', target]);
|
|
259
|
+
throw new Error(`git push failed: ${(pushR.stderr || pushR.stdout || '').trim()}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ── 9. Create MR/PR ───────────────────────────────────────────────────────
|
|
263
|
+
const { buildPrBody } = require('../../knowledge/mr.js');
|
|
264
|
+
const mrTitle = `[Knowledge] ${slug}`;
|
|
265
|
+
const mrBody = buildPrBody(actionable, slug, branch, target);
|
|
266
|
+
|
|
267
|
+
let url = null;
|
|
268
|
+
try {
|
|
269
|
+
url = await this._createMrOrPr(branch, mrTitle, mrBody);
|
|
270
|
+
} catch (e) {
|
|
271
|
+
this._git(['checkout', target]);
|
|
272
|
+
return { branch, url: null, commit_hash: commitHash, status: 'pushed_no_mr', error: e.message };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
this._git(['checkout', target]);
|
|
276
|
+
return { branch, url, commit_hash: commitHash, status: 'created' };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Deposit knowledge files directly to targetBranch — no feature branch, no MR.
|
|
281
|
+
* Use when team trusts CI-less fast path or the repo has no review gate.
|
|
282
|
+
*/
|
|
283
|
+
async depositAndPush(projectRoot, { items, slug, dryRun = false }) {
|
|
284
|
+
await this.ensure();
|
|
285
|
+
|
|
286
|
+
const actionable = (items || []).filter((p) => p.action !== 'noop');
|
|
287
|
+
if (!actionable.length) return { status: 'no_changes' };
|
|
288
|
+
|
|
289
|
+
const target = this.targetBranch;
|
|
290
|
+
|
|
291
|
+
if (dryRun) {
|
|
292
|
+
return {
|
|
293
|
+
status: 'dry_run',
|
|
294
|
+
message: `would push ${actionable.length} file(s) directly to ${target} on ${this.repoUrl}`,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// 1. Ensure on targetBranch + latest
|
|
299
|
+
this._git(['checkout', target]);
|
|
300
|
+
this._git(['pull', 'origin', target, '--ff-only']);
|
|
301
|
+
|
|
302
|
+
// 2. Copy files
|
|
303
|
+
for (const p of actionable) {
|
|
304
|
+
const srcKbDir = p.knowledgeRoot || inferKnowledgeRoot(projectRoot, p.targetAbs);
|
|
305
|
+
const rel = path.relative(srcKbDir, p.targetAbs);
|
|
306
|
+
const dest = path.join(this.localPath, rel);
|
|
307
|
+
await fs.mkdir(path.dirname(dest), { recursive: true });
|
|
308
|
+
await fs.copyFile(p.targetAbs, dest);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 3. Update .meta.json for new files
|
|
312
|
+
const newFiles = actionable.filter((p) => p.action === 'create');
|
|
313
|
+
if (newFiles.length) await this._updateMetaJsonForNew(projectRoot, newFiles);
|
|
314
|
+
|
|
315
|
+
// 4. Check for changes
|
|
316
|
+
const statusOut = this._git(['status', '--porcelain']).stdout.trim();
|
|
317
|
+
if (!statusOut) return { status: 'no_changes' };
|
|
318
|
+
|
|
319
|
+
// 5. Commit + push directly to targetBranch
|
|
320
|
+
this._git(['add', '-A']);
|
|
321
|
+
const counts = { A: 0, M: 0 };
|
|
322
|
+
for (const p of actionable) {
|
|
323
|
+
if (p.action === 'create') counts.A++;
|
|
324
|
+
else if (p.action === 'update') counts.M++;
|
|
325
|
+
}
|
|
326
|
+
const commitMsg = `chore(knowledge): [${slug}] deposit ${counts.A} added, ${counts.M} updated`;
|
|
327
|
+
const commitR = this._git(['commit', '-m', commitMsg]);
|
|
328
|
+
if (commitR.status !== 0) {
|
|
329
|
+
throw new Error(`git commit failed: ${(commitR.stderr || commitR.stdout || '').trim()}`);
|
|
330
|
+
}
|
|
331
|
+
const commitHash = this._git(['rev-parse', 'HEAD']).stdout.trim();
|
|
332
|
+
|
|
333
|
+
const pushR = this._git(['push', 'origin', target]);
|
|
334
|
+
if (pushR.status !== 0) {
|
|
335
|
+
throw new Error(`git push failed: ${(pushR.stderr || pushR.stdout || '').trim()}`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return { status: 'pushed', commit_hash: commitHash, branch: target };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ── .meta.json helpers ──────────────────────────────────────────────────────
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* For each newly created file, add an entry to the .meta.json in its category
|
|
345
|
+
* directory inside the external repo, with an empty knowledge_id (CI backfills).
|
|
346
|
+
* Mirrors arb_knowledge.py Step 5 behavior for A (add) operations.
|
|
347
|
+
*/
|
|
348
|
+
async _updateMetaJsonForNew(projectRoot, newItems) {
|
|
349
|
+
for (const p of newItems) {
|
|
350
|
+
const srcKbDir = p.knowledgeRoot || inferKnowledgeRoot(projectRoot, p.targetAbs);
|
|
351
|
+
const rel = path.relative(srcKbDir, p.targetAbs);
|
|
352
|
+
const destAbs = path.join(this.localPath, rel);
|
|
353
|
+
const catDir = path.dirname(destAbs);
|
|
354
|
+
const metaFile = path.join(catDir, '.meta.json');
|
|
355
|
+
const filename = path.basename(destAbs);
|
|
356
|
+
|
|
357
|
+
let meta = {};
|
|
358
|
+
if (fsSync.existsSync(metaFile)) {
|
|
359
|
+
try { meta = JSON.parse(await fs.readFile(metaFile, 'utf8')); } catch { meta = {}; }
|
|
360
|
+
} else {
|
|
361
|
+
// Bootstrap a minimal .meta.json if the category dir is new.
|
|
362
|
+
meta = {
|
|
363
|
+
tag_id: null,
|
|
364
|
+
tag_name: path.basename(catDir),
|
|
365
|
+
documents: {},
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
meta.documents = meta.documents || {};
|
|
370
|
+
if (!(filename in meta.documents)) {
|
|
371
|
+
meta.documents[filename] = ''; // empty = CI will backfill after WeKnora upload
|
|
372
|
+
}
|
|
373
|
+
await fs.writeFile(metaFile, JSON.stringify(meta, null, 2) + '\n', 'utf8');
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ── Git helpers ─────────────────────────────────────────────────────────────
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Clone if absent, else checkout targetBranch + pull latest.
|
|
381
|
+
* Mirrors arb_knowledge.py ensure_clone().
|
|
382
|
+
*/
|
|
383
|
+
async ensure() {
|
|
384
|
+
if (!this.repoUrl) throw new Error('kb-git: repoUrl not configured');
|
|
385
|
+
if (!this.localPath) throw new Error('kb-git: localPath not configured');
|
|
386
|
+
|
|
387
|
+
if (!fsSync.existsSync(path.join(this.localPath, '.git'))) {
|
|
388
|
+
await fs.mkdir(path.dirname(this.localPath), { recursive: true });
|
|
389
|
+
const r = cp.spawnSync('git', ['clone', this.repoUrl, this.localPath], { encoding: 'utf8' });
|
|
390
|
+
if (r.status !== 0) throw new Error(`git clone failed: ${(r.stderr || r.stdout || '').trim()}`);
|
|
391
|
+
} else {
|
|
392
|
+
this._git(['fetch', 'origin', '--no-tags']);
|
|
393
|
+
const coR = this._git(['checkout', this.targetBranch]);
|
|
394
|
+
if (coR.status !== 0) {
|
|
395
|
+
// Already on target branch or detached HEAD — ignore.
|
|
396
|
+
}
|
|
397
|
+
this._git(['pull', 'origin', this.targetBranch, '--ff-only']);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
_git(args) {
|
|
402
|
+
return cp.spawnSync('git', args, {
|
|
403
|
+
cwd: this.localPath, encoding: 'utf8',
|
|
404
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
_gitCommitPush(msg) {
|
|
409
|
+
this._git(['add', '-A']);
|
|
410
|
+
this._git(['commit', '-m', msg]);
|
|
411
|
+
this._git(['push', 'origin', this.targetBranch]);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ── MR / PR API ─────────────────────────────────────────────────────────────
|
|
415
|
+
|
|
416
|
+
async _createMrOrPr(branch, title, body) {
|
|
417
|
+
const isGitHub = this.githubToken || this.githubRepo ||
|
|
418
|
+
/github\.com/i.test(this.repoUrl);
|
|
419
|
+
|
|
420
|
+
if (isGitHub) return this._createGithubPr(branch, title, body);
|
|
421
|
+
if (this.gitlabApiUrl && this.gitlabToken && this.projectId) {
|
|
422
|
+
return this._createGitlabMr(branch, title, body);
|
|
423
|
+
}
|
|
424
|
+
return this._createViaGlabCli(branch, title, body);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** Mirrors arb_knowledge.py create_mr() using the GitLab REST API. */
|
|
428
|
+
async _createGitlabMr(branch, title, description) {
|
|
429
|
+
const projectPath = encodeURIComponent(this.projectId);
|
|
430
|
+
const url = `${this.gitlabApiUrl}/projects/${projectPath}/merge_requests`;
|
|
431
|
+
const body = {
|
|
432
|
+
source_branch: branch,
|
|
433
|
+
target_branch: this.targetBranch,
|
|
434
|
+
title,
|
|
435
|
+
description,
|
|
436
|
+
remove_source_branch: true,
|
|
437
|
+
};
|
|
438
|
+
if (this.mrReviewers.length) {
|
|
439
|
+
body.description += `\n\n/assign_reviewer ${this.mrReviewers.join(' @')}`;
|
|
440
|
+
}
|
|
441
|
+
const resp = await this._httpPost(url, body, { 'PRIVATE-TOKEN': this.gitlabToken });
|
|
442
|
+
return resp.web_url || null;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
async _createGithubPr(branch, title, body) {
|
|
446
|
+
let repo = this.githubRepo;
|
|
447
|
+
if (!repo) {
|
|
448
|
+
const m = this.repoUrl.match(/github\.com[/:]([^/]+\/[^/]+?)(?:\.git)?$/i);
|
|
449
|
+
if (!m) throw new Error('kb-git: cannot determine GitHub owner/repo from repoUrl');
|
|
450
|
+
repo = m[1];
|
|
451
|
+
}
|
|
452
|
+
const url = `https://api.github.com/repos/${repo}/pulls`;
|
|
453
|
+
const payload = { title, body, head: branch, base: this.targetBranch };
|
|
454
|
+
const resp = await this._httpPost(url, payload, {
|
|
455
|
+
Authorization: `Bearer ${this.githubToken}`,
|
|
456
|
+
'User-Agent': 'devflow-kit',
|
|
457
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
458
|
+
});
|
|
459
|
+
return resp.html_url || null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async _createViaGlabCli(branch, title, body) {
|
|
463
|
+
const r = cp.spawnSync('glab', [
|
|
464
|
+
'mr', 'create',
|
|
465
|
+
'--title', title,
|
|
466
|
+
'--description', body,
|
|
467
|
+
'--source-branch', branch,
|
|
468
|
+
'--target-branch', this.targetBranch,
|
|
469
|
+
'--yes',
|
|
470
|
+
], { cwd: this.localPath, encoding: 'utf8' });
|
|
471
|
+
if (r.status !== 0) throw new Error(`glab mr create failed: ${(r.stderr || r.stdout || '').trim()}`);
|
|
472
|
+
return (r.stdout || '').trim().split('\n').filter(Boolean).pop() || null;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
_httpPost(url, body, headers = {}) {
|
|
476
|
+
return new Promise((resolve, reject) => {
|
|
477
|
+
const parsed = new URL(url);
|
|
478
|
+
const data = JSON.stringify(body);
|
|
479
|
+
const opts = {
|
|
480
|
+
hostname: parsed.hostname,
|
|
481
|
+
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
|
482
|
+
path: parsed.pathname + (parsed.search || ''),
|
|
483
|
+
method: 'POST',
|
|
484
|
+
headers: {
|
|
485
|
+
'Content-Type': 'application/json',
|
|
486
|
+
'Content-Length': Buffer.byteLength(data),
|
|
487
|
+
...headers,
|
|
488
|
+
},
|
|
489
|
+
};
|
|
490
|
+
const lib = parsed.protocol === 'https:' ? https : http;
|
|
491
|
+
const req = lib.request(opts, (res) => {
|
|
492
|
+
let raw = '';
|
|
493
|
+
res.on('data', (c) => { raw += c; });
|
|
494
|
+
res.on('end', () => {
|
|
495
|
+
if (res.statusCode >= 400) {
|
|
496
|
+
reject(new Error(`HTTP ${res.statusCode}: ${raw.slice(0, 300)}`));
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
try { resolve(JSON.parse(raw)); }
|
|
500
|
+
catch { resolve({}); }
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
req.on('error', reject);
|
|
504
|
+
req.setTimeout(15000, () => { req.destroy(new Error('timeout')); });
|
|
505
|
+
req.write(data);
|
|
506
|
+
req.end();
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ── Utilities ───────────────────────────────────────────────────────────────
|
|
512
|
+
|
|
513
|
+
function expandHome(p) {
|
|
514
|
+
if (!p) return p;
|
|
515
|
+
if (p.startsWith('~/') || p === '~') return path.join(os.homedir(), p.slice(1));
|
|
516
|
+
return p;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function inferKnowledgeRoot(projectRoot, filePath) {
|
|
520
|
+
const legacy = path.join(projectRoot, 'devflow', 'knowledge');
|
|
521
|
+
if (filePath.startsWith(legacy + path.sep)) return legacy;
|
|
522
|
+
const marker = `${path.sep}knowledge${path.sep}`;
|
|
523
|
+
const idx = filePath.lastIndexOf(marker);
|
|
524
|
+
if (idx >= 0) return filePath.slice(0, idx + marker.length - 1);
|
|
525
|
+
return legacy;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async function walkMd(dir) {
|
|
529
|
+
return walkFile(dir, '.md');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async function walkFile(dir, suffix) {
|
|
533
|
+
const out = [];
|
|
534
|
+
if (!fsSync.existsSync(dir)) return out;
|
|
535
|
+
const stack = [dir];
|
|
536
|
+
while (stack.length) {
|
|
537
|
+
const cur = stack.pop();
|
|
538
|
+
const ents = await fs.readdir(cur, { withFileTypes: true });
|
|
539
|
+
for (const e of ents) {
|
|
540
|
+
if (e.name === '.git') continue;
|
|
541
|
+
const p = path.join(cur, e.name);
|
|
542
|
+
if (e.isDirectory()) stack.push(p);
|
|
543
|
+
else if (e.isFile() && e.name.endsWith(suffix)) out.push(p);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return out;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
module.exports = { default: KbGitProvider };
|