git-daemon 0.1.10 → 0.1.12

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/README.md CHANGED
@@ -19,7 +19,9 @@ Git Daemon is a local Node.js service that exposes a small, authenticated HTTP A
19
19
  Healthchecks are user-supplied executables/scripts stored in local suites (which can be
20
20
  private repos). The daemon runs them as jobs, streams logs over SSE, and returns a
21
21
  normalized status (`na`, `failed`, `pass-partial`, `pass-full`) plus details. Suites are
22
- configured via local paths, and each check has a `healthcheck.json` manifest.
22
+ configured via local paths, and each check has a `healthcheck.json` manifest. For a
23
+ quick demo, set `healthchecks.demo: true` in config to auto-enable the bundled example
24
+ suite (when present).
23
25
 
24
26
  ## Security model (high level)
25
27
 
@@ -197,6 +199,14 @@ curl -H "Origin: https://app.example.com" \
197
199
  http://127.0.0.1:8790/v1/healthchecks/jobs/<JOB_ID>/result
198
200
  ```
199
201
 
202
+ List healthchecks (flat):
203
+
204
+ ```bash
205
+ curl -H "Origin: https://app.example.com" \
206
+ -H "Authorization: Bearer <TOKEN>" \
207
+ "http://127.0.0.1:8790/v1/healthchecks?flat=true"
208
+ ```
209
+
200
210
  ## Configuration
201
211
 
202
212
  Config is stored in OS-specific directories:
@@ -218,6 +228,7 @@ Key settings live in `config.json`:
218
228
  - `deps.defaultSafer`: defaults to `true` for `--ignore-scripts`
219
229
  - `jobs.maxConcurrent` and `jobs.timeoutSeconds`
220
230
  - `healthchecks.suites`: array of absolute or config-relative suite paths
231
+ - `healthchecks.demo`: enable bundled example healthchecks if present
221
232
 
222
233
  Tokens are stored (hashed) in `tokens.json`. Logs are written under the configured `logging.directory` with rotation.
223
234
 
@@ -140,6 +140,11 @@
140
140
  },
141
141
  "defaultSuiteId": {
142
142
  "type": "string"
143
+ },
144
+ "demo": {
145
+ "type": "boolean",
146
+ "default": false,
147
+ "description": "When true, auto-enables the bundled example healthchecks if present."
143
148
  }
144
149
  }
145
150
  },
package/design.md CHANGED
@@ -430,7 +430,7 @@ Recommendations:
430
430
 
431
431
  ### Healthcheck endpoints
432
432
 
433
- * `GET /v1/healthchecks` → list installed suites and checks.
433
+ * `GET /v1/healthchecks` → list installed suites and checks (use `?flat=true` for a flattened list).
434
434
  * `POST /v1/healthchecks/run` → job
435
435
 
436
436
  * body: `{ repoPath, repoInfo?, suiteId?, checks?: [{ suiteId?, checkId, config?, cacheMode? }] }`
package/dist/config.js CHANGED
@@ -47,6 +47,7 @@ const defaultConfig = () => ({
47
47
  },
48
48
  healthchecks: {
49
49
  suites: [],
50
+ demo: false,
50
51
  },
51
52
  logging: {
52
53
  directory: "logs",
@@ -68,7 +69,7 @@ const loadConfig = async (configDir) => {
68
69
  try {
69
70
  const raw = await fs_1.promises.readFile(configPath, "utf8");
70
71
  const data = JSON.parse(raw);
71
- return {
72
+ const merged = {
72
73
  ...(0, exports.defaultConfig)(),
73
74
  ...data,
74
75
  server: {
@@ -107,6 +108,22 @@ const loadConfig = async (configDir) => {
107
108
  entries: data.approvals?.entries ?? [],
108
109
  },
109
110
  };
111
+ if (merged.healthchecks?.demo) {
112
+ const demoSuitePath = path_1.default.resolve(__dirname, "..", "healthchecks", "example-suite");
113
+ try {
114
+ await fs_1.promises.access(demoSuitePath);
115
+ if (!merged.healthchecks.suites.includes(demoSuitePath)) {
116
+ merged.healthchecks.suites = [
117
+ ...merged.healthchecks.suites,
118
+ demoSuitePath,
119
+ ];
120
+ }
121
+ }
122
+ catch {
123
+ // Ignore missing demo suite; config can still load.
124
+ }
125
+ }
126
+ return merged;
110
127
  }
111
128
  catch (err) {
112
129
  if (err.code !== "ENOENT") {
@@ -155,10 +155,16 @@ const description =
155
155
 
156
156
  if (!description) {
157
157
  emit({
158
- status: "na",
158
+ status: "failed",
159
159
  summary: "Missing repo description",
160
- explanation:
161
- "REPO_INFO.description is missing, so there is nothing to compare.",
160
+ explanation: "REPO_INFO.description is required for this healthcheck.",
161
+ details: [
162
+ {
163
+ code: "ABOUT_MISSING",
164
+ severity: "high",
165
+ message: "Provide a GitHub repo description to match the README.",
166
+ },
167
+ ],
162
168
  });
163
169
  process.exit(0);
164
170
  }
@@ -166,9 +172,16 @@ if (!description) {
166
172
  const readmePath = findReadme(repoPath);
167
173
  if (!readmePath) {
168
174
  emit({
169
- status: "na",
175
+ status: "failed",
170
176
  summary: "README not found",
171
- explanation: "No README file was found in the repo root.",
177
+ explanation: "A README is required to compare against the description.",
178
+ details: [
179
+ {
180
+ code: "README_MISSING",
181
+ severity: "high",
182
+ message: "Add a README with a clear first sentence.",
183
+ },
184
+ ],
172
185
  });
173
186
  process.exit(0);
174
187
  }
@@ -195,10 +208,18 @@ try {
195
208
  const firstSentence = extractFirstSentence(readmeText);
196
209
  if (!firstSentence) {
197
210
  emit({
198
- status: "na",
199
- summary: "README has no readable paragraph",
211
+ status: "failed",
212
+ summary: "README missing a first sentence",
200
213
  explanation:
201
214
  "Could not locate non-title text to compare against the description.",
215
+ details: [
216
+ {
217
+ code: "README_SENTENCE_MISSING",
218
+ severity: "high",
219
+ message:
220
+ "Add a first paragraph with a clear first sentence after the title.",
221
+ },
222
+ ],
202
223
  });
203
224
  process.exit(0);
204
225
  }
package/openapi.yaml CHANGED
@@ -321,13 +321,16 @@ paths:
321
321
  summary: List available healthchecks
322
322
  parameters:
323
323
  - $ref: '#/components/parameters/OriginHeader'
324
+ - $ref: '#/components/parameters/HealthcheckFlat'
324
325
  responses:
325
326
  '200':
326
327
  description: Healthcheck catalog
327
328
  content:
328
329
  application/json:
329
330
  schema:
330
- $ref: '#/components/schemas/HealthcheckCatalogResponse'
331
+ oneOf:
332
+ - $ref: '#/components/schemas/HealthcheckCatalogResponse'
333
+ - $ref: '#/components/schemas/HealthcheckFlatResponse'
331
334
  '401':
332
335
  $ref: '#/components/responses/Unauthorized'
333
336
  '403':
@@ -438,6 +441,14 @@ components:
438
441
  schema:
439
442
  type: boolean
440
443
  default: true
444
+ HealthcheckFlat:
445
+ name: flat
446
+ in: query
447
+ required: false
448
+ schema:
449
+ type: boolean
450
+ default: false
451
+ description: Return a flattened list of checks instead of suites.
441
452
  schemas:
442
453
  MetaResponse:
443
454
  type: object
@@ -790,6 +801,15 @@ components:
790
801
  type: array
791
802
  items:
792
803
  $ref: '#/components/schemas/HealthcheckSuite'
804
+ HealthcheckFlatResponse:
805
+ type: object
806
+ required:
807
+ - checks
808
+ properties:
809
+ checks:
810
+ type: array
811
+ items:
812
+ $ref: '#/components/schemas/HealthcheckFlatCheck'
793
813
  HealthcheckSuite:
794
814
  type: object
795
815
  required:
@@ -843,6 +863,43 @@ components:
843
863
  additionalProperties: true
844
864
  outputSchemaVersion:
845
865
  type: integer
866
+ HealthcheckFlatCheck:
867
+ type: object
868
+ required:
869
+ - suiteId
870
+ - checkId
871
+ - name
872
+ - version
873
+ - description
874
+ - timeoutSeconds
875
+ - cacheable
876
+ properties:
877
+ suiteId:
878
+ type: string
879
+ checkId:
880
+ type: string
881
+ name:
882
+ type: string
883
+ version:
884
+ type: string
885
+ description:
886
+ type: string
887
+ timeoutSeconds:
888
+ type: integer
889
+ minimum: 1
890
+ cacheable:
891
+ type: boolean
892
+ cacheMaxAgeSeconds:
893
+ type: integer
894
+ minimum: 1
895
+ permissions:
896
+ $ref: '#/components/schemas/HealthcheckPermissions'
897
+ configSchema:
898
+ type: object
899
+ description: JSON schema for check config.
900
+ additionalProperties: true
901
+ outputSchemaVersion:
902
+ type: integer
846
903
  HealthcheckPermissions:
847
904
  type: object
848
905
  properties:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-daemon",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "private": false,
5
5
  "type": "commonjs",
6
6
  "main": "dist/daemon.js",
@@ -8,6 +8,8 @@
8
8
  "build": "tsc",
9
9
  "daemon": "tsx src/daemon.ts",
10
10
  "daemon:setup": "tsx src/setup.ts",
11
+ "typecheck": "tsc --noEmit",
12
+ "verify": "npm run lint && npm run typecheck && npm test",
11
13
  "test": "vitest run",
12
14
  "test:watch": "vitest",
13
15
  "test:clone": "bash scripts/test-clone.sh",
package/src/app.ts CHANGED
@@ -54,6 +54,7 @@ import { installDeps } from "./deps";
54
54
  import { openTarget } from "./os";
55
55
  import {
56
56
  HealthcheckResultStore,
57
+ flattenHealthchecks,
57
58
  listHealthcheckSuites,
58
59
  loadHealthcheckSuites,
59
60
  resolveSelections,
@@ -530,6 +531,11 @@ export const createApp = (ctx: DaemonContext) => {
530
531
  ctx.configDir,
531
532
  ctx.config.healthchecks?.suites ?? [],
532
533
  );
534
+ const flat = _req.query.flat === "true";
535
+ if (flat) {
536
+ res.json({ checks: flattenHealthchecks(suites) });
537
+ return;
538
+ }
533
539
  res.json({ suites: listHealthcheckSuites(suites) });
534
540
  } catch (err) {
535
541
  next(err);
package/src/config.ts CHANGED
@@ -46,6 +46,7 @@ export const defaultConfig = (): AppConfig => ({
46
46
  },
47
47
  healthchecks: {
48
48
  suites: [],
49
+ demo: false,
49
50
  },
50
51
  logging: {
51
52
  directory: "logs",
@@ -67,7 +68,7 @@ export const loadConfig = async (configDir: string): Promise<AppConfig> => {
67
68
  try {
68
69
  const raw = await fs.readFile(configPath, "utf8");
69
70
  const data = JSON.parse(raw) as AppConfig;
70
- return {
71
+ const merged: AppConfig = {
71
72
  ...defaultConfig(),
72
73
  ...data,
73
74
  server: {
@@ -106,6 +107,28 @@ export const loadConfig = async (configDir: string): Promise<AppConfig> => {
106
107
  entries: data.approvals?.entries ?? [],
107
108
  },
108
109
  };
110
+
111
+ if (merged.healthchecks?.demo) {
112
+ const demoSuitePath = path.resolve(
113
+ __dirname,
114
+ "..",
115
+ "healthchecks",
116
+ "example-suite",
117
+ );
118
+ try {
119
+ await fs.access(demoSuitePath);
120
+ if (!merged.healthchecks.suites.includes(demoSuitePath)) {
121
+ merged.healthchecks.suites = [
122
+ ...merged.healthchecks.suites,
123
+ demoSuitePath,
124
+ ];
125
+ }
126
+ } catch {
127
+ // Ignore missing demo suite; config can still load.
128
+ }
129
+ }
130
+
131
+ return merged;
109
132
  } catch (err) {
110
133
  if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
111
134
  throw err;
package/src/daemon.ts CHANGED
@@ -15,6 +15,7 @@ const start = async () => {
15
15
  port: ctx.config.server.port,
16
16
  workspaceRoot: ctx.config.workspaceRoot ?? "not configured",
17
17
  originAllowlist: ctx.config.originAllowlist,
18
+ healthchecksDemo: ctx.config.healthchecks?.demo ?? false,
18
19
  };
19
20
  ctx.logger.info(startupSummary, "Git Daemon starting");
20
21
  if (process.env.GIT_DAEMON_LOG_STDOUT !== "1") {
@@ -26,6 +27,9 @@ const start = async () => {
26
27
  console.log(
27
28
  ` allowlist: ${startupSummary.originAllowlist.join(", ") || "none"}`,
28
29
  );
30
+ if (startupSummary.healthchecksDemo) {
31
+ console.log(" healthchecks: demo mode enabled");
32
+ }
29
33
  const httpsConfig = ctx.config.server.https;
30
34
  if (httpsConfig?.enabled) {
31
35
  console.log(
@@ -279,6 +279,23 @@ export const listHealthcheckSuites = (suites: HealthcheckSuite[]) =>
279
279
  })),
280
280
  }));
281
281
 
282
+ export const flattenHealthchecks = (suites: HealthcheckSuite[]) =>
283
+ suites.flatMap((suite) =>
284
+ suite.checks.map((check) => ({
285
+ suiteId: suite.id,
286
+ checkId: check.id,
287
+ name: check.name,
288
+ version: check.version,
289
+ description: check.description,
290
+ timeoutSeconds: check.timeoutSeconds,
291
+ cacheable: check.cacheable,
292
+ cacheMaxAgeSeconds: check.cacheMaxAgeSeconds,
293
+ permissions: check.permissions,
294
+ configSchema: check.configSchema,
295
+ outputSchemaVersion: check.outputSchemaVersion,
296
+ })),
297
+ );
298
+
282
299
  export const resolveSelections = (
283
300
  suites: HealthcheckSuite[],
284
301
  request: HealthcheckRunRequest,
package/src/types.ts CHANGED
@@ -34,6 +34,7 @@ export type AppConfig = {
34
34
  healthchecks?: {
35
35
  suites: string[];
36
36
  defaultSuiteId?: string;
37
+ demo?: boolean;
37
38
  };
38
39
  logging: {
39
40
  directory: string;