@shahmarasy/prodo 0.1.5 → 0.1.6

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 (67) hide show
  1. package/dist/agents/system-prompts.js +12 -12
  2. package/dist/cli/agent-command-installer.js +18 -18
  3. package/dist/cli/doctor.js +2 -2
  4. package/dist/cli/index.js +32 -30
  5. package/dist/cli/init-tui.d.ts +2 -2
  6. package/dist/cli/init-tui.js +43 -36
  7. package/dist/cli/init.d.ts +1 -0
  8. package/dist/cli/init.js +2 -1
  9. package/dist/core/artifacts.js +72 -72
  10. package/dist/core/settings.d.ts +1 -0
  11. package/dist/core/settings.js +10 -2
  12. package/dist/core/templates.js +248 -248
  13. package/dist/i18n/en.json +45 -45
  14. package/dist/i18n/tr.json +45 -45
  15. package/dist/providers/mock-provider.js +5 -5
  16. package/dist/providers/openai-provider.js +12 -12
  17. package/dist/skill-engine/context.d.ts +7 -0
  18. package/dist/skill-engine/context.js +76 -0
  19. package/dist/skill-engine/discovery.d.ts +2 -0
  20. package/dist/skill-engine/discovery.js +52 -0
  21. package/dist/skill-engine/graph.d.ts +4 -0
  22. package/dist/skill-engine/graph.js +114 -0
  23. package/dist/skill-engine/index.d.ts +11 -0
  24. package/dist/skill-engine/index.js +49 -0
  25. package/dist/skill-engine/pipeline.d.ts +9 -0
  26. package/dist/skill-engine/pipeline.js +84 -0
  27. package/dist/skill-engine/registry.d.ts +12 -0
  28. package/dist/skill-engine/registry.js +74 -0
  29. package/dist/skill-engine/types.d.ts +66 -0
  30. package/dist/skill-engine/types.js +2 -0
  31. package/dist/skill-engine/validator.d.ts +4 -0
  32. package/dist/skill-engine/validator.js +90 -0
  33. package/dist/skills/fix.d.ts +2 -0
  34. package/dist/skills/fix.js +41 -0
  35. package/dist/skills/generate-artifact.d.ts +2 -0
  36. package/dist/skills/generate-artifact.js +42 -0
  37. package/dist/skills/normalize.d.ts +2 -0
  38. package/dist/skills/normalize.js +29 -0
  39. package/dist/skills/register-core.d.ts +2 -0
  40. package/dist/skills/register-core.js +21 -0
  41. package/dist/skills/validate.d.ts +2 -0
  42. package/dist/skills/validate.js +37 -0
  43. package/package.json +4 -6
  44. package/src/cli/doctor.ts +2 -2
  45. package/src/cli/index.ts +35 -31
  46. package/src/cli/init-tui.ts +220 -208
  47. package/src/cli/init.ts +3 -2
  48. package/src/core/settings.ts +41 -35
  49. package/src/skill-engine/context.ts +90 -0
  50. package/src/skill-engine/discovery.ts +57 -0
  51. package/src/skill-engine/graph.ts +136 -0
  52. package/src/skill-engine/index.ts +55 -0
  53. package/src/skill-engine/pipeline.ts +112 -0
  54. package/src/skill-engine/registry.ts +75 -0
  55. package/src/skill-engine/types.ts +81 -0
  56. package/src/skill-engine/validator.ts +135 -0
  57. package/src/skills/fix.ts +45 -0
  58. package/src/skills/generate-artifact.ts +48 -0
  59. package/src/skills/{normalize-skill.ts → normalize.ts} +15 -12
  60. package/src/skills/register-core.ts +27 -0
  61. package/src/skills/validate.ts +40 -0
  62. package/src/skills/engine.ts +0 -94
  63. package/src/skills/fix-skill.ts +0 -38
  64. package/src/skills/generate-artifact-skill.ts +0 -32
  65. package/src/skills/generate-pipeline-skill.ts +0 -49
  66. package/src/skills/types.ts +0 -36
  67. package/src/skills/validate-skill.ts +0 -29
