@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.
Files changed (120) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +157 -0
  3. package/bin/prodo.cjs +6 -0
  4. package/dist/agent-command-installer.d.ts +4 -0
  5. package/dist/agent-command-installer.js +158 -0
  6. package/dist/agents.d.ts +15 -0
  7. package/dist/agents.js +47 -0
  8. package/dist/artifact-registry.d.ts +11 -0
  9. package/dist/artifact-registry.js +49 -0
  10. package/dist/artifacts.d.ts +9 -0
  11. package/dist/artifacts.js +514 -0
  12. package/dist/cli.d.ts +9 -0
  13. package/dist/cli.js +305 -0
  14. package/dist/consistency.d.ts +8 -0
  15. package/dist/consistency.js +268 -0
  16. package/dist/constants.d.ts +7 -0
  17. package/dist/constants.js +64 -0
  18. package/dist/doctor.d.ts +1 -0
  19. package/dist/doctor.js +123 -0
  20. package/dist/errors.d.ts +3 -0
  21. package/dist/errors.js +10 -0
  22. package/dist/hook-executor.d.ts +1 -0
  23. package/dist/hook-executor.js +175 -0
  24. package/dist/init-tui.d.ts +21 -0
  25. package/dist/init-tui.js +161 -0
  26. package/dist/init.d.ts +10 -0
  27. package/dist/init.js +307 -0
  28. package/dist/markdown.d.ts +11 -0
  29. package/dist/markdown.js +66 -0
  30. package/dist/normalize.d.ts +7 -0
  31. package/dist/normalize.js +73 -0
  32. package/dist/normalized-brief.d.ts +39 -0
  33. package/dist/normalized-brief.js +170 -0
  34. package/dist/output-index.d.ts +13 -0
  35. package/dist/output-index.js +55 -0
  36. package/dist/paths.d.ts +16 -0
  37. package/dist/paths.js +76 -0
  38. package/dist/preset-loader.d.ts +4 -0
  39. package/dist/preset-loader.js +210 -0
  40. package/dist/project-config.d.ts +14 -0
  41. package/dist/project-config.js +69 -0
  42. package/dist/providers/index.d.ts +2 -0
  43. package/dist/providers/index.js +12 -0
  44. package/dist/providers/mock-provider.d.ts +7 -0
  45. package/dist/providers/mock-provider.js +168 -0
  46. package/dist/providers/openai-provider.d.ts +11 -0
  47. package/dist/providers/openai-provider.js +69 -0
  48. package/dist/registry.d.ts +13 -0
  49. package/dist/registry.js +115 -0
  50. package/dist/settings.d.ts +6 -0
  51. package/dist/settings.js +34 -0
  52. package/dist/template-resolver.d.ts +11 -0
  53. package/dist/template-resolver.js +28 -0
  54. package/dist/templates.d.ts +33 -0
  55. package/dist/templates.js +428 -0
  56. package/dist/types.d.ts +35 -0
  57. package/dist/types.js +5 -0
  58. package/dist/utils.d.ts +6 -0
  59. package/dist/utils.js +53 -0
  60. package/dist/validate.d.ts +9 -0
  61. package/dist/validate.js +226 -0
  62. package/dist/validator.d.ts +5 -0
  63. package/dist/validator.js +80 -0
  64. package/dist/version.d.ts +1 -0
  65. package/dist/version.js +30 -0
  66. package/dist/workflow-commands.d.ts +7 -0
  67. package/dist/workflow-commands.js +28 -0
  68. package/package.json +45 -0
  69. package/presets/fintech/preset.json +1 -0
  70. package/presets/fintech/prompts/prd.md +3 -0
  71. package/presets/marketplace/preset.json +1 -0
  72. package/presets/marketplace/prompts/prd.md +3 -0
  73. package/presets/saas/preset.json +1 -0
  74. package/presets/saas/prompts/prd.md +3 -0
  75. package/src/agent-command-installer.ts +174 -0
  76. package/src/agents.ts +56 -0
  77. package/src/artifact-registry.ts +69 -0
  78. package/src/artifacts.ts +606 -0
  79. package/src/cli.ts +322 -0
  80. package/src/consistency.ts +303 -0
  81. package/src/constants.ts +72 -0
  82. package/src/doctor.ts +137 -0
  83. package/src/errors.ts +7 -0
  84. package/src/hook-executor.ts +196 -0
  85. package/src/init-tui.ts +193 -0
  86. package/src/init.ts +375 -0
  87. package/src/markdown.ts +73 -0
  88. package/src/normalize.ts +89 -0
  89. package/src/normalized-brief.ts +206 -0
  90. package/src/output-index.ts +59 -0
  91. package/src/paths.ts +72 -0
  92. package/src/preset-loader.ts +237 -0
  93. package/src/project-config.ts +78 -0
  94. package/src/providers/index.ts +12 -0
  95. package/src/providers/mock-provider.ts +188 -0
  96. package/src/providers/openai-provider.ts +87 -0
  97. package/src/registry.ts +119 -0
  98. package/src/settings.ts +34 -0
  99. package/src/template-resolver.ts +33 -0
  100. package/src/templates.ts +440 -0
  101. package/src/types.ts +46 -0
  102. package/src/utils.ts +50 -0
  103. package/src/validate.ts +246 -0
  104. package/src/validator.ts +96 -0
  105. package/src/version.ts +24 -0
  106. package/src/workflow-commands.ts +31 -0
  107. package/templates/artifacts/prd.md +219 -0
  108. package/templates/artifacts/stories.md +49 -0
  109. package/templates/artifacts/techspec.md +42 -0
  110. package/templates/artifacts/wireframe.html +260 -0
  111. package/templates/artifacts/wireframe.md +22 -0
  112. package/templates/artifacts/workflow.md +22 -0
  113. package/templates/artifacts/workflow.mmd +6 -0
  114. package/templates/commands/prodo-normalize.md +24 -0
  115. package/templates/commands/prodo-prd.md +24 -0
  116. package/templates/commands/prodo-stories.md +24 -0
  117. package/templates/commands/prodo-techspec.md +24 -0
  118. package/templates/commands/prodo-validate.md +24 -0
  119. package/templates/commands/prodo-wireframe.md +24 -0
  120. package/templates/commands/prodo-workflow.md +24 -0
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OpenAIProvider = void 0;
4
+ const errors_1 = require("../errors");
5
+ class OpenAIProvider {
6
+ apiKey;
7
+ model;
8
+ baseUrl;
9
+ constructor() {
10
+ const key = process.env.OPENAI_API_KEY;
11
+ if (!key) {
12
+ throw new errors_1.UserError("OPENAI_API_KEY missing. Set it or use PRODO_LLM_PROVIDER=mock.");
13
+ }
14
+ this.apiKey = key;
15
+ this.model = process.env.PRODO_OPENAI_MODEL ?? "gpt-4o-mini";
16
+ this.baseUrl = process.env.PRODO_OPENAI_BASE_URL ?? "https://api.openai.com/v1";
17
+ }
18
+ async generate(prompt, inputContext, schemaHint) {
19
+ const outputLanguage = typeof inputContext.outputLanguage === "string" && inputContext.outputLanguage.trim()
20
+ ? inputContext.outputLanguage.trim()
21
+ : "en";
22
+ const mode = schemaHint.artifactType;
23
+ const system = mode === "normalize"
24
+ ? `You normalize messy human product briefs into strict JSON.
25
+ Return valid JSON only, no markdown. Include confidence scores (0..1) for critical fields.`
26
+ : mode === "semantic_consistency"
27
+ ? `You detect semantic inconsistencies between paired artifacts.
28
+ Return valid JSON only: { "issues": [{level, code, check, contract_id, file, message, suggestion}] }.`
29
+ : mode === "contract_relevance"
30
+ ? `You verify whether tagged content actually matches the referenced contract text.
31
+ Return valid JSON only: { "relevant": boolean, "score": number, "reason": string }.`
32
+ : `You are a product-document generator.
33
+ Return only Markdown body content.
34
+ Headings required:
35
+ ${schemaHint.requiredHeadings.join("\n")}
36
+ Required contract tags:
37
+ ${schemaHint.requiredContracts.join(", ")}
38
+ Use tags like [G1], [F2], [C1] where relevant.
39
+ Output language: ${outputLanguage}
40
+ Do not translate required headings.`;
41
+ const user = `${prompt}\n\nContext JSON:\n${JSON.stringify(inputContext, null, 2)}`;
42
+ const response = await fetch(`${this.baseUrl}/chat/completions`, {
43
+ method: "POST",
44
+ headers: {
45
+ "Content-Type": "application/json",
46
+ Authorization: `Bearer ${this.apiKey}`
47
+ },
48
+ body: JSON.stringify({
49
+ model: this.model,
50
+ messages: [
51
+ { role: "system", content: system },
52
+ { role: "user", content: user }
53
+ ],
54
+ temperature: 0.2
55
+ })
56
+ });
57
+ if (!response.ok) {
58
+ const text = await response.text();
59
+ throw new errors_1.UserError(`OpenAI request failed (${response.status}): ${text}`);
60
+ }
61
+ const payload = (await response.json());
62
+ const content = payload.choices?.[0]?.message?.content?.trim();
63
+ if (!content) {
64
+ throw new errors_1.UserError("OpenAI provider returned an empty response.");
65
+ }
66
+ return { body: content };
67
+ }
68
+ }
69
+ exports.OpenAIProvider = OpenAIProvider;
@@ -0,0 +1,13 @@
1
+ export type OverrideRegistryEntry = {
2
+ artifact_type: string;
3
+ file: string;
4
+ sha256: string;
5
+ };
6
+ export type ProdoRegistry = {
7
+ schema_version: "1.0";
8
+ updated_at: string;
9
+ installed_presets: string[];
10
+ installed_overrides: OverrideRegistryEntry[];
11
+ };
12
+ export declare function readRegistry(cwd: string): Promise<ProdoRegistry>;
13
+ export declare function syncRegistry(cwd: string): Promise<ProdoRegistry>;
@@ -0,0 +1,115 @@
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.readRegistry = readRegistry;
7
+ exports.syncRegistry = syncRegistry;
8
+ const promises_1 = __importDefault(require("node:fs/promises"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const node_crypto_1 = require("node:crypto");
11
+ const paths_1 = require("./paths");
12
+ const utils_1 = require("./utils");
13
+ const EMPTY_REGISTRY = {
14
+ schema_version: "1.0",
15
+ updated_at: new Date(0).toISOString(),
16
+ installed_presets: [],
17
+ installed_overrides: []
18
+ };
19
+ async function sha256(filePath) {
20
+ const raw = await promises_1.default.readFile(filePath);
21
+ return (0, node_crypto_1.createHash)("sha256").update(raw).digest("hex");
22
+ }
23
+ function sanitizeRegistry(input) {
24
+ if (!input || typeof input !== "object")
25
+ return { ...EMPTY_REGISTRY };
26
+ const raw = input;
27
+ const installedPresets = Array.isArray(raw.installed_presets)
28
+ ? raw.installed_presets
29
+ .filter((value) => typeof value === "string")
30
+ .map((value) => value.trim())
31
+ .filter((value) => value.length > 0)
32
+ : [];
33
+ const installedOverrides = Array.isArray(raw.installed_overrides)
34
+ ? raw.installed_overrides
35
+ .filter((item) => !!item && typeof item === "object")
36
+ .map((item) => ({
37
+ artifact_type: typeof item.artifact_type === "string" ? item.artifact_type.trim() : "",
38
+ file: typeof item.file === "string" ? item.file.trim() : "",
39
+ sha256: typeof item.sha256 === "string" ? item.sha256.trim() : ""
40
+ }))
41
+ .filter((item) => item.artifact_type && item.file && item.sha256)
42
+ : [];
43
+ return {
44
+ schema_version: "1.0",
45
+ updated_at: typeof raw.updated_at === "string" && raw.updated_at.trim() ? raw.updated_at : EMPTY_REGISTRY.updated_at,
46
+ installed_presets: Array.from(new Set(installedPresets)),
47
+ installed_overrides: installedOverrides
48
+ };
49
+ }
50
+ async function readRegistry(cwd) {
51
+ const file = (0, paths_1.registryPath)(cwd);
52
+ if (!(await (0, utils_1.fileExists)(file)))
53
+ return { ...EMPTY_REGISTRY };
54
+ try {
55
+ const parsed = JSON.parse(await promises_1.default.readFile(file, "utf8"));
56
+ return sanitizeRegistry(parsed);
57
+ }
58
+ catch {
59
+ return { ...EMPTY_REGISTRY };
60
+ }
61
+ }
62
+ async function readInstalledPresetsFromFile(cwd) {
63
+ const file = node_path_1.default.join(cwd, ".prodo", "presets", "installed.json");
64
+ if (!(await (0, utils_1.fileExists)(file)))
65
+ return [];
66
+ try {
67
+ const parsed = JSON.parse(await promises_1.default.readFile(file, "utf8"));
68
+ if (!Array.isArray(parsed))
69
+ return [];
70
+ return parsed
71
+ .filter((value) => typeof value === "string")
72
+ .map((value) => value.trim())
73
+ .filter((value) => value.length > 0);
74
+ }
75
+ catch {
76
+ return [];
77
+ }
78
+ }
79
+ async function discoverOverrides(cwd) {
80
+ const overridesDir = node_path_1.default.join(cwd, ".prodo", "templates", "overrides");
81
+ if (!(await (0, utils_1.fileExists)(overridesDir)))
82
+ return [];
83
+ const entries = await promises_1.default.readdir(overridesDir, { withFileTypes: true });
84
+ const out = [];
85
+ for (const entry of entries) {
86
+ if (!entry.isFile())
87
+ continue;
88
+ if (!entry.name.endsWith(".md"))
89
+ continue;
90
+ const fullPath = node_path_1.default.join(overridesDir, entry.name);
91
+ out.push({
92
+ artifact_type: entry.name.replace(/\.md$/, ""),
93
+ file: fullPath,
94
+ sha256: await sha256(fullPath)
95
+ });
96
+ }
97
+ out.sort((a, b) => a.artifact_type.localeCompare(b.artifact_type));
98
+ return out;
99
+ }
100
+ async function syncRegistry(cwd) {
101
+ const existing = await readRegistry(cwd);
102
+ const discoveredPresets = await readInstalledPresetsFromFile(cwd);
103
+ const discoveredOverrides = await discoverOverrides(cwd);
104
+ const mergedPresets = Array.from(new Set([...existing.installed_presets, ...discoveredPresets])).sort();
105
+ const merged = {
106
+ schema_version: "1.0",
107
+ updated_at: new Date().toISOString(),
108
+ installed_presets: mergedPresets,
109
+ installed_overrides: discoveredOverrides
110
+ };
111
+ const file = (0, paths_1.registryPath)(cwd);
112
+ await (0, utils_1.ensureDir)(node_path_1.default.dirname(file));
113
+ await promises_1.default.writeFile(file, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
114
+ return merged;
115
+ }
@@ -0,0 +1,6 @@
1
+ export type ProdoSettings = {
2
+ lang: string;
3
+ ai?: string;
4
+ };
5
+ export declare function readSettings(cwd: string): Promise<ProdoSettings>;
6
+ export declare function writeSettings(cwd: string, settings: ProdoSettings): Promise<string>;
@@ -0,0 +1,34 @@
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.readSettings = readSettings;
7
+ exports.writeSettings = writeSettings;
8
+ const promises_1 = __importDefault(require("node:fs/promises"));
9
+ const paths_1 = require("./paths");
10
+ const utils_1 = require("./utils");
11
+ const DEFAULT_SETTINGS = {
12
+ lang: "en"
13
+ };
14
+ async function readSettings(cwd) {
15
+ const path = (0, paths_1.settingsPath)(cwd);
16
+ if (!(await (0, utils_1.fileExists)(path)))
17
+ return { ...DEFAULT_SETTINGS };
18
+ try {
19
+ const raw = await promises_1.default.readFile(path, "utf8");
20
+ const parsed = JSON.parse(raw);
21
+ return {
22
+ lang: typeof parsed.lang === "string" && parsed.lang.trim() ? parsed.lang.trim() : "en",
23
+ ai: typeof parsed.ai === "string" && parsed.ai.trim() ? parsed.ai.trim() : undefined
24
+ };
25
+ }
26
+ catch {
27
+ return { ...DEFAULT_SETTINGS };
28
+ }
29
+ }
30
+ async function writeSettings(cwd, settings) {
31
+ const path = (0, paths_1.settingsPath)(cwd);
32
+ await promises_1.default.writeFile(path, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
33
+ return path;
34
+ }
@@ -0,0 +1,11 @@
1
+ import type { ArtifactType } from "./types";
2
+ type ResolveOptions = {
3
+ cwd: string;
4
+ artifactType: ArtifactType;
5
+ };
6
+ export declare function resolveTemplate(options: ResolveOptions): Promise<{
7
+ path: string;
8
+ content: string;
9
+ } | null>;
10
+ export declare function extractRequiredHeadingsFromTemplate(content: string): string[];
11
+ export {};
@@ -0,0 +1,28 @@
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.resolveTemplate = resolveTemplate;
7
+ exports.extractRequiredHeadingsFromTemplate = extractRequiredHeadingsFromTemplate;
8
+ const promises_1 = __importDefault(require("node:fs/promises"));
9
+ const paths_1 = require("./paths");
10
+ const markdown_1 = require("./markdown");
11
+ const utils_1 = require("./utils");
12
+ async function resolveTemplate(options) {
13
+ const { cwd, artifactType } = options;
14
+ const candidates = [
15
+ ...(0, paths_1.overrideTemplateCandidatePaths)(cwd, artifactType),
16
+ ...(0, paths_1.templateCandidatePaths)(cwd, artifactType)
17
+ ];
18
+ for (const filePath of candidates) {
19
+ if (await (0, utils_1.fileExists)(filePath)) {
20
+ const content = await promises_1.default.readFile(filePath, "utf8");
21
+ return { path: filePath, content };
22
+ }
23
+ }
24
+ return null;
25
+ }
26
+ function extractRequiredHeadingsFromTemplate(content) {
27
+ return (0, markdown_1.extractRequiredHeadings)(content);
28
+ }
@@ -0,0 +1,33 @@
1
+ import type { ArtifactType } from "./types";
2
+ import type { WorkflowCommand } from "./workflow-commands";
3
+ export declare function schemaTemplate(artifactType: ArtifactType): Record<string, unknown>;
4
+ export declare function promptTemplate(artifactType: ArtifactType, lang?: string): string;
5
+ export declare function artifactTemplateTemplate(artifactType: ArtifactType, lang?: string): string;
6
+ export declare const START_BRIEF_TEMPLATE = "# Product Brief\n\n## Product Name\nExample Product\n\n## Problem\nDescribe the user problem.\n\n## Audience\nWho this product is for.\n\n## Core Features\n- Feature A\n- Feature B\n\n## Goals\n- Goal 1\n- Goal 2\n\n## Constraints\n- Budget or timeline constraints\n- Compliance or technical constraints\n";
7
+ export declare const NORMALIZED_BRIEF_TEMPLATE: {
8
+ schema_version: string;
9
+ product_name: string;
10
+ problem: string;
11
+ audience: string[];
12
+ goals: string[];
13
+ core_features: string[];
14
+ constraints: string[];
15
+ assumptions: string[];
16
+ contracts: {
17
+ goals: {
18
+ id: string;
19
+ text: string;
20
+ }[];
21
+ core_features: {
22
+ id: string;
23
+ text: string;
24
+ }[];
25
+ constraints: {
26
+ id: string;
27
+ text: string;
28
+ }[];
29
+ };
30
+ };
31
+ export declare const NORMALIZE_PROMPT_TEMPLATE = "Normalize start-brief content into JSON.\n\nReturn JSON object with keys:\n- schema_version (string)\n- product_name (string)\n- problem (string)\n- audience (string[])\n- goals (string[])\n- core_features (string[])\n- constraints (string[])\n- assumptions (string[])\n- contracts.goals[] ({id,text})\n- contracts.core_features[] ({id,text})\n- contracts.constraints[] ({id,text})\n- confidence.product_name (0..1)\n- confidence.problem (0..1)\n- confidence.audience (0..1)\n- confidence.goals (0..1)\n- confidence.core_features (0..1)\n\nRules:\n- do NOT invent missing critical content\n- keep wording concise and concrete\n- if critical field is missing, return empty and low confidence (<0.7)\n- assign deterministic IDs: goals => G1..Gn, features => F1..Fn, constraints => C1..Cn\n- input files are read-only; never modify, summarize, or rewrite `brief.md` in-place\n- write normalized output as a new JSON object only";
32
+ export declare function commandTemplate(command: WorkflowCommand): string;
33
+ export declare const HOOKS_TEMPLATE = "# Hook item fields:\n# - command: string (required)\n# - optional: boolean (default false)\n# - enabled: boolean (default true)\n# - condition: shell command; run hook only if condition exits 0\n# - timeout_ms: per-attempt timeout in milliseconds (default 30000)\n# - retry: extra retry count after first attempt (default 0)\n# - retry_delay_ms: delay between retries (default 500)\n#\n# Example:\n# hooks:\n# before_prd:\n# - command: \"node -e \\\"console.log('lint ok')\\\"\"\n# optional: false\n# enabled: true\n# condition: \"node -e \\\"process.exit(0)\\\"\"\n# timeout_ms: 15000\n# retry: 1\n# retry_delay_ms: 300\n\nhooks:\n before_normalize: []\n after_normalize: []\n before_prd: []\n after_prd: []\n before_workflow: []\n after_workflow: []\n before_wireframe: []\n after_wireframe: []\n before_stories: []\n after_stories: []\n before_techspec: []\n after_techspec: []\n before_validate: []\n after_validate: []\n";