@shahmarasy/prodo 0.1.3 → 0.1.5

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 (205) hide show
  1. package/README.md +201 -97
  2. package/bin/prodo.cjs +6 -6
  3. package/dist/agents/agent-registry.d.ts +13 -0
  4. package/dist/agents/agent-registry.js +79 -0
  5. package/dist/agents/anthropic/index.d.ts +9 -0
  6. package/dist/agents/anthropic/index.js +55 -0
  7. package/dist/agents/base.d.ts +25 -0
  8. package/dist/agents/base.js +71 -0
  9. package/dist/agents/google/index.d.ts +9 -0
  10. package/dist/agents/google/index.js +53 -0
  11. package/dist/agents/mock/index.d.ts +11 -0
  12. package/dist/agents/mock/index.js +26 -0
  13. package/dist/agents/openai/index.d.ts +9 -0
  14. package/dist/agents/openai/index.js +57 -0
  15. package/dist/agents/system-prompts.d.ts +3 -0
  16. package/dist/agents/system-prompts.js +32 -0
  17. package/dist/agents.js +4 -2
  18. package/dist/artifacts.d.ts +1 -0
  19. package/dist/artifacts.js +265 -31
  20. package/dist/cli/agent-command-installer.d.ts +4 -0
  21. package/dist/cli/agent-command-installer.js +148 -0
  22. package/dist/cli/agent-ids.d.ts +15 -0
  23. package/dist/cli/agent-ids.js +49 -0
  24. package/dist/cli/doctor.d.ts +1 -0
  25. package/dist/cli/doctor.js +144 -0
  26. package/dist/cli/fix-tui.d.ts +4 -0
  27. package/dist/cli/fix-tui.js +79 -0
  28. package/dist/cli/index.d.ts +9 -0
  29. package/dist/cli/index.js +465 -0
  30. package/dist/cli/init-tui.d.ts +23 -0
  31. package/dist/cli/init-tui.js +176 -0
  32. package/dist/cli/init.d.ts +11 -0
  33. package/dist/cli/init.js +334 -0
  34. package/dist/cli/normalize-interactive.d.ts +8 -0
  35. package/dist/cli/normalize-interactive.js +167 -0
  36. package/dist/cli/preset-loader.d.ts +4 -0
  37. package/dist/cli/preset-loader.js +210 -0
  38. package/dist/cli.js +80 -3
  39. package/dist/core/artifact-registry.d.ts +11 -0
  40. package/dist/core/artifact-registry.js +49 -0
  41. package/dist/core/artifacts.d.ts +10 -0
  42. package/dist/core/artifacts.js +892 -0
  43. package/dist/core/clean.d.ts +10 -0
  44. package/dist/core/clean.js +74 -0
  45. package/dist/core/consistency.d.ts +8 -0
  46. package/dist/core/consistency.js +328 -0
  47. package/dist/core/constants.d.ts +7 -0
  48. package/dist/core/constants.js +64 -0
  49. package/dist/core/errors.d.ts +3 -0
  50. package/dist/core/errors.js +10 -0
  51. package/dist/core/fix.d.ts +31 -0
  52. package/dist/core/fix.js +188 -0
  53. package/dist/core/hook-executor.d.ts +1 -0
  54. package/dist/core/hook-executor.js +175 -0
  55. package/dist/core/markdown.d.ts +16 -0
  56. package/dist/core/markdown.js +81 -0
  57. package/dist/core/normalize.d.ts +8 -0
  58. package/dist/core/normalize.js +125 -0
  59. package/dist/core/normalized-brief.d.ts +48 -0
  60. package/dist/core/normalized-brief.js +182 -0
  61. package/dist/core/output-index.d.ts +13 -0
  62. package/dist/core/output-index.js +55 -0
  63. package/dist/core/paths.d.ts +17 -0
  64. package/dist/core/paths.js +80 -0
  65. package/dist/core/project-config.d.ts +14 -0
  66. package/dist/core/project-config.js +69 -0
  67. package/dist/core/registry.d.ts +13 -0
  68. package/dist/core/registry.js +115 -0
  69. package/dist/core/settings.d.ts +7 -0
  70. package/dist/core/settings.js +35 -0
  71. package/dist/core/template-engine.d.ts +3 -0
  72. package/dist/core/template-engine.js +43 -0
  73. package/dist/core/template-resolver.d.ts +15 -0
  74. package/dist/core/template-resolver.js +46 -0
  75. package/dist/core/templates.d.ts +33 -0
  76. package/dist/core/templates.js +440 -0
  77. package/dist/core/terminology.d.ts +21 -0
  78. package/dist/core/terminology.js +143 -0
  79. package/dist/core/tracing.d.ts +21 -0
  80. package/dist/core/tracing.js +74 -0
  81. package/dist/core/types.d.ts +35 -0
  82. package/dist/core/types.js +5 -0
  83. package/dist/core/utils.d.ts +7 -0
  84. package/dist/core/utils.js +66 -0
  85. package/dist/core/validate.d.ts +10 -0
  86. package/dist/core/validate.js +226 -0
  87. package/dist/core/validator.d.ts +5 -0
  88. package/dist/core/validator.js +76 -0
  89. package/dist/core/version.d.ts +1 -0
  90. package/dist/core/version.js +30 -0
  91. package/dist/core/workflow-commands.d.ts +7 -0
  92. package/dist/core/workflow-commands.js +29 -0
  93. package/dist/i18n/en.json +45 -0
  94. package/dist/i18n/index.d.ts +5 -0
  95. package/dist/i18n/index.js +63 -0
  96. package/dist/i18n/tr.json +45 -0
  97. package/dist/init-tui.d.ts +3 -0
  98. package/dist/init-tui.js +28 -1
  99. package/dist/init.d.ts +1 -0
  100. package/dist/init.js +9 -3
  101. package/dist/normalize.js +55 -7
  102. package/dist/providers/index.d.ts +2 -1
  103. package/dist/providers/index.js +20 -6
  104. package/dist/providers/mock-provider.d.ts +1 -1
  105. package/dist/providers/mock-provider.js +7 -6
  106. package/dist/providers/openai-provider.d.ts +1 -1
  107. package/dist/providers/openai-provider.js +3 -2
  108. package/dist/settings.d.ts +1 -0
  109. package/dist/settings.js +2 -1
  110. package/dist/skills/engine.d.ts +10 -0
  111. package/dist/skills/engine.js +75 -0
  112. package/dist/skills/fix-skill.d.ts +2 -0
  113. package/dist/skills/fix-skill.js +38 -0
  114. package/dist/skills/generate-artifact-skill.d.ts +2 -0
  115. package/dist/skills/generate-artifact-skill.js +32 -0
  116. package/dist/skills/generate-pipeline-skill.d.ts +2 -0
  117. package/dist/skills/generate-pipeline-skill.js +45 -0
  118. package/dist/skills/normalize-skill.d.ts +2 -0
  119. package/dist/skills/normalize-skill.js +29 -0
  120. package/dist/skills/types.d.ts +28 -0
  121. package/dist/skills/types.js +2 -0
  122. package/dist/skills/validate-skill.d.ts +2 -0
  123. package/dist/skills/validate-skill.js +29 -0
  124. package/dist/templates.d.ts +1 -1
  125. package/dist/templates.js +2 -0
  126. package/dist/utils.d.ts +1 -0
  127. package/dist/utils.js +13 -0
  128. package/dist/validator.js +0 -4
  129. package/dist/workflow-commands.js +2 -1
  130. package/package.json +74 -45
  131. package/presets/fintech/preset.json +48 -1
  132. package/presets/fintech/prompts/prd.md +99 -2
  133. package/presets/marketplace/preset.json +51 -1
  134. package/presets/marketplace/prompts/prd.md +140 -2
  135. package/presets/saas/preset.json +53 -1
  136. package/presets/saas/prompts/prd.md +150 -2
  137. package/src/agents/agent-registry.ts +93 -0
  138. package/src/agents/anthropic/index.ts +86 -0
  139. package/src/agents/anthropic/manifest.json +7 -0
  140. package/src/agents/base.ts +77 -0
  141. package/src/agents/google/index.ts +79 -0
  142. package/src/agents/google/manifest.json +7 -0
  143. package/src/agents/mock/index.ts +32 -0
  144. package/src/agents/mock/manifest.json +7 -0
  145. package/src/agents/openai/index.ts +83 -0
  146. package/src/agents/openai/manifest.json +7 -0
  147. package/src/agents/system-prompts.ts +35 -0
  148. package/src/{agent-command-installer.ts → cli/agent-command-installer.ts} +164 -164
  149. package/src/{agents.ts → cli/agent-ids.ts} +58 -56
  150. package/src/{doctor.ts → cli/doctor.ts} +157 -137
  151. package/src/cli/fix-tui.ts +111 -0
  152. package/src/{cli.ts → cli/index.ts} +459 -319
  153. package/src/{init-tui.ts → cli/init-tui.ts} +208 -179
  154. package/src/{init.ts → cli/init.ts} +398 -391
  155. package/src/cli/normalize-interactive.ts +241 -0
  156. package/src/{preset-loader.ts → cli/preset-loader.ts} +237 -237
  157. package/src/{artifact-registry.ts → core/artifact-registry.ts} +69 -69
  158. package/src/{artifacts.ts → core/artifacts.ts} +1081 -777
  159. package/src/core/clean.ts +88 -0
  160. package/src/{consistency.ts → core/consistency.ts} +374 -303
  161. package/src/{constants.ts → core/constants.ts} +72 -72
  162. package/src/{errors.ts → core/errors.ts} +7 -7
  163. package/src/core/fix.ts +253 -0
  164. package/src/{hook-executor.ts → core/hook-executor.ts} +196 -196
  165. package/src/{markdown.ts → core/markdown.ts} +93 -73
  166. package/src/core/normalize.ts +145 -0
  167. package/src/{normalized-brief.ts → core/normalized-brief.ts} +227 -206
  168. package/src/{output-index.ts → core/output-index.ts} +59 -59
  169. package/src/{paths.ts → core/paths.ts} +75 -71
  170. package/src/{project-config.ts → core/project-config.ts} +78 -78
  171. package/src/{registry.ts → core/registry.ts} +119 -119
  172. package/src/{settings.ts → core/settings.ts} +35 -34
  173. package/src/core/template-engine.ts +45 -0
  174. package/src/{template-resolver.ts → core/template-resolver.ts} +54 -54
  175. package/src/{templates.ts → core/templates.ts} +452 -450
  176. package/src/core/terminology.ts +177 -0
  177. package/src/core/tracing.ts +110 -0
  178. package/src/{types.ts → core/types.ts} +46 -46
  179. package/src/{utils.ts → core/utils.ts} +64 -50
  180. package/src/{validate.ts → core/validate.ts} +252 -246
  181. package/src/{validator.ts → core/validator.ts} +92 -96
  182. package/src/{version.ts → core/version.ts} +24 -24
  183. package/src/{workflow-commands.ts → core/workflow-commands.ts} +32 -31
  184. package/src/i18n/en.json +45 -0
  185. package/src/i18n/index.ts +58 -0
  186. package/src/i18n/tr.json +45 -0
  187. package/src/providers/index.ts +29 -12
  188. package/src/providers/mock-provider.ts +200 -199
  189. package/src/providers/openai-provider.ts +88 -87
  190. package/src/skills/engine.ts +94 -0
  191. package/src/skills/fix-skill.ts +38 -0
  192. package/src/skills/generate-artifact-skill.ts +32 -0
  193. package/src/skills/generate-pipeline-skill.ts +49 -0
  194. package/src/skills/normalize-skill.ts +29 -0
  195. package/src/skills/types.ts +36 -0
  196. package/src/skills/validate-skill.ts +29 -0
  197. package/templates/commands/prodo-fix.md +46 -0
  198. package/templates/commands/prodo-normalize.md +118 -23
  199. package/templates/commands/prodo-prd.md +138 -17
  200. package/templates/commands/prodo-stories.md +153 -17
  201. package/templates/commands/prodo-techspec.md +167 -17
  202. package/templates/commands/prodo-validate.md +184 -26
  203. package/templates/commands/prodo-wireframe.md +188 -17
  204. package/templates/commands/prodo-workflow.md +200 -17
  205. package/src/normalize.ts +0 -89
