paseo-req-kit 0.0.0-stage → 1.8.0-beta.3
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 +283 -0
- package/LICENSE +21 -0
- package/README.md +146 -2
- package/SCOPE.md +11 -0
- package/bin/locate.mjs +46 -0
- package/bin/req-doctor.mjs +21 -0
- package/bin/req.mjs +179 -0
- package/bin/start-pipeline +21 -0
- package/bin/start-pipeline.mjs +22 -0
- package/config.example.json +132 -0
- package/contracts/README.md +18 -0
- package/contracts/feature-files.md +117 -0
- package/contracts/notifier-event.md +72 -0
- package/contracts/result-file.md +21 -0
- package/contracts/spec.md +36 -0
- package/contracts/state.md +49 -0
- package/contracts/verify.md +41 -0
- package/docs/adr/0001-step-agents-under-origin-chat.md +15 -0
- package/docs/adr/0002-no-directory-switch-in-chat.md +15 -0
- package/docs/adr/0003-script-engine-not-hub.md +15 -0
- package/docs/adr/0004-result-files.md +15 -0
- package/docs/adr/0005-no-default-branch-local-commits-only.md +15 -0
- package/docs/adr/0006-skill-install-targets.md +15 -0
- package/docs/adr/0007-own-interview-skill.md +15 -0
- package/docs/adr/0008-entries-do-not-call-entries.md +15 -0
- package/docs/adr/0009-versioned-install-layout.md +15 -0
- package/docs/adr/0010-generated-feature-overview.md +15 -0
- package/docs/adr/0011-resume-after-completion.md +15 -0
- package/docs/adr/0012-review-panel.md +18 -0
- package/docs/adr/0013-no-release-tooling.md +15 -0
- package/docs/adr/0014-notifier-slot.md +19 -0
- package/docs/adr/0015-incremental-continue.md +17 -0
- package/docs/adr/0016-upgrade-from-tags.md +19 -0
- package/docs/adr/0017-use-paseo-capabilities.md +18 -0
- package/docs/adr/0018-own-worktree-option.md +21 -0
- package/docs/adr/0019-parallel-orchestration.md +21 -0
- package/docs/adr/0020-scheduling-through-the-chat.md +21 -0
- package/docs/adr/0021-npm-version-manager.md +19 -0
- package/docs/adr/0022-develop-and-verify-phases.md +32 -0
- package/docs/adr/0023-types-gate-slices.md +54 -0
- package/docs/adr/0024-confirm-early-review-axes.md +27 -0
- package/docs/adr/0025-joint-requirements.md +27 -0
- package/docs/adr/README.md +29 -0
- package/docs/guide//344/275/277/347/224/250/350/200/205/346/214/207/345/215/227.md +96 -0
- package/docs/guide//345/256/211/350/243/205/344/270/216/345/215/207/347/272/247.md +348 -0
- package/docs/guide//347/273/264/346/212/244/350/200/205/346/214/207/345/215/227.md +85 -0
- package/docs/guide//351/241/271/347/233/256/345/256/232/345/210/266/344/270/216/350/277/220/350/241/214/350/247/204/345/210/231.md +356 -0
- package/engine/prompts/00-shared-context.md +104 -0
- package/engine/prompts/02-spec-review.md +46 -0
- package/engine/prompts/03-design.md +73 -0
- package/engine/prompts/03a-design-proposal.md +30 -0
- package/engine/prompts/04-design-review.md +73 -0
- package/engine/prompts/04b-design-revise.md +41 -0
- package/engine/prompts/05-implement.md +72 -0
- package/engine/prompts/06-code-review.md +70 -0
- package/engine/prompts/07-review-fix.md +41 -0
- package/engine/prompts/07b-handoff.md +45 -0
- package/engine/prompts/08-service-start.md +59 -0
- package/engine/prompts/09-verify.md +90 -0
- package/engine/prompts/10-defect-fix.md +41 -0
- package/engine/prompts/10b-reverify.md +43 -0
- package/engine/prompts/11-report.md +112 -0
- package/engine/prompts/browser-verify.md +13 -0
- package/engine/prompts/incremental.md +22 -0
- package/engine/prompts/joint.md +55 -0
- package/engine/prompts/panel-member.md +38 -0
- package/engine/prompts/paseo-services.md +9 -0
- package/engine/prompts/related-repos.md +7 -0
- package/engine/prompts/review-axis-spec.md +10 -0
- package/engine/prompts/review-axis-standards.md +11 -0
- package/engine/prompts/types/assessment.md +41 -0
- package/engine/prompts/types/defect.md +55 -0
- package/engine/prompts/types/refactor.md +39 -0
- package/engine/prompts/verify-deployed.md +14 -0
- package/engine/src/activity.ts +87 -0
- package/engine/src/cli-args.ts +217 -0
- package/engine/src/config.ts +619 -0
- package/engine/src/configure.ts +237 -0
- package/engine/src/control.ts +246 -0
- package/engine/src/daemon-check.ts +104 -0
- package/engine/src/daemon-version.ts +66 -0
- package/engine/src/digest.ts +142 -0
- package/engine/src/doctor.ts +462 -0
- package/engine/src/eval.ts +324 -0
- package/engine/src/features.ts +270 -0
- package/engine/src/gate.ts +157 -0
- package/engine/src/git.ts +30 -0
- package/engine/src/joint.ts +173 -0
- package/engine/src/launch.ts +405 -0
- package/engine/src/lessons.ts +72 -0
- package/engine/src/locations.ts +55 -0
- package/engine/src/main.ts +346 -0
- package/engine/src/messages.ts +394 -0
- package/engine/src/notifiers.ts +76 -0
- package/engine/src/notify-test.ts +101 -0
- package/engine/src/paseo-json.ts +73 -0
- package/engine/src/paseo-profiles.ts +133 -0
- package/engine/src/paseo-runtime.ts +437 -0
- package/engine/src/paths.ts +10 -0
- package/engine/src/pipeline.ts +1840 -0
- package/engine/src/playbooks.ts +46 -0
- package/engine/src/preflight.ts +337 -0
- package/engine/src/prompts.ts +72 -0
- package/engine/src/queue.ts +61 -0
- package/engine/src/runtime-loader.ts +14 -0
- package/engine/src/runtime.ts +155 -0
- package/engine/src/schedule-time.ts +51 -0
- package/engine/src/smoke-test.ts +202 -0
- package/engine/src/spec-review.ts +100 -0
- package/engine/src/state.ts +333 -0
- package/engine/src/steps.ts +266 -0
- package/engine/src/usage.ts +67 -0
- package/engine/src/worktree.ts +72 -0
- package/install.mjs +928 -0
- package/notifiers/im-template.mjs +60 -0
- package/package.json +67 -4
- package/principles/INDEX.md +11 -0
- package/skills/acceptance-criteria/SKILL.md +61 -0
- package/skills/principle-delete-before-add/SKILL.md +33 -0
- package/skills/principle-dont-block-on-humans/SKILL.md +35 -0
- package/skills/principle-fix-root-cause/SKILL.md +33 -0
- package/skills/principle-prove-it-works/SKILL.md +33 -0
- package/skills/principle-report-honestly/SKILL.md +33 -0
- package/skills/principle-speak-to-reader/SKILL.md +33 -0
- package/skills/principle-this-requirement-only/SKILL.md +33 -0
- package/skills/req/SKILL.md +101 -0
- package/skills/req/playbooks/accept.md +65 -0
- package/skills/req/playbooks/change.md +51 -0
- package/skills/req/playbooks/incident.md +55 -0
- package/skills/req/playbooks/new.md +94 -0
- package/skills/req/playbooks/small.md +48 -0
- package/skills/req/playbooks/status.md +41 -0
- package/skills/req/playbooks/takeover.md +51 -0
- package/skills/req/playbooks/verify.md +57 -0
- package/skills/req-doctor/SKILL.md +36 -0
- package/skills/req-retro/SKILL.md +81 -0
- package/skills/req-setup/SKILL.md +118 -0
- package/skills/req-setup/templates/paseo/copy-local-files.mjs +32 -0
- package/skills/req-setup/templates/paseo/with-port.mjs +36 -0
- package/skills/requirement-interview/SKILL.md +97 -0
- package/skills/requirement-kickoff/SKILL.md +17 -0
- package/skills/result-report/SKILL.md +34 -0
- package/skills/spec-writing/SKILL.md +55 -0
- package/skills/spec-writing/spec-template.md +109 -0
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
// Text the pipeline sends into the chat that launched it, and follow-ups it
|
|
2
|
+
// sends to step agents. Messages to the chat are read by that chat's agent, who
|
|
3
|
+
// relays them to the person who asked for the change (not a developer), so each one says what to tell the user
|
|
4
|
+
// and what to do next.
|
|
5
|
+
|
|
6
|
+
const HEADER =
|
|
7
|
+
"【需求开发流水线通知】这条消息来自后台运行的开发流水线,不是用户发的。请把要点转告用户:讲需求和结果,不讲代码实现,除非用户问起。";
|
|
8
|
+
|
|
9
|
+
export function blockedMessage(input: {
|
|
10
|
+
featureDir: string;
|
|
11
|
+
questions: string[];
|
|
12
|
+
resumeCommand: string;
|
|
13
|
+
/** A reminder is scheduled after this many hours without an answer. */
|
|
14
|
+
reminderHours?: number | null;
|
|
15
|
+
/** "reproduce": the defect could not be reproduced (defect playbook); default: the design review. */
|
|
16
|
+
source?: "design_review" | "reproduce";
|
|
17
|
+
}): string {
|
|
18
|
+
const list = input.questions.map((q, i) => `${i + 1}. ${q}`).join("\n");
|
|
19
|
+
return [
|
|
20
|
+
HEADER,
|
|
21
|
+
"",
|
|
22
|
+
`功能目录:${input.featureDir}`,
|
|
23
|
+
input.source === "reproduce"
|
|
24
|
+
? "情况:复现与定位这一步没能复现这个缺陷,需要需求方补充信息(复现步骤、环境、账号或数据、截图或报错)。流水线已暂停,还没有修改代码。"
|
|
25
|
+
: "情况:方案审核发现有问题必须由需求方决定,否则可能做出错误的东西。流水线已暂停,还没有开始写代码。",
|
|
26
|
+
"",
|
|
27
|
+
"需要用户决定的问题:",
|
|
28
|
+
list,
|
|
29
|
+
"",
|
|
30
|
+
"请这样处理:",
|
|
31
|
+
"1. 按需求对齐时的方式,逐个向用户提问(给出选项和你的推荐)。",
|
|
32
|
+
"2. 把答案记入功能目录下的 alignment.md,并更新 spec.md(顶部\"版本\"加 1,在 changes.md 中记录这次变更)。",
|
|
33
|
+
"3. 把更新后的要点讲给用户听,请用户再次确认 spec(保持顶部为 \"- 状态:已确认\",并更新日期)。",
|
|
34
|
+
"4. 用户确认后执行下面的命令,流水线会从方案设计开始,在已有方案的基础上处理这些回答:",
|
|
35
|
+
"",
|
|
36
|
+
input.resumeCommand,
|
|
37
|
+
...(input.reminderHours ? ["", `(如果 ${input.reminderHours} 小时内还没有回答,会在这个对话里提醒一次。)`] : []),
|
|
38
|
+
].join("\n");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function permissionMessage(input: {
|
|
42
|
+
stepTitle: string;
|
|
43
|
+
agentTitle: string;
|
|
44
|
+
requests: string[];
|
|
45
|
+
}): string {
|
|
46
|
+
return [
|
|
47
|
+
HEADER,
|
|
48
|
+
"",
|
|
49
|
+
`流水线的「${input.stepTitle}」步骤在等待权限批准,批准前会一直停在这里:`,
|
|
50
|
+
...input.requests.map((r) => `- ${r}`),
|
|
51
|
+
"",
|
|
52
|
+
`请告诉用户:在 Paseo 中打开名为「${input.agentTitle}」的 agent,查看并批准这个请求(或拒绝)。`,
|
|
53
|
+
"如果这类等待经常出现,说明流水线配置中的 agent 模式还会询问权限,需要把配置中的 agent 改成免确认模式(运行 /req-doctor 可以查看当前配置)。",
|
|
54
|
+
].join("\n");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function failedMessage(input: {
|
|
58
|
+
stepTitle: string;
|
|
59
|
+
reason: string;
|
|
60
|
+
logPath: string;
|
|
61
|
+
resumeCommand: string;
|
|
62
|
+
/** An automatic continuation is scheduled at this time (text). */
|
|
63
|
+
autoResumeAt?: string | null;
|
|
64
|
+
}): string {
|
|
65
|
+
return [
|
|
66
|
+
HEADER,
|
|
67
|
+
"",
|
|
68
|
+
`流水线在「${input.stepTitle}」步骤停止了。`,
|
|
69
|
+
`原因:${input.reason}`,
|
|
70
|
+
"",
|
|
71
|
+
"已完成的步骤会保留。请告诉用户开发暂时中断,并说明原因。用户可以先看日志判断原因;涉及环境、配置或代码的问题请开发同事处理。",
|
|
72
|
+
`日志:${input.logPath}`,
|
|
73
|
+
...(input.autoResumeAt
|
|
74
|
+
? [
|
|
75
|
+
`这是临时错误(例如限流或网络中断),已预约在 ${input.autoResumeAt} 自动继续(Paseo 定时任务,到时会在这个对话里执行继续命令)。请告诉用户这一点;用户不想自动继续时,执行暂停命令即可取消。`,
|
|
76
|
+
"也可以现在就执行下面的命令继续:",
|
|
77
|
+
]
|
|
78
|
+
: ["问题解决后,执行下面的命令可以从中断的步骤继续:"]),
|
|
79
|
+
"",
|
|
80
|
+
input.resumeCommand,
|
|
81
|
+
].join("\n");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const OUTCOME_TEXT: Record<string, string> = {
|
|
85
|
+
completed: "已完成,可以验收",
|
|
86
|
+
completed_with_issues: "已完成,但有遗留问题",
|
|
87
|
+
not_completed: "未完成",
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export function completedMessage(input: {
|
|
91
|
+
featureDir: string;
|
|
92
|
+
outcome: string;
|
|
93
|
+
summary: string;
|
|
94
|
+
reportPath: string;
|
|
95
|
+
/** Set for a develop-only run: people test by hand from this handoff note. */
|
|
96
|
+
handoffPath?: string | null;
|
|
97
|
+
}): string {
|
|
98
|
+
if (input.handoffPath) {
|
|
99
|
+
return [
|
|
100
|
+
HEADER,
|
|
101
|
+
"",
|
|
102
|
+
`需求的研发已经完成(执行方式:只做研发,流水线没有做测试验证)。研发结论:${OUTCOME_TEXT[input.outcome] ?? input.outcome}`,
|
|
103
|
+
"",
|
|
104
|
+
"摘要:",
|
|
105
|
+
input.summary,
|
|
106
|
+
"",
|
|
107
|
+
`研发报告:${input.reportPath}`,
|
|
108
|
+
`提测说明:${input.handoffPath}`,
|
|
109
|
+
"",
|
|
110
|
+
"请把结论和摘要转告用户,说明接下来需要人工测试:按 /req 的验收手册,以提测说明中的“建议重点测试”和验收标准为线索,带用户逐条测试。",
|
|
111
|
+
"如果之后想让流水线做测试验证,在用户同意后执行继续命令并加上 --phase verify。",
|
|
112
|
+
].join("\n");
|
|
113
|
+
}
|
|
114
|
+
return [
|
|
115
|
+
HEADER,
|
|
116
|
+
"",
|
|
117
|
+
`需求开发流水线已经结束。结论:${OUTCOME_TEXT[input.outcome] ?? input.outcome}`,
|
|
118
|
+
"",
|
|
119
|
+
"摘要:",
|
|
120
|
+
input.summary,
|
|
121
|
+
"",
|
|
122
|
+
`完整报告:${input.reportPath}`,
|
|
123
|
+
"",
|
|
124
|
+
"请把结论和摘要转告用户,并读一下报告中「需要你做的事」一节,提醒用户接下来要做什么。用户有疑问时,以报告和功能目录中的文件为准来回答。",
|
|
125
|
+
].join("\n");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function resultFollowUp(input: { resultFile: string; problem: string }): string {
|
|
129
|
+
return [
|
|
130
|
+
`流水线没有拿到本步骤的有效结果:${input.problem}。`,
|
|
131
|
+
`如果本步骤的工作还没有完成,请先完成它;然后按照"本步骤任务"中"结果文件"一节的要求,把结果写入 ${input.resultFile}(只含一个 JSON 对象),再结束。`,
|
|
132
|
+
].join("\n");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Sent to the step agent when checks that passed before its change now fail (the quality gate). */
|
|
136
|
+
export function gateFollowUp(input: {
|
|
137
|
+
failures: Array<{ name: string; command: string; cwd: string | null; logFile: string; tail: string }>;
|
|
138
|
+
round: number;
|
|
139
|
+
max: number;
|
|
140
|
+
/** Checks that already failed before this requirement's change: not this step's problem. */
|
|
141
|
+
preexisting: string[];
|
|
142
|
+
}): string {
|
|
143
|
+
const parts = [
|
|
144
|
+
`流水线在你这一步结束后运行了项目的检查(质量门),以下检查在改动之前通过、现在失败(第 ${input.round} 次要求修复,最多 ${input.max} 次):`,
|
|
145
|
+
"",
|
|
146
|
+
];
|
|
147
|
+
for (const f of input.failures) {
|
|
148
|
+
parts.push(`### ${f.name}`, "", `命令:\`${f.command}\`${f.cwd ? `(在 ${f.cwd} 目录)` : ""}`, `完整输出:${f.logFile}`, "", "最后的输出:", "", "```text", f.tail, "```", "");
|
|
149
|
+
}
|
|
150
|
+
if (input.preexisting.length > 0) parts.push(`(${input.preexisting.join("、")} 在改动之前就失败,不算本次引入,不需要处理。)`, "");
|
|
151
|
+
parts.push(
|
|
152
|
+
"请找到原因并修复,让这些检查重新通过;修复要针对原因,不要删除、跳过或放宽检查和测试。修完自己运行一遍这些命令确认通过,提交,并在本步骤的产物里记一笔(哪个检查、原因、怎么修的);结果文件按需要更新。",
|
|
153
|
+
"确实修不了的(例如需要环境或权限),在产物里写明原因后照常结束,流水线会把它带进评审和报告。",
|
|
154
|
+
);
|
|
155
|
+
return parts.join("\n");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function milestoneMessage(text: string): string {
|
|
159
|
+
return [
|
|
160
|
+
"【需求开发流水线通知 · 进展】这条消息来自后台流水线,不是用户发的。仅告知进展,用一句话转告用户即可,不需要其他操作。",
|
|
161
|
+
"",
|
|
162
|
+
text,
|
|
163
|
+
].join("\n");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function specReviewMessage(input: { featureDir: string; verdict: string; issues: string[]; reviewPath: string }): string {
|
|
167
|
+
if (input.verdict === "ready") {
|
|
168
|
+
return [
|
|
169
|
+
HEADER,
|
|
170
|
+
"",
|
|
171
|
+
`功能目录:${input.featureDir}`,
|
|
172
|
+
`需求说明的独立审阅已完成:没有发现需要澄清的问题(详见 ${input.reviewPath})。`,
|
|
173
|
+
"请告诉用户这一点,然后继续请用户确认需求说明。",
|
|
174
|
+
].join("\n");
|
|
175
|
+
}
|
|
176
|
+
return [
|
|
177
|
+
HEADER,
|
|
178
|
+
"",
|
|
179
|
+
`功能目录:${input.featureDir}`,
|
|
180
|
+
`需求说明的独立审阅已完成:另一个模型发现 ${input.issues.length} 个建议在确认前澄清的问题,详见 ${input.reviewPath}(其中每个问题都写成了带选项和推荐的选择题):`,
|
|
181
|
+
"",
|
|
182
|
+
...input.issues.map((q, i) => `${i + 1}. ${q}`),
|
|
183
|
+
"",
|
|
184
|
+
"请这样处理:",
|
|
185
|
+
"1. 先读审阅文件,自己核对一遍:不成立的问题直接略过,并在最后告诉用户略过了哪些、为什么。",
|
|
186
|
+
"2. 成立的问题,按需求对齐时的方式一次一个地问用户(用审阅文件中的选项和推荐)。",
|
|
187
|
+
"3. 把答案记入 alignment.md,更新 spec.md,再请用户确认需求说明。",
|
|
188
|
+
].join("\n");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function specReviewFailedMessage(input: { featureDir: string; reason: string }): string {
|
|
192
|
+
return [
|
|
193
|
+
HEADER,
|
|
194
|
+
"",
|
|
195
|
+
`功能目录:${input.featureDir}`,
|
|
196
|
+
`需求说明的独立审阅没有完成:${input.reason}`,
|
|
197
|
+
"这不影响继续:请告诉用户审阅没能完成,然后照常请用户确认需求说明。",
|
|
198
|
+
].join("\n");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Sent by a Paseo schedule into the chat when a decision has been waited for a long time. */
|
|
202
|
+
export function waitingReminderMessage(input: { featureDir: string; hours: number; questions: string[]; resumeCommand: string }): string {
|
|
203
|
+
return [
|
|
204
|
+
"【需求开发流水线通知】这是一条预约的提醒,不是用户发的。",
|
|
205
|
+
"",
|
|
206
|
+
`功能目录:${input.featureDir}`,
|
|
207
|
+
`这个需求在等需求方回答方案审核提出的问题,已经超过 ${input.hours} 小时,开发一直暂停着:`,
|
|
208
|
+
...input.questions.map((q, i) => `${i + 1}. ${q}`),
|
|
209
|
+
"",
|
|
210
|
+
"请先执行查看进度的命令确认:仍然是\"等待需求方决定\"时,在用户下次说话时提醒他这几个问题,并按之前的方式逐个询问;用户已经回答、开发已经继续或停止时,忽略这条提醒,不要打扰用户。",
|
|
211
|
+
`回答完、更新需求说明后,继续的命令是:${input.resumeCommand}`,
|
|
212
|
+
].join("\n");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Sent by a Paseo schedule into the chat to continue after a transient error. */
|
|
216
|
+
export function autoResumePrompt(input: { featureDir: string; reason: string; statusCommand: string; resumeCommand: string }): string {
|
|
217
|
+
return [
|
|
218
|
+
"【需求开发流水线通知】这是一条预约的自动继续,不是用户发的。",
|
|
219
|
+
"",
|
|
220
|
+
`功能目录:${input.featureDir}`,
|
|
221
|
+
`开发流水线之前因为临时错误中断:${input.reason}`,
|
|
222
|
+
"",
|
|
223
|
+
"请这样处理:",
|
|
224
|
+
`1. 先执行查看进度的命令:${input.statusCommand}`,
|
|
225
|
+
"2. 仍然是\"已中断,可以继续\"时,执行下面的继续命令,然后用一句话告诉用户已经自动继续:",
|
|
226
|
+
` ${input.resumeCommand}`,
|
|
227
|
+
"3. 其他情况(已经在运行、已完成、用户已经手动处理过):什么也不做,也不要回复用户。",
|
|
228
|
+
].join("\n");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Sent by a Paseo schedule into the chat while a pipeline runs (watchdogMinutes). */
|
|
232
|
+
export function watchdogPrompt(input: { featureDir: string; statusCommand: string; resumeCommand: string }): string {
|
|
233
|
+
return [
|
|
234
|
+
"【需求开发流水线通知】这是一条定时检查,不是用户发的。",
|
|
235
|
+
"",
|
|
236
|
+
`请执行查看进度的命令:${input.statusCommand}`,
|
|
237
|
+
`- 输出中的状态是"已中断(后台进程意外退出,可以继续)":执行继续命令 ${input.resumeCommand},然后用一句话告诉用户开发在意外中断后已经自动继续。`,
|
|
238
|
+
"- 其他任何情况:什么也不做,不要回复用户,不要打扰用户。",
|
|
239
|
+
`(功能目录:${input.featureDir}。这个检查在流水线结束后会自动取消。)`,
|
|
240
|
+
].join("\n");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function queuedMessage(input: { featureDir: string; max: number; holders: Array<{ featureDir: string; repo: string }> }): string {
|
|
244
|
+
return [
|
|
245
|
+
HEADER,
|
|
246
|
+
"",
|
|
247
|
+
`功能目录:${input.featureDir}`,
|
|
248
|
+
`这台机器同时最多运行 ${input.max} 条开发流水线,现在已经满了,这个需求在排队,轮到时会自动开始并通知这个对话:`,
|
|
249
|
+
...input.holders.map((h) => `- 正在运行:${h.featureDir}(${h.repo})`),
|
|
250
|
+
"",
|
|
251
|
+
"请把这一点告诉用户。不想等的话,可以执行暂停命令取消排队。",
|
|
252
|
+
].join("\n");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function dequeuedMessage(input: { featureDir: string; waitedMinutes: number }): string {
|
|
256
|
+
return [HEADER, "", `功能目录:${input.featureDir}`, `排队结束(等了约 ${Math.max(1, Math.round(input.waitedMinutes))} 分钟),开发已经开始。请用一句话告诉用户。`].join("\n");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function scheduledStartPrompt(input: { featureDir: string; command: string }): string {
|
|
260
|
+
return [
|
|
261
|
+
"【需求开发流水线通知】这是用户之前预约的开发开始时间,不是用户现在发的消息。",
|
|
262
|
+
"",
|
|
263
|
+
`功能目录:${input.featureDir}`,
|
|
264
|
+
"请执行下面的启动命令,然后用一句话告诉用户预约的开发已经开始(输出\"未启动\"时,按 /req 中的说明处理并告诉用户原因):",
|
|
265
|
+
"",
|
|
266
|
+
input.command,
|
|
267
|
+
].join("\n");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const ENVIRONMENT_HINT = {
|
|
271
|
+
local: "测试验证会在本地启动服务进行。",
|
|
272
|
+
deployed: "测试验证会在已部署的测试环境中进行:开始前要先把版本部署到测试环境,并在功能目录写好 test-env.md(地址、账号从哪里取、可以用的测试数据;不写口令本身)。",
|
|
273
|
+
} as const;
|
|
274
|
+
|
|
275
|
+
/** The develop phase is done and the run stopped at the gate (phases.mode "gated"). */
|
|
276
|
+
export function phaseDoneMessage(input: {
|
|
277
|
+
featureDir: string;
|
|
278
|
+
handoffPath: string;
|
|
279
|
+
verifyCommand: string;
|
|
280
|
+
environment: "local" | "deployed";
|
|
281
|
+
/** The verify phase is scheduled at this time (text). */
|
|
282
|
+
verifyAt?: string | null;
|
|
283
|
+
/** A reminder is scheduled after this many hours. */
|
|
284
|
+
reminderHours?: number | null;
|
|
285
|
+
/** How many times verification has handed defects back so far. */
|
|
286
|
+
handbacks?: number;
|
|
287
|
+
}): string {
|
|
288
|
+
return [
|
|
289
|
+
HEADER,
|
|
290
|
+
"",
|
|
291
|
+
`功能目录:${input.featureDir}`,
|
|
292
|
+
`研发阶段已经完成${input.handbacks ? `(这是测试退回后第 ${input.handbacks} 次修复完成)` : ""}:代码已写完并通过代码评审,提测说明在 ${input.handoffPath}。按这个需求的执行方式(分阶段),流水线在这里停下,等待测试验证。`,
|
|
293
|
+
"",
|
|
294
|
+
"请这样处理:",
|
|
295
|
+
"1. 告诉用户研发已完成、可以提测,用两三句话讲提测说明里\"这次改了什么\"和\"建议重点测试\"。",
|
|
296
|
+
`2. ${ENVIRONMENT_HINT[input.environment]}`,
|
|
297
|
+
input.verifyAt
|
|
298
|
+
? `3. 已预约在 ${input.verifyAt} 自动开始测试验证(Paseo 定时任务)。用户想提前开始或改用别的方式时,按 /req 的"开始测试验证"手册处理;想取消预约,执行暂停命令。`
|
|
299
|
+
: "3. 用户(或测试同事)准备好之后,按 /req 的\"开始测试验证\"手册处理;确认后执行下面的命令:",
|
|
300
|
+
"",
|
|
301
|
+
input.verifyCommand,
|
|
302
|
+
...(input.reminderHours && !input.verifyAt ? ["", `(如果 ${input.reminderHours} 小时内还没有开始测试验证,会在这个对话里提醒一次。)`] : []),
|
|
303
|
+
].join("\n");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Sent by a Paseo schedule when a requirement has waited at the gate for a long time. */
|
|
307
|
+
export function phaseWaitingReminderMessage(input: { featureDir: string; hours: number; handoffPath: string; verifyCommand: string }): string {
|
|
308
|
+
return [
|
|
309
|
+
"【需求开发流水线通知】这是一条预约的提醒,不是用户发的。",
|
|
310
|
+
"",
|
|
311
|
+
`功能目录:${input.featureDir}`,
|
|
312
|
+
`这个需求的研发已经完成超过 ${input.hours} 小时,一直在等待测试验证(提测说明:${input.handoffPath})。`,
|
|
313
|
+
"",
|
|
314
|
+
"请先执行查看进度的命令确认:仍然是\"研发完成,待测试验证\"时,在用户下次说话时提醒他可以提测了;已经开始测试验证、已完成或已停止时,忽略这条提醒,不要打扰用户。",
|
|
315
|
+
`开始测试验证的命令是:${input.verifyCommand}`,
|
|
316
|
+
].join("\n");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Sent by a Paseo schedule at the time given with --verify-at. */
|
|
320
|
+
export function verifyStartPrompt(input: { featureDir: string; statusCommand: string; verifyCommand: string; environment: "local" | "deployed" }): string {
|
|
321
|
+
return [
|
|
322
|
+
"【需求开发流水线通知】这是用户之前预约的测试验证开始时间,不是用户现在发的消息。",
|
|
323
|
+
"",
|
|
324
|
+
`功能目录:${input.featureDir}`,
|
|
325
|
+
"请这样处理:",
|
|
326
|
+
`1. 先执行查看进度的命令:${input.statusCommand}`,
|
|
327
|
+
"2. 状态是\"研发完成,待测试验证\"时:" +
|
|
328
|
+
(input.environment === "deployed"
|
|
329
|
+
? "检查功能目录里有 test-env.md;有就执行下面的命令,没有就不要启动,等用户下次说话时告诉他需要先写好测试环境说明。"
|
|
330
|
+
: "执行下面的命令。") +
|
|
331
|
+
"然后用一句话告诉用户预约的测试验证已经开始(输出\"未启动\"时告诉用户原因):",
|
|
332
|
+
` ${input.verifyCommand}`,
|
|
333
|
+
"3. 其他情况(已经开始、已完成、用户已经手动处理过):什么也不做,也不要回复用户。",
|
|
334
|
+
].join("\n");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** The design waits at the confirmation point (designApproval). */
|
|
338
|
+
export function approvalMessage(input: {
|
|
339
|
+
featureDir: string;
|
|
340
|
+
previewPath: string;
|
|
341
|
+
/** The prototype (UI changes), when the design made one. */
|
|
342
|
+
prototypePath: string | null;
|
|
343
|
+
approveCommand: string;
|
|
344
|
+
resumeCommand: string;
|
|
345
|
+
reminderHours?: number | null;
|
|
346
|
+
/** The run carries on with the design at this time (text) without an answer. */
|
|
347
|
+
autoContinueAt?: string | null;
|
|
348
|
+
}): string {
|
|
349
|
+
return [
|
|
350
|
+
`${HEADER.replace("【需求开发流水线通知】", "【需求开发流水线通知 · 请确认方案】")}`,
|
|
351
|
+
"",
|
|
352
|
+
`功能目录:${input.featureDir}`,
|
|
353
|
+
"情况:方案已经通过审核,还没有开始写代码。开发前请需求方确认\"做完之后会看到什么\",这时调整的成本最低。",
|
|
354
|
+
`- 做完后你会看到什么:${input.previewPath}`,
|
|
355
|
+
...(input.prototypePath ? [`- 界面原型:${input.prototypePath}(静态页面,可以在浏览器中打开)`] : []),
|
|
356
|
+
"",
|
|
357
|
+
"请这样处理:",
|
|
358
|
+
"1. 读 preview.md,用几句话把要点讲给用户:会看到的变化、主要的假设(尤其是影响界面、数据、权限的),有原型就告诉用户在哪里看。",
|
|
359
|
+
"2. 问用户:按这个做,还是有要改的地方?",
|
|
360
|
+
`3. 用户确认后,执行下面的命令继续开发:\n\n${input.approveCommand}`,
|
|
361
|
+
"4. 用户要改的:按需求对齐时的方式问清楚,把结论记入 alignment.md,更新 spec.md(顶部\"版本\"加 1,在 changes.md 中记一节,原因写\"方案确认时的修改\"),请用户再次确认需求说明,然后执行下面的命令,流水线会从方案设计开始按修改后的需求调整方案,再请用户确认一次:",
|
|
362
|
+
"",
|
|
363
|
+
input.resumeCommand,
|
|
364
|
+
...(input.reminderHours ? ["", `(如果 ${input.reminderHours} 小时内还没有确认,会在这个对话里提醒一次。)`] : []),
|
|
365
|
+
...(input.autoContinueAt ? ["", `(按团队设置,到 ${input.autoContinueAt} 仍没有确认时,会按这个方案继续开发。)`] : []),
|
|
366
|
+
].join("\n");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function approvalReminderMessage(input: { featureDir: string; hours: number; previewPath: string; approveCommand: string }): string {
|
|
370
|
+
return [
|
|
371
|
+
"【需求开发流水线通知】这是一条预约的提醒,不是用户发的。",
|
|
372
|
+
"",
|
|
373
|
+
`功能目录:${input.featureDir}`,
|
|
374
|
+
`这个需求的方案等待需求方确认已经超过 ${input.hours} 小时,开发一直暂停着(要确认的内容:${input.previewPath})。`,
|
|
375
|
+
"",
|
|
376
|
+
"请先执行查看进度的命令确认:仍然是\"等待需求方确认方案\"时,在用户下次说话时提醒他看一下并确认;已经确认、开发已经继续或停止时,忽略这条提醒,不要打扰用户。",
|
|
377
|
+
`用户确认后继续开发的命令是:${input.approveCommand}`,
|
|
378
|
+
].join("\n");
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function approvalContinuePrompt(input: { featureDir: string; hours: number; statusCommand: string; approveCommand: string }): string {
|
|
382
|
+
return [
|
|
383
|
+
"【需求开发流水线通知】这是一条预约的自动继续,不是用户发的。",
|
|
384
|
+
"",
|
|
385
|
+
`功能目录:${input.featureDir}`,
|
|
386
|
+
`这个需求的方案等待确认已经 ${input.hours} 小时。按团队的设置,超时没有确认时按方案继续开发。`,
|
|
387
|
+
"",
|
|
388
|
+
"请这样处理:",
|
|
389
|
+
`1. 先执行查看进度的命令:${input.statusCommand}`,
|
|
390
|
+
"2. 仍然是\"等待需求方确认方案\"时,执行下面的命令,然后用一句话告诉用户:方案一直没有确认,已按方案继续开发,之后仍可以修改需求:",
|
|
391
|
+
` ${input.approveCommand}`,
|
|
392
|
+
"3. 其他情况(已经确认、需求已修改、已经停止):什么也不做,也不要回复用户。",
|
|
393
|
+
].join("\n");
|
|
394
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// External notifiers: local modules the engine calls at key events, in addition to
|
|
2
|
+
// the message it sends into the originating chat. This is the slot for an internal
|
|
3
|
+
// IM (or any other channel); see notifiers/im-template.mjs and contracts/notifier-event.md.
|
|
4
|
+
//
|
|
5
|
+
// A notifier never affects the pipeline: errors and timeouts are logged and ignored.
|
|
6
|
+
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
8
|
+
|
|
9
|
+
export const NOTIFIER_EVENT_TYPES = ["milestone", "blocked", "permission", "failed", "completed", "digest", "phase_done", "approval"] as const;
|
|
10
|
+
export type NotifierEventType = (typeof NOTIFIER_EVENT_TYPES)[number];
|
|
11
|
+
|
|
12
|
+
export interface NotifierConfig {
|
|
13
|
+
/** Absolute path of the module (resolved when the config is loaded). */
|
|
14
|
+
module: string;
|
|
15
|
+
/** Events to send; all of them when omitted. */
|
|
16
|
+
events?: NotifierEventType[];
|
|
17
|
+
/** Passed to the module as its second argument, e.g. a webhook address or a group ID. */
|
|
18
|
+
options?: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface NotifierEvent {
|
|
22
|
+
/** Format version of this object. */
|
|
23
|
+
schema: 1;
|
|
24
|
+
type: NotifierEventType;
|
|
25
|
+
at: string;
|
|
26
|
+
kitVersion: string;
|
|
27
|
+
feature: { dir: string; name: string; title: string };
|
|
28
|
+
repo: string;
|
|
29
|
+
branch: string | null;
|
|
30
|
+
profile: string;
|
|
31
|
+
/** Chat agent the run reports to, if any. */
|
|
32
|
+
originAgentId: string | null;
|
|
33
|
+
/** One or two plain sentences, ready to post as is. */
|
|
34
|
+
text: string;
|
|
35
|
+
/** Type-specific fields; see contracts/notifier-event.md. */
|
|
36
|
+
details: Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type NotifierFunction = (event: NotifierEvent, options: Record<string, unknown>) => unknown;
|
|
40
|
+
|
|
41
|
+
const TIMEOUT_MS = 15_000;
|
|
42
|
+
|
|
43
|
+
async function loadNotifier(file: string): Promise<NotifierFunction> {
|
|
44
|
+
const module = (await import(pathToFileURL(file).href)) as { default?: unknown; notify?: unknown };
|
|
45
|
+
const fn = module.default ?? module.notify;
|
|
46
|
+
if (typeof fn !== "function") throw new Error(`${file} 没有导出 notify 函数(export default 或 export function notify)`);
|
|
47
|
+
return fn as NotifierFunction;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Calls every configured notifier that wants this event. Never throws. */
|
|
51
|
+
export async function dispatchNotifiers(
|
|
52
|
+
notifiers: NotifierConfig[],
|
|
53
|
+
event: NotifierEvent,
|
|
54
|
+
log: (message: string) => void,
|
|
55
|
+
): Promise<void> {
|
|
56
|
+
const wanted = notifiers.filter((n) => !n.events || n.events.includes(event.type));
|
|
57
|
+
await Promise.all(
|
|
58
|
+
wanted.map(async (n) => {
|
|
59
|
+
let timer: NodeJS.Timeout | undefined;
|
|
60
|
+
try {
|
|
61
|
+
const fn = await loadNotifier(n.module);
|
|
62
|
+
await Promise.race([
|
|
63
|
+
Promise.resolve(fn(event, n.options ?? {})),
|
|
64
|
+
new Promise((_, reject) => {
|
|
65
|
+
timer = setTimeout(() => reject(new Error(`超过 ${TIMEOUT_MS / 1000} 秒没有完成`)), TIMEOUT_MS);
|
|
66
|
+
}),
|
|
67
|
+
]);
|
|
68
|
+
log(`外部通知已发送(${event.type}):${n.module}`);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
log(`外部通知失败(${event.type},${n.module}):${(error as Error).message}。不影响流水线。`);
|
|
71
|
+
} finally {
|
|
72
|
+
if (timer) clearTimeout(timer);
|
|
73
|
+
}
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Sends one test event to every configured notifier: `req notify-test [--type blocked] [--repo <dir>]`.
|
|
2
|
+
// Lets whoever implements the IM module check it without running a pipeline.
|
|
3
|
+
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { parseArgs } from "node:util";
|
|
7
|
+
import { loadConfig } from "./config.js";
|
|
8
|
+
import { defaultConfigPath, kitVersion } from "./locations.js";
|
|
9
|
+
import { dispatchNotifiers, NOTIFIER_EVENT_TYPES, type NotifierEvent, type NotifierEventType } from "./notifiers.js";
|
|
10
|
+
|
|
11
|
+
/** A realistic event of the given type, with the details the pipeline would send. */
|
|
12
|
+
export function sampleEvent(type: NotifierEventType, repo: string): NotifierEvent {
|
|
13
|
+
const featureDir = "docs/features/20261012-sms-login";
|
|
14
|
+
const resume = `node "<req-kit>/bin/start-pipeline.mjs" --feature ${featureDir} --resume`;
|
|
15
|
+
const samples: Record<NotifierEventType, { text: string; details: Record<string, unknown> }> = {
|
|
16
|
+
milestone: { text: "技术方案已经确定(审核 1 轮),开始写代码。", details: { milestone: "design-settled" } },
|
|
17
|
+
blocked: {
|
|
18
|
+
text: "方案审核发现 1 个需要需求方决定的问题,流水线已暂停,请到发起的对话中回答。",
|
|
19
|
+
details: { questions: ["验证码有效期是 5 分钟还是 10 分钟?"], resumeCommand: resume },
|
|
20
|
+
},
|
|
21
|
+
permission: {
|
|
22
|
+
text: "「实现」步骤在等待权限批准,批准前会一直停在这里。",
|
|
23
|
+
details: { stepTitle: "实现", agentTitle: "实现 · 短信验证码登录", requests: ["运行 npm install"] },
|
|
24
|
+
},
|
|
25
|
+
failed: {
|
|
26
|
+
text: "开发流水线在「代码评审」步骤中断:agent 连续出错。",
|
|
27
|
+
details: { stepTitle: "代码评审", reason: "agent 连续出错", logPath: `${featureDir}/.pipeline/pipeline.log`, resumeCommand: resume },
|
|
28
|
+
},
|
|
29
|
+
digest: {
|
|
30
|
+
text: "【req-kit 需求总览】shop\n共 3 个需求:需要处理 1,待验收 1,开发中 1。",
|
|
31
|
+
details: { features: [{ name: "20261012-sms-login", title: "短信验证码登录", stage: "待验收" }] },
|
|
32
|
+
},
|
|
33
|
+
phase_done: {
|
|
34
|
+
text: `研发已完成,可以提测:提测说明在 ${featureDir}/handoff.md。`,
|
|
35
|
+
details: { handoffPath: `${featureDir}/handoff.md`, mode: "gated", verifyCommand: `${resume} --phase verify`, verifyAt: null, handbacks: 0 },
|
|
36
|
+
},
|
|
37
|
+
approval: {
|
|
38
|
+
text: `方案已通过审核,请需求方确认"做完后会看到什么"再开始写代码:${featureDir}/preview.md`,
|
|
39
|
+
details: { previewPath: `${featureDir}/preview.md`, approveCommand: resume.replace(/--resume$/, "--approve"), resumeCommand: resume },
|
|
40
|
+
},
|
|
41
|
+
completed: {
|
|
42
|
+
text: "开发流水线已结束:已完成,可以验收。",
|
|
43
|
+
details: { outcome: "completed", summary: "6 条验收标准全部通过。", reportPath: `${featureDir}/report.md` },
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
schema: 1,
|
|
48
|
+
type,
|
|
49
|
+
at: new Date().toISOString(),
|
|
50
|
+
kitVersion: kitVersion(),
|
|
51
|
+
feature: { dir: featureDir, name: "20261012-sms-login", title: "短信验证码登录(测试消息)" },
|
|
52
|
+
repo,
|
|
53
|
+
branch: "feature/20261012-sms-login",
|
|
54
|
+
profile: "standard",
|
|
55
|
+
originAgentId: null,
|
|
56
|
+
text: `【测试】${samples[type].text}`,
|
|
57
|
+
details: samples[type].details,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function main(): Promise<number> {
|
|
62
|
+
const { values } = parseArgs({
|
|
63
|
+
args: process.argv.slice(2),
|
|
64
|
+
options: { type: { type: "string", default: "completed" }, repo: { type: "string" }, config: { type: "string" } },
|
|
65
|
+
});
|
|
66
|
+
const type = values.type as NotifierEventType;
|
|
67
|
+
if (!NOTIFIER_EVENT_TYPES.includes(type)) {
|
|
68
|
+
console.error(`--type 只能是 ${NOTIFIER_EVENT_TYPES.join("、")} 之一`);
|
|
69
|
+
return 2;
|
|
70
|
+
}
|
|
71
|
+
const repo = path.resolve(values.repo ?? process.cwd());
|
|
72
|
+
const configPath = values.config ? path.resolve(values.config) : defaultConfigPath();
|
|
73
|
+
const { config } = loadConfig(configPath, fs.existsSync(repo) ? repo : undefined);
|
|
74
|
+
if (config.notifiers.length === 0) {
|
|
75
|
+
console.log(`没有配置外部通知(${configPath} 中的 notifiers 为空)。写法见 notifiers/im-template.mjs 开头的说明。`);
|
|
76
|
+
return 1;
|
|
77
|
+
}
|
|
78
|
+
const wanted = config.notifiers.filter((n) => !n.events || n.events.includes(type));
|
|
79
|
+
if (wanted.length === 0) {
|
|
80
|
+
console.log(`配置的通知都没有订阅 ${type} 事件。可以用 --type 换一种,例如 --type ${config.notifiers[0].events?.[0] ?? "completed"}。`);
|
|
81
|
+
return 1;
|
|
82
|
+
}
|
|
83
|
+
let failed = false;
|
|
84
|
+
// Lets a module show what it would send (the template does) without a real IM.
|
|
85
|
+
process.env.REQ_KIT_NOTIFY_TEST = "1";
|
|
86
|
+
await dispatchNotifiers(config.notifiers, sampleEvent(type, repo), (message) => {
|
|
87
|
+
if (message.startsWith("外部通知失败")) failed = true;
|
|
88
|
+
console.log(message);
|
|
89
|
+
});
|
|
90
|
+
return failed ? 1 : 0;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(import.meta.filename)) {
|
|
94
|
+
main().then(
|
|
95
|
+
(code) => process.exit(code),
|
|
96
|
+
(error: Error) => {
|
|
97
|
+
console.error(error.message);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// paseo.json checks shared by the doctor.
|
|
2
|
+
//
|
|
3
|
+
// Paseo 0.11 runs project commands with a different shell on each system:
|
|
4
|
+
// worktree.setup: bash (macOS, Linux) / powershell -ExecutionPolicy Bypass (Windows)
|
|
5
|
+
// scripts: the terminal's shell, $SHELL or /bin/sh (macOS, Linux) / cmd.exe (Windows)
|
|
6
|
+
// paseo.json is committed and shared, so commands should work in all of them: plain program
|
|
7
|
+
// invocations, with environment variables read by the program (see /req-setup's helpers).
|
|
8
|
+
|
|
9
|
+
export type PaseoJson = { worktree?: { setup?: unknown }; scripts?: Record<string, { type?: string; command?: string; port?: number }> };
|
|
10
|
+
|
|
11
|
+
export type ShellStyle = "posix" | "powershell" | "cmd";
|
|
12
|
+
|
|
13
|
+
const STYLE_NAMES: Record<ShellStyle, string> = { posix: "bash/sh", powershell: "PowerShell", cmd: "cmd.exe" };
|
|
14
|
+
|
|
15
|
+
/** Shell-specific syntax found in a command (empty: works the same everywhere). */
|
|
16
|
+
export function shellStyles(command: string): ShellStyle[] {
|
|
17
|
+
const styles = new Set<ShellStyle>();
|
|
18
|
+
if (/\$env:[A-Za-z_]/i.test(command) || /\b(Test-Path|Copy-Item|Set-Location|Get-ChildItem|Remove-Item)\b/.test(command)) styles.add("powershell");
|
|
19
|
+
if (/%[A-Za-z_][A-Za-z0-9_]*%/.test(command)) styles.add("cmd");
|
|
20
|
+
if (/(^|[^:\w`])\$\{?(?!env:)[A-Za-z_]/i.test(command) || /\[\s+-[a-z]\s/.test(command) || /(^|\s)(cp|mv|rm|export)\s/.test(command) || /\|\||&&/.test(command)) {
|
|
21
|
+
// `&&` works in cmd and PowerShell 7 too, but not in Windows PowerShell 5.1, which Paseo uses for setup.
|
|
22
|
+
styles.add("posix");
|
|
23
|
+
}
|
|
24
|
+
return [...styles];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function setupCommands(raw: PaseoJson): string[] {
|
|
28
|
+
const setup = raw.worktree?.setup;
|
|
29
|
+
if (Array.isArray(setup)) return setup.map(String);
|
|
30
|
+
return typeof setup === "string" ? setup.split(/\r?\n/).filter((l) => l.trim()) : [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface PaseoJsonIssue {
|
|
34
|
+
/** "fails": the command will not work on this machine; "portable": it only works on some systems. */
|
|
35
|
+
kind: "fails" | "portable";
|
|
36
|
+
text: string;
|
|
37
|
+
fix: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const FIX =
|
|
41
|
+
"改成各系统都一样的写法:准备命令用 node scripts/paseo/copy-local-files.mjs .env,服务用 node scripts/paseo/with-port.mjs npm run dev -- --port {PORT}(/req-setup 会写入这两个脚本)";
|
|
42
|
+
|
|
43
|
+
/** Problems of the project's paseo.json for the system Paseo runs on (process.platform by default). */
|
|
44
|
+
export function paseoJsonIssues(raw: PaseoJson, platform: NodeJS.Platform = process.platform): PaseoJsonIssue[] {
|
|
45
|
+
const windows = platform === "win32";
|
|
46
|
+
const setupShell: ShellStyle = windows ? "powershell" : "posix";
|
|
47
|
+
const scriptShell: ShellStyle = windows ? "cmd" : "posix";
|
|
48
|
+
const issues: PaseoJsonIssue[] = [];
|
|
49
|
+
const check = (where: string, command: string, shell: ShellStyle) => {
|
|
50
|
+
const styles = shellStyles(command);
|
|
51
|
+
if (styles.length === 0) return;
|
|
52
|
+
const short = command.length > 60 ? `${command.slice(0, 60)}…` : command;
|
|
53
|
+
const foreign = styles.filter((s) => s !== shell);
|
|
54
|
+
if (foreign.length > 0) {
|
|
55
|
+
issues.push({
|
|
56
|
+
kind: "fails",
|
|
57
|
+
text: `${where}「${short}」是 ${foreign.map((s) => STYLE_NAMES[s]).join("、")} 写法,这台机器上 Paseo 用 ${STYLE_NAMES[shell]} 执行,会失败`,
|
|
58
|
+
fix: FIX,
|
|
59
|
+
});
|
|
60
|
+
} else {
|
|
61
|
+
issues.push({
|
|
62
|
+
kind: "portable",
|
|
63
|
+
text: `${where}「${short}」用了 ${STYLE_NAMES[shell]} 的写法,在这台机器上可以用,换到其他系统(Windows 和 macOS/Linux 之间)会失败`,
|
|
64
|
+
fix: FIX,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
for (const cmd of setupCommands(raw)) check("paseo.json 的 worktree.setup 中", cmd, setupShell);
|
|
69
|
+
for (const [name, s] of Object.entries(raw.scripts ?? {})) {
|
|
70
|
+
check(`paseo.json 的${s.type === "service" ? "服务" : "脚本"} ${name}`, String(s.command ?? ""), scriptShell);
|
|
71
|
+
}
|
|
72
|
+
return issues;
|
|
73
|
+
}
|