@jrpool/kilotest 59.1.3 → 60.0.0

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.
Files changed (64) hide show
  1. package/README.md +2 -5
  2. package/api/awaitTest.test.ts +9 -9
  3. package/api/awaitTest.ts +12 -12
  4. package/api/getReport.test.ts +4 -4
  5. package/api/getReport.ts +9 -9
  6. package/api/listDiagnoses.test.ts +14 -14
  7. package/api/listDiagnoses.ts +8 -8
  8. package/api/listIssues.test.ts +15 -15
  9. package/api/listIssues.ts +18 -20
  10. package/api/listPages.test.ts +2 -2
  11. package/api/listPages.ts +4 -4
  12. package/api/listViolators.test.ts +10 -10
  13. package/api/listViolators.ts +10 -10
  14. package/api/orderNewTest.test.ts +1 -2
  15. package/api/orderNewTest.ts +3 -4
  16. package/api/orderRetest.test.ts +10 -10
  17. package/api/orderRetest.ts +13 -14
  18. package/api/requestRetest.test.ts +11 -11
  19. package/api/requestRetest.ts +11 -11
  20. package/api/routes.ts +9 -11
  21. package/api/schemas.ts +18 -28
  22. package/api/util.test.ts +2 -2
  23. package/api/util.ts +7 -7
  24. package/capability.md +4 -4
  25. package/docs/AI-TOOLS.md +45 -5
  26. package/docs/decisions/009-report-id.md +16 -0
  27. package/index.test.ts +91 -41
  28. package/index.ts +66 -15
  29. package/lhm.plugin.json +33 -119
  30. package/llms-full.txt +18 -24
  31. package/llms.txt +12 -12
  32. package/mcp.test.ts +11 -20
  33. package/mcp.ts +18 -18
  34. package/openapi-3.0.yaml +1847 -0
  35. package/openapi.yaml +38 -97
  36. package/package.json +8 -2
  37. package/routing.test.ts +1 -1
  38. package/scripts/generate-openapi.ts +83 -1
  39. package/scripts/postPR.ts +89 -0
  40. package/scripts/prePR.ts +64 -0
  41. package/server.json +1 -1
  42. package/util.test.ts +32 -33
  43. package/util.ts +47 -43
  44. package/web/hideReportForm/index.ts +5 -6
  45. package/web/listDiagnoses/index.ts +4 -5
  46. package/web/listDiagnoses/listDiagnoses.test.ts +13 -13
  47. package/web/listIssues/index.ts +11 -12
  48. package/web/listIssues/listIssues.test.ts +24 -24
  49. package/web/listPages/index.ts +2 -2
  50. package/web/listPages/listPages.test.ts +2 -2
  51. package/web/listTopIssues/index.ts +2 -2
  52. package/web/listViolators/index.ts +5 -6
  53. package/web/listViolators/listViolators.test.ts +12 -12
  54. package/web/reannotate/index.ts +3 -3
  55. package/web/reannotateForm/index.ts +1 -2
  56. package/web/reportDeletion.ts +9 -12
  57. package/web/requestRetest/index.ts +2 -2
  58. package/web/requestRetest/requestRetest.test.ts +7 -7
  59. package/web/requestRetestForm/index.html +2 -2
  60. package/web/requestRetestForm/index.ts +3 -4
  61. package/web/requestRetestForm/requestRetestForm.test.ts +7 -8
  62. package/web/unhideReportForm/index.ts +4 -6
  63. package/web/util.test.ts +29 -29
  64. package/web/util.ts +1 -1
package/openapi.yaml CHANGED
@@ -2,7 +2,7 @@ openapi: 3.1.0
2
2
  info:
3
3
  title: Kilotest API
