@openprd/cli 0.1.0
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/.openprd/README.md +82 -0
- package/.openprd/benchmarks/evidence/milvus-io-ai-code-review-gets-better-when-models-debate-claude-vs-gemini-vs-code.md +14 -0
- package/.openprd/benchmarks/evidence/nolanlawson-com-using-ai-to-write-better-code-more-slowly.md +14 -0
- package/.openprd/benchmarks/index.md +37 -0
- package/.openprd/benchmarks/sources.yaml +56 -0
- package/.openprd/config.yaml +50 -0
- package/.openprd/discovery/config.json +21 -0
- package/.openprd/engagements/active/flows.md +30 -0
- package/.openprd/engagements/active/handoff.md +9 -0
- package/.openprd/engagements/active/intake.md +15 -0
- package/.openprd/engagements/active/prd.md +161 -0
- package/.openprd/engagements/active/review.html +61 -0
- package/.openprd/engagements/active/roles.md +21 -0
- package/.openprd/engagements/work-units/wu-20260524015648-6d33ded7.json +23 -0
- package/.openprd/exports/.gitkeep +0 -0
- package/.openprd/knowledge/index.json +7 -0
- package/.openprd/quality/config.json +229 -0
- package/.openprd/reviews/v0001.html +1256 -0
- package/.openprd/schema/diagram-architecture.schema.yaml +49 -0
- package/.openprd/schema/diagram-product-flow.schema.yaml +52 -0
- package/.openprd/schema/prd.schema.yaml +121 -0
- package/.openprd/sessions/.gitkeep +0 -0
- package/.openprd/standards/config.json +88 -0
- package/.openprd/standards/file-manual-template.md +28 -0
- package/.openprd/standards/folder-readme-template.md +28 -0
- package/.openprd/state/.gitkeep +0 -0
- package/.openprd/state/changes.json +12 -0
- package/.openprd/state/current.json +169 -0
- package/.openprd/state/version-index.json +15 -0
- package/.openprd/state/versions/.gitkeep +0 -0
- package/.openprd/state/versions/v0001.json +121 -0
- package/.openprd/state/versions/v0001.md +161 -0
- package/.openprd/templates/agent/intake.md +6 -0
- package/.openprd/templates/agent/prd.md +21 -0
- package/.openprd/templates/b2b/intake.md +6 -0
- package/.openprd/templates/b2b/prd.md +24 -0
- package/.openprd/templates/base/intake.md +18 -0
- package/.openprd/templates/base/prd.md +67 -0
- package/.openprd/templates/company/README.md +10 -0
- package/.openprd/templates/consumer/intake.md +6 -0
- package/.openprd/templates/consumer/prd.md +19 -0
- package/.openprd/templates/diagram/architecture.contract.json +53 -0
- package/.openprd/templates/diagram/product-flow.contract.json +76 -0
- package/.openprd/templates/industry/README.md +16 -0
- package/.openprd/templates/manifest.yaml +27 -0
- package/.openprd/templates/project/README.md +14 -0
- package/.openprd/templates/session/README.md +14 -0
- package/AGENTS.md +44 -0
- package/CONTRIBUTING.md +30 -0
- package/LICENSE +21 -0
- package/README.md +727 -0
- package/README_CN.md +583 -0
- package/SECURITY.md +23 -0
- package/bin/openprd.js +5 -0
- package/docs/assets/openprd-capability-overview-en.png +0 -0
- package/docs/assets/openprd-capability-overview-zh.png +0 -0
- package/docs/assets/openprd-learning-html.png +0 -0
- package/docs/assets/openprd-quality-html.png +0 -0
- package/docs/assets/openprd-review-html.png +0 -0
- package/docs/assets/openprd-scenario-overview.png +0 -0
- package/docs/assets/openprd-scenario-overview.svg +114 -0
- package/docs/assets/openprd-self-evolving-mechanisms-en.png +0 -0
- package/docs/assets/openprd-self-evolving-mechanisms-zh.png +0 -0
- package/docs/assets/openprd-visual-compare-case-study-en.png +0 -0
- package/docs/assets/openprd-visual-compare-case-study-zh.png +0 -0
- package/package.json +59 -0
- package/scripts/openprd-dev-check.mjs +5 -0
- package/scripts/openprd-review-presentation.mjs +82 -0
- package/skills/openprd-benchmark-router/SKILL.md +92 -0
- package/skills/openprd-benchmark-router/agents/openai.yaml +4 -0
- package/skills/openprd-benchmark-router/references/benchmark-sources.md +74 -0
- package/skills/openprd-benchmark-router/references/evaluation-lenses.md +66 -0
- package/skills/openprd-benchmark-router/references/source-policy.md +35 -0
- package/skills/openprd-diagram-review/SKILL.md +91 -0
- package/skills/openprd-diagram-review/agents/openai.yaml +4 -0
- package/skills/openprd-diagram-review/examples/architecture-zh.md +8 -0
- package/skills/openprd-diagram-review/examples/product-flow-zh.md +7 -0
- package/skills/openprd-diagram-review/references/cocoon-patterns.md +17 -0
- package/skills/openprd-diagram-review/references/diagram-contracts.md +126 -0
- package/skills/openprd-diagram-review/references/review-checklist.md +10 -0
- package/skills/openprd-discovery-loop/SKILL.md +196 -0
- package/skills/openprd-discovery-loop/agents/openai.yaml +3 -0
- package/skills/openprd-harness/SKILL.md +179 -0
- package/skills/openprd-harness/agents/openai.yaml +4 -0
- package/skills/openprd-harness/examples/full-workflow-zh.md +9 -0
- package/skills/openprd-harness/references/command-map.md +71 -0
- package/skills/openprd-harness/references/examples.md +26 -0
- package/skills/openprd-harness/references/usage-guide.md +335 -0
- package/skills/openprd-harness/references/workflow-gates.md +51 -0
- package/skills/openprd-learning-review/SKILL.md +75 -0
- package/skills/openprd-learning-review/agents/openai.yaml +4 -0
- package/skills/openprd-learning-review/references/content-contract.md +125 -0
- package/skills/openprd-learning-review/references/ebook-reader.md +46 -0
- package/skills/openprd-learning-review/references/evidence-manifest.md +55 -0
- package/skills/openprd-learning-review/references/genre-library.md +43 -0
- package/skills/openprd-learning-review/references/prompt-engineering.md +71 -0
- package/skills/openprd-learning-review/references/quality-rubric.md +28 -0
- package/skills/openprd-learning-review/references/retrieval-worked-example.md +40 -0
- package/skills/openprd-learning-review/references/style-packs/xianxia-cultivation.prompt.md +67 -0
- package/skills/openprd-quality/SKILL.md +101 -0
- package/skills/openprd-requirement-intake/SKILL.md +76 -0
- package/skills/openprd-requirement-intake/agents/openai.yaml +4 -0
- package/skills/openprd-requirement-intake/references/prd-template-lenses.md +105 -0
- package/skills/openprd-requirement-intake/references/routing-rubric.md +64 -0
- package/skills/openprd-router/SKILL.md +40 -0
- package/skills/openprd-shared/SKILL.md +142 -0
- package/skills/openprd-shared/agents/openai.yaml +4 -0
- package/skills/openprd-shared/references/language-and-review.md +50 -0
- package/skills/openprd-shared/references/operating-rules.md +65 -0
- package/skills/openprd-shared/references/skill-architecture.md +70 -0
- package/skills/openprd-standards/SKILL.md +79 -0
- package/skills/openprd-standards/agents/openai.yaml +4 -0
- package/src/agent-integration.js +1717 -0
- package/src/benchmark.js +873 -0
- package/src/cli/args.js +460 -0
- package/src/cli/print.js +1423 -0
- package/src/codex-hook-runner-template.mjs +2422 -0
- package/src/dev-standards.js +372 -0
- package/src/diagram-core.js +1047 -0
- package/src/diagram-workspace.js +262 -0
- package/src/discovery.js +709 -0
- package/src/fleet.js +531 -0
- package/src/fs-utils.js +83 -0
- package/src/growth.js +545 -0
- package/src/html-artifacts.js +3803 -0
- package/src/knowledge.js +668 -0
- package/src/language-policy.js +142 -0
- package/src/learning-review.js +1655 -0
- package/src/loop.js +1290 -0
- package/src/openprd.js +1136 -0
- package/src/openspec/change-lifecycle.js +359 -0
- package/src/openspec/change-validate.js +248 -0
- package/src/openspec/constants.js +12 -0
- package/src/openspec/execute.js +300 -0
- package/src/openspec/generate.js +692 -0
- package/src/openspec/paths.js +111 -0
- package/src/openspec/tasks.js +352 -0
- package/src/prd-core.js +656 -0
- package/src/quality-html-artifact.js +1414 -0
- package/src/quality-learning.js +658 -0
- package/src/quality.js +1262 -0
- package/src/review-presentation.js +240 -0
- package/src/run-harness.js +1470 -0
- package/src/self-update.js +329 -0
- package/src/session-binding.js +140 -0
- package/src/source-inventory.js +224 -0
- package/src/standards.js +914 -0
- package/src/time.js +33 -0
- package/src/visual-compare.js +216 -0
- package/src/work-unit-migration.js +232 -0
- package/src/work-unit.js +88 -0
- package/src/workspace-core.js +1706 -0
- package/src/workspace-registry.js +162 -0
- package/src/workspace-workflow.js +1797 -0
package/src/fleet.js
ADDED
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { cjoin, exists, writeJson } from './fs-utils.js';
|
|
4
|
+
import { SOURCE_INVENTORY_IGNORE_DIRS } from './source-inventory.js';
|
|
5
|
+
import { timestamp } from './time.js';
|
|
6
|
+
import { readWorkspaceRegistry, upsertWorkspaceRegistryEntry } from './workspace-registry.js';
|
|
7
|
+
|
|
8
|
+
const FLEET_DEFAULT_MAX_DEPTH = 4;
|
|
9
|
+
const FLEET_IGNORE_DIRS = new Set([
|
|
10
|
+
...SOURCE_INVENTORY_IGNORE_DIRS,
|
|
11
|
+
'.idea',
|
|
12
|
+
'.DS_Store',
|
|
13
|
+
'.Trash',
|
|
14
|
+
'DerivedData',
|
|
15
|
+
'Library',
|
|
16
|
+
'logs',
|
|
17
|
+
'tmp',
|
|
18
|
+
]);
|
|
19
|
+
const FLEET_PROJECT_MARKERS = [
|
|
20
|
+
'.openprd',
|
|
21
|
+
'.codex',
|
|
22
|
+
'.claude',
|
|
23
|
+
'.cursor',
|
|
24
|
+
'AGENTS.md',
|
|
25
|
+
'CLAUDE.md',
|
|
26
|
+
'.git',
|
|
27
|
+
'package.json',
|
|
28
|
+
'pnpm-workspace.yaml',
|
|
29
|
+
'pyproject.toml',
|
|
30
|
+
'Cargo.toml',
|
|
31
|
+
'go.mod',
|
|
32
|
+
'deno.json',
|
|
33
|
+
'Makefile',
|
|
34
|
+
];
|
|
35
|
+
const FLEET_AGENT_MARKERS = ['.codex', '.claude', '.cursor', 'AGENTS.md', 'CLAUDE.md'];
|
|
36
|
+
|
|
37
|
+
function normalizeCsvList(value) {
|
|
38
|
+
if (!value) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
return String(value)
|
|
42
|
+
.split(',')
|
|
43
|
+
.map((item) => item.trim())
|
|
44
|
+
.filter(Boolean);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parsePositiveInteger(value, fallback) {
|
|
48
|
+
if (value === null || value === undefined || value === '') {
|
|
49
|
+
return fallback;
|
|
50
|
+
}
|
|
51
|
+
const parsed = Number.parseInt(String(value), 10);
|
|
52
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function projectPathMatches(projectRoot, candidatePath, patterns) {
|
|
56
|
+
if (patterns.length === 0) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
const absolutePath = path.resolve(candidatePath);
|
|
60
|
+
const relativePath = path.relative(projectRoot, absolutePath) || '.';
|
|
61
|
+
return patterns.some((pattern) => {
|
|
62
|
+
const resolvedPattern = path.isAbsolute(pattern) ? path.resolve(pattern) : pattern;
|
|
63
|
+
return absolutePath === resolvedPattern
|
|
64
|
+
|| absolutePath.includes(resolvedPattern)
|
|
65
|
+
|| relativePath === pattern
|
|
66
|
+
|| relativePath.includes(pattern);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function pathWithinRoot(rootPath, candidatePath) {
|
|
71
|
+
const relativePath = path.relative(rootPath, path.resolve(candidatePath));
|
|
72
|
+
return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function classifyFleetMarkers(markers) {
|
|
76
|
+
if (markers.includes('.openprd')) {
|
|
77
|
+
return 'openprd-workspace';
|
|
78
|
+
}
|
|
79
|
+
if (markers.some((marker) => FLEET_AGENT_MARKERS.includes(marker))) {
|
|
80
|
+
return 'agent-configured';
|
|
81
|
+
}
|
|
82
|
+
return 'plain-project';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function plannedFleetAction(category, options) {
|
|
86
|
+
if (category === 'openprd-workspace') {
|
|
87
|
+
if (options.updateOpenprd) {
|
|
88
|
+
return 'update';
|
|
89
|
+
}
|
|
90
|
+
if (options.backfillWorkUnits) {
|
|
91
|
+
return 'backfill-work-units';
|
|
92
|
+
}
|
|
93
|
+
if (options.doctor) {
|
|
94
|
+
return 'doctor';
|
|
95
|
+
}
|
|
96
|
+
if (options.syncRegistry) {
|
|
97
|
+
return 'sync-registry';
|
|
98
|
+
}
|
|
99
|
+
return 'report';
|
|
100
|
+
}
|
|
101
|
+
if (category === 'agent-configured') {
|
|
102
|
+
return options.setupMissing ? 'setup' : 'report';
|
|
103
|
+
}
|
|
104
|
+
return 'skip';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function detectFleetMarkers(projectPath) {
|
|
108
|
+
const entries = await fs.readdir(projectPath, { withFileTypes: true }).catch(() => []);
|
|
109
|
+
const names = new Set(entries.map((entry) => entry.name));
|
|
110
|
+
return FLEET_PROJECT_MARKERS.filter((marker) => names.has(marker));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function scanFleetProjects(rootPath, options = {}) {
|
|
114
|
+
const root = path.resolve(rootPath);
|
|
115
|
+
const maxDepth = parsePositiveInteger(options.maxDepth, FLEET_DEFAULT_MAX_DEPTH);
|
|
116
|
+
const include = normalizeCsvList(options.include);
|
|
117
|
+
const exclude = normalizeCsvList(options.exclude);
|
|
118
|
+
const projects = [];
|
|
119
|
+
const seenRealPaths = new Set();
|
|
120
|
+
|
|
121
|
+
async function walk(currentPath, depth) {
|
|
122
|
+
if (depth > maxDepth) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const name = path.basename(currentPath);
|
|
127
|
+
if (depth > 0 && FLEET_IGNORE_DIRS.has(name)) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const realPath = await fs.realpath(currentPath).catch(() => currentPath);
|
|
132
|
+
if (seenRealPaths.has(realPath)) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
seenRealPaths.add(realPath);
|
|
136
|
+
|
|
137
|
+
const markers = await detectFleetMarkers(currentPath);
|
|
138
|
+
if (markers.length > 0) {
|
|
139
|
+
const category = classifyFleetMarkers(markers);
|
|
140
|
+
const included = projectPathMatches(root, currentPath, include);
|
|
141
|
+
const excluded = exclude.length > 0 && projectPathMatches(root, currentPath, exclude);
|
|
142
|
+
if (included && !excluded) {
|
|
143
|
+
projects.push({
|
|
144
|
+
path: currentPath,
|
|
145
|
+
relativePath: path.relative(root, currentPath) || '.',
|
|
146
|
+
category,
|
|
147
|
+
markers,
|
|
148
|
+
discoverySource: 'scan',
|
|
149
|
+
registryKnown: false,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let entries = [];
|
|
155
|
+
try {
|
|
156
|
+
entries = await fs.readdir(currentPath, { withFileTypes: true });
|
|
157
|
+
} catch {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
for (const entry of entries) {
|
|
162
|
+
if (!entry.isDirectory() || entry.isSymbolicLink?.()) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (FLEET_IGNORE_DIRS.has(entry.name)) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
await walk(cjoin(currentPath, entry.name), depth + 1);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
await walk(root, 0);
|
|
173
|
+
return projects.sort((left, right) => left.path.localeCompare(right.path));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function collectRegistryFleetScope(rootPath, options, registry) {
|
|
177
|
+
const root = path.resolve(rootPath);
|
|
178
|
+
const include = normalizeCsvList(options.include);
|
|
179
|
+
const exclude = normalizeCsvList(options.exclude);
|
|
180
|
+
const projects = [];
|
|
181
|
+
let scopedKnown = 0;
|
|
182
|
+
let outsideRoot = 0;
|
|
183
|
+
|
|
184
|
+
for (const entry of registry.entries) {
|
|
185
|
+
if (!pathWithinRoot(root, entry.workspaceRoot)) {
|
|
186
|
+
outsideRoot += 1;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const included = projectPathMatches(root, entry.workspaceRoot, include);
|
|
190
|
+
const excluded = exclude.length > 0 && projectPathMatches(root, entry.workspaceRoot, exclude);
|
|
191
|
+
if (!included || excluded) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
scopedKnown += 1;
|
|
195
|
+
if (!(await exists(cjoin(entry.workspaceRoot, '.openprd')))) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
projects.push({
|
|
199
|
+
path: entry.workspaceRoot,
|
|
200
|
+
relativePath: path.relative(root, entry.workspaceRoot) || '.',
|
|
201
|
+
category: 'openprd-workspace',
|
|
202
|
+
markers: ['.openprd'],
|
|
203
|
+
discoverySource: 'registry',
|
|
204
|
+
registryKnown: true,
|
|
205
|
+
registryEntry: entry,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return { projects, scopedKnown, outsideRoot };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function mergeFleetProjects(scannedProjects, registryProjects) {
|
|
213
|
+
const merged = [];
|
|
214
|
+
const byKey = new Map();
|
|
215
|
+
|
|
216
|
+
async function mergeProject(project) {
|
|
217
|
+
const resolvedPath = path.resolve(project.path);
|
|
218
|
+
const key = await fs.realpath(resolvedPath).catch(() => resolvedPath);
|
|
219
|
+
const existing = byKey.get(key);
|
|
220
|
+
if (!existing) {
|
|
221
|
+
const next = { ...project, path: resolvedPath };
|
|
222
|
+
byKey.set(key, next);
|
|
223
|
+
merged.push(next);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
existing.markers = Array.from(new Set([...(existing.markers ?? []), ...(project.markers ?? [])]));
|
|
227
|
+
existing.registryKnown = existing.registryKnown || project.registryKnown;
|
|
228
|
+
existing.registryEntry = existing.registryEntry ?? project.registryEntry;
|
|
229
|
+
if (existing.discoverySource !== project.discoverySource) {
|
|
230
|
+
existing.discoverySource = 'scan+registry';
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
for (const project of scannedProjects) {
|
|
235
|
+
await mergeProject(project);
|
|
236
|
+
}
|
|
237
|
+
for (const project of registryProjects) {
|
|
238
|
+
await mergeProject(project);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return merged.sort((left, right) => left.path.localeCompare(right.path));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function summarizeFleetProjects(projects) {
|
|
245
|
+
const summary = {
|
|
246
|
+
total: projects.length,
|
|
247
|
+
openprd: 0,
|
|
248
|
+
agentConfigured: 0,
|
|
249
|
+
plain: 0,
|
|
250
|
+
planned: 0,
|
|
251
|
+
updated: 0,
|
|
252
|
+
setup: 0,
|
|
253
|
+
doctored: 0,
|
|
254
|
+
backfilled: 0,
|
|
255
|
+
synced: 0,
|
|
256
|
+
skipped: 0,
|
|
257
|
+
failed: 0,
|
|
258
|
+
healthAttention: 0,
|
|
259
|
+
};
|
|
260
|
+
for (const project of projects) {
|
|
261
|
+
if (project.category === 'openprd-workspace') {
|
|
262
|
+
summary.openprd += 1;
|
|
263
|
+
} else if (project.category === 'agent-configured') {
|
|
264
|
+
summary.agentConfigured += 1;
|
|
265
|
+
} else {
|
|
266
|
+
summary.plain += 1;
|
|
267
|
+
}
|
|
268
|
+
if (project.status === 'planned') {
|
|
269
|
+
summary.planned += 1;
|
|
270
|
+
} else if (project.status === 'updated') {
|
|
271
|
+
summary.updated += 1;
|
|
272
|
+
} else if (project.status === 'setup') {
|
|
273
|
+
summary.setup += 1;
|
|
274
|
+
} else if (project.status === 'doctored') {
|
|
275
|
+
summary.doctored += 1;
|
|
276
|
+
} else if (project.status === 'backfilled') {
|
|
277
|
+
summary.backfilled += 1;
|
|
278
|
+
} else if (project.status === 'synced') {
|
|
279
|
+
summary.synced += 1;
|
|
280
|
+
} else if (project.status === 'failed') {
|
|
281
|
+
summary.failed += 1;
|
|
282
|
+
} else if (project.status === 'skipped') {
|
|
283
|
+
summary.skipped += 1;
|
|
284
|
+
}
|
|
285
|
+
if (project.workUnits?.changedVersions > 0 && project.status !== 'backfilled') {
|
|
286
|
+
summary.backfilled += 1;
|
|
287
|
+
}
|
|
288
|
+
if (project.healthOk === false || (project.healthErrors?.length ?? 0) > 0) {
|
|
289
|
+
summary.healthAttention += 1;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return summary;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function collectFleetProjectHealth(projectPath, options, doctorWorkspace) {
|
|
296
|
+
if (!doctorWorkspace) {
|
|
297
|
+
return { ok: true, errors: [] };
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
const doctor = await doctorWorkspace(projectPath, {
|
|
301
|
+
tools: options.tools ?? 'all',
|
|
302
|
+
hookProfile: options.hookProfile,
|
|
303
|
+
enableUserCodexConfig: Boolean(options.enableUserCodexConfig),
|
|
304
|
+
codexHome: options.codexHome,
|
|
305
|
+
openprdHome: options.openprdHome,
|
|
306
|
+
});
|
|
307
|
+
return {
|
|
308
|
+
ok: doctor.ok,
|
|
309
|
+
errors: doctor.errors ?? [],
|
|
310
|
+
};
|
|
311
|
+
} catch (error) {
|
|
312
|
+
return {
|
|
313
|
+
ok: false,
|
|
314
|
+
errors: [error instanceof Error ? error.message : String(error)],
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function buildFleetRegistrySummary(rootPath, options, registry) {
|
|
320
|
+
const root = path.resolve(rootPath);
|
|
321
|
+
const include = normalizeCsvList(options.include);
|
|
322
|
+
const exclude = normalizeCsvList(options.exclude);
|
|
323
|
+
const scopedKnown = registry.entries.filter((entry) => {
|
|
324
|
+
if (!pathWithinRoot(root, entry.workspaceRoot)) {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
const included = projectPathMatches(root, entry.workspaceRoot, include);
|
|
328
|
+
const excluded = exclude.length > 0 && projectPathMatches(root, entry.workspaceRoot, exclude);
|
|
329
|
+
return included && !excluded;
|
|
330
|
+
}).length;
|
|
331
|
+
const outsideRoot = registry.entries.filter((entry) => !pathWithinRoot(root, entry.workspaceRoot)).length;
|
|
332
|
+
return {
|
|
333
|
+
home: registry.home,
|
|
334
|
+
registryPath: registry.registryPath,
|
|
335
|
+
knownTotal: registry.entries.length,
|
|
336
|
+
scopedKnown,
|
|
337
|
+
outsideRoot,
|
|
338
|
+
stale: registry.staleEntries.length,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function fleetWorkspaceImpl(rootPath, options = {}, dependencies = {}) {
|
|
343
|
+
const {
|
|
344
|
+
doctorWorkspace,
|
|
345
|
+
backfillWorkUnitsWorkspace,
|
|
346
|
+
setupAgentIntegrationWorkspace,
|
|
347
|
+
updateAgentIntegrationWorkspace,
|
|
348
|
+
} = dependencies;
|
|
349
|
+
const root = path.resolve(rootPath);
|
|
350
|
+
if (!(await exists(root))) {
|
|
351
|
+
throw new Error(`Fleet root does not exist: ${root}`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const hasMutationAction = Boolean(
|
|
355
|
+
options.updateOpenprd
|
|
356
|
+
|| options.setupMissing
|
|
357
|
+
|| options.doctor
|
|
358
|
+
|| options.backfillWorkUnits
|
|
359
|
+
|| options.syncRegistry,
|
|
360
|
+
);
|
|
361
|
+
const dryRun = Boolean(options.dryRun) || !hasMutationAction;
|
|
362
|
+
const registryBefore = await readWorkspaceRegistry({ openprdHome: options.openprdHome });
|
|
363
|
+
const registryScope = await collectRegistryFleetScope(root, options, registryBefore);
|
|
364
|
+
const scanned = await scanFleetProjects(root, options);
|
|
365
|
+
const mergedProjects = await mergeFleetProjects(scanned, registryScope.projects);
|
|
366
|
+
const projects = [];
|
|
367
|
+
|
|
368
|
+
for (const project of mergedProjects) {
|
|
369
|
+
const plannedAction = plannedFleetAction(project.category, options);
|
|
370
|
+
const item = {
|
|
371
|
+
...project,
|
|
372
|
+
plannedAction,
|
|
373
|
+
status: plannedAction === 'skip' ? 'skipped' : (dryRun ? 'planned' : 'skipped'),
|
|
374
|
+
ok: true,
|
|
375
|
+
changes: [],
|
|
376
|
+
errors: [],
|
|
377
|
+
workUnits: null,
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
if (plannedAction === 'skip' || plannedAction === 'report') {
|
|
381
|
+
item.status = dryRun ? 'planned' : 'skipped';
|
|
382
|
+
projects.push(item);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (dryRun) {
|
|
387
|
+
if (plannedAction === 'backfill-work-units' && backfillWorkUnitsWorkspace) {
|
|
388
|
+
const backfill = await backfillWorkUnitsWorkspace(project.path, { dryRun: true });
|
|
389
|
+
item.workUnits = {
|
|
390
|
+
totalVersions: backfill.totalVersions,
|
|
391
|
+
changedVersions: backfill.changedVersions,
|
|
392
|
+
};
|
|
393
|
+
item.changes = backfill.changes.map((change) => ({ ...change, source: 'work-unit' }));
|
|
394
|
+
item.errors = backfill.errors ?? [];
|
|
395
|
+
item.ok = backfill.ok;
|
|
396
|
+
}
|
|
397
|
+
projects.push(item);
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
if (plannedAction === 'update') {
|
|
403
|
+
const update = await updateAgentIntegrationWorkspace(project.path, {
|
|
404
|
+
tools: options.tools ?? 'all',
|
|
405
|
+
hookProfile: options.hookProfile,
|
|
406
|
+
force: Boolean(options.force),
|
|
407
|
+
enableUserCodexConfig: Boolean(options.enableUserCodexConfig),
|
|
408
|
+
codexHome: options.codexHome,
|
|
409
|
+
openprdHome: options.openprdHome,
|
|
410
|
+
});
|
|
411
|
+
const backfill = backfillWorkUnitsWorkspace
|
|
412
|
+
? await backfillWorkUnitsWorkspace(project.path, {})
|
|
413
|
+
: { ok: true, changes: [], errors: [], totalVersions: 0, changedVersions: 0 };
|
|
414
|
+
const health = await collectFleetProjectHealth(project.path, options, doctorWorkspace);
|
|
415
|
+
item.status = update.ok && backfill.ok ? 'updated' : 'failed';
|
|
416
|
+
item.ok = update.ok && backfill.ok;
|
|
417
|
+
item.registry = update.registry ?? null;
|
|
418
|
+
item.changes = [
|
|
419
|
+
...(update.migration?.changes ?? []).map((change) => ({ ...change, source: 'workspace' })),
|
|
420
|
+
...(update.changes ?? []).map((change) => ({ ...change, source: 'agent' })),
|
|
421
|
+
...(backfill.changes ?? []).map((change) => ({ ...change, source: 'work-unit' })),
|
|
422
|
+
];
|
|
423
|
+
item.doctorOk = health.ok;
|
|
424
|
+
item.healthOk = health.ok;
|
|
425
|
+
item.healthErrors = health.errors;
|
|
426
|
+
item.workUnits = {
|
|
427
|
+
totalVersions: backfill.totalVersions,
|
|
428
|
+
changedVersions: backfill.changedVersions,
|
|
429
|
+
};
|
|
430
|
+
item.errors = [...(update.doctor?.errors ?? []), ...(backfill.errors ?? [])];
|
|
431
|
+
} else if (plannedAction === 'setup') {
|
|
432
|
+
const setup = await setupAgentIntegrationWorkspace(project.path, {
|
|
433
|
+
tools: options.tools ?? 'all',
|
|
434
|
+
hookProfile: options.hookProfile,
|
|
435
|
+
force: Boolean(options.force),
|
|
436
|
+
enableUserCodexConfig: Boolean(options.enableUserCodexConfig),
|
|
437
|
+
codexHome: options.codexHome,
|
|
438
|
+
openprdHome: options.openprdHome,
|
|
439
|
+
});
|
|
440
|
+
item.status = setup.ok ? 'setup' : 'failed';
|
|
441
|
+
item.ok = setup.ok;
|
|
442
|
+
item.registry = setup.registry ?? null;
|
|
443
|
+
item.changes = [
|
|
444
|
+
...(setup.migration?.changes ?? []).map((change) => ({ ...change, source: 'workspace' })),
|
|
445
|
+
...(setup.changes ?? []).map((change) => ({ ...change, source: 'agent' })),
|
|
446
|
+
];
|
|
447
|
+
item.errors = setup.doctor?.errors ?? [];
|
|
448
|
+
} else if (plannedAction === 'doctor') {
|
|
449
|
+
const doctor = await doctorWorkspace(project.path, {
|
|
450
|
+
tools: options.tools ?? 'all',
|
|
451
|
+
hookProfile: options.hookProfile,
|
|
452
|
+
enableUserCodexConfig: Boolean(options.enableUserCodexConfig),
|
|
453
|
+
codexHome: options.codexHome,
|
|
454
|
+
openprdHome: options.openprdHome,
|
|
455
|
+
});
|
|
456
|
+
item.status = doctor.ok ? 'doctored' : 'failed';
|
|
457
|
+
item.ok = doctor.ok;
|
|
458
|
+
item.doctorOk = doctor.ok;
|
|
459
|
+
item.errors = doctor.errors ?? [];
|
|
460
|
+
} else if (plannedAction === 'backfill-work-units') {
|
|
461
|
+
if (!backfillWorkUnitsWorkspace) {
|
|
462
|
+
throw new Error('Missing fleet dependency: backfillWorkUnitsWorkspace');
|
|
463
|
+
}
|
|
464
|
+
const backfill = await backfillWorkUnitsWorkspace(project.path, {});
|
|
465
|
+
item.status = backfill.ok ? (backfill.totalVersions > 0 ? 'backfilled' : 'skipped') : 'failed';
|
|
466
|
+
item.ok = backfill.ok;
|
|
467
|
+
item.workUnits = {
|
|
468
|
+
totalVersions: backfill.totalVersions,
|
|
469
|
+
changedVersions: backfill.changedVersions,
|
|
470
|
+
};
|
|
471
|
+
item.changes = (backfill.changes ?? []).map((change) => ({ ...change, source: 'work-unit' }));
|
|
472
|
+
item.errors = backfill.errors ?? [];
|
|
473
|
+
} else if (plannedAction === 'sync-registry') {
|
|
474
|
+
const registrySync = await upsertWorkspaceRegistryEntry(project.path, {
|
|
475
|
+
openprdHome: options.openprdHome,
|
|
476
|
+
action: 'fleet-sync',
|
|
477
|
+
});
|
|
478
|
+
item.status = 'synced';
|
|
479
|
+
item.registry = registrySync;
|
|
480
|
+
}
|
|
481
|
+
} catch (error) {
|
|
482
|
+
item.status = 'failed';
|
|
483
|
+
item.ok = false;
|
|
484
|
+
item.errors = [error instanceof Error ? error.message : String(error)];
|
|
485
|
+
}
|
|
486
|
+
projects.push(item);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const registryAfter = await readWorkspaceRegistry({ openprdHome: options.openprdHome });
|
|
490
|
+
const registrySummary = buildFleetRegistrySummary(root, options, registryAfter);
|
|
491
|
+
const result = {
|
|
492
|
+
ok: projects.every((project) => project.ok),
|
|
493
|
+
action: 'fleet',
|
|
494
|
+
root,
|
|
495
|
+
dryRun,
|
|
496
|
+
tools: options.tools ?? 'all',
|
|
497
|
+
hookProfile: options.hookProfile ?? 'lite',
|
|
498
|
+
maxDepth: parsePositiveInteger(options.maxDepth, FLEET_DEFAULT_MAX_DEPTH),
|
|
499
|
+
include: normalizeCsvList(options.include),
|
|
500
|
+
exclude: normalizeCsvList(options.exclude),
|
|
501
|
+
requestedActions: {
|
|
502
|
+
updateOpenprd: Boolean(options.updateOpenprd),
|
|
503
|
+
setupMissing: Boolean(options.setupMissing),
|
|
504
|
+
doctor: Boolean(options.doctor),
|
|
505
|
+
backfillWorkUnits: Boolean(options.backfillWorkUnits),
|
|
506
|
+
syncRegistry: Boolean(options.syncRegistry),
|
|
507
|
+
},
|
|
508
|
+
scannedAt: timestamp(),
|
|
509
|
+
summary: summarizeFleetProjects(projects),
|
|
510
|
+
registry: registrySummary,
|
|
511
|
+
projects,
|
|
512
|
+
errors: projects.flatMap((project) => project.errors.map((error) => `${project.relativePath}: ${error}`)),
|
|
513
|
+
healthErrors: projects.flatMap((project) => (project.healthErrors ?? []).map((error) => `${project.relativePath}: ${error}`)),
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
if (options.report) {
|
|
517
|
+
const reportPath = path.resolve(options.report);
|
|
518
|
+
await writeJson(reportPath, result);
|
|
519
|
+
result.reportPath = reportPath;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return result;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function createFleetWorkspace(dependencies) {
|
|
526
|
+
return function fleetWorkspace(rootPath, options = {}) {
|
|
527
|
+
return fleetWorkspaceImpl(rootPath, options, dependencies);
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export { createFleetWorkspace };
|
package/src/fs-utils.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import YAML from 'yaml';
|
|
4
|
+
|
|
5
|
+
function cjoin(...parts) {
|
|
6
|
+
return path.join(...parts);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function exists(filePath) {
|
|
10
|
+
return fs.access(filePath).then(() => true).catch(() => false);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function readText(filePath) {
|
|
14
|
+
return fs.readFile(filePath, 'utf8');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function readYaml(filePath) {
|
|
18
|
+
const text = await readText(filePath);
|
|
19
|
+
const parsed = YAML.parse(text);
|
|
20
|
+
return parsed ?? {};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseYamlText(text) {
|
|
24
|
+
const parsed = YAML.parse(text);
|
|
25
|
+
return parsed ?? {};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function readJson(filePath) {
|
|
29
|
+
const text = await readText(filePath);
|
|
30
|
+
return JSON.parse(text);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function writeText(filePath, text) {
|
|
34
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
35
|
+
await fs.writeFile(filePath, text, 'utf8');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function appendText(filePath, text) {
|
|
39
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
40
|
+
await fs.appendFile(filePath, text, 'utf8');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function stringifyYaml(value) {
|
|
44
|
+
return YAML.stringify(value, { indent: 2, lineWidth: 100 });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function writeYaml(filePath, value) {
|
|
48
|
+
const text = stringifyYaml(value);
|
|
49
|
+
await writeText(filePath, text);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function writeJson(filePath, value) {
|
|
53
|
+
await writeText(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function appendJsonl(filePath, value) {
|
|
57
|
+
await appendText(filePath, `${JSON.stringify(value)}\n`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function readJsonl(filePath) {
|
|
61
|
+
const text = await readText(filePath);
|
|
62
|
+
return text
|
|
63
|
+
.split(/\r?\n/)
|
|
64
|
+
.map((line) => line.trim())
|
|
65
|
+
.filter(Boolean)
|
|
66
|
+
.map((line) => JSON.parse(line));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export {
|
|
70
|
+
appendJsonl,
|
|
71
|
+
appendText,
|
|
72
|
+
cjoin,
|
|
73
|
+
exists,
|
|
74
|
+
readJson,
|
|
75
|
+
readJsonl,
|
|
76
|
+
readText,
|
|
77
|
+
readYaml,
|
|
78
|
+
parseYamlText,
|
|
79
|
+
stringifyYaml,
|
|
80
|
+
writeJson,
|
|
81
|
+
writeText,
|
|
82
|
+
writeYaml,
|
|
83
|
+
};
|