@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,392 +1,399 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { createHash } from "node:crypto";
4
- import yaml from "js-yaml";
5
- import { installAgentCommands, type SupportedAi } from "./agent-command-installer";
6
- import { listArtifactDefinitions } from "./artifact-registry";
7
- import { ensureDir, fileExists } from "./utils";
8
- import { briefPath, outputDirPath, outputIndexPath, prodoPath } from "./paths";
9
- import { applyConfiguredPresets } from "./preset-loader";
10
- import { syncRegistry } from "./registry";
11
- import { writeSettings } from "./settings";
12
- import { buildWorkflowCommands } from "./workflow-commands";
13
- import {
14
- NORMALIZED_BRIEF_TEMPLATE,
15
- NORMALIZE_PROMPT_TEMPLATE,
16
- START_BRIEF_TEMPLATE,
17
- HOOKS_TEMPLATE,
18
- artifactTemplateTemplate,
19
- commandTemplate,
20
- promptTemplate,
21
- schemaTemplate
22
- } from "./templates";
23
-
24
- type AssetManifestItem = {
25
- source: string;
26
- target: string;
27
- source_sha256: string;
28
- target_sha256: string | null;
29
- status: "match" | "drift" | "missing" | "protected" | "updated" | "unmanaged";
30
- };
31
-
32
- type ScaffoldManifest = {
33
- schema_version: "1.0";
34
- generated_at: string;
35
- prodo_version: string;
36
- copied_asset_count: number;
37
- copied_assets: Array<{ source: string; target: string; sha256: string }>;
38
- asset_count: number;
39
- parity_summary: {
40
- match_count: number;
41
- drift_count: number;
42
- missing_count: number;
43
- protected_count: number;
44
- updated_count: number;
45
- unmanaged_count: number;
46
- };
47
- assets: AssetManifestItem[];
48
- };
49
-
50
- type SourceTargetPair = { sourceDir: string; targetDir: string };
51
- type BackupMap = Map<string, Buffer | null>;
52
-
53
- function templateFileName(artifactType: string): string {
54
- if (artifactType === "workflow") return `${artifactType}.mmd`;
55
- if (artifactType === "wireframe") return `${artifactType}.html`;
56
- return `${artifactType}.md`;
57
- }
58
-
59
- async function writeFileIfMissing(filePath: string, content: string): Promise<void> {
60
- if (await fileExists(filePath)) return;
61
- await fs.writeFile(filePath, content, "utf8");
62
- }
63
-
64
- async function readProdoVersion(cwd: string): Promise<string> {
65
- const candidates = [
66
- path.join(cwd, "package.json"),
67
- path.resolve(__dirname, "..", "package.json")
68
- ];
69
- for (const candidate of candidates) {
70
- if (!(await fileExists(candidate))) continue;
71
- try {
72
- const parsed = JSON.parse(await fs.readFile(candidate, "utf8")) as { version?: string };
73
- if (typeof parsed.version === "string" && parsed.version.trim().length > 0) return parsed.version;
74
- } catch {
75
- // ignore and continue
76
- }
77
- }
78
- return "0.0.0-dev";
79
- }
80
-
81
- async function fileSha256(filePath: string): Promise<string> {
82
- const content = await fs.readFile(filePath);
83
- return createHash("sha256").update(content).digest("hex");
84
- }
85
-
86
- async function listFilesRecursive(rootDir: string): Promise<string[]> {
87
- if (!(await fileExists(rootDir))) return [];
88
- const out: string[] = [];
89
- const walk = async (current: string): Promise<void> => {
90
- const entries = await fs.readdir(current, { withFileTypes: true });
91
- for (const entry of entries) {
92
- const full = path.join(current, entry.name);
93
- if (entry.isDirectory()) {
94
- await walk(full);
95
- } else {
96
- out.push(full);
97
- }
98
- }
99
- };
100
- await walk(rootDir);
101
- return out;
102
- }
103
-
104
- async function loadPreviousManifest(root: string): Promise<ScaffoldManifest | null> {
105
- const manifestPath = path.join(root, "scaffold-manifest.json");
106
- if (!(await fileExists(manifestPath))) return null;
107
- try {
108
- const parsed = JSON.parse(await fs.readFile(manifestPath, "utf8")) as Partial<ScaffoldManifest>;
109
- if (!Array.isArray(parsed.assets)) return null;
110
- return {
111
- schema_version: "1.0",
112
- generated_at: parsed.generated_at ?? "",
113
- prodo_version: parsed.prodo_version ?? "0.0.0-dev",
114
- copied_asset_count: Number(parsed.copied_asset_count ?? 0),
115
- copied_assets: Array.isArray(parsed.copied_assets) ? parsed.copied_assets : [],
116
- asset_count: Number(parsed.asset_count ?? 0),
117
- parity_summary: {
118
- match_count: Number(parsed.parity_summary?.match_count ?? 0),
119
- drift_count: Number(parsed.parity_summary?.drift_count ?? 0),
120
- missing_count: Number(parsed.parity_summary?.missing_count ?? 0),
121
- protected_count: Number(parsed.parity_summary?.protected_count ?? 0),
122
- updated_count: Number(parsed.parity_summary?.updated_count ?? 0),
123
- unmanaged_count: Number(parsed.parity_summary?.unmanaged_count ?? 0)
124
- },
125
- assets: parsed.assets as AssetManifestItem[]
126
- };
127
- } catch {
128
- return null;
129
- }
130
- }
131
-
132
- async function copyDirIfMissing(
133
- sourceDir: string,
134
- targetDir: string,
135
- copiedAssets: Array<{ source: string; target: string; sha256: string }>
136
- ): Promise<void> {
137
- if (!(await fileExists(sourceDir))) return;
138
- await ensureDir(targetDir);
139
- const entries = await fs.readdir(sourceDir, { withFileTypes: true });
140
- for (const entry of entries) {
141
- const src = path.join(sourceDir, entry.name);
142
- const dst = path.join(targetDir, entry.name);
143
- if (entry.isDirectory()) {
144
- await copyDirIfMissing(src, dst, copiedAssets);
145
- continue;
146
- }
147
- if (await fileExists(dst)) continue;
148
- await fs.copyFile(src, dst);
149
- copiedAssets.push({
150
- source: src,
151
- target: dst,
152
- sha256: await fileSha256(dst)
153
- });
154
- }
155
- }
156
-
157
- async function refreshLegacyCommandTemplates(sourceDir: string, targetDir: string): Promise<void> {
158
- if (!(await fileExists(sourceDir)) || !(await fileExists(targetDir))) return;
159
- const entries = await fs.readdir(sourceDir, { withFileTypes: true });
160
- for (const entry of entries) {
161
- if (!entry.isFile()) continue;
162
- if (!entry.name.startsWith("prodo-") || !entry.name.endsWith(".md")) continue;
163
- const src = path.join(sourceDir, entry.name);
164
- const dst = path.join(targetDir, entry.name);
165
- if (!(await fileExists(dst))) continue;
166
- const existing = await fs.readFile(dst, "utf8");
167
- const isLegacyRunMode = /run:\s*\n\s*action:\s*[^\n]+/m.test(existing) || /mode:\s*internal-runtime/m.test(existing);
168
- if (!isLegacyRunMode) continue;
169
- await fs.copyFile(src, dst);
170
- }
171
- }
172
-
173
- async function buildAssetManifest(
174
- pairs: SourceTargetPair[],
175
- previous: ScaffoldManifest | null,
176
- backup: BackupMap
177
- ): Promise<AssetManifestItem[]> {
178
- const previousByTarget = new Map<string, AssetManifestItem>();
179
- for (const item of previous?.assets ?? []) {
180
- previousByTarget.set(path.resolve(item.target), item);
181
- }
182
-
183
- const items: AssetManifestItem[] = [];
184
- for (const pair of pairs) {
185
- const sourceFiles = await listFilesRecursive(pair.sourceDir);
186
- for (const sourceFile of sourceFiles) {
187
- const relative = path.relative(pair.sourceDir, sourceFile);
188
- const targetFile = path.join(pair.targetDir, relative);
189
- const resolvedTarget = path.resolve(targetFile);
190
- const sourceHash = await fileSha256(sourceFile);
191
- const targetExists = await fileExists(targetFile);
192
-
193
- if (!targetExists) {
194
- await ensureDir(path.dirname(targetFile));
195
- backup.set(resolvedTarget, null);
196
- await fs.copyFile(sourceFile, targetFile);
197
- items.push({
198
- source: sourceFile,
199
- target: targetFile,
200
- source_sha256: sourceHash,
201
- target_sha256: await fileSha256(targetFile),
202
- status: "missing"
203
- });
204
- continue;
205
- }
206
-
207
- const currentTargetHash = await fileSha256(targetFile);
208
- const prev = previousByTarget.get(resolvedTarget);
209
- const prevTargetHash = prev?.target_sha256 ?? null;
210
- const prevSourceHash = prev?.source_sha256 ?? null;
211
-
212
- if (currentTargetHash === sourceHash) {
213
- items.push({
214
- source: sourceFile,
215
- target: targetFile,
216
- source_sha256: sourceHash,
217
- target_sha256: currentTargetHash,
218
- status: "match"
219
- });
220
- continue;
221
- }
222
-
223
- if (prev && prevTargetHash && prevSourceHash && currentTargetHash === prevTargetHash && prevSourceHash !== sourceHash) {
224
- if (!backup.has(resolvedTarget)) {
225
- backup.set(resolvedTarget, await fs.readFile(targetFile));
226
- }
227
- await fs.copyFile(sourceFile, targetFile);
228
- items.push({
229
- source: sourceFile,
230
- target: targetFile,
231
- source_sha256: sourceHash,
232
- target_sha256: await fileSha256(targetFile),
233
- status: "updated"
234
- });
235
- continue;
236
- }
237
-
238
- if (prev) {
239
- items.push({
240
- source: sourceFile,
241
- target: targetFile,
242
- source_sha256: sourceHash,
243
- target_sha256: currentTargetHash,
244
- status: "protected"
245
- });
246
- continue;
247
- }
248
-
249
- items.push({
250
- source: sourceFile,
251
- target: targetFile,
252
- source_sha256: sourceHash,
253
- target_sha256: currentTargetHash,
254
- status: "unmanaged"
255
- });
256
- }
257
- }
258
- return items;
259
- }
260
-
261
- async function rollbackFiles(backup: BackupMap): Promise<void> {
262
- for (const [target, content] of backup.entries()) {
263
- if (content === null) {
264
- if (await fileExists(target)) await fs.rm(target, { force: true });
265
- continue;
266
- }
267
- await ensureDir(path.dirname(target));
268
- await fs.writeFile(target, content);
269
- }
270
- }
271
-
272
- function summarizeParity(items: AssetManifestItem[]): ScaffoldManifest["parity_summary"] {
273
- const byStatus = (status: AssetManifestItem["status"]) => items.filter((item) => item.status === status).length;
274
- return {
275
- match_count: byStatus("match"),
276
- drift_count: byStatus("drift"),
277
- missing_count: byStatus("missing"),
278
- protected_count: byStatus("protected"),
279
- updated_count: byStatus("updated"),
280
- unmanaged_count: byStatus("unmanaged")
281
- };
282
- }
283
-
284
- export async function runInit(
285
- cwd: string,
286
- options?: { ai?: SupportedAi; lang?: string; preset?: string; script?: "sh" | "ps" }
287
- ): Promise<{ installedAgentFiles: string[]; settingsPath: string }> {
288
- const root = prodoPath(cwd);
289
- const artifactDefs = await listArtifactDefinitions(cwd);
290
- const artifactTypes = artifactDefs.map((item) => item.name);
291
- const workflowCommands = buildWorkflowCommands(artifactTypes);
292
- const prodoVersion = await readProdoVersion(cwd);
293
- const localRepoTemplates = path.join(cwd, "templates");
294
- const packagedTemplates = path.resolve(__dirname, "..", "templates");
295
- const projectScaffoldTemplates = (await fileExists(localRepoTemplates)) ? localRepoTemplates : packagedTemplates;
296
- const copiedAssets: Array<{ source: string; target: string; sha256: string }> = [];
297
- const backup: BackupMap = new Map();
298
- const previousManifest = await loadPreviousManifest(root);
299
-
300
- await ensureDir(path.join(root, "briefs"));
301
- await ensureDir(path.join(root, "schemas"));
302
- await ensureDir(path.join(root, "prompts"));
303
- await ensureDir(path.join(root, "commands"));
304
- await ensureDir(path.join(root, "presets"));
305
- await ensureDir(path.join(root, "templates"));
306
- await ensureDir(path.join(root, "templates", "overrides"));
307
- await ensureDir(path.join(root, "state"));
308
- await ensureDir(path.join(root, "state", "context"));
309
- for (const def of artifactDefs) {
310
- await ensureDir(outputDirPath(cwd, def.name, def.output_dir));
311
- }
312
- await ensureDir(path.join(cwd, "product-docs", "reports"));
313
- await writeFileIfMissing(
314
- outputIndexPath(cwd),
315
- `${JSON.stringify({ active: {}, history: {}, updated_at: new Date(0).toISOString() }, null, 2)}\n`
316
- );
317
-
318
- await writeFileIfMissing(briefPath(cwd), START_BRIEF_TEMPLATE);
319
- await writeFileIfMissing(
320
- path.join(root, "briefs", "normalized-brief.json"),
321
- `${JSON.stringify(NORMALIZED_BRIEF_TEMPLATE, null, 2)}\n`
322
- );
323
- await writeFileIfMissing(path.join(root, "hooks.yml"), HOOKS_TEMPLATE);
324
- await writeFileIfMissing(path.join(root, "prompts", "normalize.md"), `${NORMALIZE_PROMPT_TEMPLATE}\n`);
325
- const scriptType = options?.script ?? (process.platform === "win32" ? "ps" : "sh");
326
- await fs.writeFile(
327
- path.join(root, "init-options.json"),
328
- `${JSON.stringify({ ai: options?.ai ?? null, lang: options?.lang ?? "en", preset: options?.preset ?? null, script: scriptType }, null, 2)}\n`,
329
- "utf8"
330
- );
331
-
332
- await copyDirIfMissing(path.join(projectScaffoldTemplates, "artifacts"), path.join(root, "templates"), copiedAssets);
333
- for (const artifact of artifactDefs) {
334
- const schema = {
335
- ...schemaTemplate(artifact.name),
336
- x_required_headings: artifact.required_headings
337
- };
338
- await writeFileIfMissing(path.join(root, "schemas", `${artifact.name}.yaml`), yaml.dump(schema));
339
- await writeFileIfMissing(path.join(root, "prompts", `${artifact.name}.md`), `${promptTemplate(artifact.name, options?.lang ?? "en")}\n`);
340
- await writeFileIfMissing(
341
- path.join(root, "templates", templateFileName(artifact.name)),
342
- `${artifactTemplateTemplate(artifact.name, options?.lang ?? "en")}\n`
343
- );
344
- }
345
-
346
- await copyDirIfMissing(path.join(projectScaffoldTemplates, "commands"), path.join(root, "commands"), copiedAssets);
347
- await refreshLegacyCommandTemplates(path.join(projectScaffoldTemplates, "commands"), path.join(root, "commands"));
348
- for (const command of workflowCommands) {
349
- await writeFileIfMissing(path.join(root, "commands", `${command.name}.md`), `${commandTemplate(command)}\n`);
350
- }
351
-
352
- await applyConfiguredPresets(cwd, root, prodoVersion, options?.preset);
353
-
354
- const pairs: SourceTargetPair[] = [
355
- {
356
- sourceDir: path.join(projectScaffoldTemplates, "commands"),
357
- targetDir: path.join(root, "commands")
358
- },
359
- {
360
- sourceDir: path.join(projectScaffoldTemplates, "artifacts"),
361
- targetDir: path.join(root, "templates")
362
- }
363
- ];
364
-
365
- let parity: AssetManifestItem[] = [];
366
- try {
367
- parity = await buildAssetManifest(pairs, previousManifest, backup);
368
- } catch (error) {
369
- await rollbackFiles(backup);
370
- throw error;
371
- }
372
-
373
- const installedAgentFiles = options?.ai ? await installAgentCommands(cwd, options.ai) : [];
374
- const manifest: ScaffoldManifest = {
375
- schema_version: "1.0",
376
- generated_at: new Date().toISOString(),
377
- prodo_version: prodoVersion,
378
- copied_asset_count: copiedAssets.length,
379
- copied_assets: copiedAssets,
380
- asset_count: parity.length,
381
- parity_summary: summarizeParity(parity),
382
- assets: parity
383
- };
384
- await fs.writeFile(path.join(root, "scaffold-manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
385
- await syncRegistry(cwd);
386
- const settingsPath = await writeSettings(cwd, {
387
- lang: (options?.lang ?? "en").trim() || "en",
388
- ai: options?.ai
389
- });
390
- return { installedAgentFiles, settingsPath };
391
- }
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import yaml from "js-yaml";
5
+ import { installAgentCommands, type SupportedAi } from "./agent-command-installer";
6
+ import { listArtifactDefinitions } from "../core/artifact-registry";
7
+ import { ensureDir, fileExists } from "../core/utils";
8
+ import { briefPath, outputDirPath, outputIndexPath, prodoPath } from "../core/paths";
9
+ import { applyConfiguredPresets } from "./preset-loader";
10
+ import { syncRegistry } from "../core/registry";
11
+ import { writeSettings } from "../core/settings";
12
+ import { extractRequiredHeadingsFromTemplate } from "../core/template-resolver";
13
+ import { buildWorkflowCommands } from "../core/workflow-commands";
14
+ import {
15
+ NORMALIZED_BRIEF_TEMPLATE,
16
+ NORMALIZE_PROMPT_TEMPLATE,
17
+ START_BRIEF_TEMPLATE,
18
+ HOOKS_TEMPLATE,
19
+ artifactTemplateTemplate,
20
+ commandTemplate,
21
+ promptTemplate,
22
+ schemaTemplate
23
+ } from "../core/templates";
24
+
25
+ type AssetManifestItem = {
26
+ source: string;
27
+ target: string;
28
+ source_sha256: string;
29
+ target_sha256: string | null;
30
+ status: "match" | "drift" | "missing" | "protected" | "updated" | "unmanaged";
31
+ };
32
+
33
+ type ScaffoldManifest = {
34
+ schema_version: "1.0";
35
+ generated_at: string;
36
+ prodo_version: string;
37
+ copied_asset_count: number;
38
+ copied_assets: Array<{ source: string; target: string; sha256: string }>;
39
+ asset_count: number;
40
+ parity_summary: {
41
+ match_count: number;
42
+ drift_count: number;
43
+ missing_count: number;
44
+ protected_count: number;
45
+ updated_count: number;
46
+ unmanaged_count: number;
47
+ };
48
+ assets: AssetManifestItem[];
49
+ };
50
+
51
+ type SourceTargetPair = { sourceDir: string; targetDir: string };
52
+ type BackupMap = Map<string, Buffer | null>;
53
+
54
+ function templateFileName(artifactType: string): string {
55
+ if (artifactType === "workflow") return `${artifactType}.mmd`;
56
+ if (artifactType === "wireframe") return `${artifactType}.html`;
57
+ return `${artifactType}.md`;
58
+ }
59
+
60
+ async function writeFileIfMissing(filePath: string, content: string): Promise<void> {
61
+ if (await fileExists(filePath)) return;
62
+ await fs.writeFile(filePath, content, "utf8");
63
+ }
64
+
65
+ async function readProdoVersion(cwd: string): Promise<string> {
66
+ const candidates = [
67
+ path.join(cwd, "package.json"),
68
+ path.resolve(__dirname, "..", "..", "package.json")
69
+ ];
70
+ for (const candidate of candidates) {
71
+ if (!(await fileExists(candidate))) continue;
72
+ try {
73
+ const parsed = JSON.parse(await fs.readFile(candidate, "utf8")) as { version?: string };
74
+ if (typeof parsed.version === "string" && parsed.version.trim().length > 0) return parsed.version;
75
+ } catch {
76
+ // ignore and continue
77
+ }
78
+ }
79
+ return "0.0.0-dev";
80
+ }
81
+
82
+ async function fileSha256(filePath: string): Promise<string> {
83
+ const content = await fs.readFile(filePath);
84
+ return createHash("sha256").update(content).digest("hex");
85
+ }
86
+
87
+ async function listFilesRecursive(rootDir: string): Promise<string[]> {
88
+ if (!(await fileExists(rootDir))) return [];
89
+ const out: string[] = [];
90
+ const walk = async (current: string): Promise<void> => {
91
+ const entries = await fs.readdir(current, { withFileTypes: true });
92
+ for (const entry of entries) {
93
+ const full = path.join(current, entry.name);
94
+ if (entry.isDirectory()) {
95
+ await walk(full);
96
+ } else {
97
+ out.push(full);
98
+ }
99
+ }
100
+ };
101
+ await walk(rootDir);
102
+ return out;
103
+ }
104
+
105
+ async function loadPreviousManifest(root: string): Promise<ScaffoldManifest | null> {
106
+ const manifestPath = path.join(root, "scaffold-manifest.json");
107
+ if (!(await fileExists(manifestPath))) return null;
108
+ try {
109
+ const parsed = JSON.parse(await fs.readFile(manifestPath, "utf8")) as Partial<ScaffoldManifest>;
110
+ if (!Array.isArray(parsed.assets)) return null;
111
+ return {
112
+ schema_version: "1.0",
113
+ generated_at: parsed.generated_at ?? "",
114
+ prodo_version: parsed.prodo_version ?? "0.0.0-dev",
115
+ copied_asset_count: Number(parsed.copied_asset_count ?? 0),
116
+ copied_assets: Array.isArray(parsed.copied_assets) ? parsed.copied_assets : [],
117
+ asset_count: Number(parsed.asset_count ?? 0),
118
+ parity_summary: {
119
+ match_count: Number(parsed.parity_summary?.match_count ?? 0),
120
+ drift_count: Number(parsed.parity_summary?.drift_count ?? 0),
121
+ missing_count: Number(parsed.parity_summary?.missing_count ?? 0),
122
+ protected_count: Number(parsed.parity_summary?.protected_count ?? 0),
123
+ updated_count: Number(parsed.parity_summary?.updated_count ?? 0),
124
+ unmanaged_count: Number(parsed.parity_summary?.unmanaged_count ?? 0)
125
+ },
126
+ assets: parsed.assets as AssetManifestItem[]
127
+ };
128
+ } catch {
129
+ return null;
130
+ }
131
+ }
132
+
133
+ async function copyDirIfMissing(
134
+ sourceDir: string,
135
+ targetDir: string,
136
+ copiedAssets: Array<{ source: string; target: string; sha256: string }>
137
+ ): Promise<void> {
138
+ if (!(await fileExists(sourceDir))) return;
139
+ await ensureDir(targetDir);
140
+ const entries = await fs.readdir(sourceDir, { withFileTypes: true });
141
+ for (const entry of entries) {
142
+ const src = path.join(sourceDir, entry.name);
143
+ const dst = path.join(targetDir, entry.name);
144
+ if (entry.isDirectory()) {
145
+ await copyDirIfMissing(src, dst, copiedAssets);
146
+ continue;
147
+ }
148
+ if (await fileExists(dst)) continue;
149
+ await fs.copyFile(src, dst);
150
+ copiedAssets.push({
151
+ source: src,
152
+ target: dst,
153
+ sha256: await fileSha256(dst)
154
+ });
155
+ }
156
+ }
157
+
158
+ async function refreshLegacyCommandTemplates(sourceDir: string, targetDir: string): Promise<void> {
159
+ if (!(await fileExists(sourceDir)) || !(await fileExists(targetDir))) return;
160
+ const entries = await fs.readdir(sourceDir, { withFileTypes: true });
161
+ for (const entry of entries) {
162
+ if (!entry.isFile()) continue;
163
+ if (!entry.name.startsWith("prodo-") || !entry.name.endsWith(".md")) continue;
164
+ const src = path.join(sourceDir, entry.name);
165
+ const dst = path.join(targetDir, entry.name);
166
+ if (!(await fileExists(dst))) continue;
167
+ const existing = await fs.readFile(dst, "utf8");
168
+ const isLegacyRunMode = /run:\s*\n\s*action:\s*[^\n]+/m.test(existing) || /mode:\s*internal-runtime/m.test(existing);
169
+ if (!isLegacyRunMode) continue;
170
+ await fs.copyFile(src, dst);
171
+ }
172
+ }
173
+
174
+ async function buildAssetManifest(
175
+ pairs: SourceTargetPair[],
176
+ previous: ScaffoldManifest | null,
177
+ backup: BackupMap
178
+ ): Promise<AssetManifestItem[]> {
179
+ const previousByTarget = new Map<string, AssetManifestItem>();
180
+ for (const item of previous?.assets ?? []) {
181
+ previousByTarget.set(path.resolve(item.target), item);
182
+ }
183
+
184
+ const items: AssetManifestItem[] = [];
185
+ for (const pair of pairs) {
186
+ const sourceFiles = await listFilesRecursive(pair.sourceDir);
187
+ for (const sourceFile of sourceFiles) {
188
+ const relative = path.relative(pair.sourceDir, sourceFile);
189
+ const targetFile = path.join(pair.targetDir, relative);
190
+ const resolvedTarget = path.resolve(targetFile);
191
+ const sourceHash = await fileSha256(sourceFile);
192
+ const targetExists = await fileExists(targetFile);
193
+
194
+ if (!targetExists) {
195
+ await ensureDir(path.dirname(targetFile));
196
+ backup.set(resolvedTarget, null);
197
+ await fs.copyFile(sourceFile, targetFile);
198
+ items.push({
199
+ source: sourceFile,
200
+ target: targetFile,
201
+ source_sha256: sourceHash,
202
+ target_sha256: await fileSha256(targetFile),
203
+ status: "missing"
204
+ });
205
+ continue;
206
+ }
207
+
208
+ const currentTargetHash = await fileSha256(targetFile);
209
+ const prev = previousByTarget.get(resolvedTarget);
210
+ const prevTargetHash = prev?.target_sha256 ?? null;
211
+ const prevSourceHash = prev?.source_sha256 ?? null;
212
+
213
+ if (currentTargetHash === sourceHash) {
214
+ items.push({
215
+ source: sourceFile,
216
+ target: targetFile,
217
+ source_sha256: sourceHash,
218
+ target_sha256: currentTargetHash,
219
+ status: "match"
220
+ });
221
+ continue;
222
+ }
223
+
224
+ if (prev && prevTargetHash && prevSourceHash && currentTargetHash === prevTargetHash && prevSourceHash !== sourceHash) {
225
+ if (!backup.has(resolvedTarget)) {
226
+ backup.set(resolvedTarget, await fs.readFile(targetFile));
227
+ }
228
+ await fs.copyFile(sourceFile, targetFile);
229
+ items.push({
230
+ source: sourceFile,
231
+ target: targetFile,
232
+ source_sha256: sourceHash,
233
+ target_sha256: await fileSha256(targetFile),
234
+ status: "updated"
235
+ });
236
+ continue;
237
+ }
238
+
239
+ if (prev) {
240
+ items.push({
241
+ source: sourceFile,
242
+ target: targetFile,
243
+ source_sha256: sourceHash,
244
+ target_sha256: currentTargetHash,
245
+ status: "protected"
246
+ });
247
+ continue;
248
+ }
249
+
250
+ items.push({
251
+ source: sourceFile,
252
+ target: targetFile,
253
+ source_sha256: sourceHash,
254
+ target_sha256: currentTargetHash,
255
+ status: "unmanaged"
256
+ });
257
+ }
258
+ }
259
+ return items;
260
+ }
261
+
262
+ async function rollbackFiles(backup: BackupMap): Promise<void> {
263
+ for (const [target, content] of backup.entries()) {
264
+ if (content === null) {
265
+ if (await fileExists(target)) await fs.rm(target, { force: true });
266
+ continue;
267
+ }
268
+ await ensureDir(path.dirname(target));
269
+ await fs.writeFile(target, content);
270
+ }
271
+ }
272
+
273
+ function summarizeParity(items: AssetManifestItem[]): ScaffoldManifest["parity_summary"] {
274
+ const byStatus = (status: AssetManifestItem["status"]) => items.filter((item) => item.status === status).length;
275
+ return {
276
+ match_count: byStatus("match"),
277
+ drift_count: byStatus("drift"),
278
+ missing_count: byStatus("missing"),
279
+ protected_count: byStatus("protected"),
280
+ updated_count: byStatus("updated"),
281
+ unmanaged_count: byStatus("unmanaged")
282
+ };
283
+ }
284
+
285
+ export async function runInit(
286
+ cwd: string,
287
+ options?: { ai?: SupportedAi; lang?: string; author?: string; preset?: string; script?: "sh" | "ps" }
288
+ ): Promise<{ installedAgentFiles: string[]; settingsPath: string }> {
289
+ const root = prodoPath(cwd);
290
+ const artifactDefs = await listArtifactDefinitions(cwd);
291
+ const artifactTypes = artifactDefs.map((item) => item.name);
292
+ const workflowCommands = buildWorkflowCommands(artifactTypes);
293
+ const prodoVersion = await readProdoVersion(cwd);
294
+ const localRepoTemplates = path.join(cwd, "templates");
295
+ const packagedTemplates = path.resolve(__dirname, "..", "..", "templates");
296
+ const projectScaffoldTemplates = (await fileExists(localRepoTemplates)) ? localRepoTemplates : packagedTemplates;
297
+ const copiedAssets: Array<{ source: string; target: string; sha256: string }> = [];
298
+ const backup: BackupMap = new Map();
299
+ const previousManifest = await loadPreviousManifest(root);
300
+
301
+ await ensureDir(path.join(root, "briefs"));
302
+ await ensureDir(path.join(root, "schemas"));
303
+ await ensureDir(path.join(root, "prompts"));
304
+ await ensureDir(path.join(root, "commands"));
305
+ await ensureDir(path.join(root, "presets"));
306
+ await ensureDir(path.join(root, "templates"));
307
+ await ensureDir(path.join(root, "templates", "overrides"));
308
+ await ensureDir(path.join(root, "state"));
309
+ await ensureDir(path.join(root, "state", "context"));
310
+ for (const def of artifactDefs) {
311
+ await ensureDir(outputDirPath(cwd, def.name, def.output_dir));
312
+ }
313
+ await ensureDir(path.join(cwd, "product-docs", "reports"));
314
+ await writeFileIfMissing(
315
+ outputIndexPath(cwd),
316
+ `${JSON.stringify({ active: {}, history: {}, updated_at: new Date(0).toISOString() }, null, 2)}\n`
317
+ );
318
+
319
+ await writeFileIfMissing(briefPath(cwd), START_BRIEF_TEMPLATE);
320
+ await writeFileIfMissing(
321
+ path.join(root, "briefs", "normalized-brief.json"),
322
+ `${JSON.stringify(NORMALIZED_BRIEF_TEMPLATE, null, 2)}\n`
323
+ );
324
+ await writeFileIfMissing(path.join(root, "hooks.yml"), HOOKS_TEMPLATE);
325
+ await writeFileIfMissing(path.join(root, "prompts", "normalize.md"), `${NORMALIZE_PROMPT_TEMPLATE}\n`);
326
+ const scriptType = options?.script ?? (process.platform === "win32" ? "ps" : "sh");
327
+ await fs.writeFile(
328
+ path.join(root, "init-options.json"),
329
+ `${JSON.stringify({ ai: options?.ai ?? null, lang: options?.lang ?? "en", author: options?.author ?? null, preset: options?.preset ?? null, script: scriptType }, null, 2)}\n`,
330
+ "utf8"
331
+ );
332
+
333
+ await copyDirIfMissing(path.join(projectScaffoldTemplates, "artifacts"), path.join(root, "templates"), copiedAssets);
334
+ for (const artifact of artifactDefs) {
335
+ const markdownTemplatePath = path.join(root, "templates", `${artifact.name}.md`);
336
+ const templateHeadings =
337
+ (await fileExists(markdownTemplatePath))
338
+ ? extractRequiredHeadingsFromTemplate(await fs.readFile(markdownTemplatePath, "utf8"))
339
+ : [];
340
+ const schema = {
341
+ ...schemaTemplate(artifact.name),
342
+ x_required_headings: templateHeadings.length > 0 ? templateHeadings : artifact.required_headings
343
+ };
344
+ await writeFileIfMissing(path.join(root, "schemas", `${artifact.name}.yaml`), yaml.dump(schema));
345
+ await writeFileIfMissing(path.join(root, "prompts", `${artifact.name}.md`), `${promptTemplate(artifact.name, options?.lang ?? "en")}\n`);
346
+ await writeFileIfMissing(
347
+ path.join(root, "templates", templateFileName(artifact.name)),
348
+ `${artifactTemplateTemplate(artifact.name, options?.lang ?? "en")}\n`
349
+ );
350
+ }
351
+
352
+ await copyDirIfMissing(path.join(projectScaffoldTemplates, "commands"), path.join(root, "commands"), copiedAssets);
353
+ await refreshLegacyCommandTemplates(path.join(projectScaffoldTemplates, "commands"), path.join(root, "commands"));
354
+ for (const command of workflowCommands) {
355
+ await writeFileIfMissing(path.join(root, "commands", `${command.name}.md`), `${commandTemplate(command)}\n`);
356
+ }
357
+
358
+ await applyConfiguredPresets(cwd, root, prodoVersion, options?.preset);
359
+
360
+ const pairs: SourceTargetPair[] = [
361
+ {
362
+ sourceDir: path.join(projectScaffoldTemplates, "commands"),
363
+ targetDir: path.join(root, "commands")
364
+ },
365
+ {
366
+ sourceDir: path.join(projectScaffoldTemplates, "artifacts"),
367
+ targetDir: path.join(root, "templates")
368
+ }
369
+ ];
370
+
371
+ let parity: AssetManifestItem[] = [];
372
+ try {
373
+ parity = await buildAssetManifest(pairs, previousManifest, backup);
374
+ } catch (error) {
375
+ await rollbackFiles(backup);
376
+ throw error;
377
+ }
378
+
379
+ const installedAgentFiles = options?.ai ? await installAgentCommands(cwd, options.ai) : [];
380
+ const manifest: ScaffoldManifest = {
381
+ schema_version: "1.0",
382
+ generated_at: new Date().toISOString(),
383
+ prodo_version: prodoVersion,
384
+ copied_asset_count: copiedAssets.length,
385
+ copied_assets: copiedAssets,
386
+ asset_count: parity.length,
387
+ parity_summary: summarizeParity(parity),
388
+ assets: parity
389
+ };
390
+ await fs.writeFile(path.join(root, "scaffold-manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
391
+ await syncRegistry(cwd);
392
+ const settingsPath = await writeSettings(cwd, {
393
+ lang: (options?.lang ?? "en").trim() || "en",
394
+ ai: options?.ai,
395
+ author: (options?.author ?? "").trim() || undefined
396
+ });
397
+ return { installedAgentFiles, settingsPath };
398
+ }
392
399