4
4
  description: 'Kilotest is an ensemble testing and reporting service for front-end web quality. It uses an ensemble of 12 rule engines to test web pages for front-end quality (accessibility, usability, and web standards, including HTML, CSS, ARIA, and WCAG). Kilotest acts as a web server for human users, an MCP server for AI platforms, and an API for programmatic access. With this API you can request that a page be tested or retested, discover available reports, incrementally retrieve facts from a report: from what reports are available, to what issues were reported in one report, to what elements violated one issue, to what diagnoses rule engines gave for one violation.'
5
- version: 59.1.3
5
+ version: 59.1.4
6
6
  license:
7
7
  name: MIT
8
8
  identifier: MIT
@@ -81,7 +81,7 @@ paths:
81
81
  properties:
82
82
  identifier:
83
83
  type: string
84
- description: timeStamp-jobID identifier of the report.
84
+ description: 'Identifier of the report (example: 260503T0432-x9z).'
85
85
  completion date and time:
86
86
  type: string
87
87
  days since the report was completed:
@@ -165,25 +165,18 @@ paths:
165
165
  - response metadata
166
166
  - response content
167
167
  additionalProperties: false
168
- /api/listIssues/{timeStamp}/{jobID}:
168
+ /api/listIssues/{reportID}:
169
169
  get:
170
170
  operationId: listIssues
171
171
  summary: List basics about the issues in one report
172
172
  parameters:
173
173
  - in: path
174
- name: timeStamp
174
+ name: reportID
175
175
  schema:
176
176
  type: string
177
- description: 'Timestamp of the report in YYMMDDTHHmm format (example: 260503T0432)'
177
+ description: 'Identifier of the report (example: 260503T0432-x9z)'
178
178
  required: true
179
- description: 'Timestamp of the report in YYMMDDTHHmm format (example: 260503T0432)'
180
- - in: path
181
- name: jobID
182
- schema:
183
- type: string
184
- description: 'Job identifier of the report (example: x9z)'
185
- required: true
186
- description: 'Job identifier of the report (example: x9z)'
179
+ description: 'Identifier of the report (example: 260503T0432-x9z)'
187
180
  responses:
188
181
  '200':
189
182
  description: List basics about the issues in one report
@@ -233,7 +226,7 @@ paths:
233
226
  properties:
234
227
  identifier:
235
228
  type: string
236
- description: timeStamp-jobID identifier of the report.
229
+ description: 'Identifier of the report (example: 260503T0432-x9z).'
237
230
  completion date and time:
238
231
  type: string
239
232
  days since the report was completed:
@@ -493,7 +486,7 @@ paths:
493
486
  - response metadata
494
487
  - response content
495
488
  additionalProperties: false
496
- /api/listViolators/{issueID}/{timeStamp}/{jobID}:
489
+ /api/listViolators/{issueID}/{reportID}:
497
490
  get:
498
491
  operationId: listViolators
499
492
  summary: List elements reported as exhibiting one issue
@@ -506,19 +499,12 @@ paths:
506
499
  required: true
507
500
  description: 'Issue identifier (example: contrastPoor)'
508
501
  - in: path
509
- name: timeStamp
502
+ name: reportID
510
503
  schema:
511
504
  type: string
512
- description: 'Timestamp of the report in YYMMDDTHHmm format (example: 260503T0432)'
505
+ description: 'Identifier of the report (example: 260503T0432-x9z)'
513
506
  required: true
514
- description: 'Timestamp of the report in YYMMDDTHHmm format (example: 260503T0432)'
515
- - in: path
516
- name: jobID
517
- schema:
518
- type: string
519
- description: 'Job identifier of the report (example: x9z)'
520
- required: true
521
- description: 'Job identifier of the report (example: x9z)'
507
+ description: 'Identifier of the report (example: 260503T0432-x9z)'
522
508
  responses:
523
509
  '200':
524
510
  description: List elements reported as exhibiting one issue
@@ -628,7 +614,7 @@ paths:
628
614
  - response metadata
629
615
  - response content
630
616
  additionalProperties: false
