aweshare 0.5.8 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
- import { fmtDate } from '../../../packages/protocol/dist/index.js';
3
+ import { fmtDate, fmtTime } from '../../../packages/protocol/dist/index.js';
4
4
  import { ensureDataDir, HUB_CONFIG_TEMPLATE, initHubProduceFiles, loadHubConfig, readAdminToken, resolvePaths, } from './config.js';
5
5
  import { createHubServer, installConfigReloader } from './server.js';
6
- import { renderHubStatus, renderInvites, renderUsage, renderUsageSummary, } from './table.js';
6
+ import { refreshTargets, renderConsumers, renderHubStatus, renderInvites, renderOfferings, renderProducers, renderUsage, renderUsageSummary, } from './table.js';
7
7
  import { loadOrCreateAdminToken, loadOrCreatePepper } from './tokens.js';
8
8
  const VERSION = JSON.parse(readFileSync(new URL('../../../package.json', import.meta.url), 'utf8')).version;
9
9
  // One-liners only; flags per command live in their own -h, everything else in README.
@@ -17,14 +17,12 @@ Options:
17
17
 
18
18
  Commands:
19
19
  init Create the data dir, pepper, and admin token.
20
- serve Run the hub server.
21
- produce Run the hub serving its own models too (hub/...); 'refresh'
22
- re-anchors or raises one hub model's daily token budget.
23
- invite Mint invite codes (default); revoke or restore one by id.
24
- list Read hub state: invites (the ledger) or usage (consumption).
25
- limits Show, set or clear one consumer's limit overrides.
26
- status Live state: capacity, offering health; rosters print with --all.
27
- offering Block or restore one offering alias (the per-alias scalpel).`;
20
+ serve Run the hub server (the only runner; config.produce.toml mounts itself).
21
+ produce Hub-local models: init scaffolds, refresh re-anchors today's budgets.
22
+ admin Govern: invite mint|revoke|restore, offering revoke|restore.
23
+ list Read hub state: invites (default), producers, consumers, offerings, usage.
24
+ status Live dashboard: capacity, 5m health, admission pressure, defaults.
25
+ limits Show, set or clear one consumer's limit overrides.`;
28
26
  const COMMAND_HELP = {
29
27
  init: `Usage: aweshare hub init
30
28
 
@@ -34,7 +32,9 @@ Options:
34
32
  -h, --help Show this message and exit.`,
35
33
  serve: `Usage: aweshare hub serve [OPTIONS]
36
34
 
37
- Run the hub server.
35
+ Run the hub server — the only runner. A config.produce.toml in the
36
+ data dir mounts automatically: its [[backends]]/[[offerings]] are
37
+ served in-process as hub/... (scaffold one with: aweshare hub produce init).
38
38
 
39
39
  Tunables in config.toml (rate limits, timeouts, maxProducers) hot-reload:
40
40
  edits are picked up within a couple of seconds (or send SIGHUP at once);
@@ -44,14 +44,15 @@ Options:
44
44
  --host ADDR Address to listen on (default from AWESHARE_HUB_HOST, else 0.0.0.0).
45
45
  --port N Port to listen on, 1..65535 (default from AWESHARE_HUB_PORT, else 8787).
46
46
  -h, --help Show this message and exit.`,
47
- produce: `Usage: aweshare hub produce [init|refresh] [OPTIONS]
47
+ produce: `Usage: aweshare hub produce init|refresh [OPTIONS]
48
48
 
49
- Run the hub with its own models attached. Identical to 'serve'; the
50
- difference is intent: define [[backends]] and [[offerings]] in the data
51
- dir's config.produce.toml (namespace hub/...), put upstream keys in secrets.json
52
- next to it, and consumers reach those models as hub/<name> — served by
53
- this process directly, no producer tunnel. Same flags as serve; catalog
54
- edits hot-reload like every other config key.
49
+ Hub-local models — the hub serving its own catalog, no producer agent.
50
+ Define [[backends]] and [[offerings]] in the data dir's
51
+ config.produce.toml (namespace hub/...), put upstream keys in
52
+ secrets.json next to it, and consumers reach those models as
53
+ hub/<name>. Running the server is 'aweshare hub serve' — the same
54
+ runner mounts the catalog automatically; this group only scaffolds
55
+ and re-anchors budgets.
55
56
 
56
57
  ` +
57
58
  '`init`' +
@@ -66,21 +67,46 @@ Options:
66
67
  counting), or raise it for the rest of today. Producer-registered models
67
68
  (ns/...) are not the hub's to refresh — that producer runs
68
69
  'aweshare producer refresh' itself. A permanent raise belongs in
69
- config.produce.toml (dailyTokens), which hot-reloads.
70
+ config.produce.toml (dailyTokens), which hot-reloads. --all
71
+ bare-refreshes every hub/ model with a daily cap in one run; unlimited
72
+ ones (dailyTokens = 0) are reported and skipped, and one failure does
73
+ not stop the rest.
70
74
 
71
75
  Examples:
72
76
  aweshare hub produce refresh glm-5.3 # today restarts now
77
+ aweshare hub produce refresh --all # every hub/ model restarts today
73
78
  aweshare hub produce refresh glm-5.3 --add 2000000 # today's cap +2M
74
79
  aweshare hub produce refresh glm-5.3 --clear # drop both markers
75
80
 
76
81
  Options:
77
- --host ADDR Address to listen on (default from AWESHARE_HUB_HOST, else 0.0.0.0).
78
- --port N Port to listen on, 1..65535 (default from AWESHARE_HUB_PORT, else 8787).
79
82
  --add N refresh only: raise today's cap by N tokens until Beijing midnight.
80
83
  --clear refresh only: drop the refresh marker and any bonus.
81
- --json refresh only: print the raw API response.
84
+ --all refresh only: bare-refresh every hub/... model (no ALIAS, --add or --clear).
85
+ --json refresh only: print the raw API response(s).
82
86
  -h, --help Show this message and exit.`,
83
- invite: `Usage: aweshare hub invite [mint|revoke|restore] [ARGS] [OPTIONS]
87
+ admin: `Usage: aweshare hub admin [invite|offering] ACTION [ARGS] [OPTIONS]
88
+
89
+ The operator's write operations, one gate. Reads are 'list', the live
90
+ dashboard is 'status', tuning is 'limits' — everything that changes
91
+ hub state lives here (thin wrappers over the admin REST API).
92
+
93
+ Commands:
94
+ invite mint [...] Mint one-time invite codes.
95
+ invite revoke N Kill invite N — the sledgehammer: a pending
96
+ code stops pairing, a redeemed one suspends
97
+ the whole producer or consumer it minted.
98
+ invite restore N Undo an invite revoke.
99
+ offering revoke ALIAS The scalpel: 503 OFFERING_BLOCKED for new
100
+ requests to one alias while the producer's
101
+ other offerings keep serving.
102
+ offering restore ALIAS Undo an offering revoke.
103
+
104
+ Run 'aweshare hub admin invite -h' or 'aweshare hub admin offering -h'
105
+ for the full semantics of each group.
106
+
107
+ Options:
108
+ -h, --help Show this message and exit.`,
109
+ 'admin invite': `Usage: aweshare hub admin invite [mint|revoke|restore] [ARGS] [OPTIONS]
84
110
 
85
111
  The invite lifecycle. One code, one identity — and the invite is the
86
112
  operator's handle from mint to suspension: revoking a redeemed code
@@ -88,7 +114,7 @@ Options:
88
114
  a consumer's key stops working); restoring brings both back.
89
115
 
90
116
  Commands:
91
- mint Mint one-time invite codes (default). Printed once; recover
117
+ mint Mint one-time invite codes. Printed once; recover
92
118
  with: aweshare hub list invites --reveal. Listing lives under
93
119
  'list invites' on purpose — minting sends real codes, reading
94
120
  the ledger never should.
@@ -109,19 +135,38 @@ Options:
109
135
  token's lifetime; 'none' mints a code (and identity) that
110
136
  never expires.
111
137
  -h, --help Show this message and exit.`,
112
- list: `Usage: aweshare hub list [invites|usage] [OPTIONS]
138
+ 'admin offering': `Usage: aweshare hub admin offering revoke|restore ALIAS
139
+
140
+ Revoke one offering alias (every protocol row of it) or bring it back —
141
+ the scalpel between doing nothing and revoking the whole producer
142
+ ('admin invite revoke'). A revoke 503s new requests with
143
+ OFFERING_BLOCKED while the producer's other offerings keep serving;
144
+ 'list offerings' shows 'blocked'. Manual revokes survive producer
145
+ re-registers; auto revokes (repeated model mismatch, only when
146
+ autoBlockModelMismatch is enabled) clear once the producer re-declares
147
+ a different upstreamModel.
148
+
149
+ Options:
150
+ -h, --help Show this message and exit.`,
151
+ list: `Usage: aweshare hub list [invites|producers|consumers|offerings|usage] [OPTIONS]
113
152
 
114
- Read hub state (default: invites). The rosters and offering health live in
115
- 'aweshare hub status'; this is the ledger and the meter.
153
+ Read hub state, one table per noun (default: invites). The live
154
+ dashboard is 'aweshare hub status'.
116
155
 
117
156
  Commands:
118
- invites The invite ledger: every code, the identity it minted and its
119
- lifecycle (pending/used/suspended/revoked/expired).
120
- usage Who used how much: aggregated on the hub's SQLite, one row per
121
- consumer × model (default) — a person's rows stay together,
122
- busiest person and busiest model first. --details switches to the
123
- request log instead: newest first, one row per request, zero
124
- content stored, each row naming the consumer that made it.
157
+ invites The invite ledger: every code, the identity it minted and its
158
+ lifecycle (pending/used/suspended/revoked/expired).
159
+ producers The producer roster: name, status, online state, last seen.
160
+ consumers The consumer roster: name, status, last seen.
161
+ offerings The catalog: one row per alias, worst status first — the same
162
+ columns as 'consumer list' ('aweshare consumer list' from the
163
+ other side), with observed model, caps, live occupancy and
164
+ today's remaining tokens.
165
+ usage Who used how much: aggregated on the hub's SQLite, one row per
166
+ consumer × model (default) — a person's rows stay together,
167
+ busiest person and busiest model first. --details switches to the
168
+ request log instead: newest first, one row per request, zero
169
+ content stored, each row naming the consumer that made it.
125
170
 
126
171
  Options:
127
172
  --reveal Invites only: show the full invite codes (asi_...).
@@ -134,7 +179,7 @@ Options:
134
179
  --group-by KEY Usage only: consumer × model (default), consumer or alias.
135
180
  --since DUR Usage only: window: 30m, 12h, 7d (default), 30d… or all.
136
181
  --limit N Usage --details only: rows to show, 1..1000 (default 100).
137
- --json Print the raw API response instead of a table.
182
+ --json Print the raw API rows instead of a table.
138
183
  -h, --help Show this message and exit.`,
139
184
  limits: `Usage: aweshare hub limits NAME [OPTIONS]
140
185
 
@@ -151,31 +196,16 @@ Options:
151
196
  --clear Drop every override (back to hub-wide defaults).
152
197
  --json Print the raw API response instead of text.
153
198
  -h, --help Show this message and exit.`,
154
- status: `Usage: aweshare hub status [OPTIONS]
199
+ status: `Usage: aweshare hub status
155
200
 
156
- The hub's live state: capacity, offering health and the effective hub-wide
157
- consumer defaults. No tokens or request contents appear. Offerings are
158
- counted per deduplicated alias (one alias, several protocols, one verdict —
159
- the worst); the alias table uses the same columns as 'consumer list' and
160
- 'producer list', sorted worst status first, with live occupancy (IN USE
161
- n/max) and today's remaining daily tokens. The last-5m health line comes
162
- from the usage summary — admission 429s are never metered, so it reflects
163
- relayed outcomes only. The producer/consumer rosters are counts here; pass
164
- --all to print them in full.
165
-
166
- Options:
167
- --all Also print the producer and consumer rosters (name, status,
168
- online state, last seen).
169
- -h, --help Show this message and exit.`,
170
- offering: `Usage: aweshare hub offering block|restore ALIAS
171
-
172
- Block one offering alias (every protocol row of it) or bring it back — the
173
- scalpel between doing nothing and revoking the whole producer. A block 503s
174
- new requests with OFFERING_BLOCKED while the producer's other offerings
175
- keep serving; status shows 'blocked'. Manual blocks survive producer
176
- re-registers; auto blocks (repeated model mismatch, only when
177
- autoBlockModelMismatch is enabled) clear once the producer re-declares a
178
- different upstreamModel.
201
+ The hub's live dashboard: capacity, the last-5m health, admission
202
+ (rate-limit) pressure and the effective hub-wide consumer defaults.
203
+ No tokens or request contents appear, and no tables — every table
204
+ this summarizes lives under 'aweshare hub list' (producers, consumers,
205
+ offerings, usage). The last-5m health line comes from the usage
206
+ summary — admission 429s are never metered, so it reflects relayed
207
+ outcomes only; the rejection lines are the throttling that summary
208
+ cannot see, the tuning evidence for 'limits' and per-offering caps.
179
209
 
180
210
  Options:
181
211
  -h, --help Show this message and exit.`,
@@ -207,6 +237,26 @@ function fail(message) {
207
237
  console.error(`error: ${message}`);
208
238
  process.exit(1);
209
239
  }
240
+ /** Flags each command accepts (beyond -h/--help); anything else is a typo
241
+ * and must fail loudly. Commands whose subcommands take different flags
242
+ * (produce, admin, list) resolve the allowed list first, then call this. */
243
+ function checkFlags(cmd, flags, allowed) {
244
+ for (const name of flags.keys()) {
245
+ if (name !== 'help' && !allowed.includes(name))
246
+ fail(`unknown option '--${name}' (see: aweshare hub ${cmd} -h)`);
247
+ }
248
+ }
249
+ /** Deepest matching help entry: 'admin invite mint -h' shows the invite
250
+ * help, 'hub produce refresh -h' the produce help. */
251
+ function helpFor(cmd, positional) {
252
+ const words = [cmd, ...positional.filter((a) => !a.startsWith('-'))];
253
+ for (let n = words.length; n >= 1; n--) {
254
+ const help = COMMAND_HELP[words.slice(0, n).join(' ')];
255
+ if (help)
256
+ return help;
257
+ }
258
+ return undefined;
259
+ }
210
260
  /** Listen, turning bind errors (EADDRINUSE, unbindable host) into the CLI's
211
261
  * one-line error shape instead of an uncaught 'error' event stack trace. */
212
262
  function listenOrFail(hub, host, port) {
@@ -254,13 +304,65 @@ function adminApi() {
254
304
  signal: AbortSignal.timeout(10_000),
255
305
  });
256
306
  const data = (await res.json().catch(() => ({})));
307
+ // throw (not fail): the --all refresh loop catches per alias; every other
308
+ // caller reaches main's catch, which prints the same one-line error
257
309
  if (!res.ok) {
258
310
  const err = data.error;
259
- fail(`${res.status} ${err?.message ?? res.statusText}`);
311
+ throw new Error(`${res.status} ${err?.message ?? res.statusText}`);
260
312
  }
261
313
  return data;
262
314
  };
263
315
  }
316
+ /** The human lines after a successful refresh — shared by the single-alias
317
+ * and --all paths. Times render in the display zone like every other CLI
318
+ * time; only --json keeps the raw UTC ISO. */
319
+ function printRefreshResult(out) {
320
+ const fmt = (n) => n.toLocaleString('en-US');
321
+ if (out.reset)
322
+ console.log(`✓ ${out.alias} — today's window restarts now`);
323
+ else if (out.bonus !== null)
324
+ console.log(`✓ ${out.alias} — today's cap +${fmt(out.bonus)} until ${fmtTime(out.bonusUntil)}`);
325
+ else
326
+ console.log(`✓ ${out.alias} — refresh markers cleared, the declared budget applies`);
327
+ console.log(` today: ${fmt(out.quota.remaining)} of ${fmt(out.quota.cap)} left (${fmt(out.quota.used)} used) — resets ${fmtTime(out.quota.resetsAt)}`);
328
+ }
329
+ /** `hub produce refresh --all`: bare-refresh every hub-hosted model with a
330
+ * daily cap. One failure does not stop the rest; unlimited offerings are
331
+ * reported and skipped, and a nonzero exit code closes the run. */
332
+ async function runProduceRefreshAll(api, json) {
333
+ const list = (await api('GET', '/admin/v1/offerings'));
334
+ const { targets, unlimited } = refreshTargets(list.offerings);
335
+ const entries = [];
336
+ let failed = 0;
337
+ for (const alias of unlimited) {
338
+ if (!json)
339
+ console.log(`- ${alias} — unlimited (dailyTokens = 0), nothing to refresh`);
340
+ entries.push({ alias, skipped: 'unlimited' });
341
+ }
342
+ for (const alias of targets) {
343
+ try {
344
+ const out = (await api('POST', `/admin/v1/offerings/refresh?alias=${encodeURIComponent(alias)}`));
345
+ if (!json)
346
+ printRefreshResult(out);
347
+ entries.push(out);
348
+ }
349
+ catch (err) {
350
+ failed++;
351
+ const message = errorMessage(err);
352
+ if (!json)
353
+ console.log(`✗ ${alias} — ${message}`);
354
+ entries.push({ alias, error: message });
355
+ }
356
+ }
357
+ if (json)
358
+ console.log(JSON.stringify(entries, null, 2));
359
+ else if (targets.length === 0 && unlimited.length === 0)
360
+ console.log('no hub/ offerings to refresh (add [[offerings]] to config.produce.toml)');
361
+ if (failed > 0) {
362
+ console.error(`error: ${failed} of ${targets.length} refresh(es) failed`);
363
+ process.exitCode = 1;
364
+ }
365
+ }
264
366
  /** Flag-only commands reject stray positionals (e.g. the old `invite create`). */
