@shahmarasy/prodo 0.1.4 → 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.
- 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/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 +467 -0
- package/dist/cli/init-tui.d.ts +23 -0
- package/dist/cli/init-tui.js +183 -0
- package/dist/cli/init.d.ts +12 -0
- package/dist/cli/init.js +335 -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/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 +8 -0
- package/dist/core/settings.js +43 -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/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 +12 -11
- package/dist/providers/openai-provider.d.ts +1 -1
- package/dist/providers/openai-provider.js +13 -13
- package/dist/skill-engine/context.d.ts +7 -0
- package/dist/skill-engine/context.js +76 -0
- package/dist/skill-engine/discovery.d.ts +2 -0
- package/dist/skill-engine/discovery.js +52 -0
- package/dist/skill-engine/graph.d.ts +4 -0
- package/dist/skill-engine/graph.js +114 -0
- package/dist/skill-engine/index.d.ts +11 -0
- package/dist/skill-engine/index.js +49 -0
- package/dist/skill-engine/pipeline.d.ts +9 -0
- package/dist/skill-engine/pipeline.js +84 -0
- package/dist/skill-engine/registry.d.ts +12 -0
- package/dist/skill-engine/registry.js +74 -0
- package/dist/skill-engine/types.d.ts +66 -0
- package/dist/skill-engine/types.js +2 -0
- package/dist/skill-engine/validator.d.ts +4 -0
- package/dist/skill-engine/validator.js +90 -0
- 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/fix.d.ts +2 -0
- package/dist/skills/fix.js +41 -0
- package/dist/skills/generate-artifact-skill.d.ts +2 -0
- package/dist/skills/generate-artifact-skill.js +32 -0
- package/dist/skills/generate-artifact.d.ts +2 -0
- package/dist/skills/generate-artifact.js +42 -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/normalize.d.ts +2 -0
- package/dist/skills/normalize.js +29 -0
- package/dist/skills/register-core.d.ts +2 -0
- package/dist/skills/register-core.js +21 -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/skills/validate.d.ts +2 -0
- package/dist/skills/validate.js +37 -0
- package/package.json +72 -45
- 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 -58
- package/src/{doctor.ts → cli/doctor.ts} +157 -137
- package/src/cli/fix-tui.ts +111 -0
- package/src/{cli.ts → cli/index.ts} +463 -410
- package/src/{init-tui.ts → cli/init-tui.ts} +49 -37
- package/src/{init.ts → cli/init.ts} +399 -398
- 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 -1072
- 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/{normalize.ts → core/normalize.ts} +145 -137
- 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} +8 -2
- 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 -452
- 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 -64
- package/src/{validate.ts → core/validate.ts} +252 -246
- package/src/{validator.ts → core/validator.ts} +92 -92
- package/src/{version.ts → core/version.ts} +24 -24
- package/src/{workflow-commands.ts → core/workflow-commands.ts} +32 -32
- 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 -88
- package/src/skill-engine/context.ts +90 -0
- package/src/skill-engine/discovery.ts +57 -0
- package/src/skill-engine/graph.ts +136 -0
- package/src/skill-engine/index.ts +55 -0
- package/src/skill-engine/pipeline.ts +112 -0
- package/src/skill-engine/registry.ts +75 -0
- package/src/skill-engine/types.ts +81 -0
- package/src/skill-engine/validator.ts +135 -0
- package/src/skills/fix.ts +45 -0
- package/src/skills/generate-artifact.ts +48 -0
- package/src/skills/normalize.ts +32 -0
- package/src/skills/register-core.ts +27 -0
- package/src/skills/validate.ts +40 -0
|
@@ -1,72 +1,76 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { defaultOutputDir, PRODO_DIR } from "./constants";
|
|
3
|
-
import type { ArtifactType } from "./types";
|
|
4
|
-
|
|
5
|
-
export function prodoPath(cwd: string): string {
|
|
6
|
-
return path.join(cwd, PRODO_DIR);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function briefPath(cwd: string): string {
|
|
10
|
-
return path.join(cwd, "brief.md");
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function normalizedBriefPath(cwd: string): string {
|
|
14
|
-
return path.join(prodoPath(cwd), "briefs", "normalized-brief.json");
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function settingsPath(cwd: string): string {
|
|
18
|
-
return path.join(prodoPath(cwd), "settings.json");
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function registryPath(cwd: string): string {
|
|
22
|
-
return path.join(prodoPath(cwd), "registry.json");
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function promptPath(cwd: string, artifactType: ArtifactType): string {
|
|
26
|
-
return path.join(prodoPath(cwd), "prompts", `${artifactType}.md`);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function templatePath(cwd: string, artifactType: ArtifactType): string {
|
|
30
|
-
return path.join(prodoPath(cwd), "templates", `${artifactType}.md`);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function overrideTemplatePath(cwd: string, artifactType: ArtifactType): string {
|
|
34
|
-
return path.join(prodoPath(cwd), "templates", "overrides", `${artifactType}.md`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function templateExtensionsForArtifact(artifactType: ArtifactType): string[] {
|
|
38
|
-
if (artifactType === "workflow") return ["md", "mmd"];
|
|
39
|
-
if (artifactType === "wireframe") return ["md", "html"];
|
|
40
|
-
return ["md"];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function templateCandidatePaths(cwd: string, artifactType: ArtifactType): string[] {
|
|
44
|
-
const root = path.join(prodoPath(cwd), "templates");
|
|
45
|
-
return templateExtensionsForArtifact(artifactType).map((ext) => path.join(root, `${artifactType}.${ext}`));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function overrideTemplateCandidatePaths(cwd: string, artifactType: ArtifactType): string[] {
|
|
49
|
-
const root = path.join(prodoPath(cwd), "templates", "overrides");
|
|
50
|
-
return templateExtensionsForArtifact(artifactType).map((ext) => path.join(root, `${artifactType}.${ext}`));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function schemaPath(cwd: string, artifactType: ArtifactType): string {
|
|
54
|
-
return path.join(prodoPath(cwd), "schemas", `${artifactType}.yaml`);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function outputDirPath(cwd: string, artifactType: ArtifactType, outputDirOverride?: string): string {
|
|
58
|
-
return path.join(cwd, "product-docs", outputDirOverride ?? defaultOutputDir(artifactType));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function reportPath(cwd: string): string {
|
|
62
|
-
return path.join(cwd, "product-docs", "reports", "latest-validation.md");
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function outputIndexPath(cwd: string): string {
|
|
66
|
-
return path.join(prodoPath(cwd), "state", "index.json");
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function outputContextDirPath(cwd: string): string {
|
|
70
|
-
return path.join(prodoPath(cwd), "state", "context");
|
|
71
|
-
}
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { defaultOutputDir, PRODO_DIR } from "./constants";
|
|
3
|
+
import type { ArtifactType } from "./types";
|
|
4
|
+
|
|
5
|
+
export function prodoPath(cwd: string): string {
|
|
6
|
+
return path.join(cwd, PRODO_DIR);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function briefPath(cwd: string): string {
|
|
10
|
+
return path.join(cwd, "brief.md");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function normalizedBriefPath(cwd: string): string {
|
|
14
|
+
return path.join(prodoPath(cwd), "briefs", "normalized-brief.json");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function settingsPath(cwd: string): string {
|
|
18
|
+
return path.join(prodoPath(cwd), "settings.json");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function registryPath(cwd: string): string {
|
|
22
|
+
return path.join(prodoPath(cwd), "registry.json");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function promptPath(cwd: string, artifactType: ArtifactType): string {
|
|
26
|
+
return path.join(prodoPath(cwd), "prompts", `${artifactType}.md`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function templatePath(cwd: string, artifactType: ArtifactType): string {
|
|
30
|
+
return path.join(prodoPath(cwd), "templates", `${artifactType}.md`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function overrideTemplatePath(cwd: string, artifactType: ArtifactType): string {
|
|
34
|
+
return path.join(prodoPath(cwd), "templates", "overrides", `${artifactType}.md`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function templateExtensionsForArtifact(artifactType: ArtifactType): string[] {
|
|
38
|
+
if (artifactType === "workflow") return ["md", "mmd"];
|
|
39
|
+
if (artifactType === "wireframe") return ["md", "html"];
|
|
40
|
+
return ["md"];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function templateCandidatePaths(cwd: string, artifactType: ArtifactType): string[] {
|
|
44
|
+
const root = path.join(prodoPath(cwd), "templates");
|
|
45
|
+
return templateExtensionsForArtifact(artifactType).map((ext) => path.join(root, `${artifactType}.${ext}`));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function overrideTemplateCandidatePaths(cwd: string, artifactType: ArtifactType): string[] {
|
|
49
|
+
const root = path.join(prodoPath(cwd), "templates", "overrides");
|
|
50
|
+
return templateExtensionsForArtifact(artifactType).map((ext) => path.join(root, `${artifactType}.${ext}`));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function schemaPath(cwd: string, artifactType: ArtifactType): string {
|
|
54
|
+
return path.join(prodoPath(cwd), "schemas", `${artifactType}.yaml`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function outputDirPath(cwd: string, artifactType: ArtifactType, outputDirOverride?: string): string {
|
|
58
|
+
return path.join(cwd, "product-docs", outputDirOverride ?? defaultOutputDir(artifactType));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function reportPath(cwd: string): string {
|
|
62
|
+
return path.join(cwd, "product-docs", "reports", "latest-validation.md");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function outputIndexPath(cwd: string): string {
|
|
66
|
+
return path.join(prodoPath(cwd), "state", "index.json");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function outputContextDirPath(cwd: string): string {
|
|
70
|
+
return path.join(prodoPath(cwd), "state", "context");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function normHistoryPath(cwd: string): string {
|
|
74
|
+
return path.join(prodoPath(cwd), "_norm_history.json");
|
|
75
|
+
}
|
|
72
76
|
|
|
@@ -1,78 +1,78 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import type { ContractCoverage } from "./types";
|
|
4
|
-
import { UserError } from "./errors";
|
|
5
|
-
import { fileExists } from "./utils";
|
|
6
|
-
|
|
7
|
-
export type ArtifactConfig = {
|
|
8
|
-
name: string;
|
|
9
|
-
output_dir?: string;
|
|
10
|
-
required_headings?: string[];
|
|
11
|
-
upstream?: string[];
|
|
12
|
-
required_contracts?: Array<keyof ContractCoverage>;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export type ProdoProjectConfig = {
|
|
16
|
-
presets?: string[];
|
|
17
|
-
artifacts?: ArtifactConfig[];
|
|
18
|
-
command_packs?: string[];
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
function sanitizeStringArray(value: unknown): string[] {
|
|
22
|
-
if (!Array.isArray(value)) return [];
|
|
23
|
-
return value
|
|
24
|
-
.filter((item): item is string => typeof item === "string")
|
|
25
|
-
.map((item) => item.trim())
|
|
26
|
-
.filter((item) => item.length > 0);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function sanitizeArtifact(raw: unknown): ArtifactConfig | null {
|
|
30
|
-
if (!raw || typeof raw !== "object") return null;
|
|
31
|
-
const rec = raw as Record<string, unknown>;
|
|
32
|
-
const name = typeof rec.name === "string" ? rec.name.trim() : "";
|
|
33
|
-
if (!name) return null;
|
|
34
|
-
const outputDir = typeof rec.output_dir === "string" ? rec.output_dir.trim() : undefined;
|
|
35
|
-
const requiredHeadings = sanitizeStringArray(rec.required_headings);
|
|
36
|
-
const upstream = sanitizeStringArray(rec.upstream);
|
|
37
|
-
const requiredContracts = sanitizeStringArray(rec.required_contracts)
|
|
38
|
-
.filter((value): value is keyof ContractCoverage =>
|
|
39
|
-
value === "goals" || value === "core_features" || value === "constraints");
|
|
40
|
-
return {
|
|
41
|
-
name,
|
|
42
|
-
...(outputDir ? { output_dir: outputDir } : {}),
|
|
43
|
-
...(requiredHeadings.length > 0 ? { required_headings: requiredHeadings } : {}),
|
|
44
|
-
...(upstream.length > 0 ? { upstream } : {}),
|
|
45
|
-
...(requiredContracts.length > 0 ? { required_contracts: requiredContracts } : {})
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function sanitizeConfig(raw: unknown): ProdoProjectConfig {
|
|
50
|
-
if (!raw || typeof raw !== "object") return {};
|
|
51
|
-
const rec = raw as Record<string, unknown>;
|
|
52
|
-
const artifacts = Array.isArray(rec.artifacts)
|
|
53
|
-
? rec.artifacts.map(sanitizeArtifact).filter((item): item is ArtifactConfig => item !== null)
|
|
54
|
-
: [];
|
|
55
|
-
return {
|
|
56
|
-
presets: sanitizeStringArray(rec.presets),
|
|
57
|
-
command_packs: sanitizeStringArray(rec.command_packs),
|
|
58
|
-
...(artifacts.length > 0 ? { artifacts } : {})
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export async function readProjectConfig(cwd: string): Promise<ProdoProjectConfig> {
|
|
63
|
-
const candidates = [
|
|
64
|
-
path.join(cwd, ".prodo", "config.json"),
|
|
65
|
-
path.join(cwd, "prodo.config.json")
|
|
66
|
-
];
|
|
67
|
-
for (const candidate of candidates) {
|
|
68
|
-
if (!(await fileExists(candidate))) continue;
|
|
69
|
-
try {
|
|
70
|
-
const parsed = JSON.parse(await fs.readFile(candidate, "utf8")) as unknown;
|
|
71
|
-
return sanitizeConfig(parsed);
|
|
72
|
-
} catch {
|
|
73
|
-
throw new UserError(`Invalid project config JSON: ${candidate}`);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
return {};
|
|
77
|
-
}
|
|
78
|
-
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { ContractCoverage } from "./types";
|
|
4
|
+
import { UserError } from "./errors";
|
|
5
|
+
import { fileExists } from "./utils";
|
|
6
|
+
|
|
7
|
+
export type ArtifactConfig = {
|
|
8
|
+
name: string;
|
|
9
|
+
output_dir?: string;
|
|
10
|
+
required_headings?: string[];
|
|
11
|
+
upstream?: string[];
|
|
12
|
+
required_contracts?: Array<keyof ContractCoverage>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type ProdoProjectConfig = {
|
|
16
|
+
presets?: string[];
|
|
17
|
+
artifacts?: ArtifactConfig[];
|
|
18
|
+
command_packs?: string[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function sanitizeStringArray(value: unknown): string[] {
|
|
22
|
+
if (!Array.isArray(value)) return [];
|
|
23
|
+
return value
|
|
24
|
+
.filter((item): item is string => typeof item === "string")
|
|
25
|
+
.map((item) => item.trim())
|
|
26
|
+
.filter((item) => item.length > 0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function sanitizeArtifact(raw: unknown): ArtifactConfig | null {
|
|
30
|
+
if (!raw || typeof raw !== "object") return null;
|
|
31
|
+
const rec = raw as Record<string, unknown>;
|
|
32
|
+
const name = typeof rec.name === "string" ? rec.name.trim() : "";
|
|
33
|
+
if (!name) return null;
|
|
34
|
+
const outputDir = typeof rec.output_dir === "string" ? rec.output_dir.trim() : undefined;
|
|
35
|
+
const requiredHeadings = sanitizeStringArray(rec.required_headings);
|
|
36
|
+
const upstream = sanitizeStringArray(rec.upstream);
|
|
37
|
+
const requiredContracts = sanitizeStringArray(rec.required_contracts)
|
|
38
|
+
.filter((value): value is keyof ContractCoverage =>
|
|
39
|
+
value === "goals" || value === "core_features" || value === "constraints");
|
|
40
|
+
return {
|
|
41
|
+
name,
|
|
42
|
+
...(outputDir ? { output_dir: outputDir } : {}),
|
|
43
|
+
...(requiredHeadings.length > 0 ? { required_headings: requiredHeadings } : {}),
|
|
44
|
+
...(upstream.length > 0 ? { upstream } : {}),
|
|
45
|
+
...(requiredContracts.length > 0 ? { required_contracts: requiredContracts } : {})
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function sanitizeConfig(raw: unknown): ProdoProjectConfig {
|
|
50
|
+
if (!raw || typeof raw !== "object") return {};
|
|
51
|
+
const rec = raw as Record<string, unknown>;
|
|
52
|
+
const artifacts = Array.isArray(rec.artifacts)
|
|
53
|
+
? rec.artifacts.map(sanitizeArtifact).filter((item): item is ArtifactConfig => item !== null)
|
|
54
|
+
: [];
|
|
55
|
+
return {
|
|
56
|
+
presets: sanitizeStringArray(rec.presets),
|
|
57
|
+
command_packs: sanitizeStringArray(rec.command_packs),
|
|
58
|
+
...(artifacts.length > 0 ? { artifacts } : {})
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function readProjectConfig(cwd: string): Promise<ProdoProjectConfig> {
|
|
63
|
+
const candidates = [
|
|
64
|
+
path.join(cwd, ".prodo", "config.json"),
|
|
65
|
+
path.join(cwd, "prodo.config.json")
|
|
66
|
+
];
|
|
67
|
+
for (const candidate of candidates) {
|
|
68
|
+
if (!(await fileExists(candidate))) continue;
|
|
69
|
+
try {
|
|
70
|
+
const parsed = JSON.parse(await fs.readFile(candidate, "utf8")) as unknown;
|
|
71
|
+
return sanitizeConfig(parsed);
|
|
72
|
+
} catch {
|
|
73
|
+
throw new UserError(`Invalid project config JSON: ${candidate}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return {};
|
|
77
|
+
}
|
|
78
|
+
|
|
@@ -1,119 +1,119 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
4
|
-
import { registryPath } from "./paths";
|
|
5
|
-
import { ensureDir, fileExists } from "./utils";
|
|
6
|
-
|
|
7
|
-
export type OverrideRegistryEntry = {
|
|
8
|
-
artifact_type: string;
|
|
9
|
-
file: string;
|
|
10
|
-
sha256: string;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export type ProdoRegistry = {
|
|
14
|
-
schema_version: "1.0";
|
|
15
|
-
updated_at: string;
|
|
16
|
-
installed_presets: string[];
|
|
17
|
-
installed_overrides: OverrideRegistryEntry[];
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const EMPTY_REGISTRY: ProdoRegistry = {
|
|
21
|
-
schema_version: "1.0",
|
|
22
|
-
updated_at: new Date(0).toISOString(),
|
|
23
|
-
installed_presets: [],
|
|
24
|
-
installed_overrides: []
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
async function sha256(filePath: string): Promise<string> {
|
|
28
|
-
const raw = await fs.readFile(filePath);
|
|
29
|
-
return createHash("sha256").update(raw).digest("hex");
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function sanitizeRegistry(input: unknown): ProdoRegistry {
|
|
33
|
-
if (!input || typeof input !== "object") return { ...EMPTY_REGISTRY };
|
|
34
|
-
const raw = input as Record<string, unknown>;
|
|
35
|
-
const installedPresets = Array.isArray(raw.installed_presets)
|
|
36
|
-
? raw.installed_presets
|
|
37
|
-
.filter((value): value is string => typeof value === "string")
|
|
38
|
-
.map((value) => value.trim())
|
|
39
|
-
.filter((value) => value.length > 0)
|
|
40
|
-
: [];
|
|
41
|
-
const installedOverrides = Array.isArray(raw.installed_overrides)
|
|
42
|
-
? raw.installed_overrides
|
|
43
|
-
.filter((item): item is Record<string, unknown> => !!item && typeof item === "object")
|
|
44
|
-
.map((item) => ({
|
|
45
|
-
artifact_type: typeof item.artifact_type === "string" ? item.artifact_type.trim() : "",
|
|
46
|
-
file: typeof item.file === "string" ? item.file.trim() : "",
|
|
47
|
-
sha256: typeof item.sha256 === "string" ? item.sha256.trim() : ""
|
|
48
|
-
}))
|
|
49
|
-
.filter((item) => item.artifact_type && item.file && item.sha256)
|
|
50
|
-
: [];
|
|
51
|
-
return {
|
|
52
|
-
schema_version: "1.0",
|
|
53
|
-
updated_at: typeof raw.updated_at === "string" && raw.updated_at.trim() ? raw.updated_at : EMPTY_REGISTRY.updated_at,
|
|
54
|
-
installed_presets: Array.from(new Set(installedPresets)),
|
|
55
|
-
installed_overrides: installedOverrides
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export async function readRegistry(cwd: string): Promise<ProdoRegistry> {
|
|
60
|
-
const file = registryPath(cwd);
|
|
61
|
-
if (!(await fileExists(file))) return { ...EMPTY_REGISTRY };
|
|
62
|
-
try {
|
|
63
|
-
const parsed = JSON.parse(await fs.readFile(file, "utf8")) as unknown;
|
|
64
|
-
return sanitizeRegistry(parsed);
|
|
65
|
-
} catch {
|
|
66
|
-
return { ...EMPTY_REGISTRY };
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async function readInstalledPresetsFromFile(cwd: string): Promise<string[]> {
|
|
71
|
-
const file = path.join(cwd, ".prodo", "presets", "installed.json");
|
|
72
|
-
if (!(await fileExists(file))) return [];
|
|
73
|
-
try {
|
|
74
|
-
const parsed = JSON.parse(await fs.readFile(file, "utf8")) as unknown;
|
|
75
|
-
if (!Array.isArray(parsed)) return [];
|
|
76
|
-
return parsed
|
|
77
|
-
.filter((value): value is string => typeof value === "string")
|
|
78
|
-
.map((value) => value.trim())
|
|
79
|
-
.filter((value) => value.length > 0);
|
|
80
|
-
} catch {
|
|
81
|
-
return [];
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async function discoverOverrides(cwd: string): Promise<OverrideRegistryEntry[]> {
|
|
86
|
-
const overridesDir = path.join(cwd, ".prodo", "templates", "overrides");
|
|
87
|
-
if (!(await fileExists(overridesDir))) return [];
|
|
88
|
-
const entries = await fs.readdir(overridesDir, { withFileTypes: true });
|
|
89
|
-
const out: OverrideRegistryEntry[] = [];
|
|
90
|
-
for (const entry of entries) {
|
|
91
|
-
if (!entry.isFile()) continue;
|
|
92
|
-
if (!entry.name.endsWith(".md")) continue;
|
|
93
|
-
const fullPath = path.join(overridesDir, entry.name);
|
|
94
|
-
out.push({
|
|
95
|
-
artifact_type: entry.name.replace(/\.md$/, ""),
|
|
96
|
-
file: fullPath,
|
|
97
|
-
sha256: await sha256(fullPath)
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
out.sort((a, b) => a.artifact_type.localeCompare(b.artifact_type));
|
|
101
|
-
return out;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export async function syncRegistry(cwd: string): Promise<ProdoRegistry> {
|
|
105
|
-
const existing = await readRegistry(cwd);
|
|
106
|
-
const discoveredPresets = await readInstalledPresetsFromFile(cwd);
|
|
107
|
-
const discoveredOverrides = await discoverOverrides(cwd);
|
|
108
|
-
const mergedPresets = Array.from(new Set([...existing.installed_presets, ...discoveredPresets])).sort();
|
|
109
|
-
const merged: ProdoRegistry = {
|
|
110
|
-
schema_version: "1.0",
|
|
111
|
-
updated_at: new Date().toISOString(),
|
|
112
|
-
installed_presets: mergedPresets,
|
|
113
|
-
installed_overrides: discoveredOverrides
|
|
114
|
-
};
|
|
115
|
-
const file = registryPath(cwd);
|
|
116
|
-
await ensureDir(path.dirname(file));
|
|
117
|
-
await fs.writeFile(file, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
|
|
118
|
-
return merged;
|
|
119
|
-
}
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { registryPath } from "./paths";
|
|
5
|
+
import { ensureDir, fileExists } from "./utils";
|
|
6
|
+
|
|
7
|
+
export type OverrideRegistryEntry = {
|
|
8
|
+
artifact_type: string;
|
|
9
|
+
file: string;
|
|
10
|
+
sha256: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type ProdoRegistry = {
|
|
14
|
+
schema_version: "1.0";
|
|
15
|
+
updated_at: string;
|
|
16
|
+
installed_presets: string[];
|
|
17
|
+
installed_overrides: OverrideRegistryEntry[];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const EMPTY_REGISTRY: ProdoRegistry = {
|
|
21
|
+
schema_version: "1.0",
|
|
22
|
+
updated_at: new Date(0).toISOString(),
|
|
23
|
+
installed_presets: [],
|
|
24
|
+
installed_overrides: []
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
async function sha256(filePath: string): Promise<string> {
|
|
28
|
+
const raw = await fs.readFile(filePath);
|
|
29
|
+
return createHash("sha256").update(raw).digest("hex");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function sanitizeRegistry(input: unknown): ProdoRegistry {
|
|
33
|
+
if (!input || typeof input !== "object") return { ...EMPTY_REGISTRY };
|
|
34
|
+
const raw = input as Record<string, unknown>;
|
|
35
|
+
const installedPresets = Array.isArray(raw.installed_presets)
|
|
36
|
+
? raw.installed_presets
|
|
37
|
+
.filter((value): value is string => typeof value === "string")
|
|
38
|
+
.map((value) => value.trim())
|
|
39
|
+
.filter((value) => value.length > 0)
|
|
40
|
+
: [];
|
|
41
|
+
const installedOverrides = Array.isArray(raw.installed_overrides)
|
|
42
|
+
? raw.installed_overrides
|
|
43
|
+
.filter((item): item is Record<string, unknown> => !!item && typeof item === "object")
|
|
44
|
+
.map((item) => ({
|
|
45
|
+
artifact_type: typeof item.artifact_type === "string" ? item.artifact_type.trim() : "",
|
|
46
|
+
file: typeof item.file === "string" ? item.file.trim() : "",
|
|
47
|
+
sha256: typeof item.sha256 === "string" ? item.sha256.trim() : ""
|
|
48
|
+
}))
|
|
49
|
+
.filter((item) => item.artifact_type && item.file && item.sha256)
|
|
50
|
+
: [];
|
|
51
|
+
return {
|
|
52
|
+
schema_version: "1.0",
|
|
53
|
+
updated_at: typeof raw.updated_at === "string" && raw.updated_at.trim() ? raw.updated_at : EMPTY_REGISTRY.updated_at,
|
|
54
|
+
installed_presets: Array.from(new Set(installedPresets)),
|
|
55
|
+
installed_overrides: installedOverrides
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function readRegistry(cwd: string): Promise<ProdoRegistry> {
|
|
60
|
+
const file = registryPath(cwd);
|
|
61
|
+
if (!(await fileExists(file))) return { ...EMPTY_REGISTRY };
|
|
62
|
+
try {
|
|
63
|
+
const parsed = JSON.parse(await fs.readFile(file, "utf8")) as unknown;
|
|
64
|
+
return sanitizeRegistry(parsed);
|
|
65
|
+
} catch {
|
|
66
|
+
return { ...EMPTY_REGISTRY };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function readInstalledPresetsFromFile(cwd: string): Promise<string[]> {
|
|
71
|
+
const file = path.join(cwd, ".prodo", "presets", "installed.json");
|
|
72
|
+
if (!(await fileExists(file))) return [];
|
|
73
|
+
try {
|
|
74
|
+
const parsed = JSON.parse(await fs.readFile(file, "utf8")) as unknown;
|
|
75
|
+
if (!Array.isArray(parsed)) return [];
|
|
76
|
+
return parsed
|
|
77
|
+
.filter((value): value is string => typeof value === "string")
|
|
78
|
+
.map((value) => value.trim())
|
|
79
|
+
.filter((value) => value.length > 0);
|
|
80
|
+
} catch {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function discoverOverrides(cwd: string): Promise<OverrideRegistryEntry[]> {
|
|
86
|
+
const overridesDir = path.join(cwd, ".prodo", "templates", "overrides");
|
|
87
|
+
if (!(await fileExists(overridesDir))) return [];
|
|
88
|
+
const entries = await fs.readdir(overridesDir, { withFileTypes: true });
|
|
89
|
+
const out: OverrideRegistryEntry[] = [];
|
|
90
|
+
for (const entry of entries) {
|
|
91
|
+
if (!entry.isFile()) continue;
|
|
92
|
+
if (!entry.name.endsWith(".md")) continue;
|
|
93
|
+
const fullPath = path.join(overridesDir, entry.name);
|
|
94
|
+
out.push({
|
|
95
|
+
artifact_type: entry.name.replace(/\.md$/, ""),
|
|
96
|
+
file: fullPath,
|
|
97
|
+
sha256: await sha256(fullPath)
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
out.sort((a, b) => a.artifact_type.localeCompare(b.artifact_type));
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function syncRegistry(cwd: string): Promise<ProdoRegistry> {
|
|
105
|
+
const existing = await readRegistry(cwd);
|
|
106
|
+
const discoveredPresets = await readInstalledPresetsFromFile(cwd);
|
|
107
|
+
const discoveredOverrides = await discoverOverrides(cwd);
|
|
108
|
+
const mergedPresets = Array.from(new Set([...existing.installed_presets, ...discoveredPresets])).sort();
|
|
109
|
+
const merged: ProdoRegistry = {
|
|
110
|
+
schema_version: "1.0",
|
|
111
|
+
updated_at: new Date().toISOString(),
|
|
112
|
+
installed_presets: mergedPresets,
|
|
113
|
+
installed_overrides: discoveredOverrides
|
|
114
|
+
};
|
|
115
|
+
const file = registryPath(cwd);
|
|
116
|
+
await ensureDir(path.dirname(file));
|
|
117
|
+
await fs.writeFile(file, `${JSON.stringify(merged, null, 2)}\n`, "utf8");
|
|
118
|
+
return merged;
|
|
119
|
+
}
|
|
@@ -6,6 +6,7 @@ export type ProdoSettings = {
|
|
|
6
6
|
lang: string;
|
|
7
7
|
ai?: string;
|
|
8
8
|
author?: string;
|
|
9
|
+
provider?: string;
|
|
9
10
|
};
|
|
10
11
|
|
|
11
12
|
const DEFAULT_SETTINGS: ProdoSettings = {
|
|
@@ -21,7 +22,8 @@ export async function readSettings(cwd: string): Promise<ProdoSettings> {
|
|
|
21
22
|
return {
|
|
22
23
|
lang: typeof parsed.lang === "string" && parsed.lang.trim() ? parsed.lang.trim() : "en",
|
|
23
24
|
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
|
+
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
|
|
25
27
|
};
|
|
26
28
|
} catch {
|
|
27
29
|
return { ...DEFAULT_SETTINGS };
|
|
@@ -30,6 +32,10 @@ export async function readSettings(cwd: string): Promise<ProdoSettings> {
|
|
|
30
32
|
|
|
31
33
|
export async function writeSettings(cwd: string, settings: ProdoSettings): Promise<string> {
|
|
32
34
|
const path = settingsPath(cwd);
|
|
33
|
-
|
|
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");
|
|
34
40
|
return path;
|
|
35
41
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import nunjucks from "nunjucks";
|
|
2
|
+
|
|
3
|
+
const env = new nunjucks.Environment(null, {
|
|
4
|
+
autoescape: false,
|
|
5
|
+
throwOnUndefined: false,
|
|
6
|
+
trimBlocks: false,
|
|
7
|
+
lstripBlocks: false
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
env.addFilter("slug", (value: string): string => {
|
|
11
|
+
return (value || "")
|
|
12
|
+
.toLowerCase()
|
|
13
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
14
|
+
.replace(/^-+|-+$/g, "") || "item";
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
env.addFilter("bold", (value: string): string => {
|
|
18
|
+
return `**${value ?? ""}**`;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
env.addFilter("dateFormat", (value: string | Date | undefined): string => {
|
|
22
|
+
if (!value) return new Date().toISOString().split("T")[0];
|
|
23
|
+
const d = typeof value === "string" ? new Date(value) : value;
|
|
24
|
+
return d.toISOString().split("T")[0];
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
env.addFilter("upper", (value: string): string => {
|
|
28
|
+
return (value ?? "").toUpperCase();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
env.addFilter("default", (value: unknown, fallback: string): string => {
|
|
32
|
+
if (value === null || value === undefined || value === "") return fallback;
|
|
33
|
+
return String(value);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export function renderTemplate(
|
|
37
|
+
content: string,
|
|
38
|
+
context: Record<string, unknown>
|
|
39
|
+
): string {
|
|
40
|
+
return env.renderString(content, context);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getTemplateEnv(): nunjucks.Environment {
|
|
44
|
+
return env;
|
|
45
|
+
}
|