@holdpoint/cli 0.1.0-alpha.2 → 0.1.0-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-D7ZF5JJH.js +521 -0
- package/dist/chunk-D7ZF5JJH.js.map +1 -0
- package/dist/data/verified-mcp-registry.json +14 -0
- package/dist/index.js +1588 -603
- package/dist/index.js.map +1 -1
- package/dist/init-Y7NZBMU6.js +8 -0
- package/dist/init-Y7NZBMU6.js.map +1 -0
- package/dist/lib/scan.d.ts +20 -0
- package/dist/lib/scan.js +200 -0
- package/dist/lib/scan.js.map +1 -0
- package/dist/prompt-EQ5IFADN.js +23 -0
- package/dist/prompt-EQ5IFADN.js.map +1 -0
- package/dist/templates/HOLDPOINT_PREREQUISITES.md +10 -0
- package/dist/templates/HOLDPOINT_REFERENCE.md +372 -0
- package/dist/templates/MASTER_PROMPT.md +25 -295
- package/dist/templates/default.yaml +274 -0
- package/package.json +16 -14
- package/dist/builder-ui/assets/index-BxfWKnb5.js +0 -437
- package/dist/builder-ui/assets/index-BxfWKnb5.js.map +0 -1
- package/dist/builder-ui/assets/index-DkLHZ-in.css +0 -1
- package/dist/builder-ui/favicon.svg +0 -10
- package/dist/builder-ui/index.html +0 -14
- package/dist/templates/_base.yaml +0 -52
- package/dist/templates/fullstack.yaml +0 -93
- package/dist/templates/go.yaml +0 -60
- package/dist/templates/nextjs.yaml +0 -76
- package/dist/templates/python.yaml +0 -60
- package/dist/templates/typescript.yaml +0 -55
package/dist/index.js
CHANGED
|
@@ -1,202 +1,626 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
detectInstalledAgents,
|
|
4
|
+
ensureBundledFile,
|
|
5
|
+
initCommand,
|
|
6
|
+
mergeClaudeSettings,
|
|
7
|
+
mergeCursorHooks,
|
|
8
|
+
spliceBreadcrumb
|
|
9
|
+
} from "./chunk-D7ZF5JJH.js";
|
|
2
10
|
|
|
3
11
|
// src/index.ts
|
|
4
12
|
import { Command } from "commander";
|
|
5
13
|
|
|
6
|
-
// src/commands/
|
|
7
|
-
import { existsSync
|
|
8
|
-
import { join
|
|
9
|
-
import { fileURLToPath } from "url";
|
|
14
|
+
// src/commands/check.ts
|
|
15
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
16
|
+
import { join } from "path";
|
|
10
17
|
import chalk from "chalk";
|
|
11
18
|
import ora from "ora";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (hasNext) return "nextjs";
|
|
34
|
-
if (hasTsConfig) return "typescript";
|
|
35
|
-
if (hasPyproject) return "python";
|
|
36
|
-
if (hasGoMod) return "go";
|
|
37
|
-
return "unknown";
|
|
19
|
+
import { parseHoldpointYaml, matchesWhen } from "@holdpoint/yaml-core";
|
|
20
|
+
import { runDeterministicChecks } from "@holdpoint/yaml-core/runner";
|
|
21
|
+
import { HOOK_EVENTS, checkHook } from "@holdpoint/types";
|
|
22
|
+
import { execSync } from "child_process";
|
|
23
|
+
import { randomUUID } from "crypto";
|
|
24
|
+
import { identifyProject } from "@holdpoint/live-daemon";
|
|
25
|
+
import { BridgeClient } from "@holdpoint/sdk";
|
|
26
|
+
var COMMIT_CACHE_PATH = ".holdpoint/checked-commits.json";
|
|
27
|
+
var COMMIT_CACHE_MAX = 100;
|
|
28
|
+
var CHECK_REPORTS_PATH = ".holdpoint/check-reports.json";
|
|
29
|
+
var CHECK_REPORTS_MAX = 50;
|
|
30
|
+
function getStagedFiles() {
|
|
31
|
+
try {
|
|
32
|
+
const out = execSync("git diff --cached --name-only", {
|
|
33
|
+
encoding: "utf8",
|
|
34
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
35
|
+
});
|
|
36
|
+
return out.trim().split("\n").filter(Boolean);
|
|
37
|
+
} catch {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
38
40
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// monorepo dev fallback
|
|
49
|
-
join(process.cwd(), "templates", `${name}.yaml`)
|
|
50
|
-
// cwd fallback
|
|
51
|
-
];
|
|
52
|
-
for (const p of candidates) {
|
|
53
|
-
if (existsSync2(p)) return p;
|
|
41
|
+
function getAllChangedFiles() {
|
|
42
|
+
try {
|
|
43
|
+
const out = execSync("git diff --name-only HEAD", {
|
|
44
|
+
encoding: "utf8",
|
|
45
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
46
|
+
});
|
|
47
|
+
return out.trim().split("\n").filter(Boolean);
|
|
48
|
+
} catch {
|
|
49
|
+
return [];
|
|
54
50
|
}
|
|
55
|
-
return "";
|
|
56
51
|
}
|
|
57
|
-
function
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
52
|
+
function getLastCommitFiles() {
|
|
53
|
+
try {
|
|
54
|
+
const out = execSync("git diff --name-only HEAD~1 HEAD", {
|
|
55
|
+
encoding: "utf8",
|
|
56
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
57
|
+
});
|
|
58
|
+
return out.trim().split("\n").filter(Boolean);
|
|
59
|
+
} catch {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function getHeadSha() {
|
|
64
|
+
try {
|
|
65
|
+
return execSync("git rev-parse HEAD", {
|
|
66
|
+
encoding: "utf8",
|
|
67
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
68
|
+
}).trim();
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function readCommitCache() {
|
|
74
|
+
try {
|
|
75
|
+
const raw = readFileSync(COMMIT_CACHE_PATH, "utf8");
|
|
76
|
+
const parsed = JSON.parse(raw);
|
|
77
|
+
return new Set(Array.isArray(parsed.verified) ? parsed.verified : []);
|
|
78
|
+
} catch {
|
|
79
|
+
return /* @__PURE__ */ new Set();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function recordCommitCache(sha) {
|
|
83
|
+
try {
|
|
84
|
+
const existing = readCommitCache();
|
|
85
|
+
const updated = [sha, ...[...existing].filter((s) => s !== sha)].slice(0, COMMIT_CACHE_MAX);
|
|
86
|
+
mkdirSync(join(COMMIT_CACHE_PATH, ".."), { recursive: true });
|
|
87
|
+
writeFileSync(COMMIT_CACHE_PATH, JSON.stringify({ verified: updated }, null, 2) + "\n", "utf8");
|
|
88
|
+
} catch {
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function recordCheckReport(run) {
|
|
92
|
+
try {
|
|
93
|
+
mkdirSync(join(CHECK_REPORTS_PATH, ".."), { recursive: true });
|
|
94
|
+
let existing = { runs: [] };
|
|
95
|
+
if (existsSync(CHECK_REPORTS_PATH)) {
|
|
96
|
+
try {
|
|
97
|
+
existing = JSON.parse(readFileSync(CHECK_REPORTS_PATH, "utf8"));
|
|
98
|
+
if (!Array.isArray(existing.runs)) existing.runs = [];
|
|
99
|
+
} catch {
|
|
100
|
+
existing = { runs: [] };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const updated = {
|
|
104
|
+
runs: [run, ...existing.runs].slice(0, CHECK_REPORTS_MAX)
|
|
105
|
+
};
|
|
106
|
+
writeFileSync(CHECK_REPORTS_PATH, JSON.stringify(updated, null, 2) + "\n", "utf8");
|
|
107
|
+
} catch {
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function checkCommand(options) {
|
|
111
|
+
const hook = options.hook && HOOK_EVENTS.includes(options.hook) ? options.hook : "before_done";
|
|
112
|
+
if (!existsSync("checks.yaml")) {
|
|
113
|
+
if (options.staged || !process.stdout.isTTY || !process.stdin.isTTY) {
|
|
114
|
+
console.error(chalk.red("No checks.yaml found. Run `holdpoint init` first."));
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
const { promptYesNo } = await import("./prompt-EQ5IFADN.js");
|
|
118
|
+
console.log(
|
|
119
|
+
chalk.yellow("No checks.yaml in this directory.") + chalk.dim(" (") + process.cwd() + chalk.dim(")")
|
|
120
|
+
);
|
|
121
|
+
const shouldInit = await promptYesNo(chalk.bold("Initialise Holdpoint here?"), true);
|
|
122
|
+
if (!shouldInit) {
|
|
123
|
+
console.error(chalk.dim("Skipped. Run `holdpoint init` when you're ready."));
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
const { initCommand: initCommand2 } = await import("./init-Y7NZBMU6.js");
|
|
127
|
+
await initCommand2({});
|
|
128
|
+
console.log(
|
|
129
|
+
chalk.dim("\nReview ") + chalk.yellow("checks.yaml") + chalk.dim(" and run ") + chalk.yellow("holdpoint check") + chalk.dim(" again when you're ready.")
|
|
130
|
+
);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const yamlContent = readFileSync("checks.yaml", "utf8");
|
|
134
|
+
let config;
|
|
135
|
+
try {
|
|
136
|
+
config = parseHoldpointYaml(yamlContent);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
console.error(chalk.red("Invalid checks.yaml:"), err.message);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
const headSha = getHeadSha();
|
|
142
|
+
let changedFiles;
|
|
143
|
+
let usedHeadShaForCache = false;
|
|
144
|
+
if (options.staged) {
|
|
145
|
+
const staged = getStagedFiles();
|
|
146
|
+
if (staged.length > 0) {
|
|
147
|
+
changedFiles = staged;
|
|
148
|
+
} else {
|
|
149
|
+
if (headSha && readCommitCache().has(headSha)) {
|
|
150
|
+
console.log(
|
|
151
|
+
chalk.green(`\u2713 Commit ${headSha.slice(0, 8)} already verified \u2014 nothing to re-check.`)
|
|
152
|
+
);
|
|
153
|
+
process.exit(0);
|
|
154
|
+
}
|
|
155
|
+
const lastCommit = getLastCommitFiles();
|
|
156
|
+
if (lastCommit.length > 0) {
|
|
157
|
+
changedFiles = lastCommit;
|
|
158
|
+
usedHeadShaForCache = true;
|
|
159
|
+
console.log(
|
|
160
|
+
chalk.yellow("No staged files. Running checks scoped to the most recent commit's files.")
|
|
161
|
+
);
|
|
162
|
+
} else {
|
|
163
|
+
console.log(chalk.green("\u2713 No staged changes and no recent commit \u2014 nothing to check."));
|
|
164
|
+
process.exit(0);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
changedFiles = getAllChangedFiles();
|
|
169
|
+
if (changedFiles.length === 0) {
|
|
170
|
+
console.log(
|
|
171
|
+
chalk.yellow("No changed files detected. Running all checks with no file filter.")
|
|
172
|
+
);
|
|
173
|
+
console.log(
|
|
174
|
+
chalk.dim(
|
|
175
|
+
" Tip: if you just ran `holdpoint init`, commit the generated files to clear the git-commit check."
|
|
176
|
+
)
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const guides = Object.entries(config.context?.guides ?? {});
|
|
181
|
+
if (guides.length > 0) {
|
|
182
|
+
console.log(chalk.cyan("\nProject guides:"));
|
|
183
|
+
for (const [key, text] of guides) {
|
|
184
|
+
console.log(chalk.bold(` ${key}:`), chalk.dim(String(text).trim()));
|
|
185
|
+
}
|
|
186
|
+
console.log("");
|
|
187
|
+
}
|
|
188
|
+
const taskCount = config.checks.filter(
|
|
189
|
+
(c) => c.cmd !== void 0 && checkHook(c) === hook
|
|
190
|
+
).length;
|
|
191
|
+
const spinner = ora(`Running ${taskCount} task(s)\u2026`).start();
|
|
192
|
+
const effectiveFiles = changedFiles.length > 0 ? changedFiles : ["__all__"];
|
|
193
|
+
const results = runDeterministicChecks(config, effectiveFiles, hook);
|
|
194
|
+
const passed = results.filter((r) => r.status === "pass");
|
|
195
|
+
const failed = results.filter((r) => r.status === "fail");
|
|
196
|
+
const skipped = results.filter((r) => r.status === "skip");
|
|
197
|
+
spinner.stop();
|
|
198
|
+
for (const result of results) {
|
|
199
|
+
printResult(result);
|
|
200
|
+
}
|
|
201
|
+
console.log("");
|
|
202
|
+
console.log(
|
|
203
|
+
[
|
|
204
|
+
chalk.green(`\u2713 ${passed.length} passed`),
|
|
205
|
+
failed.length > 0 ? chalk.red(`\u2717 ${failed.length} failed`) : "",
|
|
206
|
+
skipped.length > 0 ? chalk.gray(`\u25CC ${skipped.length} skipped`) : ""
|
|
207
|
+
].filter(Boolean).join(" ")
|
|
208
|
+
);
|
|
209
|
+
const promptChecks = changedFiles.length > 0 ? config.checks.filter(
|
|
210
|
+
(c) => c.prompt !== void 0 && checkHook(c) === hook && matchesWhen(c.when, changedFiles, config.patterns)
|
|
211
|
+
) : [];
|
|
212
|
+
if (promptChecks.length > 0) {
|
|
213
|
+
console.log(`
|
|
214
|
+
${chalk.cyan("Agent prompts to act on:")}`);
|
|
215
|
+
for (const c of promptChecks) {
|
|
216
|
+
console.log(` ${chalk.yellow("\u25A1")} [${c.label}] ${c.prompt ?? ""}`);
|
|
217
|
+
}
|
|
218
|
+
} else if (changedFiles.length === 0) {
|
|
219
|
+
const totalPromptChecks = config.checks.filter((c) => c.prompt !== void 0).length;
|
|
220
|
+
if (totalPromptChecks > 0) {
|
|
221
|
+
console.log(
|
|
222
|
+
chalk.dim(
|
|
223
|
+
`
|
|
224
|
+
(${totalPromptChecks} prompt-style checks defined; they fire relative to changed files \u2014 none surfaced with no diff context)`
|
|
225
|
+
)
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const reportResults = [
|
|
230
|
+
...results.map((r) => ({
|
|
231
|
+
id: r.check.id,
|
|
232
|
+
label: r.check.label,
|
|
233
|
+
kind: "cmd",
|
|
234
|
+
status: r.status,
|
|
235
|
+
...r.output !== void 0 ? { output: r.output } : {},
|
|
236
|
+
...r.exitCode !== void 0 ? { exitCode: r.exitCode } : {},
|
|
237
|
+
...r.skipReason !== void 0 ? { skipReason: r.skipReason } : {}
|
|
238
|
+
})),
|
|
239
|
+
...promptChecks.map((c) => ({
|
|
240
|
+
id: c.id,
|
|
241
|
+
label: c.label,
|
|
242
|
+
kind: "prompt",
|
|
243
|
+
status: "shown"
|
|
244
|
+
}))
|
|
65
245
|
];
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
246
|
+
const run = {
|
|
247
|
+
sha: headSha,
|
|
248
|
+
shortSha: headSha ? headSha.slice(0, 8) : null,
|
|
249
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
250
|
+
files: changedFiles.length > 0 ? changedFiles : [],
|
|
251
|
+
results: reportResults,
|
|
252
|
+
summary: {
|
|
253
|
+
passed: passed.length,
|
|
254
|
+
failed: failed.length,
|
|
255
|
+
skipped: skipped.length,
|
|
256
|
+
shown: promptChecks.length
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
recordCheckReport(run);
|
|
260
|
+
const project = identifyProject(process.cwd());
|
|
261
|
+
const bridge = new BridgeClient();
|
|
262
|
+
const liveEvents = reportResults.filter(
|
|
263
|
+
(result) => result.kind === "cmd"
|
|
264
|
+
).map((result, index) => ({
|
|
265
|
+
v: 1,
|
|
266
|
+
id: randomUUID(),
|
|
267
|
+
ts: Date.now() + index,
|
|
268
|
+
engine: "holdpoint",
|
|
269
|
+
session_id: "check-runner",
|
|
270
|
+
project_hash: project.hash,
|
|
271
|
+
cwd: process.cwd(),
|
|
272
|
+
type: "check_run",
|
|
273
|
+
payload: {
|
|
274
|
+
check_id: result.id,
|
|
275
|
+
label: result.label,
|
|
276
|
+
status: result.status,
|
|
277
|
+
duration_ms: 0,
|
|
278
|
+
...result.output ? { output: result.output } : {}
|
|
279
|
+
}
|
|
280
|
+
}));
|
|
281
|
+
if (liveEvents.length > 0) {
|
|
282
|
+
await bridge.sendEvents(liveEvents);
|
|
283
|
+
}
|
|
284
|
+
if (failed.length > 0) {
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
if (usedHeadShaForCache && headSha) {
|
|
288
|
+
recordCommitCache(headSha);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function printResult(result) {
|
|
292
|
+
const icon = result.status === "pass" ? chalk.green("\u2713") : result.status === "fail" ? chalk.red("\u2717") : result.status === "skip" ? chalk.gray("\u25CC") : chalk.yellow("\u2026");
|
|
293
|
+
const label = result.check.label;
|
|
294
|
+
console.log(`${icon} ${label}`);
|
|
295
|
+
if (result.status === "fail" && result.output) {
|
|
296
|
+
const trimmed = result.output.trim().split("\n").slice(0, 10).join("\n");
|
|
297
|
+
console.log(chalk.dim(trimmed.replace(/^/gm, " ")));
|
|
298
|
+
}
|
|
299
|
+
if (result.status === "skip" && result.skipReason) {
|
|
300
|
+
console.log(chalk.dim(` ${result.skipReason}`));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
79
303
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
async function
|
|
85
|
-
const spinner = ora("Initialising Holdpoint\u2026").start();
|
|
86
|
-
const stack = options.stack ?? detectStack();
|
|
87
|
-
const agent = options.agent ?? detectAgent();
|
|
88
|
-
spinner.text = `Detected stack: ${chalk.cyan(stack)}, agent: ${chalk.cyan(agent)}`;
|
|
89
|
-
let yamlContent = MINIMAL_CHECKS_YAML;
|
|
304
|
+
// src/commands/validate.ts
|
|
305
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
306
|
+
import chalk2 from "chalk";
|
|
307
|
+
import { parseHoldpointYaml as parseHoldpointYaml2, validateConfig } from "@holdpoint/yaml-core";
|
|
308
|
+
async function validateCommand() {
|
|
90
309
|
if (!existsSync2("checks.yaml")) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
310
|
+
console.error(chalk2.red("No checks.yaml found. Run `holdpoint init` first."));
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
313
|
+
const text = readFileSync2("checks.yaml", "utf8");
|
|
314
|
+
let config;
|
|
315
|
+
try {
|
|
316
|
+
config = parseHoldpointYaml2(text);
|
|
317
|
+
} catch (err) {
|
|
318
|
+
console.error(chalk2.red("Parse error:"), err.message);
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
const result = validateConfig(config);
|
|
322
|
+
if (result.valid) {
|
|
323
|
+
console.log(chalk2.green("\u2713 checks.yaml is valid"));
|
|
324
|
+
console.log(
|
|
325
|
+
chalk2.dim(
|
|
326
|
+
` ${config.checks.filter((c) => c.cmd !== void 0).length} tasks, ${config.checks.filter((c) => c.prompt !== void 0).length} prompts, ${config.conditions.length} conditions`
|
|
327
|
+
)
|
|
328
|
+
);
|
|
96
329
|
} else {
|
|
97
|
-
|
|
330
|
+
console.error(chalk2.red("\u2717 checks.yaml has errors:"));
|
|
331
|
+
for (const err of result.errors) {
|
|
332
|
+
console.error(` ${chalk2.yellow(err.path)}: ${err.message}`);
|
|
333
|
+
}
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// src/commands/update.ts
|
|
339
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
340
|
+
import chalk3 from "chalk";
|
|
341
|
+
import ora2 from "ora";
|
|
342
|
+
import { parseHoldpointYaml as parseHoldpointYaml3 } from "@holdpoint/yaml-core";
|
|
343
|
+
import { buildConfigJson, buildEngine } from "@holdpoint/engine-copilot";
|
|
344
|
+
import { buildEngineJson as buildClaudeEngineJson } from "@holdpoint/engine-claude";
|
|
345
|
+
import {
|
|
346
|
+
buildCheckScript as buildCursorCheckScript,
|
|
347
|
+
buildEngine as buildCursorEngine,
|
|
348
|
+
buildHooksJson as buildCursorHooksJson
|
|
349
|
+
} from "@holdpoint/engine-cursor";
|
|
350
|
+
import {
|
|
351
|
+
buildConfigToml as buildCodexConfigToml,
|
|
352
|
+
buildHooksJson as buildCodexHooksJson,
|
|
353
|
+
buildCheckScript as buildCodexCheckScript
|
|
354
|
+
} from "@holdpoint/engine-codex";
|
|
355
|
+
var MINIMAL_PREREQUISITES = `# Holdpoint prerequisites
|
|
356
|
+
|
|
357
|
+
Holdpoint installed repo-local engine integrations for one or more AI coding agents. Before relying on them locally, review these setup notes:
|
|
358
|
+
|
|
359
|
+
- **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.
|
|
360
|
+
- **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.
|
|
361
|
+
- **OpenAI Codex** \u2014 project-level hooks require trust approval. Run \`codex trust\` in the Codex TUI or review the hook with \`/hooks\`.
|
|
362
|
+
- **General** \u2014 Holdpoint expects Node.js 18+ and a git repository so \`holdpoint init\`, \`holdpoint update\`, and \`holdpoint check\` can run normally.
|
|
363
|
+
|
|
364
|
+
Docs: https://holdpoint.dev/docs
|
|
365
|
+
`;
|
|
366
|
+
var MINIMAL_HOLDPOINT_REFERENCE = `# Holdpoint reference
|
|
367
|
+
|
|
368
|
+
Read \`MASTER_PROMPT.md\` first for the mandatory workflow, then use this file for deeper project-specific Holdpoint notes.
|
|
369
|
+
`;
|
|
370
|
+
async function updateCommand() {
|
|
371
|
+
if (!existsSync3("checks.yaml")) {
|
|
372
|
+
console.error(chalk3.red("No checks.yaml found. Run `holdpoint init` first."));
|
|
373
|
+
process.exit(1);
|
|
98
374
|
}
|
|
99
|
-
const
|
|
375
|
+
const spinner = ora2("Updating Holdpoint engine files\u2026").start();
|
|
376
|
+
const config = parseHoldpointYaml3(readFileSync3("checks.yaml", "utf8"));
|
|
377
|
+
const detected = detectInstalledAgents();
|
|
378
|
+
const agents = detected.length > 0 ? detected : ["copilot", "claude", "cursor", "codex"];
|
|
100
379
|
const generatedDir = ".github/holdpoint/generated";
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if (
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
if (
|
|
110
|
-
|
|
380
|
+
mkdirSync2(generatedDir, { recursive: true });
|
|
381
|
+
writeFileSync2(`${generatedDir}/checks.immutable.json`, buildConfigJson(config), "utf8");
|
|
382
|
+
if (agents.includes("copilot")) {
|
|
383
|
+
const extDir = ".github/extensions/holdpoint";
|
|
384
|
+
mkdirSync2(extDir, { recursive: true });
|
|
385
|
+
writeFileSync2(`${extDir}/extension.mjs`, buildEngine(config), "utf8");
|
|
386
|
+
spliceBreadcrumb(".github/copilot-instructions.md");
|
|
387
|
+
}
|
|
388
|
+
if (agents.includes("claude")) {
|
|
389
|
+
mkdirSync2(".claude", { recursive: true });
|
|
111
390
|
const settingsPath = ".claude/settings.json";
|
|
112
391
|
let existing = {};
|
|
113
|
-
if (
|
|
392
|
+
if (existsSync3(settingsPath)) {
|
|
114
393
|
try {
|
|
115
|
-
existing = JSON.parse(
|
|
394
|
+
existing = JSON.parse(readFileSync3(settingsPath, "utf8"));
|
|
116
395
|
} catch {
|
|
117
396
|
}
|
|
118
397
|
}
|
|
119
|
-
const
|
|
120
|
-
|
|
398
|
+
const hooks = JSON.parse(buildClaudeEngineJson(config));
|
|
399
|
+
writeFileSync2(
|
|
121
400
|
settingsPath,
|
|
122
|
-
JSON.stringify(
|
|
123
|
-
"utf8"
|
|
401
|
+
JSON.stringify(mergeClaudeSettings(existing, hooks), null, 2) + "\n"
|
|
124
402
|
);
|
|
403
|
+
spliceBreadcrumb("CLAUDE.md");
|
|
125
404
|
}
|
|
126
|
-
if (
|
|
405
|
+
if (agents.includes("cursor")) {
|
|
406
|
+
mkdirSync2(".cursor", { recursive: true });
|
|
407
|
+
const cursorHooksPath = ".cursor/hooks.json";
|
|
408
|
+
let existingHooks = {};
|
|
409
|
+
if (existsSync3(cursorHooksPath)) {
|
|
410
|
+
try {
|
|
411
|
+
existingHooks = JSON.parse(readFileSync3(cursorHooksPath, "utf8"));
|
|
412
|
+
} catch {
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
const cursorHooks = JSON.parse(buildCursorHooksJson(config));
|
|
416
|
+
writeFileSync2(
|
|
417
|
+
cursorHooksPath,
|
|
418
|
+
JSON.stringify(mergeCursorHooks(existingHooks, cursorHooks), null, 2) + "\n",
|
|
419
|
+
"utf8"
|
|
420
|
+
);
|
|
421
|
+
writeFileSync2(".cursor/holdpoint-hook.mjs", buildCursorCheckScript(), "utf8");
|
|
127
422
|
const cursorRules = buildCursorEngine(config);
|
|
128
423
|
const cursorPath = ".cursorrules";
|
|
129
|
-
if (
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
424
|
+
if (existsSync3(cursorPath)) {
|
|
425
|
+
const content = readFileSync3(cursorPath, "utf8");
|
|
426
|
+
const start = content.indexOf("# \u2500\u2500\u2500 Holdpoint Rules");
|
|
427
|
+
const end = content.indexOf("# \u2500\u2500\u2500 End Holdpoint Rules \u2500\u2500\u2500");
|
|
428
|
+
if (start !== -1 && end !== -1) {
|
|
429
|
+
const afterEnd = content.indexOf("\n", end);
|
|
430
|
+
const prefix = content.slice(0, start).trimEnd();
|
|
431
|
+
const suffix = content.slice(afterEnd === -1 ? end : afterEnd + 1).trimStart();
|
|
432
|
+
const updated = (prefix ? `${prefix}
|
|
433
|
+
|
|
434
|
+
` : "") + cursorRules + (suffix ? `
|
|
435
|
+
${suffix}` : "");
|
|
436
|
+
writeFileSync2(cursorPath, updated);
|
|
437
|
+
} else {
|
|
438
|
+
writeFileSync2(cursorPath, `${content.trimEnd()}
|
|
439
|
+
|
|
440
|
+
${cursorRules}`);
|
|
133
441
|
}
|
|
134
442
|
} else {
|
|
135
|
-
|
|
443
|
+
writeFileSync2(cursorPath, cursorRules);
|
|
136
444
|
}
|
|
445
|
+
spliceBreadcrumb(".cursor/rules/holdpoint.md");
|
|
137
446
|
}
|
|
138
|
-
if (
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
447
|
+
if (agents.includes("codex")) {
|
|
448
|
+
mkdirSync2(".codex", { recursive: true });
|
|
449
|
+
writeFileSync2(".codex/hooks.json", buildCodexHooksJson(config), "utf8");
|
|
450
|
+
writeFileSync2(".codex/holdpoint-check.mjs", buildCodexCheckScript(config), "utf8");
|
|
451
|
+
const configTomlPath = ".codex/config.toml";
|
|
452
|
+
if (!existsSync3(configTomlPath)) {
|
|
453
|
+
writeFileSync2(configTomlPath, buildCodexConfigToml(), "utf8");
|
|
142
454
|
} else {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
"
|
|
146
|
-
|
|
147
|
-
|
|
455
|
+
const existing = readFileSync3(configTomlPath, "utf8");
|
|
456
|
+
if (!existing.includes("[features]")) {
|
|
457
|
+
writeFileSync2(configTomlPath, existing.trimEnd() + "\n\n" + buildCodexConfigToml(), "utf8");
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
spliceBreadcrumb("AGENTS.md");
|
|
461
|
+
}
|
|
462
|
+
const wroteReference = ensureBundledFile(
|
|
463
|
+
"HOLDPOINT_REFERENCE.md",
|
|
464
|
+
"HOLDPOINT_REFERENCE.md",
|
|
465
|
+
MINIMAL_HOLDPOINT_REFERENCE
|
|
466
|
+
);
|
|
467
|
+
const wrotePrerequisites = ensureBundledFile(
|
|
468
|
+
"HOLDPOINT_PREREQUISITES.md",
|
|
469
|
+
"HOLDPOINT_PREREQUISITES.md",
|
|
470
|
+
MINIMAL_PREREQUISITES
|
|
471
|
+
);
|
|
472
|
+
spinner.succeed(chalk3.green("Engine files updated from current checks.yaml"));
|
|
473
|
+
if (wroteReference) {
|
|
474
|
+
console.log(
|
|
475
|
+
chalk3.cyan("Created HOLDPOINT_REFERENCE.md with the full Holdpoint workflow reference.")
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
if (wrotePrerequisites) {
|
|
479
|
+
console.log(
|
|
480
|
+
chalk3.cyan(
|
|
481
|
+
"Created HOLDPOINT_PREREQUISITES.md with Copilot experimental-mode and other agent setup notes."
|
|
482
|
+
)
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// src/commands/build.ts
|
|
488
|
+
import chalk4 from "chalk";
|
|
489
|
+
|
|
490
|
+
// src/lib/ensure-daemon.ts
|
|
491
|
+
import { spawn } from "child_process";
|
|
492
|
+
import { readHealthyDaemonLock } from "@holdpoint/live-daemon";
|
|
493
|
+
function sleep(ms) {
|
|
494
|
+
return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
|
|
495
|
+
}
|
|
496
|
+
async function ensureDaemon(timeoutMs = 5e3) {
|
|
497
|
+
const existing = await readHealthyDaemonLock();
|
|
498
|
+
if (existing) {
|
|
499
|
+
return { info: existing, started: false };
|
|
500
|
+
}
|
|
501
|
+
const cliEntry = process.argv[1];
|
|
502
|
+
if (!cliEntry) {
|
|
503
|
+
throw new Error("Cannot determine the current holdpoint CLI entrypoint");
|
|
504
|
+
}
|
|
505
|
+
const child = spawn(process.execPath, [cliEntry, "daemon-serve"], {
|
|
506
|
+
stdio: "ignore",
|
|
507
|
+
env: process.env,
|
|
508
|
+
cwd: process.cwd()
|
|
509
|
+
});
|
|
510
|
+
child.unref();
|
|
511
|
+
const deadline = Date.now() + timeoutMs;
|
|
512
|
+
while (Date.now() < deadline) {
|
|
513
|
+
const lock = await readHealthyDaemonLock();
|
|
514
|
+
if (lock) {
|
|
515
|
+
return { info: lock, started: true };
|
|
148
516
|
}
|
|
517
|
+
await sleep(100);
|
|
149
518
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
${chalk.cyan("Next steps:")}
|
|
153
|
-
1. Edit ${chalk.yellow("checks.yaml")} to customise your eval checkpoints
|
|
154
|
-
2. Commit ${chalk.yellow("checks.yaml")} and the generated engine files
|
|
155
|
-
3. Run ${chalk.yellow("npx @holdpoint/cli@alpha check")} at any time to validate
|
|
519
|
+
throw new Error("Daemon unavailable + cannot spawn");
|
|
520
|
+
}
|
|
156
521
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
522
|
+
// src/lib/open-browser.ts
|
|
523
|
+
import { execSync as execSync2 } from "child_process";
|
|
524
|
+
function openBrowser(url) {
|
|
525
|
+
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
526
|
+
try {
|
|
527
|
+
execSync2(`${openCmd} ${JSON.stringify(url)}`, { stdio: "ignore" });
|
|
528
|
+
} catch {
|
|
529
|
+
}
|
|
160
530
|
}
|
|
161
531
|
|
|
162
|
-
// src/
|
|
163
|
-
import { existsSync as
|
|
164
|
-
import
|
|
165
|
-
import
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
532
|
+
// src/lib/project.ts
|
|
533
|
+
import { existsSync as existsSync4 } from "fs";
|
|
534
|
+
import { dirname, join as join2 } from "path";
|
|
535
|
+
import { identifyProject as identifyProject2 } from "@holdpoint/live-daemon";
|
|
536
|
+
function findChecksYaml(startDir) {
|
|
537
|
+
let current = startDir;
|
|
538
|
+
for (; ; ) {
|
|
539
|
+
const candidate = join2(current, "checks.yaml");
|
|
540
|
+
if (existsSync4(candidate)) {
|
|
541
|
+
return candidate;
|
|
542
|
+
}
|
|
543
|
+
const parent = dirname(current);
|
|
544
|
+
if (parent === current) {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
current = parent;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
function tryResolveCurrentProject() {
|
|
551
|
+
const checksYaml = findChecksYaml(process.cwd());
|
|
552
|
+
if (checksYaml) {
|
|
553
|
+
return identifyProject2(dirname(checksYaml));
|
|
554
|
+
}
|
|
555
|
+
try {
|
|
556
|
+
return identifyProject2(process.cwd());
|
|
557
|
+
} catch {
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
function appendProjectAuthParams(url, project) {
|
|
562
|
+
if (!project) {
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
url.searchParams.set("project", project.hash);
|
|
566
|
+
url.searchParams.set("name", project.name);
|
|
567
|
+
url.searchParams.set("root", project.root);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// src/commands/build.ts
|
|
571
|
+
async function buildCommand() {
|
|
572
|
+
const { info, started } = await ensureDaemon();
|
|
573
|
+
const url = new URL("/__holdpoint/live-auth", `http://127.0.0.1:${info.port}`);
|
|
574
|
+
url.searchParams.set("token", info.token);
|
|
575
|
+
url.searchParams.set("path", "/live/");
|
|
576
|
+
url.searchParams.set("tab", "checks");
|
|
577
|
+
appendProjectAuthParams(url, tryResolveCurrentProject());
|
|
578
|
+
openBrowser(url.toString());
|
|
579
|
+
console.log(
|
|
580
|
+
chalk4.green(
|
|
581
|
+
started ? "\u2713 Started Holdpoint Live and opened the builder" : "\u2713 Opened Holdpoint builder"
|
|
582
|
+
)
|
|
583
|
+
);
|
|
584
|
+
console.log(` url: ${chalk4.cyan(url.toString())}`);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// src/commands/evolve.ts
|
|
588
|
+
import { existsSync as existsSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
589
|
+
import { execSync as execSync5 } from "child_process";
|
|
590
|
+
import chalk5 from "chalk";
|
|
591
|
+
import ora3 from "ora";
|
|
592
|
+
import { parseHoldpointYaml as parseHoldpointYaml4, generateYaml } from "@holdpoint/yaml-core";
|
|
169
593
|
|
|
170
594
|
// src/evolve/scanner.ts
|
|
171
|
-
import { existsSync as
|
|
172
|
-
import { join as
|
|
173
|
-
import { execSync } from "child_process";
|
|
595
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync } from "fs";
|
|
596
|
+
import { join as join3 } from "path";
|
|
597
|
+
import { execSync as execSync3 } from "child_process";
|
|
174
598
|
function tryReadJson(path) {
|
|
175
599
|
try {
|
|
176
|
-
return JSON.parse(
|
|
600
|
+
return JSON.parse(readFileSync4(path, "utf8"));
|
|
177
601
|
} catch {
|
|
178
602
|
return null;
|
|
179
603
|
}
|
|
180
604
|
}
|
|
181
605
|
function tryReadText(path) {
|
|
182
606
|
try {
|
|
183
|
-
return
|
|
607
|
+
return readFileSync4(path, "utf8");
|
|
184
608
|
} catch {
|
|
185
609
|
return "";
|
|
186
610
|
}
|
|
187
611
|
}
|
|
188
612
|
function scanProject(cwd = process.cwd()) {
|
|
189
|
-
const exists = (p) =>
|
|
613
|
+
const exists = (p) => existsSync5(join3(cwd, p));
|
|
190
614
|
const packageManager = exists("pnpm-lock.yaml") ? "pnpm" : exists("yarn.lock") ? "yarn" : exists("bun.lockb") ? "bun" : "npm";
|
|
191
|
-
const pkg = tryReadJson(
|
|
615
|
+
const pkg = tryReadJson(join3(cwd, "package.json"));
|
|
192
616
|
const scripts = pkg?.scripts ?? {};
|
|
193
617
|
const deps = /* @__PURE__ */ new Set([
|
|
194
618
|
...Object.keys(pkg?.dependencies ?? {}),
|
|
195
619
|
...Object.keys(pkg?.devDependencies ?? {})
|
|
196
620
|
]);
|
|
197
|
-
const pyprojectText = tryReadText(
|
|
198
|
-
const requirementsText = tryReadText(
|
|
199
|
-
const pipfileText = tryReadText(
|
|
621
|
+
const pyprojectText = tryReadText(join3(cwd, "pyproject.toml"));
|
|
622
|
+
const requirementsText = tryReadText(join3(cwd, "requirements.txt"));
|
|
623
|
+
const pipfileText = tryReadText(join3(cwd, "Pipfile"));
|
|
200
624
|
const allPyText = pyprojectText + requirementsText + pipfileText;
|
|
201
625
|
const hasPytest = exists("pytest.ini") || exists("setup.cfg") || allPyText.includes("pytest") || allPyText.includes("[tool.pytest");
|
|
202
626
|
const hasRuff = allPyText.includes("ruff") || deps.has("ruff");
|
|
@@ -241,23 +665,138 @@ function scanProject(cwd = process.cwd()) {
|
|
|
241
665
|
hasOpenApi: exists("openapi.yaml") || exists("openapi.yml") || exists("openapi.json") || exists("api/openapi.yaml"),
|
|
242
666
|
// CI
|
|
243
667
|
hasGithubActions: exists(".github/workflows"),
|
|
668
|
+
// Release tooling — gates the `changelog-update` suggest template,
|
|
669
|
+
// since projects using changesets get release notes from .changeset
|
|
670
|
+
// files automatically and don't want a manual-CHANGELOG-entry check.
|
|
671
|
+
hasChangesets: exists(".changeset/config.json"),
|
|
244
672
|
packageManager,
|
|
245
673
|
scripts,
|
|
246
674
|
deps
|
|
247
675
|
};
|
|
248
676
|
}
|
|
249
677
|
|
|
250
|
-
// src/evolve/
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
678
|
+
// src/evolve/dead-checker.ts
|
|
679
|
+
import { execSync as execSync4 } from "child_process";
|
|
680
|
+
import { readdirSync as readdirSync2, existsSync as existsSync6 } from "fs";
|
|
681
|
+
import { join as join4 } from "path";
|
|
682
|
+
var NAMED_SCOPES = /* @__PURE__ */ new Set([
|
|
683
|
+
"frontend",
|
|
684
|
+
"backend",
|
|
685
|
+
"socket",
|
|
686
|
+
"visual",
|
|
687
|
+
"python",
|
|
688
|
+
"go",
|
|
689
|
+
"rust",
|
|
690
|
+
"java",
|
|
691
|
+
"ruby",
|
|
692
|
+
"database",
|
|
693
|
+
"prisma",
|
|
694
|
+
"testing",
|
|
695
|
+
"infra",
|
|
696
|
+
"ci",
|
|
697
|
+
"docs",
|
|
698
|
+
"structural"
|
|
699
|
+
]);
|
|
700
|
+
var WALK_IGNORED = /* @__PURE__ */ new Set([
|
|
701
|
+
"node_modules",
|
|
702
|
+
".git",
|
|
703
|
+
"dist",
|
|
704
|
+
"build",
|
|
705
|
+
".next",
|
|
706
|
+
".turbo",
|
|
707
|
+
"__pycache__",
|
|
708
|
+
".venv",
|
|
709
|
+
"venv",
|
|
710
|
+
".mypy_cache",
|
|
711
|
+
"target",
|
|
712
|
+
".cache",
|
|
713
|
+
"coverage"
|
|
714
|
+
]);
|
|
715
|
+
function walkDir(dir, root, depth, maxDepth) {
|
|
716
|
+
if (depth > maxDepth) return [];
|
|
717
|
+
let entries = [];
|
|
718
|
+
try {
|
|
719
|
+
entries = readdirSync2(dir);
|
|
720
|
+
} catch {
|
|
721
|
+
return [];
|
|
722
|
+
}
|
|
723
|
+
const results = [];
|
|
724
|
+
for (const entry of entries) {
|
|
725
|
+
if (WALK_IGNORED.has(entry) || entry.startsWith(".")) continue;
|
|
726
|
+
const full = join4(dir, entry);
|
|
727
|
+
const rel = full.slice(root.length + 1);
|
|
728
|
+
results.push(rel);
|
|
729
|
+
const children = walkDir(full, root, depth + 1, maxDepth);
|
|
730
|
+
results.push(...children);
|
|
731
|
+
}
|
|
732
|
+
return results;
|
|
733
|
+
}
|
|
734
|
+
function getRepoFiles(cwd) {
|
|
735
|
+
try {
|
|
736
|
+
const out = execSync4("git ls-files", {
|
|
737
|
+
cwd,
|
|
738
|
+
encoding: "utf8",
|
|
739
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
740
|
+
});
|
|
741
|
+
const files = out.trim().split("\n").filter(Boolean);
|
|
742
|
+
if (files.length > 0) return files;
|
|
743
|
+
} catch {
|
|
744
|
+
}
|
|
745
|
+
return walkDir(cwd, cwd, 0, 6);
|
|
746
|
+
}
|
|
747
|
+
function extractPathFromRegex(pattern) {
|
|
748
|
+
const cleaned = pattern.replace(/^\^/, "").replace(/\$$/, "").replace(/\\\./g, ".").replace(/\(\?:/g, "").replace(/\)/g, "").replace(/[|*+?[\]{}()]/g, "");
|
|
749
|
+
const candidate = cleaned.replace(/\/$/, "").trim();
|
|
750
|
+
if (candidate.length > 0 && /^[\w\-./]+$/.test(candidate)) return candidate;
|
|
751
|
+
return void 0;
|
|
752
|
+
}
|
|
753
|
+
function detectStaleChecks(config, repoFiles) {
|
|
754
|
+
const stale = [];
|
|
755
|
+
const userPatterns = config.patterns ?? {};
|
|
756
|
+
for (const check of config.checks) {
|
|
757
|
+
if (!check.when) continue;
|
|
758
|
+
if (NAMED_SCOPES.has(check.when)) continue;
|
|
759
|
+
if (check.conditionId) continue;
|
|
760
|
+
const patternAlias = check.when in userPatterns ? check.when : void 0;
|
|
761
|
+
const regexStr = patternAlias ? userPatterns[patternAlias] : check.when;
|
|
762
|
+
let re;
|
|
763
|
+
try {
|
|
764
|
+
re = new RegExp(regexStr);
|
|
765
|
+
} catch {
|
|
766
|
+
stale.push({ check, reason: `Invalid regex: '${regexStr}'` });
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
const matches = repoFiles.filter((f) => re.test(f));
|
|
770
|
+
if (matches.length === 0) {
|
|
771
|
+
const label = patternAlias ? `Pattern '${patternAlias}' (= '${regexStr}')` : `Regex '${regexStr}'`;
|
|
772
|
+
const suggestedConditionPath = extractPathFromRegex(regexStr);
|
|
773
|
+
const pathGone = !suggestedConditionPath || !existsSync6(join4(process.cwd(), suggestedConditionPath));
|
|
774
|
+
if (pathGone) {
|
|
775
|
+
stale.push({
|
|
776
|
+
check,
|
|
777
|
+
reason: `${label} matches 0 files in the repo`,
|
|
778
|
+
...suggestedConditionPath ? { suggestedConditionPath } : {}
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return stale;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// src/evolve/templates.ts
|
|
787
|
+
function pmScript(profile, script, fallback) {
|
|
788
|
+
if (!profile.scripts[script]) return fallback;
|
|
789
|
+
if (profile.packageManager === "npm") return `npm run ${script}`;
|
|
790
|
+
return `${profile.packageManager} ${script}`;
|
|
791
|
+
}
|
|
792
|
+
var blockedMarkerTerms = ["TODO", "FIXME", "HACK", "XXX"];
|
|
793
|
+
var blockedMarkerLabel = `No ${blockedMarkerTerms[0]}/${blockedMarkerTerms[1]} left in changed code`;
|
|
794
|
+
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.`;
|
|
795
|
+
function getTemplates(profile) {
|
|
796
|
+
return [
|
|
797
|
+
// ── Universal checks (always proposed for any project) ──────────────────
|
|
798
|
+
{
|
|
799
|
+
id: "git-commit",
|
|
261
800
|
label: "Commit all changes before finishing",
|
|
262
801
|
cmd: 'git rev-parse --is-inside-work-tree 2>/dev/null || exit 0; [ -z "$(git status --porcelain)" ] && exit 0; git status --short; exit 1',
|
|
263
802
|
trigger: () => true
|
|
@@ -266,7 +805,11 @@ function getTemplates(profile) {
|
|
|
266
805
|
id: "changelog-update",
|
|
267
806
|
label: "Add a CHANGELOG.md entry for this session",
|
|
268
807
|
prompt: "Before committing, add an entry to CHANGELOG.md describing what was done. Use Keep a Changelog format \u2014 add under ## [Unreleased] (create the file and that section if absent). Group entries as Added, Changed, Fixed, or Removed. Be concise but specific. The entry text will serve as the commit message.",
|
|
269
|
-
|
|
808
|
+
// Don't propose this for changesets-using projects — those get
|
|
809
|
+
// release notes from .changeset/*.md files automatically and the
|
|
810
|
+
// sibling `changelog-changeset` check is what they should use
|
|
811
|
+
// instead. Proposing both would be confusing and contradictory.
|
|
812
|
+
trigger: (p) => !p.hasChangesets
|
|
270
813
|
},
|
|
271
814
|
{
|
|
272
815
|
id: "readme-sync",
|
|
@@ -276,8 +819,8 @@ function getTemplates(profile) {
|
|
|
276
819
|
},
|
|
277
820
|
{
|
|
278
821
|
id: "no-todos",
|
|
279
|
-
label:
|
|
280
|
-
prompt:
|
|
822
|
+
label: blockedMarkerLabel,
|
|
823
|
+
prompt: blockedMarkerPrompt,
|
|
281
824
|
trigger: () => true
|
|
282
825
|
},
|
|
283
826
|
// ── TypeScript / JavaScript ──────────────────────────────────────────────
|
|
@@ -414,441 +957,41 @@ function getTemplates(profile) {
|
|
|
414
957
|
];
|
|
415
958
|
}
|
|
416
959
|
|
|
417
|
-
// src/evolve
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
"python",
|
|
427
|
-
"go",
|
|
428
|
-
"rust",
|
|
429
|
-
"java",
|
|
430
|
-
"ruby",
|
|
431
|
-
"database",
|
|
432
|
-
"prisma",
|
|
433
|
-
"testing",
|
|
434
|
-
"infra",
|
|
435
|
-
"ci",
|
|
436
|
-
"docs",
|
|
437
|
-
"structural"
|
|
438
|
-
]);
|
|
439
|
-
var WALK_IGNORED = /* @__PURE__ */ new Set([
|
|
440
|
-
"node_modules",
|
|
441
|
-
".git",
|
|
442
|
-
"dist",
|
|
443
|
-
"build",
|
|
444
|
-
".next",
|
|
445
|
-
".turbo",
|
|
446
|
-
"__pycache__",
|
|
447
|
-
".venv",
|
|
448
|
-
"venv",
|
|
449
|
-
".mypy_cache",
|
|
450
|
-
"target",
|
|
451
|
-
".cache",
|
|
452
|
-
"coverage"
|
|
453
|
-
]);
|
|
454
|
-
function walkDir(dir, root, depth, maxDepth) {
|
|
455
|
-
if (depth > maxDepth) return [];
|
|
456
|
-
let entries = [];
|
|
457
|
-
try {
|
|
458
|
-
entries = readdirSync2(dir);
|
|
459
|
-
} catch {
|
|
460
|
-
return [];
|
|
461
|
-
}
|
|
462
|
-
const results = [];
|
|
463
|
-
for (const entry of entries) {
|
|
464
|
-
if (WALK_IGNORED.has(entry) || entry.startsWith(".")) continue;
|
|
465
|
-
const full = join3(dir, entry);
|
|
466
|
-
const rel = full.slice(root.length + 1);
|
|
467
|
-
results.push(rel);
|
|
468
|
-
const children = walkDir(full, root, depth + 1, maxDepth);
|
|
469
|
-
results.push(...children);
|
|
470
|
-
}
|
|
471
|
-
return results;
|
|
472
|
-
}
|
|
473
|
-
function getRepoFiles(cwd) {
|
|
474
|
-
try {
|
|
475
|
-
const out = execSync2("git ls-files", {
|
|
476
|
-
cwd,
|
|
477
|
-
encoding: "utf8",
|
|
478
|
-
stdio: ["pipe", "pipe", "ignore"]
|
|
479
|
-
});
|
|
480
|
-
const files = out.trim().split("\n").filter(Boolean);
|
|
481
|
-
if (files.length > 0) return files;
|
|
482
|
-
} catch {
|
|
483
|
-
}
|
|
484
|
-
return walkDir(cwd, cwd, 0, 6);
|
|
485
|
-
}
|
|
486
|
-
function extractPathFromRegex(pattern) {
|
|
487
|
-
const cleaned = pattern.replace(/^\^/, "").replace(/\$$/, "").replace(/\\\./g, ".").replace(/\(\?:/g, "").replace(/\)/g, "").replace(/[|*+?[\]{}()]/g, "");
|
|
488
|
-
const candidate = cleaned.replace(/\/$/, "").trim();
|
|
489
|
-
if (candidate.length > 0 && /^[\w\-./]+$/.test(candidate)) return candidate;
|
|
490
|
-
return void 0;
|
|
491
|
-
}
|
|
492
|
-
function detectStaleChecks(config, repoFiles) {
|
|
493
|
-
const stale = [];
|
|
494
|
-
const userPatterns = config.patterns ?? {};
|
|
495
|
-
for (const check of config.checks) {
|
|
496
|
-
if (!check.when) continue;
|
|
497
|
-
if (NAMED_SCOPES.has(check.when)) continue;
|
|
498
|
-
if (check.conditionId) continue;
|
|
499
|
-
const patternAlias = check.when in userPatterns ? check.when : void 0;
|
|
500
|
-
const regexStr = patternAlias ? userPatterns[patternAlias] : check.when;
|
|
501
|
-
let re;
|
|
502
|
-
try {
|
|
503
|
-
re = new RegExp(regexStr);
|
|
504
|
-
} catch {
|
|
505
|
-
stale.push({ check, reason: `Invalid regex: '${regexStr}'` });
|
|
506
|
-
continue;
|
|
507
|
-
}
|
|
508
|
-
const matches = repoFiles.filter((f) => re.test(f));
|
|
509
|
-
if (matches.length === 0) {
|
|
510
|
-
const label = patternAlias ? `Pattern '${patternAlias}' (= '${regexStr}')` : `Regex '${regexStr}'`;
|
|
511
|
-
const suggestedConditionPath = extractPathFromRegex(regexStr);
|
|
512
|
-
const pathGone = !suggestedConditionPath || !existsSync4(join3(process.cwd(), suggestedConditionPath));
|
|
513
|
-
if (pathGone) {
|
|
514
|
-
stale.push({
|
|
515
|
-
check,
|
|
516
|
-
reason: `${label} matches 0 files in the repo`,
|
|
517
|
-
...suggestedConditionPath ? { suggestedConditionPath } : {}
|
|
518
|
-
});
|
|
519
|
-
}
|
|
960
|
+
// src/commands/evolve.ts
|
|
961
|
+
function extractHeader(yaml) {
|
|
962
|
+
const lines = yaml.split("\n");
|
|
963
|
+
const commentLines = [];
|
|
964
|
+
for (const line of lines) {
|
|
965
|
+
if (line.startsWith("#") || commentLines.length > 0 && line.trim() === "") {
|
|
966
|
+
commentLines.push(line);
|
|
967
|
+
} else {
|
|
968
|
+
break;
|
|
520
969
|
}
|
|
521
970
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
// src/commands/check.ts
|
|
526
|
-
function getStagedFiles() {
|
|
527
|
-
try {
|
|
528
|
-
const out = execSync3("git diff --cached --name-only", {
|
|
529
|
-
encoding: "utf8",
|
|
530
|
-
stdio: ["pipe", "pipe", "ignore"]
|
|
531
|
-
});
|
|
532
|
-
return out.trim().split("\n").filter(Boolean);
|
|
533
|
-
} catch {
|
|
534
|
-
return [];
|
|
971
|
+
while (commentLines.length > 0 && commentLines[commentLines.length - 1]?.trim() === "") {
|
|
972
|
+
commentLines.pop();
|
|
535
973
|
}
|
|
974
|
+
return commentLines.join("\n");
|
|
536
975
|
}
|
|
537
|
-
function
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
encoding: "utf8",
|
|
541
|
-
stdio: ["pipe", "pipe", "ignore"]
|
|
542
|
-
});
|
|
543
|
-
return out.trim().split("\n").filter(Boolean);
|
|
544
|
-
} catch {
|
|
545
|
-
return [];
|
|
546
|
-
}
|
|
976
|
+
function withHeader(header, newYaml) {
|
|
977
|
+
if (!header) return newYaml;
|
|
978
|
+
return header + "\n\n" + newYaml;
|
|
547
979
|
}
|
|
548
|
-
async function
|
|
549
|
-
if (!
|
|
550
|
-
console.error(
|
|
980
|
+
async function evolveCommand(options) {
|
|
981
|
+
if (!existsSync7("checks.yaml")) {
|
|
982
|
+
console.error(chalk5.red("No checks.yaml found. Run `holdpoint init` first."));
|
|
551
983
|
process.exit(1);
|
|
552
984
|
}
|
|
553
|
-
const
|
|
554
|
-
let config;
|
|
555
|
-
try {
|
|
556
|
-
config = parseHoldpointYaml2(yamlContent);
|
|
557
|
-
} catch (err) {
|
|
558
|
-
console.error(chalk2.red("Invalid checks.yaml:"), err.message);
|
|
559
|
-
process.exit(1);
|
|
560
|
-
}
|
|
561
|
-
const changedFiles = options.staged ? getStagedFiles() : getAllChangedFiles();
|
|
562
|
-
const guides = Object.entries(config.context?.guides ?? {});
|
|
563
|
-
if (guides.length > 0) {
|
|
564
|
-
console.log(chalk2.cyan("\nProject guides:"));
|
|
565
|
-
for (const [key, text] of guides) {
|
|
566
|
-
console.log(chalk2.bold(` ${key}:`), chalk2.dim(String(text).trim()));
|
|
567
|
-
}
|
|
568
|
-
console.log("");
|
|
569
|
-
}
|
|
570
|
-
if (changedFiles.length === 0) {
|
|
571
|
-
console.log(chalk2.yellow("No changed files detected. Running all checks with no file filter."));
|
|
572
|
-
}
|
|
573
|
-
const taskCount = config.checks.filter((c) => c.cmd !== void 0).length;
|
|
574
|
-
const spinner = ora2(`Running ${taskCount} task(s)\u2026`).start();
|
|
575
|
-
const effectiveFiles = changedFiles.length > 0 ? changedFiles : ["__all__"];
|
|
576
|
-
const results = runDeterministicChecks(config, effectiveFiles);
|
|
577
|
-
const runDrift = matchesWhen("structural", effectiveFiles);
|
|
578
|
-
if (runDrift) {
|
|
579
|
-
const profile = scanProject();
|
|
580
|
-
const existingIds = new Set(config.checks.map((c) => c.id));
|
|
581
|
-
const templates = getTemplates(profile);
|
|
582
|
-
const proposals = templates.filter((t) => t.trigger(profile) && !existingIds.has(t.id));
|
|
583
|
-
const repoFiles = getRepoFiles(process.cwd());
|
|
584
|
-
const staleChecks = detectStaleChecks(config, repoFiles);
|
|
585
|
-
if (proposals.length > 0 || staleChecks.length > 0) {
|
|
586
|
-
const lines = [];
|
|
587
|
-
if (proposals.length > 0) {
|
|
588
|
-
lines.push(`${proposals.length} new check(s) available for your project stack:`);
|
|
589
|
-
for (const p of proposals) lines.push(` + ${p.label}`);
|
|
590
|
-
}
|
|
591
|
-
if (staleChecks.length > 0) {
|
|
592
|
-
lines.push(`${staleChecks.length} stale check(s) no longer match your project:`);
|
|
593
|
-
for (const s of staleChecks) lines.push(` - ${s.check.label}: ${s.reason}`);
|
|
594
|
-
}
|
|
595
|
-
lines.push("\nRun: npx @holdpoint/cli@alpha evolve --apply");
|
|
596
|
-
results.push({
|
|
597
|
-
check: { id: "__holdpoint_evolve__", label: "Evolve checks with project structure" },
|
|
598
|
-
status: "fail",
|
|
599
|
-
output: lines.join("\n")
|
|
600
|
-
});
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
const passed = results.filter((r) => r.status === "pass");
|
|
604
|
-
const failed = results.filter((r) => r.status === "fail");
|
|
605
|
-
const skipped = results.filter((r) => r.status === "skip");
|
|
606
|
-
spinner.stop();
|
|
607
|
-
for (const result of results) {
|
|
608
|
-
printResult(result);
|
|
609
|
-
}
|
|
610
|
-
console.log("");
|
|
611
|
-
console.log(
|
|
612
|
-
[
|
|
613
|
-
chalk2.green(`\u2713 ${passed.length} passed`),
|
|
614
|
-
failed.length > 0 ? chalk2.red(`\u2717 ${failed.length} failed`) : "",
|
|
615
|
-
skipped.length > 0 ? chalk2.gray(`\u25CC ${skipped.length} skipped`) : ""
|
|
616
|
-
].filter(Boolean).join(" ")
|
|
617
|
-
);
|
|
618
|
-
const promptChecks = config.checks.filter(
|
|
619
|
-
(c) => c.prompt !== void 0 && matchesWhen(c.when, changedFiles.length > 0 ? changedFiles : ["__all__"], config.patterns)
|
|
620
|
-
);
|
|
621
|
-
if (promptChecks.length > 0) {
|
|
622
|
-
console.log(`
|
|
623
|
-
${chalk2.cyan("Agent prompts to act on:")}`);
|
|
624
|
-
for (const c of promptChecks) {
|
|
625
|
-
console.log(` ${chalk2.yellow("\u25A1")} [${c.label}] ${c.prompt ?? ""}`);
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
if (failed.length > 0) {
|
|
629
|
-
process.exit(1);
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
function printResult(result) {
|
|
633
|
-
const icon = result.status === "pass" ? chalk2.green("\u2713") : result.status === "fail" ? chalk2.red("\u2717") : result.status === "skip" ? chalk2.gray("\u25CC") : chalk2.yellow("\u2026");
|
|
634
|
-
const label = result.check.label;
|
|
635
|
-
console.log(`${icon} ${label}`);
|
|
636
|
-
if (result.status === "fail" && result.output) {
|
|
637
|
-
const trimmed = result.output.trim().split("\n").slice(0, 10).join("\n");
|
|
638
|
-
console.log(chalk2.dim(trimmed.replace(/^/gm, " ")));
|
|
639
|
-
}
|
|
640
|
-
if (result.status === "skip" && result.skipReason) {
|
|
641
|
-
console.log(chalk2.dim(` ${result.skipReason}`));
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
// src/commands/validate.ts
|
|
646
|
-
import { existsSync as existsSync6, readFileSync as readFileSync4 } from "fs";
|
|
647
|
-
import chalk3 from "chalk";
|
|
648
|
-
import { parseHoldpointYaml as parseHoldpointYaml3, validateConfig } from "@holdpoint/yaml-core";
|
|
649
|
-
async function validateCommand() {
|
|
650
|
-
if (!existsSync6("checks.yaml")) {
|
|
651
|
-
console.error(chalk3.red("No checks.yaml found. Run `holdpoint init` first."));
|
|
652
|
-
process.exit(1);
|
|
653
|
-
}
|
|
654
|
-
const text = readFileSync4("checks.yaml", "utf8");
|
|
655
|
-
let config;
|
|
656
|
-
try {
|
|
657
|
-
config = parseHoldpointYaml3(text);
|
|
658
|
-
} catch (err) {
|
|
659
|
-
console.error(chalk3.red("Parse error:"), err.message);
|
|
660
|
-
process.exit(1);
|
|
661
|
-
}
|
|
662
|
-
const result = validateConfig(config);
|
|
663
|
-
if (result.valid) {
|
|
664
|
-
console.log(chalk3.green("\u2713 checks.yaml is valid"));
|
|
665
|
-
console.log(
|
|
666
|
-
chalk3.dim(
|
|
667
|
-
` ${config.checks.filter((c) => c.cmd !== void 0).length} tasks, ${config.checks.filter((c) => c.prompt !== void 0).length} prompts, ${config.conditions.length} conditions`
|
|
668
|
-
)
|
|
669
|
-
);
|
|
670
|
-
} else {
|
|
671
|
-
console.error(chalk3.red("\u2717 checks.yaml has errors:"));
|
|
672
|
-
for (const err of result.errors) {
|
|
673
|
-
console.error(` ${chalk3.yellow(err.path)}: ${err.message}`);
|
|
674
|
-
}
|
|
675
|
-
process.exit(1);
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
// src/commands/update.ts
|
|
680
|
-
import { existsSync as existsSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
681
|
-
import chalk4 from "chalk";
|
|
682
|
-
import ora3 from "ora";
|
|
683
|
-
import { parseHoldpointYaml as parseHoldpointYaml4 } from "@holdpoint/yaml-core";
|
|
684
|
-
import { buildHookJson as buildHookJson2, buildCheckScript as buildCheckScript2, buildConfigJson as buildConfigJson2 } from "@holdpoint/engine-copilot";
|
|
685
|
-
import { buildEngineJson as buildClaudeEngineJson2 } from "@holdpoint/engine-claude";
|
|
686
|
-
import { buildEngine as buildCursorEngine2 } from "@holdpoint/engine-cursor";
|
|
687
|
-
async function updateCommand() {
|
|
688
|
-
if (!existsSync7("checks.yaml")) {
|
|
689
|
-
console.error(chalk4.red("No checks.yaml found. Run `holdpoint init` first."));
|
|
690
|
-
process.exit(1);
|
|
691
|
-
}
|
|
692
|
-
const spinner = ora3("Updating Holdpoint engine files\u2026").start();
|
|
693
|
-
const agent = detectAgent();
|
|
694
|
-
const config = parseHoldpointYaml4(readFileSync5("checks.yaml", "utf8"));
|
|
695
|
-
const generatedDir = ".github/holdpoint/generated";
|
|
696
|
-
mkdirSync2(generatedDir, { recursive: true });
|
|
697
|
-
writeFileSync2(`${generatedDir}/checks.immutable.json`, buildConfigJson2(config), "utf8");
|
|
698
|
-
if (agent === "copilot" || agent === "unknown") {
|
|
699
|
-
const hooksDir = ".github/hooks";
|
|
700
|
-
mkdirSync2(hooksDir, { recursive: true });
|
|
701
|
-
writeFileSync2(`${hooksDir}/holdpoint.json`, buildHookJson2(config), "utf8");
|
|
702
|
-
writeFileSync2(`${hooksDir}/holdpoint-check.mjs`, buildCheckScript2(config), "utf8");
|
|
703
|
-
spinner.text = `Updated ${chalk4.green(".github/hooks/holdpoint.json")} and ${chalk4.green(".github/hooks/holdpoint-check.mjs")}`;
|
|
704
|
-
}
|
|
705
|
-
if (agent === "claude") {
|
|
706
|
-
mkdirSync2(".claude", { recursive: true });
|
|
707
|
-
const settingsPath = ".claude/settings.json";
|
|
708
|
-
let existing = {};
|
|
709
|
-
if (existsSync7(settingsPath)) {
|
|
710
|
-
try {
|
|
711
|
-
existing = JSON.parse(readFileSync5(settingsPath, "utf8"));
|
|
712
|
-
} catch {
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
const hooks = JSON.parse(buildClaudeEngineJson2(config));
|
|
716
|
-
writeFileSync2(settingsPath, JSON.stringify({ ...existing, hooks: hooks.hooks }, null, 2));
|
|
717
|
-
}
|
|
718
|
-
if (agent === "cursor") {
|
|
719
|
-
const cursorRules = buildCursorEngine2(config);
|
|
720
|
-
const cursorPath = ".cursorrules";
|
|
721
|
-
if (existsSync7(cursorPath)) {
|
|
722
|
-
const content = readFileSync5(cursorPath, "utf8");
|
|
723
|
-
const start = content.indexOf("# \u2500\u2500\u2500 Holdpoint Rules");
|
|
724
|
-
const end = content.indexOf("# \u2500\u2500\u2500 End Holdpoint Rules \u2500\u2500\u2500");
|
|
725
|
-
if (start !== -1 && end !== -1) {
|
|
726
|
-
const afterEnd = content.indexOf("\n", end);
|
|
727
|
-
const updated = content.slice(0, start) + cursorRules + content.slice(afterEnd === -1 ? end : afterEnd + 1);
|
|
728
|
-
writeFileSync2(cursorPath, updated);
|
|
729
|
-
} else {
|
|
730
|
-
writeFileSync2(cursorPath, content + "\n" + cursorRules);
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
spinner.succeed(chalk4.green("Engine files updated from current checks.yaml"));
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
// src/commands/build.ts
|
|
738
|
-
import { createServer } from "http";
|
|
739
|
-
import { createReadStream, existsSync as existsSync8 } from "fs";
|
|
740
|
-
import { join as join4, extname, dirname as dirname2 } from "path";
|
|
741
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
742
|
-
import { execSync as execSync4 } from "child_process";
|
|
743
|
-
import chalk5 from "chalk";
|
|
744
|
-
var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
745
|
-
var MIME = {
|
|
746
|
-
".html": "text/html; charset=utf-8",
|
|
747
|
-
".js": "text/javascript",
|
|
748
|
-
".mjs": "text/javascript",
|
|
749
|
-
".css": "text/css",
|
|
750
|
-
".svg": "image/svg+xml",
|
|
751
|
-
".png": "image/png",
|
|
752
|
-
".ico": "image/x-icon",
|
|
753
|
-
".woff": "font/woff",
|
|
754
|
-
".woff2": "font/woff2",
|
|
755
|
-
".ttf": "font/ttf",
|
|
756
|
-
".json": "application/json"
|
|
757
|
-
};
|
|
758
|
-
function serveFile(res, filePath) {
|
|
759
|
-
const mime = MIME[extname(filePath)] ?? "application/octet-stream";
|
|
760
|
-
res.writeHead(200, { "Content-Type": mime });
|
|
761
|
-
createReadStream(filePath).pipe(res);
|
|
762
|
-
}
|
|
763
|
-
function handleRequest(req, res, uiDir) {
|
|
764
|
-
const url = (req.url ?? "/").split("?")[0] ?? "/";
|
|
765
|
-
if (url === "/__holdpoint/initial-yaml") {
|
|
766
|
-
const checksPath = join4(process.cwd(), "checks.yaml");
|
|
767
|
-
if (existsSync8(checksPath)) {
|
|
768
|
-
res.writeHead(200, { "Content-Type": "text/yaml; charset=utf-8" });
|
|
769
|
-
createReadStream(checksPath).pipe(res);
|
|
770
|
-
} else {
|
|
771
|
-
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
772
|
-
res.end("checks.yaml not found in current directory");
|
|
773
|
-
}
|
|
774
|
-
return;
|
|
775
|
-
}
|
|
776
|
-
const candidate = join4(uiDir, url === "/" ? "index.html" : url);
|
|
777
|
-
const filePath = existsSync8(candidate) ? candidate : join4(uiDir, "index.html");
|
|
778
|
-
serveFile(res, filePath);
|
|
779
|
-
}
|
|
780
|
-
async function buildCommand() {
|
|
781
|
-
const port = 4321;
|
|
782
|
-
const uiDir = join4(__dirname2, "builder-ui");
|
|
783
|
-
if (!existsSync8(uiDir)) {
|
|
784
|
-
console.error(chalk5.red("\u2717 Builder UI not found.\n"));
|
|
785
|
-
console.log(chalk5.dim(" This is unexpected for a published build of @holdpoint/cli."));
|
|
786
|
-
console.log(chalk5.dim(" If you installed from source, rebuild with: pnpm turbo build\n"));
|
|
787
|
-
process.exit(1);
|
|
788
|
-
}
|
|
789
|
-
const server = createServer((req, res) => handleRequest(req, res, uiDir));
|
|
790
|
-
await new Promise((resolve, reject) => {
|
|
791
|
-
server.listen(port, () => {
|
|
792
|
-
console.log(
|
|
793
|
-
`
|
|
794
|
-
${chalk5.green("\u2713")} Holdpoint builder running at ${chalk5.cyan(`http://localhost:${port}`)}`
|
|
795
|
-
);
|
|
796
|
-
console.log(chalk5.dim(" Edit checks.yaml, then reload the page to see updates"));
|
|
797
|
-
console.log(chalk5.dim(" Press Ctrl+C to stop\n"));
|
|
798
|
-
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
799
|
-
try {
|
|
800
|
-
execSync4(`${openCmd} http://localhost:${port}`, { stdio: "ignore" });
|
|
801
|
-
} catch {
|
|
802
|
-
}
|
|
803
|
-
});
|
|
804
|
-
server.on("error", reject);
|
|
805
|
-
process.on("SIGINT", () => {
|
|
806
|
-
console.log(chalk5.dim("\n Stopping builder\u2026"));
|
|
807
|
-
server.close(() => resolve());
|
|
808
|
-
});
|
|
809
|
-
});
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
// src/commands/evolve.ts
|
|
813
|
-
import { existsSync as existsSync9, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
|
|
814
|
-
import { execSync as execSync5 } from "child_process";
|
|
815
|
-
import chalk6 from "chalk";
|
|
816
|
-
import ora4 from "ora";
|
|
817
|
-
import { parseHoldpointYaml as parseHoldpointYaml5, generateYaml } from "@holdpoint/yaml-core";
|
|
818
|
-
function extractHeader(yaml) {
|
|
819
|
-
const lines = yaml.split("\n");
|
|
820
|
-
const commentLines = [];
|
|
821
|
-
for (const line of lines) {
|
|
822
|
-
if (line.startsWith("#") || commentLines.length > 0 && line.trim() === "") {
|
|
823
|
-
commentLines.push(line);
|
|
824
|
-
} else {
|
|
825
|
-
break;
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
while (commentLines.length > 0 && commentLines[commentLines.length - 1]?.trim() === "") {
|
|
829
|
-
commentLines.pop();
|
|
830
|
-
}
|
|
831
|
-
return commentLines.join("\n");
|
|
832
|
-
}
|
|
833
|
-
function withHeader(header, newYaml) {
|
|
834
|
-
if (!header) return newYaml;
|
|
835
|
-
return header + "\n\n" + newYaml;
|
|
836
|
-
}
|
|
837
|
-
async function evolveCommand(options) {
|
|
838
|
-
if (!existsSync9("checks.yaml")) {
|
|
839
|
-
console.error(chalk6.red("No checks.yaml found. Run `holdpoint init` first."));
|
|
840
|
-
process.exit(1);
|
|
841
|
-
}
|
|
842
|
-
const spinner = ora4("Scanning project profile\u2026").start();
|
|
985
|
+
const spinner = ora3("Scanning project profile\u2026").start();
|
|
843
986
|
const cwd = process.cwd();
|
|
844
987
|
const profile = scanProject(cwd);
|
|
845
988
|
const repoFiles = getRepoFiles(cwd);
|
|
846
|
-
const yamlContent =
|
|
989
|
+
const yamlContent = readFileSync5("checks.yaml", "utf8");
|
|
847
990
|
let config;
|
|
848
991
|
try {
|
|
849
|
-
config =
|
|
992
|
+
config = parseHoldpointYaml4(yamlContent);
|
|
850
993
|
} catch (err) {
|
|
851
|
-
spinner.fail(
|
|
994
|
+
spinner.fail(chalk5.red("Invalid checks.yaml:") + " " + err.message);
|
|
852
995
|
process.exit(1);
|
|
853
996
|
}
|
|
854
997
|
spinner.stop();
|
|
@@ -857,7 +1000,7 @@ async function evolveCommand(options) {
|
|
|
857
1000
|
const allTemplates = getTemplates(profile);
|
|
858
1001
|
const proposals = allTemplates.filter((t) => t.trigger(profile) && !existingIds.has(t.id));
|
|
859
1002
|
const staleChecks = detectStaleChecks(config, repoFiles);
|
|
860
|
-
console.log(
|
|
1003
|
+
console.log(chalk5.bold("\n\u{1F4CB} Project profile:"));
|
|
861
1004
|
const traits = [
|
|
862
1005
|
["TypeScript", profile.hasTypeScript, "tsconfig.json"],
|
|
863
1006
|
["ESLint", profile.hasEslint, "eslint.config.*"],
|
|
@@ -883,44 +1026,44 @@ async function evolveCommand(options) {
|
|
|
883
1026
|
];
|
|
884
1027
|
const detected = traits.filter(([, yes]) => yes);
|
|
885
1028
|
if (detected.length === 0) {
|
|
886
|
-
console.log(
|
|
1029
|
+
console.log(chalk5.dim(" (empty project \u2014 only universal checks apply)"));
|
|
887
1030
|
} else {
|
|
888
1031
|
for (const [name, , hint] of detected) {
|
|
889
|
-
console.log(` ${
|
|
1032
|
+
console.log(` ${chalk5.green("\u2713")} ${name.padEnd(18)} ${chalk5.dim(hint)}`);
|
|
890
1033
|
}
|
|
891
1034
|
}
|
|
892
1035
|
if (staleChecks.length > 0) {
|
|
893
|
-
console.log(
|
|
1036
|
+
console.log(chalk5.bold(`
|
|
894
1037
|
\u26A0\uFE0F Stale checks (${staleChecks.length}):`));
|
|
895
1038
|
for (const { check, reason, suggestedConditionPath } of staleChecks) {
|
|
896
|
-
const fix = suggestedConditionPath ?
|
|
897
|
-
console.log(` ${
|
|
1039
|
+
const fix = suggestedConditionPath ? chalk5.dim(` \u2192 will wrap with conditionId: file_exists: ${suggestedConditionPath}`) : chalk5.dim(" \u2192 no path inferred; review manually");
|
|
1040
|
+
console.log(` ${chalk5.yellow("\u25CC")} ${chalk5.bold(check.id)} ${chalk5.dim(reason)}${fix}`);
|
|
898
1041
|
}
|
|
899
1042
|
}
|
|
900
1043
|
if (proposals.length === 0 && staleChecks.length === 0) {
|
|
901
|
-
console.log(
|
|
1044
|
+
console.log(chalk5.green("\n\u2713 checks.yaml is fully in sync with the project profile."));
|
|
902
1045
|
return;
|
|
903
1046
|
}
|
|
904
1047
|
if (proposals.length > 0) {
|
|
905
|
-
console.log(
|
|
1048
|
+
console.log(chalk5.bold(`
|
|
906
1049
|
\u{1F4A1} Proposed additions (${proposals.length}):`));
|
|
907
1050
|
for (const t of proposals) {
|
|
908
|
-
const scope = t.when ?
|
|
909
|
-
const type = t.cmd ?
|
|
910
|
-
const preview = t.cmd ?
|
|
911
|
-
console.log(` ${
|
|
1051
|
+
const scope = t.when ? chalk5.cyan(` when: ${t.when}`) : "";
|
|
1052
|
+
const type = t.cmd ? chalk5.dim("cmd") : chalk5.dim("prompt");
|
|
1053
|
+
const preview = t.cmd ? chalk5.dim(` ${t.cmd.slice(0, 80)}${t.cmd.length > 80 ? "\u2026" : ""}`) : chalk5.dim(` ${(t.prompt ?? "").slice(0, 80)}${(t.prompt?.length ?? 0) > 80 ? "\u2026" : ""}`);
|
|
1054
|
+
console.log(` ${chalk5.green("+")} ${chalk5.bold(t.id.padEnd(24))} [${type}]${scope}`);
|
|
912
1055
|
console.log(` ${preview}`);
|
|
913
1056
|
}
|
|
914
1057
|
}
|
|
915
1058
|
if (!options.apply) {
|
|
916
1059
|
console.log(
|
|
917
|
-
|
|
1060
|
+
chalk5.red(`
|
|
918
1061
|
\u2717 checks.yaml is out of sync with the project profile.`) + `
|
|
919
|
-
Run ${
|
|
1062
|
+
Run ${chalk5.bold("npx @holdpoint/cli suggest --apply")} to apply these changes.`
|
|
920
1063
|
);
|
|
921
1064
|
process.exit(1);
|
|
922
1065
|
}
|
|
923
|
-
const applySpinner =
|
|
1066
|
+
const applySpinner = ora3("Applying changes to checks.yaml\u2026").start();
|
|
924
1067
|
const newConditions = [...config.conditions];
|
|
925
1068
|
for (const t of proposals) {
|
|
926
1069
|
if (t.condition && !existingConditionIds.has(t.condition.id)) {
|
|
@@ -960,36 +1103,878 @@ async function evolveCommand(options) {
|
|
|
960
1103
|
writeFileSync3("checks.yaml", newYaml, "utf8");
|
|
961
1104
|
applySpinner.text = "Running holdpoint update\u2026";
|
|
962
1105
|
try {
|
|
963
|
-
execSync5("npx @holdpoint/cli
|
|
1106
|
+
execSync5("npx @holdpoint/cli update", { stdio: "pipe" });
|
|
964
1107
|
} catch {
|
|
965
1108
|
applySpinner.warn(
|
|
966
|
-
|
|
1109
|
+
chalk5.yellow("checks.yaml updated, but `holdpoint update` failed \u2014 run it manually.")
|
|
967
1110
|
);
|
|
968
1111
|
printAppliedSummary(proposals.length, staleChecks.length);
|
|
969
1112
|
return;
|
|
970
1113
|
}
|
|
971
|
-
applySpinner.succeed(
|
|
1114
|
+
applySpinner.succeed(chalk5.green("checks.yaml updated and engine files regenerated."));
|
|
972
1115
|
printAppliedSummary(proposals.length, staleChecks.length);
|
|
973
1116
|
}
|
|
974
1117
|
function printAppliedSummary(added, wrapped) {
|
|
975
1118
|
const parts = [];
|
|
976
|
-
if (added > 0) parts.push(
|
|
1119
|
+
if (added > 0) parts.push(chalk5.green(`${added} check${added === 1 ? "" : "s"} added`));
|
|
977
1120
|
if (wrapped > 0)
|
|
978
|
-
parts.push(
|
|
1121
|
+
parts.push(chalk5.yellow(`${wrapped} stale check${wrapped === 1 ? "" : "s"} wrapped`));
|
|
979
1122
|
if (parts.length > 0) console.log(" " + parts.join(" \xB7 "));
|
|
980
1123
|
console.log(
|
|
981
|
-
|
|
1124
|
+
chalk5.dim("\n Review checks.yaml, then commit: ") + chalk5.yellow("git add checks.yaml && git commit -m 'chore: update holdpoint checks'")
|
|
982
1125
|
);
|
|
983
1126
|
}
|
|
984
1127
|
|
|
1128
|
+
// src/commands/live.ts
|
|
1129
|
+
import chalk6 from "chalk";
|
|
1130
|
+
async function liveCommand(options = {}) {
|
|
1131
|
+
const { info, started } = await ensureDaemon();
|
|
1132
|
+
const baseUrl = new URL(`/__holdpoint/live-auth`, `http://127.0.0.1:${info.port}`);
|
|
1133
|
+
baseUrl.searchParams.set("token", info.token);
|
|
1134
|
+
baseUrl.searchParams.set("path", "/live/");
|
|
1135
|
+
const currentProject = options.project ? null : tryResolveCurrentProject();
|
|
1136
|
+
if (options.project) {
|
|
1137
|
+
baseUrl.searchParams.set("project", options.project);
|
|
1138
|
+
} else if (currentProject) {
|
|
1139
|
+
appendProjectAuthParams(baseUrl, currentProject);
|
|
1140
|
+
}
|
|
1141
|
+
openBrowser(baseUrl.toString());
|
|
1142
|
+
console.log(
|
|
1143
|
+
chalk6.green(
|
|
1144
|
+
started ? "\u2713 Started Holdpoint Live and opened the browser" : "\u2713 Opened Holdpoint Live"
|
|
1145
|
+
)
|
|
1146
|
+
);
|
|
1147
|
+
console.log(` url: ${chalk6.cyan(baseUrl.toString())}`);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// src/commands/daemon.ts
|
|
1151
|
+
import chalk7 from "chalk";
|
|
1152
|
+
import {
|
|
1153
|
+
DaemonAlreadyRunningError,
|
|
1154
|
+
isProcessAlive,
|
|
1155
|
+
readDaemonLock,
|
|
1156
|
+
readHealthyDaemonLock as readHealthyDaemonLock2,
|
|
1157
|
+
removeDaemonLock,
|
|
1158
|
+
startDaemonProcess
|
|
1159
|
+
} from "@holdpoint/live-daemon";
|
|
1160
|
+
|
|
1161
|
+
// src/version.ts
|
|
1162
|
+
var CLI_VERSION = "0.1.0-alpha.15";
|
|
1163
|
+
|
|
1164
|
+
// src/commands/daemon.ts
|
|
1165
|
+
function formatUptime(lock) {
|
|
1166
|
+
const seconds = Math.max(0, Math.floor((Date.now() - lock.started_at) / 1e3));
|
|
1167
|
+
const minutes = Math.floor(seconds / 60);
|
|
1168
|
+
const remainingSeconds = seconds % 60;
|
|
1169
|
+
return minutes > 0 ? `${minutes}m ${remainingSeconds}s` : `${remainingSeconds}s`;
|
|
1170
|
+
}
|
|
1171
|
+
async function fetchSessionCount(lock) {
|
|
1172
|
+
try {
|
|
1173
|
+
const response = await fetch(`http://127.0.0.1:${lock.port}/v1/sessions`, {
|
|
1174
|
+
headers: {
|
|
1175
|
+
authorization: `Bearer ${lock.token}`
|
|
1176
|
+
}
|
|
1177
|
+
});
|
|
1178
|
+
if (!response.ok) return null;
|
|
1179
|
+
const parsed = await response.json();
|
|
1180
|
+
return Array.isArray(parsed.sessions) ? parsed.sessions.length : null;
|
|
1181
|
+
} catch {
|
|
1182
|
+
return null;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
function sleep2(ms) {
|
|
1186
|
+
return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
|
|
1187
|
+
}
|
|
1188
|
+
async function daemonStartCommand() {
|
|
1189
|
+
const { info, started } = await ensureDaemon();
|
|
1190
|
+
const sessionCount = await fetchSessionCount(info);
|
|
1191
|
+
const headline = started ? "Started Holdpoint Live daemon" : "Reused existing Holdpoint Live daemon";
|
|
1192
|
+
console.log(chalk7.green(`\u2713 ${headline}`));
|
|
1193
|
+
console.log(` pid: ${chalk7.cyan(String(info.pid))}`);
|
|
1194
|
+
console.log(` port: ${chalk7.cyan(String(info.port))}`);
|
|
1195
|
+
console.log(` uptime: ${chalk7.cyan(formatUptime(info))}`);
|
|
1196
|
+
if (sessionCount !== null) {
|
|
1197
|
+
console.log(` sessions: ${chalk7.cyan(String(sessionCount))}`);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
async function daemonStatusCommand() {
|
|
1201
|
+
const lock = await readHealthyDaemonLock2();
|
|
1202
|
+
if (!lock) {
|
|
1203
|
+
console.log(chalk7.yellow("Holdpoint Live daemon is not running."));
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
const sessionCount = await fetchSessionCount(lock);
|
|
1207
|
+
console.log(chalk7.green("\u2713 Holdpoint Live daemon is running"));
|
|
1208
|
+
console.log(` pid: ${chalk7.cyan(String(lock.pid))}`);
|
|
1209
|
+
console.log(` port: ${chalk7.cyan(String(lock.port))}`);
|
|
1210
|
+
console.log(` uptime: ${chalk7.cyan(formatUptime(lock))}`);
|
|
1211
|
+
if (sessionCount !== null) {
|
|
1212
|
+
console.log(` sessions: ${chalk7.cyan(String(sessionCount))}`);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
async function daemonStopCommand() {
|
|
1216
|
+
const lock = readDaemonLock();
|
|
1217
|
+
if (!lock) {
|
|
1218
|
+
console.log(chalk7.yellow("Holdpoint Live daemon is not running."));
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
if (!isProcessAlive(lock.pid)) {
|
|
1222
|
+
removeDaemonLock(void 0, lock.token);
|
|
1223
|
+
console.log(chalk7.yellow("Removed stale Holdpoint Live lockfile."));
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
process.kill(lock.pid, "SIGTERM");
|
|
1227
|
+
const deadline = Date.now() + 5e3;
|
|
1228
|
+
while (Date.now() < deadline) {
|
|
1229
|
+
if (!isProcessAlive(lock.pid)) {
|
|
1230
|
+
removeDaemonLock(void 0, lock.token);
|
|
1231
|
+
console.log(chalk7.green(`\u2713 Stopped Holdpoint Live daemon (${lock.pid})`));
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
await sleep2(100);
|
|
1235
|
+
}
|
|
1236
|
+
process.kill(lock.pid, "SIGKILL");
|
|
1237
|
+
await sleep2(100);
|
|
1238
|
+
removeDaemonLock(void 0, lock.token);
|
|
1239
|
+
console.log(chalk7.green(`\u2713 Force-stopped Holdpoint Live daemon (${lock.pid})`));
|
|
1240
|
+
}
|
|
1241
|
+
async function daemonServeCommand(options) {
|
|
1242
|
+
try {
|
|
1243
|
+
const daemon2 = await startDaemonProcess({
|
|
1244
|
+
version: CLI_VERSION,
|
|
1245
|
+
...options.port ? { port: Number(options.port) } : {}
|
|
1246
|
+
});
|
|
1247
|
+
await daemon2.closed;
|
|
1248
|
+
} catch (error) {
|
|
1249
|
+
if (error instanceof DaemonAlreadyRunningError) {
|
|
1250
|
+
process.exit(0);
|
|
1251
|
+
}
|
|
1252
|
+
throw error;
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
// src/commands/engines.ts
|
|
1257
|
+
import chalk8 from "chalk";
|
|
1258
|
+
|
|
1259
|
+
// src/engines.ts
|
|
1260
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6 } from "fs";
|
|
1261
|
+
import { dirname as dirname2, join as join5, resolve, sep } from "path";
|
|
1262
|
+
import { createRequire } from "module";
|
|
1263
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
1264
|
+
import { parseEventV1 } from "@holdpoint/live-protocol";
|
|
1265
|
+
var require2 = createRequire(import.meta.url);
|
|
1266
|
+
var CLI_SRC_DIR = dirname2(fileURLToPath(import.meta.url));
|
|
1267
|
+
var MONOREPO_ROOT = resolve(CLI_SRC_DIR, "../../..");
|
|
1268
|
+
var BUILTIN_LIVE_ENGINE_PACKAGES = [
|
|
1269
|
+
"@holdpoint/engine-claude",
|
|
1270
|
+
"@holdpoint/engine-codex",
|
|
1271
|
+
"@holdpoint/engine-cursor"
|
|
1272
|
+
];
|
|
1273
|
+
var HOLDPOINT_ENGINE_KEYWORD = "holdpoint-engine";
|
|
1274
|
+
function isObject(value) {
|
|
1275
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
1276
|
+
}
|
|
1277
|
+
function isExternalLiveEnginePackageName(packageName) {
|
|
1278
|
+
return /^holdpoint-engine-[a-z0-9-]+$/.test(packageName) || /^@[a-z0-9_.-]+\/holdpoint-engine-[a-z0-9-]+$/.test(packageName);
|
|
1279
|
+
}
|
|
1280
|
+
function readJsonFile(path) {
|
|
1281
|
+
if (!existsSync8(path)) {
|
|
1282
|
+
return null;
|
|
1283
|
+
}
|
|
1284
|
+
try {
|
|
1285
|
+
const parsed = JSON.parse(readFileSync6(path, "utf8"));
|
|
1286
|
+
return isObject(parsed) ? parsed : null;
|
|
1287
|
+
} catch {
|
|
1288
|
+
return null;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
function findNearestPackageRoot(startDir) {
|
|
1292
|
+
let current = resolve(startDir);
|
|
1293
|
+
while (true) {
|
|
1294
|
+
if (existsSync8(join5(current, "package.json"))) {
|
|
1295
|
+
return current;
|
|
1296
|
+
}
|
|
1297
|
+
const parent = dirname2(current);
|
|
1298
|
+
if (parent === current) {
|
|
1299
|
+
return resolve(startDir);
|
|
1300
|
+
}
|
|
1301
|
+
current = parent;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
function findPackageRootFromFile(path) {
|
|
1305
|
+
let current = dirname2(path);
|
|
1306
|
+
while (true) {
|
|
1307
|
+
if (existsSync8(join5(current, "package.json"))) {
|
|
1308
|
+
return current;
|
|
1309
|
+
}
|
|
1310
|
+
const parent = dirname2(current);
|
|
1311
|
+
if (parent === current) {
|
|
1312
|
+
return null;
|
|
1313
|
+
}
|
|
1314
|
+
current = parent;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
function getDependencyEnginePackageNames(projectRoot) {
|
|
1318
|
+
const packageJson = readJsonFile(join5(projectRoot, "package.json"));
|
|
1319
|
+
if (!packageJson) {
|
|
1320
|
+
return [];
|
|
1321
|
+
}
|
|
1322
|
+
const packageNames = /* @__PURE__ */ new Set();
|
|
1323
|
+
for (const field of ["dependencies", "devDependencies", "optionalDependencies"]) {
|
|
1324
|
+
const deps = packageJson[field];
|
|
1325
|
+
if (!isObject(deps)) {
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1328
|
+
for (const packageName of Object.keys(deps)) {
|
|
1329
|
+
if (isExternalLiveEnginePackageName(packageName)) {
|
|
1330
|
+
packageNames.add(packageName);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
return [...packageNames];
|
|
1335
|
+
}
|
|
1336
|
+
function resolvePackageRoot(packageName, projectRoot) {
|
|
1337
|
+
try {
|
|
1338
|
+
const entryPath = require2.resolve(packageName);
|
|
1339
|
+
return findPackageRootFromFile(entryPath);
|
|
1340
|
+
} catch {
|
|
1341
|
+
}
|
|
1342
|
+
try {
|
|
1343
|
+
const entryPath = require2.resolve(packageName, {
|
|
1344
|
+
paths: [projectRoot, process.cwd()]
|
|
1345
|
+
});
|
|
1346
|
+
return findPackageRootFromFile(entryPath);
|
|
1347
|
+
} catch {
|
|
1348
|
+
}
|
|
1349
|
+
try {
|
|
1350
|
+
const packageJsonPath = require2.resolve(`${packageName}/package.json`, {
|
|
1351
|
+
paths: [projectRoot, process.cwd()]
|
|
1352
|
+
});
|
|
1353
|
+
return dirname2(packageJsonPath);
|
|
1354
|
+
} catch {
|
|
1355
|
+
if (packageName.startsWith("@holdpoint/")) {
|
|
1356
|
+
const scopedName = packageName.split("/")[1];
|
|
1357
|
+
if (scopedName) {
|
|
1358
|
+
const packageDir = resolve(MONOREPO_ROOT, "packages", scopedName);
|
|
1359
|
+
if (existsSync8(join5(packageDir, "package.json"))) {
|
|
1360
|
+
return packageDir;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
return null;
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
function formatImportError(error) {
|
|
1368
|
+
return error instanceof Error && error.message ? error.message : String(error);
|
|
1369
|
+
}
|
|
1370
|
+
function parseManifest(value) {
|
|
1371
|
+
if (!isObject(value)) {
|
|
1372
|
+
return null;
|
|
1373
|
+
}
|
|
1374
|
+
if (value.manifestVersion !== 1) {
|
|
1375
|
+
return null;
|
|
1376
|
+
}
|
|
1377
|
+
if (typeof value.id !== "string" || !/^[a-z0-9-]+$/.test(value.id)) {
|
|
1378
|
+
return null;
|
|
1379
|
+
}
|
|
1380
|
+
if (typeof value.displayName !== "string" || !value.displayName.trim()) {
|
|
1381
|
+
return null;
|
|
1382
|
+
}
|
|
1383
|
+
return {
|
|
1384
|
+
manifestVersion: 1,
|
|
1385
|
+
id: value.id,
|
|
1386
|
+
displayName: value.displayName
|
|
1387
|
+
};
|
|
1388
|
+
}
|
|
1389
|
+
function parseLiveCapabilities(value) {
|
|
1390
|
+
if (!isObject(value)) {
|
|
1391
|
+
return null;
|
|
1392
|
+
}
|
|
1393
|
+
const capabilities = {};
|
|
1394
|
+
for (const key of [
|
|
1395
|
+
"can_stream",
|
|
1396
|
+
"can_control",
|
|
1397
|
+
"can_modify_context",
|
|
1398
|
+
"can_register_tools",
|
|
1399
|
+
"control_online"
|
|
1400
|
+
]) {
|
|
1401
|
+
const entry = value[key];
|
|
1402
|
+
if (entry === void 0) {
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1405
|
+
if (typeof entry !== "boolean") {
|
|
1406
|
+
return null;
|
|
1407
|
+
}
|
|
1408
|
+
capabilities[key] = entry;
|
|
1409
|
+
}
|
|
1410
|
+
return capabilities;
|
|
1411
|
+
}
|
|
1412
|
+
function parseLiveAdapter(value, manifest) {
|
|
1413
|
+
if (!isObject(value)) {
|
|
1414
|
+
return null;
|
|
1415
|
+
}
|
|
1416
|
+
if (typeof value.id !== "string" || typeof value.displayName !== "string") {
|
|
1417
|
+
return null;
|
|
1418
|
+
}
|
|
1419
|
+
if (value.id !== manifest.id || value.displayName !== manifest.displayName) {
|
|
1420
|
+
return null;
|
|
1421
|
+
}
|
|
1422
|
+
const capabilities = parseLiveCapabilities(value.capabilities);
|
|
1423
|
+
if (!capabilities) {
|
|
1424
|
+
return null;
|
|
1425
|
+
}
|
|
1426
|
+
const generateBridgeCommand = value.generateBridgeCommand;
|
|
1427
|
+
if (typeof generateBridgeCommand !== "function") {
|
|
1428
|
+
return null;
|
|
1429
|
+
}
|
|
1430
|
+
const translateHookInput = value.translateHookInput;
|
|
1431
|
+
if (typeof translateHookInput !== "function") {
|
|
1432
|
+
return null;
|
|
1433
|
+
}
|
|
1434
|
+
return {
|
|
1435
|
+
id: value.id,
|
|
1436
|
+
displayName: value.displayName,
|
|
1437
|
+
capabilities,
|
|
1438
|
+
generateBridgeCommand: () => {
|
|
1439
|
+
const command = generateBridgeCommand();
|
|
1440
|
+
if (typeof command !== "string") {
|
|
1441
|
+
throw new Error("adapter.generateBridgeCommand() must return a string");
|
|
1442
|
+
}
|
|
1443
|
+
return command;
|
|
1444
|
+
},
|
|
1445
|
+
translateHookInput: (raw, options) => {
|
|
1446
|
+
const event = translateHookInput(raw, options);
|
|
1447
|
+
return event == null ? null : parseEventV1(event);
|
|
1448
|
+
}
|
|
1449
|
+
};
|
|
1450
|
+
}
|
|
1451
|
+
async function importModule(modulePath) {
|
|
1452
|
+
const moduleUrl = pathToFileURL(modulePath).href;
|
|
1453
|
+
return await import(moduleUrl);
|
|
1454
|
+
}
|
|
1455
|
+
function resolvePackageAssetPath(packageRoot, relativePath) {
|
|
1456
|
+
const declaredPath = resolve(packageRoot, relativePath);
|
|
1457
|
+
const sourceFallback = resolve(
|
|
1458
|
+
packageRoot,
|
|
1459
|
+
relativePath.replace(/^\.\/dist\//, "./src/").replace(/\.js$/, ".ts")
|
|
1460
|
+
);
|
|
1461
|
+
if (packageRoot.startsWith(resolve(MONOREPO_ROOT, "packages") + sep) && existsSync8(sourceFallback)) {
|
|
1462
|
+
return sourceFallback;
|
|
1463
|
+
}
|
|
1464
|
+
if (existsSync8(declaredPath)) {
|
|
1465
|
+
return declaredPath;
|
|
1466
|
+
}
|
|
1467
|
+
return sourceFallback;
|
|
1468
|
+
}
|
|
1469
|
+
async function resolveCandidate(packageName, source, projectRoot) {
|
|
1470
|
+
const packageRoot = resolvePackageRoot(packageName, projectRoot);
|
|
1471
|
+
if (!packageRoot) {
|
|
1472
|
+
return {
|
|
1473
|
+
packageName,
|
|
1474
|
+
source,
|
|
1475
|
+
status: "ignored",
|
|
1476
|
+
reason: "package could not be resolved from this project"
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
const packageJson = readJsonFile(join5(packageRoot, "package.json"));
|
|
1480
|
+
if (!packageJson) {
|
|
1481
|
+
return {
|
|
1482
|
+
packageName,
|
|
1483
|
+
source,
|
|
1484
|
+
status: "ignored",
|
|
1485
|
+
reason: "package.json could not be read"
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
const keywords = Array.isArray(packageJson.keywords) ? packageJson.keywords : [];
|
|
1489
|
+
if (!keywords.includes(HOLDPOINT_ENGINE_KEYWORD)) {
|
|
1490
|
+
return {
|
|
1491
|
+
packageName,
|
|
1492
|
+
source,
|
|
1493
|
+
status: "ignored",
|
|
1494
|
+
reason: `missing \`${HOLDPOINT_ENGINE_KEYWORD}\` keyword`
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
const metadata = isObject(packageJson.holdpoint) ? packageJson.holdpoint : void 0;
|
|
1498
|
+
if (!metadata?.manifest) {
|
|
1499
|
+
return {
|
|
1500
|
+
packageName,
|
|
1501
|
+
source,
|
|
1502
|
+
status: "ignored",
|
|
1503
|
+
reason: "missing `holdpoint.manifest` package.json field"
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
if (!metadata.adapter) {
|
|
1507
|
+
return {
|
|
1508
|
+
packageName,
|
|
1509
|
+
source,
|
|
1510
|
+
status: "ignored",
|
|
1511
|
+
reason: "missing `holdpoint.adapter` package.json field"
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
const manifestPath = resolvePackageAssetPath(packageRoot, metadata.manifest);
|
|
1515
|
+
const adapterPath = resolvePackageAssetPath(packageRoot, metadata.adapter);
|
|
1516
|
+
if (!existsSync8(manifestPath)) {
|
|
1517
|
+
return {
|
|
1518
|
+
packageName,
|
|
1519
|
+
source,
|
|
1520
|
+
status: "ignored",
|
|
1521
|
+
reason: "manifest file does not exist"
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
if (!existsSync8(adapterPath)) {
|
|
1525
|
+
return {
|
|
1526
|
+
packageName,
|
|
1527
|
+
source,
|
|
1528
|
+
status: "ignored",
|
|
1529
|
+
reason: "adapter file does not exist"
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
try {
|
|
1533
|
+
const manifestModule = await importModule(manifestPath);
|
|
1534
|
+
const manifest = parseManifest(manifestModule.manifest);
|
|
1535
|
+
if (!manifest) {
|
|
1536
|
+
return {
|
|
1537
|
+
packageName,
|
|
1538
|
+
source,
|
|
1539
|
+
status: "ignored",
|
|
1540
|
+
reason: "manifest export is invalid"
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
return {
|
|
1544
|
+
packageName,
|
|
1545
|
+
source,
|
|
1546
|
+
status: "loaded",
|
|
1547
|
+
manifest,
|
|
1548
|
+
packageRoot,
|
|
1549
|
+
adapterPath
|
|
1550
|
+
};
|
|
1551
|
+
} catch (error) {
|
|
1552
|
+
return {
|
|
1553
|
+
packageName,
|
|
1554
|
+
source,
|
|
1555
|
+
status: "ignored",
|
|
1556
|
+
reason: `manifest import failed: ${formatImportError(error)}`
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
async function discoverLiveEnginesDetailed(options) {
|
|
1561
|
+
const projectRoot = findNearestPackageRoot(options?.cwd ?? process.cwd());
|
|
1562
|
+
const dependencyPackages = getDependencyEnginePackageNames(projectRoot);
|
|
1563
|
+
const seenPackages = /* @__PURE__ */ new Set();
|
|
1564
|
+
const results = [];
|
|
1565
|
+
const loadedIds = /* @__PURE__ */ new Set();
|
|
1566
|
+
const candidates = [
|
|
1567
|
+
...BUILTIN_LIVE_ENGINE_PACKAGES.map((packageName) => ({
|
|
1568
|
+
packageName,
|
|
1569
|
+
source: "built-in"
|
|
1570
|
+
})),
|
|
1571
|
+
...dependencyPackages.map((packageName) => ({ packageName, source: "dependency" }))
|
|
1572
|
+
];
|
|
1573
|
+
for (const candidate of candidates) {
|
|
1574
|
+
if (seenPackages.has(candidate.packageName)) {
|
|
1575
|
+
continue;
|
|
1576
|
+
}
|
|
1577
|
+
seenPackages.add(candidate.packageName);
|
|
1578
|
+
const result = await resolveCandidate(candidate.packageName, candidate.source, projectRoot);
|
|
1579
|
+
if (result.status === "loaded" && result.manifest) {
|
|
1580
|
+
if (loadedIds.has(result.manifest.id)) {
|
|
1581
|
+
results.push({
|
|
1582
|
+
packageName: result.packageName,
|
|
1583
|
+
source: result.source,
|
|
1584
|
+
status: "ignored",
|
|
1585
|
+
reason: `engine id \`${result.manifest.id}\` collides with an already loaded adapter`,
|
|
1586
|
+
manifest: result.manifest
|
|
1587
|
+
});
|
|
1588
|
+
continue;
|
|
1589
|
+
}
|
|
1590
|
+
loadedIds.add(result.manifest.id);
|
|
1591
|
+
}
|
|
1592
|
+
results.push(result);
|
|
1593
|
+
}
|
|
1594
|
+
return results;
|
|
1595
|
+
}
|
|
1596
|
+
async function discoverLiveEngines(options) {
|
|
1597
|
+
const results = await discoverLiveEnginesDetailed(options);
|
|
1598
|
+
return results.map(({ packageName, source, status, reason, manifest }) => ({
|
|
1599
|
+
packageName,
|
|
1600
|
+
source,
|
|
1601
|
+
status,
|
|
1602
|
+
...reason ? { reason } : {},
|
|
1603
|
+
...manifest ? { manifest } : {}
|
|
1604
|
+
}));
|
|
1605
|
+
}
|
|
1606
|
+
async function loadLiveAdapter(engineId, options) {
|
|
1607
|
+
const results = await discoverLiveEnginesDetailed(options);
|
|
1608
|
+
const match = results.find(
|
|
1609
|
+
(result) => result.status === "loaded" && result.manifest?.id === engineId
|
|
1610
|
+
);
|
|
1611
|
+
if (!match?.adapterPath || !match.manifest) {
|
|
1612
|
+
return null;
|
|
1613
|
+
}
|
|
1614
|
+
try {
|
|
1615
|
+
const adapterModule = await importModule(match.adapterPath);
|
|
1616
|
+
return parseLiveAdapter(adapterModule.adapter, match.manifest);
|
|
1617
|
+
} catch {
|
|
1618
|
+
return null;
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
// src/commands/engines.ts
|
|
1623
|
+
async function enginesCommand(options = {}) {
|
|
1624
|
+
const engines = await discoverLiveEngines();
|
|
1625
|
+
if (options.json) {
|
|
1626
|
+
console.log(JSON.stringify(engines, null, 2));
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
if (engines.length === 0) {
|
|
1630
|
+
console.log("No Holdpoint Live engines were discovered.");
|
|
1631
|
+
return;
|
|
1632
|
+
}
|
|
1633
|
+
for (const engine of engines) {
|
|
1634
|
+
if (engine.status === "loaded" && engine.manifest) {
|
|
1635
|
+
console.log(
|
|
1636
|
+
`${chalk8.green("loaded")} ${chalk8.cyan(engine.manifest.id)} (${engine.manifest.displayName}) from ${chalk8.yellow(engine.packageName)} [${engine.source}]`
|
|
1637
|
+
);
|
|
1638
|
+
continue;
|
|
1639
|
+
}
|
|
1640
|
+
console.log(
|
|
1641
|
+
`${chalk8.yellow("ignored")} ${chalk8.yellow(engine.packageName)} [${engine.source}] \u2014 ${engine.reason ?? "unknown reason"}`
|
|
1642
|
+
);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
// src/commands/event.ts
|
|
1647
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
1648
|
+
import { parseEventV1 as parseEventV12, parseEventsBatch } from "@holdpoint/live-protocol";
|
|
1649
|
+
import { BridgeClient as BridgeClient2 } from "@holdpoint/sdk";
|
|
1650
|
+
function readStdin() {
|
|
1651
|
+
return readFileSync7(0, "utf8");
|
|
1652
|
+
}
|
|
1653
|
+
async function eventCommand(options) {
|
|
1654
|
+
const stdin = readStdin().trim();
|
|
1655
|
+
if (!stdin) {
|
|
1656
|
+
if (options.fromHook) {
|
|
1657
|
+
process.exit(0);
|
|
1658
|
+
}
|
|
1659
|
+
console.error("No JSON input received on stdin.");
|
|
1660
|
+
process.exit(3);
|
|
1661
|
+
}
|
|
1662
|
+
let raw;
|
|
1663
|
+
try {
|
|
1664
|
+
raw = JSON.parse(stdin);
|
|
1665
|
+
} catch {
|
|
1666
|
+
if (options.fromHook) {
|
|
1667
|
+
process.exit(0);
|
|
1668
|
+
}
|
|
1669
|
+
console.error("Invalid JSON input.");
|
|
1670
|
+
process.exit(3);
|
|
1671
|
+
}
|
|
1672
|
+
const client = new BridgeClient2();
|
|
1673
|
+
try {
|
|
1674
|
+
if (options.fromHook) {
|
|
1675
|
+
if (!options.engine) {
|
|
1676
|
+
process.exit(0);
|
|
1677
|
+
}
|
|
1678
|
+
const adapter = await loadLiveAdapter(options.engine);
|
|
1679
|
+
if (!adapter) {
|
|
1680
|
+
process.exit(0);
|
|
1681
|
+
}
|
|
1682
|
+
const event = adapter.translateHookInput(raw, { cwd: process.cwd() });
|
|
1683
|
+
if (!event) {
|
|
1684
|
+
process.exit(0);
|
|
1685
|
+
}
|
|
1686
|
+
await client.sendEvent(event);
|
|
1687
|
+
process.exit(0);
|
|
1688
|
+
}
|
|
1689
|
+
if (Array.isArray(raw)) {
|
|
1690
|
+
await client.sendEvents(parseEventsBatch(raw));
|
|
1691
|
+
} else {
|
|
1692
|
+
await client.sendEvent(parseEventV12(raw));
|
|
1693
|
+
}
|
|
1694
|
+
} catch (error) {
|
|
1695
|
+
console.error(error.message);
|
|
1696
|
+
process.exit(3);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
// src/commands/changeset.ts
|
|
1701
|
+
import { execSync as execSync6 } from "child_process";
|
|
1702
|
+
import { existsSync as existsSync9, readdirSync as readdirSync3, readFileSync as readFileSync8, statSync } from "fs";
|
|
1703
|
+
import { join as join6, relative } from "path";
|
|
1704
|
+
import chalk9 from "chalk";
|
|
1705
|
+
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
1706
|
+
".git",
|
|
1707
|
+
".next",
|
|
1708
|
+
".turbo",
|
|
1709
|
+
"coverage",
|
|
1710
|
+
"dist",
|
|
1711
|
+
"node_modules",
|
|
1712
|
+
"test-results"
|
|
1713
|
+
]);
|
|
1714
|
+
function runGit(command) {
|
|
1715
|
+
try {
|
|
1716
|
+
const out = execSync6(command, {
|
|
1717
|
+
encoding: "utf8",
|
|
1718
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
1719
|
+
});
|
|
1720
|
+
return out.trim().split("\n").filter(Boolean);
|
|
1721
|
+
} catch {
|
|
1722
|
+
return [];
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
function readJson(path) {
|
|
1726
|
+
try {
|
|
1727
|
+
return JSON.parse(readFileSync8(path, "utf8"));
|
|
1728
|
+
} catch {
|
|
1729
|
+
return null;
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
function normalizePath(path) {
|
|
1733
|
+
return path.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
1734
|
+
}
|
|
1735
|
+
function getDefaultBranchRef() {
|
|
1736
|
+
const [symbolic] = runGit("git symbolic-ref --quiet --short refs/remotes/origin/HEAD");
|
|
1737
|
+
if (symbolic) return symbolic;
|
|
1738
|
+
const candidates = ["origin/main", "origin/master"];
|
|
1739
|
+
for (const candidate of candidates) {
|
|
1740
|
+
if (runGit(`git rev-parse --verify ${candidate}`).length > 0) {
|
|
1741
|
+
return candidate;
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
return null;
|
|
1745
|
+
}
|
|
1746
|
+
function getBranchChangedFiles() {
|
|
1747
|
+
const defaultBranch = getDefaultBranchRef();
|
|
1748
|
+
if (!defaultBranch) return [];
|
|
1749
|
+
const [base] = runGit(`git merge-base HEAD ${defaultBranch}`);
|
|
1750
|
+
if (!base) return [];
|
|
1751
|
+
return runGit(`git diff --name-only ${base}...HEAD`);
|
|
1752
|
+
}
|
|
1753
|
+
function uniqueFiles(files) {
|
|
1754
|
+
return [...new Set(files.map(normalizePath))];
|
|
1755
|
+
}
|
|
1756
|
+
function getChangedFiles(options) {
|
|
1757
|
+
const staged = runGit("git diff --cached --name-only");
|
|
1758
|
+
if (options.staged && staged.length > 0) return staged;
|
|
1759
|
+
const untracked = runGit("git ls-files --others --exclude-standard");
|
|
1760
|
+
if (!options.staged) {
|
|
1761
|
+
const unstaged = runGit("git diff --name-only HEAD");
|
|
1762
|
+
const workingTree = uniqueFiles([...unstaged, ...untracked]);
|
|
1763
|
+
if (workingTree.length > 0) return workingTree;
|
|
1764
|
+
}
|
|
1765
|
+
const branch = getBranchChangedFiles();
|
|
1766
|
+
if (branch.length > 0 || untracked.length > 0) return uniqueFiles([...branch, ...untracked]);
|
|
1767
|
+
return runGit("git diff --name-only HEAD~1 HEAD");
|
|
1768
|
+
}
|
|
1769
|
+
function parsePnpmWorkspacePatterns() {
|
|
1770
|
+
if (!existsSync9("pnpm-workspace.yaml")) return [];
|
|
1771
|
+
const lines = readFileSync8("pnpm-workspace.yaml", "utf8").split(/\r?\n/);
|
|
1772
|
+
return lines.map((line) => line.match(/^\s*-\s*['"]?([^'"]+)['"]?\s*$/)?.[1]).filter((line) => Boolean(line)).filter((line) => !line.startsWith("!"));
|
|
1773
|
+
}
|
|
1774
|
+
function expandOneLevelWorkspacePattern(pattern) {
|
|
1775
|
+
const normalized = normalizePath(pattern).replace(/\/package\.json$/, "");
|
|
1776
|
+
if (!normalized.includes("*")) {
|
|
1777
|
+
return existsSync9(join6(normalized, "package.json")) ? [normalized] : [];
|
|
1778
|
+
}
|
|
1779
|
+
const starIndex = normalized.indexOf("*");
|
|
1780
|
+
const parent = normalized.slice(0, starIndex).replace(/\/$/, "");
|
|
1781
|
+
const suffix = normalized.slice(starIndex + 1).replace(/^\//, "");
|
|
1782
|
+
if (!parent || suffix.includes("*") || !existsSync9(parent)) {
|
|
1783
|
+
return [];
|
|
1784
|
+
}
|
|
1785
|
+
return readdirSync3(parent).map((entry) => join6(parent, entry, suffix)).map(normalizePath).filter((candidate) => existsSync9(join6(candidate, "package.json")));
|
|
1786
|
+
}
|
|
1787
|
+
function walkPackageRoots(start, roots) {
|
|
1788
|
+
let entries;
|
|
1789
|
+
try {
|
|
1790
|
+
entries = readdirSync3(start);
|
|
1791
|
+
} catch {
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
if (start !== "." && existsSync9(join6(start, "package.json"))) {
|
|
1795
|
+
roots.push(normalizePath(start));
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
for (const entry of entries) {
|
|
1799
|
+
if (IGNORED_DIRS.has(entry)) continue;
|
|
1800
|
+
const candidate = join6(start, entry);
|
|
1801
|
+
let stats;
|
|
1802
|
+
try {
|
|
1803
|
+
stats = statSync(candidate);
|
|
1804
|
+
} catch {
|
|
1805
|
+
continue;
|
|
1806
|
+
}
|
|
1807
|
+
if (stats.isDirectory()) {
|
|
1808
|
+
walkPackageRoots(candidate, roots);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
function readPackageRoot(path) {
|
|
1813
|
+
const pkg = readJson(join6(path, "package.json"));
|
|
1814
|
+
if (!pkg) return null;
|
|
1815
|
+
return {
|
|
1816
|
+
path: normalizePath(path === "." ? "" : path),
|
|
1817
|
+
name: typeof pkg.name === "string" ? pkg.name : path || "root",
|
|
1818
|
+
private: pkg.private === true
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
function discoverPackageRoots(includePatterns = []) {
|
|
1822
|
+
const explicitRoots = includePatterns.flatMap(expandOneLevelWorkspacePattern);
|
|
1823
|
+
if (explicitRoots.length > 0) {
|
|
1824
|
+
return uniquePackageRoots(
|
|
1825
|
+
explicitRoots.map(readPackageRoot).filter((pkg) => Boolean(pkg))
|
|
1826
|
+
);
|
|
1827
|
+
}
|
|
1828
|
+
const rootPackage = readJson("package.json");
|
|
1829
|
+
const workspacePatterns = [
|
|
1830
|
+
...parsePnpmWorkspacePatterns(),
|
|
1831
|
+
...extractPackageJsonWorkspacePatterns(rootPackage)
|
|
1832
|
+
];
|
|
1833
|
+
const workspaceRoots = workspacePatterns.flatMap(expandOneLevelWorkspacePattern);
|
|
1834
|
+
if (workspaceRoots.length > 0) {
|
|
1835
|
+
return uniquePackageRoots(
|
|
1836
|
+
workspaceRoots.map(readPackageRoot).filter((pkg) => Boolean(pkg)).filter((pkg) => !pkg.private)
|
|
1837
|
+
);
|
|
1838
|
+
}
|
|
1839
|
+
const discovered = [];
|
|
1840
|
+
walkPackageRoots(".", discovered);
|
|
1841
|
+
const roots = discovered.length > 0 ? discovered : existsSync9("package.json") ? ["."] : [];
|
|
1842
|
+
return uniquePackageRoots(
|
|
1843
|
+
roots.map(readPackageRoot).filter((pkg) => Boolean(pkg)).filter((pkg) => !pkg.private)
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
function extractPackageJsonWorkspacePatterns(pkg) {
|
|
1847
|
+
const workspaces = pkg?.workspaces;
|
|
1848
|
+
if (Array.isArray(workspaces)) {
|
|
1849
|
+
return workspaces.filter((entry) => typeof entry === "string");
|
|
1850
|
+
}
|
|
1851
|
+
if (workspaces && typeof workspaces === "object" && "packages" in workspaces && Array.isArray(workspaces.packages)) {
|
|
1852
|
+
return workspaces.packages.filter(
|
|
1853
|
+
(entry) => typeof entry === "string"
|
|
1854
|
+
);
|
|
1855
|
+
}
|
|
1856
|
+
return [];
|
|
1857
|
+
}
|
|
1858
|
+
function uniquePackageRoots(packages) {
|
|
1859
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
1860
|
+
for (const pkg of packages) {
|
|
1861
|
+
byPath.set(pkg.path, pkg);
|
|
1862
|
+
}
|
|
1863
|
+
return [...byPath.values()].sort((left, right) => right.path.length - left.path.length);
|
|
1864
|
+
}
|
|
1865
|
+
function isChangesetFile(file) {
|
|
1866
|
+
return /^\.changeset\/(?!README\.md$)[^/]+\.md$/.test(file);
|
|
1867
|
+
}
|
|
1868
|
+
function isReleaseAffectingPackageFile(relativePath) {
|
|
1869
|
+
if (/(^|\/)(__tests__|test|tests|spec|e2e)\//.test(relativePath) || /\.(test|spec)\.[cm]?[jt]sx?$/.test(relativePath)) {
|
|
1870
|
+
return false;
|
|
1871
|
+
}
|
|
1872
|
+
if (relativePath === "README.md" || relativePath === "CHANGELOG.md" || relativePath.startsWith("docs/") || relativePath.startsWith("dist/") || relativePath.startsWith("coverage/")) {
|
|
1873
|
+
return false;
|
|
1874
|
+
}
|
|
1875
|
+
return /^(package\.json|src\/|lib\/|bin\/|templates\/|scripts\/|[^/]+\.config\.)/.test(
|
|
1876
|
+
relativePath
|
|
1877
|
+
);
|
|
1878
|
+
}
|
|
1879
|
+
function findPackageForFile(file, packageRoots) {
|
|
1880
|
+
const normalized = normalizePath(file);
|
|
1881
|
+
return packageRoots.find((pkg) => {
|
|
1882
|
+
if (pkg.path === "") return true;
|
|
1883
|
+
return normalized === pkg.path || normalized.startsWith(`${pkg.path}/`);
|
|
1884
|
+
}) ?? null;
|
|
1885
|
+
}
|
|
1886
|
+
function analyzeChangesetRequirement(input) {
|
|
1887
|
+
const changedFiles = input.changedFiles.map(normalizePath);
|
|
1888
|
+
const hasChangeset = changedFiles.some(isChangesetFile);
|
|
1889
|
+
const requiredFiles = changedFiles.flatMap((file) => {
|
|
1890
|
+
if (file.startsWith(".changeset/")) return [];
|
|
1891
|
+
const pkg = findPackageForFile(file, input.packageRoots);
|
|
1892
|
+
if (!pkg) return [];
|
|
1893
|
+
const relativePath = pkg.path === "" ? file : normalizePath(relative(pkg.path, file));
|
|
1894
|
+
if (!isReleaseAffectingPackageFile(relativePath)) return [];
|
|
1895
|
+
return [{ file, packageName: pkg.name }];
|
|
1896
|
+
});
|
|
1897
|
+
return { requiredFiles, hasChangeset };
|
|
1898
|
+
}
|
|
1899
|
+
async function requireChangesetCommand(options) {
|
|
1900
|
+
const changedFiles = getChangedFiles(options);
|
|
1901
|
+
if (changedFiles.length === 0) {
|
|
1902
|
+
console.log(chalk9.green("\u2713 No changed files detected \u2014 no changeset required."));
|
|
1903
|
+
return;
|
|
1904
|
+
}
|
|
1905
|
+
const packageRoots = discoverPackageRoots(options.include ?? []);
|
|
1906
|
+
if (packageRoots.length === 0) {
|
|
1907
|
+
console.log(chalk9.green("\u2713 No package roots detected \u2014 no changeset required."));
|
|
1908
|
+
return;
|
|
1909
|
+
}
|
|
1910
|
+
const hasChangesetSetup = existsSync9(".changeset");
|
|
1911
|
+
const { requiredFiles, hasChangeset } = analyzeChangesetRequirement({
|
|
1912
|
+
changedFiles,
|
|
1913
|
+
packageRoots
|
|
1914
|
+
});
|
|
1915
|
+
if (requiredFiles.length === 0) {
|
|
1916
|
+
console.log(chalk9.green("\u2713 No release-affecting package files changed."));
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
if (hasChangeset) {
|
|
1920
|
+
console.log(chalk9.green("\u2713 Package changes include a changeset."));
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1923
|
+
console.error(chalk9.red("\u2717 Package changes need a changeset."));
|
|
1924
|
+
console.error("");
|
|
1925
|
+
console.error(chalk9.bold("Changed package files:"));
|
|
1926
|
+
for (const item of requiredFiles.slice(0, 12)) {
|
|
1927
|
+
console.error(` - ${item.file} (${item.packageName})`);
|
|
1928
|
+
}
|
|
1929
|
+
if (requiredFiles.length > 12) {
|
|
1930
|
+
console.error(` - \u2026and ${requiredFiles.length - 12} more`);
|
|
1931
|
+
}
|
|
1932
|
+
console.error("");
|
|
1933
|
+
if (!hasChangesetSetup) {
|
|
1934
|
+
console.error(
|
|
1935
|
+
"No .changeset directory was found. Create one and add a changeset before finishing:"
|
|
1936
|
+
);
|
|
1937
|
+
console.error(chalk9.yellow(" mkdir -p .changeset"));
|
|
1938
|
+
} else {
|
|
1939
|
+
console.error("Add a changeset before finishing:");
|
|
1940
|
+
}
|
|
1941
|
+
console.error(chalk9.yellow(" pnpm changeset"));
|
|
1942
|
+
console.error(chalk9.dim(" or add a .changeset/<name>.md file manually"));
|
|
1943
|
+
process.exit(1);
|
|
1944
|
+
}
|
|
1945
|
+
|
|
985
1946
|
// src/index.ts
|
|
986
1947
|
var program = new Command();
|
|
987
|
-
program.name("holdpoint").description("Universal eval-guard for AI coding agents (alpha)").version(
|
|
988
|
-
program.
|
|
989
|
-
program.
|
|
1948
|
+
program.name("holdpoint").description("Universal eval-guard for AI coding agents (alpha)").version(CLI_VERSION);
|
|
1949
|
+
program.action(() => {
|
|
1950
|
+
program.outputHelp();
|
|
1951
|
+
});
|
|
1952
|
+
program.command("init").description("Initialise Holdpoint in the current project").option(
|
|
1953
|
+
"--agent <agent>",
|
|
1954
|
+
"Agent to install for: copilot | claude | cursor | codex (default: all four)"
|
|
1955
|
+
).action(initCommand);
|
|
1956
|
+
program.command("check").description("Run task checks from checks.yaml").option("--staged", "Only check against git-staged files").option("--hook <event>", "Only run checks bound to this lifecycle hook (default: before_done)").action(checkCommand);
|
|
990
1957
|
program.command("validate").description("Validate checks.yaml schema and print any errors").action(validateCommand);
|
|
991
1958
|
program.command("update").description("Regenerate engine files from current checks.yaml (preserves checks.yaml)").action(updateCommand);
|
|
992
|
-
program.command("builder").description("Open the visual builder UI
|
|
993
|
-
program.command("
|
|
1959
|
+
program.command("builder").description("Open the visual builder UI in the Holdpoint daemon").action(buildCommand);
|
|
1960
|
+
program.command("live").description("Open the Holdpoint Live UI").option("--project <project>", "Open the UI focused on a specific project hash").action(liveCommand);
|
|
1961
|
+
var daemon = program.command("daemon").description("Manage the Holdpoint Live daemon");
|
|
1962
|
+
daemon.command("start").description("Start or connect to the singleton Holdpoint Live daemon").action(daemonStartCommand);
|
|
1963
|
+
daemon.command("status").description("Show Holdpoint Live daemon status").action(daemonStatusCommand);
|
|
1964
|
+
daemon.command("stop").description("Stop the running Holdpoint Live daemon").action(daemonStopCommand);
|
|
1965
|
+
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);
|
|
1966
|
+
program.command("engines").description("List discovered Holdpoint Live engine packages").option("--json", "Print machine-readable discovery output").action(enginesCommand);
|
|
1967
|
+
program.command("require-changeset").description("Ensure release-affecting package changes include a changeset").option("--staged", "Prefer git-staged files when deciding what changed").option(
|
|
1968
|
+
"--include <pattern...>",
|
|
1969
|
+
"Package directory glob(s) to enforce, e.g. packages/* apps/builder"
|
|
1970
|
+
).action(requireChangesetCommand);
|
|
1971
|
+
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);
|
|
1972
|
+
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);
|
|
1973
|
+
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) => {
|
|
1974
|
+
process.stderr.write(
|
|
1975
|
+
"warning: `holdpoint evolve` is deprecated; use `holdpoint suggest` instead.\n"
|
|
1976
|
+
);
|
|
1977
|
+
await evolveCommand(options);
|
|
1978
|
+
});
|
|
994
1979
|
program.parse();
|
|
995
1980
|
//# sourceMappingURL=index.js.map
|