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