git-daemon 0.1.8 → 0.1.10

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/git.js CHANGED
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.resolveRepoPath = exports.getRepoStatus = exports.fetchRepo = exports.cloneRepo = exports.RepoNotFoundError = void 0;
39
+ exports.resolveRepoPath = exports.listRepoBranches = exports.getRepoSummary = exports.getRepoStatus = exports.fetchRepo = exports.cloneRepo = exports.RepoNotFoundError = void 0;
40
40
  const path_1 = __importDefault(require("path"));
41
41
  const fs_1 = require("fs");
42
42
  const process_1 = require("./process");
@@ -79,6 +79,56 @@ const getRepoStatus = async (workspaceRoot, repoPath) => {
79
79
  return parseStatus(result.stdout);
80
80
  };
81
81
  exports.getRepoStatus = getRepoStatus;
82
+ const getRepoSummary = async (workspaceRoot, repoPath) => {
83
+ const resolved = await (0, exports.resolveRepoPath)(workspaceRoot, repoPath);
84
+ const { execa } = await Promise.resolve().then(() => __importStar(require("execa")));
85
+ const result = await execa("git", [
86
+ "-C",
87
+ resolved,
88
+ "status",
89
+ "--porcelain=2",
90
+ "-b",
91
+ ]);
92
+ const info = parseStatusInfo(result.stdout);
93
+ const dirty = info.stagedCount > 0 ||
94
+ info.unstagedCount > 0 ||
95
+ info.untrackedCount > 0 ||
96
+ info.conflictsCount > 0;
97
+ const upstream = info.detached ? null : info.upstream;
98
+ return {
99
+ repoPath,
100
+ exists: true,
101
+ branch: info.branch,
102
+ upstream,
103
+ ahead: upstream ? info.ahead : 0,
104
+ behind: upstream ? info.behind : 0,
105
+ dirty,
106
+ staged: info.stagedCount,
107
+ unstaged: info.unstagedCount,
108
+ untracked: info.untrackedCount,
109
+ conflicts: info.conflictsCount,
110
+ detached: info.detached,
111
+ };
112
+ };
113
+ exports.getRepoSummary = getRepoSummary;
114
+ const listRepoBranches = async (workspaceRoot, repoPath, options) => {
115
+ const resolved = await (0, exports.resolveRepoPath)(workspaceRoot, repoPath);
116
+ const includeRemote = options?.includeRemote ?? true;
117
+ const { execa } = await Promise.resolve().then(() => __importStar(require("execa")));
118
+ const refs = ["refs/heads"];
119
+ if (includeRemote) {
120
+ refs.push("refs/remotes");
121
+ }
122
+ const result = await execa("git", [
123
+ "-C",
124
+ resolved,
125
+ "for-each-ref",
126
+ "--format=%(refname)\t%(refname:short)\t%(objectname)\t%(HEAD)\t%(upstream:short)",
127
+ ...refs,
128
+ ]);
129
+ return parseBranches(result.stdout);
130
+ };
131
+ exports.listRepoBranches = listRepoBranches;
82
132
  const resolveRepoPath = async (workspaceRoot, repoPath) => {
83
133
  const resolved = await (0, workspace_1.resolveInsideWorkspace)(workspaceRoot, repoPath);
84
134
  await assertRepoExists(resolved);
@@ -99,7 +149,26 @@ const assertRepoExists = async (repoPath) => {
99
149
  }
100
150
  };