@@ -1,319 +1,459 @@
1
- import { Command } from "commander";
2
- import { createHash } from "node:crypto";
3
- import fs from "node:fs/promises";
4
- import path from "node:path";
5
- import { loadAgentCommandSet, resolveAgent } from "./agents";
6
- import { resolveAi, type SupportedAi } from "./agent-command-installer";
7
- import { listArtifactTypes } from "./artifact-registry";
8
- import { generateArtifact } from "./artifacts";
9
- import { runDoctor } from "./doctor";
10
- import { UserError } from "./errors";
11
- import { runHookPhase } from "./hook-executor";
12
- import { runInit } from "./init";
13
- import { finishInitInteractive, gatherInitSelections } from "./init-tui";
14
- import { runNormalize } from "./normalize";
15
- import { briefPath } from "./paths";
16
- import { type ArtifactType } from "./types";
17
- import { fileExists } from "./utils";
18
- import { runValidate } from "./validate";
19
- import { readCliVersion } from "./version";
20
-
21
- type RunOptions = {
22
- forcedCommand?: string;
23
- cwd?: string;
24
- argv?: string[];
25
- log?: (message: string) => void;
26
- error?: (message: string) => void;
27
- };
28
-
29
- const dynamicImport = new Function("specifier", "return import(specifier)") as (
30
- specifier: string
31
- ) => Promise<unknown>;
32
-
33
- function mapForcedCommand(forcedCommand: string): ArtifactType | "init" | "validate" | "normalize" | undefined {
34
- if (forcedCommand === "prodo-init") return "init";
35
- if (forcedCommand === "prodo-validate") return "validate";
36
- if (forcedCommand === "prodo-normalize") return "normalize";
37
- if (forcedCommand === "prodo-prd") return "prd";
38
- if (forcedCommand === "prodo-workflow") return "workflow";
39
- if (forcedCommand === "prodo-wireframe") return "wireframe";
40
- if (forcedCommand === "prodo-stories") return "stories";
41
- if (forcedCommand === "prodo-techspec") return "techspec";
42
- return undefined;
43
- }
44
-
45
- async function runArtifactCommand(
46
- type: ArtifactType,
47
- opts: { from?: string; out?: string; agent?: string },
48
- cwd: string,
49
- log: (message: string) => void,
50
- options?: { suggestValidate?: boolean }
51
- ): Promise<void> {
52
- await runHookPhase(cwd, `before_${type}`, log);
53
- const agent = resolveAgent(opts.agent);
54
- const file = await generateArtifact({
55
- artifactType: type,
56
- cwd,
57
- normalizedBriefOverride: opts.from,
58
- outPath: opts.out,
59
- agent
60
- });
61
- const agentMsg = agent ? ` [agent=${agent}]` : "";
62
- log(`${type.toUpperCase()} generated${agentMsg}: ${file}`);
63
- if (options?.suggestValidate !== false) {
64
- log("Tip: run `prodo validate` to check cross-artifact consistency.");
65
- }
66
- await runHookPhase(cwd, `after_${type}`, log);
67
- }
68
-
69
- type BriefSnapshot = {
70
- hash: string;
71
- mtimeMs: number;
72
- size: number;
73
- };
74
-
75
- async function snapshotBrief(cwd: string): Promise<BriefSnapshot | null> {
76
- const file = briefPath(cwd);
77
- if (!(await fileExists(file))) return null;
78
- const [raw, stat] = await Promise.all([fs.readFile(file), fs.stat(file)]);
79
- return {
80
- hash: createHash("sha256").update(raw).digest("hex"),
81
- mtimeMs: stat.mtimeMs,
82
- size: stat.size
83
- };
84
- }
85
-
86
- async function withBriefReadOnlyGuard(cwd: string, task: () => Promise<void>): Promise<void> {
87
- const before = await snapshotBrief(cwd);
88
- await task();
89
- const after = await snapshotBrief(cwd);
90
- if (!before) return;
91
- if (!after) {
92
- throw new UserError("Input file `brief.md` was removed during execution. Input files are read-only.");
93
- }
94
- if (before.hash !== after.hash || before.size !== after.size || before.mtimeMs !== after.mtimeMs) {
95
- throw new UserError("Input file `brief.md` was modified during execution. Input files are read-only.");
96
- }
97
- }
98
-
99
- export async function runCli(options: RunOptions = {}): Promise<number> {
100
- const cwd = options.cwd ?? process.cwd();
101
- const argv = options.argv ?? process.argv;
102
- const out = options.log ?? console.log;
103
- const err = options.error ?? console.error;
104
- const forced = options.forcedCommand ? mapForcedCommand(options.forcedCommand) : undefined;
105
-
106
- const program = new Command();
107
- const version = await readCliVersion(cwd);
108
- program
109
- .name("prodo")
110
- .description("CLI-first, prompt-powered product artifact kit")
111
- .version(`prodo ${version}`, "-v, --version", "Show Prodo version")
112
- .showHelpAfterError();
113
- const artifactTypes = await listArtifactTypes(cwd);
114
-
115
- program
116
- .command("init [target]")
117
- .option("--ai <name>", "agent integration: codex | gemini-cli | claude-cli")
118
- .option("--lang <code>", "document language (e.g. en, tr)")
119
- .option("--preset <name>", "preset to install during initialization")
120
- .action(async (target, opts) => {
121
- const projectRoot = path.resolve(cwd, target ?? ".");
122
- const selected = await gatherInitSelections({
123
- projectRoot,
124
- aiInput: opts.ai,
125
- langInput: opts.lang
126
- });
127
- const selectedAi = selected.ai as SupportedAi | undefined;
128
-
129
- if (selected.interactive) {
130
- const clack = (await dynamicImport("@clack/prompts")) as typeof import("@clack/prompts");
131
- const s = clack.spinner();
132
- s.start("Scaffolding Prodo workspace...");
133
- const result = await runInit(projectRoot, {
134
- ai: selectedAi,
135
- lang: selected.lang,
136
- preset: opts.preset,
137
- script: selected.script
138
- });
139
- s.stop("Scaffold complete.");
140
- await finishInitInteractive({
141
- projectRoot,
142
- settingsPath: result.settingsPath,
143
- ai: selectedAi,
144
- lang: selected.lang
145
- });
146
- return;
147
- }
148
-
149
- const result = await runInit(projectRoot, {
150
- ai: selectedAi,
151
- lang: selected.lang,
152
- preset: opts.preset,
153
- script: selected.script
154
- });
155
- out(`Initialized Prodo scaffold at ${path.join(projectRoot, ".prodo")}`);
156
- if (selectedAi) {
157
- out(`Agent command set installed for ${selectedAi}.`);
158
- out(`Installed ${result.installedAgentFiles.length} command files.`);
159
- out("Agent workflow: edit brief.md, then run slash commands in your agent.");
160
- } else {
161
- out("No agent selected. Use `prodo generate` for end-to-end generation.");
162
- }
163
- out(`Settings file: ${result.settingsPath}`);
164
- out("Next: edit brief.md.");
165
- });
166
-
167
- program
168
- .command("generate")
169
- .description("Run end-to-end pipeline: normalize -> generate artifacts -> validate")
170
- .option("--agent <name>", "agent profile: codex | gemini-cli | claude-cli")
171
- .option("--strict", "treat validation warnings as errors")
172
- .option("--report <path>", "validation report output path")
173
- .action(async (opts) => {
174
- if (opts.agent) resolveAgent(opts.agent);
175
- await withBriefReadOnlyGuard(cwd, async () => {
176
- await runHookPhase(cwd, "before_normalize", out);
177
- const normalizedPath = await runNormalize({ cwd });
178
- out(`Normalized brief written to: ${normalizedPath}`);
179
- await runHookPhase(cwd, "after_normalize", out);
180
-
181
- for (const type of artifactTypes) {
182
- await runArtifactCommand(type, { from: normalizedPath, agent: opts.agent }, cwd, out, {
183
- suggestValidate: false
184
- });
185
- }
186
-
187
- await runHookPhase(cwd, "before_validate", out);
188
- const result = await runValidate(cwd, {
189
- strict: Boolean(opts.strict),
190
- report: opts.report
191
- });
192
- out(`Validation report written to: ${result.reportPath}`);
193
- if (!result.pass) {
194
- throw new UserError("Validation failed. Review report and fix issues.");
195
- }
196
- out("Generation pipeline completed. Validation passed.");
197
- await runHookPhase(cwd, "after_validate", out);
198
- });
199
- });
200
-
201
- program
202
- .command("normalize", { hidden: true })
203
- .description("Advanced: normalize brief without full pipeline")
204
- .option("--brief <path>", "path to start brief markdown")
205
- .option("--out <path>", "output normalized brief json path")
206
- .option("--agent <name>", "agent profile: codex | gemini-cli | claude-cli")
207
- .action(async (opts: { brief?: string; out?: string; agent?: string }) => {
208
- if (opts.agent) resolveAgent(opts.agent);
209
- await withBriefReadOnlyGuard(cwd, async () => {
210
- await runHookPhase(cwd, "before_normalize", out);
211
- const outPath = await runNormalize({
212
- cwd,
213
- brief: opts.brief,
214
- out: opts.out
215
- });
216
- out(`Normalized brief written to: ${outPath}`);
217
- await runHookPhase(cwd, "after_normalize", out);
218
- });
219
- });
220
-
221
- program
222
- .command("doctor")
223
- .alias("check")
224
- .description("Check local environment and toolchain readiness")
225
- .action(async () => {
226
- await runDoctor(cwd, out);
227
- });
228
-
229
- for (const type of artifactTypes) {
230
- program
231
- .command(type, { hidden: true })
232
- .description(`Advanced: generate only ${type} artifact`)
233
- .option("--from <path>", "path to normalized-brief.json")
234
- .option("--out <path>", "output file path")
235
- .option("--agent <name>", "agent profile: codex | gemini-cli | claude-cli")
236
- .action(async (opts: { from?: string; out?: string; agent?: string }) => {
237
- await withBriefReadOnlyGuard(cwd, async () => {
238
- await runArtifactCommand(type, opts, cwd, out);
239
- });
240
- });
241
- }
242
-
243
- program
244
- .command("agent-commands", { hidden: true })
245
- .requiredOption("--agent <name>", "agent profile: codex | gemini-cli | claude-cli")
246
- .action(async (opts: { agent: string }) => {
247
- const agent = resolveAgent(opts.agent);
248
- if (!agent) throw new UserError("Agent is required.");
249
- const set = await loadAgentCommandSet(cwd, agent);
250
- out(`Agent: ${set.agent}`);
251
- if (set.description) out(`Description: ${set.description}`);
252
- out("");
253
- out("Recommended sequence:");
254
- for (const item of set.recommended_sequence ?? []) {
255
- out(`- ${item.command}: ${item.purpose}`);
256
- }
257
- if (set.artifact_shortcuts) {
258
- out("");
259
- out("Artifact shortcuts:");
260
- for (const [key, command] of Object.entries(set.artifact_shortcuts)) {
261
- out(`- ${key}: ${command}`);
262
- }
263
- }
264
- });
265
-
266
- program
267
- .command("validate", { hidden: true })
268
- .description("Advanced: run validation only")
269
- .option("--strict", "treat warnings as errors")
270
- .option("--report <path>", "report output path")
271
- .option("--agent <name>", "agent profile: codex | gemini-cli | claude-cli")
272
- .action(async (opts: { strict?: boolean; report?: string; agent?: string }) => {
273
- if (opts.agent) resolveAgent(opts.agent);
274
- await withBriefReadOnlyGuard(cwd, async () => {
275
- await runHookPhase(cwd, "before_validate", out);
276
- const result = await runValidate(cwd, {
277
- strict: Boolean(opts.strict),
278
- report: opts.report
279
- });
280
- out(`Validation report written to: ${result.reportPath}`);
281
- if (!result.pass) {
282
- throw new UserError("Validation failed. Review report and fix issues.");
283
- }
284
- out("Validation passed.");
285
- await runHookPhase(cwd, "after_validate", out);
286
- });
287
- });
288
-
289
- try {
290
- if (forced) {
291
- if (forced === "init") {
292
- await program.parseAsync(["node", "prodo", "init", ...argv.slice(2)]);
293
- } else if (forced === "normalize") {
294
- await program.parseAsync(["node", "prodo", "normalize", ...argv.slice(2)]);
295
- } else if (forced === "validate") {
296
- await program.parseAsync(["node", "prodo", "validate", ...argv.slice(2)]);
297
- } else {
298
- await program.parseAsync(["node", "prodo", forced, ...argv.slice(2)]);
299
- }
300
- } else {
301
- await program.parseAsync(argv);
302
- }
303
- return 0;
304
- } catch (error) {
305
- if (error instanceof UserError) {
306
- err(error.message);
307
- return 1;
308
- }
309
- const unknown = error as Error;
310
- err(unknown.message);
311
- return 1;
312
- }
313
- }
314
-
315
- if (require.main === module) {
316
- runCli().then((code) => {
317
- process.exitCode = code;
318
- });
319
- }
1
+ import { Command } from "commander";
2
+ import { createHash } from "node:crypto";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { loadAgentCommandSet, resolveAgent } from "./agent-ids";
6
+ import { resolveAi, type SupportedAi } from "./agent-command-installer";
7
+ import { listArtifactTypes } from "../core/artifact-registry";
8
+ import { generateArtifact } from "../core/artifacts";
9
+ import { runDoctor } from "./doctor";
10
+ import { UserError } from "../core/errors";
11
+ import { runHookPhase } from "../core/hook-executor";
12
+ import { runInit } from "./init";
13
+ import { finishInitInteractive, gatherInitSelections } from "./init-tui";
14
+ import { runClean } from "../core/clean";
15
+ import { buildFixProposal, applyFix, runFix } from "../core/fix";
16
+ import { runNormalize } from "../core/normalize";
17
+ import { runInteractiveNormalize } from "./normalize-interactive";
18
+ import { confirmFixExecution, displayFixProposal, displayFixResult } from "./fix-tui";
19
+ import { briefPath } from "../core/paths";
20
+ import { type ArtifactType } from "../core/types";
21
+ import { fileExists } from "../core/utils";
22
+ import { runValidate } from "../core/validate";
23
+ import { readCliVersion } from "../core/version";
24
+
25
+ type RunOptions = {
26
+ forcedCommand?: string;
27
+ cwd?: string;
28
+ argv?: string[];
29
+ log?: (message: string) => void;
30
+ error?: (message: string) => void;
31
+ };
32
+
33
+ const dynamicImport = new Function("specifier", "return import(specifier)") as (
34
+ specifier: string
35
+ ) => Promise<unknown>;
36
+
37
+ function mapForcedCommand(forcedCommand: string): ArtifactType | "init" | "validate" | "normalize" | "fix" | undefined {
38
+ if (forcedCommand === "prodo-init") return "init";
39
+ if (forcedCommand === "prodo-validate") return "validate";
40
+ if (forcedCommand === "prodo-normalize") return "normalize";
41
+ if (forcedCommand === "prodo-fix") return "fix";
42
+ if (forcedCommand === "prodo-prd") return "prd";
43
+ if (forcedCommand === "prodo-workflow") return "workflow";
44
+ if (forcedCommand === "prodo-wireframe") return "wireframe";
45
+ if (forcedCommand === "prodo-stories") return "stories";
46
+ if (forcedCommand === "prodo-techspec") return "techspec";
47
+ return undefined;
48
+ }
49
+
50
+ async function runArtifactCommand(
51
+ type: ArtifactType,
52
+ opts: { from?: string; out?: string; agent?: string; revisionType?: "default" | "fix" },
53
+ cwd: string,
54
+ log: (message: string) => void,
55
+ options?: { suggestValidate?: boolean }
56
+ ): Promise<void> {
57
+ await runHookPhase(cwd, `before_${type}`, log);
58
+ const agent = resolveAgent(opts.agent);
59
+ const file = await generateArtifact({
60
+ artifactType: type,
61
+ cwd,
62
+ normalizedBriefOverride: opts.from,
63
+ outPath: opts.out,
64
+ agent,
65
+ revisionType: opts.revisionType
66
+ });
67
+ const agentMsg = agent ? ` [agent=${agent}]` : "";
68
+ log(`${type.toUpperCase()} generated${agentMsg}: ${file}`);
69
+ if (options?.suggestValidate !== false) {
70
+ log("Tip: run `prodo validate` to check cross-artifact consistency.");
71
+ }
72
+ await runHookPhase(cwd, `after_${type}`, log);
73
+ }
74
+
75
+ type BriefSnapshot = {
76
+ hash: string;
77
+ mtimeMs: number;
78
+ size: number;
79
+ };
80
+
81
+ async function snapshotBrief(cwd: string): Promise<BriefSnapshot | null> {
82
+ const file = briefPath(cwd);
83
+ if (!(await fileExists(file))) return null;
84
+ const [raw, stat] = await Promise.all([fs.readFile(file), fs.stat(file)]);
85
+ return {
86
+ hash: createHash("sha256").update(raw).digest("hex"),
87
+ mtimeMs: stat.mtimeMs,
88
+ size: stat.size
89
+ };
90
+ }
91
+
92
+ async function withBriefReadOnlyGuard(cwd: string, task: () => Promise<void>): Promise<void> {
93
+ const before = await snapshotBrief(cwd);
94
+ await task();
95
+ const after = await snapshotBrief(cwd);
96
+ if (!before) return;
97
+ if (!after) {
98
+ throw new UserError("Input file `brief.md` was removed during execution. Input files are read-only.");
99
+ }
100
+ if (before.hash !== after.hash || before.size !== after.size || before.mtimeMs !== after.mtimeMs) {
101
+ throw new UserError("Input file `brief.md` was modified during execution. Input files are read-only.");
102
+ }
103
+ }
104
+
105
+ export async function runCli(options: RunOptions = {}): Promise<number> {
106
+ const cwd = options.cwd ?? process.cwd();
107
+ const argv = options.argv ?? process.argv;
108
+ const out = options.log ?? console.log;
109
+ const err = options.error ?? console.error;
110
+ const forced = options.forcedCommand ? mapForcedCommand(options.forcedCommand) : undefined;
111
+
112
+ const program = new Command();
113
+ const version = await readCliVersion(cwd);
114
+ program
115
+ .name("prodo")
116
+ .description("CLI-first, prompt-powered product artifact kit")
117
+ .version(`prodo ${version}`, "-v, --version", "Show Prodo version")
118
+ .showHelpAfterError();
119
+ const artifactTypes = await listArtifactTypes(cwd);
120
+
121
+ program
122
+ .command("init [target]")
123
+ .option("--ai <name>", "agent integration: codex | gemini-cli | claude-cli")
124
+ .option("--lang <code>", "document language (e.g. en, tr)")
125
+ .option("--author <name>", "document author name")
126
+ .option("--preset <name>", "preset to install during initialization")
127
+ .action(async (target, opts) => {
128
+ const projectRoot = path.resolve(cwd, target ?? ".");
129
+ const selected = await gatherInitSelections({
130
+ projectRoot,
131
+ aiInput: opts.ai,
132
+ langInput: opts.lang,
133
+ authorInput: opts.author
134
+ });
135
+ const selectedAi = selected.ai as SupportedAi | undefined;
136
+
137
+ if (selected.interactive) {
138
+ const clack = (await dynamicImport("@clack/prompts")) as typeof import("@clack/prompts");
139
+ const s = clack.spinner();
140
+ s.start("Scaffolding Prodo workspace...");
141
+ const result = await runInit(projectRoot, {
142
+ ai: selectedAi,
143
+ lang: selected.lang,
144
+ author: selected.author,
145
+ preset: opts.preset,
146
+ script: selected.script
147
+ });
148
+ s.stop("Scaffold complete.");
149
+ await finishInitInteractive({
150
+ projectRoot,
151
+ settingsPath: result.settingsPath,
152
+ ai: selectedAi,
153
+ lang: selected.lang,
154
+ author: selected.author
155
+ });
156
+ return;
157
+ }
158
+
159
+ const result = await runInit(projectRoot, {
160
+ ai: selectedAi,
161
+ lang: selected.lang,
162
+ author: selected.author,
163
+ preset: opts.preset,
164
+ script: selected.script
165
+ });
166
+ out(`Initialized Prodo scaffold at ${path.join(projectRoot, ".prodo")}`);
167
+ if (selectedAi) {
168
+ out(`Agent command set installed for ${selectedAi}.`);
169
+ out(`Installed ${result.installedAgentFiles.length} command files.`);
170
+ out("Agent workflow: edit brief.md, then run slash commands in your agent.");
171
+ } else {
172
+ out("No agent selected. Use `prodo generate` for end-to-end generation.");
173
+ }
174
+ out(`Settings file: ${result.settingsPath}`);
175
+ out(`Author: ${selected.author}`);
176
+ out("Next: edit brief.md.");
177
+ });
178
+
179
+ program
180
+ .command("generate")
181
+ .description("Run end-to-end pipeline: normalize -> generate artifacts -> validate")
182
+ .option("--agent <name>", "agent profile: codex | gemini-cli | claude-cli")
183
+ .option("--strict", "treat validation warnings as errors")
184
+ .option("--report <path>", "validation report output path")
185
+ .option("--dry-run", "show what would be generated without writing files")
186
+ .action(async (opts: { agent?: string; strict?: boolean; report?: string; dryRun?: boolean }) => {
187
+ if (opts.agent) resolveAgent(opts.agent);
188
+ if (opts.dryRun) {
189
+ out("[Dry Run] Pipeline would execute:");
190
+ out(` 1. Normalize brief.md`);
191
+ for (const type of artifactTypes) {
192
+ out(` 2. Generate ${type}`);
193
+ }
194
+ out(` 3. Validate all artifacts`);
195
+ out(`\nArtifact types: ${artifactTypes.join(", ")}`);
196
+ return;
197
+ }
198
+ await withBriefReadOnlyGuard(cwd, async () => {
199
+ await runHookPhase(cwd, "before_normalize", out);
200
+ const normalizedPath = await runNormalize({ cwd });
201
+ out(`Normalized brief written to: ${normalizedPath}`);
202
+ await runHookPhase(cwd, "after_normalize", out);
203
+
204
+ for (const type of artifactTypes) {
205
+ await runArtifactCommand(type, { from: normalizedPath, agent: opts.agent }, cwd, out, {
206
+ suggestValidate: false
207
+ });
208
+ }
209
+
210
+ await runHookPhase(cwd, "before_validate", out);
211
+ const result = await runValidate(cwd, {
212
+ strict: Boolean(opts.strict),
213
+ report: opts.report
214
+ });
215
+ out(`Validation report written to: ${result.reportPath}`);
216
+ if (!result.pass) {
217
+ throw new UserError("Validation failed. Review report and fix issues.");
218
+ }
219
+ out("Generation pipeline completed. Validation passed.");
220
+ await runHookPhase(cwd, "after_validate", out);
221
+ });
222
+ });
223
+
224
+ program
225
+ .command("fix", { hidden: true })
226
+ .description("Advanced: auto-regenerate affected artifacts from validation findings")
227
+ .option("--agent <name>", "agent profile: codex | gemini-cli | claude-cli")
228
+ .option("--strict", "treat validation warnings as errors")
229
+ .option("--report <path>", "validation report output path")
230
+ .option("--dry-run", "show fix proposal without applying changes")
231
+ .action(async (opts: { agent?: string; strict?: boolean; report?: string; dryRun?: boolean }) => {
232
+ if (opts.agent) resolveAgent(opts.agent);
233
+ await withBriefReadOnlyGuard(cwd, async () => {
234
+ const fixOpts = {
235
+ cwd,
236
+ agent: opts.agent,
237
+ strict: Boolean(opts.strict),
238
+ report: opts.report,
239
+ dryRun: Boolean(opts.dryRun),
240
+ log: out
241
+ };
242
+
243
+ if (opts.dryRun) {
244
+ const result = await runFix(fixOpts);
245
+ out(`Validation report: ${result.reportPath}`);
246
+ if (result.proposal.targets.length > 0) {
247
+ await displayFixProposal(result.proposal, out);
248
+ }
249
+ return;
250
+ }
251
+
252
+ const proposal = await buildFixProposal(fixOpts);
253
+ out(`Validation report: ${proposal.initialReport.reportPath}`);
254
+
255
+ if (proposal.targets.length === 0) {
256
+ out("No blocking issues found. Nothing to fix.");
257
+ return;
258
+ }
259
+
260
+ await displayFixProposal(proposal, out);
261
+
262
+ const confirmed = await confirmFixExecution(proposal);
263
+ if (!confirmed) {
264
+ out("Fix cancelled by user.");
265
+ return;
266
+ }
267
+
268
+ out(`Regenerating impacted artifacts: ${proposal.targets.join(", ")}`);
269
+ const result = await applyFix(cwd, proposal, fixOpts);
270
+
271
+ await displayFixResult(result, out);
272
+
273
+ if (!result.finalPass) {
274
+ throw new UserError("Fix completed but validation is still failing. Review report and retry.");
275
+ }
276
+ });
277
+ });
278
+
279
+ program
280
+ .command("normalize", { hidden: true })
281
+ .description("Advanced: normalize brief without full pipeline")
282
+ .option("--brief <path>", "path to start brief markdown")
283
+ .option("--out <path>", "output normalized brief json path")
284
+ .option("--agent <name>", "agent profile: codex | gemini-cli | claude-cli")
285
+ .option("-i, --interactive", "interactively clarify low-confidence fields")
286
+ .option("--dry-run", "show what would be normalized without writing")
287
+ .action(async (opts: { brief?: string; out?: string; agent?: string; interactive?: boolean; dryRun?: boolean }) => {
288
+ if (opts.agent) resolveAgent(opts.agent);
289
+ if (opts.dryRun) {
290
+ const briefFile = opts.brief ?? "brief.md";
291
+ out(`[Dry Run] Would normalize: ${briefFile}`);
292
+ out(`[Dry Run] Output would be written to: .prodo/briefs/normalized-brief.json`);
293
+ return;
294
+ }
295
+ await withBriefReadOnlyGuard(cwd, async () => {
296
+ await runHookPhase(cwd, "before_normalize", out);
297
+ const outPath = opts.interactive
298
+ ? await runInteractiveNormalize({ cwd, brief: opts.brief, out: opts.out, log: out })
299
+ : await runNormalize({ cwd, brief: opts.brief, out: opts.out });
300
+ out(`Normalized brief written to: ${outPath}`);
301
+ await runHookPhase(cwd, "after_normalize", out);
302
+ });
303
+ });
304
+
305
+ program
306
+ .command("doctor")
307
+ .alias("check")
308
+ .description("Check local environment and toolchain readiness")
309
+ .action(async () => {
310
+ await runDoctor(cwd, out);
311
+ });
312
+
313
+ program
314
+ .command("clean")
315
+ .description("Remove all generated artifacts, keep brief.md and config")
316
+ .option("--dry-run", "show what would be removed without deleting")
317
+ .action(async (opts: { dryRun?: boolean }) => {
318
+ const result = await runClean({
319
+ cwd,
320
+ dryRun: Boolean(opts.dryRun),
321
+ log: out
322
+ });
323
+ if (result.removedPaths.length === 0) {
324
+ out("Nothing to clean.");
325
+ } else if (!opts.dryRun) {
326
+ out(`Cleaned ${result.removedPaths.length} path(s). Project is ready for a fresh run.`);
327
+ }
328
+ });
329
+
330
+ for (const type of artifactTypes) {
331
+ program
332
+ .command(type, { hidden: true })
333
+ .description(`Advanced: generate only ${type} artifact`)
334
+ .option("--from <path>", "path to normalized-brief.json")
335
+ .option("--out <path>", "output file path")
336
+ .option("--agent <name>", "agent profile: codex | gemini-cli | claude-cli")
337
+ .action(async (opts: { from?: string; out?: string; agent?: string }) => {
338
+ await withBriefReadOnlyGuard(cwd, async () => {
339
+ await runArtifactCommand(type, opts, cwd, out);
340
+ });
341
+ });
342
+ }
343
+
344
+ program
345
+ .command("agent-commands", { hidden: true })
346
+ .requiredOption("--agent <name>", "agent profile: codex | gemini-cli | claude-cli")
347
+ .action(async (opts: { agent: string }) => {
348
+ const agent = resolveAgent(opts.agent);
349
+ if (!agent) throw new UserError("Agent is required.");
350
+ const set = await loadAgentCommandSet(cwd, agent);
351
+ out(`Agent: ${set.agent}`);
352
+ if (set.description) out(`Description: ${set.description}`);
353
+ out("");
354
+ out("Recommended sequence:");
355
+ for (const item of set.recommended_sequence ?? []) {
356
+ out(`- ${item.command}: ${item.purpose}`);
357
+ }
358
+ if (set.artifact_shortcuts) {
359
+ out("");
360
+ out("Artifact shortcuts:");
361
+ for (const [key, command] of Object.entries(set.artifact_shortcuts)) {
362
+ out(`- ${key}: ${command}`);
363
+ }
364
+ }
365
+ });
366
+
367
+ program
368
+ .command("validate", { hidden: true })
369
+ .description("Advanced: run validation only")
370
+ .option("--strict", "treat warnings as errors")
371
+ .option("--report <path>", "report output path")
372
+ .option("--agent <name>", "agent profile: codex | gemini-cli | claude-cli")
373
+ .action(async (opts: { strict?: boolean; report?: string; agent?: string }) => {
374
+ if (opts.agent) resolveAgent(opts.agent);
375
+ await withBriefReadOnlyGuard(cwd, async () => {
376
+ await runHookPhase(cwd, "before_validate", out);
377
+ const result = await runValidate(cwd, {
378
+ strict: Boolean(opts.strict),
379
+ report: opts.report
380
+ });
381
+ out(`Validation report written to: ${result.reportPath}`);
382
+ if (!result.pass) {
383
+ throw new UserError("Validation failed. Review report and fix issues.");
384
+ }
385
+ out("Validation passed.");
386
+ await runHookPhase(cwd, "after_validate", out);
387
+ });
388
+ });
389
+
390
+ program
391
+ .command("skills", { hidden: true })
392
+ .description("Advanced: manage and run skills")
393
+ .argument("[action]", "list or run", "list")
394
+ .argument("[name]", "skill name (for run)")
395
+ .option("--input <json>", "JSON input for skill execution")
396
+ .action(async (action: string, name: string | undefined, opts: { input?: string }) => {
397
+ const { getGlobalSkillEngine } = await import("../skills/engine");
398
+ const engine = getGlobalSkillEngine();
399
+
400
+ if (action === "list") {
401
+ const manifests = engine.listSkills();
402
+ if (manifests.length === 0) {
403
+ out("No skills registered.");
404
+ return;
405
+ }
406
+ out("Available skills:\n");
407
+ for (const m of manifests) {
408
+ out(` ${m.name.padEnd(25)} [${m.category}] ${m.description}`);
409
+ }
410
+ return;
411
+ }
412
+
413
+ if (action === "run") {
414
+ if (!name) throw new UserError("Skill name is required. Usage: prodo skills run <name>");
415
+ const inputs = opts.input ? JSON.parse(opts.input) as Record<string, unknown> : {};
416
+ inputs.cwd = inputs.cwd ?? cwd;
417
+ const result = await engine.execute(name, { cwd, log: out }, inputs);
418
+ out(`\nSkill "${name}" completed.`);
419
+ out(JSON.stringify(result, null, 2));
420
+ return;
421
+ }
422
+
423
+ throw new UserError(`Unknown skills action: "${action}". Use: list or run`);
424
+ });
425
+
426
+ try {
427
+ if (forced) {
428
+ if (forced === "init") {
429
+ await program.parseAsync(["node", "prodo", "init", ...argv.slice(2)]);
430
+ } else if (forced === "normalize") {
431
+ await program.parseAsync(["node", "prodo", "normalize", ...argv.slice(2)]);
432
+ } else if (forced === "validate") {
433
+ await program.parseAsync(["node", "prodo", "validate", ...argv.slice(2)]);
434
+ } else if (forced === "fix") {
435
+ await program.parseAsync(["node", "prodo", "fix", ...argv.slice(2)]);
436
+ } else {
437
+ await program.parseAsync(["node", "prodo", forced, ...argv.slice(2)]);
438
+ }
439
+ } else {
440
+ await program.parseAsync(argv);
441
+ }
442
+ return 0;
443
+ } catch (error) {
444
+ if (error instanceof UserError) {
445
+ err(error.message);
446
+ return 1;
447
+ }
448
+ const unknown = error as Error;
449
+ err(unknown.message);
450
+ return 1;
451
+ }
452
+ }
453
+
454
+ if (require.main === module) {
455
+ runCli().then((code) => {
456
+ process.exitCode = code;
457
+ });
458
+ }
459
+