@bongos/core 1.23.6 → 1.23.7

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 CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/bongos/package-core.js",
5
- "core_version": "1.23.6",
6
- "core_contract": "1.23.6",
7
- "source_commit": "05ce1a9370fbfc0af9f835a707c5c9ee9988be52",
5
+ "core_version": "1.23.7",
6
+ "core_contract": "1.23.7",
7
+ "source_commit": "b3d8fd1d43987850a49b0262cddfb454592c3baa",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-10-26T23:35:18.876Z",
9
+ "built_at": "2026-10-26T23:49:17.830Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 616,
13
13
  "agent_docs_stubbed": 29,
14
- "functional_verbatim": 3557,
14
+ "functional_verbatim": 3560,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 4203,
20
- "tree_sha256": "4f3b55cbbaaa3543f5aff4f5f6e26626e2fb433745b649921fcea34837f302f2",
19
+ "file_count": 4206,
20
+ "tree_sha256": "a3771c367fe15a46a3890c73c3a51d5233d9722c445cef33b41f0c5c2689906a",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -2937,12 +2937,12 @@
2937
2937
  {
2938
2938
  "path": "docs/module-api-changelog.md",
2939
2939
  "mode": "0000644",
2940
- "sha256": "8d3fd9e42419ebfcd1642f42725dd24c60efc03a8aafc88c9f022ab90c0152d6"
2940
+ "sha256": "91bc98777d143a46a4acdb3afc7662f9b6069fd5d2954f6b9aefd03d1d327f87"
2941
2941
  },
2942
2942
  {
2943
2943
  "path": "docs/modules-contract.md",
2944
2944
  "mode": "0000644",
2945
- "sha256": "a54fc56cc901d2580c71138906b4b27eb16529438dbfc3b293a1242cab783477"
2945
+ "sha256": "1ca002715220c25f7d270c3d87d3a5827454f4abec0f6ea813f9935d1b4ced2c"
2946
2946
  },
