git-daemon 0.1.9 → 0.1.11

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.
@@ -0,0 +1,241 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+
7
+ const start = Date.now();
8
+
9
+ const emit = ({
10
+ status,
11
+ summary,
12
+ explanation,
13
+ details = [],
14
+ metrics = {},
15
+ artifacts = [],
16
+ }) => {
17
+ const payload = {
18
+ status,
19
+ summary,
20
+ explanation,
21
+ details,
22
+ metrics,
23
+ artifacts,
24
+ durationMs: Date.now() - start,
25
+ };
26
+ process.stdout.write(JSON.stringify(payload));
27
+ };
28
+
29
+ const parseJsonEnv = (name) => {
30
+ const raw = process.env[name];
31
+ if (!raw) {
32
+ return { value: null };
33
+ }
34
+ try {
35
+ return { value: JSON.parse(raw) };
36
+ } catch (err) {
37
+ return { error: err instanceof Error ? err.message : "Invalid JSON" };
38
+ }
39
+ };
40
+
41
+ const findReadme = (repoPath) => {
42
+ const candidates = [
43
+ "README.md",
44
+ "README.MD",
45
+ "README.markdown",
46
+ "README.txt",
47
+ ];
48
+ for (const name of candidates) {
49
+ const target = path.join(repoPath, name);
50
+ if (fs.existsSync(target)) {
51
+ return target;
52
+ }
53
+ }
54
+ return null;
55
+ };
56
+
57
+ const isBadgeLine = (line) =>
58
+ /^\s*\[!\[.*\]\(.*\)\]\(.*\)\s*$/.test(line) ||
59
+ /^\s*!\[.*\]\(.*\)\s*$/.test(line) ||
60
+ /^\s*<img\b/i.test(line);
61
+
62
+ const stripMarkdown = (text) =>
63
+ text
64
+ .replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1")
65
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
66
+ .replace(/`([^`]*)`/g, "$1")
67
+ .replace(/[*_~]/g, "")
68
+ .replace(/<[^>]+>/g, "");
69
+
70
+ const normalize = (text) =>
71
+ stripMarkdown(text)
72
+ .replace(/\s+/g, " ")
73
+ .trim()
74
+ .replace(/[.!?]+$/, "")
75
+ .toLowerCase();
76
+
77
+ const extractFirstSentence = (readme) => {
78
+ const lines = readme.split(/\r?\n/);
79
+ const paragraph = [];
80
+ let inCodeBlock = false;
81
+
82
+ for (const line of lines) {
83
+ const trimmed = line.trim();
84
+ if (trimmed.startsWith("```")) {
85
+ inCodeBlock = !inCodeBlock;
86
+ continue;
87
+ }
88
+ if (inCodeBlock) {
89
+ continue;
90
+ }
91
+ if (!trimmed) {
92
+ if (paragraph.length > 0) {
93
+ break;
94
+ }
95
+ continue;
96
+ }
97
+ if (trimmed.startsWith("#") || trimmed.startsWith(">")) {
98
+ continue;
99
+ }
100
+ if (isBadgeLine(trimmed)) {
101
+ continue;
102
+ }
103
+ if (trimmed.startsWith("<!--")) {
104
+ continue;
105
+ }
106
+ paragraph.push(trimmed);
107
+ }
108
+
109
+ if (paragraph.length === 0) {
110
+ return null;
111
+ }
112
+ const text = paragraph.join(" ");
113
+ const match = text.match(/^(.+?[.!?])(?:\s|$)/);
114
+ return match ? match[1] : text;
115
+ };
116
+
117
+ const repoPath = process.env.REPO_PATH;
118
+ if (!repoPath) {
119
+ emit({
120
+ status: "failed",
121
+ summary: "Missing repo path",
122
+ explanation: "REPO_PATH environment variable is required.",
123
+ details: [
124
+ {
125
+ code: "MISSING_REPO_PATH",
126
+ severity: "high",
127
+ message: "REPO_PATH environment variable was not provided.",
128
+ },
129
+ ],
130
+ });
131
+ process.exit(0);
132
+ }
133
+
134
+ const repoInfo = parseJsonEnv("REPO_INFO");
135
+ if (repoInfo.error) {
136
+ emit({
137
+ status: "failed",
138
+ summary: "Invalid repo info",
139
+ explanation: "REPO_INFO must be valid JSON.",
140
+ details: [
141
+ {
142
+ code: "INVALID_REPO_INFO",
143
+ severity: "high",
144
+ message: repoInfo.error,
145
+ },
146
+ ],
147
+ });
148
+ process.exit(0);
149
+ }
150
+
151
+ const description =
152
+ repoInfo.value && typeof repoInfo.value.description === "string"
153
+ ? repoInfo.value.description.trim()
154
+ : "";
155
+
156
+ if (!description) {
157
+ emit({
158
+ status: "na",
159
+ summary: "Missing repo description",
160
+ explanation:
161
+ "REPO_INFO.description is missing, so there is nothing to compare.",
162
+ });
163
+ process.exit(0);
164
+ }
165
+
166
+ const readmePath = findReadme(repoPath);
167
+ if (!readmePath) {
168
+ emit({
169
+ status: "na",
170
+ summary: "README not found",
171
+ explanation: "No README file was found in the repo root.",
172
+ });
173
+ process.exit(0);
174
+ }
175
+
176
+ let readmeText = "";
177
+ try {
178
+ readmeText = fs.readFileSync(readmePath, "utf8");
179
+ } catch (err) {
180
+ emit({
181
+ status: "failed",
182
+ summary: "Failed to read README",
183
+ explanation: "Unable to read README file.",
184
+ details: [
185
+ {
186
+ code: "README_READ_FAILED",
187
+ severity: "high",
188
+ message: err instanceof Error ? err.message : "Read failed.",
189
+ },
190
+ ],
191
+ });
192
+ process.exit(0);
193
+ }
194
+
195
+ const firstSentence = extractFirstSentence(readmeText);
196
+ if (!firstSentence) {
197
+ emit({
198
+ status: "na",
199
+ summary: "README has no readable paragraph",
200
+ explanation:
201
+ "Could not locate non-title text to compare against the description.",
202
+ });
203
+ process.exit(0);
204
+ }
205
+
206
+ const normalizedDescription = normalize(description);
207
+ const normalizedSentence = normalize(firstSentence);
208
+
209
+ if (normalizedDescription === normalizedSentence) {
210
+ emit({
211
+ status: "pass-full",
212
+ summary: "Description matches README",
213
+ explanation:
214
+ "The GitHub repo description matches the first README sentence.",
215
+ });
216
+ process.exit(0);
217
+ }
218
+
219
+ emit({
220
+ status: "failed",
221
+ summary: "Description does not match README",
222
+ explanation:
223
+ "The GitHub repo description does not match the first README sentence.",
224
+ details: [
225
+ {
226
+ code: "ABOUT_MISMATCH",
227
+ severity: "medium",
228
+ message: `README sentence: "${firstSentence}"`,
229
+ remediation: `Update the GitHub description to match: "${firstSentence}"`,
230
+ },
231
+ {
232
+ code: "ABOUT_MISMATCH",
233
+ severity: "medium",
234
+ message: `Repo description: "${description}"`,
235
+ },
236
+ ],
237
+ metrics: {
238
+ readmeSentence: firstSentence,
239
+ description,
240
+ },
241
+ });
@@ -0,0 +1,17 @@
1
+ {
2
+ "id": "package-manager",
3
+ "name": "packageManager is defined",
4
+ "version": "1.0.0",
5
+ "description": "Ensures package.json declares a package manager.",
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
+ }
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+
7
+ const start = Date.now();
8
+
9
+ const emit = ({
10
+ status,
11
+ summary,
12
+ explanation,
13
+ details = [],
14
+ metrics = {},
15
+ artifacts = [],
16
+ }) => {
17
+ const payload = {
18
+ status,
19
+ summary,
20
+ explanation,
21
+ details,
22
+ metrics,
23
+ artifacts,
24
+ durationMs: Date.now() - start,
25
+ };
26
+ process.stdout.write(JSON.stringify(payload));
27
+ };
28
+
29
+ const repoPath = process.env.REPO_PATH;
30
+ if (!repoPath) {
31
+ emit({
32
+ status: "failed",
33
+ summary: "Missing repo path",
34
+ explanation: "REPO_PATH environment variable is required.",
35
+ details: [
36
+ {
37
+ code: "MISSING_REPO_PATH",
38
+ severity: "high",
39
+ message: "REPO_PATH environment variable was not provided.",
40
+ },
41
+ ],
42
+ });
43
+ process.exit(0);
44
+ }
45
+
46
+ const packageJsonPath = path.join(repoPath, "package.json");
47
+ if (!fs.existsSync(packageJsonPath)) {
48
+ emit({
49
+ status: "na",
50
+ summary: "No package.json",
51
+ explanation: "This repository does not use package.json.",
52
+ });
53
+ process.exit(0);
54
+ }
55
+
56
+ let parsed;
57
+ try {
58
+ const raw = fs.readFileSync(packageJsonPath, "utf8");
59
+ parsed = JSON.parse(raw);
60
+ } catch (err) {
61
+ emit({
62
+ status: "failed",
63
+ summary: "Invalid package.json",
64
+ explanation: "package.json could not be parsed.",
65
+ details: [
66
+ {
67
+ code: "PACKAGE_JSON_INVALID",
68
+ severity: "high",
69
+ message: err instanceof Error ? err.message : "Parse failed.",
70
+ },
71
+ ],
72
+ });
73
+ process.exit(0);
74
+ }
75
+
76
+ const packageManager =
77
+ parsed && typeof parsed.packageManager === "string"
78
+ ? parsed.packageManager.trim()
79
+ : "";
80
+
81
+ if (!packageManager) {
82
+ emit({
83
+ status: "failed",
84
+ summary: "packageManager missing",
85
+ explanation:
86
+ "Define packageManager in package.json to lock the package manager toolchain.",
87
+ details: [
88
+ {
89
+ code: "PACKAGE_MANAGER_MISSING",
90
+ severity: "medium",
91
+ message:
92
+ "Set packageManager (example: \"pnpm@8.15.4\") to enable corepack.",
93
+ },
94
+ ],
95
+ });
96
+ process.exit(0);
97
+ }
98
+
99
+ emit({
100
+ status: "pass-full",
101
+ summary: "packageManager is set",
102
+ explanation: `package.json declares "${packageManager}".`,
103
+ metrics: {
104
+ packageManager,
105
+ },
106
+ });
@@ -0,0 +1,6 @@
1
+ {
2
+ "id": "example-suite",
3
+ "name": "Example Healthchecks",
4
+ "version": "1.0.0",
5
+ "description": "Sample healthchecks for git-daemon."
6
+ }
package/openapi.yaml CHANGED
@@ -93,6 +93,8 @@ paths:
93
93
  data: {"type":"log","stream":"stdout","line":"Cloning into..."}
