@holdpoint/cli 0.1.0-alpha.14 → 0.1.0-alpha.16
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/builder-ui/assets/{index-CaEfVl3b.js → index-DezgTDRo.js} +4 -4
- package/dist/builder-ui/assets/{index-CaEfVl3b.js.map → index-DezgTDRo.js.map} +1 -1
- package/dist/builder-ui/index.html +1 -1
- package/dist/index.js +861 -113
- package/dist/index.js.map +1 -1
- package/dist/templates/HOLDPOINT_PREREQUISITES.md +9 -0
- package/dist/templates/MASTER_PROMPT.md +15 -15
- package/dist/templates/_base.yaml +1 -1
- package/dist/templates/fullstack.yaml +1 -1
- package/dist/templates/go.yaml +1 -1
- package/dist/templates/nextjs.yaml +1 -1
- package/dist/templates/python.yaml +1 -1
- package/dist/templates/typescript.yaml +1 -1
- package/package.json +11 -8
package/dist/index.js
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/init.ts
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
7
|
+
import { execSync } from "child_process";
|
|
8
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync } from "fs";
|
|
9
|
+
import { join as join2, dirname as dirname2 } from "path";
|
|
10
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
10
11
|
import chalk from "chalk";
|
|
11
12
|
import ora from "ora";
|
|
12
13
|
import { buildConfigJson, buildEngine } from "@holdpoint/engine-copilot";
|
|
@@ -57,34 +58,52 @@ function detectStack() {
|
|
|
57
58
|
return "unknown";
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
// src/
|
|
61
|
+
// src/templates.ts
|
|
62
|
+
import { copyFileSync, existsSync as existsSync2, writeFileSync } from "fs";
|
|
63
|
+
import { join, dirname } from "path";
|
|
64
|
+
import { fileURLToPath } from "url";
|
|
61
65
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
62
|
-
function
|
|
63
|
-
const name = stack === "unknown" ? "_base" : stack;
|
|
66
|
+
function getBundledTemplatePath(filename) {
|
|
64
67
|
const candidates = [
|
|
65
|
-
join(__dirname, "templates",
|
|
68
|
+
join(__dirname, "templates", filename),
|
|
66
69
|
// dist/templates/ (published package)
|
|
67
|
-
join(__dirname, "../../../templates",
|
|
70
|
+
join(__dirname, "../../../templates", filename),
|
|
68
71
|
// monorepo dev fallback
|
|
69
|
-
join(process.cwd(), "templates",
|
|
72
|
+
join(process.cwd(), "templates", filename)
|
|
70
73
|
// cwd fallback
|
|
71
74
|
];
|
|
72
|
-
for (const
|
|
73
|
-
if (existsSync2(
|
|
75
|
+
for (const candidate of candidates) {
|
|
76
|
+
if (existsSync2(candidate)) return candidate;
|
|
74
77
|
}
|
|
75
78
|
return "";
|
|
76
79
|
}
|
|
77
|
-
function
|
|
80
|
+
function ensureBundledFile(outputPath, templateFilename, fallbackContent) {
|
|
81
|
+
if (existsSync2(outputPath)) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
const templatePath = getBundledTemplatePath(templateFilename);
|
|
85
|
+
if (templatePath) {
|
|
86
|
+
copyFileSync(templatePath, outputPath);
|
|
87
|
+
} else {
|
|
88
|
+
writeFileSync(outputPath, fallbackContent, "utf8");
|
|
89
|
+
}
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/commands/init.ts
|
|
94
|
+
var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
95
|
+
function getTemplatePath(stack) {
|
|
96
|
+
const name = stack === "unknown" ? "_base" : stack;
|
|
78
97
|
const candidates = [
|
|
79
|
-
|
|
98
|
+
join2(__dirname2, "templates", `${name}.yaml`),
|
|
80
99
|
// dist/templates/ (published package)
|
|
81
|
-
|
|
100
|
+
join2(__dirname2, "../../../templates", `${name}.yaml`),
|
|
82
101
|
// monorepo dev fallback
|
|
83
|
-
|
|
102
|
+
join2(process.cwd(), "templates", `${name}.yaml`)
|
|
84
103
|
// cwd fallback
|
|
85
104
|
];
|
|
86
105
|
for (const p of candidates) {
|
|
87
|
-
if (
|
|
106
|
+
if (existsSync3(p)) return p;
|
|
88
107
|
}
|
|
89
108
|
return "";
|
|
90
109
|
}
|
|
@@ -101,47 +120,62 @@ checks:
|
|
|
101
120
|
label: "JSDoc on changed public functions"
|
|
102
121
|
prompt: "Ensure all changed public functions and exports have JSDoc comments."
|
|
103
122
|
`;
|
|
123
|
+
var MINIMAL_MASTER_PROMPT = `# Holdpoint
|
|
124
|
+
|
|
125
|
+
Run \`holdpoint check\` before marking any task complete.
|
|
126
|
+
See \`checks.yaml\` for the full list of checks.
|
|
127
|
+
`;
|
|
128
|
+
var MINIMAL_PREREQUISITES = `# Holdpoint prerequisites
|
|
129
|
+
|
|
130
|
+
Holdpoint installed repo-local adapters for one or more AI coding agents. Before relying on them locally, review these setup notes:
|
|
131
|
+
|
|
132
|
+
- **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.
|
|
133
|
+
- **OpenAI Codex** \u2014 project-level hooks require trust approval. Run \`codex trust\` in the Codex TUI or review the hook with \`/hooks\`.
|
|
134
|
+
- **General** \u2014 Holdpoint expects Node.js 18+ and a git repository so \`holdpoint init\`, \`holdpoint update\`, and \`holdpoint check\` can run normally.
|
|
135
|
+
|
|
136
|
+
Docs: https://holdpoint.dev/docs
|
|
137
|
+
`;
|
|
104
138
|
async function initCommand(options) {
|
|
105
139
|
const spinner = ora("Initialising Holdpoint\u2026").start();
|
|
106
140
|
const stack = options.stack ?? detectStack();
|
|
107
141
|
const agentOpt = options.agent;
|
|
108
142
|
const agents = !agentOpt || agentOpt === "all" ? ["copilot", "claude", "cursor", "codex"] : [agentOpt];
|
|
109
143
|
spinner.text = `Stack: ${chalk.cyan(stack)} \u2014 installing for: ${chalk.cyan(agents.join(", "))}`;
|
|
144
|
+
const pm = detectPackageManager();
|
|
110
145
|
let yamlContent = MINIMAL_CHECKS_YAML;
|
|
111
|
-
if (!
|
|
146
|
+
if (!existsSync3("checks.yaml")) {
|
|
112
147
|
const templatePath = getTemplatePath(stack);
|
|
113
148
|
if (templatePath) {
|
|
114
149
|
yamlContent = readFileSync2(templatePath, "utf8");
|
|
115
150
|
}
|
|
116
|
-
const pm = detectPackageManager();
|
|
117
151
|
if (pm !== "pnpm") {
|
|
118
152
|
yamlContent = yamlContent.replace(/\bpnpm\b/g, pm);
|
|
119
153
|
}
|
|
120
|
-
|
|
154
|
+
writeFileSync2("checks.yaml", yamlContent, "utf8");
|
|
121
155
|
} else {
|
|
122
156
|
yamlContent = readFileSync2("checks.yaml", "utf8");
|
|
123
157
|
}
|
|
124
158
|
const config = parseHoldpointYaml(yamlContent);
|
|
125
159
|
const generatedDir = ".github/holdpoint/generated";
|
|
126
160
|
mkdirSync(generatedDir, { recursive: true });
|
|
127
|
-
|
|
161
|
+
writeFileSync2(`${generatedDir}/checks.immutable.json`, buildConfigJson(config), "utf8");
|
|
128
162
|
if (agents.includes("copilot")) {
|
|
129
163
|
const extDir = ".github/extensions/holdpoint";
|
|
130
164
|
mkdirSync(extDir, { recursive: true });
|
|
131
|
-
|
|
165
|
+
writeFileSync2(join2(extDir, "extension.mjs"), buildEngine(config), "utf8");
|
|
132
166
|
}
|
|
133
167
|
if (agents.includes("claude")) {
|
|
134
168
|
mkdirSync(".claude", { recursive: true });
|
|
135
169
|
const settingsPath = ".claude/settings.json";
|
|
136
170
|
let existing = {};
|
|
137
|
-
if (
|
|
171
|
+
if (existsSync3(settingsPath)) {
|
|
138
172
|
try {
|
|
139
173
|
existing = JSON.parse(readFileSync2(settingsPath, "utf8"));
|
|
140
174
|
} catch {
|
|
141
175
|
}
|
|
142
176
|
}
|
|
143
177
|
const holdpointHooks = JSON.parse(buildClaudeEngineJson(config));
|
|
144
|
-
|
|
178
|
+
writeFileSync2(
|
|
145
179
|
settingsPath,
|
|
146
180
|
JSON.stringify({ ...existing, hooks: holdpointHooks.hooks }, null, 2),
|
|
147
181
|
"utf8"
|
|
@@ -150,63 +184,80 @@ async function initCommand(options) {
|
|
|
150
184
|
if (agents.includes("cursor")) {
|
|
151
185
|
const cursorRules = buildCursorEngine(config);
|
|
152
186
|
const cursorPath = ".cursorrules";
|
|
153
|
-
if (
|
|
187
|
+
if (existsSync3(cursorPath)) {
|
|
154
188
|
const existing = readFileSync2(cursorPath, "utf8");
|
|
155
189
|
if (!existing.includes("Holdpoint Rules")) {
|
|
156
|
-
|
|
190
|
+
writeFileSync2(cursorPath, existing + "\n" + cursorRules, "utf8");
|
|
157
191
|
}
|
|
158
192
|
} else {
|
|
159
|
-
|
|
193
|
+
writeFileSync2(cursorPath, cursorRules, "utf8");
|
|
160
194
|
}
|
|
161
195
|
}
|
|
162
196
|
if (agents.includes("codex")) {
|
|
163
197
|
mkdirSync(".codex", { recursive: true });
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
198
|
+
writeFileSync2(".codex/hooks.json", buildCodexHooksJson(config), "utf8");
|
|
199
|
+
writeFileSync2(".codex/holdpoint-check.mjs", buildCodexCheckScript(config), "utf8");
|
|
200
|
+
writeFileSync2(".codex/config.toml", buildCodexConfigToml(), "utf8");
|
|
167
201
|
const agentsMdPath = "AGENTS.md";
|
|
168
|
-
const existing =
|
|
169
|
-
|
|
202
|
+
const existing = existsSync3(agentsMdPath) ? readFileSync2(agentsMdPath, "utf8") : "";
|
|
203
|
+
writeFileSync2(agentsMdPath, spliceAgentsMd(existing, config), "utf8");
|
|
170
204
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
205
|
+
ensureBundledFile("MASTER_PROMPT.md", "MASTER_PROMPT.md", MINIMAL_MASTER_PROMPT);
|
|
206
|
+
ensureBundledFile(
|
|
207
|
+
"HOLDPOINT_PREREQUISITES.md",
|
|
208
|
+
"HOLDPOINT_PREREQUISITES.md",
|
|
209
|
+
MINIMAL_PREREQUISITES
|
|
210
|
+
);
|
|
211
|
+
spinner.text = "Installing holdpoint as a devDependency\u2026";
|
|
212
|
+
const installCmds = {
|
|
213
|
+
pnpm: "pnpm add -D holdpoint@alpha",
|
|
214
|
+
yarn: "yarn add --dev holdpoint@alpha",
|
|
215
|
+
npm: "npm install --save-dev holdpoint@alpha"
|
|
216
|
+
};
|
|
217
|
+
const installCmd = installCmds[pm];
|
|
218
|
+
try {
|
|
219
|
+
execSync(installCmd, { stdio: "pipe" });
|
|
220
|
+
spinner.succeed(chalk.bold.green("Holdpoint initialised!"));
|
|
221
|
+
} catch {
|
|
222
|
+
spinner.warn(
|
|
223
|
+
chalk.yellow(`Holdpoint initialised, but could not install the package automatically.`) + `
|
|
224
|
+
Run manually: ${chalk.yellow(installCmd)}`
|
|
225
|
+
);
|
|
182
226
|
}
|
|
183
|
-
spinner.succeed(chalk.bold.green("Holdpoint initialised!"));
|
|
184
227
|
console.log(`
|
|
185
228
|
${chalk.cyan("Next steps:")}
|
|
186
229
|
1. Edit ${chalk.yellow("checks.yaml")} to customise your eval checkpoints
|
|
187
|
-
2.
|
|
188
|
-
3.
|
|
230
|
+
2. Review ${chalk.yellow("HOLDPOINT_PREREQUISITES.md")} for agent setup notes
|
|
231
|
+
3. Commit ${chalk.yellow("checks.yaml")}, ${chalk.yellow("HOLDPOINT_PREREQUISITES.md")}, and the generated engine files
|
|
232
|
+
4. Run ${chalk.yellow("holdpoint check")} at any time to validate
|
|
189
233
|
|
|
190
|
-
|
|
234
|
+
${chalk.bgYellow.black(" Copilot local use ")} Run ${chalk.yellow("/experimental on")} in GitHub Copilot CLI so the
|
|
235
|
+
${chalk.yellow("EXTENSIONS")} feature is enabled before using Holdpoint locally.
|
|
236
|
+
See ${chalk.yellow("HOLDPOINT_PREREQUISITES.md")} for the full handoff notes.
|
|
237
|
+
|
|
238
|
+
Visual builder: ${chalk.yellow("holdpoint builder")} (opens localhost:4321)
|
|
191
239
|
Stack: ${chalk.cyan(stack)} Agents: ${chalk.cyan(agents.join(", "))}
|
|
192
240
|
`);
|
|
193
241
|
}
|
|
194
242
|
|
|
195
243
|
// src/commands/check.ts
|
|
196
|
-
import { existsSync as
|
|
197
|
-
import { join as
|
|
244
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
245
|
+
import { join as join3 } from "path";
|
|
198
246
|
import chalk2 from "chalk";
|
|
199
247
|
import ora2 from "ora";
|
|
200
248
|
import { parseHoldpointYaml as parseHoldpointYaml2, matchesWhen } from "@holdpoint/yaml-core";
|
|
201
249
|
import { runDeterministicChecks } from "@holdpoint/yaml-core/runner";
|
|
202
|
-
import { execSync } from "child_process";
|
|
250
|
+
import { execSync as execSync2 } from "child_process";
|
|
251
|
+
import { randomUUID } from "crypto";
|
|
252
|
+
import { identifyProject } from "@holdpoint/live-daemon";
|
|
253
|
+
import { BridgeClient } from "@holdpoint/sdk";
|
|
203
254
|
var COMMIT_CACHE_PATH = ".holdpoint/checked-commits.json";
|
|
204
255
|
var COMMIT_CACHE_MAX = 100;
|
|
205
256
|
var CHECK_REPORTS_PATH = ".holdpoint/check-reports.json";
|
|
206
257
|
var CHECK_REPORTS_MAX = 50;
|
|
207
258
|
function getStagedFiles() {
|
|
208
259
|
try {
|
|
209
|
-
const out =
|
|
260
|
+
const out = execSync2("git diff --cached --name-only", {
|
|
210
261
|
encoding: "utf8",
|
|
211
262
|
stdio: ["pipe", "pipe", "ignore"]
|
|
212
263
|
});
|
|
@@ -217,7 +268,7 @@ function getStagedFiles() {
|
|
|
217
268
|
}
|
|
218
269
|
function getAllChangedFiles() {
|
|
219
270
|
try {
|
|
220
|
-
const out =
|
|
271
|
+
const out = execSync2("git diff --name-only HEAD", {
|
|
221
272
|
encoding: "utf8",
|
|
222
273
|
stdio: ["pipe", "pipe", "ignore"]
|
|
223
274
|
});
|
|
@@ -228,7 +279,7 @@ function getAllChangedFiles() {
|
|
|
228
279
|
}
|
|
229
280
|
function getLastCommitFiles() {
|
|
230
281
|
try {
|
|
231
|
-
const out =
|
|
282
|
+
const out = execSync2("git diff --name-only HEAD~1 HEAD", {
|
|
232
283
|
encoding: "utf8",
|
|
233
284
|
stdio: ["pipe", "pipe", "ignore"]
|
|
234
285
|
});
|
|
@@ -239,7 +290,7 @@ function getLastCommitFiles() {
|
|
|
239
290
|
}
|
|
240
291
|
function getHeadSha() {
|
|
241
292
|
try {
|
|
242
|
-
return
|
|
293
|
+
return execSync2("git rev-parse HEAD", {
|
|
243
294
|
encoding: "utf8",
|
|
244
295
|
stdio: ["pipe", "pipe", "ignore"]
|
|
245
296
|
}).trim();
|
|
@@ -260,16 +311,16 @@ function recordCommitCache(sha) {
|
|
|
260
311
|
try {
|
|
261
312
|
const existing = readCommitCache();
|
|
262
313
|
const updated = [sha, ...[...existing].filter((s) => s !== sha)].slice(0, COMMIT_CACHE_MAX);
|
|
263
|
-
mkdirSync2(
|
|
264
|
-
|
|
314
|
+
mkdirSync2(join3(COMMIT_CACHE_PATH, ".."), { recursive: true });
|
|
315
|
+
writeFileSync3(COMMIT_CACHE_PATH, JSON.stringify({ verified: updated }, null, 2) + "\n", "utf8");
|
|
265
316
|
} catch {
|
|
266
317
|
}
|
|
267
318
|
}
|
|
268
319
|
function recordCheckReport(run) {
|
|
269
320
|
try {
|
|
270
|
-
mkdirSync2(
|
|
321
|
+
mkdirSync2(join3(CHECK_REPORTS_PATH, ".."), { recursive: true });
|
|
271
322
|
let existing = { runs: [] };
|
|
272
|
-
if (
|
|
323
|
+
if (existsSync4(CHECK_REPORTS_PATH)) {
|
|
273
324
|
try {
|
|
274
325
|
existing = JSON.parse(readFileSync3(CHECK_REPORTS_PATH, "utf8"));
|
|
275
326
|
if (!Array.isArray(existing.runs)) existing.runs = [];
|
|
@@ -280,12 +331,12 @@ function recordCheckReport(run) {
|
|
|
280
331
|
const updated = {
|
|
281
332
|
runs: [run, ...existing.runs].slice(0, CHECK_REPORTS_MAX)
|
|
282
333
|
};
|
|
283
|
-
|
|
334
|
+
writeFileSync3(CHECK_REPORTS_PATH, JSON.stringify(updated, null, 2) + "\n", "utf8");
|
|
284
335
|
} catch {
|
|
285
336
|
}
|
|
286
337
|
}
|
|
287
338
|
async function checkCommand(options) {
|
|
288
|
-
if (!
|
|
339
|
+
if (!existsSync4("checks.yaml")) {
|
|
289
340
|
console.error(chalk2.red("No checks.yaml found. Run `holdpoint init` first."));
|
|
290
341
|
process.exit(1);
|
|
291
342
|
}
|
|
@@ -404,6 +455,30 @@ ${chalk2.cyan("Agent prompts to act on:")}`);
|
|
|
404
455
|
}
|
|
405
456
|
};
|
|
406
457
|
recordCheckReport(run);
|
|
458
|
+
const project = identifyProject(process.cwd());
|
|
459
|
+
const bridge = new BridgeClient();
|
|
460
|
+
const liveEvents = reportResults.filter(
|
|
461
|
+
(result) => result.kind === "cmd"
|
|
462
|
+
).map((result, index) => ({
|
|
463
|
+
v: 1,
|
|
464
|
+
id: randomUUID(),
|
|
465
|
+
ts: Date.now() + index,
|
|
466
|
+
engine: "holdpoint",
|
|
467
|
+
session_id: "check-runner",
|
|
468
|
+
project_hash: project.hash,
|
|
469
|
+
cwd: process.cwd(),
|
|
470
|
+
type: "check_run",
|
|
471
|
+
payload: {
|
|
472
|
+
check_id: result.id,
|
|
473
|
+
label: result.label,
|
|
474
|
+
status: result.status,
|
|
475
|
+
duration_ms: 0,
|
|
476
|
+
...result.output ? { output: result.output } : {}
|
|
477
|
+
}
|
|
478
|
+
}));
|
|
479
|
+
if (liveEvents.length > 0) {
|
|
480
|
+
await bridge.sendEvents(liveEvents);
|
|
481
|
+
}
|
|
407
482
|
if (failed.length > 0) {
|
|
408
483
|
process.exit(1);
|
|
409
484
|
}
|
|
@@ -425,11 +500,11 @@ function printResult(result) {
|
|
|
425
500
|
}
|
|
426
501
|
|
|
427
502
|
// src/commands/validate.ts
|
|
428
|
-
import { existsSync as
|
|
503
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
429
504
|
import chalk3 from "chalk";
|
|
430
505
|
import { parseHoldpointYaml as parseHoldpointYaml3, validateConfig } from "@holdpoint/yaml-core";
|
|
431
506
|
async function validateCommand() {
|
|
432
|
-
if (!
|
|
507
|
+
if (!existsSync5("checks.yaml")) {
|
|
433
508
|
console.error(chalk3.red("No checks.yaml found. Run `holdpoint init` first."));
|
|
434
509
|
process.exit(1);
|
|
435
510
|
}
|
|
@@ -459,7 +534,7 @@ async function validateCommand() {
|
|
|
459
534
|
}
|
|
460
535
|
|
|
461
536
|
// src/commands/update.ts
|
|
462
|
-
import { existsSync as
|
|
537
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3 } from "fs";
|
|
463
538
|
import chalk4 from "chalk";
|
|
464
539
|
import ora3 from "ora";
|
|
465
540
|
import { parseHoldpointYaml as parseHoldpointYaml4 } from "@holdpoint/yaml-core";
|
|
@@ -472,8 +547,18 @@ import {
|
|
|
472
547
|
buildCheckScript as buildCodexCheckScript2,
|
|
473
548
|
spliceAgentsMd as spliceAgentsMd2
|
|
474
549
|
} from "@holdpoint/engine-codex";
|
|
550
|
+
var MINIMAL_PREREQUISITES2 = `# Holdpoint prerequisites
|
|
551
|
+
|
|
552
|
+
Holdpoint installed repo-local adapters for one or more AI coding agents. Before relying on them locally, review these setup notes:
|
|
553
|
+
|
|
554
|
+
- **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.
|
|
555
|
+
- **OpenAI Codex** \u2014 project-level hooks require trust approval. Run \`codex trust\` in the Codex TUI or review the hook with \`/hooks\`.
|
|
556
|
+
- **General** \u2014 Holdpoint expects Node.js 18+ and a git repository so \`holdpoint init\`, \`holdpoint update\`, and \`holdpoint check\` can run normally.
|
|
557
|
+
|
|
558
|
+
Docs: https://holdpoint.dev/docs
|
|
559
|
+
`;
|
|
475
560
|
async function updateCommand() {
|
|
476
|
-
if (!
|
|
561
|
+
if (!existsSync6("checks.yaml")) {
|
|
477
562
|
console.error(chalk4.red("No checks.yaml found. Run `holdpoint init` first."));
|
|
478
563
|
process.exit(1);
|
|
479
564
|
}
|
|
@@ -483,24 +568,24 @@ async function updateCommand() {
|
|
|
483
568
|
const agents = detected.length > 0 ? detected : ["copilot", "claude", "cursor", "codex"];
|
|
484
569
|
const generatedDir = ".github/holdpoint/generated";
|
|
485
570
|
mkdirSync3(generatedDir, { recursive: true });
|
|
486
|
-
|
|
571
|
+
writeFileSync4(`${generatedDir}/checks.immutable.json`, buildConfigJson2(config), "utf8");
|
|
487
572
|
if (agents.includes("copilot")) {
|
|
488
573
|
const extDir = ".github/extensions/holdpoint";
|
|
489
574
|
mkdirSync3(extDir, { recursive: true });
|
|
490
|
-
|
|
575
|
+
writeFileSync4(`${extDir}/extension.mjs`, buildEngine2(config), "utf8");
|
|
491
576
|
}
|
|
492
577
|
if (agents.includes("claude")) {
|
|
493
578
|
mkdirSync3(".claude", { recursive: true });
|
|
494
579
|
const settingsPath = ".claude/settings.json";
|
|
495
580
|
let existing = {};
|
|
496
|
-
if (
|
|
581
|
+
if (existsSync6(settingsPath)) {
|
|
497
582
|
try {
|
|
498
583
|
existing = JSON.parse(readFileSync5(settingsPath, "utf8"));
|
|
499
584
|
} catch {
|
|
500
585
|
}
|
|
501
586
|
}
|
|
502
587
|
const hooks = JSON.parse(buildClaudeEngineJson2(config));
|
|
503
|
-
|
|
588
|
+
writeFileSync4(
|
|
504
589
|
settingsPath,
|
|
505
590
|
JSON.stringify({ ...existing, hooks: hooks.hooks }, null, 2) + "\n"
|
|
506
591
|
);
|
|
@@ -508,47 +593,59 @@ async function updateCommand() {
|
|
|
508
593
|
if (agents.includes("cursor")) {
|
|
509
594
|
const cursorRules = buildCursorEngine2(config);
|
|
510
595
|
const cursorPath = ".cursorrules";
|
|
511
|
-
if (
|
|
596
|
+
if (existsSync6(cursorPath)) {
|
|
512
597
|
const content = readFileSync5(cursorPath, "utf8");
|
|
513
598
|
const start = content.indexOf("# \u2500\u2500\u2500 Holdpoint Rules");
|
|
514
599
|
const end = content.indexOf("# \u2500\u2500\u2500 End Holdpoint Rules \u2500\u2500\u2500");
|
|
515
600
|
if (start !== -1 && end !== -1) {
|
|
516
601
|
const afterEnd = content.indexOf("\n", end);
|
|
517
602
|
const updated = content.slice(0, start) + cursorRules + content.slice(afterEnd === -1 ? end : afterEnd + 1);
|
|
518
|
-
|
|
603
|
+
writeFileSync4(cursorPath, updated);
|
|
519
604
|
} else {
|
|
520
|
-
|
|
605
|
+
writeFileSync4(cursorPath, content + "\n" + cursorRules);
|
|
521
606
|
}
|
|
522
607
|
}
|
|
523
608
|
}
|
|
524
609
|
if (agents.includes("codex")) {
|
|
525
610
|
mkdirSync3(".codex", { recursive: true });
|
|
526
|
-
|
|
527
|
-
|
|
611
|
+
writeFileSync4(".codex/hooks.json", buildCodexHooksJson2(config), "utf8");
|
|
612
|
+
writeFileSync4(".codex/holdpoint-check.mjs", buildCodexCheckScript2(config), "utf8");
|
|
528
613
|
const configTomlPath = ".codex/config.toml";
|
|
529
|
-
if (!
|
|
530
|
-
|
|
614
|
+
if (!existsSync6(configTomlPath)) {
|
|
615
|
+
writeFileSync4(configTomlPath, buildCodexConfigToml2(), "utf8");
|
|
531
616
|
} else {
|
|
532
617
|
const existing2 = readFileSync5(configTomlPath, "utf8");
|
|
533
618
|
if (!existing2.includes("[features]")) {
|
|
534
|
-
|
|
619
|
+
writeFileSync4(configTomlPath, existing2.trimEnd() + "\n\n" + buildCodexConfigToml2(), "utf8");
|
|
535
620
|
}
|
|
536
621
|
}
|
|
537
622
|
const agentsMdPath = "AGENTS.md";
|
|
538
|
-
const existing =
|
|
539
|
-
|
|
623
|
+
const existing = existsSync6(agentsMdPath) ? readFileSync5(agentsMdPath, "utf8") : "";
|
|
624
|
+
writeFileSync4(agentsMdPath, spliceAgentsMd2(existing, config), "utf8");
|
|
540
625
|
}
|
|
626
|
+
const wrotePrerequisites = ensureBundledFile(
|
|
627
|
+
"HOLDPOINT_PREREQUISITES.md",
|
|
628
|
+
"HOLDPOINT_PREREQUISITES.md",
|
|
629
|
+
MINIMAL_PREREQUISITES2
|
|
630
|
+
);
|
|
541
631
|
spinner.succeed(chalk4.green("Engine files updated from current checks.yaml"));
|
|
632
|
+
if (wrotePrerequisites) {
|
|
633
|
+
console.log(
|
|
634
|
+
chalk4.cyan(
|
|
635
|
+
"Created HOLDPOINT_PREREQUISITES.md with Copilot experimental-mode and other agent setup notes."
|
|
636
|
+
)
|
|
637
|
+
);
|
|
638
|
+
}
|
|
542
639
|
}
|
|
543
640
|
|
|
544
641
|
// src/commands/build.ts
|
|
545
642
|
import { createServer } from "http";
|
|
546
|
-
import { createReadStream, existsSync as
|
|
547
|
-
import { join as
|
|
548
|
-
import { fileURLToPath as
|
|
549
|
-
import { execSync as
|
|
643
|
+
import { createReadStream, existsSync as existsSync7 } from "fs";
|
|
644
|
+
import { join as join4, extname, dirname as dirname3 } from "path";
|
|
645
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
646
|
+
import { execSync as execSync3 } from "child_process";
|
|
550
647
|
import chalk5 from "chalk";
|
|
551
|
-
var
|
|
648
|
+
var __dirname3 = dirname3(fileURLToPath3(import.meta.url));
|
|
552
649
|
var MIME = {
|
|
553
650
|
".html": "text/html; charset=utf-8",
|
|
554
651
|
".js": "text/javascript",
|
|
@@ -570,8 +667,8 @@ function serveFile(res, filePath) {
|
|
|
570
667
|
function handleRequest(req, res, uiDir) {
|
|
571
668
|
const url = (req.url ?? "/").split("?")[0] ?? "/";
|
|
572
669
|
if (url === "/__holdpoint/initial-yaml") {
|
|
573
|
-
const checksPath =
|
|
574
|
-
if (
|
|
670
|
+
const checksPath = join4(process.cwd(), "checks.yaml");
|
|
671
|
+
if (existsSync7(checksPath)) {
|
|
575
672
|
res.writeHead(200, { "Content-Type": "text/yaml; charset=utf-8" });
|
|
576
673
|
createReadStream(checksPath).pipe(res);
|
|
577
674
|
} else {
|
|
@@ -581,8 +678,8 @@ function handleRequest(req, res, uiDir) {
|
|
|
581
678
|
return;
|
|
582
679
|
}
|
|
583
680
|
if (url === "/__holdpoint/initial-reports") {
|
|
584
|
-
const reportsPath =
|
|
585
|
-
if (
|
|
681
|
+
const reportsPath = join4(process.cwd(), ".holdpoint", "check-reports.json");
|
|
682
|
+
if (existsSync7(reportsPath)) {
|
|
586
683
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
587
684
|
createReadStream(reportsPath).pipe(res);
|
|
588
685
|
} else {
|
|
@@ -591,21 +688,21 @@ function handleRequest(req, res, uiDir) {
|
|
|
591
688
|
}
|
|
592
689
|
return;
|
|
593
690
|
}
|
|
594
|
-
const candidate =
|
|
595
|
-
const filePath =
|
|
691
|
+
const candidate = join4(uiDir, url === "/" ? "index.html" : url);
|
|
692
|
+
const filePath = existsSync7(candidate) ? candidate : join4(uiDir, "index.html");
|
|
596
693
|
serveFile(res, filePath);
|
|
597
694
|
}
|
|
598
695
|
async function buildCommand() {
|
|
599
696
|
const port = 4321;
|
|
600
|
-
const uiDir =
|
|
601
|
-
if (!
|
|
697
|
+
const uiDir = join4(__dirname3, "builder-ui");
|
|
698
|
+
if (!existsSync7(uiDir)) {
|
|
602
699
|
console.error(chalk5.red("\u2717 Builder UI not found.\n"));
|
|
603
700
|
console.log(chalk5.dim(" This is unexpected for a published build of @holdpoint/cli."));
|
|
604
701
|
console.log(chalk5.dim(" If you installed from source, rebuild with: pnpm turbo build\n"));
|
|
605
702
|
process.exit(1);
|
|
606
703
|
}
|
|
607
704
|
const server = createServer((req, res) => handleRequest(req, res, uiDir));
|
|
608
|
-
await new Promise((
|
|
705
|
+
await new Promise((resolve2, reject) => {
|
|
609
706
|
server.listen(port, () => {
|
|
610
707
|
console.log(
|
|
611
708
|
`
|
|
@@ -615,29 +712,29 @@ ${chalk5.green("\u2713")} Holdpoint builder running at ${chalk5.cyan(`http://loc
|
|
|
615
712
|
console.log(chalk5.dim(" Press Ctrl+C to stop\n"));
|
|
616
713
|
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
617
714
|
try {
|
|
618
|
-
|
|
715
|
+
execSync3(`${openCmd} http://localhost:${port}`, { stdio: "ignore" });
|
|
619
716
|
} catch {
|
|
620
717
|
}
|
|
621
718
|
});
|
|
622
719
|
server.on("error", reject);
|
|
623
720
|
process.on("SIGINT", () => {
|
|
624
721
|
console.log(chalk5.dim("\n Stopping builder\u2026"));
|
|
625
|
-
server.close(() =>
|
|
722
|
+
server.close(() => resolve2());
|
|
626
723
|
});
|
|
627
724
|
});
|
|
628
725
|
}
|
|
629
726
|
|
|
630
727
|
// src/commands/evolve.ts
|
|
631
|
-
import { existsSync as
|
|
632
|
-
import { execSync as
|
|
728
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
729
|
+
import { execSync as execSync6 } from "child_process";
|
|
633
730
|
import chalk6 from "chalk";
|
|
634
731
|
import ora4 from "ora";
|
|
635
732
|
import { parseHoldpointYaml as parseHoldpointYaml5, generateYaml } from "@holdpoint/yaml-core";
|
|
636
733
|
|
|
637
734
|
// src/evolve/scanner.ts
|
|
638
|
-
import { existsSync as
|
|
639
|
-
import { join as
|
|
640
|
-
import { execSync as
|
|
735
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6, readdirSync } from "fs";
|
|
736
|
+
import { join as join5 } from "path";
|
|
737
|
+
import { execSync as execSync4 } from "child_process";
|
|
641
738
|
function tryReadJson(path) {
|
|
642
739
|
try {
|
|
643
740
|
return JSON.parse(readFileSync6(path, "utf8"));
|
|
@@ -653,17 +750,17 @@ function tryReadText(path) {
|
|
|
653
750
|
}
|
|
654
751
|
}
|
|
655
752
|
function scanProject(cwd = process.cwd()) {
|
|
656
|
-
const exists = (p) =>
|
|
753
|
+
const exists = (p) => existsSync8(join5(cwd, p));
|
|
657
754
|
const packageManager = exists("pnpm-lock.yaml") ? "pnpm" : exists("yarn.lock") ? "yarn" : exists("bun.lockb") ? "bun" : "npm";
|
|
658
|
-
const pkg = tryReadJson(
|
|
755
|
+
const pkg = tryReadJson(join5(cwd, "package.json"));
|
|
659
756
|
const scripts = pkg?.scripts ?? {};
|
|
660
757
|
const deps = /* @__PURE__ */ new Set([
|
|
661
758
|
...Object.keys(pkg?.dependencies ?? {}),
|
|
662
759
|
...Object.keys(pkg?.devDependencies ?? {})
|
|
663
760
|
]);
|
|
664
|
-
const pyprojectText = tryReadText(
|
|
665
|
-
const requirementsText = tryReadText(
|
|
666
|
-
const pipfileText = tryReadText(
|
|
761
|
+
const pyprojectText = tryReadText(join5(cwd, "pyproject.toml"));
|
|
762
|
+
const requirementsText = tryReadText(join5(cwd, "requirements.txt"));
|
|
763
|
+
const pipfileText = tryReadText(join5(cwd, "Pipfile"));
|
|
667
764
|
const allPyText = pyprojectText + requirementsText + pipfileText;
|
|
668
765
|
const hasPytest = exists("pytest.ini") || exists("setup.cfg") || allPyText.includes("pytest") || allPyText.includes("[tool.pytest");
|
|
669
766
|
const hasRuff = allPyText.includes("ruff") || deps.has("ruff");
|
|
@@ -715,9 +812,9 @@ function scanProject(cwd = process.cwd()) {
|
|
|
715
812
|
}
|
|
716
813
|
|
|
717
814
|
// src/evolve/dead-checker.ts
|
|
718
|
-
import { execSync as
|
|
719
|
-
import { readdirSync as readdirSync2, existsSync as
|
|
720
|
-
import { join as
|
|
815
|
+
import { execSync as execSync5 } from "child_process";
|
|
816
|
+
import { readdirSync as readdirSync2, existsSync as existsSync9 } from "fs";
|
|
817
|
+
import { join as join6 } from "path";
|
|
721
818
|
var NAMED_SCOPES = /* @__PURE__ */ new Set([
|
|
722
819
|
"frontend",
|
|
723
820
|
"backend",
|
|
@@ -762,7 +859,7 @@ function walkDir(dir, root, depth, maxDepth) {
|
|
|
762
859
|
const results = [];
|
|
763
860
|
for (const entry of entries) {
|
|
764
861
|
if (WALK_IGNORED.has(entry) || entry.startsWith(".")) continue;
|
|
765
|
-
const full =
|
|
862
|
+
const full = join6(dir, entry);
|
|
766
863
|
const rel = full.slice(root.length + 1);
|
|
767
864
|
results.push(rel);
|
|
768
865
|
const children = walkDir(full, root, depth + 1, maxDepth);
|
|
@@ -772,7 +869,7 @@ function walkDir(dir, root, depth, maxDepth) {
|
|
|
772
869
|
}
|
|
773
870
|
function getRepoFiles(cwd) {
|
|
774
871
|
try {
|
|
775
|
-
const out =
|
|
872
|
+
const out = execSync5("git ls-files", {
|
|
776
873
|
cwd,
|
|
777
874
|
encoding: "utf8",
|
|
778
875
|
stdio: ["pipe", "pipe", "ignore"]
|
|
@@ -809,7 +906,7 @@ function detectStaleChecks(config, repoFiles) {
|
|
|
809
906
|
if (matches.length === 0) {
|
|
810
907
|
const label = patternAlias ? `Pattern '${patternAlias}' (= '${regexStr}')` : `Regex '${regexStr}'`;
|
|
811
908
|
const suggestedConditionPath = extractPathFromRegex(regexStr);
|
|
812
|
-
const pathGone = !suggestedConditionPath || !
|
|
909
|
+
const pathGone = !suggestedConditionPath || !existsSync9(join6(process.cwd(), suggestedConditionPath));
|
|
813
910
|
if (pathGone) {
|
|
814
911
|
stale.push({
|
|
815
912
|
check,
|
|
@@ -1010,7 +1107,7 @@ function withHeader(header, newYaml) {
|
|
|
1010
1107
|
return header + "\n\n" + newYaml;
|
|
1011
1108
|
}
|
|
1012
1109
|
async function evolveCommand(options) {
|
|
1013
|
-
if (!
|
|
1110
|
+
if (!existsSync10("checks.yaml")) {
|
|
1014
1111
|
console.error(chalk6.red("No checks.yaml found. Run `holdpoint init` first."));
|
|
1015
1112
|
process.exit(1);
|
|
1016
1113
|
}
|
|
@@ -1132,10 +1229,10 @@ async function evolveCommand(options) {
|
|
|
1132
1229
|
};
|
|
1133
1230
|
const header = extractHeader(yamlContent);
|
|
1134
1231
|
const newYaml = withHeader(header, generateYaml(updatedConfig));
|
|
1135
|
-
|
|
1232
|
+
writeFileSync5("checks.yaml", newYaml, "utf8");
|
|
1136
1233
|
applySpinner.text = "Running holdpoint update\u2026";
|
|
1137
1234
|
try {
|
|
1138
|
-
|
|
1235
|
+
execSync6("npx @holdpoint/cli@alpha update", { stdio: "pipe" });
|
|
1139
1236
|
} catch {
|
|
1140
1237
|
applySpinner.warn(
|
|
1141
1238
|
chalk6.yellow("checks.yaml updated, but `holdpoint update` failed \u2014 run it manually.")
|
|
@@ -1157,9 +1254,652 @@ function printAppliedSummary(added, wrapped) {
|
|
|
1157
1254
|
);
|
|
1158
1255
|
}
|
|
1159
1256
|
|
|
1257
|
+
// src/commands/live.ts
|
|
1258
|
+
import { existsSync as existsSync11 } from "fs";
|
|
1259
|
+
import { dirname as dirname4, join as join7 } from "path";
|
|
1260
|
+
import chalk7 from "chalk";
|
|
1261
|
+
import { identifyProject as identifyProject2 } from "@holdpoint/live-daemon";
|
|
1262
|
+
|
|
1263
|
+
// src/lib/ensure-daemon.ts
|
|
1264
|
+
import { spawn } from "child_process";
|
|
1265
|
+
import { readHealthyDaemonLock } from "@holdpoint/live-daemon";
|
|
1266
|
+
function sleep(ms) {
|
|
1267
|
+
return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
|
|
1268
|
+
}
|
|
1269
|
+
async function ensureDaemon(timeoutMs = 5e3) {
|
|
1270
|
+
const existing = await readHealthyDaemonLock();
|
|
1271
|
+
if (existing) {
|
|
1272
|
+
return { info: existing, started: false };
|
|
1273
|
+
}
|
|
1274
|
+
const cliEntry = process.argv[1];
|
|
1275
|
+
if (!cliEntry) {
|
|
1276
|
+
throw new Error("Cannot determine the current holdpoint CLI entrypoint");
|
|
1277
|
+
}
|
|
1278
|
+
const child = spawn(process.execPath, [cliEntry, "daemon-serve"], {
|
|
1279
|
+
stdio: "ignore",
|
|
1280
|
+
env: process.env,
|
|
1281
|
+
cwd: process.cwd()
|
|
1282
|
+
});
|
|
1283
|
+
child.unref();
|
|
1284
|
+
const deadline = Date.now() + timeoutMs;
|
|
1285
|
+
while (Date.now() < deadline) {
|
|
1286
|
+
const lock = await readHealthyDaemonLock();
|
|
1287
|
+
if (lock) {
|
|
1288
|
+
return { info: lock, started: true };
|
|
1289
|
+
}
|
|
1290
|
+
await sleep(100);
|
|
1291
|
+
}
|
|
1292
|
+
throw new Error("Daemon unavailable + cannot spawn");
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// src/lib/open-browser.ts
|
|
1296
|
+
import { execSync as execSync7 } from "child_process";
|
|
1297
|
+
function openBrowser(url) {
|
|
1298
|
+
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
1299
|
+
try {
|
|
1300
|
+
execSync7(`${openCmd} ${JSON.stringify(url)}`, { stdio: "ignore" });
|
|
1301
|
+
} catch {
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// src/commands/live.ts
|
|
1306
|
+
function findChecksYaml(startDir) {
|
|
1307
|
+
let current = startDir;
|
|
1308
|
+
for (; ; ) {
|
|
1309
|
+
const candidate = join7(current, "checks.yaml");
|
|
1310
|
+
if (existsSync11(candidate)) {
|
|
1311
|
+
return candidate;
|
|
1312
|
+
}
|
|
1313
|
+
const parent = dirname4(current);
|
|
1314
|
+
if (parent === current) {
|
|
1315
|
+
return null;
|
|
1316
|
+
}
|
|
1317
|
+
current = parent;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
function tryResolveCurrentProject() {
|
|
1321
|
+
const checksYaml = findChecksYaml(process.cwd());
|
|
1322
|
+
if (checksYaml) {
|
|
1323
|
+
return identifyProject2(dirname4(checksYaml));
|
|
1324
|
+
}
|
|
1325
|
+
try {
|
|
1326
|
+
return identifyProject2(process.cwd());
|
|
1327
|
+
} catch {
|
|
1328
|
+
return null;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
async function liveCommand(options = {}) {
|
|
1332
|
+
const { info, started } = await ensureDaemon();
|
|
1333
|
+
const baseUrl = new URL(`/__holdpoint/live-auth`, `http://127.0.0.1:${info.port}`);
|
|
1334
|
+
baseUrl.searchParams.set("token", info.token);
|
|
1335
|
+
const currentProject = options.project ? null : tryResolveCurrentProject();
|
|
1336
|
+
if (options.project) {
|
|
1337
|
+
baseUrl.searchParams.set("project", options.project);
|
|
1338
|
+
} else if (currentProject) {
|
|
1339
|
+
baseUrl.searchParams.set("project", currentProject.hash);
|
|
1340
|
+
baseUrl.searchParams.set("name", currentProject.name);
|
|
1341
|
+
baseUrl.searchParams.set("root", currentProject.root);
|
|
1342
|
+
}
|
|
1343
|
+
openBrowser(baseUrl.toString());
|
|
1344
|
+
console.log(
|
|
1345
|
+
chalk7.green(
|
|
1346
|
+
started ? "\u2713 Started Holdpoint Live and opened the browser" : "\u2713 Opened Holdpoint Live"
|
|
1347
|
+
)
|
|
1348
|
+
);
|
|
1349
|
+
console.log(` url: ${chalk7.cyan(baseUrl.toString())}`);
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
// src/commands/daemon.ts
|
|
1353
|
+
import chalk8 from "chalk";
|
|
1354
|
+
import {
|
|
1355
|
+
DaemonAlreadyRunningError,
|
|
1356
|
+
isProcessAlive,
|
|
1357
|
+
readDaemonLock,
|
|
1358
|
+
readHealthyDaemonLock as readHealthyDaemonLock2,
|
|
1359
|
+
removeDaemonLock,
|
|
1360
|
+
startDaemonProcess
|
|
1361
|
+
} from "@holdpoint/live-daemon";
|
|
1362
|
+
|
|
1363
|
+
// src/version.ts
|
|
1364
|
+
var CLI_VERSION = "0.1.0-alpha.15";
|
|
1365
|
+
|
|
1366
|
+
// src/commands/daemon.ts
|
|
1367
|
+
function formatUptime(lock) {
|
|
1368
|
+
const seconds = Math.max(0, Math.floor((Date.now() - lock.started_at) / 1e3));
|
|
1369
|
+
const minutes = Math.floor(seconds / 60);
|
|
1370
|
+
const remainingSeconds = seconds % 60;
|
|
1371
|
+
return minutes > 0 ? `${minutes}m ${remainingSeconds}s` : `${remainingSeconds}s`;
|
|
1372
|
+
}
|
|
1373
|
+
async function fetchSessionCount(lock) {
|
|
1374
|
+
try {
|
|
1375
|
+
const response = await fetch(`http://127.0.0.1:${lock.port}/v1/sessions`, {
|
|
1376
|
+
headers: {
|
|
1377
|
+
authorization: `Bearer ${lock.token}`
|
|
1378
|
+
}
|
|
1379
|
+
});
|
|
1380
|
+
if (!response.ok) return null;
|
|
1381
|
+
const parsed = await response.json();
|
|
1382
|
+
return Array.isArray(parsed.sessions) ? parsed.sessions.length : null;
|
|
1383
|
+
} catch {
|
|
1384
|
+
return null;
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
function sleep2(ms) {
|
|
1388
|
+
return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
|
|
1389
|
+
}
|
|
1390
|
+
async function daemonStartCommand() {
|
|
1391
|
+
const { info, started } = await ensureDaemon();
|
|
1392
|
+
const sessionCount = await fetchSessionCount(info);
|
|
1393
|
+
const headline = started ? "Started Holdpoint Live daemon" : "Reused existing Holdpoint Live daemon";
|
|
1394
|
+
console.log(chalk8.green(`\u2713 ${headline}`));
|
|
1395
|
+
console.log(` pid: ${chalk8.cyan(String(info.pid))}`);
|
|
1396
|
+
console.log(` port: ${chalk8.cyan(String(info.port))}`);
|
|
1397
|
+
console.log(` uptime: ${chalk8.cyan(formatUptime(info))}`);
|
|
1398
|
+
if (sessionCount !== null) {
|
|
1399
|
+
console.log(` sessions: ${chalk8.cyan(String(sessionCount))}`);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
async function daemonStatusCommand() {
|
|
1403
|
+
const lock = await readHealthyDaemonLock2();
|
|
1404
|
+
if (!lock) {
|
|
1405
|
+
console.log(chalk8.yellow("Holdpoint Live daemon is not running."));
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
const sessionCount = await fetchSessionCount(lock);
|
|
1409
|
+
console.log(chalk8.green("\u2713 Holdpoint Live daemon is running"));
|
|
1410
|
+
console.log(` pid: ${chalk8.cyan(String(lock.pid))}`);
|
|
1411
|
+
console.log(` port: ${chalk8.cyan(String(lock.port))}`);
|
|
1412
|
+
console.log(` uptime: ${chalk8.cyan(formatUptime(lock))}`);
|
|
1413
|
+
if (sessionCount !== null) {
|
|
1414
|
+
console.log(` sessions: ${chalk8.cyan(String(sessionCount))}`);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
async function daemonStopCommand() {
|
|
1418
|
+
const lock = readDaemonLock();
|
|
1419
|
+
if (!lock) {
|
|
1420
|
+
console.log(chalk8.yellow("Holdpoint Live daemon is not running."));
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
if (!isProcessAlive(lock.pid)) {
|
|
1424
|
+
removeDaemonLock(void 0, lock.token);
|
|
1425
|
+
console.log(chalk8.yellow("Removed stale Holdpoint Live lockfile."));
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
process.kill(lock.pid, "SIGTERM");
|
|
1429
|
+
const deadline = Date.now() + 5e3;
|
|
1430
|
+
while (Date.now() < deadline) {
|
|
1431
|
+
if (!isProcessAlive(lock.pid)) {
|
|
1432
|
+
removeDaemonLock(void 0, lock.token);
|
|
1433
|
+
console.log(chalk8.green(`\u2713 Stopped Holdpoint Live daemon (${lock.pid})`));
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
await sleep2(100);
|
|
1437
|
+
}
|
|
1438
|
+
process.kill(lock.pid, "SIGKILL");
|
|
1439
|
+
await sleep2(100);
|
|
1440
|
+
removeDaemonLock(void 0, lock.token);
|
|
1441
|
+
console.log(chalk8.green(`\u2713 Force-stopped Holdpoint Live daemon (${lock.pid})`));
|
|
1442
|
+
}
|
|
1443
|
+
async function daemonServeCommand(options) {
|
|
1444
|
+
try {
|
|
1445
|
+
const daemon2 = await startDaemonProcess({
|
|
1446
|
+
version: CLI_VERSION,
|
|
1447
|
+
...options.port ? { port: Number(options.port) } : {}
|
|
1448
|
+
});
|
|
1449
|
+
await daemon2.closed;
|
|
1450
|
+
} catch (error) {
|
|
1451
|
+
if (error instanceof DaemonAlreadyRunningError) {
|
|
1452
|
+
process.exit(0);
|
|
1453
|
+
}
|
|
1454
|
+
throw error;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// src/commands/engines.ts
|
|
1459
|
+
import chalk9 from "chalk";
|
|
1460
|
+
|
|
1461
|
+
// src/engines.ts
|
|
1462
|
+
import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
|
|
1463
|
+
import { dirname as dirname5, join as join8, resolve, sep } from "path";
|
|
1464
|
+
import { createRequire } from "module";
|
|
1465
|
+
import { fileURLToPath as fileURLToPath4, pathToFileURL } from "url";
|
|
1466
|
+
import { parseEventV1 } from "@holdpoint/live-protocol";
|
|
1467
|
+
var require2 = createRequire(import.meta.url);
|
|
1468
|
+
var CLI_SRC_DIR = dirname5(fileURLToPath4(import.meta.url));
|
|
1469
|
+
var MONOREPO_ROOT = resolve(CLI_SRC_DIR, "../../..");
|
|
1470
|
+
var BUILTIN_LIVE_ENGINE_PACKAGES = ["@holdpoint/engine-claude"];
|
|
1471
|
+
var HOLDPOINT_ENGINE_KEYWORD = "holdpoint-engine";
|
|
1472
|
+
function isObject(value) {
|
|
1473
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
1474
|
+
}
|
|
1475
|
+
function isExternalLiveEnginePackageName(packageName) {
|
|
1476
|
+
return /^holdpoint-engine-[a-z0-9-]+$/.test(packageName) || /^@[a-z0-9_.-]+\/holdpoint-engine-[a-z0-9-]+$/.test(packageName);
|
|
1477
|
+
}
|
|
1478
|
+
function readJsonFile(path) {
|
|
1479
|
+
if (!existsSync12(path)) {
|
|
1480
|
+
return null;
|
|
1481
|
+
}
|
|
1482
|
+
try {
|
|
1483
|
+
const parsed = JSON.parse(readFileSync8(path, "utf8"));
|
|
1484
|
+
return isObject(parsed) ? parsed : null;
|
|
1485
|
+
} catch {
|
|
1486
|
+
return null;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
function findNearestPackageRoot(startDir) {
|
|
1490
|
+
let current = resolve(startDir);
|
|
1491
|
+
while (true) {
|
|
1492
|
+
if (existsSync12(join8(current, "package.json"))) {
|
|
1493
|
+
return current;
|
|
1494
|
+
}
|
|
1495
|
+
const parent = dirname5(current);
|
|
1496
|
+
if (parent === current) {
|
|
1497
|
+
return resolve(startDir);
|
|
1498
|
+
}
|
|
1499
|
+
current = parent;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
function findPackageRootFromFile(path) {
|
|
1503
|
+
let current = dirname5(path);
|
|
1504
|
+
while (true) {
|
|
1505
|
+
if (existsSync12(join8(current, "package.json"))) {
|
|
1506
|
+
return current;
|
|
1507
|
+
}
|
|
1508
|
+
const parent = dirname5(current);
|
|
1509
|
+
if (parent === current) {
|
|
1510
|
+
return null;
|
|
1511
|
+
}
|
|
1512
|
+
current = parent;
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
function getDependencyEnginePackageNames(projectRoot) {
|
|
1516
|
+
const packageJson = readJsonFile(join8(projectRoot, "package.json"));
|
|
1517
|
+
if (!packageJson) {
|
|
1518
|
+
return [];
|
|
1519
|
+
}
|
|
1520
|
+
const packageNames = /* @__PURE__ */ new Set();
|
|
1521
|
+
for (const field of ["dependencies", "devDependencies", "optionalDependencies"]) {
|
|
1522
|
+
const deps = packageJson[field];
|
|
1523
|
+
if (!isObject(deps)) {
|
|
1524
|
+
continue;
|
|
1525
|
+
}
|
|
1526
|
+
for (const packageName of Object.keys(deps)) {
|
|
1527
|
+
if (isExternalLiveEnginePackageName(packageName)) {
|
|
1528
|
+
packageNames.add(packageName);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
return [...packageNames];
|
|
1533
|
+
}
|
|
1534
|
+
function resolvePackageRoot(packageName, projectRoot) {
|
|
1535
|
+
try {
|
|
1536
|
+
const entryPath = require2.resolve(packageName);
|
|
1537
|
+
return findPackageRootFromFile(entryPath);
|
|
1538
|
+
} catch {
|
|
1539
|
+
}
|
|
1540
|
+
try {
|
|
1541
|
+
const entryPath = require2.resolve(packageName, {
|
|
1542
|
+
paths: [projectRoot, process.cwd()]
|
|
1543
|
+
});
|
|
1544
|
+
return findPackageRootFromFile(entryPath);
|
|
1545
|
+
} catch {
|
|
1546
|
+
}
|
|
1547
|
+
try {
|
|
1548
|
+
const packageJsonPath = require2.resolve(`${packageName}/package.json`, {
|
|
1549
|
+
paths: [projectRoot, process.cwd()]
|
|
1550
|
+
});
|
|
1551
|
+
return dirname5(packageJsonPath);
|
|
1552
|
+
} catch {
|
|
1553
|
+
if (packageName.startsWith("@holdpoint/")) {
|
|
1554
|
+
const scopedName = packageName.split("/")[1];
|
|
1555
|
+
if (scopedName) {
|
|
1556
|
+
const packageDir = resolve(MONOREPO_ROOT, "packages", scopedName);
|
|
1557
|
+
if (existsSync12(join8(packageDir, "package.json"))) {
|
|
1558
|
+
return packageDir;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
return null;
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
function formatImportError(error) {
|
|
1566
|
+
return error instanceof Error && error.message ? error.message : String(error);
|
|
1567
|
+
}
|
|
1568
|
+
function parseManifest(value) {
|
|
1569
|
+
if (!isObject(value)) {
|
|
1570
|
+
return null;
|
|
1571
|
+
}
|
|
1572
|
+
if (value.manifestVersion !== 1) {
|
|
1573
|
+
return null;
|
|
1574
|
+
}
|
|
1575
|
+
if (typeof value.id !== "string" || !/^[a-z0-9-]+$/.test(value.id)) {
|
|
1576
|
+
return null;
|
|
1577
|
+
}
|
|
1578
|
+
if (typeof value.displayName !== "string" || !value.displayName.trim()) {
|
|
1579
|
+
return null;
|
|
1580
|
+
}
|
|
1581
|
+
return {
|
|
1582
|
+
manifestVersion: 1,
|
|
1583
|
+
id: value.id,
|
|
1584
|
+
displayName: value.displayName
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
function parseLiveCapabilities(value) {
|
|
1588
|
+
if (!isObject(value)) {
|
|
1589
|
+
return null;
|
|
1590
|
+
}
|
|
1591
|
+
const capabilities = {};
|
|
1592
|
+
for (const key of [
|
|
1593
|
+
"can_stream",
|
|
1594
|
+
"can_control",
|
|
1595
|
+
"can_modify_context",
|
|
1596
|
+
"can_register_tools",
|
|
1597
|
+
"control_online"
|
|
1598
|
+
]) {
|
|
1599
|
+
const entry = value[key];
|
|
1600
|
+
if (entry === void 0) {
|
|
1601
|
+
continue;
|
|
1602
|
+
}
|
|
1603
|
+
if (typeof entry !== "boolean") {
|
|
1604
|
+
return null;
|
|
1605
|
+
}
|
|
1606
|
+
capabilities[key] = entry;
|
|
1607
|
+
}
|
|
1608
|
+
return capabilities;
|
|
1609
|
+
}
|
|
1610
|
+
function parseLiveAdapter(value, manifest) {
|
|
1611
|
+
if (!isObject(value)) {
|
|
1612
|
+
return null;
|
|
1613
|
+
}
|
|
1614
|
+
if (typeof value.id !== "string" || typeof value.displayName !== "string") {
|
|
1615
|
+
return null;
|
|
1616
|
+
}
|
|
1617
|
+
if (value.id !== manifest.id || value.displayName !== manifest.displayName) {
|
|
1618
|
+
return null;
|
|
1619
|
+
}
|
|
1620
|
+
const capabilities = parseLiveCapabilities(value.capabilities);
|
|
1621
|
+
if (!capabilities) {
|
|
1622
|
+
return null;
|
|
1623
|
+
}
|
|
1624
|
+
const generateBridgeCommand = value.generateBridgeCommand;
|
|
1625
|
+
if (typeof generateBridgeCommand !== "function") {
|
|
1626
|
+
return null;
|
|
1627
|
+
}
|
|
1628
|
+
const translateHookInput = value.translateHookInput;
|
|
1629
|
+
if (typeof translateHookInput !== "function") {
|
|
1630
|
+
return null;
|
|
1631
|
+
}
|
|
1632
|
+
return {
|
|
1633
|
+
id: value.id,
|
|
1634
|
+
displayName: value.displayName,
|
|
1635
|
+
capabilities,
|
|
1636
|
+
generateBridgeCommand: () => {
|
|
1637
|
+
const command = generateBridgeCommand();
|
|
1638
|
+
if (typeof command !== "string") {
|
|
1639
|
+
throw new Error("adapter.generateBridgeCommand() must return a string");
|
|
1640
|
+
}
|
|
1641
|
+
return command;
|
|
1642
|
+
},
|
|
1643
|
+
translateHookInput: (raw, options) => {
|
|
1644
|
+
const event = translateHookInput(raw, options);
|
|
1645
|
+
return event == null ? null : parseEventV1(event);
|
|
1646
|
+
}
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
async function importModule(modulePath) {
|
|
1650
|
+
const moduleUrl = pathToFileURL(modulePath).href;
|
|
1651
|
+
return await import(moduleUrl);
|
|
1652
|
+
}
|
|
1653
|
+
function resolvePackageAssetPath(packageRoot, relativePath) {
|
|
1654
|
+
const declaredPath = resolve(packageRoot, relativePath);
|
|
1655
|
+
const sourceFallback = resolve(
|
|
1656
|
+
packageRoot,
|
|
1657
|
+
relativePath.replace(/^\.\/dist\//, "./src/").replace(/\.js$/, ".ts")
|
|
1658
|
+
);
|
|
1659
|
+
if (packageRoot.startsWith(resolve(MONOREPO_ROOT, "packages") + sep) && existsSync12(sourceFallback)) {
|
|
1660
|
+
return sourceFallback;
|
|
1661
|
+
}
|
|
1662
|
+
if (existsSync12(declaredPath)) {
|
|
1663
|
+
return declaredPath;
|
|
1664
|
+
}
|
|
1665
|
+
return sourceFallback;
|
|
1666
|
+
}
|
|
1667
|
+
async function resolveCandidate(packageName, source, projectRoot) {
|
|
1668
|
+
const packageRoot = resolvePackageRoot(packageName, projectRoot);
|
|
1669
|
+
if (!packageRoot) {
|
|
1670
|
+
return {
|
|
1671
|
+
packageName,
|
|
1672
|
+
source,
|
|
1673
|
+
status: "ignored",
|
|
1674
|
+
reason: "package could not be resolved from this project"
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
const packageJson = readJsonFile(join8(packageRoot, "package.json"));
|
|
1678
|
+
if (!packageJson) {
|
|
1679
|
+
return {
|
|
1680
|
+
packageName,
|
|
1681
|
+
source,
|
|
1682
|
+
status: "ignored",
|
|
1683
|
+
reason: "package.json could not be read"
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
const keywords = Array.isArray(packageJson.keywords) ? packageJson.keywords : [];
|
|
1687
|
+
if (!keywords.includes(HOLDPOINT_ENGINE_KEYWORD)) {
|
|
1688
|
+
return {
|
|
1689
|
+
packageName,
|
|
1690
|
+
source,
|
|
1691
|
+
status: "ignored",
|
|
1692
|
+
reason: `missing \`${HOLDPOINT_ENGINE_KEYWORD}\` keyword`
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
const metadata = isObject(packageJson.holdpoint) ? packageJson.holdpoint : void 0;
|
|
1696
|
+
if (!metadata?.manifest) {
|
|
1697
|
+
return {
|
|
1698
|
+
packageName,
|
|
1699
|
+
source,
|
|
1700
|
+
status: "ignored",
|
|
1701
|
+
reason: "missing `holdpoint.manifest` package.json field"
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
if (!metadata.adapter) {
|
|
1705
|
+
return {
|
|
1706
|
+
packageName,
|
|
1707
|
+
source,
|
|
1708
|
+
status: "ignored",
|
|
1709
|
+
reason: "missing `holdpoint.adapter` package.json field"
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
const manifestPath = resolvePackageAssetPath(packageRoot, metadata.manifest);
|
|
1713
|
+
const adapterPath = resolvePackageAssetPath(packageRoot, metadata.adapter);
|
|
1714
|
+
if (!existsSync12(manifestPath)) {
|
|
1715
|
+
return {
|
|
1716
|
+
packageName,
|
|
1717
|
+
source,
|
|
1718
|
+
status: "ignored",
|
|
1719
|
+
reason: "manifest file does not exist"
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
if (!existsSync12(adapterPath)) {
|
|
1723
|
+
return {
|
|
1724
|
+
packageName,
|
|
1725
|
+
source,
|
|
1726
|
+
status: "ignored",
|
|
1727
|
+
reason: "adapter file does not exist"
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
try {
|
|
1731
|
+
const manifestModule = await importModule(manifestPath);
|
|
1732
|
+
const manifest = parseManifest(manifestModule.manifest);
|
|
1733
|
+
if (!manifest) {
|
|
1734
|
+
return {
|
|
1735
|
+
packageName,
|
|
1736
|
+
source,
|
|
1737
|
+
status: "ignored",
|
|
1738
|
+
reason: "manifest export is invalid"
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
return {
|
|
1742
|
+
packageName,
|
|
1743
|
+
source,
|
|
1744
|
+
status: "loaded",
|
|
1745
|
+
manifest,
|
|
1746
|
+
packageRoot,
|
|
1747
|
+
adapterPath
|
|
1748
|
+
};
|
|
1749
|
+
} catch (error) {
|
|
1750
|
+
return {
|
|
1751
|
+
packageName,
|
|
1752
|
+
source,
|
|
1753
|
+
status: "ignored",
|
|
1754
|
+
reason: `manifest import failed: ${formatImportError(error)}`
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
async function discoverLiveEnginesDetailed(options) {
|
|
1759
|
+
const projectRoot = findNearestPackageRoot(options?.cwd ?? process.cwd());
|
|
1760
|
+
const dependencyPackages = getDependencyEnginePackageNames(projectRoot);
|
|
1761
|
+
const seenPackages = /* @__PURE__ */ new Set();
|
|
1762
|
+
const results = [];
|
|
1763
|
+
const loadedIds = /* @__PURE__ */ new Set();
|
|
1764
|
+
const candidates = [
|
|
1765
|
+
...BUILTIN_LIVE_ENGINE_PACKAGES.map((packageName) => ({
|
|
1766
|
+
packageName,
|
|
1767
|
+
source: "built-in"
|
|
1768
|
+
})),
|
|
1769
|
+
...dependencyPackages.map((packageName) => ({ packageName, source: "dependency" }))
|
|
1770
|
+
];
|
|
1771
|
+
for (const candidate of candidates) {
|
|
1772
|
+
if (seenPackages.has(candidate.packageName)) {
|
|
1773
|
+
continue;
|
|
1774
|
+
}
|
|
1775
|
+
seenPackages.add(candidate.packageName);
|
|
1776
|
+
const result = await resolveCandidate(candidate.packageName, candidate.source, projectRoot);
|
|
1777
|
+
if (result.status === "loaded" && result.manifest) {
|
|
1778
|
+
if (loadedIds.has(result.manifest.id)) {
|
|
1779
|
+
results.push({
|
|
1780
|
+
packageName: result.packageName,
|
|
1781
|
+
source: result.source,
|
|
1782
|
+
status: "ignored",
|
|
1783
|
+
reason: `engine id \`${result.manifest.id}\` collides with an already loaded adapter`,
|
|
1784
|
+
manifest: result.manifest
|
|
1785
|
+
});
|
|
1786
|
+
continue;
|
|
1787
|
+
}
|
|
1788
|
+
loadedIds.add(result.manifest.id);
|
|
1789
|
+
}
|
|
1790
|
+
results.push(result);
|
|
1791
|
+
}
|
|
1792
|
+
return results;
|
|
1793
|
+
}
|
|
1794
|
+
async function discoverLiveEngines(options) {
|
|
1795
|
+
const results = await discoverLiveEnginesDetailed(options);
|
|
1796
|
+
return results.map(({ packageName, source, status, reason, manifest }) => ({
|
|
1797
|
+
packageName,
|
|
1798
|
+
source,
|
|
1799
|
+
status,
|
|
1800
|
+
...reason ? { reason } : {},
|
|
1801
|
+
...manifest ? { manifest } : {}
|
|
1802
|
+
}));
|
|
1803
|
+
}
|
|
1804
|
+
async function loadLiveAdapter(engineId, options) {
|
|
1805
|
+
const results = await discoverLiveEnginesDetailed(options);
|
|
1806
|
+
const match = results.find(
|
|
1807
|
+
(result) => result.status === "loaded" && result.manifest?.id === engineId
|
|
1808
|
+
);
|
|
1809
|
+
if (!match?.adapterPath || !match.manifest) {
|
|
1810
|
+
return null;
|
|
1811
|
+
}
|
|
1812
|
+
try {
|
|
1813
|
+
const adapterModule = await importModule(match.adapterPath);
|
|
1814
|
+
return parseLiveAdapter(adapterModule.adapter, match.manifest);
|
|
1815
|
+
} catch {
|
|
1816
|
+
return null;
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
// src/commands/engines.ts
|
|
1821
|
+
async function enginesCommand(options = {}) {
|
|
1822
|
+
const engines = await discoverLiveEngines();
|
|
1823
|
+
if (options.json) {
|
|
1824
|
+
console.log(JSON.stringify(engines, null, 2));
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
if (engines.length === 0) {
|
|
1828
|
+
console.log("No Holdpoint live adapters were discovered.");
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
for (const engine of engines) {
|
|
1832
|
+
if (engine.status === "loaded" && engine.manifest) {
|
|
1833
|
+
console.log(
|
|
1834
|
+
`${chalk9.green("loaded")} ${chalk9.cyan(engine.manifest.id)} (${engine.manifest.displayName}) from ${chalk9.yellow(engine.packageName)} [${engine.source}]`
|
|
1835
|
+
);
|
|
1836
|
+
continue;
|
|
1837
|
+
}
|
|
1838
|
+
console.log(
|
|
1839
|
+
`${chalk9.yellow("ignored")} ${chalk9.yellow(engine.packageName)} [${engine.source}] \u2014 ${engine.reason ?? "unknown reason"}`
|
|
1840
|
+
);
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
// src/commands/event.ts
|
|
1845
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
1846
|
+
import { parseEventV1 as parseEventV12, parseEventsBatch } from "@holdpoint/live-protocol";
|
|
1847
|
+
import { BridgeClient as BridgeClient2 } from "@holdpoint/sdk";
|
|
1848
|
+
function readStdin() {
|
|
1849
|
+
return readFileSync9(0, "utf8");
|
|
1850
|
+
}
|
|
1851
|
+
async function eventCommand(options) {
|
|
1852
|
+
const stdin = readStdin().trim();
|
|
1853
|
+
if (!stdin) {
|
|
1854
|
+
if (options.fromHook) {
|
|
1855
|
+
process.exit(0);
|
|
1856
|
+
}
|
|
1857
|
+
console.error("No JSON input received on stdin.");
|
|
1858
|
+
process.exit(3);
|
|
1859
|
+
}
|
|
1860
|
+
let raw;
|
|
1861
|
+
try {
|
|
1862
|
+
raw = JSON.parse(stdin);
|
|
1863
|
+
} catch {
|
|
1864
|
+
if (options.fromHook) {
|
|
1865
|
+
process.exit(0);
|
|
1866
|
+
}
|
|
1867
|
+
console.error("Invalid JSON input.");
|
|
1868
|
+
process.exit(3);
|
|
1869
|
+
}
|
|
1870
|
+
const client = new BridgeClient2();
|
|
1871
|
+
try {
|
|
1872
|
+
if (options.fromHook) {
|
|
1873
|
+
if (!options.engine) {
|
|
1874
|
+
process.exit(0);
|
|
1875
|
+
}
|
|
1876
|
+
const adapter = await loadLiveAdapter(options.engine);
|
|
1877
|
+
if (!adapter) {
|
|
1878
|
+
process.exit(0);
|
|
1879
|
+
}
|
|
1880
|
+
const event = adapter.translateHookInput(raw, { cwd: process.cwd() });
|
|
1881
|
+
if (!event) {
|
|
1882
|
+
process.exit(0);
|
|
1883
|
+
}
|
|
1884
|
+
await client.sendEvent(event);
|
|
1885
|
+
process.exit(0);
|
|
1886
|
+
}
|
|
1887
|
+
if (Array.isArray(raw)) {
|
|
1888
|
+
await client.sendEvents(parseEventsBatch(raw));
|
|
1889
|
+
} else {
|
|
1890
|
+
await client.sendEvent(parseEventV12(raw));
|
|
1891
|
+
}
|
|
1892
|
+
} catch (error) {
|
|
1893
|
+
console.error(error.message);
|
|
1894
|
+
process.exit(3);
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1160
1898
|
// src/index.ts
|
|
1161
1899
|
var program = new Command();
|
|
1162
|
-
program.name("holdpoint").description("Universal eval-guard for AI coding agents (alpha)").version(
|
|
1900
|
+
program.name("holdpoint").description("Universal eval-guard for AI coding agents (alpha)").version(CLI_VERSION).action(() => {
|
|
1901
|
+
void liveCommand();
|
|
1902
|
+
});
|
|
1163
1903
|
program.command("init").description("Initialise Holdpoint in the current project").option("--stack <stack>", "Stack type: typescript | python | nextjs | fullstack").option(
|
|
1164
1904
|
"--agent <agent>",
|
|
1165
1905
|
"Agent to install for: copilot | claude | cursor | codex (default: all four)"
|
|
@@ -1168,6 +1908,14 @@ program.command("check").description("Run task checks from checks.yaml").option(
|
|
|
1168
1908
|
program.command("validate").description("Validate checks.yaml schema and print any errors").action(validateCommand);
|
|
1169
1909
|
program.command("update").description("Regenerate engine files from current checks.yaml (preserves checks.yaml)").action(updateCommand);
|
|
1170
1910
|
program.command("builder").description("Open the visual builder UI on localhost:4321").action(buildCommand);
|
|
1911
|
+
program.command("live").description("Open the Holdpoint Live UI").option("--project <project>", "Open the UI focused on a specific project hash").action(liveCommand);
|
|
1912
|
+
var daemon = program.command("daemon").description("Manage the Holdpoint Live daemon");
|
|
1913
|
+
daemon.command("start").description("Start or connect to the singleton Holdpoint Live daemon").action(daemonStartCommand);
|
|
1914
|
+
daemon.command("status").description("Show Holdpoint Live daemon status").action(daemonStatusCommand);
|
|
1915
|
+
daemon.command("stop").description("Stop the running Holdpoint Live daemon").action(daemonStopCommand);
|
|
1916
|
+
program.command("event").description("Internal: read event JSON from stdin and publish it to Holdpoint Live").option("--engine <engine>", "Engine name when converting native hook payloads").option("--from-hook", "Interpret stdin as an engine-native hook payload").action(eventCommand);
|
|
1917
|
+
program.command("engines").description("List discovered Holdpoint Live adapter packages").option("--json", "Print machine-readable discovery output").action(enginesCommand);
|
|
1918
|
+
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);
|
|
1171
1919
|
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);
|
|
1172
1920
|
program.parse();
|
|
1173
1921
|
//# sourceMappingURL=index.js.map
|