101
151
  const parseStatus = (output) => {
102
- let branch = "";
152
+ const info = parseStatusInfo(output);
153
+ const clean = info.stagedCount === 0 &&
154
+ info.unstagedCount === 0 &&
155
+ info.untrackedCount === 0 &&
156
+ info.conflictsCount === 0;
157
+ return {
158
+ branch: info.branch,
159
+ ahead: info.ahead,
160
+ behind: info.behind,
161
+ stagedCount: info.stagedCount,
162
+ unstagedCount: info.unstagedCount,
163
+ untrackedCount: info.untrackedCount,
164
+ conflictsCount: info.conflictsCount,
165
+ clean,
166
+ };
167
+ };
168
+ const parseStatusInfo = (output) => {
169
+ let head = "";
170
+ let oid = "";
171
+ let upstream = null;
103
172
  let ahead = 0;
104
173
  let behind = 0;
105
174
  let stagedCount = 0;
@@ -109,7 +178,16 @@ const parseStatus = (output) => {
109
178
  const lines = output.split(/\r?\n/);
110
179
  for (const line of lines) {
111
180
  if (line.startsWith("# branch.head")) {
112
- branch = line.split(" ").slice(2).join(" ").trim();
181
+ head = line.split(" ").slice(2).join(" ").trim();
182
+ continue;
183
+ }
184
+ if (line.startsWith("# branch.oid")) {
185
+ oid = line.split(" ").slice(2).join(" ").trim();
186
+ continue;
187
+ }
188
+ if (line.startsWith("# branch.upstream")) {
189
+ const value = line.split(" ").slice(2).join(" ").trim();
190
+ upstream = value.length > 0 ? value : null;
113
191
  continue;
114
192
  }
115
193
  if (line.startsWith("# branch.ab")) {
@@ -139,18 +217,49 @@ const parseStatus = (output) => {
139
217
  }
140
218
  }
141
219
  }
142
- const clean = stagedCount === 0 &&
143
- unstagedCount === 0 &&
144
- untrackedCount === 0 &&
145
- conflictsCount === 0;
220
+ const detached = head === "(detached)";
221
+ let branch = head;
222
+ if (detached) {
223
+ branch = oid || head;
224
+ upstream = null;
225
+ }
146
226
  return {
147
227
  branch,
228
+ upstream,
148
229
  ahead,
149
230
  behind,
150
231
  stagedCount,
151
232
  unstagedCount,
152
233
  untrackedCount,
153
234
  conflictsCount,
154
- clean,
235
+ detached,
155
236
  };
156
237
  };
238
+ const parseBranches = (output) => {
239
+ const branches = [];
240
+ const lines = output.split(/\r?\n/);
241
+ for (const line of lines) {
242
+ if (!line) {
243
+ continue;
244
+ }
245
+ const [fullName, shortName, , headFlag, upstream] = line.split("\t");
246
+ if (!fullName || !shortName) {
247
+ continue;
248
+ }
249
+ if (fullName.startsWith("refs/remotes/") && fullName.endsWith("/HEAD")) {
250
+ continue;
251
+ }
252
+ const type = fullName.startsWith("refs/heads/") ? "local" : "remote";
253
+ const branch = {
254
+ name: shortName,
255
+ fullName,
256
+ type,
257
+ current: headFlag === "*",
258
+ };
259
+ if (upstream) {
260
+ branch.upstream = upstream;
261
+ }
262
+ branches.push(branch);
263
+ }
264
+ return branches;
265
+ };
@@ -0,0 +1,425 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.HealthcheckResultStore = exports.runHealthchecks = exports.resolveSelections = exports.listHealthcheckSuites = exports.loadHealthcheckSuites = void 0;
7
+ const path_1 = __importDefault(require("path"));
8
+ const fs_1 = require("fs");
9
+ const execa_1 = require("execa");
10
+ const tree_kill_1 = __importDefault(require("tree-kill"));
11
+ const STATUS_ORDER = {
12
+ failed: 3,
13
+ "pass-partial": 2,
14
+ "pass-full": 1,
15
+ na: 0,
16
+ };
17
+ const IGNORED_DIRS = new Set(["node_modules", ".git", "dist", "logs"]);
18
+ const readJsonFile = async (filePath) => {
19
+ const raw = await fs_1.promises.readFile(filePath, "utf8");
20
+ return JSON.parse(raw);
21
+ };
22
+ const getString = (value) => typeof value === "string" && value.trim() ? value.trim() : null;
23
+ const getNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
24
+ const getBoolean = (value) => typeof value === "boolean" ? value : null;
25
+ const normalizeSuiteMeta = async (root) => {
26
+ const suitePath = path_1.default.join(root, "suite.json");
27
+ try {
28
+ const parsed = await readJsonFile(suitePath);
29
+ const id = getString(parsed.id) ?? path_1.default.basename(root);
30
+ const name = getString(parsed.name) ?? id;
31
+ return {
32
+ id,
33
+ name,
34
+ version: getString(parsed.version) ?? undefined,
35
+ description: getString(parsed.description) ?? undefined,
36
+ };
37
+ }
38
+ catch {
39
+ const id = path_1.default.basename(root);
40
+ return { id, name: id, version: undefined, description: undefined };
41
+ }
42
+ };
43
+ const collectManifests = async (root) => {
44
+ const results = [];
45
+ const walk = async (dir) => {
46
+ const entries = await fs_1.promises.readdir(dir, { withFileTypes: true });
47
+ for (const entry of entries) {
48
+ if (entry.name.startsWith(".") && entry.isDirectory()) {
49
+ continue;
50
+ }
51
+ const entryPath = path_1.default.join(dir, entry.name);
52
+ if (entry.isDirectory()) {
53
+ if (IGNORED_DIRS.has(entry.name)) {
54
+ continue;
55
+ }
56
+ await walk(entryPath);
57
+ continue;
58
+ }
59
+ if (entry.isFile() && entry.name === "healthcheck.json") {
60
+ results.push(entryPath);
61
+ }
62
+ }
63
+ };
64
+ await walk(root);
65
+ return results;
66
+ };
67
+ const parseManifest = async (manifestPath) => {
68
+ let parsed;
69
+ try {
70
+ parsed = await readJsonFile(manifestPath);
71
+ }
72
+ catch {
73
+ return null;
74
+ }
75
+ const id = getString(parsed.id);
76
+ const name = getString(parsed.name) ?? id;
77
+ const version = getString(parsed.version);
78
+ const description = getString(parsed.description) ?? "";
79
+ const entrypoint = getString(parsed.entrypoint);
80
+ if (!id || !name || !version || !entrypoint) {
81
+ return null;
82
+ }
83
+ const root = path_1.default.dirname(manifestPath);
84
+ const entrypointPath = path_1.default.resolve(root, entrypoint);
85
+ try {
86
+ await fs_1.promises.access(entrypointPath);
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ const argsValue = Array.isArray(parsed.args)
92
+ ? parsed.args.filter((item) => typeof item === "string")
93
+ : [];
94
+ const timeoutSeconds = getNumber(parsed.timeoutSeconds) ?? 60;
95
+ const cacheable = getBoolean(parsed.cacheable) ?? false;
96
+ const cacheMaxAgeSeconds = getNumber(parsed.cacheMaxAgeSeconds) ?? undefined;
97
+ return {
98
+ id,
99
+ name,
100
+ version,
101
+ description,
102
+ entrypointPath,
103
+ args: argsValue,
104
+ timeoutSeconds,
105
+ cacheable,
106
+ cacheMaxAgeSeconds,
107
+ permissions: parsed.permissions,
108
+ configSchema: parsed.configSchema,
109
+ outputSchemaVersion: getNumber(parsed.outputSchemaVersion) ?? undefined,
110
+ root,
111
+ };
112
+ };
113
+ const loadHealthcheckSuites = async (configDir, suitePaths) => {
114
+ const suites = [];
115
+ for (const suitePath of suitePaths) {
116
+ const resolved = path_1.default.isAbsolute(suitePath)
117
+ ? suitePath
118
+ : path_1.default.resolve(configDir, suitePath);
119
+ let stats;
120
+ try {
121
+ stats = await fs_1.promises.stat(resolved);
122
+ }
123
+ catch {
124
+ continue;
125
+ }
126
+ if (!stats.isDirectory()) {
127
+ continue;
128
+ }
129
+ const meta = await normalizeSuiteMeta(resolved);
130
+ const manifestPaths = await collectManifests(resolved);
131
+ const checks = [];
132
+ for (const manifestPath of manifestPaths) {
133
+ const manifest = await parseManifest(manifestPath);
134
+ if (manifest) {
135
+ checks.push(manifest);
136
+ }
137
+ }
138
+ suites.push({
139
+ ...meta,
140
+ root: resolved,
141
+ checks,
142
+ });
143
+ }
144
+ return suites;
145
+ };
146
+ exports.loadHealthcheckSuites = loadHealthcheckSuites;
147
+ const listHealthcheckSuites = (suites) => suites.map((suite) => ({
148
+ id: suite.id,
149
+ name: suite.name,
150
+ version: suite.version,
151
+ description: suite.description,
152
+ checks: suite.checks.map((check) => ({
153
+ id: check.id,
154
+ name: check.name,
155
+ version: check.version,
156
+ description: check.description,
157
+ timeoutSeconds: check.timeoutSeconds,
158
+ cacheable: check.cacheable,
159
+ cacheMaxAgeSeconds: check.cacheMaxAgeSeconds,
160
+ permissions: check.permissions,
161
+ configSchema: check.configSchema,
162
+ outputSchemaVersion: check.outputSchemaVersion,
163
+ })),
164
+ }));
165
+ exports.listHealthcheckSuites = listHealthcheckSuites;
166
+ const resolveSelections = (suites, request, defaultSuiteId) => {
167
+ if (suites.length === 0) {
168
+ throw new Error("No healthcheck suites configured.");
169
+ }
170
+ const suiteId = request.suiteId ??
171
+ defaultSuiteId ??
172
+ (suites.length === 1 ? suites[0].id : null);
173
+ const suiteById = new Map(suites.map((suite) => [suite.id, suite]));
174
+ const selections = [];
175
+ if (request.checks && request.checks.length > 0) {
176
+ for (const selection of request.checks) {
177
+ const targetSuiteId = selection.suiteId ?? suiteId;
178
+ if (!targetSuiteId) {
179
+ throw new Error("Suite id is required to select healthchecks.");
180
+ }
181
+ const suite = suiteById.get(targetSuiteId);
182
+ if (!suite) {
183
+ throw new Error(`Healthcheck suite not found: ${targetSuiteId}.`);
184
+ }
185
+ const check = suite.checks.find((item) => item.id === selection.checkId);
186
+ if (!check) {
187
+ throw new Error(`Healthcheck not found: ${targetSuiteId}/${selection.checkId}.`);
188
+ }
189
+ selections.push({
190
+ suiteId: targetSuiteId,
191
+ check,
192
+ config: selection.config,
193
+ });
194
+ }
195
+ }
196
+ else {
197
+ if (!suiteId) {
198
+ throw new Error("Suite id is required to run healthchecks.");
199
+ }
200
+ const suite = suiteById.get(suiteId);
201
+ if (!suite) {
202
+ throw new Error(`Healthcheck suite not found: ${suiteId}.`);
203
+ }
204
+ for (const check of suite.checks) {
205
+ selections.push({ suiteId, check });
206
+ }
207
+ }
208
+ return selections;
209
+ };
210
+ exports.resolveSelections = resolveSelections;
211
+ const attachLineReader = (stream, onLine) => {
212
+ if (!stream) {
213
+ return;
214
+ }
215
+ let buffer = "";
216
+ stream.on("data", (chunk) => {
217
+ buffer += chunk.toString();
218
+ const lines = buffer.split(/\r?\n/);
219
+ buffer = lines.pop() ?? "";
220
+ for (const line of lines) {
221
+ if (line.length > 0) {
222
+ onLine(line);
223
+ }
224
+ }
225
+ });
226
+ stream.on("end", () => {
227
+ if (buffer.length > 0) {
228
+ onLine(buffer);
229
+ }
230
+ });
231
+ };
232
+ const parseOutput = (stdout) => {
233
+ const trimmed = stdout.trim();
234
+ if (!trimmed) {
235
+ return { error: "No output provided." };
236
+ }
237
+ const attempt = (value) => {
238
+ try {
239
+ return { value: JSON.parse(value) };
240
+ }
241
+ catch (err) {
242
+ return {
243
+ error: err instanceof Error ? err.message : "Invalid JSON output.",
244
+ };
245
+ }
246
+ };
247
+ const first = attempt(trimmed);
248
+ if (!first.error) {
249
+ return first;
250
+ }
251
+ const lines = trimmed.split(/\r?\n/).filter(Boolean);
252
+ if (lines.length > 1) {
253
+ return attempt(lines[lines.length - 1]);
254
+ }
255
+ return first;
256
+ };
257
+ const normalizeStatus = (value) => {
258
+ if (value === "na" ||
259
+ value === "failed" ||
260
+ value === "pass-partial" ||
261
+ value === "pass-full") {
262
+ return value;
263
+ }
264
+ return null;
265
+ };
266
+ const buildFailureResult = (suiteId, checkId, summary, explanation, details) => ({
267
+ suiteId,
268
+ checkId,
269
+ status: "failed",
270
+ summary,
271
+ explanation,
272
+ details,
273
+ cached: false,
274
+ });
275
+ const runCheck = async (ctx, selection, repoPath, repoInfo) => {
276
+ const { check, suiteId } = selection;
277
+ const start = Date.now();
278
+ ctx.progress({
279
+ kind: "healthcheck",
280
+ detail: `${check.id}: running`,
281
+ });
282
+ const env = {
283
+ ...process.env,
284
+ REPO_PATH: repoPath,
285
+ };
286
+ if (repoInfo) {
287
+ env.REPO_INFO = JSON.stringify(repoInfo);
288
+ }
289
+ if (selection.config !== undefined) {
290
+ env.CHECK_CONFIG = JSON.stringify(selection.config);
291
+ }
292
+ const subprocess = (0, execa_1.execa)(check.entrypointPath, check.args, {
293
+ cwd: repoPath,
294
+ env,
295
+ timeout: check.timeoutSeconds * 1000,
296
+ stdout: "pipe",
297
+ stderr: "pipe",
298
+ });
299
+ const pid = subprocess.pid;
300
+ if (pid) {
301
+ ctx.setCancel(() => new Promise((resolve) => {
302
+ (0, tree_kill_1.default)(pid, "SIGTERM", () => resolve());
303
+ }));
304
+ }
305
+ attachLineReader(subprocess.stderr, ctx.logStderr);
306
+ let stdout = "";
307
+ if (subprocess.stdout) {
308
+ subprocess.stdout.on("data", (chunk) => {
309
+ stdout += chunk.toString();
310
+ });
311
+ }
312
+ try {
313
+ await subprocess;
314
+ }
315
+ catch (err) {
316
+ const message = err instanceof Error ? err.message : "Healthcheck failed.";
317
+ if (err.stdout) {
318
+ stdout += String(err.stdout);
319
+ }
320
+ return buildFailureResult(suiteId, check.id, "Healthcheck failed to run", message);
321
+ }
322
+ const parsed = parseOutput(stdout);
323
+ if (parsed.error || !parsed.value) {
324
+ return buildFailureResult(suiteId, check.id, "Invalid healthcheck output", parsed.error ?? "Unable to parse healthcheck output.");
325
+ }
326
+ const status = normalizeStatus(parsed.value.status);
327
+ const summary = getString(parsed.value.summary);
328
+ const explanation = getString(parsed.value.explanation);
329
+ if (!status || !summary || !explanation) {
330
+ return buildFailureResult(suiteId, check.id, "Invalid healthcheck output", "Healthcheck output is missing required fields.");
331
+ }
332
+ const details = Array.isArray(parsed.value.details)
333
+ ? parsed.value.details
334
+ : undefined;
335
+ const metrics = parsed.value.metrics && typeof parsed.value.metrics === "object"
336
+ ? parsed.value.metrics
337
+ : undefined;
338
+ const artifacts = Array.isArray(parsed.value.artifacts)
339
+ ? parsed.value.artifacts
340
+ : undefined;
341
+ const durationMs = getNumber(parsed.value.durationMs) ?? Math.max(0, Date.now() - start);
342
+ return {
343
+ suiteId,
344
+ checkId: check.id,
345
+ status,
346
+ summary,
347
+ explanation,
348
+ details,
349
+ metrics,
350
+ artifacts,
351
+ durationMs,
352
+ cached: false,
353
+ };
354
+ };
355
+ const summarize = (checks) => {
356
+ const counts = {
357
+ failed: 0,
358
+ "pass-partial": 0,
359
+ "pass-full": 0,
360
+ na: 0,
361
+ };
362
+ for (const check of checks) {
363
+ counts[check.status] += 1;
364
+ }
365
+ return `failed ${counts.failed}, partial ${counts["pass-partial"]}, full ${counts["pass-full"]}, na ${counts.na}`;
366
+ };
367
+ const aggregateStatus = (checks) => {
368
+ let winner = "na";
369
+ for (const check of checks) {
370
+ if (STATUS_ORDER[check.status] > STATUS_ORDER[winner]) {
371
+ winner = check.status;
372
+ }
373
+ }
374
+ return winner;
375
+ };
376
+ const runHealthchecks = async (ctx, jobId, repoPath, selections, repoInfo) => {
377
+ const startedAt = new Date().toISOString();
378
+ const results = [];
379
+ for (const selection of selections) {
380
+ if (ctx.isCancelled()) {
381
+ break;
382
+ }
383
+ const result = await runCheck(ctx, selection, repoPath, repoInfo);
384
+ results.push(result);
385
+ ctx.progress({
386
+ kind: "healthcheck",
387
+ detail: `${selection.check.id}: ${result.status}`,
388
+ });
389
+ }
390
+ const finishedAt = new Date().toISOString();
391
+ const status = aggregateStatus(results);
392
+ return {
393
+ jobId,
394
+ repoPath,
395
+ status,
396
+ summary: summarize(results),
397
+ checks: results,
398
+ startedAt,
399
+ finishedAt,
400
+ };
401
+ };
402
+ exports.runHealthchecks = runHealthchecks;
403
+ class HealthcheckResultStore {
404
+ constructor(maxEntries = 200) {
405
+ this.results = new Map();
406
+ this.order = [];
407
+ this.maxEntries = maxEntries;
408
+ }
409
+ get(jobId) {
410
+ return this.results.get(jobId);
411
+ }
412
+ set(jobId, result) {
413
+ if (!this.results.has(jobId)) {
414
+ this.order.push(jobId);
415
+ }
416
+ this.results.set(jobId, result);
417
+ while (this.order.length > this.maxEntries) {
418
+ const oldest = this.order.shift();
419
+ if (oldest) {
420
+ this.results.delete(oldest);
421
+ }
422
+ }
423
+ }
424
+ }
425
+ exports.HealthcheckResultStore = HealthcheckResultStore;
package/dist/jobs.js CHANGED
@@ -124,6 +124,7 @@ class JobManager {
124
124
  }, this.timeoutMs);
125
125
  }