94
94
 
95
95
  data: {"type":"progress","kind":"git","percent":42,"detail":"receiving objects"}
96
+
97
+ data: {"type":"progress","kind":"healthcheck","detail":"lint: running"}
96
98
  '401':
97
99
  $ref: '#/components/responses/Unauthorized'
98
100
  '403':
@@ -314,6 +316,77 @@ paths:
314
316
  $ref: '#/components/responses/UnprocessableEntity'
315
317
  '500':
316
318
  $ref: '#/components/responses/InternalError'
319
+ /v1/healthchecks:
320
+ get:
321
+ summary: List available healthchecks
322
+ parameters:
323
+ - $ref: '#/components/parameters/OriginHeader'
324
+ responses:
325
+ '200':
326
+ description: Healthcheck catalog
327
+ content:
328
+ application/json:
329
+ schema:
330
+ $ref: '#/components/schemas/HealthcheckCatalogResponse'
331
+ '401':
332
+ $ref: '#/components/responses/Unauthorized'
333
+ '403':
334
+ $ref: '#/components/responses/Forbidden'
335
+ '500':
336
+ $ref: '#/components/responses/InternalError'
337
+ /v1/healthchecks/run:
338
+ post:
339
+ summary: Run healthchecks for a repository
340
+ parameters:
341
+ - $ref: '#/components/parameters/OriginHeader'
342
+ requestBody:
343
+ required: true
344
+ content:
345
+ application/json:
346
+ schema:
347
+ $ref: '#/components/schemas/HealthcheckRunRequest'
348
+ responses:
349
+ '202':
350
+ description: Job created
351
+ content:
352
+ application/json:
353
+ schema:
354
+ $ref: '#/components/schemas/JobResponse'
355
+ '401':
356
+ $ref: '#/components/responses/Unauthorized'
357
+ '403':
358
+ $ref: '#/components/responses/Forbidden'
359
+ '404':
360
+ $ref: '#/components/responses/NotFound'
361
+ '409':
362
+ $ref: '#/components/responses/Conflict'
363
+ '422':
364
+ $ref: '#/components/responses/UnprocessableEntity'
365
+ '500':
366
+ $ref: '#/components/responses/InternalError'
367
+ /v1/healthchecks/jobs/{id}/result:
368
+ get:
369
+ summary: Get healthcheck results for a job
370
+ parameters:
371
+ - $ref: '#/components/parameters/OriginHeader'
372
+ - $ref: '#/components/parameters/JobId'
373
+ responses:
374
+ '200':
375
+ description: Healthcheck results
376
+ content:
377
+ application/json:
378
+ schema:
379
+ $ref: '#/components/schemas/HealthcheckRunResult'
380
+ '401':
381
+ $ref: '#/components/responses/Unauthorized'
382
+ '403':
383
+ $ref: '#/components/responses/Forbidden'
384
+ '404':
385
+ $ref: '#/components/responses/NotFound'
386
+ '409':
387
+ $ref: '#/components/responses/Conflict'
388
+ '500':
389
+ $ref: '#/components/responses/InternalError'
317
390
  /v1/diagnostics:
318
391
  get:
319
392
  summary: Get diagnostics bundle metadata
@@ -708,6 +781,210 @@ components:
708
781
  safer:
709
782
  type: boolean
710
783
  default: true
784
+ HealthcheckCatalogResponse:
785
+ type: object
786
+ required:
787
+ - suites
788
+ properties:
789
+ suites:
790
+ type: array
791
+ items:
792
+ $ref: '#/components/schemas/HealthcheckSuite'
793
+ HealthcheckSuite:
794
+ type: object
795
+ required:
796
+ - id
797
+ - name
798
+ - checks
799
+ properties:
800
+ id:
801
+ type: string
802
+ name:
803
+ type: string
804
+ version:
805
+ type: string
806
+ description:
807
+ type: string
808
+ checks:
809
+ type: array
810
+ items:
811
+ $ref: '#/components/schemas/HealthcheckDefinition'
812
+ HealthcheckDefinition:
813
+ type: object
814
+ required:
815
+ - id
816
+ - name
817
+ - version
818
+ - description
819
+ - timeoutSeconds
820
+ - cacheable
821
+ properties:
822
+ id:
823
+ type: string
824
+ name:
825
+ type: string
826
+ version:
827
+ type: string
828
+ description:
829
+ type: string
830
+ timeoutSeconds:
831
+ type: integer
832
+ minimum: 1
833
+ cacheable:
834
+ type: boolean
835
+ cacheMaxAgeSeconds:
836
+ type: integer
837
+ minimum: 1
838
+ permissions:
839
+ $ref: '#/components/schemas/HealthcheckPermissions'
840
+ configSchema:
841
+ type: object
842
+ description: JSON schema for check config.
843
+ additionalProperties: true
844
+ outputSchemaVersion:
845
+ type: integer
846
+ HealthcheckPermissions:
847
+ type: object
848
+ properties:
849
+ repoRead:
850
+ type: boolean
851
+ network:
852
+ type: boolean
853
+ secrets:
854
+ type: array
855
+ items:
856
+ type: string
857
+ HealthcheckRunRequest:
858
+ type: object
859
+ required:
860
+ - repoPath
861
+ properties:
862
+ repoPath:
863
+ type: string
864
+ repoInfo:
865
+ type: object
866
+ description: UI-supplied repo metadata passed through to checks.
867
+ additionalProperties: true
868
+ suiteId:
869
+ type: string
870
+ checks:
871
+ type: array
872
+ items:
873
+ $ref: '#/components/schemas/HealthcheckSelection'
874
+ HealthcheckSelection:
875
+ type: object
876
+ required:
877
+ - checkId
878
+ properties:
879
+ suiteId:
880
+ type: string
881
+ checkId:
882
+ type: string
883
+ config:
884
+ type: object
885
+ additionalProperties: true
886
+ cacheMode:
887
+ type: string
888
+ enum: [prefer, refresh, bypass]
889
+ default: prefer
890
+ HealthcheckRunResult:
891
+ type: object
892
+ required:
893
+ - jobId
894
+ - repoPath
895
+ - status
896
+ - checks
897
+ properties:
898
+ jobId:
899
+ type: string
900
+ repoPath:
901
+ type: string
902
+ status:
903
+ $ref: '#/components/schemas/HealthcheckStatus'
904
+ summary:
905
+ type: string
906
+ checks:
907
+ type: array
908
+ items:
909
+ $ref: '#/components/schemas/HealthcheckResult'
910
+ startedAt:
911
+ type: string
912
+ format: date-time
913
+ finishedAt:
914
+ type: string
915
+ format: date-time
916
+ HealthcheckResult:
917
+ type: object
918
+ required:
919
+ - checkId
920
+ - status
921
+ - summary
922
+ - explanation
923
+ properties:
924
+ suiteId:
925
+ type: string
926
+ checkId:
927
+ type: string
928
+ status:
929
+ $ref: '#/components/schemas/HealthcheckStatus'
930
+ summary:
931
+ type: string
932
+ explanation:
933
+ type: string
934
+ details:
935
+ type: array
936
+ items:
937
+ $ref: '#/components/schemas/HealthcheckDetail'
938
+ metrics:
939
+ type: object
940
+ additionalProperties: true
941
+ artifacts:
942
+ type: array
943
+ items:
944
+ $ref: '#/components/schemas/HealthcheckArtifact'
945
+ durationMs:
946
+ type: integer
947
+ minimum: 0
948
+ cached:
949
+ type: boolean
950
+ HealthcheckDetail:
951
+ type: object
952
+ required:
953
+ - severity
954
+ - message
955
+ properties:
956
+ code:
957
+ type: string
958
+ severity:
959
+ type: string
960
+ enum: [info, low, medium, high, critical]
961
+ message:
962
+ type: string
963
+ path:
964
+ type: string
965
+ line:
966
+ type: integer
967
+ minimum: 1
968
+ column:
969
+ type: integer
970
+ minimum: 1
971
+ remediation:
972
+ type: string
973
+ HealthcheckArtifact:
974
+ type: object
975
+ required:
976
+ - name
977
+ - path
978
+ properties:
979
+ name:
980
+ type: string
981
+ path:
982
+ type: string
983
+ contentType:
984
+ type: string
985
+ HealthcheckStatus:
986
+ type: string
987
+ enum: [na, failed, pass-partial, pass-full]
711
988
  CancelResponse:
712
989
  type: object
713
990
  required:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-daemon",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
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",