631
- /api/listDiagnoses/{catalogIndex}/{issueID}/{timeStamp}/{jobID}:
617
+ /api/listDiagnoses/{catalogIndex}/{issueID}/{reportID}:
632
618
  get:
633
619
  operationId: listDiagnoses
634
620
  summary: List diagnoses of how one element exhibited one issue
@@ -648,19 +634,12 @@ paths:
648
634
  required: true
649
635
  description: 'Issue identifier (example: contrastPoor)'
650
636
  - in: path
651
- name: timeStamp
652
- schema:
653
- type: string
654
- description: 'Timestamp of the report in YYMMDDTHHmm format (example: 260503T0432)'
655
- required: true
656
- description: 'Timestamp of the report in YYMMDDTHHmm format (example: 260503T0432)'
657
- - in: path
658
- name: jobID
637
+ name: reportID
659
638
  schema:
660
639
  type: string
661
- description: 'Job identifier of the report (example: x9z)'
640
+ description: 'Identifier of the report (example: 260503T0432-x9z)'
662
641
  required: true
663
- description: 'Job identifier of the report (example: x9z)'
642
+ description: 'Identifier of the report (example: 260503T0432-x9z)'
664
643
  responses:
665
644
  '200':
666
645
  description: List diagnoses of how one element exhibited one issue
@@ -788,25 +767,18 @@ paths:
788
767
  - response metadata
789
768
  - response content
790
769
  additionalProperties: false
791
- /api/getReport/{timeStamp}/{jobID}:
770
+ /api/getReport/{reportID}:
792
771
  get:
793
772
  operationId: getReport
794
773
  summary: Get one full report in JSON
795
774
  parameters:
796
775
  - in: path
797
- name: timeStamp
776
+ name: reportID
798
777
  schema:
799
778
  type: string
800
- description: 'Timestamp of the report in YYMMDDTHHmm format (example: 260503T0432)'
779
+ description: 'Identifier of the report (example: 260503T0432-x9z)'
801
780
  required: true
802
- description: 'Timestamp of the report in YYMMDDTHHmm format (example: 260503T0432)'
803
- - in: path
804
- name: jobID
805
- schema:
806
- type: string
807
- description: 'Job identifier of the report (example: x9z)'
808
- required: true
809
- description: 'Job identifier of the report (example: x9z)'
781
+ description: 'Identifier of the report (example: 260503T0432-x9z)'
810
782
  responses:
811
783
  '200':
812
784
  description: Get one full report in JSON
@@ -1003,25 +975,18 @@ paths:
1003
975
  - response metadata
1004
976
  - response content
1005
977
  additionalProperties: false
1006
- /api/requestRetest/{timeStamp}/{jobID}:
978
+ /api/requestRetest/{reportID}:
1007
979
  post:
1008
980
  operationId: requestRetest
1009
981
  summary: Request that a page be retested
1010
982
  parameters:
1011
983
  - in: path
1012
- name: timeStamp
984
+ name: reportID
1013
985
  schema:
1014
986
  type: string
1015
- description: 'Timestamp of the latest report about the page in YYMMDDTHHmm format (example: 260503T0432)'
987
+ description: 'Identifier of the latest report about the page (example: 260503T0432-x9z)'
1016
988
  required: true
1017
- description: 'Timestamp of the latest report about the page in YYMMDDTHHmm format (example: 260503T0432)'
1018
- - in: path
1019
- name: jobID
1020
- schema:
1021
- type: string
1022
- description: 'Job identifier of the latest report about the page (example: x9z)'
1023
- required: true
1024
- description: 'Job identifier of the latest report about the page (example: x9z)'
989
+ description: 'Identifier of the latest report about the page (example: 260503T0432-x9z)'
1025
990
  requestBody:
1026
991
  content:
1027
992
  application/json:
@@ -1365,7 +1330,8 @@ paths:
1365
1330
  type: string
1366
1331
  report identifier:
1367
1332
  anyOf:
