@bongos/core 1.23.5 → 1.23.6

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.
@@ -357,9 +357,12 @@ module.exports = function buildAccessRequestsRouter() {
357
357
  // (the device_code is spent), so the CLI can't re-poll the flow — it polls this
358
358
  // instead, then restarts sign-in once admitted. Exposes nothing the public POST
359
359
  // above doesn't already reveal through its 409s (already_member / already_approved).
360
- // status ∈ member | invited | expired | pending | dismissed | none; admitted =
361
- // member||invited. `expired` is an invite past its 30 days (task 1002969): the door
362
- // is shut, and the CLI stops waiting rather than polling a door that will not open.
360
+ // status ∈ member | invited | not_admitted; admitted = member||invited (ADR 0217 D4,
361
+ // task 1003359). `not_admitted` covers pending, declined, a lapsed invite and no
362
+ // request at all, so a caller who is not the subject cannot learn that someone
363
+ // applied here or was turned down. It does NOT close the membership oracle —
364
+ // `admitted` still answers that, and the limiter below is what bounds enumeration.
365
+ // A lapsed invite answering `not_admitted` rather than `expired` amends ADR 0356 D4.
363
366
  // rank: public — a would-be builder is by definition outside the system.
364
367
  // Declared BEFORE `GET /access-requests` so the literal `/status` path is matched
365
368
  // as its own route (not swallowed as a query on the list route).
@@ -370,9 +373,7 @@ module.exports = function buildAccessRequestsRouter() {
370
373
  // per-IP budget: core's GET /auth/web/admission-status (the boolean twin of
371
374
  // this answer) mounts the very same instance, so the two cannot be alternated
372
375
  // for double the rate. The limiter runs before the queries, so a refused probe
373
- // costs no DB work. Whether the ANSWER should also collapse — so a caller who
374
- // is not the subject cannot tell pending/dismissed/none apart — is the owner's
375
- // call and is filed as a blocker, not decided here.
376
+ // costs no DB work.
376
377
  router.get('/access-requests/status', accountExistenceReadRateLimit, async (req, res) => {
377
378
  const login = normalizeLogin(req.query.github_login);
378
379
  if (!GITHUB_LOGIN_RE.test(login)) {
@@ -386,7 +387,7 @@ module.exports = function buildAccessRequestsRouter() {
386
387
  if (member.rows.length > 0) {
387
388
  return res.json({ github_login: login, status: 'member', admitted: true });
388
389
  }
389
- // Prefer an invite that still admits, else the most recent request.
390
+ // A live invite sorts first, so the top row admits only if it is one.
390
391
  const ar = await pool.query(
391
392
  `SELECT status, (status = 'invited' AND ${inviteExpiredSql()}) AS expired
392
393
  FROM access_requests
@@ -396,8 +397,8 @@ module.exports = function buildAccessRequestsRouter() {
396
397
  [login]
397
398
  );
398
399
  const top = ar.rows[0];
399
- const status = !top ? 'none' : top.expired ? 'expired' : top.status;
400
- return res.json({ github_login: login, status, admitted: status === 'invited' });
400
+ const invited = !!top && top.status === 'invited' && !top.expired;
401
+ return res.json({ github_login: login, status: invited ? 'invited' : 'not_admitted', admitted: invited });
401
402
  } catch (err) {
402
403
  console.error('[gds] GET /access-requests/status', err);
403
404
  return res.fail('status_failed', { status: 500, message: 'internal error' });
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.23.5",
3
+ "version": "1.23.6",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.23.5",
9
+ "version": "1.23.6",
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.5",
3
+ "version": "1.23.6",
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",
@@ -10413,5 +10413,15 @@
10413
10413
  "id": "1004895",
10414
10414
  "text": "A broken module version can now be pulled back from the store, so new installs and updates stop getting it while projects that already have it are told."
10415
10415
  }
10416
+ ],
10417
+ "1.23.6": [
10418
+ {
10419
+ "id": "1004894",
10420
+ "text": "Module authors can now set a price for their module, in money or credits; paid modules show their price and are marked not yet buyable until buying opens."
10421
+ },
10422
+ {
10423
+ "id": "1003359",
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
+ }
10416
10426
  ]
10417
10427
  }
@@ -43,17 +43,24 @@ function normalizeInstanceBase(raw) {
43
43
  return s;
44
44
  }
45
45
 
46
- // Decide what an /access-requests/status poll means for the sign-in loop.
46
+ // Decide what an /access-requests/status poll means for the sign-in loop. The probe
47
+ // answers only admitted or not (ADR 0217 D4), so anything else is a wait — even an
48
+ // older instance's `dismissed`/`expired`, which the loop no longer reads.
47
49
  function pollDecision(body) {
48
50
  if (!body || typeof body !== 'object') return 'wait';
49
51
  if (body.admitted === true) return 'admitted';
50
52
  if (body.status === 'member' || body.status === 'invited') return 'admitted';
51
- if (body.status === 'dismissed') return 'dismissed';
52
- // An invite past its 30 days (task 1002969): the door is shut until they invite again.
53
- if (body.status === 'expired') return 'expired';
54
53
  return 'wait';
55
54
  }
56
55
 
56
+ // What the loop says when it stops waiting. It cannot know whether the request is
57
+ // still pending, was declined, or rode a lapsed invite, so it promises nothing.
58
+ function waitTimeoutMessage(base) {
59
+ return "✖ Stopped waiting after 30 minutes — you haven't been let in yet. If your request was turned down "
60
+ + "or your invite has run out, waiting longer won't change that. Ask the project's owner, "
61
+ + `then re-run \`bongos login ${base}\`.`;
62
+ }
63
+
57
64
  // --- I/O helpers -------------------------------------------------------------
58
65
 
59
66
  async function fetchJson(url, opts = {}) {
@@ -300,16 +307,8 @@ async function main() {
300
307
  log('\n ✓ Approved! Finishing sign-in — approve once more in your browser.');
301
308
  break;
302
309
  }
303
- if (decision === 'dismissed') {
304
- console.error('\n✖ your access request was declined.');
305
- process.exit(1);
306
- }
307
- if (decision === 'expired') {
308
- console.error('\n✖ your invite has expired — ask the project to invite you again.');
309
- process.exit(1);
310
- }
311
310
  if (Date.now() > admitDeadline) {
312
- console.error('\n✖ timed out waiting for approval — re-run `bongos login` once an Archon has approved you.');
311
+ console.error(`\n${waitTimeoutMessage(base)}`);
313
312
  process.exit(1);
314
313
  }
315
314
  process.stdout.write('.');
@@ -357,4 +356,4 @@ if (require.main === module) {
357
356
  });
358
357
  }
359
358
 
360
- module.exports = { normalizeInstanceBase, pollDecision, runDeviceFlow, logOtherInstances };
359
+ module.exports = { normalizeInstanceBase, pollDecision, waitTimeoutMessage, runDeviceFlow, logOtherInstances };
@@ -0,0 +1,182 @@
1
+ 'use strict';
2
+ //
3
+ // scripts/bongos/module-store-cli.js — the module STORE from the terminal: the store
4
+ // client every store verb calls, and `bongos module store` / `bongos module price`
5
+ // (tasks 1003811, 1004894). Carved out of scripts/bongos/module.js, which requires
6
+ // and re-exports all of it, so `bongos module <verb>` and every importer are
7
+ // unchanged.
8
+ //
9
+ // WHY its own file. module.js crossed the 1,500-line ratchet (fitness.js) when the
10
+ // price command landed, and these pieces share one concern — talking to the store —
11
+ // while the rest of module.js works on the checkout's own modules/ tree.
12
+ //
13
+ // Like module.js it may be required by the server process, so cli-lib's
14
+ // session/fetch plumbing is required lazily inside storeRequest, never at load.
15
+
16
+ const { KEY_RE } = require('../../src/module-loader/manifest-schema');
17
+
18
+ // The store half of the CLI, over three calls (src/bongos/routes/modules.js):
19
+ // acquire (take the entitlement, learn the version), download the tarball, and
20
+ // once the files are placed, record the version acquired. Injected in tests.
21
+ function storeRequest() {
22
+ // Lazy require — cli-lib's session/fetch plumbing must never load in the server
23
+ // process, which requires this file too (the same reason cmdPublish gives).
24
+ const { readSessionToken, dispatchedFetch, noKeepAliveDispatcher, CLI_USER_AGENT } = require('./cli-lib');
25
+ const session = readSessionToken();
26
+ if (!session) throw new Error('no Bongos session — run `bongos setup` first');
27
+ const call = async (method, urlPath, { json, binary = false } = {}) => {
28
+ const res = await dispatchedFetch(`${session.apiBase}/api/bongos${urlPath}`, {
29
+ method,
30
+ headers: {
31
+ Authorization: `Bearer ${session.token}`, 'User-Agent': CLI_USER_AGENT,
32
+ ...(json ? { 'Content-Type': 'application/json' } : {}),
33
+ },
34
+ body: json ? JSON.stringify(json) : undefined,
35
+ dispatcher: noKeepAliveDispatcher(),
36
+ });
37
+ if (binary && res.ok) return { ok: true, status: res.status, buf: Buffer.from(await res.arrayBuffer()) };
38
+ const text = await res.text();
39
+ let data;
40
+ try { data = text ? JSON.parse(text) : null; } catch { data = { raw: text }; }
41
+ return { ok: res.ok, status: res.status, data };
42
+ };
43
+ // Where the store lives, so install can print a full link to the how-to page.
44
+ call.apiBase = session.apiBase;
45
+ return call;
46
+ }
47
+
48
+ // The hall page that shows a version's HOWTO.md (task 1004366) — the Modules tab,
49
+ // opened on one version. null when the version shipped without one (published
50
+ // before ADR 0347's gate), so install never prints a link that would 404.
51
+ function howtoLink(key, version, files, apiBase) {
52
+ if (!files.some((f) => f.path === 'HOWTO.md')) return null;
53
+ const p = `/builders/modules?howto=${encodeURIComponent(key)}&version=${encodeURIComponent(version)}`;
54
+ return apiBase ? `${String(apiBase).replace(/\/+$/, '')}${p}` : p;
55
+ }
56
+
57
+ function storeError(res) {
58
+ const err = res.data && (res.data.error || res.data);
59
+ return (err && (err.message || err.code)) || `HTTP ${res.status}`;
60
+ }
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // bongos module store — what the store lists (task 1003811)
64
+ // ---------------------------------------------------------------------------
65
+
66
+ const STORE_USAGE = 'usage: bongos module store [--sort <part>] [--max-cents N] [--max-credits N] [--json]';
67
+
68
+ // The query GET /store/modules takes, from argv. Returns { query } or { error }.
69
+ // The server checks the values too; this only refuses what is not a value at all.
70
+ function storeQuery(args) {
71
+ const params = [];
72
+ for (const [flag, param] of [['--sort', 'sort'], ['--max-cents', 'max_cents'], ['--max-credits', 'max_credits']]) {
73
+ const i = args.indexOf(flag);
74
+ if (i === -1) continue;
75
+ const v = args[i + 1];
76
+ if (!v || v.startsWith('--')) return { error: `${flag} needs a value` };
77
+ params.push(`${param}=${encodeURIComponent(v)}`);
78
+ }
79
+ return { query: params.length ? `?${params.join('&')}` : '' };
80
+ }
81
+
82
+ function formatPrice(price) {
83
+ if (!price || price.free) return 'free';
84
+ const parts = [];
85
+ if (price.cents != null) parts.push(`$${(price.cents / 100).toFixed(2)}`);
86
+ if (price.credits != null) parts.push(`${price.credits} credits`);
87
+ return parts.join(' or ') || 'free';
88
+ }
89
+
90
+ function formatPlace(category) {
91
+ if (!category) return '';
92
+ return category.label ? `${category.parent_title} › ${category.label}` : category.parent_title;
93
+ }
94
+
95
+ // Pure renderer over the GET /store/modules body. It keeps the server's order and
96
+ // adds nothing to it: listed in rank, then New, never ranked.
97
+ function formatStore(body) {
98
+ const out = [];
99
+ out.push('bongos module store — what the store lists, best first');
100
+ out.push(` floor ${body.floor} out of 100 · at most ${body.cap} per sub-category`);
101
+ out.push(` ${body.ranking}`);
102
+ if (body.sort) {
103
+ const part = (body.sort_parts || []).find((x) => x.part === body.sort);
104
+ out.push(` sorted by ${part ? part.title : body.sort}`);
105
+ }
106
+ const max = body.max_price;
107
+ if (max) out.push(` at most ${[max.cents != null ? `$${(max.cents / 100).toFixed(2)}` : null, max.credits != null ? `${max.credits} credits` : null].filter(Boolean).join(' or ')}`);
108
+ out.push('');
109
+ const listed = body.listed || [];
110
+ if (!listed.length) out.push(' Nothing is listed yet.');
111
+ listed.forEach((m, i) => {
112
+ out.push(` ${String(i + 1).padStart(2)}. ${m.key}@${m.version} ${m.overall}/100 ${formatPrice(m.price)}${m.buyable === false ? ' (not yet buyable)' : ''}${m.category ? ` ${formatPlace(m.category)}` : ''}`);
113
+ });
114
+ const fresh = body.new || [];
115
+ if (fresh.length) {
116
+ out.push('');
117
+ out.push(' New — not ranked until projects have installed and used them:');
118
+ for (const m of fresh) out.push(` ${m.key}@${m.version} ${formatPrice(m.price)}${m.buyable === false ? ' (not yet buyable)' : ''}${m.category ? ` ${formatPlace(m.category)}` : ''}`);
119
+ }
120
+ out.push('');
121
+ out.push(' Why a module is not here: GET /store/modules/<key>/listing');
122
+ return out.join('\n');
123
+ }
124
+
125
+ async function cmdStore(args, { log = console.log, errlog = console.error, request } = {}) {
126
+ const q = storeQuery(args);
127
+ if (q.error) { errlog(`bongos module store: ${q.error}`); errlog(STORE_USAGE); return 2; }
128
+ let call;
129
+ try { call = request || storeRequest(); }
130
+ catch (e) { errlog(`bongos module store: ${e.message}`); return 1; }
131
+ let res;
132
+ try { res = await call('GET', `/store/modules${q.query}`); }
133
+ catch (e) { errlog(`bongos module store: the store could not be reached — ${e.message}`); return 1; }
134
+ if (!res.ok) { errlog(`bongos module store: the store answered ${storeError(res)}`); return res.status === 400 ? 2 : 1; }
135
+ if (args.includes('--json')) { log(JSON.stringify(res.data, null, 2)); return 0; }
136
+ log(formatStore(res.data || {}));
137
+ return 0;
138
+ }
139
+
140
+ // ---------------------------------------------------------------------------
141
+ // bongos module price — the author sets a module's price (task 1004894)
142
+ // ---------------------------------------------------------------------------
143
+
144
+ const PRICE_USAGE = 'usage: bongos module price <key> [--cents N] [--credits N] (0 in either makes it free)';
145
+
146
+ // argv -> { key, cents, credits } or { error }. Pure.
147
+ function priceArgs(args) {
148
+ const key = args[0];
149
+ if (!key || key.startsWith('--') || !KEY_RE.test(key)) return { error: 'name the module: a lowercase kebab-case key' };
150
+ const out = { key, cents: null, credits: null };
151
+ for (const [flag, field] of [['--cents', 'cents'], ['--credits', 'credits']]) {
152
+ const i = args.indexOf(flag);
153
+ if (i === -1) continue;
154
+ const v = args[i + 1];
155
+ if (!v || !/^\d{1,9}$/.test(v)) return { error: `${flag} needs a whole number, 0 or more` };
156
+ out[field] = Number(v);
157
+ }
158
+ if (out.cents === null && out.credits === null) return { error: 'give --cents, --credits or both' };
159
+ return out;
160
+ }
161
+
162
+ async function cmdPrice(args, { log = console.log, errlog = console.error, request } = {}) {
163
+ const p = priceArgs(args);
164
+ if (p.error) { errlog(`bongos module price: ${p.error}`); errlog(PRICE_USAGE); return 2; }
165
+ let call;
166
+ try { call = request || storeRequest(); }
167
+ catch (e) { errlog(`bongos module price: ${e.message}`); return 1; }
168
+ let res;
169
+ try { res = await call('POST', `/store/modules/${encodeURIComponent(p.key)}/price`, { json: { cents: p.cents, credits: p.credits } }); }
170
+ catch (e) { errlog(`bongos module price: the store could not be reached — ${e.message}`); return 1; }
171
+ if (!res.ok) { errlog(`bongos module price: the store answered ${storeError(res)}`); return res.status === 400 ? 2 : 1; }
172
+ const withFree = (p) => ({ ...p, free: p.cents === 0 || p.credits === 0 });
173
+ const shown = formatPrice(withFree(res.data.price));
174
+ const was = res.data.previous ? formatPrice(withFree(res.data.previous)) : 'free (no price set)';
175
+ log(`✓ "${p.key}" now costs ${shown} (was ${was}).`);
176
+ if (shown !== 'free') log(' Buying is not open yet, so the store lists it as not yet buyable until it is.');
177
+ return 0;
178
+ }
179
+
180
+ module.exports = {
181
+ storeRequest, storeError, howtoLink, formatStore, cmdStore, priceArgs, cmdPrice,
182
+ };
@@ -39,6 +39,12 @@
39
39
  // sub-category live in one place (src/bongos/module-listing.js), so the hall
40
40
  // and the terminal cannot disagree about which modules are the best.
41
41
  //
42
+ // bongos module price <key> [--cents N] [--credits N]
43
+ // Task 1004894 — the module's author sets its price in the store, in money
44
+ // (cents), credits or both; 0 in either makes it free. The store keeps every
45
+ // change and shows the newest. A paid module is listed as not yet buyable until
46
+ // buying arrives (task 1003788).
47
+ //
42
48
  // bongos module check <key>
43
49
  // ADR 0107 §4 — the submit-time pre-check a module must clear before it can
44
50
  // be upstreamed into the shared core catalog. REUSES (never re-types) the
@@ -82,6 +88,8 @@ const { validateManifest, assertValidManifest, KEY_RE, SUNSET_STATUSES } = requi
82
88
  const { parseVersion, cmp } = require('../../src/module-loader/semver');
83
89
  const loader = require('../../src/module-loader/loader');
84
90
  const catalog = require('../../src/module-loader/catalog');
91
+ // The store client and the `store` / `price` verbs (task 1004894 carve).
92
+ const { storeRequest, storeError, howtoLink, formatStore, cmdStore, priceArgs, cmdPrice } = require('./module-store-cli');
85
93
  const { resolveModules, MODULE_REGISTRY, NEUTRAL_PATH, INSTANCE_PATH } = require('../../src/modules');
86
94
  // The public-mirror publish boundary (ADR 0098) — REUSED, never re-typed, for
87
95
  // the ADR 0107 §4 upstreaming pre-check. Only matchesDeny is used (not the full
@@ -958,18 +966,31 @@ async function cmdSubmit(args, {
958
966
  // Distinct from `submit`, which proposes a module INTO core (ADR 0135).
959
967
  async function cmdPublish(args, {
960
968
  log = console.log, errlog = console.error,
961
- modulesDir = MODULES_DIR, pack, upload, writeFile = fs.writeFileSync,
969
+ modulesDir = MODULES_DIR, pack, upload, writeFile = fs.writeFileSync, priceRequest,
962
970
  } = {}) {
963
- const key = args.find((a, i) => !a.startsWith('-') && args[i - 1] !== '--out');
971
+ const PUBLISH_USAGE = 'usage: bongos module publish <key> [--dry-run] [--out <dir>] [--cents N] [--credits N]';
972
+ const VALUED = new Set(['--out', '--cents', '--credits']);
973
+ const key = args.find((a, i) => !a.startsWith('-') && !VALUED.has(args[i - 1]));
964
974
  if (!key) {
965
- errlog('usage: bongos module publish <key> [--dry-run] [--out <dir>]');
975
+ errlog(PUBLISH_USAGE);
966
976
  return 2;
967
977
  }
978
+ // An optional price set right after the upload (task 1004894), with the same rules
979
+ // as `bongos module price`, checked BEFORE anything is packed or sent.
980
+ const priceFlags = [];
981
+ for (const flag of ['--cents', '--credits']) {
982
+ const i = args.indexOf(flag);
983
+ if (i !== -1) priceFlags.push(flag, args[i + 1]);
984
+ }
985
+ if (priceFlags.length) {
986
+ const p = priceArgs([key, ...priceFlags]);
987
+ if (p.error) { errlog(`bongos module publish: ${p.error}`); errlog(PUBLISH_USAGE); return 2; }
988
+ }
968
989
  const dryRun = args.includes('--dry-run');
969
990
  const outIdx = args.indexOf('--out');
970
991
  const outDir = outIdx !== -1 ? args[outIdx + 1] : null;
971
992
  if (outIdx !== -1 && (!outDir || outDir.startsWith('--'))) {
972
- errlog('usage: bongos module publish <key> [--dry-run] [--out <dir>]');
993
+ errlog(PUBLISH_USAGE);
973
994
  return 2;
974
995
  }
975
996
 
@@ -1037,135 +1058,18 @@ async function cmdPublish(args, {
1037
1058
  if (res.data && res.data.channel === 'tester') {
1038
1059
  log(' It goes to testing projects first, and reaches every project once it has been tested (ADR 0344).');
1039
1060
  }
1040
- return 0;
1061
+ if (!priceFlags.length) return 0;
1062
+ // The version is published whatever happens next; a price that fails is reported
1063
+ // with the one command that sets it, and the exit code says something needs doing.
1064
+ const priced = await cmdPrice([key, ...priceFlags], { log, errlog, request: priceRequest });
1065
+ if (priced !== 0) errlog(` The version is published, but its price was not set. Run \`bongos module price ${key} ${priceFlags.join(' ')}\` to set it.`);
1066
+ return priced === 0 ? 0 : 1;
1041
1067
  }
1042
1068
 
1043
1069
  // ---------------------------------------------------------------------------
1044
1070
  // bongos module install <key> — acquire a module from the store (ADR 0338 D1)
1045
1071
  // ---------------------------------------------------------------------------
1046
1072
 
1047
- // The store half of the CLI, over three calls (src/bongos/routes/modules.js):
1048
- // acquire (take the entitlement, learn the version), download the tarball, and
1049
- // once the files are placed, record the version acquired. Injected in tests.
1050
- function storeRequest() {
1051
- // Lazy require — cli-lib's session/fetch plumbing must never load in the server
1052
- // process, which requires this file too (the same reason cmdPublish gives).
1053
- const { readSessionToken, dispatchedFetch, noKeepAliveDispatcher, CLI_USER_AGENT } = require('./cli-lib');
1054
- const session = readSessionToken();
1055
- if (!session) throw new Error('no Bongos session — run `bongos setup` first');
1056
- const call = async (method, urlPath, { json, binary = false } = {}) => {
1057
- const res = await dispatchedFetch(`${session.apiBase}/api/bongos${urlPath}`, {
1058
- method,
1059
- headers: {
1060
- Authorization: `Bearer ${session.token}`, 'User-Agent': CLI_USER_AGENT,
1061
- ...(json ? { 'Content-Type': 'application/json' } : {}),
1062
- },
1063
- body: json ? JSON.stringify(json) : undefined,
1064
- dispatcher: noKeepAliveDispatcher(),
1065
- });
1066
- if (binary && res.ok) return { ok: true, status: res.status, buf: Buffer.from(await res.arrayBuffer()) };
1067
- const text = await res.text();
1068
- let data;
1069
- try { data = text ? JSON.parse(text) : null; } catch { data = { raw: text }; }
1070
- return { ok: res.ok, status: res.status, data };
1071
- };
1072
- // Where the store lives, so install can print a full link to the how-to page.
1073
- call.apiBase = session.apiBase;
1074
- return call;
1075
- }
1076
-
1077
- // The hall page that shows a version's HOWTO.md (task 1004366) — the Modules tab,
1078
- // opened on one version. null when the version shipped without one (published
1079
- // before ADR 0347's gate), so install never prints a link that would 404.
1080
- function howtoLink(key, version, files, apiBase) {
1081
- if (!files.some((f) => f.path === 'HOWTO.md')) return null;
1082
- const p = `/builders/modules?howto=${encodeURIComponent(key)}&version=${encodeURIComponent(version)}`;
1083
- return apiBase ? `${String(apiBase).replace(/\/+$/, '')}${p}` : p;
1084
- }
1085
-
1086
- function storeError(res) {
1087
- const err = res.data && (res.data.error || res.data);
1088
- return (err && (err.message || err.code)) || `HTTP ${res.status}`;
1089
- }
1090
-
1091
- // ---------------------------------------------------------------------------
1092
- // bongos module store — what the store lists (task 1003811)
1093
- // ---------------------------------------------------------------------------
1094
-
1095
- const STORE_USAGE = 'usage: bongos module store [--sort <part>] [--max-cents N] [--max-credits N] [--json]';
1096
-
1097
- // The query GET /store/modules takes, from argv. Returns { query } or { error }.
1098
- // The server checks the values too; this only refuses what is not a value at all.
1099
- function storeQuery(args) {
1100
- const params = [];
1101
- for (const [flag, param] of [['--sort', 'sort'], ['--max-cents', 'max_cents'], ['--max-credits', 'max_credits']]) {
1102
- const i = args.indexOf(flag);
1103
- if (i === -1) continue;
1104
- const v = args[i + 1];
1105
- if (!v || v.startsWith('--')) return { error: `${flag} needs a value` };
1106
- params.push(`${param}=${encodeURIComponent(v)}`);
1107
- }
1108
- return { query: params.length ? `?${params.join('&')}` : '' };
1109
- }
1110
-
1111
- function formatPrice(price) {
1112
- if (!price || price.free) return 'free';
1113
- const parts = [];
1114
- if (price.cents != null) parts.push(`$${(price.cents / 100).toFixed(2)}`);
1115
- if (price.credits != null) parts.push(`${price.credits} credits`);
1116
- return parts.join(' or ') || 'free';
1117
- }
1118
-
1119
- function formatPlace(category) {
1120
- if (!category) return '';
1121
- return category.label ? `${category.parent_title} › ${category.label}` : category.parent_title;
1122
- }
1123
-
1124
- // Pure renderer over the GET /store/modules body. It keeps the server's order and
1125
- // adds nothing to it: listed in rank, then New, never ranked.
1126
- function formatStore(body) {
1127
- const out = [];
1128
- out.push('bongos module store — what the store lists, best first');
1129
- out.push(` floor ${body.floor} out of 100 · at most ${body.cap} per sub-category`);
1130
- out.push(` ${body.ranking}`);
1131
- if (body.sort) {
1132
- const part = (body.sort_parts || []).find((x) => x.part === body.sort);
1133
- out.push(` sorted by ${part ? part.title : body.sort}`);
1134
- }
1135
- const max = body.max_price;
1136
- if (max) out.push(` at most ${[max.cents != null ? `$${(max.cents / 100).toFixed(2)}` : null, max.credits != null ? `${max.credits} credits` : null].filter(Boolean).join(' or ')}`);
1137
- out.push('');
1138
- const listed = body.listed || [];
1139
- if (!listed.length) out.push(' Nothing is listed yet.');
1140
- listed.forEach((m, i) => {
1141
- out.push(` ${String(i + 1).padStart(2)}. ${m.key}@${m.version} ${m.overall}/100 ${formatPrice(m.price)}${m.category ? ` ${formatPlace(m.category)}` : ''}`);
1142
- });
1143
- const fresh = body.new || [];
1144
- if (fresh.length) {
1145
- out.push('');
1146
- out.push(' New — not ranked until projects have installed and used them:');
1147
- for (const m of fresh) out.push(` ${m.key}@${m.version} ${formatPrice(m.price)}${m.category ? ` ${formatPlace(m.category)}` : ''}`);
1148
- }
1149
- out.push('');
1150
- out.push(' Why a module is not here: GET /store/modules/<key>/listing');
1151
- return out.join('\n');
1152
- }
1153
-
1154
- async function cmdStore(args, { log = console.log, errlog = console.error, request } = {}) {
1155
- const q = storeQuery(args);
1156
- if (q.error) { errlog(`bongos module store: ${q.error}`); errlog(STORE_USAGE); return 2; }
1157
- let call;
1158
- try { call = request || storeRequest(); }
1159
- catch (e) { errlog(`bongos module store: ${e.message}`); return 1; }
1160
- let res;
1161
- try { res = await call('GET', `/store/modules${q.query}`); }
1162
- catch (e) { errlog(`bongos module store: the store could not be reached — ${e.message}`); return 1; }
1163
- if (!res.ok) { errlog(`bongos module store: the store answered ${storeError(res)}`); return res.status === 400 ? 2 : 1; }
1164
- if (args.includes('--json')) { log(JSON.stringify(res.data, null, 2)); return 0; }
1165
- log(formatStore(res.data || {}));
1166
- return 0;
1167
- }
1168
-
1169
1073
  // Where install places a module: the INSTANCE's own modules/ root (ADR 0108 §1),
1170
1074
  // which the loader discovers. In a single checkout that is this repo's modules/.
1171
1075
  function instanceModulesDir() {
@@ -1427,13 +1331,15 @@ function printHelp(out = console.log) {
1427
1331
  out(' Browse the module catalog — what you can enable, with author + origin credit');
1428
1332
  out(' bongos module store [--sort <part>] [--max-cents N] [--max-credits N] [--json]');
1429
1333
  out(' What the store lists, best first: above the quality floor, ranked, ten per sub-category');
1334
+ out(' bongos module price <key> [--cents N] [--credits N]');
1335
+ out(' Set your module\'s price in the store (0 in either makes it free)');
1430
1336
  out(' bongos upgrade [--no-store] Pre-check enabled modules against the core version (fail-closed),');
1431
1337
  out(' then list newer store versions of installed modules (never blocks)');
1432
1338
  out(' bongos module check <key> [--sign-off "Name <email>"]');
1433
1339
  out(' AGPL/publish pre-check before upstreaming + record a DCO-style sign-off');
1434
1340
  out(' bongos module submit <key> [--sign-off "Name <email>"]');
1435
1341
  out(' Pre-check + sign-off, then file into the rank-gated upstreaming review queue');
1436
- out(' bongos module publish <key> [--dry-run] [--out <dir>]');
1342
+ out(' bongos module publish <key> [--dry-run] [--out <dir>] [--cents N] [--credits N]');
1437
1343
  out(' Pack a module version (tarball + hash manifest) and upload it to the module store');
1438
1344
  out(' bongos module install <key> [--version X.Y.Z]');
1439
1345
  out(' Acquire a module from the store, verify it and place it under modules/ (not enabled)');
@@ -1447,6 +1353,7 @@ async function main(argv) {
1447
1353
  if (sub === 'new') return cmdNew(args.slice(1));
1448
1354
  if (sub === 'list' || sub === 'ls') return cmdList(args.slice(1));
1449
1355
  if (sub === 'store') return cmdStore(args.slice(1));
1356
+ if (sub === 'price') return cmdPrice(args.slice(1));
1450
1357
  if (sub === 'upgrade') return cmdUpgrade(args.slice(1));
1451
1358
  if (sub === 'check') return cmdCheck(args.slice(1));
1452
1359
  if (sub === 'submit') return cmdSubmit(args.slice(1));
@@ -1463,7 +1370,7 @@ async function main(argv) {
1463
1370
  }
1464
1371
 
1465
1372
  module.exports = {
1466
- main, cmdNew, cmdList, cmdStore, formatStore, cmdUpgrade, cmdCheck, cmdSubmit, cmdPublish, cmdInstall, cmdUpdate, printHelp,
1373
+ main, cmdNew, cmdList, cmdStore, formatStore, cmdPrice, cmdUpgrade, cmdCheck, cmdSubmit, cmdPublish, cmdInstall, cmdUpdate, printHelp,
1467
1374
  storeRequest,
1468
1375
  gatherCatalog, formatCatalog, ownProjectName,
1469
1376
  scaffoldModule, gatherUpgrade, evaluateUpgrade, gatherUpdates, evaluateUpdates, buildManifest,
@@ -0,0 +1,6 @@
1
+ // scripts/gds/module-store-cli.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-store-cli.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-store-cli.js');
@@ -373,9 +373,9 @@ function firstTimeAdmissionBlocked({ existing, approved, login }) {
373
373
  // existing request-access page rather than a spinner that never resolves.
374
374
  //
375
375
  // NB the onboarding module's public GET /access-requests/status (used by the
376
- // CLI) reports member|invited|pending|none but knows nothing about the
377
- // firstAdmin bypass or open enrollment — so it reports the OWNER as 'none'
378
- // forever. That is exactly why this auth-domain probe exists.
376
+ // CLI) reports member|invited|not_admitted but knows nothing about the
377
+ // firstAdmin bypass or open enrollment — so it reports the OWNER as
378
+ // 'not_admitted' forever. That is exactly why this auth-domain probe exists.
379
379
  async function webAdmissionStatus(login) {
380
380
  const name = String(login || '').trim();
381
381
  if (!name) return { admitted: false, expectedSoon: false };
@@ -38,7 +38,7 @@
38
38
  // by BOTH routes that answer "does this GitHub login
39
39
  // have an account here?": the onboarding module's
40
40
  // GET /access-requests/status (the richer
41
- // member|invited|pending|dismissed|none) and core's
41
+ // member|invited|not_admitted) and core's
42
42
  // GET /auth/web/admission-status (its boolean twin).
43
43
  // Task 1003339 / ADR 0209. They are ONE oracle read
44
44
  // two ways, so they share one budget — give each its
@@ -152,6 +152,15 @@ function priceOf(row) {
152
152
  return { cents, credits, free: !row || cents === 0 || credits === 0 };
153
153
  }
154
154
 
155
+ // Whether a module can be bought today. Buying arrives with task 1003788 (area 8's
156
+ // wallet); until then only a free module can be had, and a paid one is listed with
157
+ // its price and marked not yet buyable (owner, 2026-10-26). Flip BUYING_OPEN when
158
+ // the buy action ships, and nothing else needs to change. Pure.
159
+ const BUYING_OPEN = false;
160
+ function buyableNow(price) {
161
+ return !!(price && (price.free || BUYING_OPEN));
162
+ }
163
+
155
164
  // Within a buyer's maximum ({ cents, credits }, either may be null): a free module
156
165
  // always is; a priced one is when it is offered at or under a cap the buyer set. Pure.
157
166
  function withinMaxPrice(price, max) {
@@ -228,7 +237,7 @@ function newestPerModule(rows) {
228
237
  }
229
238
 
230
239
  // The catalog: { floor, cap, ranking, sort, sort_parts, max_price, listed: [...], new: [...] },
231
- // each entry { key, title, version, published_at, price, category, overall, is_new,
240
+ // each entry { key, title, version, published_at, price, buyable, category, overall, is_new,
232
241
  // parts, strengths, weaknesses, unmeasured } (the last four from readParts). Excluded
233
242
  // versions are left out entirely: a module below the bar is not listed at all, nor
234
243
  // is one past the cap of its sub-category, nor one over the buyer's `maxPrice`.
@@ -291,9 +300,10 @@ async function buildCatalog({ db, floor = QUALITY_FLOOR, sort = null, maxPrice =
291
300
  const score = scores.get(String(r.version_id)) || null;
292
301
  const { placement } = placeVersion(score, floor);
293
302
  if (placement === 'excluded') continue;
303
+ const price = priceOf(prices.get(r.module_key));
294
304
  out[placement].push({
295
305
  key: r.module_key, title: r.title, version: r.version, published_at: r.published_at,
296
- price: priceOf(prices.get(r.module_key)), category: categoryOf(cats.get(r.module_key)),
306
+ price, category: categoryOf(cats.get(r.module_key)), buyable: buyableNow(price),
297
307
  overall: placement === 'new' ? null : score.overall, is_new: placement === 'new',
298
308
  ...readParts(signals.get(String(r.version_id)) || [], { floor }),
299
309
  });