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