1368
- - $ref: '#/components/schemas/ReportIdentifier'
1333
+ - type: string
1334
+ description: 'Identifier of the report, to pass to listIssues once the report is ready (example: 260503T0432-x9z).'
1369
1335
  - type: 'null'
1370
1336
  how you can check for completion:
1371
1337
  type: string
@@ -1390,25 +1356,18 @@ paths:
1390
1356
  - response metadata
1391
1357
  - response content
1392
1358
  additionalProperties: false
1393
- /api/orderRetest/{timeStamp}/{jobID}:
1359
+ /api/orderRetest/{reportID}:
1394
1360
  post:
1395
1361
  operationId: orderRetest
1396
1362
  summary: Order that a page be retested immediately, without manual approval
1397
1363
  parameters:
1398
1364
  - in: path
1399
- name: timeStamp
1365
+ name: reportID
1400
1366
  schema:
1401
1367
  type: string
1402
- description: 'Timestamp of the latest report about the page in YYMMDDTHHmm format (example: 260503T0432)'
1368
+ description: 'Identifier of the latest report about the page (example: 260503T0432-x9z)'
1403
1369
  required: true
1404
- description: 'Timestamp of the latest report about the page in YYMMDDTHHmm format (example: 260503T0432)'
1405
- - in: path
1406
- name: jobID
1407
- schema:
1408
- type: string
1409
- description: 'Job identifier of the latest report about the page (example: x9z)'
1410
- required: true
1411
- description: 'Job identifier of the latest report about the page (example: x9z)'
1370
+ description: 'Identifier of the latest report about the page (example: 260503T0432-x9z)'
1412
1371
  requestBody:
1413
1372
  content:
1414
1373
  application/json:
@@ -1519,7 +1478,8 @@ paths:
1519
1478
  type: string
1520
1479
  report identifier:
1521
1480
  anyOf:
1522
- - $ref: '#/components/schemas/ReportIdentifier'
1481
+ - type: string
1482
+ description: 'Identifier of the report, to pass to listIssues once the report is ready (example: 260503T0432-x9z).'
1523
1483
  - type: 'null'
1524
1484
  how you can check for completion:
1525
1485
  type: string
@@ -1544,25 +1504,18 @@ paths:
1544
1504
  - response metadata
1545
1505
  - response content
1546
1506
  additionalProperties: false
1547
- /api/awaitTest/{timeStamp}/{jobID}:
1507
+ /api/awaitTest/{reportID}:
1548
1508
  post:
1549
1509
  operationId: awaitTest
1550
1510
  summary: Wait for a job ordered via orderNewTest or orderRetest to complete
1551
1511
  parameters:
1552
1512
  - in: path
1553
- name: timeStamp
1513
+ name: reportID
1554
1514
  schema:
1555
1515
  type: string
1556
- description: 'Timestamp of the ordered job, from the report identifier returned by orderNewTest, in YYMMDDTHHmm format (example: 260503T0432)'
1516
+ description: 'Report identifier returned by orderNewTest or orderRetest (example: 260503T0432-x9z)'
1557
1517
  required: true
1558
- description: 'Timestamp of the ordered job, from the report identifier returned by orderNewTest, in YYMMDDTHHmm format (example: 260503T0432)'
1559
- - in: path
1560
- name: jobID
1561
- schema:
1562
- type: string
1563
- description: 'Job identifier of the ordered job, from the report identifier returned by orderNewTest (example: x9z)'
1564
- required: true
1565
- description: 'Job identifier of the ordered job, from the report identifier returned by orderNewTest (example: x9z)'
1518
+ description: 'Report identifier returned by orderNewTest or orderRetest (example: 260503T0432-x9z)'
1566
1519
  requestBody:
1567
1520
  content:
1568
1521
  application/json:
@@ -1631,7 +1584,8 @@ paths:
1631
1584
  - type: object
1632
1585
  properties:
1633
1586
  report identifier:
1634
- $ref: '#/components/schemas/ReportIdentifier'
1587
+ type: string
1588
+ description: 'Identifier of the report, to pass to listIssues once the report is ready (example: 260503T0432-x9z).'
1635
1589
  required:
1636
1590
  - report identifier
1637
1591
  additionalProperties: false
@@ -1792,7 +1746,7 @@ components:
1792
1746
  properties:
1793
1747
  identifier:
1794
1748
  type: string
1795
- description: timeStamp-jobID identifier of the report.
1749
+ description: 'Identifier of the report (example: 260503T0432-x9z).'
1796
1750
  completion date and time:
1797
1751
  type: string
1798
1752
  days since the report was completed:
@@ -1882,16 +1836,3 @@ components:
1882
1836
  required:
1883
1837
  - error
1884
1838
  additionalProperties: false
1885
- ReportIdentifier:
1886
- type: object
1887
- properties:
1888
- timeStamp:
1889
- type: string
1890
- description: Timestamp to pass to listIssues once the report is ready.
1891
- jobID:
1892
- type: string
1893
- description: Job identifier to pass to listIssues once the report is ready.
1894
- required:
1895
- - timeStamp
1896
- - jobID
1897
- additionalProperties: false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jrpool/kilotest",
3
- "version": "59.1.3",
3
+ "version": "60.0.0",
4
4
  "description": "Ensemble testing for front-end web quality",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -14,7 +14,13 @@
14
14
  "test": "c8 node --require ./test/setup.ts --experimental-test-module-mocks --test --test-concurrency=1 --test-randomize '**/*.test.ts'",
15
15
  "test:smoke": "node smokeTest.ts",
16
16
  "checkinfo": "node checkinfo.ts",
17
- "generate": "node scripts/generate-openapi.ts && node scripts/update-versions.ts"
17
+ "generate": "node scripts/generate-openapi.ts && node scripts/update-versions.ts",
18
+ "version": "npm run generate",
19
+ "prepublishOnly": "test \"$(git branch --show-current)\" = main",
20
+ "postpublish": "npm run publish:lhm",
21
+ "publish:lhm": "npx -y @lobehub/market-cli plugin update --dir .",
22
+ "prePR": "node scripts/prePR.ts",
23
+ "postPR": "node scripts/postPR.ts"
18
24
  },
19
25
  "repository": {
20
26
  "type": "git",
package/routing.test.ts CHANGED
@@ -68,7 +68,7 @@ test('GET /api/requestNewTest returns an error, not a test-request response', as
68
68
  });
69
69
 
70
70
  test('GET /api/requestRetest returns an error, not a retest-request response', async () => {
71
- const result = await get('/api/requestRetest/260101T0001/ct');
71
+ const result = await get('/api/requestRetest/260101T0001-ct');
72
72
  assert.ok(result.body.includes('Invalid service request'));
73
73
  });
74
74
 
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  generate-openapi.ts
3
- Generates openapi.yaml from the Kilotest API route table (api/routes.ts) and Zod schemas (api/schemas.ts), keeping the OpenAPI document in sync with schemas.ts’s single source of truth.
3
+ Generates openapi.yaml from the Kilotest API route table (api/routes.ts) and Zod schemas (api/schemas.ts), keeping the OpenAPI document in sync with schemas.ts’s single source of truth. Also generates openapi-3.0.yaml, an OpenAPI 3.0.3 conversion for registries whose importers do not accept 3.1 documents (currently the RapidAPI Hub, which is updated via its Hub Listing > Definitions > CI/CD > Import OpenAPI upload).
4
4
  */
5
5
 
6
6
  // IMPORTS
@@ -17,6 +17,7 @@ import {version} from '../api/version.ts';
17
17
 
18
18
  const {license} = pkg;
19
19
  const outputPath = path.join(import.meta.dirname, '..', 'openapi.yaml');
