@bongos/core 1.20.69 → 1.20.70
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/.bongos-core.json +30 -25
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +3 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +50 -3
- package/docs/api-reference.md +3 -2
- package/docs/module-api-changelog.md +2 -0
- package/docs/modules-contract.md +2 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +6 -0
- package/scripts/gds/module.js +85 -7
- package/scripts/gds/upgrade.js +1 -1
- package/src/bongos/module-store.js +25 -1
- package/src/bongos/routes/modules.js +26 -1
- package/src/module-api.js +1 -1
- package/tests/module_cli.mjs +7 -7
- package/tests/module_upgrade_updates.mjs +256 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.20.
|
|
6
|
-
"core_contract": "1.20.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.20.70",
|
|
6
|
+
"core_contract": "1.20.70",
|
|
7
|
+
"source_commit": "31aa1da4c574b3429cbe3bea1cee188486c0845b",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-10-01T14:
|
|
9
|
+
"built_at": "2026-10-01T14:41:01.807Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 572,
|
|
13
13
|
"agent_docs_stubbed": 27,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 2795,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 3395,
|
|
20
|
+
"tree_sha256": "ba829522471ea62d370430e52214b3298f81333cef684d98d98231286a345e34",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -317,12 +317,12 @@
|
|
|
317
317
|
{
|
|
318
318
|
"path": "clients/bongos-client/README.md",
|
|
319
319
|
"mode": "0000644",
|
|
320
|
-
"sha256": "
|
|
320
|
+
"sha256": "472c0606ff4fe4e775d56e038702960834838d8767e984427f5de29d31f9cbe9"
|
|
321
321
|
},
|
|
322
322
|
{
|
|
323
323
|
"path": "clients/bongos-client/bongos-client.global.js",
|
|
324
324
|
"mode": "0000644",
|
|
325
|
-
"sha256": "
|
|
325
|
+
"sha256": "d641fbda06eb527c39d2eabbc7afc9f9182b03d766917e9fd84d69e86e9cc397"
|
|
326
326
|
},
|
|
327
327
|
{
|
|
328
328
|
"path": "clients/bongos-client/examples/hello-world.mjs",
|
|
@@ -332,17 +332,17 @@
|
|
|
332
332
|
{
|
|
333
333
|
"path": "clients/bongos-client/index.cjs",
|
|
334
334
|
"mode": "0000644",
|
|
335
|
-
"sha256": "
|
|
335
|
+
"sha256": "73b143d7faa4d930ab82f9f31b178816cc5a8ea87754de7e0575441905d4562a"
|
|
336
336
|
},
|
|
337
337
|
{
|
|
338
338
|
"path": "clients/bongos-client/index.d.ts",
|
|
339
339
|
"mode": "0000644",
|
|
340
|
-
"sha256": "
|
|
340
|
+
"sha256": "05ba628ec9c3761d8814af291cd5b11a02dd279c6e8df232b20ca632cc1e4336"
|
|
341
341
|
},
|
|
342
342
|
{
|
|
343
343
|
"path": "clients/bongos-client/index.mjs",
|
|
344
344
|
"mode": "0000644",
|
|
345
|
-
"sha256": "
|
|
345
|
+
"sha256": "11a7eb441c5533244c695a4c60fe7b170e87eb4081af77703e796e8370e971e5"
|
|
346
346
|
},
|
|
347
347
|
{
|
|
348
348
|
"path": "clients/bongos-client/package.json",
|
|
@@ -2262,12 +2262,12 @@
|
|
|
2262
2262
|
{
|
|
2263
2263
|
"path": "docs/api-reference.md",
|
|
2264
2264
|
"mode": "0000644",
|
|
2265
|
-
"sha256": "
|
|
2265
|
+
"sha256": "a24995419c2daa05deca5f2ed06656801c313f7499039a739b1bfa2cbc87e595"
|
|
2266
2266
|
},
|
|
2267
2267
|
{
|
|
2268
2268
|
"path": "docs/api/openapi.json",
|
|
2269
2269
|
"mode": "0000644",
|
|
2270
|
-
"sha256": "
|
|
2270
|
+
"sha256": "63946432b10d130a8f1749ced38bb733449ea4ed8039c9794287d3a269249b0c"
|
|
2271
2271
|
},
|
|
2272
2272
|
{
|
|
2273
2273
|
"path": "docs/architecture.md",
|
|
@@ -2802,12 +2802,12 @@
|
|
|
2802
2802
|
{
|
|
2803
2803
|
"path": "docs/module-api-changelog.md",
|
|
2804
2804
|
"mode": "0000644",
|
|
2805
|
-
"sha256": "
|
|
2805
|
+
"sha256": "82e53cbebb5b042447d74679e2501c04a79e55d473e4cb3976578572152d5cf6"
|
|
2806
2806
|
},
|
|
2807
2807
|
{
|
|
2808
2808
|
"path": "docs/modules-contract.md",
|
|
2809
2809
|
"mode": "0000644",
|
|
2810
|
-
"sha256": "
|
|
2810
|
+
"sha256": "33eb2665aea5cc00feb1de80b673f6063f98b2a8bd2caeb6fa4b09e41965c1df"
|
|
2811
2811
|
},
|
|
2812
2812
|
{
|
|
2813
2813
|
"path": "docs/onboarding/diagrams/01-task-lifecycle.mmd",
|
|
@@ -9457,12 +9457,12 @@
|
|
|
9457
9457
|
{
|
|
9458
9458
|
"path": "package-lock.json",
|
|
9459
9459
|
"mode": "0000644",
|
|
9460
|
-
"sha256": "
|
|
9460
|
+
"sha256": "ce7e8e4e85602173606cdf3d320047cb41654ab0662ea2840537a7f10b4dfab4"
|
|
9461
9461
|
},
|
|
9462
9462
|
{
|
|
9463
9463
|
"path": "package.json",
|
|
9464
9464
|
"mode": "0000644",
|
|
9465
|
-
"sha256": "
|
|
9465
|
+
"sha256": "f82ba870c1432fd2fd421397df493f3c052bab48a920d403558364ccd720a109"
|
|
9466
9466
|
},
|
|
9467
9467
|
{
|
|
9468
9468
|
"path": "public-docs/index.html",
|
|
@@ -9482,7 +9482,7 @@
|
|
|
9482
9482
|
{
|
|
9483
9483
|
"path": "release-notes.json",
|
|
9484
9484
|
"mode": "0000644",
|
|
9485
|
-
"sha256": "
|
|
9485
|
+
"sha256": "936955084e861a2c76032f3219e888b6c83c137c2ad6f0ba744ad7f65c5492a3"
|
|
9486
9486
|
},
|
|
9487
9487
|
{
|
|
9488
9488
|
"path": "scripts/bongos-mcp.js",
|
|
@@ -10342,7 +10342,7 @@
|
|
|
10342
10342
|
{
|
|
10343
10343
|
"path": "scripts/gds/module.js",
|
|
10344
10344
|
"mode": "0000644",
|
|
10345
|
-
"sha256": "
|
|
10345
|
+
"sha256": "240a50d80078e81b692cea24d823340d797f3193afd6bec0dcb5d9afb0669c54"
|
|
10346
10346
|
},
|
|
10347
10347
|
{
|
|
10348
10348
|
"path": "scripts/gds/move-escalation.js",
|
|
@@ -11147,7 +11147,7 @@
|
|
|
11147
11147
|
{
|
|
11148
11148
|
"path": "scripts/gds/upgrade.js",
|
|
11149
11149
|
"mode": "0000644",
|
|
11150
|
-
"sha256": "
|
|
11150
|
+
"sha256": "0646b9d86c01d190ed1b670694d2585827241b94792c70852b670a06327afb40"
|
|
11151
11151
|
},
|
|
11152
11152
|
{
|
|
11153
11153
|
"path": "scripts/gds/validate-design.js",
|
|
@@ -11437,7 +11437,7 @@
|
|
|
11437
11437
|
{
|
|
11438
11438
|
"path": "src/bongos/module-store.js",
|
|
11439
11439
|
"mode": "0000644",
|
|
11440
|
-
"sha256": "
|
|
11440
|
+
"sha256": "4d4cb32e7fdc80c2f31c76a04d939c62980515eb06f7d3862a91c5163a9a70ce"
|
|
11441
11441
|
},
|
|
11442
11442
|
{
|
|
11443
11443
|
"path": "src/bongos/module-submissions.js",
|
|
@@ -11587,7 +11587,7 @@
|
|
|
11587
11587
|
{
|
|
11588
11588
|
"path": "src/bongos/routes/modules.js",
|
|
11589
11589
|
"mode": "0000644",
|
|
11590
|
-
"sha256": "
|
|
11590
|
+
"sha256": "abf74a67f9706eed2d8134c8bff0e1ab55831493aeb4217e5a0f16326e85c64b"
|
|
11591
11591
|
},
|
|
11592
11592
|
{
|
|
11593
11593
|
"path": "src/bongos/routes/my-sessions.js",
|
|
@@ -11662,7 +11662,7 @@
|
|
|
11662
11662
|
{
|
|
11663
11663
|
"path": "src/module-api.js",
|
|
11664
11664
|
"mode": "0000644",
|
|
11665
|
-
"sha256": "
|
|
11665
|
+
"sha256": "4ac89563e04b58f5b2a3662990aeb1916f614ba099fe1414f10a51865168cb48"
|
|
11666
11666
|
},
|
|
11667
11667
|
{
|
|
11668
11668
|
"path": "src/module-loader/catalog.js",
|
|
@@ -14742,7 +14742,7 @@
|
|
|
14742
14742
|
{
|
|
14743
14743
|
"path": "tests/module_cli.mjs",
|
|
14744
14744
|
"mode": "0000644",
|
|
14745
|
-
"sha256": "
|
|
14745
|
+
"sha256": "18fa817150d1d390f5f1c44651aea7e667227c8063393b2d0f82ff0b9f7fbaa8"
|
|
14746
14746
|
},
|
|
14747
14747
|
{
|
|
14748
14748
|
"path": "tests/module_contributions.mjs",
|
|
@@ -14824,6 +14824,11 @@
|
|
|
14824
14824
|
"mode": "0000644",
|
|
14825
14825
|
"sha256": "1b50c9f68bfe731b29ddcce6947847b6e6f3f7bc5b0a08bbacc4afb25d5a3d90"
|
|
14826
14826
|
},
|
|
14827
|
+
{
|
|
14828
|
+
"path": "tests/module_upgrade_updates.mjs",
|
|
14829
|
+
"mode": "0000644",
|
|
14830
|
+
"sha256": "7260dabcf71a69d4ce41609e8a88c94c112fdda354f774dadf0a2648f4feacaf"
|
|
14831
|
+
},
|
|
14827
14832
|
{
|
|
14828
14833
|
"path": "tests/modules.mjs",
|
|
14829
14834
|
"mode": "0000644",
|
|
@@ -5,7 +5,7 @@ A **generated**, zero-dependency typed client for the Bongos API — produced fr
|
|
|
5
5
|
by hand; it regenerates when the spec changes, so it can never drift from the routes.
|
|
6
6
|
|
|
7
7
|
- API version: **v1** (served at `/api/bongos/v1`)
|
|
8
|
-
-
|
|
8
|
+
- 486 operations across 73 resource groups
|
|
9
9
|
|
|
10
10
|
## Use it from your project
|
|
11
11
|
|
|
@@ -1087,6 +1087,8 @@ function createClient(opts = {}) {
|
|
|
1087
1087
|
getStoreModulesKeyVersionsVersionHowto: (args) => request("GET", "/store/modules/{key}/versions/{version}/howto", { hasBody: false }, args),
|
|
1088
1088
|
// GET /store/modules/{key}/versions/{version}/tarball — rank: any-builder — GET /store/modules/:key/versions/:version/tarball
|
|
1089
1089
|
getStoreModulesKeyVersionsVersionTarball: (args) => request("GET", "/store/modules/{key}/versions/{version}/tarball", { hasBody: false }, args),
|
|
1090
|
+
// GET /store/modules/latest — rank: any-builder — GET /store/modules/latest
|
|
1091
|
+
getStoreModulesLatest: (args) => request("GET", "/store/modules/latest", { hasBody: false }, args),
|
|
1090
1092
|
},
|
|
1091
1093
|
"taskRecommendations": {
|
|
1092
1094
|
// POST /task-recommendations — rank: any-builder — POST /task-recommendations
|
|
@@ -1086,6 +1086,8 @@ function createClient(opts = {}) {
|
|
|
1086
1086
|
getStoreModulesKeyVersionsVersionHowto: (args) => request("GET", "/store/modules/{key}/versions/{version}/howto", { hasBody: false }, args),
|
|
1087
1087
|
// GET /store/modules/{key}/versions/{version}/tarball — rank: any-builder — GET /store/modules/:key/versions/:version/tarball
|
|
1088
1088
|
getStoreModulesKeyVersionsVersionTarball: (args) => request("GET", "/store/modules/{key}/versions/{version}/tarball", { hasBody: false }, args),
|
|
1089
|
+
// GET /store/modules/latest — rank: any-builder — GET /store/modules/latest
|
|
1090
|
+
getStoreModulesLatest: (args) => request("GET", "/store/modules/latest", { hasBody: false }, args),
|
|
1089
1091
|
},
|
|
1090
1092
|
"taskRecommendations": {
|
|
1091
1093
|
// POST /task-recommendations — rank: any-builder — POST /task-recommendations
|
|
@@ -167,6 +167,7 @@ export interface GetSpecialitiesResponse { specialities: unknown }
|
|
|
167
167
|
export interface GetSsoPubkeyResponse { alg: unknown; format: unknown; public_key_b64: unknown }
|
|
168
168
|
export interface GetStoreEntitlementsResponse { entitlements: unknown }
|
|
169
169
|
export interface GetStoreModulesKeyVersionsVersionHowtoResponse { module_key: unknown; title: unknown; version: unknown; published_at: unknown; delisted: unknown; markdown: unknown; artifact_url: unknown }
|
|
170
|
+
export interface GetStoreModulesLatestResponse { latest: unknown }
|
|
170
171
|
export interface GetTaskRecommendationsForMeResponse { recommendations: unknown; count: unknown }
|
|
171
172
|
export interface GetTaskRecommendationsMineResponse { recommendations: unknown; count: unknown }
|
|
172
173
|
export interface GetTasksClaimableResponse { claimable: unknown; claimable_total: unknown; active_claims: unknown }
|
|
@@ -1533,6 +1534,8 @@ export interface BongosClient {
|
|
|
1533
1534
|
getStoreModulesKeyVersionsVersionHowto(args?: RequestArgs): Promise<GetStoreModulesKeyVersionsVersionHowtoResponse>;
|
|
1534
1535
|
/** GET /store/modules/{key}/versions/{version}/tarball — rank: any-builder */
|
|
1535
1536
|
getStoreModulesKeyVersionsVersionTarball(args?: RequestArgs): Promise<ApiResponse>;
|
|
1537
|
+
/** GET /store/modules/latest — rank: any-builder */
|
|
1538
|
+
getStoreModulesLatest(args?: RequestArgs): Promise<GetStoreModulesLatestResponse>;
|
|
1536
1539
|
};
|
|
1537
1540
|
"taskRecommendations": {
|
|
1538
1541
|
/** POST /task-recommendations — rank: any-builder */
|
|
@@ -1083,6 +1083,8 @@ export function createClient(opts = {}) {
|
|
|
1083
1083
|
getStoreModulesKeyVersionsVersionHowto: (args) => request("GET", "/store/modules/{key}/versions/{version}/howto", { hasBody: false }, args),
|
|
1084
1084
|
// GET /store/modules/{key}/versions/{version}/tarball — rank: any-builder — GET /store/modules/:key/versions/:version/tarball
|
|
1085
1085
|
getStoreModulesKeyVersionsVersionTarball: (args) => request("GET", "/store/modules/{key}/versions/{version}/tarball", { hasBody: false }, args),
|
|
1086
|
+
// GET /store/modules/latest — rank: any-builder — GET /store/modules/latest
|
|
1087
|
+
getStoreModulesLatest: (args) => request("GET", "/store/modules/latest", { hasBody: false }, args),
|
|
1086
1088
|
},
|
|
1087
1089
|
"taskRecommendations": {
|
|
1088
1090
|
// POST /task-recommendations — rank: any-builder — POST /task-recommendations
|
package/docs/api/openapi.json
CHANGED
|
@@ -19794,6 +19794,44 @@
|
|
|
19794
19794
|
]
|
|
19795
19795
|
}
|
|
19796
19796
|
},
|
|
19797
|
+
"/store/modules/latest": {
|
|
19798
|
+
"get": {
|
|
19799
|
+
"operationId": "get_store_modules_latest",
|
|
19800
|
+
"tags": [
|
|
19801
|
+
"store"
|
|
19802
|
+
],
|
|
19803
|
+
"summary": "GET /store/modules/latest",
|
|
19804
|
+
"description": "The newest published version of each named module — what `bongos upgrade` compares an instance's installed store modules against to report the updates available (task 1003787). `?keys=a,b,c`, at most STORE_LATEST_MAX_KEYS. rank: any-builder — a read of public store metadata (version numbers and the core range each needs, the same facts the how-to page shows). It takes no entitlement and changes nothing, unlike acquire; a key the store does not know is simply absent from the answer.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
|
|
19805
|
+
"x-rank": "any-builder",
|
|
19806
|
+
"x-source": "src/bongos/routes/modules.js",
|
|
19807
|
+
"responses": {
|
|
19808
|
+
"200": {
|
|
19809
|
+
"description": "Success.",
|
|
19810
|
+
"content": {
|
|
19811
|
+
"application/json": {
|
|
19812
|
+
"schema": {
|
|
19813
|
+
"$ref": "#/components/schemas/GetStoreModulesLatestResponse"
|
|
19814
|
+
}
|
|
19815
|
+
}
|
|
19816
|
+
}
|
|
19817
|
+
},
|
|
19818
|
+
"400": {
|
|
19819
|
+
"$ref": "#/components/responses/BadRequest"
|
|
19820
|
+
},
|
|
19821
|
+
"401": {
|
|
19822
|
+
"$ref": "#/components/responses/Unauthorized"
|
|
19823
|
+
},
|
|
19824
|
+
"403": {
|
|
19825
|
+
"$ref": "#/components/responses/Forbidden"
|
|
19826
|
+
}
|
|
19827
|
+
},
|
|
19828
|
+
"security": [
|
|
19829
|
+
{
|
|
19830
|
+
"builderSession": []
|
|
19831
|
+
}
|
|
19832
|
+
]
|
|
19833
|
+
}
|
|
19834
|
+
},
|
|
19797
19835
|
"/store/modules/{key}/acquire": {
|
|
19798
19836
|
"post": {
|
|
19799
19837
|
"operationId": "post_store_modules_key_acquire",
|
|
@@ -24851,6 +24889,15 @@
|
|
|
24851
24889
|
"artifact_url"
|
|
24852
24890
|
]
|
|
24853
24891
|
},
|
|
24892
|
+
"GetStoreModulesLatestResponse": {
|
|
24893
|
+
"type": "object",
|
|
24894
|
+
"properties": {
|
|
24895
|
+
"latest": {}
|
|
24896
|
+
},
|
|
24897
|
+
"required": [
|
|
24898
|
+
"latest"
|
|
24899
|
+
]
|
|
24900
|
+
},
|
|
24854
24901
|
"GetTaskRecommendationsForMeResponse": {
|
|
24855
24902
|
"type": "object",
|
|
24856
24903
|
"properties": {
|
|
@@ -30997,9 +31044,9 @@
|
|
|
30997
31044
|
"description": "A required dependency/feature is not configured or is temporarily down."
|
|
30998
31045
|
}
|
|
30999
31046
|
},
|
|
31000
|
-
"x-endpoint-count":
|
|
31001
|
-
"x-schema-count":
|
|
31047
|
+
"x-endpoint-count": 486,
|
|
31048
|
+
"x-schema-count": 514,
|
|
31002
31049
|
"x-undocumented-bodies": 12,
|
|
31003
|
-
"x-response-schemas":
|
|
31050
|
+
"x-response-schemas": 344,
|
|
31004
31051
|
"x-generated-by": "scripts/gds/gen-api-docs.js"
|
|
31005
31052
|
}
|
package/docs/api-reference.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Bongos API reference
|
|
4
4
|
|
|
5
|
-
> **Generated from the live route files** — the route file is authoritative.
|
|
5
|
+
> **Generated from the live route files** — the route file is authoritative. 486 endpoints across 89 route files.
|
|
6
6
|
> Machine-readable spec: [`docs/api/openapi.json`](api/openapi.json) (OpenAPI 3.1). Rendered docs site: **`/docs`** (e.g. `cloudbongos.com/docs`).
|
|
7
7
|
|
|
8
8
|
Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust-boundary-server-enforced-permissions.md)): `public` < `any-builder` < `metic+archon` < `archon`.
|
|
@@ -769,7 +769,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
|
|
|
769
769
|
| GET | `/api/bongos/sso/pubkey` | `public` | — | GET /sso/pubkey — the hub's Ed25519 assertion-verification key (public). |
|
|
770
770
|
| POST | `/api/bongos/sso/token` | `public` | `client_id`, `client_secret`, `code`, `redirect_uri` | POST /sso/token — server-to-server code redemption. |
|
|
771
771
|
|
|
772
|
-
## `store` (
|
|
772
|
+
## `store` (8)
|
|
773
773
|
|
|
774
774
|
| Method | Path | Rank | Body | Description |
|
|
775
775
|
|---|---|---|---|---|
|
|
@@ -780,6 +780,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
|
|
|
780
780
|
| POST | `/api/bongos/store/modules/:key/versions` | `metic+archon` | _undocumented_ | POST /api/bongos/store/modules/:key/versions — publish one module version to the store (task 1004271, ADR 0338 D1). |
|
|
781
781
|
| GET | `/api/bongos/store/modules/:key/versions/:version/howto` | `any-builder` | — | GET /api/bongos/store/modules/:key/versions/:version/howto — the how-to a version shipped with (task 1004366, ADR 0347 D1/D3), read from … |
|
|
782
782
|
| GET | `/api/bongos/store/modules/:key/versions/:version/tarball` | `any-builder` | — | |
|
|
783
|
+
| GET | `/api/bongos/store/modules/latest` | `any-builder` | — | The newest published version of each named module — what `bongos upgrade` compares an instance's installed store modules against to repor… |
|
|
783
784
|
|
|
784
785
|
## `task-recommendations` (4)
|
|
785
786
|
|
|
@@ -2795,5 +2795,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
2795
2795
|
landed since 1.20.67 with no explicit bump. run 36871565601. (task 1002620)
|
|
2796
2796
|
1.20.69 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2797
2797
|
landed since 1.20.68 with no explicit bump. run 36873353617. (task 1002620)
|
|
2798
|
+
1.20.70 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
2799
|
+
landed since 1.20.69 with no explicit bump. run 36877499272. (task 1002620)
|
|
2798
2800
|
---------------------------------------------------------------------------
|
|
2799
2801
|
```
|
package/docs/modules-contract.md
CHANGED
|
@@ -380,6 +380,8 @@ Run `bongos upgrade` to confirm coreVersion compatibility, then `node scripts/gd
|
|
|
380
380
|
- The new version is written beside the old one and swapped in; any failure leaves the installed version in place. Files the new version dropped are gone afterwards.
|
|
381
381
|
- The on/off switch is untouched. A module bundled with the core is refused; it updates with the core (`bongos upgrade`).
|
|
382
382
|
|
|
383
|
+
**`bongos upgrade` says which installed modules have an update** (task 1003787), below its compatibility rows, so one command answers "what is stale here" for the core and its modules. It asks `GET /store/modules/latest?keys=…` for the newest version of every module here with an exact version — a read that takes no entitlement — and lists each newer one with `bongos module update <key>`, or says it waits for the core when that version needs a newer one. A delisted module is flagged as getting no further versions (ADR 0338 D2). None of this changes the exit code: an incompatible enabled module still blocks and nothing else does. With no session or no reachable store it says "not checked" and the pre-check stands; `--no-store` skips the read.
|
|
384
|
+
|
|
383
385
|
## Carving an existing core domain (the `db.js` per-domain pattern)
|
|
384
386
|
|
|
385
387
|
Most BONGOS-V1 module work is not scaffolding a *new* module — it's **carving an existing core domain out of the monolith** (ADR 0091, criterion C7). The reference carve is the kernel slice `src/bongos/db-kernel.js` (BV1.R69); the feature precedents are `modules/discord/db.js` + `modules/game/db.js`. Every later carve (memory R72, grading R73, …) copies this recipe. The invariant is **OTB byte-identical** at every step — you are *moving* code, not changing behavior.
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.70",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.20.
|
|
9
|
+
"version": "1.20.70",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.70",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -8543,5 +8543,11 @@
|
|
|
8543
8543
|
"id": "1004490",
|
|
8544
8544
|
"text": "Session notes for today's account-page work are on file, so the next session can pick up without re-discovering what was done."
|
|
8545
8545
|
}
|
|
8546
|
+
],
|
|
8547
|
+
"1.20.70": [
|
|
8548
|
+
{
|
|
8549
|
+
"id": "1003787",
|
|
8550
|
+
"text": "Running 'bongos upgrade' now also tells you which installed modules have a newer version in the store, and the command to update each one. It never stops an upgrade on that account."
|
|
8551
|
+
}
|
|
8546
8552
|
]
|
|
8547
8553
|
}
|
package/scripts/gds/module.js
CHANGED
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
// bongos module new <key> scaffold modules/<key>/
|
|
59
59
|
// bongos module new <key> --force overwrite an existing modules/<key>/
|
|
60
60
|
// bongos module list [--catalog] browse the module catalog (author + origin credit)
|
|
61
|
-
// bongos upgrade
|
|
61
|
+
// bongos upgrade [--no-store] pre-check enabled modules vs the core version + list store updates
|
|
62
62
|
// bongos module check <key> [--sign-off "Name <email>"] AGPL/publish pre-check + DCO sign-off
|
|
63
63
|
// bongos module install <key> [--version X.Y.Z] acquire from the store, verify, place (not enabled)
|
|
64
64
|
// bongos module update <key> [--version X.Y.Z] move an installed store module forward
|
|
@@ -371,10 +371,10 @@ function gatherUpgrade({ modulesDir = MODULES_DIR, core = coreVersion(), env = p
|
|
|
371
371
|
|
|
372
372
|
const rows = [];
|
|
373
373
|
for (const { key, manifest } of loaded) {
|
|
374
|
-
rows.push({ key, coreVersion: manifest.coreVersion, enabled: enabled[key] === true, satisfied: true, ...upkeep(manifest) });
|
|
374
|
+
rows.push({ key, version: manifest.version || null, coreVersion: manifest.coreVersion, enabled: enabled[key] === true, satisfied: true, ...upkeep(manifest) });
|
|
375
375
|
}
|
|
376
376
|
for (const { key, manifest, reason } of skippedFull) {
|
|
377
|
-
rows.push({ key, coreVersion: manifest ? manifest.coreVersion : null, enabled: enabled[key] === true, satisfied: false, reason, ...upkeep(manifest) });
|
|
377
|
+
rows.push({ key, version: (manifest && manifest.version) || null, coreVersion: manifest ? manifest.coreVersion : null, enabled: enabled[key] === true, satisfied: false, reason, ...upkeep(manifest) });
|
|
378
378
|
}
|
|
379
379
|
rows.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
|
|
380
380
|
return { rows, errors, core };
|
|
@@ -663,7 +663,76 @@ function cmdNew(args, { log = console.log, errlog = console.error } = {}) {
|
|
|
663
663
|
return 0;
|
|
664
664
|
}
|
|
665
665
|
|
|
666
|
-
|
|
666
|
+
// ---------------------------------------------------------------------------
|
|
667
|
+
// Available module updates — the store half of `bongos upgrade` (task 1003787)
|
|
668
|
+
// ---------------------------------------------------------------------------
|
|
669
|
+
|
|
670
|
+
// Ask the store for the newest version of every module here that carries an exact
|
|
671
|
+
// version (the I/O half). A key the store does not know is absent from the answer,
|
|
672
|
+
// so a module built here, or one the core ships, simply has nothing to report.
|
|
673
|
+
// Never throws: no session or an unreachable store comes back as
|
|
674
|
+
// { checked: false, reason }, because the compatibility pre-check must work
|
|
675
|
+
// offline and must never fail on the store's account.
|
|
676
|
+
async function gatherUpdates(rows, { request } = {}) {
|
|
677
|
+
const keys = rows.filter((r) => r.version && parseVersion(String(r.version))).map((r) => r.key);
|
|
678
|
+
if (!keys.length) return { checked: true, latest: {} };
|
|
679
|
+
let call;
|
|
680
|
+
try { call = request || storeRequest(); }
|
|
681
|
+
catch (e) { return { checked: false, reason: e.message }; }
|
|
682
|
+
let res;
|
|
683
|
+
try { res = await call('GET', `/store/modules/latest?keys=${keys.map(encodeURIComponent).join(',')}`); }
|
|
684
|
+
catch (e) { return { checked: false, reason: `the store could not be reached — ${e.message}` }; }
|
|
685
|
+
// A 404 here is a server whose core predates this route, not a missing module.
|
|
686
|
+
if (res.status === 404) return { checked: false, reason: "this server's store does not answer update checks yet (its core is older)" };
|
|
687
|
+
if (!res.ok) return { checked: false, reason: `the store answered ${storeError(res)}` };
|
|
688
|
+
return { checked: true, latest: (res.data && res.data.latest) || {} };
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Decide what is stale from rows + the store's answer (pure — tests drive it).
|
|
692
|
+
// UPDATES = a module whose store version is newer than the one installed; `fits`
|
|
693
|
+
// says whether that newer version can mount on this core (when it cannot, the
|
|
694
|
+
// update waits for the core — it is not something to run now). DELISTED = an
|
|
695
|
+
// installed module the store has delisted: it keeps working, but no further
|
|
696
|
+
// versions will come (ADR 0338 D2). Neither ever blocks — an update is news, and
|
|
697
|
+
// what blocks `bongos upgrade` is still only an incompatible ENABLED module.
|
|
698
|
+
function evaluateUpdates(rows = [], latest = {}, core) {
|
|
699
|
+
const { satisfies } = require('../../src/module-loader/semver');
|
|
700
|
+
const updates = [];
|
|
701
|
+
const delisted = [];
|
|
702
|
+
for (const r of rows) {
|
|
703
|
+
const store = latest[r.key];
|
|
704
|
+
const have = r.version && parseVersion(String(r.version));
|
|
705
|
+
if (!store || !have) continue;
|
|
706
|
+
if (store.status === 'delisted') { delisted.push({ key: r.key, installed: r.version, enabled: r.enabled }); continue; }
|
|
707
|
+
const next = parseVersion(String(store.version));
|
|
708
|
+
if (!next || cmp(next, have) <= 0) continue;
|
|
709
|
+
let fits = false;
|
|
710
|
+
try { fits = !!(core && store.core_version && satisfies(core, store.core_version)); } catch { fits = false; }
|
|
711
|
+
updates.push({ key: r.key, installed: r.version, latest: store.version, coreVersion: store.core_version, enabled: r.enabled, fits });
|
|
712
|
+
}
|
|
713
|
+
return { updates, delisted };
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function reportUpdates(found, verdict, { log, errlog }) {
|
|
717
|
+
log('\nModule updates (from the store):');
|
|
718
|
+
if (!found.checked) {
|
|
719
|
+
log(` not checked — ${found.reason}. The compatibility check above still stands.`);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const { updates, delisted } = evaluateUpdates(verdict.rows, found.latest, verdict.core);
|
|
723
|
+
for (const u of updates) {
|
|
724
|
+
const how = u.fits
|
|
725
|
+
? `run \`bongos module update ${u.key}\``
|
|
726
|
+
: `needs core ${u.coreVersion}; this core is ${verdict.core}, so it waits until the core moves`;
|
|
727
|
+
log(` ↑ ${u.key.padEnd(18)} ${u.installed} → ${u.latest} — ${how}`);
|
|
728
|
+
}
|
|
729
|
+
for (const d of delisted) {
|
|
730
|
+
errlog(` ! ${d.key.padEnd(18)} ${d.installed} — delisted in the store: it keeps working, but no further versions will come`);
|
|
731
|
+
}
|
|
732
|
+
if (!updates.length && !delisted.length) log(' every installed store module is on its newest version.');
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
async function cmdUpgrade(args = [], { log = console.log, errlog = console.error, gather = gatherUpgrade, checkUpdates = gatherUpdates } = {}) {
|
|
667
736
|
const data = gather();
|
|
668
737
|
const verdict = evaluateUpgrade(data);
|
|
669
738
|
|
|
@@ -701,6 +770,11 @@ function cmdUpgrade(_args, { log = console.log, errlog = console.error, gather =
|
|
|
701
770
|
errlog(` ! ${verdict.undeclared.length} module(s) declare no \`maintenance\` block — nobody is on the hook for them as the core advances (ADR 0166): ${verdict.undeclared.map((r) => r.key).join(', ')}`);
|
|
702
771
|
}
|
|
703
772
|
|
|
773
|
+
// What is stale here, beside what is broken: the store's newer versions of the
|
|
774
|
+
// modules installed from it. Reported either way, and it never changes the exit
|
|
775
|
+
// code. `--no-store` skips the read (offline, or a CI pre-check).
|
|
776
|
+
if (!args.includes('--no-store')) reportUpdates(await checkUpdates(verdict.rows), verdict, { log, errlog });
|
|
777
|
+
|
|
704
778
|
if (!verdict.ok) {
|
|
705
779
|
errlog(`\nREFUSING: ${verdict.blocking.length} enabled module(s) incompatible with core ${verdict.core}: ${verdict.blocking.map((b) => b.key).join(', ')}.`);
|
|
706
780
|
errlog(`Widen each module's coreVersion to a range that includes ${verdict.core} (or hold the core upgrade) before proceeding.`);
|
|
@@ -1200,7 +1274,8 @@ function printHelp(out = console.log) {
|
|
|
1200
1274
|
out(' bongos module new <key> Scaffold modules/<key>/ (manifest + skeleton + sample route + HOWTO.md + CLAUDE.md)');
|
|
1201
1275
|
out(' bongos module list [--catalog] [--search <text>] [--json]');
|
|
1202
1276
|
out(' Browse the module catalog — what you can enable, with author + origin credit');
|
|
1203
|
-
out(' bongos upgrade
|
|
1277
|
+
out(' bongos upgrade [--no-store] Pre-check enabled modules against the core version (fail-closed),');
|
|
1278
|
+
out(' then list newer store versions of installed modules (never blocks)');
|
|
1204
1279
|
out(' bongos module check <key> [--sign-off "Name <email>"]');
|
|
1205
1280
|
out(' AGPL/publish pre-check before upstreaming + record a DCO-style sign-off');
|
|
1206
1281
|
out(' bongos module submit <key> [--sign-off "Name <email>"]');
|
|
@@ -1236,7 +1311,7 @@ async function main(argv) {
|
|
|
1236
1311
|
module.exports = {
|
|
1237
1312
|
main, cmdNew, cmdList, cmdUpgrade, cmdCheck, cmdSubmit, cmdPublish, cmdInstall, cmdUpdate, printHelp,
|
|
1238
1313
|
gatherCatalog, formatCatalog, ownProjectName,
|
|
1239
|
-
scaffoldModule, gatherUpgrade, evaluateUpgrade, buildManifest,
|
|
1314
|
+
scaffoldModule, gatherUpgrade, evaluateUpgrade, gatherUpdates, evaluateUpdates, buildManifest,
|
|
1240
1315
|
formatMaintenanceHint, isPastRemoveAfter,
|
|
1241
1316
|
checkModulePublishability, recordSignOff, readSignOff, DCO_STATEMENT,
|
|
1242
1317
|
listModuleFiles, packageModuleForSubmission,
|
|
@@ -1245,8 +1320,11 @@ module.exports = {
|
|
|
1245
1320
|
};
|
|
1246
1321
|
|
|
1247
1322
|
if (require.main === module) {
|
|
1323
|
+
// Set the exit code and drain, never a hard exit: install, update and upgrade end
|
|
1324
|
+
// after a store request, and a hard exit there aborts natively on Windows + Node 24
|
|
1325
|
+
// (task 1002734).
|
|
1248
1326
|
Promise.resolve(main(process.argv)).then(
|
|
1249
|
-
(code) => process.
|
|
1327
|
+
(code) => { process.exitCode = code; },
|
|
1250
1328
|
(e) => { console.error(e); process.exit(1); }
|
|
1251
1329
|
);
|
|
1252
1330
|
}
|
package/scripts/gds/upgrade.js
CHANGED
|
@@ -1495,5 +1495,5 @@ module.exports = {
|
|
|
1495
1495
|
};
|
|
1496
1496
|
|
|
1497
1497
|
if (require.main === module) {
|
|
1498
|
-
main().then((code) => process.exit(code)).catch((e) => { process.stderr.write(`✖ ${e.message}\n`); process.exit(1); });
|
|
1498
|
+
main().then((code) => { if (arg('--to', process.argv.slice(2))) process.exit(code); else process.exitCode = code; }).catch((e) => { process.stderr.write(`✖ ${e.message}\n`); process.exit(1); }); // bare pre-check drains: it ends after a store read (task 1003787), and a hard exit there aborts on Windows (task 1002734)
|
|
1499
1499
|
}
|
|
@@ -208,6 +208,30 @@ async function publishedVersionSet(keys, { db } = {}) {
|
|
|
208
208
|
return new Set(rows.map((r) => `${r.module_key}@${r.version}`));
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
// The newest published version of each of these modules, for `bongos upgrade`'s
|
|
212
|
+
// "updates available" report (task 1003787). Read-only: unlike resolveAcquirable it
|
|
213
|
+
// takes no entitlement and refuses nothing, so a delisted module still answers,
|
|
214
|
+
// with its status, and the caller says it gets no further versions (ADR 0338 D2).
|
|
215
|
+
// A key the store does not know is simply absent. One query for the whole list.
|
|
216
|
+
// Returns { [key]: { version, core_version, status } }.
|
|
217
|
+
async function latestVersions(keys, { db } = {}) {
|
|
218
|
+
if (!keys.length) return {};
|
|
219
|
+
const pool = db || require('./pool').pool;
|
|
220
|
+
const { rows } = await pool.query(
|
|
221
|
+
`SELECT v.module_key, v.version, v.core_version, m.status
|
|
222
|
+
FROM store_module_versions v JOIN store_modules m ON m.module_key = v.module_key
|
|
223
|
+
WHERE v.module_key = ANY($1)`, [keys]);
|
|
224
|
+
const out = {};
|
|
225
|
+
for (const r of rows) {
|
|
226
|
+
const next = parseVersion(r.version);
|
|
227
|
+
if (!next) continue;
|
|
228
|
+
const have = out[r.module_key];
|
|
229
|
+
if (have && cmp(parseVersion(have.version), next) >= 0) continue;
|
|
230
|
+
out[r.module_key] = { version: r.version, core_version: r.core_version, status: r.status };
|
|
231
|
+
}
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
|
|
211
235
|
// The tarball on disk for a version row. artifact_path is relative to the store dir
|
|
212
236
|
// (relativeArtifactPath), and artifactPath() re-validates key + version, so a row
|
|
213
237
|
// can never point a read outside the store.
|
|
@@ -221,5 +245,5 @@ function versionArtifactFile(row, { dir = storeDir() } = {}) {
|
|
|
221
245
|
|
|
222
246
|
module.exports = {
|
|
223
247
|
stageArtifact, commitArtifact, removeArtifact, relativeArtifactPath, publishVersion,
|
|
224
|
-
isFreePrice, resolveAcquirable, resolveReadable, publishedVersionSet, versionArtifactFile,
|
|
248
|
+
isFreePrice, resolveAcquirable, resolveReadable, publishedVersionSet, latestVersions, versionArtifactFile,
|
|
225
249
|
};
|
|
@@ -81,7 +81,7 @@ const moduleArtifact = require('../../../scripts/gds/module-artifact');
|
|
|
81
81
|
const { checkEntitlement, listEntitlements, grantEntitlement, recordAcquired } = require('../module-entitlements');
|
|
82
82
|
const {
|
|
83
83
|
stageArtifact, commitArtifact, removeArtifact, relativeArtifactPath, publishVersion,
|
|
84
|
-
resolveAcquirable, resolveReadable, publishedVersionSet, versionArtifactFile,
|
|
84
|
+
resolveAcquirable, resolveReadable, publishedVersionSet, latestVersions, versionArtifactFile,
|
|
85
85
|
} = require('../module-store');
|
|
86
86
|
const { KEY_RE } = require('../../module-loader/manifest-schema');
|
|
87
87
|
const { parseVersion } = require('../../module-loader/semver');
|
|
@@ -154,6 +154,11 @@ function listModules() {
|
|
|
154
154
|
// the oldest entry is dropped.
|
|
155
155
|
const HOWTO_CACHE_MAX = 200;
|
|
156
156
|
|
|
157
|
+
// How many module keys one GET /store/modules/latest may ask about (task 1003787).
|
|
158
|
+
// An instance installs a handful; the cap keeps the query string and the ANY()
|
|
159
|
+
// list bounded.
|
|
160
|
+
const STORE_LATEST_MAX_KEYS = 200;
|
|
161
|
+
|
|
157
162
|
module.exports = function buildModulesRouter() {
|
|
158
163
|
const router = express.Router();
|
|
159
164
|
// Checked how-tos by "key@version:tarball_sha256". Per router, so a test's fresh
|
|
@@ -565,6 +570,26 @@ module.exports = function buildModulesRouter() {
|
|
|
565
570
|
});
|
|
566
571
|
}));
|
|
567
572
|
|
|
573
|
+
// The newest published version of each named module — what `bongos upgrade`
|
|
574
|
+
// compares an instance's installed store modules against to report the updates
|
|
575
|
+
// available (task 1003787). `?keys=a,b,c`, at most STORE_LATEST_MAX_KEYS.
|
|
576
|
+
//
|
|
577
|
+
// rank: any-builder — a read of public store metadata (version numbers and the
|
|
578
|
+
// core range each needs, the same facts the how-to page shows). It takes no
|
|
579
|
+
// entitlement and changes nothing, unlike acquire; a key the store does not know
|
|
580
|
+
// is simply absent from the answer.
|
|
581
|
+
router.get('/store/modules/latest', auth.requireBuilder,
|
|
582
|
+
asyncHandler('GET /store/modules/latest', async (req, res) => {
|
|
583
|
+
const raw = typeof req.query.keys === 'string' ? req.query.keys : '';
|
|
584
|
+
const keys = [...new Set(raw.split(',').map((k) => k.trim()).filter(Boolean))];
|
|
585
|
+
if (!keys.length || keys.length > STORE_LATEST_MAX_KEYS || !keys.every((k) => KEY_RE.test(k))) {
|
|
586
|
+
return res.fail('bad_request', { status: 400, message: `keys is a comma-separated list of 1 to ${STORE_LATEST_MAX_KEYS} module keys (lowercase kebab-case).` });
|
|
587
|
+
}
|
|
588
|
+
const latest = await latestVersions(keys);
|
|
589
|
+
res.set('Cache-Control', 'no-store');
|
|
590
|
+
res.json({ latest });
|
|
591
|
+
}));
|
|
592
|
+
|
|
568
593
|
router.post('/store/modules/:key/acquired', auth.requireBuilder,
|
|
569
594
|
asyncHandler('POST /store/modules/:key/acquired', async (req, res) => {
|
|
570
595
|
const { key } = req.params;
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.20.
|
|
78
|
+
const CORE_VERSION = '1.20.70'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
package/tests/module_cli.mjs
CHANGED
|
@@ -156,14 +156,14 @@ test('gatherUpgrade: a satisfied enabled module passes; the same module disabled
|
|
|
156
156
|
loader._reset();
|
|
157
157
|
});
|
|
158
158
|
|
|
159
|
-
test('cmdUpgrade: exit 0 with no modules, exit 1 when an enabled module is incompatible', () => {
|
|
159
|
+
test('cmdUpgrade: exit 0 with no modules, exit 1 when an enabled module is incompatible', async () => {
|
|
160
160
|
const lines = [];
|
|
161
161
|
const sink = { log: (s) => lines.push(String(s)), errlog: (s) => lines.push(String(s)) };
|
|
162
162
|
|
|
163
|
-
const empty = mod.cmdUpgrade([], { ...sink, gather: () => ({ rows: [], errors: [], core: '1.0.0' }) });
|
|
163
|
+
const empty = await mod.cmdUpgrade([], { checkUpdates: async () => ({ checked: true, latest: {} }), ...sink, gather: () => ({ rows: [], errors: [], core: '1.0.0' }) });
|
|
164
164
|
assert.equal(empty, 0);
|
|
165
165
|
|
|
166
|
-
const bad = mod.cmdUpgrade([], {
|
|
166
|
+
const bad = await mod.cmdUpgrade([], { checkUpdates: async () => ({ checked: true, latest: {} }),
|
|
167
167
|
...sink,
|
|
168
168
|
gather: () => ({ core: '1.0.0', errors: [], rows: [{ key: 'future', version: '1.0.0', coreVersion: '^2.0.0', enabled: true, satisfied: false }] }),
|
|
169
169
|
});
|
|
@@ -474,13 +474,13 @@ test('gatherUpgrade: carries each module\'s maintenance posture onto its row', (
|
|
|
474
474
|
loader._reset();
|
|
475
475
|
});
|
|
476
476
|
|
|
477
|
-
test('cmdUpgrade: a DISABLED incompatible module is reported with its owner, not silently dropped', () => {
|
|
477
|
+
test('cmdUpgrade: a DISABLED incompatible module is reported with its owner, not silently dropped', async () => {
|
|
478
478
|
// The catalog-rot case: an accepted module ships default:false (ADR 0107 §6), so
|
|
479
479
|
// every rotted catalog entry is disabled by construction. Exit stays 0 — it breaks
|
|
480
480
|
// nothing at boot — but it must be visible and attributed.
|
|
481
481
|
const lines = [];
|
|
482
482
|
const sink = { log: (s) => lines.push(String(s)), errlog: (s) => lines.push(String(s)) };
|
|
483
|
-
const code = mod.cmdUpgrade([], {
|
|
483
|
+
const code = await mod.cmdUpgrade([], { checkUpdates: async () => ({ checked: true, latest: {} }),
|
|
484
484
|
...sink,
|
|
485
485
|
gather: () => ({
|
|
486
486
|
core: '2.0.0',
|
|
@@ -498,10 +498,10 @@ test('cmdUpgrade: a DISABLED incompatible module is reported with its owner, not
|
|
|
498
498
|
assert.match(out, /maintainer Ada <ada@example\.com>/, 'the owner is named so someone can be told');
|
|
499
499
|
});
|
|
500
500
|
|
|
501
|
-
test('cmdUpgrade: reports an expired deprecation and modules with no declared upkeep', () => {
|
|
501
|
+
test('cmdUpgrade: reports an expired deprecation and modules with no declared upkeep', async () => {
|
|
502
502
|
const lines = [];
|
|
503
503
|
const sink = { log: (s) => lines.push(String(s)), errlog: (s) => lines.push(String(s)) };
|
|
504
|
-
const code = mod.cmdUpgrade([], {
|
|
504
|
+
const code = await mod.cmdUpgrade([], { checkUpdates: async () => ({ checked: true, latest: {} }),
|
|
505
505
|
...sink,
|
|
506
506
|
gather: () => ({
|
|
507
507
|
core: '1.13.0',
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// tests/module_upgrade_updates.mjs — `bongos upgrade` reports available module
|
|
2
|
+
// updates beside coreVersion drift (task 1003787, ADR 0338 D1, criterion
|
|
3
|
+
// wa5-module-economy). Two halves, no port and no real DB:
|
|
4
|
+
// - the CLI: cmdUpgrade against injected rows and a fake store, so one command
|
|
5
|
+
// answers "what is stale here" — and the exit code is still decided by the
|
|
6
|
+
// compatibility check alone (an incompatible ENABLED module blocks, a disabled
|
|
7
|
+
// one never does, an update never does);
|
|
8
|
+
// - the server: the real GET /store/modules/latest handler off the real router,
|
|
9
|
+
// with pool.js replaced in the require cache (the module_store_howto.mjs harness).
|
|
10
|
+
//
|
|
11
|
+
// Run: node tests/module_upgrade_updates.mjs
|
|
12
|
+
|
|
13
|
+
import { strict as assert } from 'node:assert';
|
|
14
|
+
import { test } from 'node:test';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
|
|
19
|
+
// ---- the fake registry the route reads ----
|
|
20
|
+
const state = { modules: {}, versions: [] };
|
|
21
|
+
const pool = {
|
|
22
|
+
async query(sql, params = []) {
|
|
23
|
+
if (/FROM store_module_versions v JOIN store_modules m/.test(sql)) {
|
|
24
|
+
return {
|
|
25
|
+
rows: state.versions
|
|
26
|
+
.filter((v) => params[0].includes(v.module_key) && state.modules[v.module_key])
|
|
27
|
+
.map((v) => ({ ...v, status: state.modules[v.module_key].status })),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return { rows: [] };
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
const poolPath = require.resolve('../src/bongos/pool.js');
|
|
34
|
+
require.cache[poolPath] = { id: poolPath, filename: poolPath, loaded: true, exports: { pool: { connect: async () => ({ ...pool, release() {} }), query: pool.query } } };
|
|
35
|
+
|
|
36
|
+
const buildModulesRouter = require('../src/bongos/routes/modules.js');
|
|
37
|
+
const mod = require('../scripts/gds/module.js');
|
|
38
|
+
|
|
39
|
+
function handlerFor(router, method, p) {
|
|
40
|
+
const layer = router.stack.find((l) => l.route && l.route.path === p && l.route.methods[method]);
|
|
41
|
+
assert.ok(layer, `no ${method.toUpperCase()} ${p} on the router`);
|
|
42
|
+
const stack = layer.route.stack;
|
|
43
|
+
return { handle: stack[stack.length - 1].handle, names: stack.map((s) => s.name) };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function invoke(handle, req) {
|
|
47
|
+
const res = {
|
|
48
|
+
statusCode: 200, body: undefined, headers: {},
|
|
49
|
+
json(b) { this.body = b; return this; },
|
|
50
|
+
status(c) { this.statusCode = c; return this; },
|
|
51
|
+
set(k, v) { this.headers[k] = v; return this; },
|
|
52
|
+
fail(code, opts) { this.statusCode = opts.status; this.body = { error: code, ...opts }; return this; },
|
|
53
|
+
};
|
|
54
|
+
let nextErr = null;
|
|
55
|
+
await handle(req, res, (err) => { nextErr = err; });
|
|
56
|
+
if (nextErr) throw nextErr;
|
|
57
|
+
return res;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const row = (key, version, extra = {}) => ({ key, version, coreVersion: '^1.0.0', enabled: true, satisfied: true, maintenance: { status: 'maintained' }, maintainer: 'Ada', ...extra });
|
|
61
|
+
const sink = () => { const out = []; const err = []; return { out, err, log: (s) => out.push(String(s)), errlog: (s) => err.push(String(s)) }; };
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// the decision (pure)
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
test('evaluateUpdates: only a NEWER store version is an update; equal, older and unknown are not', () => {
|
|
68
|
+
const rows = [row('weather', '1.0.0'), row('clock', '2.0.0'), row('dice', '1.5.0'), row('built-here', '0.1.0')];
|
|
69
|
+
const latest = {
|
|
70
|
+
weather: { version: '1.2.0', core_version: '^1.0.0', status: 'listed' },
|
|
71
|
+
clock: { version: '2.0.0', core_version: '^1.0.0', status: 'listed' },
|
|
72
|
+
dice: { version: '1.4.0', core_version: '^1.0.0', status: 'listed' },
|
|
73
|
+
};
|
|
74
|
+
const { updates, delisted } = mod.evaluateUpdates(rows, latest, '1.19.0');
|
|
75
|
+
assert.deepEqual(updates.map((u) => [u.key, u.installed, u.latest, u.fits]), [['weather', '1.0.0', '1.2.0', true]]);
|
|
76
|
+
assert.deepEqual(delisted, []);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('evaluateUpdates: semver order, not text — 1.10.0 is newer than 1.9.0', () => {
|
|
80
|
+
const { updates } = mod.evaluateUpdates([row('weather', '1.9.0')], { weather: { version: '1.10.0', core_version: '^1.0.0', status: 'listed' } }, '1.19.0');
|
|
81
|
+
assert.equal(updates.length, 1);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('evaluateUpdates: an update that needs a newer core is reported as waiting, not as runnable', () => {
|
|
85
|
+
const { updates } = mod.evaluateUpdates([row('weather', '1.0.0')], { weather: { version: '2.0.0', core_version: '^2.0.0', status: 'listed' } }, '1.19.0');
|
|
86
|
+
assert.equal(updates.length, 1);
|
|
87
|
+
assert.equal(updates[0].fits, false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('evaluateUpdates: a delisted module is reported apart, never offered as an update', () => {
|
|
91
|
+
const { updates, delisted } = mod.evaluateUpdates([row('weather', '1.0.0', { enabled: false })], { weather: { version: '1.2.0', core_version: '^1.0.0', status: 'delisted' } }, '1.19.0');
|
|
92
|
+
assert.deepEqual(updates, []);
|
|
93
|
+
assert.deepEqual(delisted.map((d) => d.key), ['weather']);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('evaluateUpdates: a row with no exact version has nothing to compare against', () => {
|
|
97
|
+
const { updates } = mod.evaluateUpdates([row('weather', null)], { weather: { version: '1.2.0', core_version: '^1.0.0', status: 'listed' } }, '1.19.0');
|
|
98
|
+
assert.deepEqual(updates, []);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// gatherUpdates (the store read)
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
test('gatherUpdates: one read for every versioned module, keys in the query string', async () => {
|
|
106
|
+
const calls = [];
|
|
107
|
+
const request = async (method, urlPath) => { calls.push([method, urlPath]); return { ok: true, status: 200, data: { latest: { weather: { version: '1.2.0' } } } }; };
|
|
108
|
+
const got = await mod.gatherUpdates([row('weather', '1.0.0'), row('clock', '2.0.0'), row('loose', null)], { request });
|
|
109
|
+
assert.deepEqual(calls, [['GET', '/store/modules/latest?keys=weather,clock']]);
|
|
110
|
+
assert.equal(got.checked, true);
|
|
111
|
+
assert.equal(got.latest.weather.version, '1.2.0');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('gatherUpdates: it never acquires — no POST, so asking takes no entitlement', async () => {
|
|
115
|
+
const methods = [];
|
|
116
|
+
await mod.gatherUpdates([row('weather', '1.0.0')], { request: async (m) => { methods.push(m); return { ok: true, status: 200, data: { latest: {} } }; } });
|
|
117
|
+
assert.deepEqual(methods, ['GET']);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('gatherUpdates: an unreachable store or a refusal is "not checked", never a throw', async () => {
|
|
121
|
+
const down = await mod.gatherUpdates([row('weather', '1.0.0')], { request: async () => { throw new Error('ECONNREFUSED'); } });
|
|
122
|
+
assert.equal(down.checked, false);
|
|
123
|
+
assert.match(down.reason, /could not be reached/);
|
|
124
|
+
const refused = await mod.gatherUpdates([row('weather', '1.0.0')], { request: async () => ({ ok: false, status: 401, data: { error: { message: 'session expired' } } }) });
|
|
125
|
+
assert.equal(refused.checked, false);
|
|
126
|
+
assert.match(refused.reason, /session expired/);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('gatherUpdates: nothing versioned means nothing to ask — no store call at all', async () => {
|
|
130
|
+
const got = await mod.gatherUpdates([row('loose', null)], { request: async () => { throw new Error('must not be called'); } });
|
|
131
|
+
assert.deepEqual(got, { checked: true, latest: {} });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// cmdUpgrade — one report, the old exit-code asymmetry intact
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
const storeSays = (latest) => async () => ({ checked: true, latest });
|
|
139
|
+
|
|
140
|
+
test('cmdUpgrade: lists an available update beside the compatibility rows, and still exits 0', async () => {
|
|
141
|
+
const s = sink();
|
|
142
|
+
const code = await mod.cmdUpgrade([], {
|
|
143
|
+
...s,
|
|
144
|
+
gather: () => ({ core: '1.19.0', errors: [], rows: [row('weather', '1.0.0')] }),
|
|
145
|
+
checkUpdates: storeSays({ weather: { version: '1.2.0', core_version: '^1.0.0', status: 'listed' } }),
|
|
146
|
+
});
|
|
147
|
+
assert.equal(code, 0, 'an update is news, never a refusal');
|
|
148
|
+
const out = s.out.join('\n');
|
|
149
|
+
assert.match(out, /✓ weather/, 'the compatibility row is still there');
|
|
150
|
+
assert.match(out, /Module updates \(from the store\)/);
|
|
151
|
+
assert.match(out, /weather\s+1\.0\.0 → 1\.2\.0\s+— run `bongos module update weather`/);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('cmdUpgrade: an incompatible ENABLED module still refuses, with the updates reported too', async () => {
|
|
155
|
+
const s = sink();
|
|
156
|
+
const code = await mod.cmdUpgrade([], {
|
|
157
|
+
...s,
|
|
158
|
+
gather: () => ({ core: '2.0.0', errors: [], rows: [row('weather', '1.0.0', { coreVersion: '^1.0.0', satisfied: false })] }),
|
|
159
|
+
checkUpdates: storeSays({ weather: { version: '2.0.0', core_version: '^2.0.0', status: 'listed' } }),
|
|
160
|
+
});
|
|
161
|
+
assert.equal(code, 1, 'the block is unchanged');
|
|
162
|
+
assert.match(s.err.join('\n'), /REFUSING: 1 enabled module/);
|
|
163
|
+
assert.match(s.out.join('\n'), /weather\s+1\.0\.0 → 2\.0\.0\s+— run `bongos module update weather`/, 'and the way out is named: the newer version fits this core');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('cmdUpgrade: an incompatible DISABLED module never blocks, update or not', async () => {
|
|
167
|
+
const s = sink();
|
|
168
|
+
const code = await mod.cmdUpgrade([], {
|
|
169
|
+
...s,
|
|
170
|
+
gather: () => ({ core: '2.0.0', errors: [], rows: [row('weather', '1.0.0', { enabled: false, satisfied: false })] }),
|
|
171
|
+
checkUpdates: storeSays({ weather: { version: '1.5.0', core_version: '^1.0.0', status: 'listed' } }),
|
|
172
|
+
});
|
|
173
|
+
assert.equal(code, 0);
|
|
174
|
+
assert.match(s.out.join('\n'), /needs core \^1\.0\.0; this core is 2\.0\.0, so it waits until the core moves/);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('cmdUpgrade: nothing newer says so in one line', async () => {
|
|
178
|
+
const s = sink();
|
|
179
|
+
await mod.cmdUpgrade([], { ...s, gather: () => ({ core: '1.19.0', errors: [], rows: [row('weather', '1.2.0')] }), checkUpdates: storeSays({ weather: { version: '1.2.0', core_version: '^1.0.0', status: 'listed' } }) });
|
|
180
|
+
assert.match(s.out.join('\n'), /every installed store module is on its newest version/);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('cmdUpgrade: a delisted installed module is flagged', async () => {
|
|
184
|
+
const s = sink();
|
|
185
|
+
await mod.cmdUpgrade([], { ...s, gather: () => ({ core: '1.19.0', errors: [], rows: [row('weather', '1.2.0')] }), checkUpdates: storeSays({ weather: { version: '1.2.0', core_version: '^1.0.0', status: 'delisted' } }) });
|
|
186
|
+
assert.match(s.err.join('\n'), /weather.*delisted in the store: it keeps working, but no further versions will come/);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('cmdUpgrade: an unreachable store costs the update list, never the pre-check or its exit code', async () => {
|
|
190
|
+
const s = sink();
|
|
191
|
+
const code = await mod.cmdUpgrade([], { ...s, gather: () => ({ core: '1.19.0', errors: [], rows: [row('weather', '1.0.0')] }), checkUpdates: async () => ({ checked: false, reason: 'no Bongos session — run `bongos setup` first' }) });
|
|
192
|
+
assert.equal(code, 0);
|
|
193
|
+
const out = s.out.join('\n');
|
|
194
|
+
assert.match(out, /not checked — no Bongos session/);
|
|
195
|
+
assert.match(out, /OK — every enabled module satisfies core 1\.19\.0/);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('cmdUpgrade --no-store: the store is not asked at all', async () => {
|
|
199
|
+
const s = sink();
|
|
200
|
+
const code = await mod.cmdUpgrade(['--no-store'], { ...s, gather: () => ({ core: '1.19.0', errors: [], rows: [row('weather', '1.0.0')] }), checkUpdates: async () => { throw new Error('must not be called'); } });
|
|
201
|
+
assert.equal(code, 0);
|
|
202
|
+
assert.doesNotMatch(s.out.join('\n'), /Module updates/);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('gatherUpgrade rows carry the installed version, so the store can be compared against', () => {
|
|
206
|
+
const data = mod.gatherUpgrade({ env: {}, neutral: {}, instance: {} });
|
|
207
|
+
const versioned = data.rows.filter((r) => r.version);
|
|
208
|
+
assert.ok(versioned.length > 0, 'the core modules declare a version');
|
|
209
|
+
for (const r of versioned) assert.equal(typeof r.version, 'string');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// GET /store/modules/latest — the server half
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
const router = buildModulesRouter();
|
|
217
|
+
const latestRoute = handlerFor(router, 'get', '/store/modules/latest');
|
|
218
|
+
const ask = (keys) => invoke(latestRoute.handle, { query: keys == null ? {} : { keys }, builder: { id: 7 } });
|
|
219
|
+
|
|
220
|
+
state.modules = {
|
|
221
|
+
weather: { module_key: 'weather', status: 'listed' },
|
|
222
|
+
gone: { module_key: 'gone', status: 'delisted' },
|
|
223
|
+
};
|
|
224
|
+
state.versions = [
|
|
225
|
+
{ module_key: 'weather', version: '1.9.0', core_version: '^1.0.0' },
|
|
226
|
+
{ module_key: 'weather', version: '1.10.0', core_version: '^1.2.0' },
|
|
227
|
+
{ module_key: 'weather', version: '1.2.0', core_version: '^1.0.0' },
|
|
228
|
+
{ module_key: 'gone', version: '0.3.0', core_version: '^1.0.0' },
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
test('the route is open to any signed-in builder: requireBuilder and no permission gate', () => {
|
|
232
|
+
assert.deepEqual(latestRoute.names.slice(0, 1), ['requireBuilder']);
|
|
233
|
+
assert.equal(latestRoute.names.length, 2, 'builder gate, then the handler');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('the route answers the newest version per key, in semver order', async () => {
|
|
237
|
+
const r = await ask('weather');
|
|
238
|
+
assert.equal(r.statusCode, 200, JSON.stringify(r.body));
|
|
239
|
+
assert.deepEqual(r.body.latest.weather, { version: '1.10.0', core_version: '^1.2.0', status: 'listed' });
|
|
240
|
+
assert.equal(r.headers['Cache-Control'], 'no-store');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('the route still answers a delisted module, with its status; an unknown key is absent', async () => {
|
|
244
|
+
const r = await ask('weather,gone,nope');
|
|
245
|
+
assert.equal(r.body.latest.gone.status, 'delisted');
|
|
246
|
+
assert.equal('nope' in r.body.latest, false);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('the route refuses a missing, malformed or oversized key list', async () => {
|
|
250
|
+
assert.equal((await ask(null)).statusCode, 400);
|
|
251
|
+
assert.equal((await ask('')).statusCode, 400);
|
|
252
|
+
assert.equal((await ask('Not_A_Key')).statusCode, 400);
|
|
253
|
+
assert.equal((await ask('../etc')).statusCode, 400);
|
|
254
|
+
const many = Array.from({ length: 201 }, (_, i) => `m-${i}`).join(',');
|
|
255
|
+
assert.equal((await ask(many)).statusCode, 400);
|
|
256
|
+
});
|