@holdpoint/cli 0.1.0-alpha.15 → 0.1.0-alpha.17
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 +1511 -289
- package/dist/index.js.map +1 -1
- package/dist/templates/HOLDPOINT_PREREQUISITES.md +10 -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 +16 -14
- package/dist/builder-ui/assets/index-3J1uDBNl.css +0 -1
- package/dist/builder-ui/assets/index-CaEfVl3b.js +0 -457
- package/dist/builder-ui/assets/index-CaEfVl3b.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, dirname } from "path";
|
|
10
|
-
import { fileURLToPath } from "url";
|
|
11
|
-
import
|
|
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
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
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,49 +54,272 @@ 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
|
-
// src/
|
|
58
|
+
// src/templates.ts
|
|
59
|
+
import { copyFileSync, existsSync as existsSync2, writeFileSync } from "fs";
|
|
60
|
+
import { join, dirname } from "path";
|
|
61
|
+
import { fileURLToPath } from "url";
|
|
62
62
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
63
|
-
function
|
|
64
|
-
const name = stack === "unknown" ? "_base" : stack;
|
|
63
|
+
function getBundledTemplatePath(filename) {
|
|
65
64
|
const candidates = [
|
|
66
|
-
join(__dirname, "templates",
|
|
65
|
+
join(__dirname, "templates", filename),
|
|
67
66
|
// dist/templates/ (published package)
|
|
68
|
-
join(__dirname, "../../../templates",
|
|
67
|
+
join(__dirname, "../../../templates", filename),
|
|
69
68
|
// monorepo dev fallback
|
|
70
|
-
join(process.cwd(), "templates",
|
|
69
|
+
join(process.cwd(), "templates", filename)
|
|
71
70
|
// cwd fallback
|
|
72
71
|
];
|
|
73
|
-
for (const
|
|
74
|
-
if (existsSync2(
|
|
72
|
+
for (const candidate of candidates) {
|
|
73
|
+
if (existsSync2(candidate)) return candidate;
|
|
75
74
|
}
|
|
76
75
|
return "";
|
|
77
76
|
}
|
|
78
|
-
function
|
|
77
|
+
function ensureBundledFile(outputPath, templateFilename, fallbackContent) {
|
|
78
|
+
if (existsSync2(outputPath)) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
const templatePath = getBundledTemplatePath(templateFilename);
|
|
82
|
+
if (templatePath) {
|
|
83
|
+
copyFileSync(templatePath, outputPath);
|
|
84
|
+
} else {
|
|
85
|
+
writeFileSync(outputPath, fallbackContent, "utf8");
|
|
86
|
+
}
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
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
|
+
|
|
310
|
+
// src/commands/init.ts
|
|
311
|
+
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
312
|
+
function getDefaultTemplatePath() {
|
|
79
313
|
const candidates = [
|
|
80
|
-
|
|
314
|
+
join2(__dirname2, "templates", "default.yaml"),
|
|
81
315
|
// dist/templates/ (published package)
|
|
82
|
-
|
|
316
|
+
join2(__dirname2, "../../../templates", "default.yaml"),
|
|
83
317
|
// monorepo dev fallback
|
|
84
|
-
|
|
318
|
+
join2(process.cwd(), "templates", "default.yaml")
|
|
85
319
|
// cwd fallback
|
|
86
320
|
];
|
|
87
321
|
for (const p of candidates) {
|
|
88
|
-
if (
|
|
322
|
+
if (existsSync4(p)) return p;
|
|
89
323
|
}
|
|
90
324
|
return "";
|
|
91
325
|
}
|
|
@@ -102,85 +336,122 @@ checks:
|
|
|
102
336
|
label: "JSDoc on changed public functions"
|
|
103
337
|
prompt: "Ensure all changed public functions and exports have JSDoc comments."
|
|
104
338
|
`;
|
|
339
|
+
var MINIMAL_MASTER_PROMPT = `# Holdpoint
|
|
340
|
+
|
|
341
|
+
Run \`holdpoint check\` before marking any task complete.
|
|
342
|
+
See \`checks.yaml\` for the full list of checks.
|
|
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
|
+
`;
|
|
348
|
+
var MINIMAL_PREREQUISITES = `# Holdpoint prerequisites
|
|
349
|
+
|
|
350
|
+
Holdpoint installed repo-local engine integrations for one or more AI coding agents. Before relying on them locally, review these setup notes:
|
|
351
|
+
|
|
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.
|
|
354
|
+
- **OpenAI Codex** \u2014 project-level hooks require trust approval. Run \`codex trust\` in the Codex TUI or review the hook with \`/hooks\`.
|
|
355
|
+
- **General** \u2014 Holdpoint expects Node.js 18+ and a git repository so \`holdpoint init\`, \`holdpoint update\`, and \`holdpoint check\` can run normally.
|
|
356
|
+
|
|
357
|
+
Docs: https://holdpoint.dev/docs
|
|
358
|
+
`;
|
|
105
359
|
async function initCommand(options) {
|
|
106
360
|
const spinner = ora("Initialising Holdpoint\u2026").start();
|
|
107
|
-
const stack = options.stack ?? detectStack();
|
|
108
361
|
const agentOpt = options.agent;
|
|
109
362
|
const agents = !agentOpt || agentOpt === "all" ? ["copilot", "claude", "cursor", "codex"] : [agentOpt];
|
|
110
|
-
spinner.text = `
|
|
363
|
+
spinner.text = `Installing for: ${chalk2.cyan(agents.join(", "))}`;
|
|
111
364
|
const pm = detectPackageManager();
|
|
112
365
|
let yamlContent = MINIMAL_CHECKS_YAML;
|
|
113
|
-
if (!
|
|
114
|
-
const templatePath =
|
|
366
|
+
if (!existsSync4("checks.yaml")) {
|
|
367
|
+
const templatePath = getDefaultTemplatePath();
|
|
115
368
|
if (templatePath) {
|
|
116
|
-
yamlContent =
|
|
369
|
+
yamlContent = readFileSync3(templatePath, "utf8");
|
|
117
370
|
}
|
|
118
371
|
if (pm !== "pnpm") {
|
|
119
372
|
yamlContent = yamlContent.replace(/\bpnpm\b/g, pm);
|
|
120
373
|
}
|
|
121
|
-
|
|
374
|
+
writeFileSync3("checks.yaml", yamlContent, "utf8");
|
|
122
375
|
} else {
|
|
123
|
-
yamlContent =
|
|
376
|
+
yamlContent = readFileSync3("checks.yaml", "utf8");
|
|
124
377
|
}
|
|
125
378
|
const config = parseHoldpointYaml(yamlContent);
|
|
126
379
|
const generatedDir = ".github/holdpoint/generated";
|
|
127
|
-
|
|
128
|
-
|
|
380
|
+
mkdirSync2(generatedDir, { recursive: true });
|
|
381
|
+
writeFileSync3(`${generatedDir}/checks.immutable.json`, buildConfigJson(config), "utf8");
|
|
129
382
|
if (agents.includes("copilot")) {
|
|
130
383
|
const extDir = ".github/extensions/holdpoint";
|
|
131
|
-
|
|
132
|
-
|
|
384
|
+
mkdirSync2(extDir, { recursive: true });
|
|
385
|
+
writeFileSync3(join2(extDir, "extension.mjs"), buildEngine(config), "utf8");
|
|
386
|
+
spliceBreadcrumb(".github/copilot-instructions.md");
|
|
133
387
|
}
|
|
134
388
|
if (agents.includes("claude")) {
|
|
135
|
-
|
|
389
|
+
mkdirSync2(".claude", { recursive: true });
|
|
136
390
|
const settingsPath = ".claude/settings.json";
|
|
137
391
|
let existing = {};
|
|
138
|
-
if (
|
|
392
|
+
if (existsSync4(settingsPath)) {
|
|
139
393
|
try {
|
|
140
|
-
existing = JSON.parse(
|
|
394
|
+
existing = JSON.parse(readFileSync3(settingsPath, "utf8"));
|
|
141
395
|
} catch {
|
|
142
396
|
}
|
|
143
397
|
}
|
|
144
398
|
const holdpointHooks = JSON.parse(buildClaudeEngineJson(config));
|
|
145
|
-
|
|
399
|
+
writeFileSync3(
|
|
146
400
|
settingsPath,
|
|
147
|
-
JSON.stringify(
|
|
401
|
+
JSON.stringify(mergeClaudeSettings(existing, holdpointHooks), null, 2),
|
|
148
402
|
"utf8"
|
|
149
403
|
);
|
|
404
|
+
spliceBreadcrumb("CLAUDE.md");
|
|
150
405
|
}
|
|
151
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");
|
|
152
423
|
const cursorRules = buildCursorEngine(config);
|
|
153
424
|
const cursorPath = ".cursorrules";
|
|
154
|
-
if (
|
|
155
|
-
const existing =
|
|
425
|
+
if (existsSync4(cursorPath)) {
|
|
426
|
+
const existing = readFileSync3(cursorPath, "utf8");
|
|
156
427
|
if (!existing.includes("Holdpoint Rules")) {
|
|
157
|
-
|
|
428
|
+
writeFileSync3(cursorPath, `${existing.trimEnd()}
|
|
429
|
+
|
|
430
|
+
${cursorRules}`, "utf8");
|
|
158
431
|
}
|
|
159
432
|
} else {
|
|
160
|
-
|
|
433
|
+
writeFileSync3(cursorPath, cursorRules, "utf8");
|
|
161
434
|
}
|
|
435
|
+
spliceBreadcrumb(".cursor/rules/holdpoint.md");
|
|
162
436
|
}
|
|
163
437
|
if (agents.includes("codex")) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const existing = existsSync2(agentsMdPath) ? readFileSync2(agentsMdPath, "utf8") : "";
|
|
170
|
-
writeFileSync(agentsMdPath, spliceAgentsMd(existing, config), "utf8");
|
|
171
|
-
}
|
|
172
|
-
if (!existsSync2("MASTER_PROMPT.md")) {
|
|
173
|
-
const guidePath = getMasterPromptPath();
|
|
174
|
-
if (guidePath) {
|
|
175
|
-
copyFileSync(guidePath, "MASTER_PROMPT.md");
|
|
176
|
-
} else {
|
|
177
|
-
writeFileSync(
|
|
178
|
-
"MASTER_PROMPT.md",
|
|
179
|
-
"# Holdpoint\n\nRun `holdpoint check` before marking any task complete.\nSee `checks.yaml` for the full list of checks.\n",
|
|
180
|
-
"utf8"
|
|
181
|
-
);
|
|
182
|
-
}
|
|
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");
|
|
183
443
|
}
|
|
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
|
+
);
|
|
450
|
+
ensureBundledFile(
|
|
451
|
+
"HOLDPOINT_PREREQUISITES.md",
|
|
452
|
+
"HOLDPOINT_PREREQUISITES.md",
|
|
453
|
+
MINIMAL_PREREQUISITES
|
|
454
|
+
);
|
|
184
455
|
spinner.text = "Installing holdpoint as a devDependency\u2026";
|
|
185
456
|
const installCmds = {
|
|
186
457
|
pnpm: "pnpm add -D holdpoint@alpha",
|
|
@@ -189,40 +460,46 @@ async function initCommand(options) {
|
|
|
189
460
|
};
|
|
190
461
|
const installCmd = installCmds[pm];
|
|
191
462
|
try {
|
|
192
|
-
|
|
193
|
-
spinner.succeed(
|
|
463
|
+
execSync2(installCmd, { stdio: "pipe" });
|
|
464
|
+
spinner.succeed(chalk2.bold.green("Holdpoint initialised!"));
|
|
194
465
|
} catch {
|
|
195
466
|
spinner.warn(
|
|
196
|
-
|
|
197
|
-
Run manually: ${
|
|
467
|
+
chalk2.yellow(`Holdpoint initialised, but could not install the package automatically.`) + `
|
|
468
|
+
Run manually: ${chalk2.yellow(installCmd)}`
|
|
198
469
|
);
|
|
199
470
|
}
|
|
471
|
+
const preflight = runPreflight(agents);
|
|
472
|
+
printPreflight(preflight);
|
|
200
473
|
console.log(`
|
|
201
|
-
${
|
|
202
|
-
1. Edit ${
|
|
203
|
-
2.
|
|
204
|
-
3.
|
|
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
|
|
205
479
|
|
|
206
|
-
Visual builder: ${
|
|
207
|
-
|
|
480
|
+
Visual builder: ${chalk2.yellow("holdpoint builder")} (opens the daemon at /builder)
|
|
481
|
+
Agents: ${chalk2.cyan(agents.join(", "))}
|
|
208
482
|
`);
|
|
209
483
|
}
|
|
210
484
|
|
|
211
485
|
// src/commands/check.ts
|
|
212
|
-
import { existsSync as
|
|
213
|
-
import { join as
|
|
214
|
-
import
|
|
486
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3 } from "fs";
|
|
487
|
+
import { join as join3 } from "path";
|
|
488
|
+
import chalk3 from "chalk";
|
|
215
489
|
import ora2 from "ora";
|
|
216
490
|
import { parseHoldpointYaml as parseHoldpointYaml2, matchesWhen } from "@holdpoint/yaml-core";
|
|
217
491
|
import { runDeterministicChecks } from "@holdpoint/yaml-core/runner";
|
|
218
|
-
import { execSync as
|
|
492
|
+
import { execSync as execSync3 } from "child_process";
|
|
493
|
+
import { randomUUID } from "crypto";
|
|
494
|
+
import { identifyProject } from "@holdpoint/live-daemon";
|
|
495
|
+
import { BridgeClient } from "@holdpoint/sdk";
|
|
219
496
|
var COMMIT_CACHE_PATH = ".holdpoint/checked-commits.json";
|
|
220
497
|
var COMMIT_CACHE_MAX = 100;
|
|
221
498
|
var CHECK_REPORTS_PATH = ".holdpoint/check-reports.json";
|
|
222
499
|
var CHECK_REPORTS_MAX = 50;
|
|
223
500
|
function getStagedFiles() {
|
|
224
501
|
try {
|
|
225
|
-
const out =
|
|
502
|
+
const out = execSync3("git diff --cached --name-only", {
|
|
226
503
|
encoding: "utf8",
|
|
227
504
|
stdio: ["pipe", "pipe", "ignore"]
|
|
228
505
|
});
|
|
@@ -233,7 +510,7 @@ function getStagedFiles() {
|
|
|
233
510
|
}
|
|
234
511
|
function getAllChangedFiles() {
|
|
235
512
|
try {
|
|
236
|
-
const out =
|
|
513
|
+
const out = execSync3("git diff --name-only HEAD", {
|
|
237
514
|
encoding: "utf8",
|
|
238
515
|
stdio: ["pipe", "pipe", "ignore"]
|
|
239
516
|
});
|
|
@@ -244,7 +521,7 @@ function getAllChangedFiles() {
|
|
|
244
521
|
}
|
|
245
522
|
function getLastCommitFiles() {
|
|
246
523
|
try {
|
|
247
|
-
const out =
|
|
524
|
+
const out = execSync3("git diff --name-only HEAD~1 HEAD", {
|
|
248
525
|
encoding: "utf8",
|
|
249
526
|
stdio: ["pipe", "pipe", "ignore"]
|
|
250
527
|
});
|
|
@@ -255,7 +532,7 @@ function getLastCommitFiles() {
|
|
|
255
532
|
}
|
|
256
533
|
function getHeadSha() {
|
|
257
534
|
try {
|
|
258
|
-
return
|
|
535
|
+
return execSync3("git rev-parse HEAD", {
|
|
259
536
|
encoding: "utf8",
|
|
260
537
|
stdio: ["pipe", "pipe", "ignore"]
|
|
261
538
|
}).trim();
|
|
@@ -265,7 +542,7 @@ function getHeadSha() {
|
|
|
265
542
|
}
|
|
266
543
|
function readCommitCache() {
|
|
267
544
|
try {
|
|
268
|
-
const raw =
|
|
545
|
+
const raw = readFileSync4(COMMIT_CACHE_PATH, "utf8");
|
|
269
546
|
const parsed = JSON.parse(raw);
|
|
270
547
|
return new Set(Array.isArray(parsed.verified) ? parsed.verified : []);
|
|
271
548
|
} catch {
|
|
@@ -276,18 +553,18 @@ function recordCommitCache(sha) {
|
|
|
276
553
|
try {
|
|
277
554
|
const existing = readCommitCache();
|
|
278
555
|
const updated = [sha, ...[...existing].filter((s) => s !== sha)].slice(0, COMMIT_CACHE_MAX);
|
|
279
|
-
|
|
280
|
-
|
|
556
|
+
mkdirSync3(join3(COMMIT_CACHE_PATH, ".."), { recursive: true });
|
|
557
|
+
writeFileSync4(COMMIT_CACHE_PATH, JSON.stringify({ verified: updated }, null, 2) + "\n", "utf8");
|
|
281
558
|
} catch {
|
|
282
559
|
}
|
|
283
560
|
}
|
|
284
561
|
function recordCheckReport(run) {
|
|
285
562
|
try {
|
|
286
|
-
|
|
563
|
+
mkdirSync3(join3(CHECK_REPORTS_PATH, ".."), { recursive: true });
|
|
287
564
|
let existing = { runs: [] };
|
|
288
|
-
if (
|
|
565
|
+
if (existsSync5(CHECK_REPORTS_PATH)) {
|
|
289
566
|
try {
|
|
290
|
-
existing = JSON.parse(
|
|
567
|
+
existing = JSON.parse(readFileSync4(CHECK_REPORTS_PATH, "utf8"));
|
|
291
568
|
if (!Array.isArray(existing.runs)) existing.runs = [];
|
|
292
569
|
} catch {
|
|
293
570
|
existing = { runs: [] };
|
|
@@ -296,21 +573,21 @@ function recordCheckReport(run) {
|
|
|
296
573
|
const updated = {
|
|
297
574
|
runs: [run, ...existing.runs].slice(0, CHECK_REPORTS_MAX)
|
|
298
575
|
};
|
|
299
|
-
|
|
576
|
+
writeFileSync4(CHECK_REPORTS_PATH, JSON.stringify(updated, null, 2) + "\n", "utf8");
|
|
300
577
|
} catch {
|
|
301
578
|
}
|
|
302
579
|
}
|
|
303
580
|
async function checkCommand(options) {
|
|
304
|
-
if (!
|
|
305
|
-
console.error(
|
|
581
|
+
if (!existsSync5("checks.yaml")) {
|
|
582
|
+
console.error(chalk3.red("No checks.yaml found. Run `holdpoint init` first."));
|
|
306
583
|
process.exit(1);
|
|
307
584
|
}
|
|
308
|
-
const yamlContent =
|
|
585
|
+
const yamlContent = readFileSync4("checks.yaml", "utf8");
|
|
309
586
|
let config;
|
|
310
587
|
try {
|
|
311
588
|
config = parseHoldpointYaml2(yamlContent);
|
|
312
589
|
} catch (err) {
|
|
313
|
-
console.error(
|
|
590
|
+
console.error(chalk3.red("Invalid checks.yaml:"), err.message);
|
|
314
591
|
process.exit(1);
|
|
315
592
|
}
|
|
316
593
|
const headSha = getHeadSha();
|
|
@@ -323,7 +600,7 @@ async function checkCommand(options) {
|
|
|
323
600
|
} else {
|
|
324
601
|
if (headSha && readCommitCache().has(headSha)) {
|
|
325
602
|
console.log(
|
|
326
|
-
|
|
603
|
+
chalk3.green(`\u2713 Commit ${headSha.slice(0, 8)} already verified \u2014 nothing to re-check.`)
|
|
327
604
|
);
|
|
328
605
|
process.exit(0);
|
|
329
606
|
}
|
|
@@ -332,10 +609,10 @@ async function checkCommand(options) {
|
|
|
332
609
|
changedFiles = lastCommit;
|
|
333
610
|
usedHeadShaForCache = true;
|
|
334
611
|
console.log(
|
|
335
|
-
|
|
612
|
+
chalk3.yellow("No staged files. Running checks scoped to the most recent commit's files.")
|
|
336
613
|
);
|
|
337
614
|
} else {
|
|
338
|
-
console.log(
|
|
615
|
+
console.log(chalk3.green("\u2713 No staged changes and no recent commit \u2014 nothing to check."));
|
|
339
616
|
process.exit(0);
|
|
340
617
|
}
|
|
341
618
|
}
|
|
@@ -343,10 +620,10 @@ async function checkCommand(options) {
|
|
|
343
620
|
changedFiles = getAllChangedFiles();
|
|
344
621
|
if (changedFiles.length === 0) {
|
|
345
622
|
console.log(
|
|
346
|
-
|
|
623
|
+
chalk3.yellow("No changed files detected. Running all checks with no file filter.")
|
|
347
624
|
);
|
|
348
625
|
console.log(
|
|
349
|
-
|
|
626
|
+
chalk3.dim(
|
|
350
627
|
" Tip: if you just ran `holdpoint init`, commit the generated files to clear the git-commit check."
|
|
351
628
|
)
|
|
352
629
|
);
|
|
@@ -354,9 +631,9 @@ async function checkCommand(options) {
|
|
|
354
631
|
}
|
|
355
632
|
const guides = Object.entries(config.context?.guides ?? {});
|
|
356
633
|
if (guides.length > 0) {
|
|
357
|
-
console.log(
|
|
634
|
+
console.log(chalk3.cyan("\nProject guides:"));
|
|
358
635
|
for (const [key, text] of guides) {
|
|
359
|
-
console.log(
|
|
636
|
+
console.log(chalk3.bold(` ${key}:`), chalk3.dim(String(text).trim()));
|
|
360
637
|
}
|
|
361
638
|
console.log("");
|
|
362
639
|
}
|
|
@@ -374,9 +651,9 @@ async function checkCommand(options) {
|
|
|
374
651
|
console.log("");
|
|
375
652
|
console.log(
|
|
376
653
|
[
|
|
377
|
-
|
|
378
|
-
failed.length > 0 ?
|
|
379
|
-
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`) : ""
|
|
380
657
|
].filter(Boolean).join(" ")
|
|
381
658
|
);
|
|
382
659
|
const promptChecks = config.checks.filter(
|
|
@@ -384,9 +661,9 @@ async function checkCommand(options) {
|
|
|
384
661
|
);
|
|
385
662
|
if (promptChecks.length > 0) {
|
|
386
663
|
console.log(`
|
|
387
|
-
${
|
|
664
|
+
${chalk3.cyan("Agent prompts to act on:")}`);
|
|
388
665
|
for (const c of promptChecks) {
|
|
389
|
-
console.log(` ${
|
|
666
|
+
console.log(` ${chalk3.yellow("\u25A1")} [${c.label}] ${c.prompt ?? ""}`);
|
|
390
667
|
}
|
|
391
668
|
}
|
|
392
669
|
const reportResults = [
|
|
@@ -420,6 +697,30 @@ ${chalk2.cyan("Agent prompts to act on:")}`);
|
|
|
420
697
|
}
|
|
421
698
|
};
|
|
422
699
|
recordCheckReport(run);
|
|
700
|
+
const project = identifyProject(process.cwd());
|
|
701
|
+
const bridge = new BridgeClient();
|
|
702
|
+
const liveEvents = reportResults.filter(
|
|
703
|
+
(result) => result.kind === "cmd"
|
|
704
|
+
).map((result, index) => ({
|
|
705
|
+
v: 1,
|
|
706
|
+
id: randomUUID(),
|
|
707
|
+
ts: Date.now() + index,
|
|
708
|
+
engine: "holdpoint",
|
|
709
|
+
session_id: "check-runner",
|
|
710
|
+
project_hash: project.hash,
|
|
711
|
+
cwd: process.cwd(),
|
|
712
|
+
type: "check_run",
|
|
713
|
+
payload: {
|
|
714
|
+
check_id: result.id,
|
|
715
|
+
label: result.label,
|
|
716
|
+
status: result.status,
|
|
717
|
+
duration_ms: 0,
|
|
718
|
+
...result.output ? { output: result.output } : {}
|
|
719
|
+
}
|
|
720
|
+
}));
|
|
721
|
+
if (liveEvents.length > 0) {
|
|
722
|
+
await bridge.sendEvents(liveEvents);
|
|
723
|
+
}
|
|
423
724
|
if (failed.length > 0) {
|
|
424
725
|
process.exit(1);
|
|
425
726
|
}
|
|
@@ -428,258 +729,337 @@ ${chalk2.cyan("Agent prompts to act on:")}`);
|
|
|
428
729
|
}
|
|
429
730
|
}
|
|
430
731
|
function printResult(result) {
|
|
431
|
-
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");
|
|
432
733
|
const label = result.check.label;
|
|
433
734
|
console.log(`${icon} ${label}`);
|
|
434
735
|
if (result.status === "fail" && result.output) {
|
|
435
736
|
const trimmed = result.output.trim().split("\n").slice(0, 10).join("\n");
|
|
436
|
-
console.log(
|
|
737
|
+
console.log(chalk3.dim(trimmed.replace(/^/gm, " ")));
|
|
437
738
|
}
|
|
438
739
|
if (result.status === "skip" && result.skipReason) {
|
|
439
|
-
console.log(
|
|
740
|
+
console.log(chalk3.dim(` ${result.skipReason}`));
|
|
440
741
|
}
|
|
441
742
|
}
|
|
442
743
|
|
|
443
744
|
// src/commands/validate.ts
|
|
444
|
-
import { existsSync as
|
|
445
|
-
import
|
|
745
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
746
|
+
import chalk4 from "chalk";
|
|
446
747
|
import { parseHoldpointYaml as parseHoldpointYaml3, validateConfig } from "@holdpoint/yaml-core";
|
|
447
748
|
async function validateCommand() {
|
|
448
|
-
if (!
|
|
449
|
-
console.error(
|
|
749
|
+
if (!existsSync6("checks.yaml")) {
|
|
750
|
+
console.error(chalk4.red("No checks.yaml found. Run `holdpoint init` first."));
|
|
450
751
|
process.exit(1);
|
|
451
752
|
}
|
|
452
|
-
const text =
|
|
753
|
+
const text = readFileSync5("checks.yaml", "utf8");
|
|
453
754
|
let config;
|
|
454
755
|
try {
|
|
455
756
|
config = parseHoldpointYaml3(text);
|
|
456
757
|
} catch (err) {
|
|
457
|
-
console.error(
|
|
758
|
+
console.error(chalk4.red("Parse error:"), err.message);
|
|
458
759
|
process.exit(1);
|
|
459
760
|
}
|
|
460
761
|
const result = validateConfig(config);
|
|
461
762
|
if (result.valid) {
|
|
462
|
-
console.log(
|
|
763
|
+
console.log(chalk4.green("\u2713 checks.yaml is valid"));
|
|
463
764
|
console.log(
|
|
464
|
-
|
|
765
|
+
chalk4.dim(
|
|
465
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`
|
|
466
767
|
)
|
|
467
768
|
);
|
|
468
769
|
} else {
|
|
469
|
-
console.error(
|
|
770
|
+
console.error(chalk4.red("\u2717 checks.yaml has errors:"));
|
|
470
771
|
for (const err of result.errors) {
|
|
471
|
-
console.error(` ${
|
|
772
|
+
console.error(` ${chalk4.yellow(err.path)}: ${err.message}`);
|
|
472
773
|
}
|
|
473
774
|
process.exit(1);
|
|
474
775
|
}
|
|
475
776
|
}
|
|
476
777
|
|
|
477
778
|
// src/commands/update.ts
|
|
478
|
-
import { existsSync as
|
|
479
|
-
import
|
|
779
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync5, mkdirSync as mkdirSync4 } from "fs";
|
|
780
|
+
import chalk5 from "chalk";
|
|
480
781
|
import ora3 from "ora";
|
|
481
782
|
import { parseHoldpointYaml as parseHoldpointYaml4 } from "@holdpoint/yaml-core";
|
|
482
783
|
import { buildConfigJson as buildConfigJson2, buildEngine as buildEngine2 } from "@holdpoint/engine-copilot";
|
|
483
784
|
import { buildEngineJson as buildClaudeEngineJson2 } from "@holdpoint/engine-claude";
|
|
484
|
-
import {
|
|
785
|
+
import {
|
|
786
|
+
buildCheckScript as buildCursorCheckScript2,
|
|
787
|
+
buildEngine as buildCursorEngine2,
|
|
788
|
+
buildHooksJson as buildCursorHooksJson2
|
|
789
|
+
} from "@holdpoint/engine-cursor";
|
|
485
790
|
import {
|
|
486
791
|
buildConfigToml as buildCodexConfigToml2,
|
|
487
792
|
buildHooksJson as buildCodexHooksJson2,
|
|
488
|
-
buildCheckScript as buildCodexCheckScript2
|
|
489
|
-
spliceAgentsMd as spliceAgentsMd2
|
|
793
|
+
buildCheckScript as buildCodexCheckScript2
|
|
490
794
|
} from "@holdpoint/engine-codex";
|
|
795
|
+
var MINIMAL_PREREQUISITES2 = `# Holdpoint prerequisites
|
|
796
|
+
|
|
797
|
+
Holdpoint installed repo-local engine integrations for one or more AI coding agents. Before relying on them locally, review these setup notes:
|
|
798
|
+
|
|
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.
|
|
801
|
+
- **OpenAI Codex** \u2014 project-level hooks require trust approval. Run \`codex trust\` in the Codex TUI or review the hook with \`/hooks\`.
|
|
802
|
+
- **General** \u2014 Holdpoint expects Node.js 18+ and a git repository so \`holdpoint init\`, \`holdpoint update\`, and \`holdpoint check\` can run normally.
|
|
803
|
+
|
|
804
|
+
Docs: https://holdpoint.dev/docs
|
|
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
|
+
`;
|
|
491
810
|
async function updateCommand() {
|
|
492
|
-
if (!
|
|
493
|
-
console.error(
|
|
811
|
+
if (!existsSync7("checks.yaml")) {
|
|
812
|
+
console.error(chalk5.red("No checks.yaml found. Run `holdpoint init` first."));
|
|
494
813
|
process.exit(1);
|
|
495
814
|
}
|
|
496
815
|
const spinner = ora3("Updating Holdpoint engine files\u2026").start();
|
|
497
|
-
const config = parseHoldpointYaml4(
|
|
816
|
+
const config = parseHoldpointYaml4(readFileSync6("checks.yaml", "utf8"));
|
|
498
817
|
const detected = detectInstalledAgents();
|
|
499
818
|
const agents = detected.length > 0 ? detected : ["copilot", "claude", "cursor", "codex"];
|
|
500
819
|
const generatedDir = ".github/holdpoint/generated";
|
|
501
|
-
|
|
502
|
-
|
|
820
|
+
mkdirSync4(generatedDir, { recursive: true });
|
|
821
|
+
writeFileSync5(`${generatedDir}/checks.immutable.json`, buildConfigJson2(config), "utf8");
|
|
503
822
|
if (agents.includes("copilot")) {
|
|
504
823
|
const extDir = ".github/extensions/holdpoint";
|
|
505
|
-
|
|
506
|
-
|
|
824
|
+
mkdirSync4(extDir, { recursive: true });
|
|
825
|
+
writeFileSync5(`${extDir}/extension.mjs`, buildEngine2(config), "utf8");
|
|
826
|
+
spliceBreadcrumb(".github/copilot-instructions.md");
|
|
507
827
|
}
|
|
508
828
|
if (agents.includes("claude")) {
|
|
509
|
-
|
|
829
|
+
mkdirSync4(".claude", { recursive: true });
|
|
510
830
|
const settingsPath = ".claude/settings.json";
|
|
511
831
|
let existing = {};
|
|
512
|
-
if (
|
|
832
|
+
if (existsSync7(settingsPath)) {
|
|
513
833
|
try {
|
|
514
|
-
existing = JSON.parse(
|
|
834
|
+
existing = JSON.parse(readFileSync6(settingsPath, "utf8"));
|
|
515
835
|
} catch {
|
|
516
836
|
}
|
|
517
837
|
}
|
|
518
838
|
const hooks = JSON.parse(buildClaudeEngineJson2(config));
|
|
519
|
-
|
|
839
|
+
writeFileSync5(
|
|
520
840
|
settingsPath,
|
|
521
|
-
JSON.stringify(
|
|
841
|
+
JSON.stringify(mergeClaudeSettings(existing, hooks), null, 2) + "\n"
|
|
522
842
|
);
|
|
843
|
+
spliceBreadcrumb("CLAUDE.md");
|
|
523
844
|
}
|
|
524
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");
|
|
525
862
|
const cursorRules = buildCursorEngine2(config);
|
|
526
863
|
const cursorPath = ".cursorrules";
|
|
527
|
-
if (
|
|
528
|
-
const content =
|
|
864
|
+
if (existsSync7(cursorPath)) {
|
|
865
|
+
const content = readFileSync6(cursorPath, "utf8");
|
|
529
866
|
const start = content.indexOf("# \u2500\u2500\u2500 Holdpoint Rules");
|
|
530
867
|
const end = content.indexOf("# \u2500\u2500\u2500 End Holdpoint Rules \u2500\u2500\u2500");
|
|
531
868
|
if (start !== -1 && end !== -1) {
|
|
532
869
|
const afterEnd = content.indexOf("\n", end);
|
|
533
|
-
const
|
|
534
|
-
|
|
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);
|
|
535
877
|
} else {
|
|
536
|
-
|
|
878
|
+
writeFileSync5(cursorPath, `${content.trimEnd()}
|
|
879
|
+
|
|
880
|
+
${cursorRules}`);
|
|
537
881
|
}
|
|
882
|
+
} else {
|
|
883
|
+
writeFileSync5(cursorPath, cursorRules);
|
|
538
884
|
}
|
|
885
|
+
spliceBreadcrumb(".cursor/rules/holdpoint.md");
|
|
539
886
|
}
|
|
540
887
|
if (agents.includes("codex")) {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
888
|
+
mkdirSync4(".codex", { recursive: true });
|
|
889
|
+
writeFileSync5(".codex/hooks.json", buildCodexHooksJson2(config), "utf8");
|
|
890
|
+
writeFileSync5(".codex/holdpoint-check.mjs", buildCodexCheckScript2(config), "utf8");
|
|
544
891
|
const configTomlPath = ".codex/config.toml";
|
|
545
|
-
if (!
|
|
546
|
-
|
|
892
|
+
if (!existsSync7(configTomlPath)) {
|
|
893
|
+
writeFileSync5(configTomlPath, buildCodexConfigToml2(), "utf8");
|
|
547
894
|
} else {
|
|
548
|
-
const
|
|
549
|
-
if (!
|
|
550
|
-
|
|
895
|
+
const existing = readFileSync6(configTomlPath, "utf8");
|
|
896
|
+
if (!existing.includes("[features]")) {
|
|
897
|
+
writeFileSync5(configTomlPath, existing.trimEnd() + "\n\n" + buildCodexConfigToml2(), "utf8");
|
|
551
898
|
}
|
|
552
899
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
900
|
+
spliceBreadcrumb("AGENTS.md");
|
|
901
|
+
}
|
|
902
|
+
const wroteReference = ensureBundledFile(
|
|
903
|
+
"HOLDPOINT_REFERENCE.md",
|
|
904
|
+
"HOLDPOINT_REFERENCE.md",
|
|
905
|
+
MINIMAL_HOLDPOINT_REFERENCE2
|
|
906
|
+
);
|
|
907
|
+
const wrotePrerequisites = ensureBundledFile(
|
|
908
|
+
"HOLDPOINT_PREREQUISITES.md",
|
|
909
|
+
"HOLDPOINT_PREREQUISITES.md",
|
|
910
|
+
MINIMAL_PREREQUISITES2
|
|
911
|
+
);
|
|
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
|
+
}
|
|
918
|
+
if (wrotePrerequisites) {
|
|
919
|
+
console.log(
|
|
920
|
+
chalk5.cyan(
|
|
921
|
+
"Created HOLDPOINT_PREREQUISITES.md with Copilot experimental-mode and other agent setup notes."
|
|
922
|
+
)
|
|
923
|
+
);
|
|
556
924
|
}
|
|
557
|
-
spinner.succeed(chalk4.green("Engine files updated from current checks.yaml"));
|
|
558
925
|
}
|
|
559
926
|
|
|
560
927
|
// src/commands/build.ts
|
|
561
|
-
import
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
import {
|
|
565
|
-
import {
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
const checksPath = join3(process.cwd(), "checks.yaml");
|
|
590
|
-
if (existsSync6(checksPath)) {
|
|
591
|
-
res.writeHead(200, { "Content-Type": "text/yaml; charset=utf-8" });
|
|
592
|
-
createReadStream(checksPath).pipe(res);
|
|
593
|
-
} else {
|
|
594
|
-
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
595
|
-
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 };
|
|
596
956
|
}
|
|
597
|
-
|
|
957
|
+
await sleep(100);
|
|
598
958
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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;
|
|
607
982
|
}
|
|
983
|
+
const parent = dirname4(current);
|
|
984
|
+
if (parent === current) {
|
|
985
|
+
return null;
|
|
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) {
|
|
608
1003
|
return;
|
|
609
1004
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
1005
|
+
url.searchParams.set("project", project.hash);
|
|
1006
|
+
url.searchParams.set("name", project.name);
|
|
1007
|
+
url.searchParams.set("root", project.root);
|
|
613
1008
|
}
|
|
1009
|
+
|
|
1010
|
+
// src/commands/build.ts
|
|
614
1011
|
async function buildCommand() {
|
|
615
|
-
const
|
|
616
|
-
const
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
`
|
|
628
|
-
${chalk5.green("\u2713")} Holdpoint builder running at ${chalk5.cyan(`http://localhost:${port}`)}`
|
|
629
|
-
);
|
|
630
|
-
console.log(chalk5.dim(" Edit checks.yaml, then reload the page to see updates"));
|
|
631
|
-
console.log(chalk5.dim(" Press Ctrl+C to stop\n"));
|
|
632
|
-
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
633
|
-
try {
|
|
634
|
-
execSync3(`${openCmd} http://localhost:${port}`, { stdio: "ignore" });
|
|
635
|
-
} catch {
|
|
636
|
-
}
|
|
637
|
-
});
|
|
638
|
-
server.on("error", reject);
|
|
639
|
-
process.on("SIGINT", () => {
|
|
640
|
-
console.log(chalk5.dim("\n Stopping builder\u2026"));
|
|
641
|
-
server.close(() => resolve());
|
|
642
|
-
});
|
|
643
|
-
});
|
|
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())}`);
|
|
644
1024
|
}
|
|
645
1025
|
|
|
646
1026
|
// src/commands/evolve.ts
|
|
647
|
-
import { existsSync as
|
|
648
|
-
import { execSync as
|
|
649
|
-
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";
|
|
650
1030
|
import ora4 from "ora";
|
|
651
1031
|
import { parseHoldpointYaml as parseHoldpointYaml5, generateYaml } from "@holdpoint/yaml-core";
|
|
652
1032
|
|
|
653
1033
|
// src/evolve/scanner.ts
|
|
654
|
-
import { existsSync as
|
|
655
|
-
import { join as
|
|
656
|
-
import { execSync as
|
|
1034
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7, readdirSync } from "fs";
|
|
1035
|
+
import { join as join5 } from "path";
|
|
1036
|
+
import { execSync as execSync5 } from "child_process";
|
|
657
1037
|
function tryReadJson(path) {
|
|
658
1038
|
try {
|
|
659
|
-
return JSON.parse(
|
|
1039
|
+
return JSON.parse(readFileSync7(path, "utf8"));
|
|
660
1040
|
} catch {
|
|
661
1041
|
return null;
|
|
662
1042
|
}
|
|
663
1043
|
}
|
|
664
1044
|
function tryReadText(path) {
|
|
665
1045
|
try {
|
|
666
|
-
return
|
|
1046
|
+
return readFileSync7(path, "utf8");
|
|
667
1047
|
} catch {
|
|
668
1048
|
return "";
|
|
669
1049
|
}
|
|
670
1050
|
}
|
|
671
1051
|
function scanProject(cwd = process.cwd()) {
|
|
672
|
-
const exists = (p) =>
|
|
1052
|
+
const exists = (p) => existsSync9(join5(cwd, p));
|
|
673
1053
|
const packageManager = exists("pnpm-lock.yaml") ? "pnpm" : exists("yarn.lock") ? "yarn" : exists("bun.lockb") ? "bun" : "npm";
|
|
674
|
-
const pkg = tryReadJson(
|
|
1054
|
+
const pkg = tryReadJson(join5(cwd, "package.json"));
|
|
675
1055
|
const scripts = pkg?.scripts ?? {};
|
|
676
1056
|
const deps = /* @__PURE__ */ new Set([
|
|
677
1057
|
...Object.keys(pkg?.dependencies ?? {}),
|
|
678
1058
|
...Object.keys(pkg?.devDependencies ?? {})
|
|
679
1059
|
]);
|
|
680
|
-
const pyprojectText = tryReadText(
|
|
681
|
-
const requirementsText = tryReadText(
|
|
682
|
-
const pipfileText = tryReadText(
|
|
1060
|
+
const pyprojectText = tryReadText(join5(cwd, "pyproject.toml"));
|
|
1061
|
+
const requirementsText = tryReadText(join5(cwd, "requirements.txt"));
|
|
1062
|
+
const pipfileText = tryReadText(join5(cwd, "Pipfile"));
|
|
683
1063
|
const allPyText = pyprojectText + requirementsText + pipfileText;
|
|
684
1064
|
const hasPytest = exists("pytest.ini") || exists("setup.cfg") || allPyText.includes("pytest") || allPyText.includes("[tool.pytest");
|
|
685
1065
|
const hasRuff = allPyText.includes("ruff") || deps.has("ruff");
|
|
@@ -731,9 +1111,9 @@ function scanProject(cwd = process.cwd()) {
|
|
|
731
1111
|
}
|
|
732
1112
|
|
|
733
1113
|
// src/evolve/dead-checker.ts
|
|
734
|
-
import { execSync as
|
|
735
|
-
import { readdirSync as readdirSync2, existsSync as
|
|
736
|
-
import { join as
|
|
1114
|
+
import { execSync as execSync6 } from "child_process";
|
|
1115
|
+
import { readdirSync as readdirSync2, existsSync as existsSync10 } from "fs";
|
|
1116
|
+
import { join as join6 } from "path";
|
|
737
1117
|
var NAMED_SCOPES = /* @__PURE__ */ new Set([
|
|
738
1118
|
"frontend",
|
|
739
1119
|
"backend",
|
|
@@ -778,7 +1158,7 @@ function walkDir(dir, root, depth, maxDepth) {
|
|
|
778
1158
|
const results = [];
|
|
779
1159
|
for (const entry of entries) {
|
|
780
1160
|
if (WALK_IGNORED.has(entry) || entry.startsWith(".")) continue;
|
|
781
|
-
const full =
|
|
1161
|
+
const full = join6(dir, entry);
|
|
782
1162
|
const rel = full.slice(root.length + 1);
|
|
783
1163
|
results.push(rel);
|
|
784
1164
|
const children = walkDir(full, root, depth + 1, maxDepth);
|
|
@@ -788,7 +1168,7 @@ function walkDir(dir, root, depth, maxDepth) {
|
|
|
788
1168
|
}
|
|
789
1169
|
function getRepoFiles(cwd) {
|
|
790
1170
|
try {
|
|
791
|
-
const out =
|
|
1171
|
+
const out = execSync6("git ls-files", {
|
|
792
1172
|
cwd,
|
|
793
1173
|
encoding: "utf8",
|
|
794
1174
|
stdio: ["pipe", "pipe", "ignore"]
|
|
@@ -825,7 +1205,7 @@ function detectStaleChecks(config, repoFiles) {
|
|
|
825
1205
|
if (matches.length === 0) {
|
|
826
1206
|
const label = patternAlias ? `Pattern '${patternAlias}' (= '${regexStr}')` : `Regex '${regexStr}'`;
|
|
827
1207
|
const suggestedConditionPath = extractPathFromRegex(regexStr);
|
|
828
|
-
const pathGone = !suggestedConditionPath || !
|
|
1208
|
+
const pathGone = !suggestedConditionPath || !existsSync10(join6(process.cwd(), suggestedConditionPath));
|
|
829
1209
|
if (pathGone) {
|
|
830
1210
|
stale.push({
|
|
831
1211
|
check,
|
|
@@ -844,6 +1224,9 @@ function pmScript(profile, script, fallback) {
|
|
|
844
1224
|
if (profile.packageManager === "npm") return `npm run ${script}`;
|
|
845
1225
|
return `${profile.packageManager} ${script}`;
|
|
846
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.`;
|
|
847
1230
|
function getTemplates(profile) {
|
|
848
1231
|
return [
|
|
849
1232
|
// ── Universal checks (always proposed for any project) ──────────────────
|
|
@@ -867,8 +1250,8 @@ function getTemplates(profile) {
|
|
|
867
1250
|
},
|
|
868
1251
|
{
|
|
869
1252
|
id: "no-todos",
|
|
870
|
-
label:
|
|
871
|
-
prompt:
|
|
1253
|
+
label: blockedMarkerLabel,
|
|
1254
|
+
prompt: blockedMarkerPrompt,
|
|
872
1255
|
trigger: () => true
|
|
873
1256
|
},
|
|
874
1257
|
// ── TypeScript / JavaScript ──────────────────────────────────────────────
|
|
@@ -1026,20 +1409,20 @@ function withHeader(header, newYaml) {
|
|
|
1026
1409
|
return header + "\n\n" + newYaml;
|
|
1027
1410
|
}
|
|
1028
1411
|
async function evolveCommand(options) {
|
|
1029
|
-
if (!
|
|
1030
|
-
console.error(
|
|
1412
|
+
if (!existsSync11("checks.yaml")) {
|
|
1413
|
+
console.error(chalk7.red("No checks.yaml found. Run `holdpoint init` first."));
|
|
1031
1414
|
process.exit(1);
|
|
1032
1415
|
}
|
|
1033
1416
|
const spinner = ora4("Scanning project profile\u2026").start();
|
|
1034
1417
|
const cwd = process.cwd();
|
|
1035
1418
|
const profile = scanProject(cwd);
|
|
1036
1419
|
const repoFiles = getRepoFiles(cwd);
|
|
1037
|
-
const yamlContent =
|
|
1420
|
+
const yamlContent = readFileSync8("checks.yaml", "utf8");
|
|
1038
1421
|
let config;
|
|
1039
1422
|
try {
|
|
1040
1423
|
config = parseHoldpointYaml5(yamlContent);
|
|
1041
1424
|
} catch (err) {
|
|
1042
|
-
spinner.fail(
|
|
1425
|
+
spinner.fail(chalk7.red("Invalid checks.yaml:") + " " + err.message);
|
|
1043
1426
|
process.exit(1);
|
|
1044
1427
|
}
|
|
1045
1428
|
spinner.stop();
|
|
@@ -1048,7 +1431,7 @@ async function evolveCommand(options) {
|
|
|
1048
1431
|
const allTemplates = getTemplates(profile);
|
|
1049
1432
|
const proposals = allTemplates.filter((t) => t.trigger(profile) && !existingIds.has(t.id));
|
|
1050
1433
|
const staleChecks = detectStaleChecks(config, repoFiles);
|
|
1051
|
-
console.log(
|
|
1434
|
+
console.log(chalk7.bold("\n\u{1F4CB} Project profile:"));
|
|
1052
1435
|
const traits = [
|
|
1053
1436
|
["TypeScript", profile.hasTypeScript, "tsconfig.json"],
|
|
1054
1437
|
["ESLint", profile.hasEslint, "eslint.config.*"],
|
|
@@ -1074,40 +1457,40 @@ async function evolveCommand(options) {
|
|
|
1074
1457
|
];
|
|
1075
1458
|
const detected = traits.filter(([, yes]) => yes);
|
|
1076
1459
|
if (detected.length === 0) {
|
|
1077
|
-
console.log(
|
|
1460
|
+
console.log(chalk7.dim(" (empty project \u2014 only universal checks apply)"));
|
|
1078
1461
|
} else {
|
|
1079
1462
|
for (const [name, , hint] of detected) {
|
|
1080
|
-
console.log(` ${
|
|
1463
|
+
console.log(` ${chalk7.green("\u2713")} ${name.padEnd(18)} ${chalk7.dim(hint)}`);
|
|
1081
1464
|
}
|
|
1082
1465
|
}
|
|
1083
1466
|
if (staleChecks.length > 0) {
|
|
1084
|
-
console.log(
|
|
1467
|
+
console.log(chalk7.bold(`
|
|
1085
1468
|
\u26A0\uFE0F Stale checks (${staleChecks.length}):`));
|
|
1086
1469
|
for (const { check, reason, suggestedConditionPath } of staleChecks) {
|
|
1087
|
-
const fix = suggestedConditionPath ?
|
|
1088
|
-
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}`);
|
|
1089
1472
|
}
|
|
1090
1473
|
}
|
|
1091
1474
|
if (proposals.length === 0 && staleChecks.length === 0) {
|
|
1092
|
-
console.log(
|
|
1475
|
+
console.log(chalk7.green("\n\u2713 checks.yaml is fully in sync with the project profile."));
|
|
1093
1476
|
return;
|
|
1094
1477
|
}
|
|
1095
1478
|
if (proposals.length > 0) {
|
|
1096
|
-
console.log(
|
|
1479
|
+
console.log(chalk7.bold(`
|
|
1097
1480
|
\u{1F4A1} Proposed additions (${proposals.length}):`));
|
|
1098
1481
|
for (const t of proposals) {
|
|
1099
|
-
const scope = t.when ?
|
|
1100
|
-
const type = t.cmd ?
|
|
1101
|
-
const preview = t.cmd ?
|
|
1102
|
-
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}`);
|
|
1103
1486
|
console.log(` ${preview}`);
|
|
1104
1487
|
}
|
|
1105
1488
|
}
|
|
1106
1489
|
if (!options.apply) {
|
|
1107
1490
|
console.log(
|
|
1108
|
-
|
|
1491
|
+
chalk7.red(`
|
|
1109
1492
|
\u2717 checks.yaml is out of sync with the project profile.`) + `
|
|
1110
|
-
Run ${
|
|
1493
|
+
Run ${chalk7.bold("npx @holdpoint/cli@alpha suggest --apply")} to apply these changes.`
|
|
1111
1494
|
);
|
|
1112
1495
|
process.exit(1);
|
|
1113
1496
|
}
|
|
@@ -1148,42 +1531,881 @@ async function evolveCommand(options) {
|
|
|
1148
1531
|
};
|
|
1149
1532
|
const header = extractHeader(yamlContent);
|
|
1150
1533
|
const newYaml = withHeader(header, generateYaml(updatedConfig));
|
|
1151
|
-
|
|
1534
|
+
writeFileSync6("checks.yaml", newYaml, "utf8");
|
|
1152
1535
|
applySpinner.text = "Running holdpoint update\u2026";
|
|
1153
1536
|
try {
|
|
1154
|
-
|
|
1537
|
+
execSync7("npx @holdpoint/cli@alpha update", { stdio: "pipe" });
|
|
1155
1538
|
} catch {
|
|
1156
1539
|
applySpinner.warn(
|
|
1157
|
-
|
|
1540
|
+
chalk7.yellow("checks.yaml updated, but `holdpoint update` failed \u2014 run it manually.")
|
|
1158
1541
|
);
|
|
1159
1542
|
printAppliedSummary(proposals.length, staleChecks.length);
|
|
1160
1543
|
return;
|
|
1161
1544
|
}
|
|
1162
|
-
applySpinner.succeed(
|
|
1545
|
+
applySpinner.succeed(chalk7.green("checks.yaml updated and engine files regenerated."));
|
|
1163
1546
|
printAppliedSummary(proposals.length, staleChecks.length);
|
|
1164
1547
|
}
|
|
1165
1548
|
function printAppliedSummary(added, wrapped) {
|
|
1166
1549
|
const parts = [];
|
|
1167
|
-
if (added > 0) parts.push(
|
|
1550
|
+
if (added > 0) parts.push(chalk7.green(`${added} check${added === 1 ? "" : "s"} added`));
|
|
1168
1551
|
if (wrapped > 0)
|
|
1169
|
-
parts.push(
|
|
1552
|
+
parts.push(chalk7.yellow(`${wrapped} stale check${wrapped === 1 ? "" : "s"} wrapped`));
|
|
1170
1553
|
if (parts.length > 0) console.log(" " + parts.join(" \xB7 "));
|
|
1171
1554
|
console.log(
|
|
1172
|
-
|
|
1555
|
+
chalk7.dim("\n Review checks.yaml, then commit: ") + chalk7.yellow("git add checks.yaml && git commit -m 'chore: update holdpoint checks'")
|
|
1556
|
+
);
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
// src/commands/live.ts
|
|
1560
|
+
import chalk8 from "chalk";
|
|
1561
|
+
async function liveCommand(options = {}) {
|
|
1562
|
+
const { info, started } = await ensureDaemon();
|
|
1563
|
+
const baseUrl = new URL(`/__holdpoint/live-auth`, `http://127.0.0.1:${info.port}`);
|
|
1564
|
+
baseUrl.searchParams.set("token", info.token);
|
|
1565
|
+
baseUrl.searchParams.set("path", "/live/");
|
|
1566
|
+
const currentProject = options.project ? null : tryResolveCurrentProject();
|
|
1567
|
+
if (options.project) {
|
|
1568
|
+
baseUrl.searchParams.set("project", options.project);
|
|
1569
|
+
} else if (currentProject) {
|
|
1570
|
+
appendProjectAuthParams(baseUrl, currentProject);
|
|
1571
|
+
}
|
|
1572
|
+
openBrowser(baseUrl.toString());
|
|
1573
|
+
console.log(
|
|
1574
|
+
chalk8.green(
|
|
1575
|
+
started ? "\u2713 Started Holdpoint Live and opened the browser" : "\u2713 Opened Holdpoint Live"
|
|
1576
|
+
)
|
|
1577
|
+
);
|
|
1578
|
+
console.log(` url: ${chalk8.cyan(baseUrl.toString())}`);
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
// src/commands/daemon.ts
|
|
1582
|
+
import chalk9 from "chalk";
|
|
1583
|
+
import {
|
|
1584
|
+
DaemonAlreadyRunningError,
|
|
1585
|
+
isProcessAlive,
|
|
1586
|
+
readDaemonLock,
|
|
1587
|
+
readHealthyDaemonLock as readHealthyDaemonLock2,
|
|
1588
|
+
removeDaemonLock,
|
|
1589
|
+
startDaemonProcess
|
|
1590
|
+
} from "@holdpoint/live-daemon";
|
|
1591
|
+
|
|
1592
|
+
// src/version.ts
|
|
1593
|
+
var CLI_VERSION = "0.1.0-alpha.15";
|
|
1594
|
+
|
|
1595
|
+
// src/commands/daemon.ts
|
|
1596
|
+
function formatUptime(lock) {
|
|
1597
|
+
const seconds = Math.max(0, Math.floor((Date.now() - lock.started_at) / 1e3));
|
|
1598
|
+
const minutes = Math.floor(seconds / 60);
|
|
1599
|
+
const remainingSeconds = seconds % 60;
|
|
1600
|
+
return minutes > 0 ? `${minutes}m ${remainingSeconds}s` : `${remainingSeconds}s`;
|
|
1601
|
+
}
|
|
1602
|
+
async function fetchSessionCount(lock) {
|
|
1603
|
+
try {
|
|
1604
|
+
const response = await fetch(`http://127.0.0.1:${lock.port}/v1/sessions`, {
|
|
1605
|
+
headers: {
|
|
1606
|
+
authorization: `Bearer ${lock.token}`
|
|
1607
|
+
}
|
|
1608
|
+
});
|
|
1609
|
+
if (!response.ok) return null;
|
|
1610
|
+
const parsed = await response.json();
|
|
1611
|
+
return Array.isArray(parsed.sessions) ? parsed.sessions.length : null;
|
|
1612
|
+
} catch {
|
|
1613
|
+
return null;
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
function sleep2(ms) {
|
|
1617
|
+
return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
|
|
1618
|
+
}
|
|
1619
|
+
async function daemonStartCommand() {
|
|
1620
|
+
const { info, started } = await ensureDaemon();
|
|
1621
|
+
const sessionCount = await fetchSessionCount(info);
|
|
1622
|
+
const headline = started ? "Started Holdpoint Live daemon" : "Reused existing Holdpoint Live daemon";
|
|
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))}`);
|
|
1627
|
+
if (sessionCount !== null) {
|
|
1628
|
+
console.log(` sessions: ${chalk9.cyan(String(sessionCount))}`);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
async function daemonStatusCommand() {
|
|
1632
|
+
const lock = await readHealthyDaemonLock2();
|
|
1633
|
+
if (!lock) {
|
|
1634
|
+
console.log(chalk9.yellow("Holdpoint Live daemon is not running."));
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
const sessionCount = await fetchSessionCount(lock);
|
|
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))}`);
|
|
1642
|
+
if (sessionCount !== null) {
|
|
1643
|
+
console.log(` sessions: ${chalk9.cyan(String(sessionCount))}`);
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
async function daemonStopCommand() {
|
|
1647
|
+
const lock = readDaemonLock();
|
|
1648
|
+
if (!lock) {
|
|
1649
|
+
console.log(chalk9.yellow("Holdpoint Live daemon is not running."));
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
if (!isProcessAlive(lock.pid)) {
|
|
1653
|
+
removeDaemonLock(void 0, lock.token);
|
|
1654
|
+
console.log(chalk9.yellow("Removed stale Holdpoint Live lockfile."));
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1657
|
+
process.kill(lock.pid, "SIGTERM");
|
|
1658
|
+
const deadline = Date.now() + 5e3;
|
|
1659
|
+
while (Date.now() < deadline) {
|
|
1660
|
+
if (!isProcessAlive(lock.pid)) {
|
|
1661
|
+
removeDaemonLock(void 0, lock.token);
|
|
1662
|
+
console.log(chalk9.green(`\u2713 Stopped Holdpoint Live daemon (${lock.pid})`));
|
|
1663
|
+
return;
|
|
1664
|
+
}
|
|
1665
|
+
await sleep2(100);
|
|
1666
|
+
}
|
|
1667
|
+
process.kill(lock.pid, "SIGKILL");
|
|
1668
|
+
await sleep2(100);
|
|
1669
|
+
removeDaemonLock(void 0, lock.token);
|
|
1670
|
+
console.log(chalk9.green(`\u2713 Force-stopped Holdpoint Live daemon (${lock.pid})`));
|
|
1671
|
+
}
|
|
1672
|
+
async function daemonServeCommand(options) {
|
|
1673
|
+
try {
|
|
1674
|
+
const daemon2 = await startDaemonProcess({
|
|
1675
|
+
version: CLI_VERSION,
|
|
1676
|
+
...options.port ? { port: Number(options.port) } : {}
|
|
1677
|
+
});
|
|
1678
|
+
await daemon2.closed;
|
|
1679
|
+
} catch (error) {
|
|
1680
|
+
if (error instanceof DaemonAlreadyRunningError) {
|
|
1681
|
+
process.exit(0);
|
|
1682
|
+
}
|
|
1683
|
+
throw error;
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
// src/commands/engines.ts
|
|
1688
|
+
import chalk10 from "chalk";
|
|
1689
|
+
|
|
1690
|
+
// src/engines.ts
|
|
1691
|
+
import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
|
|
1692
|
+
import { dirname as dirname5, join as join7, resolve, sep } from "path";
|
|
1693
|
+
import { createRequire } from "module";
|
|
1694
|
+
import { fileURLToPath as fileURLToPath3, pathToFileURL } from "url";
|
|
1695
|
+
import { parseEventV1 } from "@holdpoint/live-protocol";
|
|
1696
|
+
var require2 = createRequire(import.meta.url);
|
|
1697
|
+
var CLI_SRC_DIR = dirname5(fileURLToPath3(import.meta.url));
|
|
1698
|
+
var MONOREPO_ROOT = resolve(CLI_SRC_DIR, "../../..");
|
|
1699
|
+
var BUILTIN_LIVE_ENGINE_PACKAGES = [
|
|
1700
|
+
"@holdpoint/engine-claude",
|
|
1701
|
+
"@holdpoint/engine-codex",
|
|
1702
|
+
"@holdpoint/engine-cursor"
|
|
1703
|
+
];
|
|
1704
|
+
var HOLDPOINT_ENGINE_KEYWORD = "holdpoint-engine";
|
|
1705
|
+
function isObject3(value) {
|
|
1706
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
1707
|
+
}
|
|
1708
|
+
function isExternalLiveEnginePackageName(packageName) {
|
|
1709
|
+
return /^holdpoint-engine-[a-z0-9-]+$/.test(packageName) || /^@[a-z0-9_.-]+\/holdpoint-engine-[a-z0-9-]+$/.test(packageName);
|
|
1710
|
+
}
|
|
1711
|
+
function readJsonFile(path) {
|
|
1712
|
+
if (!existsSync12(path)) {
|
|
1713
|
+
return null;
|
|
1714
|
+
}
|
|
1715
|
+
try {
|
|
1716
|
+
const parsed = JSON.parse(readFileSync9(path, "utf8"));
|
|
1717
|
+
return isObject3(parsed) ? parsed : null;
|
|
1718
|
+
} catch {
|
|
1719
|
+
return null;
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
function findNearestPackageRoot(startDir) {
|
|
1723
|
+
let current = resolve(startDir);
|
|
1724
|
+
while (true) {
|
|
1725
|
+
if (existsSync12(join7(current, "package.json"))) {
|
|
1726
|
+
return current;
|
|
1727
|
+
}
|
|
1728
|
+
const parent = dirname5(current);
|
|
1729
|
+
if (parent === current) {
|
|
1730
|
+
return resolve(startDir);
|
|
1731
|
+
}
|
|
1732
|
+
current = parent;
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
function findPackageRootFromFile(path) {
|
|
1736
|
+
let current = dirname5(path);
|
|
1737
|
+
while (true) {
|
|
1738
|
+
if (existsSync12(join7(current, "package.json"))) {
|
|
1739
|
+
return current;
|
|
1740
|
+
}
|
|
1741
|
+
const parent = dirname5(current);
|
|
1742
|
+
if (parent === current) {
|
|
1743
|
+
return null;
|
|
1744
|
+
}
|
|
1745
|
+
current = parent;
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
function getDependencyEnginePackageNames(projectRoot) {
|
|
1749
|
+
const packageJson = readJsonFile(join7(projectRoot, "package.json"));
|
|
1750
|
+
if (!packageJson) {
|
|
1751
|
+
return [];
|
|
1752
|
+
}
|
|
1753
|
+
const packageNames = /* @__PURE__ */ new Set();
|
|
1754
|
+
for (const field of ["dependencies", "devDependencies", "optionalDependencies"]) {
|
|
1755
|
+
const deps = packageJson[field];
|
|
1756
|
+
if (!isObject3(deps)) {
|
|
1757
|
+
continue;
|
|
1758
|
+
}
|
|
1759
|
+
for (const packageName of Object.keys(deps)) {
|
|
1760
|
+
if (isExternalLiveEnginePackageName(packageName)) {
|
|
1761
|
+
packageNames.add(packageName);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
return [...packageNames];
|
|
1766
|
+
}
|
|
1767
|
+
function resolvePackageRoot(packageName, projectRoot) {
|
|
1768
|
+
try {
|
|
1769
|
+
const entryPath = require2.resolve(packageName);
|
|
1770
|
+
return findPackageRootFromFile(entryPath);
|
|
1771
|
+
} catch {
|
|
1772
|
+
}
|
|
1773
|
+
try {
|
|
1774
|
+
const entryPath = require2.resolve(packageName, {
|
|
1775
|
+
paths: [projectRoot, process.cwd()]
|
|
1776
|
+
});
|
|
1777
|
+
return findPackageRootFromFile(entryPath);
|
|
1778
|
+
} catch {
|
|
1779
|
+
}
|
|
1780
|
+
try {
|
|
1781
|
+
const packageJsonPath = require2.resolve(`${packageName}/package.json`, {
|
|
1782
|
+
paths: [projectRoot, process.cwd()]
|
|
1783
|
+
});
|
|
1784
|
+
return dirname5(packageJsonPath);
|
|
1785
|
+
} catch {
|
|
1786
|
+
if (packageName.startsWith("@holdpoint/")) {
|
|
1787
|
+
const scopedName = packageName.split("/")[1];
|
|
1788
|
+
if (scopedName) {
|
|
1789
|
+
const packageDir = resolve(MONOREPO_ROOT, "packages", scopedName);
|
|
1790
|
+
if (existsSync12(join7(packageDir, "package.json"))) {
|
|
1791
|
+
return packageDir;
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
return null;
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
function formatImportError(error) {
|
|
1799
|
+
return error instanceof Error && error.message ? error.message : String(error);
|
|
1800
|
+
}
|
|
1801
|
+
function parseManifest(value) {
|
|
1802
|
+
if (!isObject3(value)) {
|
|
1803
|
+
return null;
|
|
1804
|
+
}
|
|
1805
|
+
if (value.manifestVersion !== 1) {
|
|
1806
|
+
return null;
|
|
1807
|
+
}
|
|
1808
|
+
if (typeof value.id !== "string" || !/^[a-z0-9-]+$/.test(value.id)) {
|
|
1809
|
+
return null;
|
|
1810
|
+
}
|
|
1811
|
+
if (typeof value.displayName !== "string" || !value.displayName.trim()) {
|
|
1812
|
+
return null;
|
|
1813
|
+
}
|
|
1814
|
+
return {
|
|
1815
|
+
manifestVersion: 1,
|
|
1816
|
+
id: value.id,
|
|
1817
|
+
displayName: value.displayName
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
function parseLiveCapabilities(value) {
|
|
1821
|
+
if (!isObject3(value)) {
|
|
1822
|
+
return null;
|
|
1823
|
+
}
|
|
1824
|
+
const capabilities = {};
|
|
1825
|
+
for (const key of [
|
|
1826
|
+
"can_stream",
|
|
1827
|
+
"can_control",
|
|
1828
|
+
"can_modify_context",
|
|
1829
|
+
"can_register_tools",
|
|
1830
|
+
"control_online"
|
|
1831
|
+
]) {
|
|
1832
|
+
const entry = value[key];
|
|
1833
|
+
if (entry === void 0) {
|
|
1834
|
+
continue;
|
|
1835
|
+
}
|
|
1836
|
+
if (typeof entry !== "boolean") {
|
|
1837
|
+
return null;
|
|
1838
|
+
}
|
|
1839
|
+
capabilities[key] = entry;
|
|
1840
|
+
}
|
|
1841
|
+
return capabilities;
|
|
1842
|
+
}
|
|
1843
|
+
function parseLiveAdapter(value, manifest) {
|
|
1844
|
+
if (!isObject3(value)) {
|
|
1845
|
+
return null;
|
|
1846
|
+
}
|
|
1847
|
+
if (typeof value.id !== "string" || typeof value.displayName !== "string") {
|
|
1848
|
+
return null;
|
|
1849
|
+
}
|
|
1850
|
+
if (value.id !== manifest.id || value.displayName !== manifest.displayName) {
|
|
1851
|
+
return null;
|
|
1852
|
+
}
|
|
1853
|
+
const capabilities = parseLiveCapabilities(value.capabilities);
|
|
1854
|
+
if (!capabilities) {
|
|
1855
|
+
return null;
|
|
1856
|
+
}
|
|
1857
|
+
const generateBridgeCommand = value.generateBridgeCommand;
|
|
1858
|
+
if (typeof generateBridgeCommand !== "function") {
|
|
1859
|
+
return null;
|
|
1860
|
+
}
|
|
1861
|
+
const translateHookInput = value.translateHookInput;
|
|
1862
|
+
if (typeof translateHookInput !== "function") {
|
|
1863
|
+
return null;
|
|
1864
|
+
}
|
|
1865
|
+
return {
|
|
1866
|
+
id: value.id,
|
|
1867
|
+
displayName: value.displayName,
|
|
1868
|
+
capabilities,
|
|
1869
|
+
generateBridgeCommand: () => {
|
|
1870
|
+
const command = generateBridgeCommand();
|
|
1871
|
+
if (typeof command !== "string") {
|
|
1872
|
+
throw new Error("adapter.generateBridgeCommand() must return a string");
|
|
1873
|
+
}
|
|
1874
|
+
return command;
|
|
1875
|
+
},
|
|
1876
|
+
translateHookInput: (raw, options) => {
|
|
1877
|
+
const event = translateHookInput(raw, options);
|
|
1878
|
+
return event == null ? null : parseEventV1(event);
|
|
1879
|
+
}
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
async function importModule(modulePath) {
|
|
1883
|
+
const moduleUrl = pathToFileURL(modulePath).href;
|
|
1884
|
+
return await import(moduleUrl);
|
|
1885
|
+
}
|
|
1886
|
+
function resolvePackageAssetPath(packageRoot, relativePath) {
|
|
1887
|
+
const declaredPath = resolve(packageRoot, relativePath);
|
|
1888
|
+
const sourceFallback = resolve(
|
|
1889
|
+
packageRoot,
|
|
1890
|
+
relativePath.replace(/^\.\/dist\//, "./src/").replace(/\.js$/, ".ts")
|
|
1891
|
+
);
|
|
1892
|
+
if (packageRoot.startsWith(resolve(MONOREPO_ROOT, "packages") + sep) && existsSync12(sourceFallback)) {
|
|
1893
|
+
return sourceFallback;
|
|
1894
|
+
}
|
|
1895
|
+
if (existsSync12(declaredPath)) {
|
|
1896
|
+
return declaredPath;
|
|
1897
|
+
}
|
|
1898
|
+
return sourceFallback;
|
|
1899
|
+
}
|
|
1900
|
+
async function resolveCandidate(packageName, source, projectRoot) {
|
|
1901
|
+
const packageRoot = resolvePackageRoot(packageName, projectRoot);
|
|
1902
|
+
if (!packageRoot) {
|
|
1903
|
+
return {
|
|
1904
|
+
packageName,
|
|
1905
|
+
source,
|
|
1906
|
+
status: "ignored",
|
|
1907
|
+
reason: "package could not be resolved from this project"
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1910
|
+
const packageJson = readJsonFile(join7(packageRoot, "package.json"));
|
|
1911
|
+
if (!packageJson) {
|
|
1912
|
+
return {
|
|
1913
|
+
packageName,
|
|
1914
|
+
source,
|
|
1915
|
+
status: "ignored",
|
|
1916
|
+
reason: "package.json could not be read"
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
const keywords = Array.isArray(packageJson.keywords) ? packageJson.keywords : [];
|
|
1920
|
+
if (!keywords.includes(HOLDPOINT_ENGINE_KEYWORD)) {
|
|
1921
|
+
return {
|
|
1922
|
+
packageName,
|
|
1923
|
+
source,
|
|
1924
|
+
status: "ignored",
|
|
1925
|
+
reason: `missing \`${HOLDPOINT_ENGINE_KEYWORD}\` keyword`
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
const metadata = isObject3(packageJson.holdpoint) ? packageJson.holdpoint : void 0;
|
|
1929
|
+
if (!metadata?.manifest) {
|
|
1930
|
+
return {
|
|
1931
|
+
packageName,
|
|
1932
|
+
source,
|
|
1933
|
+
status: "ignored",
|
|
1934
|
+
reason: "missing `holdpoint.manifest` package.json field"
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
if (!metadata.adapter) {
|
|
1938
|
+
return {
|
|
1939
|
+
packageName,
|
|
1940
|
+
source,
|
|
1941
|
+
status: "ignored",
|
|
1942
|
+
reason: "missing `holdpoint.adapter` package.json field"
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1945
|
+
const manifestPath = resolvePackageAssetPath(packageRoot, metadata.manifest);
|
|
1946
|
+
const adapterPath = resolvePackageAssetPath(packageRoot, metadata.adapter);
|
|
1947
|
+
if (!existsSync12(manifestPath)) {
|
|
1948
|
+
return {
|
|
1949
|
+
packageName,
|
|
1950
|
+
source,
|
|
1951
|
+
status: "ignored",
|
|
1952
|
+
reason: "manifest file does not exist"
|
|
1953
|
+
};
|
|
1954
|
+
}
|
|
1955
|
+
if (!existsSync12(adapterPath)) {
|
|
1956
|
+
return {
|
|
1957
|
+
packageName,
|
|
1958
|
+
source,
|
|
1959
|
+
status: "ignored",
|
|
1960
|
+
reason: "adapter file does not exist"
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
try {
|
|
1964
|
+
const manifestModule = await importModule(manifestPath);
|
|
1965
|
+
const manifest = parseManifest(manifestModule.manifest);
|
|
1966
|
+
if (!manifest) {
|
|
1967
|
+
return {
|
|
1968
|
+
packageName,
|
|
1969
|
+
source,
|
|
1970
|
+
status: "ignored",
|
|
1971
|
+
reason: "manifest export is invalid"
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
return {
|
|
1975
|
+
packageName,
|
|
1976
|
+
source,
|
|
1977
|
+
status: "loaded",
|
|
1978
|
+
manifest,
|
|
1979
|
+
packageRoot,
|
|
1980
|
+
adapterPath
|
|
1981
|
+
};
|
|
1982
|
+
} catch (error) {
|
|
1983
|
+
return {
|
|
1984
|
+
packageName,
|
|
1985
|
+
source,
|
|
1986
|
+
status: "ignored",
|
|
1987
|
+
reason: `manifest import failed: ${formatImportError(error)}`
|
|
1988
|
+
};
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
async function discoverLiveEnginesDetailed(options) {
|
|
1992
|
+
const projectRoot = findNearestPackageRoot(options?.cwd ?? process.cwd());
|
|
1993
|
+
const dependencyPackages = getDependencyEnginePackageNames(projectRoot);
|
|
1994
|
+
const seenPackages = /* @__PURE__ */ new Set();
|
|
1995
|
+
const results = [];
|
|
1996
|
+
const loadedIds = /* @__PURE__ */ new Set();
|
|
1997
|
+
const candidates = [
|
|
1998
|
+
...BUILTIN_LIVE_ENGINE_PACKAGES.map((packageName) => ({
|
|
1999
|
+
packageName,
|
|
2000
|
+
source: "built-in"
|
|
2001
|
+
})),
|
|
2002
|
+
...dependencyPackages.map((packageName) => ({ packageName, source: "dependency" }))
|
|
2003
|
+
];
|
|
2004
|
+
for (const candidate of candidates) {
|
|
2005
|
+
if (seenPackages.has(candidate.packageName)) {
|
|
2006
|
+
continue;
|
|
2007
|
+
}
|
|
2008
|
+
seenPackages.add(candidate.packageName);
|
|
2009
|
+
const result = await resolveCandidate(candidate.packageName, candidate.source, projectRoot);
|
|
2010
|
+
if (result.status === "loaded" && result.manifest) {
|
|
2011
|
+
if (loadedIds.has(result.manifest.id)) {
|
|
2012
|
+
results.push({
|
|
2013
|
+
packageName: result.packageName,
|
|
2014
|
+
source: result.source,
|
|
2015
|
+
status: "ignored",
|
|
2016
|
+
reason: `engine id \`${result.manifest.id}\` collides with an already loaded adapter`,
|
|
2017
|
+
manifest: result.manifest
|
|
2018
|
+
});
|
|
2019
|
+
continue;
|
|
2020
|
+
}
|
|
2021
|
+
loadedIds.add(result.manifest.id);
|
|
2022
|
+
}
|
|
2023
|
+
results.push(result);
|
|
2024
|
+
}
|
|
2025
|
+
return results;
|
|
2026
|
+
}
|
|
2027
|
+
async function discoverLiveEngines(options) {
|
|
2028
|
+
const results = await discoverLiveEnginesDetailed(options);
|
|
2029
|
+
return results.map(({ packageName, source, status, reason, manifest }) => ({
|
|
2030
|
+
packageName,
|
|
2031
|
+
source,
|
|
2032
|
+
status,
|
|
2033
|
+
...reason ? { reason } : {},
|
|
2034
|
+
...manifest ? { manifest } : {}
|
|
2035
|
+
}));
|
|
2036
|
+
}
|
|
2037
|
+
async function loadLiveAdapter(engineId, options) {
|
|
2038
|
+
const results = await discoverLiveEnginesDetailed(options);
|
|
2039
|
+
const match = results.find(
|
|
2040
|
+
(result) => result.status === "loaded" && result.manifest?.id === engineId
|
|
2041
|
+
);
|
|
2042
|
+
if (!match?.adapterPath || !match.manifest) {
|
|
2043
|
+
return null;
|
|
2044
|
+
}
|
|
2045
|
+
try {
|
|
2046
|
+
const adapterModule = await importModule(match.adapterPath);
|
|
2047
|
+
return parseLiveAdapter(adapterModule.adapter, match.manifest);
|
|
2048
|
+
} catch {
|
|
2049
|
+
return null;
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
// src/commands/engines.ts
|
|
2054
|
+
async function enginesCommand(options = {}) {
|
|
2055
|
+
const engines = await discoverLiveEngines();
|
|
2056
|
+
if (options.json) {
|
|
2057
|
+
console.log(JSON.stringify(engines, null, 2));
|
|
2058
|
+
return;
|
|
2059
|
+
}
|
|
2060
|
+
if (engines.length === 0) {
|
|
2061
|
+
console.log("No Holdpoint Live engines were discovered.");
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
2064
|
+
for (const engine of engines) {
|
|
2065
|
+
if (engine.status === "loaded" && engine.manifest) {
|
|
2066
|
+
console.log(
|
|
2067
|
+
`${chalk10.green("loaded")} ${chalk10.cyan(engine.manifest.id)} (${engine.manifest.displayName}) from ${chalk10.yellow(engine.packageName)} [${engine.source}]`
|
|
2068
|
+
);
|
|
2069
|
+
continue;
|
|
2070
|
+
}
|
|
2071
|
+
console.log(
|
|
2072
|
+
`${chalk10.yellow("ignored")} ${chalk10.yellow(engine.packageName)} [${engine.source}] \u2014 ${engine.reason ?? "unknown reason"}`
|
|
2073
|
+
);
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
// src/commands/event.ts
|
|
2078
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
2079
|
+
import { parseEventV1 as parseEventV12, parseEventsBatch } from "@holdpoint/live-protocol";
|
|
2080
|
+
import { BridgeClient as BridgeClient2 } from "@holdpoint/sdk";
|
|
2081
|
+
function readStdin() {
|
|
2082
|
+
return readFileSync10(0, "utf8");
|
|
2083
|
+
}
|
|
2084
|
+
async function eventCommand(options) {
|
|
2085
|
+
const stdin = readStdin().trim();
|
|
2086
|
+
if (!stdin) {
|
|
2087
|
+
if (options.fromHook) {
|
|
2088
|
+
process.exit(0);
|
|
2089
|
+
}
|
|
2090
|
+
console.error("No JSON input received on stdin.");
|
|
2091
|
+
process.exit(3);
|
|
2092
|
+
}
|
|
2093
|
+
let raw;
|
|
2094
|
+
try {
|
|
2095
|
+
raw = JSON.parse(stdin);
|
|
2096
|
+
} catch {
|
|
2097
|
+
if (options.fromHook) {
|
|
2098
|
+
process.exit(0);
|
|
2099
|
+
}
|
|
2100
|
+
console.error("Invalid JSON input.");
|
|
2101
|
+
process.exit(3);
|
|
2102
|
+
}
|
|
2103
|
+
const client = new BridgeClient2();
|
|
2104
|
+
try {
|
|
2105
|
+
if (options.fromHook) {
|
|
2106
|
+
if (!options.engine) {
|
|
2107
|
+
process.exit(0);
|
|
2108
|
+
}
|
|
2109
|
+
const adapter = await loadLiveAdapter(options.engine);
|
|
2110
|
+
if (!adapter) {
|
|
2111
|
+
process.exit(0);
|
|
2112
|
+
}
|
|
2113
|
+
const event = adapter.translateHookInput(raw, { cwd: process.cwd() });
|
|
2114
|
+
if (!event) {
|
|
2115
|
+
process.exit(0);
|
|
2116
|
+
}
|
|
2117
|
+
await client.sendEvent(event);
|
|
2118
|
+
process.exit(0);
|
|
2119
|
+
}
|
|
2120
|
+
if (Array.isArray(raw)) {
|
|
2121
|
+
await client.sendEvents(parseEventsBatch(raw));
|
|
2122
|
+
} else {
|
|
2123
|
+
await client.sendEvent(parseEventV12(raw));
|
|
2124
|
+
}
|
|
2125
|
+
} catch (error) {
|
|
2126
|
+
console.error(error.message);
|
|
2127
|
+
process.exit(3);
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
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)
|
|
1173
2275
|
);
|
|
1174
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
|
+
}
|
|
1175
2376
|
|
|
1176
2377
|
// src/index.ts
|
|
1177
2378
|
var program = new Command();
|
|
1178
|
-
program.name("holdpoint").description("Universal eval-guard for AI coding agents (alpha)").version(
|
|
1179
|
-
program.
|
|
2379
|
+
program.name("holdpoint").description("Universal eval-guard for AI coding agents (alpha)").version(CLI_VERSION);
|
|
2380
|
+
program.action(() => {
|
|
2381
|
+
program.outputHelp();
|
|
2382
|
+
});
|
|
2383
|
+
program.command("init").description("Initialise Holdpoint in the current project").option(
|
|
1180
2384
|
"--agent <agent>",
|
|
1181
2385
|
"Agent to install for: copilot | claude | cursor | codex (default: all four)"
|
|
1182
2386
|
).action(initCommand);
|
|
1183
2387
|
program.command("check").description("Run task checks from checks.yaml").option("--staged", "Only check against git-staged files").action(checkCommand);
|
|
1184
2388
|
program.command("validate").description("Validate checks.yaml schema and print any errors").action(validateCommand);
|
|
1185
2389
|
program.command("update").description("Regenerate engine files from current checks.yaml (preserves checks.yaml)").action(updateCommand);
|
|
1186
|
-
program.command("builder").description("Open the visual builder UI
|
|
1187
|
-
program.command("
|
|
2390
|
+
program.command("builder").description("Open the visual builder UI in the Holdpoint daemon").action(buildCommand);
|
|
2391
|
+
program.command("live").description("Open the Holdpoint Live UI").option("--project <project>", "Open the UI focused on a specific project hash").action(liveCommand);
|
|
2392
|
+
var daemon = program.command("daemon").description("Manage the Holdpoint Live daemon");
|
|
2393
|
+
daemon.command("start").description("Start or connect to the singleton Holdpoint Live daemon").action(daemonStartCommand);
|
|
2394
|
+
daemon.command("status").description("Show Holdpoint Live daemon status").action(daemonStatusCommand);
|
|
2395
|
+
daemon.command("stop").description("Stop the running Holdpoint Live daemon").action(daemonStopCommand);
|
|
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);
|
|
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);
|
|
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);
|
|
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
|
+
});
|
|
1188
2410
|
program.parse();
|
|
1189
2411
|
//# sourceMappingURL=index.js.map
|