20
+ const output30Path = path.join(import.meta.dirname, '..', 'openapi-3.0.yaml');
20
21
 
21
22
  // FUNCTIONS
22
23
 
@@ -52,6 +53,84 @@ const buildPaths = () => {
52
53
  });
53
54
  return paths;
54
55
  };
56
+ // Converts a generated OpenAPI 3.1 document in place to an OpenAPI 3.0.3
57
+ // document: 'const' schemas become single-member 'enum's, type arrays lose
58
+ // their 'null' members and gain 'nullable', 'null'-unioned schemas become
59
+ // 'nullable', and 3.1-only info fields are removed. Nullable unions whose
60
+ // branches cannot be merged into one typed schema gain an extra object-typed
61
+ // nullable branch instead, because 3.0 validators require a 'type' wherever
62
+ // 'nullable' appears.
63
+ const downgrade30 = (document: any) => {
64
+ document.openapi = '3.0.3';
65
+ document.info.license = {
66
+ name: license,
67
+ url: 'https://github.com/jrpool/kilotest/blob/main/LICENSE'
68
+ };
69
+ const refType = (ref: string) => {
70
+ const match = /^#\/components\/schemas\/(.+)$/.exec(ref);
71
+ const target = match?.[1] && document.components?.schemas?.[match[1]];
72
+ return target && typeof target === 'object' ? target.type : null;
73
+ };
74
+ const walk = (node: any): void => {
75
+ if (Array.isArray(node)) {
76
+ node.forEach(walk);
77
+ return;
78
+ }
79
+ if (!node || typeof node !== 'object') {
80
+ return;
81
+ }
82
+ if ('const' in node) {
83
+ node.enum = [node.const];
84
+ delete node.const;
85
+ }
86
+ if (Array.isArray(node.type)) {
87
+ const types = node.type.filter((t: string) => t !== 'null');
88
+ const hasNull = types.length !== node.type.length;
89
+ if (types.length === 1) {
90
+ node.type = types[0];
91
+ }
92
+ else {
93
+ node.oneOf = types.map((t: string) => ({type: t}));
94
+ delete node.type;
95
+ }
96
+ if (hasNull) {
97
+ node.nullable = true;
98
+ }
99
+ }
100
+ else if (node.type === 'null') {
101
+ node.type = 'object';
102
+ node.nullable = true;
103
+ }
104
+ if (Array.isArray(node.anyOf)) {
105
+ const nullIndex = node.anyOf.findIndex(
106
+ (branch: any) => branch && typeof branch === 'object' && branch.type === 'null'
107
+ );
108
+ if (nullIndex !== -1) {
109
+ const others = node.anyOf.filter((_: any, i: number) => i !== nullIndex);
110
+ delete node.anyOf;
111
+ if (others.length === 1 && others[0].$ref) {
112
+ node.allOf = [others[0]];
113
+ const type = refType(others[0].$ref);
114
+ if (type) {
115
+ node.type = type;
116
+ }
117
+ node.nullable = true;
118
+ }
119
+ else if (others.length === 1 && others[0].type) {
120
+ Object.assign(node, others[0]);
121
+ node.nullable = true;
122
+ }
123
+ else {
124
+ node.anyOf = [...others, {type: 'object', nullable: true}];
125
+ }
126
+ }
127
+ }
128
+ for (const value of Object.values(node)) {
129
+ walk(value);
130
+ }
131
+ };
132
+ walk(document);
133
+ };
55
134
  // Generates and writes the openapi.yaml file.
