@holdpoint/cli 0.1.0-alpha.16 → 0.1.0-alpha.18
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/index.js +883 -393
- package/dist/index.js.map +1 -1
- package/dist/templates/HOLDPOINT_PREREQUISITES.md +1 -0
- package/dist/templates/HOLDPOINT_REFERENCE.md +341 -0
- package/dist/templates/MASTER_PROMPT.md +25 -295
- package/dist/templates/default.yaml +239 -0
- package/package.json +17 -19
- package/dist/builder-ui/assets/index-3J1uDBNl.css +0 -1
- package/dist/builder-ui/assets/index-DezgTDRo.js +0 -457
- package/dist/builder-ui/assets/index-DezgTDRo.js.map +0 -1
- package/dist/builder-ui/favicon.svg +0 -10
- package/dist/builder-ui/index.html +0 -14
- package/dist/templates/_base.yaml +0 -57
- 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 -52
package/dist/index.js
CHANGED
|
@@ -4,20 +4,23 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/init.ts
|
|
7
|
-
import { execSync } from "child_process";
|
|
8
|
-
import { existsSync as
|
|
9
|
-
import { join as join2, dirname as
|
|
7
|
+
import { execSync as execSync2 } from "child_process";
|
|
8
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
9
|
+
import { join as join2, dirname as dirname3 } from "path";
|
|
10
10
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
11
|
-
import
|
|
11
|
+
import chalk2 from "chalk";
|
|
12
12
|
import ora from "ora";
|
|
13
13
|
import { buildConfigJson, buildEngine } from "@holdpoint/engine-copilot";
|
|
14
14
|
import { buildEngineJson as buildClaudeEngineJson } from "@holdpoint/engine-claude";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
buildCheckScript as buildCursorCheckScript,
|
|
17
|
+
buildEngine as buildCursorEngine,
|
|
18
|
+
buildHooksJson as buildCursorHooksJson
|
|
19
|
+
} from "@holdpoint/engine-cursor";
|
|
16
20
|
import {
|
|
17
21
|
buildConfigToml as buildCodexConfigToml,
|
|
18
22
|
buildHooksJson as buildCodexHooksJson,
|
|
19
|
-
buildCheckScript as buildCodexCheckScript
|
|
20
|
-
spliceAgentsMd
|
|
23
|
+
buildCheckScript as buildCodexCheckScript
|
|
21
24
|
} from "@holdpoint/engine-codex";
|
|
22
25
|
import { parseHoldpointYaml } from "@holdpoint/yaml-core";
|
|
23
26
|
|
|
@@ -32,9 +35,17 @@ function detectInstalledAgents() {
|
|
|
32
35
|
const agents = [];
|
|
33
36
|
if (existsSync(".github/extensions/holdpoint/extension.mjs")) agents.push("copilot");
|
|
34
37
|
if (existsSync(".claude/settings.json")) agents.push("claude");
|
|
38
|
+
if (existsSync(".cursor/hooks.json")) {
|
|
39
|
+
try {
|
|
40
|
+
if (readFileSync(".cursor/hooks.json", "utf8").includes("HOLDPOINT_MANAGED=cursor")) {
|
|
41
|
+
agents.push("cursor");
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
}
|
|
45
|
+
}
|
|
35
46
|
if (existsSync(".cursorrules")) {
|
|
36
47
|
try {
|
|
37
|
-
if (readFileSync(".cursorrules", "utf8").includes("Holdpoint Rules")) {
|
|
48
|
+
if (!agents.includes("cursor") && readFileSync(".cursorrules", "utf8").includes("Holdpoint Rules")) {
|
|
38
49
|
agents.push("cursor");
|
|
39
50
|
}
|
|
40
51
|
} catch {
|
|
@@ -43,20 +54,6 @@ function detectInstalledAgents() {
|
|
|
43
54
|
if (existsSync(".codex/holdpoint-check.mjs")) agents.push("codex");
|
|
44
55
|
return agents;
|
|
45
56
|
}
|
|
46
|
-
function detectStack() {
|
|
47
|
-
const hasNext = existsSync("next.config.ts") || existsSync("next.config.js") || existsSync("next.config.mjs");
|
|
48
|
-
const hasTsConfig = existsSync("tsconfig.json");
|
|
49
|
-
const hasPyproject = existsSync("pyproject.toml") || existsSync("requirements.txt") || existsSync("setup.py");
|
|
50
|
-
const hasPrisma = existsSync("prisma/schema.prisma");
|
|
51
|
-
const hasApi = existsSync("server") || existsSync("api") || existsSync("backend");
|
|
52
|
-
const hasGoMod = existsSync("go.mod");
|
|
53
|
-
if (hasNext && (hasPrisma || hasApi)) return "fullstack";
|
|
54
|
-
if (hasNext) return "nextjs";
|
|
55
|
-
if (hasTsConfig) return "typescript";
|
|
56
|
-
if (hasPyproject) return "python";
|
|
57
|
-
if (hasGoMod) return "go";
|
|
58
|
-
return "unknown";
|
|
59
|
-
}
|
|
60
57
|
|
|
61
58
|
// src/templates.ts
|
|
62
59
|
import { copyFileSync, existsSync as existsSync2, writeFileSync } from "fs";
|
|
@@ -90,20 +87,239 @@ function ensureBundledFile(outputPath, templateFilename, fallbackContent) {
|
|
|
90
87
|
return true;
|
|
91
88
|
}
|
|
92
89
|
|
|
90
|
+
// src/lib/preflight.ts
|
|
91
|
+
import { execSync } from "child_process";
|
|
92
|
+
import chalk from "chalk";
|
|
93
|
+
function silentExec(cmd) {
|
|
94
|
+
try {
|
|
95
|
+
const stdout = execSync(cmd, { stdio: ["ignore", "pipe", "ignore"] }).toString();
|
|
96
|
+
return { ok: true, stdout };
|
|
97
|
+
} catch {
|
|
98
|
+
return { ok: false, stdout: "" };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function checkCopilot() {
|
|
102
|
+
const gh = silentExec("gh --version");
|
|
103
|
+
if (!gh.ok) {
|
|
104
|
+
return {
|
|
105
|
+
agent: "copilot",
|
|
106
|
+
status: "action_required",
|
|
107
|
+
message: "GitHub CLI not found on PATH",
|
|
108
|
+
command: "brew install gh # or: https://cli.github.com/"
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const copilot = silentExec("gh copilot --version");
|
|
112
|
+
if (!copilot.ok) {
|
|
113
|
+
return {
|
|
114
|
+
agent: "copilot",
|
|
115
|
+
status: "action_required",
|
|
116
|
+
message: "Copilot CLI extension not installed",
|
|
117
|
+
command: "gh extension install github/gh-copilot"
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
agent: "copilot",
|
|
122
|
+
status: "action_required",
|
|
123
|
+
message: "Copilot CLI detected \u2014 experimental mode required for EXTENSIONS",
|
|
124
|
+
command: "Inside Copilot CLI, run: /experimental on"
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function checkClaude() {
|
|
128
|
+
const claude = silentExec("claude --version");
|
|
129
|
+
if (!claude.ok) {
|
|
130
|
+
return {
|
|
131
|
+
agent: "claude",
|
|
132
|
+
status: "unknown",
|
|
133
|
+
message: "Claude Code binary not on PATH (hooks still written for when it is)"
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
agent: "claude",
|
|
138
|
+
status: "ok",
|
|
139
|
+
message: "Claude Code detected \u2014 hooks installed at .claude/settings.json"
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function checkCursor() {
|
|
143
|
+
return {
|
|
144
|
+
agent: "cursor",
|
|
145
|
+
status: "ok",
|
|
146
|
+
message: "Cursor \u2014 .cursor/hooks.json gate + .cursor/rules breadcrumb installed",
|
|
147
|
+
docs: "https://holdpoint.dev/docs#cursor"
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function checkCodex() {
|
|
151
|
+
const codex = silentExec("codex --version");
|
|
152
|
+
if (!codex.ok) {
|
|
153
|
+
return {
|
|
154
|
+
agent: "codex",
|
|
155
|
+
status: "action_required",
|
|
156
|
+
message: "Codex CLI not found on PATH",
|
|
157
|
+
command: "Install Codex: https://github.com/openai/codex"
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
agent: "codex",
|
|
162
|
+
status: "action_required",
|
|
163
|
+
message: "Codex detected \u2014 project-level hooks require trust approval",
|
|
164
|
+
command: "In the Codex TUI: codex trust (or /hooks to review)"
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
var CHECKS = {
|
|
168
|
+
copilot: checkCopilot,
|
|
169
|
+
claude: checkClaude,
|
|
170
|
+
cursor: checkCursor,
|
|
171
|
+
codex: checkCodex
|
|
172
|
+
};
|
|
173
|
+
function runPreflight(agents) {
|
|
174
|
+
return agents.flatMap((agent) => {
|
|
175
|
+
const check = CHECKS[agent];
|
|
176
|
+
return check ? [check()] : [];
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
function printPreflight(results) {
|
|
180
|
+
if (results.length === 0) return;
|
|
181
|
+
const ok = results.filter((r) => r.status === "ok");
|
|
182
|
+
const unknown = results.filter((r) => r.status === "unknown");
|
|
183
|
+
const action = results.filter((r) => r.status === "action_required");
|
|
184
|
+
console.log("");
|
|
185
|
+
console.log(chalk.bold("Agent preflight:"));
|
|
186
|
+
for (const r of ok) {
|
|
187
|
+
console.log(` ${chalk.green("\u2713")} ${r.agent.padEnd(7)} ${chalk.dim(r.message)}`);
|
|
188
|
+
}
|
|
189
|
+
for (const r of unknown) {
|
|
190
|
+
console.log(` ${chalk.dim("?")} ${r.agent.padEnd(7)} ${chalk.dim(r.message)}`);
|
|
191
|
+
}
|
|
192
|
+
for (const r of action) {
|
|
193
|
+
console.log(` ${chalk.yellow("\u2192")} ${chalk.bold(r.agent.padEnd(7))} ${r.message}`);
|
|
194
|
+
if (r.command) console.log(` ${chalk.cyan(r.command)}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/claude-settings.ts
|
|
199
|
+
var HOLDPOINT_CLAUDE_HOOK_MARKER = "HOLDPOINT_MANAGED=claude";
|
|
200
|
+
function isObject(value) {
|
|
201
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
202
|
+
}
|
|
203
|
+
function asHookArray(value) {
|
|
204
|
+
return Array.isArray(value) ? value : [];
|
|
205
|
+
}
|
|
206
|
+
function isManagedHookCommand(value) {
|
|
207
|
+
return isObject(value) && typeof value.command === "string" && value.command.includes(HOLDPOINT_CLAUDE_HOOK_MARKER);
|
|
208
|
+
}
|
|
209
|
+
function isLegacyManagedHookCommand(value) {
|
|
210
|
+
if (!isObject(value) || typeof value.command !== "string") return false;
|
|
211
|
+
return value.command === "node_modules/.bin/holdpoint event --engine claude --from-hook || true" || value.command === "node_modules/.bin/holdpoint check --staged";
|
|
212
|
+
}
|
|
213
|
+
function isManagedHookEntry(value) {
|
|
214
|
+
if (!isObject(value)) return false;
|
|
215
|
+
const hooks = asHookArray(value.hooks);
|
|
216
|
+
return hooks.length > 0 && (hooks.every(isManagedHookCommand) || hooks.every(isLegacyManagedHookCommand));
|
|
217
|
+
}
|
|
218
|
+
function mergeClaudeSettings(existing, generated) {
|
|
219
|
+
const existingHooks = isObject(existing.hooks) ? existing.hooks : {};
|
|
220
|
+
const generatedHooks = isObject(generated.hooks) ? generated.hooks : {};
|
|
221
|
+
const mergedHooks = {};
|
|
222
|
+
for (const eventName of /* @__PURE__ */ new Set([
|
|
223
|
+
...Object.keys(existingHooks),
|
|
224
|
+
...Object.keys(generatedHooks)
|
|
225
|
+
])) {
|
|
226
|
+
const preserved = asHookArray(existingHooks[eventName]).filter(
|
|
227
|
+
(entry) => !isManagedHookEntry(entry)
|
|
228
|
+
);
|
|
229
|
+
const next = asHookArray(generatedHooks[eventName]);
|
|
230
|
+
if (preserved.length > 0 || next.length > 0) {
|
|
231
|
+
mergedHooks[eventName] = [...preserved, ...next];
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return { ...existing, ...generated, hooks: mergedHooks };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/cursor-hooks.ts
|
|
238
|
+
var HOLDPOINT_CURSOR_HOOK_MARKER = "HOLDPOINT_MANAGED=cursor";
|
|
239
|
+
function isObject2(value) {
|
|
240
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
241
|
+
}
|
|
242
|
+
function asHookArray2(value) {
|
|
243
|
+
return Array.isArray(value) ? value : [];
|
|
244
|
+
}
|
|
245
|
+
function isManagedCursorHook(value) {
|
|
246
|
+
return isObject2(value) && typeof value.command === "string" && (value.command.includes(HOLDPOINT_CURSOR_HOOK_MARKER) || value.command.includes(".cursor/holdpoint-hook.mjs"));
|
|
247
|
+
}
|
|
248
|
+
function mergeCursorHooks(existing, generated) {
|
|
249
|
+
const existingHooks = isObject2(existing.hooks) ? existing.hooks : {};
|
|
250
|
+
const generatedHooks = isObject2(generated.hooks) ? generated.hooks : {};
|
|
251
|
+
const mergedHooks = {};
|
|
252
|
+
for (const eventName of /* @__PURE__ */ new Set([
|
|
253
|
+
...Object.keys(existingHooks),
|
|
254
|
+
...Object.keys(generatedHooks)
|
|
255
|
+
])) {
|
|
256
|
+
const preserved = asHookArray2(existingHooks[eventName]).filter(
|
|
257
|
+
(entry) => !isManagedCursorHook(entry)
|
|
258
|
+
);
|
|
259
|
+
const next = asHookArray2(generatedHooks[eventName]);
|
|
260
|
+
if (preserved.length > 0 || next.length > 0) {
|
|
261
|
+
mergedHooks[eventName] = [...preserved, ...next];
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return { ...existing, ...generated, hooks: mergedHooks };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/lib/instructions-breadcrumb.ts
|
|
268
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync } from "fs";
|
|
269
|
+
import { dirname as dirname2 } from "path";
|
|
270
|
+
var START_MARKER = "<!-- HOLDPOINT_MANAGED \u2014 content between these markers is auto-generated by holdpoint init / holdpoint update -->";
|
|
271
|
+
var END_MARKER = "<!-- /HOLDPOINT_MANAGED -->";
|
|
272
|
+
var BREADCRUMB_BODY = `## Holdpoint workflow
|
|
273
|
+
|
|
274
|
+
This repo uses [Holdpoint](https://holdpoint.dev) to gate task completion on deterministic checks.
|
|
275
|
+
|
|
276
|
+
Before marking any task done:
|
|
277
|
+
|
|
278
|
+
1. Run \`holdpoint check\` (or it will run automatically via Stop / TaskCompleted hooks).
|
|
279
|
+
2. Fix any failures. Re-run until exit 0.
|
|
280
|
+
3. Never bypass with \`--no-verify\` or by skipping the agent's stop hook.
|
|
281
|
+
|
|
282
|
+
Full workflow reference: [\`MASTER_PROMPT.md\`](./MASTER_PROMPT.md) (always injected at session start).
|
|
283
|
+
Deep reference: [\`HOLDPOINT_REFERENCE.md\`](./HOLDPOINT_REFERENCE.md) (read on demand).
|
|
284
|
+
Active checks: [\`checks.yaml\`](./checks.yaml).`;
|
|
285
|
+
function spliceBreadcrumb(filePath, body = BREADCRUMB_BODY, createIfMissing = true) {
|
|
286
|
+
const block = `${START_MARKER}
|
|
287
|
+
|
|
288
|
+
${body}
|
|
289
|
+
|
|
290
|
+
${END_MARKER}`;
|
|
291
|
+
if (!existsSync3(filePath)) {
|
|
292
|
+
if (!createIfMissing) return;
|
|
293
|
+
mkdirSync(dirname2(filePath), { recursive: true });
|
|
294
|
+
writeFileSync2(filePath, block + "\n", "utf8");
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const existing = readFileSync2(filePath, "utf8");
|
|
298
|
+
const startIdx = existing.indexOf(START_MARKER);
|
|
299
|
+
const endIdx = existing.indexOf(END_MARKER);
|
|
300
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
301
|
+
const before = existing.slice(0, startIdx);
|
|
302
|
+
const after = existing.slice(endIdx + END_MARKER.length);
|
|
303
|
+
writeFileSync2(filePath, before + block + after, "utf8");
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const sep2 = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
307
|
+
writeFileSync2(filePath, existing + sep2 + block + "\n", "utf8");
|
|
308
|
+
}
|
|
309
|
+
|
|
93
310
|
// src/commands/init.ts
|
|
94
|
-
var __dirname2 =
|
|
95
|
-
function
|
|
96
|
-
const name = stack === "unknown" ? "_base" : stack;
|
|
311
|
+
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
312
|
+
function getDefaultTemplatePath() {
|
|
97
313
|
const candidates = [
|
|
98
|
-
join2(__dirname2, "templates",
|
|
314
|
+
join2(__dirname2, "templates", "default.yaml"),
|
|
99
315
|
// dist/templates/ (published package)
|
|
100
|
-
join2(__dirname2, "../../../templates",
|
|
316
|
+
join2(__dirname2, "../../../templates", "default.yaml"),
|
|
101
317
|
// monorepo dev fallback
|
|
102
|
-
join2(process.cwd(), "templates",
|
|
318
|
+
join2(process.cwd(), "templates", "default.yaml")
|
|
103
319
|
// cwd fallback
|
|
104
320
|
];
|
|
105
321
|
for (const p of candidates) {
|
|
106
|
-
if (
|
|
322
|
+
if (existsSync4(p)) return p;
|
|
107
323
|
}
|
|
108
324
|
return "";
|
|
109
325
|
}
|
|
@@ -125,11 +341,16 @@ var MINIMAL_MASTER_PROMPT = `# Holdpoint
|
|
|
125
341
|
Run \`holdpoint check\` before marking any task complete.
|
|
126
342
|
See \`checks.yaml\` for the full list of checks.
|
|
127
343
|
`;
|
|
344
|
+
var MINIMAL_HOLDPOINT_REFERENCE = `# Holdpoint reference
|
|
345
|
+
|
|
346
|
+
Read \`MASTER_PROMPT.md\` first for the mandatory workflow, then use this file for deeper project-specific Holdpoint notes.
|
|
347
|
+
`;
|
|
128
348
|
var MINIMAL_PREREQUISITES = `# Holdpoint prerequisites
|
|
129
349
|
|
|
130
|
-
Holdpoint installed repo-local
|
|
350
|
+
Holdpoint installed repo-local engine integrations for one or more AI coding agents. Before relying on them locally, review these setup notes:
|
|
131
351
|
|
|
132
352
|
- **GitHub Copilot CLI** \u2014 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.
|
|
353
|
+
- **Cursor** \u2014 project-level hooks run in trusted workspaces. After opening the repo in Cursor, confirm the workspace is trusted and review Settings \u2192 Hooks if hooks do not fire.
|
|
133
354
|
- **OpenAI Codex** \u2014 project-level hooks require trust approval. Run \`codex trust\` in the Codex TUI or review the hook with \`/hooks\`.
|
|
134
355
|
- **General** \u2014 Holdpoint expects Node.js 18+ and a git repository so \`holdpoint init\`, \`holdpoint update\`, and \`holdpoint check\` can run normally.
|
|
135
356
|
|
|
@@ -137,72 +358,95 @@ Docs: https://holdpoint.dev/docs
|
|
|
137
358
|
`;
|
|
138
359
|
async function initCommand(options) {
|
|
139
360
|
const spinner = ora("Initialising Holdpoint\u2026").start();
|
|
140
|
-
const stack = options.stack ?? detectStack();
|
|
141
361
|
const agentOpt = options.agent;
|
|
142
362
|
const agents = !agentOpt || agentOpt === "all" ? ["copilot", "claude", "cursor", "codex"] : [agentOpt];
|
|
143
|
-
spinner.text = `
|
|
363
|
+
spinner.text = `Installing for: ${chalk2.cyan(agents.join(", "))}`;
|
|
144
364
|
const pm = detectPackageManager();
|
|
145
365
|
let yamlContent = MINIMAL_CHECKS_YAML;
|
|
146
|
-
if (!
|
|
147
|
-
const templatePath =
|
|
366
|
+
if (!existsSync4("checks.yaml")) {
|
|
367
|
+
const templatePath = getDefaultTemplatePath();
|
|
148
368
|
if (templatePath) {
|
|
149
|
-
yamlContent =
|
|
369
|
+
yamlContent = readFileSync3(templatePath, "utf8");
|
|
150
370
|
}
|
|
151
371
|
if (pm !== "pnpm") {
|
|
152
372
|
yamlContent = yamlContent.replace(/\bpnpm\b/g, pm);
|
|
153
373
|
}
|
|
154
|
-
|
|
374
|
+
writeFileSync3("checks.yaml", yamlContent, "utf8");
|
|
155
375
|
} else {
|
|
156
|
-
yamlContent =
|
|
376
|
+
yamlContent = readFileSync3("checks.yaml", "utf8");
|
|
157
377
|
}
|
|
158
378
|
const config = parseHoldpointYaml(yamlContent);
|
|
159
379
|
const generatedDir = ".github/holdpoint/generated";
|
|
160
|
-
|
|
161
|
-
|
|
380
|
+
mkdirSync2(generatedDir, { recursive: true });
|
|
381
|
+
writeFileSync3(`${generatedDir}/checks.immutable.json`, buildConfigJson(config), "utf8");
|
|
162
382
|
if (agents.includes("copilot")) {
|
|
163
383
|
const extDir = ".github/extensions/holdpoint";
|
|
164
|
-
|
|
165
|
-
|
|
384
|
+
mkdirSync2(extDir, { recursive: true });
|
|
385
|
+
writeFileSync3(join2(extDir, "extension.mjs"), buildEngine(config), "utf8");
|
|
386
|
+
spliceBreadcrumb(".github/copilot-instructions.md");
|
|
166
387
|
}
|
|
167
388
|
if (agents.includes("claude")) {
|
|
168
|
-
|
|
389
|
+
mkdirSync2(".claude", { recursive: true });
|
|
169
390
|
const settingsPath = ".claude/settings.json";
|
|
170
391
|
let existing = {};
|
|
171
|
-
if (
|
|
392
|
+
if (existsSync4(settingsPath)) {
|
|
172
393
|
try {
|
|
173
|
-
existing = JSON.parse(
|
|
394
|
+
existing = JSON.parse(readFileSync3(settingsPath, "utf8"));
|
|
174
395
|
} catch {
|
|
175
396
|
}
|
|
176
397
|
}
|
|
177
398
|
const holdpointHooks = JSON.parse(buildClaudeEngineJson(config));
|
|
178
|
-
|
|
399
|
+
writeFileSync3(
|
|
179
400
|
settingsPath,
|
|
180
|
-
JSON.stringify(
|
|
401
|
+
JSON.stringify(mergeClaudeSettings(existing, holdpointHooks), null, 2),
|
|
181
402
|
"utf8"
|
|
182
403
|
);
|
|
404
|
+
spliceBreadcrumb("CLAUDE.md");
|
|
183
405
|
}
|
|
184
406
|
if (agents.includes("cursor")) {
|
|
407
|
+
mkdirSync2(".cursor", { recursive: true });
|
|
408
|
+
const cursorHooksPath = ".cursor/hooks.json";
|
|
409
|
+
let existingHooks = {};
|
|
410
|
+
if (existsSync4(cursorHooksPath)) {
|
|
411
|
+
try {
|
|
412
|
+
existingHooks = JSON.parse(readFileSync3(cursorHooksPath, "utf8"));
|
|
413
|
+
} catch {
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
const cursorHooks = JSON.parse(buildCursorHooksJson(config));
|
|
417
|
+
writeFileSync3(
|
|
418
|
+
cursorHooksPath,
|
|
419
|
+
JSON.stringify(mergeCursorHooks(existingHooks, cursorHooks), null, 2) + "\n",
|
|
420
|
+
"utf8"
|
|
421
|
+
);
|
|
422
|
+
writeFileSync3(".cursor/holdpoint-hook.mjs", buildCursorCheckScript(), "utf8");
|
|
185
423
|
const cursorRules = buildCursorEngine(config);
|
|
186
424
|
const cursorPath = ".cursorrules";
|
|
187
|
-
if (
|
|
188
|
-
const existing =
|
|
425
|
+
if (existsSync4(cursorPath)) {
|
|
426
|
+
const existing = readFileSync3(cursorPath, "utf8");
|
|
189
427
|
if (!existing.includes("Holdpoint Rules")) {
|
|
190
|
-
|
|
428
|
+
writeFileSync3(cursorPath, `${existing.trimEnd()}
|
|
429
|
+
|
|
430
|
+
${cursorRules}`, "utf8");
|
|
191
431
|
}
|
|
192
432
|
} else {
|
|
193
|
-
|
|
433
|
+
writeFileSync3(cursorPath, cursorRules, "utf8");
|
|
194
434
|
}
|
|
435
|
+
spliceBreadcrumb(".cursor/rules/holdpoint.md");
|
|
195
436
|
}
|
|
196
437
|
if (agents.includes("codex")) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const existing = existsSync3(agentsMdPath) ? readFileSync2(agentsMdPath, "utf8") : "";
|
|
203
|
-
writeFileSync2(agentsMdPath, spliceAgentsMd(existing, config), "utf8");
|
|
438
|
+
mkdirSync2(".codex", { recursive: true });
|
|
439
|
+
writeFileSync3(".codex/hooks.json", buildCodexHooksJson(config), "utf8");
|
|
440
|
+
writeFileSync3(".codex/holdpoint-check.mjs", buildCodexCheckScript(config), "utf8");
|
|
441
|
+
writeFileSync3(".codex/config.toml", buildCodexConfigToml(), "utf8");
|
|
442
|
+
spliceBreadcrumb("AGENTS.md");
|
|
204
443
|
}
|
|
205
444
|
ensureBundledFile("MASTER_PROMPT.md", "MASTER_PROMPT.md", MINIMAL_MASTER_PROMPT);
|
|
445
|
+
ensureBundledFile(
|
|
446
|
+
"HOLDPOINT_REFERENCE.md",
|
|
447
|
+
"HOLDPOINT_REFERENCE.md",
|
|
448
|
+
MINIMAL_HOLDPOINT_REFERENCE
|
|
449
|
+
);
|
|
206
450
|
ensureBundledFile(
|
|
207
451
|
"HOLDPOINT_PREREQUISITES.md",
|
|
208
452
|
"HOLDPOINT_PREREQUISITES.md",
|
|
@@ -210,44 +454,42 @@ async function initCommand(options) {
|
|
|
210
454
|
);
|
|
211
455
|
spinner.text = "Installing holdpoint as a devDependency\u2026";
|
|
212
456
|
const installCmds = {
|
|
213
|
-
pnpm: "pnpm add -D holdpoint
|
|
214
|
-
yarn: "yarn add --dev holdpoint
|
|
215
|
-
npm: "npm install --save-dev holdpoint
|
|
457
|
+
pnpm: "pnpm add -D holdpoint",
|
|
458
|
+
yarn: "yarn add --dev holdpoint",
|
|
459
|
+
npm: "npm install --save-dev holdpoint"
|
|
216
460
|
};
|
|
217
461
|
const installCmd = installCmds[pm];
|
|
218
462
|
try {
|
|
219
|
-
|
|
220
|
-
spinner.succeed(
|
|
463
|
+
execSync2(installCmd, { stdio: "pipe" });
|
|
464
|
+
spinner.succeed(chalk2.bold.green("Holdpoint initialised!"));
|
|
221
465
|
} catch {
|
|
222
466
|
spinner.warn(
|
|
223
|
-
|
|
224
|
-
Run manually: ${
|
|
467
|
+
chalk2.yellow(`Holdpoint initialised, but could not install the package automatically.`) + `
|
|
468
|
+
Run manually: ${chalk2.yellow(installCmd)}`
|
|
225
469
|
);
|
|
226
470
|
}
|
|
471
|
+
const preflight = runPreflight(agents);
|
|
472
|
+
printPreflight(preflight);
|
|
227
473
|
console.log(`
|
|
228
|
-
${
|
|
229
|
-
1. Edit ${
|
|
230
|
-
2.
|
|
231
|
-
3. Commit ${
|
|
232
|
-
4. Run ${
|
|
474
|
+
${chalk2.cyan("Next steps:")}
|
|
475
|
+
1. Edit ${chalk2.yellow("checks.yaml")} to customise your eval checkpoints
|
|
476
|
+
2. Address any ${chalk2.yellow("\u2192")} items above (full notes in ${chalk2.yellow("HOLDPOINT_PREREQUISITES.md")})
|
|
477
|
+
3. Commit ${chalk2.yellow("checks.yaml")}, ${chalk2.yellow("HOLDPOINT_PREREQUISITES.md")}, and the generated engine files
|
|
478
|
+
4. Run ${chalk2.yellow("holdpoint check")} at any time to validate
|
|
233
479
|
|
|
234
|
-
|
|
235
|
-
${
|
|
236
|
-
See ${chalk.yellow("HOLDPOINT_PREREQUISITES.md")} for the full handoff notes.
|
|
237
|
-
|
|
238
|
-
Visual builder: ${chalk.yellow("holdpoint builder")} (opens localhost:4321)
|
|
239
|
-
Stack: ${chalk.cyan(stack)} Agents: ${chalk.cyan(agents.join(", "))}
|
|
480
|
+
Visual builder: ${chalk2.yellow("holdpoint builder")} (opens the daemon at /builder)
|
|
481
|
+
Agents: ${chalk2.cyan(agents.join(", "))}
|
|
240
482
|
`);
|
|
241
483
|
}
|
|
242
484
|
|
|
243
485
|
// src/commands/check.ts
|
|
244
|
-
import { existsSync as
|
|
486
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3 } from "fs";
|
|
245
487
|
import { join as join3 } from "path";
|
|
246
|
-
import
|
|
488
|
+
import chalk3 from "chalk";
|
|
247
489
|
import ora2 from "ora";
|
|
248
490
|
import { parseHoldpointYaml as parseHoldpointYaml2, matchesWhen } from "@holdpoint/yaml-core";
|
|
249
491
|
import { runDeterministicChecks } from "@holdpoint/yaml-core/runner";
|
|
250
|
-
import { execSync as
|
|
492
|
+
import { execSync as execSync3 } from "child_process";
|
|
251
493
|
import { randomUUID } from "crypto";
|
|
252
494
|
import { identifyProject } from "@holdpoint/live-daemon";
|
|
253
495
|
import { BridgeClient } from "@holdpoint/sdk";
|
|
@@ -257,7 +499,7 @@ var CHECK_REPORTS_PATH = ".holdpoint/check-reports.json";
|
|
|
257
499
|
var CHECK_REPORTS_MAX = 50;
|
|
258
500
|
function getStagedFiles() {
|
|
259
501
|
try {
|
|
260
|
-
const out =
|
|
502
|
+
const out = execSync3("git diff --cached --name-only", {
|
|
261
503
|
encoding: "utf8",
|
|
262
504
|
stdio: ["pipe", "pipe", "ignore"]
|
|
263
505
|
});
|
|
@@ -268,7 +510,7 @@ function getStagedFiles() {
|
|
|
268
510
|
}
|
|
269
511
|
function getAllChangedFiles() {
|
|
270
512
|
try {
|
|
271
|
-
const out =
|
|
513
|
+
const out = execSync3("git diff --name-only HEAD", {
|
|
272
514
|
encoding: "utf8",
|
|
273
515
|
stdio: ["pipe", "pipe", "ignore"]
|
|
274
516
|
});
|
|
@@ -279,7 +521,7 @@ function getAllChangedFiles() {
|
|
|
279
521
|
}
|
|
280
522
|
function getLastCommitFiles() {
|
|
281
523
|
try {
|
|
282
|
-
const out =
|
|
524
|
+
const out = execSync3("git diff --name-only HEAD~1 HEAD", {
|
|
283
525
|
encoding: "utf8",
|
|
284
526
|
stdio: ["pipe", "pipe", "ignore"]
|
|
285
527
|
});
|
|
@@ -290,7 +532,7 @@ function getLastCommitFiles() {
|
|
|
290
532
|
}
|
|
291
533
|
function getHeadSha() {
|
|
292
534
|
try {
|
|
293
|
-
return
|
|
535
|
+
return execSync3("git rev-parse HEAD", {
|
|
294
536
|
encoding: "utf8",
|
|
295
537
|
stdio: ["pipe", "pipe", "ignore"]
|
|
296
538
|
}).trim();
|
|
@@ -300,7 +542,7 @@ function getHeadSha() {
|
|
|
300
542
|
}
|
|
301
543
|
function readCommitCache() {
|
|
302
544
|
try {
|
|
303
|
-
const raw =
|
|
545
|
+
const raw = readFileSync4(COMMIT_CACHE_PATH, "utf8");
|
|
304
546
|
const parsed = JSON.parse(raw);
|
|
305
547
|
return new Set(Array.isArray(parsed.verified) ? parsed.verified : []);
|
|
306
548
|
} catch {
|
|
@@ -311,18 +553,18 @@ function recordCommitCache(sha) {
|
|
|
311
553
|
try {
|
|
312
554
|
const existing = readCommitCache();
|
|
313
555
|
const updated = [sha, ...[...existing].filter((s) => s !== sha)].slice(0, COMMIT_CACHE_MAX);
|
|
314
|
-
|
|
315
|
-
|
|
556
|
+
mkdirSync3(join3(COMMIT_CACHE_PATH, ".."), { recursive: true });
|
|
557
|
+
writeFileSync4(COMMIT_CACHE_PATH, JSON.stringify({ verified: updated }, null, 2) + "\n", "utf8");
|
|
316
558
|
} catch {
|
|
317
559
|
}
|
|
318
560
|
}
|
|
319
561
|
function recordCheckReport(run) {
|
|
320
562
|
try {
|
|
321
|
-
|
|
563
|
+
mkdirSync3(join3(CHECK_REPORTS_PATH, ".."), { recursive: true });
|
|
322
564
|
let existing = { runs: [] };
|
|
323
|
-
if (
|
|
565
|
+
if (existsSync5(CHECK_REPORTS_PATH)) {
|
|
324
566
|
try {
|
|
325
|
-
existing = JSON.parse(
|
|
567
|
+
existing = JSON.parse(readFileSync4(CHECK_REPORTS_PATH, "utf8"));
|
|
326
568
|
if (!Array.isArray(existing.runs)) existing.runs = [];
|
|
327
569
|
} catch {
|
|
328
570
|
existing = { runs: [] };
|
|
@@ -331,21 +573,21 @@ function recordCheckReport(run) {
|
|
|
331
573
|
const updated = {
|
|
332
574
|
runs: [run, ...existing.runs].slice(0, CHECK_REPORTS_MAX)
|
|
333
575
|
};
|
|
334
|
-
|
|
576
|
+
writeFileSync4(CHECK_REPORTS_PATH, JSON.stringify(updated, null, 2) + "\n", "utf8");
|
|
335
577
|
} catch {
|
|
336
578
|
}
|
|
337
579
|
}
|
|
338
580
|
async function checkCommand(options) {
|
|
339
|
-
if (!
|
|
340
|
-
console.error(
|
|
581
|
+
if (!existsSync5("checks.yaml")) {
|
|
582
|
+
console.error(chalk3.red("No checks.yaml found. Run `holdpoint init` first."));
|
|
341
583
|
process.exit(1);
|
|
342
584
|
}
|
|
343
|
-
const yamlContent =
|
|
585
|
+
const yamlContent = readFileSync4("checks.yaml", "utf8");
|
|
344
586
|
let config;
|
|
345
587
|
try {
|
|
346
588
|
config = parseHoldpointYaml2(yamlContent);
|
|
347
589
|
} catch (err) {
|
|
348
|
-
console.error(
|
|
590
|
+
console.error(chalk3.red("Invalid checks.yaml:"), err.message);
|
|
349
591
|
process.exit(1);
|
|
350
592
|
}
|
|
351
593
|
const headSha = getHeadSha();
|
|
@@ -358,7 +600,7 @@ async function checkCommand(options) {
|
|
|
358
600
|
} else {
|
|
359
601
|
if (headSha && readCommitCache().has(headSha)) {
|
|
360
602
|
console.log(
|
|
361
|
-
|
|
603
|
+
chalk3.green(`\u2713 Commit ${headSha.slice(0, 8)} already verified \u2014 nothing to re-check.`)
|
|
362
604
|
);
|
|
363
605
|
process.exit(0);
|
|
364
606
|
}
|
|
@@ -367,10 +609,10 @@ async function checkCommand(options) {
|
|
|
367
609
|
changedFiles = lastCommit;
|
|
368
610
|
usedHeadShaForCache = true;
|
|
369
611
|
console.log(
|
|
370
|
-
|
|
612
|
+
chalk3.yellow("No staged files. Running checks scoped to the most recent commit's files.")
|
|
371
613
|
);
|
|
372
614
|
} else {
|
|
373
|
-
console.log(
|
|
615
|
+
console.log(chalk3.green("\u2713 No staged changes and no recent commit \u2014 nothing to check."));
|
|
374
616
|
process.exit(0);
|
|
375
617
|
}
|
|
376
618
|
}
|
|
@@ -378,10 +620,10 @@ async function checkCommand(options) {
|
|
|
378
620
|
changedFiles = getAllChangedFiles();
|
|
379
621
|
if (changedFiles.length === 0) {
|
|
380
622
|
console.log(
|
|
381
|
-
|
|
623
|
+
chalk3.yellow("No changed files detected. Running all checks with no file filter.")
|
|
382
624
|
);
|
|
383
625
|
console.log(
|
|
384
|
-
|
|
626
|
+
chalk3.dim(
|
|
385
627
|
" Tip: if you just ran `holdpoint init`, commit the generated files to clear the git-commit check."
|
|
386
628
|
)
|
|
387
629
|
);
|
|
@@ -389,9 +631,9 @@ async function checkCommand(options) {
|
|
|
389
631
|
}
|
|
390
632
|
const guides = Object.entries(config.context?.guides ?? {});
|
|
391
633
|
if (guides.length > 0) {
|
|
392
|
-
console.log(
|
|
634
|
+
console.log(chalk3.cyan("\nProject guides:"));
|
|
393
635
|
for (const [key, text] of guides) {
|
|
394
|
-
console.log(
|
|
636
|
+
console.log(chalk3.bold(` ${key}:`), chalk3.dim(String(text).trim()));
|
|
395
637
|
}
|
|
396
638
|
console.log("");
|
|
397
639
|
}
|
|
@@ -409,9 +651,9 @@ async function checkCommand(options) {
|
|
|
409
651
|
console.log("");
|
|
410
652
|
console.log(
|
|
411
653
|
[
|
|
412
|
-
|
|
413
|
-
failed.length > 0 ?
|
|
414
|
-
skipped.length > 0 ?
|
|
654
|
+
chalk3.green(`\u2713 ${passed.length} passed`),
|
|
655
|
+
failed.length > 0 ? chalk3.red(`\u2717 ${failed.length} failed`) : "",
|
|
656
|
+
skipped.length > 0 ? chalk3.gray(`\u25CC ${skipped.length} skipped`) : ""
|
|
415
657
|
].filter(Boolean).join(" ")
|
|
416
658
|
);
|
|
417
659
|
const promptChecks = config.checks.filter(
|
|
@@ -419,9 +661,9 @@ async function checkCommand(options) {
|
|
|
419
661
|
);
|
|
420
662
|
if (promptChecks.length > 0) {
|
|
421
663
|
console.log(`
|
|
422
|
-
${
|
|
664
|
+
${chalk3.cyan("Agent prompts to act on:")}`);
|
|
423
665
|
for (const c of promptChecks) {
|
|
424
|
-
console.log(` ${
|
|
666
|
+
console.log(` ${chalk3.yellow("\u25A1")} [${c.label}] ${c.prompt ?? ""}`);
|
|
425
667
|
}
|
|
426
668
|
}
|
|
427
669
|
const reportResults = [
|
|
@@ -487,151 +729,195 @@ ${chalk2.cyan("Agent prompts to act on:")}`);
|
|
|
487
729
|
}
|
|
488
730
|
}
|
|
489
731
|
function printResult(result) {
|
|
490
|
-
const icon = result.status === "pass" ?
|
|
732
|
+
const icon = result.status === "pass" ? chalk3.green("\u2713") : result.status === "fail" ? chalk3.red("\u2717") : result.status === "skip" ? chalk3.gray("\u25CC") : chalk3.yellow("\u2026");
|
|
491
733
|
const label = result.check.label;
|
|
492
734
|
console.log(`${icon} ${label}`);
|
|
493
735
|
if (result.status === "fail" && result.output) {
|
|
494
736
|
const trimmed = result.output.trim().split("\n").slice(0, 10).join("\n");
|
|
495
|
-
console.log(
|
|
737
|
+
console.log(chalk3.dim(trimmed.replace(/^/gm, " ")));
|
|
496
738
|
}
|
|
497
739
|
if (result.status === "skip" && result.skipReason) {
|
|
498
|
-
console.log(
|
|
740
|
+
console.log(chalk3.dim(` ${result.skipReason}`));
|
|
499
741
|
}
|
|
500
742
|
}
|
|
501
743
|
|
|
502
744
|
// src/commands/validate.ts
|
|
503
|
-
import { existsSync as
|
|
504
|
-
import
|
|
745
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
746
|
+
import chalk4 from "chalk";
|
|
505
747
|
import { parseHoldpointYaml as parseHoldpointYaml3, validateConfig } from "@holdpoint/yaml-core";
|
|
506
748
|
async function validateCommand() {
|
|
507
|
-
if (!
|
|
508
|
-
console.error(
|
|
749
|
+
if (!existsSync6("checks.yaml")) {
|
|
750
|
+
console.error(chalk4.red("No checks.yaml found. Run `holdpoint init` first."));
|
|
509
751
|
process.exit(1);
|
|
510
752
|
}
|
|
511
|
-
const text =
|
|
753
|
+
const text = readFileSync5("checks.yaml", "utf8");
|
|
512
754
|
let config;
|
|
513
755
|
try {
|
|
514
756
|
config = parseHoldpointYaml3(text);
|
|
515
757
|
} catch (err) {
|
|
516
|
-
console.error(
|
|
758
|
+
console.error(chalk4.red("Parse error:"), err.message);
|
|
517
759
|
process.exit(1);
|
|
518
760
|
}
|
|
519
761
|
const result = validateConfig(config);
|
|
520
762
|
if (result.valid) {
|
|
521
|
-
console.log(
|
|
763
|
+
console.log(chalk4.green("\u2713 checks.yaml is valid"));
|
|
522
764
|
console.log(
|
|
523
|
-
|
|
765
|
+
chalk4.dim(
|
|
524
766
|
` ${config.checks.filter((c) => c.cmd !== void 0).length} tasks, ${config.checks.filter((c) => c.prompt !== void 0).length} prompts, ${config.conditions.length} conditions`
|
|
525
767
|
)
|
|
526
768
|
);
|
|
527
769
|
} else {
|
|
528
|
-
console.error(
|
|
770
|
+
console.error(chalk4.red("\u2717 checks.yaml has errors:"));
|
|
529
771
|
for (const err of result.errors) {
|
|
530
|
-
console.error(` ${
|
|
772
|
+
console.error(` ${chalk4.yellow(err.path)}: ${err.message}`);
|
|
531
773
|
}
|
|
532
774
|
process.exit(1);
|
|
533
775
|
}
|
|
534
776
|
}
|
|
535
777
|
|
|
536
778
|
// src/commands/update.ts
|
|
537
|
-
import { existsSync as
|
|
538
|
-
import
|
|
779
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4 } from "fs";
|
|
780
|
+
import chalk5 from "chalk";
|
|
539
781
|
import ora3 from "ora";
|
|
540
782
|
import { parseHoldpointYaml as parseHoldpointYaml4 } from "@holdpoint/yaml-core";
|
|
541
783
|
import { buildConfigJson as buildConfigJson2, buildEngine as buildEngine2 } from "@holdpoint/engine-copilot";
|
|
542
784
|
import { buildEngineJson as buildClaudeEngineJson2 } from "@holdpoint/engine-claude";
|
|
543
|
-
import {
|
|
785
|
+
import {
|
|
786
|
+
buildCheckScript as buildCursorCheckScript2,
|
|
787
|
+
buildEngine as buildCursorEngine2,
|
|
788
|
+
buildHooksJson as buildCursorHooksJson2
|
|
789
|
+
} from "@holdpoint/engine-cursor";
|
|
544
790
|
import {
|
|
545
791
|
buildConfigToml as buildCodexConfigToml2,
|
|
546
792
|
buildHooksJson as buildCodexHooksJson2,
|
|
547
|
-
buildCheckScript as buildCodexCheckScript2
|
|
548
|
-
spliceAgentsMd as spliceAgentsMd2
|
|
793
|
+
buildCheckScript as buildCodexCheckScript2
|
|
549
794
|
} from "@holdpoint/engine-codex";
|
|
550
795
|
var MINIMAL_PREREQUISITES2 = `# Holdpoint prerequisites
|
|
551
796
|
|
|
552
|
-
Holdpoint installed repo-local
|
|
797
|
+
Holdpoint installed repo-local engine integrations for one or more AI coding agents. Before relying on them locally, review these setup notes:
|
|
553
798
|
|
|
554
799
|
- **GitHub Copilot CLI** \u2014 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.
|
|
800
|
+
- **Cursor** \u2014 project-level hooks run in trusted workspaces. After opening the repo in Cursor, confirm the workspace is trusted and review Settings \u2192 Hooks if hooks do not fire.
|
|
555
801
|
- **OpenAI Codex** \u2014 project-level hooks require trust approval. Run \`codex trust\` in the Codex TUI or review the hook with \`/hooks\`.
|
|
556
802
|
- **General** \u2014 Holdpoint expects Node.js 18+ and a git repository so \`holdpoint init\`, \`holdpoint update\`, and \`holdpoint check\` can run normally.
|
|
557
803
|
|
|
558
804
|
Docs: https://holdpoint.dev/docs
|
|
559
805
|
`;
|
|
806
|
+
var MINIMAL_HOLDPOINT_REFERENCE2 = `# Holdpoint reference
|
|
807
|
+
|
|
808
|
+
Read \`MASTER_PROMPT.md\` first for the mandatory workflow, then use this file for deeper project-specific Holdpoint notes.
|
|
809
|
+
`;
|
|
560
810
|
async function updateCommand() {
|
|
561
|
-
if (!
|
|
562
|
-
console.error(
|
|
811
|
+
if (!existsSync7("checks.yaml")) {
|
|
812
|
+
console.error(chalk5.red("No checks.yaml found. Run `holdpoint init` first."));
|
|
563
813
|
process.exit(1);
|
|
564
814
|
}
|
|
565
815
|
const spinner = ora3("Updating Holdpoint engine files\u2026").start();
|
|
566
|
-
const config = parseHoldpointYaml4(
|
|
816
|
+
const config = parseHoldpointYaml4(readFileSync6("checks.yaml", "utf8"));
|
|
567
817
|
const detected = detectInstalledAgents();
|
|
568
818
|
const agents = detected.length > 0 ? detected : ["copilot", "claude", "cursor", "codex"];
|
|
569
819
|
const generatedDir = ".github/holdpoint/generated";
|
|
570
|
-
|
|
571
|
-
|
|
820
|
+
mkdirSync4(generatedDir, { recursive: true });
|
|
821
|
+
writeFileSync5(`${generatedDir}/checks.immutable.json`, buildConfigJson2(config), "utf8");
|
|
572
822
|
if (agents.includes("copilot")) {
|
|
573
823
|
const extDir = ".github/extensions/holdpoint";
|
|
574
|
-
|
|
575
|
-
|
|
824
|
+
mkdirSync4(extDir, { recursive: true });
|
|
825
|
+
writeFileSync5(`${extDir}/extension.mjs`, buildEngine2(config), "utf8");
|
|
826
|
+
spliceBreadcrumb(".github/copilot-instructions.md");
|
|
576
827
|
}
|
|
577
828
|
if (agents.includes("claude")) {
|
|
578
|
-
|
|
829
|
+
mkdirSync4(".claude", { recursive: true });
|
|
579
830
|
const settingsPath = ".claude/settings.json";
|
|
580
831
|
let existing = {};
|
|
581
|
-
if (
|
|
832
|
+
if (existsSync7(settingsPath)) {
|
|
582
833
|
try {
|
|
583
|
-
existing = JSON.parse(
|
|
834
|
+
existing = JSON.parse(readFileSync6(settingsPath, "utf8"));
|
|
584
835
|
} catch {
|
|
585
836
|
}
|
|
586
837
|
}
|
|
587
838
|
const hooks = JSON.parse(buildClaudeEngineJson2(config));
|
|
588
|
-
|
|
839
|
+
writeFileSync5(
|
|
589
840
|
settingsPath,
|
|
590
|
-
JSON.stringify(
|
|
841
|
+
JSON.stringify(mergeClaudeSettings(existing, hooks), null, 2) + "\n"
|
|
591
842
|
);
|
|
843
|
+
spliceBreadcrumb("CLAUDE.md");
|
|
592
844
|
}
|
|
593
845
|
if (agents.includes("cursor")) {
|
|
846
|
+
mkdirSync4(".cursor", { recursive: true });
|
|
847
|
+
const cursorHooksPath = ".cursor/hooks.json";
|
|
848
|
+
let existingHooks = {};
|
|
849
|
+
if (existsSync7(cursorHooksPath)) {
|
|
850
|
+
try {
|
|
851
|
+
existingHooks = JSON.parse(readFileSync6(cursorHooksPath, "utf8"));
|
|
852
|
+
} catch {
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
const cursorHooks = JSON.parse(buildCursorHooksJson2(config));
|
|
856
|
+
writeFileSync5(
|
|
857
|
+
cursorHooksPath,
|
|
858
|
+
JSON.stringify(mergeCursorHooks(existingHooks, cursorHooks), null, 2) + "\n",
|
|
859
|
+
"utf8"
|
|
860
|
+
);
|
|
861
|
+
writeFileSync5(".cursor/holdpoint-hook.mjs", buildCursorCheckScript2(), "utf8");
|
|
594
862
|
const cursorRules = buildCursorEngine2(config);
|
|
595
863
|
const cursorPath = ".cursorrules";
|
|
596
|
-
if (
|
|
597
|
-
const content =
|
|
864
|
+
if (existsSync7(cursorPath)) {
|
|
865
|
+
const content = readFileSync6(cursorPath, "utf8");
|
|
598
866
|
const start = content.indexOf("# \u2500\u2500\u2500 Holdpoint Rules");
|
|
599
867
|
const end = content.indexOf("# \u2500\u2500\u2500 End Holdpoint Rules \u2500\u2500\u2500");
|
|
600
868
|
if (start !== -1 && end !== -1) {
|
|
601
869
|
const afterEnd = content.indexOf("\n", end);
|
|
602
|
-
const
|
|
603
|
-
|
|
870
|
+
const prefix = content.slice(0, start).trimEnd();
|
|
871
|
+
const suffix = content.slice(afterEnd === -1 ? end : afterEnd + 1).trimStart();
|
|
872
|
+
const updated = (prefix ? `${prefix}
|
|
873
|
+
|
|
874
|
+
` : "") + cursorRules + (suffix ? `
|
|
875
|
+
${suffix}` : "");
|
|
876
|
+
writeFileSync5(cursorPath, updated);
|
|
604
877
|
} else {
|
|
605
|
-
|
|
878
|
+
writeFileSync5(cursorPath, `${content.trimEnd()}
|
|
879
|
+
|
|
880
|
+
${cursorRules}`);
|
|
606
881
|
}
|
|
882
|
+
} else {
|
|
883
|
+
writeFileSync5(cursorPath, cursorRules);
|
|
607
884
|
}
|
|
885
|
+
spliceBreadcrumb(".cursor/rules/holdpoint.md");
|
|
608
886
|
}
|
|
609
887
|
if (agents.includes("codex")) {
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
888
|
+
mkdirSync4(".codex", { recursive: true });
|
|
889
|
+
writeFileSync5(".codex/hooks.json", buildCodexHooksJson2(config), "utf8");
|
|
890
|
+
writeFileSync5(".codex/holdpoint-check.mjs", buildCodexCheckScript2(config), "utf8");
|
|
613
891
|
const configTomlPath = ".codex/config.toml";
|
|
614
|
-
if (!
|
|
615
|
-
|
|
892
|
+
if (!existsSync7(configTomlPath)) {
|
|
893
|
+
writeFileSync5(configTomlPath, buildCodexConfigToml2(), "utf8");
|
|
616
894
|
} else {
|
|
617
|
-
const
|
|
618
|
-
if (!
|
|
619
|
-
|
|
895
|
+
const existing = readFileSync6(configTomlPath, "utf8");
|
|
896
|
+
if (!existing.includes("[features]")) {
|
|
897
|
+
writeFileSync5(configTomlPath, existing.trimEnd() + "\n\n" + buildCodexConfigToml2(), "utf8");
|
|
620
898
|
}
|
|
621
899
|
}
|
|
622
|
-
|
|
623
|
-
const existing = existsSync6(agentsMdPath) ? readFileSync5(agentsMdPath, "utf8") : "";
|
|
624
|
-
writeFileSync4(agentsMdPath, spliceAgentsMd2(existing, config), "utf8");
|
|
900
|
+
spliceBreadcrumb("AGENTS.md");
|
|
625
901
|
}
|
|
902
|
+
const wroteReference = ensureBundledFile(
|
|
903
|
+
"HOLDPOINT_REFERENCE.md",
|
|
904
|
+
"HOLDPOINT_REFERENCE.md",
|
|
905
|
+
MINIMAL_HOLDPOINT_REFERENCE2
|
|
906
|
+
);
|
|
626
907
|
const wrotePrerequisites = ensureBundledFile(
|
|
627
908
|
"HOLDPOINT_PREREQUISITES.md",
|
|
628
909
|
"HOLDPOINT_PREREQUISITES.md",
|
|
629
910
|
MINIMAL_PREREQUISITES2
|
|
630
911
|
);
|
|
631
|
-
spinner.succeed(
|
|
912
|
+
spinner.succeed(chalk5.green("Engine files updated from current checks.yaml"));
|
|
913
|
+
if (wroteReference) {
|
|
914
|
+
console.log(
|
|
915
|
+
chalk5.cyan("Created HOLDPOINT_REFERENCE.md with the full Holdpoint workflow reference.")
|
|
916
|
+
);
|
|
917
|
+
}
|
|
632
918
|
if (wrotePrerequisites) {
|
|
633
919
|
console.log(
|
|
634
|
-
|
|
920
|
+
chalk5.cyan(
|
|
635
921
|
"Created HOLDPOINT_PREREQUISITES.md with Copilot experimental-mode and other agent setup notes."
|
|
636
922
|
)
|
|
637
923
|
);
|
|
@@ -639,118 +925,131 @@ async function updateCommand() {
|
|
|
639
925
|
}
|
|
640
926
|
|
|
641
927
|
// src/commands/build.ts
|
|
642
|
-
import
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
import {
|
|
646
|
-
import {
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
const checksPath = join4(process.cwd(), "checks.yaml");
|
|
671
|
-
if (existsSync7(checksPath)) {
|
|
672
|
-
res.writeHead(200, { "Content-Type": "text/yaml; charset=utf-8" });
|
|
673
|
-
createReadStream(checksPath).pipe(res);
|
|
674
|
-
} else {
|
|
675
|
-
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
676
|
-
res.end("checks.yaml not found in current directory");
|
|
928
|
+
import chalk6 from "chalk";
|
|
929
|
+
|
|
930
|
+
// src/lib/ensure-daemon.ts
|
|
931
|
+
import { spawn } from "child_process";
|
|
932
|
+
import { readHealthyDaemonLock } from "@holdpoint/live-daemon";
|
|
933
|
+
function sleep(ms) {
|
|
934
|
+
return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
|
|
935
|
+
}
|
|
936
|
+
async function ensureDaemon(timeoutMs = 5e3) {
|
|
937
|
+
const existing = await readHealthyDaemonLock();
|
|
938
|
+
if (existing) {
|
|
939
|
+
return { info: existing, started: false };
|
|
940
|
+
}
|
|
941
|
+
const cliEntry = process.argv[1];
|
|
942
|
+
if (!cliEntry) {
|
|
943
|
+
throw new Error("Cannot determine the current holdpoint CLI entrypoint");
|
|
944
|
+
}
|
|
945
|
+
const child = spawn(process.execPath, [cliEntry, "daemon-serve"], {
|
|
946
|
+
stdio: "ignore",
|
|
947
|
+
env: process.env,
|
|
948
|
+
cwd: process.cwd()
|
|
949
|
+
});
|
|
950
|
+
child.unref();
|
|
951
|
+
const deadline = Date.now() + timeoutMs;
|
|
952
|
+
while (Date.now() < deadline) {
|
|
953
|
+
const lock = await readHealthyDaemonLock();
|
|
954
|
+
if (lock) {
|
|
955
|
+
return { info: lock, started: true };
|
|
677
956
|
}
|
|
678
|
-
|
|
957
|
+
await sleep(100);
|
|
679
958
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
959
|
+
throw new Error("Daemon unavailable + cannot spawn");
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// src/lib/open-browser.ts
|
|
963
|
+
import { execSync as execSync4 } from "child_process";
|
|
964
|
+
function openBrowser(url) {
|
|
965
|
+
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
966
|
+
try {
|
|
967
|
+
execSync4(`${openCmd} ${JSON.stringify(url)}`, { stdio: "ignore" });
|
|
968
|
+
} catch {
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// src/lib/project.ts
|
|
973
|
+
import { existsSync as existsSync8 } from "fs";
|
|
974
|
+
import { dirname as dirname4, join as join4 } from "path";
|
|
975
|
+
import { identifyProject as identifyProject2 } from "@holdpoint/live-daemon";
|
|
976
|
+
function findChecksYaml(startDir) {
|
|
977
|
+
let current = startDir;
|
|
978
|
+
for (; ; ) {
|
|
979
|
+
const candidate = join4(current, "checks.yaml");
|
|
980
|
+
if (existsSync8(candidate)) {
|
|
981
|
+
return candidate;
|
|
982
|
+
}
|
|
983
|
+
const parent = dirname4(current);
|
|
984
|
+
if (parent === current) {
|
|
985
|
+
return null;
|
|
688
986
|
}
|
|
987
|
+
current = parent;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
function tryResolveCurrentProject() {
|
|
991
|
+
const checksYaml = findChecksYaml(process.cwd());
|
|
992
|
+
if (checksYaml) {
|
|
993
|
+
return identifyProject2(dirname4(checksYaml));
|
|
994
|
+
}
|
|
995
|
+
try {
|
|
996
|
+
return identifyProject2(process.cwd());
|
|
997
|
+
} catch {
|
|
998
|
+
return null;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
function appendProjectAuthParams(url, project) {
|
|
1002
|
+
if (!project) {
|
|
689
1003
|
return;
|
|
690
1004
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
1005
|
+
url.searchParams.set("project", project.hash);
|
|
1006
|
+
url.searchParams.set("name", project.name);
|
|
1007
|
+
url.searchParams.set("root", project.root);
|
|
694
1008
|
}
|
|
1009
|
+
|
|
1010
|
+
// src/commands/build.ts
|
|
695
1011
|
async function buildCommand() {
|
|
696
|
-
const
|
|
697
|
-
const
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
`
|
|
709
|
-
${chalk5.green("\u2713")} Holdpoint builder running at ${chalk5.cyan(`http://localhost:${port}`)}`
|
|
710
|
-
);
|
|
711
|
-
console.log(chalk5.dim(" Edit checks.yaml, then reload the page to see updates"));
|
|
712
|
-
console.log(chalk5.dim(" Press Ctrl+C to stop\n"));
|
|
713
|
-
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
714
|
-
try {
|
|
715
|
-
execSync3(`${openCmd} http://localhost:${port}`, { stdio: "ignore" });
|
|
716
|
-
} catch {
|
|
717
|
-
}
|
|
718
|
-
});
|
|
719
|
-
server.on("error", reject);
|
|
720
|
-
process.on("SIGINT", () => {
|
|
721
|
-
console.log(chalk5.dim("\n Stopping builder\u2026"));
|
|
722
|
-
server.close(() => resolve2());
|
|
723
|
-
});
|
|
724
|
-
});
|
|
1012
|
+
const { info, started } = await ensureDaemon();
|
|
1013
|
+
const url = new URL("/__holdpoint/live-auth", `http://127.0.0.1:${info.port}`);
|
|
1014
|
+
url.searchParams.set("token", info.token);
|
|
1015
|
+
url.searchParams.set("path", "/builder/");
|
|
1016
|
+
appendProjectAuthParams(url, tryResolveCurrentProject());
|
|
1017
|
+
openBrowser(url.toString());
|
|
1018
|
+
console.log(
|
|
1019
|
+
chalk6.green(
|
|
1020
|
+
started ? "\u2713 Started Holdpoint Live and opened the builder" : "\u2713 Opened Holdpoint builder"
|
|
1021
|
+
)
|
|
1022
|
+
);
|
|
1023
|
+
console.log(` url: ${chalk6.cyan(url.toString())}`);
|
|
725
1024
|
}
|
|
726
1025
|
|
|
727
1026
|
// src/commands/evolve.ts
|
|
728
|
-
import { existsSync as
|
|
729
|
-
import { execSync as
|
|
730
|
-
import
|
|
1027
|
+
import { existsSync as existsSync11, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
|
|
1028
|
+
import { execSync as execSync7 } from "child_process";
|
|
1029
|
+
import chalk7 from "chalk";
|
|
731
1030
|
import ora4 from "ora";
|
|
732
1031
|
import { parseHoldpointYaml as parseHoldpointYaml5, generateYaml } from "@holdpoint/yaml-core";
|
|
733
1032
|
|
|
734
1033
|
// src/evolve/scanner.ts
|
|
735
|
-
import { existsSync as
|
|
1034
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7, readdirSync } from "fs";
|
|
736
1035
|
import { join as join5 } from "path";
|
|
737
|
-
import { execSync as
|
|
1036
|
+
import { execSync as execSync5 } from "child_process";
|
|
738
1037
|
function tryReadJson(path) {
|
|
739
1038
|
try {
|
|
740
|
-
return JSON.parse(
|
|
1039
|
+
return JSON.parse(readFileSync7(path, "utf8"));
|
|
741
1040
|
} catch {
|
|
742
1041
|
return null;
|
|
743
1042
|
}
|
|
744
1043
|
}
|
|
745
1044
|
function tryReadText(path) {
|
|
746
1045
|
try {
|
|
747
|
-
return
|
|
1046
|
+
return readFileSync7(path, "utf8");
|
|
748
1047
|
} catch {
|
|
749
1048
|
return "";
|
|
750
1049
|
}
|
|
751
1050
|
}
|
|
752
1051
|
function scanProject(cwd = process.cwd()) {
|
|
753
|
-
const exists = (p) =>
|
|
1052
|
+
const exists = (p) => existsSync9(join5(cwd, p));
|
|
754
1053
|
const packageManager = exists("pnpm-lock.yaml") ? "pnpm" : exists("yarn.lock") ? "yarn" : exists("bun.lockb") ? "bun" : "npm";
|
|
755
1054
|
const pkg = tryReadJson(join5(cwd, "package.json"));
|
|
756
1055
|
const scripts = pkg?.scripts ?? {};
|
|
@@ -812,8 +1111,8 @@ function scanProject(cwd = process.cwd()) {
|
|
|
812
1111
|
}
|
|
813
1112
|
|
|
814
1113
|
// src/evolve/dead-checker.ts
|
|
815
|
-
import { execSync as
|
|
816
|
-
import { readdirSync as readdirSync2, existsSync as
|
|
1114
|
+
import { execSync as execSync6 } from "child_process";
|
|
1115
|
+
import { readdirSync as readdirSync2, existsSync as existsSync10 } from "fs";
|
|
817
1116
|
import { join as join6 } from "path";
|
|
818
1117
|
var NAMED_SCOPES = /* @__PURE__ */ new Set([
|
|
819
1118
|
"frontend",
|
|
@@ -869,7 +1168,7 @@ function walkDir(dir, root, depth, maxDepth) {
|
|
|
869
1168
|
}
|
|
870
1169
|
function getRepoFiles(cwd) {
|
|
871
1170
|
try {
|
|
872
|
-
const out =
|
|
1171
|
+
const out = execSync6("git ls-files", {
|
|
873
1172
|
cwd,
|
|
874
1173
|
encoding: "utf8",
|
|
875
1174
|
stdio: ["pipe", "pipe", "ignore"]
|
|
@@ -906,7 +1205,7 @@ function detectStaleChecks(config, repoFiles) {
|
|
|
906
1205
|
if (matches.length === 0) {
|
|
907
1206
|
const label = patternAlias ? `Pattern '${patternAlias}' (= '${regexStr}')` : `Regex '${regexStr}'`;
|
|
908
1207
|
const suggestedConditionPath = extractPathFromRegex(regexStr);
|
|
909
|
-
const pathGone = !suggestedConditionPath || !
|
|
1208
|
+
const pathGone = !suggestedConditionPath || !existsSync10(join6(process.cwd(), suggestedConditionPath));
|
|
910
1209
|
if (pathGone) {
|
|
911
1210
|
stale.push({
|
|
912
1211
|
check,
|
|
@@ -925,6 +1224,9 @@ function pmScript(profile, script, fallback) {
|
|
|
925
1224
|
if (profile.packageManager === "npm") return `npm run ${script}`;
|
|
926
1225
|
return `${profile.packageManager} ${script}`;
|
|
927
1226
|
}
|
|
1227
|
+
var blockedMarkerTerms = ["TODO", "FIXME", "HACK", "XXX"];
|
|
1228
|
+
var blockedMarkerLabel = `No ${blockedMarkerTerms[0]}/${blockedMarkerTerms[1]} left in changed code`;
|
|
1229
|
+
var blockedMarkerPrompt = `Scan the files you changed for any ${blockedMarkerTerms.join(", ")} comments. Either resolve them before finishing or convert them to GitHub issues. Don't leave incomplete work silently behind.`;
|
|
928
1230
|
function getTemplates(profile) {
|
|
929
1231
|
return [
|
|
930
1232
|
// ── Universal checks (always proposed for any project) ──────────────────
|
|
@@ -948,8 +1250,8 @@ function getTemplates(profile) {
|
|
|
948
1250
|
},
|
|
949
1251
|
{
|
|
950
1252
|
id: "no-todos",
|
|
951
|
-
label:
|
|
952
|
-
prompt:
|
|
1253
|
+
label: blockedMarkerLabel,
|
|
1254
|
+
prompt: blockedMarkerPrompt,
|
|
953
1255
|
trigger: () => true
|
|
954
1256
|
},
|
|
955
1257
|
// ── TypeScript / JavaScript ──────────────────────────────────────────────
|
|
@@ -1107,20 +1409,20 @@ function withHeader(header, newYaml) {
|
|
|
1107
1409
|
return header + "\n\n" + newYaml;
|
|
1108
1410
|
}
|
|
1109
1411
|
async function evolveCommand(options) {
|
|
1110
|
-
if (!
|
|
1111
|
-
console.error(
|
|
1412
|
+
if (!existsSync11("checks.yaml")) {
|
|
1413
|
+
console.error(chalk7.red("No checks.yaml found. Run `holdpoint init` first."));
|
|
1112
1414
|
process.exit(1);
|
|
1113
1415
|
}
|
|
1114
1416
|
const spinner = ora4("Scanning project profile\u2026").start();
|
|
1115
1417
|
const cwd = process.cwd();
|
|
1116
1418
|
const profile = scanProject(cwd);
|
|
1117
1419
|
const repoFiles = getRepoFiles(cwd);
|
|
1118
|
-
const yamlContent =
|
|
1420
|
+
const yamlContent = readFileSync8("checks.yaml", "utf8");
|
|
1119
1421
|
let config;
|
|
1120
1422
|
try {
|
|
1121
1423
|
config = parseHoldpointYaml5(yamlContent);
|
|
1122
1424
|
} catch (err) {
|
|
1123
|
-
spinner.fail(
|
|
1425
|
+
spinner.fail(chalk7.red("Invalid checks.yaml:") + " " + err.message);
|
|
1124
1426
|
process.exit(1);
|
|
1125
1427
|
}
|
|
1126
1428
|
spinner.stop();
|
|
@@ -1129,7 +1431,7 @@ async function evolveCommand(options) {
|
|
|
1129
1431
|
const allTemplates = getTemplates(profile);
|
|
1130
1432
|
const proposals = allTemplates.filter((t) => t.trigger(profile) && !existingIds.has(t.id));
|
|
1131
1433
|
const staleChecks = detectStaleChecks(config, repoFiles);
|
|
1132
|
-
console.log(
|
|
1434
|
+
console.log(chalk7.bold("\n\u{1F4CB} Project profile:"));
|
|
1133
1435
|
const traits = [
|
|
1134
1436
|
["TypeScript", profile.hasTypeScript, "tsconfig.json"],
|
|
1135
1437
|
["ESLint", profile.hasEslint, "eslint.config.*"],
|
|
@@ -1155,40 +1457,40 @@ async function evolveCommand(options) {
|
|
|
1155
1457
|
];
|
|
1156
1458
|
const detected = traits.filter(([, yes]) => yes);
|
|
1157
1459
|
if (detected.length === 0) {
|
|
1158
|
-
console.log(
|
|
1460
|
+
console.log(chalk7.dim(" (empty project \u2014 only universal checks apply)"));
|
|
1159
1461
|
} else {
|
|
1160
1462
|
for (const [name, , hint] of detected) {
|
|
1161
|
-
console.log(` ${
|
|
1463
|
+
console.log(` ${chalk7.green("\u2713")} ${name.padEnd(18)} ${chalk7.dim(hint)}`);
|
|
1162
1464
|
}
|
|
1163
1465
|
}
|
|
1164
1466
|
if (staleChecks.length > 0) {
|
|
1165
|
-
console.log(
|
|
1467
|
+
console.log(chalk7.bold(`
|
|
1166
1468
|
\u26A0\uFE0F Stale checks (${staleChecks.length}):`));
|
|
1167
1469
|
for (const { check, reason, suggestedConditionPath } of staleChecks) {
|
|
1168
|
-
const fix = suggestedConditionPath ?
|
|
1169
|
-
console.log(` ${
|
|
1470
|
+
const fix = suggestedConditionPath ? chalk7.dim(` \u2192 will wrap with conditionId: file_exists: ${suggestedConditionPath}`) : chalk7.dim(" \u2192 no path inferred; review manually");
|
|
1471
|
+
console.log(` ${chalk7.yellow("\u25CC")} ${chalk7.bold(check.id)} ${chalk7.dim(reason)}${fix}`);
|
|
1170
1472
|
}
|
|
1171
1473
|
}
|
|
1172
1474
|
if (proposals.length === 0 && staleChecks.length === 0) {
|
|
1173
|
-
console.log(
|
|
1475
|
+
console.log(chalk7.green("\n\u2713 checks.yaml is fully in sync with the project profile."));
|
|
1174
1476
|
return;
|
|
1175
1477
|
}
|
|
1176
1478
|
if (proposals.length > 0) {
|
|
1177
|
-
console.log(
|
|
1479
|
+
console.log(chalk7.bold(`
|
|
1178
1480
|
\u{1F4A1} Proposed additions (${proposals.length}):`));
|
|
1179
1481
|
for (const t of proposals) {
|
|
1180
|
-
const scope = t.when ?
|
|
1181
|
-
const type = t.cmd ?
|
|
1182
|
-
const preview = t.cmd ?
|
|
1183
|
-
console.log(` ${
|
|
1482
|
+
const scope = t.when ? chalk7.cyan(` when: ${t.when}`) : "";
|
|
1483
|
+
const type = t.cmd ? chalk7.dim("cmd") : chalk7.dim("prompt");
|
|
1484
|
+
const preview = t.cmd ? chalk7.dim(` ${t.cmd.slice(0, 80)}${t.cmd.length > 80 ? "\u2026" : ""}`) : chalk7.dim(` ${(t.prompt ?? "").slice(0, 80)}${(t.prompt?.length ?? 0) > 80 ? "\u2026" : ""}`);
|
|
1485
|
+
console.log(` ${chalk7.green("+")} ${chalk7.bold(t.id.padEnd(24))} [${type}]${scope}`);
|
|
1184
1486
|
console.log(` ${preview}`);
|
|
1185
1487
|
}
|
|
1186
1488
|
}
|
|
1187
1489
|
if (!options.apply) {
|
|
1188
1490
|
console.log(
|
|
1189
|
-
|
|
1491
|
+
chalk7.red(`
|
|
1190
1492
|
\u2717 checks.yaml is out of sync with the project profile.`) + `
|
|
1191
|
-
Run ${
|
|
1493
|
+
Run ${chalk7.bold("npx @holdpoint/cli suggest --apply")} to apply these changes.`
|
|
1192
1494
|
);
|
|
1193
1495
|
process.exit(1);
|
|
1194
1496
|
}
|
|
@@ -1229,128 +1531,55 @@ async function evolveCommand(options) {
|
|
|
1229
1531
|
};
|
|
1230
1532
|
const header = extractHeader(yamlContent);
|
|
1231
1533
|
const newYaml = withHeader(header, generateYaml(updatedConfig));
|
|
1232
|
-
|
|
1534
|
+
writeFileSync6("checks.yaml", newYaml, "utf8");
|
|
1233
1535
|
applySpinner.text = "Running holdpoint update\u2026";
|
|
1234
1536
|
try {
|
|
1235
|
-
|
|
1537
|
+
execSync7("npx @holdpoint/cli update", { stdio: "pipe" });
|
|
1236
1538
|
} catch {
|
|
1237
1539
|
applySpinner.warn(
|
|
1238
|
-
|
|
1540
|
+
chalk7.yellow("checks.yaml updated, but `holdpoint update` failed \u2014 run it manually.")
|
|
1239
1541
|
);
|
|
1240
1542
|
printAppliedSummary(proposals.length, staleChecks.length);
|
|
1241
1543
|
return;
|
|
1242
1544
|
}
|
|
1243
|
-
applySpinner.succeed(
|
|
1545
|
+
applySpinner.succeed(chalk7.green("checks.yaml updated and engine files regenerated."));
|
|
1244
1546
|
printAppliedSummary(proposals.length, staleChecks.length);
|
|
1245
1547
|
}
|
|
1246
1548
|
function printAppliedSummary(added, wrapped) {
|
|
1247
1549
|
const parts = [];
|
|
1248
|
-
if (added > 0) parts.push(
|
|
1550
|
+
if (added > 0) parts.push(chalk7.green(`${added} check${added === 1 ? "" : "s"} added`));
|
|
1249
1551
|
if (wrapped > 0)
|
|
1250
|
-
parts.push(
|
|
1552
|
+
parts.push(chalk7.yellow(`${wrapped} stale check${wrapped === 1 ? "" : "s"} wrapped`));
|
|
1251
1553
|
if (parts.length > 0) console.log(" " + parts.join(" \xB7 "));
|
|
1252
1554
|
console.log(
|
|
1253
|
-
|
|
1555
|
+
chalk7.dim("\n Review checks.yaml, then commit: ") + chalk7.yellow("git add checks.yaml && git commit -m 'chore: update holdpoint checks'")
|
|
1254
1556
|
);
|
|
1255
1557
|
}
|
|
1256
1558
|
|
|
1257
1559
|
// src/commands/live.ts
|
|
1258
|
-
import
|
|
1259
|
-
import { dirname as dirname4, join as join7 } from "path";
|
|
1260
|
-
import chalk7 from "chalk";
|
|
1261
|
-
import { identifyProject as identifyProject2 } from "@holdpoint/live-daemon";
|
|
1262
|
-
|
|
1263
|
-
// src/lib/ensure-daemon.ts
|
|
1264
|
-
import { spawn } from "child_process";
|
|
1265
|
-
import { readHealthyDaemonLock } from "@holdpoint/live-daemon";
|
|
1266
|
-
function sleep(ms) {
|
|
1267
|
-
return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
|
|
1268
|
-
}
|
|
1269
|
-
async function ensureDaemon(timeoutMs = 5e3) {
|
|
1270
|
-
const existing = await readHealthyDaemonLock();
|
|
1271
|
-
if (existing) {
|
|
1272
|
-
return { info: existing, started: false };
|
|
1273
|
-
}
|
|
1274
|
-
const cliEntry = process.argv[1];
|
|
1275
|
-
if (!cliEntry) {
|
|
1276
|
-
throw new Error("Cannot determine the current holdpoint CLI entrypoint");
|
|
1277
|
-
}
|
|
1278
|
-
const child = spawn(process.execPath, [cliEntry, "daemon-serve"], {
|
|
1279
|
-
stdio: "ignore",
|
|
1280
|
-
env: process.env,
|
|
1281
|
-
cwd: process.cwd()
|
|
1282
|
-
});
|
|
1283
|
-
child.unref();
|
|
1284
|
-
const deadline = Date.now() + timeoutMs;
|
|
1285
|
-
while (Date.now() < deadline) {
|
|
1286
|
-
const lock = await readHealthyDaemonLock();
|
|
1287
|
-
if (lock) {
|
|
1288
|
-
return { info: lock, started: true };
|
|
1289
|
-
}
|
|
1290
|
-
await sleep(100);
|
|
1291
|
-
}
|
|
1292
|
-
throw new Error("Daemon unavailable + cannot spawn");
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
// src/lib/open-browser.ts
|
|
1296
|
-
import { execSync as execSync7 } from "child_process";
|
|
1297
|
-
function openBrowser(url) {
|
|
1298
|
-
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
1299
|
-
try {
|
|
1300
|
-
execSync7(`${openCmd} ${JSON.stringify(url)}`, { stdio: "ignore" });
|
|
1301
|
-
} catch {
|
|
1302
|
-
}
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
// src/commands/live.ts
|
|
1306
|
-
function findChecksYaml(startDir) {
|
|
1307
|
-
let current = startDir;
|
|
1308
|
-
for (; ; ) {
|
|
1309
|
-
const candidate = join7(current, "checks.yaml");
|
|
1310
|
-
if (existsSync11(candidate)) {
|
|
1311
|
-
return candidate;
|
|
1312
|
-
}
|
|
1313
|
-
const parent = dirname4(current);
|
|
1314
|
-
if (parent === current) {
|
|
1315
|
-
return null;
|
|
1316
|
-
}
|
|
1317
|
-
current = parent;
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
function tryResolveCurrentProject() {
|
|
1321
|
-
const checksYaml = findChecksYaml(process.cwd());
|
|
1322
|
-
if (checksYaml) {
|
|
1323
|
-
return identifyProject2(dirname4(checksYaml));
|
|
1324
|
-
}
|
|
1325
|
-
try {
|
|
1326
|
-
return identifyProject2(process.cwd());
|
|
1327
|
-
} catch {
|
|
1328
|
-
return null;
|
|
1329
|
-
}
|
|
1330
|
-
}
|
|
1560
|
+
import chalk8 from "chalk";
|
|
1331
1561
|
async function liveCommand(options = {}) {
|
|
1332
1562
|
const { info, started } = await ensureDaemon();
|
|
1333
1563
|
const baseUrl = new URL(`/__holdpoint/live-auth`, `http://127.0.0.1:${info.port}`);
|
|
1334
1564
|
baseUrl.searchParams.set("token", info.token);
|
|
1565
|
+
baseUrl.searchParams.set("path", "/live/");
|
|
1335
1566
|
const currentProject = options.project ? null : tryResolveCurrentProject();
|
|
1336
1567
|
if (options.project) {
|
|
1337
1568
|
baseUrl.searchParams.set("project", options.project);
|
|
1338
1569
|
} else if (currentProject) {
|
|
1339
|
-
baseUrl
|
|
1340
|
-
baseUrl.searchParams.set("name", currentProject.name);
|
|
1341
|
-
baseUrl.searchParams.set("root", currentProject.root);
|
|
1570
|
+
appendProjectAuthParams(baseUrl, currentProject);
|
|
1342
1571
|
}
|
|
1343
1572
|
openBrowser(baseUrl.toString());
|
|
1344
1573
|
console.log(
|
|
1345
|
-
|
|
1574
|
+
chalk8.green(
|
|
1346
1575
|
started ? "\u2713 Started Holdpoint Live and opened the browser" : "\u2713 Opened Holdpoint Live"
|
|
1347
1576
|
)
|
|
1348
1577
|
);
|
|
1349
|
-
console.log(` url: ${
|
|
1578
|
+
console.log(` url: ${chalk8.cyan(baseUrl.toString())}`);
|
|
1350
1579
|
}
|
|
1351
1580
|
|
|
1352
1581
|
// src/commands/daemon.ts
|
|
1353
|
-
import
|
|
1582
|
+
import chalk9 from "chalk";
|
|
1354
1583
|
import {
|
|
1355
1584
|
DaemonAlreadyRunningError,
|
|
1356
1585
|
isProcessAlive,
|
|
@@ -1391,38 +1620,38 @@ async function daemonStartCommand() {
|
|
|
1391
1620
|
const { info, started } = await ensureDaemon();
|
|
1392
1621
|
const sessionCount = await fetchSessionCount(info);
|
|
1393
1622
|
const headline = started ? "Started Holdpoint Live daemon" : "Reused existing Holdpoint Live daemon";
|
|
1394
|
-
console.log(
|
|
1395
|
-
console.log(` pid: ${
|
|
1396
|
-
console.log(` port: ${
|
|
1397
|
-
console.log(` uptime: ${
|
|
1623
|
+
console.log(chalk9.green(`\u2713 ${headline}`));
|
|
1624
|
+
console.log(` pid: ${chalk9.cyan(String(info.pid))}`);
|
|
1625
|
+
console.log(` port: ${chalk9.cyan(String(info.port))}`);
|
|
1626
|
+
console.log(` uptime: ${chalk9.cyan(formatUptime(info))}`);
|
|
1398
1627
|
if (sessionCount !== null) {
|
|
1399
|
-
console.log(` sessions: ${
|
|
1628
|
+
console.log(` sessions: ${chalk9.cyan(String(sessionCount))}`);
|
|
1400
1629
|
}
|
|
1401
1630
|
}
|
|
1402
1631
|
async function daemonStatusCommand() {
|
|
1403
1632
|
const lock = await readHealthyDaemonLock2();
|
|
1404
1633
|
if (!lock) {
|
|
1405
|
-
console.log(
|
|
1634
|
+
console.log(chalk9.yellow("Holdpoint Live daemon is not running."));
|
|
1406
1635
|
return;
|
|
1407
1636
|
}
|
|
1408
1637
|
const sessionCount = await fetchSessionCount(lock);
|
|
1409
|
-
console.log(
|
|
1410
|
-
console.log(` pid: ${
|
|
1411
|
-
console.log(` port: ${
|
|
1412
|
-
console.log(` uptime: ${
|
|
1638
|
+
console.log(chalk9.green("\u2713 Holdpoint Live daemon is running"));
|
|
1639
|
+
console.log(` pid: ${chalk9.cyan(String(lock.pid))}`);
|
|
1640
|
+
console.log(` port: ${chalk9.cyan(String(lock.port))}`);
|
|
1641
|
+
console.log(` uptime: ${chalk9.cyan(formatUptime(lock))}`);
|
|
1413
1642
|
if (sessionCount !== null) {
|
|
1414
|
-
console.log(` sessions: ${
|
|
1643
|
+
console.log(` sessions: ${chalk9.cyan(String(sessionCount))}`);
|
|
1415
1644
|
}
|
|
1416
1645
|
}
|
|
1417
1646
|
async function daemonStopCommand() {
|
|
1418
1647
|
const lock = readDaemonLock();
|
|
1419
1648
|
if (!lock) {
|
|
1420
|
-
console.log(
|
|
1649
|
+
console.log(chalk9.yellow("Holdpoint Live daemon is not running."));
|
|
1421
1650
|
return;
|
|
1422
1651
|
}
|
|
1423
1652
|
if (!isProcessAlive(lock.pid)) {
|
|
1424
1653
|
removeDaemonLock(void 0, lock.token);
|
|
1425
|
-
console.log(
|
|
1654
|
+
console.log(chalk9.yellow("Removed stale Holdpoint Live lockfile."));
|
|
1426
1655
|
return;
|
|
1427
1656
|
}
|
|
1428
1657
|
process.kill(lock.pid, "SIGTERM");
|
|
@@ -1430,7 +1659,7 @@ async function daemonStopCommand() {
|
|
|
1430
1659
|
while (Date.now() < deadline) {
|
|
1431
1660
|
if (!isProcessAlive(lock.pid)) {
|
|
1432
1661
|
removeDaemonLock(void 0, lock.token);
|
|
1433
|
-
console.log(
|
|
1662
|
+
console.log(chalk9.green(`\u2713 Stopped Holdpoint Live daemon (${lock.pid})`));
|
|
1434
1663
|
return;
|
|
1435
1664
|
}
|
|
1436
1665
|
await sleep2(100);
|
|
@@ -1438,7 +1667,7 @@ async function daemonStopCommand() {
|
|
|
1438
1667
|
process.kill(lock.pid, "SIGKILL");
|
|
1439
1668
|
await sleep2(100);
|
|
1440
1669
|
removeDaemonLock(void 0, lock.token);
|
|
1441
|
-
console.log(
|
|
1670
|
+
console.log(chalk9.green(`\u2713 Force-stopped Holdpoint Live daemon (${lock.pid})`));
|
|
1442
1671
|
}
|
|
1443
1672
|
async function daemonServeCommand(options) {
|
|
1444
1673
|
try {
|
|
@@ -1456,20 +1685,24 @@ async function daemonServeCommand(options) {
|
|
|
1456
1685
|
}
|
|
1457
1686
|
|
|
1458
1687
|
// src/commands/engines.ts
|
|
1459
|
-
import
|
|
1688
|
+
import chalk10 from "chalk";
|
|
1460
1689
|
|
|
1461
1690
|
// src/engines.ts
|
|
1462
|
-
import { existsSync as existsSync12, readFileSync as
|
|
1463
|
-
import { dirname as dirname5, join as
|
|
1691
|
+
import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
|
|
1692
|
+
import { dirname as dirname5, join as join7, resolve, sep } from "path";
|
|
1464
1693
|
import { createRequire } from "module";
|
|
1465
|
-
import { fileURLToPath as
|
|
1694
|
+
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "url";
|
|
1466
1695
|
import { parseEventV1 } from "@holdpoint/live-protocol";
|
|
1467
1696
|
var require2 = createRequire(import.meta.url);
|
|
1468
|
-
var CLI_SRC_DIR = dirname5(
|
|
1697
|
+
var CLI_SRC_DIR = dirname5(fileURLToPath3(import.meta.url));
|
|
1469
1698
|
var MONOREPO_ROOT = resolve(CLI_SRC_DIR, "../../..");
|
|
1470
|
-
var BUILTIN_LIVE_ENGINE_PACKAGES = [
|
|
1699
|
+
var BUILTIN_LIVE_ENGINE_PACKAGES = [
|
|
1700
|
+
"@holdpoint/engine-claude",
|
|
1701
|
+
"@holdpoint/engine-codex",
|
|
1702
|
+
"@holdpoint/engine-cursor"
|
|
1703
|
+
];
|
|
1471
1704
|
var HOLDPOINT_ENGINE_KEYWORD = "holdpoint-engine";
|
|
1472
|
-
function
|
|
1705
|
+
function isObject3(value) {
|
|
1473
1706
|
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
1474
1707
|
}
|
|
1475
1708
|
function isExternalLiveEnginePackageName(packageName) {
|
|
@@ -1480,8 +1713,8 @@ function readJsonFile(path) {
|
|
|
1480
1713
|
return null;
|
|
1481
1714
|
}
|
|
1482
1715
|
try {
|
|
1483
|
-
const parsed = JSON.parse(
|
|
1484
|
-
return
|
|
1716
|
+
const parsed = JSON.parse(readFileSync9(path, "utf8"));
|
|
1717
|
+
return isObject3(parsed) ? parsed : null;
|
|
1485
1718
|
} catch {
|
|
1486
1719
|
return null;
|
|
1487
1720
|
}
|
|
@@ -1489,7 +1722,7 @@ function readJsonFile(path) {
|
|
|
1489
1722
|
function findNearestPackageRoot(startDir) {
|
|
1490
1723
|
let current = resolve(startDir);
|
|
1491
1724
|
while (true) {
|
|
1492
|
-
if (existsSync12(
|
|
1725
|
+
if (existsSync12(join7(current, "package.json"))) {
|
|
1493
1726
|
return current;
|
|
1494
1727
|
}
|
|
1495
1728
|
const parent = dirname5(current);
|
|
@@ -1502,7 +1735,7 @@ function findNearestPackageRoot(startDir) {
|
|
|
1502
1735
|
function findPackageRootFromFile(path) {
|
|
1503
1736
|
let current = dirname5(path);
|
|
1504
1737
|
while (true) {
|
|
1505
|
-
if (existsSync12(
|
|
1738
|
+
if (existsSync12(join7(current, "package.json"))) {
|
|
1506
1739
|
return current;
|
|
1507
1740
|
}
|
|
1508
1741
|
const parent = dirname5(current);
|
|
@@ -1513,14 +1746,14 @@ function findPackageRootFromFile(path) {
|
|
|
1513
1746
|
}
|
|
1514
1747
|
}
|
|
1515
1748
|
function getDependencyEnginePackageNames(projectRoot) {
|
|
1516
|
-
const packageJson = readJsonFile(
|
|
1749
|
+
const packageJson = readJsonFile(join7(projectRoot, "package.json"));
|
|
1517
1750
|
if (!packageJson) {
|
|
1518
1751
|
return [];
|
|
1519
1752
|
}
|
|
1520
1753
|
const packageNames = /* @__PURE__ */ new Set();
|
|
1521
1754
|
for (const field of ["dependencies", "devDependencies", "optionalDependencies"]) {
|
|
1522
1755
|
const deps = packageJson[field];
|
|
1523
|
-
if (!
|
|
1756
|
+
if (!isObject3(deps)) {
|
|
1524
1757
|
continue;
|
|
1525
1758
|
}
|
|
1526
1759
|
for (const packageName of Object.keys(deps)) {
|
|
@@ -1554,7 +1787,7 @@ function resolvePackageRoot(packageName, projectRoot) {
|
|
|
1554
1787
|
const scopedName = packageName.split("/")[1];
|
|
1555
1788
|
if (scopedName) {
|
|
1556
1789
|
const packageDir = resolve(MONOREPO_ROOT, "packages", scopedName);
|
|
1557
|
-
if (existsSync12(
|
|
1790
|
+
if (existsSync12(join7(packageDir, "package.json"))) {
|
|
1558
1791
|
return packageDir;
|
|
1559
1792
|
}
|
|
1560
1793
|
}
|
|
@@ -1566,7 +1799,7 @@ function formatImportError(error) {
|
|
|
1566
1799
|
return error instanceof Error && error.message ? error.message : String(error);
|
|
1567
1800
|
}
|
|
1568
1801
|
function parseManifest(value) {
|
|
1569
|
-
if (!
|
|
1802
|
+
if (!isObject3(value)) {
|
|
1570
1803
|
return null;
|
|
1571
1804
|
}
|
|
1572
1805
|
if (value.manifestVersion !== 1) {
|
|
@@ -1585,7 +1818,7 @@ function parseManifest(value) {
|
|
|
1585
1818
|
};
|
|
1586
1819
|
}
|
|
1587
1820
|
function parseLiveCapabilities(value) {
|
|
1588
|
-
if (!
|
|
1821
|
+
if (!isObject3(value)) {
|
|
1589
1822
|
return null;
|
|
1590
1823
|
}
|
|
1591
1824
|
const capabilities = {};
|
|
@@ -1608,7 +1841,7 @@ function parseLiveCapabilities(value) {
|
|
|
1608
1841
|
return capabilities;
|
|
1609
1842
|
}
|
|
1610
1843
|
function parseLiveAdapter(value, manifest) {
|
|
1611
|
-
if (!
|
|
1844
|
+
if (!isObject3(value)) {
|
|
1612
1845
|
return null;
|
|
1613
1846
|
}
|
|
1614
1847
|
if (typeof value.id !== "string" || typeof value.displayName !== "string") {
|
|
@@ -1674,7 +1907,7 @@ async function resolveCandidate(packageName, source, projectRoot) {
|
|
|
1674
1907
|
reason: "package could not be resolved from this project"
|
|
1675
1908
|
};
|
|
1676
1909
|
}
|
|
1677
|
-
const packageJson = readJsonFile(
|
|
1910
|
+
const packageJson = readJsonFile(join7(packageRoot, "package.json"));
|
|
1678
1911
|
if (!packageJson) {
|
|
1679
1912
|
return {
|
|
1680
1913
|
packageName,
|
|
@@ -1692,7 +1925,7 @@ async function resolveCandidate(packageName, source, projectRoot) {
|
|
|
1692
1925
|
reason: `missing \`${HOLDPOINT_ENGINE_KEYWORD}\` keyword`
|
|
1693
1926
|
};
|
|
1694
1927
|
}
|
|
1695
|
-
const metadata =
|
|
1928
|
+
const metadata = isObject3(packageJson.holdpoint) ? packageJson.holdpoint : void 0;
|
|
1696
1929
|
if (!metadata?.manifest) {
|
|
1697
1930
|
return {
|
|
1698
1931
|
packageName,
|
|
@@ -1825,28 +2058,28 @@ async function enginesCommand(options = {}) {
|
|
|
1825
2058
|
return;
|
|
1826
2059
|
}
|
|
1827
2060
|
if (engines.length === 0) {
|
|
1828
|
-
console.log("No Holdpoint
|
|
2061
|
+
console.log("No Holdpoint Live engines were discovered.");
|
|
1829
2062
|
return;
|
|
1830
2063
|
}
|
|
1831
2064
|
for (const engine of engines) {
|
|
1832
2065
|
if (engine.status === "loaded" && engine.manifest) {
|
|
1833
2066
|
console.log(
|
|
1834
|
-
`${
|
|
2067
|
+
`${chalk10.green("loaded")} ${chalk10.cyan(engine.manifest.id)} (${engine.manifest.displayName}) from ${chalk10.yellow(engine.packageName)} [${engine.source}]`
|
|
1835
2068
|
);
|
|
1836
2069
|
continue;
|
|
1837
2070
|
}
|
|
1838
2071
|
console.log(
|
|
1839
|
-
`${
|
|
2072
|
+
`${chalk10.yellow("ignored")} ${chalk10.yellow(engine.packageName)} [${engine.source}] \u2014 ${engine.reason ?? "unknown reason"}`
|
|
1840
2073
|
);
|
|
1841
2074
|
}
|
|
1842
2075
|
}
|
|
1843
2076
|
|
|
1844
2077
|
// src/commands/event.ts
|
|
1845
|
-
import { readFileSync as
|
|
2078
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
1846
2079
|
import { parseEventV1 as parseEventV12, parseEventsBatch } from "@holdpoint/live-protocol";
|
|
1847
2080
|
import { BridgeClient as BridgeClient2 } from "@holdpoint/sdk";
|
|
1848
2081
|
function readStdin() {
|
|
1849
|
-
return
|
|
2082
|
+
return readFileSync10(0, "utf8");
|
|
1850
2083
|
}
|
|
1851
2084
|
async function eventCommand(options) {
|
|
1852
2085
|
const stdin = readStdin().trim();
|
|
@@ -1895,27 +2128,284 @@ async function eventCommand(options) {
|
|
|
1895
2128
|
}
|
|
1896
2129
|
}
|
|
1897
2130
|
|
|
2131
|
+
// src/commands/changeset.ts
|
|
2132
|
+
import { execSync as execSync8 } from "child_process";
|
|
2133
|
+
import { existsSync as existsSync13, readdirSync as readdirSync3, readFileSync as readFileSync11, statSync } from "fs";
|
|
2134
|
+
import { join as join8, relative } from "path";
|
|
2135
|
+
import chalk11 from "chalk";
|
|
2136
|
+
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
2137
|
+
".git",
|
|
2138
|
+
".next",
|
|
2139
|
+
".turbo",
|
|
2140
|
+
"coverage",
|
|
2141
|
+
"dist",
|
|
2142
|
+
"node_modules",
|
|
2143
|
+
"test-results"
|
|
2144
|
+
]);
|
|
2145
|
+
function runGit(command) {
|
|
2146
|
+
try {
|
|
2147
|
+
const out = execSync8(command, {
|
|
2148
|
+
encoding: "utf8",
|
|
2149
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
2150
|
+
});
|
|
2151
|
+
return out.trim().split("\n").filter(Boolean);
|
|
2152
|
+
} catch {
|
|
2153
|
+
return [];
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
function readJson(path) {
|
|
2157
|
+
try {
|
|
2158
|
+
return JSON.parse(readFileSync11(path, "utf8"));
|
|
2159
|
+
} catch {
|
|
2160
|
+
return null;
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
function normalizePath(path) {
|
|
2164
|
+
return path.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
2165
|
+
}
|
|
2166
|
+
function getDefaultBranchRef() {
|
|
2167
|
+
const [symbolic] = runGit("git symbolic-ref --quiet --short refs/remotes/origin/HEAD");
|
|
2168
|
+
if (symbolic) return symbolic;
|
|
2169
|
+
const candidates = ["origin/main", "origin/master"];
|
|
2170
|
+
for (const candidate of candidates) {
|
|
2171
|
+
if (runGit(`git rev-parse --verify ${candidate}`).length > 0) {
|
|
2172
|
+
return candidate;
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
return null;
|
|
2176
|
+
}
|
|
2177
|
+
function getBranchChangedFiles() {
|
|
2178
|
+
const defaultBranch = getDefaultBranchRef();
|
|
2179
|
+
if (!defaultBranch) return [];
|
|
2180
|
+
const [base] = runGit(`git merge-base HEAD ${defaultBranch}`);
|
|
2181
|
+
if (!base) return [];
|
|
2182
|
+
return runGit(`git diff --name-only ${base}...HEAD`);
|
|
2183
|
+
}
|
|
2184
|
+
function uniqueFiles(files) {
|
|
2185
|
+
return [...new Set(files.map(normalizePath))];
|
|
2186
|
+
}
|
|
2187
|
+
function getChangedFiles(options) {
|
|
2188
|
+
const staged = runGit("git diff --cached --name-only");
|
|
2189
|
+
if (options.staged && staged.length > 0) return staged;
|
|
2190
|
+
const untracked = runGit("git ls-files --others --exclude-standard");
|
|
2191
|
+
if (!options.staged) {
|
|
2192
|
+
const unstaged = runGit("git diff --name-only HEAD");
|
|
2193
|
+
const workingTree = uniqueFiles([...unstaged, ...untracked]);
|
|
2194
|
+
if (workingTree.length > 0) return workingTree;
|
|
2195
|
+
}
|
|
2196
|
+
const branch = getBranchChangedFiles();
|
|
2197
|
+
if (branch.length > 0 || untracked.length > 0) return uniqueFiles([...branch, ...untracked]);
|
|
2198
|
+
return runGit("git diff --name-only HEAD~1 HEAD");
|
|
2199
|
+
}
|
|
2200
|
+
function parsePnpmWorkspacePatterns() {
|
|
2201
|
+
if (!existsSync13("pnpm-workspace.yaml")) return [];
|
|
2202
|
+
const lines = readFileSync11("pnpm-workspace.yaml", "utf8").split(/\r?\n/);
|
|
2203
|
+
return lines.map((line) => line.match(/^\s*-\s*['"]?([^'"]+)['"]?\s*$/)?.[1]).filter((line) => Boolean(line)).filter((line) => !line.startsWith("!"));
|
|
2204
|
+
}
|
|
2205
|
+
function expandOneLevelWorkspacePattern(pattern) {
|
|
2206
|
+
const normalized = normalizePath(pattern).replace(/\/package\.json$/, "");
|
|
2207
|
+
if (!normalized.includes("*")) {
|
|
2208
|
+
return existsSync13(join8(normalized, "package.json")) ? [normalized] : [];
|
|
2209
|
+
}
|
|
2210
|
+
const starIndex = normalized.indexOf("*");
|
|
2211
|
+
const parent = normalized.slice(0, starIndex).replace(/\/$/, "");
|
|
2212
|
+
const suffix = normalized.slice(starIndex + 1).replace(/^\//, "");
|
|
2213
|
+
if (!parent || suffix.includes("*") || !existsSync13(parent)) {
|
|
2214
|
+
return [];
|
|
2215
|
+
}
|
|
2216
|
+
return readdirSync3(parent).map((entry) => join8(parent, entry, suffix)).map(normalizePath).filter((candidate) => existsSync13(join8(candidate, "package.json")));
|
|
2217
|
+
}
|
|
2218
|
+
function walkPackageRoots(start, roots) {
|
|
2219
|
+
let entries;
|
|
2220
|
+
try {
|
|
2221
|
+
entries = readdirSync3(start);
|
|
2222
|
+
} catch {
|
|
2223
|
+
return;
|
|
2224
|
+
}
|
|
2225
|
+
if (start !== "." && existsSync13(join8(start, "package.json"))) {
|
|
2226
|
+
roots.push(normalizePath(start));
|
|
2227
|
+
return;
|
|
2228
|
+
}
|
|
2229
|
+
for (const entry of entries) {
|
|
2230
|
+
if (IGNORED_DIRS.has(entry)) continue;
|
|
2231
|
+
const candidate = join8(start, entry);
|
|
2232
|
+
let stats;
|
|
2233
|
+
try {
|
|
2234
|
+
stats = statSync(candidate);
|
|
2235
|
+
} catch {
|
|
2236
|
+
continue;
|
|
2237
|
+
}
|
|
2238
|
+
if (stats.isDirectory()) {
|
|
2239
|
+
walkPackageRoots(candidate, roots);
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
function readPackageRoot(path) {
|
|
2244
|
+
const pkg = readJson(join8(path, "package.json"));
|
|
2245
|
+
if (!pkg) return null;
|
|
2246
|
+
return {
|
|
2247
|
+
path: normalizePath(path === "." ? "" : path),
|
|
2248
|
+
name: typeof pkg.name === "string" ? pkg.name : path || "root",
|
|
2249
|
+
private: pkg.private === true
|
|
2250
|
+
};
|
|
2251
|
+
}
|
|
2252
|
+
function discoverPackageRoots(includePatterns = []) {
|
|
2253
|
+
const explicitRoots = includePatterns.flatMap(expandOneLevelWorkspacePattern);
|
|
2254
|
+
if (explicitRoots.length > 0) {
|
|
2255
|
+
return uniquePackageRoots(
|
|
2256
|
+
explicitRoots.map(readPackageRoot).filter((pkg) => Boolean(pkg))
|
|
2257
|
+
);
|
|
2258
|
+
}
|
|
2259
|
+
const rootPackage = readJson("package.json");
|
|
2260
|
+
const workspacePatterns = [
|
|
2261
|
+
...parsePnpmWorkspacePatterns(),
|
|
2262
|
+
...extractPackageJsonWorkspacePatterns(rootPackage)
|
|
2263
|
+
];
|
|
2264
|
+
const workspaceRoots = workspacePatterns.flatMap(expandOneLevelWorkspacePattern);
|
|
2265
|
+
if (workspaceRoots.length > 0) {
|
|
2266
|
+
return uniquePackageRoots(
|
|
2267
|
+
workspaceRoots.map(readPackageRoot).filter((pkg) => Boolean(pkg)).filter((pkg) => !pkg.private)
|
|
2268
|
+
);
|
|
2269
|
+
}
|
|
2270
|
+
const discovered = [];
|
|
2271
|
+
walkPackageRoots(".", discovered);
|
|
2272
|
+
const roots = discovered.length > 0 ? discovered : existsSync13("package.json") ? ["."] : [];
|
|
2273
|
+
return uniquePackageRoots(
|
|
2274
|
+
roots.map(readPackageRoot).filter((pkg) => Boolean(pkg)).filter((pkg) => !pkg.private)
|
|
2275
|
+
);
|
|
2276
|
+
}
|
|
2277
|
+
function extractPackageJsonWorkspacePatterns(pkg) {
|
|
2278
|
+
const workspaces = pkg?.workspaces;
|
|
2279
|
+
if (Array.isArray(workspaces)) {
|
|
2280
|
+
return workspaces.filter((entry) => typeof entry === "string");
|
|
2281
|
+
}
|
|
2282
|
+
if (workspaces && typeof workspaces === "object" && "packages" in workspaces && Array.isArray(workspaces.packages)) {
|
|
2283
|
+
return workspaces.packages.filter(
|
|
2284
|
+
(entry) => typeof entry === "string"
|
|
2285
|
+
);
|
|
2286
|
+
}
|
|
2287
|
+
return [];
|
|
2288
|
+
}
|
|
2289
|
+
function uniquePackageRoots(packages) {
|
|
2290
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
2291
|
+
for (const pkg of packages) {
|
|
2292
|
+
byPath.set(pkg.path, pkg);
|
|
2293
|
+
}
|
|
2294
|
+
return [...byPath.values()].sort((left, right) => right.path.length - left.path.length);
|
|
2295
|
+
}
|
|
2296
|
+
function isChangesetFile(file) {
|
|
2297
|
+
return /^\.changeset\/(?!README\.md$)[^/]+\.md$/.test(file);
|
|
2298
|
+
}
|
|
2299
|
+
function isReleaseAffectingPackageFile(relativePath) {
|
|
2300
|
+
if (/(^|\/)(__tests__|test|tests|spec|e2e)\//.test(relativePath) || /\.(test|spec)\.[cm]?[jt]sx?$/.test(relativePath)) {
|
|
2301
|
+
return false;
|
|
2302
|
+
}
|
|
2303
|
+
if (relativePath === "README.md" || relativePath === "CHANGELOG.md" || relativePath.startsWith("docs/") || relativePath.startsWith("dist/") || relativePath.startsWith("coverage/")) {
|
|
2304
|
+
return false;
|
|
2305
|
+
}
|
|
2306
|
+
return /^(package\.json|src\/|lib\/|bin\/|templates\/|scripts\/|[^/]+\.config\.)/.test(
|
|
2307
|
+
relativePath
|
|
2308
|
+
);
|
|
2309
|
+
}
|
|
2310
|
+
function findPackageForFile(file, packageRoots) {
|
|
2311
|
+
const normalized = normalizePath(file);
|
|
2312
|
+
return packageRoots.find((pkg) => {
|
|
2313
|
+
if (pkg.path === "") return true;
|
|
2314
|
+
return normalized === pkg.path || normalized.startsWith(`${pkg.path}/`);
|
|
2315
|
+
}) ?? null;
|
|
2316
|
+
}
|
|
2317
|
+
function analyzeChangesetRequirement(input) {
|
|
2318
|
+
const changedFiles = input.changedFiles.map(normalizePath);
|
|
2319
|
+
const hasChangeset = changedFiles.some(isChangesetFile);
|
|
2320
|
+
const requiredFiles = changedFiles.flatMap((file) => {
|
|
2321
|
+
if (file.startsWith(".changeset/")) return [];
|
|
2322
|
+
const pkg = findPackageForFile(file, input.packageRoots);
|
|
2323
|
+
if (!pkg) return [];
|
|
2324
|
+
const relativePath = pkg.path === "" ? file : normalizePath(relative(pkg.path, file));
|
|
2325
|
+
if (!isReleaseAffectingPackageFile(relativePath)) return [];
|
|
2326
|
+
return [{ file, packageName: pkg.name }];
|
|
2327
|
+
});
|
|
2328
|
+
return { requiredFiles, hasChangeset };
|
|
2329
|
+
}
|
|
2330
|
+
async function requireChangesetCommand(options) {
|
|
2331
|
+
const changedFiles = getChangedFiles(options);
|
|
2332
|
+
if (changedFiles.length === 0) {
|
|
2333
|
+
console.log(chalk11.green("\u2713 No changed files detected \u2014 no changeset required."));
|
|
2334
|
+
return;
|
|
2335
|
+
}
|
|
2336
|
+
const packageRoots = discoverPackageRoots(options.include ?? []);
|
|
2337
|
+
if (packageRoots.length === 0) {
|
|
2338
|
+
console.log(chalk11.green("\u2713 No package roots detected \u2014 no changeset required."));
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2341
|
+
const hasChangesetSetup = existsSync13(".changeset");
|
|
2342
|
+
const { requiredFiles, hasChangeset } = analyzeChangesetRequirement({
|
|
2343
|
+
changedFiles,
|
|
2344
|
+
packageRoots
|
|
2345
|
+
});
|
|
2346
|
+
if (requiredFiles.length === 0) {
|
|
2347
|
+
console.log(chalk11.green("\u2713 No release-affecting package files changed."));
|
|
2348
|
+
return;
|
|
2349
|
+
}
|
|
2350
|
+
if (hasChangeset) {
|
|
2351
|
+
console.log(chalk11.green("\u2713 Package changes include a changeset."));
|
|
2352
|
+
return;
|
|
2353
|
+
}
|
|
2354
|
+
console.error(chalk11.red("\u2717 Package changes need a changeset."));
|
|
2355
|
+
console.error("");
|
|
2356
|
+
console.error(chalk11.bold("Changed package files:"));
|
|
2357
|
+
for (const item of requiredFiles.slice(0, 12)) {
|
|
2358
|
+
console.error(` - ${item.file} (${item.packageName})`);
|
|
2359
|
+
}
|
|
2360
|
+
if (requiredFiles.length > 12) {
|
|
2361
|
+
console.error(` - \u2026and ${requiredFiles.length - 12} more`);
|
|
2362
|
+
}
|
|
2363
|
+
console.error("");
|
|
2364
|
+
if (!hasChangesetSetup) {
|
|
2365
|
+
console.error(
|
|
2366
|
+
"No .changeset directory was found. Create one and add a changeset before finishing:"
|
|
2367
|
+
);
|
|
2368
|
+
console.error(chalk11.yellow(" mkdir -p .changeset"));
|
|
2369
|
+
} else {
|
|
2370
|
+
console.error("Add a changeset before finishing:");
|
|
2371
|
+
}
|
|
2372
|
+
console.error(chalk11.yellow(" pnpm changeset"));
|
|
2373
|
+
console.error(chalk11.dim(" or add a .changeset/<name>.md file manually"));
|
|
2374
|
+
process.exit(1);
|
|
2375
|
+
}
|
|
2376
|
+
|
|
1898
2377
|
// src/index.ts
|
|
1899
2378
|
var program = new Command();
|
|
1900
|
-
program.name("holdpoint").description("Universal eval-guard for AI coding agents (alpha)").version(CLI_VERSION)
|
|
1901
|
-
|
|
2379
|
+
program.name("holdpoint").description("Universal eval-guard for AI coding agents (alpha)").version(CLI_VERSION);
|
|
2380
|
+
program.action(() => {
|
|
2381
|
+
program.outputHelp();
|
|
1902
2382
|
});
|
|
1903
|
-
program.command("init").description("Initialise Holdpoint in the current project").option(
|
|
2383
|
+
program.command("init").description("Initialise Holdpoint in the current project").option(
|
|
1904
2384
|
"--agent <agent>",
|
|
1905
2385
|
"Agent to install for: copilot | claude | cursor | codex (default: all four)"
|
|
1906
2386
|
).action(initCommand);
|
|
1907
2387
|
program.command("check").description("Run task checks from checks.yaml").option("--staged", "Only check against git-staged files").action(checkCommand);
|
|
1908
2388
|
program.command("validate").description("Validate checks.yaml schema and print any errors").action(validateCommand);
|
|
1909
2389
|
program.command("update").description("Regenerate engine files from current checks.yaml (preserves checks.yaml)").action(updateCommand);
|
|
1910
|
-
program.command("builder").description("Open the visual builder UI
|
|
2390
|
+
program.command("builder").description("Open the visual builder UI in the Holdpoint daemon").action(buildCommand);
|
|
1911
2391
|
program.command("live").description("Open the Holdpoint Live UI").option("--project <project>", "Open the UI focused on a specific project hash").action(liveCommand);
|
|
1912
2392
|
var daemon = program.command("daemon").description("Manage the Holdpoint Live daemon");
|
|
1913
2393
|
daemon.command("start").description("Start or connect to the singleton Holdpoint Live daemon").action(daemonStartCommand);
|
|
1914
2394
|
daemon.command("status").description("Show Holdpoint Live daemon status").action(daemonStatusCommand);
|
|
1915
2395
|
daemon.command("stop").description("Stop the running Holdpoint Live daemon").action(daemonStopCommand);
|
|
1916
2396
|
program.command("event").description("Internal: read event JSON from stdin and publish it to Holdpoint Live").option("--engine <engine>", "Engine name when converting native hook payloads").option("--from-hook", "Interpret stdin as an engine-native hook payload").action(eventCommand);
|
|
1917
|
-
program.command("engines").description("List discovered Holdpoint Live
|
|
2397
|
+
program.command("engines").description("List discovered Holdpoint Live engine packages").option("--json", "Print machine-readable discovery output").action(enginesCommand);
|
|
2398
|
+
program.command("require-changeset").description("Ensure release-affecting package changes include a changeset").option("--staged", "Prefer git-staged files when deciding what changed").option(
|
|
2399
|
+
"--include <pattern...>",
|
|
2400
|
+
"Package directory glob(s) to enforce, e.g. packages/* apps/builder"
|
|
2401
|
+
).action(requireChangesetCommand);
|
|
1918
2402
|
program.command("daemon-serve").description("Internal: run the Holdpoint Live daemon in the foreground").option("--port <port>", "Fixed port for the daemon process").action(daemonServeCommand);
|
|
1919
|
-
program.command("
|
|
2403
|
+
program.command("suggest").description("Scan project and propose (or apply) new checks to keep checks.yaml in sync").option("--apply", "Write proposed changes to checks.yaml and regenerate engine files").action(evolveCommand);
|
|
2404
|
+
program.command("evolve", { hidden: true }).description("Deprecated alias for `holdpoint suggest`").option("--apply", "Write proposed changes to checks.yaml and regenerate engine files").action(async (options) => {
|
|
2405
|
+
process.stderr.write(
|
|
2406
|
+
"warning: `holdpoint evolve` is deprecated; use `holdpoint suggest` instead.\n"
|
|
2407
|
+
);
|
|
2408
|
+
await evolveCommand(options);
|
|
2409
|
+
});
|
|
1920
2410
|
program.parse();
|
|
1921
2411
|
//# sourceMappingURL=index.js.map
|