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
package/install.mjs
ADDED
|
@@ -0,0 +1,928 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// req-kit installer. Run it from the version directory you want to use:
|
|
3
|
+
//
|
|
4
|
+
// node install.mjs install this version, or switch to it (upgrade / rollback)
|
|
5
|
+
// node install.mjs --check report only, change nothing
|
|
6
|
+
// node install.mjs --import-legacy <dir> first copy config and prompts.local from the old script version
|
|
7
|
+
// node install.mjs --target <dir> also install into another skills directory (repeatable, remembered)
|
|
8
|
+
// node install.mjs --force overwrite same-named skills that req-kit did not install
|
|
9
|
+
// node install.mjs --prune list old version directories that can be deleted
|
|
10
|
+
// node install.mjs --uninstall remove the skills req-kit installed (keeps ~/.req-kit)
|
|
11
|
+
// node install.mjs --configure configuration wizard: pick the agents from what Paseo offers
|
|
12
|
+
// node install.mjs --upgrade [version] download a version (default: the newest) next to this one,
|
|
13
|
+
// install its dependencies and switch to it
|
|
14
|
+
// node install.mjs --latest print the newest published version
|
|
15
|
+
//
|
|
16
|
+
// Programs live in the version directory (e.g. ~/tools/req-kit/versions/1.0.0); machine
|
|
17
|
+
// settings live in ~/.req-kit (REQ_KIT_HOME overrides it) and survive upgrades.
|
|
18
|
+
// Skills are copied into each agent's user-level skills directory, so they are available
|
|
19
|
+
// in every Paseo chat on this machine. Installing also writes the `req` command into
|
|
20
|
+
// ~/.req-kit/bin. Only plain Node.js (and git for --upgrade) is used here: no dependencies.
|
|
21
|
+
|
|
22
|
+
import { spawnSync } from "node:child_process";
|
|
23
|
+
import crypto from "node:crypto";
|
|
24
|
+
import fs from "node:fs";
|
|
25
|
+
import os from "node:os";
|
|
26
|
+
import path from "node:path";
|
|
27
|
+
import { fileURLToPath } from "node:url";
|
|
28
|
+
import { parseArgs } from "node:util";
|
|
29
|
+
import { findDependency, findTsxCli, layoutOf } from "./bin/locate.mjs";
|
|
30
|
+
|
|
31
|
+
const KIT_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
const PKG = JSON.parse(fs.readFileSync(path.join(KIT_ROOT, "package.json"), "utf8"));
|
|
33
|
+
const VERSION = PKG.version;
|
|
34
|
+
const HOME_DIR = os.homedir();
|
|
35
|
+
const KIT_HOME = process.env.REQ_KIT_HOME?.trim() ? path.resolve(process.env.REQ_KIT_HOME.trim()) : path.join(HOME_DIR, ".req-kit");
|
|
36
|
+
const CONFIG = path.join(KIT_HOME, "config.json");
|
|
37
|
+
const ACTIVE = path.join(KIT_HOME, "active.json");
|
|
38
|
+
const LOG_DIR = path.join(KIT_HOME, "install-log");
|
|
39
|
+
const MARKER = ".req-kit-install.json";
|
|
40
|
+
/** Written by the script version (requirement-pipeline) installer. */
|
|
41
|
+
const LEGACY_MARKER = ".installed-by-requirement-pipeline.json";
|
|
42
|
+
const LEGACY_DIR = path.join(HOME_DIR, "tools", "requirement-pipeline");
|
|
43
|
+
const SKILLS_SOURCE = path.join(KIT_ROOT, "skills");
|
|
44
|
+
const slash = (p) => p.split(path.sep).join("/");
|
|
45
|
+
|
|
46
|
+
const { values, positionals } = parseArgs({
|
|
47
|
+
allowPositionals: true,
|
|
48
|
+
options: {
|
|
49
|
+
configure: { type: "boolean", default: false },
|
|
50
|
+
upgrade: { type: "boolean", default: false },
|
|
51
|
+
latest: { type: "boolean", default: false },
|
|
52
|
+
from: { type: "string" },
|
|
53
|
+
beta: { type: "boolean", default: false },
|
|
54
|
+
setup: { type: "boolean", default: false },
|
|
55
|
+
use: { type: "boolean", default: false },
|
|
56
|
+
versions: { type: "boolean", default: false },
|
|
57
|
+
check: { type: "boolean", default: false },
|
|
58
|
+
quiet: { type: "boolean", default: false },
|
|
59
|
+
uninstall: { type: "boolean", default: false },
|
|
60
|
+
force: { type: "boolean", default: false },
|
|
61
|
+
prune: { type: "boolean", default: false },
|
|
62
|
+
target: { type: "string", multiple: true, default: [] },
|
|
63
|
+
"import-legacy": { type: "string" },
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
let failures = 0;
|
|
68
|
+
let warnings = 0;
|
|
69
|
+
const ok = (text) => console.log(`✓ ${text}`);
|
|
70
|
+
const warn = (text, fix) => {
|
|
71
|
+
warnings += 1;
|
|
72
|
+
console.log(`⚠ ${text}`);
|
|
73
|
+
if (fix) console.log(` 修复:${fix}`);
|
|
74
|
+
};
|
|
75
|
+
const fail = (text, fix) => {
|
|
76
|
+
failures += 1;
|
|
77
|
+
console.log(`✗ ${text}`);
|
|
78
|
+
if (fix) console.log(` 修复:${fix}`);
|
|
79
|
+
};
|
|
80
|
+
const say = (text = "") => {
|
|
81
|
+
if (!values.quiet) console.log(text);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
function readJson(file) {
|
|
85
|
+
try {
|
|
86
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function sha256(text) {
|
|
93
|
+
return crypto.createHash("sha256").update(text).digest("hex");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function writeLog(entry) {
|
|
97
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
98
|
+
fs.appendFileSync(path.join(LOG_DIR, "install.jsonl"), `${JSON.stringify({ at: new Date().toISOString(), ...entry })}\n`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// Skills: render from skills/, with absolute paths filled in for this version.
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
const PLACEHOLDERS = {
|
|
106
|
+
"{{REQ_KIT_ROOT}}": slash(KIT_ROOT),
|
|
107
|
+
"{{PIPELINE_LAUNCHER}}": slash(path.join(KIT_ROOT, "bin", "start-pipeline.mjs")),
|
|
108
|
+
"{{REQ_KIT_VERSION}}": VERSION,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
function listFiles(dir, base = dir) {
|
|
112
|
+
const out = [];
|
|
113
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
114
|
+
const full = path.join(dir, entry.name);
|
|
115
|
+
if (entry.isDirectory()) out.push(...listFiles(full, base));
|
|
116
|
+
else out.push(slash(path.relative(base, full)));
|
|
117
|
+
}
|
|
118
|
+
return out.sort();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** { skillName: { relativePath: content } } */
|
|
122
|
+
function renderSkills() {
|
|
123
|
+
const skills = {};
|
|
124
|
+
for (const name of fs.readdirSync(SKILLS_SOURCE).sort()) {
|
|
125
|
+
const dir = path.join(SKILLS_SOURCE, name);
|
|
126
|
+
if (!fs.statSync(dir).isDirectory()) continue;
|
|
127
|
+
const files = {};
|
|
128
|
+
for (const rel of listFiles(dir)) {
|
|
129
|
+
let text = fs.readFileSync(path.join(dir, rel), "utf8");
|
|
130
|
+
if (rel.endsWith(".md")) {
|
|
131
|
+
for (const [key, value] of Object.entries(PLACEHOLDERS)) text = text.replaceAll(key, value);
|
|
132
|
+
const left = text.match(/\{\{[A-Z_]+\}\}/);
|
|
133
|
+
if (left) throw new Error(`skills/${name}/${rel} 中有未知的占位符 ${left[0]}`);
|
|
134
|
+
}
|
|
135
|
+
files[rel] = text;
|
|
136
|
+
}
|
|
137
|
+
skills[name] = files;
|
|
138
|
+
}
|
|
139
|
+
return skills;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function skillRoots(active) {
|
|
143
|
+
const roots = [{ dir: path.join(HOME_DIR, ".agents", "skills"), label: "通用目录 ~/.agents/skills(Codex、Pi、OpenCode 等)" }];
|
|
144
|
+
if (fs.existsSync(path.join(HOME_DIR, ".claude"))) {
|
|
145
|
+
roots.push({ dir: path.join(HOME_DIR, ".claude", "skills"), label: "Claude Code ~/.claude/skills" });
|
|
146
|
+
}
|
|
147
|
+
const extra = new Set([...(active?.targets ?? []), ...values.target.map((t) => path.resolve(t))]);
|
|
148
|
+
for (const dir of extra) {
|
|
149
|
+
if (!roots.some((r) => path.resolve(r.dir) === path.resolve(dir))) roots.push({ dir, label: `自定义目录 ${dir}` });
|
|
150
|
+
}
|
|
151
|
+
return roots;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** "ours" (req-kit marker), "legacy" (script-version marker), "foreign", "symlink" or "absent". */
|
|
155
|
+
function ownership(dir) {
|
|
156
|
+
if (!fs.existsSync(dir)) {
|
|
157
|
+
try {
|
|
158
|
+
fs.lstatSync(dir);
|
|
159
|
+
return "symlink"; // dangling link
|
|
160
|
+
} catch {
|
|
161
|
+
return "absent";
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (fs.lstatSync(dir).isSymbolicLink()) return "symlink";
|
|
165
|
+
if (fs.existsSync(path.join(dir, MARKER))) return "ours";
|
|
166
|
+
if (fs.existsSync(path.join(dir, LEGACY_MARKER))) return "legacy";
|
|
167
|
+
return "foreign";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Names of req-kit skills present in a skills directory (any version). */
|
|
171
|
+
function installedByUs(rootDir) {
|
|
172
|
+
if (!fs.existsSync(rootDir)) return [];
|
|
173
|
+
return fs
|
|
174
|
+
.readdirSync(rootDir)
|
|
175
|
+
.filter((name) => ["ours", "legacy"].includes(ownership(path.join(rootDir, name))))
|
|
176
|
+
.sort();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function checkRoot(root, skills) {
|
|
180
|
+
const problems = [];
|
|
181
|
+
for (const [name, files] of Object.entries(skills)) {
|
|
182
|
+
const dir = path.join(root.dir, name);
|
|
183
|
+
const owner = ownership(dir);
|
|
184
|
+
if (owner === "absent") {
|
|
185
|
+
problems.push(`${name} 未安装`);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (owner === "symlink" || owner === "foreign") {
|
|
189
|
+
problems.push(`${name} 已存在但不是 req-kit 安装的`);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (owner === "legacy") {
|
|
193
|
+
problems.push(`${name} 是旧的脚本版安装的`);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const marker = readJson(path.join(dir, MARKER));
|
|
197
|
+
if (marker?.version !== VERSION || path.resolve(marker?.root ?? "") !== path.resolve(KIT_ROOT)) {
|
|
198
|
+
problems.push(`${name} 来自 ${marker?.version ?? "未知版本"}(${marker?.root ?? "未知目录"})`);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const changed = Object.entries(files).some(([rel, text]) => {
|
|
202
|
+
const file = path.join(dir, rel);
|
|
203
|
+
return !fs.existsSync(file) || fs.readFileSync(file, "utf8") !== text;
|
|
204
|
+
});
|
|
205
|
+
if (changed) problems.push(`${name} 的文件与本版本不一致`);
|
|
206
|
+
}
|
|
207
|
+
const stale = installedByUs(root.dir).filter((name) => !(name in skills));
|
|
208
|
+
if (problems.length === 0 && stale.length === 0) {
|
|
209
|
+
ok(`${root.label}:${Object.keys(skills).length} 个 skill 已是 ${VERSION}`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
for (const problem of problems) fail(`${root.label}:${problem}`, `在 ${KIT_ROOT} 中运行 node install.mjs`);
|
|
213
|
+
for (const name of stale) warn(`${root.label}:${name} 是旧版本 req-kit 留下的,本版本已不再使用`, `在 ${KIT_ROOT} 中运行 node install.mjs 会删除它`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function installRoot(root, skills) {
|
|
217
|
+
fs.mkdirSync(root.dir, { recursive: true });
|
|
218
|
+
const blocked = [];
|
|
219
|
+
for (const [name, files] of Object.entries(skills)) {
|
|
220
|
+
const dir = path.join(root.dir, name);
|
|
221
|
+
const owner = ownership(dir);
|
|
222
|
+
if (owner === "symlink") {
|
|
223
|
+
blocked.push(name);
|
|
224
|
+
fail(`${root.label}:${dir} 是符号链接,未改动`, "删除它后重新运行,或用 --target 安装到其他目录");
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (owner === "foreign" && !values.force) {
|
|
228
|
+
blocked.push(name);
|
|
229
|
+
fail(`${root.label}:${dir} 已存在且不是 req-kit 安装的,未覆盖`, "确认可以覆盖时加 --force");
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
233
|
+
const hashes = {};
|
|
234
|
+
for (const [rel, text] of Object.entries(files)) {
|
|
235
|
+
const file = path.join(dir, rel);
|
|
236
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
237
|
+
fs.writeFileSync(file, text);
|
|
238
|
+
hashes[rel] = sha256(text);
|
|
239
|
+
}
|
|
240
|
+
const marker = { name: "req-kit", skill: name, version: VERSION, root: KIT_ROOT, installedAt: new Date().toISOString(), files: hashes };
|
|
241
|
+
fs.writeFileSync(path.join(dir, MARKER), `${JSON.stringify(marker, null, 2)}\n`);
|
|
242
|
+
}
|
|
243
|
+
const removed = [];
|
|
244
|
+
for (const name of installedByUs(root.dir)) {
|
|
245
|
+
if (name in skills) continue;
|
|
246
|
+
fs.rmSync(path.join(root.dir, name), { recursive: true, force: true });
|
|
247
|
+
removed.push(name);
|
|
248
|
+
}
|
|
249
|
+
const count = Object.keys(skills).length - blocked.length;
|
|
250
|
+
ok(`${root.label}:已安装 ${count} 个 skill${removed.length ? `,删除了旧版本留下的 ${removed.join("、")}` : ""}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function uninstallRoot(root) {
|
|
254
|
+
const names = installedByUs(root.dir);
|
|
255
|
+
for (const name of names) fs.rmSync(path.join(root.dir, name), { recursive: true, force: true });
|
|
256
|
+
if (names.length) ok(`${root.label}:已删除 ${names.join("、")}`);
|
|
257
|
+
else ok(`${root.label}:没有 req-kit 安装的 skill`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
// Environment and configuration
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
function checkRuntime() {
|
|
265
|
+
const [major] = process.versions.node.split(".").map(Number);
|
|
266
|
+
if (major < 22) fail(`需要 Node.js 22 或更新版本,当前为 ${process.versions.node}`, "升级 Node.js");
|
|
267
|
+
else ok(`Node.js ${process.versions.node}`);
|
|
268
|
+
const missing = ["tsx", "@getpaseo/client"].filter((m) => !findDependency(KIT_ROOT, m));
|
|
269
|
+
if (missing.length) {
|
|
270
|
+
const fix =
|
|
271
|
+
LAYOUT.kind === "npm"
|
|
272
|
+
? `重新安装这个版本:删除 ${path.join(LAYOUT.versionsDir, VERSION)} 后运行 req upgrade ${VERSION}`
|
|
273
|
+
: LAYOUT.kind === "manager"
|
|
274
|
+
? `重新安装管理器:npm i -g ${PACKAGE_NAME}`
|
|
275
|
+
: `在 ${KIT_ROOT} 中执行 npm install`;
|
|
276
|
+
fail(`依赖未安装(${missing.join("、")})`, fix);
|
|
277
|
+
}
|
|
278
|
+
else ok("依赖已安装");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function importLegacy(from) {
|
|
282
|
+
const dir = path.resolve(from);
|
|
283
|
+
const config = path.join(dir, "pipeline.config.json");
|
|
284
|
+
if (!fs.existsSync(config)) {
|
|
285
|
+
fail(`在 ${dir} 中没有找到 pipeline.config.json`, "确认旧脚本版的安装目录(默认 ~/tools/requirement-pipeline)");
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
fs.mkdirSync(KIT_HOME, { recursive: true });
|
|
289
|
+
if (fs.existsSync(CONFIG) && !values.force) {
|
|
290
|
+
warn(`${CONFIG} 已存在,未覆盖`, "确认要用旧配置覆盖时加 --force(原文件会备份)");
|
|
291
|
+
} else {
|
|
292
|
+
if (fs.existsSync(CONFIG)) fs.copyFileSync(CONFIG, `${CONFIG}.bak-${Date.now()}`);
|
|
293
|
+
fs.copyFileSync(config, CONFIG);
|
|
294
|
+
ok(`已导入旧配置:${config} → ${CONFIG}`);
|
|
295
|
+
}
|
|
296
|
+
const prompts = path.join(dir, "prompts.local");
|
|
297
|
+
if (fs.existsSync(prompts)) {
|
|
298
|
+
const dest = path.join(KIT_HOME, "prompts.local");
|
|
299
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
300
|
+
const copied = [];
|
|
301
|
+
for (const name of fs.readdirSync(prompts)) {
|
|
302
|
+
const target = path.join(dest, name);
|
|
303
|
+
if (fs.existsSync(target) && !values.force) {
|
|
304
|
+
warn(`prompts.local/${name} 已存在,未覆盖`);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
fs.copyFileSync(path.join(prompts, name), target);
|
|
308
|
+
copied.push(name);
|
|
309
|
+
}
|
|
310
|
+
if (copied.length) ok(`已导入提示词覆盖:${copied.join("、")} → ${dest}`);
|
|
311
|
+
}
|
|
312
|
+
writeLog({ action: "import-legacy", from: dir });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function checkConfig() {
|
|
316
|
+
if (!fs.existsSync(CONFIG)) {
|
|
317
|
+
const legacy = fs.existsSync(path.join(LEGACY_DIR, "pipeline.config.json"));
|
|
318
|
+
if (values.check) {
|
|
319
|
+
fail(`还没有本机配置 ${CONFIG}`, legacy ? `从旧脚本版导入:node install.mjs --import-legacy "${LEGACY_DIR}"` : "运行 node install.mjs 生成");
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (legacy) {
|
|
323
|
+
fail(`还没有本机配置 ${CONFIG},但发现了旧脚本版的配置`, `运行 node install.mjs --import-legacy "${LEGACY_DIR}" 导入`);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
fs.mkdirSync(KIT_HOME, { recursive: true });
|
|
327
|
+
fs.copyFileSync(path.join(KIT_ROOT, "config.example.json"), CONFIG);
|
|
328
|
+
fail(`已生成本机配置 ${CONFIG},请填写两个 agent 的 provider/model`, "运行配置向导 node install.mjs --configure,按提示选择即可(也可以手工编辑,写法见 docs/guide/安装与升级.md)");
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const text = fs.readFileSync(CONFIG, "utf8");
|
|
332
|
+
if (readJson(CONFIG) === null) fail(`${CONFIG} 不是合法的 JSON`, "修正格式");
|
|
333
|
+
else if (text.includes("CHANGE-ME")) fail(`${CONFIG} 中的 provider 还是示例值`, "运行配置向导 node install.mjs --configure(或手工填写真实的 provider/model)");
|
|
334
|
+
else ok(`本机配置 ${CONFIG}`);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function checkLegacyLeftovers(roots) {
|
|
338
|
+
if (fs.existsSync(LEGACY_DIR) && path.resolve(LEGACY_DIR) !== path.resolve(KIT_ROOT)) {
|
|
339
|
+
warn(`发现旧脚本版目录 ${LEGACY_DIR}`, "确认它启动的流水线都已结束、配置已导入后,可以删除它");
|
|
340
|
+
}
|
|
341
|
+
for (const root of roots) {
|
|
342
|
+
for (const dep of ["grilling", "domain-modeling"]) {
|
|
343
|
+
if (fs.existsSync(path.join(root.dir, dep, "SKILL.md"))) {
|
|
344
|
+
say(` ${root.label}:${dep} 是旧版本依赖的外部 skill,req-kit 已不再需要,可以保留或自行卸载。`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function checkActive(active) {
|
|
351
|
+
if (!active) {
|
|
352
|
+
if (values.check) fail("还没有启用任何 req-kit 版本", `在 ${KIT_ROOT} 中运行 node install.mjs`);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (path.resolve(active.root) === path.resolve(KIT_ROOT)) ok(`当前启用的版本:${active.version}(${active.root})`);
|
|
356
|
+
else warn(`当前启用的是 ${active.version}(${active.root}),不是这个目录(${VERSION})`, `要改用这个版本,在 ${KIT_ROOT} 中运行 node install.mjs`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function prune(active) {
|
|
360
|
+
const versionsDir = LAYOUT.versionsDir;
|
|
361
|
+
if (!versionsDir || !fs.existsSync(versionsDir)) {
|
|
362
|
+
say(`这个安装目录不在 versions/ 下(${KIT_ROOT}),没有可以整理的旧版本。`);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const history = fs.existsSync(path.join(LOG_DIR, "install.jsonl"))
|
|
366
|
+
? fs.readFileSync(path.join(LOG_DIR, "install.jsonl"), "utf8").split("\n").filter(Boolean).map((l) => readJson2(l)).filter(Boolean)
|
|
367
|
+
: [];
|
|
368
|
+
// A version's directory is versions/<version>, whichever layout its program has inside.
|
|
369
|
+
const versionDirOf = (root) => {
|
|
370
|
+
const rel = path.relative(versionsDir, path.resolve(root));
|
|
371
|
+
return rel && !rel.startsWith("..") && !path.isAbsolute(rel) ? path.join(versionsDir, rel.split(path.sep)[0]) : path.resolve(root);
|
|
372
|
+
};
|
|
373
|
+
const activeDir = versionDirOf(active?.root ?? KIT_ROOT);
|
|
374
|
+
const previous = [...history].reverse().find((e) => e.action === "activate" && e.root && versionDirOf(e.root) !== activeDir);
|
|
375
|
+
const keep = new Set([activeDir, previous ? versionDirOf(previous.root) : null].filter(Boolean));
|
|
376
|
+
const candidates = fs
|
|
377
|
+
.readdirSync(versionsDir)
|
|
378
|
+
.map((name) => path.join(versionsDir, name))
|
|
379
|
+
.filter((dir) => fs.statSync(dir).isDirectory() && !keep.has(path.resolve(dir)));
|
|
380
|
+
say(`保留:${[...keep].join("、")}`);
|
|
381
|
+
if (candidates.length === 0) {
|
|
382
|
+
say("没有可以删除的旧版本。");
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
say("可以删除的旧版本(请先确认没有由它们启动、仍在运行的流水线):");
|
|
386
|
+
for (const dir of candidates) say(` ${dir}`);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function readJson2(line) {
|
|
390
|
+
try {
|
|
391
|
+
return JSON.parse(line);
|
|
392
|
+
} catch {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
// The `req` command
|
|
399
|
+
// ---------------------------------------------------------------------------
|
|
400
|
+
|
|
401
|
+
const SHIM_DIR = path.join(KIT_HOME, "bin");
|
|
402
|
+
const SHIMS = {
|
|
403
|
+
req: (entry) => `#!/bin/sh\n# Written by req-kit install.mjs; points at the active version.\nexec node "${slash(entry)}" "$@"\n`,
|
|
404
|
+
"req.cmd": (entry) => `@echo off\r\nrem Written by req-kit install.mjs; points at the active version.\r\nnode "${entry}" %*\r\n`,
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
function onPath(dir) {
|
|
408
|
+
const want = path.resolve(dir).toLowerCase();
|
|
409
|
+
return (process.env.PATH ?? process.env.Path ?? "")
|
|
410
|
+
.split(path.delimiter)
|
|
411
|
+
.filter(Boolean)
|
|
412
|
+
.some((p) => path.resolve(p).toLowerCase() === want);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function pathHint() {
|
|
416
|
+
if (onPath(SHIM_DIR)) return;
|
|
417
|
+
say(` 要在任何位置直接输入 req,把 ${SHIM_DIR} 加入 PATH(只需一次,之后新开的终端生效):`);
|
|
418
|
+
if (process.platform === "win32") {
|
|
419
|
+
say(` PowerShell:[Environment]::SetEnvironmentVariable("Path", [Environment]::GetEnvironmentVariable("Path", "User") + ";${SHIM_DIR}", "User")`);
|
|
420
|
+
} else {
|
|
421
|
+
say(` echo 'export PATH="${SHIM_DIR}:$PATH"' >> ~/.bashrc (zsh 用 ~/.zshrc)`);
|
|
422
|
+
}
|
|
423
|
+
say(` 不加也可以:用完整路径运行 ${path.join(SHIM_DIR, process.platform === "win32" ? "req.cmd" : "req")}`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function writeShims() {
|
|
427
|
+
const entry = path.join(KIT_ROOT, "bin", "req.mjs");
|
|
428
|
+
fs.mkdirSync(SHIM_DIR, { recursive: true });
|
|
429
|
+
for (const [name, render] of Object.entries(SHIMS)) {
|
|
430
|
+
const file = path.join(SHIM_DIR, name);
|
|
431
|
+
fs.writeFileSync(file, render(entry));
|
|
432
|
+
if (name === "req") fs.chmodSync(file, 0o755);
|
|
433
|
+
}
|
|
434
|
+
ok(`req 命令:${SHIM_DIR}`);
|
|
435
|
+
// Installed through the npm manager, `req` is already on PATH.
|
|
436
|
+
if (LAYOUT.kind !== "npm") pathHint();
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function checkShims(active) {
|
|
440
|
+
const file = path.join(SHIM_DIR, "req");
|
|
441
|
+
if (!fs.existsSync(file)) {
|
|
442
|
+
warn(`还没有 req 命令(${SHIM_DIR})`, `在 ${KIT_ROOT} 中运行 node install.mjs`);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
const entry = path.join(active?.root ?? KIT_ROOT, "bin", "req.mjs");
|
|
446
|
+
if (fs.readFileSync(file, "utf8").includes(slash(entry))) ok(`req 命令:${SHIM_DIR}`);
|
|
447
|
+
else warn("req 命令指向的不是当前启用的版本", `在当前启用版本的目录中运行 node install.mjs`);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ---------------------------------------------------------------------------
|
|
451
|
+
// Versions: where they live, which to install, installing and switching
|
|
452
|
+
// ---------------------------------------------------------------------------
|
|
453
|
+
|
|
454
|
+
const PACKAGE_NAME = JSON.parse(fs.readFileSync(path.join(KIT_ROOT, "package.json"), "utf8")).name;
|
|
455
|
+
const LAYOUT = layoutOf(KIT_ROOT);
|
|
456
|
+
const RELEASE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
|
|
457
|
+
|
|
458
|
+
/** Semver order; a prerelease (1.3.0-beta.1) comes before its release (1.3.0). */
|
|
459
|
+
function compareVersions(a, b) {
|
|
460
|
+
const pa = RELEASE.exec(a);
|
|
461
|
+
const pb = RELEASE.exec(b);
|
|
462
|
+
for (let i = 1; i <= 3; i++) {
|
|
463
|
+
const d = Number(pa[i]) - Number(pb[i]);
|
|
464
|
+
if (d !== 0) return d;
|
|
465
|
+
}
|
|
466
|
+
if (!pa[4] || !pb[4]) return (pa[4] ? -1 : 0) - (pb[4] ? -1 : 0);
|
|
467
|
+
const xa = pa[4].split(".");
|
|
468
|
+
const xb = pb[4].split(".");
|
|
469
|
+
for (let i = 0; i < Math.max(xa.length, xb.length); i++) {
|
|
470
|
+
if (xa[i] === undefined) return -1;
|
|
471
|
+
if (xb[i] === undefined) return 1;
|
|
472
|
+
const na = /^\d+$/.test(xa[i]);
|
|
473
|
+
const nb = /^\d+$/.test(xb[i]);
|
|
474
|
+
if (na && nb && Number(xa[i]) !== Number(xb[i])) return Number(xa[i]) - Number(xb[i]);
|
|
475
|
+
if (na !== nb) return na ? -1 : 1;
|
|
476
|
+
if (xa[i] !== xb[i]) return xa[i] < xb[i] ? -1 : 1;
|
|
477
|
+
}
|
|
478
|
+
return 0;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** "0.11.2" satisfies ">=0.11.0 <0.12.0" (the ranges req-kit declares; prerelease tags ignored). */
|
|
482
|
+
function satisfies(version, range) {
|
|
483
|
+
const parse = (v) => v.replace(/^v/, "").split(/[-+]/)[0].split(".").map((n) => Number(n) || 0);
|
|
484
|
+
const cmp = (a, b) => {
|
|
485
|
+
for (let i = 0; i < 3; i++) if ((a[i] ?? 0) !== (b[i] ?? 0)) return (a[i] ?? 0) - (b[i] ?? 0);
|
|
486
|
+
return 0;
|
|
487
|
+
};
|
|
488
|
+
const v = parse(version);
|
|
489
|
+
return range
|
|
490
|
+
.trim()
|
|
491
|
+
.split(/\s+/)
|
|
492
|
+
.every((part) => {
|
|
493
|
+
const m = /^(>=|<=|>|<|=)?(.+)$/.exec(part);
|
|
494
|
+
if (!m) return false;
|
|
495
|
+
const c = cmp(v, parse(m[2]));
|
|
496
|
+
return { ">=": c >= 0, "<=": c <= 0, ">": c > 0, "<": c < 0, "=": c === 0 }[m[1] ?? "="];
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** The program directory of an installed version, in either layout; null when not installed. */
|
|
501
|
+
function versionRoot(versionsDir, version) {
|
|
502
|
+
const dir = path.join(versionsDir, version);
|
|
503
|
+
for (const root of [dir, path.join(dir, "node_modules", PACKAGE_NAME)]) {
|
|
504
|
+
if (readJson(path.join(root, "package.json"))?.version === version && fs.existsSync(path.join(root, "install.mjs"))) return root;
|
|
505
|
+
}
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function installedVersions(versionsDir) {
|
|
510
|
+
if (!versionsDir || !fs.existsSync(versionsDir)) return [];
|
|
511
|
+
return fs
|
|
512
|
+
.readdirSync(versionsDir)
|
|
513
|
+
.filter((name) => RELEASE.test(name) && versionRoot(versionsDir, name))
|
|
514
|
+
.sort((a, b) => compareVersions(b, a));
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** The version the machine uses now (the manager's own version does not count). */
|
|
518
|
+
function currentVersion() {
|
|
519
|
+
return readJson(ACTIVE)?.version ?? VERSION;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/** Runs npm (REQ_KIT_NPM: a script standing in for npm, for tests). */
|
|
523
|
+
function npm(args, options = {}) {
|
|
524
|
+
const fake = process.env.REQ_KIT_NPM?.trim();
|
|
525
|
+
const windows = process.platform === "win32" && !fake;
|
|
526
|
+
const quoted = windows ? args.map((a) => (/[\s&()^]/.test(a) ? `"${a}"` : a)) : args;
|
|
527
|
+
return spawnSync(fake ? process.execPath : windows ? "npm.cmd" : "npm", fake ? [fake, ...args] : quoted, {
|
|
528
|
+
encoding: "utf8",
|
|
529
|
+
windowsHide: true,
|
|
530
|
+
shell: windows,
|
|
531
|
+
cwd: options.cwd,
|
|
532
|
+
timeout: options.timeout ?? 900_000,
|
|
533
|
+
stdio: options.inherit ? "inherit" : "pipe",
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function npmJson(args) {
|
|
538
|
+
const result = npm([...args, "--json"], { timeout: 60_000 });
|
|
539
|
+
if (result.error) throw new Error(`无法运行 npm:${result.error.message}`);
|
|
540
|
+
if (result.status !== 0) {
|
|
541
|
+
const lines = `${result.stderr ?? ""}`.trim().split("\n").filter(Boolean);
|
|
542
|
+
throw new Error(lines.find((l) => /npm (ERR|error)/.test(l)) ?? lines.at(-1) ?? `npm 退出码 ${result.status}`);
|
|
543
|
+
}
|
|
544
|
+
return JSON.parse(result.stdout.trim() || "null");
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function npmInstall(spec, prefix) {
|
|
548
|
+
const result = npm(["install", "--prefix", prefix, "--omit=dev", "--no-audit", "--no-fund", "--no-save", spec], { inherit: true });
|
|
549
|
+
if (result.error) throw new Error(`无法运行 npm:${result.error.message}`);
|
|
550
|
+
if (result.status !== 0) throw new Error(`npm install ${spec} 失败(退出码 ${result.status})。请检查网络或 npm 镜像设置(npm config get registry)。`);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** Installs a version into versions/<version> through npm; the directory appears only when complete. */
|
|
554
|
+
function installWithNpm(versionsDir, version, spec) {
|
|
555
|
+
const existing = versionRoot(versionsDir, version);
|
|
556
|
+
if (existing) {
|
|
557
|
+
console.log(`${version} 已经装过(${existing}),直接使用。`);
|
|
558
|
+
return existing;
|
|
559
|
+
}
|
|
560
|
+
const dest = path.join(versionsDir, version);
|
|
561
|
+
if (fs.existsSync(dest)) throw new Error(`${dest} 已存在但不是完整的 ${version}。确认没有用处后删除它,再重新运行。`);
|
|
562
|
+
fs.mkdirSync(versionsDir, { recursive: true });
|
|
563
|
+
const partial = `${dest}.partial-${Date.now()}`;
|
|
564
|
+
fs.mkdirSync(partial);
|
|
565
|
+
try {
|
|
566
|
+
console.log(`安装 ${version}(npm)→ ${dest}`);
|
|
567
|
+
npmInstall(spec, partial);
|
|
568
|
+
fs.renameSync(partial, dest);
|
|
569
|
+
} finally {
|
|
570
|
+
fs.rmSync(partial, { recursive: true, force: true });
|
|
571
|
+
}
|
|
572
|
+
const root = versionRoot(versionsDir, version);
|
|
573
|
+
if (!root) throw new Error(`npm 安装完成,但 ${dest} 中没有找到 ${PACKAGE_NAME} ${version}`);
|
|
574
|
+
return root;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** Version of the local Paseo daemon, read through this copy's engine; null when unknown. */
|
|
578
|
+
function localDaemonVersion() {
|
|
579
|
+
const override = process.env.REQ_KIT_DAEMON_VERSION?.trim();
|
|
580
|
+
if (override) return override === "none" ? null : override;
|
|
581
|
+
const tsx = findTsxCli(KIT_ROOT);
|
|
582
|
+
if (!tsx) return null;
|
|
583
|
+
const result = spawnSync(process.execPath, [tsx, path.join(KIT_ROOT, "engine", "src", "daemon-version.ts"), "--config", CONFIG], {
|
|
584
|
+
encoding: "utf8",
|
|
585
|
+
timeout: 20_000,
|
|
586
|
+
windowsHide: true,
|
|
587
|
+
});
|
|
588
|
+
return result.status === 0 ? result.stdout.trim().split("\n")[0] || null : null;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* The newest published version worth installing: releases only unless the current one is a
|
|
593
|
+
* prerelease (or --beta), and one whose reqKit.paseo range accepts the local daemon.
|
|
594
|
+
*/
|
|
595
|
+
function chooseNpmVersion(current) {
|
|
596
|
+
const raw = npmJson(["view", PACKAGE_NAME, "versions"]);
|
|
597
|
+
const published = (Array.isArray(raw) ? raw : [raw]).filter((v) => typeof v === "string" && RELEASE.test(v)).sort((a, b) => compareVersions(b, a));
|
|
598
|
+
const wanted = positionals[0]?.replace(/^v/, "");
|
|
599
|
+
if (wanted) {
|
|
600
|
+
if (!published.includes(wanted)) throw new Error(`npm 上没有 ${PACKAGE_NAME} ${wanted}。已发布的版本:${published.slice(0, 8).join("、") || "无"}`);
|
|
601
|
+
return { version: wanted, notes: [], daemon: null };
|
|
602
|
+
}
|
|
603
|
+
const prerelease = current.includes("-") || values.beta;
|
|
604
|
+
const candidates = published.filter((v) => (prerelease || !v.includes("-")) && compareVersions(v, current) > 0);
|
|
605
|
+
if (candidates.length === 0) return { version: null, notes: [], daemon: null };
|
|
606
|
+
const daemon = localDaemonVersion();
|
|
607
|
+
if (!daemon) {
|
|
608
|
+
return { version: candidates[0], notes: ["没能读到本机 Paseo 的版本(daemon 没有运行?),没有按 Paseo 版本筛选,升级后请运行 req doctor"], daemon };
|
|
609
|
+
}
|
|
610
|
+
const notes = [];
|
|
611
|
+
for (const version of candidates.slice(0, 10)) {
|
|
612
|
+
let range = null;
|
|
613
|
+
try {
|
|
614
|
+
range = npmJson(["view", `${PACKAGE_NAME}@${version}`, "reqKit.paseo"]);
|
|
615
|
+
} catch {
|
|
616
|
+
// The registry did not say; treat it as compatible.
|
|
617
|
+
}
|
|
618
|
+
if (typeof range !== "string" || satisfies(daemon, range)) return { version, notes, daemon };
|
|
619
|
+
notes.push(`${version} 需要 Paseo ${range},本机是 ${daemon}`);
|
|
620
|
+
}
|
|
621
|
+
return { version: null, notes, daemon };
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function repoUrl() {
|
|
625
|
+
if (values.from && values.from !== "git" && values.from !== "npm") return values.from;
|
|
626
|
+
if (process.env.REQ_KIT_REPO_URL?.trim()) return process.env.REQ_KIT_REPO_URL.trim();
|
|
627
|
+
const repo = typeof PKG.repository === "string" ? PKG.repository : PKG.repository?.url;
|
|
628
|
+
const github = /^github:(.+)$/.exec(repo ?? "");
|
|
629
|
+
return github ? `https://github.com/${github[1]}.git` : repo;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** Git tags as published versions, newest first. Throws with a readable message. */
|
|
633
|
+
function publishedTags(url) {
|
|
634
|
+
const result = spawnSync("git", ["ls-remote", "--tags", "--refs", url], { encoding: "utf8", timeout: 30_000, windowsHide: true });
|
|
635
|
+
if (result.error) throw new Error(`无法运行 git:${result.error.message}。从 git 升级需要安装 git。`);
|
|
636
|
+
if (result.status !== 0) {
|
|
637
|
+
const lines = (result.stderr || "").trim().split("\n").filter(Boolean);
|
|
638
|
+
const reason = (lines.find((l) => l.startsWith("fatal:")) ?? lines[0] ?? `git 退出码 ${result.status}`).replace(/^fatal:\s*/, "");
|
|
639
|
+
throw new Error(`无法读取 ${url} 的版本列表:${reason}。请检查网络(公司网络可能需要代理),或用 --from <地址> 指定内网镜像。`);
|
|
640
|
+
}
|
|
641
|
+
return result.stdout
|
|
642
|
+
.split("\n")
|
|
643
|
+
.map((line) => /refs\/tags\/(.+)$/.exec(line.trim())?.[1])
|
|
644
|
+
.filter((tag) => tag && RELEASE.test(tag))
|
|
645
|
+
.sort((a, b) => compareVersions(b, a));
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/** Where upgrades come from: npm for npm installs and the manager, git for git installs; --from overrides. */
|
|
649
|
+
function upgradeSource() {
|
|
650
|
+
if (values.from === "npm") return "npm";
|
|
651
|
+
if (values.from) return "git";
|
|
652
|
+
return LAYOUT.kind === "git" ? "git" : "npm";
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/** Picks the version to install from git tags. */
|
|
656
|
+
function chooseGitVersion(current) {
|
|
657
|
+
const tags = publishedTags(repoUrl());
|
|
658
|
+
const wanted = positionals[0]?.replace(/^v/, "");
|
|
659
|
+
if (wanted) {
|
|
660
|
+
const tag = tags.find((t) => t.replace(/^v/, "") === wanted);
|
|
661
|
+
if (!tag) throw new Error(`没有找到版本 ${wanted}。已发布的版本:${tags.slice(0, 8).join("、") || "无"}`);
|
|
662
|
+
return { version: wanted, tag, notes: [] };
|
|
663
|
+
}
|
|
664
|
+
const prerelease = current.includes("-") || values.beta;
|
|
665
|
+
const tag = tags.find((t) => (prerelease || !t.includes("-")) && compareVersions(t.replace(/^v/, ""), current) > 0) ?? null;
|
|
666
|
+
return { version: tag ? tag.replace(/^v/, "") : null, tag, notes: [] };
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function installWithGit(versionsDir, version, tag) {
|
|
670
|
+
const url = repoUrl();
|
|
671
|
+
const dest = path.join(versionsDir, version);
|
|
672
|
+
if (versionRoot(versionsDir, version)) {
|
|
673
|
+
console.log(`${dest} 已经下载过,直接使用。`);
|
|
674
|
+
} else {
|
|
675
|
+
if (fs.existsSync(dest)) throw new Error(`${dest} 已存在但不是完整的 ${version}。确认没有用处后删除它,再重新运行。`);
|
|
676
|
+
console.log(`下载 ${version}:${url} → ${dest}`);
|
|
677
|
+
const partial = `${dest}.partial-${Date.now()}`;
|
|
678
|
+
try {
|
|
679
|
+
const clone = spawnSync("git", ["-c", "advice.detachedHead=false", "clone", "--quiet", "--depth", "1", "--branch", tag, url, partial], {
|
|
680
|
+
stdio: "inherit",
|
|
681
|
+
windowsHide: true,
|
|
682
|
+
});
|
|
683
|
+
if (clone.status !== 0) throw new Error(`git clone 失败(退出码 ${clone.status})`);
|
|
684
|
+
fs.renameSync(partial, dest);
|
|
685
|
+
} finally {
|
|
686
|
+
fs.rmSync(partial, { recursive: true, force: true });
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (process.env.REQ_KIT_SKIP_NPM === "1") {
|
|
690
|
+
console.log("跳过 npm install(REQ_KIT_SKIP_NPM=1)。");
|
|
691
|
+
} else if (!findTsxCli(dest)) {
|
|
692
|
+
console.log("安装依赖(npm install)……");
|
|
693
|
+
const result = npm(["install", "--omit=dev", "--no-audit", "--no-fund"], { cwd: dest, inherit: true });
|
|
694
|
+
if (result.status !== 0) throw new Error(`npm install 失败(退出码 ${result.status})`);
|
|
695
|
+
}
|
|
696
|
+
return dest;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/** Runs a version's own installer, which installs its skills and records it as active. */
|
|
700
|
+
function activate(root) {
|
|
701
|
+
return spawnSync(process.execPath, [path.join(root, "install.mjs"), ...values.target.flatMap((t) => ["--target", t])], {
|
|
702
|
+
stdio: "inherit",
|
|
703
|
+
windowsHide: true,
|
|
704
|
+
}).status ?? 1;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function latestCommand() {
|
|
708
|
+
if (!LAYOUT.versionsDir && upgradeSource() === "npm") {
|
|
709
|
+
console.error(`这个目录(${KIT_ROOT})是开发用的检出,不能自动升级。`);
|
|
710
|
+
return 1;
|
|
711
|
+
}
|
|
712
|
+
const current = currentVersion();
|
|
713
|
+
let choice;
|
|
714
|
+
try {
|
|
715
|
+
choice = upgradeSource() === "npm" ? chooseNpmVersion(current) : chooseGitVersion(current);
|
|
716
|
+
} catch (error) {
|
|
717
|
+
console.error(error.message);
|
|
718
|
+
return 1;
|
|
719
|
+
}
|
|
720
|
+
if (values.quiet) {
|
|
721
|
+
if (choice.version) console.log(choice.version);
|
|
722
|
+
else console.log(current);
|
|
723
|
+
return 0;
|
|
724
|
+
}
|
|
725
|
+
for (const note of choice.notes) console.log(`· ${note}`);
|
|
726
|
+
if (choice.version) console.log(`有新版本 ${choice.version}(当前 ${current})。升级:req upgrade`);
|
|
727
|
+
else console.log(`已是最新版本 ${current}${choice.notes.length ? "(能支持本机 Paseo 的版本中)" : ""}。`);
|
|
728
|
+
return 0;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function upgradeCommand() {
|
|
732
|
+
const versionsDir = LAYOUT.versionsDir;
|
|
733
|
+
if (!versionsDir) {
|
|
734
|
+
console.error(`这个安装目录不在 versions/ 下(${KIT_ROOT}),无法自动升级。请按 docs/guide/安装与升级.md 安装。`);
|
|
735
|
+
return 1;
|
|
736
|
+
}
|
|
737
|
+
const current = currentVersion();
|
|
738
|
+
const source = upgradeSource();
|
|
739
|
+
let root;
|
|
740
|
+
let target;
|
|
741
|
+
try {
|
|
742
|
+
const choice = source === "npm" ? chooseNpmVersion(current) : chooseGitVersion(current);
|
|
743
|
+
for (const note of choice.notes) console.log(`· ${note}`);
|
|
744
|
+
if (!choice.version) {
|
|
745
|
+
console.log(`已是最新版本 ${current}${choice.notes.length ? "(能支持本机 Paseo 的版本中)" : ""},不需要升级。`);
|
|
746
|
+
return 0;
|
|
747
|
+
}
|
|
748
|
+
target = choice.version;
|
|
749
|
+
root =
|
|
750
|
+
source === "npm"
|
|
751
|
+
? installWithNpm(versionsDir, target, `${PACKAGE_NAME}@${target}`)
|
|
752
|
+
: versionRoot(versionsDir, target) ?? installWithGit(versionsDir, target, choice.tag);
|
|
753
|
+
if (source === "git") root = versionRoot(versionsDir, target) ?? root;
|
|
754
|
+
} catch (error) {
|
|
755
|
+
console.error(`升级没有完成:${error.message}`);
|
|
756
|
+
console.error(`当前版本 ${current} 仍然可用,没有任何改动。`);
|
|
757
|
+
return 1;
|
|
758
|
+
}
|
|
759
|
+
console.log(`\n切换到 ${target}:`);
|
|
760
|
+
const status = activate(root);
|
|
761
|
+
const now = readJson(ACTIVE);
|
|
762
|
+
if (now?.version === target && path.resolve(now.root) === path.resolve(root)) {
|
|
763
|
+
console.log(`\n已升级到 ${target}${status === 0 ? "" : ",但还有需要处理的项(见上面的 ✗)"}。正在运行的流水线不受影响,继续用启动它的版本;之后启动的用新版本。`);
|
|
764
|
+
console.log(`更新内容见 ${path.join(root, "CHANGELOG.md")}。要回到 ${current}:req use ${current}`);
|
|
765
|
+
} else {
|
|
766
|
+
console.log(`\n没有切换到 ${target}(见上面的输出),仍在使用 ${current}。`);
|
|
767
|
+
}
|
|
768
|
+
return status;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/** npm i -g, then `req setup`: installs this package's own version under versions/ and switches to it. */
|
|
772
|
+
function setupCommand() {
|
|
773
|
+
if (LAYOUT.kind !== "manager") {
|
|
774
|
+
return activate(KIT_ROOT);
|
|
775
|
+
}
|
|
776
|
+
let root;
|
|
777
|
+
try {
|
|
778
|
+
root = versionRoot(LAYOUT.versionsDir, VERSION);
|
|
779
|
+
if (!root) {
|
|
780
|
+
// Install from this very package (no download of req-kit itself; its dependencies come from npm).
|
|
781
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "req-kit-pack-"));
|
|
782
|
+
try {
|
|
783
|
+
const packed = npmJson(["pack", KIT_ROOT, "--pack-destination", tmp]);
|
|
784
|
+
const file = Array.isArray(packed) ? packed[0]?.filename : null;
|
|
785
|
+
if (!file) throw new Error("npm pack 没有生成安装包");
|
|
786
|
+
root = installWithNpm(LAYOUT.versionsDir, VERSION, path.join(tmp, path.basename(file)));
|
|
787
|
+
} finally {
|
|
788
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
} catch (error) {
|
|
792
|
+
console.error(`安装没有完成:${error.message}`);
|
|
793
|
+
return 1;
|
|
794
|
+
}
|
|
795
|
+
console.log(`\n启用 ${VERSION}:`);
|
|
796
|
+
const status = activate(root);
|
|
797
|
+
const config = readJson(CONFIG);
|
|
798
|
+
if (config && JSON.stringify(config).includes("CHANGE-ME")) {
|
|
799
|
+
console.log("\n下一步:运行 req configure,按提示为 builder 和 reviewer 选择 agent。");
|
|
800
|
+
}
|
|
801
|
+
return status;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function useCommand() {
|
|
805
|
+
const version = positionals[0]?.replace(/^v/, "");
|
|
806
|
+
const versionsDir = LAYOUT.versionsDir;
|
|
807
|
+
if (!version || !versionsDir) {
|
|
808
|
+
console.error("用法:req use <版本>。已安装的版本见 req versions。");
|
|
809
|
+
return 2;
|
|
810
|
+
}
|
|
811
|
+
const root = versionRoot(versionsDir, version);
|
|
812
|
+
if (!root) {
|
|
813
|
+
console.error(`没有安装 ${version}。已安装:${installedVersions(versionsDir).join("、") || "无"}。要安装它:req upgrade ${version}`);
|
|
814
|
+
return 1;
|
|
815
|
+
}
|
|
816
|
+
return activate(root);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function versionsCommand() {
|
|
820
|
+
const versionsDir = LAYOUT.versionsDir;
|
|
821
|
+
const active = readJson(ACTIVE);
|
|
822
|
+
const installed = installedVersions(versionsDir);
|
|
823
|
+
console.log(`安装目录:${versionsDir ?? KIT_ROOT}`);
|
|
824
|
+
if (installed.length === 0) console.log("还没有安装任何版本。运行 req setup。");
|
|
825
|
+
for (const v of installed) {
|
|
826
|
+
const root = versionRoot(versionsDir, v);
|
|
827
|
+
const mark = active && path.resolve(active.root) === path.resolve(root) ? " ← 当前启用" : "";
|
|
828
|
+
console.log(` ${v}${root.includes(`${path.sep}node_modules${path.sep}`) ? "(npm)" : "(git)"}${mark}`);
|
|
829
|
+
}
|
|
830
|
+
if (LAYOUT.kind === "manager") console.log(`管理器(req 命令):${VERSION},${KIT_ROOT}`);
|
|
831
|
+
try {
|
|
832
|
+
const tags = npmJson(["view", PACKAGE_NAME, "dist-tags"]);
|
|
833
|
+
const shown = Object.entries(tags ?? {}).map(([tag, v]) => `${tag} ${v}`);
|
|
834
|
+
if (shown.length) console.log(`npm 上的版本:${shown.join(",")}`);
|
|
835
|
+
} catch {
|
|
836
|
+
// Offline or not published: nothing to add.
|
|
837
|
+
}
|
|
838
|
+
return 0;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function configureCommand() {
|
|
842
|
+
const tsx = findTsxCli(KIT_ROOT);
|
|
843
|
+
if (!tsx) {
|
|
844
|
+
console.error(`依赖未安装:在 ${KIT_ROOT} 中执行 npm install 后再运行配置向导。`);
|
|
845
|
+
return 1;
|
|
846
|
+
}
|
|
847
|
+
const result = spawnSync(process.execPath, [tsx, path.join(KIT_ROOT, "engine", "src", "configure.ts"), "--config", CONFIG], {
|
|
848
|
+
stdio: "inherit",
|
|
849
|
+
windowsHide: true,
|
|
850
|
+
});
|
|
851
|
+
return result.status ?? 1;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// ---------------------------------------------------------------------------
|
|
855
|
+
|
|
856
|
+
if (values.latest) process.exit(latestCommand());
|
|
857
|
+
if (values.upgrade) process.exit(upgradeCommand());
|
|
858
|
+
if (values.configure) process.exit(configureCommand());
|
|
859
|
+
if (values.setup) process.exit(setupCommand());
|
|
860
|
+
if (values.use) process.exit(useCommand());
|
|
861
|
+
if (values.versions) process.exit(versionsCommand());
|
|
862
|
+
|
|
863
|
+
// The npm-installed manager is not a version itself: plain installs go through setup,
|
|
864
|
+
// and checks or removal concern the version it manages.
|
|
865
|
+
if (LAYOUT.kind === "manager") {
|
|
866
|
+
if (values.check || values.uninstall || values.prune) {
|
|
867
|
+
const activeRoot = readJson(ACTIVE)?.root;
|
|
868
|
+
if (!activeRoot || !fs.existsSync(path.join(activeRoot, "install.mjs"))) {
|
|
869
|
+
console.log("✗ 还没有启用任何 req-kit 版本\n 修复:运行 req setup");
|
|
870
|
+
process.exit(1);
|
|
871
|
+
}
|
|
872
|
+
process.exit(spawnSync(process.execPath, [path.join(activeRoot, "install.mjs"), ...process.argv.slice(2)], { stdio: "inherit", windowsHide: true }).status ?? 1);
|
|
873
|
+
}
|
|
874
|
+
process.exit(setupCommand());
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const active = readJson(ACTIVE);
|
|
878
|
+
const roots = skillRoots(active);
|
|
879
|
+
|
|
880
|
+
if (values.uninstall) {
|
|
881
|
+
for (const root of roots) uninstallRoot(root);
|
|
882
|
+
if (fs.existsSync(ACTIVE)) fs.rmSync(ACTIVE);
|
|
883
|
+
for (const name of Object.keys(SHIMS)) fs.rmSync(path.join(SHIM_DIR, name), { force: true });
|
|
884
|
+
writeLog({ action: "uninstall", version: VERSION, root: KIT_ROOT });
|
|
885
|
+
say(`\n已卸载 skill。本机配置保留在 ${KIT_HOME};不再使用时可以手工删除它和安装目录。`);
|
|
886
|
+
process.exit(failures === 0 ? 0 : 1);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
if (values.prune) {
|
|
890
|
+
prune(active);
|
|
891
|
+
process.exit(0);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
say(values.check ? `检查 req-kit ${VERSION}(${KIT_ROOT}):\n` : `安装 req-kit ${VERSION}(${KIT_ROOT}):\n`);
|
|
895
|
+
if (values["import-legacy"] && !values.check) importLegacy(values["import-legacy"]);
|
|
896
|
+
checkRuntime();
|
|
897
|
+
checkConfig();
|
|
898
|
+
|
|
899
|
+
const skills = renderSkills();
|
|
900
|
+
if (values.check) {
|
|
901
|
+
checkActive(active);
|
|
902
|
+
for (const root of roots) checkRoot(root, skills);
|
|
903
|
+
checkShims(active);
|
|
904
|
+
} else {
|
|
905
|
+
for (const root of roots) installRoot(root, skills);
|
|
906
|
+
const targets = roots.filter((r) => !r.label.startsWith("通用目录") && !r.label.startsWith("Claude Code")).map((r) => r.dir);
|
|
907
|
+
fs.mkdirSync(KIT_HOME, { recursive: true });
|
|
908
|
+
const record = { version: VERSION, root: KIT_ROOT, targets, activatedAt: new Date().toISOString() };
|
|
909
|
+
fs.writeFileSync(ACTIVE, `${JSON.stringify(record, null, 2)}\n`);
|
|
910
|
+
writeLog({ action: "activate", version: VERSION, root: KIT_ROOT, previous: active ? { version: active.version, root: active.root } : null });
|
|
911
|
+
if (active && path.resolve(active.root) !== path.resolve(KIT_ROOT)) ok(`已从 ${active.version} 切换到 ${VERSION}`);
|
|
912
|
+
else ok(`已启用 ${VERSION}`);
|
|
913
|
+
writeShims();
|
|
914
|
+
}
|
|
915
|
+
checkLegacyLeftovers(roots);
|
|
916
|
+
|
|
917
|
+
say("");
|
|
918
|
+
say(`启动命令:node "${PLACEHOLDERS["{{PIPELINE_LAUNCHER}}"]}";终端中查看进度等:req help`);
|
|
919
|
+
if (failures === 0) {
|
|
920
|
+
say(
|
|
921
|
+
values.check
|
|
922
|
+
? "一切就绪。"
|
|
923
|
+
: "完成。新开的 Paseo 对话中即可使用 /req(Pi 中为 /skill:req);已经打开的对话需要新开一个才能看到。建议接着运行 node bin/req-doctor.mjs --no-project 检查 daemon 和 agent。",
|
|
924
|
+
);
|
|
925
|
+
} else {
|
|
926
|
+
say(`有 ${failures} 项需要处理,见上面的 ✗。处理后重新运行 node install.mjs${values.check ? " --check" : ""}。`);
|
|
927
|
+
}
|
|
928
|
+
process.exit(failures === 0 ? 0 : 1);
|