56
135
  const generate = () => {
57
136
  const document = createDocument({
@@ -78,6 +157,9 @@ const generate = () => {
78
157
  });
79
158
  fs.writeFileSync(outputPath, yaml.dump(document, {noRefs: true, lineWidth: -1}));
80
159
  console.log(`Wrote ${outputPath}`);
160
+ downgrade30(document);
161
+ fs.writeFileSync(output30Path, yaml.dump(document, {noRefs: true, lineWidth: -1}));
162
+ console.log(`Wrote ${output30Path}`);
81
163
  };
82
164
 
83
165
  // EXECUTION
@@ -0,0 +1,89 @@
1
+ /*
2
+ postPR.ts
3
+ Finalizes a release after the prePR pull request has been merged: checks out main, pulls, verifies that the merge landed, publishes to npm (the "postpublish" hook then updates the LobeHub listing), deletes the feature branch, and prunes remote-tracking branches. The feature-branch name is read from the marker file written by prePR.ts, or from an optional command-line argument.
4
+ */
5
+
6
+ // IMPORTS
7
+
8
+ import {spawnSync} from 'node:child_process';
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+
12
+ // CONSTANTS
13
+
14
+ const markerFile = 'kilotest-pr-branch';
15
+
16
+ // FUNCTIONS
17
+
18
+ // Runs a command with inherited stdio and throws on failure.
19
+ const run = (command: string, args: string[]): void => {
20
+ if (spawnSync(command, args, {stdio: 'inherit'}).status !== 0) {
21
+ throw new Error(`${command} ${args.join(' ')} failed`);
22
+ }
23
+ };
24
+ // Runs a command and returns its trimmed standard output.
25
+ const capture = (command: string, args: string[]): string => {
26
+ const result = spawnSync(command, args, {encoding: 'utf8'});
27
+ if (result.status !== 0) {
28
+ throw new Error(`${command} ${args.join(' ')} failed`);
29
+ }
30
+ return result.stdout.trim();
31
+ };
32
+ // Returns the absolute path of a file inside the git directory.
33
+ const gitPath = (name: string): string =>
34
+ path.join(capture('git', ['rev-parse', '--absolute-git-dir']), name);
35
+ // Exits unless the working tree is clean.
36
+ const requireCleanTree = (): void => {
37
+ const dirty = capture('git', ['status', '--porcelain']);
38
+ if (dirty) {
39
+ console.error(`Working tree is not clean. Commit or stash all changes first:\n${dirty}`);
40
+ process.exit(1);
41
+ }
42
+ };
43
+ // Returns the version field of package.json, either from the working tree or from a git ref.
44
+ const versionOf = (ref?: string): string => {
45
+ const content = ref
46
+ ? capture('git', ['show', `${ref}:package.json`])
47
+ : fs.readFileSync('package.json', 'utf8');
48
+ return JSON.parse(content).version;
49
+ };
50
+
51
+ // EXECUTION
52
+
53
+ const marker = gitPath(markerFile);
54
+ const argBranch = process.argv[2] ?? '';
55
+ const branch = argBranch ||
56
+ (fs.existsSync(marker) ? fs.readFileSync(marker, 'utf8').trim() : '');
57
+ if (!branch) {
58
+ console.error('No feature branch recorded. Run prePR first, or pass the branch name: npm run postPR -- <branch>');
59
+ process.exit(1);
60
+ }
61
+ if (branch === 'main') {
62
+ console.error('Refusing to treat main as the feature branch');
63
+ process.exit(1);
64
+ }
65
+ if (!capture('git', ['branch', '--list', branch])) {
66
+ console.error(`Branch ${branch} does not exist locally; it may already have been deleted`);
67
+ process.exit(1);
68
+ }
69
+ requireCleanTree();
70
+ run('git', ['checkout', 'main']);
71
+ run('git', ['pull', '--ff-only']);
72
+ // Aborts if the pull request has not been merged: under squash-merges the feature branch is never an ancestor of main, so version equality is the merge check.
73
+ const branchVersion = versionOf(branch);
74
+ const mainVersion = versionOf();
75
+ if (branchVersion !== mainVersion) {
76
+ console.error(`main is at version ${mainVersion} but ${branch} is at ${branchVersion}; merge the pull request first`);
77
+ process.exit(1);
78
+ }
79
+ run('npm', ['publish']);
80
+ // Branch cleanup is best-effort: deletion must precede pruning, because "git branch -d" checks mergedness against the upstream ref, which pruning would remove.
81
+ const deleted = spawnSync('git', ['branch', '-d', branch], {stdio: 'inherit'});
82
+ if (deleted.status !== 0) {
83
+ console.warn(`Could not delete ${branch}; delete it manually after confirming the merge`);
84
+ }
85
+ if (spawnSync('git', ['remote', 'prune', 'origin'], {stdio: 'inherit'}).status !== 0) {
86
+ console.warn('git remote prune origin failed');
87
+ }
88
+ fs.rmSync(marker, {force: true});
89
+ console.log('Release complete');
@@ -0,0 +1,64 @@
1
+ /*
2
+ prePR.ts
3
+ Prepares a release pull request on the current feature branch: verifies a clean working tree, bumps the package version (the npm "version" hook regenerates openapi.yaml, server.json, and lhm.plugin.json), commits and pushes the result, records the branch name for postPR.ts, and prints the remaining manual steps.
4
+ */
5
+
6
+ // IMPORTS
7
+
8
+ import {spawnSync} from 'node:child_process';
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+
12
+ // CONSTANTS
13
+
14
+ const levels = ['major', 'minor', 'patch'];
15
+ const usage = 'Usage: npm run prePR -- <major|minor|patch>';
16
+ const markerFile = 'kilotest-pr-branch';
17
+
18
+ // FUNCTIONS
19
+
20
+ // Runs a command with inherited stdio and throws on failure.
21
+ const run = (command: string, args: string[]): void => {
22
+ if (spawnSync(command, args, {stdio: 'inherit'}).status !== 0) {
23
+ throw new Error(`${command} ${args.join(' ')} failed`);
24
+ }
25
+ };
26
+ // Runs a command and returns its trimmed standard output.
27
+ const capture = (command: string, args: string[]): string => {
28
+ const result = spawnSync(command, args, {encoding: 'utf8'});
29
+ if (result.status !== 0) {
30
+ throw new Error(`${command} ${args.join(' ')} failed`);
31
+ }
32
+ return result.stdout.trim();
33
+ };
34
+ // Returns the absolute path of a file inside the git directory.
35
+ const gitPath = (name: string): string =>
36
+ path.join(capture('git', ['rev-parse', '--absolute-git-dir']), name);
37
+ // Exits unless the working tree is clean, so all substantive changes are committed before the version bump.
38
+ const requireCleanTree = (): void => {
39
+ const dirty = capture('git', ['status', '--porcelain']);
40
+ if (dirty) {
41
+ console.error(`Working tree is not clean. Commit or stash all changes first:\n${dirty}`);
42
+ process.exit(1);
43
+ }
44
+ };
45
+
46
+ // EXECUTION
47
+
48
+ const level = process.argv[2] ?? '';
49
+ if (!levels.includes(level)) {
50
+ console.error(usage);
51
+ process.exit(1);
52
+ }
53
+ const branch = capture('git', ['branch', '--show-current']);
54
+ if (!branch || branch === 'main') {
55
+ console.error('prePR must run on a feature branch, not on main');
56
+ process.exit(1);
57
+ }
58
+ requireCleanTree();
59
+ run('npm', ['version', level, '--no-git-tag-version']);
60
+ run('git', ['add', '-A']);
61
+ run('git', ['commit', '-m', 'update version and generated files']);
62
+ run('git', ['push', '-u', 'origin', branch]);
63
+ fs.writeFileSync(gitPath(markerFile), `${branch}\n`);
64
+ console.log('Submit a pull request, merge it to main, then run: npm run postPR');
package/server.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "url": "https://github.com/jrpool/kilotest",
7
7
  "source": "github"
8
8
  },
9
- "version": "59.1.3",
9
+ "version": "59.1.4",
10
10
  "remotes": [
11
11
  {
12
12
  "type": "streamable-http",