126
126
  const ctx = {
127
+ jobId: job.id,
127
128
  logStdout: (line) => job.emit({ type: "log", stream: "stdout", line }),
128
129
  logStderr: (line) => job.emit({ type: "log", stream: "stderr", line }),
129
130
  progress: (event) => job.emit({ type: "progress", ...event }),
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.depsInstallRequestSchema = exports.osOpenRequestSchema = exports.gitStatusQuerySchema = exports.gitFetchRequestSchema = exports.gitCloneRequestSchema = exports.pairRequestSchema = void 0;
3
+ exports.healthcheckRunRequestSchema = exports.depsInstallRequestSchema = exports.osOpenRequestSchema = exports.gitBranchesQuerySchema = exports.gitSummaryQuerySchema = exports.gitStatusQuerySchema = exports.gitFetchRequestSchema = exports.gitCloneRequestSchema = exports.pairRequestSchema = void 0;
4
4
  const zod_1 = require("zod");
5
5
  const MAX_PATH_LENGTH = 4096;
6
6
  const isValidRepoUrl = (value) => {
@@ -50,6 +50,13 @@ exports.gitFetchRequestSchema = zod_1.z.object({
50
50
  exports.gitStatusQuerySchema = zod_1.z.object({
51
51
  repoPath: zod_1.z.string().min(1).max(MAX_PATH_LENGTH),
52
52
  });
53
+ exports.gitSummaryQuerySchema = zod_1.z.object({
54
+ repoPath: zod_1.z.string().min(1).max(MAX_PATH_LENGTH),
55
+ });
56
+ exports.gitBranchesQuerySchema = zod_1.z.object({
57
+ repoPath: zod_1.z.string().min(1).max(MAX_PATH_LENGTH),
58
+ includeRemote: zod_1.z.enum(["true", "false"]).optional(),
59
+ });
53
60
  exports.osOpenRequestSchema = zod_1.z.object({
54
61
  target: zod_1.z.enum(["folder", "terminal", "vscode"]),
55
62
  path: zod_1.z.string().min(1).max(MAX_PATH_LENGTH),
@@ -60,3 +67,15 @@ exports.depsInstallRequestSchema = zod_1.z.object({
60
67
  mode: zod_1.z.enum(["auto", "ci", "install"]).optional(),
61
68
  safer: zod_1.z.boolean().optional(),
62
69
  });
70
+ const healthcheckSelectionSchema = zod_1.z.object({
71
+ suiteId: zod_1.z.string().min(1).optional(),
72
+ checkId: zod_1.z.string().min(1),
73
+ config: zod_1.z.record(zod_1.z.unknown()).optional(),
74
+ cacheMode: zod_1.z.enum(["prefer", "refresh", "bypass"]).optional(),
75
+ });
76
+ exports.healthcheckRunRequestSchema = zod_1.z.object({
77
+ repoPath: zod_1.z.string().min(1).max(MAX_PATH_LENGTH),
78
+ repoInfo: zod_1.z.record(zod_1.z.unknown()).optional(),
79
+ suiteId: zod_1.z.string().min(1).optional(),
80
+ checks: zod_1.z.array(healthcheckSelectionSchema).optional(),
81
+ });
@@ -0,0 +1,10 @@
1
+ # Example Healthchecks
2
+
3
+ This suite includes two sample checks:
4
+
5
+ - `about-description`: Verify the GitHub repo description matches the first
6
+ sentence of the README (ignores headings and badges).
7
+ - `package-manager`: If a repo has `package.json`, require a `packageManager`
8
+ field.
9
+
10
+ Each check is defined by a `healthcheck.json` manifest and a `run.js` entrypoint.
@@ -0,0 +1,17 @@
1
+ {
2
+ "id": "about-description",
3
+ "name": "About description matches README",
4
+ "version": "1.0.0",
5
+ "description": "Checks that the GitHub description matches the first README sentence.",
6
+ "entrypoint": "./run.js",
7
+ "args": [],
8
+ "timeoutSeconds": 30,
9
+ "permissions": {
10
+ "repoRead": true,
11
+ "network": false,
12
+ "secrets": []
13
+ },
14
+ "cacheable": true,
15
+ "cacheMaxAgeSeconds": 3600,
16
+ "outputSchemaVersion": 1
17
+ }