@holdpoint/cli 0.1.0-alpha.2 → 0.1.0-alpha.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js 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-COPLLMYJ.js";
2
10
 
3
11
  // src/index.ts
4
12
  import { Command } from "commander";
5
13
 
6
- // src/commands/init.ts
7
- import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync, copyFileSync } from "fs";
8
- import { join, dirname } from "path";
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 { buildHookJson, buildCheckScript, buildConfigJson } from "@holdpoint/engine-copilot";
13
- import { buildEngineJson as buildClaudeEngineJson } from "@holdpoint/engine-claude";
14
- import { buildEngine as buildCursorEngine } from "@holdpoint/engine-cursor";
15
- import { parseHoldpointYaml } from "@holdpoint/yaml-core";
16
-
17
- // src/detect.ts
18
- import { existsSync } from "fs";
19
- function detectAgent() {
20
- if (existsSync(".github/extensions")) return "copilot";
21
- if (existsSync(".claude")) return "claude";
22
- if (existsSync(".cursorrules")) return "cursor";
23
- return "unknown";
24
- }
25
- function detectStack() {
26
- const hasNext = existsSync("next.config.ts") || existsSync("next.config.js") || existsSync("next.config.mjs");
27
- const hasTsConfig = existsSync("tsconfig.json");
28
- const hasPyproject = existsSync("pyproject.toml") || existsSync("requirements.txt") || existsSync("setup.py");
29
- const hasPrisma = existsSync("prisma/schema.prisma");
30
- const hasApi = existsSync("server") || existsSync("api") || existsSync("backend");
31
- const hasGoMod = existsSync("go.mod");
32
- if (hasNext && (hasPrisma || hasApi)) return "fullstack";
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
- // src/commands/init.ts
41
- var __dirname = dirname(fileURLToPath(import.meta.url));
42
- function getTemplatePath(stack) {
43
- const name = stack === "unknown" ? "_base" : stack;
44
- const candidates = [
45
- join(__dirname, "templates", `${name}.yaml`),
46
- // dist/templates/ (published package)
47
- join(__dirname, "../../../templates", `${name}.yaml`),
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 getMasterPromptPath() {
58
- const candidates = [
59
- join(__dirname, "templates/MASTER_PROMPT.md"),
60
- // dist/templates/ (published package)
61
- join(__dirname, "../../../templates/MASTER_PROMPT.md"),
62
- // monorepo dev fallback
63
- join(process.cwd(), "templates/MASTER_PROMPT.md")
64
- // cwd fallback
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-FNQ5GQBD.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
- for (const p of candidates) {
67
- if (existsSync2(p)) return p;
68
- }
69
- return "";
70
- }
71
- var MINIMAL_CHECKS_YAML = `version: 1
72
- context:
73
- guides: {}
74
- conditions: []
75
- checks:
76
- - id: lint
77
- label: "Lint codebase"
78
- cmd: "echo 'Add your lint command here'"
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
- - id: jsdoc
81
- label: "JSDoc on changed public functions"
82
- prompt: "Ensure all changed public functions and exports have JSDoc comments."
83
- `;
84
- async function initCommand(options) {
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
- const templatePath = getTemplatePath(stack);
92
- if (templatePath) {
93
- yamlContent = readFileSync(templatePath, "utf8");
94
- }
95
- writeFileSync("checks.yaml", yamlContent, "utf8");
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
- yamlContent = readFileSync("checks.yaml", "utf8");
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 config = parseHoldpointYaml(yamlContent);
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
- mkdirSync(generatedDir, { recursive: true });
102
- writeFileSync(`${generatedDir}/checks.immutable.json`, buildConfigJson(config), "utf8");
103
- if (agent === "copilot" || agent === "unknown") {
104
- const hooksDir = ".github/hooks";
105
- mkdirSync(hooksDir, { recursive: true });
106
- writeFileSync(join(hooksDir, "holdpoint.json"), buildHookJson(config), "utf8");
107
- writeFileSync(join(hooksDir, "holdpoint-check.mjs"), buildCheckScript(config), "utf8");
108
- }
109
- if (agent === "claude") {
110
- mkdirSync(".claude", { recursive: true });
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 (existsSync2(settingsPath)) {
392
+ if (existsSync3(settingsPath)) {
114
393
  try {
115
- existing = JSON.parse(readFileSync(settingsPath, "utf8"));
394
+ existing = JSON.parse(readFileSync3(settingsPath, "utf8"));
116
395
  } catch {
117
396
  }
118
397
  }
119
- const holdpointHooks = JSON.parse(buildClaudeEngineJson(config));
120
- writeFileSync(
398
+ const hooks = JSON.parse(buildClaudeEngineJson(config));
399
+ writeFileSync2(
121
400
  settingsPath,
122
- JSON.stringify({ ...existing, hooks: holdpointHooks.hooks }, null, 2),
123
- "utf8"
401
+ JSON.stringify(mergeClaudeSettings(existing, hooks), null, 2) + "\n"
124
402
  );
403
+ spliceBreadcrumb("CLAUDE.md");
125
404
  }
126
- if (agent === "cursor") {
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 (existsSync2(cursorPath)) {
130
- const existing = readFileSync(cursorPath, "utf8");
131
- if (!existing.includes("Holdpoint Rules")) {
132
- writeFileSync(cursorPath, existing + "\n" + cursorRules, "utf8");
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
- writeFileSync(cursorPath, cursorRules, "utf8");
443
+ writeFileSync2(cursorPath, cursorRules);
136
444
  }
445
+ spliceBreadcrumb(".cursor/rules/holdpoint.md");
137
446
  }
138
- if (!existsSync2("MASTER_PROMPT.md")) {
139
- const guidePath = getMasterPromptPath();
140
- if (guidePath) {
141
- copyFileSync(guidePath, "MASTER_PROMPT.md");
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
- writeFileSync(
144
- "MASTER_PROMPT.md",
145
- "# Holdpoint\n\nRun `npx @holdpoint/cli@alpha check` before marking any task complete.\nSee `checks.yaml` for the full list of checks.\n",
146
- "utf8"
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
- spinner.succeed(chalk.bold.green("Holdpoint initialised!"));
151
- console.log(`
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
- Visual builder: ${chalk.yellow("npx @holdpoint/cli@alpha builder")} (opens localhost:4321)
158
- Stack: ${chalk.cyan(stack)} Agent: ${chalk.cyan(agent)}
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/commands/check.ts
163
- import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
164
- import chalk2 from "chalk";
165
- import ora2 from "ora";
166
- import { parseHoldpointYaml as parseHoldpointYaml2, matchesWhen } from "@holdpoint/yaml-core";
167
- import { runDeterministicChecks } from "@holdpoint/yaml-core/runner";
168
- import { execSync as execSync3 } from "child_process";
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 existsSync3, readFileSync as readFileSync2, readdirSync } from "fs";
172
- import { join as join2 } from "path";
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(readFileSync2(path, "utf8"));
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 readFileSync2(path, "utf8");
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) => existsSync3(join2(cwd, 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(join2(cwd, "package.json"));
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(join2(cwd, "pyproject.toml"));
198
- const requirementsText = tryReadText(join2(cwd, "requirements.txt"));
199
- const pipfileText = tryReadText(join2(cwd, "Pipfile"));
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/templates.ts
251
- function pmScript(profile, script, fallback) {
252
- if (!profile.scripts[script]) return fallback;
253
- if (profile.packageManager === "npm") return `npm run ${script}`;
254
- return `${profile.packageManager} ${script}`;
255
- }
256
- function getTemplates(profile) {
257
- return [
258
- // ── Universal checks (always proposed for any project) ──────────────────
259
- {
260
- id: "git-commit",
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
- trigger: () => true
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: "No TODO/FIXME left in changed code",
280
- prompt: "Scan the files you changed for any TODO, FIXME, HACK, or XXX comments. Either resolve them before finishing or convert them to GitHub issues. Don't leave incomplete work silently behind.",
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/dead-checker.ts
418
- import { execSync as execSync2 } from "child_process";
419
- import { readdirSync as readdirSync2, existsSync as existsSync4 } from "fs";
420
- import { join as join3 } from "path";
421
- var NAMED_SCOPES = /* @__PURE__ */ new Set([
422
- "frontend",
423
- "backend",
424
- "socket",
425
- "visual",
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
- return stale;
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 getAllChangedFiles() {
538
- try {
539
- const out = execSync3("git diff --name-only HEAD", {
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 checkCommand(options) {
549
- if (!existsSync5("checks.yaml")) {
550
- console.error(chalk2.red("No checks.yaml found. Run `holdpoint init` first."));
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 yamlContent = readFileSync3("checks.yaml", "utf8");
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 = readFileSync6("checks.yaml", "utf8");
989
+ const yamlContent = readFileSync5("checks.yaml", "utf8");
847
990
  let config;
848
991
  try {
849
- config = parseHoldpointYaml5(yamlContent);
992
+ config = parseHoldpointYaml4(yamlContent);
850
993
  } catch (err) {
851
- spinner.fail(chalk6.red("Invalid checks.yaml:") + " " + err.message);
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(chalk6.bold("\n\u{1F4CB} Project profile:"));
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(chalk6.dim(" (empty project \u2014 only universal checks apply)"));
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(` ${chalk6.green("\u2713")} ${name.padEnd(18)} ${chalk6.dim(hint)}`);
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(chalk6.bold(`
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 ? chalk6.dim(` \u2192 will wrap with conditionId: file_exists: ${suggestedConditionPath}`) : chalk6.dim(" \u2192 no path inferred; review manually");
897
- console.log(` ${chalk6.yellow("\u25CC")} ${chalk6.bold(check.id)} ${chalk6.dim(reason)}${fix}`);
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(chalk6.green("\n\u2713 checks.yaml is fully in sync with the project profile."));
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(chalk6.bold(`
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 ? chalk6.cyan(` when: ${t.when}`) : "";
909
- const type = t.cmd ? chalk6.dim("cmd") : chalk6.dim("prompt");
910
- const preview = t.cmd ? chalk6.dim(` ${t.cmd.slice(0, 80)}${t.cmd.length > 80 ? "\u2026" : ""}`) : chalk6.dim(` ${(t.prompt ?? "").slice(0, 80)}${(t.prompt?.length ?? 0) > 80 ? "\u2026" : ""}`);
911
- console.log(` ${chalk6.green("+")} ${chalk6.bold(t.id.padEnd(24))} [${type}]${scope}`);
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
- chalk6.red(`
1060
+ chalk5.red(`
918
1061
  \u2717 checks.yaml is out of sync with the project profile.`) + `
919
- Run ${chalk6.bold("npx @holdpoint/cli@alpha evolve --apply")} to apply these changes.`
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 = ora4("Applying changes to checks.yaml\u2026").start();
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@alpha update", { stdio: "pipe" });
1106
+ execSync5("npx @holdpoint/cli update", { stdio: "pipe" });
964
1107
  } catch {
965
1108
  applySpinner.warn(
966
- chalk6.yellow("checks.yaml updated, but `holdpoint update` failed \u2014 run it manually.")
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(chalk6.green("checks.yaml updated and engine files regenerated."));
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(chalk6.green(`${added} check${added === 1 ? "" : "s"} added`));
1119
+ if (added > 0) parts.push(chalk5.green(`${added} check${added === 1 ? "" : "s"} added`));
977
1120
  if (wrapped > 0)
978
- parts.push(chalk6.yellow(`${wrapped} stale check${wrapped === 1 ? "" : "s"} wrapped`));
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
- chalk6.dim("\n Review checks.yaml, then commit: ") + chalk6.yellow("git add checks.yaml && git commit -m 'chore: evolve holdpoint checks'")
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("0.1.0-alpha.2");
988
- program.command("init").description("Initialise Holdpoint in the current project").option("--stack <stack>", "Stack type: typescript | python | nextjs | fullstack").option("--agent <agent>", "Agent type: copilot | claude | cursor").action(initCommand);
989
- program.command("check").description("Run task checks from checks.yaml").option("--staged", "Only check against git-staged files").action(checkCommand);
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 on localhost:4321").action(buildCommand);
993
- program.command("evolve").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);
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