@rowansenne/workflow-run 0.21.2 → 0.22.1
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 +33 -0
- package/README.md +32 -14
- package/bin/lib/docsrepo.mjs +52 -18
- package/bin/lib/doctor.mjs +4 -1
- package/bin/lib/install.mjs +22 -4
- package/bin/workflow-run.mjs +7 -2
- package/create-release-md-doc/SKILL.md.tpl +1 -1
- package/create-tech-md-doc/SKILL.md.tpl +1 -1
- package/dev/SKILL.md.tpl +20 -14
- package/dev/references/delivery-template.md +8 -3
- package/dev/skill.json +2 -2
- package/md-to-wiki/SKILL.md.tpl +1 -1
- package/package.json +1 -1
- package/project-review/SKILL.md.tpl +3 -2
- package/workflow/SKILL.md.tpl +2 -2
- package/workflow/references/feedback.md +34 -0
- package/workflow/references/profile.md +1 -1
- package/workflow/references/requirements.md +11 -9
- package/workflow/scripts/artifacts.mjs +6 -5
- package/workflow/scripts/feedback.mjs +204 -0
- package/workflow/scripts/requirement.mjs +79 -19
- package/workflow/scripts/resolve.mjs +4 -3
- package/workflow/scripts/view.mjs +5 -4
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
// wiki 发布记录与 DocNexus 沉淀,供 /workflow view 等直接读取。
|
|
4
4
|
// 本工具的产出都在项目根的 workflow-run/ 里(需求产物 docs/requirements/、DocNexus 工作区 docnexus/、项目模板 templates/、临时文件 tmp/),
|
|
5
5
|
// 不往项目的其他地方写。两种用法:
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// 两种用法的目录结构完全一样:产物都在 workflow-run/docs/requirements/<需求名>/<项目名>/,一个需求横跨多个服务时各项目各占一个子目录,
|
|
7
|
+
// 所以两种用法之间切换只是换一个 git 仓库来跟踪 workflow-run/,文件不用挪:
|
|
8
|
+
// - 默认:workflow-run/ 随代码提交;项目名没配置时取 git 远端仓库名(第一次 bind 时写进 profile.json,之后固定)
|
|
9
|
+
// - 独立文档仓库:团队文档仓库克隆到 workflow-run/(它自己是 git 仓库,代码仓库忽略它),项目名要先配置(各服务在同一个仓库里,名字由团队定);
|
|
10
|
+
// 文档仓库单独提交(写完由用户确认,可用 artifacts.commit_prompt 关闭提交提示)
|
|
11
|
+
// 需求目录写进项目根 .ignore,不参与代码搜索。旧版本(随代码提交时没有 <项目名>/ 这一层)的产物由安装器 upgrade 时迁移(migrateLayout)
|
|
9
12
|
// 需求名由用户在流程开始或第一次落盘时给出;用户不给时按分支名生成。分支 → 需求的绑定记在 meta.json 的 branches。
|
|
10
13
|
// 说明见 workflow/references/requirements.md。
|
|
11
14
|
//
|
|
@@ -110,13 +113,17 @@ export function artifactsConfig(root) {
|
|
|
110
113
|
return { project: c.project ? String(c.project) : '', commit_prompt: c.commit_prompt !== false, profile_dir: dir };
|
|
111
114
|
}
|
|
112
115
|
|
|
113
|
-
//
|
|
116
|
+
// 产物位置:两种模式都是 workflow-run/docs/requirements/<需求>/<项目>/。
|
|
117
|
+
// mode 只决定谁来提交(project = 随代码提交,repo = workflow-run/ 是独立的文档仓库)、引用怎么写、项目名能不能自动取
|
|
114
118
|
export function layout(root, cfg = artifactsConfig(root)) {
|
|
115
119
|
const home = homeDir(root);
|
|
116
120
|
const base = path.join(root, REQ_DIR);
|
|
117
|
-
if (!homeIsRepo(root))
|
|
121
|
+
if (!homeIsRepo(root)) {
|
|
122
|
+
const project = cfg.project ? dirName(cfg.project) : suggestProject(root);
|
|
123
|
+
return { mode: 'project', ok: true, base, home, repo: null, project, project_configured: !!cfg.project, commit_prompt: cfg.commit_prompt, reqDir: (slug) => path.join(base, slug, project), ref: (abs) => relOf(root, abs) };
|
|
124
|
+
}
|
|
118
125
|
const repoName = remoteName(home);
|
|
119
|
-
const L = { mode: 'repo', base, home, repo: home, repo_rel: HOME, repo_name: repoName, project: cfg.project ? dirName(cfg.project) : null, commit_prompt: cfg.commit_prompt };
|
|
126
|
+
const L = { mode: 'repo', base, home, repo: home, repo_rel: HOME, repo_name: repoName, project: cfg.project ? dirName(cfg.project) : null, project_configured: !!cfg.project, commit_prompt: cfg.commit_prompt };
|
|
120
127
|
L.reqDir = (slug) => path.join(base, slug, L.project ?? '_');
|
|
121
128
|
// 文档里引用产物用「文档仓库名:仓库内路径」,各服务、各人的克隆里都一样
|
|
122
129
|
L.ref = (abs) => `${repoName}:${toPosix(path.relative(home, abs))}`;
|
|
@@ -157,9 +164,10 @@ export function requirements(root, L = layout(root)) {
|
|
|
157
164
|
return out;
|
|
158
165
|
}
|
|
159
166
|
|
|
160
|
-
//
|
|
167
|
+
// 所有项目的需求(跨服务视图、bind 时找同名需求):{ slug, name, projects:[{ project, abs, dir, meta }] }。
|
|
168
|
+
// 两种模式结构相同;随代码提交时通常只有本项目一个
|
|
161
169
|
export function repoRequirements(root, L = layout(root)) {
|
|
162
|
-
if (
|
|
170
|
+
if (!isDir(L.base)) return [];
|
|
163
171
|
const out = [];
|
|
164
172
|
for (const d of fs.readdirSync(L.base, { withFileTypes: true })) {
|
|
165
173
|
if (skipDir(d)) continue;
|
|
@@ -178,7 +186,7 @@ export function repoRequirements(root, L = layout(root)) {
|
|
|
178
186
|
// 产物文件(view / artifacts.mjs 读):{ abs, rel(相对项目根), cite(文档里引用它的写法), slug, project, kind, version }
|
|
179
187
|
export function artifactFiles(root, { allProjects = false } = {}) {
|
|
180
188
|
const L = layout(root);
|
|
181
|
-
const dirs = allProjects
|
|
189
|
+
const dirs = allProjects
|
|
182
190
|
? repoRequirements(root, L).flatMap((r) => r.projects.map((p) => ({ abs: p.abs, slug: r.slug, project: p.project })))
|
|
183
191
|
: requirements(root, L).map((r) => ({ abs: r.abs, slug: r.slug, project: L.project }));
|
|
184
192
|
const out = [];
|
|
@@ -251,15 +259,15 @@ export function which(root, branch) {
|
|
|
251
259
|
const L = layout(root);
|
|
252
260
|
if (!L.ok) return notReady(L);
|
|
253
261
|
const r = find(root, branch, L);
|
|
254
|
-
const where = L.mode === 'repo' ? { mode: 'repo', repo: L.repo_rel, project: L.project } : { mode: 'project' };
|
|
262
|
+
const where = L.mode === 'repo' ? { mode: 'repo', repo: L.repo_rel, project: L.project } : { mode: 'project', project: L.project };
|
|
255
263
|
if (r) return { bound: true, ...where, branch, name: r.name, dir: r.dir, meta: sync(root, r.dir) };
|
|
256
264
|
const recent = requirements(root, L).sort((a, b) => String(b.updated).localeCompare(String(a.updated))).slice(0, 5).map(({ name, dir, branches, updated }) => ({ name, dir, branches, updated }));
|
|
257
|
-
//
|
|
265
|
+
// 其他项目已有的需求(跨服务需求,本项目可以用同一个名字加入);文档仓库里常见,随代码提交时一般没有
|
|
258
266
|
const mine = new Set(requirements(root, L).map((x) => x.slug));
|
|
259
|
-
const others =
|
|
267
|
+
const others = repoRequirements(root, L).filter((x) => !mine.has(x.slug)).sort((a, b) => String(b.updated).localeCompare(String(a.updated))).slice(0, 10).map((x) => ({ name: x.name, projects: x.projects.map((p) => p.project), updated: x.updated }));
|
|
260
268
|
return {
|
|
261
|
-
bound: false, ...where, branch, suggested_name: branch ? dirName(branch.replace(/\//g, '-')) : null, recent, ...(L.mode === 'repo' ? { other_projects: others } : {}),
|
|
262
|
-
hint: `当前分支还没有绑定需求:问用户这个需求叫什么(给出 suggested_name 作为默认;也可以选 recent 里已有的需求继续${L.mode === 'repo' ? ',或加入 other_projects 里其他服务已有的需求' : ''}),然后运行 bind --name <需求名>;用户不给就运行 bind(按分支名生成)。`,
|
|
269
|
+
bound: false, ...where, branch, suggested_name: branch ? dirName(branch.replace(/\//g, '-')) : null, recent, ...(others.length || L.mode === 'repo' ? { other_projects: others } : {}),
|
|
270
|
+
hint: `当前分支还没有绑定需求:问用户这个需求叫什么(给出 suggested_name 作为默认;也可以选 recent 里已有的需求继续${others.length || L.mode === 'repo' ? ',或加入 other_projects 里其他服务已有的需求' : ''}),然后运行 bind --name <需求名>;用户不给就运行 bind(按分支名生成)。`,
|
|
263
271
|
};
|
|
264
272
|
}
|
|
265
273
|
|
|
@@ -269,25 +277,28 @@ export function bind(root, { name = null, branch, base = null }) {
|
|
|
269
277
|
if (!branch) return { ok: false, error: '无法确定当前分支(detached HEAD?),请用 --branch 指定' };
|
|
270
278
|
const named = name != null && String(name).trim() !== '';
|
|
271
279
|
const display = named ? String(name).trim() : branch.replace(/\//g, '-');
|
|
272
|
-
//
|
|
273
|
-
const pool =
|
|
280
|
+
// 同名需求沿用(按所有项目找,跨服务的需求共用一个目录);目录名被别的需求占了就加 -2、-3
|
|
281
|
+
const pool = repoRequirements(root, L).map((r) => ({ slug: r.slug, name: r.name }));
|
|
274
282
|
const target = pool.find((r) => r.name === display) ?? null;
|
|
275
283
|
let slug = target?.slug;
|
|
276
284
|
if (!slug) {
|
|
277
285
|
slug = dirName(display);
|
|
278
|
-
for (let i = 2; pool.some((r) => r.slug === slug) ||
|
|
286
|
+
for (let i = 2; pool.some((r) => r.slug === slug) || isDir(L.reqDir(slug)); i++) slug = `${dirName(display)}-${i}`;
|
|
279
287
|
}
|
|
288
|
+
// 随代码提交时项目名是自动取的:第一次落盘时写进 profile.json 固定下来,之后改了 git 远端名也不会换目录
|
|
289
|
+
// (只在项目里装过 skills、有 profile.json 时写;直接从安装包运行脚本时不写)
|
|
290
|
+
if (!L.project_configured && fs.existsSync(path.join(profileDir(root), 'profile.json'))) setConfig(root, { project: L.project });
|
|
280
291
|
const dir = L.reqDir(slug);
|
|
281
292
|
const existedHere = isDir(dir);
|
|
282
293
|
fs.mkdirSync(dir, { recursive: true });
|
|
283
294
|
const f = path.join(dir, 'meta.json');
|
|
284
|
-
const meta = readJson(f) ?? { name: target?.name ?? display, named_by: named ? 'user' : 'branch',
|
|
295
|
+
const meta = readJson(f) ?? { name: target?.name ?? display, named_by: named ? 'user' : 'branch', project: L.project, branches: [], base, created: now() };
|
|
285
296
|
if (!meta.branches.includes(branch)) meta.branches.push(branch);
|
|
286
297
|
if (base && !meta.base) meta.base = base;
|
|
287
298
|
meta.updated = now();
|
|
288
299
|
fs.writeFileSync(f, `${JSON.stringify(meta, null, 2)}\n`);
|
|
289
300
|
const rel = relOf(root, dir);
|
|
290
|
-
return { ok: true, mode: L.mode, created: !existedHere, joined: !!target && !existedHere
|
|
301
|
+
return { ok: true, mode: L.mode, created: !existedHere, joined: !!target && !existedHere, name: meta.name, named_by: meta.named_by, dir: rel, project: L.project, branches: sync(root, rel).branches };
|
|
291
302
|
}
|
|
292
303
|
|
|
293
304
|
// 本次落盘路径:文件名固定(review.md …),已有时先把当前版复制到 tmp/old-artifacts/<种类>_v<N>.md(previous,复审对照用),
|
|
@@ -347,6 +358,55 @@ export function pending(root, branch, dirRel = null) {
|
|
|
347
358
|
};
|
|
348
359
|
}
|
|
349
360
|
|
|
361
|
+
// ------------------------------------------------------------------ 旧结构迁移
|
|
362
|
+
// 旧版本随代码提交时没有 <项目名>/ 这一层:文件直接放在 docs/requirements/<需求>/ 下。
|
|
363
|
+
// 列出这样的需求目录(目录里直接有文件的;隐藏文件不算)
|
|
364
|
+
export function legacyRequirements(root) {
|
|
365
|
+
const base = path.join(root, REQ_DIR);
|
|
366
|
+
if (!isDir(base)) return [];
|
|
367
|
+
return fs.readdirSync(base, { withFileTypes: true }).filter((d) => !skipDir(d)).map((d) => d.name)
|
|
368
|
+
.filter((slug) => fs.readdirSync(path.join(base, slug), { withFileTypes: true }).some((e) => e.isFile() && !e.name.startsWith('.')));
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// 把旧结构的需求目录挪到 <需求>/<项目名>/:被 git 跟踪的用 git mv(保留历史),其余直接移动;
|
|
372
|
+
// tmp/old-artifacts/<需求>/ 下归档的上一版也一起挪,复审对照还能找到。
|
|
373
|
+
// 目标里已有同名文件时这个需求整个跳过,留给用户处理。项目名没配置时按 layout 自动取的写进 profile.json。
|
|
374
|
+
// 返回 { project, moved: [{ from, to }], skipped: [{ dir, reason }] }
|
|
375
|
+
export function migrateLayout(root) {
|
|
376
|
+
const L = layout(root);
|
|
377
|
+
const slugs = legacyRequirements(root);
|
|
378
|
+
if (!slugs.length) return { project: L.project, moved: [], skipped: [] };
|
|
379
|
+
if (!L.project) return { project: null, moved: [], skipped: slugs.map((s) => ({ dir: `${REQ_DIR}/${s}`, reason: '还没配置项目名' })) };
|
|
380
|
+
const repoOf = (abs) => (L.mode === 'repo' ? L.repo : root);
|
|
381
|
+
const tracked = (abs) => git(repoOf(abs), ['ls-files', '--error-unmatch', '--', path.relative(repoOf(abs), abs)]).status === 0;
|
|
382
|
+
const moved = []; const skipped = [];
|
|
383
|
+
const move = (from, to, useGit) => {
|
|
384
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
385
|
+
const cwd = repoOf(from);
|
|
386
|
+
if (useGit && tracked(from) && git(cwd, ['mv', '--', path.relative(cwd, from), path.relative(cwd, to)]).status === 0) return;
|
|
387
|
+
fs.renameSync(from, to);
|
|
388
|
+
};
|
|
389
|
+
for (const slug of slugs) {
|
|
390
|
+
const dir = path.join(L.base, slug);
|
|
391
|
+
const files = fs.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isFile() && !e.name.startsWith('.')).map((e) => e.name);
|
|
392
|
+
const target = path.join(dir, L.project);
|
|
393
|
+
const clash = files.filter((f) => fs.existsSync(path.join(target, f)));
|
|
394
|
+
if (clash.length) { skipped.push({ dir: relOf(root, dir), reason: `${relOf(root, target)} 里已有 ${clash.join('、')}` }); continue; }
|
|
395
|
+
for (const f of files) {
|
|
396
|
+
move(path.join(dir, f), path.join(target, f), true);
|
|
397
|
+
moved.push({ from: relOf(root, path.join(dir, f)), to: relOf(root, path.join(target, f)) });
|
|
398
|
+
}
|
|
399
|
+
const m = readJson(path.join(target, 'meta.json'));
|
|
400
|
+
if (m && m.project !== L.project) fs.writeFileSync(path.join(target, 'meta.json'), `${JSON.stringify({ ...m, project: L.project }, null, 2)}\n`);
|
|
401
|
+
const old = path.join(root, OLD_DIR, slug);
|
|
402
|
+
if (isDir(old)) {
|
|
403
|
+
for (const e of fs.readdirSync(old, { withFileTypes: true })) if (e.isFile()) move(path.join(old, e.name), path.join(old, L.project, e.name), false);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (moved.length && !L.project_configured && fs.existsSync(path.join(profileDir(root), 'profile.json'))) setConfig(root, { project: L.project });
|
|
407
|
+
return { project: L.project, moved, skipped };
|
|
408
|
+
}
|
|
409
|
+
|
|
350
410
|
// ------------------------------------------------------------------ 配置
|
|
351
411
|
// 修改 artifacts 配置:团队共用的写 profile.json,--local 写个人的 profile.local.json(不提交)
|
|
352
412
|
export function setConfig(root, patch, { local = false } = {}) {
|
|
@@ -68,7 +68,8 @@ export function docnexusConnected(root) {
|
|
|
68
68
|
|
|
69
69
|
export function resolveTemplate(root, skill, id) {
|
|
70
70
|
const meta = skillMeta(skill);
|
|
71
|
-
|
|
71
|
+
if (!meta) return { ok: false, error: 'unknown', message: `skill「${skill}」没有安装(${rel(root, SKILLS_DIR)} 下找不到 ${skill}/skill.json);重跑安装并在 --skills 里带上它` };
|
|
72
|
+
const t = meta.templates.find((x) => x.id === id);
|
|
72
73
|
if (!t) return { ok: false, error: 'unknown', message: `skill「${skill}」没有登记模板「${id}」(见 ${skill}/skill.json 的 templates)` };
|
|
73
74
|
const lib = path.join(docnexusRoot(root), 'docnexus', 'library', 'templates', skill, id);
|
|
74
75
|
const project = path.join(root, TEMPLATES_DIR, skill, id);
|
|
@@ -195,8 +196,8 @@ export function status(root) {
|
|
|
195
196
|
categories: Object.fromEntries(skillMetas().map((m) => [m.name, resolveCategory(root, m.name)]).filter(([, c]) => c.ok).map(([n, c]) => [n, c.category])),
|
|
196
197
|
how: dn ? '缺模板时运行 npx @rowansenne/workflow-run seed-templates' : '重跑 npx @rowansenne/workflow-run 并加 --with-docnexus',
|
|
197
198
|
},
|
|
198
|
-
//
|
|
199
|
-
artifacts: (() => { const L = layout(root); return { mode: L.mode, ok: L.ok, ...(L.mode === 'repo' ? { repo: L.repo_rel,
|
|
199
|
+
// 产物位置:项目里(默认)或独立文档仓库,两种都是 docs/requirements/<需求>/<项目>/(workflow/references/requirements.md)
|
|
200
|
+
artifacts: (() => { const L = layout(root); return { mode: L.mode, ok: L.ok, dir: 'workflow-run/docs/requirements', project: L.project, ...(L.mode === 'repo' ? { repo: L.repo_rel, commit_prompt: L.commit_prompt } : {}), how: L.ok ? null : L.hint }; })(),
|
|
200
201
|
// 代码图谱:可用时读代码、查调用链与影响面优先用它(见 workflow/references/codegraph.md)
|
|
201
202
|
codegraph: codegraphStatus(root),
|
|
202
203
|
database: {
|
|
@@ -12,16 +12,17 @@ import path from 'node:path';
|
|
|
12
12
|
import { spawnSync } from 'node:child_process';
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
14
14
|
import { collect, KINDS } from './artifacts.mjs';
|
|
15
|
-
import {
|
|
15
|
+
import { repoRequirements, layout, parseDocnexus, REQ_DIR } from './requirement.mjs';
|
|
16
16
|
import { parseReview, parseDelivery, captureState, buildReport } from './report.mjs';
|
|
17
17
|
|
|
18
18
|
const DEFAULT_OUT = 'workflow-run/tmp/view/index.html';
|
|
19
19
|
|
|
20
20
|
// 页面数据:按需求分组
|
|
21
21
|
export function viewData(root) {
|
|
22
|
-
//
|
|
22
|
+
// 连同其他项目(服务)的产物一起看,一个需求下按项目区分(文档仓库里常见;两种模式目录结构相同)
|
|
23
23
|
const L = layout(root);
|
|
24
|
-
const
|
|
24
|
+
const reqDirOf = (slug) => (L.mode === 'repo' ? `${L.repo_name}:docs/requirements/${slug}` : `${REQ_DIR}/${slug}`);
|
|
25
|
+
const metas = new Map(repoRequirements(root, L).map((r) => [r.slug, { name: r.name, dir: reqDirOf(r.slug), branches: [...new Set(r.projects.flatMap((p) => p.meta.branches ?? []))], meta: r.projects.find((p) => p.project === L.project)?.meta ?? r.projects[0]?.meta ?? {} }]));
|
|
25
26
|
const groups = new Map();
|
|
26
27
|
for (const a of collect(root, { allProjects: true })) {
|
|
27
28
|
const id = a.requirement ? `req:${a.requirement}` : `branch:${a.branch ?? '(无分支)'}`;
|
|
@@ -29,7 +30,7 @@ export function viewData(root) {
|
|
|
29
30
|
const m = a.requirement ? metas.get(a.requirement) : null;
|
|
30
31
|
groups.set(id, {
|
|
31
32
|
id, name: m?.name ?? (a.requirement ?? a.branch ?? '(无分支)'),
|
|
32
|
-
dir: m?.dir ?? (a.requirement
|
|
33
|
+
dir: m?.dir ?? (a.requirement ? reqDirOf(a.requirement) : null), named_by: m?.meta?.named_by ?? null, branches: new Set(m?.branches ?? []), projects: new Set(), artifacts: [],
|
|
33
34
|
});
|
|
34
35
|
}
|
|
35
36
|
const g = groups.get(id);
|