2947
2947
  {
2948
2948
  "path": "docs/onboarding/diagrams/01-task-lifecycle.mmd",
@@ -10837,12 +10837,12 @@
10837
10837
  {
10838
10838
  "path": "package-lock.json",
10839
10839
  "mode": "0000644",
10840
- "sha256": "d399eed08dca874f7e3a4f6daecaeebbff9d9d7d99896cfbc23c992786d0b9f5"
10840
+ "sha256": "b2707b768f0b92534351f237e4c1f2ad46894471f0f639f770bfa0b361fa584f"
10841
10841
  },
10842
10842
  {
10843
10843
  "path": "package.json",
10844
10844
  "mode": "0000644",
10845
- "sha256": "91e4472ae55582eec26d9e0c1264e8fd5c4132441fb6bffdf10a3b90c1ddcdbb"
10845
+ "sha256": "2e911efb9044f425ef86de18b03ccadf10913f7db81eac836fc07ab901bd2011"
10846
10846
  },
10847
10847
  {
10848
10848
  "path": "public-docs/index.html",
@@ -10862,7 +10862,7 @@
10862
10862
  {
10863
10863
  "path": "release-notes.json",
10864
10864
  "mode": "0000644",
10865
- "sha256": "20ffa719f32b7a90a464a880922c08e19de95ebee64d5703168ff7d2aa9db69c"
10865
+ "sha256": "56aff299cc54f61132be2b6d1877e50cc35282ed1e759726246d0ae0f131f343"
10866
10866
  },
10867
10867
  {
10868
10868
  "path": "scripts/bongos/CLAUDE.md",
@@ -11734,6 +11734,11 @@
11734
11734
  "mode": "0000644",
11735
11735
  "sha256": "0334f9d2e31559bbc661ae0be5404499719d76e670560fa5f4ba72d7c14d0ac7"
11736
11736
  },
11737
+ {
11738
+ "path": "scripts/bongos/module-remove.js",
11739
+ "mode": "0000644",
11740
+ "sha256": "9470fb2f45b4cd1d8467bc590de324a87365b7449f0d0bf74ab9baa7b65e8585"
11741
+ },
11737
11742
  {
11738
11743
  "path": "scripts/bongos/module-store-cli.js",
11739
11744
  "mode": "0000644",
@@ -11757,7 +11762,7 @@
11757
11762
  {
11758
11763
  "path": "scripts/bongos/module.js",
11759
11764
  "mode": "0000644",
11760
- "sha256": "f771cd502f959a281f4aaf60b65b19c240c5a5f7828a030001f7f01fcc75b222"
11765
+ "sha256": "34b1c30a3192d28f0260950e88f27a2bb44077d7917987697b9cdbb4093b590b"
11761
11766
  },
11762
11767
  {
11763
11768
  "path": "scripts/bongos/move-escalation.js",
@@ -13404,6 +13409,11 @@
13404
13409
  "mode": "0000644",
13405
13410
  "sha256": "053d6df274f015359f4eb52361aae55857f643672fc3746c941bfed348092b21"
13406
13411
  },
13412
+ {
13413
+ "path": "scripts/gds/module-remove.js",
13414
+ "mode": "0000644",
13415
+ "sha256": "26c003efeb97c9ed3860dbcd9d35132828e55655b5f911bc7767aac82768b5b2"
13416
+ },
13407
13417
  {
13408
13418
  "path": "scripts/gds/module-store-cli.js",
13409
13419
  "mode": "0000644",
@@ -14727,7 +14737,7 @@
14727
14737
  {
14728
14738
  "path": "src/module-api.js",
14729
14739
  "mode": "0000644",
14730
- "sha256": "9c9c3ae365588e980585b171efb35ea63dcad8d21c8a6b8e3e5a9bf140d7dd34"
14740
+ "sha256": "6b5b56cd550854c711eef1cd99f72c7c7f52a781bb0b6391845ff8a3fb1f78c9"
14731
14741
  },
14732
14742
  {
14733
14743
  "path": "src/module-loader/catalog.js",
@@ -18489,6 +18499,11 @@
18489
18499
  "mode": "0000644",
18490
18500
  "sha256": "51ba43c92ccd963cead34b5cad855fc4d19b4698e9a27bacaa904cd999776f9e"
18491
18501
  },
18502
+ {
18503
+ "path": "tests/module_remove.mjs",
18504
+ "mode": "0000644",
18505
+ "sha256": "c9557ecb6efe346334e6d190a41b04efad156bdf5c7ce773e01ccb35e31e5124"
18506
+ },
18492
18507
  {
18493
18508
  "path": "tests/module_route_rank.mjs",
18494
18509
  "mode": "0000644",
@@ -3229,5 +3229,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
3229
3229
  landed since 1.23.4 with no explicit bump. run 40417462733. (task 1002620)
3230
3230
  1.23.6 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3231
3231
  landed since 1.23.5 with no explicit bump. run 40420658396. (task 1002620)
3232
+ 1.23.7 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3233
+ landed since 1.23.6 with no explicit bump. run 40422218359. (task 1002620)
3232
3234
  ---------------------------------------------------------------------------
3233
3235
  ```
@@ -424,6 +424,8 @@ Run `bongos upgrade` to confirm coreVersion compatibility, then `node scripts/bo
424
424
  - 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.
425
425
  - The on/off switch is untouched. A module bundled with the core is refused; it updates with the core (`bongos upgrade`).
426
426
 
427
+ `bongos module remove <key>` (task 1004896) takes a store module off this project. It refuses a core-shipped module, a module that is switched on (in config/env, or in the hall's Modules tab when the server answers), and a key the store does not know — that is the project's own code, not a store copy. It never deletes: the folder moves to `var/removed-modules/<key>-<version>-<stamp>/` (git-ignored), and the output says how to put it back. The module's database tables are left in place, so reinstalling brings its data back (owner, 2026-10-26), and the entitlement is untouched, so `bongos module install` puts it back without paying again. Nothing on the server is written: update checks and install reports read the modules on disk.
428
+
427
429
  **`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.
428
430
 
429
431
  ## Carving an existing core domain (the `db.js` per-domain pattern)
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.23.6",
3
+ "version": "1.23.7",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.23.6",
9
+ "version": "1.23.7",
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.23.6",
3
+ "version": "1.23.7",
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",
@@ -10423,5 +10423,11 @@
10423
10423
  "id": "1003359",
10424
10424
  "text": "People checking whether someone has applied to a project can no longer tell a pending, declined or never-filed request apart, and the sign-in tool no longer promises an approval that may never come."
10425
10425
  }
10426
+ ],
10427
+ "1.23.7": [
10428
+ {
10429
+ "id": "1004896",
10430
+ "text": "Builders can now take a module they installed from the store back off their project with one command; its folder is kept aside so nothing is lost."
10431
+ }
10426
10432
  ]
10427
10433
  }
@@ -0,0 +1,122 @@
1
+ 'use strict';
2
+ //
3
+ // scripts/bongos/module-remove.js — `bongos module remove <key>`: take a module this
4
+ // project installed from the store back off it (task 1004896, goal 1000091).
5
+ //
6
+ // WHY it moves the folder aside instead of deleting it. Install places a store
7
+ // module under the instance's own modules/, which is also where a project keeps the
8
+ // modules it builds itself; the only thing that tells the two apart is whether the
9
+ // store knows the key. A wrong guess must cost nothing, so the folder is MOVED to
10
+ // var/removed-modules/<key>-<version>-<stamp>/ and the command says where: putting it
11
+ // back is one move, and deleting it for good is a separate, deliberate step.
12
+ //
13
+ // What it refuses, before touching anything:
14
+ // - a module the core ships (it is the core's, not this project's);
15
+ // - a module that is switched on — in config/env here, or in the hall's on/off
16
+ // switch when the server answers — because the loader would mount a module whose
17
+ // files are gone;
18
+ // - a key the store does not know: that is this project's own code, not a store copy.
19
+ // What it leaves alone, on purpose (owner, 2026-10-26): the module's database tables
20
+ // (reinstalling brings the data back; the output says so), and the entitlement (a
21
+ // paid module can be installed again without paying twice). Nothing on the server is
22
+ // written: update checks and install reports read the modules on disk, so once the
23
+ // folder is gone they stop on their own.
24
+ //
25
+ // It lives in its own file because scripts/bongos/module.js sits near the 1,500-line
26
+ // ratchet; module.js dispatches to it and re-exports cmdRemove.
27
+
28
+ const fs = require('node:fs');
29
+ const path = require('node:path');
30
+ const { KEY_RE } = require('../../src/module-loader/manifest-schema');
31
+
32
+ const REMOVE_USAGE = 'usage: bongos module remove <key>';
33
+
34
+ // Where removed folders go: <instance root>/var/removed-modules (beside the other
35
+ // instance-local state the core keeps under var/, never inside the core package).
36
+ function removedModulesDir(instanceRoot) {
37
+ return path.join(instanceRoot, 'var', 'removed-modules');
38
+ }
39
+
40
+ // A filesystem-safe timestamp for the parked folder's name. Pure.
41
+ function stamp(now) {
42
+ return now.toISOString().replace(/[-:]/g, '').replace(/\.\d+Z$/, 'Z');
43
+ }
44
+
45
+ // Is the key switched on in the hall's switch? true / false, or null when the
46
+ // server could not say (offline, older core) — then the local verdict stands.
47
+ async function hallEnabled(call, key) {
48
+ try {
49
+ const res = await call('GET', '/modules');
50
+ if (!res.ok || !res.data || !Array.isArray(res.data.modules)) return null;
51
+ const row = res.data.modules.find((m) => m.key === key);
52
+ return row ? row.enabled === true : null;
53
+ } catch { return null; }
54
+ }
55
+
56
+ async function cmdRemove(args, {
57
+ log = console.log, errlog = console.error,
58
+ gather, request, storeRequest, storeError,
59
+ instanceRoot = require('../../src/instance-config').resolveInstanceRoot(),
60
+ now = () => new Date(),
61
+ } = {}) {
62
+ const key = args[0];
63
+ if (!key || key.startsWith('--') || !KEY_RE.test(key)) {
64
+ errlog('bongos module remove: name the module — a lowercase kebab-case key');
65
+ errlog(REMOVE_USAGE);
66
+ return 2;
67
+ }
68
+
69
+ const dir = path.join(instanceRoot, 'modules', key);
70
+ let rows = [];
71
+ try { rows = gather().rows; } catch (e) { errlog(`bongos module remove: ${e.message}`); return 1; }
72
+ const row = rows.find((r) => r.key === key);
73
+ if (row && row.source === 'core') {
74
+ errlog(`REFUSING: "${key}" ships with the core, not from the store. Switch it off instead if you do not want it.`);
75
+ return 1;
76
+ }
77
+ if (!fs.existsSync(dir)) {
78
+ errlog(`REFUSING: "${key}" is not installed in this project (no modules/${key}/ folder).`);
79
+ return 1;
80
+ }
81
+ if (row && row.enabled) {
82
+ errlog(`REFUSING: "${key}" is switched on. Switch it off first — remove "${key}": true from config/modules.json (or its <PREFIX>_MODULE_ setting) — then run this again.`);
83
+ return 1;
84
+ }
85
+
86
+ let call;
87
+ try { call = request || storeRequest(); }
88
+ catch (e) { errlog(`bongos module remove: ${e.message}`); return 1; }
89
+ if (await hallEnabled(call, key) === true) {
90
+ errlog(`REFUSING: "${key}" is switched on in the hall's Modules tab. Switch it off there first, then run this again.`);
91
+ return 1;
92
+ }
93
+ let res;
94
+ try { res = await call('GET', `/store/modules/${encodeURIComponent(key)}/listing`); }
95
+ catch (e) { errlog(`bongos module remove: the store could not be reached — ${e.message}`); return 1; }
96
+ if (res.status === 404) {
97
+ errlog(`REFUSING: the store has no module "${key}", so modules/${key}/ is this project's own code, not a store copy. Nothing was moved; delete it by hand if you mean to.`);
98
+ return 1;
99
+ }
100
+ if (!res.ok) { errlog(`bongos module remove: the store answered ${storeError(res)}`); return 1; }
101
+
102
+ const version = (row && row.version) || 'unknown';
103
+ const parked = path.join(removedModulesDir(instanceRoot), `${key}-${version}-${stamp(now())}`);
104
+ try {
105
+ fs.mkdirSync(path.dirname(parked), { recursive: true });
106
+ // A `*` .gitignore, so a parked folder never dirties the instance tree that
107
+ // `bongos upgrade` requires clean (the var/task-visuals precedent, task 1004381).
108
+ const ignore = path.join(path.dirname(parked), '.gitignore');
109
+ if (!fs.existsSync(ignore)) fs.writeFileSync(ignore, '*\n');
110
+ fs.renameSync(dir, parked);
111
+ } catch (e) {
112
+ errlog(`bongos module remove: could not move modules/${key}/ aside — ${e.message}. Nothing was removed.`);
113
+ return 1;
114
+ }
115
+ log(`✓ Removed "${key}" ${version} from this project.`);
116
+ log(` Its folder was moved to ${path.relative(instanceRoot, parked)} — move it back to modules/${key}/ to undo, or delete it when you are sure.`);
117
+ log(' Its database tables were left in place, so reinstalling brings its data back.');
118
+ log(` You still own it: \`bongos module install ${key}\` puts it back without buying it again.`);
119
+ return 0;
120
+ }
121
+
122
+ module.exports = { cmdRemove };
@@ -1070,6 +1070,12 @@ async function cmdPublish(args, {
1070
1070
  // bongos module install <key> — acquire a module from the store (ADR 0338 D1)
1071
1071
  // ---------------------------------------------------------------------------
1072
1072
 
1073
+ // `bongos module remove` lives in its own file (task 1004896); this hands it the
1074
+ // catalog and the store client it needs.
1075
+ function cmdRemove(args, deps = {}) {
1076
+ return require('./module-remove').cmdRemove(args, { gather: gatherCatalog, storeRequest, storeError, ...deps });
1077
+ }
1078
+
1073
1079
  // Where install places a module: the INSTANCE's own modules/ root (ADR 0108 §1),
1074
1080
  // which the loader discovers. In a single checkout that is this repo's modules/.
1075
1081
  function instanceModulesDir() {
@@ -1333,6 +1339,7 @@ function printHelp(out = console.log) {
1333
1339
  out(' What the store lists, best first: above the quality floor, ranked, ten per sub-category');
1334
1340
  out(' bongos module price <key> [--cents N] [--credits N]');
1335
1341
  out(' Set your module\'s price in the store (0 in either makes it free)');
1342
+ out(' bongos module remove <key> Take a store module off this project (switch it off first; its folder is kept aside)');
1336
1343
  out(' bongos upgrade [--no-store] Pre-check enabled modules against the core version (fail-closed),');
1337
1344
  out(' then list newer store versions of installed modules (never blocks)');
1338
1345
  out(' bongos module check <key> [--sign-off "Name <email>"]');
@@ -1354,6 +1361,7 @@ async function main(argv) {
1354
1361
  if (sub === 'list' || sub === 'ls') return cmdList(args.slice(1));
1355
1362
  if (sub === 'store') return cmdStore(args.slice(1));
1356
1363
  if (sub === 'price') return cmdPrice(args.slice(1));
1364
+ if (sub === 'remove') return cmdRemove(args.slice(1));
1357
1365
  if (sub === 'upgrade') return cmdUpgrade(args.slice(1));
1358
1366
  if (sub === 'check') return cmdCheck(args.slice(1));
1359
1367
  if (sub === 'submit') return cmdSubmit(args.slice(1));
@@ -1370,7 +1378,7 @@ async function main(argv) {
1370
1378
  }
1371
1379
 
1372
1380
  module.exports = {
1373
- main, cmdNew, cmdList, cmdStore, formatStore, cmdPrice, cmdUpgrade, cmdCheck, cmdSubmit, cmdPublish, cmdInstall, cmdUpdate, printHelp,
1381
+ main, cmdNew, cmdList, cmdStore, formatStore, cmdPrice, cmdRemove, cmdUpgrade, cmdCheck, cmdSubmit, cmdPublish, cmdInstall, cmdUpdate, printHelp,
1374
1382
  storeRequest,
1375
1383
  gatherCatalog, formatCatalog, ownProjectName,
1376
1384
  scaffoldModule, gatherUpgrade, evaluateUpgrade, gatherUpdates, evaluateUpdates, buildManifest,
@@ -0,0 +1,6 @@
1
+ // scripts/gds/module-remove.js — @bongos-forwarder (task 1004848). Generated by
2
+ // scripts/bongos/codemod-rename-scripts-gds.js; do not edit.
3
+ // The real script is scripts/bongos/module-remove.js. This forwarder keeps callers that still name
4
+ // the old path working, and warns once so they get repointed. Task 1003706 removes it.
5
+ 'use strict';
6
+ require('../bongos/forwarder.js').forward(module, 'module-remove.js');
package/src/module-api.js CHANGED
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
81
81
  // MAJOR (see allowBoxScope below): passes the request through untouched.
82
82
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
83
83
 
84
- const CORE_VERSION = '1.23.6'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
84
+ const CORE_VERSION = '1.23.7'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
85
85
 
86
86
  // A namespaced logger so a module's log lines are attributable + consistent.
87
87
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -0,0 +1,121 @@
1
+ // tests/module_remove.mjs — `bongos module remove <key>` takes a store module off a
2
+ // project without losing anything (task 1004896, goal 1000091). Runs the real
3
+ // cmdRemove against a throwaway instance folder on disk and a stand-in store: no
4
+ // port, no real DB, no session.
5
+ //
6
+ // Run: node tests/module_remove.mjs
7
+
8
+ import { strict as assert } from 'node:assert';
9
+ import { test } from 'node:test';
10
+ import { createRequire } from 'node:module';
11
+ import fs from 'node:fs';
12
+ import os from 'node:os';
13
+ import path from 'node:path';
14
+
15
+ const require = createRequire(import.meta.url);
16
+ const { cmdRemove } = require('../scripts/bongos/module-remove.js');
17
+ const mod = require('../scripts/bongos/module.js');
18
+
19
+ function instance() {
20
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'module-remove-'));
21
+ const dir = path.join(root, 'modules', 'weather');
22
+ fs.mkdirSync(dir, { recursive: true });
23
+ fs.writeFileSync(path.join(dir, 'module.json'), '{"key":"weather","version":"1.2.0"}');
24
+ fs.writeFileSync(path.join(dir, 'index.js'), 'module.exports = {};');
25
+ return { root, dir };
26
+ }
27
+ const row = (over = {}) => ({ key: 'weather', source: 'instance', enabled: false, version: '1.2.0', ...over });
28
+
29
+ // A stand-in store: GET /modules answers the hall's switch, GET .../listing whether
30
+ // the store knows the key. Every call is recorded, so a test can prove nothing was written.
31
+ function store({ hall = false, known = true, reachable = true } = {}) {
32
+ const calls = [];
33
+ const request = async (method, url) => {
34
+ calls.push(`${method} ${url}`);
35
+ if (!reachable) throw new Error('ECONNREFUSED');
36
+ if (url === '/modules') return { ok: true, status: 200, data: { modules: hall === null ? [] : [{ key: 'weather', enabled: hall }] } };
37
+ if (url === '/store/modules/weather/listing') {
38
+ return known ? { ok: true, status: 200, data: { module_key: 'weather', listed: true } } : { ok: false, status: 404, data: { error: 'module_not_found' } };
39
+ }
40
+ return { ok: false, status: 500, data: null };
41
+ };
42
+ return { request, calls };
43
+ }
44
+
45
+ async function run(args, { rows = [row()], st = store(), root } = {}) {
46
+ const out = [];
47
+ const err = [];
48
+ const code = await cmdRemove(args, {
49
+ log: (l) => out.push(l), errlog: (l) => err.push(l),
50
+ gather: () => ({ rows }), request: st.request, storeError: (r) => `HTTP ${r.status}`,
51
+ instanceRoot: root, now: () => new Date('2026-10-26T12:00:00Z'),
52
+ });
53
+ return { code, out: out.join('\n'), err: err.join('\n') };
54
+ }
55
+
56
+ test('a store module that is switched off is moved aside, not deleted, and the output says how to undo it', async () => {
57
+ const { root, dir } = instance();
58
+ const st = store();
59
+ const r = await run(['weather'], { st, root });
60
+ assert.equal(r.code, 0, r.err);
61
+ assert.equal(fs.existsSync(dir), false, 'modules/weather/ is gone from the project');
62
+ const parked = path.join(root, 'var', 'removed-modules', 'weather-1.2.0-20261026T120000Z');
63
+ assert.equal(fs.readFileSync(path.join(parked, 'index.js'), 'utf8'), 'module.exports = {};', 'every file kept, byte for byte');
64
+ assert.equal(fs.readFileSync(path.join(root, 'var', 'removed-modules', '.gitignore'), 'utf8'), '*\n', 'the parked folder never dirties the instance tree');
65
+ assert.match(r.out, /Removed "weather" 1\.2\.0/);
66
+ assert.match(r.out, /move it back to modules\/weather\/ to undo/);
67
+ assert.match(r.out, /database tables were left in place/, 'owner decision 2026-10-26: keep the tables');
68
+ assert.match(r.out, /without buying it again/, 'the entitlement is untouched');
69
+ assert.ok(st.calls.every((c) => c.startsWith('GET ')), 'nothing on the server is written');
70
+ });
71
+
72
+ test('a module switched on in config is refused, and nothing moves', async () => {
73
+ const { root, dir } = instance();
74
+ const r = await run(['weather'], { rows: [row({ enabled: true })], root });
75
+ assert.equal(r.code, 1);
76
+ assert.match(r.err, /is switched on\. Switch it off first/);
77
+ assert.ok(fs.existsSync(dir));
78
+ });
79
+
80
+ test('a module switched on only in the hall is refused too', async () => {
81
+ const { root, dir } = instance();
82
+ const r = await run(['weather'], { st: store({ hall: true }), root });
83
+ assert.equal(r.code, 1);
84
+ assert.match(r.err, /switched on in the hall's Modules tab/);
85
+ assert.ok(fs.existsSync(dir));
86
+ });
87
+
88
+ test('a key the store does not know is the project\'s own code: refused, nothing moved', async () => {
89
+ const { root, dir } = instance();
90
+ const r = await run(['weather'], { st: store({ known: false }), root });
91
+ assert.equal(r.code, 1);
92
+ assert.match(r.err, /this project's own code, not a store copy/);
93
+ assert.ok(fs.existsSync(dir));
94
+ });
95
+
96
+ test('a core module, a module not installed, and a bad key are refused', async () => {
97
+ const { root, dir } = instance();
98
+ const core = await run(['weather'], { rows: [row({ source: 'core' })], root });
99
+ assert.equal(core.code, 1);
100
+ assert.match(core.err, /ships with the core/);
101
+ const absent = await run(['rain'], { rows: [], root });
102
+ assert.equal(absent.code, 1);
103
+ assert.match(absent.err, /not installed in this project/);
104
+ for (const args of [[], ['--force'], ['Weather']]) assert.equal((await run(args, { root })).code, 2, args.join(' '));
105
+ assert.ok(fs.existsSync(dir));
106
+ });
107
+
108
+ test('an unreachable store refuses rather than guessing, and nothing moves', async () => {
109
+ const { root, dir } = instance();
110
+ const r = await run(['weather'], { st: store({ reachable: false }), root });
111
+ assert.equal(r.code, 1);
112
+ assert.match(r.err, /could not be reached/);
113
+ assert.ok(fs.existsSync(dir));
114
+ });
115
+
116
+ test('`bongos module remove` is dispatched and documented in the help', () => {
117
+ const out = [];
118
+ mod.printHelp((l) => out.push(l));
119
+ assert.match(out.join('\n'), /bongos module remove <key>/);
120
+ assert.equal(typeof mod.cmdRemove, 'function');
121
+ });