@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
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ArtifactType } from "./types";
|
|
2
|
+
type ArtifactMap = Partial<Record<ArtifactType, string>>;
|
|
3
|
+
type ArtifactHistoryMap = Partial<Record<ArtifactType, string[]>>;
|
|
4
|
+
export type OutputIndex = {
|
|
5
|
+
active: ArtifactMap;
|
|
6
|
+
history: ArtifactHistoryMap;
|
|
7
|
+
updated_at: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function loadOutputIndex(cwd: string): Promise<OutputIndex>;
|
|
10
|
+
export declare function saveOutputIndex(cwd: string, index: OutputIndex): Promise<void>;
|
|
11
|
+
export declare function setActiveArtifact(cwd: string, type: ArtifactType, filePath: string): Promise<void>;
|
|
12
|
+
export declare function getActiveArtifactPath(cwd: string, type: ArtifactType): Promise<string | undefined>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
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.loadOutputIndex = loadOutputIndex;
|
|
7
|
+
exports.saveOutputIndex = saveOutputIndex;
|
|
8
|
+
exports.setActiveArtifact = setActiveArtifact;
|
|
9
|
+
exports.getActiveArtifactPath = getActiveArtifactPath;
|
|
10
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
11
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
const paths_1 = require("./paths");
|
|
13
|
+
const utils_1 = require("./utils");
|
|
14
|
+
function defaultIndex() {
|
|
15
|
+
return {
|
|
16
|
+
active: {},
|
|
17
|
+
history: {},
|
|
18
|
+
updated_at: new Date(0).toISOString()
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
async function loadOutputIndex(cwd) {
|
|
22
|
+
const indexPath = (0, paths_1.outputIndexPath)(cwd);
|
|
23
|
+
if (!(await (0, utils_1.fileExists)(indexPath)))
|
|
24
|
+
return defaultIndex();
|
|
25
|
+
const raw = await promises_1.default.readFile(indexPath, "utf8");
|
|
26
|
+
const parsed = JSON.parse(raw);
|
|
27
|
+
return {
|
|
28
|
+
active: parsed.active ?? {},
|
|
29
|
+
history: parsed.history ?? {},
|
|
30
|
+
updated_at: parsed.updated_at ?? new Date(0).toISOString()
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
async function saveOutputIndex(cwd, index) {
|
|
34
|
+
const indexPath = (0, paths_1.outputIndexPath)(cwd);
|
|
35
|
+
await (0, utils_1.ensureDir)(node_path_1.default.dirname(indexPath));
|
|
36
|
+
await promises_1.default.writeFile(indexPath, `${JSON.stringify(index, null, 2)}\n`, "utf8");
|
|
37
|
+
}
|
|
38
|
+
async function setActiveArtifact(cwd, type, filePath) {
|
|
39
|
+
const index = await loadOutputIndex(cwd);
|
|
40
|
+
const normalizedPath = node_path_1.default.resolve(filePath);
|
|
41
|
+
const existing = index.history[type] ?? [];
|
|
42
|
+
index.active[type] = normalizedPath;
|
|
43
|
+
index.history[type] = [normalizedPath, ...existing.filter((item) => item !== normalizedPath)].slice(0, 100);
|
|
44
|
+
index.updated_at = new Date().toISOString();
|
|
45
|
+
await saveOutputIndex(cwd, index);
|
|
46
|
+
}
|
|
47
|
+
async function getActiveArtifactPath(cwd, type) {
|
|
48
|
+
const index = await loadOutputIndex(cwd);
|
|
49
|
+
const candidate = index.active[type];
|
|
50
|
+
if (!candidate)
|
|
51
|
+
return undefined;
|
|
52
|
+
if (await (0, utils_1.fileExists)(candidate))
|
|
53
|
+
return candidate;
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
package/dist/paths.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ArtifactType } from "./types";
|
|
2
|
+
export declare function prodoPath(cwd: string): string;
|
|
3
|
+
export declare function briefPath(cwd: string): string;
|
|
4
|
+
export declare function normalizedBriefPath(cwd: string): string;
|
|
5
|
+
export declare function settingsPath(cwd: string): string;
|
|
6
|
+
export declare function registryPath(cwd: string): string;
|
|
7
|
+
export declare function promptPath(cwd: string, artifactType: ArtifactType): string;
|
|
8
|
+
export declare function templatePath(cwd: string, artifactType: ArtifactType): string;
|
|
9
|
+
export declare function overrideTemplatePath(cwd: string, artifactType: ArtifactType): string;
|
|
10
|
+
export declare function templateCandidatePaths(cwd: string, artifactType: ArtifactType): string[];
|
|
11
|
+
export declare function overrideTemplateCandidatePaths(cwd: string, artifactType: ArtifactType): string[];
|
|
12
|
+
export declare function schemaPath(cwd: string, artifactType: ArtifactType): string;
|
|
13
|
+
export declare function outputDirPath(cwd: string, artifactType: ArtifactType, outputDirOverride?: string): string;
|
|
14
|
+
export declare function reportPath(cwd: string): string;
|
|
15
|
+
export declare function outputIndexPath(cwd: string): string;
|
|
16
|
+
export declare function outputContextDirPath(cwd: string): string;
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
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.prodoPath = prodoPath;
|
|
7
|
+
exports.briefPath = briefPath;
|
|
8
|
+
exports.normalizedBriefPath = normalizedBriefPath;
|
|
9
|
+
exports.settingsPath = settingsPath;
|
|
10
|
+
exports.registryPath = registryPath;
|
|
11
|
+
exports.promptPath = promptPath;
|
|
12
|
+
exports.templatePath = templatePath;
|
|
13
|
+
exports.overrideTemplatePath = overrideTemplatePath;
|
|
14
|
+
exports.templateCandidatePaths = templateCandidatePaths;
|
|
15
|
+
exports.overrideTemplateCandidatePaths = overrideTemplateCandidatePaths;
|
|
16
|
+
exports.schemaPath = schemaPath;
|
|
17
|
+
exports.outputDirPath = outputDirPath;
|
|
18
|
+
exports.reportPath = reportPath;
|
|
19
|
+
exports.outputIndexPath = outputIndexPath;
|
|
20
|
+
exports.outputContextDirPath = outputContextDirPath;
|
|
21
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
22
|
+
const constants_1 = require("./constants");
|
|
23
|
+
function prodoPath(cwd) {
|
|
24
|
+
return node_path_1.default.join(cwd, constants_1.PRODO_DIR);
|
|
25
|
+
}
|
|
26
|
+
function briefPath(cwd) {
|
|
27
|
+
return node_path_1.default.join(cwd, "brief.md");
|
|
28
|
+
}
|
|
29
|
+
function normalizedBriefPath(cwd) {
|
|
30
|
+
return node_path_1.default.join(prodoPath(cwd), "briefs", "normalized-brief.json");
|
|
31
|
+
}
|
|
32
|
+
function settingsPath(cwd) {
|
|
33
|
+
return node_path_1.default.join(prodoPath(cwd), "settings.json");
|
|
34
|
+
}
|
|
35
|
+
function registryPath(cwd) {
|
|
36
|
+
return node_path_1.default.join(prodoPath(cwd), "registry.json");
|
|
37
|
+
}
|
|
38
|
+
function promptPath(cwd, artifactType) {
|
|
39
|
+
return node_path_1.default.join(prodoPath(cwd), "prompts", `${artifactType}.md`);
|
|
40
|
+
}
|
|
41
|
+
function templatePath(cwd, artifactType) {
|
|
42
|
+
return node_path_1.default.join(prodoPath(cwd), "templates", `${artifactType}.md`);
|
|
43
|
+
}
|
|
44
|
+
function overrideTemplatePath(cwd, artifactType) {
|
|
45
|
+
return node_path_1.default.join(prodoPath(cwd), "templates", "overrides", `${artifactType}.md`);
|
|
46
|
+
}
|
|
47
|
+
function templateExtensionsForArtifact(artifactType) {
|
|
48
|
+
if (artifactType === "workflow")
|
|
49
|
+
return ["md", "mmd"];
|
|
50
|
+
if (artifactType === "wireframe")
|
|
51
|
+
return ["md", "html"];
|
|
52
|
+
return ["md"];
|
|
53
|
+
}
|
|
54
|
+
function templateCandidatePaths(cwd, artifactType) {
|
|
55
|
+
const root = node_path_1.default.join(prodoPath(cwd), "templates");
|
|
56
|
+
return templateExtensionsForArtifact(artifactType).map((ext) => node_path_1.default.join(root, `${artifactType}.${ext}`));
|
|
57
|
+
}
|
|
58
|
+
function overrideTemplateCandidatePaths(cwd, artifactType) {
|
|
59
|
+
const root = node_path_1.default.join(prodoPath(cwd), "templates", "overrides");
|
|
60
|
+
return templateExtensionsForArtifact(artifactType).map((ext) => node_path_1.default.join(root, `${artifactType}.${ext}`));
|
|
61
|
+
}
|
|
62
|
+
function schemaPath(cwd, artifactType) {
|
|
63
|
+
return node_path_1.default.join(prodoPath(cwd), "schemas", `${artifactType}.yaml`);
|
|
64
|
+
}
|
|
65
|
+
function outputDirPath(cwd, artifactType, outputDirOverride) {
|
|
66
|
+
return node_path_1.default.join(cwd, "product-docs", outputDirOverride ?? (0, constants_1.defaultOutputDir)(artifactType));
|
|
67
|
+
}
|
|
68
|
+
function reportPath(cwd) {
|
|
69
|
+
return node_path_1.default.join(cwd, "product-docs", "reports", "latest-validation.md");
|
|
70
|
+
}
|
|
71
|
+
function outputIndexPath(cwd) {
|
|
72
|
+
return node_path_1.default.join(prodoPath(cwd), "state", "index.json");
|
|
73
|
+
}
|
|
74
|
+
function outputContextDirPath(cwd) {
|
|
75
|
+
return node_path_1.default.join(prodoPath(cwd), "state", "context");
|
|
76
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
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.applyConfiguredPresets = applyConfiguredPresets;
|
|
7
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
10
|
+
const errors_1 = require("./errors");
|
|
11
|
+
const project_config_1 = require("./project-config");
|
|
12
|
+
const utils_1 = require("./utils");
|
|
13
|
+
function parseVersion(version) {
|
|
14
|
+
return version.split(".").map((part) => Number(part.replace(/[^0-9]/g, "")) || 0).slice(0, 3);
|
|
15
|
+
}
|
|
16
|
+
function cmpVersion(a, b) {
|
|
17
|
+
const left = parseVersion(a);
|
|
18
|
+
const right = parseVersion(b);
|
|
19
|
+
for (let i = 0; i < 3; i++) {
|
|
20
|
+
if ((left[i] ?? 0) > (right[i] ?? 0))
|
|
21
|
+
return 1;
|
|
22
|
+
if ((left[i] ?? 0) < (right[i] ?? 0))
|
|
23
|
+
return -1;
|
|
24
|
+
}
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
async function readPresetManifest(presetDir) {
|
|
28
|
+
const candidates = ["preset.yaml", "preset.yml", "preset.json"];
|
|
29
|
+
for (const name of candidates) {
|
|
30
|
+
const file = node_path_1.default.join(presetDir, name);
|
|
31
|
+
if (!(await (0, utils_1.fileExists)(file)))
|
|
32
|
+
continue;
|
|
33
|
+
if (name.endsWith(".json")) {
|
|
34
|
+
const parsed = JSON.parse(await promises_1.default.readFile(file, "utf8"));
|
|
35
|
+
const presetName = typeof parsed.name === "string" ? parsed.name.trim() : node_path_1.default.basename(presetDir);
|
|
36
|
+
return {
|
|
37
|
+
name: presetName,
|
|
38
|
+
version: typeof parsed.version === "string" ? parsed.version : undefined,
|
|
39
|
+
priority: typeof parsed.priority === "number" ? parsed.priority : 0,
|
|
40
|
+
min_prodo_version: typeof parsed.min_prodo_version === "string" ? parsed.min_prodo_version : undefined,
|
|
41
|
+
max_prodo_version: typeof parsed.max_prodo_version === "string" ? parsed.max_prodo_version : undefined,
|
|
42
|
+
command_packs: Array.isArray(parsed.command_packs)
|
|
43
|
+
? parsed.command_packs.filter((item) => typeof item === "string")
|
|
44
|
+
: []
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const parsed = js_yaml_1.default.load(await promises_1.default.readFile(file, "utf8")) ?? {};
|
|
48
|
+
const presetName = typeof parsed.name === "string" ? parsed.name.trim() : node_path_1.default.basename(presetDir);
|
|
49
|
+
return {
|
|
50
|
+
name: presetName,
|
|
51
|
+
version: typeof parsed.version === "string" ? parsed.version : undefined,
|
|
52
|
+
priority: typeof parsed.priority === "number" ? parsed.priority : 0,
|
|
53
|
+
min_prodo_version: typeof parsed.min_prodo_version === "string" ? parsed.min_prodo_version : undefined,
|
|
54
|
+
max_prodo_version: typeof parsed.max_prodo_version === "string" ? parsed.max_prodo_version : undefined,
|
|
55
|
+
command_packs: Array.isArray(parsed.command_packs)
|
|
56
|
+
? parsed.command_packs.filter((item) => typeof item === "string")
|
|
57
|
+
: []
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
throw new errors_1.UserError(`Preset manifest is missing in ${presetDir} (expected preset.yaml or preset.json).`);
|
|
61
|
+
}
|
|
62
|
+
async function collectFilesRecursive(rootDir) {
|
|
63
|
+
if (!(await (0, utils_1.fileExists)(rootDir)))
|
|
64
|
+
return [];
|
|
65
|
+
const out = [];
|
|
66
|
+
const walk = async (current) => {
|
|
67
|
+
const entries = await promises_1.default.readdir(current, { withFileTypes: true });
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
const full = node_path_1.default.join(current, entry.name);
|
|
70
|
+
if (entry.isDirectory()) {
|
|
71
|
+
await walk(full);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
out.push(full);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
await walk(rootDir);
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
async function collectPresetOps(presetDir, prodoRoot, priority, order) {
|
|
82
|
+
const lanes = ["prompts", "schemas", "templates", "commands"];
|
|
83
|
+
const ops = [];
|
|
84
|
+
for (const lane of lanes) {
|
|
85
|
+
const sourceBase = node_path_1.default.join(presetDir, lane);
|
|
86
|
+
const files = await collectFilesRecursive(sourceBase);
|
|
87
|
+
for (const source of files) {
|
|
88
|
+
const relative = node_path_1.default.relative(sourceBase, source);
|
|
89
|
+
ops.push({
|
|
90
|
+
source,
|
|
91
|
+
target: node_path_1.default.join(prodoRoot, lane, relative),
|
|
92
|
+
priority,
|
|
93
|
+
order
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return ops;
|
|
98
|
+
}
|
|
99
|
+
async function resolvePresetDir(projectRoot, presetName) {
|
|
100
|
+
const candidates = [
|
|
101
|
+
node_path_1.default.join(projectRoot, "presets", presetName),
|
|
102
|
+
node_path_1.default.resolve(__dirname, "..", "presets", presetName)
|
|
103
|
+
];
|
|
104
|
+
for (const candidate of candidates) {
|
|
105
|
+
if (await (0, utils_1.fileExists)(candidate))
|
|
106
|
+
return candidate;
|
|
107
|
+
}
|
|
108
|
+
throw new errors_1.UserError(`Preset not found: ${presetName}. Create presets/${presetName} with a preset manifest.`);
|
|
109
|
+
}
|
|
110
|
+
async function writeInstalledPresets(prodoRoot, names) {
|
|
111
|
+
const file = node_path_1.default.join(prodoRoot, "presets", "installed.json");
|
|
112
|
+
await (0, utils_1.ensureDir)(node_path_1.default.dirname(file));
|
|
113
|
+
await promises_1.default.writeFile(file, `${JSON.stringify(Array.from(new Set(names)).sort(), null, 2)}\n`, "utf8");
|
|
114
|
+
}
|
|
115
|
+
async function readInstalledPresets(prodoRoot) {
|
|
116
|
+
const file = node_path_1.default.join(prodoRoot, "presets", "installed.json");
|
|
117
|
+
if (!(await (0, utils_1.fileExists)(file)))
|
|
118
|
+
return [];
|
|
119
|
+
try {
|
|
120
|
+
const parsed = JSON.parse(await promises_1.default.readFile(file, "utf8"));
|
|
121
|
+
if (!Array.isArray(parsed))
|
|
122
|
+
return [];
|
|
123
|
+
return parsed
|
|
124
|
+
.filter((item) => typeof item === "string")
|
|
125
|
+
.map((item) => item.trim())
|
|
126
|
+
.filter((item) => item.length > 0);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async function applyCopyOps(ops) {
|
|
133
|
+
const selected = new Map();
|
|
134
|
+
for (const op of ops) {
|
|
135
|
+
const current = selected.get(op.target);
|
|
136
|
+
if (!current) {
|
|
137
|
+
selected.set(op.target, op);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (op.priority > current.priority || (op.priority === current.priority && op.order > current.order)) {
|
|
141
|
+
selected.set(op.target, op);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
for (const op of selected.values()) {
|
|
145
|
+
await (0, utils_1.ensureDir)(node_path_1.default.dirname(op.target));
|
|
146
|
+
await promises_1.default.copyFile(op.source, op.target);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async function collectCommandPackOps(projectRoot, prodoRoot, names) {
|
|
150
|
+
const ops = [];
|
|
151
|
+
for (const [index, name] of names.entries()) {
|
|
152
|
+
const base = node_path_1.default.join(projectRoot, "command-packs", name);
|
|
153
|
+
if (!(await (0, utils_1.fileExists)(base))) {
|
|
154
|
+
throw new errors_1.UserError(`Command pack not found: command-packs/${name}`);
|
|
155
|
+
}
|
|
156
|
+
const laneMap = {
|
|
157
|
+
commands: "commands"
|
|
158
|
+
};
|
|
159
|
+
for (const [sourceLane, targetLane] of Object.entries(laneMap)) {
|
|
160
|
+
const sourceBase = node_path_1.default.join(base, sourceLane);
|
|
161
|
+
const files = await collectFilesRecursive(sourceBase);
|
|
162
|
+
for (const source of files) {
|
|
163
|
+
const relative = node_path_1.default.relative(sourceBase, source);
|
|
164
|
+
ops.push({
|
|
165
|
+
source,
|
|
166
|
+
target: node_path_1.default.join(prodoRoot, targetLane, relative),
|
|
167
|
+
priority: 100,
|
|
168
|
+
order: index
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return ops;
|
|
174
|
+
}
|
|
175
|
+
async function applyConfiguredPresets(projectRoot, prodoRoot, prodoVersion, presetOverride) {
|
|
176
|
+
const config = await (0, project_config_1.readProjectConfig)(projectRoot);
|
|
177
|
+
const presets = Array.from(new Set([...(config.presets ?? []), ...(presetOverride ? [presetOverride] : [])]));
|
|
178
|
+
const existingInstalled = await readInstalledPresets(prodoRoot);
|
|
179
|
+
const allOps = [];
|
|
180
|
+
const installedNames = [...existingInstalled];
|
|
181
|
+
const commandPacks = new Set(config.command_packs ?? []);
|
|
182
|
+
for (const [order, presetName] of presets.entries()) {
|
|
183
|
+
const presetDir = await resolvePresetDir(projectRoot, presetName);
|
|
184
|
+
const manifest = await readPresetManifest(presetDir);
|
|
185
|
+
if (manifest.min_prodo_version && cmpVersion(prodoVersion, manifest.min_prodo_version) < 0) {
|
|
186
|
+
throw new errors_1.UserError(`Preset ${presetName} requires prodo >= ${manifest.min_prodo_version}, current is ${prodoVersion}.`);
|
|
187
|
+
}
|
|
188
|
+
if (manifest.max_prodo_version && cmpVersion(prodoVersion, manifest.max_prodo_version) > 0) {
|
|
189
|
+
throw new errors_1.UserError(`Preset ${presetName} supports prodo <= ${manifest.max_prodo_version}, current is ${prodoVersion}.`);
|
|
190
|
+
}
|
|
191
|
+
for (const pack of manifest.command_packs ?? []) {
|
|
192
|
+
if (pack.trim())
|
|
193
|
+
commandPacks.add(pack.trim());
|
|
194
|
+
}
|
|
195
|
+
installedNames.push(manifest.name || presetName);
|
|
196
|
+
allOps.push(...(await collectPresetOps(presetDir, prodoRoot, manifest.priority ?? 0, order)));
|
|
197
|
+
}
|
|
198
|
+
const commandPackList = Array.from(commandPacks);
|
|
199
|
+
if (commandPackList.length > 0) {
|
|
200
|
+
const commandPackOps = await collectCommandPackOps(projectRoot, prodoRoot, commandPackList);
|
|
201
|
+
allOps.push(...commandPackOps);
|
|
202
|
+
}
|
|
203
|
+
if (allOps.length > 0)
|
|
204
|
+
await applyCopyOps(allOps);
|
|
205
|
+
await writeInstalledPresets(prodoRoot, installedNames);
|
|
206
|
+
return {
|
|
207
|
+
installedPresets: Array.from(new Set(installedNames)),
|
|
208
|
+
appliedFiles: Array.from(new Set(allOps.map((item) => item.target)))
|
|
209
|
+
};
|
|
210
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ContractCoverage } from "./types";
|
|
2
|
+
export type ArtifactConfig = {
|
|
3
|
+
name: string;
|
|
4
|
+
output_dir?: string;
|
|
5
|
+
required_headings?: string[];
|
|
6
|
+
upstream?: string[];
|
|
7
|
+
required_contracts?: Array<keyof ContractCoverage>;
|
|
8
|
+
};
|
|
9
|
+
export type ProdoProjectConfig = {
|
|
10
|
+
presets?: string[];
|
|
11
|
+
artifacts?: ArtifactConfig[];
|
|
12
|
+
command_packs?: string[];
|
|
13
|
+
};
|
|
14
|
+
export declare function readProjectConfig(cwd: string): Promise<ProdoProjectConfig>;
|
|
@@ -0,0 +1,69 @@
|
|
|
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.readProjectConfig = readProjectConfig;
|
|
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 utils_1 = require("./utils");
|
|
11
|
+
function sanitizeStringArray(value) {
|
|
12
|
+
if (!Array.isArray(value))
|
|
13
|
+
return [];
|
|
14
|
+
return value
|
|
15
|
+
.filter((item) => typeof item === "string")
|
|
16
|
+
.map((item) => item.trim())
|
|
17
|
+
.filter((item) => item.length > 0);
|
|
18
|
+
}
|
|
19
|
+
function sanitizeArtifact(raw) {
|
|
20
|
+
if (!raw || typeof raw !== "object")
|
|
21
|
+
return null;
|
|
22
|
+
const rec = raw;
|
|
23
|
+
const name = typeof rec.name === "string" ? rec.name.trim() : "";
|
|
24
|
+
if (!name)
|
|
25
|
+
return null;
|
|
26
|
+
const outputDir = typeof rec.output_dir === "string" ? rec.output_dir.trim() : undefined;
|
|
27
|
+
const requiredHeadings = sanitizeStringArray(rec.required_headings);
|
|
28
|
+
const upstream = sanitizeStringArray(rec.upstream);
|
|
29
|
+
const requiredContracts = sanitizeStringArray(rec.required_contracts)
|
|
30
|
+
.filter((value) => value === "goals" || value === "core_features" || value === "constraints");
|
|
31
|
+
return {
|
|
32
|
+
name,
|
|
33
|
+
...(outputDir ? { output_dir: outputDir } : {}),
|
|
34
|
+
...(requiredHeadings.length > 0 ? { required_headings: requiredHeadings } : {}),
|
|
35
|
+
...(upstream.length > 0 ? { upstream } : {}),
|
|
36
|
+
...(requiredContracts.length > 0 ? { required_contracts: requiredContracts } : {})
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function sanitizeConfig(raw) {
|
|
40
|
+
if (!raw || typeof raw !== "object")
|
|
41
|
+
return {};
|
|
42
|
+
const rec = raw;
|
|
43
|
+
const artifacts = Array.isArray(rec.artifacts)
|
|
44
|
+
? rec.artifacts.map(sanitizeArtifact).filter((item) => item !== null)
|
|
45
|
+
: [];
|
|
46
|
+
return {
|
|
47
|
+
presets: sanitizeStringArray(rec.presets),
|
|
48
|
+
command_packs: sanitizeStringArray(rec.command_packs),
|
|
49
|
+
...(artifacts.length > 0 ? { artifacts } : {})
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async function readProjectConfig(cwd) {
|
|
53
|
+
const candidates = [
|
|
54
|
+
node_path_1.default.join(cwd, ".prodo", "config.json"),
|
|
55
|
+
node_path_1.default.join(cwd, "prodo.config.json")
|
|
56
|
+
];
|
|
57
|
+
for (const candidate of candidates) {
|
|
58
|
+
if (!(await (0, utils_1.fileExists)(candidate)))
|
|
59
|
+
continue;
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(await promises_1.default.readFile(candidate, "utf8"));
|
|
62
|
+
return sanitizeConfig(parsed);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
throw new errors_1.UserError(`Invalid project config JSON: ${candidate}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createProvider = createProvider;
|
|
4
|
+
const mock_provider_1 = require("./mock-provider");
|
|
5
|
+
const openai_provider_1 = require("./openai-provider");
|
|
6
|
+
function createProvider() {
|
|
7
|
+
const provider = (process.env.PRODO_LLM_PROVIDER ?? "mock").toLowerCase();
|
|
8
|
+
if (provider === "openai") {
|
|
9
|
+
return new openai_provider_1.OpenAIProvider();
|
|
10
|
+
}
|
|
11
|
+
return new mock_provider_1.MockProvider();
|
|
12
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { LLMProvider, ProviderSchemaHint } from "../types";
|
|
2
|
+
export declare class MockProvider implements LLMProvider {
|
|
3
|
+
generate(_prompt: string, inputContext: Record<string, unknown>, schemaHint: ProviderSchemaHint): Promise<{
|
|
4
|
+
body: string;
|
|
5
|
+
frontmatter?: Record<string, unknown>;
|
|
6
|
+
}>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MockProvider = void 0;
|
|
4
|
+
function asStringArray(value) {
|
|
5
|
+
if (!Array.isArray(value))
|
|
6
|
+
return [];
|
|
7
|
+
return value.filter((item) => typeof item === "string");
|
|
8
|
+
}
|
|
9
|
+
function asContracts(value) {
|
|
10
|
+
if (!Array.isArray(value))
|
|
11
|
+
return [];
|
|
12
|
+
return value
|
|
13
|
+
.map((item) => {
|
|
14
|
+
if (!item || typeof item !== "object")
|
|
15
|
+
return null;
|
|
16
|
+
const record = item;
|
|
17
|
+
if (typeof record.id !== "string" || typeof record.text !== "string")
|
|
18
|
+
return null;
|
|
19
|
+
return { id: record.id, text: record.text };
|
|
20
|
+
})
|
|
21
|
+
.filter((item) => item !== null);
|
|
22
|
+
}
|
|
23
|
+
function extractValue(markdown, heading) {
|
|
24
|
+
const aliases = heading.split("|").map((item) => item.trim().toLowerCase());
|
|
25
|
+
const lines = markdown.split(/\r?\n/);
|
|
26
|
+
let collect = false;
|
|
27
|
+
const out = [];
|
|
28
|
+
for (const raw of lines) {
|
|
29
|
+
const headingMatch = raw.match(/^#{1,6}\s+(.+?)\s*$/);
|
|
30
|
+
if (headingMatch) {
|
|
31
|
+
const current = headingMatch[1].trim().toLowerCase();
|
|
32
|
+
if (collect && !aliases.includes(current))
|
|
33
|
+
break;
|
|
34
|
+
collect = aliases.includes(current);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (!collect)
|
|
38
|
+
continue;
|
|
39
|
+
const cleaned = raw.replace(/^\s*[-*]\s*/, "").trim();
|
|
40
|
+
if (cleaned.length > 0)
|
|
41
|
+
out.push(cleaned);
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
function normalizeWithMock(inputContext) {
|
|
46
|
+
const brief = typeof inputContext.briefMarkdown === "string" ? inputContext.briefMarkdown : "";
|
|
47
|
+
const product = extractValue(brief, "Product Name|Name")[0] ?? "";
|
|
48
|
+
const problem = extractValue(brief, "Problem|Problem Statement").join(" ");
|
|
49
|
+
const audience = extractValue(brief, "Audience|Users|Persona");
|
|
50
|
+
const goals = extractValue(brief, "Goals|Objectives");
|
|
51
|
+
const features = extractValue(brief, "Core Features|Features|Capabilities");
|
|
52
|
+
const constraints = extractValue(brief, "Constraints|Limitations|Restrictions");
|
|
53
|
+
const assumptions = extractValue(brief, "Assumptions|Open Questions");
|
|
54
|
+
const contracts = {
|
|
55
|
+
goals: goals.map((text, index) => ({ id: `G${index + 1}`, text })),
|
|
56
|
+
core_features: features.map((text, index) => ({ id: `F${index + 1}`, text })),
|
|
57
|
+
constraints: constraints.map((text, index) => ({ id: `C${index + 1}`, text }))
|
|
58
|
+
};
|
|
59
|
+
const confidence = {
|
|
60
|
+
product_name: product ? 0.95 : 0.25,
|
|
61
|
+
problem: problem ? 0.95 : 0.25,
|
|
62
|
+
audience: audience.length > 0 ? 0.95 : 0.25,
|
|
63
|
+
goals: goals.length > 0 ? 0.95 : 0.25,
|
|
64
|
+
core_features: features.length > 0 ? 0.95 : 0.25
|
|
65
|
+
};
|
|
66
|
+
return JSON.stringify({
|
|
67
|
+
schema_version: "1.0",
|
|
68
|
+
product_name: product,
|
|
69
|
+
problem,
|
|
70
|
+
audience,
|
|
71
|
+
goals,
|
|
72
|
+
core_features: features,
|
|
73
|
+
constraints,
|
|
74
|
+
assumptions,
|
|
75
|
+
contracts,
|
|
76
|
+
confidence
|
|
77
|
+
}, null, 2);
|
|
78
|
+
}
|
|
79
|
+
function normalizeSectionItems(inputContext) {
|
|
80
|
+
const normalizedBrief = (inputContext.normalizedBrief ?? {});
|
|
81
|
+
const goals = asStringArray(normalizedBrief.goals);
|
|
82
|
+
const features = asStringArray(normalizedBrief.core_features);
|
|
83
|
+
const constraints = asStringArray(normalizedBrief.constraints);
|
|
84
|
+
return [...goals, ...features, ...constraints];
|
|
85
|
+
}
|
|
86
|
+
function coverageItems(schemaHint, inputContext) {
|
|
87
|
+
const catalog = (inputContext.contractCatalog ?? {});
|
|
88
|
+
const values = [];
|
|
89
|
+
if (schemaHint.requiredContracts.includes("goals"))
|
|
90
|
+
values.push(...asContracts(catalog.goals));
|
|
91
|
+
if (schemaHint.requiredContracts.includes("core_features"))
|
|
92
|
+
values.push(...asContracts(catalog.core_features));
|
|
93
|
+
if (schemaHint.requiredContracts.includes("constraints"))
|
|
94
|
+
values.push(...asContracts(catalog.constraints));
|
|
95
|
+
return values;
|
|
96
|
+
}
|
|
97
|
+
function headingBlock(heading, items, fallbackText, coverage) {
|
|
98
|
+
const selected = items.slice(0, 2);
|
|
99
|
+
const contractBullets = coverage.map((item) => `- [${item.id}] ${item.text}`);
|
|
100
|
+
const bullets = [...contractBullets, ...selected.map((item) => `- ${item}`)];
|
|
101
|
+
const finalBullets = bullets.length > 0 ? bullets.join("\n") : `- ${fallbackText}`;
|
|
102
|
+
return `${heading}\n${finalBullets}\n`;
|
|
103
|
+
}
|
|
104
|
+
function buildArtifactBody(schemaHint, inputContext) {
|
|
105
|
+
const normalizedBrief = (inputContext.normalizedBrief ?? {});
|
|
106
|
+
const productName = typeof normalizedBrief.product_name === "string" ? normalizedBrief.product_name : "Product";
|
|
107
|
+
const lang = typeof inputContext.outputLanguage === "string" ? inputContext.outputLanguage.toLowerCase() : "en";
|
|
108
|
+
const items = normalizeSectionItems(inputContext);
|
|
109
|
+
const coverage = coverageItems(schemaHint, inputContext);
|
|
110
|
+
const fallback = lang === "tr" ? "Detay daha sonra netlestirilecek." : "To be refined.";
|
|
111
|
+
const sections = schemaHint.requiredHeadings.map((heading) => headingBlock(heading, items, fallback, coverage));
|
|
112
|
+
const title = lang === "tr"
|
|
113
|
+
? `# ${productName} icin ${schemaHint.artifactType.toUpperCase()}`
|
|
114
|
+
: `# ${schemaHint.artifactType.toUpperCase()} for ${productName}`;
|
|
115
|
+
if (schemaHint.artifactType === "workflow") {
|
|
116
|
+
return `${title}\n\n${sections.join("\n")}\n\n\`\`\`mermaid
|
|
117
|
+
flowchart TD
|
|
118
|
+
A[Start] --> B[[F1] User Action]
|
|
119
|
+
B --> C[System Step]
|
|
120
|
+
C --> D[Done]
|
|
121
|
+
\`\`\``.trim();
|
|
122
|
+
}
|
|
123
|
+
return `${title}\n\n${sections.join("\n")}\n\n${lang === "tr" ? "Not" : "Note"}: ${fallback}`.trim();
|
|
124
|
+
}
|
|
125
|
+
function semanticIssuesWithMock(inputContext) {
|
|
126
|
+
const pair = inputContext.pair;
|
|
127
|
+
const leftBody = typeof pair?.left_body === "string" ? pair.left_body : "";
|
|
128
|
+
const rightBody = typeof pair?.right_body === "string" ? pair.right_body : "";
|
|
129
|
+
const leftFile = typeof pair?.left_file === "string" ? pair.left_file : "";
|
|
130
|
+
const rightFile = typeof pair?.right_file === "string" ? pair.right_file : "";
|
|
131
|
+
const issues = [];
|
|
132
|
+
const contradiction = /guest checkout/i.test(leftBody) && /(auth required|requires auth|must login)/i.test(rightBody);
|
|
133
|
+
if (contradiction) {
|
|
134
|
+
issues.push({
|
|
135
|
+
level: "error",
|
|
136
|
+
code: "semantic_contradiction",
|
|
137
|
+
check: "semantic_consistency",
|
|
138
|
+
contract_id: "F1",
|
|
139
|
+
file: rightFile || leftFile,
|
|
140
|
+
message: "Workflow allows guest checkout but paired artifact requires authentication.",
|
|
141
|
+
suggestion: "Align auth behavior across both artifacts."
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return JSON.stringify({ issues }, null, 2);
|
|
145
|
+
}
|
|
146
|
+
function contractRelevanceWithMock(inputContext) {
|
|
147
|
+
const contractText = typeof inputContext.contract_text === "string" ? inputContext.contract_text.toLowerCase() : "";
|
|
148
|
+
const contextText = typeof inputContext.context_text === "string" ? inputContext.context_text.toLowerCase() : "";
|
|
149
|
+
const terms = contractText
|
|
150
|
+
.split(/\W+/)
|
|
151
|
+
.map((term) => term.trim())
|
|
152
|
+
.filter((term) => term.length > 3);
|
|
153
|
+
const overlap = terms.filter((term) => contextText.includes(term)).length;
|
|
154
|
+
const relevant = terms.length === 0 ? true : overlap / terms.length >= 0.25;
|
|
155
|
+
return JSON.stringify({ relevant, score: terms.length === 0 ? 1 : overlap / terms.length }, null, 2);
|
|
156
|
+
}
|
|
157
|
+
class MockProvider {
|
|
158
|
+
async generate(_prompt, inputContext, schemaHint) {
|
|
159
|
+
if (schemaHint.artifactType === "normalize")
|
|
160
|
+
return { body: normalizeWithMock(inputContext) };
|
|
161
|
+
if (schemaHint.artifactType === "semantic_consistency")
|
|
162
|
+
return { body: semanticIssuesWithMock(inputContext) };
|
|
163
|
+
if (schemaHint.artifactType === "contract_relevance")
|
|
164
|
+
return { body: contractRelevanceWithMock(inputContext) };
|
|
165
|
+
return { body: buildArtifactBody(schemaHint, inputContext) };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
exports.MockProvider = MockProvider;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { LLMProvider, ProviderSchemaHint } from "../types";
|
|
2
|
+
export declare class OpenAIProvider implements LLMProvider {
|
|
3
|
+
private readonly apiKey;
|
|
4
|
+
private readonly model;
|
|
5
|
+
private readonly baseUrl;
|
|
6
|
+
constructor();
|
|
7
|
+
generate(prompt: string, inputContext: Record<string, unknown>, schemaHint: ProviderSchemaHint): Promise<{
|
|
8
|
+
body: string;
|
|
9
|
+
frontmatter?: Record<string, unknown>;
|
|
10
|
+
}>;
|
|
11
|
+
}
|