@shahmarasy/prodo 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/LICENSE +21 -0
- package/README.md +157 -0
- package/bin/prodo.cjs +6 -0
- package/dist/agent-command-installer.d.ts +4 -0
- package/dist/agent-command-installer.js +158 -0
- package/dist/agents.d.ts +15 -0
- package/dist/agents.js +47 -0
- package/dist/artifact-registry.d.ts +11 -0
- package/dist/artifact-registry.js +49 -0
- package/dist/artifacts.d.ts +9 -0
- package/dist/artifacts.js +514 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.js +305 -0
- package/dist/consistency.d.ts +8 -0
- package/dist/consistency.js +268 -0
- package/dist/constants.d.ts +7 -0
- package/dist/constants.js +64 -0
- package/dist/doctor.d.ts +1 -0
- package/dist/doctor.js +123 -0
- package/dist/errors.d.ts +3 -0
- package/dist/errors.js +10 -0
- package/dist/hook-executor.d.ts +1 -0
- package/dist/hook-executor.js +175 -0
- package/dist/init-tui.d.ts +21 -0
- package/dist/init-tui.js +161 -0
- package/dist/init.d.ts +10 -0
- package/dist/init.js +307 -0
- package/dist/markdown.d.ts +11 -0
- package/dist/markdown.js +66 -0
- package/dist/normalize.d.ts +7 -0
- package/dist/normalize.js +73 -0
- package/dist/normalized-brief.d.ts +39 -0
- package/dist/normalized-brief.js +170 -0
- package/dist/output-index.d.ts +13 -0
- package/dist/output-index.js +55 -0
- package/dist/paths.d.ts +16 -0
- package/dist/paths.js +76 -0
- package/dist/preset-loader.d.ts +4 -0
- package/dist/preset-loader.js +210 -0
- package/dist/project-config.d.ts +14 -0
- package/dist/project-config.js +69 -0
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.js +12 -0
- package/dist/providers/mock-provider.d.ts +7 -0
- package/dist/providers/mock-provider.js +168 -0
- package/dist/providers/openai-provider.d.ts +11 -0
- package/dist/providers/openai-provider.js +69 -0
- package/dist/registry.d.ts +13 -0
- package/dist/registry.js +115 -0
- package/dist/settings.d.ts +6 -0
- package/dist/settings.js +34 -0
- package/dist/template-resolver.d.ts +11 -0
- package/dist/template-resolver.js +28 -0
- package/dist/templates.d.ts +33 -0
- package/dist/templates.js +428 -0
- package/dist/types.d.ts +35 -0
- package/dist/types.js +5 -0
- package/dist/utils.d.ts +6 -0
- package/dist/utils.js +53 -0
- package/dist/validate.d.ts +9 -0
- package/dist/validate.js +226 -0
- package/dist/validator.d.ts +5 -0
- package/dist/validator.js +80 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +30 -0
- package/dist/workflow-commands.d.ts +7 -0
- package/dist/workflow-commands.js +28 -0
- package/package.json +45 -0
- package/presets/fintech/preset.json +1 -0
- package/presets/fintech/prompts/prd.md +3 -0
- package/presets/marketplace/preset.json +1 -0
- package/presets/marketplace/prompts/prd.md +3 -0
- package/presets/saas/preset.json +1 -0
- package/presets/saas/prompts/prd.md +3 -0
- package/src/agent-command-installer.ts +174 -0
- package/src/agents.ts +56 -0
- package/src/artifact-registry.ts +69 -0
- package/src/artifacts.ts +606 -0
- package/src/cli.ts +322 -0
- package/src/consistency.ts +303 -0
- package/src/constants.ts +72 -0
- package/src/doctor.ts +137 -0
- package/src/errors.ts +7 -0
- package/src/hook-executor.ts +196 -0
- package/src/init-tui.ts +193 -0
- package/src/init.ts +375 -0
- package/src/markdown.ts +73 -0
- package/src/normalize.ts +89 -0
- package/src/normalized-brief.ts +206 -0
- package/src/output-index.ts +59 -0
- package/src/paths.ts +72 -0
- package/src/preset-loader.ts +237 -0
- package/src/project-config.ts +78 -0
- package/src/providers/index.ts +12 -0
- package/src/providers/mock-provider.ts +188 -0
- package/src/providers/openai-provider.ts +87 -0
- package/src/registry.ts +119 -0
- package/src/settings.ts +34 -0
- package/src/template-resolver.ts +33 -0
- package/src/templates.ts +440 -0
- package/src/types.ts +46 -0
- package/src/utils.ts +50 -0
- package/src/validate.ts +246 -0
- package/src/validator.ts +96 -0
- package/src/version.ts +24 -0
- package/src/workflow-commands.ts +31 -0
- package/templates/artifacts/prd.md +219 -0
- package/templates/artifacts/stories.md +49 -0
- package/templates/artifacts/techspec.md +42 -0
- package/templates/artifacts/wireframe.html +260 -0
- package/templates/artifacts/wireframe.md +22 -0
- package/templates/artifacts/workflow.md +22 -0
- package/templates/artifacts/workflow.mmd +6 -0
- package/templates/commands/prodo-normalize.md +24 -0
- package/templates/commands/prodo-prd.md +24 -0
- package/templates/commands/prodo-stories.md +24 -0
- package/templates/commands/prodo-techspec.md +24 -0
- package/templates/commands/prodo-validate.md +24 -0
- package/templates/commands/prodo-wireframe.md +24 -0
- package/templates/commands/prodo-workflow.md +24 -0
package/dist/init.js
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runInit = runInit;
|
|
7
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const node_crypto_1 = require("node:crypto");
|
|
10
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
11
|
+
const agent_command_installer_1 = require("./agent-command-installer");
|
|
12
|
+
const artifact_registry_1 = require("./artifact-registry");
|
|
13
|
+
const utils_1 = require("./utils");
|
|
14
|
+
const paths_1 = require("./paths");
|
|
15
|
+
const preset_loader_1 = require("./preset-loader");
|
|
16
|
+
const registry_1 = require("./registry");
|
|
17
|
+
const settings_1 = require("./settings");
|
|
18
|
+
const workflow_commands_1 = require("./workflow-commands");
|
|
19
|
+
const templates_1 = require("./templates");
|
|
20
|
+
function templateFileName(artifactType) {
|
|
21
|
+
if (artifactType === "workflow")
|
|
22
|
+
return `${artifactType}.mmd`;
|
|
23
|
+
if (artifactType === "wireframe")
|
|
24
|
+
return `${artifactType}.html`;
|
|
25
|
+
return `${artifactType}.md`;
|
|
26
|
+
}
|
|
27
|
+
async function writeFileIfMissing(filePath, content) {
|
|
28
|
+
if (await (0, utils_1.fileExists)(filePath))
|
|
29
|
+
return;
|
|
30
|
+
await promises_1.default.writeFile(filePath, content, "utf8");
|
|
31
|
+
}
|
|
32
|
+
async function readProdoVersion(cwd) {
|
|
33
|
+
const candidates = [
|
|
34
|
+
node_path_1.default.join(cwd, "package.json"),
|
|
35
|
+
node_path_1.default.resolve(__dirname, "..", "package.json")
|
|
36
|
+
];
|
|
37
|
+
for (const candidate of candidates) {
|
|
38
|
+
if (!(await (0, utils_1.fileExists)(candidate)))
|
|
39
|
+
continue;
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(await promises_1.default.readFile(candidate, "utf8"));
|
|
42
|
+
if (typeof parsed.version === "string" && parsed.version.trim().length > 0)
|
|
43
|
+
return parsed.version;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// ignore and continue
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return "0.0.0-dev";
|
|
50
|
+
}
|
|
51
|
+
async function fileSha256(filePath) {
|
|
52
|
+
const content = await promises_1.default.readFile(filePath);
|
|
53
|
+
return (0, node_crypto_1.createHash)("sha256").update(content).digest("hex");
|
|
54
|
+
}
|
|
55
|
+
async function listFilesRecursive(rootDir) {
|
|
56
|
+
if (!(await (0, utils_1.fileExists)(rootDir)))
|
|
57
|
+
return [];
|
|
58
|
+
const out = [];
|
|
59
|
+
const walk = async (current) => {
|
|
60
|
+
const entries = await promises_1.default.readdir(current, { withFileTypes: true });
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
const full = node_path_1.default.join(current, entry.name);
|
|
63
|
+
if (entry.isDirectory()) {
|
|
64
|
+
await walk(full);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
out.push(full);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
await walk(rootDir);
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
async function loadPreviousManifest(root) {
|
|
75
|
+
const manifestPath = node_path_1.default.join(root, "scaffold-manifest.json");
|
|
76
|
+
if (!(await (0, utils_1.fileExists)(manifestPath)))
|
|
77
|
+
return null;
|
|
78
|
+
try {
|
|
79
|
+
const parsed = JSON.parse(await promises_1.default.readFile(manifestPath, "utf8"));
|
|
80
|
+
if (!Array.isArray(parsed.assets))
|
|
81
|
+
return null;
|
|
82
|
+
return {
|
|
83
|
+
schema_version: "1.0",
|
|
84
|
+
generated_at: parsed.generated_at ?? "",
|
|
85
|
+
prodo_version: parsed.prodo_version ?? "0.0.0-dev",
|
|
86
|
+
copied_asset_count: Number(parsed.copied_asset_count ?? 0),
|
|
87
|
+
copied_assets: Array.isArray(parsed.copied_assets) ? parsed.copied_assets : [],
|
|
88
|
+
asset_count: Number(parsed.asset_count ?? 0),
|
|
89
|
+
parity_summary: {
|
|
90
|
+
match_count: Number(parsed.parity_summary?.match_count ?? 0),
|
|
91
|
+
drift_count: Number(parsed.parity_summary?.drift_count ?? 0),
|
|
92
|
+
missing_count: Number(parsed.parity_summary?.missing_count ?? 0),
|
|
93
|
+
protected_count: Number(parsed.parity_summary?.protected_count ?? 0),
|
|
94
|
+
updated_count: Number(parsed.parity_summary?.updated_count ?? 0),
|
|
95
|
+
unmanaged_count: Number(parsed.parity_summary?.unmanaged_count ?? 0)
|
|
96
|
+
},
|
|
97
|
+
assets: parsed.assets
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function copyDirIfMissing(sourceDir, targetDir, copiedAssets) {
|
|
105
|
+
if (!(await (0, utils_1.fileExists)(sourceDir)))
|
|
106
|
+
return;
|
|
107
|
+
await (0, utils_1.ensureDir)(targetDir);
|
|
108
|
+
const entries = await promises_1.default.readdir(sourceDir, { withFileTypes: true });
|
|
109
|
+
for (const entry of entries) {
|
|
110
|
+
const src = node_path_1.default.join(sourceDir, entry.name);
|
|
111
|
+
const dst = node_path_1.default.join(targetDir, entry.name);
|
|
112
|
+
if (entry.isDirectory()) {
|
|
113
|
+
await copyDirIfMissing(src, dst, copiedAssets);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (await (0, utils_1.fileExists)(dst))
|
|
117
|
+
continue;
|
|
118
|
+
await promises_1.default.copyFile(src, dst);
|
|
119
|
+
copiedAssets.push({
|
|
120
|
+
source: src,
|
|
121
|
+
target: dst,
|
|
122
|
+
sha256: await fileSha256(dst)
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function buildAssetManifest(pairs, previous, backup) {
|
|
127
|
+
const previousByTarget = new Map();
|
|
128
|
+
for (const item of previous?.assets ?? []) {
|
|
129
|
+
previousByTarget.set(node_path_1.default.resolve(item.target), item);
|
|
130
|
+
}
|
|
131
|
+
const items = [];
|
|
132
|
+
for (const pair of pairs) {
|
|
133
|
+
const sourceFiles = await listFilesRecursive(pair.sourceDir);
|
|
134
|
+
for (const sourceFile of sourceFiles) {
|
|
135
|
+
const relative = node_path_1.default.relative(pair.sourceDir, sourceFile);
|
|
136
|
+
const targetFile = node_path_1.default.join(pair.targetDir, relative);
|
|
137
|
+
const resolvedTarget = node_path_1.default.resolve(targetFile);
|
|
138
|
+
const sourceHash = await fileSha256(sourceFile);
|
|
139
|
+
const targetExists = await (0, utils_1.fileExists)(targetFile);
|
|
140
|
+
if (!targetExists) {
|
|
141
|
+
await (0, utils_1.ensureDir)(node_path_1.default.dirname(targetFile));
|
|
142
|
+
backup.set(resolvedTarget, null);
|
|
143
|
+
await promises_1.default.copyFile(sourceFile, targetFile);
|
|
144
|
+
items.push({
|
|
145
|
+
source: sourceFile,
|
|
146
|
+
target: targetFile,
|
|
147
|
+
source_sha256: sourceHash,
|
|
148
|
+
target_sha256: await fileSha256(targetFile),
|
|
149
|
+
status: "missing"
|
|
150
|
+
});
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const currentTargetHash = await fileSha256(targetFile);
|
|
154
|
+
const prev = previousByTarget.get(resolvedTarget);
|
|
155
|
+
const prevTargetHash = prev?.target_sha256 ?? null;
|
|
156
|
+
const prevSourceHash = prev?.source_sha256 ?? null;
|
|
157
|
+
if (currentTargetHash === sourceHash) {
|
|
158
|
+
items.push({
|
|
159
|
+
source: sourceFile,
|
|
160
|
+
target: targetFile,
|
|
161
|
+
source_sha256: sourceHash,
|
|
162
|
+
target_sha256: currentTargetHash,
|
|
163
|
+
status: "match"
|
|
164
|
+
});
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (prev && prevTargetHash && prevSourceHash && currentTargetHash === prevTargetHash && prevSourceHash !== sourceHash) {
|
|
168
|
+
if (!backup.has(resolvedTarget)) {
|
|
169
|
+
backup.set(resolvedTarget, await promises_1.default.readFile(targetFile));
|
|
170
|
+
}
|
|
171
|
+
await promises_1.default.copyFile(sourceFile, targetFile);
|
|
172
|
+
items.push({
|
|
173
|
+
source: sourceFile,
|
|
174
|
+
target: targetFile,
|
|
175
|
+
source_sha256: sourceHash,
|
|
176
|
+
target_sha256: await fileSha256(targetFile),
|
|
177
|
+
status: "updated"
|
|
178
|
+
});
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (prev) {
|
|
182
|
+
items.push({
|
|
183
|
+
source: sourceFile,
|
|
184
|
+
target: targetFile,
|
|
185
|
+
source_sha256: sourceHash,
|
|
186
|
+
target_sha256: currentTargetHash,
|
|
187
|
+
status: "protected"
|
|
188
|
+
});
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
items.push({
|
|
192
|
+
source: sourceFile,
|
|
193
|
+
target: targetFile,
|
|
194
|
+
source_sha256: sourceHash,
|
|
195
|
+
target_sha256: currentTargetHash,
|
|
196
|
+
status: "unmanaged"
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return items;
|
|
201
|
+
}
|
|
202
|
+
async function rollbackFiles(backup) {
|
|
203
|
+
for (const [target, content] of backup.entries()) {
|
|
204
|
+
if (content === null) {
|
|
205
|
+
if (await (0, utils_1.fileExists)(target))
|
|
206
|
+
await promises_1.default.rm(target, { force: true });
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
await (0, utils_1.ensureDir)(node_path_1.default.dirname(target));
|
|
210
|
+
await promises_1.default.writeFile(target, content);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function summarizeParity(items) {
|
|
214
|
+
const byStatus = (status) => items.filter((item) => item.status === status).length;
|
|
215
|
+
return {
|
|
216
|
+
match_count: byStatus("match"),
|
|
217
|
+
drift_count: byStatus("drift"),
|
|
218
|
+
missing_count: byStatus("missing"),
|
|
219
|
+
protected_count: byStatus("protected"),
|
|
220
|
+
updated_count: byStatus("updated"),
|
|
221
|
+
unmanaged_count: byStatus("unmanaged")
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
async function runInit(cwd, options) {
|
|
225
|
+
const root = (0, paths_1.prodoPath)(cwd);
|
|
226
|
+
const artifactDefs = await (0, artifact_registry_1.listArtifactDefinitions)(cwd);
|
|
227
|
+
const artifactTypes = artifactDefs.map((item) => item.name);
|
|
228
|
+
const workflowCommands = (0, workflow_commands_1.buildWorkflowCommands)(artifactTypes);
|
|
229
|
+
const prodoVersion = await readProdoVersion(cwd);
|
|
230
|
+
const localRepoTemplates = node_path_1.default.join(cwd, "templates");
|
|
231
|
+
const packagedTemplates = node_path_1.default.resolve(__dirname, "..", "templates");
|
|
232
|
+
const projectScaffoldTemplates = (await (0, utils_1.fileExists)(localRepoTemplates)) ? localRepoTemplates : packagedTemplates;
|
|
233
|
+
const copiedAssets = [];
|
|
234
|
+
const backup = new Map();
|
|
235
|
+
const previousManifest = await loadPreviousManifest(root);
|
|
236
|
+
await (0, utils_1.ensureDir)(node_path_1.default.join(root, "briefs"));
|
|
237
|
+
await (0, utils_1.ensureDir)(node_path_1.default.join(root, "schemas"));
|
|
238
|
+
await (0, utils_1.ensureDir)(node_path_1.default.join(root, "prompts"));
|
|
239
|
+
await (0, utils_1.ensureDir)(node_path_1.default.join(root, "commands"));
|
|
240
|
+
await (0, utils_1.ensureDir)(node_path_1.default.join(root, "presets"));
|
|
241
|
+
await (0, utils_1.ensureDir)(node_path_1.default.join(root, "templates"));
|
|
242
|
+
await (0, utils_1.ensureDir)(node_path_1.default.join(root, "templates", "overrides"));
|
|
243
|
+
await (0, utils_1.ensureDir)(node_path_1.default.join(root, "state"));
|
|
244
|
+
await (0, utils_1.ensureDir)(node_path_1.default.join(root, "state", "context"));
|
|
245
|
+
for (const def of artifactDefs) {
|
|
246
|
+
await (0, utils_1.ensureDir)((0, paths_1.outputDirPath)(cwd, def.name, def.output_dir));
|
|
247
|
+
}
|
|
248
|
+
await (0, utils_1.ensureDir)(node_path_1.default.join(cwd, "product-docs", "reports"));
|
|
249
|
+
await writeFileIfMissing((0, paths_1.outputIndexPath)(cwd), `${JSON.stringify({ active: {}, history: {}, updated_at: new Date(0).toISOString() }, null, 2)}\n`);
|
|
250
|
+
await writeFileIfMissing((0, paths_1.briefPath)(cwd), templates_1.START_BRIEF_TEMPLATE);
|
|
251
|
+
await writeFileIfMissing(node_path_1.default.join(root, "briefs", "normalized-brief.json"), `${JSON.stringify(templates_1.NORMALIZED_BRIEF_TEMPLATE, null, 2)}\n`);
|
|
252
|
+
await writeFileIfMissing(node_path_1.default.join(root, "hooks.yml"), templates_1.HOOKS_TEMPLATE);
|
|
253
|
+
await writeFileIfMissing(node_path_1.default.join(root, "prompts", "normalize.md"), `${templates_1.NORMALIZE_PROMPT_TEMPLATE}\n`);
|
|
254
|
+
const scriptType = options?.script ?? (process.platform === "win32" ? "ps" : "sh");
|
|
255
|
+
await promises_1.default.writeFile(node_path_1.default.join(root, "init-options.json"), `${JSON.stringify({ ai: options?.ai ?? null, lang: options?.lang ?? "en", preset: options?.preset ?? null, script: scriptType }, null, 2)}\n`, "utf8");
|
|
256
|
+
await copyDirIfMissing(node_path_1.default.join(projectScaffoldTemplates, "artifacts"), node_path_1.default.join(root, "templates"), copiedAssets);
|
|
257
|
+
for (const artifact of artifactDefs) {
|
|
258
|
+
const schema = {
|
|
259
|
+
...(0, templates_1.schemaTemplate)(artifact.name),
|
|
260
|
+
x_required_headings: artifact.required_headings
|
|
261
|
+
};
|
|
262
|
+
await writeFileIfMissing(node_path_1.default.join(root, "schemas", `${artifact.name}.yaml`), js_yaml_1.default.dump(schema));
|
|
263
|
+
await writeFileIfMissing(node_path_1.default.join(root, "prompts", `${artifact.name}.md`), `${(0, templates_1.promptTemplate)(artifact.name, options?.lang ?? "en")}\n`);
|
|
264
|
+
await writeFileIfMissing(node_path_1.default.join(root, "templates", templateFileName(artifact.name)), `${(0, templates_1.artifactTemplateTemplate)(artifact.name, options?.lang ?? "en")}\n`);
|
|
265
|
+
}
|
|
266
|
+
await copyDirIfMissing(node_path_1.default.join(projectScaffoldTemplates, "commands"), node_path_1.default.join(root, "commands"), copiedAssets);
|
|
267
|
+
for (const command of workflowCommands) {
|
|
268
|
+
await writeFileIfMissing(node_path_1.default.join(root, "commands", `${command.name}.md`), `${(0, templates_1.commandTemplate)(command)}\n`);
|
|
269
|
+
}
|
|
270
|
+
await (0, preset_loader_1.applyConfiguredPresets)(cwd, root, prodoVersion, options?.preset);
|
|
271
|
+
const pairs = [
|
|
272
|
+
{
|
|
273
|
+
sourceDir: node_path_1.default.join(projectScaffoldTemplates, "commands"),
|
|
274
|
+
targetDir: node_path_1.default.join(root, "commands")
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
sourceDir: node_path_1.default.join(projectScaffoldTemplates, "artifacts"),
|
|
278
|
+
targetDir: node_path_1.default.join(root, "templates")
|
|
279
|
+
}
|
|
280
|
+
];
|
|
281
|
+
let parity = [];
|
|
282
|
+
try {
|
|
283
|
+
parity = await buildAssetManifest(pairs, previousManifest, backup);
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
await rollbackFiles(backup);
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
const installedAgentFiles = options?.ai ? await (0, agent_command_installer_1.installAgentCommands)(cwd, options.ai) : [];
|
|
290
|
+
const manifest = {
|
|
291
|
+
schema_version: "1.0",
|
|
292
|
+
generated_at: new Date().toISOString(),
|
|
293
|
+
prodo_version: prodoVersion,
|
|
294
|
+
copied_asset_count: copiedAssets.length,
|
|
295
|
+
copied_assets: copiedAssets,
|
|
296
|
+
asset_count: parity.length,
|
|
297
|
+
parity_summary: summarizeParity(parity),
|
|
298
|
+
assets: parity
|
|
299
|
+
};
|
|
300
|
+
await promises_1.default.writeFile(node_path_1.default.join(root, "scaffold-manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
301
|
+
await (0, registry_1.syncRegistry)(cwd);
|
|
302
|
+
const settingsPath = await (0, settings_1.writeSettings)(cwd, {
|
|
303
|
+
lang: (options?.lang ?? "en").trim() || "en",
|
|
304
|
+
ai: options?.ai
|
|
305
|
+
});
|
|
306
|
+
return { installedAgentFiles, settingsPath };
|
|
307
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type MarkdownSection = {
|
|
2
|
+
heading: string;
|
|
3
|
+
headingKey: string;
|
|
4
|
+
level: number;
|
|
5
|
+
textLines: string[];
|
|
6
|
+
listItems: string[];
|
|
7
|
+
};
|
|
8
|
+
export declare function normalizeHeadingKey(heading: string): string;
|
|
9
|
+
export declare function parseMarkdownSections(markdown: string): MarkdownSection[];
|
|
10
|
+
export declare function extractRequiredHeadings(content: string): string[];
|
|
11
|
+
export declare function sectionTextMap(content: string): Map<string, string>;
|
package/dist/markdown.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeHeadingKey = normalizeHeadingKey;
|
|
4
|
+
exports.parseMarkdownSections = parseMarkdownSections;
|
|
5
|
+
exports.extractRequiredHeadings = extractRequiredHeadings;
|
|
6
|
+
exports.sectionTextMap = sectionTextMap;
|
|
7
|
+
function normalizeText(input) {
|
|
8
|
+
return input.trim().replace(/\s+/g, " ");
|
|
9
|
+
}
|
|
10
|
+
function normalizeHeadingKey(heading) {
|
|
11
|
+
return normalizeText(heading)
|
|
12
|
+
.toLowerCase()
|
|
13
|
+
.replace(/[^a-z0-9\s]/g, " ")
|
|
14
|
+
.replace(/\s+/g, " ")
|
|
15
|
+
.trim();
|
|
16
|
+
}
|
|
17
|
+
function parseMarkdownSections(markdown) {
|
|
18
|
+
const sections = [];
|
|
19
|
+
let current = null;
|
|
20
|
+
for (const rawLine of markdown.split(/\r?\n/)) {
|
|
21
|
+
const headingMatch = rawLine.match(/^\s*(#{1,6})\s+(.+?)\s*$/);
|
|
22
|
+
if (headingMatch) {
|
|
23
|
+
const title = normalizeText(headingMatch[2]);
|
|
24
|
+
current = {
|
|
25
|
+
heading: title,
|
|
26
|
+
headingKey: normalizeHeadingKey(title),
|
|
27
|
+
level: headingMatch[1].length,
|
|
28
|
+
textLines: [],
|
|
29
|
+
listItems: []
|
|
30
|
+
};
|
|
31
|
+
sections.push(current);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (!current)
|
|
35
|
+
continue;
|
|
36
|
+
const listMatch = rawLine.match(/^\s*(?:[-*+]|\d+\.)\s+(.+?)\s*$/);
|
|
37
|
+
if (listMatch) {
|
|
38
|
+
const item = normalizeText(listMatch[1]);
|
|
39
|
+
if (item.length > 0 && !current.listItems.includes(item))
|
|
40
|
+
current.listItems.push(item);
|
|
41
|
+
if (item.length > 0)
|
|
42
|
+
current.textLines.push(item);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const text = normalizeText(rawLine);
|
|
46
|
+
if (text.length > 0)
|
|
47
|
+
current.textLines.push(text);
|
|
48
|
+
}
|
|
49
|
+
return sections;
|
|
50
|
+
}
|
|
51
|
+
function extractRequiredHeadings(content) {
|
|
52
|
+
const sections = parseMarkdownSections(content);
|
|
53
|
+
return sections
|
|
54
|
+
.filter((section) => section.level === 2)
|
|
55
|
+
.map((section) => `## ${section.heading}`)
|
|
56
|
+
.filter((heading) => heading.length > 3);
|
|
57
|
+
}
|
|
58
|
+
function sectionTextMap(content) {
|
|
59
|
+
const sections = parseMarkdownSections(content);
|
|
60
|
+
const mapped = new Map();
|
|
61
|
+
for (const section of sections) {
|
|
62
|
+
const parts = [...section.listItems, ...section.textLines].filter((item) => item.length > 0);
|
|
63
|
+
mapped.set(`## ${section.heading}`, parts.join("\n").trim());
|
|
64
|
+
}
|
|
65
|
+
return mapped;
|
|
66
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runNormalize = runNormalize;
|
|
7
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const errors_1 = require("./errors");
|
|
10
|
+
const normalized_brief_1 = require("./normalized-brief");
|
|
11
|
+
const paths_1 = require("./paths");
|
|
12
|
+
const providers_1 = require("./providers");
|
|
13
|
+
const settings_1 = require("./settings");
|
|
14
|
+
const utils_1 = require("./utils");
|
|
15
|
+
function extractJsonObject(raw) {
|
|
16
|
+
const trimmed = raw.trim();
|
|
17
|
+
const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
18
|
+
const candidate = fenced ? fenced[1] : trimmed;
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(candidate);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
throw new errors_1.UserError("Normalizer provider did not return valid JSON.");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function runNormalize(options) {
|
|
27
|
+
const { cwd } = options;
|
|
28
|
+
const root = (0, paths_1.prodoPath)(cwd);
|
|
29
|
+
if (!(await (0, utils_1.fileExists)(root))) {
|
|
30
|
+
throw new errors_1.UserError("Missing .prodo directory. Run `prodo init .` first.");
|
|
31
|
+
}
|
|
32
|
+
const inPath = options.brief ? node_path_1.default.resolve(cwd, options.brief) : (0, paths_1.briefPath)(cwd);
|
|
33
|
+
if (!(await (0, utils_1.fileExists)(inPath))) {
|
|
34
|
+
throw new errors_1.UserError(`Brief file not found: ${inPath}`);
|
|
35
|
+
}
|
|
36
|
+
const rawBrief = await promises_1.default.readFile(inPath, "utf8");
|
|
37
|
+
const normalizePromptPath = node_path_1.default.join(root, "prompts", "normalize.md");
|
|
38
|
+
const normalizePrompt = await promises_1.default.readFile(normalizePromptPath, "utf8");
|
|
39
|
+
const settings = await (0, settings_1.readSettings)(cwd);
|
|
40
|
+
const provider = (0, providers_1.createProvider)();
|
|
41
|
+
const generated = await provider.generate(normalizePrompt, {
|
|
42
|
+
briefMarkdown: rawBrief,
|
|
43
|
+
sourceBriefPath: inPath,
|
|
44
|
+
outputLanguage: settings.lang
|
|
45
|
+
}, {
|
|
46
|
+
artifactType: "normalize",
|
|
47
|
+
requiredHeadings: [],
|
|
48
|
+
requiredContracts: []
|
|
49
|
+
});
|
|
50
|
+
const parsed = extractJsonObject(generated.body);
|
|
51
|
+
const withContracts = {
|
|
52
|
+
...parsed,
|
|
53
|
+
contracts: parsed.contracts ??
|
|
54
|
+
(0, normalized_brief_1.buildContractsFromArrays)({
|
|
55
|
+
goals: Array.isArray(parsed.goals) ? parsed.goals.filter((x) => typeof x === "string") : [],
|
|
56
|
+
core_features: Array.isArray(parsed.core_features)
|
|
57
|
+
? parsed.core_features.filter((x) => typeof x === "string")
|
|
58
|
+
: [],
|
|
59
|
+
constraints: Array.isArray(parsed.constraints)
|
|
60
|
+
? parsed.constraints.filter((x) => typeof x === "string")
|
|
61
|
+
: []
|
|
62
|
+
})
|
|
63
|
+
};
|
|
64
|
+
const normalized = (0, normalized_brief_1.parseNormalizedBriefOrThrow)(withContracts);
|
|
65
|
+
(0, normalized_brief_1.requireConfidenceOrThrow)(normalized, ["product_name", "problem", "audience", "goals", "core_features"], 0.7);
|
|
66
|
+
const outPath = options.out ? node_path_1.default.resolve(cwd, options.out) : (0, paths_1.normalizedBriefPath)(cwd);
|
|
67
|
+
if (!(0, utils_1.isPathInside)((0, paths_1.prodoPath)(cwd), outPath)) {
|
|
68
|
+
throw new errors_1.UserError("Normalize output must be inside `.prodo/`.");
|
|
69
|
+
}
|
|
70
|
+
await promises_1.default.mkdir(node_path_1.default.dirname(outPath), { recursive: true });
|
|
71
|
+
await promises_1.default.writeFile(outPath, `${JSON.stringify(normalized, null, 2)}\n`, "utf8");
|
|
72
|
+
return outPath;
|
|
73
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ValidationIssue } from "./types";
|
|
2
|
+
export type BriefContractItem = {
|
|
3
|
+
id: string;
|
|
4
|
+
text: string;
|
|
5
|
+
};
|
|
6
|
+
export type NormalizedBrief = {
|
|
7
|
+
schema_version: string;
|
|
8
|
+
product_name: string;
|
|
9
|
+
problem: string;
|
|
10
|
+
audience: string[];
|
|
11
|
+
goals: string[];
|
|
12
|
+
core_features: string[];
|
|
13
|
+
constraints: string[];
|
|
14
|
+
assumptions: string[];
|
|
15
|
+
contracts: {
|
|
16
|
+
goals: BriefContractItem[];
|
|
17
|
+
core_features: BriefContractItem[];
|
|
18
|
+
constraints: BriefContractItem[];
|
|
19
|
+
};
|
|
20
|
+
confidence?: Record<string, number>;
|
|
21
|
+
};
|
|
22
|
+
type BriefContracts = NormalizedBrief["contracts"];
|
|
23
|
+
export declare function buildContractsFromArrays(input: {
|
|
24
|
+
goals: string[];
|
|
25
|
+
core_features: string[];
|
|
26
|
+
constraints: string[];
|
|
27
|
+
}): BriefContracts;
|
|
28
|
+
export declare function parseNormalizedBrief(input: Record<string, unknown>): {
|
|
29
|
+
brief: NormalizedBrief;
|
|
30
|
+
issues: ValidationIssue[];
|
|
31
|
+
};
|
|
32
|
+
export declare function parseNormalizedBriefOrThrow(input: Record<string, unknown>): NormalizedBrief;
|
|
33
|
+
export declare function requireConfidenceOrThrow(brief: NormalizedBrief, fields: Array<keyof Pick<NormalizedBrief, "product_name" | "problem" | "audience" | "goals" | "core_features">>, threshold?: number): void;
|
|
34
|
+
export declare function contractIds(contracts: BriefContracts): {
|
|
35
|
+
goals: string[];
|
|
36
|
+
core_features: string[];
|
|
37
|
+
constraints: string[];
|
|
38
|
+
};
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.buildContractsFromArrays = buildContractsFromArrays;
|
|
7
|
+
exports.parseNormalizedBrief = parseNormalizedBrief;
|
|
8
|
+
exports.parseNormalizedBriefOrThrow = parseNormalizedBriefOrThrow;
|
|
9
|
+
exports.requireConfidenceOrThrow = requireConfidenceOrThrow;
|
|
10
|
+
exports.contractIds = contractIds;
|
|
11
|
+
const _2020_1 = __importDefault(require("ajv/dist/2020"));
|
|
12
|
+
const errors_1 = require("./errors");
|
|
13
|
+
const schema = {
|
|
14
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
15
|
+
type: "object",
|
|
16
|
+
required: [
|
|
17
|
+
"schema_version",
|
|
18
|
+
"product_name",
|
|
19
|
+
"problem",
|
|
20
|
+
"audience",
|
|
21
|
+
"goals",
|
|
22
|
+
"core_features",
|
|
23
|
+
"constraints",
|
|
24
|
+
"assumptions",
|
|
25
|
+
"contracts"
|
|
26
|
+
],
|
|
27
|
+
properties: {
|
|
28
|
+
schema_version: { type: "string", minLength: 1 },
|
|
29
|
+
product_name: { type: "string", minLength: 2 },
|
|
30
|
+
problem: { type: "string", minLength: 10 },
|
|
31
|
+
audience: { type: "array", minItems: 1, items: { type: "string", minLength: 2 } },
|
|
32
|
+
goals: { type: "array", minItems: 1, items: { type: "string", minLength: 2 } },
|
|
33
|
+
core_features: { type: "array", minItems: 1, items: { type: "string", minLength: 2 } },
|
|
34
|
+
constraints: { type: "array", items: { type: "string", minLength: 2 } },
|
|
35
|
+
assumptions: { type: "array", items: { type: "string", minLength: 2 } },
|
|
36
|
+
contracts: {
|
|
37
|
+
type: "object",
|
|
38
|
+
required: ["goals", "core_features", "constraints"],
|
|
39
|
+
properties: {
|
|
40
|
+
goals: { $ref: "#/$defs/contractArray" },
|
|
41
|
+
core_features: { $ref: "#/$defs/contractArray" },
|
|
42
|
+
constraints: { $ref: "#/$defs/contractArray" }
|
|
43
|
+
},
|
|
44
|
+
additionalProperties: false
|
|
45
|
+
},
|
|
46
|
+
confidence: {
|
|
47
|
+
type: "object",
|
|
48
|
+
additionalProperties: { type: "number", minimum: 0, maximum: 1 }
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
$defs: {
|
|
52
|
+
contractItem: {
|
|
53
|
+
type: "object",
|
|
54
|
+
required: ["id", "text"],
|
|
55
|
+
properties: {
|
|
56
|
+
id: { type: "string", pattern: "^[A-Z]+[0-9]+$" },
|
|
57
|
+
text: { type: "string", minLength: 2 }
|
|
58
|
+
},
|
|
59
|
+
additionalProperties: false
|
|
60
|
+
},
|
|
61
|
+
contractArray: { type: "array", items: { $ref: "#/$defs/contractItem" } }
|
|
62
|
+
},
|
|
63
|
+
additionalProperties: false
|
|
64
|
+
};
|
|
65
|
+
const ajv = new _2020_1.default({ allErrors: true, strict: false });
|
|
66
|
+
const validateFn = ajv.compile(schema);
|
|
67
|
+
function asString(value) {
|
|
68
|
+
if (typeof value !== "string")
|
|
69
|
+
return undefined;
|
|
70
|
+
const trimmed = value.trim();
|
|
71
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
72
|
+
}
|
|
73
|
+
function asStringArray(value) {
|
|
74
|
+
if (!Array.isArray(value))
|
|
75
|
+
return [];
|
|
76
|
+
return value
|
|
77
|
+
.map((item) => asString(item))
|
|
78
|
+
.filter((item) => typeof item === "string");
|
|
79
|
+
}
|
|
80
|
+
function asContracts(value) {
|
|
81
|
+
if (!Array.isArray(value))
|
|
82
|
+
return [];
|
|
83
|
+
return value
|
|
84
|
+
.map((item) => {
|
|
85
|
+
if (!item || typeof item !== "object")
|
|
86
|
+
return null;
|
|
87
|
+
const record = item;
|
|
88
|
+
const id = asString(record.id);
|
|
89
|
+
const text = asString(record.text);
|
|
90
|
+
if (!id || !text)
|
|
91
|
+
return null;
|
|
92
|
+
return { id, text };
|
|
93
|
+
})
|
|
94
|
+
.filter((item) => item !== null);
|
|
95
|
+
}
|
|
96
|
+
function asConfidence(value) {
|
|
97
|
+
if (!value || typeof value !== "object")
|
|
98
|
+
return undefined;
|
|
99
|
+
const result = {};
|
|
100
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
101
|
+
if (typeof raw !== "number" || Number.isNaN(raw))
|
|
102
|
+
continue;
|
|
103
|
+
result[key] = raw;
|
|
104
|
+
}
|
|
105
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
106
|
+
}
|
|
107
|
+
function normalizeInput(input) {
|
|
108
|
+
const rawContracts = input.contracts ?? {};
|
|
109
|
+
return {
|
|
110
|
+
schema_version: asString(input.schema_version) ?? "1.0",
|
|
111
|
+
product_name: asString(input.product_name) ?? "",
|
|
112
|
+
problem: asString(input.problem) ?? "",
|
|
113
|
+
audience: asStringArray(input.audience),
|
|
114
|
+
goals: asStringArray(input.goals),
|
|
115
|
+
core_features: asStringArray(input.core_features),
|
|
116
|
+
constraints: asStringArray(input.constraints),
|
|
117
|
+
assumptions: asStringArray(input.assumptions),
|
|
118
|
+
contracts: {
|
|
119
|
+
goals: asContracts(rawContracts.goals),
|
|
120
|
+
core_features: asContracts(rawContracts.core_features),
|
|
121
|
+
constraints: asContracts(rawContracts.constraints)
|
|
122
|
+
},
|
|
123
|
+
confidence: asConfidence(input.confidence)
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function buildContractsFromArrays(input) {
|
|
127
|
+
return {
|
|
128
|
+
goals: input.goals.map((text, index) => ({ id: `G${index + 1}`, text })),
|
|
129
|
+
core_features: input.core_features.map((text, index) => ({ id: `F${index + 1}`, text })),
|
|
130
|
+
constraints: input.constraints.map((text, index) => ({ id: `C${index + 1}`, text }))
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function parseNormalizedBrief(input) {
|
|
134
|
+
const normalized = normalizeInput(input);
|
|
135
|
+
const valid = validateFn(normalized);
|
|
136
|
+
if (valid)
|
|
137
|
+
return { brief: normalized, issues: [] };
|
|
138
|
+
const errors = validateFn.errors ?? [];
|
|
139
|
+
const issues = errors.map((error) => ({
|
|
140
|
+
level: "error",
|
|
141
|
+
code: "normalized_brief_invalid",
|
|
142
|
+
check: "schema",
|
|
143
|
+
field: error.instancePath || error.schemaPath,
|
|
144
|
+
message: `Normalized brief schema error: ${error.message ?? "unknown error"}`,
|
|
145
|
+
suggestion: "Fix missing content in start brief and rerun `prodo normalize`."
|
|
146
|
+
}));
|
|
147
|
+
return { brief: normalized, issues };
|
|
148
|
+
}
|
|
149
|
+
function parseNormalizedBriefOrThrow(input) {
|
|
150
|
+
const { brief, issues } = parseNormalizedBrief(input);
|
|
151
|
+
if (issues.length > 0) {
|
|
152
|
+
const detail = issues.map((issue) => `- ${issue.message}`).join("\n");
|
|
153
|
+
throw new errors_1.UserError(`Normalized brief is invalid:\n${detail}`);
|
|
154
|
+
}
|
|
155
|
+
return brief;
|
|
156
|
+
}
|
|
157
|
+
function requireConfidenceOrThrow(brief, fields, threshold = 0.7) {
|
|
158
|
+
const confidence = brief.confidence ?? {};
|
|
159
|
+
const missing = fields.filter((field) => (confidence[field] ?? 0) < threshold);
|
|
160
|
+
if (missing.length > 0) {
|
|
161
|
+
throw new errors_1.UserError(`Normalization confidence too low for: ${missing.join(", ")}. Improve brief clarity and rerun \`prodo normalize\`.`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function contractIds(contracts) {
|
|
165
|
+
return {
|
|
166
|
+
goals: contracts.goals.map((item) => item.id),
|
|
167
|
+
core_features: contracts.core_features.map((item) => item.id),
|
|
168
|
+
constraints: contracts.constraints.map((item) => item.id)
|
|
169
|
+
};
|
|
170
|
+
}
|