@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.
- package/README.md +201 -97
- package/bin/prodo.cjs +6 -6
- package/dist/agents/agent-registry.d.ts +13 -0
- package/dist/agents/agent-registry.js +79 -0
- package/dist/agents/anthropic/index.d.ts +9 -0
- package/dist/agents/anthropic/index.js +55 -0
- package/dist/agents/base.d.ts +25 -0
- package/dist/agents/base.js +71 -0
- package/dist/agents/google/index.d.ts +9 -0
- package/dist/agents/google/index.js +53 -0
- package/dist/agents/mock/index.d.ts +11 -0
- package/dist/agents/mock/index.js +26 -0
- package/dist/agents/openai/index.d.ts +9 -0
- package/dist/agents/openai/index.js +57 -0
- package/dist/agents/system-prompts.d.ts +3 -0
- package/dist/agents/system-prompts.js +32 -0
- package/dist/agents.js +4 -2
- package/dist/artifacts.d.ts +1 -0
- package/dist/artifacts.js +265 -31
- package/dist/cli/agent-command-installer.d.ts +4 -0
- package/dist/cli/agent-command-installer.js +148 -0
- package/dist/cli/agent-ids.d.ts +15 -0
- package/dist/cli/agent-ids.js +49 -0
- package/dist/cli/doctor.d.ts +1 -0
- package/dist/cli/doctor.js +144 -0
- package/dist/cli/fix-tui.d.ts +4 -0
- package/dist/cli/fix-tui.js +79 -0
- package/dist/cli/index.d.ts +9 -0
- package/dist/cli/index.js +465 -0
- package/dist/cli/init-tui.d.ts +23 -0
- package/dist/cli/init-tui.js +176 -0
- package/dist/cli/init.d.ts +11 -0
- package/dist/cli/init.js +334 -0
- package/dist/cli/normalize-interactive.d.ts +8 -0
- package/dist/cli/normalize-interactive.js +167 -0
- package/dist/cli/preset-loader.d.ts +4 -0
- package/dist/cli/preset-loader.js +210 -0
- package/dist/cli.js +80 -3
- package/dist/core/artifact-registry.d.ts +11 -0
- package/dist/core/artifact-registry.js +49 -0
- package/dist/core/artifacts.d.ts +10 -0
- package/dist/core/artifacts.js +892 -0
- package/dist/core/clean.d.ts +10 -0
- package/dist/core/clean.js +74 -0
- package/dist/core/consistency.d.ts +8 -0
- package/dist/core/consistency.js +328 -0
- package/dist/core/constants.d.ts +7 -0
- package/dist/core/constants.js +64 -0
- package/dist/core/errors.d.ts +3 -0
- package/dist/core/errors.js +10 -0
- package/dist/core/fix.d.ts +31 -0
- package/dist/core/fix.js +188 -0
- package/dist/core/hook-executor.d.ts +1 -0
- package/dist/core/hook-executor.js +175 -0
- package/dist/core/markdown.d.ts +16 -0
- package/dist/core/markdown.js +81 -0
- package/dist/core/normalize.d.ts +8 -0
- package/dist/core/normalize.js +125 -0
- package/dist/core/normalized-brief.d.ts +48 -0
- package/dist/core/normalized-brief.js +182 -0
- package/dist/core/output-index.d.ts +13 -0
- package/dist/core/output-index.js +55 -0
- package/dist/core/paths.d.ts +17 -0
- package/dist/core/paths.js +80 -0
- package/dist/core/project-config.d.ts +14 -0
- package/dist/core/project-config.js +69 -0
- package/dist/core/registry.d.ts +13 -0
- package/dist/core/registry.js +115 -0
- package/dist/core/settings.d.ts +7 -0
- package/dist/core/settings.js +35 -0
- package/dist/core/template-engine.d.ts +3 -0
- package/dist/core/template-engine.js +43 -0
- package/dist/core/template-resolver.d.ts +15 -0
- package/dist/core/template-resolver.js +46 -0
- package/dist/core/templates.d.ts +33 -0
- package/dist/core/templates.js +440 -0
- package/dist/core/terminology.d.ts +21 -0
- package/dist/core/terminology.js +143 -0
- package/dist/core/tracing.d.ts +21 -0
- package/dist/core/tracing.js +74 -0
- package/dist/core/types.d.ts +35 -0
- package/dist/core/types.js +5 -0
- package/dist/core/utils.d.ts +7 -0
- package/dist/core/utils.js +66 -0
- package/dist/core/validate.d.ts +10 -0
- package/dist/core/validate.js +226 -0
- package/dist/core/validator.d.ts +5 -0
- package/dist/core/validator.js +76 -0
- package/dist/core/version.d.ts +1 -0
- package/dist/core/version.js +30 -0
- package/dist/core/workflow-commands.d.ts +7 -0
- package/dist/core/workflow-commands.js +29 -0
- package/dist/i18n/en.json +45 -0
- package/dist/i18n/index.d.ts +5 -0
- package/dist/i18n/index.js +63 -0
- package/dist/i18n/tr.json +45 -0
- package/dist/init-tui.d.ts +3 -0
- package/dist/init-tui.js +28 -1
- package/dist/init.d.ts +1 -0
- package/dist/init.js +9 -3
- package/dist/normalize.js +55 -7
- package/dist/providers/index.d.ts +2 -1
- package/dist/providers/index.js +20 -6
- package/dist/providers/mock-provider.d.ts +1 -1
- package/dist/providers/mock-provider.js +7 -6
- package/dist/providers/openai-provider.d.ts +1 -1
- package/dist/providers/openai-provider.js +3 -2
- package/dist/settings.d.ts +1 -0
- package/dist/settings.js +2 -1
- package/dist/skills/engine.d.ts +10 -0
- package/dist/skills/engine.js +75 -0
- package/dist/skills/fix-skill.d.ts +2 -0
- package/dist/skills/fix-skill.js +38 -0
- package/dist/skills/generate-artifact-skill.d.ts +2 -0
- package/dist/skills/generate-artifact-skill.js +32 -0
- package/dist/skills/generate-pipeline-skill.d.ts +2 -0
- package/dist/skills/generate-pipeline-skill.js +45 -0
- package/dist/skills/normalize-skill.d.ts +2 -0
- package/dist/skills/normalize-skill.js +29 -0
- package/dist/skills/types.d.ts +28 -0
- package/dist/skills/types.js +2 -0
- package/dist/skills/validate-skill.d.ts +2 -0
- package/dist/skills/validate-skill.js +29 -0
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +2 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +13 -0
- package/dist/validator.js +0 -4
- package/dist/workflow-commands.js +2 -1
- package/package.json +74 -45
- package/presets/fintech/preset.json +48 -1
- package/presets/fintech/prompts/prd.md +99 -2
- package/presets/marketplace/preset.json +51 -1
- package/presets/marketplace/prompts/prd.md +140 -2
- package/presets/saas/preset.json +53 -1
- package/presets/saas/prompts/prd.md +150 -2
- package/src/agents/agent-registry.ts +93 -0
- package/src/agents/anthropic/index.ts +86 -0
- package/src/agents/anthropic/manifest.json +7 -0
- package/src/agents/base.ts +77 -0
- package/src/agents/google/index.ts +79 -0
- package/src/agents/google/manifest.json +7 -0
- package/src/agents/mock/index.ts +32 -0
- package/src/agents/mock/manifest.json +7 -0
- package/src/agents/openai/index.ts +83 -0
- package/src/agents/openai/manifest.json +7 -0
- package/src/agents/system-prompts.ts +35 -0
- package/src/{agent-command-installer.ts → cli/agent-command-installer.ts} +164 -164
- package/src/{agents.ts → cli/agent-ids.ts} +58 -56
- package/src/{doctor.ts → cli/doctor.ts} +157 -137
- package/src/cli/fix-tui.ts +111 -0
- package/src/{cli.ts → cli/index.ts} +459 -319
- package/src/{init-tui.ts → cli/init-tui.ts} +208 -179
- package/src/{init.ts → cli/init.ts} +398 -391
- package/src/cli/normalize-interactive.ts +241 -0
- package/src/{preset-loader.ts → cli/preset-loader.ts} +237 -237
- package/src/{artifact-registry.ts → core/artifact-registry.ts} +69 -69
- package/src/{artifacts.ts → core/artifacts.ts} +1081 -777
- package/src/core/clean.ts +88 -0
- package/src/{consistency.ts → core/consistency.ts} +374 -303
- package/src/{constants.ts → core/constants.ts} +72 -72
- package/src/{errors.ts → core/errors.ts} +7 -7
- package/src/core/fix.ts +253 -0
- package/src/{hook-executor.ts → core/hook-executor.ts} +196 -196
- package/src/{markdown.ts → core/markdown.ts} +93 -73
- package/src/core/normalize.ts +145 -0
- package/src/{normalized-brief.ts → core/normalized-brief.ts} +227 -206
- package/src/{output-index.ts → core/output-index.ts} +59 -59
- package/src/{paths.ts → core/paths.ts} +75 -71
- package/src/{project-config.ts → core/project-config.ts} +78 -78
- package/src/{registry.ts → core/registry.ts} +119 -119
- package/src/{settings.ts → core/settings.ts} +35 -34
- package/src/core/template-engine.ts +45 -0
- package/src/{template-resolver.ts → core/template-resolver.ts} +54 -54
- package/src/{templates.ts → core/templates.ts} +452 -450
- package/src/core/terminology.ts +177 -0
- package/src/core/tracing.ts +110 -0
- package/src/{types.ts → core/types.ts} +46 -46
- package/src/{utils.ts → core/utils.ts} +64 -50
- package/src/{validate.ts → core/validate.ts} +252 -246
- package/src/{validator.ts → core/validator.ts} +92 -96
- package/src/{version.ts → core/version.ts} +24 -24
- package/src/{workflow-commands.ts → core/workflow-commands.ts} +32 -31
- package/src/i18n/en.json +45 -0
- package/src/i18n/index.ts +58 -0
- package/src/i18n/tr.json +45 -0
- package/src/providers/index.ts +29 -12
- package/src/providers/mock-provider.ts +200 -199
- package/src/providers/openai-provider.ts +88 -87
- package/src/skills/engine.ts +94 -0
- package/src/skills/fix-skill.ts +38 -0
- package/src/skills/generate-artifact-skill.ts +32 -0
- package/src/skills/generate-pipeline-skill.ts +49 -0
- package/src/skills/normalize-skill.ts +29 -0
- package/src/skills/types.ts +36 -0
- package/src/skills/validate-skill.ts +29 -0
- package/templates/commands/prodo-fix.md +46 -0
- package/templates/commands/prodo-normalize.md +118 -23
- package/templates/commands/prodo-prd.md +138 -17
- package/templates/commands/prodo-stories.md +153 -17
- package/templates/commands/prodo-techspec.md +167 -17
- package/templates/commands/prodo-validate.md +184 -26
- package/templates/commands/prodo-wireframe.md +188 -17
- package/templates/commands/prodo-workflow.md +200 -17
- package/src/normalize.ts +0 -89
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { BaseAgent, type AgentConfig } from "../base";
|
|
2
|
+
import { buildSystemPrompt, buildUserMessage } from "../system-prompts";
|
|
3
|
+
import { UserError } from "../../core/errors";
|
|
4
|
+
import type { GenerateResult, ProviderSchemaHint } from "../../core/types";
|
|
5
|
+
|
|
6
|
+
const dynamicImport = new Function("specifier", "return import(specifier)") as (
|
|
7
|
+
specifier: string
|
|
8
|
+
) => Promise<unknown>;
|
|
9
|
+
|
|
10
|
+
export class GoogleAgent extends BaseAgent {
|
|
11
|
+
readonly name = "google";
|
|
12
|
+
readonly displayName = "Google Gemini";
|
|
13
|
+
readonly sdkRequired = "@google/generative-ai";
|
|
14
|
+
|
|
15
|
+
getConfig(): AgentConfig {
|
|
16
|
+
return {
|
|
17
|
+
name: this.name,
|
|
18
|
+
displayName: this.displayName,
|
|
19
|
+
sdkRequired: this.sdkRequired,
|
|
20
|
+
envVars: ["GOOGLE_API_KEY"],
|
|
21
|
+
defaultModel: "gemini-2.0-flash"
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async generate(
|
|
26
|
+
prompt: string,
|
|
27
|
+
inputContext: Record<string, unknown>,
|
|
28
|
+
schemaHint: ProviderSchemaHint
|
|
29
|
+
): Promise<GenerateResult> {
|
|
30
|
+
const apiKey = process.env.GOOGLE_API_KEY;
|
|
31
|
+
if (!apiKey) {
|
|
32
|
+
throw new UserError("GOOGLE_API_KEY is not set. Set it to use the Google Gemini agent.");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const model = process.env.PRODO_GOOGLE_MODEL ?? "gemini-2.0-flash";
|
|
36
|
+
|
|
37
|
+
const outputLanguage =
|
|
38
|
+
typeof inputContext.outputLanguage === "string" && inputContext.outputLanguage.trim()
|
|
39
|
+
? inputContext.outputLanguage.trim()
|
|
40
|
+
: "en";
|
|
41
|
+
|
|
42
|
+
const system = buildSystemPrompt(schemaHint, outputLanguage);
|
|
43
|
+
const user = buildUserMessage(prompt, inputContext);
|
|
44
|
+
|
|
45
|
+
let GoogleGenerativeAI: new (apiKey: string) => {
|
|
46
|
+
getGenerativeModel: (config: { model: string; systemInstruction: string }) => {
|
|
47
|
+
generateContent: (content: string) => Promise<{
|
|
48
|
+
response: { text: () => string };
|
|
49
|
+
}>;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const mod = (await dynamicImport("@google/generative-ai")) as {
|
|
55
|
+
GoogleGenerativeAI: typeof GoogleGenerativeAI;
|
|
56
|
+
};
|
|
57
|
+
GoogleGenerativeAI = mod.GoogleGenerativeAI;
|
|
58
|
+
} catch {
|
|
59
|
+
throw new UserError(
|
|
60
|
+
"Google Generative AI SDK is not installed. Run: npm install @google/generative-ai"
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const client = new GoogleGenerativeAI(apiKey);
|
|
65
|
+
const generativeModel = client.getGenerativeModel({
|
|
66
|
+
model,
|
|
67
|
+
systemInstruction: system
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const result = await generativeModel.generateContent(user);
|
|
71
|
+
const content = result.response.text().trim();
|
|
72
|
+
|
|
73
|
+
if (!content) {
|
|
74
|
+
throw new UserError("Google Gemini agent returned an empty response.");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { body: content };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { BaseAgent, type AgentConfig } from "../base";
|
|
2
|
+
import { MockProvider } from "../../providers/mock-provider";
|
|
3
|
+
import type { GenerateResult, ProviderSchemaHint } from "../../core/types";
|
|
4
|
+
|
|
5
|
+
export class MockAgent extends BaseAgent {
|
|
6
|
+
readonly name = "mock";
|
|
7
|
+
readonly displayName = "Mock (Testing)";
|
|
8
|
+
readonly sdkRequired = null;
|
|
9
|
+
|
|
10
|
+
private readonly provider = new MockProvider();
|
|
11
|
+
|
|
12
|
+
async generate(
|
|
13
|
+
prompt: string,
|
|
14
|
+
inputContext: Record<string, unknown>,
|
|
15
|
+
schemaHint: ProviderSchemaHint
|
|
16
|
+
): Promise<GenerateResult> {
|
|
17
|
+
return this.provider.generate(prompt, inputContext, schemaHint);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async isAvailable(): Promise<boolean> {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
getConfig(): AgentConfig {
|
|
25
|
+
return {
|
|
26
|
+
name: this.name,
|
|
27
|
+
displayName: this.displayName,
|
|
28
|
+
sdkRequired: null,
|
|
29
|
+
envVars: []
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { BaseAgent, type AgentConfig } from "../base";
|
|
2
|
+
import { buildSystemPrompt, buildUserMessage } from "../system-prompts";
|
|
3
|
+
import { UserError } from "../../core/errors";
|
|
4
|
+
import type { GenerateResult, ProviderSchemaHint } from "../../core/types";
|
|
5
|
+
|
|
6
|
+
const dynamicImport = new Function("specifier", "return import(specifier)") as (
|
|
7
|
+
specifier: string
|
|
8
|
+
) => Promise<unknown>;
|
|
9
|
+
|
|
10
|
+
export class OpenAIAgent extends BaseAgent {
|
|
11
|
+
readonly name = "openai";
|
|
12
|
+
readonly displayName = "OpenAI";
|
|
13
|
+
readonly sdkRequired = "openai";
|
|
14
|
+
|
|
15
|
+
getConfig(): AgentConfig {
|
|
16
|
+
return {
|
|
17
|
+
name: this.name,
|
|
18
|
+
displayName: this.displayName,
|
|
19
|
+
sdkRequired: this.sdkRequired,
|
|
20
|
+
envVars: ["OPENAI_API_KEY"],
|
|
21
|
+
defaultModel: "gpt-4o-mini"
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async generate(
|
|
26
|
+
prompt: string,
|
|
27
|
+
inputContext: Record<string, unknown>,
|
|
28
|
+
schemaHint: ProviderSchemaHint
|
|
29
|
+
): Promise<GenerateResult> {
|
|
30
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
31
|
+
if (!apiKey) {
|
|
32
|
+
throw new UserError("OPENAI_API_KEY is not set. Set it to use the OpenAI agent.");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const model = process.env.PRODO_OPENAI_MODEL ?? "gpt-4o-mini";
|
|
36
|
+
const baseURL = process.env.PRODO_OPENAI_BASE_URL ?? undefined;
|
|
37
|
+
|
|
38
|
+
const outputLanguage =
|
|
39
|
+
typeof inputContext.outputLanguage === "string" && inputContext.outputLanguage.trim()
|
|
40
|
+
? inputContext.outputLanguage.trim()
|
|
41
|
+
: "en";
|
|
42
|
+
|
|
43
|
+
const system = buildSystemPrompt(schemaHint, outputLanguage);
|
|
44
|
+
const user = buildUserMessage(prompt, inputContext);
|
|
45
|
+
|
|
46
|
+
let OpenAIConstructor: new (opts: { apiKey: string; baseURL?: string }) => {
|
|
47
|
+
chat: {
|
|
48
|
+
completions: {
|
|
49
|
+
create: (params: Record<string, unknown>) => Promise<{
|
|
50
|
+
choices: Array<{ message?: { content?: string } }>;
|
|
51
|
+
}>;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const mod = (await dynamicImport("openai")) as { default: typeof OpenAIConstructor };
|
|
58
|
+
OpenAIConstructor = mod.default;
|
|
59
|
+
} catch {
|
|
60
|
+
throw new UserError(
|
|
61
|
+
"OpenAI SDK is not installed. Run: npm install openai"
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const client = new OpenAIConstructor({ apiKey, baseURL });
|
|
66
|
+
|
|
67
|
+
const response = await client.chat.completions.create({
|
|
68
|
+
model,
|
|
69
|
+
messages: [
|
|
70
|
+
{ role: "system", content: system },
|
|
71
|
+
{ role: "user", content: user }
|
|
72
|
+
],
|
|
73
|
+
temperature: 0.2
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const content = response.choices[0]?.message?.content?.trim();
|
|
77
|
+
if (!content) {
|
|
78
|
+
throw new UserError("OpenAI agent returned an empty response.");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { body: content };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ProviderSchemaHint } from "../core/types";
|
|
2
|
+
|
|
3
|
+
export function buildSystemPrompt(schemaHint: ProviderSchemaHint, outputLanguage: string): string {
|
|
4
|
+
const mode = schemaHint.artifactType;
|
|
5
|
+
|
|
6
|
+
if (mode === "normalize") {
|
|
7
|
+
return `You normalize messy human product briefs into strict JSON.
|
|
8
|
+
Return valid JSON only, no markdown. Include confidence scores (0..1) for critical fields.
|
|
9
|
+
Preserve source language and Unicode characters exactly; never transliterate Turkish letters to ASCII.`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (mode === "semantic_consistency") {
|
|
13
|
+
return `You detect semantic inconsistencies between paired artifacts.
|
|
14
|
+
Return valid JSON only: { "issues": [{level, code, check, contract_id, file, message, suggestion}] }.`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (mode === "contract_relevance") {
|
|
18
|
+
return `You verify whether tagged content actually matches the referenced contract text.
|
|
19
|
+
Return valid JSON only: { "relevant": boolean, "score": number, "reason": string }.`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return `You are a product-document generator.
|
|
23
|
+
Return only Markdown body content.
|
|
24
|
+
Headings required:
|
|
25
|
+
${schemaHint.requiredHeadings.join("\n")}
|
|
26
|
+
Required contract tags:
|
|
27
|
+
${schemaHint.requiredContracts.join(", ")}
|
|
28
|
+
Use tags like [G1], [F2], [C1] where relevant.
|
|
29
|
+
Output language: ${outputLanguage}
|
|
30
|
+
Do not translate required headings.`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function buildUserMessage(prompt: string, inputContext: Record<string, unknown>): string {
|
|
34
|
+
return `${prompt}\n\nContext JSON:\n${JSON.stringify(inputContext, null, 2)}`;
|
|
35
|
+
}
|
|
@@ -1,164 +1,164 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import yaml from "js-yaml";
|
|
4
|
-
import { UserError } from "
|
|
5
|
-
import { ensureDir, fileExists } from "
|
|
6
|
-
|
|
7
|
-
export const AI_ALIASES: Record<string, "codex" | "gemini-cli" | "claude-cli"> = {
|
|
8
|
-
codex: "codex",
|
|
9
|
-
gemini: "gemini-cli",
|
|
10
|
-
"gemmini-cli": "gemini-cli",
|
|
11
|
-
"gemmini": "gemini-cli",
|
|
12
|
-
"gemini-cli": "gemini-cli",
|
|
13
|
-
claude: "claude-cli",
|
|
14
|
-
"claude-cli": "claude-cli"
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export type SupportedAi = "codex" | "gemini-cli" | "claude-cli";
|
|
18
|
-
|
|
19
|
-
type AgentConfig = {
|
|
20
|
-
baseDir: string;
|
|
21
|
-
format: "markdown" | "toml" | "skill";
|
|
22
|
-
extension: string;
|
|
23
|
-
argsPlaceholder: string;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const AGENT_CONFIG: Record<SupportedAi, AgentConfig> = {
|
|
27
|
-
"claude-cli": {
|
|
28
|
-
baseDir: ".claude/commands",
|
|
29
|
-
format: "markdown",
|
|
30
|
-
extension: ".md",
|
|
31
|
-
argsPlaceholder: "$ARGUMENTS"
|
|
32
|
-
},
|
|
33
|
-
"gemini-cli": {
|
|
34
|
-
baseDir: ".gemini/commands",
|
|
35
|
-
format: "toml",
|
|
36
|
-
extension: ".toml",
|
|
37
|
-
argsPlaceholder: "{{args}}"
|
|
38
|
-
},
|
|
39
|
-
codex: {
|
|
40
|
-
baseDir: ".agents/skills",
|
|
41
|
-
format: "skill",
|
|
42
|
-
extension: "/SKILL.md",
|
|
43
|
-
argsPlaceholder: "$ARGUMENTS"
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
type ParsedTemplate = {
|
|
48
|
-
frontmatter: Record<string, unknown>;
|
|
49
|
-
body: string;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
function parseFrontmatter(content: string): ParsedTemplate {
|
|
53
|
-
if (!content.startsWith("---")) return { frontmatter: {}, body: content };
|
|
54
|
-
const end = content.indexOf("\n---", 4);
|
|
55
|
-
if (end === -1) return { frontmatter: {}, body: content };
|
|
56
|
-
const fmRaw = content.slice(3, end).trim();
|
|
57
|
-
const body = content.slice(end + 4).trimStart();
|
|
58
|
-
const frontmatter = (yaml.load(fmRaw) as Record<string, unknown>) ?? {};
|
|
59
|
-
return { frontmatter, body };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function renderFrontmatter(frontmatter: Record<string, unknown>): string {
|
|
63
|
-
if (Object.keys(frontmatter).length === 0) return "";
|
|
64
|
-
return `---\n${yaml.dump(frontmatter)}---\n`;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function sanitizeFrontmatter(frontmatter: Record<string, unknown>): Record<string, unknown> {
|
|
68
|
-
const out = { ...frontmatter };
|
|
69
|
-
delete out.run;
|
|
70
|
-
delete out.scripts;
|
|
71
|
-
return out;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function toTomlPrompt(body: string, frontmatter: Record<string, unknown>, argsPlaceholder: string): string {
|
|
75
|
-
const description = String(frontmatter.description ?? "Prodo command");
|
|
76
|
-
const promptBody = body.replaceAll("$ARGUMENTS", argsPlaceholder);
|
|
77
|
-
return `description = "${description.replace(/"/g, '\\"')}"
|
|
78
|
-
|
|
79
|
-
prompt = """
|
|
80
|
-
Important execution rule:
|
|
81
|
-
- This is an agent slash command, not a shell command.
|
|
82
|
-
- Do NOT run \`prodo-normalize\`, \`prodo-prd\`, or \`prodo ...\` in shell.
|
|
83
|
-
- Execute the workflow directly using workspace files.
|
|
84
|
-
|
|
85
|
-
${promptBody}
|
|
86
|
-
"""`;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function toSkill(name: string, body: string, frontmatter: Record<string, unknown>): string {
|
|
90
|
-
const description = String(frontmatter.description ?? "Prodo workflow command");
|
|
91
|
-
return `---
|
|
92
|
-
name: ${name}
|
|
93
|
-
description: ${description}
|
|
94
|
-
compatibility: Requires Prodo project scaffold (.prodo)
|
|
95
|
-
metadata:
|
|
96
|
-
author: prodo
|
|
97
|
-
source: .prodo/commands/${name}.md
|
|
98
|
-
---
|
|
99
|
-
|
|
100
|
-
${body}`;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function resolveAi(ai?: string): SupportedAi | undefined {
|
|
104
|
-
if (!ai) return undefined;
|
|
105
|
-
const normalized = AI_ALIASES[ai.trim().toLowerCase()];
|
|
106
|
-
if (!normalized) {
|
|
107
|
-
throw new UserError("Unsupported --ai value. Use: codex | gemini-cli | claude-cli");
|
|
108
|
-
}
|
|
109
|
-
return normalized;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async function loadCommandTemplateNames(commandTemplatesDir: string): Promise<string[]> {
|
|
113
|
-
if (!(await fileExists(commandTemplatesDir))) {
|
|
114
|
-
throw new UserError(`Missing command templates directory: ${commandTemplatesDir}`);
|
|
115
|
-
}
|
|
116
|
-
const entries = await fs.readdir(commandTemplatesDir, { withFileTypes: true });
|
|
117
|
-
return entries
|
|
118
|
-
.filter((entry) => entry.isFile())
|
|
119
|
-
.map((entry) => entry.name)
|
|
120
|
-
.filter((name) => name.endsWith(".md") && name.startsWith("prodo-"))
|
|
121
|
-
.map((name) => name.replace(/\.md$/, ""))
|
|
122
|
-
.sort();
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export async function installAgentCommands(projectRoot: string, ai: SupportedAi): Promise<string[]> {
|
|
126
|
-
const cfg = AGENT_CONFIG[ai];
|
|
127
|
-
const target = path.join(projectRoot, cfg.baseDir);
|
|
128
|
-
const commandTemplatesDir = path.join(projectRoot, ".prodo", "commands");
|
|
129
|
-
const commandNames = await loadCommandTemplateNames(commandTemplatesDir);
|
|
130
|
-
await ensureDir(target);
|
|
131
|
-
|
|
132
|
-
const written: string[] = [];
|
|
133
|
-
for (const commandName of commandNames) {
|
|
134
|
-
const templatePath = path.join(commandTemplatesDir, `${commandName}.md`);
|
|
135
|
-
if (!(await fileExists(templatePath))) {
|
|
136
|
-
throw new UserError(`Missing command template: ${templatePath}`);
|
|
137
|
-
}
|
|
138
|
-
const raw = await fs.readFile(templatePath, "utf8");
|
|
139
|
-
const parsed = parseFrontmatter(raw);
|
|
140
|
-
|
|
141
|
-
if (cfg.format === "skill") {
|
|
142
|
-
const skillDir = path.join(target, commandName);
|
|
143
|
-
await ensureDir(skillDir);
|
|
144
|
-
const outPath = path.join(skillDir, "SKILL.md");
|
|
145
|
-
const content = toSkill(commandName, parsed.body.replaceAll("$ARGUMENTS", cfg.argsPlaceholder), parsed.frontmatter);
|
|
146
|
-
await fs.writeFile(outPath, content, "utf8");
|
|
147
|
-
written.push(outPath);
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (cfg.format === "toml") {
|
|
152
|
-
const outPath = path.join(target, `${commandName}${cfg.extension}`);
|
|
153
|
-
await fs.writeFile(outPath, toTomlPrompt(parsed.body, parsed.frontmatter, cfg.argsPlaceholder), "utf8");
|
|
154
|
-
written.push(outPath);
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const outPath = path.join(target, `${commandName}${cfg.extension}`);
|
|
159
|
-
const replacedBody = parsed.body.replaceAll("$ARGUMENTS", cfg.argsPlaceholder);
|
|
160
|
-
await fs.writeFile(outPath, `${renderFrontmatter(sanitizeFrontmatter(parsed.frontmatter))}\n${replacedBody}`, "utf8");
|
|
161
|
-
written.push(outPath);
|
|
162
|
-
}
|
|
163
|
-
return written;
|
|
164
|
-
}
|
|
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 { ensureDir, fileExists } from "../core/utils";
|
|
6
|
+
|
|
7
|
+
export const AI_ALIASES: Record<string, "codex" | "gemini-cli" | "claude-cli"> = {
|
|
8
|
+
codex: "codex",
|
|
9
|
+
gemini: "gemini-cli",
|
|
10
|
+
"gemmini-cli": "gemini-cli",
|
|
11
|
+
"gemmini": "gemini-cli",
|
|
12
|
+
"gemini-cli": "gemini-cli",
|
|
13
|
+
claude: "claude-cli",
|
|
14
|
+
"claude-cli": "claude-cli"
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type SupportedAi = "codex" | "gemini-cli" | "claude-cli";
|
|
18
|
+
|
|
19
|
+
type AgentConfig = {
|
|
20
|
+
baseDir: string;
|
|
21
|
+
format: "markdown" | "toml" | "skill";
|
|
22
|
+
extension: string;
|
|
23
|
+
argsPlaceholder: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const AGENT_CONFIG: Record<SupportedAi, AgentConfig> = {
|
|
27
|
+
"claude-cli": {
|
|
28
|
+
baseDir: ".claude/commands",
|
|
29
|
+
format: "markdown",
|
|
30
|
+
extension: ".md",
|
|
31
|
+
argsPlaceholder: "$ARGUMENTS"
|
|
32
|
+
},
|
|
33
|
+
"gemini-cli": {
|
|
34
|
+
baseDir: ".gemini/commands",
|
|
35
|
+
format: "toml",
|
|
36
|
+
extension: ".toml",
|
|
37
|
+
argsPlaceholder: "{{args}}"
|
|
38
|
+
},
|
|
39
|
+
codex: {
|
|
40
|
+
baseDir: ".agents/skills",
|
|
41
|
+
format: "skill",
|
|
42
|
+
extension: "/SKILL.md",
|
|
43
|
+
argsPlaceholder: "$ARGUMENTS"
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
type ParsedTemplate = {
|
|
48
|
+
frontmatter: Record<string, unknown>;
|
|
49
|
+
body: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function parseFrontmatter(content: string): ParsedTemplate {
|
|
53
|
+
if (!content.startsWith("---")) return { frontmatter: {}, body: content };
|
|
54
|
+
const end = content.indexOf("\n---", 4);
|
|
55
|
+
if (end === -1) return { frontmatter: {}, body: content };
|
|
56
|
+
const fmRaw = content.slice(3, end).trim();
|
|
57
|
+
const body = content.slice(end + 4).trimStart();
|
|
58
|
+
const frontmatter = (yaml.load(fmRaw) as Record<string, unknown>) ?? {};
|
|
59
|
+
return { frontmatter, body };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function renderFrontmatter(frontmatter: Record<string, unknown>): string {
|
|
63
|
+
if (Object.keys(frontmatter).length === 0) return "";
|
|
64
|
+
return `---\n${yaml.dump(frontmatter)}---\n`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function sanitizeFrontmatter(frontmatter: Record<string, unknown>): Record<string, unknown> {
|
|
68
|
+
const out = { ...frontmatter };
|
|
69
|
+
delete out.run;
|
|
70
|
+
delete out.scripts;
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function toTomlPrompt(body: string, frontmatter: Record<string, unknown>, argsPlaceholder: string): string {
|
|
75
|
+
const description = String(frontmatter.description ?? "Prodo command");
|
|
76
|
+
const promptBody = body.replaceAll("$ARGUMENTS", argsPlaceholder);
|
|
77
|
+
return `description = "${description.replace(/"/g, '\\"')}"
|
|
78
|
+
|
|
79
|
+
prompt = """
|
|
80
|
+
Important execution rule:
|
|
81
|
+
- This is an agent slash command, not a shell command.
|
|
82
|
+
- Do NOT run \`prodo-normalize\`, \`prodo-prd\`, or \`prodo ...\` in shell.
|
|
83
|
+
- Execute the workflow directly using workspace files.
|
|
84
|
+
|
|
85
|
+
${promptBody}
|
|
86
|
+
"""`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function toSkill(name: string, body: string, frontmatter: Record<string, unknown>): string {
|
|
90
|
+
const description = String(frontmatter.description ?? "Prodo workflow command");
|
|
91
|
+
return `---
|
|
92
|
+
name: ${name}
|
|
93
|
+
description: ${description}
|
|
94
|
+
compatibility: Requires Prodo project scaffold (.prodo)
|
|
95
|
+
metadata:
|
|
96
|
+
author: prodo
|
|
97
|
+
source: .prodo/commands/${name}.md
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
${body}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function resolveAi(ai?: string): SupportedAi | undefined {
|
|
104
|
+
if (!ai) return undefined;
|
|
105
|
+
const normalized = AI_ALIASES[ai.trim().toLowerCase()];
|
|
106
|
+
if (!normalized) {
|
|
107
|
+
throw new UserError("Unsupported --ai value. Use: codex | gemini-cli | claude-cli");
|
|
108
|
+
}
|
|
109
|
+
return normalized;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function loadCommandTemplateNames(commandTemplatesDir: string): Promise<string[]> {
|
|
113
|
+
if (!(await fileExists(commandTemplatesDir))) {
|
|
114
|
+
throw new UserError(`Missing command templates directory: ${commandTemplatesDir}`);
|
|
115
|
+
}
|
|
116
|
+
const entries = await fs.readdir(commandTemplatesDir, { withFileTypes: true });
|
|
117
|
+
return entries
|
|
118
|
+
.filter((entry) => entry.isFile())
|
|
119
|
+
.map((entry) => entry.name)
|
|
120
|
+
.filter((name) => name.endsWith(".md") && name.startsWith("prodo-"))
|
|
121
|
+
.map((name) => name.replace(/\.md$/, ""))
|
|
122
|
+
.sort();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function installAgentCommands(projectRoot: string, ai: SupportedAi): Promise<string[]> {
|
|
126
|
+
const cfg = AGENT_CONFIG[ai];
|
|
127
|
+
const target = path.join(projectRoot, cfg.baseDir);
|
|
128
|
+
const commandTemplatesDir = path.join(projectRoot, ".prodo", "commands");
|
|
129
|
+
const commandNames = await loadCommandTemplateNames(commandTemplatesDir);
|
|
130
|
+
await ensureDir(target);
|
|
131
|
+
|
|
132
|
+
const written: string[] = [];
|
|
133
|
+
for (const commandName of commandNames) {
|
|
134
|
+
const templatePath = path.join(commandTemplatesDir, `${commandName}.md`);
|
|
135
|
+
if (!(await fileExists(templatePath))) {
|
|
136
|
+
throw new UserError(`Missing command template: ${templatePath}`);
|
|
137
|
+
}
|
|
138
|
+
const raw = await fs.readFile(templatePath, "utf8");
|
|
139
|
+
const parsed = parseFrontmatter(raw);
|
|
140
|
+
|
|
141
|
+
if (cfg.format === "skill") {
|
|
142
|
+
const skillDir = path.join(target, commandName);
|
|
143
|
+
await ensureDir(skillDir);
|
|
144
|
+
const outPath = path.join(skillDir, "SKILL.md");
|
|
145
|
+
const content = toSkill(commandName, parsed.body.replaceAll("$ARGUMENTS", cfg.argsPlaceholder), parsed.frontmatter);
|
|
146
|
+
await fs.writeFile(outPath, content, "utf8");
|
|
147
|
+
written.push(outPath);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (cfg.format === "toml") {
|
|
152
|
+
const outPath = path.join(target, `${commandName}${cfg.extension}`);
|
|
153
|
+
await fs.writeFile(outPath, toTomlPrompt(parsed.body, parsed.frontmatter, cfg.argsPlaceholder), "utf8");
|
|
154
|
+
written.push(outPath);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const outPath = path.join(target, `${commandName}${cfg.extension}`);
|
|
159
|
+
const replacedBody = parsed.body.replaceAll("$ARGUMENTS", cfg.argsPlaceholder);
|
|
160
|
+
await fs.writeFile(outPath, `${renderFrontmatter(sanitizeFrontmatter(parsed.frontmatter))}\n${replacedBody}`, "utf8");
|
|
161
|
+
written.push(outPath);
|
|
162
|
+
}
|
|
163
|
+
return written;
|
|
164
|
+
}
|