265
367
  function noPositionals(cmd, positional) {
266
368
  const arg = positional[0];
@@ -282,6 +384,17 @@ function fmtLimits(name, overrides) {
282
384
  ? `${name}: ${set.join(', ')}`
283
385
  : `${name}: no overrides — hub-wide defaults apply (AWESHARE_CONSUMER_* env vars)`;
284
386
  }
387
+ /** The five readable tables; bare `list` defaults to the ledger (a read,
388
+ * and the one operators reach for first). */
389
+ const LIST_TARGETS = ['invites', 'producers', 'consumers', 'offerings', 'usage'];
390
+ /** Flags each list target accepts — a flag from another target is a typo. */
391
+ const LIST_FLAGS = {
392
+ invites: ['reveal', 'token', 'json'],
393
+ producers: ['json'],
394
+ consumers: ['json'],
395
+ offerings: ['json'],
396
+ usage: ['details', 'consumer', 'producer', 'alias', 'group-by', 'since', 'limit', 'json'],
397
+ };
285
398
  async function main() {
286
399
  const [cmd, ...rest] = process.argv.slice(2);
287
400
  const { positional, flags } = parseArgs(rest);
@@ -293,7 +406,7 @@ async function main() {
293
406
  console.log(USAGE);
294
407
  return;
295
408
  }
296
- const help = COMMAND_HELP[cmd];
409
+ const help = helpFor(cmd, positional);
297
410
  if (help && (positional.includes('-h') || flags.get('help') === 'true')) {
298
411
  console.log(help);
299
412
  return;
@@ -301,6 +414,8 @@ async function main() {
301
414
  const paths = resolvePaths();
302
415
  switch (cmd) {
303
416
  case 'init': {
417
+ checkFlags('init', flags, []);
418
+ noPositionals('init', positional);
304
419
  ensureDataDir(paths);
305
420
  loadOrCreatePepper(paths);
306
421
  const tokenExisted = existsSync(paths.adminTokenFile);
@@ -319,8 +434,7 @@ async function main() {
319
434
  return;
320
435
  }
321
436
  case 'serve': {
322
- // same runner — 'produce' just names the intent; the local catalog is
323
- // config-driven and loads either way
437
+ checkFlags('serve', flags, ['host', 'port']);
324
438
  noPositionals('serve', positional);
325
439
  const hub = createHubServer({ paths });
326
440
  installConfigReloader(hub);
@@ -333,11 +447,20 @@ async function main() {
333
447
  return;
334
448
  }
335
449
  case 'produce': {
336
- if (positional[0] === 'init') {
450
+ const sub = positional[0];
451
+ if (!sub) {
452
+ // serve absorbed the runner: the local catalog is config-driven and
453
+ // mounts either way, so a second runner only invited drift
454
+ fail("'hub produce' no longer runs the server — 'aweshare hub serve' is the only runner and mounts config.produce.toml by itself;\n" +
455
+ "this group is produce init (scaffold) and produce refresh (re-anchor today's budgets)");
456
+ }
457
+ if (sub !== 'init' && sub !== 'refresh') {
458
+ fail(`unknown produce action '${sub}' — expected init or refresh (the runner is: aweshare hub serve)`);
459
+ }
460
+ if (sub === 'init') {
461
+ checkFlags('produce', flags, []);
337
462
  if (positional[1])
338
463
  fail(`unexpected argument '${positional[1]}' (see: aweshare hub produce -h)`);
339
- if (flags.size > 0)
340
- fail(`'aweshare hub produce init' takes no options`);
341
464
  const files = initHubProduceFiles(paths);
342
465
  loadOrCreatePepper(paths);
343
466
  const adminToken = loadOrCreateAdminToken(paths);
@@ -347,77 +470,127 @@ async function main() {
347
470
  console.log(`secrets file: ${paths.secretsFile} (${files.secretsCreated ? 'created' : 'already exists; kept'})`);
348
471
  console.log(`data dir: ${paths.dataDir}`);
349
472
  console.log(`admin token: ${adminToken}`);
350
- console.log('next: add [[backends]] / [[offerings]] to config.produce.toml, fill secrets.json, then run: aweshare hub produce');
473
+ console.log('next: add [[backends]] / [[offerings]] to config.produce.toml, fill secrets.json, then run: aweshare hub serve');
351
474
  return;
352
475
  }
353
- if (positional[0] === 'refresh') {
354
- const raw = positional[1];
355
- if (!raw)
356
- fail('produce refresh: alias is required (hub/model — the hub/ prefix is optional)');
357
- if (positional[2])
358
- fail(`unexpected argument '${positional[2]}' (see: aweshare hub produce -h)`);
359
- // hub-hosted offerings live in the reserved hub/ namespace — accept
360
- // the bare name too; anything else the hub rejects as not its own
361
- const alias = raw.includes('/') ? raw : `hub/${raw}`;
362
- const params = new URLSearchParams({ alias });
363
- const add = flags.get('add');
364
- if (add !== undefined) {
365
- const n = Number(add);
366
- if (!Number.isSafeInteger(n) || n <= 0)
367
- fail(`--add must be a positive integer, got '${add}'`);
368
- params.set('add', String(n));
369
- }
370
- if (flags.get('clear') === 'true') {
371
- if (add !== undefined)
372
- fail('--clear drops both markers — drop --add too');
373
- params.set('clear', 'true');
374
- }
476
+ checkFlags('produce', flags, ['add', 'clear', 'all', 'json']);
477
+ const raw = positional[1];
478
+ const all = flags.get('all') === 'true';
479
+ if (positional[2])
480
+ fail(`unexpected argument '${positional[2]}' (see: aweshare hub produce -h)`);
481
+ if (all) {
482
+ if (raw)
483
+ fail(`--all refreshes every hub/ model — drop the alias '${raw}'`);
484
+ if (flags.get('add') !== undefined || flags.get('clear') === 'true')
485
+ fail('--all is a bare refresh of every hub/ model — --add/--clear stay single-alias');
375
486
  const api = adminApi();
376
- const out = (await api('POST', `/admin/v1/offerings/refresh?${params}`));
377
- if (flags.get('json') === 'true') {
378
- console.log(JSON.stringify(out, null, 2));
379
- return;
380
- }
381
- const fmt = (n) => n.toLocaleString('en-US');
382
- if (out.reset)
383
- console.log(`✓ ${out.alias} — today's window restarts now`);
384
- else if (out.bonus !== null)
385
- console.log(`✓ ${out.alias} — today's cap +${fmt(out.bonus)} until ${out.bonusUntil}`);
386
- else
387
- console.log(`✓ ${out.alias} — refresh markers cleared, the declared budget applies`);
388
- console.log(` today: ${fmt(out.quota.remaining)} of ${fmt(out.quota.cap)} left (${fmt(out.quota.used)} used) — resets ${out.quota.resetsAt}`);
389
- if (out.reset)
390
- console.log(` undo: aweshare hub produce refresh ${out.alias} --clear`);
487
+ await runProduceRefreshAll(api, flags.get('json') === 'true');
391
488
  return;
392
489
  }
393
- // same runner as serve — 'produce' names the intent; the local catalog
394
- // is config-driven and loads either way.
395
- noPositionals('produce', positional);
396
- const hub = createHubServer({ paths });
397
- installConfigReloader(hub);
398
- const host = flags.get('host') ?? hub.config.host;
399
- const port = Number(flags.get('port') ?? hub.config.port);
400
- if (!Number.isInteger(port) || port < 1 || port > 65535) {
401
- fail(`--port must be an integer between 1 and 65535, got '${flags.get('port')}'`);
490
+ if (!raw)
491
+ fail('produce refresh: alias is required (hub/model — the hub/ prefix is optional), or --all for every hub/ model');
492
+ // hub-hosted offerings live in the reserved hub/ namespace — accept
493
+ // the bare name too; anything else the hub rejects as not its own
494
+ const alias = raw.includes('/') ? raw : `hub/${raw}`;
495
+ const params = new URLSearchParams({ alias });
496
+ const add = flags.get('add');
497
+ if (add !== undefined) {
498
+ const n = Number(add);
499
+ if (!Number.isSafeInteger(n) || n <= 0)
500
+ fail(`--add must be a positive integer, got '${add}'`);
501
+ params.set('add', String(n));
402
502
  }
403
- listenOrFail(hub, host, port);
503
+ if (flags.get('clear') === 'true') {
504
+ if (add !== undefined)
505
+ fail('--clear drops both markers — drop --add too');
506
+ params.set('clear', 'true');
507
+ }
508
+ const api = adminApi();
509
+ const out = (await api('POST', `/admin/v1/offerings/refresh?${params}`));
510
+ if (flags.get('json') === 'true') {
511
+ console.log(JSON.stringify(out, null, 2));
512
+ return;
513
+ }
514
+ printRefreshResult(out);
515
+ if (out.reset)
516
+ console.log(` undo: aweshare hub produce refresh ${out.alias} --clear`);
404
517
  return;
405
518
  }
406
- case 'invite': {
407
- // the invite lifecycle: mint (default), revoke, restore — one code, one
408
- // identity, and every suspension travels through the invite that minted it
409
- const sub = positional[0] ?? 'mint';
410
- if (sub !== 'mint' && sub !== 'revoke' && sub !== 'restore') {
411
- fail(`unknown invite action '${sub}' — expected mint, revoke or restore`);
519
+ case 'admin': {
520
+ // the write gate: every state-changing verb lives here — bare calls
521
+ // print help (the safest action), nothing mutates by accident
522
+ const group = positional[0];
523
+ if (!group) {
524
+ console.log(COMMAND_HELP.admin);
525
+ return;
526
+ }
527
+ if (group !== 'invite' && group !== 'offering') {
528
+ fail(`unknown admin target '${group}' — expected invite or offering (see: aweshare hub admin -h)`);
529
+ }
530
+ const action = positional[1];
531
+ if (!action) {
532
+ console.log(COMMAND_HELP[`admin ${group}`]);
533
+ return;
412
534
  }
413
535
  const api = adminApi();
414
- if (sub === 'revoke' || sub === 'restore') {
415
- const id = positional[1];
536
+ if (group === 'invite') {
537
+ if (action !== 'mint' && action !== 'revoke' && action !== 'restore') {
538
+ fail(`unknown invite action '${action}' — expected mint, revoke or restore`);
539
+ }
540
+ if (action === 'mint') {
541
+ checkFlags('admin invite', flags, ['role', 'name', 'count', 'expires-in']);
542
+ if (positional[2])
543
+ fail(`unexpected argument '${positional[2]}' (see: aweshare hub admin invite -h)`);
544
+ const body = {};
545
+ const role = flags.get('role') ?? 'producer';
546
+ if (role !== 'producer' && role !== 'consumer')
547
+ fail("--role must be 'producer' or 'consumer'");
548
+ body.role = role;
549
+ const name = flags.get('name');
550
+ if (name)
551
+ body.name = name;
552
+ if (role === 'consumer' && !name)
553
+ fail('consumer invites need --name (limits, usage and suspension reference consumers by name)');
554
+ const count = flags.get('count');
555
+ if (role === 'consumer' && count && count !== '1')
556
+ fail('consumer invites are one per name — drop --count and mint one per consumer');
557
+ // absent flag still mints a 7d code — an unexpiring secret should be a
558
+ // deliberate choice (`none` here; API callers may omit expiresAt). Since
559
+ // v12 the expiry also bounds the minted identity's lifetime.
560
+ const expiresIn = flags.get('expires-in') ?? '7d';
561
+ if (expiresIn !== 'none') {
562
+ body.expiresAt = new Date(Date.now() + parseDuration(expiresIn)).toISOString();
563
+ }
564
+ if (count) {
565
+ const n = Number(count);
566
+ if (!Number.isInteger(n) || n < 1 || n > 100) {
567
+ fail(`--count must be an integer between 1 and 100, got '${count}'`);
568
+ }
569
+ body.count = n;
570
+ }
571
+ const out = (await api('POST', '/admin/v1/invites', body));
572
+ for (const invite of out.invites)
573
+ console.log(invite.code);
574
+ if (role === 'consumer') {
575
+ console.error('(save this now; codes print only here — re-view later with: aweshare hub list invites --reveal.\n' +
576
+ `hand it to '${name}' — they redeem it and keep the returned asc_ token:\n` +
577
+ ' aweshare consumer join --hub https://YOUR-HUB --code CODE\n' +
578
+ 'no aweshare installed? curl -s -X POST https://YOUR-HUB/invites/v1/redeem -H \'content-type: application/json\' -d \'{"code":"asi_..."}\')');
579
+ }
580
+ else {
581
+ console.error(name
582
+ ? '(save these now; codes print only here — re-view later with: aweshare hub list invites --reveal. redeem: aweshare producer join --hub URL --code CODE)'
583
+ : '(save these now; codes print only here — re-view later with: aweshare hub list invites --reveal. redeem: aweshare producer join --hub URL --code CODE --name NAME --email YOU@EXAMPLE.COM)');
584
+ }
585
+ return;
586
+ }
587
+ checkFlags('admin invite', flags, []);
588
+ const id = positional[2];
416
589
  if (!id)
417
- fail(`invite ${sub}: id is required (find ids with: aweshare hub list invites)`);
418
- if (positional[2])
419
- fail(`unexpected argument '${positional[2]}' (see: aweshare hub invite -h)`);
420
- if (sub === 'revoke') {
590
+ fail(`invite ${action}: id is required (find ids with: aweshare hub list invites)`);
591
+ if (positional[3])
592
+ fail(`unexpected argument '${positional[3]}' (see: aweshare hub admin invite -h)`);
593
+ if (action === 'revoke') {
421
594
  const out = (await api('DELETE', `/admin/v1/invites/${id}`));
422
595
  if (!out.revoked && !out.suspended) {
423
596
  console.log(`invite ${id} was already revoked — nothing to do`);
@@ -429,7 +602,7 @@ async function main() {
429
602
  else {
430
603
  console.log(`✓ invite ${id} revoked — the code no longer pairs`);
431
604
  }
432
- console.log(` undo: aweshare hub invite restore ${id}`);
605
+ console.log(` undo: aweshare hub admin invite restore ${id}`);
433
606
  return;
434
607
  }
435
608
  const out = (await api('POST', `/admin/v1/invites/${id}/restore`));
@@ -448,65 +621,39 @@ async function main() {
448
621
  }
449
622
  return;
450
623
  }
451
- if (positional[1])
452
- fail(`unexpected argument '${positional[1]}' (see: aweshare hub invite -h)`);
453
- // minting is the one state-changing read-adjacent verb — refuse the old
454
- // ledger flag loudly instead of silently minting a code
455
- if (flags.has('list')) {
456
- fail("'hub invite --list' became its own command — the ledger is: aweshare hub list invites");
457
- }
458
- const body = {};
459
- const role = flags.get('role') ?? 'producer';
460
- if (role !== 'producer' && role !== 'consumer')
461
- fail("--role must be 'producer' or 'consumer'");
462
- body.role = role;
463
- const name = flags.get('name');
464
- if (name)
465
- body.name = name;
466
- if (role === 'consumer' && !name)
467
- fail('consumer invites need --name (limits, usage and suspension reference consumers by name)');
468
- const count = flags.get('count');
469
- if (role === 'consumer' && count && count !== '1')
470
- fail('consumer invites are one per name — drop --count and mint one per consumer');
471
- // absent flag still mints a 7d code — an unexpiring secret should be a
472
- // deliberate choice (`none` here; API callers may omit expiresAt). Since
473
- // v12 the expiry also bounds the minted identity's lifetime.
474
- const expiresIn = flags.get('expires-in') ?? '7d';
475
- if (expiresIn !== 'none') {
476
- body.expiresAt = new Date(Date.now() + parseDuration(expiresIn)).toISOString();
477
- }
478
- if (count) {
479
- const n = Number(count);
480
- if (!Number.isInteger(n) || n < 1 || n > 100) {
481
- fail(`--count must be an integer between 1 and 100, got '${count}'`);
482
- }
483
- body.count = n;
624
+ // offering: the per-alias scalpel ('block' renamed to 'revoke' — the
625
+ // hub error code stays OFFERING_BLOCKED, the API path stays /block)
626
+ if (action === 'block') {
627
+ fail("'block' became 'revoke' — it is 'aweshare hub admin offering revoke ALIAS' now");
484
628
  }
485
- const out = (await api('POST', '/admin/v1/invites', body));
486
- for (const invite of out.invites)
487
- console.log(invite.code);
488
- if (role === 'consumer') {
489
- console.error('(save this now; codes print only here — re-view later with: aweshare hub list invites --reveal.\n' +
490
- `hand it to '${name}' — they redeem it and keep the returned asc_ token:\n` +
491
- ' aweshare consumer join --hub https://YOUR-HUB --code CODE\n' +
492
- 'no aweshare installed? curl -s -X POST https://YOUR-HUB/invites/v1/redeem -H \'content-type: application/json\' -d \'{"code":"asi_..."}\')');
629
+ if (action !== 'revoke' && action !== 'restore') {
630
+ fail(`unknown offering action '${action}' — expected revoke or restore`);
493
631
  }
494
- else {
495
- console.error(name
496
- ? '(save these now; codes print only here — re-view later with: aweshare hub list invites --reveal. redeem: aweshare producer join --hub URL --code CODE)'
497
- : '(save these now; codes print only here — re-view later with: aweshare hub list invites --reveal. redeem: aweshare producer join --hub URL --code CODE --name NAME --email YOU@EXAMPLE.COM)');
632
+ checkFlags('admin offering', flags, []);
633
+ const alias = positional[2];
634
+ if (!alias)
635
+ fail(`offering ${action}: alias is required (ns/model)`);
636
+ if (positional[3])
637
+ fail(`unexpected argument '${positional[3]}' (see: aweshare hub admin offering -h)`);
638
+ if (action === 'revoke') {
639
+ const out = (await api('POST', `/admin/v1/offerings/block?alias=${encodeURIComponent(alias)}`));
640
+ console.log(`✓ ${alias} revoked (${out.offerings} protocol row(s)) — new requests get 503 OFFERING_BLOCKED`);
641
+ console.log(` undo: aweshare hub admin offering restore ${alias}`);
642
+ return;
498
643
  }
644
+ const out = (await api('POST', `/admin/v1/offerings/restore?alias=${encodeURIComponent(alias)}`));
645
+ console.log(`✓ ${alias} restored (${out.offerings} protocol row(s)) — requests flow again`);
499
646
  return;
500
647
  }
501
648
  case 'list': {
502
- // the read verbs: the invite ledger and the meter (rosters and offering
503
- // health are `status`); bare `list` defaults to the ledger
504
- const sub = positional[0] ?? 'invites';
649
+ // the read verbs, one table per noun; bare `list` defaults to the ledger
650
+ const sub = (positional[0] ?? 'invites');
505
651
  if (positional[1])
506
652
  fail(`unexpected argument '${positional[1]}' (see: aweshare hub list -h)`);
507
- if (sub !== 'invites' && sub !== 'usage') {
508
- fail(`unknown list target '${sub}' — expected invites or usage`);
653
+ if (!LIST_TARGETS.includes(sub)) {
654
+ fail(`unknown list target '${sub}' — expected ${new Intl.ListFormat('en', { type: 'disjunction' }).format(LIST_TARGETS)}`);
509
655
  }
656
+ checkFlags('list', flags, LIST_FLAGS[sub]);
510
657
  const api = adminApi();
511
658
  if (sub === 'invites') {
512
659
  const reveal = flags.get('reveal') === 'true';
@@ -528,6 +675,26 @@ async function main() {
528
675
  }
529
676
  return;
530
677
  }
678
+ if (sub === 'producers' || sub === 'consumers') {
679
+ const roster = (await api('GET', '/admin/v1/tokens'));
680
+ if (flags.get('json') === 'true') {
681
+ console.log(JSON.stringify(sub === 'producers' ? roster.producers : roster.consumers));
682
+ return;
683
+ }
684
+ console.log(sub === 'producers'
685
+ ? renderProducers(roster.producers)
686
+ : renderConsumers(roster.consumers));
687
+ return;
688
+ }
689
+ if (sub === 'offerings') {
690
+ const data = (await api('GET', '/admin/v1/offerings'));
691
+ if (flags.get('json') === 'true') {
692
+ console.log(JSON.stringify(data.offerings, null, 2));
693
+ return;
694
+ }
695
+ console.log(renderOfferings(data.offerings));
696
+ return;
697
+ }
531
698
  const params = new URLSearchParams();
532
699
  const consumer = flags.get('consumer');
533
700
  if (consumer)
@@ -598,10 +765,8 @@ async function main() {
598
765
  console.log(renderUsage(rows, (id) => names.get(id) ?? `#${id}`));
599
766
  return;
600
767
  }
601
- case 'usage':
602
- // usage became a `list` subcommand — the read surface is list + status
603
- return fail("'hub usage' moved — it is 'aweshare hub list usage' now");
604
768
  case 'limits': {
769
+ checkFlags('limits', flags, [...LIMIT_FLAGS.map(([flag]) => flag), 'clear', 'json']);
605
770
  const name = positional[0];
606
771
  if (!name)
607
772
  fail('consumer name is required (see: aweshare hub limits -h)');
@@ -640,7 +805,8 @@ async function main() {
640
805
  return;
641
806
  }
642
807
  case 'status': {
643
- noPositionals(cmd, positional);
808
+ checkFlags('status', flags, []);
809
+ noPositionals('status', positional);
644
810
  const api = adminApi();
645
811
  const [roster, offeringsRes, usageRes, rejectionsRes] = await Promise.all([
646
812
  api('GET', '/admin/v1/tokens'),
@@ -676,37 +842,23 @@ async function main() {
676
842
  offerings: offeringsRes.offerings,
677
843
  recent,
678
844
  rejections,
679
- all: flags.get('all') === 'true',
680
845
  }));
681
846
  return;
682
847
  }
683
- case 'offering': {
684
- const action = positional[0];
685
- if (action !== 'block' && action !== 'restore') {
686
- fail(`expected 'block' or 'restore' (see: aweshare hub offering -h), got '${action ?? ''}'`);
687
- }
688
- const alias = positional[1];
689
- if (!alias)
690
- fail(`offering ${action}: alias is required (ns/model)`);
691
- if (positional[2])
692
- fail(`unexpected argument '${positional[2]}' (see: aweshare hub offering -h)`);
693
- const api = adminApi();
694
- if (action === 'block') {
695
- const out = (await api('POST', `/admin/v1/offerings/block?alias=${encodeURIComponent(alias)}`));
696
- console.log(`✓ ${alias} blocked (${out.offerings} protocol row(s)) — new requests get 503 OFFERING_BLOCKED`);
697
- console.log(` undo: aweshare hub offering restore ${alias}`);
698
- return;
699
- }
700
- const out = (await api('POST', `/admin/v1/offerings/restore?alias=${encodeURIComponent(alias)}`));
701
- console.log(`✓ ${alias} restored (${out.offerings} protocol row(s)) — requests flow again`);
702
- return;
703
- }
848
+ case 'invite':
849
+ // invite mint/revoke/restore moved under the admin gate
850
+ return fail("'hub invite' moved — it is 'aweshare hub admin invite mint|revoke|restore' now");
851
+ case 'offering':
852
+ return fail("'hub offering' moved — block/restore live under 'aweshare hub admin offering revoke|restore' now");
853
+ case 'usage':
854
+ // usage became a `list` subcommand — the read surface is list + status
855
+ return fail("'hub usage' moved — it is 'aweshare hub list usage' now");
704
856
  case 'revoke':
705
- // revoke/restore became `invite` subcommands — the invite is the
706
- // lifecycle handle, same shape as `offering block|restore`
707
- return fail("'hub revoke --id N' moved — it is 'aweshare hub invite revoke N' now");
857
+ // revoke/restore became `admin invite` subcommands — the invite is the
858
+ // lifecycle handle, same shape as `admin offering revoke|restore`
859
+ return fail("'hub revoke --id N' moved — it is 'aweshare hub admin invite revoke N' now");
708
860
  case 'restore':
709
- return fail("'hub restore --id N' moved — it is 'aweshare hub invite restore N' now");
861
+ return fail("'hub restore --id N' moved — it is 'aweshare hub admin invite restore N' now");
710
862
  default:
711
863
  fail(`unknown command: ${cmd}\n\n${USAGE}`);
712
864
  }