@@ -1,208 +1,220 @@
1
- import path from "node:path";
2
- import os from "node:os";
3
- import { resolveAi, type SupportedAi } from "./agent-command-installer";
4
- import { UserError } from "../core/errors";
5
- import { fileExists } from "../core/utils";
6
-
7
- export type InitSelections = {
8
- ai?: SupportedAi;
9
- script: "sh" | "ps";
10
- lang: "tr" | "en";
11
- author: string;
12
- interactive: boolean;
13
- };
14
-
15
- type GatherInitUiOptions = {
16
- projectRoot: string;
17
- aiInput?: string;
18
- langInput?: string;
19
- authorInput?: string;
20
- };
21
-
22
- type ClackPrompts = typeof import("@clack/prompts");
23
-
24
- const dynamicImport = new Function("specifier", "return import(specifier)") as (
25
- specifier: string
26
- ) => Promise<unknown>;
27
-
28
- async function loadClack(): Promise<ClackPrompts> {
29
- return (await dynamicImport("@clack/prompts")) as ClackPrompts;
30
- }
31
-
32
- function isInteractiveTerminal(): boolean {
33
- return Boolean(process.stdin.isTTY && process.stdout.isTTY && !process.env.CI);
34
- }
35
-
36
- function color(text: string, code: string): string {
37
- return `${code}${text}\u001B[0m`;
38
- }
39
-
40
- function stripAnsi(input: string): string {
41
- return input.replace(/\u001B\[[0-9;]*m/g, "");
42
- }
43
-
44
- function centerLine(line: string, width: number): string {
45
- const visible = stripAnsi(line).length;
46
- const left = Math.max(0, Math.floor((width - visible) / 2));
47
- return `${" ".repeat(left)}${line}`;
48
- }
49
-
50
- function centerBlock(lines: string[], width: number): string {
51
- return lines.map((line) => centerLine(line, width)).join("\n");
52
- }
53
-
54
- function terminalWidth(): number {
55
- const columns = process.stdout.columns ?? 100;
56
- return Math.max(80, columns);
57
- }
58
-
59
- function renderLogo(): string {
60
- const cyan = "\u001B[38;5;45m";
61
- const blue = "\u001B[38;5;39m";
62
- const lines = [
63
- "██████╗ ██████╗ ██████╗ ██████╗ ██████╗ ",
64
- "██╔══██╗██╔══██╗██╔═══██╗██╔══██╗██╔═══██╗",
65
- "██████╔╝██████╔╝██║ ██║██║ ██║██║ ██║",
66
- "██╔═══╝ ██╔══██╗██║ ██║██║ ██║██║ ██║",
67
- "██║ ██║ ██║╚██████╔╝██████╔╝╚██████╔╝"
68
- ];
69
- return lines.map((line, idx) => color(line, idx % 2 === 0 ? cyan : blue)).join("\n");
70
- }
71
-
72
- function renderProjectBox(projectName: string, projectRoot: string): string {
73
- const content = [`Project ${projectName}`, `Directory ${projectRoot}`];
74
- const innerWidth = Math.max(...content.map((line) => line.length)) + 2;
75
- const top = `┌${"─".repeat(innerWidth)}┐`;
76
- const rows = content.map((line) => `│ ${line.padEnd(innerWidth - 1)}│`);
77
- const bottom = `└${"─".repeat(innerWidth)}┘`;
78
- return [top, ...rows, bottom].join("\n");
79
- }
80
-
81
- async function detectAi(projectRoot: string): Promise<SupportedAi | undefined> {
82
- if (await fileExists(path.join(projectRoot, ".agents"))) return "codex";
83
- if (await fileExists(path.join(projectRoot, ".gemini"))) return "gemini-cli";
84
- if (await fileExists(path.join(projectRoot, ".claude"))) return "claude-cli";
85
- return undefined;
86
- }
87
-
88
- function normalizeLang(lang?: string): "tr" | "en" {
89
- if ((lang ?? "").trim().toLowerCase().startsWith("tr")) return "tr";
90
- return "en";
91
- }
92
-
93
- function modelChoiceFromAi(ai?: SupportedAi): "codex" | "gemini" | "auto-detect" {
94
- if (ai === "codex") return "codex";
95
- if (ai === "gemini-cli") return "gemini";
96
- return "auto-detect";
97
- }
98
-
99
- function defaultAuthorName(authorInput?: string): string {
100
- const explicit = (authorInput ?? "").trim();
101
- if (explicit.length > 0) return explicit;
102
- try {
103
- const username = os.userInfo().username.trim();
104
- if (username.length > 0) return username;
105
- } catch {
106
- // ignore lookup errors and continue with fallback
107
- }
108
- return "Product Author";
109
- }
110
-
111
- export async function gatherInitSelections(options: GatherInitUiOptions): Promise<InitSelections> {
112
- const clack = await loadClack();
113
- const defaultLang = normalizeLang(options.langInput);
114
- const fallbackScript: "sh" | "ps" = process.platform === "win32" ? "ps" : "sh";
115
- const parsedAi = resolveAi(options.aiInput);
116
- const defaultAuthor = defaultAuthorName(options.authorInput);
117
-
118
- if (!isInteractiveTerminal()) {
119
- return {
120
- ai: parsedAi,
121
- script: fallbackScript,
122
- lang: defaultLang,
123
- author: defaultAuthor,
124
- interactive: false
125
- };
126
- }
127
-
128
- const detectedAi = await detectAi(options.projectRoot);
129
- const initialModel = modelChoiceFromAi(parsedAi ?? detectedAi);
130
- const projectName = path.basename(options.projectRoot) || ".";
131
- const width = terminalWidth();
132
- const subtitle = color("Prodo — Product Artifact Toolkit", "\u001B[1;37m");
133
- const signature = color("Crafted by Codex, guided by Shahmarasy intelligence", "\u001B[38;5;244m");
134
- const hero = [
135
- "",
136
- centerBlock(renderLogo().split("\n"), width),
137
- "",
138
- centerLine(subtitle, width),
139
- centerLine(signature, width),
140
- ""
141
- ].join("\n");
142
-
143
- clack.intro(hero);
144
- clack.note(renderProjectBox(projectName, options.projectRoot), "Project Setup");
145
-
146
- const model = await clack.select({
147
- message: "Select model",
148
- initialValue: initialModel,
149
- options: [
150
- { value: "codex", label: "codex", hint: "Native Codex flow" },
151
- { value: "gemini", label: "gemini", hint: "Gemini CLI command set" },
152
- { value: "auto-detect", label: "auto-detect", hint: "Detect from local agent directories" }
153
- ]
154
- });
155
- if (clack.isCancel(model)) {
156
- clack.cancel("Initialization cancelled.");
157
- throw new UserError("Initialization cancelled.");
158
- }
159
-
160
- const lang = await clack.select({
161
- message: "Select language",
162
- initialValue: defaultLang,
163
- options: [
164
- { value: "tr", label: "tr", hint: "Turkish" },
165
- { value: "en", label: "en", hint: "English" }
166
- ]
167
- });
168
- if (clack.isCancel(lang)) {
169
- clack.cancel("Initialization cancelled.");
170
- throw new UserError("Initialization cancelled.");
171
- }
172
-
173
- const author = await clack.text({
174
- message: "Author name",
175
- placeholder: "Shahmarasy",
176
- defaultValue: defaultAuthor
177
- });
178
- if (clack.isCancel(author)) {
179
- clack.cancel("Initialization cancelled.");
180
- throw new UserError("Initialization cancelled.");
181
- }
182
-
183
- let selectedAi: SupportedAi | undefined;
184
- if (model === "codex") selectedAi = "codex";
185
- else if (model === "gemini") selectedAi = "gemini-cli";
186
- else selectedAi = detectedAi;
187
-
188
- return {
189
- ai: selectedAi,
190
- script: fallbackScript,
191
- lang,
192
- author: String(author).trim() || defaultAuthor,
193
- interactive: true
194
- };
195
- }
196
-
197
- export function finishInitInteractive(summary: {
198
- projectRoot: string;
199
- settingsPath: string;
200
- ai?: SupportedAi;
201
- lang: "tr" | "en";
202
- author: string;
203
- }): Promise<void> {
204
- const aiText = summary.ai ?? "none";
205
- return loadClack().then((clack) => clack.outro(
206
- `Scaffold complete.\nAI: ${aiText}\nLanguage: ${summary.lang}\nAuthor: ${summary.author}\nSettings: ${summary.settingsPath}\nNext: edit brief.md`
207
- ));
208
- }
1
+ import path from "node:path";
2
+ import os from "node:os";
3
+ import { resolveAi, type SupportedAi } from "./agent-command-installer";
4
+ import { UserError } from "../core/errors";
5
+ import { fileExists } from "../core/utils";
6
+
7
+ export type InitSelections = {
8
+ ai?: SupportedAi;
9
+ script: "sh" | "ps";
10
+ lang: string;
11
+ author: string;
12
+ interactive: boolean;
13
+ };
14
+
15
+ type GatherInitUiOptions = {
16
+ projectRoot: string;
17
+ aiInput?: string;
18
+ langInput?: string;
19
+ authorInput?: string;
20
+ };
21
+
22
+ type ClackPrompts = typeof import("@clack/prompts");
23
+
24
+ const dynamicImport = new Function("specifier", "return import(specifier)") as (
25
+ specifier: string
26
+ ) => Promise<unknown>;
27
+
28
+ async function loadClack(): Promise<ClackPrompts> {
29
+ return (await dynamicImport("@clack/prompts")) as ClackPrompts;
30
+ }
31
+
32
+ function isInteractiveTerminal(): boolean {
33
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY && !process.env.CI);
34
+ }
35
+
36
+ function color(text: string, code: string): string {
37
+ return `${code}${text}\u001B[0m`;
38
+ }
39
+
40
+ function stripAnsi(input: string): string {
41
+ return input.replace(/\u001B\[[0-9;]*m/g, "");
42
+ }
43
+
44
+ function centerLine(line: string, width: number): string {
45
+ const visible = stripAnsi(line).length;
46
+ const left = Math.max(0, Math.floor((width - visible) / 2));
47
+ return `${" ".repeat(left)}${line}`;
48
+ }
49
+
50
+ function centerBlock(lines: string[], width: number): string {
51
+ return lines.map((line) => centerLine(line, width)).join("\n");
52
+ }
53
+
54
+ function terminalWidth(): number {
55
+ const columns = process.stdout.columns ?? 100;
56
+ return Math.max(80, columns);
57
+ }
58
+
59
+ function renderLogo(): string {
60
+ const cyan = "\u001B[38;5;45m";
61
+ const blue = "\u001B[38;5;39m";
62
+ const lines = [
63
+ "██████╗ ██████╗ ██████╗ ██████╗ ██████╗ ",
64
+ "██╔══██╗██╔══██╗██╔═══██╗██╔══██╗██╔═══██╗",
65
+ "██████╔╝██████╔╝██║ ██║██║ ██║██║ ██║",
66
+ "██╔═══╝ ██╔══██╗██║ ██║██║ ██║██║ ██║",
67
+ "██║ ██║ ██║╚██████╔╝██████╔╝╚██████╔╝"
68
+ ];
69
+ return lines.map((line, idx) => color(line, idx % 2 === 0 ? cyan : blue)).join("\n");
70
+ }
71
+
72
+ function renderProjectBox(projectName: string, projectRoot: string): string {
73
+ const content = [`Project ${projectName}`, `Directory ${projectRoot}`];
74
+ const innerWidth = Math.max(...content.map((line) => line.length)) + 2;
75
+ const top = `┌${"─".repeat(innerWidth)}┐`;
76
+ const rows = content.map((line) => `│ ${line.padEnd(innerWidth - 1)}│`);
77
+ const bottom = `└${"─".repeat(innerWidth)}┘`;
78
+ return [top, ...rows, bottom].join("\n");
79
+ }
80
+
81
+ async function detectAi(projectRoot: string): Promise<SupportedAi | undefined> {
82
+ if (await fileExists(path.join(projectRoot, ".claude"))) return "claude-cli";
83
+ if (await fileExists(path.join(projectRoot, ".agents"))) return "codex";
84
+ if (await fileExists(path.join(projectRoot, ".gemini"))) return "gemini-cli";
85
+ return undefined;
86
+ }
87
+
88
+ function normalizeLang(lang?: string): string {
89
+ const raw = (lang ?? "").trim().toLowerCase();
90
+ if (raw.startsWith("tr")) return "tr";
91
+ return "en";
92
+ }
93
+
94
+ function defaultAuthorName(authorInput?: string): string {
95
+ const explicit = (authorInput ?? "").trim();
96
+ if (explicit.length > 0) return explicit;
97
+ try {
98
+ const username = os.userInfo().username.trim();
99
+ if (username.length > 0) return username;
100
+ } catch {
101
+ // ignore
102
+ }
103
+ return "Product Author";
104
+ }
105
+
106
+ export async function gatherInitSelections(options: GatherInitUiOptions): Promise<InitSelections> {
107
+ const clack = await loadClack();
108
+ const defaultLang = normalizeLang(options.langInput);
109
+ const fallbackScript: "sh" | "ps" = process.platform === "win32" ? "ps" : "sh";
110
+ const parsedAi = resolveAi(options.aiInput);
111
+ const defaultAuthor = defaultAuthorName(options.authorInput);
112
+
113
+ if (!isInteractiveTerminal()) {
114
+ return {
115
+ ai: parsedAi,
116
+ script: fallbackScript,
117
+ lang: defaultLang,
118
+ author: defaultAuthor,
119
+ interactive: false
120
+ };
121
+ }
122
+
123
+ const detectedAi = await detectAi(options.projectRoot);
124
+ const projectName = path.basename(options.projectRoot) || ".";
125
+ const width = terminalWidth();
126
+ const subtitle = color("Prodo — AI-Powered Product Owner", "\u001B[1;37m");
127
+ const signature = color("Built by Shahmarasy · Works with Claude, Codex, and Gemini", "\u001B[38;5;244m");
128
+ const hero = [
129
+ "",
130
+ centerBlock(renderLogo().split("\n"), width),
131
+ "",
132
+ centerLine(subtitle, width),
133
+ centerLine(signature, width),
134
+ ""
135
+ ].join("\n");
136
+
137
+ clack.intro(hero);
138
+ clack.note(renderProjectBox(projectName, options.projectRoot), "Project Setup");
139
+
140
+ const agentChoice = await clack.select({
141
+ message: "Which AI agent will you use?",
142
+ initialValue: parsedAi ?? detectedAi ?? "claude-cli",
143
+ options: [
144
+ { value: "claude-cli", label: "Claude Code", hint: "Slash commands → .claude/commands/" },
145
+ { value: "codex", label: "Codex", hint: "Skills → .agents/skills/" },
146
+ { value: "gemini-cli", label: "Gemini CLI", hint: "Commands → .gemini/commands/" }
147
+ ]
148
+ });
149
+ if (clack.isCancel(agentChoice)) {
150
+ clack.cancel("Initialization cancelled.");
151
+ throw new UserError("Initialization cancelled.");
152
+ }
153
+
154
+ const lang = await clack.select({
155
+ message: "Document language",
156
+ initialValue: defaultLang,
157
+ options: [
158
+ { value: "en", label: "English" },
159
+ { value: "tr", label: "Türkçe" }
160
+ ]
161
+ });
162
+ if (clack.isCancel(lang)) {
163
+ clack.cancel("Initialization cancelled.");
164
+ throw new UserError("Initialization cancelled.");
165
+ }
166
+
167
+ const author = await clack.text({
168
+ message: "Author name",
169
+ placeholder: "Your name",
170
+ defaultValue: defaultAuthor
171
+ });
172
+ if (clack.isCancel(author)) {
173
+ clack.cancel("Initialization cancelled.");
174
+ throw new UserError("Initialization cancelled.");
175
+ }
176
+
177
+ return {
178
+ ai: agentChoice as SupportedAi,
179
+ script: fallbackScript,
180
+ lang: String(lang),
181
+ author: String(author).trim() || defaultAuthor,
182
+ interactive: true
183
+ };
184
+ }
185
+
186
+ export function finishInitInteractive(summary: {
187
+ projectRoot: string;
188
+ settingsPath: string;
189
+ ai?: SupportedAi;
190
+ lang: string;
191
+ author: string;
192
+ }): Promise<void> {
193
+ const agentLabel = summary.ai === "claude-cli" ? "Claude Code"
194
+ : summary.ai === "codex" ? "Codex"
195
+ : summary.ai === "gemini-cli" ? "Gemini CLI"
196
+ : "none";
197
+
198
+ const commandPrefix = summary.ai === "codex" ? "$" : "/";
199
+ const commands = [
200
+ `${commandPrefix}prodo-normalize`,
201
+ `${commandPrefix}prodo-prd`,
202
+ `${commandPrefix}prodo-workflow`,
203
+ `${commandPrefix}prodo-wireframe`,
204
+ `${commandPrefix}prodo-stories`,
205
+ `${commandPrefix}prodo-techspec`,
206
+ `${commandPrefix}prodo-validate`
207
+ ];
208
+
209
+ return loadClack().then((clack) => clack.outro(
210
+ `Scaffold complete!\n\n` +
211
+ ` Agent: ${agentLabel}\n` +
212
+ ` Language: ${summary.lang}\n` +
213
+ ` Author: ${summary.author}\n\n` +
214
+ `Next steps:\n` +
215
+ ` 1. Edit brief.md with your product description\n` +
216
+ ` 2. Open this folder in ${agentLabel}\n` +
217
+ ` 3. Run commands in sequence:\n` +
218
+ ` ${commands.join("\n ")}`
219
+ ));
220
+ }
package/src/cli/init.ts CHANGED
@@ -284,7 +284,7 @@ function summarizeParity(items: AssetManifestItem[]): ScaffoldManifest["parity_s
284
284
 
285
285
  export async function runInit(
286
286
  cwd: string,
287
- options?: { ai?: SupportedAi; lang?: string; author?: string; preset?: string; script?: "sh" | "ps" }
287
+ options?: { ai?: SupportedAi; lang?: string; author?: string; preset?: string; script?: "sh" | "ps"; provider?: string }
288
288
  ): Promise<{ installedAgentFiles: string[]; settingsPath: string }> {
289
289
  const root = prodoPath(cwd);
290
290
  const artifactDefs = await listArtifactDefinitions(cwd);
@@ -392,7 +392,8 @@ export async function runInit(
392
392
  const settingsPath = await writeSettings(cwd, {
393
393
  lang: (options?.lang ?? "en").trim() || "en",
394
394
  ai: options?.ai,
395
- author: (options?.author ?? "").trim() || undefined
395
+ author: (options?.author ?? "").trim() || undefined,
396
+ provider: options?.provider
396
397
  });
397
398
  return { installedAgentFiles, settingsPath };
398
399
  }
@@ -1,35 +1,41 @@
1
- import fs from "node:fs/promises";
2
- import { settingsPath } from "./paths";
3
- import { fileExists } from "./utils";
4
-
5
- export type ProdoSettings = {
6
- lang: string;
7
- ai?: string;
8
- author?: string;
9
- };
10
-
11
- const DEFAULT_SETTINGS: ProdoSettings = {
12
- lang: "en"
13
- };
14
-
15
- export async function readSettings(cwd: string): Promise<ProdoSettings> {
16
- const path = settingsPath(cwd);
17
- if (!(await fileExists(path))) return { ...DEFAULT_SETTINGS };
18
- try {
19
- const raw = await fs.readFile(path, "utf8");
20
- const parsed = JSON.parse(raw) as Partial<ProdoSettings>;
21
- return {
22
- lang: typeof parsed.lang === "string" && parsed.lang.trim() ? parsed.lang.trim() : "en",
23
- ai: typeof parsed.ai === "string" && parsed.ai.trim() ? parsed.ai.trim() : undefined,
24
- author: typeof parsed.author === "string" && parsed.author.trim() ? parsed.author.trim() : undefined
25
- };
26
- } catch {
27
- return { ...DEFAULT_SETTINGS };
28
- }
29
- }
30
-
31
- export async function writeSettings(cwd: string, settings: ProdoSettings): Promise<string> {
32
- const path = settingsPath(cwd);
33
- await fs.writeFile(path, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
34
- return path;
35
- }
1
+ import fs from "node:fs/promises";
2
+ import { settingsPath } from "./paths";
3
+ import { fileExists } from "./utils";
4
+
5
+ export type ProdoSettings = {
6
+ lang: string;
7
+ ai?: string;
8
+ author?: string;
9
+ provider?: string;
10
+ };
11
+
12
+ const DEFAULT_SETTINGS: ProdoSettings = {
13
+ lang: "en"
14
+ };
15
+
16
+ export async function readSettings(cwd: string): Promise<ProdoSettings> {
17
+ const path = settingsPath(cwd);
18
+ if (!(await fileExists(path))) return { ...DEFAULT_SETTINGS };
19
+ try {
20
+ const raw = await fs.readFile(path, "utf8");
21
+ const parsed = JSON.parse(raw) as Partial<ProdoSettings>;
22
+ return {
23
+ lang: typeof parsed.lang === "string" && parsed.lang.trim() ? parsed.lang.trim() : "en",
24
+ ai: typeof parsed.ai === "string" && parsed.ai.trim() ? parsed.ai.trim() : undefined,
25
+ author: typeof parsed.author === "string" && parsed.author.trim() ? parsed.author.trim() : undefined,
26
+ provider: typeof parsed.provider === "string" && parsed.provider.trim() ? parsed.provider.trim() : undefined
27
+ };
28
+ } catch {
29
+ return { ...DEFAULT_SETTINGS };
30
+ }
31
+ }
32
+
33
+ export async function writeSettings(cwd: string, settings: ProdoSettings): Promise<string> {
34
+ const path = settingsPath(cwd);
35
+ const clean: Record<string, unknown> = { lang: settings.lang };
36
+ if (settings.ai) clean.ai = settings.ai;
37
+ if (settings.author) clean.author = settings.author;
38
+ if (settings.provider) clean.provider = settings.provider;
39
+ await fs.writeFile(path, `${JSON.stringify(clean, null, 2)}\n`, "utf8");
40
+ return path;
41
+ }
@@ -0,0 +1,90 @@
1
+ import { normalizedBriefPath } from "../core/paths";
2
+ import { getActiveArtifactPath } from "../core/output-index";
3
+ import { fileExists } from "../core/utils";
4
+ import type { ArtifactType } from "../core/types";
5
+ import type { PipelineState } from "./types";
6
+
7
+ export function createPipelineState(cwd: string): PipelineState {
8
+ return {
9
+ cwd,
10
+ normalizedBriefPath: undefined,
11
+ generatedArtifacts: new Map(),
12
+ validationResult: undefined,
13
+ custom: {},
14
+ startedAt: new Date().toISOString(),
15
+ completedSkills: []
16
+ };
17
+ }
18
+
19
+ export async function hydrateStateFromDisk(
20
+ cwd: string,
21
+ state: PipelineState,
22
+ artifactTypes: ArtifactType[]
23
+ ): Promise<void> {
24
+ const nbPath = normalizedBriefPath(cwd);
25
+ if (!state.normalizedBriefPath && (await fileExists(nbPath))) {
26
+ state.normalizedBriefPath = nbPath;
27
+ }
28
+
29
+ for (const type of artifactTypes) {
30
+ if (!state.generatedArtifacts.has(type)) {
31
+ const active = await getActiveArtifactPath(cwd, type);
32
+ if (active) {
33
+ state.generatedArtifacts.set(type, active);
34
+ }
35
+ }
36
+ }
37
+ }
38
+
39
+ export function isOutputSatisfied(
40
+ outputName: string,
41
+ state: PipelineState,
42
+ artifactType?: string
43
+ ): boolean {
44
+ if (outputName === "normalizedBriefPath") {
45
+ return typeof state.normalizedBriefPath === "string" && state.normalizedBriefPath.length > 0;
46
+ }
47
+ if (outputName === "artifactPath" && artifactType) {
48
+ return state.generatedArtifacts.has(artifactType);
49
+ }
50
+ if (outputName === "validationResult") {
51
+ return state.validationResult !== undefined;
52
+ }
53
+ return false;
54
+ }
55
+
56
+ export function wireInputsFromState(
57
+ inputNames: string[],
58
+ state: PipelineState
59
+ ): Record<string, unknown> {
60
+ const inputs: Record<string, unknown> = {};
61
+ for (const name of inputNames) {
62
+ if (name === "cwd") inputs.cwd = state.cwd;
63
+ else if (name === "normalizedBriefPath") inputs.normalizedBriefPath = state.normalizedBriefPath;
64
+ else if (name in state.custom) inputs[name] = state.custom[name];
65
+ }
66
+ return inputs;
67
+ }
68
+
69
+ export function wireOutputsToState(
70
+ outputs: Record<string, unknown>,
71
+ state: PipelineState,
72
+ skillName: string
73
+ ): void {
74
+ if ("normalizedBriefPath" in outputs && typeof outputs.normalizedBriefPath === "string") {
75
+ state.normalizedBriefPath = outputs.normalizedBriefPath;
76
+ }
77
+ if ("artifactPath" in outputs && typeof outputs.artifactPath === "string") {
78
+ const artifactType = skillName;
79
+ state.generatedArtifacts.set(artifactType, outputs.artifactPath);
80
+ }
81
+ if ("validationResult" in outputs && outputs.validationResult && typeof outputs.validationResult === "object") {
82
+ state.validationResult = outputs.validationResult as PipelineState["validationResult"];
83
+ }
84
+
85
+ for (const [key, value] of Object.entries(outputs)) {
86
+ if (!["normalizedBriefPath", "artifactPath", "validationResult"].includes(key)) {
87
+ state.custom[key] = value;
88
+ }
89
+ }
90
+ }