@holdpoint/cli 0.1.0-alpha.2 → 0.1.0-alpha.21
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/dist/chunk-D7ZF5JJH.js +521 -0
- package/dist/chunk-D7ZF5JJH.js.map +1 -0
- package/dist/data/verified-mcp-registry.json +14 -0
- package/dist/index.js +1588 -603
- package/dist/index.js.map +1 -1
- package/dist/init-Y7NZBMU6.js +8 -0
- package/dist/init-Y7NZBMU6.js.map +1 -0
- package/dist/lib/scan.d.ts +20 -0
- package/dist/lib/scan.js +200 -0
- package/dist/lib/scan.js.map +1 -0
- package/dist/prompt-EQ5IFADN.js +23 -0
- package/dist/prompt-EQ5IFADN.js.map +1 -0
- package/dist/templates/HOLDPOINT_PREREQUISITES.md +10 -0
- package/dist/templates/HOLDPOINT_REFERENCE.md +372 -0
- package/dist/templates/MASTER_PROMPT.md +25 -295
- package/dist/templates/default.yaml +274 -0
- package/package.json +16 -14
- package/dist/builder-ui/assets/index-BxfWKnb5.js +0 -437
- package/dist/builder-ui/assets/index-BxfWKnb5.js.map +0 -1
- package/dist/builder-ui/assets/index-DkLHZ-in.css +0 -1
- package/dist/builder-ui/favicon.svg +0 -10
- package/dist/builder-ui/index.html +0 -14
- package/dist/templates/_base.yaml +0 -52
- package/dist/templates/fullstack.yaml +0 -93
- package/dist/templates/go.yaml +0 -60
- package/dist/templates/nextjs.yaml +0 -76
- package/dist/templates/python.yaml +0 -60
- package/dist/templates/typescript.yaml +0 -55
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
interface ScanResult {
|
|
2
|
+
mcp: {
|
|
3
|
+
server: string;
|
|
4
|
+
verified: boolean;
|
|
5
|
+
}[];
|
|
6
|
+
deps: {
|
|
7
|
+
name: string;
|
|
8
|
+
severity: string;
|
|
9
|
+
title: string;
|
|
10
|
+
}[];
|
|
11
|
+
}
|
|
12
|
+
declare function extractPackageName(value: string): string | undefined;
|
|
13
|
+
declare function parseAuditJson(raw: string): ScanResult["deps"];
|
|
14
|
+
declare function runScan(root: string): Promise<ScanResult>;
|
|
15
|
+
declare const __scanInternalsForTests: {
|
|
16
|
+
parseAuditJson: typeof parseAuditJson;
|
|
17
|
+
extractPackageName: typeof extractPackageName;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export { type ScanResult, __scanInternalsForTests, runScan };
|
package/dist/lib/scan.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/lib/scan.ts
|
|
4
|
+
import { execFile } from "child_process";
|
|
5
|
+
import { existsSync, readFileSync } from "fs";
|
|
6
|
+
import { dirname, join, resolve } from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
import { promisify } from "util";
|
|
9
|
+
var execFileAsync = promisify(execFile);
|
|
10
|
+
var HIGH_SEVERITIES = /* @__PURE__ */ new Set(["high", "critical"]);
|
|
11
|
+
var AUDIT_MAX_FINDINGS = 5;
|
|
12
|
+
var AUDIT_TIMEOUT_MS = 8e3;
|
|
13
|
+
function readJson(path) {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
16
|
+
} catch {
|
|
17
|
+
return void 0;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function registryPath() {
|
|
21
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
return join(here, "../data/verified-mcp-registry.json");
|
|
23
|
+
}
|
|
24
|
+
function loadRegistry() {
|
|
25
|
+
const parsed = readJson(registryPath());
|
|
26
|
+
const entries = Array.isArray(parsed) ? parsed : [];
|
|
27
|
+
return new Set(entries.filter((entry) => typeof entry === "string"));
|
|
28
|
+
}
|
|
29
|
+
function asStringArray(value) {
|
|
30
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
31
|
+
}
|
|
32
|
+
function mcpServersFromConfig(config) {
|
|
33
|
+
if (!config || typeof config !== "object") return [];
|
|
34
|
+
const obj = config;
|
|
35
|
+
const servers = obj.mcpServers ?? obj.servers;
|
|
36
|
+
if (!servers || typeof servers !== "object" || Array.isArray(servers)) return [];
|
|
37
|
+
return Object.entries(servers).map(([key, raw]) => {
|
|
38
|
+
const server = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
39
|
+
const record = server;
|
|
40
|
+
return {
|
|
41
|
+
key,
|
|
42
|
+
...typeof record.name === "string" ? { name: record.name } : {},
|
|
43
|
+
...typeof record.command === "string" ? { command: record.command } : {},
|
|
44
|
+
args: asStringArray(record.args)
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function extractPackageName(value) {
|
|
49
|
+
const normalized = value.replace(/\\/g, "/");
|
|
50
|
+
const nodeModules = normalized.lastIndexOf("node_modules/");
|
|
51
|
+
if (nodeModules >= 0) {
|
|
52
|
+
const after = normalized.slice(nodeModules + "node_modules/".length);
|
|
53
|
+
const parts = after.split("/").filter(Boolean);
|
|
54
|
+
if (parts[0]?.startsWith("@") && parts[1]) return `${parts[0]}/${parts[1]}`;
|
|
55
|
+
return parts[0];
|
|
56
|
+
}
|
|
57
|
+
if (normalized.startsWith("@")) {
|
|
58
|
+
const [scope, name] = normalized.split("/");
|
|
59
|
+
return scope && name ? `${scope}/${name}` : void 0;
|
|
60
|
+
}
|
|
61
|
+
if (!normalized.includes("/") && /^[a-z0-9@._-]+$/i.test(normalized)) return normalized;
|
|
62
|
+
return void 0;
|
|
63
|
+
}
|
|
64
|
+
function serverCandidates(entry) {
|
|
65
|
+
const values = [entry.name, entry.command, ...entry.args].filter((value) => typeof value === "string" && value.trim().length > 0).map((value) => value.trim());
|
|
66
|
+
const packages = values.map(extractPackageName).filter((value) => Boolean(value));
|
|
67
|
+
return [...values, ...packages];
|
|
68
|
+
}
|
|
69
|
+
function readMcp(root, registry) {
|
|
70
|
+
const files = [join(root, ".mcp.json"), join(root, ".claude/mcp.json")];
|
|
71
|
+
const results = [];
|
|
72
|
+
for (const file of files) {
|
|
73
|
+
if (!existsSync(file)) continue;
|
|
74
|
+
for (const entry of mcpServersFromConfig(readJson(file))) {
|
|
75
|
+
const verified = serverCandidates(entry).some((candidate) => registry.has(candidate));
|
|
76
|
+
results.push({ server: entry.name ?? entry.key, verified });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return results;
|
|
80
|
+
}
|
|
81
|
+
function detectPackageManager(root) {
|
|
82
|
+
if (existsSync(join(root, "pnpm-lock.yaml"))) return "pnpm";
|
|
83
|
+
if (existsSync(join(root, "yarn.lock"))) return "yarn";
|
|
84
|
+
if (existsSync(join(root, "package-lock.json")) || existsSync(join(root, "npm-shrinkwrap.json"))) {
|
|
85
|
+
return "npm";
|
|
86
|
+
}
|
|
87
|
+
return void 0;
|
|
88
|
+
}
|
|
89
|
+
function firstTitle(value) {
|
|
90
|
+
if (typeof value === "string") return value;
|
|
91
|
+
if (Array.isArray(value)) {
|
|
92
|
+
for (const entry of value) {
|
|
93
|
+
const title = firstTitle(entry);
|
|
94
|
+
if (title) return title;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (value && typeof value === "object") {
|
|
98
|
+
const record = value;
|
|
99
|
+
return typeof record.title === "string" ? record.title : void 0;
|
|
100
|
+
}
|
|
101
|
+
return void 0;
|
|
102
|
+
}
|
|
103
|
+
function addFinding(findings, seen, name, severity, title) {
|
|
104
|
+
if (!name || typeof severity !== "string") return;
|
|
105
|
+
const normalizedSeverity = severity.toLowerCase();
|
|
106
|
+
if (!HIGH_SEVERITIES.has(normalizedSeverity)) return;
|
|
107
|
+
const normalizedTitle = firstTitle(title) ?? "Security advisory";
|
|
108
|
+
const key = `${name}\0${normalizedSeverity}\0${normalizedTitle}`;
|
|
109
|
+
if (seen.has(key)) return;
|
|
110
|
+
seen.add(key);
|
|
111
|
+
findings.push({ name, severity: normalizedSeverity, title: normalizedTitle });
|
|
112
|
+
}
|
|
113
|
+
function parseAuditJson(raw) {
|
|
114
|
+
const findings = [];
|
|
115
|
+
const seen = /* @__PURE__ */ new Set();
|
|
116
|
+
const parseOne = (value) => {
|
|
117
|
+
if (!value || typeof value !== "object") return;
|
|
118
|
+
const data = value;
|
|
119
|
+
if (data.vulnerabilities && typeof data.vulnerabilities === "object") {
|
|
120
|
+
for (const [name, vuln] of Object.entries(data.vulnerabilities)) {
|
|
121
|
+
if (!vuln || typeof vuln !== "object") continue;
|
|
122
|
+
const v = vuln;
|
|
123
|
+
addFinding(findings, seen, name, v.severity, v.via ?? v.title);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (data.advisories && typeof data.advisories === "object") {
|
|
127
|
+
for (const advisory of Object.values(data.advisories)) {
|
|
128
|
+
if (!advisory || typeof advisory !== "object") continue;
|
|
129
|
+
const a = advisory;
|
|
130
|
+
addFinding(
|
|
131
|
+
findings,
|
|
132
|
+
seen,
|
|
133
|
+
typeof a.module_name === "string" ? a.module_name : void 0,
|
|
134
|
+
a.severity,
|
|
135
|
+
a.title
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (data.type === "auditAdvisory" && data.data && typeof data.data === "object") {
|
|
140
|
+
const advisory = data.data.advisory;
|
|
141
|
+
if (advisory && typeof advisory === "object") {
|
|
142
|
+
const a = advisory;
|
|
143
|
+
addFinding(
|
|
144
|
+
findings,
|
|
145
|
+
seen,
|
|
146
|
+
typeof a.module_name === "string" ? a.module_name : void 0,
|
|
147
|
+
a.severity,
|
|
148
|
+
a.title
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
try {
|
|
154
|
+
parseOne(JSON.parse(raw));
|
|
155
|
+
} catch {
|
|
156
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
157
|
+
const trimmed = line.trim();
|
|
158
|
+
if (!trimmed) continue;
|
|
159
|
+
try {
|
|
160
|
+
parseOne(JSON.parse(trimmed));
|
|
161
|
+
} catch {
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return findings.slice(0, AUDIT_MAX_FINDINGS);
|
|
166
|
+
}
|
|
167
|
+
async function runAudit(root) {
|
|
168
|
+
const pm = detectPackageManager(root);
|
|
169
|
+
if (!pm) return [];
|
|
170
|
+
try {
|
|
171
|
+
const { stdout } = await execFileAsync(pm, ["audit", "--json"], {
|
|
172
|
+
cwd: root,
|
|
173
|
+
encoding: "utf8",
|
|
174
|
+
maxBuffer: 1024 * 1024 * 10,
|
|
175
|
+
timeout: AUDIT_TIMEOUT_MS
|
|
176
|
+
});
|
|
177
|
+
return parseAuditJson(String(stdout || ""));
|
|
178
|
+
} catch (error) {
|
|
179
|
+
const stdout = error.stdout;
|
|
180
|
+
if (typeof stdout !== "string" || !stdout.trim()) return [];
|
|
181
|
+
return parseAuditJson(stdout);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async function runScan(root) {
|
|
185
|
+
const resolvedRoot = resolve(root);
|
|
186
|
+
const registry = loadRegistry();
|
|
187
|
+
return {
|
|
188
|
+
mcp: readMcp(resolvedRoot, registry),
|
|
189
|
+
deps: await runAudit(resolvedRoot)
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
var __scanInternalsForTests = {
|
|
193
|
+
parseAuditJson,
|
|
194
|
+
extractPackageName
|
|
195
|
+
};
|
|
196
|
+
export {
|
|
197
|
+
__scanInternalsForTests,
|
|
198
|
+
runScan
|
|
199
|
+
};
|
|
200
|
+
//# sourceMappingURL=scan.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/lib/scan.ts"],"sourcesContent":["import { execFile } from \"node:child_process\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { promisify } from \"node:util\";\n\nconst execFileAsync = promisify(execFile);\nconst HIGH_SEVERITIES = new Set([\"high\", \"critical\"]);\nconst AUDIT_MAX_FINDINGS = 5;\nconst AUDIT_TIMEOUT_MS = 8_000;\n\nexport interface ScanResult {\n mcp: { server: string; verified: boolean }[];\n deps: { name: string; severity: string; title: string }[];\n}\n\ntype PackageManager = \"pnpm\" | \"yarn\" | \"npm\";\n\ntype McpEntry = {\n key: string;\n name?: string;\n command?: string;\n args: string[];\n};\n\nfunction readJson(path: string): unknown {\n try {\n return JSON.parse(readFileSync(path, \"utf8\"));\n } catch {\n return undefined;\n }\n}\n\nfunction registryPath(): string {\n const here = dirname(fileURLToPath(import.meta.url));\n return join(here, \"../data/verified-mcp-registry.json\");\n}\n\nfunction loadRegistry(): Set<string> {\n const parsed = readJson(registryPath());\n const entries = Array.isArray(parsed) ? parsed : [];\n return new Set(entries.filter((entry): entry is string => typeof entry === \"string\"));\n}\n\nfunction asStringArray(value: unknown): string[] {\n return Array.isArray(value)\n ? value.filter((entry): entry is string => typeof entry === \"string\")\n : [];\n}\n\nfunction mcpServersFromConfig(config: unknown): McpEntry[] {\n if (!config || typeof config !== \"object\") return [];\n const obj = config as Record<string, unknown>;\n const servers = obj.mcpServers ?? obj.servers;\n if (!servers || typeof servers !== \"object\" || Array.isArray(servers)) return [];\n\n return Object.entries(servers as Record<string, unknown>).map(([key, raw]) => {\n const server = raw && typeof raw === \"object\" && !Array.isArray(raw) ? raw : {};\n const record = server as Record<string, unknown>;\n return {\n key,\n ...(typeof record.name === \"string\" ? { name: record.name } : {}),\n ...(typeof record.command === \"string\" ? { command: record.command } : {}),\n args: asStringArray(record.args),\n };\n });\n}\n\nfunction extractPackageName(value: string): string | undefined {\n const normalized = value.replace(/\\\\/g, \"/\");\n const nodeModules = normalized.lastIndexOf(\"node_modules/\");\n if (nodeModules >= 0) {\n const after = normalized.slice(nodeModules + \"node_modules/\".length);\n const parts = after.split(\"/\").filter(Boolean);\n if (parts[0]?.startsWith(\"@\") && parts[1]) return `${parts[0]}/${parts[1]}`;\n return parts[0];\n }\n if (normalized.startsWith(\"@\")) {\n const [scope, name] = normalized.split(\"/\");\n return scope && name ? `${scope}/${name}` : undefined;\n }\n if (!normalized.includes(\"/\") && /^[a-z0-9@._-]+$/i.test(normalized)) return normalized;\n return undefined;\n}\n\nfunction serverCandidates(entry: McpEntry): string[] {\n const values = [entry.name, entry.command, ...entry.args]\n .filter((value): value is string => typeof value === \"string\" && value.trim().length > 0)\n .map((value) => value.trim());\n const packages = values\n .map(extractPackageName)\n .filter((value): value is string => Boolean(value));\n return [...values, ...packages];\n}\n\nfunction readMcp(root: string, registry: Set<string>): ScanResult[\"mcp\"] {\n const files = [join(root, \".mcp.json\"), join(root, \".claude/mcp.json\")];\n const results: ScanResult[\"mcp\"] = [];\n for (const file of files) {\n if (!existsSync(file)) continue;\n for (const entry of mcpServersFromConfig(readJson(file))) {\n const verified = serverCandidates(entry).some((candidate) => registry.has(candidate));\n results.push({ server: entry.name ?? entry.key, verified });\n }\n }\n return results;\n}\n\nfunction detectPackageManager(root: string): PackageManager | undefined {\n if (existsSync(join(root, \"pnpm-lock.yaml\"))) return \"pnpm\";\n if (existsSync(join(root, \"yarn.lock\"))) return \"yarn\";\n if (\n existsSync(join(root, \"package-lock.json\")) ||\n existsSync(join(root, \"npm-shrinkwrap.json\"))\n ) {\n return \"npm\";\n }\n return undefined;\n}\n\nfunction firstTitle(value: unknown): string | undefined {\n if (typeof value === \"string\") return value;\n if (Array.isArray(value)) {\n for (const entry of value) {\n const title = firstTitle(entry);\n if (title) return title;\n }\n }\n if (value && typeof value === \"object\") {\n const record = value as Record<string, unknown>;\n return typeof record.title === \"string\" ? record.title : undefined;\n }\n return undefined;\n}\n\nfunction addFinding(\n findings: ScanResult[\"deps\"],\n seen: Set<string>,\n name: string | undefined,\n severity: unknown,\n title: unknown,\n): void {\n if (!name || typeof severity !== \"string\") return;\n const normalizedSeverity = severity.toLowerCase();\n if (!HIGH_SEVERITIES.has(normalizedSeverity)) return;\n const normalizedTitle = firstTitle(title) ?? \"Security advisory\";\n const key = `${name}\\0${normalizedSeverity}\\0${normalizedTitle}`;\n if (seen.has(key)) return;\n seen.add(key);\n findings.push({ name, severity: normalizedSeverity, title: normalizedTitle });\n}\n\nfunction parseAuditJson(raw: string): ScanResult[\"deps\"] {\n const findings: ScanResult[\"deps\"] = [];\n const seen = new Set<string>();\n const parseOne = (value: unknown) => {\n if (!value || typeof value !== \"object\") return;\n const data = value as Record<string, unknown>;\n\n if (data.vulnerabilities && typeof data.vulnerabilities === \"object\") {\n for (const [name, vuln] of Object.entries(data.vulnerabilities as Record<string, unknown>)) {\n if (!vuln || typeof vuln !== \"object\") continue;\n const v = vuln as Record<string, unknown>;\n addFinding(findings, seen, name, v.severity, v.via ?? v.title);\n }\n }\n\n if (data.advisories && typeof data.advisories === \"object\") {\n for (const advisory of Object.values(data.advisories as Record<string, unknown>)) {\n if (!advisory || typeof advisory !== \"object\") continue;\n const a = advisory as Record<string, unknown>;\n addFinding(\n findings,\n seen,\n typeof a.module_name === \"string\" ? a.module_name : undefined,\n a.severity,\n a.title,\n );\n }\n }\n\n if (data.type === \"auditAdvisory\" && data.data && typeof data.data === \"object\") {\n const advisory = (data.data as Record<string, unknown>).advisory;\n if (advisory && typeof advisory === \"object\") {\n const a = advisory as Record<string, unknown>;\n addFinding(\n findings,\n seen,\n typeof a.module_name === \"string\" ? a.module_name : undefined,\n a.severity,\n a.title,\n );\n }\n }\n };\n\n try {\n parseOne(JSON.parse(raw));\n } catch {\n for (const line of raw.split(/\\r?\\n/)) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n try {\n parseOne(JSON.parse(trimmed));\n } catch {\n // Ignore non-JSON audit chatter.\n }\n }\n }\n return findings.slice(0, AUDIT_MAX_FINDINGS);\n}\n\nasync function runAudit(root: string): Promise<ScanResult[\"deps\"]> {\n const pm = detectPackageManager(root);\n if (!pm) return [];\n try {\n const { stdout } = await execFileAsync(pm, [\"audit\", \"--json\"], {\n cwd: root,\n encoding: \"utf8\",\n maxBuffer: 1024 * 1024 * 10,\n timeout: AUDIT_TIMEOUT_MS,\n });\n return parseAuditJson(String(stdout || \"\"));\n } catch (error) {\n const stdout = (error as { stdout?: unknown }).stdout;\n if (typeof stdout !== \"string\" || !stdout.trim()) return [];\n return parseAuditJson(stdout);\n }\n}\n\n// Verified MCP registry is bundled statically; community PRs welcome.\nexport async function runScan(root: string): Promise<ScanResult> {\n const resolvedRoot = resolve(root);\n const registry = loadRegistry();\n return {\n mcp: readMcp(resolvedRoot, registry),\n deps: await runAudit(resolvedRoot),\n };\n}\n\nexport const __scanInternalsForTests = {\n parseAuditJson,\n extractPackageName,\n};\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/lib/prompt.ts
|
|
4
|
+
import readline from "readline/promises";
|
|
5
|
+
import { stdin, stdout } from "process";
|
|
6
|
+
async function promptYesNo(question, defaultYes = true) {
|
|
7
|
+
if (!stdout.isTTY || !stdin.isTTY) {
|
|
8
|
+
return defaultYes;
|
|
9
|
+
}
|
|
10
|
+
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
11
|
+
try {
|
|
12
|
+
const suffix = defaultYes ? " [Y/n] " : " [y/N] ";
|
|
13
|
+
const answer = (await rl.question(question + suffix)).trim().toLowerCase();
|
|
14
|
+
if (answer === "") return defaultYes;
|
|
15
|
+
return answer === "y" || answer === "yes";
|
|
16
|
+
} finally {
|
|
17
|
+
rl.close();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export {
|
|
21
|
+
promptYesNo
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=prompt-EQ5IFADN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/prompt.ts"],"sourcesContent":["import readline from \"node:readline/promises\";\nimport { stdin, stdout } from \"node:process\";\n\n/**\n * Ask a yes/no question on the terminal and resolve to true/false.\n *\n * Defaults to true on empty input (`Y/n` style). Returns `defaultYes` and\n * skips the prompt entirely when stdout isn't a TTY (CI, piped invocation,\n * agent hook) so we never block on input nobody can provide. Callers that\n * need different non-TTY behaviour should check `stdout.isTTY` first.\n */\nexport async function promptYesNo(question: string, defaultYes = true): Promise<boolean> {\n if (!stdout.isTTY || !stdin.isTTY) {\n return defaultYes;\n }\n const rl = readline.createInterface({ input: stdin, output: stdout });\n try {\n const suffix = defaultYes ? \" [Y/n] \" : \" [y/N] \";\n const answer = (await rl.question(question + suffix)).trim().toLowerCase();\n if (answer === \"\") return defaultYes;\n return answer === \"y\" || answer === \"yes\";\n } finally {\n rl.close();\n }\n}\n"],"mappings":";;;AAAA,OAAO,cAAc;AACrB,SAAS,OAAO,cAAc;AAU9B,eAAsB,YAAY,UAAkB,aAAa,MAAwB;AACvF,MAAI,CAAC,OAAO,SAAS,CAAC,MAAM,OAAO;AACjC,WAAO;AAAA,EACT;AACA,QAAM,KAAK,SAAS,gBAAgB,EAAE,OAAO,OAAO,QAAQ,OAAO,CAAC;AACpE,MAAI;AACF,UAAM,SAAS,aAAa,YAAY;AACxC,UAAM,UAAU,MAAM,GAAG,SAAS,WAAW,MAAM,GAAG,KAAK,EAAE,YAAY;AACzE,QAAI,WAAW,GAAI,QAAO;AAC1B,WAAO,WAAW,OAAO,WAAW;AAAA,EACtC,UAAE;AACA,OAAG,MAAM;AAAA,EACX;AACF;","names":[]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Holdpoint prerequisites
|
|
2
|
+
|
|
3
|
+
Holdpoint installed repo-local adapters for one or more AI coding agents. Before relying on them locally, review these setup notes:
|
|
4
|
+
|
|
5
|
+
- **GitHub Copilot CLI** — Holdpoint's `.github/extensions/holdpoint/extension.mjs` uses the Copilot CLI **EXTENSIONS** feature. Today that feature is gated behind experimental mode. In Copilot CLI, run `/experimental on` so **EXTENSIONS** appears in the enabled feature set before using Holdpoint locally.
|
|
6
|
+
- **Cursor** — project-level hooks run in trusted workspaces. After opening the repo in Cursor, confirm the workspace is trusted and review Settings → Hooks if hooks do not fire.
|
|
7
|
+
- **OpenAI Codex** — project-level hooks require trust approval. Run `codex trust` in the Codex TUI or review the hook with `/hooks`.
|
|
8
|
+
- **General** — Holdpoint expects Node.js 18+ and a git repository so `holdpoint init`, `holdpoint update`, and `holdpoint check` can run normally.
|
|
9
|
+
|
|
10
|
+
Docs: https://holdpoint.dev/docs
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
# Holdpoint — Eval Checkpoints
|
|
2
|
+
|
|
3
|
+
This project uses [Holdpoint](https://github.com/holdpoint-dev/holdpoint) to enforce
|
|
4
|
+
eval checkpoints. Before marking any task done, all checks must pass.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## The Rule
|
|
9
|
+
|
|
10
|
+
Before marking **any** task complete:
|
|
11
|
+
|
|
12
|
+
1. Run `holdpoint check` — all tasks must exit 0.
|
|
13
|
+
2. `holdpoint check` also prints every **prompt** check whose `when` matches the
|
|
14
|
+
files you changed. Read and act on each listed instruction before finishing.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## The Suggest Loop
|
|
19
|
+
|
|
20
|
+
`checks.yaml` is not static — it grows alongside the project automatically.
|
|
21
|
+
|
|
22
|
+
**`holdpoint-suggest` is a deterministic check** in `checks.yaml` that fires whenever you change a structural file (`package.json`, `pyproject.toml`, `go.mod`, `Dockerfile`, `tsconfig.json`, `vitest.config.*`, etc.). When it fires, `holdpoint suggest` runs and **exits 1 if `checks.yaml` is out of sync** — blocking task completion until you apply the proposals.
|
|
23
|
+
|
|
24
|
+
When blocked by `holdpoint-suggest`, run:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
holdpoint suggest --apply # scan, apply proposals, regenerate engine files
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Then commit:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
git add checks.yaml .github/holdpoint/generated/
|
|
34
|
+
git commit -m "chore: suggest holdpoint checks"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`holdpoint suggest --apply` is idempotent — safe to re-run at any time. It only adds checks for tools/patterns detected in the project and wraps stale checks (whose `when:` pattern no longer matches any file) with `conditionId: file_exists` so they auto-skip instead of failing.
|
|
38
|
+
|
|
39
|
+
**What triggers evolution:**
|
|
40
|
+
|
|
41
|
+
- New dependency in `package.json` / `pyproject.toml` / `go.mod` / `Cargo.toml`
|
|
42
|
+
- New `Dockerfile`, `docker-compose.yml`, `*.tf`, `openapi.yaml`
|
|
43
|
+
- New test runner config (`vitest.config.*`, `jest.config.*`, `playwright.config.*`)
|
|
44
|
+
- New CI workflow in `.github/workflows/`
|
|
45
|
+
- New TypeScript setup (`tsconfig.json`)
|
|
46
|
+
|
|
47
|
+
**What does NOT trigger it:** `.ts` / `.py` / `.go` source files, docs, styles, tests — minor work proceeds without interruption.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Git workflow best practices
|
|
52
|
+
|
|
53
|
+
Prefer the least-disruptive git workflow that still satisfies the task:
|
|
54
|
+
|
|
55
|
+
- Use a branch + PR when the user requests it, when work targets protected
|
|
56
|
+
`main`, or when remote CI/review is part of the task.
|
|
57
|
+
- For small local fixes, commit directly on the current branch and do not open a
|
|
58
|
+
PR unless the user asks.
|
|
59
|
+
- If already on a feature branch, keep committing there instead of creating
|
|
60
|
+
another branch.
|
|
61
|
+
- After committing, decide whether to push: push when a PR, remote review, CI
|
|
62
|
+
run, or handoff needs it; otherwise leave the commit local and report the
|
|
63
|
+
branch/commit.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## checks.yaml — Full Reference
|
|
68
|
+
|
|
69
|
+
`checks.yaml` at the project root is the single source of truth. Edit it to add,
|
|
70
|
+
remove, or change checkpoints.
|
|
71
|
+
|
|
72
|
+
After every edit, regenerate the engine files and commit everything together:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
holdpoint update
|
|
76
|
+
git add checks.yaml .github/holdpoint/generated/ .github/hooks/
|
|
77
|
+
git commit -m "chore: update holdpoint checks"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Top-level structure
|
|
81
|
+
|
|
82
|
+
```yaml
|
|
83
|
+
version: 1
|
|
84
|
+
|
|
85
|
+
context:
|
|
86
|
+
guides: # project notes shown when `holdpoint check` runs
|
|
87
|
+
setup: >
|
|
88
|
+
Use pnpm, not npm. Node 20+ required.
|
|
89
|
+
|
|
90
|
+
session_context_files:
|
|
91
|
+
- MASTER_PROMPT.md # injected into Copilot/Codex sessions when supported
|
|
92
|
+
|
|
93
|
+
conditions: # gate checks on file/env state
|
|
94
|
+
- id: dist-built
|
|
95
|
+
operator: file_exists
|
|
96
|
+
path: dist/index.js
|
|
97
|
+
|
|
98
|
+
checks: # list of all checks — each has on/when + cmd (task) or prompt
|
|
99
|
+
- ...
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
### Deterministic check
|
|
105
|
+
|
|
106
|
+
```yaml
|
|
107
|
+
- id: lint # unique slug, kebab-case
|
|
108
|
+
label: "ESLint — all packages" # human-readable label shown in output
|
|
109
|
+
# on: before_done # lifecycle hook (default; only value today)
|
|
110
|
+
# when: frontend # file filter — omit to run on every task
|
|
111
|
+
cmd: "pnpm turbo lint" # shell command; must exit 0 to pass
|
|
112
|
+
conditionId: dist-built # optional: skip if condition is not met
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Prompt check
|
|
116
|
+
|
|
117
|
+
```yaml
|
|
118
|
+
- id: migration-review
|
|
119
|
+
label: "Review DB migration"
|
|
120
|
+
when: "^prisma/migrations/" # only fires when migration files change
|
|
121
|
+
prompt: >
|
|
122
|
+
Open the new migration file. Confirm it is backward-compatible
|
|
123
|
+
and does not drop or truncate data without a fallback.
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
### `on` — lifecycle hooks
|
|
129
|
+
|
|
130
|
+
`on` specifies _when in the agent lifecycle_ a check fires. Omit it to use the default.
|
|
131
|
+
|
|
132
|
+
| Value | Fires |
|
|
133
|
+
| ------------- | ---------------------------------- |
|
|
134
|
+
| `before_done` | Before the agent marks a task done |
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
### `when` — file filters
|
|
139
|
+
|
|
140
|
+
`when` is an optional file filter. If omitted the check runs on every task.
|
|
141
|
+
|
|
142
|
+
| Value | Fires when changed files match |
|
|
143
|
+
| ----------- | -------------------------------------------------------------------------------------------------- |
|
|
144
|
+
| _(absent)_ | Every task — no file filter applied |
|
|
145
|
+
| `frontend` | `**/*.tsx`, `**/*.jsx`, `**/*.css`, `**/*.scss`, `**/tailwind.config.*`, `apps/**` |
|
|
146
|
+
| `backend` | `**/api/**`, `**/server/**`, `**/routes/**`, `**/controllers/**`, `packages/*/src/**` |
|
|
147
|
+
| `socket` | `**/socket/**`, `**/ws/**`, `**/websocket/**` |
|
|
148
|
+
| `visual` | `**/*.stories.{ts,tsx}`, `**/__screenshots__/**`, `**/*.snap` |
|
|
149
|
+
| `python` | `**/*.py`, `**/*.pyi`, `**/requirements*.txt`, `**/pyproject.toml`, `**/setup.py`, `**/pytest.ini` |
|
|
150
|
+
| `go` | `**/*.go`, `**/go.mod`, `**/go.sum` |
|
|
151
|
+
| `rust` | `**/*.rs`, `**/Cargo.toml`, `**/Cargo.lock` |
|
|
152
|
+
| `java` | `**/*.java`, `**/*.kt`, `**/*.gradle`, `**/*.gradle.kts`, `**/pom.xml` |
|
|
153
|
+
| `ruby` | `**/*.rb`, `**/Gemfile`, `**/Gemfile.lock`, `**/Rakefile` |
|
|
154
|
+
| `database` | `**/*.sql`, `**/migrations/**`, `**/db/**`, `**/database/**`, `**/prisma/**`, `**/*.prisma` |
|
|
155
|
+
| `prisma` | `**/prisma/**`, `**/*.prisma` — focused subset of `database` for Prisma-specific checks |
|
|
156
|
+
| `testing` | `**/*.test.*`, `**/*.spec.*`, `**/__tests__/**`, `**/test/**`, `**/tests/**`, `**/spec/**` |
|
|
157
|
+
| `infra` | `**/Dockerfile*`, `**/docker-compose.*`, `**/*.tf`, `**/*.tfvars`, `**/k8s/**`, `**/kubernetes/**` |
|
|
158
|
+
| `ci` | `**/.github/workflows/**`, `**/.circleci/**`, `**/Jenkinsfile`, `**/.gitlab-ci.yml` |
|
|
159
|
+
| `docs` | `**/*.mdx`, `**/*.rst`, `**/docs/**`, `**/documentation/**` |
|
|
160
|
+
| `"^src/.*"` | Any JavaScript regex tested against each changed file path |
|
|
161
|
+
|
|
162
|
+
Regex example — fires only when files under `src/api/` change:
|
|
163
|
+
|
|
164
|
+
```yaml
|
|
165
|
+
when: "^src/api/" # new RegExp(when).test(filePath)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
> **Note:** Named scopes use glob matching; plain strings are treated as JavaScript regexes.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
### Conditions
|
|
173
|
+
|
|
174
|
+
Conditions let you skip a check when a prerequisite is not yet met (e.g. a build
|
|
175
|
+
artefact doesn't exist yet).
|
|
176
|
+
|
|
177
|
+
| Operator | What it checks |
|
|
178
|
+
| ----------------- | ------------------------------------------------- |
|
|
179
|
+
| `file_exists` | A file or directory exists at `path` |
|
|
180
|
+
| `file_contains` | The file at `path` contains the substring `value` |
|
|
181
|
+
| `env_var_set` | The environment variable named `value` is set |
|
|
182
|
+
| `shell_returns_0` | The shell command in `cmd` exits with code 0 |
|
|
183
|
+
|
|
184
|
+
```yaml
|
|
185
|
+
conditions:
|
|
186
|
+
- id: packages-built
|
|
187
|
+
operator: file_exists
|
|
188
|
+
path: packages/yaml-core/dist/index.js
|
|
189
|
+
|
|
190
|
+
checks:
|
|
191
|
+
- id: validate-templates
|
|
192
|
+
label: "Templates parse against schema"
|
|
193
|
+
conditionId: packages-built # skipped (◌) when dist is absent
|
|
194
|
+
cmd: "node dist/validate.js templates/"
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
### Context guides
|
|
200
|
+
|
|
201
|
+
`context.guides` is a freeform key → multiline-string map. Guides are printed
|
|
202
|
+
at the start of `holdpoint check` output as project-level reminders to whoever
|
|
203
|
+
(or whatever) is running the checks.
|
|
204
|
+
|
|
205
|
+
```yaml
|
|
206
|
+
context:
|
|
207
|
+
guides:
|
|
208
|
+
setup: >
|
|
209
|
+
This project requires Node 20 and pnpm 9+.
|
|
210
|
+
Run `pnpm install` from the repo root before any other command.
|
|
211
|
+
architecture: >
|
|
212
|
+
API routes live in src/api/. Models live in src/models/.
|
|
213
|
+
Client code must never import from server modules.
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Adding a New Check
|
|
219
|
+
|
|
220
|
+
1. Open `checks.yaml`.
|
|
221
|
+
2. Add your entry under `checks:`.
|
|
222
|
+
3. Run `holdpoint update`.
|
|
223
|
+
4. Commit `checks.yaml` and the generated files.
|
|
224
|
+
|
|
225
|
+
**Add a task check (runs a shell command automatically):**
|
|
226
|
+
|
|
227
|
+
```yaml
|
|
228
|
+
checks:
|
|
229
|
+
- id: vitest
|
|
230
|
+
label: "Vitest — unit tests"
|
|
231
|
+
cmd: "pnpm vitest run"
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**Add a scoped task (fires only on matching file changes):**
|
|
235
|
+
|
|
236
|
+
```yaml
|
|
237
|
+
checks:
|
|
238
|
+
- id: openapi-sync
|
|
239
|
+
label: "OpenAPI types are up to date"
|
|
240
|
+
when: "^src/api/"
|
|
241
|
+
cmd: "pnpm generate:types && git diff --exit-code src/generated/"
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
**Add an agent prompt checkpoint:**
|
|
245
|
+
|
|
246
|
+
```yaml
|
|
247
|
+
checks:
|
|
248
|
+
- id: jsdoc
|
|
249
|
+
label: "JSDoc on changed public functions"
|
|
250
|
+
prompt: >
|
|
251
|
+
For every public function or export you modified, ensure there is an
|
|
252
|
+
accurate JSDoc comment: description, @param, and @returns.
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**Enforce changelog and git commit on every task (recommended):**
|
|
256
|
+
|
|
257
|
+
```yaml
|
|
258
|
+
checks:
|
|
259
|
+
- id: changelog-update
|
|
260
|
+
label: "Add a CHANGELOG.md entry for this session"
|
|
261
|
+
prompt: >
|
|
262
|
+
Before committing, add an entry to CHANGELOG.md describing what was done.
|
|
263
|
+
Use Keep a Changelog format — add under ## [Unreleased] (create the file
|
|
264
|
+
and that section if absent). Group entries as Added, Changed, Fixed, or Removed.
|
|
265
|
+
Be concise but specific. The entry text will serve as the commit message.
|
|
266
|
+
|
|
267
|
+
- id: readme-sync
|
|
268
|
+
label: "Update README.md if user-facing changes were made"
|
|
269
|
+
prompt: >
|
|
270
|
+
If you added, changed, or removed user-facing functionality — CLI commands,
|
|
271
|
+
configuration options, public APIs, or significant new features — update
|
|
272
|
+
README.md to reflect those changes.
|
|
273
|
+
|
|
274
|
+
- id: git-workflow
|
|
275
|
+
label: "Use the right git workflow"
|
|
276
|
+
prompt: >
|
|
277
|
+
Choose the least-disruptive git workflow: use branch + PR for requested
|
|
278
|
+
feature branches or protected-main work; for small local fixes, commit on
|
|
279
|
+
the current branch without opening a PR unless asked; if already on a
|
|
280
|
+
feature branch, keep committing there instead of creating another branch.
|
|
281
|
+
Push only when a PR, remote review, CI run, or handoff needs it; otherwise
|
|
282
|
+
leave the commit local and report the branch/commit.
|
|
283
|
+
|
|
284
|
+
- id: git-commit
|
|
285
|
+
label: "Commit all changes before finishing"
|
|
286
|
+
cmd: 'git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0; [ -z "$(git status --porcelain)" ] && exit 0; git status --short; exit 1'
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
When the `git-commit` check fails (uncommitted changes remain), the agent will also see
|
|
290
|
+
the `changelog-update` and `readme-sync` prompt reminders inline — ensuring it updates
|
|
291
|
+
the changelog, syncs docs, _then_ commits before it can mark the task done.
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## `session_context_files`
|
|
296
|
+
|
|
297
|
+
`session_context_files` is an optional list of project files that Holdpoint injects
|
|
298
|
+
as context at the start of every Copilot session. Use it for files the agent should
|
|
299
|
+
always read before starting work.
|
|
300
|
+
|
|
301
|
+
```yaml
|
|
302
|
+
session_context_files:
|
|
303
|
+
- MASTER_PROMPT.md
|
|
304
|
+
- AGENT_CONTEXT.md
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Files are resolved relative to the repo root and must stay inside it (traversal
|
|
308
|
+
paths like `../../etc/passwd` are rejected). If a file doesn't exist it is silently
|
|
309
|
+
skipped.
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## `inject_datetime`
|
|
314
|
+
|
|
315
|
+
Holdpoint can inject the current date and time into every prompt the agent receives.
|
|
316
|
+
This fixes a common failure mode where models anchor their sense of "now" to their
|
|
317
|
+
training cutoff and make stale assumptions (e.g. treating months-old information as
|
|
318
|
+
current, or not knowing what year it is).
|
|
319
|
+
|
|
320
|
+
The feature is **on by default** — no configuration needed. To opt out:
|
|
321
|
+
|
|
322
|
+
```yaml
|
|
323
|
+
inject_datetime: false
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
When enabled, each prompt submission includes:
|
|
327
|
+
|
|
328
|
+
```
|
|
329
|
+
Current date and time: 2026-05-29T14:23:45.123Z (UTC)
|
|
330
|
+
Provided by Holdpoint — use this to avoid knowledge-cutoff confusion.
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
**Agent support:**
|
|
334
|
+
|
|
335
|
+
| Agent | Hook | Notes |
|
|
336
|
+
| ------- | ----------------------- | ---------------------------------------------------------------- |
|
|
337
|
+
| Claude | `UserPromptSubmit` | Fires on every prompt via `additionalContext` |
|
|
338
|
+
| Cursor | `beforeSubmitPrompt` | Fires on every prompt via `additional_context` |
|
|
339
|
+
| Codex | `UserPromptSubmit` | Fires on every prompt via `hookSpecificOutput.additionalContext` |
|
|
340
|
+
| Copilot | `onUserPromptSubmitted` | Fires on every prompt via `additionalContext` |
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
## Commands
|
|
345
|
+
|
|
346
|
+
| Command | What it does |
|
|
347
|
+
| ----------------------------- | ------------------------------------------------------- |
|
|
348
|
+
| `holdpoint check` | Run checks against all files changed vs HEAD |
|
|
349
|
+
| `holdpoint check --staged` | Run checks against staged files only |
|
|
350
|
+
| `holdpoint suggest` | Scan project and show proposed additions to checks.yaml |
|
|
351
|
+
| `holdpoint suggest --apply` | Apply proposals and regenerate engine files |
|
|
352
|
+
| `holdpoint require-changeset` | Require `.changeset/*.md` for package changes |
|
|
353
|
+
| `holdpoint update` | Regenerate engine files from the current `checks.yaml` |
|
|
354
|
+
| `holdpoint validate` | Validate `checks.yaml` schema (no commands run) |
|
|
355
|
+
| `holdpoint builder` | Open the daemon-served visual builder UI |
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## Generated files (do not edit directly)
|
|
360
|
+
|
|
361
|
+
| File | Agent |
|
|
362
|
+
| --------------------------------------------------- | ------- |
|
|
363
|
+
| `.github/holdpoint/generated/checks.immutable.json` | all |
|
|
364
|
+
| `.github/hooks/holdpoint.json` | Copilot |
|
|
365
|
+
| `.github/hooks/holdpoint-check.mjs` | Copilot |
|
|
366
|
+
| `.claude/settings.json` | Claude |
|
|
367
|
+
| `.cursor/hooks.json` | Cursor |
|
|
368
|
+
| `.cursor/holdpoint-hook.mjs` | Cursor |
|
|
369
|
+
| `.cursorrules` (Holdpoint section) | Cursor |
|
|
370
|
+
|
|
371
|
+
All generated files are overwritten by `holdpoint update`. Edit `checks.yaml`,
|
|
372
|
+
then run `update` — never edit the generated files directly.
|