@shahmarasy/prodo 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +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/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/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 +1 -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/package.json +74 -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} +459 -410
- package/src/{init-tui.ts → cli/init-tui.ts} +208 -208
- package/src/{init.ts → cli/init.ts} +398 -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} +35 -35
- 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/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
|
@@ -1,196 +1,196 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
4
|
-
import yaml from "js-yaml";
|
|
5
|
-
import { UserError } from "./errors";
|
|
6
|
-
import { fileExists } from "./utils";
|
|
7
|
-
|
|
8
|
-
type HookItem = {
|
|
9
|
-
command?: string;
|
|
10
|
-
optional?: boolean;
|
|
11
|
-
enabled?: boolean;
|
|
12
|
-
description?: string;
|
|
13
|
-
prompt?: string;
|
|
14
|
-
extension?: string;
|
|
15
|
-
condition?: string;
|
|
16
|
-
timeout_ms?: number;
|
|
17
|
-
retry?: number;
|
|
18
|
-
retry_delay_ms?: number;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
type HooksConfig = {
|
|
22
|
-
hooks?: Record<string, HookItem[]>;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
function hooksPath(cwd: string): string {
|
|
26
|
-
return path.join(cwd, ".prodo", "hooks.yml");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async function runShellCommand(
|
|
30
|
-
command: string,
|
|
31
|
-
cwd: string,
|
|
32
|
-
timeoutMs: number
|
|
33
|
-
): Promise<{ code: number; stdout: string; stderr: string; timedOut: boolean }> {
|
|
34
|
-
const parsed = parseCommand(command);
|
|
35
|
-
if (!parsed) {
|
|
36
|
-
return { code: 1, stdout: "", stderr: "Invalid hook command syntax.", timedOut: false };
|
|
37
|
-
}
|
|
38
|
-
return new Promise((resolve) => {
|
|
39
|
-
const child = spawn(parsed.bin, parsed.args, { cwd, shell: false, stdio: ["ignore", "pipe", "pipe"] });
|
|
40
|
-
let stdout = "";
|
|
41
|
-
let stderr = "";
|
|
42
|
-
let timedOut = false;
|
|
43
|
-
const timer = setTimeout(() => {
|
|
44
|
-
timedOut = true;
|
|
45
|
-
child.kill();
|
|
46
|
-
}, Math.max(1000, timeoutMs));
|
|
47
|
-
|
|
48
|
-
child.stdout.on("data", (chunk) => {
|
|
49
|
-
stdout += chunk.toString();
|
|
50
|
-
});
|
|
51
|
-
child.stderr.on("data", (chunk) => {
|
|
52
|
-
stderr += chunk.toString();
|
|
53
|
-
});
|
|
54
|
-
child.on("close", (code) => {
|
|
55
|
-
clearTimeout(timer);
|
|
56
|
-
resolve({ code: code ?? 1, stdout, stderr, timedOut });
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function parseCommand(command: string): { bin: string; args: string[] } | null {
|
|
62
|
-
const src = command.trim();
|
|
63
|
-
if (!src) return null;
|
|
64
|
-
const out: string[] = [];
|
|
65
|
-
let current = "";
|
|
66
|
-
let quote: '"' | "'" | null = null;
|
|
67
|
-
let escaping = false;
|
|
68
|
-
for (let i = 0; i < src.length; i += 1) {
|
|
69
|
-
const ch = src[i];
|
|
70
|
-
if (escaping) {
|
|
71
|
-
current += ch;
|
|
72
|
-
escaping = false;
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
if (ch === "\\") {
|
|
76
|
-
escaping = true;
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
if (quote) {
|
|
80
|
-
if (ch === quote) {
|
|
81
|
-
quote = null;
|
|
82
|
-
} else {
|
|
83
|
-
current += ch;
|
|
84
|
-
}
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
if (ch === "'" || ch === '"') {
|
|
88
|
-
quote = ch;
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
if (/\s/.test(ch)) {
|
|
92
|
-
if (current.length > 0) {
|
|
93
|
-
out.push(current);
|
|
94
|
-
current = "";
|
|
95
|
-
}
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
current += ch;
|
|
99
|
-
}
|
|
100
|
-
if (escaping || quote) return null;
|
|
101
|
-
if (current.length > 0) out.push(current);
|
|
102
|
-
if (out.length === 0) return null;
|
|
103
|
-
return { bin: out[0], args: out.slice(1) };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function toPositiveInt(value: unknown, fallback: number): number {
|
|
107
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
108
|
-
const normalized = Math.floor(value);
|
|
109
|
-
return normalized > 0 ? normalized : fallback;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function toNonNegativeInt(value: unknown, fallback: number): number {
|
|
113
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
114
|
-
const normalized = Math.floor(value);
|
|
115
|
-
return normalized >= 0 ? normalized : fallback;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async function sleep(ms: number): Promise<void> {
|
|
119
|
-
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
async function evaluateCondition(condition: string, cwd: string): Promise<boolean> {
|
|
123
|
-
const trimmed = condition.trim();
|
|
124
|
-
if (!trimmed) return true;
|
|
125
|
-
const result = await runShellCommand(trimmed, cwd, 10_000);
|
|
126
|
-
return !result.timedOut && result.code === 0;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
async function readHooks(cwd: string): Promise<HooksConfig | null> {
|
|
130
|
-
const file = hooksPath(cwd);
|
|
131
|
-
if (!(await fileExists(file))) return null;
|
|
132
|
-
try {
|
|
133
|
-
const raw = await fs.readFile(file, "utf8");
|
|
134
|
-
const parsed = yaml.load(raw) as HooksConfig;
|
|
135
|
-
return parsed ?? null;
|
|
136
|
-
} catch {
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export async function runHookPhase(
|
|
142
|
-
cwd: string,
|
|
143
|
-
phaseKey: string,
|
|
144
|
-
log: (message: string) => void
|
|
145
|
-
): Promise<void> {
|
|
146
|
-
const config = await readHooks(cwd);
|
|
147
|
-
const phaseHooks = config?.hooks?.[phaseKey];
|
|
148
|
-
if (!Array.isArray(phaseHooks) || phaseHooks.length === 0) return;
|
|
149
|
-
|
|
150
|
-
for (const hook of phaseHooks) {
|
|
151
|
-
if (hook?.enabled === false) continue;
|
|
152
|
-
const command = typeof hook?.command === "string" ? hook.command.trim() : "";
|
|
153
|
-
if (!command) continue;
|
|
154
|
-
if (typeof hook.condition === "string" && hook.condition.trim()) {
|
|
155
|
-
const pass = await evaluateCondition(hook.condition, cwd);
|
|
156
|
-
if (!pass) {
|
|
157
|
-
log(`[Hook:skipped:${phaseKey}] condition=false for ${command}`);
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const label = hook.extension || hook.description || command;
|
|
163
|
-
if (hook.optional) {
|
|
164
|
-
log(`[Hook:optional:${phaseKey}] ${label}`);
|
|
165
|
-
if (hook.prompt) log(` Prompt: ${hook.prompt}`);
|
|
166
|
-
log(` To run manually: ${command}`);
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const timeoutMs = toPositiveInt(hook.timeout_ms, 30_000);
|
|
171
|
-
const retries = toNonNegativeInt(hook.retry, 0);
|
|
172
|
-
const retryDelayMs = toNonNegativeInt(hook.retry_delay_ms, 500);
|
|
173
|
-
const attempts = 1 + retries;
|
|
174
|
-
|
|
175
|
-
let lastDetail = "";
|
|
176
|
-
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
177
|
-
log(`[Hook:mandatory:${phaseKey}] Running (attempt ${attempt}/${attempts}): ${command}`);
|
|
178
|
-
const result = await runShellCommand(command, cwd, timeoutMs);
|
|
179
|
-
if (!result.timedOut && result.code === 0) {
|
|
180
|
-
lastDetail = "";
|
|
181
|
-
break;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const stderr = result.stderr.trim();
|
|
185
|
-
const stdout = result.stdout.trim();
|
|
186
|
-
lastDetail = result.timedOut ? `Timed out after ${timeoutMs}ms` : stderr || stdout || "unknown error";
|
|
187
|
-
if (attempt < attempts) {
|
|
188
|
-
await sleep(retryDelayMs);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (lastDetail) {
|
|
193
|
-
throw new UserError(`Mandatory hook failed (${phaseKey}): ${command}\n${lastDetail}`);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import yaml from "js-yaml";
|
|
5
|
+
import { UserError } from "./errors";
|
|
6
|
+
import { fileExists } from "./utils";
|
|
7
|
+
|
|
8
|
+
type HookItem = {
|
|
9
|
+
command?: string;
|
|
10
|
+
optional?: boolean;
|
|
11
|
+
enabled?: boolean;
|
|
12
|
+
description?: string;
|
|
13
|
+
prompt?: string;
|
|
14
|
+
extension?: string;
|
|
15
|
+
condition?: string;
|
|
16
|
+
timeout_ms?: number;
|
|
17
|
+
retry?: number;
|
|
18
|
+
retry_delay_ms?: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type HooksConfig = {
|
|
22
|
+
hooks?: Record<string, HookItem[]>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function hooksPath(cwd: string): string {
|
|
26
|
+
return path.join(cwd, ".prodo", "hooks.yml");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function runShellCommand(
|
|
30
|
+
command: string,
|
|
31
|
+
cwd: string,
|
|
32
|
+
timeoutMs: number
|
|
33
|
+
): Promise<{ code: number; stdout: string; stderr: string; timedOut: boolean }> {
|
|
34
|
+
const parsed = parseCommand(command);
|
|
35
|
+
if (!parsed) {
|
|
36
|
+
return { code: 1, stdout: "", stderr: "Invalid hook command syntax.", timedOut: false };
|
|
37
|
+
}
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
const child = spawn(parsed.bin, parsed.args, { cwd, shell: false, stdio: ["ignore", "pipe", "pipe"] });
|
|
40
|
+
let stdout = "";
|
|
41
|
+
let stderr = "";
|
|
42
|
+
let timedOut = false;
|
|
43
|
+
const timer = setTimeout(() => {
|
|
44
|
+
timedOut = true;
|
|
45
|
+
child.kill();
|
|
46
|
+
}, Math.max(1000, timeoutMs));
|
|
47
|
+
|
|
48
|
+
child.stdout.on("data", (chunk) => {
|
|
49
|
+
stdout += chunk.toString();
|
|
50
|
+
});
|
|
51
|
+
child.stderr.on("data", (chunk) => {
|
|
52
|
+
stderr += chunk.toString();
|
|
53
|
+
});
|
|
54
|
+
child.on("close", (code) => {
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
resolve({ code: code ?? 1, stdout, stderr, timedOut });
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseCommand(command: string): { bin: string; args: string[] } | null {
|
|
62
|
+
const src = command.trim();
|
|
63
|
+
if (!src) return null;
|
|
64
|
+
const out: string[] = [];
|
|
65
|
+
let current = "";
|
|
66
|
+
let quote: '"' | "'" | null = null;
|
|
67
|
+
let escaping = false;
|
|
68
|
+
for (let i = 0; i < src.length; i += 1) {
|
|
69
|
+
const ch = src[i];
|
|
70
|
+
if (escaping) {
|
|
71
|
+
current += ch;
|
|
72
|
+
escaping = false;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (ch === "\\") {
|
|
76
|
+
escaping = true;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (quote) {
|
|
80
|
+
if (ch === quote) {
|
|
81
|
+
quote = null;
|
|
82
|
+
} else {
|
|
83
|
+
current += ch;
|
|
84
|
+
}
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (ch === "'" || ch === '"') {
|
|
88
|
+
quote = ch;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (/\s/.test(ch)) {
|
|
92
|
+
if (current.length > 0) {
|
|
93
|
+
out.push(current);
|
|
94
|
+
current = "";
|
|
95
|
+
}
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
current += ch;
|
|
99
|
+
}
|
|
100
|
+
if (escaping || quote) return null;
|
|
101
|
+
if (current.length > 0) out.push(current);
|
|
102
|
+
if (out.length === 0) return null;
|
|
103
|
+
return { bin: out[0], args: out.slice(1) };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function toPositiveInt(value: unknown, fallback: number): number {
|
|
107
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
108
|
+
const normalized = Math.floor(value);
|
|
109
|
+
return normalized > 0 ? normalized : fallback;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function toNonNegativeInt(value: unknown, fallback: number): number {
|
|
113
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
114
|
+
const normalized = Math.floor(value);
|
|
115
|
+
return normalized >= 0 ? normalized : fallback;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function sleep(ms: number): Promise<void> {
|
|
119
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function evaluateCondition(condition: string, cwd: string): Promise<boolean> {
|
|
123
|
+
const trimmed = condition.trim();
|
|
124
|
+
if (!trimmed) return true;
|
|
125
|
+
const result = await runShellCommand(trimmed, cwd, 10_000);
|
|
126
|
+
return !result.timedOut && result.code === 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function readHooks(cwd: string): Promise<HooksConfig | null> {
|
|
130
|
+
const file = hooksPath(cwd);
|
|
131
|
+
if (!(await fileExists(file))) return null;
|
|
132
|
+
try {
|
|
133
|
+
const raw = await fs.readFile(file, "utf8");
|
|
134
|
+
const parsed = yaml.load(raw) as HooksConfig;
|
|
135
|
+
return parsed ?? null;
|
|
136
|
+
} catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function runHookPhase(
|
|
142
|
+
cwd: string,
|
|
143
|
+
phaseKey: string,
|
|
144
|
+
log: (message: string) => void
|
|
145
|
+
): Promise<void> {
|
|
146
|
+
const config = await readHooks(cwd);
|
|
147
|
+
const phaseHooks = config?.hooks?.[phaseKey];
|
|
148
|
+
if (!Array.isArray(phaseHooks) || phaseHooks.length === 0) return;
|
|
149
|
+
|
|
150
|
+
for (const hook of phaseHooks) {
|
|
151
|
+
if (hook?.enabled === false) continue;
|
|
152
|
+
const command = typeof hook?.command === "string" ? hook.command.trim() : "";
|
|
153
|
+
if (!command) continue;
|
|
154
|
+
if (typeof hook.condition === "string" && hook.condition.trim()) {
|
|
155
|
+
const pass = await evaluateCondition(hook.condition, cwd);
|
|
156
|
+
if (!pass) {
|
|
157
|
+
log(`[Hook:skipped:${phaseKey}] condition=false for ${command}`);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const label = hook.extension || hook.description || command;
|
|
163
|
+
if (hook.optional) {
|
|
164
|
+
log(`[Hook:optional:${phaseKey}] ${label}`);
|
|
165
|
+
if (hook.prompt) log(` Prompt: ${hook.prompt}`);
|
|
166
|
+
log(` To run manually: ${command}`);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const timeoutMs = toPositiveInt(hook.timeout_ms, 30_000);
|
|
171
|
+
const retries = toNonNegativeInt(hook.retry, 0);
|
|
172
|
+
const retryDelayMs = toNonNegativeInt(hook.retry_delay_ms, 500);
|
|
173
|
+
const attempts = 1 + retries;
|
|
174
|
+
|
|
175
|
+
let lastDetail = "";
|
|
176
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
177
|
+
log(`[Hook:mandatory:${phaseKey}] Running (attempt ${attempt}/${attempts}): ${command}`);
|
|
178
|
+
const result = await runShellCommand(command, cwd, timeoutMs);
|
|
179
|
+
if (!result.timedOut && result.code === 0) {
|
|
180
|
+
lastDetail = "";
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const stderr = result.stderr.trim();
|
|
185
|
+
const stdout = result.stdout.trim();
|
|
186
|
+
lastDetail = result.timedOut ? `Timed out after ${timeoutMs}ms` : stderr || stdout || "unknown error";
|
|
187
|
+
if (attempt < attempts) {
|
|
188
|
+
await sleep(retryDelayMs);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (lastDetail) {
|
|
193
|
+
throw new UserError(`Mandatory hook failed (${phaseKey}): ${command}\n${lastDetail}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -1,73 +1,93 @@
|
|
|
1
|
-
export type MarkdownSection = {
|
|
2
|
-
heading: string;
|
|
3
|
-
headingKey: string;
|
|
4
|
-
level: number;
|
|
5
|
-
textLines: string[];
|
|
6
|
-
listItems: string[];
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
function normalizeText(input: string): string {
|
|
10
|
-
return input.trim().replace(/\s+/g, " ");
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function normalizeHeadingKey(heading: string): string {
|
|
14
|
-
return normalizeText(heading)
|
|
15
|
-
.toLowerCase()
|
|
16
|
-
.replace(/[^a-z0-9\s]/g, " ")
|
|
17
|
-
.replace(/\s+/g, " ")
|
|
18
|
-
.trim();
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function parseMarkdownSections(markdown: string): MarkdownSection[] {
|
|
22
|
-
const sections: MarkdownSection[] = [];
|
|
23
|
-
let current: MarkdownSection | null = null;
|
|
24
|
-
|
|
25
|
-
for (const rawLine of markdown.split(/\r?\n/)) {
|
|
26
|
-
const headingMatch = rawLine.match(/^\s*(#{1,6})\s+(.+?)\s*$/);
|
|
27
|
-
if (headingMatch) {
|
|
28
|
-
const title = normalizeText(headingMatch[2]);
|
|
29
|
-
current = {
|
|
30
|
-
heading: title,
|
|
31
|
-
headingKey: normalizeHeadingKey(title),
|
|
32
|
-
level: headingMatch[1].length,
|
|
33
|
-
textLines: [],
|
|
34
|
-
listItems: []
|
|
35
|
-
};
|
|
36
|
-
sections.push(current);
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (!current) continue;
|
|
41
|
-
const listMatch = rawLine.match(/^\s*(?:[-*+]|\d+\.)\s+(.+?)\s*$/);
|
|
42
|
-
if (listMatch) {
|
|
43
|
-
const item = normalizeText(listMatch[1]);
|
|
44
|
-
if (item.length > 0 && !current.listItems.includes(item)) current.listItems.push(item);
|
|
45
|
-
if (item.length > 0) current.textLines.push(item);
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const text = normalizeText(rawLine);
|
|
50
|
-
if (text.length > 0) current.textLines.push(text);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return sections;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function extractRequiredHeadings(content: string): string[] {
|
|
57
|
-
const sections = parseMarkdownSections(content);
|
|
58
|
-
return sections
|
|
59
|
-
.filter((section) => section.level === 2)
|
|
60
|
-
.map((section) => `## ${section.heading}`)
|
|
61
|
-
.filter((heading) => heading.length > 3);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
1
|
+
export type MarkdownSection = {
|
|
2
|
+
heading: string;
|
|
3
|
+
headingKey: string;
|
|
4
|
+
level: number;
|
|
5
|
+
textLines: string[];
|
|
6
|
+
listItems: string[];
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function normalizeText(input: string): string {
|
|
10
|
+
return input.trim().replace(/\s+/g, " ");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function normalizeHeadingKey(heading: string): string {
|
|
14
|
+
return normalizeText(heading)
|
|
15
|
+
.toLowerCase()
|
|
16
|
+
.replace(/[^a-z0-9\s]/g, " ")
|
|
17
|
+
.replace(/\s+/g, " ")
|
|
18
|
+
.trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function parseMarkdownSections(markdown: string): MarkdownSection[] {
|
|
22
|
+
const sections: MarkdownSection[] = [];
|
|
23
|
+
let current: MarkdownSection | null = null;
|
|
24
|
+
|
|
25
|
+
for (const rawLine of markdown.split(/\r?\n/)) {
|
|
26
|
+
const headingMatch = rawLine.match(/^\s*(#{1,6})\s+(.+?)\s*$/);
|
|
27
|
+
if (headingMatch) {
|
|
28
|
+
const title = normalizeText(headingMatch[2]);
|
|
29
|
+
current = {
|
|
30
|
+
heading: title,
|
|
31
|
+
headingKey: normalizeHeadingKey(title),
|
|
32
|
+
level: headingMatch[1].length,
|
|
33
|
+
textLines: [],
|
|
34
|
+
listItems: []
|
|
35
|
+
};
|
|
36
|
+
sections.push(current);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!current) continue;
|
|
41
|
+
const listMatch = rawLine.match(/^\s*(?:[-*+]|\d+\.)\s+(.+?)\s*$/);
|
|
42
|
+
if (listMatch) {
|
|
43
|
+
const item = normalizeText(listMatch[1]);
|
|
44
|
+
if (item.length > 0 && !current.listItems.includes(item)) current.listItems.push(item);
|
|
45
|
+
if (item.length > 0) current.textLines.push(item);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const text = normalizeText(rawLine);
|
|
50
|
+
if (text.length > 0) current.textLines.push(text);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return sections;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function extractRequiredHeadings(content: string): string[] {
|
|
57
|
+
const sections = parseMarkdownSections(content);
|
|
58
|
+
return sections
|
|
59
|
+
.filter((section) => section.level === 2)
|
|
60
|
+
.map((section) => `## ${section.heading}`)
|
|
61
|
+
.filter((heading) => heading.length > 3);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type TaggedLine = {
|
|
65
|
+
contractId: string;
|
|
66
|
+
line: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export function taggedLinesByContract(body: string): TaggedLine[] {
|
|
70
|
+
const lines = body
|
|
71
|
+
.split(/\r?\n/)
|
|
72
|
+
.map((line) => line.trim())
|
|
73
|
+
.filter((line) => line.length > 0);
|
|
74
|
+
const tagged: TaggedLine[] = [];
|
|
75
|
+
for (const line of lines) {
|
|
76
|
+
const matches = line.match(/\[([GFC][0-9]+)\]/g) ?? [];
|
|
77
|
+
for (const match of matches) {
|
|
78
|
+
tagged.push({ contractId: match.slice(1, -1), line });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return tagged;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function sectionTextMap(content: string): Map<string, string> {
|
|
85
|
+
const sections = parseMarkdownSections(content);
|
|
86
|
+
const mapped = new Map<string, string>();
|
|
87
|
+
for (const section of sections) {
|
|
88
|
+
const parts = [...section.listItems, ...section.textLines].filter((item) => item.length > 0);
|
|
89
|
+
mapped.set(`## ${section.heading}`, parts.join("\n").trim());
|
|
90
|
+
}
|
|
91
|
+
return mapped;
|
|
92
|
+
}
|
|
93
|
+
|