@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,237 +1,237 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import yaml from "js-yaml";
4
- import { UserError } from "./errors";
5
- import { readProjectConfig } from "./project-config";
6
- import { ensureDir, fileExists } from "./utils";
7
-
8
- type PresetManifest = {
9
- name: string;
10
- version?: string;
11
- priority?: number;
12
- min_prodo_version?: string;
13
- max_prodo_version?: string;
14
- command_packs?: string[];
15
- };
16
-
17
- type CopyOp = {
18
- source: string;
19
- target: string;
20
- priority: number;
21
- order: number;
22
- };
23
-
24
- function parseVersion(version: string): number[] {
25
- return version.split(".").map((part) => Number(part.replace(/[^0-9]/g, "")) || 0).slice(0, 3);
26
- }
27
-
28
- function cmpVersion(a: string, b: string): number {
29
- const left = parseVersion(a);
30
- const right = parseVersion(b);
31
- for (let i = 0; i < 3; i++) {
32
- if ((left[i] ?? 0) > (right[i] ?? 0)) return 1;
33
- if ((left[i] ?? 0) < (right[i] ?? 0)) return -1;
34
- }
35
- return 0;
36
- }
37
-
38
- async function readPresetManifest(presetDir: string): Promise<PresetManifest> {
39
- const candidates = ["preset.yaml", "preset.yml", "preset.json"];
40
- for (const name of candidates) {
41
- const file = path.join(presetDir, name);
42
- if (!(await fileExists(file))) continue;
43
- if (name.endsWith(".json")) {
44
- const parsed = JSON.parse(await fs.readFile(file, "utf8")) as Record<string, unknown>;
45
- const presetName = typeof parsed.name === "string" ? parsed.name.trim() : path.basename(presetDir);
46
- return {
47
- name: presetName,
48
- version: typeof parsed.version === "string" ? parsed.version : undefined,
49
- priority: typeof parsed.priority === "number" ? parsed.priority : 0,
50
- min_prodo_version: typeof parsed.min_prodo_version === "string" ? parsed.min_prodo_version : undefined,
51
- max_prodo_version: typeof parsed.max_prodo_version === "string" ? parsed.max_prodo_version : undefined,
52
- command_packs: Array.isArray(parsed.command_packs)
53
- ? parsed.command_packs.filter((item): item is string => typeof item === "string")
54
- : []
55
- };
56
- }
57
- const parsed = (yaml.load(await fs.readFile(file, "utf8")) as Record<string, unknown>) ?? {};
58
- const presetName = typeof parsed.name === "string" ? parsed.name.trim() : path.basename(presetDir);
59
- return {
60
- name: presetName,
61
- version: typeof parsed.version === "string" ? parsed.version : undefined,
62
- priority: typeof parsed.priority === "number" ? parsed.priority : 0,
63
- min_prodo_version: typeof parsed.min_prodo_version === "string" ? parsed.min_prodo_version : undefined,
64
- max_prodo_version: typeof parsed.max_prodo_version === "string" ? parsed.max_prodo_version : undefined,
65
- command_packs: Array.isArray(parsed.command_packs)
66
- ? parsed.command_packs.filter((item): item is string => typeof item === "string")
67
- : []
68
- };
69
- }
70
- throw new UserError(`Preset manifest is missing in ${presetDir} (expected preset.yaml or preset.json).`);
71
- }
72
-
73
- async function collectFilesRecursive(rootDir: string): Promise<string[]> {
74
- if (!(await fileExists(rootDir))) return [];
75
- const out: string[] = [];
76
- const walk = async (current: string): Promise<void> => {
77
- const entries = await fs.readdir(current, { withFileTypes: true });
78
- for (const entry of entries) {
79
- const full = path.join(current, entry.name);
80
- if (entry.isDirectory()) {
81
- await walk(full);
82
- } else {
83
- out.push(full);
84
- }
85
- }
86
- };
87
- await walk(rootDir);
88
- return out;
89
- }
90
-
91
- async function collectPresetOps(
92
- presetDir: string,
93
- prodoRoot: string,
94
- priority: number,
95
- order: number
96
- ): Promise<CopyOp[]> {
97
- const lanes = ["prompts", "schemas", "templates", "commands"];
98
- const ops: CopyOp[] = [];
99
- for (const lane of lanes) {
100
- const sourceBase = path.join(presetDir, lane);
101
- const files = await collectFilesRecursive(sourceBase);
102
- for (const source of files) {
103
- const relative = path.relative(sourceBase, source);
104
- ops.push({
105
- source,
106
- target: path.join(prodoRoot, lane, relative),
107
- priority,
108
- order
109
- });
110
- }
111
- }
112
- return ops;
113
- }
114
-
115
- async function resolvePresetDir(projectRoot: string, presetName: string): Promise<string> {
116
- const candidates = [
117
- path.join(projectRoot, "presets", presetName),
118
- path.resolve(__dirname, "..", "presets", presetName)
119
- ];
120
- for (const candidate of candidates) {
121
- if (await fileExists(candidate)) return candidate;
122
- }
123
- throw new UserError(`Preset not found: ${presetName}. Create presets/${presetName} with a preset manifest.`);
124
- }
125
-
126
- async function writeInstalledPresets(prodoRoot: string, names: string[]): Promise<void> {
127
- const file = path.join(prodoRoot, "presets", "installed.json");
128
- await ensureDir(path.dirname(file));
129
- await fs.writeFile(file, `${JSON.stringify(Array.from(new Set(names)).sort(), null, 2)}\n`, "utf8");
130
- }
131
-
132
- async function readInstalledPresets(prodoRoot: string): Promise<string[]> {
133
- const file = path.join(prodoRoot, "presets", "installed.json");
134
- if (!(await fileExists(file))) return [];
135
- try {
136
- const parsed = JSON.parse(await fs.readFile(file, "utf8")) as unknown;
137
- if (!Array.isArray(parsed)) return [];
138
- return parsed
139
- .filter((item): item is string => typeof item === "string")
140
- .map((item) => item.trim())
141
- .filter((item) => item.length > 0);
142
- } catch {
143
- return [];
144
- }
145
- }
146
-
147
- async function applyCopyOps(ops: CopyOp[]): Promise<void> {
148
- const selected = new Map<string, CopyOp>();
149
- for (const op of ops) {
150
- const current = selected.get(op.target);
151
- if (!current) {
152
- selected.set(op.target, op);
153
- continue;
154
- }
155
- if (op.priority > current.priority || (op.priority === current.priority && op.order > current.order)) {
156
- selected.set(op.target, op);
157
- }
158
- }
159
- for (const op of selected.values()) {
160
- await ensureDir(path.dirname(op.target));
161
- await fs.copyFile(op.source, op.target);
162
- }
163
- }
164
-
165
- async function collectCommandPackOps(projectRoot: string, prodoRoot: string, names: string[]): Promise<CopyOp[]> {
166
- const ops: CopyOp[] = [];
167
- for (const [index, name] of names.entries()) {
168
- const base = path.join(projectRoot, "command-packs", name);
169
- if (!(await fileExists(base))) {
170
- throw new UserError(`Command pack not found: command-packs/${name}`);
171
- }
172
- const laneMap: Record<string, string> = {
173
- commands: "commands"
174
- };
175
- for (const [sourceLane, targetLane] of Object.entries(laneMap)) {
176
- const sourceBase = path.join(base, sourceLane);
177
- const files = await collectFilesRecursive(sourceBase);
178
- for (const source of files) {
179
- const relative = path.relative(sourceBase, source);
180
- ops.push({
181
- source,
182
- target: path.join(prodoRoot, targetLane, relative),
183
- priority: 100,
184
- order: index
185
- });
186
- }
187
- }
188
- }
189
- return ops;
190
- }
191
-
192
- export async function applyConfiguredPresets(
193
- projectRoot: string,
194
- prodoRoot: string,
195
- prodoVersion: string,
196
- presetOverride?: string
197
- ): Promise<{ installedPresets: string[]; appliedFiles: string[] }> {
198
- const config = await readProjectConfig(projectRoot);
199
- const presets = Array.from(new Set([...(config.presets ?? []), ...(presetOverride ? [presetOverride] : [])]));
200
- const existingInstalled = await readInstalledPresets(prodoRoot);
201
- const allOps: CopyOp[] = [];
202
- const installedNames: string[] = [...existingInstalled];
203
- const commandPacks = new Set<string>(config.command_packs ?? []);
204
-
205
- for (const [order, presetName] of presets.entries()) {
206
- const presetDir = await resolvePresetDir(projectRoot, presetName);
207
- const manifest = await readPresetManifest(presetDir);
208
- if (manifest.min_prodo_version && cmpVersion(prodoVersion, manifest.min_prodo_version) < 0) {
209
- throw new UserError(
210
- `Preset ${presetName} requires prodo >= ${manifest.min_prodo_version}, current is ${prodoVersion}.`
211
- );
212
- }
213
- if (manifest.max_prodo_version && cmpVersion(prodoVersion, manifest.max_prodo_version) > 0) {
214
- throw new UserError(
215
- `Preset ${presetName} supports prodo <= ${manifest.max_prodo_version}, current is ${prodoVersion}.`
216
- );
217
- }
218
- for (const pack of manifest.command_packs ?? []) {
219
- if (pack.trim()) commandPacks.add(pack.trim());
220
- }
221
- installedNames.push(manifest.name || presetName);
222
- allOps.push(...(await collectPresetOps(presetDir, prodoRoot, manifest.priority ?? 0, order)));
223
- }
224
-
225
- const commandPackList = Array.from(commandPacks);
226
- if (commandPackList.length > 0) {
227
- const commandPackOps = await collectCommandPackOps(projectRoot, prodoRoot, commandPackList);
228
- allOps.push(...commandPackOps);
229
- }
230
- if (allOps.length > 0) await applyCopyOps(allOps);
231
- await writeInstalledPresets(prodoRoot, installedNames);
232
-
233
- return {
234
- installedPresets: Array.from(new Set(installedNames)),
235
- appliedFiles: Array.from(new Set(allOps.map((item) => item.target)))
236
- };
237
- }
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import yaml from "js-yaml";
4
+ import { UserError } from "../core/errors";
5
+ import { readProjectConfig } from "../core/project-config";
6
+ import { ensureDir, fileExists } from "../core/utils";
7
+
8
+ type PresetManifest = {
9
+ name: string;
10
+ version?: string;
11
+ priority?: number;
12
+ min_prodo_version?: string;
13
+ max_prodo_version?: string;
14
+ command_packs?: string[];
15
+ };
16
+
17
+ type CopyOp = {
18
+ source: string;
19
+ target: string;
20
+ priority: number;
21
+ order: number;
22
+ };
23
+
24
+ function parseVersion(version: string): number[] {
25
+ return version.split(".").map((part) => Number(part.replace(/[^0-9]/g, "")) || 0).slice(0, 3);
26
+ }
27
+
28
+ function cmpVersion(a: string, b: string): number {
29
+ const left = parseVersion(a);
30
+ const right = parseVersion(b);
31
+ for (let i = 0; i < 3; i++) {
32
+ if ((left[i] ?? 0) > (right[i] ?? 0)) return 1;
33
+ if ((left[i] ?? 0) < (right[i] ?? 0)) return -1;
34
+ }
35
+ return 0;
36
+ }
37
+
38
+ async function readPresetManifest(presetDir: string): Promise<PresetManifest> {
39
+ const candidates = ["preset.yaml", "preset.yml", "preset.json"];
40
+ for (const name of candidates) {
41
+ const file = path.join(presetDir, name);
42
+ if (!(await fileExists(file))) continue;
43
+ if (name.endsWith(".json")) {
44
+ const parsed = JSON.parse(await fs.readFile(file, "utf8")) as Record<string, unknown>;
45
+ const presetName = typeof parsed.name === "string" ? parsed.name.trim() : path.basename(presetDir);
46
+ return {
47
+ name: presetName,
48
+ version: typeof parsed.version === "string" ? parsed.version : undefined,
49
+ priority: typeof parsed.priority === "number" ? parsed.priority : 0,
50
+ min_prodo_version: typeof parsed.min_prodo_version === "string" ? parsed.min_prodo_version : undefined,
51
+ max_prodo_version: typeof parsed.max_prodo_version === "string" ? parsed.max_prodo_version : undefined,
52
+ command_packs: Array.isArray(parsed.command_packs)
53
+ ? parsed.command_packs.filter((item): item is string => typeof item === "string")
54
+ : []
55
+ };
56
+ }
57
+ const parsed = (yaml.load(await fs.readFile(file, "utf8")) as Record<string, unknown>) ?? {};
58
+ const presetName = typeof parsed.name === "string" ? parsed.name.trim() : path.basename(presetDir);
59
+ return {
60
+ name: presetName,
61
+ version: typeof parsed.version === "string" ? parsed.version : undefined,
62
+ priority: typeof parsed.priority === "number" ? parsed.priority : 0,
63
+ min_prodo_version: typeof parsed.min_prodo_version === "string" ? parsed.min_prodo_version : undefined,
64
+ max_prodo_version: typeof parsed.max_prodo_version === "string" ? parsed.max_prodo_version : undefined,
65
+ command_packs: Array.isArray(parsed.command_packs)
66
+ ? parsed.command_packs.filter((item): item is string => typeof item === "string")
67
+ : []
68
+ };
69
+ }
70
+ throw new UserError(`Preset manifest is missing in ${presetDir} (expected preset.yaml or preset.json).`);
71
+ }
72
+
73
+ async function collectFilesRecursive(rootDir: string): Promise<string[]> {
74
+ if (!(await fileExists(rootDir))) return [];
75
+ const out: string[] = [];
76
+ const walk = async (current: string): Promise<void> => {
77
+ const entries = await fs.readdir(current, { withFileTypes: true });
78
+ for (const entry of entries) {
79
+ const full = path.join(current, entry.name);
80
+ if (entry.isDirectory()) {
81
+ await walk(full);
82
+ } else {
83
+ out.push(full);
84
+ }
85
+ }
86
+ };
87
+ await walk(rootDir);
88
+ return out;
89
+ }
90
+
91
+ async function collectPresetOps(
92
+ presetDir: string,
93
+ prodoRoot: string,
94
+ priority: number,
95
+ order: number
96
+ ): Promise<CopyOp[]> {
97
+ const lanes = ["prompts", "schemas", "templates", "commands"];
98
+ const ops: CopyOp[] = [];
99
+ for (const lane of lanes) {
100
+ const sourceBase = path.join(presetDir, lane);
101
+ const files = await collectFilesRecursive(sourceBase);
102
+ for (const source of files) {
103
+ const relative = path.relative(sourceBase, source);
104
+ ops.push({
105
+ source,
106
+ target: path.join(prodoRoot, lane, relative),
107
+ priority,
108
+ order
109
+ });
110
+ }
111
+ }
112
+ return ops;
113
+ }
114
+
115
+ async function resolvePresetDir(projectRoot: string, presetName: string): Promise<string> {
116
+ const candidates = [
117
+ path.join(projectRoot, "presets", presetName),
118
+ path.resolve(__dirname, "..", "..", "presets", presetName)
119
+ ];
120
+ for (const candidate of candidates) {
121
+ if (await fileExists(candidate)) return candidate;
122
+ }
123
+ throw new UserError(`Preset not found: ${presetName}. Create presets/${presetName} with a preset manifest.`);
124
+ }
125
+
126
+ async function writeInstalledPresets(prodoRoot: string, names: string[]): Promise<void> {
127
+ const file = path.join(prodoRoot, "presets", "installed.json");
128
+ await ensureDir(path.dirname(file));
129
+ await fs.writeFile(file, `${JSON.stringify(Array.from(new Set(names)).sort(), null, 2)}\n`, "utf8");
130
+ }
131
+
132
+ async function readInstalledPresets(prodoRoot: string): Promise<string[]> {
133
+ const file = path.join(prodoRoot, "presets", "installed.json");
134
+ if (!(await fileExists(file))) return [];
135
+ try {
136
+ const parsed = JSON.parse(await fs.readFile(file, "utf8")) as unknown;
137
+ if (!Array.isArray(parsed)) return [];
138
+ return parsed
139
+ .filter((item): item is string => typeof item === "string")
140
+ .map((item) => item.trim())
141
+ .filter((item) => item.length > 0);
142
+ } catch {
143
+ return [];
144
+ }
145
+ }
146
+
147
+ async function applyCopyOps(ops: CopyOp[]): Promise<void> {
148
+ const selected = new Map<string, CopyOp>();
149
+ for (const op of ops) {
150
+ const current = selected.get(op.target);
151
+ if (!current) {
152
+ selected.set(op.target, op);
153
+ continue;
154
+ }
155
+ if (op.priority > current.priority || (op.priority === current.priority && op.order > current.order)) {
156
+ selected.set(op.target, op);
157
+ }
158
+ }
159
+ for (const op of selected.values()) {
160
+ await ensureDir(path.dirname(op.target));
161
+ await fs.copyFile(op.source, op.target);
162
+ }
163
+ }
164
+
165
+ async function collectCommandPackOps(projectRoot: string, prodoRoot: string, names: string[]): Promise<CopyOp[]> {
166
+ const ops: CopyOp[] = [];
167
+ for (const [index, name] of names.entries()) {
168
+ const base = path.join(projectRoot, "command-packs", name);
169
+ if (!(await fileExists(base))) {
170
+ throw new UserError(`Command pack not found: command-packs/${name}`);
171
+ }
172
+ const laneMap: Record<string, string> = {
173
+ commands: "commands"
174
+ };
175
+ for (const [sourceLane, targetLane] of Object.entries(laneMap)) {
176
+ const sourceBase = path.join(base, sourceLane);
177
+ const files = await collectFilesRecursive(sourceBase);
178
+ for (const source of files) {
179
+ const relative = path.relative(sourceBase, source);
180
+ ops.push({
181
+ source,
182
+ target: path.join(prodoRoot, targetLane, relative),
183
+ priority: 100,
184
+ order: index
185
+ });
186
+ }
187
+ }
188
+ }
189
+ return ops;
190
+ }
191
+
192
+ export async function applyConfiguredPresets(
193
+ projectRoot: string,
194
+ prodoRoot: string,
195
+ prodoVersion: string,
196
+ presetOverride?: string
197
+ ): Promise<{ installedPresets: string[]; appliedFiles: string[] }> {
198
+ const config = await readProjectConfig(projectRoot);
199
+ const presets = Array.from(new Set([...(config.presets ?? []), ...(presetOverride ? [presetOverride] : [])]));
200
+ const existingInstalled = await readInstalledPresets(prodoRoot);
201
+ const allOps: CopyOp[] = [];
202
+ const installedNames: string[] = [...existingInstalled];
203
+ const commandPacks = new Set<string>(config.command_packs ?? []);
204
+
205
+ for (const [order, presetName] of presets.entries()) {
206
+ const presetDir = await resolvePresetDir(projectRoot, presetName);
207
+ const manifest = await readPresetManifest(presetDir);
208
+ if (manifest.min_prodo_version && cmpVersion(prodoVersion, manifest.min_prodo_version) < 0) {
209
+ throw new UserError(
210
+ `Preset ${presetName} requires prodo >= ${manifest.min_prodo_version}, current is ${prodoVersion}.`
211
+ );
212
+ }
213
+ if (manifest.max_prodo_version && cmpVersion(prodoVersion, manifest.max_prodo_version) > 0) {
214
+ throw new UserError(
215
+ `Preset ${presetName} supports prodo <= ${manifest.max_prodo_version}, current is ${prodoVersion}.`
216
+ );
217
+ }
218
+ for (const pack of manifest.command_packs ?? []) {
219
+ if (pack.trim()) commandPacks.add(pack.trim());
220
+ }
221
+ installedNames.push(manifest.name || presetName);
222
+ allOps.push(...(await collectPresetOps(presetDir, prodoRoot, manifest.priority ?? 0, order)));
223
+ }
224
+
225
+ const commandPackList = Array.from(commandPacks);
226
+ if (commandPackList.length > 0) {
227
+ const commandPackOps = await collectCommandPackOps(projectRoot, prodoRoot, commandPackList);
228
+ allOps.push(...commandPackOps);
229
+ }
230
+ if (allOps.length > 0) await applyCopyOps(allOps);
231
+ await writeInstalledPresets(prodoRoot, installedNames);
232
+
233
+ return {
234
+ installedPresets: Array.from(new Set(installedNames)),
235
+ appliedFiles: Array.from(new Set(allOps.map((item) => item.target)))
236
+ };
237
+ }
@@ -1,69 +1,69 @@
1
- import {
2
- defaultOutputDir,
3
- defaultRequiredContractsByArtifact,
4
- defaultRequiredHeadings,
5
- defaultUpstreamByArtifact
6
- } from "./constants";
7
- import { readProjectConfig } from "./project-config";
8
- import { ARTIFACT_TYPES } from "./types";
9
- import type { ArtifactType, ContractCoverage } from "./types";
10
-
11
- export type ArtifactDefinition = {
12
- name: ArtifactType;
13
- output_dir: string;
14
- required_headings: string[];
15
- upstream: ArtifactType[];
16
- required_contracts: Array<keyof ContractCoverage>;
17
- };
18
-
19
- function normalizeName(name: string): string {
20
- return name.trim().toLowerCase().replace(/[^a-z0-9_-]/g, "-");
21
- }
22
-
23
- function toDefinition(partial: {
24
- name: string;
25
- output_dir?: string;
26
- required_headings?: string[];
27
- upstream?: string[];
28
- required_contracts?: Array<keyof ContractCoverage>;
29
- }): ArtifactDefinition {
30
- const name = normalizeName(partial.name);
31
- const outputDir = partial.output_dir?.trim() || defaultOutputDir(name);
32
- const requiredHeadings = partial.required_headings?.length ? partial.required_headings : defaultRequiredHeadings(name);
33
- const upstream = (partial.upstream?.length ? partial.upstream : defaultUpstreamByArtifact(name)).map(normalizeName);
34
- const requiredContracts = partial.required_contracts?.length
35
- ? partial.required_contracts
36
- : defaultRequiredContractsByArtifact(name);
37
- return {
38
- name,
39
- output_dir: outputDir,
40
- required_headings: requiredHeadings,
41
- upstream,
42
- required_contracts: requiredContracts
43
- };
44
- }
45
-
46
- export async function listArtifactDefinitions(cwd: string): Promise<ArtifactDefinition[]> {
47
- const config = await readProjectConfig(cwd);
48
- const base = ARTIFACT_TYPES.map((name) => toDefinition({ name }));
49
- const byName = new Map<string, ArtifactDefinition>(base.map((item) => [item.name, item]));
50
- for (const extra of config.artifacts ?? []) {
51
- const merged = toDefinition(extra);
52
- byName.set(merged.name, merged);
53
- }
54
- return Array.from(byName.values());
55
- }
56
-
57
- export async function listArtifactTypes(cwd: string): Promise<ArtifactType[]> {
58
- const defs = await listArtifactDefinitions(cwd);
59
- return defs.map((item) => item.name);
60
- }
61
-
62
- export async function getArtifactDefinition(cwd: string, artifactType: string): Promise<ArtifactDefinition> {
63
- const normalized = normalizeName(artifactType);
64
- const defs = await listArtifactDefinitions(cwd);
65
- const found = defs.find((item) => item.name === normalized);
66
- if (found) return found;
67
- return toDefinition({ name: normalized });
68
- }
69
-
1
+ import {
2
+ defaultOutputDir,
3
+ defaultRequiredContractsByArtifact,
4
+ defaultRequiredHeadings,
5
+ defaultUpstreamByArtifact
6
+ } from "./constants";
7
+ import { readProjectConfig } from "./project-config";
8
+ import { ARTIFACT_TYPES } from "./types";
9
+ import type { ArtifactType, ContractCoverage } from "./types";
10
+
11
+ export type ArtifactDefinition = {
12
+ name: ArtifactType;
13
+ output_dir: string;
14
+ required_headings: string[];
15
+ upstream: ArtifactType[];
16
+ required_contracts: Array<keyof ContractCoverage>;
17
+ };
18
+
19
+ function normalizeName(name: string): string {
20
+ return name.trim().toLowerCase().replace(/[^a-z0-9_-]/g, "-");
21
+ }
22
+
23
+ function toDefinition(partial: {
24
+ name: string;
25
+ output_dir?: string;
26
+ required_headings?: string[];
27
+ upstream?: string[];
28
+ required_contracts?: Array<keyof ContractCoverage>;
29
+ }): ArtifactDefinition {
30
+ const name = normalizeName(partial.name);
31
+ const outputDir = partial.output_dir?.trim() || defaultOutputDir(name);
32
+ const requiredHeadings = partial.required_headings?.length ? partial.required_headings : defaultRequiredHeadings(name);
33
+ const upstream = (partial.upstream?.length ? partial.upstream : defaultUpstreamByArtifact(name)).map(normalizeName);
34
+ const requiredContracts = partial.required_contracts?.length
35
+ ? partial.required_contracts
36
+ : defaultRequiredContractsByArtifact(name);
37
+ return {
38
+ name,
39
+ output_dir: outputDir,
40
+ required_headings: requiredHeadings,
41
+ upstream,
42
+ required_contracts: requiredContracts
43
+ };
44
+ }
45
+
46
+ export async function listArtifactDefinitions(cwd: string): Promise<ArtifactDefinition[]> {
47
+ const config = await readProjectConfig(cwd);
48
+ const base = ARTIFACT_TYPES.map((name) => toDefinition({ name }));
49
+ const byName = new Map<string, ArtifactDefinition>(base.map((item) => [item.name, item]));
50
+ for (const extra of config.artifacts ?? []) {
51
+ const merged = toDefinition(extra);
52
+ byName.set(merged.name, merged);
53
+ }
54
+ return Array.from(byName.values());
55
+ }
56
+
57
+ export async function listArtifactTypes(cwd: string): Promise<ArtifactType[]> {
58
+ const defs = await listArtifactDefinitions(cwd);
59
+ return defs.map((item) => item.name);
60
+ }
61
+
62
+ export async function getArtifactDefinition(cwd: string, artifactType: string): Promise<ArtifactDefinition> {
63
+ const normalized = normalizeName(artifactType);
64
+ const defs = await listArtifactDefinitions(cwd);
65
+ const found = defs.find((item) => item.name === normalized);
66
+ if (found) return found;
67
+ return toDefinition({ name: normalized });
68
+ }
69
+